diff --git a/src/app.d.ts b/src/app.d.ts index 6e4e789..378d7be 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -15,7 +15,6 @@ declare global { did: Did; handle: Handle; displayName: string; - avatar: string; }; } // interface PageData {} diff --git a/src/lib/server/constants.ts b/src/lib/server/constants.ts new file mode 100644 index 0000000..dfd3bdd --- /dev/null +++ b/src/lib/server/constants.ts @@ -0,0 +1,3 @@ +export const SESSION_COOKIE = "atproto_session"; +export const HANDLE_COOKIE = "atproto_handle"; +export const OAUTH_COOKIE_PREFIX = "atproto_oauth_"; diff --git a/src/lib/server/oauth.ts b/src/lib/server/oauth.ts index 30af5fa..2b41c03 100644 --- a/src/lib/server/oauth.ts +++ b/src/lib/server/oauth.ts @@ -10,13 +10,14 @@ import { import { NodeDnsHandleResolver } from "@atcute/identity-resolver-node"; import { OAuthClient, scope, type Store } from "@atcute/oauth-node-client"; import { decryptText, encryptText } from "$lib/server/crypto.ts"; +import { OAUTH_COOKIE_PREFIX } from "$lib/server/constants.ts"; import { env } from "$env/dynamic/private"; import { dev } from "$app/environment"; import { Buffer } from "node:buffer"; class CookieStore implements Store { #cookies: Cookies; - #prefix = "atproto_oauth_"; + #prefix = OAUTH_COOKIE_PREFIX; #maxAge = 60 * 60 * 24 * 7; constructor(event: { cookies: Cookies }, options?: { maxAge?: number }) { diff --git a/src/lib/server/session.ts b/src/lib/server/session.ts index 8f84f0b..2a9d073 100644 --- a/src/lib/server/session.ts +++ b/src/lib/server/session.ts @@ -3,45 +3,80 @@ import { Client } from "@atcute/client"; import { isDid, isHandle } from "@atcute/lexicons/syntax"; import { createOAuthClient } from "$lib/server/oauth.ts"; import { decryptText } from "./crypto.ts"; +import { dev } from "$app/environment"; import { env } from "$env/dynamic/private"; +import { HANDLE_COOKIE, SESSION_COOKIE } from "./constants.ts"; -export const destroySession = async (event: RequestEvent): Promise => { - event.cookies.delete("atproto_session", { path: "/" }); - if (event.locals.user === undefined) { - return; - } - try { - const oAuthClient = createOAuthClient(event); - // await event.locals.user.session.signOut(); - await oAuthClient.revoke(event.locals.user.did); - } catch { - // Do nothing? +/** + * Logout + */ +export const destroySession = async ( + event: RequestEvent, +): Promise => { + event.cookies.delete(SESSION_COOKIE, { path: "/" }); + if (event.locals.user) { + try { + const oAuthClient = createOAuthClient(event); + await oAuthClient.revoke(event.locals.user.did); + } catch { + // Do nothing? + } + event.locals.user = undefined; } event.locals.oAuthClient = undefined; }; +/** + * Login + * @returns {URL} OAuth redirect + */ +export const startSession = async ( + event: RequestEvent, + handle: string, +): Promise => { + if (isHandle(handle) === false) { + throw new Error("invalid handle"); + } + const oAuthClient = createOAuthClient(event); + const { url } = await oAuthClient.authorize({ + target: { "type": "account", identifier: handle }, + }); + // Temporary to remember handle across oauth flow + event.cookies.set( + HANDLE_COOKIE, + handle, + { + httpOnly: true, + maxAge: 60 * 10, + path: "/", + sameSite: "lax", + secure: !dev, + }, + ); + return url; +}; + +/** + * Setup OAuth client from cookies + */ export const restoreSession = async (event: RequestEvent): Promise => { const { cookies } = event; - // Read the cookie - const encrypted = cookies.get("atproto_session"); + const encrypted = cookies.get(SESSION_COOKIE); if (encrypted === undefined) { return; } - // Parse and validate or delete + // Parse and validate or delete cookie let data; try { const decrypted = await decryptText(encrypted, env.PRIVATE_COOKIE_KEY); data = JSON.parse(decrypted); } catch { - cookies.delete("atproto_session", { path: "/" }); + cookies.delete(SESSION_COOKIE, { path: "/" }); return; } - // [TODO] validate data type? + // [TODO] ArkType data validation? try { - if ( - isDid(data.did) === false || - isHandle(data.handle) === false - ) { + if (isDid(data.did) === false || isHandle(data.handle) === false) { throw new Error(); } const oAuthClient = createOAuthClient(event); @@ -53,7 +88,7 @@ export const restoreSession = async (event: RequestEvent): Promise => { session, }; } catch { - cookies.delete("atproto_session", { path: "/" }); + cookies.delete(SESSION_COOKIE, { path: "/" }); return; } }; diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index b3930e8..0b8a154 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -4,9 +4,9 @@ export const load: LayoutServerLoad = (event) => { let user = undefined; if (event.locals.user) { user = { + did: event.locals.user.did, handle: event.locals.user.handle, displayName: event.locals.user.displayName, - avatar: event.locals.user.avatar, }; } return { diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 0572ef8..07b4462 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,8 +1,5 @@ import { type Actions, fail, redirect } from "@sveltejs/kit"; -import { isActorIdentifier } from "@atcute/lexicons/syntax"; -import { createOAuthClient } from "$lib/server/oauth.ts"; -import { destroySession } from "../lib/server/session.ts"; -import { dev } from "$app/environment"; +import { destroySession, startSession } from "$lib/server/session.ts"; export const actions = { logout: async (event) => { @@ -12,28 +9,12 @@ export const actions = { login: async (event) => { const formData = await event.request.formData(); const handle = formData.get("handle"); - if (isActorIdentifier(handle) === false) { + let url: URL; + try { + url = await startSession(event, String(handle ?? "")); + } catch { return fail(400, { handle, invalid: true }); } - const oAuthClient = createOAuthClient(event); - const { url } = await oAuthClient.authorize({ - target: { - "type": "account", - identifier: handle, - }, - }); - // [TODO] encrypt handle necessary? - event.cookies.set( - "atproto_handle", - handle, - { - httpOnly: true, - maxAge: 60 * 5, - path: "/", - sameSite: "lax", - secure: !dev, - }, - ); redirect(303, url); }, } satisfies Actions; diff --git a/src/routes/oauth/callback/+server.ts b/src/routes/oauth/callback/+server.ts index af4d677..b590983 100644 --- a/src/routes/oauth/callback/+server.ts +++ b/src/routes/oauth/callback/+server.ts @@ -5,17 +5,18 @@ import { Client, ok } from "@atcute/client"; import { redirect } from "@sveltejs/kit"; import { createOAuthClient } from "$lib/server/oauth.ts"; import { encryptText } from "$lib/server/crypto.ts"; +import { HANDLE_COOKIE, SESSION_COOKIE } from "$lib/server/constants.ts"; import { env } from "$env/dynamic/private"; import { dev } from "$app/environment"; export const GET: RequestHandler = async (event) => { const { url, cookies } = event; - const handle = cookies.get("atproto_handle"); + const handle = cookies.get(HANDLE_COOKIE); if (handle === undefined) { return redirect(303, "/?error=expired"); } - cookies.delete("atproto_handle", { path: "/" }); + cookies.delete(HANDLE_COOKIE, { path: "/" }); let session: OAuthSession; try { @@ -29,8 +30,7 @@ export const GET: RequestHandler = async (event) => { const data = { handle, did: session.did, - displayName: "", - avatar: "", + displayName: handle, }; try { @@ -40,15 +40,9 @@ export const GET: RequestHandler = async (event) => { params: { actor: session.did }, }), ); - // if (profile.handle) { - // data.handle = profile.handle; - // } if (profile.displayName) { data.displayName = profile.displayName; } - if (profile.avatar) { - data.avatar = profile.avatar; - } } catch { // No Bluesky account? } @@ -59,7 +53,7 @@ export const GET: RequestHandler = async (event) => { ); cookies.set( - "atproto_session", + SESSION_COOKIE, encrypted, { httpOnly: true,