2025-12-10 20:52:44 +08:00
|
|
|
<template>
|
2025-12-12 16:15:36 +08:00
|
|
|
<Dialog
|
|
|
|
|
v-model="isOpen"
|
2026-01-19 03:19:17 +08:00
|
|
|
size="md"
|
|
|
|
|
no-padding
|
2025-12-12 16:15:36 +08:00
|
|
|
>
|
2026-01-19 03:19:17 +08:00
|
|
|
<div class="px-6 py-6 sm:px-8 sm:py-8">
|
2025-12-10 20:52:44 +08:00
|
|
|
<!-- Logo 和标题 -->
|
2026-01-20 01:58:55 +08:00
|
|
|
<div class="flex flex-col items-center text-center mb-8">
|
2026-01-19 03:19:17 +08:00
|
|
|
<img
|
|
|
|
|
src="/aether_adaptive.svg"
|
2026-02-13 21:50:05 +08:00
|
|
|
:alt="siteName"
|
2026-01-20 01:58:55 +08:00
|
|
|
class="h-16 w-16 mb-4"
|
2026-01-19 03:19:17 +08:00
|
|
|
>
|
2026-01-20 01:58:55 +08:00
|
|
|
<h2 class="text-2xl font-semibold text-foreground">
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ t('auth.login.title', { siteName }) }}
|
2025-12-10 20:52:44 +08:00
|
|
|
</h2>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-11 10:03:10 +08:00
|
|
|
<!-- Demo 模式提示 -->
|
2025-12-12 16:15:36 +08:00
|
|
|
<div
|
|
|
|
|
v-if="isDemo"
|
2026-01-19 03:19:17 +08:00
|
|
|
class="rounded-lg border border-primary/20 bg-primary/5 p-3 mb-5"
|
2025-12-12 16:15:36 +08:00
|
|
|
>
|
2026-01-19 03:19:17 +08:00
|
|
|
<p class="text-xs font-medium text-foreground mb-2">
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ t('auth.login.demoMode') }}
|
2026-01-19 03:19:17 +08:00
|
|
|
</p>
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors w-full"
|
|
|
|
|
@click="fillDemoAccount('admin')"
|
|
|
|
|
>
|
|
|
|
|
<span class="inline-flex items-center justify-center w-4 h-4 rounded bg-primary/20 text-primary text-[10px] font-bold">A</span>
|
|
|
|
|
<span>admin@demo.aether.io / demo123</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors w-full"
|
|
|
|
|
@click="fillDemoAccount('user')"
|
|
|
|
|
>
|
|
|
|
|
<span class="inline-flex items-center justify-center w-4 h-4 rounded bg-muted text-muted-foreground text-[10px] font-bold">U</span>
|
|
|
|
|
<span>user@demo.aether.io / demo123</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- OAuth 登录按钮 -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="oauthProviders.length > 0"
|
|
|
|
|
class="mb-5"
|
|
|
|
|
>
|
|
|
|
|
<!-- 单个 provider: 完整按钮 -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="oauthProviders.length === 1"
|
|
|
|
|
class="space-y-2"
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="oauth-btn"
|
|
|
|
|
@click="handleOAuthLogin(oauthProviders[0].provider_type)"
|
|
|
|
|
>
|
2026-02-22 00:43:41 +08:00
|
|
|
<!-- eslint-disable vue/no-v-html -->
|
2026-01-19 03:19:17 +08:00
|
|
|
<span
|
|
|
|
|
class="oauth-icon"
|
2026-05-17 21:22:20 +08:00
|
|
|
v-html="getOAuthIcon(oauthProviders[0].provider_type, oauthProviders[0].icon_url)"
|
2026-01-19 03:19:17 +08:00
|
|
|
/>
|
2026-02-22 00:43:41 +08:00
|
|
|
<!-- eslint-enable vue/no-v-html -->
|
2026-06-30 14:14:12 +08:00
|
|
|
<span>{{ t('auth.login.oauthWithProvider', { provider: oauthProviders[0].display_name }) }}</span>
|
2026-01-19 03:19:17 +08:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 多个 provider: 图标按钮组 -->
|
|
|
|
|
<div
|
|
|
|
|
v-else
|
|
|
|
|
class="flex flex-col items-center gap-3"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
<span class="text-xs text-muted-foreground">{{ t('auth.login.oauthOptions') }}</span>
|
2026-01-19 03:19:17 +08:00
|
|
|
<div class="flex items-center justify-center gap-3">
|
|
|
|
|
<button
|
|
|
|
|
v-for="p in oauthProviders"
|
|
|
|
|
:key="p.provider_type"
|
|
|
|
|
type="button"
|
|
|
|
|
class="oauth-icon-btn"
|
|
|
|
|
:title="p.display_name"
|
|
|
|
|
@click="handleOAuthLogin(p.provider_type)"
|
2025-12-12 16:15:36 +08:00
|
|
|
>
|
2026-02-22 00:43:41 +08:00
|
|
|
<!-- eslint-disable vue/no-v-html -->
|
2026-01-19 03:19:17 +08:00
|
|
|
<span
|
|
|
|
|
class="oauth-icon-lg"
|
2026-05-17 21:22:20 +08:00
|
|
|
v-html="getOAuthIcon(p.provider_type, p.icon_url)"
|
2025-12-12 16:15:36 +08:00
|
|
|
/>
|
2026-02-22 00:43:41 +08:00
|
|
|
<!-- eslint-enable vue/no-v-html -->
|
2026-01-19 03:19:17 +08:00
|
|
|
</button>
|
2025-12-11 10:03:10 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-19 03:19:17 +08:00
|
|
|
<!-- 分隔线 -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="oauthProviders.length > 0"
|
|
|
|
|
class="flex items-center gap-3 mb-5"
|
|
|
|
|
>
|
|
|
|
|
<div class="flex-1 h-px bg-border" />
|
2026-06-30 14:14:12 +08:00
|
|
|
<span class="text-xs text-muted-foreground px-2">{{ t('auth.login.accountPassword') }}</span>
|
2026-01-19 03:19:17 +08:00
|
|
|
<div class="flex-1 h-px bg-border" />
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-02 16:17:24 +08:00
|
|
|
<!-- 认证方式切换 -->
|
2026-01-06 14:38:42 +08:00
|
|
|
<div
|
2026-01-02 16:17:24 +08:00
|
|
|
v-if="showAuthTypeTabs"
|
2026-01-19 03:19:17 +08:00
|
|
|
class="auth-type-tabs mb-4"
|
2026-01-02 16:17:24 +08:00
|
|
|
>
|
2026-01-06 14:38:42 +08:00
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-01-07 14:55:07 +08:00
|
|
|
class="auth-tab"
|
|
|
|
|
:class="[authType === 'local' && 'active']"
|
2026-01-06 14:38:42 +08:00
|
|
|
@click="authType = 'local'"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ t('auth.login.local') }}
|
2026-01-06 14:38:42 +08:00
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-01-07 14:55:07 +08:00
|
|
|
class="auth-tab"
|
|
|
|
|
:class="[authType === 'ldap' && 'active']"
|
2026-01-06 14:38:42 +08:00
|
|
|
@click="authType = 'ldap'"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ t('auth.login.ldap') }}
|
2026-01-06 14:38:42 +08:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
<!-- 登录表单 -->
|
2025-12-12 16:15:36 +08:00
|
|
|
<form
|
2026-05-17 16:03:57 +00:00
|
|
|
ref="loginFormEl"
|
|
|
|
|
name="login"
|
|
|
|
|
action="/api/auth/login"
|
|
|
|
|
method="post"
|
2025-12-12 16:15:36 +08:00
|
|
|
class="space-y-4"
|
2026-05-17 16:03:57 +00:00
|
|
|
autocomplete="on"
|
|
|
|
|
data-form-type="login"
|
2025-12-12 16:15:36 +08:00
|
|
|
@submit.prevent="handleLogin"
|
|
|
|
|
>
|
2026-01-19 03:19:17 +08:00
|
|
|
<div class="space-y-1.5">
|
2026-01-06 14:38:42 +08:00
|
|
|
<div class="flex items-center justify-between">
|
2026-01-19 03:19:17 +08:00
|
|
|
<Label
|
2026-05-17 16:03:57 +00:00
|
|
|
for="username"
|
2026-01-19 03:19:17 +08:00
|
|
|
class="text-sm"
|
|
|
|
|
>
|
|
|
|
|
{{ emailLabel }}
|
|
|
|
|
</Label>
|
2026-01-06 14:38:42 +08:00
|
|
|
<button
|
|
|
|
|
v-if="ldapExclusive && authType === 'ldap'"
|
|
|
|
|
type="button"
|
|
|
|
|
class="text-xs text-muted-foreground/60 hover:text-muted-foreground transition-colors"
|
|
|
|
|
@click="authType = 'local'"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ t('auth.login.adminLocal') }}
|
2026-01-06 14:38:42 +08:00
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
v-if="ldapExclusive && authType === 'local'"
|
|
|
|
|
type="button"
|
|
|
|
|
class="text-xs text-muted-foreground/60 hover:text-muted-foreground transition-colors"
|
|
|
|
|
@click="authType = 'ldap'"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ t('auth.login.backToLdap') }}
|
2026-01-06 14:38:42 +08:00
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-12-10 20:52:44 +08:00
|
|
|
<Input
|
2026-05-17 16:03:57 +00:00
|
|
|
id="username"
|
2025-12-10 20:52:44 +08:00
|
|
|
v-model="form.email"
|
2026-01-06 14:38:42 +08:00
|
|
|
type="text"
|
2026-05-17 16:03:57 +00:00
|
|
|
name="username"
|
2025-12-10 20:52:44 +08:00
|
|
|
required
|
2026-06-30 14:14:12 +08:00
|
|
|
:placeholder="t('auth.login.usernamePlaceholder')"
|
2026-05-17 16:03:57 +00:00
|
|
|
autocomplete="username"
|
|
|
|
|
autocapitalize="none"
|
|
|
|
|
spellcheck="false"
|
|
|
|
|
:disable-autofill="false"
|
2025-12-10 20:52:44 +08:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-19 03:19:17 +08:00
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
<Label
|
2026-05-17 16:03:57 +00:00
|
|
|
for="password"
|
2026-01-19 03:19:17 +08:00
|
|
|
class="text-sm"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ t('auth.login.password') }}
|
2026-01-19 03:19:17 +08:00
|
|
|
</Label>
|
2025-12-10 20:52:44 +08:00
|
|
|
<Input
|
2026-05-17 16:03:57 +00:00
|
|
|
id="password"
|
2025-12-10 20:52:44 +08:00
|
|
|
v-model="form.password"
|
2026-04-11 17:39:02 +08:00
|
|
|
type="password"
|
2026-05-17 16:03:57 +00:00
|
|
|
name="password"
|
2025-12-10 20:52:44 +08:00
|
|
|
required
|
2026-06-30 14:14:12 +08:00
|
|
|
:placeholder="t('auth.login.passwordPlaceholder')"
|
2026-05-17 16:03:57 +00:00
|
|
|
autocomplete="current-password"
|
|
|
|
|
:disable-autofill="false"
|
2025-12-10 20:52:44 +08:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-19 03:19:17 +08:00
|
|
|
<!-- 登录按钮 -->
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
:disabled="authStore.loading"
|
2026-01-20 01:58:55 +08:00
|
|
|
class="w-full h-12"
|
2026-01-19 03:19:17 +08:00
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ authStore.loading ? t('auth.login.submitting') : t('auth.login.submit') }}
|
2026-01-19 03:19:17 +08:00
|
|
|
</Button>
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
<!-- 提示信息 -->
|
2025-12-12 16:15:36 +08:00
|
|
|
<p
|
2025-12-30 17:15:48 +08:00
|
|
|
v-if="!isDemo && !allowRegistration"
|
2026-01-19 03:19:17 +08:00
|
|
|
class="text-xs text-muted-foreground text-center"
|
2025-12-12 16:15:36 +08:00
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ t('auth.login.contactAdmin') }}
|
2025-12-10 20:52:44 +08:00
|
|
|
</p>
|
|
|
|
|
</form>
|
2025-12-30 17:15:48 +08:00
|
|
|
|
|
|
|
|
<!-- 注册链接 -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="allowRegistration"
|
2026-01-19 03:19:17 +08:00
|
|
|
class="mt-5 pt-5 border-t border-border text-center text-sm text-muted-foreground"
|
2025-12-30 17:15:48 +08:00
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ t('auth.login.noAccount') }}
|
2026-01-19 03:19:17 +08:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="text-primary hover:text-primary/80 font-medium transition-colors"
|
2025-12-30 17:15:48 +08:00
|
|
|
@click="handleSwitchToRegister"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ t('auth.login.registerNow') }}
|
2026-01-19 03:19:17 +08:00
|
|
|
</button>
|
2025-12-30 17:15:48 +08:00
|
|
|
</div>
|
2025-12-10 20:52:44 +08:00
|
|
|
</div>
|
|
|
|
|
</Dialog>
|
2025-12-30 17:15:48 +08:00
|
|
|
|
|
|
|
|
<!-- Register Dialog -->
|
|
|
|
|
<RegisterDialog
|
|
|
|
|
v-model:open="showRegisterDialog"
|
|
|
|
|
:require-email-verification="requireEmailVerification"
|
2026-01-19 03:19:17 +08:00
|
|
|
:email-configured="emailConfigured"
|
2026-03-12 01:11:35 +08:00
|
|
|
:password-policy-level="passwordPolicyLevel"
|
2026-05-16 00:51:44 +08:00
|
|
|
:turnstile-enabled="turnstileEnabled"
|
|
|
|
|
:turnstile-site-key="turnstileSiteKey"
|
2026-05-16 17:37:58 +08:00
|
|
|
:privacy-policy="privacyPolicy"
|
2025-12-30 17:15:48 +08:00
|
|
|
@success="handleRegisterSuccess"
|
|
|
|
|
@switch-to-login="handleSwitchToLogin"
|
|
|
|
|
/>
|
2025-12-10 20:52:44 +08:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-12-30 17:15:48 +08:00
|
|
|
import { ref, watch, computed, onMounted } from 'vue'
|
2026-05-16 17:37:58 +08:00
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
2025-12-10 20:52:44 +08:00
|
|
|
import { Dialog } from '@/components/ui'
|
|
|
|
|
import Button from '@/components/ui/button.vue'
|
|
|
|
|
import Input from '@/components/ui/input.vue'
|
|
|
|
|
import Label from '@/components/ui/label.vue'
|
|
|
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
|
|
import { useToast } from '@/composables/useToast'
|
2026-02-13 21:50:05 +08:00
|
|
|
import { useSiteInfo } from '@/composables/useSiteInfo'
|
2026-03-12 01:11:35 +08:00
|
|
|
import { normalizePasswordPolicyLevel, type PasswordPolicyLevel } from '@/utils/passwordPolicy'
|
2025-12-11 10:03:10 +08:00
|
|
|
import { isDemoMode, DEMO_ACCOUNTS } from '@/config/demo'
|
2025-12-30 17:15:48 +08:00
|
|
|
import RegisterDialog from './RegisterDialog.vue'
|
2026-05-16 17:37:58 +08:00
|
|
|
import { authApi, type RegistrationPrivacyPolicySettings } from '@/api/auth'
|
2026-01-19 03:19:17 +08:00
|
|
|
import { oauthApi, type OAuthProviderInfo } from '@/api/oauth'
|
2026-03-17 16:34:09 +08:00
|
|
|
import { getClientDeviceId } from '@/utils/deviceId'
|
2026-01-19 03:19:17 +08:00
|
|
|
import { getApiUrl } from '@/utils/url'
|
2026-01-19 19:34:31 +08:00
|
|
|
import { getOAuthIcon } from '@/utils/oauth-icons'
|
2026-05-26 23:22:30 +08:00
|
|
|
import { navigateAfterLogin } from '@/features/auth/utils/loginRedirect'
|
2026-06-30 14:14:12 +08:00
|
|
|
import { useI18n } from '@/i18n'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
modelValue: boolean
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
'update:modelValue': [value: boolean]
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
2026-05-16 17:37:58 +08:00
|
|
|
const route = useRoute()
|
2025-12-10 20:52:44 +08:00
|
|
|
const authStore = useAuthStore()
|
|
|
|
|
const { success: showSuccess, warning: showWarning, error: showError } = useToast()
|
2026-02-13 21:50:05 +08:00
|
|
|
const { siteName } = useSiteInfo()
|
2026-06-30 14:14:12 +08:00
|
|
|
const { t } = useI18n()
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
const isOpen = ref(props.modelValue)
|
2025-12-11 10:03:10 +08:00
|
|
|
const isDemo = computed(() => isDemoMode())
|
2025-12-30 17:15:48 +08:00
|
|
|
const showRegisterDialog = ref(false)
|
|
|
|
|
const requireEmailVerification = ref(false)
|
2026-01-19 03:19:17 +08:00
|
|
|
const emailConfigured = ref(true) // 邮箱服务是否已配置
|
2026-03-12 01:11:35 +08:00
|
|
|
const passwordPolicyLevel = ref<PasswordPolicyLevel>('weak')
|
2026-05-16 00:51:44 +08:00
|
|
|
const turnstileEnabled = ref(false)
|
|
|
|
|
const turnstileSiteKey = ref<string | null>(null)
|
2025-12-30 17:15:48 +08:00
|
|
|
const allowRegistration = ref(false) // 由系统配置控制,默认关闭
|
2026-05-16 17:37:58 +08:00
|
|
|
const privacyPolicy = ref<RegistrationPrivacyPolicySettings>({
|
|
|
|
|
enabled: false,
|
|
|
|
|
format: 'markdown',
|
|
|
|
|
content: '',
|
|
|
|
|
version: ''
|
|
|
|
|
})
|
2025-12-10 20:52:44 +08:00
|
|
|
|
2026-01-02 16:17:24 +08:00
|
|
|
// LDAP authentication settings
|
2026-01-06 14:38:42 +08:00
|
|
|
const PREFERRED_AUTH_TYPE_KEY = 'aether_preferred_auth_type'
|
|
|
|
|
function getStoredAuthType(): 'local' | 'ldap' {
|
|
|
|
|
const stored = localStorage.getItem(PREFERRED_AUTH_TYPE_KEY)
|
|
|
|
|
return (stored === 'ldap' || stored === 'local') ? stored : 'local'
|
|
|
|
|
}
|
|
|
|
|
const authType = ref<'local' | 'ldap'>(getStoredAuthType())
|
2026-01-02 16:17:24 +08:00
|
|
|
const localEnabled = ref(true)
|
|
|
|
|
const ldapEnabled = ref(false)
|
|
|
|
|
const ldapExclusive = ref(false)
|
|
|
|
|
|
2026-01-19 03:19:17 +08:00
|
|
|
const oauthProviders = ref<OAuthProviderInfo[]>([])
|
2026-05-17 16:03:57 +00:00
|
|
|
const loginFormEl = ref<HTMLFormElement | null>(null)
|
2026-01-19 03:19:17 +08:00
|
|
|
|
2026-01-06 14:38:42 +08:00
|
|
|
// 保存用户的认证类型偏好
|
|
|
|
|
watch(authType, (newType) => {
|
|
|
|
|
localStorage.setItem(PREFERRED_AUTH_TYPE_KEY, newType)
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-02 16:17:24 +08:00
|
|
|
const showAuthTypeTabs = computed(() => {
|
|
|
|
|
return localEnabled.value && ldapEnabled.value && !ldapExclusive.value
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const emailLabel = computed(() => {
|
2026-06-30 14:14:12 +08:00
|
|
|
return t('auth.login.usernameEmail')
|
2026-01-02 16:17:24 +08:00
|
|
|
})
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
watch(() => props.modelValue, (val) => {
|
|
|
|
|
isOpen.value = val
|
|
|
|
|
// 打开对话框时重置表单
|
|
|
|
|
if (val) {
|
|
|
|
|
form.value = {
|
|
|
|
|
email: '',
|
|
|
|
|
password: ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
watch(isOpen, (val) => {
|
|
|
|
|
emit('update:modelValue', val)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const form = ref({
|
|
|
|
|
email: '',
|
|
|
|
|
password: ''
|
|
|
|
|
})
|
|
|
|
|
|
2025-12-11 10:03:10 +08:00
|
|
|
function fillDemoAccount(type: 'admin' | 'user') {
|
|
|
|
|
const account = DEMO_ACCOUNTS[type]
|
|
|
|
|
form.value.email = account.email
|
|
|
|
|
form.value.password = account.password
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 16:03:57 +00:00
|
|
|
async function handleLogin(event?: Event) {
|
|
|
|
|
const { email, password } = readCurrentLoginCredentials(event)
|
|
|
|
|
|
|
|
|
|
if (!email || !password) {
|
2026-06-30 14:14:12 +08:00
|
|
|
showWarning(t('auth.login.required'))
|
2025-12-10 20:52:44 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 16:03:57 +00:00
|
|
|
const success = await authStore.login(email, password, authType.value)
|
2025-12-10 20:52:44 +08:00
|
|
|
if (success) {
|
2026-05-17 16:03:57 +00:00
|
|
|
const targetPath = consumeStoredRedirectPath() ?? (authStore.canAccessAdmin ? '/admin/dashboard' : '/dashboard')
|
|
|
|
|
|
2026-05-26 23:22:30 +08:00
|
|
|
await navigateAfterLogin(router, targetPath)
|
2026-05-17 16:03:57 +00:00
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
showSuccess(t('auth.login.successRedirecting'))
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
// 关闭对话框
|
|
|
|
|
isOpen.value = false
|
|
|
|
|
} else {
|
2026-06-30 14:14:12 +08:00
|
|
|
showError(authStore.error || t('auth.login.failed'))
|
2025-12-10 20:52:44 +08:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-30 17:15:48 +08:00
|
|
|
|
2026-05-17 16:03:57 +00:00
|
|
|
function readCurrentLoginCredentials(event?: Event): { email: string; password: string } {
|
|
|
|
|
const formElement = event?.currentTarget instanceof HTMLFormElement
|
|
|
|
|
? event.currentTarget
|
|
|
|
|
: loginFormEl.value
|
|
|
|
|
|
|
|
|
|
const emailInput = formElement?.elements.namedItem('username')
|
|
|
|
|
const passwordInput = formElement?.elements.namedItem('password')
|
|
|
|
|
|
|
|
|
|
const email = emailInput instanceof HTMLInputElement
|
|
|
|
|
? emailInput.value.trim()
|
|
|
|
|
: form.value.email.trim()
|
|
|
|
|
const password = passwordInput instanceof HTMLInputElement
|
|
|
|
|
? passwordInput.value
|
|
|
|
|
: form.value.password
|
|
|
|
|
|
|
|
|
|
form.value.email = email
|
|
|
|
|
form.value.password = password
|
|
|
|
|
|
|
|
|
|
return { email, password }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function consumeStoredRedirectPath(): string | null {
|
|
|
|
|
const redirectPath = sessionStorage.getItem('redirectPath')
|
|
|
|
|
if (redirectPath) {
|
|
|
|
|
sessionStorage.removeItem('redirectPath')
|
|
|
|
|
}
|
|
|
|
|
if (!redirectPath || redirectPath === '/' || !redirectPath.startsWith('/') || redirectPath.startsWith('//')) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
return redirectPath
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 03:19:17 +08:00
|
|
|
function handleOAuthLogin(providerType: string) {
|
|
|
|
|
// 如果 sessionStorage 中没有 redirectPath(用户直接点击登录而非被守卫拦截),
|
|
|
|
|
// 则不设置,让 AuthCallback 使用默认跳转逻辑
|
2026-03-17 16:34:09 +08:00
|
|
|
const authorizeUrl = new URL(
|
|
|
|
|
getApiUrl(`/api/oauth/${providerType}/authorize`),
|
|
|
|
|
window.location.origin,
|
|
|
|
|
)
|
|
|
|
|
authorizeUrl.searchParams.set('client_device_id', getClientDeviceId())
|
|
|
|
|
window.location.href = authorizeUrl.toString()
|
2026-01-19 03:19:17 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-30 17:15:48 +08:00
|
|
|
function handleSwitchToRegister() {
|
|
|
|
|
isOpen.value = false
|
|
|
|
|
showRegisterDialog.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleRegisterSuccess() {
|
|
|
|
|
showRegisterDialog.value = false
|
2026-06-30 14:14:12 +08:00
|
|
|
showSuccess(t('auth.register.successLogin'))
|
2025-12-30 17:15:48 +08:00
|
|
|
isOpen.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleSwitchToLogin() {
|
|
|
|
|
showRegisterDialog.value = false
|
|
|
|
|
isOpen.value = true
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-02 16:17:24 +08:00
|
|
|
// Load authentication and registration settings on mount
|
2025-12-30 17:15:48 +08:00
|
|
|
onMounted(async () => {
|
|
|
|
|
try {
|
2026-01-19 03:19:17 +08:00
|
|
|
const [regSettings, authSettings, providers] = await Promise.all([
|
|
|
|
|
authApi.getRegistrationSettings(),
|
|
|
|
|
authApi.getAuthSettings(),
|
|
|
|
|
oauthApi.getProviders().catch(() => []),
|
|
|
|
|
])
|
|
|
|
|
|
2026-01-02 16:17:24 +08:00
|
|
|
allowRegistration.value = !!regSettings.enable_registration
|
|
|
|
|
requireEmailVerification.value = !!regSettings.require_email_verification
|
2026-01-19 03:19:17 +08:00
|
|
|
emailConfigured.value = !!regSettings.email_configured
|
2026-03-12 01:11:35 +08:00
|
|
|
passwordPolicyLevel.value = normalizePasswordPolicyLevel(regSettings.password_policy_level)
|
2026-05-16 00:51:44 +08:00
|
|
|
turnstileEnabled.value = !!regSettings.turnstile_enabled
|
|
|
|
|
turnstileSiteKey.value = regSettings.turnstile_site_key || null
|
2026-05-16 17:37:58 +08:00
|
|
|
privacyPolicy.value = regSettings.privacy_policy ?? {
|
|
|
|
|
enabled: false,
|
|
|
|
|
format: 'markdown',
|
|
|
|
|
content: '',
|
|
|
|
|
version: ''
|
|
|
|
|
}
|
2026-01-02 16:17:24 +08:00
|
|
|
|
|
|
|
|
localEnabled.value = authSettings.local_enabled
|
|
|
|
|
ldapEnabled.value = authSettings.ldap_enabled
|
|
|
|
|
ldapExclusive.value = authSettings.ldap_exclusive
|
2026-01-04 16:27:02 +08:00
|
|
|
// 若仅允许 LDAP 登录,则禁用本地注册入口
|
|
|
|
|
if (ldapExclusive.value) {
|
|
|
|
|
allowRegistration.value = false
|
|
|
|
|
}
|
2026-01-02 16:17:24 +08:00
|
|
|
|
|
|
|
|
// Set default auth type based on settings
|
|
|
|
|
if (authSettings.ldap_exclusive) {
|
|
|
|
|
authType.value = 'ldap'
|
|
|
|
|
} else if (!authSettings.local_enabled && authSettings.ldap_enabled) {
|
|
|
|
|
authType.value = 'ldap'
|
|
|
|
|
} else {
|
|
|
|
|
authType.value = 'local'
|
|
|
|
|
}
|
2026-01-19 03:19:17 +08:00
|
|
|
|
|
|
|
|
oauthProviders.value = providers
|
2026-05-16 17:37:58 +08:00
|
|
|
if (allowRegistration.value && (route.path === '/register' || typeof route.query.invite === 'string')) {
|
|
|
|
|
isOpen.value = false
|
|
|
|
|
showRegisterDialog.value = true
|
|
|
|
|
}
|
2026-01-15 17:03:19 +08:00
|
|
|
} catch {
|
2026-01-02 16:17:24 +08:00
|
|
|
// If获取失败,保持默认:关闭注册 & 关闭邮箱验证 & 使用本地认证
|
2025-12-30 17:15:48 +08:00
|
|
|
allowRegistration.value = false
|
|
|
|
|
requireEmailVerification.value = false
|
2026-01-19 03:19:17 +08:00
|
|
|
emailConfigured.value = false
|
2026-03-12 01:11:35 +08:00
|
|
|
passwordPolicyLevel.value = 'weak'
|
2026-05-16 00:51:44 +08:00
|
|
|
turnstileEnabled.value = false
|
|
|
|
|
turnstileSiteKey.value = null
|
2026-05-16 17:37:58 +08:00
|
|
|
privacyPolicy.value = {
|
|
|
|
|
enabled: false,
|
|
|
|
|
format: 'markdown',
|
|
|
|
|
content: '',
|
|
|
|
|
version: ''
|
|
|
|
|
}
|
2026-01-02 16:17:24 +08:00
|
|
|
localEnabled.value = true
|
|
|
|
|
ldapEnabled.value = false
|
|
|
|
|
ldapExclusive.value = false
|
|
|
|
|
authType.value = 'local'
|
2026-01-19 03:19:17 +08:00
|
|
|
oauthProviders.value = []
|
2025-12-30 17:15:48 +08:00
|
|
|
}
|
|
|
|
|
})
|
2025-12-10 20:52:44 +08:00
|
|
|
</script>
|
2026-01-06 14:38:42 +08:00
|
|
|
|
|
|
|
|
<style scoped>
|
2026-01-19 03:19:17 +08:00
|
|
|
.oauth-btn {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 0.625rem 1rem;
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: hsl(var(--foreground));
|
2026-01-20 01:58:55 +08:00
|
|
|
background: hsl(var(--muted) / 0.5);
|
|
|
|
|
border: 1px solid hsl(var(--border) / 0.6);
|
|
|
|
|
border-radius: 0.75rem;
|
2026-01-19 03:19:17 +08:00
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.15s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.oauth-btn:hover {
|
|
|
|
|
background: hsl(var(--muted));
|
|
|
|
|
border-color: hsl(var(--primary) / 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.oauth-icon {
|
|
|
|
|
width: 1.25rem;
|
|
|
|
|
height: 1.25rem;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.oauth-icon :deep(svg) {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.oauth-icon-btn {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 3rem;
|
|
|
|
|
height: 3rem;
|
2026-01-20 01:58:55 +08:00
|
|
|
background: hsl(var(--muted) / 0.5);
|
|
|
|
|
border: 1px solid hsl(var(--border) / 0.6);
|
2026-01-19 03:19:17 +08:00
|
|
|
border-radius: 0.75rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.15s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.oauth-icon-btn:hover {
|
|
|
|
|
background: hsl(var(--muted));
|
|
|
|
|
border-color: hsl(var(--primary) / 0.5);
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.oauth-icon-lg {
|
|
|
|
|
width: 1.5rem;
|
|
|
|
|
height: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.oauth-icon-lg :deep(svg) {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 14:38:42 +08:00
|
|
|
.auth-type-tabs {
|
|
|
|
|
display: flex;
|
|
|
|
|
border-bottom: 1px solid hsl(var(--border));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.auth-tab {
|
|
|
|
|
flex: 1;
|
2026-01-19 03:19:17 +08:00
|
|
|
padding: 0.5rem 1rem;
|
2026-01-06 14:38:42 +08:00
|
|
|
font-size: 0.875rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: hsl(var(--muted-foreground));
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: color 0.15s ease;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.auth-tab::after {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: -1px;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
height: 2px;
|
|
|
|
|
background: transparent;
|
|
|
|
|
transition: background 0.15s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.auth-tab:hover:not(.active) {
|
|
|
|
|
color: hsl(var(--foreground));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.auth-tab.active {
|
2026-01-19 03:19:17 +08:00
|
|
|
color: hsl(var(--primary));
|
2026-01-06 14:38:42 +08:00
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.auth-tab.active::after {
|
2026-01-19 03:19:17 +08:00
|
|
|
background: hsl(var(--primary));
|
2026-01-06 14:38:42 +08:00
|
|
|
}
|
|
|
|
|
</style>
|