u
This commit is contained in:
@@ -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;
|
||||
};
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 // 启用
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user