@@ -14,17 +14,36 @@ declare module "fastify" {
1414}
1515
1616async function authPlugin ( app : FastifyInstance ) {
17- // API key auth via Bearer token
1817 app . decorateRequest ( "accountId" , undefined ) ;
1918 app . decorateRequest ( "apiKeyScope" , undefined ) ;
2019 app . decorateRequest ( "apiKeyCollectionId" , undefined ) ;
2120 app . decorateRequest ( "sessionUserId" , undefined ) ;
2221
23- app . addHook ( "onRequest" , async ( request : FastifyRequest ) => {
22+ // Paths that never require auth (even for POST)
23+ const publicPaths = new Set ( [
24+ "/api/health" ,
25+ "/api/accounts/signup" ,
26+ "/api/accounts/login" ,
27+ "/api/accounts/forgot-password" ,
28+ "/api/accounts/reset-password" ,
29+ ] ) ;
30+
31+ const internalToken = process . env . INTERNAL_API_TOKEN ?? "internal-dev-token" ;
32+
33+ app . addHook ( "onRequest" , async ( request : FastifyRequest , reply : FastifyReply ) => {
34+ // Internal service calls from Astro SSR
35+ const internalHeader = request . headers [ "x-internal-token" ] ;
36+ if ( internalHeader === internalToken ) {
37+ request . apiKeyScope = "read" ;
38+ return ;
39+ }
40+
41+ // API key auth via Bearer token
2442 const auth = request . headers . authorization ;
2543 if ( auth ?. startsWith ( "Bearer " ) ) {
2644 const token = auth . slice ( 7 ) ;
2745 const keys = await db . select ( ) . from ( schema . apiKeys ) ;
46+ let matched = false ;
2847 for ( const key of keys ) {
2948 const match = await bcrypt . compare ( token , key . keyHash ) ;
3049 if ( match ) {
@@ -35,9 +54,14 @@ async function authPlugin(app: FastifyInstance) {
3554 . update ( schema . apiKeys )
3655 . set ( { lastUsedAt : new Date ( ) } )
3756 . where ( eq ( schema . apiKeys . id , key . id ) ) ;
38- return ;
57+ matched = true ;
58+ break ;
3959 }
4060 }
61+ if ( ! matched ) {
62+ return reply . status ( 401 ) . send ( { error : "Invalid API key" , statusCode : 401 } ) ;
63+ }
64+ return ;
4165 }
4266
4367 // Session cookie auth
@@ -62,6 +86,16 @@ async function authPlugin(app: FastifyInstance) {
6286 // Invalid or expired cookie — ignore silently
6387 }
6488 }
89+
90+ // Public GETs are allowed without auth (rate-limited by IP)
91+ if ( request . method === "GET" ) return ;
92+
93+ // All writes (POST/PATCH/PUT/DELETE) require auth, except public paths
94+ if ( ! request . accountId ) {
95+ const path = request . url . split ( "?" ) [ 0 ] ;
96+ if ( publicPaths . has ( path ) ) return ;
97+ return reply . status ( 401 ) . send ( { error : "Authentication required" , statusCode : 401 } ) ;
98+ }
6599 } ) ;
66100}
67101
0 commit comments