refactor(auth): replace JWT/password-lock with token guards

This commit is contained in:
belisards
2026-05-03 16:31:00 -03:00
parent 7b29e39e9f
commit 4f3017a02d
15 changed files with 337 additions and 1146 deletions

View File

@@ -1,83 +0,0 @@
'use client';
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { authApi } from './api';
import { useRouter } from 'next/navigation';
interface AuthContextType {
isAuthenticated: boolean;
isLoading: boolean;
username: string | null;
login: (username: string, password: string) => Promise<void>;
logout: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [username, setUsername] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
// Check authentication on mount using httpOnly cookies
useEffect(() => {
const initAuth = async () => {
try {
// Call /api/auth/me - cookies are sent automatically
const user = await authApi.me();
setIsAuthenticated(true);
setUsername(user.username);
} catch (error) {
// Not authenticated or session expired
setIsAuthenticated(false);
setUsername(null);
} finally {
setIsLoading(false);
}
};
initAuth();
}, []);
const login = async (username: string, password: string) => {
// Server sets httpOnly cookies automatically
await authApi.login(username, password);
setIsAuthenticated(true);
setUsername(username);
};
const logout = async () => {
try {
// Server clears httpOnly cookies
await authApi.logout();
} catch (error) {
// Continue with logout even if API call fails
}
setIsAuthenticated(false);
setUsername(null);
router.push('/');
};
return (
<AuthContext.Provider
value={{
isAuthenticated,
isLoading,
username,
login,
logout,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@@ -1,143 +0,0 @@
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
// Initialize secrets (auto-generate if not provided)
const secrets = initializeSecrets();
// Token expiry times
const TOKEN_EXPIRY = '72h';
const REFRESH_TOKEN_EXPIRY = '30d';
/**
* Initialize JWT secrets - auto-generate and persist if not provided in environment
*/
function initializeSecrets(): { secret: string; refreshSecret: string } {
const dataDir = path.join(process.cwd(), 'data');
const secretsFile = path.join(dataDir, 'secrets.json');
// If provided in environment, use those
if (process.env.SECRET) {
return {
secret: process.env.SECRET,
refreshSecret: process.env.REFRESH_SECRET || process.env.SECRET, // Use same secret if refresh not provided
};
}
// Ensure data directory exists
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
// Try to load existing secrets
if (fs.existsSync(secretsFile)) {
try {
const data = JSON.parse(fs.readFileSync(secretsFile, 'utf-8'));
return data;
} catch {
// Failed to load, will generate new ones
}
}
// Generate new cryptographically secure secrets (512 bits each)
const newSecrets = {
secret: crypto.randomBytes(64).toString('hex'),
refreshSecret: crypto.randomBytes(64).toString('hex'),
};
// Save to file with restricted permissions
try {
fs.writeFileSync(secretsFile, JSON.stringify(newSecrets, null, 2), { mode: 0o600 });
} catch (error) {
console.error('⚠️ Failed to save secrets file:', error);
console.error('⚠️ WARNING: Using in-memory secrets - tokens will be invalid after restart!');
}
return newSecrets;
}
export interface TokenPayload {
username: string;
type: 'access' | 'refresh';
}
/**
* Generate an access token
*/
export function generateAccessToken(username: string): string {
return jwt.sign(
{ username, type: 'access' } as TokenPayload,
secrets.secret,
{ expiresIn: TOKEN_EXPIRY } as jwt.SignOptions
);
}
/**
* Generate a refresh token
*/
export function generateRefreshToken(username: string): string {
return jwt.sign(
{ username, type: 'refresh' } as TokenPayload,
secrets.refreshSecret,
{ expiresIn: REFRESH_TOKEN_EXPIRY } as jwt.SignOptions
);
}
/**
* Verify an access token
*/
export function verifyAccessToken(token: string): TokenPayload | null {
try {
const payload = jwt.verify(token, secrets.secret) as TokenPayload;
if (payload.type !== 'access') {
return null;
}
return payload;
} catch (error) {
return null;
}
}
/**
* Verify a refresh token
*/
export function verifyRefreshToken(token: string): TokenPayload | null {
try {
const payload = jwt.verify(token, secrets.refreshSecret) as TokenPayload;
if (payload.type !== 'refresh') {
return null;
}
return payload;
} catch (error) {
return null;
}
}
/**
* Validate admin credentials against environment variables
*/
export function validateAdminCredentials(username: string, password: string): boolean {
const adminUsername = process.env.ADMIN_USERNAME || 'admin';
const adminPassword = process.env.ADMIN_PASSWORD;
if (!adminPassword) {
console.error('❌ ADMIN_PASSWORD not set in environment variables');
return false;
}
return username === adminUsername && password === adminPassword;
}
export function isSecureCookie(request: { headers: { get(name: string): string | null }; url: string }): boolean {
if (process.env.COOKIE_SECURE !== undefined) {
return process.env.COOKIE_SECURE === 'true';
}
if (process.env.NODE_ENV === 'production') {
return (
request.headers.get('x-forwarded-proto') === 'https' ||
request.url.startsWith('https://')
);
}
return false;
}