feat(songs): 歌曲管理支持批量驳回、投稿留言自动审查与排期范围可见性过滤
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 6m31s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Failing after 4s

- 新增 /api/admin/songs/batch-reject.post 批量驳回接口(事务清排期/投票/扣接纳数/黑名单去重/异步通知)
- requireSongAdmin 三角色校验 + createApiError 统一错误包装
- scheduleDaysBefore/After 四字段排期可见范围限制(开放 API 与公开排期接口三处缓存路径 + DB 条件过滤)
- submissionNoteAutoModeration 基于黑名单 KEYWORD/LANGUAGE/GENRE 自动判定留言 publicStatus,命中自动关闭 submissionNotePublic
- 更新 songs/request.post 与 songs/[id]/update.put 接入自动审查逻辑
This commit is contained in:
2026-09-06 17:03:20 +08:00
parent 43e2a05666
commit bd8df1f026
53 changed files with 10486 additions and 245 deletions
+68 -17
View File
@@ -61,6 +61,12 @@
>
<RotateCcw :size="14" class="text-blue-500/70" /> 未播放 ({{ selectedSongs.length }})
</button>
<button
class="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-amber-400 text-[11px] font-black rounded-lg border border-zinc-700/50 transition-all active:scale-95 uppercase tracking-widest"
@click="batchReject"
>
<X :size="14" class="text-amber-500/70" /> 驳回 ({{ selectedSongs.length }})
</button>
<button
class="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-red-400 text-[11px] font-black rounded-lg border border-zinc-700/50 transition-all active:scale-95 uppercase tracking-widest"
@click="batchDelete"
@@ -408,7 +414,7 @@
@click.stop
>
<div class="px-8 py-6 border-b border-zinc-800/50 flex items-center justify-between">
<h3 class="text-xl font-black text-zinc-100">驳回歌曲</h3>
<h3 class="text-xl font-black text-zinc-100">{{ isBatchReject ? `批量驳回歌曲 (${selectedSongs.length} 首)` : '驳回歌曲' }}</h3>
<button
class="text-zinc-500 hover:text-zinc-300 transition-colors"
@click="cancelReject"
@@ -418,7 +424,7 @@
</div>
<div class="p-8 space-y-6">
<div class="p-5 bg-zinc-950 border border-zinc-800 rounded-lg flex items-center gap-4">
<div v-if="!isBatchReject" class="p-5 bg-zinc-950 border border-zinc-800 rounded-lg flex items-center gap-4">
<div
class="w-10 h-10 rounded-lg bg-red-500/10 text-red-500 flex items-center justify-center border border-red-500/10"
>
@@ -429,6 +435,17 @@
<p class="text-xs text-zinc-500">投稿人: {{ rejectSongInfo.requester }}</p>
</div>
</div>
<div v-else class="p-5 bg-zinc-950 border border-zinc-800 rounded-lg flex items-center gap-4">
<div
class="w-10 h-10 rounded-lg bg-amber-500/10 text-amber-500 flex items-center justify-center border border-amber-500/10"
>
<ListMusic :size="18" />
</div>
<div>
<h4 class="font-bold text-zinc-100 text-sm">即将批量驳回 {{ selectedSongs.length }} 首歌曲</h4>
<p class="text-xs text-zinc-500">驳回后将一并清理投票、排期记录并发送通知给对应投稿人</p>
</div>
</div>
<div class="space-y-2">
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
@@ -961,6 +978,7 @@ import {
X,
Trash2,
Music,
ListMusic,
Heart,
Download,
ChevronDown,
@@ -1040,6 +1058,7 @@ const showRejectDialog = ref(false)
const rejectLoading = ref(false)
const rejectReason = ref('')
const addToBlacklist = ref(false)
const isBatchReject = ref(false)
const rejectSongInfo = ref({
id: null,
title: '',
@@ -1475,6 +1494,7 @@ const rejectSong = (songId) => {
const song = songs.value.find((s) => s.id === songId)
if (!song) return
isBatchReject.value = false
rejectSongInfo.value = {
id: song.id,
title: song.title || '',
@@ -1487,6 +1507,22 @@ const rejectSong = (songId) => {
showRejectDialog.value = true
}
// 批量驳回歌曲
const batchReject = () => {
if (selectedSongs.value.length === 0) return
isBatchReject.value = true
rejectSongInfo.value = {
id: null,
title: '',
artist: '',
requester: ''
}
rejectReason.value = ''
addToBlacklist.value = false
showRejectDialog.value = true
}
// 确认驳回
const confirmReject = async () => {
if (!rejectReason.value.trim()) {
@@ -1496,26 +1532,40 @@ const confirmReject = async () => {
rejectLoading.value = true
try {
await $fetch('/api/admin/songs/reject', {
method: 'POST',
body: {
songId: rejectSongInfo.value.id,
reason: rejectReason.value.trim(),
addToBlacklist: addToBlacklist.value
if (isBatchReject.value) {
await $fetch('/api/admin/songs/batch-reject', {
method: 'POST',
body: {
songIds: selectedSongs.value,
reason: rejectReason.value.trim(),
addToBlacklist: addToBlacklist.value
}
})
await refreshSongs(true)
selectedSongs.value = []
showNotification(`批量驳回成功,共处理 ${selectedSongs.value.length} 首歌曲`, 'success')
} else {
await $fetch('/api/admin/songs/reject', {
method: 'POST',
body: {
songId: rejectSongInfo.value.id,
reason: rejectReason.value.trim(),
addToBlacklist: addToBlacklist.value
}
})
await refreshSongs(true)
const index = selectedSongs.value.indexOf(rejectSongInfo.value.id)
if (index > -1) {
selectedSongs.value.splice(index, 1)
}
})
await refreshSongs(true)
const index = selectedSongs.value.indexOf(rejectSongInfo.value.id)
if (index > -1) {
selectedSongs.value.splice(index, 1)
showNotification('歌曲驳回成功,已通知投稿人', 'success')
}
showRejectDialog.value = false
showNotification('歌曲驳回成功,已通知投稿人', 'success')
} catch (error) {
} catch (error: any) {
console.error('驳回歌曲失败:', error)
showNotification('驳回失败: ' + (error.data?.message || error.message), 'error')
} finally {
@@ -1528,6 +1578,7 @@ const cancelReject = () => {
showRejectDialog.value = false
rejectReason.value = ''
addToBlacklist.value = false
isBatchReject.value = false
rejectSongInfo.value = {
id: null,
title: '',
+11 -2
View File
@@ -26,7 +26,17 @@
<AuthProvidersGoogleIcon />
</button>
<!-- 其他提供商可在此添加 -->
<button
v-if="config.public.oauth.aggregate"
type="button"
class="w-12 h-12 rounded-full border border-[var(--border-color)] bg-[var(--bg-secondary)] text-[var(--text-primary)] flex items-center justify-center cursor-pointer transition-all duration-200 hover:-translate-y-0.5 hover:shadow-sm hover:bg-[var(--bg-tertiary)] hover:bg-[#12B7F5] hover:text-white hover:border-[#12B7F5]"
title="使用 QQ / 微信登录"
@click="loginWith('aggregate')"
>
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true">
<path d="M8.5 4C4.9 4 2 6.8 2 10.3c0 2 1.1 3.7 2.8 4.9L4 18l3-1.5c.5.1 1 .2 1.5.2h.6c-.1-.4-.1-.8 0-1.2H8.5C5.5 15.5 3 13.2 3 10.3 3 7.4 5.5 5 8.5 5h1.6c.3-1.1 1.3-1.9 2.5-1.9.5 0 1 .2 1.4.5C13.3 3 10.7 4 8.5 4zM10.1 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm8.4 1.2c-2.5 0-4.5 2-4.5 4.5s2 4.5 4.5 4.5c.6 0 1.2-.1 1.7-.3L22 17.5l-.8-2.3c1.4-1 2.3-2.6 2.3-4.3C23.5 9.2 21.5 7.2 19 7.2c-.5 0-1 0-1.4.1C16.9 7.8 17.9 7.2 19 7.2zM17 9.7c-.8 0-1.5.7-1.5 1.5s.7 1.5 1.5 1.5 1.5-.7 1.5-1.5-.7-1.5-1.5-1.5z"/>
</svg>
</button>
</div>
</div>
</template>
@@ -39,7 +49,6 @@ const hasEnabledProviders = computed(() => {
})
const loginWith = (provider: string) => {
// 外部导航到 API 端点
navigateTo(`/api/auth/${provider}`, { external: true })
}
</script>
+34 -2
View File
@@ -19,7 +19,9 @@ const siteConfig = ref({
siteVersion: '',
submissionGuidelines: '',
icpNumber: '',
gonganNumber: ''
gonganNumber: '',
defaultTheme: null,
enabledThemes: null
})
const isLoaded = ref(false)
@@ -57,7 +59,9 @@ export const useSiteConfig = () => {
gonganNumber: '',
enableReplayRequests: false,
enableCollaborativeSubmission: true,
enableSubmissionRemarks: false
enableSubmissionRemarks: false,
defaultTheme: 'System',
enabledThemes: '["System","ClassicDark","ClassicLight","ModernLight"]'
}
isLoaded.value = true
} finally {
@@ -96,6 +100,32 @@ export const useSiteConfig = () => {
const enableSubmissionRemarks = computed(() => siteConfig.value.enableSubmissionRemarks === true)
const smtpEnabled = computed(() => !!siteConfig.value.smtpEnabled)
function parseEnabledThemes(raw) {
if (Array.isArray(raw)) return raw.filter(Boolean)
if (typeof raw === 'string') {
const s = raw.trim()
if (!s) return []
if (s.startsWith('[') || s.startsWith('{')) {
try {
const parsed = JSON.parse(s)
if (Array.isArray(parsed)) return parsed.filter(Boolean)
if (parsed && Array.isArray(parsed.items)) return parsed.items.filter(Boolean)
} catch (_) {
// fallback to comma split
}
}
return s.split(',').map((x) => x.trim()).filter(Boolean)
}
return []
}
const defaultTheme = computed(() =>
typeof siteConfig.value.defaultTheme === 'string' && siteConfig.value.defaultTheme
? siteConfig.value.defaultTheme
: null
)
const enabledThemes = computed(() => parseEnabledThemes(siteConfig.value.enabledThemes))
// 初始化配置(仅在客户端执行)
const initSiteConfig = async () => {
if (typeof window !== 'undefined' && !isLoaded.value) {
@@ -126,6 +156,8 @@ export const useSiteConfig = () => {
enableCollaborativeSubmission,
enableSubmissionRemarks,
smtpEnabled,
defaultTheme,
enabledThemes,
fetchSiteConfig,
initSiteConfig,
refreshSiteConfig
+112 -44
View File
@@ -1,4 +1,4 @@
import { computed, ref, watch } from 'vue'
import { computed, ref, watch, nextTick } from 'vue'
const STORAGE_KEY = 'voicehub:theme'
type ThemeName = string & {}
@@ -15,6 +15,25 @@ function getMetaThemeColor() {
return document.querySelector<HTMLMetaElement>('meta[name="theme-color"]')
}
function parseEnabledThemesRaw(raw: unknown): string[] {
if (Array.isArray(raw)) return raw.filter(Boolean) as string[]
if (typeof raw === 'string') {
const s = raw.trim()
if (!s) return []
if (s.startsWith('[') || s.startsWith('{')) {
try {
const parsed = JSON.parse(s)
if (Array.isArray(parsed)) return parsed.filter(Boolean) as string[]
if (parsed && Array.isArray((parsed as any).items)) return (parsed as any).items.filter(Boolean) as string[]
} catch (_) {
// fallback
}
}
return s.split(',').map((x) => x.trim()).filter(Boolean)
}
return []
}
function applyTheme(theme: string) {
if (!isBrowser()) return
@@ -24,11 +43,12 @@ function applyTheme(theme: string) {
root.setAttribute('data-theme', theme)
if (appRoot) appRoot.setAttribute('data-theme', theme)
if (theme === 'dark') {
const effective = resolveEffectiveTheme(theme)
if (effective === 'dark') {
root.classList.add('dark')
root.classList.remove('light')
root.style.colorScheme = 'dark'
} else if (theme === 'light') {
} else if (effective === 'light') {
root.classList.add('light')
root.classList.remove('dark')
root.style.colorScheme = 'light'
@@ -39,11 +59,7 @@ function applyTheme(theme: string) {
const meta = getMetaThemeColor()
if (meta) {
if (theme === 'light') {
meta.content = '#f8fafc'
} else {
meta.content = '#111111'
}
meta.content = effective === 'light' ? '#f8fafc' : '#111111'
}
try {
@@ -59,18 +75,37 @@ function detectSystemTheme(): 'dark' | 'light' {
return prefersDark ? 'dark' : 'light'
}
function resolveEffectiveTheme(theme: string): 'dark' | 'light' | string {
if (theme === 'System' || theme === 'system') {
return detectSystemTheme()
}
if (theme === 'dark' || theme.endsWith('Dark')) return 'dark'
if (theme === 'light' || theme.endsWith('Light')) return 'light'
return theme
}
export function useTheme() {
const config = useRuntimeConfig()
const { isLoaded: siteConfigLoaded, defaultTheme: siteDefaultTheme, enabledThemes: siteEnabledThemes, initSiteConfig } = useSiteConfig()
const enabledThemes = computed<string[]>(() => {
const list = (config.public?.enabledThemes as unknown) as string[] | undefined
return Array.isArray(list) && list.length > 0 ? list : ['dark', 'light']
if (siteConfigLoaded.value && siteEnabledThemes.value.length > 0) {
return [...new Set(siteEnabledThemes.value)]
}
const fromRuntime = parseEnabledThemesRaw(config.public?.enabledThemes)
if (fromRuntime.length > 0) return fromRuntime
return ['System', 'dark', 'light']
})
const defaultTheme = computed<string>(() => {
const value = config.public?.defaultTheme as string | undefined
if (value && enabledThemes.value.includes(value)) return value
return enabledThemes.value.includes('dark') ? 'dark' : enabledThemes.value[0] || 'dark'
if (siteConfigLoaded.value && siteDefaultTheme.value) {
return siteDefaultTheme.value
}
const fromRuntime = config.public?.defaultTheme
if (typeof fromRuntime === 'string' && fromRuntime) {
return fromRuntime
}
return 'System'
})
const theme = computed<string>({
@@ -79,21 +114,22 @@ export function useTheme() {
})
const isDark = computed(() => {
const t = theme.value
if (t === 'dark') return true
if (t === 'light') return false
return detectSystemTheme() === 'dark'
return resolveEffectiveTheme(theme.value) === 'dark'
})
function isThemeEnabled(name: string) {
return enabledThemes.value.includes(name)
}
function firstValidTheme(candidate: string): string {
const list = enabledThemes.value
if (list.includes(candidate)) return candidate
if (list.includes(defaultTheme.value)) return defaultTheme.value
return list[0] ?? 'System'
}
function setTheme(next: string, { persist = true } = {}) {
let candidate = next
if (!enabledThemes.value.includes(candidate)) {
candidate = defaultTheme.value
}
const candidate = firstValidTheme(next)
if (currentTheme.value === candidate && initialized.value) return
@@ -110,7 +146,7 @@ export function useTheme() {
}
function toggleTheme() {
const order = enabledThemes.value.length >= 2 ? enabledThemes.value : ['dark', 'light']
const order = enabledThemes.value.length >= 2 ? enabledThemes.value : ['System', 'dark', 'light']
const current = theme.value
const idx = order.indexOf(current)
const next = order[(idx + 1) % order.length]
@@ -121,8 +157,31 @@ export function useTheme() {
setTheme(defaultTheme.value, { persist: false })
}
function init() {
function onSystemThemeChange() {
const stored = (() => {
try {
return localStorage.getItem(STORAGE_KEY)
} catch {
return null
}
})()
if (!stored) {
applyTheme(theme.value)
}
}
async function init() {
if (initialized.value || !isBrowser()) return
if (!siteConfigLoaded.value) {
try {
await initSiteConfig()
await nextTick()
} catch (_) {
/* ignore */
}
}
initialized.value = true
let stored: string | null = null
@@ -132,34 +191,43 @@ export function useTheme() {
stored = null
}
if (stored && enabledThemes.value.includes(stored)) {
setTheme(stored)
return
if (stored) {
const valid = firstValidTheme(stored)
if (valid === stored || enabledThemes.value.includes(stored)) {
setTheme(stored)
watchSystemTheme()
return
}
}
setTheme(defaultTheme.value, { persist: false })
watchSystemTheme()
}
if (window.matchMedia) {
const mql = window.matchMedia('(prefers-color-scheme: dark)')
const onChange = () => {
let s: string | null = null
try {
s = localStorage.getItem(STORAGE_KEY)
} catch {
s = null
}
if (!s) {
setTheme(defaultTheme.value, { persist: false })
}
}
if (typeof mql.addEventListener === 'function') {
mql.addEventListener('change', onChange)
} else if (typeof mql.addListener === 'function') {
mql.addListener(onChange)
}
function watchSystemTheme() {
if (!isBrowser() || !window.matchMedia) return
const mql = window.matchMedia('(prefers-color-scheme: dark)')
if (typeof mql.addEventListener === 'function') {
mql.addEventListener('change', onSystemThemeChange)
} else if (typeof (mql as any).addListener === 'function') {
;(mql as any).addListener(onSystemThemeChange)
}
}
watch(defaultTheme, () => {
if (!initialized.value) return
const stored = (() => {
try {
return localStorage.getItem(STORAGE_KEY)
} catch {
return null
}
})()
if (!stored) {
setTheme(defaultTheme.value, { persist: false })
}
})
return {
theme,
currentTheme,
@@ -0,0 +1,318 @@
CREATE TYPE "public"."card_code_status" AS ENUM('AVAILABLE', 'LOCKED', 'REDEEMED', 'INVALID');--> statement-breakpoint
ALTER TYPE "public"."BlacklistType" ADD VALUE 'LANGUAGE';--> statement-breakpoint
ALTER TYPE "public"."BlacklistType" ADD VALUE 'GENRE';--> statement-breakpoint
ALTER TYPE "public"."user_status" ADD VALUE 'pending';--> statement-breakpoint
ALTER TYPE "public"."user_status" ADD VALUE 'rejected';--> statement-breakpoint
CREATE TABLE "auth_sessions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" integer NOT NULL,
"session_token" varchar(255) NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"ip_address" text,
"user_agent" text,
"last_active_at" timestamp with time zone DEFAULT now() NOT NULL,
"is_revoked" boolean DEFAULT false NOT NULL,
"browser" text,
"operating_system" text,
"device" text,
"login_method" text DEFAULT 'password' NOT NULL,
"revoked_at" timestamp with time zone,
"revoked_reason" text,
CONSTRAINT "auth_sessions_session_token_unique" UNIQUE("session_token")
);
--> statement-breakpoint
CREATE TABLE "backup_records" (
"id" serial PRIMARY KEY NOT NULL,
"filename" text NOT NULL,
"file_path" text NOT NULL,
"file_size" bigint NOT NULL,
"type" text DEFAULT 'MANUAL' NOT NULL,
"status" text DEFAULT 'COMPLETED' NOT NULL,
"error_message" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"started_at" timestamp with time zone,
"completed_at" timestamp with time zone,
"created_by" integer,
"expires_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "card_code_redemptions" (
"id" serial PRIMARY KEY NOT NULL,
"card_code_id" integer NOT NULL,
"user_id" integer NOT NULL,
"redeemed_at" timestamp with time zone DEFAULT now() NOT NULL,
"song_id" integer,
"purpose" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "card_codes" (
"id" serial PRIMARY KEY NOT NULL,
"code" varchar(64) NOT NULL,
"status" "card_code_status" DEFAULT 'AVAILABLE' NOT NULL,
"type" text NOT NULL,
"value" integer DEFAULT 1 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"expires_at" timestamp with time zone,
"locked_at" timestamp with time zone,
"locked_by" integer,
"batch_id" varchar(64),
"note" text,
CONSTRAINT "card_codes_code_unique" UNIQUE("code")
);
--> statement-breakpoint
CREATE TABLE "ChangelogEntry" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"version" text NOT NULL,
"title" text NOT NULL,
"content" text NOT NULL,
"published" boolean DEFAULT false NOT NULL,
"publishedAt" timestamp,
"createdBy" integer
);
--> statement-breakpoint
CREATE TABLE "ExpectedPlayDate" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"date" text NOT NULL,
"availableSequences" text NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
CONSTRAINT "ExpectedPlayDate_date_unique" UNIQUE("date")
);
--> statement-breakpoint
CREATE TABLE "GradeClass" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"grade" text NOT NULL,
"class" text NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
CONSTRAINT "grade_class_unique" UNIQUE("grade","class")
);
--> statement-breakpoint
CREATE TABLE "oauth_providers" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"displayName" text,
"enabled" boolean DEFAULT true NOT NULL,
"clientId" text,
"clientSecret" text,
"authorizationEndpoint" text,
"tokenEndpoint" text,
"userInfoEndpoint" text,
"scope" text,
"redirectUri" text,
"usernameClaim" text DEFAULT 'preferred_username',
"nameClaim" text DEFAULT 'name',
"emailClaim" text DEFAULT 'email',
"avatarClaim" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"sortOrder" integer DEFAULT 0 NOT NULL,
"icon" text
);
--> statement-breakpoint
CREATE TABLE "passkey_challenges" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"challenge" text NOT NULL,
"type" text NOT NULL,
"user_id" integer,
"expires_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "schedule_song_pool" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"song_id" integer NOT NULL,
"target_play_date" timestamp,
"priority" integer DEFAULT 0 NOT NULL,
"added_by" integer,
"note" text,
"status" text DEFAULT 'PENDING' NOT NULL
);
--> statement-breakpoint
CREATE TABLE "SongComment" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"songId" integer NOT NULL,
"userId" integer NOT NULL,
"content" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "submission_restriction_exemptions" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"exemption_type" text NOT NULL,
"reason" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"expires_at" timestamp with time zone,
"created_by" integer,
CONSTRAINT "submission_restriction_exemptions_user_type_unique" UNIQUE("user_id","exemption_type")
);
--> statement-breakpoint
CREATE TABLE "user_passkeys" (
"id" text PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"name" text,
"public_key" text NOT NULL,
"credential_device_type" text NOT NULL,
"credential_backed_up" boolean NOT NULL,
"transports" text,
"counter" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"last_used_at" timestamp with time zone
);
--> statement-breakpoint
ALTER TABLE "User" ALTER COLUMN "forcePasswordChange" SET DEFAULT false;--> statement-breakpoint
ALTER TABLE "Notification" ADD COLUMN "batch_id" uuid;--> statement-breakpoint
ALTER TABLE "Notification" ADD COLUMN "source" text DEFAULT 'SYSTEM' NOT NULL;--> statement-breakpoint
ALTER TABLE "Notification" ADD COLUMN "sender_id" integer;--> statement-breakpoint
ALTER TABLE "Notification" ADD COLUMN "sender_name" text;--> statement-breakpoint
ALTER TABLE "Notification" ADD COLUMN "sender_username" text;--> statement-breakpoint
ALTER TABLE "Notification" ADD COLUMN "title" text;--> statement-breakpoint
ALTER TABLE "Notification" ADD COLUMN "important" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "Notification" ADD COLUMN "user_deleted" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "Schedule" ADD COLUMN "replay_request_id" integer;--> statement-breakpoint
ALTER TABLE "Song" ADD COLUMN "expectedPlayDate" text;--> statement-breakpoint
ALTER TABLE "Song" ADD COLUMN "expectedPlaySequence" integer;--> statement-breakpoint
ALTER TABLE "Song" ADD COLUMN "durationSeconds" integer;--> statement-breakpoint
ALTER TABLE "Song" ADD COLUMN "submissionNotePublicStatus" text;--> statement-breakpoint
ALTER TABLE "Song" ADD COLUMN "cardCodeId" integer;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "siteVersion" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "submissionNoteRequiresApproval" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "instanceId" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "telemetryEnabled" boolean DEFAULT true NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "showBeianIcon" boolean DEFAULT true NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "scheduleDaysDisplay" integer DEFAULT 7 NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "scheduleDaysEditable" integer DEFAULT 3 NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "scheduleDaysRequestable" integer DEFAULT 14 NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "scheduleDaysBeforeEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "scheduleDaysBefore" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "scheduleDaysAfterEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "scheduleDaysAfter" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "forcePasswordChangeOnFirstLogin" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "enableSubmissionRestriction" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "submissionRestrictionScope" text DEFAULT 'all' NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "sameSongRestrictionHours" integer;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "sameArtistRestrictionHours" integer;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "cardCodeEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "cardCodeSubmissionRequired" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "cardCodeVoteEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "cardCodeReplayEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "enableCardCodeRequests" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "requireCardCodeForRequests" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "enableCardCodeLimitBypass" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "submissionRestrictionEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "submissionRestrictionGradeEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "submissionRestrictionClassEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "submissionRestrictionGraduateEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "submissionRestrictionWithdrawnEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "captchaProvider" text DEFAULT 'TURNSTILE';--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "captchaEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "turnstileSiteKey" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "turnstileSecretKey" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "allowRegister" boolean DEFAULT true NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "registerRequireEmail" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "registerRequireEmailVerify" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "registerDefaultRole" text DEFAULT 'USER';--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "registerEnabledGradeClasses" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "registerRequiresApproval" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "oauthRegisterRequiresApproval" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "registerEmailRequired" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "registerRequiresGradeClass" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "oauthEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "oauthAllowRegister" boolean DEFAULT true NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "oauthAllowLogin" boolean DEFAULT true NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "oauthAllowBind" boolean DEFAULT true NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "allowOAuthRegistration" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "oauthRedirectUri" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "oauthStateSecret" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "oauthProviders" text DEFAULT '[]';--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "githubOAuthEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "githubClientId" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "githubClientSecret" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "casdoorOAuthEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "casdoorServerUrl" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "casdoorClientId" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "casdoorClientSecret" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "casdoorOrganizationName" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "googleOAuthEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "googleClientId" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "googleClientSecret" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "aggregateOAuthEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "aggregateOAuthAppId" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "aggregateOAuthAppKey" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "aggregateOAuthLoginType" text DEFAULT 'qq';--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "aggregateOAuthEndpoint" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthDisplayName" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthAuthorizeUrl" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthTokenUrl" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthUserInfoUrl" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthScope" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthClientId" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthClientSecret" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthUserIdField" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthUsernameField" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthNameField" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthEmailField" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthAvatarField" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "captchaMaxFailures" integer DEFAULT 3 NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "autoBackupEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "autoBackupIntervalHours" integer DEFAULT 24 NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "autoBackupRetentionDays" integer DEFAULT 30 NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "autoBackupConfig" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "statisticsCode" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "statisticsCodeEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "defaultTheme" text DEFAULT 'System' NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "enabledThemes" text DEFAULT '["System","ClassicDark","ClassicLight","ModernLight"]' NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "enabledPlatforms" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "platformOrder" text;--> statement-breakpoint
ALTER TABLE "User" ADD COLUMN "studentId" text;--> statement-breakpoint
ALTER TABLE "User" ADD COLUMN "tokenVersion" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE "User" ADD COLUMN "avatarProvider" text;--> statement-breakpoint
ALTER TABLE "User" ADD COLUMN "avatarProviderUserId" text;--> statement-breakpoint
ALTER TABLE "User" ADD COLUMN "remark" text;--> statement-breakpoint
ALTER TABLE "auth_sessions" ADD CONSTRAINT "auth_sessions_user_id_User_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "card_code_redemptions" ADD CONSTRAINT "card_code_redemptions_card_code_id_card_codes_id_fk" FOREIGN KEY ("card_code_id") REFERENCES "public"."card_codes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "card_code_redemptions" ADD CONSTRAINT "card_code_redemptions_user_id_User_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "card_code_redemptions" ADD CONSTRAINT "card_code_redemptions_song_id_Song_id_fk" FOREIGN KEY ("song_id") REFERENCES "public"."Song"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "schedule_song_pool" ADD CONSTRAINT "schedule_song_pool_song_id_Song_id_fk" FOREIGN KEY ("song_id") REFERENCES "public"."Song"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "submission_restriction_exemptions" ADD CONSTRAINT "submission_restriction_exemptions_user_id_User_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_passkeys" ADD CONSTRAINT "user_passkeys_user_id_User_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "auth_sessions_user_active_idx" ON "auth_sessions" USING btree ("user_id","is_revoked","expires_at");--> statement-breakpoint
CREATE INDEX "auth_sessions_expires_at_idx" ON "auth_sessions" USING btree ("expires_at");--> statement-breakpoint
CREATE INDEX "auth_sessions_last_active_idx" ON "auth_sessions" USING btree ("last_active_at");--> statement-breakpoint
CREATE UNIQUE INDEX "auth_sessions_session_token_unique" ON "auth_sessions" USING btree ("session_token");--> statement-breakpoint
CREATE INDEX "backup_records_created_at_idx" ON "backup_records" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "backup_records_status_idx" ON "backup_records" USING btree ("status");--> statement-breakpoint
CREATE INDEX "card_code_redemptions_user_id_idx" ON "card_code_redemptions" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "card_code_redemptions_card_code_id_idx" ON "card_code_redemptions" USING btree ("card_code_id");--> statement-breakpoint
CREATE INDEX "card_codes_status_idx" ON "card_codes" USING btree ("status");--> statement-breakpoint
CREATE INDEX "card_codes_batch_id_idx" ON "card_codes" USING btree ("batch_id");--> statement-breakpoint
CREATE INDEX "card_codes_expires_at_idx" ON "card_codes" USING btree ("expires_at");--> statement-breakpoint
CREATE INDEX "passkey_challenges_challenge_idx" ON "passkey_challenges" USING btree ("challenge");--> statement-breakpoint
CREATE INDEX "passkey_challenges_expires_at_idx" ON "passkey_challenges" USING btree ("expires_at");--> statement-breakpoint
CREATE INDEX "schedule_song_pool_song_id_idx" ON "schedule_song_pool" USING btree ("song_id");--> statement-breakpoint
CREATE INDEX "schedule_song_pool_target_date_idx" ON "schedule_song_pool" USING btree ("target_play_date");--> statement-breakpoint
CREATE INDEX "submission_restriction_exemptions_user_id_idx" ON "submission_restriction_exemptions" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "user_passkeys_user_id_idx" ON "user_passkeys" USING btree ("user_id");--> statement-breakpoint
ALTER TABLE "Song" ADD CONSTRAINT "Song_cardCodeId_card_codes_id_fk" FOREIGN KEY ("cardCodeId") REFERENCES "public"."card_codes"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "notification_user_important_read_created_idx" ON "Notification" USING btree ("userId","important","read","createdAt");--> statement-breakpoint
CREATE INDEX "notification_batch_id_idx" ON "Notification" USING btree ("batch_id");--> statement-breakpoint
CREATE INDEX "schedule_published_song_idx" ON "Schedule" USING btree ("publishedAt","songId");--> statement-breakpoint
CREATE INDEX "schedule_published_date_idx" ON "Schedule" USING btree ("publishedAt","playDate");--> statement-breakpoint
CREATE INDEX "song_card_code_id_idx" ON "Song" USING btree ("cardCodeId");--> statement-breakpoint
CREATE INDEX "song_semester_created_at_idx" ON "Song" USING btree ("semester","createdAt");--> statement-breakpoint
CREATE INDEX "song_requester_id_idx" ON "Song" USING btree ("requesterId");--> statement-breakpoint
CREATE UNIQUE INDEX "User_username_unique" ON "User" USING btree ("username");--> statement-breakpoint
CREATE INDEX "vote_user_song_idx" ON "Vote" USING btree ("userId","songId");--> statement-breakpoint
ALTER TABLE "Vote" ADD CONSTRAINT "vote_song_user_unique" UNIQUE("songId","userId");
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -141,6 +141,13 @@
"when": 1776518400000,
"tag": "20260418112000_add_song_comments",
"breakpoints": true
},
{
"idx": 20,
"version": "7",
"when": 1788684403309,
"tag": "20260906084643_auto-migration",
"breakpoints": true
}
]
}
}
+63 -3
View File
@@ -51,9 +51,16 @@ export const authSessions = pgTable('auth_sessions', {
userAgent: text('user_agent'),
lastActiveAt: timestamp('last_active_at', { withTimezone: true }).defaultNow().notNull(),
isRevoked: boolean('is_revoked').default(false).notNull(),
browser: text('browser'),
operatingSystem: text('operating_system'),
device: text('device'),
loginMethod: text('login_method').default('password').notNull(),
revokedAt: timestamp('revoked_at', { withTimezone: true }),
revokedReason: text('revoked_reason')
}, (table) => [
index('auth_sessions_user_id_idx').on(table.userId),
index('auth_sessions_user_active_idx').on(table.userId, table.isRevoked, table.expiresAt),
index('auth_sessions_expires_at_idx').on(table.expiresAt),
index('auth_sessions_last_active_idx').on(table.lastActiveAt),
uniqueIndex('auth_sessions_session_token_unique').on(table.sessionToken),
]);
@@ -258,16 +265,29 @@ export const systemSettings = pgTable('SystemSettings', {
enableReplayRequests: boolean('enableReplayRequests').default(false).notNull(),
enableCollaborativeSubmission: boolean('enableCollaborativeSubmission').default(true).notNull(),
enableSubmissionRemarks: boolean('enableSubmissionRemarks').default(false).notNull(),
submissionNoteRequiresApproval: boolean('submissionNoteRequiresApproval').default(false).notNull(),
instanceId: text('instanceId'),
telemetryEnabled: boolean('telemetryEnabled').default(true).notNull(),
showBeianIcon: boolean('showBeianIcon').default(true).notNull(),
scheduleDaysDisplay: integer('scheduleDaysDisplay').default(7).notNull(),
scheduleDaysEditable: integer('scheduleDaysEditable').default(3).notNull(),
scheduleDaysRequestable: integer('scheduleDaysRequestable').default(14).notNull(),
scheduleDaysBeforeEnabled: boolean('scheduleDaysBeforeEnabled').default(false).notNull(),
scheduleDaysBefore: integer('scheduleDaysBefore').default(1).notNull(),
scheduleDaysAfterEnabled: boolean('scheduleDaysAfterEnabled').default(false).notNull(),
scheduleDaysAfter: integer('scheduleDaysAfter').default(1).notNull(),
forcePasswordChangeOnFirstLogin: boolean('forcePasswordChangeOnFirstLogin').default(false).notNull(),
enableSubmissionRestriction: boolean('enableSubmissionRestriction').default(false).notNull(),
submissionRestrictionScope: text('submissionRestrictionScope').default('all').notNull(),
sameSongRestrictionHours: integer('sameSongRestrictionHours'),
sameArtistRestrictionHours: integer('sameArtistRestrictionHours'),
cardCodeEnabled: boolean('cardCodeEnabled').default(false).notNull(),
cardCodeSubmissionRequired: boolean('cardCodeSubmissionRequired').default(false).notNull(),
cardCodeVoteEnabled: boolean('cardCodeVoteEnabled').default(false).notNull(),
cardCodeReplayEnabled: boolean('cardCodeReplayEnabled').default(false).notNull(),
enableCardCodeRequests: boolean('enableCardCodeRequests').default(false).notNull(),
requireCardCodeForRequests: boolean('requireCardCodeForRequests').default(false).notNull(),
enableCardCodeLimitBypass: boolean('enableCardCodeLimitBypass').default(false).notNull(),
submissionRestrictionEnabled: boolean('submissionRestrictionEnabled').default(false).notNull(),
submissionRestrictionGradeEnabled: boolean('submissionRestrictionGradeEnabled').default(false).notNull(),
submissionRestrictionClassEnabled: boolean('submissionRestrictionClassEnabled').default(false).notNull(),
@@ -282,16 +302,56 @@ export const systemSettings = pgTable('SystemSettings', {
registerRequireEmailVerify: boolean('registerRequireEmailVerify').default(false).notNull(),
registerDefaultRole: text('registerDefaultRole').default('USER'),
registerEnabledGradeClasses: text('registerEnabledGradeClasses'),
registerRequiresApproval: boolean('registerRequiresApproval').default(false).notNull(),
oauthRegisterRequiresApproval: boolean('oauthRegisterRequiresApproval').default(false).notNull(),
registerEmailRequired: boolean('registerEmailRequired').default(false).notNull(),
registerRequiresGradeClass: boolean('registerRequiresGradeClass').default(false).notNull(),
oauthEnabled: boolean('oauthEnabled').default(false).notNull(),
oauthAllowRegister: boolean('oauthAllowRegister').default(true).notNull(),
oauthAllowLogin: boolean('oauthAllowLogin').default(true).notNull(),
oauthAllowBind: boolean('oauthAllowBind').default(true).notNull(),
allowOAuthRegistration: boolean('allowOAuthRegistration').default(false).notNull(),
oauthRedirectUri: text('oauthRedirectUri'),
oauthStateSecret: text('oauthStateSecret'),
oauthProviders: text('oauthProviders').default('[]'),
githubOAuthEnabled: boolean('githubOAuthEnabled').default(false).notNull(),
githubClientId: text('githubClientId'),
githubClientSecret: text('githubClientSecret'),
casdoorOAuthEnabled: boolean('casdoorOAuthEnabled').default(false).notNull(),
casdoorServerUrl: text('casdoorServerUrl'),
casdoorClientId: text('casdoorClientId'),
casdoorClientSecret: text('casdoorClientSecret'),
casdoorOrganizationName: text('casdoorOrganizationName'),
googleOAuthEnabled: boolean('googleOAuthEnabled').default(false).notNull(),
googleClientId: text('googleClientId'),
googleClientSecret: text('googleClientSecret'),
aggregateOAuthEnabled: boolean('aggregateOAuthEnabled').default(false).notNull(),
aggregateOAuthAppId: text('aggregateOAuthAppId'),
aggregateOAuthAppKey: text('aggregateOAuthAppKey'),
aggregateOAuthLoginType: text('aggregateOAuthLoginType').default('qq'),
aggregateOAuthEndpoint: text('aggregateOAuthEndpoint'),
customOAuthEnabled: boolean('customOAuthEnabled').default(false).notNull(),
customOAuthDisplayName: text('customOAuthDisplayName'),
customOAuthAuthorizeUrl: text('customOAuthAuthorizeUrl'),
customOAuthTokenUrl: text('customOAuthTokenUrl'),
customOAuthUserInfoUrl: text('customOAuthUserInfoUrl'),
customOAuthScope: text('customOAuthScope'),
customOAuthClientId: text('customOAuthClientId'),
customOAuthClientSecret: text('customOAuthClientSecret'),
customOAuthUserIdField: text('customOAuthUserIdField'),
customOAuthUsernameField: text('customOAuthUsernameField'),
customOAuthNameField: text('customOAuthNameField'),
customOAuthEmailField: text('customOAuthEmailField'),
customOAuthAvatarField: text('customOAuthAvatarField'),
captchaMaxFailures: integer('captchaMaxFailures').default(3).notNull(),
autoBackupEnabled: boolean('autoBackupEnabled').default(false).notNull(),
autoBackupIntervalHours: integer('autoBackupIntervalHours').default(24).notNull(),
autoBackupRetentionDays: integer('autoBackupRetentionDays').default(30).notNull(),
autoBackupConfig: text('autoBackupConfig'),
statisticsCode: text('statisticsCode'),
defaultTheme: text('defaultTheme').default('light'),
enabledThemes: text('enabledThemes').default('light,dark'),
statisticsCodeEnabled: boolean('statisticsCodeEnabled').default(false).notNull(),
defaultTheme: text('defaultTheme').default('System').notNull(),
enabledThemes: text('enabledThemes').default('[\"System\",\"ClassicDark\",\"ClassicLight\",\"ModernLight\"]').notNull(),
enabledPlatforms: text('enabledPlatforms'),
platformOrder: text('platformOrder'),
});
+29
View File
@@ -0,0 +1,29 @@
import * as Sentry from '@sentry/vue'
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig()
const dsn = config.public.sentry?.dsn
if (!dsn) return
const app = nuxtApp.vueApp
const router = nuxtApp.$router
Sentry.init({
app,
dsn,
environment: config.sentry?.environment || 'production',
release: config.sentry?.release || undefined,
tracesSampleRate: config.public.sentry?.tracesSampleRate ?? 1.0,
integrations: router
? [
Sentry.browserTracingIntegration({ router }),
Sentry.replayIntegration()
]
: [
Sentry.replayIntegration()
],
replaysSessionSampleRate: config.public.sentry?.replaySessionSampleRate ?? 0,
replaysOnErrorSampleRate: config.public.sentry?.replayOnErrorSampleRate ?? 0
})
})
+2
View File
@@ -0,0 +1,2 @@
export const PASSWORD_MIN_LENGTH = 6
export const PASSWORD_MAX_LENGTH = 64