release: PicHost v1.0.0
Publish Docker image / build-and-push (push) Canceled after 0s
ci / ci (22, ubuntu-latest) (push) Canceled after 0s

Multi-user image hosting with RBAC, upload preferences, and admin settings.
This commit is contained in:
O96u committed 2026-08-22 14:36:57 +08:00
commit bc4cba7e52
115 files changed
+26508

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
import { isAllowRegistration } from '../../utils/db'
import { isAdminSecretConfigured, isApiUploadTokenConfigured } from '../../utils/env'
import { isInitialized } from '../../utils/auth'
export default defineEventHandler((event) => {
return {
initialized: isInitialized(),
allowRegistration: isAllowRegistration(),
legacyMode: !isInitialized() && isAdminSecretConfigured(event),
adminSecretConfigured: isAdminSecretConfigured(event),
apiUploadTokenConfigured: isApiUploadTokenConfigured(event)
}
})
+86
View File
@@ -0,0 +1,86 @@
import {
checkAdminSecret,
setLegacyAuthCookie
} from '../../utils/access'
import { createApiError } from '../../utils/api-error'
import {
createSession,
isInitialized,
verifyPassword
} from '../../utils/auth'
import { findUserByUsername } from '../../utils/db'
import { getAdminSecret } from '../../utils/env'
import { clientIp, logInfo, logWarn } from '../../utils/logger'
interface LoginBody {
username?: string
password?: string
secret?: string
}
const loginAttempts = new Map<string, { count: number, resetAt: number }>()
const MAX_ATTEMPTS = 10
const WINDOW_MS = 15 * 60 * 1000
function checkRateLimit(ip: string): void {
const now = Date.now()
const entry = loginAttempts.get(ip)
if (!entry || now > entry.resetAt) {
loginAttempts.set(ip, { count: 1, resetAt: now + WINDOW_MS })
return
}
entry.count += 1
if (entry.count > MAX_ATTEMPTS) {
throw createError({
statusCode: 429,
statusMessage: '登录尝试过于频繁,请稍后再试'
})
}
}
export default defineEventHandler(async (event) => {
const ip = clientIp(event)
checkRateLimit(ip)
const body = await readBody<LoginBody>(event)
const initialized = isInitialized()
if (!initialized) {
const adminSecret = getAdminSecret(event)
if (!adminSecret) {
createApiError(event, 'FORBIDDEN', '请先完成系统初始化', 403)
}
const input = body?.secret?.trim()
if (!input || !(await checkAdminSecret(input, adminSecret))) {
logWarn('legacy login failed: invalid secret', { ip })
createApiError(event, 'UNAUTHORIZED', '密钥错误', 401)
}
await setLegacyAuthCookie(event, adminSecret)
logInfo('legacy login success', { ip })
return { success: true, needsMigration: true }
}
const username = body?.username?.trim() ?? ''
const password = body?.password ?? ''
if (!username || !password) {
createApiError(event, 'INVALID_REQUEST', '请输入用户名和密码', 400)
}
const user = findUserByUsername(username)
if (!user || !verifyPassword(password, user.password_hash)) {
logWarn('login failed: invalid credentials', { ip, username })
createApiError(event, 'UNAUTHORIZED', '用户名或密码错误', 401)
}
await createSession(event, user.id)
logInfo('login success', { ip, username: user.username })
return {
success: true,
user: { id: user.id, username: user.username, role: user.role }
}
})
+10
View File
@@ -0,0 +1,10 @@
import { destroySession } from '../../utils/auth'
import { clearLegacyAuthCookie } from '../../utils/access'
import { clientIp, logInfo } from '../../utils/logger'
export default defineEventHandler(async (event) => {
await destroySession(event)
clearLegacyAuthCookie(event)
logInfo('logout', { ip: clientIp(event) })
return { success: true }
})
+14
View File
@@ -0,0 +1,14 @@
import { getCurrentUser } from '../../utils/auth'
import { createApiError } from '../../utils/api-error'
export default defineEventHandler(async (event) => {
const user = await getCurrentUser(event)
if (!user) {
createApiError(event, 'UNAUTHORIZED', '未登录', 401)
}
return {
authenticated: true,
user: { id: user.id, username: user.username, role: user.role }
}
})
+75
View File
@@ -0,0 +1,75 @@
import {
clearLegacyAuthCookie,
verifyLegacyAuth
} from '../../utils/access'
import { createApiError } from '../../utils/api-error'
import {
createSession,
createUserAccount,
isInitialized
} from '../../utils/auth'
import { setAllowRegistration } from '../../utils/db'
import { clientIp, logInfo } from '../../utils/logger'
interface MigrateBody {
username?: string
password?: string
allowRegistration?: boolean
}
export default defineEventHandler(async (event) => {
if (isInitialized()) {
createApiError(event, 'FORBIDDEN', '系统已初始化', 403)
}
if (!(await verifyLegacyAuth(event))) {
createApiError(event, 'UNAUTHORIZED', '请先使用管理密钥登录', 401)
}
const body = await readBody<MigrateBody>(event)
const username = body?.username?.trim() ?? ''
const password = body?.password ?? ''
try {
const user = await createUserAccount({
username,
password,
role: 'admin'
})
setAllowRegistration(Boolean(body?.allowRegistration))
clearLegacyAuthCookie(event)
await createSession(event, user.id)
logInfo('legacy migration complete', {
ip: clientIp(event),
username: user.username
})
return {
success: true,
user: { id: user.id, username: user.username, role: user.role }
}
} catch (error: unknown) {
if (error instanceof Error) {
if (error.message === 'INVALID_USERNAME') {
createApiError(
event,
'INVALID_REQUEST',
'用户名需为 3–32 位字母、数字或下划线',
400
)
} else if (error.message === 'INVALID_PASSWORD') {
createApiError(
event,
'INVALID_REQUEST',
'密码至少 8 位',
400
)
} else if (error.message === 'USERNAME_TAKEN') {
createApiError(event, 'INVALID_REQUEST', '用户名已被占用', 400)
}
}
throw error
}
})
+43
View File
@@ -0,0 +1,43 @@
import { createApiError } from '../../utils/api-error'
import { requireUserAuth } from '../../utils/access'
import {
hashPassword,
isValidPassword,
verifyPassword
} from '../../utils/auth'
import { findUserById, updateUserPassword } from '../../utils/db'
import { clientIp, logInfo } from '../../utils/logger'
interface PasswordBody {
currentPassword?: string
newPassword?: string
}
export default defineEventHandler(async (event) => {
const authUser = await requireUserAuth(event)
const body = await readBody<PasswordBody>(event)
const currentPassword = body?.currentPassword ?? ''
const newPassword = body?.newPassword ?? ''
if (!currentPassword || !newPassword) {
createApiError(event, 'INVALID_REQUEST', '请填写当前密码和新密码', 400)
}
if (!isValidPassword(newPassword)) {
createApiError(event, 'INVALID_REQUEST', '新密码至少 8 位', 400)
}
if (currentPassword === newPassword) {
createApiError(event, 'INVALID_REQUEST', '新密码不能与当前密码相同', 400)
}
const user = findUserById(authUser.id)
if (!user || !verifyPassword(currentPassword, user.password_hash)) {
createApiError(event, 'UNAUTHORIZED', '当前密码错误', 401)
}
updateUserPassword(authUser.id, hashPassword(newPassword))
logInfo('password changed', { ip: clientIp(event), username: authUser.username })
return { success: true }
})
+65
View File
@@ -0,0 +1,65 @@
import { createApiError } from '../../utils/api-error'
import {
createSession,
createUserAccount,
isInitialized
} from '../../utils/auth'
import { isAllowRegistration } from '../../utils/db'
import { clientIp, logInfo } from '../../utils/logger'
interface RegisterBody {
username?: string
password?: string
}
export default defineEventHandler(async (event) => {
if (!isInitialized()) {
createApiError(event, 'FORBIDDEN', '请先完成系统初始化', 403)
}
if (!isAllowRegistration()) {
createApiError(event, 'FORBIDDEN', '当前未开放用户注册', 403)
}
const body = await readBody<RegisterBody>(event)
const username = body?.username?.trim() ?? ''
const password = body?.password ?? ''
try {
const user = await createUserAccount({
username,
password,
role: 'user'
})
await createSession(event, user.id)
logInfo('register success', { ip: clientIp(event), username: user.username })
return {
success: true,
user: { id: user.id, username: user.username, role: user.role }
}
} catch (error: unknown) {
if (error instanceof Error) {
if (error.message === 'INVALID_USERNAME') {
createApiError(
event,
'INVALID_REQUEST',
'用户名需为 3–32 位字母、数字或下划线',
400
)
} else if (error.message === 'INVALID_PASSWORD') {
createApiError(
event,
'INVALID_REQUEST',
'密码至少 8 位',
400
)
} else if (error.message === 'USERNAME_TAKEN') {
createApiError(event, 'INVALID_REQUEST', '用户名已被占用', 400)
}
}
throw error
}
})
+24
View File
@@ -0,0 +1,24 @@
import { isAllowRegistration } from '../../utils/db'
import { isAdminSecretConfigured } from '../../utils/env'
import {
getCurrentUser,
isInitialized
} from '../../utils/auth'
import { verifyLegacyAuth } from '../../utils/access'
export default defineEventHandler(async (event) => {
const initialized = isInitialized()
const user = await getCurrentUser(event)
const legacyMode = !initialized && isAdminSecretConfigured(event)
const needsMigration = legacyMode && (await verifyLegacyAuth(event))
return {
initialized,
allowRegistration: isAllowRegistration(),
legacyMode,
needsMigration,
user: user
? { id: user.id, username: user.username, role: user.role }
: null
}
})