feat(songs): 歌曲管理支持批量驳回、投稿留言自动审查与排期范围可见性过滤
- 新增 /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:
@@ -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: '',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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'),
|
||||
});
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,2 @@
|
||||
export const PASSWORD_MIN_LENGTH = 6
|
||||
export const PASSWORD_MAX_LENGTH = 64
|
||||
Reference in New Issue
Block a user