This commit is contained in:
Matthias Hochmeister
2026-02-23 17:08:58 +01:00
commit f09748f4a1
97 changed files with 17729 additions and 0 deletions

View File

@@ -0,0 +1,246 @@
import { Request, Response } from 'express';
import authentikService from '../services/authentik.service';
import tokenService from '../services/token.service';
import userService from '../services/user.service';
import logger from '../utils/logger';
import { AuthRequest } from '../types/auth.types';
class AuthController {
/**
* Handle OAuth callback
* POST /api/auth/callback
*/
async handleCallback(req: Request, res: Response): Promise<void> {
try {
const { code } = req.body as AuthRequest;
// Validate code
if (!code) {
res.status(400).json({
success: false,
message: 'Authorization code is required',
});
return;
}
logger.info('Processing OAuth callback', { hasCode: !!code });
// Step 1: Exchange code for tokens
const tokens = await authentikService.exchangeCodeForTokens(code);
// Step 2: Get user info from Authentik
const userInfo = await authentikService.getUserInfo(tokens.access_token);
// Step 3: Verify ID token if present
if (tokens.id_token) {
try {
authentikService.verifyIdToken(tokens.id_token);
} catch (error) {
logger.warn('ID token verification failed', { error });
}
}
// Step 4: Find or create user in database
let user = await userService.findByAuthentikSub(userInfo.sub);
if (!user) {
// User doesn't exist, create new user
logger.info('Creating new user from Authentik', {
sub: userInfo.sub,
email: userInfo.email,
});
user = await userService.createUser({
email: userInfo.email,
authentik_sub: userInfo.sub,
preferred_username: userInfo.preferred_username,
given_name: userInfo.given_name,
family_name: userInfo.family_name,
name: userInfo.name,
profile_picture_url: userInfo.picture,
});
} else {
// User exists, update last login
logger.info('Existing user logging in', {
userId: user.id,
email: user.email,
});
await userService.updateLastLogin(user.id);
}
// Check if user is active
if (!user.is_active) {
logger.warn('Inactive user attempted login', { userId: user.id });
res.status(403).json({
success: false,
message: 'User account is inactive',
});
return;
}
// Step 5: Generate internal JWT token
const accessToken = tokenService.generateToken({
userId: user.id,
email: user.email,
authentikSub: user.authentik_sub,
});
// Generate refresh token
const refreshToken = tokenService.generateRefreshToken({
userId: user.id,
email: user.email,
});
logger.info('User authenticated successfully', {
userId: user.id,
email: user.email,
});
// Step 6: Return tokens and user info
res.status(200).json({
success: true,
message: 'Authentication successful',
data: {
accessToken,
refreshToken,
user: {
id: user.id,
email: user.email,
name: user.name,
preferredUsername: user.preferred_username,
givenName: user.given_name,
familyName: user.family_name,
profilePictureUrl: user.profile_picture_url,
isActive: user.is_active,
},
},
});
} catch (error) {
logger.error('OAuth callback error', { error });
const message =
error instanceof Error ? error.message : 'Authentication failed';
res.status(500).json({
success: false,
message,
});
}
}
/**
* Handle logout
* POST /api/auth/logout
*/
async handleLogout(req: Request, res: Response): Promise<void> {
try {
// In a stateless JWT setup, logout is handled client-side by removing the token
// However, we can log the event for audit purposes
if (req.user) {
logger.info('User logged out', {
userId: req.user.id,
email: req.user.email,
});
}
res.status(200).json({
success: true,
message: 'Logout successful',
});
} catch (error) {
logger.error('Logout error', { error });
res.status(500).json({
success: false,
message: 'Logout failed',
});
}
}
/**
* Handle token refresh
* POST /api/auth/refresh
*/
async handleRefresh(req: Request, res: Response): Promise<void> {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
res.status(400).json({
success: false,
message: 'Refresh token is required',
});
return;
}
// Verify refresh token
let decoded;
try {
decoded = tokenService.verifyRefreshToken(refreshToken);
} catch (error) {
const message = error instanceof Error ? error.message : 'Invalid refresh token';
res.status(401).json({
success: false,
message,
});
return;
}
// Get user from database
const user = await userService.findById(decoded.userId);
if (!user) {
logger.warn('Refresh token valid but user not found', {
userId: decoded.userId,
});
res.status(401).json({
success: false,
message: 'User not found',
});
return;
}
if (!user.is_active) {
logger.warn('Inactive user attempted token refresh', {
userId: user.id,
});
res.status(403).json({
success: false,
message: 'User account is inactive',
});
return;
}
// Generate new access token
const accessToken = tokenService.generateToken({
userId: user.id,
email: user.email,
authentikSub: user.authentik_sub,
});
logger.info('Token refreshed successfully', {
userId: user.id,
email: user.email,
});
res.status(200).json({
success: true,
message: 'Token refreshed successfully',
data: {
accessToken,
},
});
} catch (error) {
logger.error('Token refresh error', { error });
res.status(500).json({
success: false,
message: 'Token refresh failed',
});
}
}
}
export default new AuthController();