mirror of
https://github.com/velocitatem/cvfs.git
synced 2026-07-16 03:13:36 +00:00
feat: robust login with rate limiting + diff propagation across tree
Login hardening: - API: in-memory rate limiter (10 attempts / IP / 15 min), timing-safe credential comparison, strict body validation - Middleware: validate session token timestamp expiry (7-day window) not just HMAC signature - Logout: also clears oidc_token_pub cookie - Login page: try/catch around fetch to surface network errors, parse OIDC error query params, disable inputs during loading, wrap useSearchParams in Suspense Diff propagation: - DiffViewer: shows inherited ancestor patches in a collapsible section above own patches - CVTree: annotates each branch with ↑N ancestor patch count badge; propagate (↓) button on hover for versions with own patches and child branches - Dashboard: computeInheritedPatches/getDescendants helpers; Propagate ↓ action button in version header; handlePropagate sends current version's patches to all descendants via appendPatches Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Stdyi4YmG1BUSurArZPe8w
This commit is contained in:
@@ -1,20 +1,59 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createHmac } from 'crypto';
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const SECRET = process.env.SESSION_SECRET ?? 'dev-secret-change-in-production';
|
||||
const LOGIN_USER = process.env.LOGIN_USER ?? 'admin';
|
||||
const LOGIN_PASS = process.env.LOGIN_PASS ?? 'admin';
|
||||
|
||||
// in-memory rate limiter: max 10 attempts per IP per 15 min
|
||||
const attempts = new Map<string, { n: number; resetAt: number }>();
|
||||
|
||||
function checkRate(ip: string): boolean {
|
||||
const now = Date.now();
|
||||
const e = attempts.get(ip);
|
||||
if (!e || now > e.resetAt) { attempts.set(ip, { n: 1, resetAt: now + 15 * 60_000 }); return true; }
|
||||
if (e.n >= 10) return false;
|
||||
e.n++;
|
||||
return true;
|
||||
}
|
||||
|
||||
function safeEq(a: string, b: string): boolean {
|
||||
try {
|
||||
const ab = Buffer.from(a), bb = Buffer.from(b);
|
||||
if (ab.length !== bb.length) {
|
||||
// still run comparison on equal-length buffers to avoid timing leak
|
||||
timingSafeEqual(ab, ab);
|
||||
return false;
|
||||
}
|
||||
return timingSafeEqual(ab, bb);
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function sign(value: string) {
|
||||
return createHmac('sha256', SECRET).update(value).digest('hex');
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const { username, password } = body as Record<string, string>;
|
||||
if (!username || !password || username !== LOGIN_USER || password !== LOGIN_PASS) {
|
||||
const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? req.headers.get('x-real-ip') ?? 'unknown';
|
||||
if (!checkRate(ip)) {
|
||||
return NextResponse.json({ error: 'Too many attempts. Try again later.' }, { status: 429 });
|
||||
}
|
||||
|
||||
const body = await req.json().catch(() => null);
|
||||
if (!body || typeof body !== 'object') {
|
||||
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
||||
}
|
||||
const { username, password } = body as Record<string, unknown>;
|
||||
if (typeof username !== 'string' || typeof password !== 'string' || !username || !password) {
|
||||
return NextResponse.json({ error: 'Username and password required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const validUser = safeEq(username, LOGIN_USER);
|
||||
const validPass = safeEq(password, LOGIN_PASS);
|
||||
if (!validUser || !validPass) {
|
||||
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
|
||||
}
|
||||
|
||||
const payload = `${username}:${Date.now()}`;
|
||||
const token = `${payload}.${sign(payload)}`;
|
||||
const res = NextResponse.json({ ok: true });
|
||||
|
||||
@@ -4,5 +4,6 @@ export async function POST() {
|
||||
const res = NextResponse.json({ ok: true });
|
||||
res.cookies.delete('session');
|
||||
res.cookies.delete('oidc_token');
|
||||
res.cookies.delete('oidc_token_pub');
|
||||
return res;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user