chore: update gitignore for phase02

This commit is contained in:
hibna
2026-02-21 13:22:51 +03:00
parent 2215003a4d
commit 8eb7c90958
16 changed files with 479 additions and 29 deletions
+196
View File
@@ -0,0 +1,196 @@
import type { FastifyInstance } from 'fastify';
import { eq } from 'drizzle-orm';
import { users } from '@source/database';
import { hashPassword, verifyPassword } from '../../lib/password.js';
import { signAccessToken, signRefreshToken, verifyRefreshToken } from '../../lib/jwt.js';
import type { AccessTokenPayload, RefreshTokenPayload } from '../../lib/jwt.js';
import { AppError } from '../../lib/errors.js';
import { RegisterSchema, LoginSchema } from './schemas.js';
const REFRESH_COOKIE_NAME = 'refresh_token';
const REFRESH_COOKIE_OPTIONS = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax' as const,
path: '/api/auth',
maxAge: 7 * 24 * 60 * 60, // 7 days in seconds
};
export default async function authRoutes(app: FastifyInstance) {
// POST /api/auth/register
app.post('/register', { schema: RegisterSchema }, async (request, reply) => {
const { email, username, password } = request.body as {
email: string;
username: string;
password: string;
};
// Check if email already exists
const existingEmail = await app.db.query.users.findFirst({
where: eq(users.email, email),
});
if (existingEmail) {
throw AppError.conflict('Email already in use', 'EMAIL_TAKEN');
}
// Check if username already exists
const existingUsername = await app.db.query.users.findFirst({
where: eq(users.username, username),
});
if (existingUsername) {
throw AppError.conflict('Username already in use', 'USERNAME_TAKEN');
}
const passwordHash = await hashPassword(password);
const [user] = await app.db
.insert(users)
.values({
email,
username,
passwordHash,
})
.returning({
id: users.id,
email: users.email,
username: users.username,
isSuperAdmin: users.isSuperAdmin,
});
// Generate tokens
const accessToken = signAccessToken(app, {
sub: user!.id,
email: user!.email,
isSuperAdmin: user!.isSuperAdmin,
});
const refreshToken = signRefreshToken(app, {
sub: user!.id,
type: 'refresh',
});
reply.setCookie(REFRESH_COOKIE_NAME, refreshToken, REFRESH_COOKIE_OPTIONS);
return reply.code(201).send({
user: {
id: user!.id,
email: user!.email,
username: user!.username,
isSuperAdmin: user!.isSuperAdmin,
},
accessToken,
});
});
// POST /api/auth/login
app.post('/login', { schema: LoginSchema }, async (request, reply) => {
const { email, password } = request.body as { email: string; password: string };
const user = await app.db.query.users.findFirst({
where: eq(users.email, email),
});
if (!user) {
throw AppError.unauthorized('Invalid email or password', 'INVALID_CREDENTIALS');
}
const isValid = await verifyPassword(user.passwordHash, password);
if (!isValid) {
throw AppError.unauthorized('Invalid email or password', 'INVALID_CREDENTIALS');
}
const accessToken = signAccessToken(app, {
sub: user.id,
email: user.email,
isSuperAdmin: user.isSuperAdmin,
});
const refreshToken = signRefreshToken(app, {
sub: user.id,
type: 'refresh',
});
reply.setCookie(REFRESH_COOKIE_NAME, refreshToken, REFRESH_COOKIE_OPTIONS);
return {
user: {
id: user.id,
email: user.email,
username: user.username,
isSuperAdmin: user.isSuperAdmin,
avatarUrl: user.avatarUrl,
},
accessToken,
};
});
// POST /api/auth/refresh
app.post('/refresh', async (request, reply) => {
const token = request.cookies[REFRESH_COOKIE_NAME];
if (!token) {
throw AppError.unauthorized('No refresh token', 'NO_REFRESH_TOKEN');
}
let payload: RefreshTokenPayload;
try {
payload = verifyRefreshToken(app, token);
} catch {
reply.clearCookie(REFRESH_COOKIE_NAME, { path: '/api/auth' });
throw AppError.unauthorized('Invalid refresh token', 'INVALID_REFRESH_TOKEN');
}
const user = await app.db.query.users.findFirst({
where: eq(users.id, payload.sub),
});
if (!user) {
reply.clearCookie(REFRESH_COOKIE_NAME, { path: '/api/auth' });
throw AppError.unauthorized('User not found', 'USER_NOT_FOUND');
}
// Token rotation: issue new tokens
const accessToken = signAccessToken(app, {
sub: user.id,
email: user.email,
isSuperAdmin: user.isSuperAdmin,
});
const newRefreshToken = signRefreshToken(app, {
sub: user.id,
type: 'refresh',
});
reply.setCookie(REFRESH_COOKIE_NAME, newRefreshToken, REFRESH_COOKIE_OPTIONS);
return { accessToken };
});
// POST /api/auth/logout
app.post('/logout', async (_request, reply) => {
reply.clearCookie(REFRESH_COOKIE_NAME, { path: '/api/auth' });
return { success: true };
});
// GET /api/auth/me
app.get('/me', { onRequest: [app.authenticate] }, async (request) => {
const payload = request.user;
const user = await app.db.query.users.findFirst({
where: eq(users.id, payload.sub),
columns: {
id: true,
email: true,
username: true,
isSuperAdmin: true,
avatarUrl: true,
createdAt: true,
},
});
if (!user) {
throw AppError.notFound('User not found');
}
return { user };
});
}