style: run prettier on entire codebase
This commit is contained in:
@@ -21,7 +21,7 @@ function getEasterSunday(year: number): string {
|
||||
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
||||
const month = Math.floor((h + l - 7 * m + 114) / 31);
|
||||
const day = ((h + l - 7 * m + 114) % 31) + 1;
|
||||
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/** All Czech public holidays for a year (11 fixed + 2 Easter-based) */
|
||||
@@ -51,8 +51,9 @@ export function getHolidays(year: number): string[] {
|
||||
const easterMonday = new Date(easterDate);
|
||||
easterMonday.setDate(easterMonday.getDate() + 1);
|
||||
|
||||
const fmt = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
holidays.push(fmt(goodFriday)); // Velký pátek
|
||||
const fmt = (d: Date) =>
|
||||
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
holidays.push(fmt(goodFriday)); // Velký pátek
|
||||
holidays.push(fmt(easterMonday)); // Velikonoční pondělí
|
||||
|
||||
holidays.sort();
|
||||
@@ -67,7 +68,11 @@ export function isHoliday(dateStr: string): boolean {
|
||||
}
|
||||
|
||||
/** Business days in a month (Mon-Fri excluding public holidays) */
|
||||
export function getBusinessDaysInMonth(year: number, month: number, upToDay?: number): number {
|
||||
export function getBusinessDaysInMonth(
|
||||
year: number,
|
||||
month: number,
|
||||
upToDay?: number,
|
||||
): number {
|
||||
const holidays = getHolidays(year);
|
||||
let count = 0;
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
@@ -77,7 +82,7 @@ export function getBusinessDaysInMonth(year: number, month: number, upToDay?: nu
|
||||
const date = new Date(year, month, day);
|
||||
const dow = date.getDay();
|
||||
if (dow !== 0 && dow !== 6) {
|
||||
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const dateStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||
if (!holidays.includes(dateStr)) {
|
||||
count++;
|
||||
}
|
||||
@@ -87,6 +92,10 @@ export function getBusinessDaysInMonth(year: number, month: number, upToDay?: nu
|
||||
}
|
||||
|
||||
/** Monthly work fund in hours (business days × 8) */
|
||||
export function getMonthlyWorkFund(year: number, month: number, upToDay?: number): number {
|
||||
export function getMonthlyWorkFund(
|
||||
year: number,
|
||||
month: number,
|
||||
upToDay?: number,
|
||||
): number {
|
||||
return getBusinessDaysInMonth(year, month, upToDay) * 8;
|
||||
}
|
||||
|
||||
@@ -1,45 +1,48 @@
|
||||
import crypto from 'crypto';
|
||||
import { config } from '../config/env';
|
||||
import crypto from "crypto";
|
||||
import { config } from "../config/env";
|
||||
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const ALGORITHM = "aes-256-gcm";
|
||||
const IV_LENGTH = 12;
|
||||
const TAG_LENGTH = 16;
|
||||
|
||||
export function encrypt(plaintext: string): string {
|
||||
const key = Buffer.from(config.totp.encryptionKey, 'hex');
|
||||
const key = Buffer.from(config.totp.encryptionKey, "hex");
|
||||
const iv = crypto.randomBytes(IV_LENGTH);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
||||
|
||||
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(plaintext, "utf8"),
|
||||
cipher.final(),
|
||||
]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
// Use PHP-compatible format: base64(nonce + ciphertext + tag)
|
||||
return Buffer.concat([iv, encrypted, tag]).toString('base64');
|
||||
return Buffer.concat([iv, encrypted, tag]).toString("base64");
|
||||
}
|
||||
|
||||
export function decrypt(ciphertext: string): string {
|
||||
const key = Buffer.from(config.totp.encryptionKey, 'hex');
|
||||
const key = Buffer.from(config.totp.encryptionKey, "hex");
|
||||
|
||||
// Detect format: PHP uses base64(nonce+ciphertext+tag), TS uses hex:hex:hex
|
||||
const parts = ciphertext.split(':');
|
||||
const parts = ciphertext.split(":");
|
||||
if (parts.length === 3) {
|
||||
// TS format: iv:encrypted:tag (hex)
|
||||
const iv = Buffer.from(parts[0], 'hex');
|
||||
const iv = Buffer.from(parts[0], "hex");
|
||||
const encrypted = parts[1];
|
||||
const tag = Buffer.from(parts[2], 'hex');
|
||||
const tag = Buffer.from(parts[2], "hex");
|
||||
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
// PHP format: base64(nonce + ciphertext + tag)
|
||||
const raw = Buffer.from(ciphertext, 'base64');
|
||||
const raw = Buffer.from(ciphertext, "base64");
|
||||
if (raw.length < IV_LENGTH + TAG_LENGTH + 1) {
|
||||
throw new Error('Invalid ciphertext format');
|
||||
throw new Error("Invalid ciphertext format");
|
||||
}
|
||||
|
||||
const iv = raw.subarray(0, IV_LENGTH);
|
||||
@@ -51,5 +54,5 @@ export function decrypt(ciphertext: string): string {
|
||||
|
||||
let decrypted = decipher.update(encrypted);
|
||||
const final = decipher.final();
|
||||
return Buffer.concat([decrypted, final]).toString('utf8');
|
||||
return Buffer.concat([decrypted, final]).toString("utf8");
|
||||
}
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
import { PaginationQuery, PaginationMeta } from '../types';
|
||||
import { PaginationQuery, PaginationMeta } from "../types";
|
||||
|
||||
export function parsePagination(query: Record<string, unknown>): {
|
||||
page: number;
|
||||
limit: number;
|
||||
skip: number;
|
||||
sort: string;
|
||||
order: 'asc' | 'desc';
|
||||
order: "asc" | "desc";
|
||||
search: string;
|
||||
} {
|
||||
const page = Math.max(1, parseInt(String(query.page || '1'), 10) || 1);
|
||||
const limit = Math.min(100, Math.max(1, parseInt(String(query.limit || query.per_page || '25'), 10) || 25));
|
||||
const sort = String(query.sort || 'id');
|
||||
const order = String(query.order || '').toLowerCase() === 'asc' ? 'asc' : 'desc';
|
||||
const search = String(query.search || '');
|
||||
const page = Math.max(1, parseInt(String(query.page || "1"), 10) || 1);
|
||||
const limit = Math.min(
|
||||
100,
|
||||
Math.max(
|
||||
1,
|
||||
parseInt(String(query.limit || query.per_page || "25"), 10) || 25,
|
||||
),
|
||||
);
|
||||
const sort = String(query.sort || "id");
|
||||
const order =
|
||||
String(query.order || "").toLowerCase() === "asc" ? "asc" : "desc";
|
||||
const search = String(query.search || "");
|
||||
|
||||
return { page, limit, skip: (page - 1) * limit, sort, order, search };
|
||||
}
|
||||
|
||||
export function buildPaginationMeta(total: number, page: number, limit: number): PaginationMeta {
|
||||
export function buildPaginationMeta(
|
||||
total: number,
|
||||
page: number,
|
||||
limit: number,
|
||||
): PaginationMeta {
|
||||
return {
|
||||
page,
|
||||
limit,
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { ApiResponse, PaginationMeta } from '../types';
|
||||
import { FastifyReply } from "fastify";
|
||||
import { ApiResponse, PaginationMeta } from "../types";
|
||||
|
||||
export function success<T>(reply: FastifyReply, data: T, statusCode = 200, message?: string): void {
|
||||
const response: ApiResponse<T> & { message?: string } = { success: true, data };
|
||||
export function success<T>(
|
||||
reply: FastifyReply,
|
||||
data: T,
|
||||
statusCode = 200,
|
||||
message?: string,
|
||||
): void {
|
||||
const response: ApiResponse<T> & { message?: string } = {
|
||||
success: true,
|
||||
data,
|
||||
};
|
||||
if (message) response.message = message;
|
||||
reply.status(statusCode).send(response);
|
||||
}
|
||||
@@ -12,18 +20,26 @@ export function paginated<T>(
|
||||
data: T,
|
||||
pagination: PaginationMeta,
|
||||
): void {
|
||||
reply.status(200).send({ success: true, data, pagination } satisfies ApiResponse<T>);
|
||||
reply
|
||||
.status(200)
|
||||
.send({ success: true, data, pagination } satisfies ApiResponse<T>);
|
||||
}
|
||||
|
||||
export function error(reply: FastifyReply, message: string, statusCode = 400): void {
|
||||
reply.status(statusCode).send({ success: false, error: message } satisfies ApiResponse);
|
||||
export function error(
|
||||
reply: FastifyReply,
|
||||
message: string,
|
||||
statusCode = 400,
|
||||
): void {
|
||||
reply
|
||||
.status(statusCode)
|
||||
.send({ success: false, error: message } satisfies ApiResponse);
|
||||
}
|
||||
|
||||
/** Parse and validate a numeric ID from route params. Returns NaN-safe number or sends 400 error. */
|
||||
export function parseId(raw: string, reply: FastifyReply): number | null {
|
||||
const id = parseInt(raw, 10);
|
||||
if (isNaN(id) || id <= 0) {
|
||||
error(reply, 'Neplatné ID', 400);
|
||||
error(reply, "Neplatné ID", 400);
|
||||
return null;
|
||||
}
|
||||
return id;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as OTPAuthLib from 'otpauth';
|
||||
import { decrypt } from './encryption';
|
||||
import * as OTPAuthLib from "otpauth";
|
||||
import { decrypt } from "./encryption";
|
||||
|
||||
export const OTPAuth = {
|
||||
verify(encryptedSecret: string, code: string): boolean {
|
||||
@@ -7,7 +7,7 @@ export const OTPAuth = {
|
||||
const secret = decrypt(encryptedSecret);
|
||||
const totp = new OTPAuthLib.TOTP({
|
||||
secret: OTPAuthLib.Secret.fromBase32(secret),
|
||||
algorithm: 'SHA1',
|
||||
algorithm: "SHA1",
|
||||
digits: 6,
|
||||
period: 30,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user