feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import { db, users, userIdentities } from '~/drizzle/db'
|
||||
import { eq, or, like, inArray } from 'drizzle-orm'
|
||||
import { createApiError } from '~~/server/utils/apiError'
|
||||
import { resolveAvatarSource } from '~~/server/utils/user-avatar'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = event.context.user
|
||||
|
||||
if (!user) {
|
||||
throw createApiError(401, 'AUTH_UNAUTHORIZED_ACCESS', '未授权访问')
|
||||
}
|
||||
|
||||
if (!['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '只有管理员才能清除聚合登录绑定'
|
||||
})
|
||||
}
|
||||
|
||||
const result = await db.transaction(async (tx) => {
|
||||
// 1. 查询所有聚合登录相关的身份
|
||||
const targetIdentities = await tx.query.userIdentities.findMany({
|
||||
where: or(
|
||||
eq(userIdentities.provider, 'aggregate'),
|
||||
like(userIdentities.provider, 'aggregate:%')
|
||||
),
|
||||
columns: {
|
||||
id: true,
|
||||
userId: true,
|
||||
provider: true,
|
||||
providerUserId: true
|
||||
}
|
||||
})
|
||||
|
||||
if (targetIdentities.length === 0) {
|
||||
return { count: 0, usersAffected: 0 }
|
||||
}
|
||||
|
||||
const affectedUserIds = Array.from(new Set(targetIdentities.map((item) => item.userId)))
|
||||
|
||||
// 2. 检查受影响用户的头像来源
|
||||
const targetKeys = new Set(
|
||||
targetIdentities.map((identity) => `${identity.provider}:${identity.providerUserId}`)
|
||||
)
|
||||
|
||||
const affectedUsers = await tx.query.users.findMany({
|
||||
where: inArray(users.id, affectedUserIds),
|
||||
columns: {
|
||||
id: true,
|
||||
avatarProvider: true,
|
||||
avatarProviderUserId: true
|
||||
}
|
||||
})
|
||||
|
||||
const allIdentitiesForAffected = await tx.query.userIdentities.findMany({
|
||||
where: inArray(userIdentities.userId, affectedUserIds),
|
||||
columns: {
|
||||
id: true,
|
||||
userId: true,
|
||||
provider: true,
|
||||
providerUserId: true,
|
||||
providerUsername: true,
|
||||
avatar: true,
|
||||
createdAt: true
|
||||
}
|
||||
})
|
||||
|
||||
// 按用户分组剩余身份
|
||||
const identitiesByUser = new Map<number, typeof allIdentitiesForAffected>()
|
||||
for (const identity of allIdentitiesForAffected) {
|
||||
if (!targetKeys.has(`${identity.provider}:${identity.providerUserId}`)) {
|
||||
if (!identitiesByUser.has(identity.userId)) {
|
||||
identitiesByUser.set(identity.userId, [])
|
||||
}
|
||||
identitiesByUser.get(identity.userId)!.push(identity)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新需要重置头像的用户
|
||||
for (const userRecord of affectedUsers) {
|
||||
const isAvatarSourceDeleted =
|
||||
userRecord.avatarProvider &&
|
||||
(userRecord.avatarProvider === 'aggregate' ||
|
||||
userRecord.avatarProvider.startsWith('aggregate:') ||
|
||||
targetKeys.has(`${userRecord.avatarProvider}:${userRecord.avatarProviderUserId}`))
|
||||
|
||||
if (isAvatarSourceDeleted) {
|
||||
const remaining = identitiesByUser.get(userRecord.id) || []
|
||||
const fallback = resolveAvatarSource(userRecord, remaining)
|
||||
await tx
|
||||
.update(users)
|
||||
.set({
|
||||
avatarProvider: fallback?.provider ?? null,
|
||||
avatarProviderUserId: fallback?.providerUserId ?? null
|
||||
})
|
||||
.where(eq(users.id, userRecord.id))
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 批量删除所有聚合登录身份
|
||||
await tx
|
||||
.delete(userIdentities)
|
||||
.where(
|
||||
or(
|
||||
eq(userIdentities.provider, 'aggregate'),
|
||||
like(userIdentities.provider, 'aggregate:%')
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
count: targetIdentities.length,
|
||||
usersAffected: affectedUserIds.length
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
count: result.count,
|
||||
usersAffected: result.usersAffected
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { systemSettings } from '~/drizzle/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { SYSTEM_SETTINGS_DEFAULTS } from '../../../utils/system-settings-defaults'
|
||||
import { maskSystemSettingsSecrets } from './secretMask'
|
||||
import {
|
||||
getAggregateOAuthLoginTypesOrDefault,
|
||||
isSafeAggregateOAuthUrl
|
||||
} from '~~/server/utils/oauth-providers'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = event.context.user
|
||||
|
||||
if (!user) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: '未授权访问'
|
||||
})
|
||||
}
|
||||
|
||||
if (!['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '只有管理员才能导入环境配置'
|
||||
})
|
||||
}
|
||||
|
||||
const body = await readBody(event)
|
||||
const provider = typeof body.provider === 'string' ? body.provider : ''
|
||||
|
||||
const updateData: Record<string, any> = {}
|
||||
|
||||
if (provider === 'base') {
|
||||
if (!process.env.OAUTH_REDIRECT_URI && !process.env.OAUTH_STATE_SECRET) {
|
||||
throw createError({ statusCode: 400, message: '未检测到可导入的基础 OAuth 环境配置' })
|
||||
}
|
||||
if (process.env.OAUTH_REDIRECT_URI) {
|
||||
updateData.oauthRedirectUri = process.env.OAUTH_REDIRECT_URI
|
||||
}
|
||||
if (process.env.OAUTH_STATE_SECRET) {
|
||||
updateData.oauthStateSecret = process.env.OAUTH_STATE_SECRET
|
||||
}
|
||||
} else if (provider === 'github') {
|
||||
if (!process.env.GITHUB_CLIENT_ID || !process.env.GITHUB_CLIENT_SECRET) {
|
||||
throw createError({ statusCode: 400, message: '未检测到完整的 GitHub 环境配置' })
|
||||
}
|
||||
updateData.githubOAuthEnabled = true
|
||||
updateData.githubClientId = process.env.GITHUB_CLIENT_ID
|
||||
updateData.githubClientSecret = process.env.GITHUB_CLIENT_SECRET
|
||||
} else if (provider === 'casdoor') {
|
||||
if (
|
||||
!process.env.CASDOOR_ENDPOINT ||
|
||||
!process.env.CASDOOR_CLIENT_ID ||
|
||||
!process.env.CASDOOR_CLIENT_SECRET
|
||||
) {
|
||||
throw createError({ statusCode: 400, message: '未检测到完整的 Casdoor 环境配置' })
|
||||
}
|
||||
updateData.casdoorOAuthEnabled = true
|
||||
updateData.casdoorServerUrl = process.env.CASDOOR_ENDPOINT
|
||||
updateData.casdoorClientId = process.env.CASDOOR_CLIENT_ID
|
||||
updateData.casdoorClientSecret = process.env.CASDOOR_CLIENT_SECRET
|
||||
updateData.casdoorOrganizationName = process.env.CASDOOR_ORGANIZATION_NAME || 'built-in'
|
||||
} else if (provider === 'google') {
|
||||
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
|
||||
throw createError({ statusCode: 400, message: '未检测到完整的 Google 环境配置' })
|
||||
}
|
||||
updateData.googleOAuthEnabled = true
|
||||
updateData.googleClientId = process.env.GOOGLE_CLIENT_ID
|
||||
updateData.googleClientSecret = process.env.GOOGLE_CLIENT_SECRET
|
||||
} else if (provider === 'aggregate') {
|
||||
const appId = process.env.AGGREGATE_OAUTH_APP_ID?.trim()
|
||||
const appKey = process.env.AGGREGATE_OAUTH_APP_KEY?.trim()
|
||||
if (!appId || !appKey) {
|
||||
throw createError({ statusCode: 400, message: '未检测到完整的聚合登陆环境配置' })
|
||||
}
|
||||
const loginTypes = getAggregateOAuthLoginTypesOrDefault(process.env.AGGREGATE_OAUTH_LOGIN_TYPE)
|
||||
if (loginTypes.length === 0) {
|
||||
throw createError({ statusCode: 400, message: '未检测到受支持的聚合登录方式' })
|
||||
}
|
||||
const endpoint = process.env.AGGREGATE_OAUTH_ENDPOINT?.trim()
|
||||
if (!endpoint) {
|
||||
throw createError({ statusCode: 400, message: '未检测到 AGGREGATE_OAUTH_ENDPOINT 环境配置' })
|
||||
}
|
||||
if (!isSafeAggregateOAuthUrl(endpoint)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: 'AGGREGATE_OAUTH_ENDPOINT 公网地址必须使用 HTTPS,内网地址可使用 HTTP'
|
||||
})
|
||||
}
|
||||
updateData.aggregateOAuthEnabled = true
|
||||
updateData.aggregateOAuthAppId = appId
|
||||
updateData.aggregateOAuthAppKey = appKey
|
||||
updateData.aggregateOAuthLoginType = JSON.stringify(loginTypes)
|
||||
updateData.aggregateOAuthEndpoint = endpoint
|
||||
} else {
|
||||
throw createError({ statusCode: 400, message: '不支持的导入类型' })
|
||||
}
|
||||
|
||||
const settingsResult = await db.select().from(systemSettings).limit(1)
|
||||
let settings = settingsResult[0]
|
||||
|
||||
if (!settings) {
|
||||
const inserted = await db
|
||||
.insert(systemSettings)
|
||||
.values({ ...SYSTEM_SETTINGS_DEFAULTS, ...updateData })
|
||||
.returning()
|
||||
settings = inserted[0]
|
||||
} else {
|
||||
const updated = await db
|
||||
.update(systemSettings)
|
||||
.set(updateData)
|
||||
.where(eq(systemSettings.id, settings.id))
|
||||
.returning()
|
||||
settings = updated[0]
|
||||
}
|
||||
|
||||
return maskSystemSettingsSecrets(settings)
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
const hasEnvValue = (value?: string) => typeof value === 'string' && value.trim().length > 0
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
// 检查用户认证和权限
|
||||
const user = event.context.user
|
||||
|
||||
if (!user) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: '未授权访问'
|
||||
})
|
||||
}
|
||||
|
||||
if (!['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '只有管理员才能查看环境变量'
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
hasBaseConfig: !!(process.env.OAUTH_REDIRECT_URI || process.env.OAUTH_STATE_SECRET),
|
||||
hasGithubConfig: !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET),
|
||||
hasCasdoorConfig: !!(
|
||||
process.env.CASDOOR_ENDPOINT &&
|
||||
process.env.CASDOOR_CLIENT_ID &&
|
||||
process.env.CASDOOR_CLIENT_SECRET
|
||||
),
|
||||
hasGoogleConfig: !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET),
|
||||
hasAggregateConfig:
|
||||
hasEnvValue(process.env.AGGREGATE_OAUTH_APP_ID) &&
|
||||
hasEnvValue(process.env.AGGREGATE_OAUTH_APP_KEY)
|
||||
}
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,75 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { systemSettings } from '~/drizzle/schema'
|
||||
import { maskSystemSettingsSecrets } from './secretMask'
|
||||
import { MUSIC_SOURCE_PLATFORMS } from '~~/server/config/constants'
|
||||
import { SYSTEM_SETTINGS_DEFAULTS } from '~~/server/utils/system-settings-defaults'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// 检查用户认证和权限
|
||||
const user = event.context.user
|
||||
|
||||
if (!user) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: '未授权访问'
|
||||
})
|
||||
}
|
||||
|
||||
if (!['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '只有管理员才能查看系统设置'
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
// 系统设置包含敏感配置,始终从数据库读取。
|
||||
const settingsResult = await db.select().from(systemSettings).limit(1)
|
||||
let settings = settingsResult[0]
|
||||
|
||||
if (!settings) {
|
||||
const newSettingsResult = await db
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
...SYSTEM_SETTINGS_DEFAULTS,
|
||||
telemetryEnabled: true,
|
||||
enablePlayTimeSelection: false,
|
||||
siteTitle: process.env.NUXT_PUBLIC_SITE_TITLE || 'VoiceHub',
|
||||
siteLogoUrl: process.env.NUXT_PUBLIC_SITE_LOGO || '/favicon.ico',
|
||||
schoolLogoHomeUrl: null,
|
||||
schoolLogoPrintUrl: null,
|
||||
siteDescription:
|
||||
process.env.NUXT_PUBLIC_SITE_DESCRIPTION || '校园广播站点歌系统 - 让你的声音被听见',
|
||||
submissionGuidelines: '请遵守校园规定,提交健康向上的歌曲。',
|
||||
icpNumber: null,
|
||||
gonganNumber: null,
|
||||
showBeianIcon: false,
|
||||
enableSubmissionLimit: false,
|
||||
dailySubmissionLimit: null,
|
||||
weeklySubmissionLimit: null,
|
||||
monthlySubmissionLimit: null,
|
||||
showBlacklistKeywords: false,
|
||||
enableCollaborativeSubmission: true,
|
||||
enableSubmissionRemarks: false,
|
||||
enableSubmissionRestriction: false,
|
||||
submissionRestrictionScope: 'all',
|
||||
sameSongRestrictionHours: null,
|
||||
sameArtistRestrictionHours: null,
|
||||
enabledPlatforms: JSON.stringify([...MUSIC_SOURCE_PLATFORMS]),
|
||||
platformOrder: JSON.stringify([...MUSIC_SOURCE_PLATFORMS]),
|
||||
defaultTheme: 'System',
|
||||
enabledThemes: JSON.stringify(['System', 'ClassicDark', 'ClassicLight', 'ModernLight'])
|
||||
})
|
||||
.returning()
|
||||
settings = newSettingsResult[0]
|
||||
}
|
||||
|
||||
return maskSystemSettingsSecrets(settings)
|
||||
} catch (error) {
|
||||
console.error('获取系统设置失败:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: '获取系统设置失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
export const SMTP_PASSWORD_MASK = '****************'
|
||||
export const SECRET_FIELD_MASK = '••••••••••••••••'
|
||||
|
||||
const SECRET_FIELDS = [
|
||||
['smtpPassword', SMTP_PASSWORD_MASK],
|
||||
['oauthStateSecret', SECRET_FIELD_MASK],
|
||||
['githubClientSecret', SECRET_FIELD_MASK],
|
||||
['casdoorClientSecret', SECRET_FIELD_MASK],
|
||||
['googleClientSecret', SECRET_FIELD_MASK],
|
||||
['aggregateOAuthAppKey', SECRET_FIELD_MASK],
|
||||
['customOAuthClientSecret', SECRET_FIELD_MASK],
|
||||
['turnstileSecretKey', SECRET_FIELD_MASK]
|
||||
]
|
||||
|
||||
export const maskSystemSettingsSecrets = <T extends Record<string, any>>(settings: T): T => {
|
||||
if (!settings) return settings
|
||||
|
||||
return {
|
||||
...settings,
|
||||
smtpPassword: settings.smtpPassword ? SMTP_PASSWORD_MASK : settings.smtpPassword,
|
||||
oauthStateSecret: settings.oauthStateSecret ? SECRET_FIELD_MASK : settings.oauthStateSecret,
|
||||
githubClientSecret: settings.githubClientSecret ? SECRET_FIELD_MASK : settings.githubClientSecret,
|
||||
casdoorClientSecret: settings.casdoorClientSecret ? SECRET_FIELD_MASK : settings.casdoorClientSecret,
|
||||
googleClientSecret: settings.googleClientSecret ? SECRET_FIELD_MASK : settings.googleClientSecret,
|
||||
aggregateOAuthAppKey: settings.aggregateOAuthAppKey ? SECRET_FIELD_MASK : settings.aggregateOAuthAppKey,
|
||||
customOAuthClientSecret: settings.customOAuthClientSecret
|
||||
? SECRET_FIELD_MASK
|
||||
: settings.customOAuthClientSecret,
|
||||
turnstileSecretKey: settings.turnstileSecretKey ? SECRET_FIELD_MASK : settings.turnstileSecretKey
|
||||
}
|
||||
}
|
||||
|
||||
/** 从脱敏备份中移除掩码字段,避免把掩码写成真实密钥。 */
|
||||
export const omitMaskedSystemSettingsSecrets = <T extends Record<string, any>>(settings: T): T => {
|
||||
if (!settings) return settings
|
||||
|
||||
const result = { ...settings }
|
||||
for (const [field, mask] of SECRET_FIELDS) {
|
||||
if (result[field] === mask) delete result[field]
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user