165 lines
4.4 KiB
TypeScript
165 lines
4.4 KiB
TypeScript
import { FastifyPluginAsync } from 'fastify';
|
|
import { z } from 'zod';
|
|
import { db } from '../config/database.js';
|
|
import { users } from '../db/schema.js';
|
|
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(),
|
|
});
|
|
|
|
const authRoute: FastifyPluginAsync = async (fastify) => {
|
|
const giteaService = new GiteaService();
|
|
|
|
/**
|
|
* GET /auth/gitea
|
|
* Initiate OAuth flow
|
|
*/
|
|
fastify.get('/auth/gitea', async (request, reply) => {
|
|
// Generate CSRF state token
|
|
const state = giteaService.generateState();
|
|
|
|
// Store state in session
|
|
request.session.set('oauth_state', state);
|
|
|
|
// Generate authorization URL
|
|
const authUrl = giteaService.getAuthorizationUrl(state);
|
|
|
|
// Redirect to Gitea
|
|
return reply.redirect(authUrl);
|
|
});
|
|
|
|
/**
|
|
* GET /auth/callback
|
|
* OAuth callback endpoint
|
|
*/
|
|
fastify.get('/auth/callback', {
|
|
schema: {
|
|
querystring: callbackSchema,
|
|
},
|
|
}, async (request, reply) => {
|
|
try {
|
|
const { code, state } = request.query as z.infer<typeof callbackSchema>;
|
|
|
|
// Verify CSRF state
|
|
const expectedState = request.session.get('oauth_state');
|
|
if (!expectedState || state !== expectedState) {
|
|
return reply.code(400).send({ error: 'Invalid state parameter' });
|
|
}
|
|
|
|
// Clear state from session
|
|
request.session.delete('oauth_state');
|
|
|
|
// Exchange code for access token
|
|
const tokenResponse = await giteaService.exchangeCodeForToken(code);
|
|
|
|
// Fetch user info from Gitea
|
|
const giteaUser = await giteaService.getUserInfo(tokenResponse.access_token);
|
|
|
|
// Check if user is allowed
|
|
if (!giteaService.isUserAllowed(giteaUser.login)) {
|
|
return reply.code(403).send({
|
|
error: 'Access denied. You are not authorized to access this CMS.'
|
|
});
|
|
}
|
|
|
|
// Find or create user in database
|
|
let [user] = await db
|
|
.select()
|
|
.from(users)
|
|
.where(eq(users.giteaId, giteaUser.id.toString()))
|
|
.limit(1);
|
|
|
|
if (!user) {
|
|
// Create new user
|
|
[user] = await db.insert(users).values({
|
|
giteaId: giteaUser.id.toString(),
|
|
giteaUsername: giteaUser.login,
|
|
giteaEmail: giteaUser.email,
|
|
displayName: giteaUser.full_name,
|
|
avatarUrl: giteaUser.avatar_url,
|
|
lastLogin: new Date(),
|
|
}).returning();
|
|
} else {
|
|
// Update existing user
|
|
[user] = await db
|
|
.update(users)
|
|
.set({
|
|
giteaUsername: giteaUser.login,
|
|
giteaEmail: giteaUser.email,
|
|
displayName: giteaUser.full_name,
|
|
avatarUrl: giteaUser.avatar_url,
|
|
lastLogin: new Date(),
|
|
})
|
|
.where(eq(users.id, user.id))
|
|
.returning();
|
|
}
|
|
|
|
// Generate JWT for session management
|
|
const token = fastify.jwt.sign(
|
|
{
|
|
id: user.id,
|
|
giteaId: user.giteaId,
|
|
username: user.giteaUsername,
|
|
role: user.role,
|
|
},
|
|
{ expiresIn: '24h' }
|
|
);
|
|
|
|
// Redirect to frontend with token
|
|
const frontendUrl = env.FRONTEND_URL;
|
|
return reply.redirect(`${frontendUrl}/auth/callback?token=${token}`);
|
|
|
|
} catch (error) {
|
|
fastify.log.error('OAuth callback error:', error);
|
|
return reply.code(500).send({ error: 'Authentication failed' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /auth/me
|
|
* Get current user info
|
|
*/
|
|
fastify.get('/auth/me', {
|
|
preHandler: [fastify.authenticate],
|
|
}, async (request, reply) => {
|
|
const userId = request.user.id;
|
|
|
|
const [user] = await db
|
|
.select()
|
|
.from(users)
|
|
.where(eq(users.id, userId))
|
|
.limit(1);
|
|
|
|
if (!user) {
|
|
return reply.code(404).send({ error: 'User not found' });
|
|
}
|
|
|
|
return {
|
|
id: user.id,
|
|
username: user.giteaUsername,
|
|
email: user.giteaEmail,
|
|
displayName: user.displayName,
|
|
avatarUrl: user.avatarUrl,
|
|
role: user.role,
|
|
};
|
|
});
|
|
|
|
/**
|
|
* POST /auth/logout
|
|
* Logout (client-side token deletion)
|
|
*/
|
|
fastify.post('/auth/logout', {
|
|
preHandler: [fastify.authenticate],
|
|
}, async (request, reply) => {
|
|
// For JWT, logout is primarily client-side (delete token)
|
|
// You could maintain a token blacklist in Redis for production
|
|
return { message: 'Logged out successfully' };
|
|
});
|
|
};
|
|
|
|
export default authRoute;
|