This commit is contained in:
D8D Developer
2025-06-27 03:02:46 +00:00
parent 24c1b80dda
commit 12c6ed592f
6 changed files with 164 additions and 327 deletions

View File

@@ -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<typeof authClient.me.$get, 200>;
// 创建认证上下文
const AuthContext = createContext<AuthContextType | null>(null);
const AuthContext = createContext<AuthContextType<User> | null>(null);
// 认证提供器组件
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const queryClient = useQueryClient();
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(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<void> => {
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<void> => {
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 (
<AuthContext.Provider
value={{
user,
token,
login: handleLogin,
logout: handleLogout,
isAuthenticated,
isLoading
}}
>
{children}
</AuthContext.Provider>
);
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 (
<AuthContext.Provider
value={{
user,
token,
login: handleLogin,
logout: handleLogout,
isAuthenticated,
isLoading
}}
>
{children}
</AuthContext.Provider>
);
};
// 使用上下文的钩子
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;
};

View File

@@ -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({

View File

@@ -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;
}

View File

@@ -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));
}

View File

@@ -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<boolean> 发送是否成功
*/
static async sendVerificationSMS(
phoneNumber: string,
code: string,
templateCode?: string,
signName?: string
): Promise<boolean> {
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;
}
}
}

View File

@@ -13,3 +13,34 @@ export interface AuthContextType<T> {
isAuthenticated: boolean;
isLoading: boolean;
}
// 启用/禁用状态枚举
export enum EnableStatus {
DISABLED = 0, // 禁用
ENABLED = 1 // 启用
}
// 启用/禁用状态中文映射
export const EnableStatusNameMap: Record<EnableStatus, string> = {
[EnableStatus.DISABLED]: '禁用',
[EnableStatus.ENABLED]: '启用'
};
// 删除状态枚举
export enum DeleteStatus {
NOT_DELETED = 0, // 未删除
DELETED = 1 // 已删除
}
// 删除状态中文映射
export const DeleteStatusNameMap: Record<DeleteStatus, string> = {
[DeleteStatus.NOT_DELETED]: '未删除',
[DeleteStatus.DELETED]: '已删除'
};
// 启用/禁用状态枚举
export enum DisabledStatus {
DISABLED = 1, // 禁用
ENABLED = 0 // 启用
}