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
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:
1 parent
8408d01197
commit
430c4de0f4
42 files changed
+1103
-72
No files matched your search
+3
-1
@@ -35,7 +35,9 @@ const baseUrl = computed(() => {
|
||||
|
||||
const tokenDisplay = computed(() => settings.value?.apiUploadToken ?? '')
|
||||
const hasToken = computed(() => tokenDisplay.value.length > 0)
|
||||
const canRegenerate = computed(() => !settings.value?.envTokenOverride && !loading.value)
|
||||
const canRegenerate = computed(() =>
|
||||
!settings.value?.envTokenOverride && !loading.value
|
||||
)
|
||||
|
||||
const authHeader = computed(() =>
|
||||
hasToken.value ? tokenDisplay.value : 'YOUR-TOKEN'
|
||||
|
||||
@@ -401,6 +401,7 @@ watch(isAuthenticated, async (authed, prev) => {
|
||||
:view-mode="viewMode"
|
||||
:selected-count="selectedCount"
|
||||
:loading="galleryLoading"
|
||||
:show-batch-delete="true"
|
||||
@update:storage-backend="handleStorageBackendChange"
|
||||
@update:upload-source="handleUploadSourceChange"
|
||||
@update:view-mode="viewMode = $event"
|
||||
@@ -430,7 +431,8 @@ watch(isAuthenticated, async (authed, prev) => {
|
||||
show-storage
|
||||
:items="items"
|
||||
:selected-keys="selectedKeys"
|
||||
selectable
|
||||
:selectable="true"
|
||||
:allow-delete="true"
|
||||
@update:selected-keys="updateSelectedKeys"
|
||||
@preview="openPreview"
|
||||
@delete="(image) => requestDelete(image.key)"
|
||||
@@ -442,6 +444,7 @@ watch(isAuthenticated, async (authed, prev) => {
|
||||
:items="items"
|
||||
:selected-keys="selectedKeys"
|
||||
:selectable="true"
|
||||
:show-delete="true"
|
||||
@update:selected-keys="updateSelectedKeys"
|
||||
@preview="openPreview"
|
||||
@delete="(image) => requestDelete(image.key)"
|
||||
@@ -452,6 +455,7 @@ watch(isAuthenticated, async (authed, prev) => {
|
||||
:image="previewImage"
|
||||
show-storage
|
||||
:deleting="previewImage ? deletingKeys.has(previewImage.key) : false"
|
||||
:allow-delete="true"
|
||||
@delete="requestPreviewDelete"
|
||||
/>
|
||||
|
||||
|
||||
@@ -206,6 +206,7 @@ function requestPreviewDelete() {
|
||||
v-model:open="previewOpen"
|
||||
:image="previewImage"
|
||||
:deleting="previewImage ? deletingKeys.has(previewImage.key) : false"
|
||||
:allow-delete="true"
|
||||
@delete="requestPreviewDelete"
|
||||
/>
|
||||
</AppShell>
|
||||
|
||||
+39
-4
@@ -2,9 +2,10 @@
|
||||
import logoLight from '~/assets/image/logo-light.png'
|
||||
import logoDark from '~/assets/image/logo-dark.png'
|
||||
import { isPasswordValid } from '~/utils/password-strength'
|
||||
import type { LoginVerificationPayload } from '~/types/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const { register, fetchStatus } = useAuth()
|
||||
const { register, fetchStatus, authStatus } = useAuth()
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -12,14 +13,34 @@ const username = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const loading = ref(false)
|
||||
const verificationRef = ref<{ reset: () => Promise<void> } | null>(null)
|
||||
const verificationPayload = ref<LoginVerificationPayload | null>(null)
|
||||
|
||||
const loginVerification = computed(() =>
|
||||
authStatus.value?.loginVerification ?? { method: 'slider' as const }
|
||||
)
|
||||
|
||||
const canSubmit = computed(() =>
|
||||
Boolean(username.value && password.value && confirmPassword.value)
|
||||
Boolean(verificationPayload.value)
|
||||
&& Boolean(username.value && password.value && confirmPassword.value)
|
||||
&& password.value === confirmPassword.value
|
||||
&& isPasswordValid(password.value)
|
||||
&& AUTH_USERNAME_PATTERN.test(username.value.trim())
|
||||
)
|
||||
|
||||
function onVerificationVerified(payload: LoginVerificationPayload) {
|
||||
verificationPayload.value = payload
|
||||
}
|
||||
|
||||
function onVerificationCleared() {
|
||||
verificationPayload.value = null
|
||||
}
|
||||
|
||||
async function resetVerification() {
|
||||
verificationPayload.value = null
|
||||
await verificationRef.value?.reset()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const status = await fetchStatus()
|
||||
if (!status.initialized && !status.legacyMode) {
|
||||
@@ -32,7 +53,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
async function submit() {
|
||||
if (!canSubmit.value || loading.value) return
|
||||
if (!canSubmit.value || loading.value || !verificationPayload.value) return
|
||||
|
||||
if (password.value !== confirmPassword.value) {
|
||||
toast.add({ title: t('auth.passwordMismatch'), color: 'error' })
|
||||
@@ -41,11 +62,16 @@ async function submit() {
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await register(username.value.trim(), password.value)
|
||||
const result = await register(
|
||||
username.value.trim(),
|
||||
password.value,
|
||||
verificationPayload.value
|
||||
)
|
||||
if (result.ok) {
|
||||
toast.add({ title: t('auth.registerSuccess'), color: 'success' })
|
||||
await router.replace('/')
|
||||
} else {
|
||||
await resetVerification()
|
||||
toast.add({ title: result.error, color: 'error' })
|
||||
}
|
||||
} finally {
|
||||
@@ -142,6 +168,15 @@ async function submit() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<LoginVerification
|
||||
ref="verificationRef"
|
||||
:method="loginVerification.method"
|
||||
:turnstile-site-key="loginVerification.turnstileSiteKey"
|
||||
:cap-api-endpoint="loginVerification.capApiEndpoint"
|
||||
@verified="onVerificationVerified"
|
||||
@cleared="onVerificationCleared"
|
||||
/>
|
||||
|
||||
<UButton
|
||||
type="submit"
|
||||
:label="t('auth.register')"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ADMIN_SETTINGS_TABS, USER_SETTINGS_TABS } from '~/types/settings'
|
||||
|
||||
type SettingSource = 'env' | 'db' | 'none'
|
||||
type WebpQualitySource = 'env' | 'db' | 'default'
|
||||
type LoginVerificationMethod = 'slider' | 'turnstile' | 'cap'
|
||||
|
||||
interface SettingsResponse {
|
||||
apiUploadToken: string
|
||||
@@ -33,6 +34,11 @@ interface SettingsResponse {
|
||||
storageUseDatePath: boolean
|
||||
storageUseDatePathSource: SettingSource
|
||||
allowRegistration: boolean
|
||||
loginVerificationMethod: LoginVerificationMethod
|
||||
turnstileSiteKey: string
|
||||
turnstileSecretKey: string
|
||||
capApiEndpoint: string
|
||||
capSecret: string
|
||||
appVersion: string
|
||||
}
|
||||
|
||||
@@ -98,6 +104,11 @@ const imageBaseUrlDraft = ref('')
|
||||
const hideFolderInUrlDraft = ref(false)
|
||||
const storageUseDatePathDraft = ref(true)
|
||||
const allowRegistrationDraft = ref(false)
|
||||
const loginVerificationMethodDraft = ref<LoginVerificationMethod>('slider')
|
||||
const turnstileSiteKeyDraft = ref('')
|
||||
const turnstileSecretKeyDraft = ref('')
|
||||
const capApiEndpointDraft = ref('')
|
||||
const capSecretDraft = ref('')
|
||||
const domainSeparationDraft = ref(false)
|
||||
const disableDomainSeparationOpen = ref(false)
|
||||
const pendingDomainSeparationDisable = ref(false)
|
||||
@@ -120,6 +131,11 @@ const hasServerChanges = computed(() => {
|
||||
|| hideFolderInUrlDraft.value !== settings.value.hideFolderInUrl
|
||||
|| storageUseDatePathDraft.value !== settings.value.storageUseDatePath
|
||||
|| allowRegistrationDraft.value !== settings.value.allowRegistration
|
||||
|| loginVerificationMethodDraft.value !== settings.value.loginVerificationMethod
|
||||
|| turnstileSiteKeyDraft.value !== settings.value.turnstileSiteKey
|
||||
|| turnstileSecretKeyDraft.value !== settings.value.turnstileSecretKey
|
||||
|| capApiEndpointDraft.value !== settings.value.capApiEndpoint
|
||||
|| capSecretDraft.value !== settings.value.capSecret
|
||||
})
|
||||
|
||||
function sourceBadge(source: SettingSource | WebpQualitySource) {
|
||||
@@ -150,6 +166,11 @@ function applySettings(data: SettingsResponse) {
|
||||
hideFolderInUrlDraft.value = data.hideFolderInUrl
|
||||
storageUseDatePathDraft.value = data.storageUseDatePath
|
||||
allowRegistrationDraft.value = data.allowRegistration
|
||||
loginVerificationMethodDraft.value = data.loginVerificationMethod
|
||||
turnstileSiteKeyDraft.value = data.turnstileSiteKey
|
||||
turnstileSecretKeyDraft.value = data.turnstileSecretKey
|
||||
capApiEndpointDraft.value = data.capApiEndpoint
|
||||
capSecretDraft.value = data.capSecret
|
||||
domainSeparationDraft.value = data.domainSeparation
|
||||
}
|
||||
|
||||
@@ -221,7 +242,12 @@ async function saveServerSettings() {
|
||||
imageBaseUrl: imageBaseUrlDraft.value,
|
||||
hideFolderInUrl: hideFolderInUrlDraft.value,
|
||||
storageUseDatePath: storageUseDatePathDraft.value,
|
||||
allowRegistration: allowRegistrationDraft.value
|
||||
allowRegistration: allowRegistrationDraft.value,
|
||||
loginVerificationMethod: loginVerificationMethodDraft.value,
|
||||
turnstileSiteKey: turnstileSiteKeyDraft.value,
|
||||
turnstileSecretKey: turnstileSecretKeyDraft.value,
|
||||
capApiEndpoint: capApiEndpointDraft.value,
|
||||
capSecret: capSecretDraft.value
|
||||
})
|
||||
toast.add({ title: t('settings.saved'), color: 'success' })
|
||||
} catch (error: unknown) {
|
||||
@@ -279,9 +305,27 @@ async function checkLatestRelease(options: { notify?: boolean, refresh?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const loginVerificationOptions = computed(() => [
|
||||
{ value: 'slider' as const, label: t('settings.loginVerificationSlider') },
|
||||
{ value: 'turnstile' as const, label: t('settings.loginVerificationTurnstile') },
|
||||
{ value: 'cap' as const, label: t('settings.loginVerificationCap') }
|
||||
])
|
||||
|
||||
const loginVerificationHint = computed(() => {
|
||||
switch (loginVerificationMethodDraft.value) {
|
||||
case 'turnstile':
|
||||
return t('settings.loginVerificationTurnstileHint')
|
||||
case 'cap':
|
||||
return t('settings.loginVerificationCapHint')
|
||||
default:
|
||||
return t('settings.loginVerificationSliderHint')
|
||||
}
|
||||
})
|
||||
|
||||
async function loadPage() {
|
||||
if (isAdmin.value) {
|
||||
await Promise.all([loadSettings(), checkLatestRelease()])
|
||||
await loadSettings()
|
||||
await checkLatestRelease()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,7 +426,10 @@ watch(isAdmin, () => {
|
||||
:title="t('settings.systemInfo')"
|
||||
:hint="t('settings.systemInfoHint')"
|
||||
>
|
||||
<template #action>
|
||||
<template
|
||||
v-if="isAdmin"
|
||||
#action
|
||||
>
|
||||
<UButton
|
||||
:label="t('settings.checkUpdate')"
|
||||
icon="i-lucide-refresh-cw"
|
||||
@@ -712,6 +759,75 @@ watch(isAdmin, () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingsSection
|
||||
:title="t('settings.loginVerification')"
|
||||
:hint="t('settings.loginVerificationHint')"
|
||||
>
|
||||
<SettingsGroup>
|
||||
<div class="space-y-4 px-4 py-4 sm:px-5">
|
||||
<URadioGroup
|
||||
v-model="loginVerificationMethodDraft"
|
||||
:items="loginVerificationOptions"
|
||||
/>
|
||||
<p class="text-xs leading-relaxed text-muted">
|
||||
{{ loginVerificationHint }}
|
||||
</p>
|
||||
<div
|
||||
v-if="loginVerificationMethodDraft === 'turnstile'"
|
||||
class="space-y-3 rounded-xl border border-default bg-muted/20 p-4"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-highlighted">
|
||||
{{ t('settings.turnstileSiteKey') }}
|
||||
</label>
|
||||
<UInput
|
||||
v-model="turnstileSiteKeyDraft"
|
||||
:placeholder="t('settings.turnstileSiteKeyPlaceholder')"
|
||||
class="w-full font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-highlighted">
|
||||
{{ t('settings.turnstileSecretKey') }}
|
||||
</label>
|
||||
<UInput
|
||||
v-model="turnstileSecretKeyDraft"
|
||||
type="password"
|
||||
:placeholder="t('settings.turnstileSecretKeyPlaceholder')"
|
||||
class="w-full font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="loginVerificationMethodDraft === 'cap'"
|
||||
class="space-y-3 rounded-xl border border-default bg-muted/20 p-4"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-highlighted">
|
||||
{{ t('settings.capApiEndpoint') }}
|
||||
</label>
|
||||
<UInput
|
||||
v-model="capApiEndpointDraft"
|
||||
:placeholder="t('settings.capApiEndpointPlaceholder')"
|
||||
class="w-full font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-highlighted">
|
||||
{{ t('settings.capSecretKey') }}
|
||||
</label>
|
||||
<UInput
|
||||
v-model="capSecretDraft"
|
||||
type="password"
|
||||
:placeholder="t('settings.capSecretKeyPlaceholder')"
|
||||
class="w-full font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsGroup>
|
||||
</SettingsSection>
|
||||
</SettingsPanel>
|
||||
<div
|
||||
v-else
|
||||
|
||||
Reference in new issue
Block a user