-
-
- {/* 保存成功提示 */}
- {showSaveSuccess && (
-
- ✓ 音乐保存成功!
+ {/* 音乐列表区域 - 只有登录用户才显示 */}
+ {isAuthenticated && (
+
+
+
+
+ 我的音乐
+
- )}
- {/* 登录提示 */}
- {showLoginPrompt && (
-
-
请先登录后再保存音乐
-
-
+ {/* 错误提示 */}
+ {error && (
+
+ {error}
+
+
+ )}
+
+ {/* 保存成功提示 */}
+ {showSaveSuccess && (
+
+
✓ 音乐保存成功!
+
+
+ )}
+
+ {/* 音乐列表 - 自动显示 */}
+
+ {console.log('🎵 渲染音乐列表:', { isLoadingMusic, listLength: savedMusicList.length })}
+ {isLoadingMusic ? (
+
加载中...
+ ) : savedMusicList.length === 0 ? (
+
+
+
还没有保存的音乐
+
选择声音并点击保存按钮来创建你的第一首音乐
+
+ ) : (
+
+ {savedMusicList.map((music) => (
+
+ {editingId === music.id.toString() ? (
+
+
setEditingName(e.target.value)}
+ className={styles.editInput}
+ placeholder="输入音乐名称"
+ maxLength={50}
+ />
+
+
+
+
+
+ ) : (
+
+
+
+
+ {
+ setEditingId(music.id.toString());
+ setEditingName(music.name);
+ }}
+ title="点击编辑名称"
+ >
+ {music.name}
+
+
+
+ {/* 展开时显示收录的声音名字 */}
+ {expandedMusic.has(music.id) && (
+
+ {music.sounds && music.sounds.length > 0 ? (
+ music.sounds.map((soundId: string, index: number) => {
+ // 从所有声音中查找对应的声音名称
+ const allSounds = localizedCategories
+ .map(category => category.sounds)
+ .flat();
+ const sound = allSounds.find(s => s.id === soundId);
+ return sound ? (
+
+ {sound.label}{index < music.sounds.length - 1 ? ', ' : ''}
+
+ ) : null;
+ })
+ ) : (
+ 暂无声音
+ )}
+
+ )}
+
+
+
+ )}
+
+ ))}
+
+ )}
- )}
-
+
+ )}
+
+ {/* 登录提示 */}
+ {showLoginPrompt && (
+
+
请先登录后再保存音乐
+
+
+
+ )}
);
}
\ No newline at end of file
diff --git a/src/components/slider/slider.module.css b/src/components/slider/slider.module.css
index cb713e3..c336081 100644
--- a/src/components/slider/slider.module.css
+++ b/src/components/slider/slider.module.css
@@ -27,13 +27,15 @@
width: 16px;
height: 16px;
cursor: pointer;
- background: var(--color-neutral-950);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--color-border);
border-radius: 50%;
box-shadow: 0 0 3px var(--color-neutral-50);
}
.sliderThumb:hover {
- background: var(--color-neutral-800);
+ background: var(--bg-secondary);
+ border-color: var(--color-foreground);
}
.sliderThumb:focus {
diff --git a/src/components/sounds/sound/range/range.module.css b/src/components/sounds/sound/range/range.module.css
index e36bb7f..444bfc8 100644
--- a/src/components/sounds/sound/range/range.module.css
+++ b/src/components/sounds/sound/range/range.module.css
@@ -74,9 +74,10 @@
height: 14px;
margin-top: -3px;
appearance: none;
- background-color: var(--bg-tertiary);
+ background-color: var(--color-neutral-700);
border: 1px solid var(--color-border);
border-radius: 50%;
+ box-shadow: 0 0 2px var(--color-neutral-400);
}
&:not(:disabled):focus::-webkit-slider-thumb {
@@ -97,11 +98,12 @@
width: 14px;
height: 14px;
margin-top: -3px;
- background-color: var(--bg-tertiary);
+ background-color: var(--color-neutral-700);
border: none;
border: 1px solid var(--color-border);
border-radius: 0;
border-radius: 50%;
+ box-shadow: 0 0 2px var(--color-neutral-400);
}
&:not(:disabled):focus::-moz-range-thumb {
diff --git a/src/components/sounds/sounds.module.css b/src/components/sounds/sounds.module.css
index 6ac1f0f..5639afa 100644
--- a/src/components/sounds/sounds.module.css
+++ b/src/components/sounds/sounds.module.css
@@ -8,8 +8,12 @@
.soundsContainer {
display: flex;
flex-direction: column;
- gap: 20px;
+ gap: 16px;
margin-top: 20px;
+ background: var(--bg-secondary);
+ border-radius: 12px;
+ border: 1px solid var(--color-border);
+ padding: 16px;
}
.saveSection {
@@ -28,7 +32,7 @@
display: flex;
align-items: center;
gap: 8px;
- padding: 12px 20px;
+ padding: 10px 16px;
background: linear-gradient(135deg, #10b981, #059669);
color: white;
border: none;
@@ -37,6 +41,346 @@
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
+ min-height: 40px;
+}
+
+/* 音乐管理区域 */
+.musicSection {
+ border-top: 1px solid var(--color-border);
+ padding-top: 16px;
+}
+
+.musicHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+
+.musicTitle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--color-foreground);
+}
+
+.musicIcon {
+ color: var(--color-foreground-subtle);
+ font-size: 14px;
+}
+
+.toggleMusicList {
+ background: transparent;
+ border: 1px solid var(--color-border);
+ color: var(--color-foreground-subtle);
+ border-radius: 4px;
+ padding: 4px 8px;
+ font-size: 12px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.toggleMusicList:hover {
+ background: var(--component-hover);
+ border-color: var(--color-foreground-subtle);
+ color: var(--color-foreground);
+}
+
+.musicList {
+ max-height: 300px;
+ overflow-y: auto;
+ border-radius: 6px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--color-border);
+}
+
+.musicItem {
+ padding: 12px;
+ border-bottom: 1px solid var(--color-border);
+ transition: background-color 0.2s ease;
+}
+
+.musicItem:last-child {
+ border-bottom: none;
+}
+
+.musicItem:hover {
+ background: var(--component-hover);
+}
+
+.musicContent {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.playButton {
+ background: var(--color-foreground);
+ color: var(--bg-primary);
+ border: none;
+ border-radius: 4px;
+ padding: 6px 8px;
+ cursor: pointer;
+ font-size: 12px;
+ transition: all 0.2s ease;
+ flex-shrink: 0;
+}
+
+.playButton:hover {
+ background: var(--color-foreground-subtle);
+}
+
+.musicName {
+ flex: 1;
+ font-size: 14px;
+ color: var(--color-foreground);
+ cursor: pointer;
+ padding: 4px 8px;
+ border-radius: 4px;
+ transition: background-color 0.2s ease;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.musicName:hover {
+ background: var(--component-hover);
+}
+
+/* 音乐信息容器 */
+.musicInfo {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ gap: 2px;
+}
+
+/* 音乐名称行 */
+.musicNameRow {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+/* 展开按钮 */
+.expandButton {
+ background: transparent;
+ border: 1px solid var(--color-border);
+ color: var(--color-foreground-subtle);
+ border-radius: 4px;
+ padding: 2px 6px;
+ font-size: 10px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ flex-shrink: 0;
+ height: 20px;
+ line-height: 1;
+}
+
+.expandButton:hover {
+ background: var(--component-hover);
+ border-color: var(--color-foreground-subtle);
+ color: var(--color-foreground);
+}
+
+/* 声音名字显示 */
+.soundNames {
+ font-size: 12px;
+ color: var(--color-foreground-subtle);
+ line-height: 1.3;
+ word-break: break-all;
+}
+
+.soundName {
+ color: var(--color-foreground-subtle);
+}
+
+.noSounds {
+ color: var(--color-foreground-subtler);
+ font-style: italic;
+}
+
+.deleteButton {
+ background: #ef4444;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 6px 8px;
+ cursor: pointer;
+ font-size: 12px;
+ transition: all 0.2s ease;
+ flex-shrink: 0;
+}
+
+.deleteButton:hover {
+ background: #dc2626;
+}
+
+/* 编辑表单样式 */
+.editForm {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 0;
+}
+
+.editInput {
+ flex: 1;
+ padding: 6px 8px;
+ border: 1px solid var(--color-border);
+ border-radius: 4px;
+ font-size: 14px;
+ background: var(--input-bg);
+ color: var(--color-foreground);
+ outline: none;
+}
+
+.editInput:focus {
+ border-color: var(--color-muted);
+ box-shadow: 0 0 0 2px var(--color-muted);
+}
+
+.editButtons {
+ display: flex;
+ gap: 4px;
+}
+
+.editButton {
+ border: none;
+ border-radius: 4px;
+ padding: 6px 8px;
+ cursor: pointer;
+ font-size: 12px;
+ font-weight: 500;
+ transition: all 0.2s ease;
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.editButton.saveButton {
+ background: #10b981;
+ color: white;
+}
+
+.editButton.saveButton:hover {
+ background: #059669;
+}
+
+.editButton.cancelButton {
+ background: var(--color-muted);
+ color: var(--color-foreground);
+}
+
+.editButton.cancelButton:hover {
+ background: var(--color-foreground-subtle);
+}
+
+/* 音乐名称配置区域 */
+.musicNameConfig {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 16px;
+ /* 移除边框和背景 */
+ padding: 0;
+ background: transparent;
+ border: none;
+ border-radius: 0;
+}
+
+.musicNameInput {
+ width: 6.25em; /* 5em * 1.25 = 6.25em,增大1/4 */
+ padding: 8px 12px;
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ font-size: 14px;
+ background: var(--input-bg);
+ color: var(--color-foreground);
+ outline: none;
+ transition: all 0.2s ease;
+ flex-shrink: 0;
+}
+
+.musicNameInput:focus {
+ border-color: var(--color-muted);
+ box-shadow: 0 0 0 2px var(--color-muted);
+}
+
+.musicNameInput::placeholder {
+ color: var(--color-foreground-subtler);
+}
+
+/* 空状态和加载状态 */
+.empty {
+ padding: 24px;
+ text-align: center;
+ color: var(--color-foreground-subtle);
+}
+
+.emptyIcon {
+ font-size: 24px;
+ color: var(--color-foreground-subtler);
+ margin-bottom: 8px;
+}
+
+.empty p {
+ margin: 4px 0;
+ font-size: 14px;
+}
+
+.emptyHint {
+ font-size: 12px !important;
+ color: var(--color-foreground-subtler) !important;
+}
+
+.loading {
+ padding: 16px;
+ text-align: center;
+ color: var(--color-foreground-subtle);
+ font-size: 14px;
+}
+
+/* 错误提示样式 */
+.error {
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid #ef4444;
+ color: #dc2626;
+ padding: 8px 12px;
+ border-radius: 4px;
+ font-size: 13px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+
+.errorClose {
+ background: none;
+ border: none;
+ color: #dc2626;
+ font-size: 16px;
+ cursor: pointer;
+ padding: 0;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 2px;
+ transition: background-color 0.2s ease;
+}
+
+.errorClose:hover {
+ background: rgba(239, 68, 68, 0.2);
}
.saveButton:hover:not(:disabled) {
@@ -56,13 +400,48 @@
}
.saveSuccess {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ padding: 16px;
+ background: var(--component-bg);
+ border: 1px solid var(--color-border);
+ border-radius: 8px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ color: var(--color-foreground);
+ text-align: center;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+ z-index: 1002;
+ min-width: 250px;
+ font-size: 14px;
+ animation: slideIn 0.3s ease-out;
+}
+
+.saveSuccess p {
+ margin: 0;
+ font-size: 14px;
+ color: var(--color-foreground-subtle);
+}
+
+.saveSuccess button {
padding: 8px 16px;
- background: linear-gradient(135deg, #10b981, #059669);
- color: white;
- border-radius: 6px;
+ margin: 0 4px;
+ border: none;
+ border-radius: 4px;
font-size: 13px;
font-weight: 500;
- animation: slideIn 0.3s ease-out;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ background: var(--color-foreground);
+ color: var(--bg-primary);
+}
+
+.saveSuccess button:hover {
+ background: var(--color-foreground-subtle);
}
.loginPrompt {
diff --git a/src/hooks/useNotification.ts b/src/hooks/useNotification.ts
new file mode 100644
index 0000000..d0f884b
--- /dev/null
+++ b/src/hooks/useNotification.ts
@@ -0,0 +1,38 @@
+import { useState } from 'react';
+
+interface NotificationState {
+ showNotification: boolean;
+ notificationMessage: string;
+ notificationType: 'success' | 'error';
+}
+
+export function useNotification() {
+ const [state, setState] = useState
({
+ showNotification: false,
+ notificationMessage: '',
+ notificationType: 'success'
+ });
+
+ const showNotificationMessage = (message: string, type: 'success' | 'error') => {
+ setState({
+ showNotification: true,
+ notificationMessage: message,
+ notificationType: type
+ });
+
+ // 3秒后自动关闭
+ setTimeout(() => {
+ setState(prev => ({ ...prev, showNotification: false }));
+ }, 3000);
+ };
+
+ const hideNotification = () => {
+ setState(prev => ({ ...prev, showNotification: false }));
+ };
+
+ return {
+ ...state,
+ showNotificationMessage,
+ hideNotification
+ };
+}
\ No newline at end of file
diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts
new file mode 100644
index 0000000..ff461f1
--- /dev/null
+++ b/src/lib/api-client.ts
@@ -0,0 +1,135 @@
+import { useAuthStore } from '@/stores/auth';
+
+/**
+ * API客户端辅助函数,自动添加JWT Authorization头
+ */
+export class ApiClient {
+ /**
+ * 发起API请求
+ * @param url - API URL
+ * @param options - fetch options
+ * @returns Promise
+ */
+ static async fetch(url: string, options: RequestInit = {}): Promise {
+ // 获取token - 尝试多种方式获取Zustand store
+ let token = null;
+
+ try {
+ // 方法1: 通过useAuthStore.getState()获取
+ token = useAuthStore.getState().getToken();
+ console.log('🔐 方法1获取token结果:', token ? '成功' : '失败');
+ } catch (e) {
+ console.warn('无法通过useAuthStore.getState()获取token:', e);
+ }
+
+ // 如果方法1失败,尝试方法2: 从localStorage直接获取
+ if (!token) {
+ try {
+ const authStorage = localStorage.getItem('auth-storage');
+ console.log('🔐 localStorage auth-storage:', authStorage ? '存在' : '不存在');
+ if (authStorage) {
+ const parsed = JSON.parse(authStorage);
+ token = parsed.state?.token;
+ console.log('🔐 方法2获取token结果:', token ? '成功' : '失败');
+ }
+ } catch (e) {
+ console.warn('无法从localStorage获取token:', e);
+ }
+ }
+
+ // 创建新的headers对象
+ const headers = new Headers(options.headers || {});
+
+ // 添加Content-Type(如果没有的话)
+ if (!headers.has('Content-Type') && (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH')) {
+ headers.set('Content-Type', 'application/json');
+ }
+
+ // 添加Authorization头(如果有token)
+ if (token) {
+ headers.set('Authorization', `Bearer ${token}`);
+ console.log('🔑 已添加Authorization头,URL:', url);
+ } else {
+ console.warn('⚠️ 没有找到token,请求URL:', url);
+ }
+
+ // 发起请求
+ const response = await fetch(url, {
+ ...options,
+ headers,
+ });
+
+ console.log('📡 API响应:', url, response.status);
+ return response;
+ }
+
+ /**
+ * 发起POST请求
+ * @param url - API URL
+ * @param data - 请求数据
+ * @returns Promise
+ */
+ static async post(url: string, data?: any): Promise {
+ return this.fetch(url, {
+ method: 'POST',
+ body: data ? JSON.stringify(data) : undefined,
+ });
+ }
+
+ /**
+ * 发起GET请求
+ * @param url - API URL
+ * @returns Promise
+ */
+ static async get(url: string): Promise {
+ return this.fetch(url, {
+ method: 'GET',
+ });
+ }
+
+ /**
+ * 发起PUT请求
+ * @param url - API URL
+ * @param data - 请求数据
+ * @returns Promise
+ */
+ static async put(url: string, data?: any): Promise {
+ return this.fetch(url, {
+ method: 'PUT',
+ body: data ? JSON.stringify(data) : undefined,
+ });
+ }
+
+ /**
+ * 发起DELETE请求
+ * @param url - API URL
+ * @returns Promise
+ */
+ static async delete(url: string): Promise {
+ return this.fetch(url, {
+ method: 'DELETE',
+ });
+ }
+}
+
+/**
+ * 简化的API调用函数
+ * @param url - API URL
+ * @param data - 请求数据
+ * @param method - HTTP方法
+ * @returns Promise
+ */
+export async function apiCall(url: string, data?: any, method: 'POST' | 'GET' | 'PUT' | 'DELETE' = 'POST'): Promise {
+ const response = await ApiClient.fetch(url, {
+ method,
+ body: data ? JSON.stringify(data) : undefined,
+ });
+
+ const result = await response.json();
+
+ if (!response.ok) {
+ throw new Error(result.error || 'API 调用失败');
+ }
+
+ return result;
+}
\ No newline at end of file
diff --git a/src/lib/auth-middleware.ts b/src/lib/auth-middleware.ts
new file mode 100644
index 0000000..d2ecc95
--- /dev/null
+++ b/src/lib/auth-middleware.ts
@@ -0,0 +1,166 @@
+import type { APIRoute } from 'astro';
+import { authenticateUser } from '@/lib/database';
+
+/**
+ * 认证中间件 - 统一处理用户身份验证
+ * @param request - Astro请求对象
+ * @returns 认证结果对象
+ */
+export interface AuthResult {
+ success: boolean;
+ user?: {
+ id: number;
+ username: string;
+ };
+ error?: {
+ message: string;
+ status: number;
+ };
+ data?: any;
+}
+
+/**
+ * 验证请求并解析JSON数据
+ * @param request - Astro请求对象
+ * @param requiredFields - 必需的字段数组
+ * @returns 认证结果
+ */
+export async function authenticateRequest(
+ request: Request,
+ requiredFields: string[] = ['username', 'password']
+): Promise {
+ try {
+ // 验证请求体
+ const body = await request.text();
+ if (!body.trim()) {
+ return {
+ success: false,
+ error: {
+ message: '请求体不能为空',
+ status: 400
+ }
+ };
+ }
+
+ // 解析JSON
+ let data;
+ try {
+ data = JSON.parse(body);
+ } catch (parseError) {
+ return {
+ success: false,
+ error: {
+ message: '请求格式错误,请检查JSON格式',
+ status: 400
+ }
+ };
+ }
+
+ // 验证必需字段
+ const missingFields = requiredFields.filter(field => {
+ // 对于密码字段,我们允许空字符串,但不允许undefined/null
+ if (field === 'password') {
+ return data[field] === undefined || data[field] === null;
+ }
+ return !data[field];
+ });
+ if (missingFields.length > 0) {
+ return {
+ success: false,
+ error: {
+ message: `缺少必需字段: ${missingFields.join(', ')}`,
+ status: 400
+ }
+ };
+ }
+
+ // 验证用户身份
+ const user = authenticateUser(data.username, data.password);
+ if (!user) {
+ return {
+ success: false,
+ error: {
+ message: '用户认证失败,请检查用户名和密码',
+ status: 401
+ }
+ };
+ }
+
+ return {
+ success: true,
+ user: {
+ id: user.id,
+ username: user.username
+ },
+ data
+ };
+
+ } catch (error) {
+ console.error('认证过程出错:', error);
+ return {
+ success: false,
+ error: {
+ message: '服务器内部错误',
+ status: 500
+ }
+ };
+ }
+}
+
+/**
+ * 创建标准化的API响应
+ * @param success - 是否成功
+ * @param data - 响应数据
+ * @param message - 响应消息
+ * @param status - HTTP状态码
+ * @returns Response对象
+ */
+export function createApiResponse(
+ success: boolean,
+ data?: any,
+ message?: string,
+ status: number = 200
+): Response {
+ const responseBody = {
+ success,
+ ...(message && { message }),
+ ...(data && data)
+ };
+
+ return new Response(JSON.stringify(responseBody), {
+ status,
+ headers: { 'Content-Type': 'application/json' }
+ });
+}
+
+/**
+ * 创建错误响应
+ * @param message - 错误消息
+ * @param status - HTTP状态码
+ * @returns Response对象
+ */
+export function createErrorResponse(message: string, status: number = 500): Response {
+ return createApiResponse(false, undefined, message, status);
+}
+
+/**
+ * 处理API错误的统一函数
+ * @param error - 错误对象
+ * @param operation - 操作描述
+ * @returns Response对象
+ */
+export function handleApiError(error: unknown, operation: string): Response {
+ console.error(`${operation}错误:`, error);
+
+ let errorMessage = `${operation}失败,请稍后再试`;
+ let status = 500;
+
+ if (error instanceof SyntaxError && error.message.includes('JSON')) {
+ errorMessage = '请求格式错误';
+ status = 400;
+ } else if (error instanceof Error) {
+ errorMessage = error.message;
+ }
+
+ return createErrorResponse(errorMessage, status);
+}
\ No newline at end of file
diff --git a/src/lib/jwt-auth-middleware.ts b/src/lib/jwt-auth-middleware.ts
new file mode 100644
index 0000000..68a531f
--- /dev/null
+++ b/src/lib/jwt-auth-middleware.ts
@@ -0,0 +1,195 @@
+import type { APIRoute } from 'astro';
+import { verifyJWT, extractTokenFromHeader } from '@/lib/jwt';
+
+export interface JWTAuthResult {
+ success: boolean;
+ user?: {
+ id: number;
+ username: string;
+ };
+ error?: {
+ message: string;
+ status: number;
+ };
+ data?: any;
+}
+
+/**
+ * JWT认证中间件 - 验证请求中的JWT Token
+ * @param request - Astro请求对象
+ * @returns 认证结果对象
+ */
+export async function authenticateJWTRequest(request: Request): Promise {
+ try {
+ // 从Authorization头中提取token
+ const authHeader = request.headers.get('Authorization');
+ const token = extractTokenFromHeader(authHeader);
+
+ if (!token) {
+ return {
+ success: false,
+ error: {
+ message: '缺少授权令牌,请先登录',
+ status: 401
+ }
+ };
+ }
+
+ // 验证JWT token
+ const payload = verifyJWT(token);
+ if (!payload) {
+ return {
+ success: false,
+ error: {
+ message: '授权令牌无效或已过期,请重新登录',
+ status: 401
+ }
+ };
+ }
+
+ return {
+ success: true,
+ user: {
+ id: payload.userId,
+ username: payload.username
+ }
+ };
+
+ } catch (error) {
+ console.error('JWT认证过程出错:', error);
+ return {
+ success: false,
+ error: {
+ message: '认证服务异常',
+ status: 500
+ }
+ };
+ }
+}
+
+/**
+ * 验证请求体并解析JSON数据
+ * @param request - Astro请求对象
+ * @param requiredFields - 必需的字段数组
+ * @returns 解析结果
+ */
+export async function parseRequestBody(
+ request: Request,
+ requiredFields: string[] = []
+): Promise<{ success: boolean; data?: any; error?: { message: string; status: number } }> {
+ try {
+ // 验证请求体
+ const body = await request.text();
+ if (!body.trim()) {
+ return {
+ success: false,
+ error: {
+ message: '请求体不能为空',
+ status: 400
+ }
+ };
+ }
+
+ // 解析JSON
+ let data;
+ try {
+ data = JSON.parse(body);
+ } catch (parseError) {
+ return {
+ success: false,
+ error: {
+ message: '请求格式错误,请检查JSON格式',
+ status: 400
+ }
+ };
+ }
+
+ // 验证必需字段
+ const missingFields = requiredFields.filter(field => !data[field]);
+ if (missingFields.length > 0) {
+ return {
+ success: false,
+ error: {
+ message: `缺少必需字段: ${missingFields.join(', ')}`,
+ status: 400
+ }
+ };
+ }
+
+ return {
+ success: true,
+ data
+ };
+
+ } catch (error) {
+ console.error('请求体解析出错:', error);
+ return {
+ success: false,
+ error: {
+ message: '服务器内部错误',
+ status: 500
+ }
+ };
+ }
+}
+
+/**
+ * 创建标准化的API响应
+ * @param success - 是否成功
+ * @param data - 响应数据
+ * @param message - 响应消息
+ * @param status - HTTP状态码
+ * @returns Response对象
+ */
+export function createApiResponse(
+ success: boolean,
+ data?: any,
+ message?: string,
+ status: number = 200
+): Response {
+ const responseBody = {
+ success,
+ ...(message && { message }),
+ ...(data && data)
+ };
+
+ return new Response(JSON.stringify(responseBody), {
+ status,
+ headers: { 'Content-Type': 'application/json' }
+ });
+}
+
+/**
+ * 创建错误响应
+ * @param message - 错误消息
+ * @param status - HTTP状态码
+ * @returns Response对象
+ */
+export function createErrorResponse(message: string, status: number = 500): Response {
+ return new Response(JSON.stringify({ error: message }), {
+ status,
+ headers: { 'Content-Type': 'application/json' }
+ });
+}
+
+/**
+ * 处理API错误的统一函数
+ * @param error - 错误对象
+ * @param operation - 操作描述
+ * @returns Response对象
+ */
+export function handleApiError(error: unknown, operation: string): Response {
+ console.error(`${operation}错误:`, error);
+
+ let errorMessage = `${operation}失败,请稍后再试`;
+ let status = 500;
+
+ if (error instanceof SyntaxError && error.message.includes('JSON')) {
+ errorMessage = '请求格式错误';
+ status = 400;
+ } else if (error instanceof Error) {
+ errorMessage = error.message;
+ }
+
+ return createErrorResponse(errorMessage, status);
+}
\ No newline at end of file
diff --git a/src/lib/jwt.ts b/src/lib/jwt.ts
new file mode 100644
index 0000000..bbaa48e
--- /dev/null
+++ b/src/lib/jwt.ts
@@ -0,0 +1,118 @@
+import crypto from 'crypto';
+
+// JWT密钥 - 在生产环境中应该使用环境变量
+const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
+const JWT_ALGORITHM = 'HS256';
+const JWT_EXPIRES_IN = 7 * 24 * 60 * 60; // 7天过期
+
+export interface JWTPayload {
+ userId: number;
+ username: string;
+ iat?: number;
+ exp?: number;
+}
+
+/**
+ * 创建JWT Token
+ */
+export function createJWT(payload: Omit): string {
+ const header = {
+ alg: JWT_ALGORITHM,
+ typ: 'JWT'
+ };
+
+ const now = Math.floor(Date.now() / 1000);
+ const jwtPayload: JWTPayload = {
+ ...payload,
+ iat: now,
+ exp: now + JWT_EXPIRES_IN
+ };
+
+ // Base64URL编码
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
+ const encodedPayload = base64UrlEncode(JSON.stringify(jwtPayload));
+
+ // 创建签名
+ const signatureInput = `${encodedHeader}.${encodedPayload}`;
+ const signature = crypto
+ .createHmac('sha256', JWT_SECRET)
+ .update(signatureInput)
+ .digest('base64url');
+
+ return `${signatureInput}.${signature}`;
+}
+
+/**
+ * 验证JWT Token
+ */
+export function verifyJWT(token: string): JWTPayload | null {
+ try {
+ const parts = token.split('.');
+ if (parts.length !== 3) {
+ return null;
+ }
+
+ const [encodedHeader, encodedPayload, signature] = parts;
+
+ // 验证签名
+ const signatureInput = `${encodedHeader}.${encodedPayload}`;
+ const expectedSignature = crypto
+ .createHmac('sha256', JWT_SECRET)
+ .update(signatureInput)
+ .digest('base64url');
+
+ if (signature !== expectedSignature) {
+ return null;
+ }
+
+ // 解析payload
+ const payload = JSON.parse(base64UrlDecode(encodedPayload)) as JWTPayload;
+
+ // 检查过期时间
+ if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
+ return null;
+ }
+
+ return payload;
+ } catch (error) {
+ console.error('JWT验证错误:', error);
+ return null;
+ }
+}
+
+/**
+ * 从Authorization头中提取Token
+ */
+export function extractTokenFromHeader(authHeader: string | null): string | null {
+ if (!authHeader) {
+ return null;
+ }
+
+ // 支持 "Bearer token" 格式
+ if (authHeader.startsWith('Bearer ')) {
+ return authHeader.substring(7);
+ }
+
+ // 直接返回token
+ return authHeader;
+}
+
+/**
+ * Base64URL编码
+ */
+function base64UrlEncode(str: string): string {
+ return Buffer.from(str)
+ .toString('base64')
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=/g, '');
+}
+
+/**
+ * Base64URL解码
+ */
+function base64UrlDecode(str: string): string {
+ // 添加填充字符
+ str += '='.repeat((4 - str.length % 4) % 4);
+ return Buffer.from(str.replace(/\-/g, '+').replace(/_/g, '/'), 'base64').toString();
+}
\ No newline at end of file
diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts
index 544943b..777536a 100644
--- a/src/pages/api/auth/login.ts
+++ b/src/pages/api/auth/login.ts
@@ -1,5 +1,6 @@
import type { APIRoute } from 'astro';
import { authenticateUser } from '@/lib/database';
+import { createJWT } from '@/lib/jwt';
export const POST: APIRoute = async ({ request }) => {
try {
@@ -32,13 +33,21 @@ export const POST: APIRoute = async ({ request }) => {
});
}
+ // 创建JWT token
+ const token = createJWT({
+ userId: user.id,
+ username: user.username
+ });
+
return new Response(JSON.stringify({
success: true,
user: {
id: user.id,
username: user.username,
created_at: user.created_at
- }
+ },
+ token,
+ expiresIn: 7 * 24 * 60 * 60 // 7天,单位:秒
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
diff --git a/src/pages/api/auth/music/delete.ts b/src/pages/api/auth/music/delete.ts
index 670f4b6..61e676c 100644
--- a/src/pages/api/auth/music/delete.ts
+++ b/src/pages/api/auth/music/delete.ts
@@ -1,44 +1,50 @@
import type { APIRoute } from 'astro';
import { deleteMusic } from '@/lib/database';
-import { authenticateUser } from '@/lib/database';
+import { authenticateJWTRequest, parseRequestBody, handleApiError } from '@/lib/jwt-auth-middleware';
export const POST: APIRoute = async ({ request }) => {
+ // 首先进行JWT认证
+ const authResult = await authenticateJWTRequest(request);
+ if (!authResult.success) {
+ return new Response(JSON.stringify({ error: authResult.error!.message }), {
+ status: authResult.error!.status,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ // 解析请求体
+ const bodyResult = await parseRequestBody(request, ['musicId']);
+ if (!bodyResult.success) {
+ return new Response(JSON.stringify({ error: bodyResult.error!.message }), {
+ status: bodyResult.error!.status,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
try {
- const body = await request.text();
+ const { user } = authResult;
+ const { data } = bodyResult;
+ const { musicId } = data;
- if (!body.trim()) {
- return new Response(JSON.stringify({ error: '请求体不能为空' }), {
+ // 验证音乐ID
+ if (!musicId || (typeof musicId !== 'string' && typeof musicId !== 'number')) {
+ return new Response(JSON.stringify({
+ error: '音乐ID不能为空且必须是有效的标识符'
+ }), {
status: 400,
- headers: { 'Content-Type': 'application/json' },
- });
- }
-
- const { musicId, username, password } = JSON.parse(body);
-
- // 验证输入
- if (!musicId || !username || !password) {
- return new Response(JSON.stringify({ error: '音乐ID和用户信息不能为空' }), {
- status: 400,
- headers: { 'Content-Type': 'application/json' },
- });
- }
-
- // 验证用户身份
- const user = authenticateUser(username, password);
- if (!user) {
- return new Response(JSON.stringify({ error: '用户认证失败' }), {
- status: 401,
- headers: { 'Content-Type': 'application/json' },
+ headers: { 'Content-Type': 'application/json' }
});
}
// 删除音乐记录
- const success = deleteMusic(musicId, user.id);
+ const success = deleteMusic(musicId.toString(), user!.id);
if (!success) {
- return new Response(JSON.stringify({ error: '音乐不存在或无权限删除' }), {
+ return new Response(JSON.stringify({
+ error: '音乐不存在或无权限删除'
+ }), {
status: 404,
- headers: { 'Content-Type': 'application/json' },
+ headers: { 'Content-Type': 'application/json' }
});
}
@@ -47,23 +53,10 @@ export const POST: APIRoute = async ({ request }) => {
message: '音乐删除成功'
}), {
status: 200,
- headers: { 'Content-Type': 'application/json' },
+ headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
- console.error('删除音乐错误:', error);
-
- let errorMessage = '删除音乐失败,请稍后再试';
-
- if (error instanceof SyntaxError && error.message.includes('JSON')) {
- errorMessage = '请求格式错误';
- } else if (error instanceof Error) {
- errorMessage = error.message;
- }
-
- return new Response(JSON.stringify({ error: errorMessage }), {
- status: 500,
- headers: { 'Content-Type': 'application/json' },
- });
+ return handleApiError(error, '删除音乐');
}
};
\ No newline at end of file
diff --git a/src/pages/api/auth/music/list.ts b/src/pages/api/auth/music/list.ts
index 61217af..8a842d8 100644
--- a/src/pages/api/auth/music/list.ts
+++ b/src/pages/api/auth/music/list.ts
@@ -1,75 +1,63 @@
import type { APIRoute } from 'astro';
import { getUserMusic } from '@/lib/database';
-import { authenticateUser } from '@/lib/database';
+import { authenticateJWTRequest, handleApiError } from '@/lib/jwt-auth-middleware';
export const POST: APIRoute = async ({ request }) => {
+ // 使用JWT认证中间件
+ const authResult = await authenticateJWTRequest(request);
+ if (!authResult.success) {
+ return new Response(JSON.stringify({ error: authResult.error!.message }), {
+ status: authResult.error!.status,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
try {
- const body = await request.text();
-
- if (!body.trim()) {
- return new Response(JSON.stringify({ error: '请求体不能为空' }), {
- status: 400,
- headers: { 'Content-Type': 'application/json' },
- });
- }
-
- const { username, password } = JSON.parse(body);
-
- // 验证输入
- if (!username || !password) {
- return new Response(JSON.stringify({ error: '用户名和密码不能为空' }), {
- status: 400,
- headers: { 'Content-Type': 'application/json' },
- });
- }
-
- // 验证用户身份
- const user = authenticateUser(username, password);
- if (!user) {
- return new Response(JSON.stringify({ error: '用户认证失败' }), {
- status: 401,
- headers: { 'Content-Type': 'application/json' },
- });
- }
+ const { user } = authResult;
// 获取用户音乐列表
- const musicList = getUserMusic(user.id);
+ const musicList = getUserMusic(user!.id);
- // 解析JSON字段
- const formattedMusicList = musicList.map(music => ({
- id: music.id,
- name: music.name,
- sounds: JSON.parse(music.sounds),
- volume: JSON.parse(music.volume),
- speed: JSON.parse(music.speed),
- rate: JSON.parse(music.rate),
- random_effects: JSON.parse(music.random_effects),
- created_at: music.created_at,
- updated_at: music.updated_at
- }));
+ // 解析JSON字段并格式化数据
+ const formattedMusicList = musicList.map(music => {
+ try {
+ return {
+ id: music.id,
+ name: music.name,
+ sounds: JSON.parse(music.sounds),
+ volume: JSON.parse(music.volume),
+ speed: JSON.parse(music.speed),
+ rate: JSON.parse(music.rate),
+ random_effects: JSON.parse(music.random_effects),
+ created_at: music.created_at,
+ updated_at: music.updated_at
+ };
+ } catch (parseError) {
+ console.error(`解析音乐数据失败 (ID: ${music.id}):`, parseError);
+ // 返回安全的默认值
+ return {
+ id: music.id,
+ name: music.name,
+ sounds: [],
+ volume: {},
+ speed: {},
+ rate: {},
+ random_effects: {},
+ created_at: music.created_at,
+ updated_at: music.updated_at
+ };
+ }
+ });
return new Response(JSON.stringify({
success: true,
musicList: formattedMusicList
}), {
status: 200,
- headers: { 'Content-Type': 'application/json' },
+ headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
- console.error('获取音乐列表错误:', error);
-
- let errorMessage = '获取音乐列表失败,请稍后再试';
-
- if (error instanceof SyntaxError && error.message.includes('JSON')) {
- errorMessage = '请求格式错误';
- } else if (error instanceof Error) {
- errorMessage = error.message;
- }
-
- return new Response(JSON.stringify({ error: errorMessage }), {
- status: 500,
- headers: { 'Content-Type': 'application/json' },
- });
+ return handleApiError(error, '获取音乐列表');
}
};
\ No newline at end of file
diff --git a/src/pages/api/auth/music/rename.ts b/src/pages/api/auth/music/rename.ts
index 264d731..ba1f973 100644
--- a/src/pages/api/auth/music/rename.ts
+++ b/src/pages/api/auth/music/rename.ts
@@ -1,44 +1,69 @@
import type { APIRoute } from 'astro';
import { updateMusicName } from '@/lib/database';
-import { authenticateUser } from '@/lib/database';
+import { authenticateJWTRequest, parseRequestBody, handleApiError } from '@/lib/jwt-auth-middleware';
export const POST: APIRoute = async ({ request }) => {
+ // 首先进行JWT认证
+ const authResult = await authenticateJWTRequest(request);
+ if (!authResult.success) {
+ return new Response(JSON.stringify({ error: authResult.error!.message }), {
+ status: authResult.error!.status,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ // 解析请求体
+ const bodyResult = await parseRequestBody(request, ['musicId', 'name']);
+ if (!bodyResult.success) {
+ return new Response(JSON.stringify({ error: bodyResult.error!.message }), {
+ status: bodyResult.error!.status,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
try {
- const body = await request.text();
-
- if (!body.trim()) {
- return new Response(JSON.stringify({ error: '请求体不能为空' }), {
- status: 400,
- headers: { 'Content-Type': 'application/json' },
- });
- }
-
- const { musicId, name, username, password } = JSON.parse(body);
+ const { user } = authResult;
+ const { data } = bodyResult;
+ const { musicId, name } = data;
// 验证输入
- if (!musicId || !name || !username || !password) {
- return new Response(JSON.stringify({ error: '音乐ID、新名称和用户信息不能为空' }), {
+ if (!musicId || (typeof musicId !== 'string' && typeof musicId !== 'number')) {
+ return new Response(JSON.stringify({
+ error: '音乐ID不能为空且必须是有效的标识符'
+ }), {
status: 400,
- headers: { 'Content-Type': 'application/json' },
+ headers: { 'Content-Type': 'application/json' }
});
}
- // 验证用户身份
- const user = authenticateUser(username, password);
- if (!user) {
- return new Response(JSON.stringify({ error: '用户认证失败' }), {
- status: 401,
- headers: { 'Content-Type': 'application/json' },
+ if (!name || typeof name !== 'string' || name.trim().length === 0) {
+ return new Response(JSON.stringify({
+ error: '音乐名称不能为空且必须是有效的字符串'
+ }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ // 验证名称长度
+ if (name.trim().length > 100) {
+ return new Response(JSON.stringify({
+ error: '音乐名称长度不能超过100个字符'
+ }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' }
});
}
// 更新音乐名称
- const success = updateMusicName(musicId, name, user.id);
+ const success = updateMusicName(musicId.toString(), name.trim(), user!.id);
if (!success) {
- return new Response(JSON.stringify({ error: '音乐不存在或无权限修改' }), {
+ return new Response(JSON.stringify({
+ error: '音乐不存在或无权限修改'
+ }), {
status: 404,
- headers: { 'Content-Type': 'application/json' },
+ headers: { 'Content-Type': 'application/json' }
});
}
@@ -47,23 +72,10 @@ export const POST: APIRoute = async ({ request }) => {
message: '音乐名称更新成功'
}), {
status: 200,
- headers: { 'Content-Type': 'application/json' },
+ headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
- console.error('重命名音乐错误:', error);
-
- let errorMessage = '重命名音乐失败,请稍后再试';
-
- if (error instanceof SyntaxError && error.message.includes('JSON')) {
- errorMessage = '请求格式错误';
- } else if (error instanceof Error) {
- errorMessage = error.message;
- }
-
- return new Response(JSON.stringify({ error: errorMessage }), {
- status: 500,
- headers: { 'Content-Type': 'application/json' },
- });
+ return handleApiError(error, '重命名音乐');
}
};
\ No newline at end of file
diff --git a/src/pages/api/auth/music/save.ts b/src/pages/api/auth/music/save.ts
index 052c5f7..8b9d7cd 100644
--- a/src/pages/api/auth/music/save.ts
+++ b/src/pages/api/auth/music/save.ts
@@ -1,40 +1,44 @@
import type { APIRoute } from 'astro';
import { createMusic } from '@/lib/database';
-import { authenticateUser } from '@/lib/database';
+import { authenticateJWTRequest, parseRequestBody, handleApiError } from '@/lib/jwt-auth-middleware';
export const POST: APIRoute = async ({ request }) => {
+ // 首先进行JWT认证
+ const authResult = await authenticateJWTRequest(request);
+ if (!authResult.success) {
+ return new Response(JSON.stringify({ error: authResult.error!.message }), {
+ status: authResult.error!.status,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ // 解析请求体
+ const bodyResult = await parseRequestBody(request, ['name', 'sounds']);
+ if (!bodyResult.success) {
+ return new Response(JSON.stringify({ error: bodyResult.error!.message }), {
+ status: bodyResult.error!.status,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
try {
- const body = await request.text();
-
- if (!body.trim()) {
- return new Response(JSON.stringify({ error: '请求体不能为空' }), {
- status: 400,
- headers: { 'Content-Type': 'application/json' },
- });
- }
-
- const { name, sounds, volume, speed, rate, random_effects, username, password } = JSON.parse(body);
+ const { user } = authResult;
+ const { data } = bodyResult;
+ const { name, sounds, volume, speed, rate, random_effects } = data;
// 验证输入
- if (!name || !sounds || !username || !password) {
- return new Response(JSON.stringify({ error: '音乐名称、声音配置和用户信息不能为空' }), {
+ if (!name || !sounds || !Array.isArray(sounds)) {
+ return new Response(JSON.stringify({
+ error: '音乐名称和声音配置不能为空,声音必须是数组'
+ }), {
status: 400,
- headers: { 'Content-Type': 'application/json' },
- });
- }
-
- // 验证用户身份
- const user = authenticateUser(username, password);
- if (!user) {
- return new Response(JSON.stringify({ error: '用户认证失败' }), {
- status: 401,
- headers: { 'Content-Type': 'application/json' },
+ headers: { 'Content-Type': 'application/json' }
});
}
// 创建音乐记录
const music = await createMusic({
- user_id: user.id,
+ user_id: user!.id,
name,
sounds,
volume: volume || {},
@@ -45,6 +49,7 @@ export const POST: APIRoute = async ({ request }) => {
return new Response(JSON.stringify({
success: true,
+ message: '音乐保存成功',
music: {
id: music.id,
name: music.name,
@@ -52,23 +57,10 @@ export const POST: APIRoute = async ({ request }) => {
}
}), {
status: 201,
- headers: { 'Content-Type': 'application/json' },
+ headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
- console.error('保存音乐错误:', error);
-
- let errorMessage = '保存音乐失败,请稍后再试';
-
- if (error instanceof SyntaxError && error.message.includes('JSON')) {
- errorMessage = '请求格式错误';
- } else if (error instanceof Error) {
- errorMessage = error.message;
- }
-
- return new Response(JSON.stringify({ error: errorMessage }), {
- status: 500,
- headers: { 'Content-Type': 'application/json' },
- });
+ return handleApiError(error, '保存音乐');
}
};
\ No newline at end of file
diff --git a/src/pages/api/auth/register.ts b/src/pages/api/auth/register.ts
index 45d64d7..5b4fa61 100644
--- a/src/pages/api/auth/register.ts
+++ b/src/pages/api/auth/register.ts
@@ -1,5 +1,6 @@
import type { APIRoute } from 'astro';
import { createUser } from '@/lib/database';
+import { createJWT } from '@/lib/jwt';
export const POST: APIRoute = async ({ request }) => {
try {
@@ -39,13 +40,21 @@ export const POST: APIRoute = async ({ request }) => {
// 创建用户
const user = await createUser({ username, password });
+ // 创建JWT token
+ const token = createJWT({
+ userId: user.id,
+ username: user.username
+ });
+
return new Response(JSON.stringify({
success: true,
user: {
id: user.id,
username: user.username,
created_at: user.created_at
- }
+ },
+ token,
+ expiresIn: 7 * 24 * 60 * 60 // 7天,单位:秒
}), {
status: 201,
headers: { 'Content-Type': 'application/json' },
diff --git a/src/stores/auth.ts b/src/stores/auth.ts
index feb1024..112ab54 100644
--- a/src/stores/auth.ts
+++ b/src/stores/auth.ts
@@ -12,7 +12,7 @@ interface AuthState {
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
- sessionPassword: string | null; // 仅当前会话使用的密码,不持久化
+ token: string | null; // JWT token
}
interface AuthStore extends AuthState {
@@ -23,6 +23,8 @@ interface AuthStore extends AuthState {
clearError: () => void;
setLoading: (loading: boolean) => void;
checkAuth: () => Promise;
+ getToken: () => string | null;
+ setToken: (token: string) => void;
}
/**
@@ -57,7 +59,7 @@ export const useAuthStore = create()(
isAuthenticated: false,
isLoading: false,
error: null,
- sessionPassword: null,
+ token: null,
// Actions
login: async (userData) => {
@@ -66,20 +68,14 @@ export const useAuthStore = create()(
try {
const result = await apiCall('/api/auth/login', userData);
const user = result.user;
+ const token = result.token;
set({
user,
isAuthenticated: true,
isLoading: false,
error: null,
- });
-
- set({
- user,
- isAuthenticated: true,
- isLoading: false,
- error: null,
- sessionPassword: userData.password, // 保存密码用于当前会话的API调用
+ token,
});
console.log('✅ 用户登录成功:', user.username);
@@ -90,7 +86,7 @@ export const useAuthStore = create()(
isAuthenticated: false,
isLoading: false,
error: errorMessage,
- sessionPassword: null,
+ token: null,
});
console.error('❌ 登录失败:', error);
throw error;
@@ -103,20 +99,14 @@ export const useAuthStore = create()(
try {
const result = await apiCall('/api/auth/register', userData);
const user = result.user;
+ const token = result.token;
set({
user,
isAuthenticated: true,
isLoading: false,
error: null,
- });
-
- set({
- user,
- isAuthenticated: true,
- isLoading: false,
- error: null,
- sessionPassword: userData.password, // 保存密码用于当前会话的API调用
+ token,
});
console.log('✅ 用户注册成功:', user.username);
@@ -127,7 +117,7 @@ export const useAuthStore = create()(
isAuthenticated: false,
isLoading: false,
error: errorMessage,
- sessionPassword: null,
+ token: null,
});
console.error('❌ 注册失败:', error);
throw error;
@@ -140,7 +130,7 @@ export const useAuthStore = create()(
isAuthenticated: false,
isLoading: false,
error: null,
- sessionPassword: null, // 清除会话密码
+ token: null,
});
console.log('✅ 用户已登出');
},
@@ -154,37 +144,33 @@ export const useAuthStore = create()(
},
checkAuth: async () => {
- const { user, isAuthenticated } = get();
+ const { user, isAuthenticated, token } = get();
// 如果已经有用户信息且已认证,则直接返回
- if (user && isAuthenticated) {
+ if (user && isAuthenticated && token) {
return;
}
- // 这里可以添加token验证或会话验证逻辑
- // 目前简单检查本地存储的用户信息
- set({ isLoading: true });
-
- try {
- // 如果有用户信息但未认证,可以尝试验证
- if (user && !isAuthenticated) {
- set({
- isAuthenticated: true,
- isLoading: false,
- error: null,
- });
- } else {
- set({ isLoading: false });
- }
- } catch (error) {
- console.error('❌ 认证检查失败:', error);
+ // zustand persist会自动从localStorage恢复token
+ // 如果有token但没有用户信息,说明token可能无效
+ if (token && !user) {
+ console.warn('发现token但缺少用户信息,可能需要重新登录');
set({
- user: null,
- isAuthenticated: false,
- isLoading: false,
- error: '认证检查失败',
+ token: null,
+ isAuthenticated: false
});
}
+
+ set({ isLoading: false });
+ },
+
+ getToken: () => {
+ const { token } = get();
+ return token;
+ },
+
+ setToken: (newToken: string) => {
+ set({ token: newToken });
},
}),
{
@@ -192,7 +178,7 @@ export const useAuthStore = create()(
partialize: (state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
- // 不包含 sessionPassword,仅存储在内存中
+ token: state.token, // 现在也保存token
}),
}
)
diff --git a/test-jwt.js b/test-jwt.js
new file mode 100644
index 0000000..ebf9ff4
--- /dev/null
+++ b/test-jwt.js
@@ -0,0 +1,71 @@
+import crypto from 'crypto';
+
+// JWT配置 (与src/lib/jwt.ts保持一致)
+const JWT_SECRET = 'your-secret-key-change-in-production';
+const JWT_ALGORITHM = 'HS256';
+const JWT_EXPIRES_IN = 7 * 24 * 60 * 60; // 7天
+
+function base64UrlEncode(data) {
+ return Buffer.from(data)
+ .toString('base64')
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=/g, '');
+}
+
+function createJWT(payload) {
+ const header = { alg: JWT_ALGORITHM, typ: 'JWT' };
+ const now = Math.floor(Date.now() / 1000);
+ const jwtPayload = { ...payload, iat: now, exp: now + JWT_EXPIRES_IN };
+
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
+ const encodedPayload = base64UrlEncode(JSON.stringify(jwtPayload));
+ const signatureInput = `${encodedHeader}.${encodedPayload}`;
+
+ const signature = crypto
+ .createHmac('sha256', JWT_SECRET)
+ .update(signatureInput)
+ .digest('base64url');
+
+ return `${signatureInput}.${signature}`;
+}
+
+// 创建测试用的JWT token (用户ID: 1, username: test123)
+const testToken = createJWT({
+ userId: 1,
+ username: 'test123'
+});
+
+console.log('测试JWT Token:', testToken);
+
+// 测试API调用
+async function testMusicAPI() {
+ try {
+ const response = await fetch('http://localhost:4323/api/auth/music/list', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${testToken}`
+ },
+ body: JSON.stringify({})
+ });
+
+ console.log('API响应状态:', response.status);
+ console.log('API响应头:', Object.fromEntries(response.headers.entries()));
+
+ const text = await response.text();
+ console.log('API响应内容:', text);
+
+ try {
+ const data = JSON.parse(text);
+ console.log('解析后的数据:', data);
+ } catch (e) {
+ console.log('响应不是有效的JSON');
+ }
+
+ } catch (error) {
+ console.error('API调用失败:', error);
+ }
+}
+
+testMusicAPI();
\ No newline at end of file
diff --git a/users.db b/users.db
new file mode 100644
index 0000000..e69de29