Attic is a cozy space with lofty ambitions. attic.social

cookie constants

dbushell.com b62ee852 5d78a193

verified
+72 -59
-1
src/app.d.ts
··· 15 15 did: Did; 16 16 handle: Handle; 17 17 displayName: string; 18 - avatar: string; 19 18 }; 20 19 } 21 20 // interface PageData {}
+3
src/lib/server/constants.ts
··· 1 + export const SESSION_COOKIE = "atproto_session"; 2 + export const HANDLE_COOKIE = "atproto_handle"; 3 + export const OAUTH_COOKIE_PREFIX = "atproto_oauth_";
+2 -1
src/lib/server/oauth.ts
··· 10 10 import { NodeDnsHandleResolver } from "@atcute/identity-resolver-node"; 11 11 import { OAuthClient, scope, type Store } from "@atcute/oauth-node-client"; 12 12 import { decryptText, encryptText } from "$lib/server/crypto.ts"; 13 + import { OAUTH_COOKIE_PREFIX } from "$lib/server/constants.ts"; 13 14 import { env } from "$env/dynamic/private"; 14 15 import { dev } from "$app/environment"; 15 16 import { Buffer } from "node:buffer"; 16 17 17 18 class CookieStore<K extends string, V> implements Store<K, V> { 18 19 #cookies: Cookies; 19 - #prefix = "atproto_oauth_"; 20 + #prefix = OAUTH_COOKIE_PREFIX; 20 21 #maxAge = 60 * 60 * 24 * 7; 21 22 22 23 constructor(event: { cookies: Cookies }, options?: { maxAge?: number }) {
+56 -21
src/lib/server/session.ts
··· 3 3 import { isDid, isHandle } from "@atcute/lexicons/syntax"; 4 4 import { createOAuthClient } from "$lib/server/oauth.ts"; 5 5 import { decryptText } from "./crypto.ts"; 6 + import { dev } from "$app/environment"; 6 7 import { env } from "$env/dynamic/private"; 8 + import { HANDLE_COOKIE, SESSION_COOKIE } from "./constants.ts"; 7 9 8 - export const destroySession = async (event: RequestEvent): Promise<void> => { 9 - event.cookies.delete("atproto_session", { path: "/" }); 10 - if (event.locals.user === undefined) { 11 - return; 10 + /** 11 + * Logout 12 + */ 13 + export const destroySession = async ( 14 + event: RequestEvent, 15 + ): Promise<void> => { 16 + event.cookies.delete(SESSION_COOKIE, { path: "/" }); 17 + if (event.locals.user) { 18 + try { 19 + const oAuthClient = createOAuthClient(event); 20 + await oAuthClient.revoke(event.locals.user.did); 21 + } catch { 22 + // Do nothing? 23 + } 24 + event.locals.user = undefined; 12 25 } 13 - try { 14 - const oAuthClient = createOAuthClient(event); 15 - // await event.locals.user.session.signOut(); 16 - await oAuthClient.revoke(event.locals.user.did); 17 - } catch { 18 - // Do nothing? 26 + event.locals.oAuthClient = undefined; 27 + }; 28 + 29 + /** 30 + * Login 31 + * @returns {URL} OAuth redirect 32 + */ 33 + export const startSession = async ( 34 + event: RequestEvent, 35 + handle: string, 36 + ): Promise<URL> => { 37 + if (isHandle(handle) === false) { 38 + throw new Error("invalid handle"); 19 39 } 20 - event.locals.oAuthClient = undefined; 40 + const oAuthClient = createOAuthClient(event); 41 + const { url } = await oAuthClient.authorize({ 42 + target: { "type": "account", identifier: handle }, 43 + }); 44 + // Temporary to remember handle across oauth flow 45 + event.cookies.set( 46 + HANDLE_COOKIE, 47 + handle, 48 + { 49 + httpOnly: true, 50 + maxAge: 60 * 10, 51 + path: "/", 52 + sameSite: "lax", 53 + secure: !dev, 54 + }, 55 + ); 56 + return url; 21 57 }; 22 58 59 + /** 60 + * Setup OAuth client from cookies 61 + */ 23 62 export const restoreSession = async (event: RequestEvent): Promise<void> => { 24 63 const { cookies } = event; 25 - // Read the cookie 26 - const encrypted = cookies.get("atproto_session"); 64 + const encrypted = cookies.get(SESSION_COOKIE); 27 65 if (encrypted === undefined) { 28 66 return; 29 67 } 30 - // Parse and validate or delete 68 + // Parse and validate or delete cookie 31 69 let data; 32 70 try { 33 71 const decrypted = await decryptText(encrypted, env.PRIVATE_COOKIE_KEY); 34 72 data = JSON.parse(decrypted); 35 73 } catch { 36 - cookies.delete("atproto_session", { path: "/" }); 74 + cookies.delete(SESSION_COOKIE, { path: "/" }); 37 75 return; 38 76 } 39 - // [TODO] validate data type? 77 + // [TODO] ArkType data validation? 40 78 try { 41 - if ( 42 - isDid(data.did) === false || 43 - isHandle(data.handle) === false 44 - ) { 79 + if (isDid(data.did) === false || isHandle(data.handle) === false) { 45 80 throw new Error(); 46 81 } 47 82 const oAuthClient = createOAuthClient(event); ··· 53 88 session, 54 89 }; 55 90 } catch { 56 - cookies.delete("atproto_session", { path: "/" }); 91 + cookies.delete(SESSION_COOKIE, { path: "/" }); 57 92 return; 58 93 } 59 94 };
+1 -1
src/routes/+layout.server.ts
··· 4 4 let user = undefined; 5 5 if (event.locals.user) { 6 6 user = { 7 + did: event.locals.user.did, 7 8 handle: event.locals.user.handle, 8 9 displayName: event.locals.user.displayName, 9 - avatar: event.locals.user.avatar, 10 10 }; 11 11 } 12 12 return {
+5 -24
src/routes/+page.server.ts
··· 1 1 import { type Actions, fail, redirect } from "@sveltejs/kit"; 2 - import { isActorIdentifier } from "@atcute/lexicons/syntax"; 3 - import { createOAuthClient } from "$lib/server/oauth.ts"; 4 - import { destroySession } from "../lib/server/session.ts"; 5 - import { dev } from "$app/environment"; 2 + import { destroySession, startSession } from "$lib/server/session.ts"; 6 3 7 4 export const actions = { 8 5 logout: async (event) => { ··· 12 9 login: async (event) => { 13 10 const formData = await event.request.formData(); 14 11 const handle = formData.get("handle"); 15 - if (isActorIdentifier(handle) === false) { 12 + let url: URL; 13 + try { 14 + url = await startSession(event, String(handle ?? "")); 15 + } catch { 16 16 return fail(400, { handle, invalid: true }); 17 17 } 18 - const oAuthClient = createOAuthClient(event); 19 - const { url } = await oAuthClient.authorize({ 20 - target: { 21 - "type": "account", 22 - identifier: handle, 23 - }, 24 - }); 25 - // [TODO] encrypt handle necessary? 26 - event.cookies.set( 27 - "atproto_handle", 28 - handle, 29 - { 30 - httpOnly: true, 31 - maxAge: 60 * 5, 32 - path: "/", 33 - sameSite: "lax", 34 - secure: !dev, 35 - }, 36 - ); 37 18 redirect(303, url); 38 19 }, 39 20 } satisfies Actions;
+5 -11
src/routes/oauth/callback/+server.ts
··· 5 5 import { redirect } from "@sveltejs/kit"; 6 6 import { createOAuthClient } from "$lib/server/oauth.ts"; 7 7 import { encryptText } from "$lib/server/crypto.ts"; 8 + import { HANDLE_COOKIE, SESSION_COOKIE } from "$lib/server/constants.ts"; 8 9 import { env } from "$env/dynamic/private"; 9 10 import { dev } from "$app/environment"; 10 11 11 12 export const GET: RequestHandler = async (event) => { 12 13 const { url, cookies } = event; 13 14 14 - const handle = cookies.get("atproto_handle"); 15 + const handle = cookies.get(HANDLE_COOKIE); 15 16 if (handle === undefined) { 16 17 return redirect(303, "/?error=expired"); 17 18 } 18 - cookies.delete("atproto_handle", { path: "/" }); 19 + cookies.delete(HANDLE_COOKIE, { path: "/" }); 19 20 20 21 let session: OAuthSession; 21 22 try { ··· 29 30 const data = { 30 31 handle, 31 32 did: session.did, 32 - displayName: "", 33 - avatar: "", 33 + displayName: handle, 34 34 }; 35 35 36 36 try { ··· 40 40 params: { actor: session.did }, 41 41 }), 42 42 ); 43 - // if (profile.handle) { 44 - // data.handle = profile.handle; 45 - // } 46 43 if (profile.displayName) { 47 44 data.displayName = profile.displayName; 48 - } 49 - if (profile.avatar) { 50 - data.avatar = profile.avatar; 51 45 } 52 46 } catch { 53 47 // No Bluesky account? ··· 59 53 ); 60 54 61 55 cookies.set( 62 - "atproto_session", 56 + SESSION_COOKIE, 63 57 encrypted, 64 58 { 65 59 httpOnly: true,