初次提交
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s

This commit is contained in:
2026-09-06 08:19:02 +08:00
commit 4ce4649aaf
1061 changed files with 337258 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
import { apiKeyPermissions, apiKeys, db } from '~/drizzle/db'
import { eq } from 'drizzle-orm'
/**
* 删除API Key
* DELETE /api/admin/api-keys/[id]
*/
export default defineEventHandler(async (event) => {
// 检查用户权限 - 只有超级管理员可以管理API Key
const user = event.context.user
if (!user || user.role !== 'SUPER_ADMIN') {
throw createError({
statusCode: 403,
statusMessage: '只有超级管理员可以管理API Key'
})
}
const apiKeyId = getRouterParam(event, 'id')
if (!apiKeyId) {
throw createError({
statusCode: 400,
statusMessage: 'API Key ID不能为空'
})
}
try {
// 检查API Key是否存在
const existingApiKey = await db
.select({
id: apiKeys.id,
name: apiKeys.name
})
.from(apiKeys)
.where(eq(apiKeys.id, apiKeyId))
.limit(1)
if (existingApiKey.length === 0) {
throw createError({
statusCode: 404,
statusMessage: 'API Key不存在'
})
}
const apiKeyName = existingApiKey[0].name
// 开始事务删除
await db.transaction(async (tx) => {
// 删除API Key权限
await tx.delete(apiKeyPermissions).where(eq(apiKeyPermissions.apiKeyId, apiKeyId))
// 删除API访问日志(可选,根据业务需求决定是否保留历史日志)
// 这里我们选择保留日志记录,只删除API Key本身
// await tx.delete(apiLogs)
// .where(eq(apiLogs.apiKeyId, apiKeyId))
// 删除API Key
await tx.delete(apiKeys).where(eq(apiKeys.id, apiKeyId))
})
return {
success: true,
message: `API Key "${apiKeyName}" 删除成功`,
data: {
id: apiKeyId,
name: apiKeyName
}
}
} catch (error: any) {
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
statusMessage: `删除API Key失败: ${error.message}`
})
}
})
+92
View File
@@ -0,0 +1,92 @@
import { apiKeyPermissions, apiKeys, db, users } from '~/drizzle/db'
import { eq } from 'drizzle-orm'
/**
* 获取API Key详情
* GET /api/admin/api-keys/[id]
*/
export default defineEventHandler(async (event) => {
// 检查用户权限 - 只有超级管理员可以管理API Key
const user = event.context.user
if (!user || user.role !== 'SUPER_ADMIN') {
throw createError({
statusCode: 403,
statusMessage: '只有超级管理员可以管理API Key'
})
}
const apiKeyId = getRouterParam(event, 'id')
if (!apiKeyId) {
throw createError({
statusCode: 400,
statusMessage: 'API Key ID不能为空'
})
}
try {
// 获取API Key基本信息
const apiKeyResult = await db
.select({
id: apiKeys.id,
name: apiKeys.name,
description: apiKeys.description,
keyPrefix: apiKeys.keyPrefix,
isActive: apiKeys.isActive,
expiresAt: apiKeys.expiresAt,
usageCount: apiKeys.usageCount,
lastUsedAt: apiKeys.lastUsedAt,
createdAt: apiKeys.createdAt,
updatedAt: apiKeys.updatedAt,
createdBy: apiKeys.createdByUserId,
creatorName: users.name
})
.from(apiKeys)
.leftJoin(users, eq(apiKeys.createdByUserId, users.id))
.where(eq(apiKeys.id, apiKeyId))
.limit(1)
const apiKey = apiKeyResult[0]
if (!apiKey) {
throw createError({
statusCode: 404,
statusMessage: 'API Key不存在'
})
}
// 获取API Key权限
const permissionsResult = await db
.select({
permission: apiKeyPermissions.permission
})
.from(apiKeyPermissions)
.where(eq(apiKeyPermissions.apiKeyId, apiKeyId))
const permissions = permissionsResult.map((p) => p.permission)
// 计算状态
const isExpired = apiKey.expiresAt ? new Date() > apiKey.expiresAt : false
const status = !apiKey.isActive ? 'inactive' : isExpired ? 'expired' : 'active'
return {
success: true,
data: {
...apiKey,
permissions,
isExpired,
status
}
}
} catch (error: any) {
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
statusMessage: `获取API Key详情失败: ${error.message}`
})
}
})
+163
View File
@@ -0,0 +1,163 @@
import { apiKeyPermissions, apiKeys, db } from '~/drizzle/db'
import { eq } from 'drizzle-orm'
import { z } from 'zod'
import { getBeijingTime } from '~/utils/timeUtils'
/**
* 更新API Key
* PUT /api/admin/api-keys/[id]
*/
// 请求体验证schema
const updateApiKeySchema = z.object({
name: z
.string()
.min(1, 'API Key名称不能为空')
.max(100, 'API Key名称不能超过100个字符')
.optional(),
description: z.string().max(500, '描述不能超过500个字符').optional().nullable(),
isActive: z.boolean().optional(),
expiresAt: z.string().optional().nullable(),
permissions: z
.array(z.enum(['schedules:read', 'songs:read']))
.min(1, '至少需要选择一个权限')
.optional()
})
export default defineEventHandler(async (event) => {
// 检查用户权限 - 只有超级管理员可以管理API Key
const user = event.context.user
if (!user || user.role !== 'SUPER_ADMIN') {
throw createError({
statusCode: 403,
statusMessage: '只有超级管理员可以管理API Key'
})
}
const apiKeyId = getRouterParam(event, 'id')
if (!apiKeyId) {
throw createError({
statusCode: 400,
statusMessage: 'API Key ID不能为空'
})
}
try {
// 验证请求体
const body = await readBody(event)
const validatedData = updateApiKeySchema.parse(body)
// 检查API Key是否存在
const existingApiKey = await db
.select({ id: apiKeys.id })
.from(apiKeys)
.where(eq(apiKeys.id, apiKeyId))
.limit(1)
if (existingApiKey.length === 0) {
throw createError({
statusCode: 404,
statusMessage: 'API Key不存在'
})
}
// 处理过期时间
let expiresAt = undefined
if ('expiresAt' in validatedData) {
if (validatedData.expiresAt === null) {
expiresAt = null
} else if (validatedData.expiresAt) {
try {
// 处理预设选项格式 (3d, 7d, 30d, 60d, 90d)
if (validatedData.expiresAt.match(/^\d+d$/)) {
const days = parseInt(validatedData.expiresAt.replace('d', ''))
expiresAt = getBeijingTime()
expiresAt.setDate(expiresAt.getDate() + days)
} else {
// 处理传统的日期时间格式(向后兼容)
expiresAt = new Date(validatedData.expiresAt)
// 验证日期是否有效
if (isNaN(expiresAt.getTime())) {
throw createError({
statusCode: 400,
statusMessage: '无效的过期时间格式'
})
}
// 验证过期时间不能是过去的时间
if (expiresAt <= getBeijingTime()) {
throw createError({
statusCode: 400,
statusMessage: '过期时间不能是过去的时间'
})
}
}
} catch (error: any) {
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 400,
statusMessage: '无效的过期时间格式'
})
}
}
}
// 开始事务
const result = await db.transaction(async (tx) => {
// 准备更新数据
const updateData: any = {}
if (validatedData.name !== undefined) updateData.name = validatedData.name
if ('description' in validatedData) updateData.description = validatedData.description
if (validatedData.isActive !== undefined) updateData.isActive = validatedData.isActive
if ('expiresAt' in validatedData) updateData.expiresAt = expiresAt
// 更新API Key基本信息
await tx.update(apiKeys).set(updateData).where(eq(apiKeys.id, apiKeyId))
// 更新权限(如果提供了权限数据)
if (validatedData.permissions) {
// 删除现有权限
await tx.delete(apiKeyPermissions).where(eq(apiKeyPermissions.apiKeyId, apiKeyId))
// 插入新权限
const permissionValues = validatedData.permissions.map((permission) => ({
apiKeyId,
permission
}))
await tx.insert(apiKeyPermissions).values(permissionValues)
}
return { apiKeyId }
})
return {
success: true,
message: 'API Key更新成功',
data: {
id: result.apiKeyId
}
}
} catch (error: any) {
if (error.statusCode) {
throw error
}
// 处理Zod验证错误
if (error.name === 'ZodError') {
throw createError({
statusCode: 400,
statusMessage: `请求参数验证失败: ${error.errors.map((e: any) => e.message).join(', ')}`
})
}
throw createError({
statusCode: 500,
statusMessage: `更新API Key失败: ${error.message}`
})
}
})
+199
View File
@@ -0,0 +1,199 @@
import { apiKeyPermissions, apiKeys, db, users } from '~/drizzle/db'
import { and, count, desc, eq, inArray, sql } from 'drizzle-orm'
/**
* 获取API Key列表
* GET /api/admin/api-keys
*/
export default defineEventHandler(async (event) => {
// 检查用户权限 - 只有超级管理员可以管理API Key
const user = event.context.user
if (!user || user.role !== 'SUPER_ADMIN') {
throw createError({
statusCode: 403,
statusMessage: '只有超级管理员可以管理API Key'
})
}
const query = getQuery(event)
const page = parseInt(query.page as string) || 1
const limit = parseInt(query.limit as string) || 20
const search = (query.search as string) || ''
const status = query.status as string // 'active', 'inactive', 'expired'
const offset = (page - 1) * limit
try {
// 构建查询条件
const whereConditions = []
if (search) {
whereConditions.push(
sql`${apiKeys.name} ILIKE ${`%${search}%`} OR ${apiKeys.description} ILIKE ${`%${search}%`}`
)
}
if (status === 'active') {
whereConditions.push(
and(
eq(apiKeys.isActive, true),
sql`(${apiKeys.expiresAt} IS NULL OR ${apiKeys.expiresAt} > NOW())`
)
)
} else if (status === 'inactive') {
whereConditions.push(eq(apiKeys.isActive, false))
} else if (status === 'expired') {
whereConditions.push(
and(
eq(apiKeys.isActive, true),
sql`${apiKeys.expiresAt} IS NOT NULL AND ${apiKeys.expiresAt} <= NOW()`
)
)
}
const whereClause =
whereConditions.length > 0
? whereConditions.length === 1
? whereConditions[0]
: and(...whereConditions)
: undefined
// 获取API Key列表
const apiKeysList = await db
.select({
id: apiKeys.id,
name: apiKeys.name,
description: apiKeys.description,
keyPrefix: apiKeys.keyPrefix,
isActive: apiKeys.isActive,
expiresAt: apiKeys.expiresAt,
usageCount: apiKeys.usageCount,
lastUsedAt: apiKeys.lastUsedAt,
createdAt: apiKeys.createdAt,
updatedAt: apiKeys.updatedAt,
createdByUserId: apiKeys.createdByUserId,
creatorName: users.name
})
.from(apiKeys)
.leftJoin(users, eq(apiKeys.createdByUserId, users.id))
.where(whereClause)
.orderBy(desc(apiKeys.createdAt))
.limit(limit)
.offset(offset)
// 获取总数
const totalResult = await db.select({ count: count() }).from(apiKeys).where(whereClause)
const total = totalResult[0]?.count || 0
// 获取每个API Key的权限
const apiKeyIds = apiKeysList.map((key) => key.id).filter((id) => id != null)
let permissions = []
if (apiKeyIds.length > 0) {
try {
permissions = await db
.select({
apiKeyId: apiKeyPermissions.apiKeyId,
permission: apiKeyPermissions.permission
})
.from(apiKeyPermissions)
.where(inArray(apiKeyPermissions.apiKeyId, apiKeyIds))
// 确保permissions是数组
if (!Array.isArray(permissions)) {
permissions = []
}
} catch (permError) {
console.error('获取权限数据失败:', permError)
permissions = []
}
}
// 组织权限数据
const permissionsMap: Record<string, string[]> = {}
try {
if (permissions && Array.isArray(permissions) && permissions.length > 0) {
for (const perm of permissions) {
if (
perm &&
typeof perm === 'object' &&
perm.hasOwnProperty('apiKeyId') &&
perm.hasOwnProperty('permission') &&
perm.apiKeyId != null &&
perm.permission != null
) {
const apiKeyId = String(perm.apiKeyId)
const permission = String(perm.permission)
if (apiKeyId && permission) {
if (!permissionsMap[apiKeyId]) {
permissionsMap[apiKeyId] = []
}
permissionsMap[apiKeyId].push(permission)
}
}
}
}
} catch (mapError) {
console.error('组织权限数据失败:', mapError)
// permissionsMap已经初始化为空对象,继续执行
}
// 组合结果
let result = []
try {
if (Array.isArray(apiKeysList)) {
result = apiKeysList
.map((key) => {
// 确保key对象存在且有必要的属性
if (!key || typeof key !== 'object' || key.id == null) {
return null
}
try {
const keyId = String(key.id)
const expiresAt = key.expiresAt ? new Date(key.expiresAt) : null
const now = new Date()
return {
...key,
permissions: permissionsMap[keyId] || [],
isExpired: expiresAt ? now > expiresAt : false,
status: !key.isActive
? 'inactive'
: expiresAt && now > expiresAt
? 'expired'
: 'active'
}
} catch (keyError) {
console.error('处理API Key数据失败:', keyError, key)
return null
}
})
.filter(Boolean) // 过滤掉null值
}
} catch (resultError) {
console.error('组合结果失败:', resultError)
result = []
}
return {
success: true,
data: {
items: result,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
}
}
} catch (error: any) {
throw createError({
statusCode: 500,
statusMessage: `获取API Key列表失败: ${error.message}`
})
}
})
+161
View File
@@ -0,0 +1,161 @@
import { apiKeyPermissions, apiKeys, db } from '~/drizzle/db'
import crypto from 'crypto'
import { z } from 'zod'
import { getBeijingTime } from '~/utils/timeUtils'
/**
* 创建API Key
* POST /api/admin/api-keys
*/
// 请求体验证schema
const createApiKeySchema = z.object({
name: z.string().min(1, 'API Key名称不能为空').max(100, 'API Key名称不能超过100个字符'),
description: z
.union([z.string().max(500, '描述不能超过500个字符'), z.null(), z.undefined()])
.optional(),
expiresAt: z.union([z.string(), z.null(), z.undefined()]).optional(),
permissions: z.array(z.enum(['schedules:read', 'songs:read'])).min(1, '至少需要选择一个权限')
})
export default defineEventHandler(async (event) => {
// 检查用户权限 - 只有超级管理员可以管理API Key
const user = event.context.user
if (!user || user.role !== 'SUPER_ADMIN') {
throw createError({
statusCode: 403,
statusMessage: '只有超级管理员可以管理API Key'
})
}
try {
// 验证请求体
const body = await readBody(event)
const validatedData = createApiKeySchema.parse(body)
// 生成API Key
const apiKey = generateApiKey()
const keyPrefix = apiKey.substring(0, 10) // vhub_xxxxx
const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex')
// 处理过期时间
let expiresAt: Date | null = null
if (
validatedData.expiresAt &&
typeof validatedData.expiresAt === 'string' &&
validatedData.expiresAt.trim() !== ''
) {
try {
// 处理预设选项格式 (3d, 7d, 30d, 60d, 90d)
if (validatedData.expiresAt.match(/^\d+d$/)) {
const days = parseInt(validatedData.expiresAt.replace('d', ''))
expiresAt = getBeijingTime()
expiresAt.setDate(expiresAt.getDate() + days)
} else {
// 处理传统的日期时间格式(向后兼容)
expiresAt = new Date(validatedData.expiresAt)
// 验证日期是否有效
if (isNaN(expiresAt.getTime())) {
throw createError({
statusCode: 400,
statusMessage: '无效的过期时间格式'
})
}
// 验证过期时间不能是过去的时间
if (expiresAt <= getBeijingTime()) {
throw createError({
statusCode: 400,
statusMessage: '过期时间不能是过去的时间'
})
}
}
} catch (error: any) {
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 400,
statusMessage: '无效的过期时间格式'
})
}
}
// 开始事务
const result = await db.transaction(async (tx) => {
// 插入API Key记录
const apiKeyResult = await tx
.insert(apiKeys)
.values({
name: validatedData.name,
description: validatedData.description || null,
keyPrefix,
keyHash,
isActive: true,
expiresAt,
usageCount: 0,
createdByUserId: user.id
})
.returning({ id: apiKeys.id })
const apiKeyId = apiKeyResult[0].id
// 插入权限记录
const permissionValues = validatedData.permissions.map((permission) => ({
apiKeyId,
permission
}))
await tx.insert(apiKeyPermissions).values(permissionValues)
return {
id: apiKeyId,
apiKey, // 只在创建时返回完整的API Key
name: validatedData.name,
description: validatedData.description,
keyPrefix,
isActive: true,
expiresAt,
permissions: validatedData.permissions,
usageCount: 0,
createdBy: user.id,
creatorName: user.name
}
})
return {
success: true,
message: 'API Key创建成功',
data: result
}
} catch (error: any) {
if (error.statusCode) {
throw error
}
// 处理Zod验证错误
if (error.name === 'ZodError') {
throw createError({
statusCode: 400,
statusMessage: `请求参数验证失败: ${error.errors.map((e: any) => e.message).join(', ')}`
})
}
throw createError({
statusCode: 500,
statusMessage: `创建API Key失败: ${error.message}`
})
}
})
/**
* 生成API Key
* 格式: vhub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (37字符)
*/
function generateApiKey(): string {
const prefix = 'vhub_'
const randomBytes = crypto.randomBytes(16).toString('hex') // 32字符
return prefix + randomBytes
}
+71
View File
@@ -0,0 +1,71 @@
import { ApiLogService } from '~~/server/services/apiLogService'
/**
* 获取API访问日志
* GET /api/admin/api-keys/logs
*/
export default defineEventHandler(async (event) => {
// 检查用户权限 - 只有超级管理员可以查看API日志
const user = event.context.user
if (!user || user.role !== 'SUPER_ADMIN') {
throw createError({
statusCode: 403,
statusMessage: '只有超级管理员可以查看API日志'
})
}
const query = getQuery(event)
const page = parseInt(query.page as string) || 1
const limit = parseInt(query.limit as string) || 50
const apiKeyId = query.apiKeyId as string
const endpoint = query.endpoint as string
const method = query.method as string
const statusCode = query.statusCode ? parseInt(query.statusCode as string) : undefined
const startDate = query.startDate as string
const endDate = query.endDate as string
const ipAddress = query.ipAddress as string
const sortBy = (query.sortBy as string) || 'createdAt'
const sortOrder = (query.sortOrder as string) || 'desc'
const offset = (page - 1) * limit
try {
// 使用ApiLogService获取日志
const result = await ApiLogService.getLogs({
apiKeyId,
endpoint,
method: method?.toUpperCase(),
statusCode,
ipAddress,
startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined,
page,
limit,
sortBy,
sortOrder
})
// 获取统计信息
const stats = await ApiLogService.getStats({
apiKeyId,
endpoint,
method: method?.toUpperCase(),
statusCode,
ipAddress,
startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined
})
return {
logs: result.logs,
pagination: result.pagination,
stats: stats.basic
}
} catch (error) {
console.error('获取API访问日志失败:', error)
throw createError({
statusCode: 500,
statusMessage: '获取API访问日志失败'
})
}
})