feat: 添加 OAuth 认证支持及相关改进

- 新增 OAuth 模块,支持 LinuxDo/GitHub/Google 等第三方登录
- 用户邮箱改为可选字段,支持无邮箱注册
- 新增模块配置验证状态 (config_validated/config_error)
- 系统设置界面改为分块独立保存
- 用户设置新增 OAuth 绑定管理和首次密码设置
- 登录界面支持 OAuth 按钮展示
- 邮箱验证设置移至邮件设置页面
This commit is contained in:
fawney19
2026-01-19 03:19:17 +08:00
parent e2e14fd09c
commit 3d88dfd98a
61 changed files with 4548 additions and 950 deletions

View File

@@ -65,14 +65,14 @@ export interface VerificationStatusResponse {
}
export interface RegisterRequest {
email: string
email?: string
username: string
password: string
}
export interface RegisterResponse {
user_id: string
email: string
email?: string
username: string
message: string
}
@@ -80,6 +80,7 @@ export interface RegisterResponse {
export interface RegistrationSettingsResponse {
enable_registration: boolean
require_email_verification: boolean
email_configured: boolean
}
export interface AuthSettingsResponse {

View File

@@ -3,7 +3,7 @@ import type { ActivityHeatmap } from '@/types/activity'
export interface Profile {
id: string // UUID
email: string
email?: string | null
username: string
role: string
is_active: boolean
@@ -13,7 +13,8 @@ export interface Profile {
created_at: string
updated_at?: string
last_login_at?: string
auth_source?: 'local' | 'ldap'
auth_source?: 'local' | 'ldap' | 'oauth'
has_password?: boolean
preferences?: UserPreferences
}
@@ -132,7 +133,7 @@ export interface ApiKey {
// 不再需要 ProviderBinding 接口
export interface ChangePasswordRequest {
old_password: string
old_password?: string // 可选:首次设置密码时不需要
new_password: string
}

View File

@@ -5,6 +5,8 @@ export interface ModuleStatus {
available: boolean
enabled: boolean
active: boolean
config_validated: boolean
config_error: string | null
display_name: string
description: string
category: 'auth' | 'monitoring' | 'security' | 'integration'

136
frontend/src/api/oauth.ts Normal file
View File

@@ -0,0 +1,136 @@
import apiClient from './client'
export interface OAuthProviderInfo {
provider_type: string
display_name: string
}
export interface OAuthProvidersResponse {
providers: OAuthProviderInfo[]
}
export interface OAuthLinkInfo {
provider_type: string
display_name: string
provider_username?: string | null
provider_email?: string | null
linked_at?: string | null
last_login_at?: string | null
provider_enabled?: boolean
}
export interface OAuthLinksResponse {
links: OAuthLinkInfo[]
}
// Admin
export interface SupportedOAuthType {
provider_type: string
display_name: string
default_authorization_url: string
default_token_url: string
default_userinfo_url: string
default_scopes: string[]
}
export interface OAuthProviderAdminConfig {
provider_type: string
display_name: string
client_id: string
has_secret: boolean
authorization_url_override?: string | null
token_url_override?: string | null
userinfo_url_override?: string | null
scopes?: string[] | null
redirect_uri: string
frontend_callback_url: string
attribute_mapping?: Record<string, any> | null
extra_config?: Record<string, any> | null
is_enabled: boolean
}
export interface OAuthProviderUpsertRequest {
display_name: string
client_id: string
client_secret?: string
authorization_url_override?: string | null
token_url_override?: string | null
userinfo_url_override?: string | null
scopes?: string[] | null
redirect_uri: string
frontend_callback_url: string
attribute_mapping?: Record<string, any> | null
extra_config?: Record<string, any> | null
is_enabled: boolean
force?: boolean
}
export interface OAuthProviderTestResponse {
authorization_url_reachable: boolean
token_url_reachable: boolean
secret_status: 'likely_valid' | 'invalid' | 'unknown' | 'not_provided' | string
details?: string
}
export interface OAuthProviderTestRequest {
client_id: string
client_secret?: string
authorization_url_override?: string | null
token_url_override?: string | null
redirect_uri: string
}
export const oauthApi = {
async getProviders(): Promise<OAuthProviderInfo[]> {
const response = await apiClient.get<OAuthProvidersResponse>('/api/oauth/providers')
return response.data.providers || []
},
async getBindableProviders(): Promise<OAuthProviderInfo[]> {
const response = await apiClient.get<OAuthProvidersResponse>('/api/user/oauth/bindable-providers')
return response.data.providers || []
},
async getMyLinks(): Promise<OAuthLinkInfo[]> {
const response = await apiClient.get<OAuthLinksResponse>('/api/user/oauth/links')
return response.data.links || []
},
async unbind(providerType: string): Promise<{ message: string }> {
const response = await apiClient.delete<{ message: string }>(`/api/user/oauth/${providerType}`)
return response.data
},
admin: {
async getSupportedTypes(): Promise<SupportedOAuthType[]> {
const response = await apiClient.get<SupportedOAuthType[]>('/api/admin/oauth/supported-types')
return response.data || []
},
async listProviderConfigs(): Promise<OAuthProviderAdminConfig[]> {
const response = await apiClient.get<OAuthProviderAdminConfig[]>('/api/admin/oauth/providers')
return response.data || []
},
async getProviderConfig(providerType: string): Promise<OAuthProviderAdminConfig> {
const response = await apiClient.get<OAuthProviderAdminConfig>(`/api/admin/oauth/providers/${providerType}`)
return response.data
},
async upsertProviderConfig(providerType: string, payload: OAuthProviderUpsertRequest): Promise<OAuthProviderAdminConfig> {
const response = await apiClient.put<OAuthProviderAdminConfig>(`/api/admin/oauth/providers/${providerType}`, payload)
return response.data
},
async deleteProviderConfig(providerType: string): Promise<{ message: string }> {
const response = await apiClient.delete<{ message: string }>(`/api/admin/oauth/providers/${providerType}`)
return response.data
},
async testProviderConfig(providerType: string, payload: OAuthProviderTestRequest): Promise<OAuthProviderTestResponse> {
const response = await apiClient.post<OAuthProviderTestResponse>(`/api/admin/oauth/providers/${providerType}/test`, payload)
return response.data
},
}
}