Add CMS features with admin interface and OAuth authentication integration
- Introduced Caddy server for serving frontend and API backend. - Implemented admin dashboard for creating, editing, and managing events. - Replaced session-based authentication with token-based OAuth using Gitea. - Added support for drag-and-drop event reordering in the admin interface. - Standardized Fastify route validation with JSON schemas. - Enhanced authentication flow with cookie-based state and secure token storage. - Reworked backend routes to handle publishing, event management, and content updates. - Updated `Dockerfile.caddy` and `fly.toml` for deployment configuration.
This commit is contained in:
@ -6,10 +6,15 @@ import { eq } from 'drizzle-orm';
|
||||
import { GiteaService } from '../services/gitea.service.js';
|
||||
import { env } from '../config/env.js';
|
||||
|
||||
const callbackSchema = z.object({
|
||||
code: z.string(),
|
||||
state: z.string(),
|
||||
});
|
||||
// Use explicit JSON schema for Fastify route validation to avoid provider issues
|
||||
const callbackQueryJsonSchema = {
|
||||
type: 'object',
|
||||
required: ['code', 'state'],
|
||||
properties: {
|
||||
code: { type: 'string' },
|
||||
state: { type: 'string' },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const authRoute: FastifyPluginAsync = async (fastify) => {
|
||||
const giteaService = new GiteaService();
|
||||
@ -22,8 +27,15 @@ const authRoute: FastifyPluginAsync = async (fastify) => {
|
||||
// Generate CSRF state token
|
||||
const state = giteaService.generateState();
|
||||
|
||||
// Store state in session
|
||||
request.session.set('oauth_state', state);
|
||||
// Store state in a short-lived cookie
|
||||
reply.setCookie('oauth_state', state, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
// Use HTTPS-based detection to avoid setting Secure on localhost HTTP
|
||||
secure: !!env.FRONTEND_URL && env.FRONTEND_URL.startsWith('https'),
|
||||
maxAge: 10 * 60, // 10 minutes
|
||||
});
|
||||
|
||||
// Generate authorization URL
|
||||
const authUrl = giteaService.getAuthorizationUrl(state);
|
||||
@ -38,20 +50,20 @@ const authRoute: FastifyPluginAsync = async (fastify) => {
|
||||
*/
|
||||
fastify.get('/auth/callback', {
|
||||
schema: {
|
||||
querystring: callbackSchema,
|
||||
querystring: callbackQueryJsonSchema,
|
||||
},
|
||||
}, async (request, reply) => {
|
||||
try {
|
||||
const { code, state } = request.query as z.infer<typeof callbackSchema>;
|
||||
const { code, state } = request.query as { code: string; state: string };
|
||||
|
||||
// Verify CSRF state
|
||||
const expectedState = request.session.get('oauth_state');
|
||||
// Verify CSRF state from cookie
|
||||
const expectedState = request.cookies?.oauth_state as string | undefined;
|
||||
if (!expectedState || state !== expectedState) {
|
||||
return reply.code(400).send({ error: 'Invalid state parameter' });
|
||||
}
|
||||
|
||||
// Clear state from session
|
||||
request.session.delete('oauth_state');
|
||||
// Clear state cookie
|
||||
reply.clearCookie('oauth_state', { path: '/' });
|
||||
|
||||
// Exchange code for access token
|
||||
const tokenResponse = await giteaService.exchangeCodeForToken(code);
|
||||
@ -103,18 +115,27 @@ const authRoute: FastifyPluginAsync = async (fastify) => {
|
||||
{
|
||||
id: user.id,
|
||||
giteaId: user.giteaId,
|
||||
username: user.giteaUsername,
|
||||
role: user.role,
|
||||
username: user.giteaUsername || '',
|
||||
role: user.role ?? 'admin',
|
||||
},
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
// Redirect to frontend with token
|
||||
// Also set token as HttpOnly cookie so subsequent API calls authenticate reliably
|
||||
reply.setCookie('token', token, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: !!env.FRONTEND_URL && env.FRONTEND_URL.startsWith('https'),
|
||||
maxAge: 60 * 60 * 24, // 24h
|
||||
});
|
||||
|
||||
// Redirect to admin dashboard
|
||||
const frontendUrl = env.FRONTEND_URL;
|
||||
return reply.redirect(`${frontendUrl}/auth/callback?token=${token}`);
|
||||
return reply.redirect(`${frontendUrl}/admin`);
|
||||
|
||||
} catch (error) {
|
||||
fastify.log.error('OAuth callback error:', error);
|
||||
fastify.log.error({ err: error }, 'OAuth callback error');
|
||||
return reply.code(500).send({ error: 'Authentication failed' });
|
||||
}
|
||||
});
|
||||
@ -139,12 +160,14 @@ const authRoute: FastifyPluginAsync = async (fastify) => {
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.giteaUsername,
|
||||
email: user.giteaEmail,
|
||||
displayName: user.displayName,
|
||||
avatarUrl: user.avatarUrl,
|
||||
role: user.role,
|
||||
user: {
|
||||
id: user.id,
|
||||
giteaUsername: user.giteaUsername,
|
||||
giteaEmail: user.giteaEmail,
|
||||
displayName: user.displayName,
|
||||
avatarUrl: user.avatarUrl,
|
||||
role: user.role,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@ -157,6 +180,7 @@ const authRoute: FastifyPluginAsync = async (fastify) => {
|
||||
}, async (request, reply) => {
|
||||
// For JWT, logout is primarily client-side (delete token)
|
||||
// You could maintain a token blacklist in Redis for production
|
||||
reply.clearCookie('token', { path: '/' });
|
||||
return { message: 'Logged out successfully' };
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user