chore: sync various improvements and fixes

- Update gitignore and serena config
- Improve connection and voting stores
- Enhance admin routes and socket handling
- Update client-screen views
- Add auth middleware

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2026-02-03 23:31:38 +08:00
parent 39caecdd95
commit 83bf1d3a43
25 changed files with 284 additions and 122 deletions

View File

@@ -2,24 +2,18 @@ import { createRouter, createWebHistory, type RouteLocationNormalized } from 'vu
// Admin auth constants
const ADMIN_TOKEN_KEY = 'gala_admin_token';
const ADMIN_ACCESS_CODE = '20268888';
// Auth guard for admin routes
function requireAdminAuth(to: RouteLocationNormalized) {
const token = localStorage.getItem(ADMIN_TOKEN_KEY);
if (!token || token !== generateToken(ADMIN_ACCESS_CODE)) {
if (!token) {
return { path: '/admin/login', query: { redirect: to.fullPath } };
}
return true;
}
// Simple token generator (not cryptographically secure, but sufficient for internal event)
function generateToken(code: string): string {
return btoa(`gala2026:${code}:${code.split('').reverse().join('')}`);
}
// Export for use in login component
export { ADMIN_TOKEN_KEY, ADMIN_ACCESS_CODE, generateToken };
export { ADMIN_TOKEN_KEY };
const router = createRouter({
history: createWebHistory('/screen/'),

View File

@@ -14,6 +14,7 @@ import type {
} from '@gala/shared/types';
import { PRIZE_CONFIG } from '@gala/shared/types';
import { SOCKET_EVENTS } from '@gala/shared/constants';
import { ADMIN_TOKEN_KEY } from '../router';
type GalaSocket = Socket<ServerToClientEvents, ClientToServerEvents>;
@@ -162,6 +163,8 @@ export const useAdminStore = defineStore('admin', () => {
isConnecting.value = true;
restoreState();
const adminToken = localStorage.getItem(ADMIN_TOKEN_KEY) || '';
const socketInstance = io(import.meta.env.VITE_SOCKET_URL || '', {
reconnection: true,
reconnectionAttempts: Infinity,
@@ -169,6 +172,7 @@ export const useAdminStore = defineStore('admin', () => {
reconnectionDelayMax: 5000,
timeout: 10000,
transports: ['websocket', 'polling'],
auth: adminToken ? { token: adminToken } : undefined,
});
socketInstance.on('connect', () => {
@@ -181,6 +185,7 @@ export const useAdminStore = defineStore('admin', () => {
userId: 'admin_main',
userName: 'Admin Console',
role: 'admin',
sessionToken: adminToken || undefined,
}, () => { });
// Request state sync

View File

@@ -4,6 +4,7 @@ import { useRouter } from 'vue-router';
import { useAdminStore } from '../stores/admin';
import { PRIZE_CONFIG } from '@gala/shared/types';
import type { PrizeConfig, LotteryRound } from '@gala/shared/types';
import { ADMIN_TOKEN_KEY } from '../router';
// 简单的防抖函数
function debounce<T extends (...args: any[]) => void>(fn: T, delay: number = 300): (...args: Parameters<T>) => void {
@@ -35,6 +36,15 @@ function debounceLeading<T extends (...args: any[]) => void>(fn: T, delay: numbe
const router = useRouter();
const admin = useAdminStore();
const adminToken = () => localStorage.getItem(ADMIN_TOKEN_KEY) || '';
function getAdminHeaders(extra?: Record<string, string>) {
return {
'Content-Type': 'application/json',
'x-session-token': adminToken(),
...extra,
};
}
// Local UI state
const confirmResetCode = ref('');
@@ -89,7 +99,9 @@ async function readJsonSafe(res: Response): Promise<any> {
async function loadPrizeConfig() {
prizeConfigLoading.value = true;
try {
const res = await fetch('/api/admin/prizes');
const res = await fetch('/api/admin/prizes', {
headers: getAdminHeaders(),
});
const data = await readJsonSafe(res);
if (!res.ok) {
throw new Error(data?.error || data?.message || `加载奖项配置失败(${res.status})`);
@@ -112,7 +124,7 @@ async function savePrizeConfig() {
try {
const res = await fetch('/api/admin/prizes', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
headers: getAdminHeaders(),
body: JSON.stringify({ prizes: editingPrizes.value }),
});
const data = await readJsonSafe(res);
@@ -172,6 +184,7 @@ async function importParticipants() {
const response = await fetch('/api/admin/participants/import', {
method: 'POST',
headers: { 'x-session-token': adminToken() },
body: formData,
});
@@ -219,7 +232,9 @@ const tagLabels: Record<string, string> = {
// Load existing participants from server
async function loadParticipants() {
try {
const response = await fetch('/api/admin/participants');
const response = await fetch('/api/admin/participants', {
headers: getAdminHeaders(),
});
const data = await readJsonSafe(response);
if (!response.ok) {
throw new Error(data?.error || data?.message || `加载参与者失败(${response.status})`);
@@ -331,7 +346,7 @@ async function redrawCurrentRound() {
try {
const res = await fetch('/api/admin/lottery/redraw', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: getAdminHeaders(),
});
const data = await res.json();
if (data.success) {
@@ -386,7 +401,7 @@ async function confirmAdvancedCleanup() {
try {
const res = await fetch('/api/admin/cleanup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: getAdminHeaders(),
body: JSON.stringify({
lottery: cleanupOptions.value.lottery,
voting: cleanupOptions.value.voting,

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { ADMIN_TOKEN_KEY, ADMIN_ACCESS_CODE, generateToken } from '../router';
import { ADMIN_TOKEN_KEY } from '../router';
const router = useRouter();
const route = useRoute();
@@ -20,22 +20,29 @@ async function handleLogin() {
isLoading.value = true;
// Simulate network delay for UX
await new Promise(resolve => setTimeout(resolve, 500));
try {
const response = await fetch('/api/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accessCode: accessCode.value.trim() }),
});
const result = await response.json();
if (accessCode.value === ADMIN_ACCESS_CODE) {
// Save token to localStorage
localStorage.setItem(ADMIN_TOKEN_KEY, generateToken(ADMIN_ACCESS_CODE));
if (!response.ok || !result.success) {
error.value = result.error || '访问码错误';
accessCode.value = '';
return;
}
localStorage.setItem(ADMIN_TOKEN_KEY, result.data.sessionToken);
// Redirect to console or original destination
const redirect = route.query.redirect as string || '/admin/director-console';
router.push(redirect);
} else {
error.value = '访问码错误';
accessCode.value = '';
} catch (e) {
error.value = '网络错误,请重试';
} finally {
isLoading.value = false;
}
isLoading.value = false;
}
function handleKeydown(e: KeyboardEvent) {

View File

@@ -22,7 +22,9 @@ function goBack() {
// Fetch lottery results from API
async function fetchLotteryResults() {
try {
const res = await fetch('/api/admin/lottery/results');
const res = await fetch('/api/admin/lottery/results', {
headers: { 'x-session-token': localStorage.getItem('gala_admin_token') || '' },
});
const data = await res.json();
if (data.success && data.data?.draws) {
lotteryResults.value = data.data.draws;

View File

@@ -30,7 +30,7 @@ const prizes = ref<Array<{ level: string; name: string; winnerCount: number; poo
// 从 API 获取奖项配置
async function fetchPrizes() {
try {
const response = await fetch('/api/admin/prizes');
const response = await fetch('/api/public/prizes');
const data = await response.json();
if (data.success && data.data?.prizes) {
prizes.value = data.data.prizes;
@@ -53,7 +53,7 @@ let realParticipants: Participant[] = [];
async function fetchParticipants() {
try {
isLoading.value = true;
const response = await fetch('/api/admin/participants');
const response = await fetch('/api/public/participants');
const data = await response.json();
if (data.success && data.data?.participants) {