diff --git a/src/client/admin/hooks/AuthProvider.tsx b/src/client/admin/hooks/AuthProvider.tsx index eddb351..f7d0ec9 100644 --- a/src/client/admin/hooks/AuthProvider.tsx +++ b/src/client/admin/hooks/AuthProvider.tsx @@ -1,158 +1,140 @@ import React, { useState, useEffect, createContext, useContext } from 'react'; import { - useQuery, - useQueryClient, + useQuery, + useQueryClient, } from '@tanstack/react-query'; import axios from 'axios'; import 'dayjs/locale/zh-cn'; import type { - User, AuthContextType + AuthContextType } from '@/share/types'; import { authClient } from '@/client/api'; +import type { InferResponseType, InferRequestType } from 'hono/client'; + +type User = InferResponseType; + // 创建认证上下文 -const AuthContext = createContext(null); +const AuthContext = createContext | null>(null); // 认证提供器组件 export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [user, setUser] = useState(null); - const [token, setToken] = useState(localStorage.getItem('token')); - const [isAuthenticated, setIsAuthenticated] = useState(false); - const queryClient = useQueryClient(); + const [user, setUser] = useState(null); + const [token, setToken] = useState(localStorage.getItem('token')); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const queryClient = useQueryClient(); - // 声明handleLogout函数 - const handleLogout = async () => { - try { - // 如果已登录,调用登出API - if (token) { - await authClient.logout.$post(); - } - } catch (error) { - console.error('登出请求失败:', error); - } finally { - // 清除本地状态 - setToken(null); - setUser(null); - setIsAuthenticated(false); - localStorage.removeItem('token'); - // 清除Authorization头 - delete axios.defaults.headers.common['Authorization']; - console.log('登出时已删除全局Authorization头'); - // 清除所有查询缓存 - queryClient.clear(); + // 声明handleLogout函数 + const handleLogout = async () => { + try { + // 如果已登录,调用登出API + if (token) { + await authClient.logout.$post(); + } + } catch (error) { + console.error('登出请求失败:', error); + } finally { + // 清除本地状态 + setToken(null); + setUser(null); + setIsAuthenticated(false); + localStorage.removeItem('token'); + // 清除Authorization头 + delete axios.defaults.headers.common['Authorization']; + console.log('登出时已删除全局Authorization头'); + // 清除所有查询缓存 + queryClient.clear(); + } + }; + + // 使用useQuery检查登录状态 + const { isLoading } = useQuery({ + queryKey: ['auth', 'status', token], + queryFn: async () => { + if (!token) { + setIsAuthenticated(false); + setUser(null); + return null; + } + + try { + // 设置全局默认请求头 + axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; + // 使用API验证当前用户 + const res = await authClient.me.$get(); + if (res.status !== 200) { + const result = await res.json(); + throw new Error(result.message) } - }; + const currentUser = await res.json(); + setUser(currentUser); + setIsAuthenticated(true); + return { isValid: true, user: currentUser }; + } catch (error) { + return { isValid: false }; + } + }, + enabled: !!token, + refetchOnWindowFocus: false, + retry: false + }); - // 使用useQuery检查登录状态 - const { isLoading } = useQuery({ - queryKey: ['auth', 'status', token], - queryFn: async () => { - if (!token) { - setIsAuthenticated(false); - setUser(null); - return null; - } - - try { - // 设置全局默认请求头 - axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; - // 使用API验证当前用户 - const res = await authClient.me.$get(); - if(res.status !== 200){ - const result = await res.json(); - throw new Error(result.message) - } - const currentUser = await res.json(); - setUser(currentUser); - setIsAuthenticated(true); - return { isValid: true, user: currentUser }; - } catch (error) { - // 如果API调用失败,自动登出 - // handleLogout(); - return { isValid: false }; - } - }, - enabled: !!token, - refetchOnWindowFocus: false, - retry: false - }); - - // 设置请求拦截器 - useEffect(() => { - // 设置响应拦截器处理401错误 - const responseInterceptor = axios.interceptors.response.use( - (response) => response, - (error) => { - if (error.response?.status === 401) { - console.log('检测到401错误,执行登出操作'); - handleLogout(); - } - return Promise.reject(error); - } - ); - - // 清理拦截器 - return () => { - axios.interceptors.response.eject(responseInterceptor); - }; - }, [token]); - - const handleLogin = async (username: string, password: string, latitude?: number, longitude?: number): Promise => { - try { - // 使用AuthAPI登录 - const response = await authClient.login.$post({ - json: { - username, - password - } - }) - if (response.status !== 200) { - const result = await response.json() - throw new Error(result.message); - } - - const result = await response.json() - - // 保存token和用户信息 - const { token: newToken, user: newUser } = result; - - // 设置全局默认请求头 - axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`; - - // 保存状态 - setToken(newToken); - setUser(newUser); - setIsAuthenticated(true); - localStorage.setItem('token', newToken); - - } catch (error) { - console.error('登录失败:', error); - throw error; + const handleLogin = async (username: string, password: string, latitude?: number, longitude?: number): Promise => { + try { + // 使用AuthAPI登录 + const response = await authClient.login.$post({ + json: { + username, + password } - }; + }) + if (response.status !== 200) { + const result = await response.json() + throw new Error(result.message); + } - return ( - - {children} - - ); + const result = await response.json() + + // 保存token和用户信息 + const { token: newToken, user: newUser } = result; + + // 设置全局默认请求头 + axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`; + + // 保存状态 + setToken(newToken); + setUser(newUser); + setIsAuthenticated(true); + localStorage.setItem('token', newToken); + + } catch (error) { + console.error('登录失败:', error); + throw error; + } + }; + + return ( + + {children} + + ); }; // 使用上下文的钩子 export const useAuth = () => { - const context = useContext(AuthContext); - if (!context) { - throw new Error('useAuth必须在AuthProvider内部使用'); - } - return context; + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth必须在AuthProvider内部使用'); + } + return context; }; \ No newline at end of file diff --git a/src/server/modules/users/user.entity.ts b/src/server/modules/users/user.entity.ts index 69c2ef6..30fcfe5 100644 --- a/src/server/modules/users/user.entity.ts +++ b/src/server/modules/users/user.entity.ts @@ -1,7 +1,7 @@ import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'; import { Role, RoleSchema } from './role.entity'; -import { MessageEntity } from './message.entity'; -import { z } from 'zod'; +import { z } from '@hono/zod-openapi'; +import { DeleteStatus, DisabledStatus } from '@/share/types'; @Entity({ name: 'users' }) export class UserEntity { @@ -26,19 +26,19 @@ export class UserEntity { @Column({ name: 'name', type: 'varchar', length: 255, nullable: true, comment: '真实姓名' }) name!: string | null; - @Column({ name: 'is_disabled', type: 'int', default: 0, comment: '是否禁用(0:启用,1:禁用)' }) - isDisabled!: number; + @Column({ name: 'avatar', type: 'varchar', length: 255, nullable: true, comment: '头像' }) + avatar!: string | null; - @Column({ name: 'is_deleted', type: 'int', default: 0, comment: '是否删除(0:未删除,1:已删除)' }) - isDeleted!: number; + @Column({ name: 'is_disabled', type: 'int', default: DisabledStatus.ENABLED, comment: '是否禁用(0:启用,1:禁用)' }) + isDisabled!: DisabledStatus; + + @Column({ name: 'is_deleted', type: 'int', default: DeleteStatus.NOT_DELETED, comment: '是否删除(0:未删除,1:已删除)' }) + isDeleted!: DeleteStatus; @ManyToMany(() => Role) @JoinTable() roles!: Role[]; - @OneToMany(() => MessageEntity, (message) => message.sender) - senderMessages!: MessageEntity[]; - @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) createdAt!: Date; @@ -76,12 +76,16 @@ export const UserSchema = z.object({ example: '张三', description: '真实姓名' }), - isDisabled: z.number().int().min(0).max(1).default(0).openapi({ - example: 0, + avatar: z.string().max(255).nullable().openapi({ + example: 'https://example.com/avatar.jpg', + description: '用户头像' + }), + isDisabled: z.number().int().min(0).max(1).default(DisabledStatus.ENABLED).openapi({ + example: DisabledStatus.ENABLED, description: '是否禁用(0:启用,1:禁用)' }), - isDeleted: z.number().int().min(0).max(1).default(0).openapi({ - example: 0, + isDeleted: z.number().int().min(0).max(1).default(DeleteStatus.NOT_DELETED).openapi({ + example: DeleteStatus.NOT_DELETED, description: '是否删除(0:未删除,1:已删除)' }), roles: z.array(RoleSchema).optional().openapi({ diff --git a/src/server/types/smsTypes.ts b/src/server/types/smsTypes.ts deleted file mode 100644 index 414bf14..0000000 --- a/src/server/types/smsTypes.ts +++ /dev/null @@ -1,69 +0,0 @@ -export interface DeviceStatus { - signalStrength: number; - carrier: string; - mode: '短信' | '电话' | '语音' | '余额查询'; -} - -export interface SmsItem { - id: string; - phone: string; - content: string; - taskId: string; - status: 'pending' | 'success' | 'failed'; - createdAt: string; - updatedAt: string; -} - -export interface SmsResponse { - data: { - list: SmsItem[]; - status: DeviceStatus; - }; -} - -// 短信接口配置 -export interface SmsConfig { - apiUrl: string; - username: string; - encryptedPassword: string; // 加密存储的密码 - timeout?: number; // 超时时间(毫秒) - maxRetries?: number; // 最大重试次数 - enableMock?: boolean; // 是否启用模拟模式 -} - -// API请求/响应类型 -export interface SmsApiRequest { - phone: string; - content: string; - signName?: string; - templateCode?: string; - username?: string; // 认证用户名 - password?: string; // 认证密码 -} - -export interface SmsApiResponse { - success: boolean; - code: string; - message?: string; - data?: { - taskId: string; - serialNumbers?: string[]; - }; -} - -// 错误类型 -export interface SmsError { - code: string; - message: string; - timestamp: string; - stack?: string; -} - -// 性能指标 -export interface SmsMetrics { - requestCount: number; - successCount: number; - failureCount: number; - averageLatency: number; - lastRequestTime?: string; -} \ No newline at end of file diff --git a/src/server/utils/env-init.ts b/src/server/utils/env-init.ts deleted file mode 100644 index 5d495da..0000000 --- a/src/server/utils/env-init.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { randomBytes } from 'crypto'; -import { writeFileSync, existsSync, readFileSync, appendFileSync } from 'fs'; - -export function generateJwtSecret(): string { - if (existsSync('.env')) { - const envContent = readFileSync('.env', 'utf-8'); - if (envContent.includes('JWT_SECRET')) { - return 'JWT_SECRET已存在'; - } - } - - const secret = randomBytes(64).toString('hex'); - const envLine = `JWT_SECRET=${secret}\n`; - - if (existsSync('.env')) { - appendFileSync('.env', envLine); - } else { - writeFileSync('.env', envLine); - } - - return secret; -} - -export function checkRequiredEnvVars(): boolean { - const requiredVars = [ - 'DB_HOST', - 'DB_PORT', - 'DB_USERNAME', - 'DB_PASSWORD', - 'DB_DATABASE', - 'JWT_SECRET' - ]; - - if (!existsSync('.env')) return false; - - const envContent = readFileSync('.env', 'utf-8'); - return requiredVars.every(varName => envContent.includes(varName)); -} \ No newline at end of file diff --git a/src/server/utils/sms.aliyun.ts b/src/server/utils/sms.aliyun.ts deleted file mode 100644 index 68a2b9c..0000000 --- a/src/server/utils/sms.aliyun.ts +++ /dev/null @@ -1,73 +0,0 @@ -import Dysmsapi20170525, * as $Dysmsapi20170525 from '@alicloud/dysmsapi20170525'; -import OpenApi, * as $OpenApi from '@alicloud/openapi-client'; -import Util, * as $Util from '@alicloud/tea-util'; -import process from 'node:process' - -type SMSError = { - message?: string; - data?: { - Recommend?: string; - }; -}; - -export class SMS { - /** - * 创建阿里云短信客户端 - * @throws {Error} 当阿里云配置缺失时抛出错误 - */ - static createClient(): Dysmsapi20170525 { - const accessKeyId = process.env.ALICLOUD_ACCESS_KEY; - const accessKeySecret = process.env.ALICLOUD_SECRET_KEY; - - if (!accessKeyId || !accessKeySecret) { - throw new Error('阿里云配置缺失'); - } - - const config = new $OpenApi.Config({ - accessKeyId, - accessKeySecret, - endpoint: 'dysmsapi.aliyuncs.com' - }); - - return new (Dysmsapi20170525 as any).default(config); - } - - /** - * 发送验证码短信 - * @param phoneNumber 接收手机号 - * @param code 验证码 - * @param templateCode 短信模板代码(可选) - * @param signName 短信签名(可选) - * @returns Promise 发送是否成功 - */ - static async sendVerificationSMS( - phoneNumber: string, - code: string, - templateCode?: string, - signName?: string - ): Promise { - try { - const client = this.createClient(); - - const sendSmsRequest = new $Dysmsapi20170525.SendSmsRequest({ - signName: signName || process.env.SMS_DEFAULT_SIGN_NAME || '多八多', - templateCode: templateCode || process.env.SMS_DEFAULT_TEMPLATE_CODE || 'SMS_164760103', - phoneNumbers: phoneNumber, - templateParam: JSON.stringify({ code }) - }); - - const runtime = new $Util.RuntimeOptions({}); - await client.sendSmsWithOptions(sendSmsRequest, runtime); - return true; - } catch (error: unknown) { - const err = error as SMSError; - console.error('短信发送失败:', err.message); - - if (err.data?.Recommend) { - console.error('建议:', err.data.Recommend); - } - - return false; - } - } -} \ No newline at end of file diff --git a/src/share/types.ts b/src/share/types.ts index 25d2949..1b85ae7 100644 --- a/src/share/types.ts +++ b/src/share/types.ts @@ -13,3 +13,34 @@ export interface AuthContextType { isAuthenticated: boolean; isLoading: boolean; } + +// 启用/禁用状态枚举 +export enum EnableStatus { + DISABLED = 0, // 禁用 + ENABLED = 1 // 启用 +} + +// 启用/禁用状态中文映射 +export const EnableStatusNameMap: Record = { + [EnableStatus.DISABLED]: '禁用', + [EnableStatus.ENABLED]: '启用' +}; + +// 删除状态枚举 +export enum DeleteStatus { + NOT_DELETED = 0, // 未删除 + DELETED = 1 // 已删除 +} + +// 删除状态中文映射 +export const DeleteStatusNameMap: Record = { + [DeleteStatus.NOT_DELETED]: '未删除', + [DeleteStatus.DELETED]: '已删除' +}; + +// 启用/禁用状态枚举 +export enum DisabledStatus { + DISABLED = 1, // 禁用 + ENABLED = 0 // 启用 +} +