mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: 添加 OAuth 认证支持及相关改进
- 新增 OAuth 模块,支持 LinuxDo/GitHub/Google 等第三方登录 - 用户邮箱改为可选字段,支持无邮箱注册 - 新增模块配置验证状态 (config_validated/config_error) - 系统设置界面改为分块独立保存 - 用户设置新增 OAuth 绑定管理和首次密码设置 - 登录界面支持 OAuth 按钮展示 - 邮箱验证设置移至邮件设置页面
This commit is contained in:
@@ -1,75 +1,111 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="isOpen"
|
||||
size="lg"
|
||||
size="md"
|
||||
no-padding
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<div class="px-6 py-6 sm:px-8 sm:py-8">
|
||||
<!-- Logo 和标题 -->
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="mb-4 rounded-3xl border border-primary/30 dark:border-[#cc785c]/30 bg-primary/5 dark:bg-transparent p-4 shadow-inner shadow-white/40 dark:shadow-[#cc785c]/10">
|
||||
<img
|
||||
src="/aether_adaptive.svg"
|
||||
alt="Logo"
|
||||
class="h-16 w-16"
|
||||
>
|
||||
</div>
|
||||
<h2 class="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
欢迎回来
|
||||
<div class="flex flex-col items-center text-center mb-6">
|
||||
<img
|
||||
src="/aether_adaptive.svg"
|
||||
alt="Aether"
|
||||
class="h-10 w-10 mb-3"
|
||||
>
|
||||
<h2 class="text-xl font-semibold text-foreground">
|
||||
登录到 Aether
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Demo 模式提示 -->
|
||||
<div
|
||||
v-if="isDemo"
|
||||
class="rounded-lg border border-primary/20 dark:border-primary/30 bg-primary/5 dark:bg-primary/10 p-4"
|
||||
class="rounded-lg border border-primary/20 bg-primary/5 p-3 mb-5"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0 text-primary dark:text-primary/90">
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
<p class="text-xs font-medium text-foreground mb-2">
|
||||
演示模式
|
||||
</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)"
|
||||
>
|
||||
<span
|
||||
class="oauth-icon"
|
||||
v-html="getOAuthIcon(oauthProviders[0].provider_type)"
|
||||
/>
|
||||
<span>使用 {{ oauthProviders[0].display_name }} 登录</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 多个 provider: 图标按钮组 -->
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center gap-3"
|
||||
>
|
||||
<span class="text-xs text-muted-foreground">使用以下方式登录</span>
|
||||
<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)"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
|
||||
clip-rule="evenodd"
|
||||
<span
|
||||
class="oauth-icon-lg"
|
||||
v-html="getOAuthIcon(p.provider_type)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-foreground">
|
||||
演示模式
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
当前处于演示模式,所有数据均为模拟数据。
|
||||
</p>
|
||||
<div class="mt-3 space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors group"
|
||||
@click="fillDemoAccount('admin')"
|
||||
>
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-primary/20 dark:bg-primary/30 text-primary text-[10px] font-bold group-hover:bg-primary/30 dark:group-hover:bg-primary/40 transition-colors">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 group"
|
||||
@click="fillDemoAccount('user')"
|
||||
>
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-muted text-muted-foreground text-[10px] font-bold group-hover:bg-muted/80 transition-colors">U</span>
|
||||
<span>普通用户:user@demo.aether.io / demo123</span>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div
|
||||
v-if="oauthProviders.length > 0"
|
||||
class="flex items-center gap-3 mb-5"
|
||||
>
|
||||
<div class="flex-1 h-px bg-border" />
|
||||
<span class="text-xs text-muted-foreground px-2">或使用账号密码</span>
|
||||
<div class="flex-1 h-px bg-border" />
|
||||
</div>
|
||||
|
||||
<!-- 认证方式切换 -->
|
||||
<div
|
||||
v-if="showAuthTypeTabs"
|
||||
class="auth-type-tabs"
|
||||
class="auth-type-tabs mb-4"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -89,15 +125,19 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<form
|
||||
class="space-y-4"
|
||||
@submit.prevent="handleLogin"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="login-email">{{ emailLabel }}</Label>
|
||||
<Label
|
||||
for="login-email"
|
||||
class="text-sm"
|
||||
>
|
||||
{{ emailLabel }}
|
||||
</Label>
|
||||
<button
|
||||
v-if="ldapExclusive && authType === 'ldap'"
|
||||
type="button"
|
||||
@@ -120,72 +160,69 @@
|
||||
v-model="form.email"
|
||||
type="text"
|
||||
required
|
||||
placeholder="username 或 email"
|
||||
placeholder="用户名或邮箱"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="login-password">密码</Label>
|
||||
<div class="space-y-1.5">
|
||||
<Label
|
||||
for="login-password"
|
||||
class="text-sm"
|
||||
>
|
||||
密码
|
||||
</Label>
|
||||
<Input
|
||||
id="login-password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
required
|
||||
placeholder="********"
|
||||
placeholder="输入密码"
|
||||
autocomplete="off"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="authStore.loading"
|
||||
class="w-full h-10"
|
||||
>
|
||||
{{ authStore.loading ? '登录中...' : '登录' }}
|
||||
</Button>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<p
|
||||
v-if="!isDemo && !allowRegistration"
|
||||
class="text-xs text-slate-400 dark:text-muted-foreground/80"
|
||||
class="text-xs text-muted-foreground text-center"
|
||||
>
|
||||
如需开通账户,请联系管理员配置访问权限
|
||||
如需开通账户,请联系管理员
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<!-- 注册链接 -->
|
||||
<div
|
||||
v-if="allowRegistration"
|
||||
class="mt-4 text-center text-sm"
|
||||
class="mt-5 pt-5 border-t border-border text-center text-sm text-muted-foreground"
|
||||
>
|
||||
还没有账户?
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-auto p-0"
|
||||
<button
|
||||
type="button"
|
||||
class="text-primary hover:text-primary/80 font-medium transition-colors"
|
||||
@click="handleSwitchToRegister"
|
||||
>
|
||||
立即注册
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="w-full sm:w-auto border-slate-200 dark:border-slate-600 text-slate-500 dark:text-slate-400 hover:text-primary hover:border-primary/50 hover:bg-primary/5 dark:hover:text-primary dark:hover:border-primary/50 dark:hover:bg-primary/10"
|
||||
@click="isOpen = false"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="authStore.loading"
|
||||
class="w-full sm:w-auto bg-primary hover:bg-primary/90 text-white border-0"
|
||||
@click="handleLogin"
|
||||
>
|
||||
{{ authStore.loading ? '登录中...' : '登录' }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- Register Dialog -->
|
||||
<RegisterDialog
|
||||
v-model:open="showRegisterDialog"
|
||||
:require-email-verification="requireEmailVerification"
|
||||
:email-configured="emailConfigured"
|
||||
@success="handleRegisterSuccess"
|
||||
@switch-to-login="handleSwitchToLogin"
|
||||
/>
|
||||
@@ -203,6 +240,19 @@ import { useToast } from '@/composables/useToast'
|
||||
import { isDemoMode, DEMO_ACCOUNTS } from '@/config/demo'
|
||||
import RegisterDialog from './RegisterDialog.vue'
|
||||
import { authApi } from '@/api/auth'
|
||||
import { oauthApi, type OAuthProviderInfo } from '@/api/oauth'
|
||||
import { getApiUrl } from '@/utils/url'
|
||||
|
||||
// OAuth provider icons
|
||||
const OAUTH_ICONS: Record<string, string> = {
|
||||
linuxdo: `<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg"><clipPath id="ld"><circle cx="60" cy="60" r="47"/></clipPath><circle fill="#f0f0f0" cx="60" cy="60" r="50"/><rect fill="#1c1c1e" clip-path="url(#ld)" x="10" y="10" width="100" height="30"/><rect fill="#f0f0f0" clip-path="url(#ld)" x="10" y="40" width="100" height="40"/><rect fill="#ffb003" clip-path="url(#ld)" x="10" y="80" width="100" height="30"/></svg>`,
|
||||
github: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>`,
|
||||
google: `<svg viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>`,
|
||||
}
|
||||
|
||||
function getOAuthIcon(providerType: string): string {
|
||||
return OAUTH_ICONS[providerType] || OAUTH_ICONS.github
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
@@ -220,6 +270,7 @@ const isOpen = ref(props.modelValue)
|
||||
const isDemo = computed(() => isDemoMode())
|
||||
const showRegisterDialog = ref(false)
|
||||
const requireEmailVerification = ref(false)
|
||||
const emailConfigured = ref(true) // 邮箱服务是否已配置
|
||||
const allowRegistration = ref(false) // 由系统配置控制,默认关闭
|
||||
|
||||
// LDAP authentication settings
|
||||
@@ -233,6 +284,8 @@ const localEnabled = ref(true)
|
||||
const ldapEnabled = ref(false)
|
||||
const ldapExclusive = ref(false)
|
||||
|
||||
const oauthProviders = ref<OAuthProviderInfo[]>([])
|
||||
|
||||
// 保存用户的认证类型偏好
|
||||
watch(authType, (newType) => {
|
||||
localStorage.setItem(PREFERRED_AUTH_TYPE_KEY, newType)
|
||||
@@ -296,6 +349,12 @@ async function handleLogin() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleOAuthLogin(providerType: string) {
|
||||
// 如果 sessionStorage 中没有 redirectPath(用户直接点击登录而非被守卫拦截),
|
||||
// 则不设置,让 AuthCallback 使用默认跳转逻辑
|
||||
window.location.href = getApiUrl(`/api/oauth/${providerType}/authorize`)
|
||||
}
|
||||
|
||||
function handleSwitchToRegister() {
|
||||
isOpen.value = false
|
||||
showRegisterDialog.value = true
|
||||
@@ -315,13 +374,16 @@ function handleSwitchToLogin() {
|
||||
// Load authentication and registration settings on mount
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Load registration settings
|
||||
const regSettings = await authApi.getRegistrationSettings()
|
||||
const [regSettings, authSettings, providers] = await Promise.all([
|
||||
authApi.getRegistrationSettings(),
|
||||
authApi.getAuthSettings(),
|
||||
oauthApi.getProviders().catch(() => []),
|
||||
])
|
||||
|
||||
allowRegistration.value = !!regSettings.enable_registration
|
||||
requireEmailVerification.value = !!regSettings.require_email_verification
|
||||
emailConfigured.value = !!regSettings.email_configured
|
||||
|
||||
// Load authentication settings
|
||||
const authSettings = await authApi.getAuthSettings()
|
||||
localEnabled.value = authSettings.local_enabled
|
||||
ldapEnabled.value = authSettings.ldap_enabled
|
||||
ldapExclusive.value = authSettings.ldap_exclusive
|
||||
@@ -338,19 +400,85 @@ onMounted(async () => {
|
||||
} else {
|
||||
authType.value = 'local'
|
||||
}
|
||||
|
||||
oauthProviders.value = providers
|
||||
} catch {
|
||||
// If获取失败,保持默认:关闭注册 & 关闭邮箱验证 & 使用本地认证
|
||||
allowRegistration.value = false
|
||||
requireEmailVerification.value = false
|
||||
emailConfigured.value = false
|
||||
localEnabled.value = true
|
||||
ldapEnabled.value = false
|
||||
ldapExclusive.value = false
|
||||
authType.value = 'local'
|
||||
oauthProviders.value = []
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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));
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
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;
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
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%;
|
||||
}
|
||||
|
||||
.auth-type-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
@@ -358,7 +486,7 @@ onMounted(async () => {
|
||||
|
||||
.auth-tab {
|
||||
flex: 1;
|
||||
padding: 0.625rem 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--muted-foreground));
|
||||
@@ -385,11 +513,11 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.auth-tab.active {
|
||||
color: var(--book-cloth);
|
||||
color: hsl(var(--primary));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.auth-tab.active::after {
|
||||
background: var(--book-cloth);
|
||||
background: hsl(var(--primary));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
注册新账户
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
请填写您的邮箱和个人信息完成注册
|
||||
{{ emailConfigured ? '请填写您的信息完成注册' : '请填写用户名和密码完成注册' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -28,27 +28,40 @@
|
||||
data-form-type="other"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<!-- Email -->
|
||||
<div class="space-y-2">
|
||||
<Label for="reg-email">邮箱 <span class="text-muted-foreground">*</span></Label>
|
||||
<!-- Email (仅当邮箱服务已配置时显示) -->
|
||||
<div
|
||||
v-if="emailConfigured"
|
||||
class="space-y-2"
|
||||
>
|
||||
<Label for="reg-email">
|
||||
邮箱
|
||||
<span
|
||||
v-if="requireEmailVerification"
|
||||
class="text-destructive"
|
||||
>*</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-muted-foreground text-xs"
|
||||
>(可选)</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="reg-email"
|
||||
v-model="formData.email"
|
||||
type="email"
|
||||
placeholder="hello@example.com"
|
||||
required
|
||||
:required="requireEmailVerification"
|
||||
disable-autofill
|
||||
:disabled="isLoading || emailVerified"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Verification Code Section -->
|
||||
<!-- Verification Code Section (仅当需要邮箱验证时显示) -->
|
||||
<div
|
||||
v-if="requireEmailVerification"
|
||||
v-if="emailConfigured && requireEmailVerification"
|
||||
class="space-y-3"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<Label>验证码 <span class="text-muted-foreground">*</span></Label>
|
||||
<Label>验证码 <span class="text-destructive">*</span></Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
@@ -113,7 +126,7 @@
|
||||
|
||||
<!-- Username -->
|
||||
<div class="space-y-2">
|
||||
<Label for="reg-uname">用户名 <span class="text-muted-foreground">*</span></Label>
|
||||
<Label for="reg-uname">用户名 <span class="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="reg-uname"
|
||||
v-model="formData.username"
|
||||
@@ -127,7 +140,7 @@
|
||||
|
||||
<!-- Password -->
|
||||
<div class="space-y-2">
|
||||
<Label :for="`pwd-${formNonce}`">密码 <span class="text-muted-foreground">*</span></Label>
|
||||
<Label :for="`pwd-${formNonce}`">密码 <span class="text-destructive">*</span></Label>
|
||||
<Input
|
||||
:id="`pwd-${formNonce}`"
|
||||
v-model="formData.password"
|
||||
@@ -146,7 +159,7 @@
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="space-y-2">
|
||||
<Label :for="`pwd-confirm-${formNonce}`">确认密码 <span class="text-muted-foreground">*</span></Label>
|
||||
<Label :for="`pwd-confirm-${formNonce}`">确认密码 <span class="text-destructive">*</span></Label>
|
||||
<Input
|
||||
:id="`pwd-confirm-${formNonce}`"
|
||||
v-model="formData.confirmPassword"
|
||||
@@ -210,6 +223,7 @@ import Label from '@/components/ui/label.vue'
|
||||
interface Props {
|
||||
open?: boolean
|
||||
requireEmailVerification?: boolean
|
||||
emailConfigured?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
@@ -220,7 +234,8 @@ interface Emits {
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
requireEmailVerification: false
|
||||
requireEmailVerification: false,
|
||||
emailConfigured: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
@@ -353,17 +368,19 @@ const sendCodeButtonText = computed(() => {
|
||||
})
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
// 基本信息:用户名和密码必填
|
||||
const hasBasicInfo =
|
||||
formData.value.email &&
|
||||
formData.value.username &&
|
||||
formData.value.password &&
|
||||
formData.value.confirmPassword
|
||||
|
||||
if (!hasBasicInfo) return false
|
||||
|
||||
// If email verification is required, check if verified
|
||||
if (props.requireEmailVerification && !emailVerified.value) {
|
||||
return false
|
||||
// 如果需要邮箱验证,邮箱和验证都必须完成
|
||||
if (props.requireEmailVerification) {
|
||||
if (!formData.value.email || !emailVerified.value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check password match
|
||||
@@ -608,11 +625,17 @@ const handleSubmit = async () => {
|
||||
loadingText.value = '注册中...'
|
||||
|
||||
try {
|
||||
const response = await authApi.register({
|
||||
email: formData.value.email,
|
||||
// 构建请求数据:邮箱可选
|
||||
const registerData: { email?: string; username: string; password: string } = {
|
||||
username: formData.value.username,
|
||||
password: formData.value.password
|
||||
})
|
||||
}
|
||||
// 只有当邮箱有值时才添加
|
||||
if (formData.value.email && formData.value.email.trim()) {
|
||||
registerData.email = formData.value.email
|
||||
}
|
||||
|
||||
const response = await authApi.register(registerData)
|
||||
|
||||
success(response.message || '欢迎加入!请登录以继续', '注册成功')
|
||||
|
||||
|
||||
@@ -118,14 +118,13 @@
|
||||
<Label
|
||||
for="form-email"
|
||||
class="text-sm font-medium"
|
||||
>邮箱 <span class="text-muted-foreground">*</span></Label>
|
||||
>邮箱</Label>
|
||||
<Input
|
||||
id="form-email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
autocomplete="off"
|
||||
data-form-type="other"
|
||||
required
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
@@ -472,11 +471,10 @@ const { isEditMode, handleDialogUpdate, handleCancel } = useFormDialog({
|
||||
// 表单验证
|
||||
const isFormValid = computed(() => {
|
||||
const hasUsername = form.value.username.trim().length > 0
|
||||
const hasEmail = form.value.email.trim().length > 0
|
||||
const hasPassword = isEditMode.value || form.value.password.length >= 6
|
||||
// 编辑模式下如果填写了密码,必须确认密码一致
|
||||
const passwordConfirmed = !isEditMode.value || form.value.password.length === 0 || form.value.password === form.value.confirmPassword
|
||||
return hasUsername && hasEmail && hasPassword && passwordConfirmed
|
||||
return hasUsername && hasPassword && passwordConfirmed
|
||||
})
|
||||
|
||||
// 加载访问控制选项
|
||||
@@ -508,16 +506,11 @@ function toggleSelection(field: 'allowed_providers' | 'allowed_api_formats' | 'a
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
// 验证邮箱必填
|
||||
if (!form.value.email || !form.value.email.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const data: UserFormData & { password?: string; unlimited?: boolean } = {
|
||||
username: form.value.username,
|
||||
email: form.value.email.trim(),
|
||||
email: form.value.email.trim() || '',
|
||||
quota_usd: form.value.unlimited ? null : form.value.quota,
|
||||
role: form.value.role,
|
||||
allowed_providers: form.value.allowed_providers.length > 0 ? form.value.allowed_providers : null,
|
||||
|
||||
Reference in New Issue
Block a user