Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lastlogin for dev mode #775

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions deno/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// watchexec --verbose -i deno/** pnpm build
// deno run --unstable-net --unstable-sloppy-imports --watch -A serve-ssl.ts
import { serveDir } from "jsr:@std/http/file-server";
import { byPattern } from "jsr:@http/route/by-pattern";
import freshPathMapper from "jsr:@http/discovery/fresh-path-mapper";
import { discoverRoutes } from "jsr:@http/discovery/discover-routes";
import { asSerializablePattern } from "jsr:@http/discovery/as-serializable-pattern";
import { byPattern } from "jsr:@http/route/by-pattern";
import { handle } from "jsr:@http/route/handle";
import * as path from "jsr:@std/path";

Expand All @@ -27,12 +27,6 @@ const env = {
* @param {string} modulePath - The full path to the module to import
* @param {boolean} [verbose=false] - Whether to enable verbose logging
* @returns {Function|null} - A fetch handler function or null if the module is invalid
*
* @example
* const handler = adaptCloudflareHandler('/path/to/module.ts');
* if (handler) {
* app.get('/route', handler);
* }
*/
function adaptCloudflareHandler(modulePath: string, verbose = false) {
const modulePathShort = modulePath.substring(
Expand Down Expand Up @@ -119,8 +113,12 @@ async function cfRoutes(fileRootUrl: string, prefix: string, verbose = false) {
return handlers;
}

const verbose = false;
const cfHandlers = await cfRoutes(import.meta.resolve("../functions"), "/api", verbose);
const verbose = true;

const cfHandlers = [
...(await cfRoutes(import.meta.resolve("../functions"), "/api", verbose)),
...(await cfRoutes(import.meta.resolve("../functions"), "/_auth", verbose)), // will remove this once we have more customization in lastlogin lib
];

const serveOpts = { fsRoot: "build" };

Expand Down
11 changes: 11 additions & 0 deletions functions/_auth/callback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { wrap_lastlogin } from "../lastlogin";

interface Env {
JWT_SECRET: string;
}
// this file exists solely so we can declare a route for lastlogin that we wrap so it can process oauth callback
export const onRequestGet: PagesFunction<Env> = async ({ request, env }) => {
return wrap_lastlogin(env.JWT_SECRET, async (request: Request) => {

Check failure on line 8 in functions/_auth/callback.ts

View workflow job for this annotation

GitHub Actions / Lint

'request' is defined but never used. Allowed unused args must match /^_/u
throw new Error("/_auth isn't supposed to get called, it's a dummy endpoint for lastlogin");
})(request);
};
13 changes: 12 additions & 1 deletion functions/api/login.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createResourcesForEnv } from "../utils";
import { handleGithubLogin } from "../github";
import { handleGoogleLogin } from "../google";
import { handleLastLogin } from "../lastlogin";

interface Env {
ENVIRONMENT: string;
Expand Down Expand Up @@ -47,7 +48,17 @@ export const onRequestGet: PagesFunction<Env> = async ({ request, env }) => {
const code = reqUrl.searchParams.get("code");

const { appUrl, isDev, tokenProvider } = createResourcesForEnv(env.ENVIRONMENT, request.url);

// use lastlogin for dev
if (isDev) {
return handleLastLogin(
request,
provider === "google" ? provider : "github",
chatId,
JWT_SECRET,
tokenProvider,
appUrl
);
}
if (provider === "google") {
return handleGoogleLogin({
isDev,
Expand Down
2 changes: 2 additions & 0 deletions functions/api/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export async function handleLogout({
["Location", url],
["Set-Cookie", tokenProvider.serializeToken("access_token", accessToken, 0)],
["Set-Cookie", tokenProvider.serializeToken("id_token", idToken, 0)],
// temp hack until we can override lastlogin cookie name to be called "access_token"
["Set-Cookie", tokenProvider.serializeToken("lastlogin_jwt", idToken, 0)],
]),
});
}
Expand Down
57 changes: 57 additions & 0 deletions functions/lastlogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { lastlogin, deleteCookie } from "@pomdtr/lastlogin";

Check failure on line 1 in functions/lastlogin.ts

View workflow job for this annotation

GitHub Actions / Lint

'deleteCookie' is defined but never used. Allowed unused vars must match /^_/u
import { TokenProvider } from "./token-provider";
import { errorResponse } from "./utils";
import { requestDevUserInfo } from "./github";
import { requestGoogleDevUserInfo } from "./google";

export function wrap_lastlogin(
secretKey: string,
handler: (request: Request) => Promise<Response>
) {
return lastlogin(handler, {
verifyEmail: (_) => true, // we accept all emails
secretKey,
});
}

export async function handleLastLogin(
request: Request,
provider: "google" | "github",
chatId: string | null,
JWT_SECRET: string,
tokenProvider: TokenProvider,
appUrl: string
) {
const wrapped_fetch = wrap_lastlogin(JWT_SECRET, async (request) => {
// this whole body would be unnecessary in a normal lastlogin-wrapped request
const email = request.headers.get("X-Lastlogin-Email");
console.log(`X-Lastlogin-Email ${email}!`);
if (!email) {
return errorResponse(403, "Lastlogin failed us");
}
const avatarUrl = (provider === "google" ? requestGoogleDevUserInfo() : requestDevUserInfo())
.avatarUrl;
// User info goes in a non HTTP-Only cookie that browser can read
const idToken = await tokenProvider.createToken(
email,
{ username: email, name: email, avatarUrl },
JWT_SECRET
);
// API authorization goes in an HTTP-Only cookie that only functions can read
const accessToken = await tokenProvider.createToken(email, { role: "api" }, JWT_SECRET);

// Return to the root or a specific chat if we have an id
const url = new URL(chatId ? `/c/${chatId}` : "/", appUrl).href;

return new Response(null, {
status: 302,
headers: new Headers([
["Location", url],
["Set-Cookie", tokenProvider.serializeToken("access_token", accessToken)],
["Set-Cookie", tokenProvider.serializeToken("id_token", idToken)],
]),
});
});

return wrapped_fetch(request);
}
6 changes: 3 additions & 3 deletions functions/token-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ export class TokenProvider {
// Format for inclusion in Set-Cookie header. By default
// use 30 day expiry, but allow override (e.g., 0 to remove cookie)
// "Set-Cookie: __Host-SID=<session token>; path=/; Secure; HttpOnly; SameSite=Strict."
serializeToken(name: "access_token" | "id_token", token: string, maxAge = 2592000) {
const { isDev, accessTokenName, idTokenName } = this;
const cookieName = name === "access_token" ? accessTokenName : idTokenName;
serializeToken(name: "access_token" | string, token: string, maxAge = 2592000) {
const { isDev, accessTokenName } = this;
const cookieName = name === "access_token" ? accessTokenName : name;

return serialize(cookieName, token, {
// Access tokens can't be read by browser, but id tokens can
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@eslint/compat": "^1.2.4",
"@pomdtr/lastlogin": "npm:@jsr/pomdtr__lastlogin@^0.5.13",
"@szhsin/react-menu": "^4.2.3",
"@uiw/react-codemirror": "^4.23.7",
"browser-image-compression": "^2.0.2",
Expand Down
77 changes: 77 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading