70ddbd0bcd
- Add username/password login as additional auth mechanism alongside existing token - First login must use token; password can be configured in Settings > Account - Password login returns the existing static token (no auth middleware changes) - Add account settings: setup, change password, change username, remove password - Add logout button to sidebar footer - Add version changelog popup (click version number in sidebar) - Support all 8 locales (en, zh, de, es, fr, ja, ko, pt) - Bump version to 0.4.3 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
153 lines
4.0 KiB
TypeScript
153 lines
4.0 KiB
TypeScript
import type { Context } from 'koa'
|
|
import { getCredentials, setCredentials, verifyCredentials, deleteCredentials } from '../services/credentials'
|
|
import { getToken } from '../services/auth'
|
|
|
|
/**
|
|
* 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 valid = await verifyCredentials(username, password)
|
|
if (!valid) {
|
|
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
|
|
}
|
|
|
|
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 }
|
|
}
|