Files
insforge--insforge/backend/src/api/routes/auth/admin.routes.ts
T
wehub-resource-sync 3a28426bf4
Lint and Format Check / lint-and-format (push) Failing after 0s
Check Migrations / Check for duplicate migration numbers (push) Failing after 1s
CI Pre-merge Check / CI Pre-merge Check (push) Failing after 2m17s
chore: import upstream snapshot with attribution
2026-07-13 12:23:40 +08:00

171 lines
5.6 KiB
TypeScript

import { Router, Request, Response, NextFunction } from 'express';
import { AuthService } from '@/services/auth/auth.service.js';
import { AuthRequest, verifyToken } from '@/api/middlewares/auth.js';
import { TokenManager } from '@/infra/security/token.manager.js';
import { AppError } from '@/utils/errors.js';
import { successResponse } from '@/utils/response.js';
import {
ADMIN_REFRESH_TOKEN_COOKIE_NAME,
setAdminRefreshTokenCookie,
clearAdminRefreshTokenCookie,
} from '@/utils/cookies.js';
import {
ERROR_CODES,
createAdminSessionRequestSchema,
exchangeAdminSessionRequestSchema,
type CreateAdminSessionResponse,
type GetCurrentAdminSessionResponse,
} from '@insforge/shared-schemas';
import logger from '@/utils/logger.js';
const router = Router();
const authService = AuthService.getInstance();
// POST /api/auth/admin/sessions/exchange - Exchange authorization code for admin session
router.post('/sessions/exchange', async (req: Request, res: Response, next: NextFunction) => {
try {
const validationResult = exchangeAdminSessionRequestSchema.safeParse(req.body);
if (!validationResult.success) {
throw new AppError(
validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
400,
ERROR_CODES.INVALID_INPUT
);
}
const { code } = validationResult.data;
const result: CreateAdminSessionResponse =
await authService.adminLoginWithAuthorizationCode(code);
// Set refresh token as httpOnly cookie + CSRF token for web clients
const tokenManager = TokenManager.getInstance();
const { refreshToken, csrfToken } = tokenManager.generateRefreshTokenWithCsrf(
result.admin.sub,
'admin'
);
setAdminRefreshTokenCookie(res, refreshToken);
successResponse(res, { ...result, csrfToken });
} catch (error) {
if (error instanceof AppError) {
next(error);
} else {
logger.error('[Auth:AdminSessionExchange] Failed to exchange admin session', { error });
next(new AppError('Failed to exchange admin session', 500, ERROR_CODES.INTERNAL_ERROR));
}
}
});
// POST /api/auth/admin/sessions - Create admin session (web only)
router.post('/sessions', (req: Request, res: Response, next: NextFunction) => {
try {
const validationResult = createAdminSessionRequestSchema.safeParse(req.body);
if (!validationResult.success) {
throw new AppError(
validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
400,
ERROR_CODES.INVALID_INPUT
);
}
const { username, password } = validationResult.data;
const result: CreateAdminSessionResponse = authService.adminLogin(username, password);
// Set refresh token as httpOnly cookie + CSRF token for web clients
const tokenManager = TokenManager.getInstance();
const { refreshToken, csrfToken } = tokenManager.generateRefreshTokenWithCsrf(
result.admin.sub,
'admin'
);
setAdminRefreshTokenCookie(res, refreshToken);
successResponse(res, { ...result, csrfToken });
} catch (error) {
next(error);
}
});
// GET /api/auth/admin/sessions/current - Get current dashboard admin session
router.get(
'/sessions/current',
verifyToken,
(req: AuthRequest, res: Response, next: NextFunction) => {
try {
if (req.user?.role !== 'project_admin' || !req.user.id) {
throw new AppError('Admin access required', 403, ERROR_CODES.AUTH_UNAUTHORIZED);
}
const response: GetCurrentAdminSessionResponse = {
admin: {
sub: req.user.id,
},
};
successResponse(res, response);
} catch (error) {
next(error);
}
}
);
// POST /api/auth/admin/refresh - Refresh admin dashboard access token
// Uses a dashboard-specific httpOnly cookie + X-CSRF-Token header.
router.post('/refresh', (req: Request, res: Response, next: NextFunction) => {
try {
const tokenManager = TokenManager.getInstance();
const refreshToken = req.cookies?.[ADMIN_REFRESH_TOKEN_COOKIE_NAME];
if (!refreshToken) {
throw new AppError('No admin refresh token provided', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
}
const payload = tokenManager.verifyRefreshToken(refreshToken);
if (payload.sessionType !== 'admin') {
throw new AppError('Invalid admin refresh session type', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
}
const csrfHeader = req.headers['x-csrf-token'] as string | undefined;
if (!tokenManager.verifyCsrfToken(csrfHeader, payload)) {
logger.warn('[Auth:AdminRefresh] CSRF token validation failed');
throw new AppError('Invalid CSRF token', 403, ERROR_CODES.AUTH_UNAUTHORIZED);
}
const newAccessToken = tokenManager.generateAccessToken({
sub: payload.sub,
role: 'project_admin',
});
const { refreshToken: newRefreshToken, csrfToken: newCsrfToken } =
tokenManager.generateRefreshTokenWithCsrf(payload.sub, 'admin', payload.csrfNonce);
setAdminRefreshTokenCookie(res, newRefreshToken);
successResponse(res, {
admin: {
sub: payload.sub,
},
accessToken: newAccessToken,
csrfToken: newCsrfToken,
});
} catch (error) {
if (error instanceof AppError && error.statusCode === 401) {
clearAdminRefreshTokenCookie(res);
}
next(error);
}
});
// POST /api/auth/admin/logout - Logout dashboard session
router.post('/logout', (_req: Request, res: Response, next: NextFunction) => {
try {
clearAdminRefreshTokenCookie(res);
successResponse(res, {
success: true,
message: 'Logged out successfully',
});
} catch (error) {
next(error);
}
});
export default router;