style: run prettier on entire codebase
This commit is contained in:
@@ -1,21 +1,26 @@
|
||||
import crypto from 'crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import prisma from '../config/database';
|
||||
import { config } from '../config/env';
|
||||
import { AuthData, JwtPayload } from '../types';
|
||||
import crypto from "crypto";
|
||||
import jwt from "jsonwebtoken";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { FastifyRequest, FastifyReply } from "fastify";
|
||||
import prisma from "../config/database";
|
||||
import { config } from "../config/env";
|
||||
import { AuthData, JwtPayload } from "../types";
|
||||
|
||||
// Pre-computed bcrypt hash for timing-safe comparison when user not found
|
||||
const DUMMY_HASH = '$2a$12$LJ3m4ys3Lg4oLBFnYP2amuPBzJnJBbGzCl5Y6X9Y8r0q5.s3L6OyO';
|
||||
const DUMMY_HASH =
|
||||
"$2a$12$LJ3m4ys3Lg4oLBFnYP2amuPBzJnJBbGzCl5Y6X9Y8r0q5.s3L6OyO";
|
||||
|
||||
// --- Token helpers ---
|
||||
|
||||
function hashToken(token: string): string {
|
||||
return crypto.createHash('sha256').update(token).digest('hex');
|
||||
return crypto.createHash("sha256").update(token).digest("hex");
|
||||
}
|
||||
|
||||
function generateAccessToken(user: { id: number; username: string; roleName: string | null }): string {
|
||||
function generateAccessToken(user: {
|
||||
id: number;
|
||||
username: string;
|
||||
roleName: string | null;
|
||||
}): string {
|
||||
return jwt.sign(
|
||||
{ sub: user.id, username: user.username, role: user.roleName },
|
||||
config.jwt.secret,
|
||||
@@ -24,7 +29,7 @@ function generateAccessToken(user: { id: number; username: string; roleName: str
|
||||
}
|
||||
|
||||
function generateRefreshToken(): string {
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
return crypto.randomBytes(32).toString("hex");
|
||||
}
|
||||
|
||||
// --- Auth data loading ---
|
||||
@@ -45,10 +50,12 @@ async function loadAuthData(userId: number): Promise<AuthData | null> {
|
||||
|
||||
if (!user || !user.is_active) return null;
|
||||
|
||||
const isAdmin = user.roles?.name === 'admin';
|
||||
const isAdmin = user.roles?.name === "admin";
|
||||
const permissions = isAdmin
|
||||
? (await prisma.permissions.findMany()).map((p: { name: string }) => p.name)
|
||||
: (user.roles?.role_permissions ?? []).map((rp: { permissions: { name: string } }) => rp.permissions.name);
|
||||
: (user.roles?.role_permissions ?? []).map(
|
||||
(rp: { permissions: { name: string } }) => rp.permissions.name,
|
||||
);
|
||||
|
||||
return {
|
||||
userId: user.id,
|
||||
@@ -60,7 +67,9 @@ async function loadAuthData(userId: number): Promise<AuthData | null> {
|
||||
roleName: user.roles?.name ?? null,
|
||||
permissions,
|
||||
totp_enabled: !!user.totp_enabled,
|
||||
require_2fa: !!(await prisma.company_settings.findFirst({ select: { require_2fa: true } }))?.require_2fa,
|
||||
require_2fa: !!(
|
||||
await prisma.company_settings.findFirst({ select: { require_2fa: true } })
|
||||
)?.require_2fa,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,9 +81,14 @@ export async function login(
|
||||
rememberMe: boolean,
|
||||
request: FastifyRequest,
|
||||
): Promise<
|
||||
| { type: 'success'; accessToken: string; refreshToken: string; user: AuthData }
|
||||
| { type: 'totp_required'; loginToken: string }
|
||||
| { type: 'error'; message: string; status: number }
|
||||
| {
|
||||
type: "success";
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
user: AuthData;
|
||||
}
|
||||
| { type: "totp_required"; loginToken: string }
|
||||
| { type: "error"; message: string; status: number }
|
||||
> {
|
||||
const user = await prisma.users.findFirst({
|
||||
where: {
|
||||
@@ -86,40 +100,60 @@ export async function login(
|
||||
if (!user) {
|
||||
// Timing-safe: run bcrypt even when user not found
|
||||
await bcrypt.compare(password, DUMMY_HASH);
|
||||
return { type: 'error', message: 'Neplatné přihlašovací údaje', status: 401 };
|
||||
return {
|
||||
type: "error",
|
||||
message: "Neplatné přihlašovací údaje",
|
||||
status: 401,
|
||||
};
|
||||
}
|
||||
|
||||
if (!user.is_active) {
|
||||
return { type: 'error', message: 'Účet je deaktivován', status: 403 };
|
||||
return { type: "error", message: "Účet je deaktivován", status: 403 };
|
||||
}
|
||||
|
||||
// Check lockout
|
||||
if (user.locked_until && new Date(user.locked_until) > new Date()) {
|
||||
return { type: 'error', message: 'Účet je dočasně uzamčen. Zkuste to později.', status: 429 };
|
||||
return {
|
||||
type: "error",
|
||||
message: "Účet je dočasně uzamčen. Zkuste to později.",
|
||||
status: 429,
|
||||
};
|
||||
}
|
||||
|
||||
const passwordValid = await bcrypt.compare(password, user.password_hash);
|
||||
if (!passwordValid) {
|
||||
const attempts = (user.failed_login_attempts ?? 0) + 1;
|
||||
const updateData: Record<string, unknown> = { failed_login_attempts: attempts };
|
||||
const updateData: Record<string, unknown> = {
|
||||
failed_login_attempts: attempts,
|
||||
};
|
||||
|
||||
if (attempts >= config.security.maxLoginAttempts) {
|
||||
updateData.locked_until = new Date(Date.now() + config.security.lockoutMinutes * 60_000);
|
||||
updateData.locked_until = new Date(
|
||||
Date.now() + config.security.lockoutMinutes * 60_000,
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.users.update({ where: { id: user.id }, data: updateData });
|
||||
return { type: 'error', message: 'Neplatné přihlašovací údaje', status: 401 };
|
||||
return {
|
||||
type: "error",
|
||||
message: "Neplatné přihlašovací údaje",
|
||||
status: 401,
|
||||
};
|
||||
}
|
||||
|
||||
// Reset failed attempts
|
||||
await prisma.users.update({
|
||||
where: { id: user.id },
|
||||
data: { failed_login_attempts: 0, locked_until: null, last_login: new Date() },
|
||||
data: {
|
||||
failed_login_attempts: 0,
|
||||
locked_until: null,
|
||||
last_login: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Check if 2FA is enabled
|
||||
if (user.totp_enabled) {
|
||||
const loginToken = crypto.randomBytes(32).toString('hex');
|
||||
const loginToken = crypto.randomBytes(32).toString("hex");
|
||||
const tokenHash = hashToken(loginToken);
|
||||
|
||||
await prisma.totp_login_tokens.create({
|
||||
@@ -130,13 +164,17 @@ export async function login(
|
||||
},
|
||||
});
|
||||
|
||||
return { type: 'totp_required', loginToken };
|
||||
return { type: "totp_required", loginToken };
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
const authData = await loadAuthData(user.id);
|
||||
if (!authData) {
|
||||
return { type: 'error', message: 'Chyba načítání uživatelských dat', status: 500 };
|
||||
return {
|
||||
type: "error",
|
||||
message: "Chyba načítání uživatelských dat",
|
||||
status: 500,
|
||||
};
|
||||
}
|
||||
|
||||
const accessToken = generateAccessToken({
|
||||
@@ -159,19 +197,30 @@ export async function login(
|
||||
expires_at: new Date(Date.now() + expiresIn * 1000),
|
||||
remember_me: rememberMe,
|
||||
ip_address: request.ip,
|
||||
user_agent: request.headers['user-agent'] ?? null,
|
||||
user_agent: request.headers["user-agent"] ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
return { type: 'success', accessToken, refreshToken: refreshTokenRaw, user: authData };
|
||||
return {
|
||||
type: "success",
|
||||
accessToken,
|
||||
refreshToken: refreshTokenRaw,
|
||||
user: authData,
|
||||
};
|
||||
}
|
||||
|
||||
export async function refreshAccessToken(
|
||||
refreshTokenRaw: string,
|
||||
request: FastifyRequest,
|
||||
): Promise<
|
||||
| { type: 'success'; accessToken: string; refreshToken: string; user: AuthData; rememberMe: boolean }
|
||||
| { type: 'error'; message: string; status: number }
|
||||
| {
|
||||
type: "success";
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
user: AuthData;
|
||||
rememberMe: boolean;
|
||||
}
|
||||
| { type: "error"; message: string; status: number }
|
||||
> {
|
||||
const tokenHash = hashToken(refreshTokenRaw);
|
||||
|
||||
@@ -179,13 +228,17 @@ export async function refreshAccessToken(
|
||||
where: { token_hash: tokenHash },
|
||||
});
|
||||
|
||||
if (!storedToken || storedToken.replaced_at || new Date(storedToken.expires_at) < new Date()) {
|
||||
return { type: 'error', message: 'Neplatný refresh token', status: 401 };
|
||||
if (
|
||||
!storedToken ||
|
||||
storedToken.replaced_at ||
|
||||
new Date(storedToken.expires_at) < new Date()
|
||||
) {
|
||||
return { type: "error", message: "Neplatný refresh token", status: 401 };
|
||||
}
|
||||
|
||||
const authData = await loadAuthData(storedToken.user_id);
|
||||
if (!authData) {
|
||||
return { type: 'error', message: 'Uživatel nenalezen', status: 401 };
|
||||
return { type: "error", message: "Uživatel nenalezen", status: 401 };
|
||||
}
|
||||
|
||||
// Rotate refresh token
|
||||
@@ -208,7 +261,7 @@ export async function refreshAccessToken(
|
||||
expires_at: new Date(Date.now() + expiresIn * 1000),
|
||||
remember_me: storedToken.remember_me,
|
||||
ip_address: request.ip,
|
||||
user_agent: request.headers['user-agent'] ?? null,
|
||||
user_agent: request.headers["user-agent"] ?? null,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
@@ -219,12 +272,20 @@ export async function refreshAccessToken(
|
||||
roleName: authData.roleName,
|
||||
});
|
||||
|
||||
return { type: 'success', accessToken, refreshToken: newRefreshTokenRaw, user: authData, rememberMe: storedToken.remember_me ?? false };
|
||||
return {
|
||||
type: "success",
|
||||
accessToken,
|
||||
refreshToken: newRefreshTokenRaw,
|
||||
user: authData,
|
||||
rememberMe: storedToken.remember_me ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
export async function logout(refreshTokenRaw: string): Promise<void> {
|
||||
const tokenHash = hashToken(refreshTokenRaw);
|
||||
const token = await prisma.refresh_tokens.findFirst({ where: { token_hash: tokenHash } });
|
||||
const token = await prisma.refresh_tokens.findFirst({
|
||||
where: { token_hash: tokenHash },
|
||||
});
|
||||
|
||||
if (token) {
|
||||
// Delete all tokens for this user from the same IP + user agent (same browser session)
|
||||
@@ -237,16 +298,25 @@ export async function logout(refreshTokenRaw: string): Promise<void> {
|
||||
});
|
||||
} else {
|
||||
// Fallback: just delete by hash
|
||||
await prisma.refresh_tokens.deleteMany({ where: { token_hash: tokenHash } });
|
||||
await prisma.refresh_tokens.deleteMany({
|
||||
where: { token_hash: tokenHash },
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up expired tokens
|
||||
await prisma.refresh_tokens.deleteMany({ where: { expires_at: { lt: new Date() } } });
|
||||
await prisma.refresh_tokens.deleteMany({
|
||||
where: { expires_at: { lt: new Date() } },
|
||||
});
|
||||
}
|
||||
|
||||
export async function verifyAccessToken(token: string): Promise<AuthData | null> {
|
||||
export async function verifyAccessToken(
|
||||
token: string,
|
||||
): Promise<AuthData | null> {
|
||||
try {
|
||||
const payload = jwt.verify(token, config.jwt.secret) as unknown as JwtPayload;
|
||||
const payload = jwt.verify(
|
||||
token,
|
||||
config.jwt.secret,
|
||||
) as unknown as JwtPayload;
|
||||
return loadAuthData(payload.sub);
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user