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
|
CORS_ORIGINS=http://localhost:3000
|
||||||
|
|
||||||
DATABASE_URL=postgresql://luciano:luciano%40@localhost:5432/ketopath?schema=public
|
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/helmet": "^11.1.1",
|
||||||
"@fastify/rate-limit": "^9.1.0",
|
"@fastify/rate-limit": "^9.1.0",
|
||||||
"@fastify/sensible": "^5.6.0",
|
"@fastify/sensible": "^5.6.0",
|
||||||
|
"@ketopath/auth": "workspace:*",
|
||||||
"@ketopath/db": "workspace:*",
|
"@ketopath/db": "workspace:*",
|
||||||
"@ketopath/shared": "workspace:*",
|
"@ketopath/shared": "workspace:*",
|
||||||
|
"better-auth": "^1.0.21",
|
||||||
"fastify": "^4.28.1",
|
"fastify": "^4.28.1",
|
||||||
"fastify-plugin": "^4.5.1",
|
"fastify-plugin": "^4.5.1",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import Fastify, { type FastifyInstance } from 'fastify';
|
|||||||
import { env } from './config/env.js';
|
import { env } from './config/env.js';
|
||||||
import { dbRoutes } from './modules/db/db.routes.js';
|
import { dbRoutes } from './modules/db/db.routes.js';
|
||||||
import { healthRoutes } from './modules/health/health.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';
|
import { prismaPlugin } from './plugins/prisma.js';
|
||||||
|
|
||||||
export async function buildApp(): Promise<FastifyInstance> {
|
export async function buildApp(): Promise<FastifyInstance> {
|
||||||
@@ -36,9 +38,11 @@ export async function buildApp(): Promise<FastifyInstance> {
|
|||||||
});
|
});
|
||||||
await app.register(sensible);
|
await app.register(sensible);
|
||||||
await app.register(prismaPlugin);
|
await app.register(prismaPlugin);
|
||||||
|
await app.register(authPlugin);
|
||||||
|
|
||||||
await app.register(healthRoutes);
|
await app.register(healthRoutes);
|
||||||
await app.register(dbRoutes);
|
await app.register(dbRoutes);
|
||||||
|
await app.register(meRoutes);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,8 @@ const envSchema = z.object({
|
|||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
),
|
),
|
||||||
DATABASE_URL: z.string().url(),
|
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>;
|
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} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
transpilePackages: ['@ketopath/auth', '@ketopath/shared', '@ketopath/ui'],
|
transpilePackages: ['@ketopath/auth', '@ketopath/db', '@ketopath/shared', '@ketopath/ui'],
|
||||||
experimental: {
|
experimental: {
|
||||||
typedRoutes: true,
|
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);
|
export default withNextIntl(nextConfig);
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
export { auth, type Auth } from './server';
|
export { auth, type Auth } from './server.js';
|
||||||
export { readAuthEnv, type AuthEnv } from './env';
|
export { readAuthEnv, type AuthEnv } from './env.js';
|
||||||
@@ -2,7 +2,7 @@ import { prisma } from '@ketopath/db';
|
|||||||
import { betterAuth } from 'better-auth';
|
import { betterAuth } from 'better-auth';
|
||||||
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
||||||
|
|
||||||
import { readAuthEnv } from './env';
|
import { readAuthEnv } from './env.js';
|
||||||
|
|
||||||
const env = readAuthEnv();
|
const env = readAuthEnv();
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
{
|
{
|
||||||
"extends": "@ketopath/tsconfig/base.json",
|
"extends": "@ketopath/tsconfig/node.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"lib": ["DOM", "ES2022"],
|
"lib": ["DOM", "ES2022"]
|
||||||
"types": ["node"]
|
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { prisma } from './client';
|
export { prisma } from './client.js';
|
||||||
export {
|
export {
|
||||||
ActivityLevel,
|
ActivityLevel,
|
||||||
CookingTime,
|
CookingTime,
|
||||||
|
|||||||
Generated
BIN
Binary file not shown.
Reference in new issue
Block a user