From 5f17f95d6dd32627878a9f4603a2366860d5616b Mon Sep 17 00:00:00 2001 From: luciano Date: Wed, 29 Apr 2026 13:50:14 +0200 Subject: [PATCH] feat(api): add Better Auth plugin and protected GET /me endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - authPlugin runs preHandler that turns Fastify request headers into a Headers object, calls auth.api.getSession, and decorates request.user / request.session - requireAuth() preHandler short-circuits with 401 when not signed in - New module modules/me with GET /me returning the authenticated user/session - env validates BETTER_AUTH_SECRET (≥32) and BETTER_AUTH_URL — must match web - Restored .js extensions in shared packages so NodeNext-resolution consumers (api) typecheck cleanly; Next webpack now uses extensionAlias to map .js → .ts - Re-enabled NodeNext for packages/auth and packages/db tsconfigs Verified end-to-end: - POST /api/auth/sign-in/email on web returns session cookie - GET /me on api with the cookie returns 200 + user/session - GET /me without the cookie returns 401 Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/api/.env.example | 8 ++++++ apps/api/package.json | 2 ++ apps/api/src/app.ts | 4 +++ apps/api/src/config/env.ts | 2 ++ apps/api/src/modules/me/me.routes.ts | 10 +++++++ apps/api/src/plugins/auth.ts | 39 +++++++++++++++++++++++++++ apps/web/next.config.mjs | 11 +++++++- packages/auth/src/index.ts | 4 +-- packages/auth/src/server.ts | 2 +- packages/auth/tsconfig.json | 5 ++-- packages/db/src/index.ts | 2 +- pnpm-lock.yaml | Bin 217428 -> 217814 bytes 12 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 apps/api/src/modules/me/me.routes.ts create mode 100644 apps/api/src/plugins/auth.ts diff --git a/apps/api/.env.example b/apps/api/.env.example index 5e4cc4a..3bd8fbb 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -7,3 +7,11 @@ LOG_LEVEL=info CORS_ORIGINS=http://localhost:3000 DATABASE_URL=postgresql://luciano:luciano%40@localhost:5432/ketopath?schema=public + +# Better Auth — DEVE coincidere con apps/web/.env (sessione condivisa) +BETTER_AUTH_SECRET= +BETTER_AUTH_URL=http://localhost:3000 + +# Google OAuth (configurato a fine progetto) +# GOOGLE_CLIENT_ID= +# GOOGLE_CLIENT_SECRET= diff --git a/apps/api/package.json b/apps/api/package.json index acb69d1..440bdc6 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -15,8 +15,10 @@ "@fastify/helmet": "^11.1.1", "@fastify/rate-limit": "^9.1.0", "@fastify/sensible": "^5.6.0", + "@ketopath/auth": "workspace:*", "@ketopath/db": "workspace:*", "@ketopath/shared": "workspace:*", + "better-auth": "^1.0.21", "fastify": "^4.28.1", "fastify-plugin": "^4.5.1", "zod": "^3.23.8" diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index adbea94..b393115 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -7,6 +7,8 @@ import Fastify, { type FastifyInstance } from 'fastify'; import { env } from './config/env.js'; import { dbRoutes } from './modules/db/db.routes.js'; import { healthRoutes } from './modules/health/health.routes.js'; +import { meRoutes } from './modules/me/me.routes.js'; +import { authPlugin } from './plugins/auth.js'; import { prismaPlugin } from './plugins/prisma.js'; export async function buildApp(): Promise { @@ -36,9 +38,11 @@ export async function buildApp(): Promise { }); await app.register(sensible); await app.register(prismaPlugin); + await app.register(authPlugin); await app.register(healthRoutes); await app.register(dbRoutes); + await app.register(meRoutes); return app; } diff --git a/apps/api/src/config/env.ts b/apps/api/src/config/env.ts index 7abd932..52adf2f 100644 --- a/apps/api/src/config/env.ts +++ b/apps/api/src/config/env.ts @@ -15,6 +15,8 @@ const envSchema = z.object({ .filter(Boolean), ), DATABASE_URL: z.string().url(), + BETTER_AUTH_SECRET: z.string().min(32), + BETTER_AUTH_URL: z.string().url(), }); export type Env = z.infer; diff --git a/apps/api/src/modules/me/me.routes.ts b/apps/api/src/modules/me/me.routes.ts new file mode 100644 index 0000000..948d1d5 --- /dev/null +++ b/apps/api/src/modules/me/me.routes.ts @@ -0,0 +1,10 @@ +import type { FastifyPluginAsync } from 'fastify'; + +import { requireAuth } from '../../plugins/auth.js'; + +export const meRoutes: FastifyPluginAsync = async (fastify) => { + fastify.get('/me', { preHandler: requireAuth() }, async (request) => ({ + user: request.user, + session: request.session, + })); +}; diff --git a/apps/api/src/plugins/auth.ts b/apps/api/src/plugins/auth.ts new file mode 100644 index 0000000..47ebe37 --- /dev/null +++ b/apps/api/src/plugins/auth.ts @@ -0,0 +1,39 @@ +import { auth } from '@ketopath/auth'; +import type { FastifyReply, FastifyRequest } from 'fastify'; +import fp from 'fastify-plugin'; + +type SessionPayload = Awaited>; +type User = NonNullable['user']; +type Session = NonNullable['session']; + +declare module 'fastify' { + interface FastifyRequest { + user: User | null; + session: Session | null; + } +} + +export const authPlugin = fp(async (app) => { + app.decorateRequest('user', null); + app.decorateRequest('session', null); + + app.addHook('preHandler', async (request) => { + const headers = new Headers(); + for (const [key, value] of Object.entries(request.headers)) { + if (typeof value === 'string') headers.set(key, value); + else if (Array.isArray(value)) headers.set(key, value.join(', ')); + } + + const result = await auth.api.getSession({ headers }); + request.user = result?.user ?? null; + request.session = result?.session ?? null; + }); +}); + +export function requireAuth() { + return async (request: FastifyRequest, reply: FastifyReply): Promise => { + if (!request.user || !request.session) { + return reply.code(401).send({ error: 'unauthorized' }); + } + }; +} diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index ec6068c..caa3379 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -5,10 +5,19 @@ const withNextIntl = createNextIntlPlugin('./src/i18n.ts'); /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, - transpilePackages: ['@ketopath/auth', '@ketopath/shared', '@ketopath/ui'], + transpilePackages: ['@ketopath/auth', '@ketopath/db', '@ketopath/shared', '@ketopath/ui'], experimental: { typedRoutes: true, }, + webpack(config) { + // I package interni usano .js nelle import (NodeNext). Webpack non lo + // risolve di default per i file TS: gli diciamo di provare anche .ts/.tsx. + config.resolve.extensionAlias = { + ...config.resolve.extensionAlias, + '.js': ['.ts', '.tsx', '.js', '.jsx'], + }; + return config; + }, }; export default withNextIntl(nextConfig); diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 8e8e3b7..54f39cc 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -1,2 +1,2 @@ -export { auth, type Auth } from './server'; -export { readAuthEnv, type AuthEnv } from './env'; +export { auth, type Auth } from './server.js'; +export { readAuthEnv, type AuthEnv } from './env.js'; diff --git a/packages/auth/src/server.ts b/packages/auth/src/server.ts index 9d72257..f1cd296 100644 --- a/packages/auth/src/server.ts +++ b/packages/auth/src/server.ts @@ -2,7 +2,7 @@ import { prisma } from '@ketopath/db'; import { betterAuth } from 'better-auth'; import { prismaAdapter } from 'better-auth/adapters/prisma'; -import { readAuthEnv } from './env'; +import { readAuthEnv } from './env.js'; const env = readAuthEnv(); diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json index 79c7de1..be069dd 100644 --- a/packages/auth/tsconfig.json +++ b/packages/auth/tsconfig.json @@ -1,11 +1,10 @@ { - "extends": "@ketopath/tsconfig/base.json", + "extends": "@ketopath/tsconfig/node.json", "compilerOptions": { "outDir": "dist", "rootDir": "src", "composite": true, - "lib": ["DOM", "ES2022"], - "types": ["node"] + "lib": ["DOM", "ES2022"] }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 198cde3..cf10914 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1,4 +1,4 @@ -export { prisma } from './client'; +export { prisma } from './client.js'; export { ActivityLevel, CookingTime, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c456f346bf9f69f34b44b5c65b27176a6be9f56f..2ef8663a52d54815155edd7cbe3055c6b9f35274 100644 GIT binary patch delta 59 zcmcbzi1*r3-VIFble4)JC+iE@PF}#yGufLxZ*!@TA|rcRVsS}kTIFOz7P01Q?Csas O8G)E-`!#mvI!^$1;}n