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:
@@ -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/'),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user