import type { Context } from 'koa' import { getCredentials, setCredentials, verifyCredentials, deleteCredentials } from '../services/credentials' import { getToken } from '../services/auth' import { checkPassword, recordPasswordFailure, recordPasswordSuccess, extractIp, getLockedIps, unlockIp, unlockAll } from '../services/login-limiter' /** * GET /api/auth/status * Check if username/password login is configured (public). */ export async function authStatus(ctx: Context) { const cred = await getCredentials() ctx.body = { hasPasswordLogin: !!cred, username: cred?.username || null, } } /** * POST /api/auth/login * Authenticate with username/password (public). * Returns the static token on success. */ export async function login(ctx: Context) { const { username, password } = ctx.request.body as { username?: string; password?: string } if (!username || !password) { ctx.status = 400 ctx.body = { error: 'Username and password are required' } return } const ip = extractIp(ctx) const result = checkPassword(ip) if (!result.allowed) { ctx.status = result.status ctx.body = { error: 'Too many login attempts, please try again later' } return } const valid = await verifyCredentials(username, password) if (!valid) { recordPasswordFailure(ip) ctx.status = 401 ctx.body = { error: 'Invalid username or password' } return } const token = await getToken() if (!token) { ctx.status = 500 ctx.body = { error: 'Auth is disabled on this server' } return } recordPasswordSuccess(ip) ctx.body = { token } } /** * POST /api/auth/setup * Set up username/password (protected). */ export async function setupPassword(ctx: Context) { const { username, password } = ctx.request.body as { username?: string; password?: string } if (!username || !password) { ctx.status = 400 ctx.body = { error: 'Username and password are required' } return } if (username.length < 2) { ctx.status = 400 ctx.body = { error: 'Username must be at least 2 characters' } return } if (password.length < 6) { ctx.status = 400 ctx.body = { error: 'Password must be at least 6 characters' } return } await setCredentials(username, password) ctx.body = { success: true } } /** * POST /api/auth/change-password * Change password (protected). */ export async function changePassword(ctx: Context) { const { currentPassword, newPassword } = ctx.request.body as { currentPassword?: string; newPassword?: string } if (!currentPassword || !newPassword) { ctx.status = 400 ctx.body = { error: 'Current password and new password are required' } return } if (newPassword.length < 6) { ctx.status = 400 ctx.body = { error: 'New password must be at least 6 characters' } return } const cred = await getCredentials() if (!cred) { ctx.status = 400 ctx.body = { error: 'Password login not configured' } return } // Verify current password — use the username from stored credentials const valid = await verifyCredentials(cred.username, currentPassword) if (!valid) { ctx.status = 400 ctx.body = { error: 'Current password is incorrect' } return } await setCredentials(cred.username, newPassword) ctx.body = { success: true } } /** * POST /api/auth/change-username * Change username (protected). */ export async function changeUsername(ctx: Context) { const { currentPassword, newUsername } = ctx.request.body as { currentPassword?: string; newUsername?: string } if (!currentPassword || !newUsername) { ctx.status = 400 ctx.body = { error: 'Current password and new username are required' } return } if (newUsername.length < 2) { ctx.status = 400 ctx.body = { error: 'Username must be at least 2 characters' } return } const cred = await getCredentials() if (!cred) { ctx.status = 400 ctx.body = { error: 'Password login not configured' } return } const valid = await verifyCredentials(cred.username, currentPassword) if (!valid) { ctx.status = 400 ctx.body = { error: 'Current password is incorrect' } return } // Update username, keep the same password await setCredentials(newUsername, currentPassword) ctx.body = { success: true } } /** * DELETE /api/auth/password * Remove username/password login (protected). */ export async function removePassword(ctx: Context) { await deleteCredentials() ctx.body = { success: true } } /** * GET /api/auth/locked-ips * List all currently locked IPs (protected). */ export async function listLockedIps(ctx: Context) { const locks = getLockedIps() ctx.body = { locks } } /** * DELETE /api/auth/locked-ips?ip=xxx * Unlock a specific IP. No ip param = unlock all. */ export async function unlockIpHandler(ctx: Context) { const ip = ctx.query.ip as string if (ip) { const found = unlockIp(ip) if (!found) { ctx.status = 404 ctx.body = { error: 'IP not locked' } return } ctx.body = { success: true } return } // No IP specified — unlock all const count = unlockAll() ctx.body = { success: true, count } }