Files
Hermes-ui/packages/server/src/controllers/auth.ts
T

194 lines
5.1 KiB
TypeScript
Raw Normal View History

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 }
}