release: v1.2.7 — 登录人机验证(滑块 / Turnstile / Cap)
Publish Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Canceled after 0s
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Canceled after 0s
Publish Docker image / merge (push) Canceled after 0s
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
GitHub Release / release (push) Canceled after 0s

This commit is contained in:
O96u committed 2026-09-03 13:53:43 +08:00
1 parent 8408d01197
commit 430c4de0f4
42 files changed
+1103 -72

No files matched your search

+3 -2
View File
@@ -1,5 +1,5 @@
import { isAllowRegistration } from '../../utils/db'
import { isAdminSecretConfigured, isApiUploadTokenConfigured } from '../../utils/env'
import { getLoginVerificationPublicConfig, isAdminSecretConfigured, isApiUploadTokenConfigured } from '../../utils/env'
import { isInitialized } from '../../utils/auth'
export default defineEventHandler((event) => {
@@ -8,6 +8,7 @@ export default defineEventHandler((event) => {
allowRegistration: isAllowRegistration(),
legacyMode: !isInitialized() && isAdminSecretConfigured(event),
adminSecretConfigured: isAdminSecretConfigured(event),
apiUploadTokenConfigured: isApiUploadTokenConfigured(event)
apiUploadTokenConfigured: isApiUploadTokenConfigured(event),
loginVerification: getLoginVerificationPublicConfig(event)
}
})
+5 -15
View File
@@ -1,4 +1,3 @@
import type { H3Event } from 'h3'
import {
checkAdminSecret,
setLegacyAuthCookie
@@ -7,19 +6,18 @@ import { createApiError } from '../../utils/api-error'
import {
createSession,
isInitialized,
rowToAuthUser,
verifyPassword
} from '../../utils/auth'
import { findUserByUsername } from '../../utils/db'
import { getAdminSecret } from '../../utils/env'
import { verifyLoginCaptcha } from '../../utils/login-captcha'
import { verifyLoginVerification, type VerificationBody } from '../../utils/login-verification'
import { clientIp, logInfo, logWarn } from '../../utils/logger'
interface LoginBody {
interface LoginBody extends VerificationBody {
username?: string
password?: string
secret?: string
captchaId?: string
captchaPosition?: number
}
const loginAttempts = new Map<string, { count: number, resetAt: number }>()
@@ -42,20 +40,12 @@ function checkRateLimit(ip: string): void {
}
}
function verifyCaptchaOrFail(event: H3Event, body: LoginBody) {
const id = body.captchaId?.trim() ?? ''
const position = Number(body.captchaPosition)
if (!id || !verifyLoginCaptcha(id, position)) {
createApiError(event, 'INVALID_REQUEST', '请先完成滑块验证', 400)
}
}
export default defineEventHandler(async (event) => {
const ip = clientIp(event)
checkRateLimit(ip)
const body = await readBody<LoginBody>(event)
verifyCaptchaOrFail(event, body ?? {})
await verifyLoginVerification(event, body ?? {})
const initialized = isInitialized()
if (!initialized) {
@@ -94,6 +84,6 @@ export default defineEventHandler(async (event) => {
return {
success: true,
user: { id: user.id, username: user.username, role: user.role }
user: rowToAuthUser(user)
}
})
+5 -1
View File
@@ -9,6 +9,10 @@ export default defineEventHandler(async (event) => {
return {
authenticated: true,
user: { id: user.id, username: user.username, role: user.role }
user: {
id: user.id,
username: user.username,
role: user.role
}
}
})
+9 -2
View File
@@ -5,9 +5,10 @@ import {
isInitialized
} from '../../utils/auth'
import { isAllowRegistration } from '../../utils/db'
import { verifyLoginVerification, type VerificationBody } from '../../utils/login-verification'
import { clientIp, logInfo } from '../../utils/logger'
interface RegisterBody {
interface RegisterBody extends VerificationBody {
username?: string
password?: string
}
@@ -22,6 +23,8 @@ export default defineEventHandler(async (event) => {
}
const body = await readBody<RegisterBody>(event)
await verifyLoginVerification(event, body ?? {})
const username = body?.username?.trim() ?? ''
const password = body?.password ?? ''
@@ -38,7 +41,11 @@ export default defineEventHandler(async (event) => {
return {
success: true,
user: { id: user.id, username: user.username, role: user.role }
user: {
id: user.id,
username: user.username,
role: user.role
}
}
} catch (error: unknown) {
if (error instanceof Error) {
+7 -2
View File
@@ -1,5 +1,5 @@
import { isAllowRegistration } from '../../utils/db'
import { isAdminSecretConfigured } from '../../utils/env'
import { getLoginVerificationPublicConfig, isAdminSecretConfigured } from '../../utils/env'
import {
getCurrentUser,
isInitialized
@@ -17,8 +17,13 @@ export default defineEventHandler(async (event) => {
allowRegistration: isAllowRegistration(),
legacyMode,
needsMigration,
loginVerification: getLoginVerificationPublicConfig(event),
user: user
? { id: user.id, username: user.username, role: user.role }
? {
id: user.id,
username: user.username,
role: user.role
}
: null
}
})
+87
View File
@@ -9,6 +9,11 @@ import {
SETTINGS_IMAGE_BASE_URL,
SETTINGS_SITE_BASE_URL,
SETTINGS_WEBP_QUALITY,
SETTINGS_LOGIN_VERIFICATION_METHOD,
SETTINGS_TURNSTILE_SITE_KEY,
SETTINGS_TURNSTILE_SECRET_KEY,
SETTINGS_CAP_API_ENDPOINT,
SETTINGS_CAP_SECRET,
setAllowRegistration
} from '../../utils/db'
import {
@@ -16,6 +21,8 @@ import {
isValidImageBaseUrl,
isValidRefererHost,
isValidSiteBaseUrl,
normalizeCapApiEndpoint,
parseLoginVerificationMethod,
MAX_AUTO_DELETE_DAYS,
normalizeImageBaseUrl,
normalizeRefererHosts,
@@ -23,6 +30,7 @@ import {
parseAutoDeleteDays,
parseWebpQuality,
setGlobalAutoDeletePolicy,
validateLoginVerificationSettings,
validateSettingsDomainPatch
} from '../../utils/env'
@@ -36,6 +44,11 @@ interface SettingsPatchBody {
storageUseDatePath?: unknown
autoDeleteDays?: unknown
allowRegistration?: unknown
loginVerificationMethod?: unknown
turnstileSiteKey?: unknown
turnstileSecretKey?: unknown
capApiEndpoint?: unknown
capSecret?: unknown
}
export default defineEventHandler(async (event) => {
@@ -161,5 +174,79 @@ export default defineEventHandler(async (event) => {
setSetting(SETTINGS_STORAGE_USE_DATE_PATH, body.storageUseDatePath ? 'true' : 'false')
}
const currentMethod = parseLoginVerificationMethod(
getSetting(SETTINGS_LOGIN_VERIFICATION_METHOD) ?? 'slider'
) ?? 'slider'
let nextMethod = currentMethod
let nextTurnstileSiteKey = getSetting(SETTINGS_TURNSTILE_SITE_KEY) ?? ''
let nextTurnstileSecretKey = getSetting(SETTINGS_TURNSTILE_SECRET_KEY) ?? ''
let nextCapApiEndpoint = getSetting(SETTINGS_CAP_API_ENDPOINT) ?? ''
let nextCapSecret = getSetting(SETTINGS_CAP_SECRET) ?? ''
if (body.loginVerificationMethod !== undefined) {
const parsed = parseLoginVerificationMethod(String(body.loginVerificationMethod))
if (!parsed) {
createApiError(event, 'INVALID_REQUEST', '无效的登录验证方式', 400)
}
nextMethod = parsed
}
if (body.turnstileSiteKey !== undefined) {
nextTurnstileSiteKey = typeof body.turnstileSiteKey === 'string'
? body.turnstileSiteKey.trim()
: ''
}
if (body.turnstileSecretKey !== undefined) {
nextTurnstileSecretKey = typeof body.turnstileSecretKey === 'string'
? body.turnstileSecretKey.trim()
: ''
}
if (body.capApiEndpoint !== undefined) {
nextCapApiEndpoint = typeof body.capApiEndpoint === 'string'
? normalizeCapApiEndpoint(body.capApiEndpoint)
: ''
}
if (body.capSecret !== undefined) {
nextCapSecret = typeof body.capSecret === 'string'
? body.capSecret.trim()
: ''
}
if (body.loginVerificationMethod !== undefined
|| body.turnstileSiteKey !== undefined
|| body.turnstileSecretKey !== undefined
|| body.capApiEndpoint !== undefined
|| body.capSecret !== undefined) {
const settingsError = validateLoginVerificationSettings(
nextMethod,
nextTurnstileSiteKey,
nextTurnstileSecretKey,
nextCapApiEndpoint,
nextCapSecret
)
if (settingsError) {
createApiError(event, 'INVALID_REQUEST', settingsError, 400)
}
}
if (body.loginVerificationMethod !== undefined) {
setSetting(SETTINGS_LOGIN_VERIFICATION_METHOD, nextMethod)
}
if (body.turnstileSiteKey !== undefined) {
setSetting(SETTINGS_TURNSTILE_SITE_KEY, nextTurnstileSiteKey)
}
if (body.turnstileSecretKey !== undefined) {
setSetting(SETTINGS_TURNSTILE_SECRET_KEY, nextTurnstileSecretKey)
}
if (body.capApiEndpoint !== undefined) {
setSetting(SETTINGS_CAP_API_ENDPOINT, nextCapApiEndpoint)
}
if (body.capSecret !== undefined) {
setSetting(SETTINGS_CAP_SECRET, nextCapSecret)
}
return getSettingsPayload(event)
})
+1 -1
View File
@@ -41,7 +41,7 @@ export default defineEventHandler(async (event) => {
const userFilter = await getImageUserFilter(event)
try {
if (user.role === 'admin' && userFilter === 'admin') {
if (userFilter === 'admin') {
const activity = getActivityStats()
const [storedCount, byFolder, storageUsage] = await Promise.all([
countImages(),