feat: align with upstream 1.5.9.9

This commit is contained in:
2026-09-12 18:21:11 +08:00
commit 34f0679117
805 changed files with 376275 additions and 0 deletions
@@ -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
+75
View File
@@ -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
}