mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-03-10 04:51:47 +00:00
refactor: migrate from ESLint to Biome and extract SQL queries to data.ts
- Replace ESLint with Biome for linting and formatting - Configure Biome with tabs, double quotes, and organized imports - Move all SQL/Drizzle queries from page.tsx files to data.ts files - Create new data.ts files for: ajustes, dashboard, relatorios/categorias - Update existing data.ts files: extrato, fatura (add lancamentos queries) - Remove all drizzle-orm imports from page.tsx files - Update README.md with new tooling info Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,10 @@
|
||||
* Handles JWT generation, validation, and token hashing for device authentication.
|
||||
*/
|
||||
|
||||
import crypto from "crypto";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const JWT_SECRET = process.env.BETTER_AUTH_SECRET || "opensheets-secret-change-me";
|
||||
const JWT_SECRET =
|
||||
process.env.BETTER_AUTH_SECRET || "opensheets-secret-change-me";
|
||||
const ACCESS_TOKEN_EXPIRY = 7 * 24 * 60 * 60; // 7 days in seconds
|
||||
const REFRESH_TOKEN_EXPIRY = 90 * 24 * 60 * 60; // 90 days in seconds
|
||||
|
||||
@@ -15,19 +16,19 @@ const REFRESH_TOKEN_EXPIRY = 90 * 24 * 60 * 60; // 90 days in seconds
|
||||
// ============================================================================
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string; // userId
|
||||
type: "api_access" | "api_refresh";
|
||||
tokenId: string;
|
||||
deviceId?: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
sub: string; // userId
|
||||
type: "api_access" | "api_refresh";
|
||||
tokenId: string;
|
||||
deviceId?: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
export interface TokenPair {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
tokenId: string;
|
||||
expiresAt: Date;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
tokenId: string;
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -38,56 +39,59 @@ export interface TokenPair {
|
||||
* Base64URL encode a string
|
||||
*/
|
||||
function base64UrlEncode(str: string): string {
|
||||
return Buffer.from(str)
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
return Buffer.from(str)
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64URL decode a string
|
||||
*/
|
||||
function base64UrlDecode(str: string): string {
|
||||
str = str.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const pad = str.length % 4;
|
||||
if (pad) {
|
||||
str += "=".repeat(4 - pad);
|
||||
}
|
||||
return Buffer.from(str, "base64").toString();
|
||||
str = str.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const pad = str.length % 4;
|
||||
if (pad) {
|
||||
str += "=".repeat(4 - pad);
|
||||
}
|
||||
return Buffer.from(str, "base64").toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HMAC-SHA256 signature
|
||||
*/
|
||||
function createSignature(data: string): string {
|
||||
return crypto
|
||||
.createHmac("sha256", JWT_SECRET)
|
||||
.update(data)
|
||||
.digest("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
return crypto
|
||||
.createHmac("sha256", JWT_SECRET)
|
||||
.update(data)
|
||||
.digest("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JWT token
|
||||
*/
|
||||
export function createJwt(payload: Omit<JwtPayload, "iat" | "exp">, expiresIn: number): string {
|
||||
const header = { alg: "HS256", typ: "JWT" };
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
export function createJwt(
|
||||
payload: Omit<JwtPayload, "iat" | "exp">,
|
||||
expiresIn: number,
|
||||
): string {
|
||||
const header = { alg: "HS256", typ: "JWT" };
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const fullPayload: JwtPayload = {
|
||||
...payload,
|
||||
iat: now,
|
||||
exp: now + expiresIn,
|
||||
};
|
||||
const fullPayload: JwtPayload = {
|
||||
...payload,
|
||||
iat: now,
|
||||
exp: now + expiresIn,
|
||||
};
|
||||
|
||||
const headerEncoded = base64UrlEncode(JSON.stringify(header));
|
||||
const payloadEncoded = base64UrlEncode(JSON.stringify(fullPayload));
|
||||
const signature = createSignature(`${headerEncoded}.${payloadEncoded}`);
|
||||
const headerEncoded = base64UrlEncode(JSON.stringify(header));
|
||||
const payloadEncoded = base64UrlEncode(JSON.stringify(fullPayload));
|
||||
const signature = createSignature(`${headerEncoded}.${payloadEncoded}`);
|
||||
|
||||
return `${headerEncoded}.${payloadEncoded}.${signature}`;
|
||||
return `${headerEncoded}.${payloadEncoded}.${signature}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,30 +99,37 @@ export function createJwt(payload: Omit<JwtPayload, "iat" | "exp">, expiresIn: n
|
||||
* @returns The decoded payload or null if invalid
|
||||
*/
|
||||
export function verifyJwt(token: string): JwtPayload | null {
|
||||
try {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) return null;
|
||||
try {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
const [headerEncoded, payloadEncoded, signature] = parts;
|
||||
const expectedSignature = createSignature(`${headerEncoded}.${payloadEncoded}`);
|
||||
const [headerEncoded, payloadEncoded, signature] = parts;
|
||||
const expectedSignature = createSignature(
|
||||
`${headerEncoded}.${payloadEncoded}`,
|
||||
);
|
||||
|
||||
// Constant-time comparison to prevent timing attacks
|
||||
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
|
||||
return null;
|
||||
}
|
||||
// Constant-time comparison to prevent timing attacks
|
||||
if (
|
||||
!crypto.timingSafeEqual(
|
||||
Buffer.from(signature),
|
||||
Buffer.from(expectedSignature),
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const payload: JwtPayload = JSON.parse(base64UrlDecode(payloadEncoded));
|
||||
const payload: JwtPayload = JSON.parse(base64UrlDecode(payloadEncoded));
|
||||
|
||||
// Check expiration
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (payload.exp < now) {
|
||||
return null;
|
||||
}
|
||||
// Check expiration
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (payload.exp < now) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return payload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -129,76 +140,86 @@ export function verifyJwt(token: string): JwtPayload | null {
|
||||
* Generate a random token ID
|
||||
*/
|
||||
export function generateTokenId(): string {
|
||||
return crypto.randomUUID();
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random API token with prefix
|
||||
*/
|
||||
export function generateApiToken(): string {
|
||||
const randomPart = crypto.randomBytes(32).toString("base64url");
|
||||
return `os_${randomPart}`;
|
||||
const randomPart = crypto.randomBytes(32).toString("base64url");
|
||||
return `os_${randomPart}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash a token using SHA-256
|
||||
*/
|
||||
export function hashToken(token: string): string {
|
||||
return crypto.createHash("sha256").update(token).digest("hex");
|
||||
return crypto.createHash("sha256").update(token).digest("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display prefix of a token (first 8 chars after prefix)
|
||||
*/
|
||||
export function getTokenPrefix(token: string): string {
|
||||
// Remove "os_" prefix and get first 8 chars
|
||||
const withoutPrefix = token.replace(/^os_/, "");
|
||||
return `os_${withoutPrefix.substring(0, 8)}...`;
|
||||
// Remove "os_" prefix and get first 8 chars
|
||||
const withoutPrefix = token.replace(/^os_/, "");
|
||||
return `os_${withoutPrefix.substring(0, 8)}...`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a complete token pair (access + refresh)
|
||||
*/
|
||||
export function generateTokenPair(userId: string, deviceId?: string): TokenPair {
|
||||
const tokenId = generateTokenId();
|
||||
const expiresAt = new Date(Date.now() + ACCESS_TOKEN_EXPIRY * 1000);
|
||||
export function generateTokenPair(
|
||||
userId: string,
|
||||
deviceId?: string,
|
||||
): TokenPair {
|
||||
const tokenId = generateTokenId();
|
||||
const expiresAt = new Date(Date.now() + ACCESS_TOKEN_EXPIRY * 1000);
|
||||
|
||||
const accessToken = createJwt(
|
||||
{ sub: userId, type: "api_access", tokenId, deviceId },
|
||||
ACCESS_TOKEN_EXPIRY
|
||||
);
|
||||
const accessToken = createJwt(
|
||||
{ sub: userId, type: "api_access", tokenId, deviceId },
|
||||
ACCESS_TOKEN_EXPIRY,
|
||||
);
|
||||
|
||||
const refreshToken = createJwt(
|
||||
{ sub: userId, type: "api_refresh", tokenId, deviceId },
|
||||
REFRESH_TOKEN_EXPIRY
|
||||
);
|
||||
const refreshToken = createJwt(
|
||||
{ sub: userId, type: "api_refresh", tokenId, deviceId },
|
||||
REFRESH_TOKEN_EXPIRY,
|
||||
);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenId,
|
||||
expiresAt,
|
||||
};
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenId,
|
||||
expiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh an access token using a refresh token
|
||||
*/
|
||||
export function refreshAccessToken(refreshToken: string): { accessToken: string; expiresAt: Date } | null {
|
||||
const payload = verifyJwt(refreshToken);
|
||||
export function refreshAccessToken(
|
||||
refreshToken: string,
|
||||
): { accessToken: string; expiresAt: Date } | null {
|
||||
const payload = verifyJwt(refreshToken);
|
||||
|
||||
if (!payload || payload.type !== "api_refresh") {
|
||||
return null;
|
||||
}
|
||||
if (!payload || payload.type !== "api_refresh") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expiresAt = new Date(Date.now() + ACCESS_TOKEN_EXPIRY * 1000);
|
||||
const expiresAt = new Date(Date.now() + ACCESS_TOKEN_EXPIRY * 1000);
|
||||
|
||||
const accessToken = createJwt(
|
||||
{ sub: payload.sub, type: "api_access", tokenId: payload.tokenId, deviceId: payload.deviceId },
|
||||
ACCESS_TOKEN_EXPIRY
|
||||
);
|
||||
const accessToken = createJwt(
|
||||
{
|
||||
sub: payload.sub,
|
||||
type: "api_access",
|
||||
tokenId: payload.tokenId,
|
||||
deviceId: payload.deviceId,
|
||||
},
|
||||
ACCESS_TOKEN_EXPIRY,
|
||||
);
|
||||
|
||||
return { accessToken, expiresAt };
|
||||
return { accessToken, expiresAt };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -209,9 +230,9 @@ export function refreshAccessToken(refreshToken: string): { accessToken: string;
|
||||
* Extract bearer token from Authorization header
|
||||
*/
|
||||
export function extractBearerToken(authHeader: string | null): string | null {
|
||||
if (!authHeader) return null;
|
||||
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
||||
return match ? match[1] : null;
|
||||
if (!authHeader) return null;
|
||||
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,20 +240,23 @@ export function extractBearerToken(authHeader: string | null): string | null {
|
||||
* @deprecated Use validateHashToken for os_xxx tokens
|
||||
*/
|
||||
export function validateApiToken(token: string): JwtPayload | null {
|
||||
const payload = verifyJwt(token);
|
||||
if (!payload || payload.type !== "api_access") {
|
||||
return null;
|
||||
}
|
||||
return payload;
|
||||
const payload = verifyJwt(token);
|
||||
if (!payload || payload.type !== "api_access") {
|
||||
return null;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a hash-based API token (os_xxx format)
|
||||
* Returns the token hash for database lookup
|
||||
*/
|
||||
export function validateHashToken(token: string): { valid: boolean; tokenHash?: string } {
|
||||
if (!token || !token.startsWith("os_")) {
|
||||
return { valid: false };
|
||||
}
|
||||
return { valid: true, tokenHash: hashToken(token) };
|
||||
export function validateHashToken(token: string): {
|
||||
valid: boolean;
|
||||
tokenHash?: string;
|
||||
} {
|
||||
if (!token || !token.startsWith("os_")) {
|
||||
return { valid: false };
|
||||
}
|
||||
return { valid: true, tokenHash: hashToken(token) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user