Add frontend service with auth, MFA, and content management
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled

- Node.js/Express backend with TypeScript
- SQLite database for users, sessions, and content metadata
- Authentication with TOTP and WebAuthn MFA support
- Admin user auto-created on first startup
- User content gallery with view/delete functionality
- RunPod API proxy (keeps API keys server-side)
- Docker setup with CI/CD for Gitea registry

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-01-07 04:57:08 +00:00
parent 8a5610a1e4
commit 890543fb77
33 changed files with 6851 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
import { randomBytes, createCipheriv, createDecipheriv, scrypt } from 'crypto';
import { promisify } from 'util';
import argon2 from 'argon2';
import { config } from '../config.js';
const scryptAsync = promisify(scrypt);
// Password hashing with Argon2id
export async function hashPassword(password: string): Promise<string> {
return argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536,
timeCost: 3,
parallelism: 4,
});
}
export async function verifyPassword(hash: string, password: string): Promise<boolean> {
try {
return await argon2.verify(hash, password);
} catch {
return false;
}
}
// Generate cryptographically secure random strings
export function generateSecureToken(length: number = 32): string {
return randomBytes(length).toString('hex');
}
export function generateSessionId(): string {
return randomBytes(32).toString('base64url');
}
// AES-256-GCM encryption for TOTP secrets
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16;
const AUTH_TAG_LENGTH = 16;
async function deriveKey(): Promise<Buffer> {
const keyHex = config.encryptionKey;
if (keyHex.length !== 64) {
throw new Error('ENCRYPTION_KEY must be 64 hex characters (32 bytes)');
}
return Buffer.from(keyHex, 'hex');
}
export async function encrypt(plaintext: string): Promise<string> {
const key = await deriveKey();
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Format: iv:authTag:ciphertext (all hex)
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}
export async function decrypt(ciphertext: string): Promise<string> {
const key = await deriveKey();
const parts = ciphertext.split(':');
if (parts.length !== 3) {
throw new Error('Invalid ciphertext format');
}
const iv = Buffer.from(parts[0], 'hex');
const authTag = Buffer.from(parts[1], 'hex');
const encrypted = parts[2];
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}