[READ-ONLY] a fast, modern browser for the npm registry

refactor: moved the oauth to useSession (#867)

Co-authored-by: Daniel Roe <daniel@roe.dev>

authored by baileytownsend.dev

Daniel Roe and committed by
GitHub
b85af80b 07ce0e40

+105 -113
+1 -3
app/composables/useAtproto.ts
··· 1 - import type { UserSession } from '#shared/schemas/userSession' 2 - 3 1 export function useAtproto() { 4 2 const { 5 3 data: user, 6 4 pending, 7 5 clear, 8 - } = useFetch<UserSession | null>('/api/auth/session', { 6 + } = useFetch('/api/auth/session', { 9 7 server: false, 10 8 immediate: !import.meta.test, 11 9 })
-10
modules/cache.ts
··· 27 27 ...nitroConfig.storage[FETCH_CACHE_STORAGE_BASE], 28 28 driver: 'vercel-runtime-cache', 29 29 } 30 - 31 - const env = process.env.VERCEL_ENV 32 - 33 - nitroConfig.storage['oauth-atproto-state'] = { 34 - driver: env === 'production' ? 'vercel-kv' : 'vercel-runtime-cache', 35 - } 36 - 37 - nitroConfig.storage['oauth-atproto-session'] = { 38 - driver: env === 'production' ? 'vercel-kv' : 'vercel-runtime-cache', 39 - } 40 30 }) 41 31 }, 42 32 })
-8
nuxt.config.ts
··· 151 151 driver: 'fsLite', 152 152 base: './.cache/fetch', 153 153 }, 154 - 'oauth-atproto-state': { 155 - driver: 'fsLite', 156 - base: './.cache/atproto-oauth/state', 157 - }, 158 - 'oauth-atproto-session': { 159 - driver: 'fsLite', 160 - base: './.cache/atproto-oauth/session', 161 - }, 162 154 }, 163 155 typescript: { 164 156 tsConfig: {
+10 -9
server/api/auth/atproto.get.ts
··· 3 3 import { createError, getQuery, sendRedirect } from 'h3' 4 4 import { useOAuthStorage } from '#server/utils/atproto/storage' 5 5 import { SLINGSHOT_HOST } from '#shared/utils/constants' 6 - import type { UserSession } from '#shared/schemas/userSession' 6 + import { useServerSession } from '#server/utils/server-session' 7 + import type { PublicUserSession } from '#shared/schemas/publicUserSession' 7 8 8 9 export default defineEventHandler(async event => { 9 10 const config = useRuntimeConfig(event) ··· 16 17 17 18 const query = getQuery(event) 18 19 const clientMetadata = getOauthClientMetadata() 19 - const { stateStore, sessionStore } = useOAuthStorage(event) 20 + const session = await useServerSession(event) 21 + const { stateStore, sessionStore } = useOAuthStorage(session) 20 22 21 23 const atclient = new NodeOAuthClient({ 22 24 stateStore, ··· 48 50 const agent = new Agent(authSession) 49 51 event.context.agent = agent 50 52 51 - const session = await useSession(event, { 52 - password: config.sessionPassword, 53 - }) 54 - 55 53 const response = await fetch( 56 54 `https://${SLINGSHOT_HOST}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${agent.did}`, 57 55 { headers: { 'User-Agent': 'npmx' } }, 58 56 ) 59 - const miniDoc = (await response.json()) as UserSession 60 - 61 - await session.update(miniDoc) 57 + if (response.ok) { 58 + const miniDoc: PublicUserSession = await response.json() 59 + await session.update({ 60 + public: miniDoc, 61 + }) 62 + } 62 63 63 64 return sendRedirect(event, '/') 64 65 })
+4 -2
server/api/auth/session.delete.ts
··· 1 1 export default eventHandlerWithOAuthSession(async (event, oAuthSession, serverSession) => { 2 - await Promise.all([oAuthSession?.signOut(), serverSession.clear()]) 3 - 2 + // Even tho the signOut also clears part of the server cache should be done in order 3 + // to let the oAuth package do any other clean up it may need 4 + await oAuthSession?.signOut() 5 + await serverSession.clear() 4 6 return 'Session cleared' 5 7 })
+2 -2
server/api/auth/session.get.ts
··· 1 - import { UserSessionSchema } from '#shared/schemas/userSession' 1 + import { PublicUserSessionSchema } from '#shared/schemas/publicUserSession' 2 2 import { safeParse } from 'valibot' 3 3 4 4 export default eventHandlerWithOAuthSession(async (event, oAuthSession, serverSession) => { 5 - const result = safeParse(UserSessionSchema, serverSession.data) 5 + const result = safeParse(PublicUserSessionSchema, serverSession.data.public) 6 6 if (!result.success) { 7 7 return null 8 8 }
+16 -26
server/utils/atproto/oauth-session-store.ts
··· 1 1 import type { NodeSavedSession, NodeSavedSessionStore } from '@atproto/oauth-client-node' 2 - import type { H3Event } from 'h3' 3 - 4 - /** 5 - * Storage key prefix for oauth session storage. 6 - */ 7 - export const OAUTH_SESSION_CACHE_STORAGE_BASE = 'oauth-atproto-session' 2 + import type { UserServerSession } from '#shared/types/userSession' 3 + import type { SessionManager } from 'h3' 8 4 9 5 export class OAuthSessionStore implements NodeSavedSessionStore { 10 - // TODO: not sure if we will support multi accounts, but if we do in the future will need to change this around 11 - private readonly cookieKey = 'oauth:atproto:session' 12 - private readonly storage = useStorage(OAUTH_SESSION_CACHE_STORAGE_BASE) 6 + private readonly session: SessionManager<UserServerSession> 13 7 14 - constructor(private event: H3Event) {} 8 + constructor(session: SessionManager<UserServerSession>) { 9 + this.session = session 10 + } 15 11 16 12 async get(): Promise<NodeSavedSession | undefined> { 17 - const sessionKey = getCookie(this.event, this.cookieKey) 18 - if (!sessionKey) return 19 - const result = await this.storage.getItem<NodeSavedSession>(sessionKey) 20 - if (!result) return 21 - return result 13 + const sessionData = this.session.data 14 + if (!sessionData) return undefined 15 + return sessionData.oauthSession 22 16 } 23 17 24 - async set(key: string, val: NodeSavedSession) { 25 - setCookie(this.event, this.cookieKey, key, { 26 - httpOnly: true, 27 - secure: !import.meta.dev, 28 - sameSite: 'lax', 18 + async set(_key: string, val: NodeSavedSession) { 19 + // We are ignoring the key since the mapping is already done in the session 20 + await this.session.update({ 21 + oauthSession: val, 29 22 }) 30 - await this.storage.setItem<NodeSavedSession>(key, val) 31 23 } 32 24 33 25 async del() { 34 - const sessionKey = getCookie(this.event, this.cookieKey) 35 - if (sessionKey) { 36 - await this.storage.del(sessionKey) 37 - } 38 - deleteCookie(this.event, this.cookieKey) 26 + await this.session.update({ 27 + oauthSession: undefined, 28 + }) 39 29 } 40 30 }
+16 -25
server/utils/atproto/oauth-state-store.ts
··· 1 1 import type { NodeSavedState, NodeSavedStateStore } from '@atproto/oauth-client-node' 2 - import type { H3Event } from 'h3' 3 - 4 - /** 5 - * Storage key prefix for oauth state storage. 6 - */ 7 - export const OAUTH_STATE_CACHE_STORAGE_BASE = 'oauth-atproto-state' 2 + import type { UserServerSession } from '#shared/types/userSession' 3 + import type { SessionManager } from 'h3' 8 4 9 5 export class OAuthStateStore implements NodeSavedStateStore { 10 - private readonly cookieKey = 'oauth:atproto:state' 11 - private readonly storage = useStorage(OAUTH_STATE_CACHE_STORAGE_BASE) 6 + private readonly session: SessionManager<UserServerSession> 12 7 13 - constructor(private event: H3Event) {} 8 + constructor(session: SessionManager<UserServerSession>) { 9 + this.session = session 10 + } 14 11 15 12 async get(): Promise<NodeSavedState | undefined> { 16 - const stateKey = getCookie(this.event, this.cookieKey) 17 - if (!stateKey) return 18 - const result = await this.storage.getItem<NodeSavedState>(stateKey) 19 - if (!result) return 20 - return result 13 + const sessionData = this.session.data 14 + if (!sessionData) return undefined 15 + return sessionData.oauthState 21 16 } 22 17 23 - async set(key: string, val: NodeSavedState) { 24 - setCookie(this.event, this.cookieKey, key, { 25 - httpOnly: true, 26 - secure: !import.meta.dev, 27 - sameSite: 'lax', 18 + async set(_key: string, val: NodeSavedState) { 19 + // We are ignoring the key since the mapping is already done in the session 20 + await this.session.update({ 21 + oauthState: val, 28 22 }) 29 - await this.storage.setItem<NodeSavedState>(key, val) 30 23 } 31 24 32 25 async del() { 33 - const stateKey = getCookie(this.event, this.cookieKey) 34 - deleteCookie(this.event, this.cookieKey) 35 - if (stateKey) { 36 - await this.storage.del(stateKey) 37 - } 26 + await this.session.update({ 27 + oauthState: undefined, 28 + }) 38 29 } 39 30 }
+4 -14
server/utils/atproto/oauth.ts
··· 4 4 import { parse } from 'valibot' 5 5 import { getOAuthLock } from '#server/utils/atproto/lock' 6 6 import { useOAuthStorage } from '#server/utils/atproto/storage' 7 - import { UNSET_NUXT_SESSION_PASSWORD } from '#shared/utils/constants' 8 7 import { OAuthMetadataSchema } from '#shared/schemas/oauth' 9 8 // @ts-expect-error virtual file from oauth module 10 9 import { clientUri } from '#oauth/config' 10 + import { useServerSession } from '#server/utils/server-session' 11 11 // TODO: limit scope as features gets added. atproto just allows login so no scary login screen till we have scopes 12 12 export const scope = 'atproto' 13 13 ··· 44 44 45 45 async function getOAuthSession(event: H3Event): Promise<OAuthSession | undefined> { 46 46 const clientMetadata = getOauthClientMetadata() 47 - const { stateStore, sessionStore } = useOAuthStorage(event) 47 + const serverSession = await useServerSession(event) 48 + const { stateStore, sessionStore } = useOAuthStorage(serverSession) 48 49 49 50 const client = new NodeOAuthClient({ 50 51 stateStore, ··· 64 65 handler: EventHandlerWithOAuthSession<T, D>, 65 66 ) { 66 67 return defineEventHandler(async event => { 67 - const config = useRuntimeConfig(event) 68 - 69 - if (!config.sessionPassword) { 70 - throw createError({ 71 - status: 500, 72 - message: UNSET_NUXT_SESSION_PASSWORD, 73 - }) 74 - } 75 - 76 - const serverSession = await useSession(event, { 77 - password: config.sessionPassword, 78 - }) 68 + const serverSession = await useServerSession(event) 79 69 80 70 const oAuthSession = await getOAuthSession(event) 81 71 return await handler(event, oAuthSession, serverSession)
+5 -4
server/utils/atproto/storage.ts
··· 1 - import type { H3Event } from 'h3' 1 + import type { SessionManager } from 'h3' 2 2 import { OAuthStateStore } from './oauth-state-store' 3 3 import { OAuthSessionStore } from './oauth-session-store' 4 + import type { UserServerSession } from '#shared/types/userSession' 4 5 5 - export const useOAuthStorage = (event: H3Event) => { 6 + export const useOAuthStorage = (session: SessionManager<UserServerSession>) => { 6 7 return { 7 - stateStore: new OAuthStateStore(event), 8 - sessionStore: new OAuthSessionStore(event), 8 + stateStore: new OAuthStateStore(session), 9 + sessionStore: new OAuthSessionStore(session), 9 10 } 10 11 }
+22
server/utils/server-session.ts
··· 1 + // This is for getting the session on the npmx server and differs from the OAuthSession 2 + import type { H3Event } from 'h3' 3 + import type { UserServerSession } from '#shared/types/userSession' 4 + 5 + /** 6 + * Get's the user's session that is stored on the server 7 + * @param event 8 + * @returns 9 + */ 10 + export const useServerSession = async (event: H3Event) => { 11 + const config = useRuntimeConfig(event) 12 + 13 + if (!config.sessionPassword) { 14 + throw new Error('Session password is not configured') 15 + } 16 + 17 + const serverSession = useSession<UserServerSession>(event, { 18 + password: config.sessionPassword, 19 + }) 20 + 21 + return serverSession 22 + }
+11
shared/schemas/publicUserSession.ts
··· 1 + import { object, string, pipe, url } from 'valibot' 2 + import type { InferOutput } from 'valibot' 3 + 4 + export const PublicUserSessionSchema = object({ 5 + // Safe to pass to the frontend 6 + did: string(), 7 + handle: string(), 8 + pds: pipe(string(), url()), 9 + }) 10 + 11 + export type PublicUserSession = InferOutput<typeof PublicUserSessionSchema>
-10
shared/schemas/userSession.ts
··· 1 - import { object, string, pipe, url } from 'valibot' 2 - import type { InferOutput } from 'valibot' 3 - 4 - export const UserSessionSchema = object({ 5 - did: string(), 6 - handle: string(), 7 - pds: pipe(string(), url()), 8 - }) 9 - 10 - export type UserSession = InferOutput<typeof UserSessionSchema>
+14
shared/types/userSession.ts
··· 1 + import type { NodeSavedSession, NodeSavedState } from '@atproto/oauth-client-node' 2 + 3 + export interface UserServerSession { 4 + public: { 5 + did: string 6 + handle: string 7 + pds: string 8 + } 9 + // Only to be used in the atproto session and state stores 10 + // Will need to change to Record<string, T> and add a current logged in user if we ever want to support 11 + // multiple did logins per server session 12 + oauthSession: NodeSavedSession | undefined 13 + oauthState: NodeSavedState | undefined 14 + }