feat(api): add Better Auth plugin and protected GET /me endpoint
- 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) <noreply@anthropic.com>
This commit is contained in:
1 parent
0139b13fc6
commit
5f17f95d6d
12 files changed
+81
-8
No files matched your search
@@ -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=
|
||||
@@ -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"
|
||||
|
||||
@@ -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<FastifyInstance> {
|
||||
@@ -36,9 +38,11 @@ export async function buildApp(): Promise<FastifyInstance> {
|
||||
});
|
||||
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;
|
||||
}
|
||||
@@ -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<typeof envSchema>;
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { auth } from '@ketopath/auth';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import fp from 'fastify-plugin';
|
||||
|
||||
type SessionPayload = Awaited<ReturnType<typeof auth.api.getSession>>;
|
||||
type User = NonNullable<SessionPayload>['user'];
|
||||
type Session = NonNullable<SessionPayload>['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<void> => {
|
||||
if (!request.user || !request.session) {
|
||||
return reply.code(401).send({ error: 'unauthorized' });
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { prisma } from './client';
|
||||
export { prisma } from './client.js';
|
||||
export {
|
||||
ActivityLevel,
|
||||
CookingTime,
|
||||
|
||||
Generated
BIN
Binary file not shown.
Reference in new issue
Block a user