diff --git a/app/composables/useAtproto.ts b/app/composables/useAtproto.ts index 9c38ae776..5e0a22eb0 100644 --- a/app/composables/useAtproto.ts +++ b/app/composables/useAtproto.ts @@ -1,11 +1,9 @@ -import type { UserSession } from '#shared/schemas/userSession' - export function useAtproto() { const { data: user, pending, clear, - } = useFetch('/api/auth/session', { + } = useFetch('/api/auth/session', { server: false, immediate: !import.meta.test, }) diff --git a/modules/cache.ts b/modules/cache.ts index ba58e59aa..3964ce91d 100644 --- a/modules/cache.ts +++ b/modules/cache.ts @@ -27,16 +27,6 @@ export default defineNuxtModule({ ...nitroConfig.storage[FETCH_CACHE_STORAGE_BASE], driver: 'vercel-runtime-cache', } - - const env = process.env.VERCEL_ENV - - nitroConfig.storage['oauth-atproto-state'] = { - driver: env === 'production' ? 'vercel-kv' : 'vercel-runtime-cache', - } - - nitroConfig.storage['oauth-atproto-session'] = { - driver: env === 'production' ? 'vercel-kv' : 'vercel-runtime-cache', - } }) }, }) diff --git a/nuxt.config.ts b/nuxt.config.ts index c0fb9d646..edebdd443 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -151,14 +151,6 @@ export default defineNuxtConfig({ driver: 'fsLite', base: './.cache/fetch', }, - 'oauth-atproto-state': { - driver: 'fsLite', - base: './.cache/atproto-oauth/state', - }, - 'oauth-atproto-session': { - driver: 'fsLite', - base: './.cache/atproto-oauth/session', - }, }, typescript: { tsConfig: { diff --git a/server/api/auth/atproto.get.ts b/server/api/auth/atproto.get.ts index 287823ce7..ee88c4cce 100644 --- a/server/api/auth/atproto.get.ts +++ b/server/api/auth/atproto.get.ts @@ -3,7 +3,8 @@ import { NodeOAuthClient } from '@atproto/oauth-client-node' import { createError, getQuery, sendRedirect } from 'h3' import { useOAuthStorage } from '#server/utils/atproto/storage' import { SLINGSHOT_HOST } from '#shared/utils/constants' -import type { UserSession } from '#shared/schemas/userSession' +import { useServerSession } from '#server/utils/server-session' +import type { PublicUserSession } from '#shared/schemas/publicUserSession' export default defineEventHandler(async event => { const config = useRuntimeConfig(event) @@ -16,7 +17,8 @@ export default defineEventHandler(async event => { const query = getQuery(event) const clientMetadata = getOauthClientMetadata() - const { stateStore, sessionStore } = useOAuthStorage(event) + const session = await useServerSession(event) + const { stateStore, sessionStore } = useOAuthStorage(session) const atclient = new NodeOAuthClient({ stateStore, @@ -48,17 +50,16 @@ export default defineEventHandler(async event => { const agent = new Agent(authSession) event.context.agent = agent - const session = await useSession(event, { - password: config.sessionPassword, - }) - const response = await fetch( `https://${SLINGSHOT_HOST}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${agent.did}`, { headers: { 'User-Agent': 'npmx' } }, ) - const miniDoc = (await response.json()) as UserSession - - await session.update(miniDoc) + if (response.ok) { + const miniDoc: PublicUserSession = await response.json() + await session.update({ + public: miniDoc, + }) + } return sendRedirect(event, '/') }) diff --git a/server/api/auth/session.delete.ts b/server/api/auth/session.delete.ts index ce1e6b8ef..a1d4f90b1 100644 --- a/server/api/auth/session.delete.ts +++ b/server/api/auth/session.delete.ts @@ -1,5 +1,7 @@ export default eventHandlerWithOAuthSession(async (event, oAuthSession, serverSession) => { - await Promise.all([oAuthSession?.signOut(), serverSession.clear()]) - + // Even tho the signOut also clears part of the server cache should be done in order + // to let the oAuth package do any other clean up it may need + await oAuthSession?.signOut() + await serverSession.clear() return 'Session cleared' }) diff --git a/server/api/auth/session.get.ts b/server/api/auth/session.get.ts index 64903ca70..9dbd11d7a 100644 --- a/server/api/auth/session.get.ts +++ b/server/api/auth/session.get.ts @@ -1,8 +1,8 @@ -import { UserSessionSchema } from '#shared/schemas/userSession' +import { PublicUserSessionSchema } from '#shared/schemas/publicUserSession' import { safeParse } from 'valibot' export default eventHandlerWithOAuthSession(async (event, oAuthSession, serverSession) => { - const result = safeParse(UserSessionSchema, serverSession.data) + const result = safeParse(PublicUserSessionSchema, serverSession.data.public) if (!result.success) { return null } diff --git a/server/utils/atproto/oauth-session-store.ts b/server/utils/atproto/oauth-session-store.ts index bd967d80b..532f9894d 100644 --- a/server/utils/atproto/oauth-session-store.ts +++ b/server/utils/atproto/oauth-session-store.ts @@ -1,40 +1,30 @@ import type { NodeSavedSession, NodeSavedSessionStore } from '@atproto/oauth-client-node' -import type { H3Event } from 'h3' - -/** - * Storage key prefix for oauth session storage. - */ -export const OAUTH_SESSION_CACHE_STORAGE_BASE = 'oauth-atproto-session' +import type { UserServerSession } from '#shared/types/userSession' +import type { SessionManager } from 'h3' export class OAuthSessionStore implements NodeSavedSessionStore { - // TODO: not sure if we will support multi accounts, but if we do in the future will need to change this around - private readonly cookieKey = 'oauth:atproto:session' - private readonly storage = useStorage(OAUTH_SESSION_CACHE_STORAGE_BASE) + private readonly session: SessionManager - constructor(private event: H3Event) {} + constructor(session: SessionManager) { + this.session = session + } async get(): Promise { - const sessionKey = getCookie(this.event, this.cookieKey) - if (!sessionKey) return - const result = await this.storage.getItem(sessionKey) - if (!result) return - return result + const sessionData = this.session.data + if (!sessionData) return undefined + return sessionData.oauthSession } - async set(key: string, val: NodeSavedSession) { - setCookie(this.event, this.cookieKey, key, { - httpOnly: true, - secure: !import.meta.dev, - sameSite: 'lax', + async set(_key: string, val: NodeSavedSession) { + // We are ignoring the key since the mapping is already done in the session + await this.session.update({ + oauthSession: val, }) - await this.storage.setItem(key, val) } async del() { - const sessionKey = getCookie(this.event, this.cookieKey) - if (sessionKey) { - await this.storage.del(sessionKey) - } - deleteCookie(this.event, this.cookieKey) + await this.session.update({ + oauthSession: undefined, + }) } } diff --git a/server/utils/atproto/oauth-state-store.ts b/server/utils/atproto/oauth-state-store.ts index 8a0386e5f..d15a95e96 100644 --- a/server/utils/atproto/oauth-state-store.ts +++ b/server/utils/atproto/oauth-state-store.ts @@ -1,39 +1,30 @@ import type { NodeSavedState, NodeSavedStateStore } from '@atproto/oauth-client-node' -import type { H3Event } from 'h3' - -/** - * Storage key prefix for oauth state storage. - */ -export const OAUTH_STATE_CACHE_STORAGE_BASE = 'oauth-atproto-state' +import type { UserServerSession } from '#shared/types/userSession' +import type { SessionManager } from 'h3' export class OAuthStateStore implements NodeSavedStateStore { - private readonly cookieKey = 'oauth:atproto:state' - private readonly storage = useStorage(OAUTH_STATE_CACHE_STORAGE_BASE) + private readonly session: SessionManager - constructor(private event: H3Event) {} + constructor(session: SessionManager) { + this.session = session + } async get(): Promise { - const stateKey = getCookie(this.event, this.cookieKey) - if (!stateKey) return - const result = await this.storage.getItem(stateKey) - if (!result) return - return result + const sessionData = this.session.data + if (!sessionData) return undefined + return sessionData.oauthState } - async set(key: string, val: NodeSavedState) { - setCookie(this.event, this.cookieKey, key, { - httpOnly: true, - secure: !import.meta.dev, - sameSite: 'lax', + async set(_key: string, val: NodeSavedState) { + // We are ignoring the key since the mapping is already done in the session + await this.session.update({ + oauthState: val, }) - await this.storage.setItem(key, val) } async del() { - const stateKey = getCookie(this.event, this.cookieKey) - deleteCookie(this.event, this.cookieKey) - if (stateKey) { - await this.storage.del(stateKey) - } + await this.session.update({ + oauthState: undefined, + }) } } diff --git a/server/utils/atproto/oauth.ts b/server/utils/atproto/oauth.ts index fed470666..7330ede55 100644 --- a/server/utils/atproto/oauth.ts +++ b/server/utils/atproto/oauth.ts @@ -4,10 +4,10 @@ import { NodeOAuthClient } from '@atproto/oauth-client-node' import { parse } from 'valibot' import { getOAuthLock } from '#server/utils/atproto/lock' import { useOAuthStorage } from '#server/utils/atproto/storage' -import { UNSET_NUXT_SESSION_PASSWORD } from '#shared/utils/constants' import { OAuthMetadataSchema } from '#shared/schemas/oauth' // @ts-expect-error virtual file from oauth module import { clientUri } from '#oauth/config' +import { useServerSession } from '#server/utils/server-session' // TODO: limit scope as features gets added. atproto just allows login so no scary login screen till we have scopes export const scope = 'atproto' @@ -44,7 +44,8 @@ type EventHandlerWithOAuthSession = ( async function getOAuthSession(event: H3Event): Promise { const clientMetadata = getOauthClientMetadata() - const { stateStore, sessionStore } = useOAuthStorage(event) + const serverSession = await useServerSession(event) + const { stateStore, sessionStore } = useOAuthStorage(serverSession) const client = new NodeOAuthClient({ stateStore, @@ -64,18 +65,7 @@ export function eventHandlerWithOAuthSession( handler: EventHandlerWithOAuthSession, ) { return defineEventHandler(async event => { - const config = useRuntimeConfig(event) - - if (!config.sessionPassword) { - throw createError({ - status: 500, - message: UNSET_NUXT_SESSION_PASSWORD, - }) - } - - const serverSession = await useSession(event, { - password: config.sessionPassword, - }) + const serverSession = await useServerSession(event) const oAuthSession = await getOAuthSession(event) return await handler(event, oAuthSession, serverSession) diff --git a/server/utils/atproto/storage.ts b/server/utils/atproto/storage.ts index 64f39623e..9575250cc 100644 --- a/server/utils/atproto/storage.ts +++ b/server/utils/atproto/storage.ts @@ -1,10 +1,11 @@ -import type { H3Event } from 'h3' +import type { SessionManager } from 'h3' import { OAuthStateStore } from './oauth-state-store' import { OAuthSessionStore } from './oauth-session-store' +import type { UserServerSession } from '#shared/types/userSession' -export const useOAuthStorage = (event: H3Event) => { +export const useOAuthStorage = (session: SessionManager) => { return { - stateStore: new OAuthStateStore(event), - sessionStore: new OAuthSessionStore(event), + stateStore: new OAuthStateStore(session), + sessionStore: new OAuthSessionStore(session), } } diff --git a/server/utils/server-session.ts b/server/utils/server-session.ts new file mode 100644 index 000000000..1d41fff7b --- /dev/null +++ b/server/utils/server-session.ts @@ -0,0 +1,22 @@ +// This is for getting the session on the npmx server and differs from the OAuthSession +import type { H3Event } from 'h3' +import type { UserServerSession } from '#shared/types/userSession' + +/** + * Get's the user's session that is stored on the server + * @param event + * @returns + */ +export const useServerSession = async (event: H3Event) => { + const config = useRuntimeConfig(event) + + if (!config.sessionPassword) { + throw new Error('Session password is not configured') + } + + const serverSession = useSession(event, { + password: config.sessionPassword, + }) + + return serverSession +} diff --git a/shared/schemas/userSession.ts b/shared/schemas/publicUserSession.ts similarity index 51% rename from shared/schemas/userSession.ts rename to shared/schemas/publicUserSession.ts index ccbdc934f..2ead3080f 100644 --- a/shared/schemas/userSession.ts +++ b/shared/schemas/publicUserSession.ts @@ -1,10 +1,11 @@ import { object, string, pipe, url } from 'valibot' import type { InferOutput } from 'valibot' -export const UserSessionSchema = object({ +export const PublicUserSessionSchema = object({ + // Safe to pass to the frontend did: string(), handle: string(), pds: pipe(string(), url()), }) -export type UserSession = InferOutput +export type PublicUserSession = InferOutput diff --git a/shared/types/userSession.ts b/shared/types/userSession.ts new file mode 100644 index 000000000..081c1b296 --- /dev/null +++ b/shared/types/userSession.ts @@ -0,0 +1,14 @@ +import type { NodeSavedSession, NodeSavedState } from '@atproto/oauth-client-node' + +export interface UserServerSession { + public: { + did: string + handle: string + pds: string + } + // Only to be used in the atproto session and state stores + // Will need to change to Record and add a current logged in user if we ever want to support + // multiple did logins per server session + oauthSession: NodeSavedSession | undefined + oauthState: NodeSavedState | undefined +}