生产管理系统 - 系统参数配置集成于首页loading
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,59 +0,0 @@
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Search } from 'lucide-react'
|
||||
|
||||
interface LoginLogFiltersProps {
|
||||
searchKeyword: string
|
||||
onSearchChange: (value: string) => void
|
||||
statusFilter: string
|
||||
onStatusFilterChange: (value: string) => void
|
||||
dateRange: string
|
||||
onDateRangeChange: (value: string) => void
|
||||
}
|
||||
|
||||
export function LoginLogFilters({
|
||||
searchKeyword,
|
||||
onSearchChange,
|
||||
statusFilter,
|
||||
onStatusFilterChange,
|
||||
dateRange,
|
||||
onDateRangeChange
|
||||
}: LoginLogFiltersProps) {
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索用户名、IP地址、位置..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="登录状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="success">成功</SelectItem>
|
||||
<SelectItem value="failed">失败</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={dateRange} onValueChange={onDateRangeChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="时间范围" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部时间</SelectItem>
|
||||
<SelectItem value="today">今天</SelectItem>
|
||||
<SelectItem value="week">近7天</SelectItem>
|
||||
<SelectItem value="month">近30天</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Shield } from 'lucide-react'
|
||||
|
||||
export function LoginLogInfo() {
|
||||
return (
|
||||
<Card className="p-4 bg-blue-50 border-blue-200">
|
||||
<h4 className="text-blue-900 mb-2">
|
||||
<Shield className="w-4 h-4 inline mr-2" />
|
||||
登录日志说明
|
||||
</h4>
|
||||
<ul className="space-y-1 text-sm text-blue-800">
|
||||
<li>• 记录所有用户的登录行为,包括成功和失败的登录尝试</li>
|
||||
<li>• 支持按用户名、IP地址、位置等多条件检索</li>
|
||||
<li>• 登录失败会记录失败原因,便于安全审计</li>
|
||||
<li>• 可用于识别异常登录行为和未授权访问尝试</li>
|
||||
<li>• 日志数据可导出用于长期归档和合规审计</li>
|
||||
</ul>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { LoginLog } from '@/types/monitor'
|
||||
|
||||
interface LoginLogStatsProps {
|
||||
logs: LoginLog[]
|
||||
}
|
||||
|
||||
export function LoginLogStats({ logs }: LoginLogStatsProps) {
|
||||
const stats = [
|
||||
{
|
||||
label: '总登录次数',
|
||||
value: logs.length,
|
||||
color: 'text-blue-600',
|
||||
bg: 'bg-blue-100',
|
||||
},
|
||||
{
|
||||
label: '成功登录',
|
||||
value: logs.filter(l => l.status === 'success').length,
|
||||
color: 'text-green-600',
|
||||
bg: 'bg-green-100',
|
||||
},
|
||||
{
|
||||
label: '失败登录',
|
||||
value: logs.filter(l => l.status === 'failed').length,
|
||||
color: 'text-red-600',
|
||||
bg: 'bg-red-100',
|
||||
},
|
||||
{
|
||||
label: '今日登录',
|
||||
value: logs.filter(l => {
|
||||
const logDate = new Date(l.loginTime)
|
||||
const today = new Date()
|
||||
return logDate.toDateString() === today.toDateString()
|
||||
}).length,
|
||||
color: 'text-purple-600',
|
||||
bg: 'bg-purple-100',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{stats.map((stat, index) => (
|
||||
<Card key={index} className="p-4">
|
||||
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
||||
<div className={`mt-2 ${stat.color}`}>{stat.value}</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { LoginLog } from '@/types/monitor'
|
||||
import { Shield, Monitor, MapPin } from 'lucide-react'
|
||||
|
||||
interface LoginLogTableProps {
|
||||
logs: LoginLog[]
|
||||
}
|
||||
|
||||
export function LoginLogTable({ logs }: LoginLogTableProps) {
|
||||
return (
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>用户名</TableHead>
|
||||
<TableHead>登录时间</TableHead>
|
||||
<TableHead>IP地址</TableHead>
|
||||
<TableHead>设备/浏览器</TableHead>
|
||||
<TableHead>位置</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{logs.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
||||
暂无登录日志
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
logs.map((log) => (
|
||||
<TableRow key={log.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-4 h-4 text-gray-400" />
|
||||
{log.username}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{new Date(log.loginTime).toLocaleString('zh-CN')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-xs px-2 py-1 rounded">
|
||||
{log.ipAddress}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Monitor className="w-4 h-4 text-gray-400" />
|
||||
<div className="text-sm">
|
||||
<div>{log.device}</div>
|
||||
{log.browser && (
|
||||
<div className="text-xs text-muted-foreground">{log.browser}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{log.location && (
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<MapPin className="w-3 h-3 text-gray-400" />
|
||||
{log.location}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{log.status === 'success' ? (
|
||||
<Badge className="bg-green-100 text-green-700">成功</Badge>
|
||||
) : (
|
||||
<div>
|
||||
<Badge className="bg-red-100 text-red-700">失败</Badge>
|
||||
{log.failReason && (
|
||||
<p className="text-xs text-red-600 mt-1">{log.failReason}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1 @@
|
||||
export { LoginLogStats } from './LoginLogStats'
|
||||
export { LoginLogFilters } from './LoginLogFilters'
|
||||
export { LoginLogTable } from './LoginLogTable'
|
||||
export { LoginLogInfo } from './LoginLogInfo'
|
||||
export * from './loginLogApi'
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* filekorolheader: 登录日志API - 登录日志相关接口调用
|
||||
* 功能:获取登录日志列表、统计、导出等功能
|
||||
* 路径:/central-config/monitor/login-log/components/loginLogApi
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用SDK生成的API接口
|
||||
*/
|
||||
import {
|
||||
listLoginLogsApiV1LogsLoginLoginLogsGet,
|
||||
getLoginStatisticsApiV1LogsLoginLoginLogsStatisticsGet,
|
||||
exportLoginLogsApiV1LogsLoginLoginLogsExportGet
|
||||
} from '@/lib/api/sdk.gen';
|
||||
|
||||
// 登录日志接口
|
||||
export interface LoginLog {
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
username: string;
|
||||
user_id: string | null;
|
||||
status: 'success' | 'failed';
|
||||
method: string;
|
||||
ip_address: string;
|
||||
user_agent: string;
|
||||
location: string | null;
|
||||
failure_reason: string | null;
|
||||
attempt_count: number;
|
||||
is_suspicious: boolean;
|
||||
}
|
||||
|
||||
// 分页参数接口
|
||||
export interface LoginLogsQueryParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
username?: string;
|
||||
status?: string;
|
||||
start_time?: string;
|
||||
end_time?: string;
|
||||
ip_address?: string;
|
||||
sort_order?: 'asc' | 'desc';
|
||||
order_by?: string;
|
||||
}
|
||||
|
||||
// 分页状态接口
|
||||
export interface PaginationState {
|
||||
page: number;
|
||||
size: number;
|
||||
total: number;
|
||||
totalPages?: number;
|
||||
hasNext?: boolean;
|
||||
hasPrev?: boolean;
|
||||
}
|
||||
|
||||
// 统计数据接口
|
||||
export interface LoginStatistics {
|
||||
total_logins: number;
|
||||
successful_logins: number;
|
||||
failed_logins: number;
|
||||
unique_users: number;
|
||||
success_rate: number;
|
||||
suspicious_logins: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录日志列表
|
||||
*/
|
||||
export const fetchLoginLogs = async (params: LoginLogsQueryParams = {}) => {
|
||||
try {
|
||||
// Get token from localStorage
|
||||
const storedUser = localStorage.getItem('user');
|
||||
let headers = {};
|
||||
|
||||
if (storedUser) {
|
||||
const userData = JSON.parse(storedUser);
|
||||
if (userData.token) {
|
||||
headers = {
|
||||
'Authorization': `Bearer ${userData.token}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const response = await listLoginLogsApiV1LogsLoginLoginLogsGet({
|
||||
headers,
|
||||
query: {
|
||||
page: params.page || 1,
|
||||
size: params.size || 10,
|
||||
username: params.username,
|
||||
status: params.status,
|
||||
start_time: params.start_time,
|
||||
end_time: params.end_time,
|
||||
ip_address: params.ip_address,
|
||||
sort_order: params.sort_order || 'desc',
|
||||
order_by: params.order_by || 'created_at',
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
data: response.data?.data || [],
|
||||
page: response.data?.page || 1,
|
||||
size: response.data?.size || 10,
|
||||
total: response.data?.total || 0,
|
||||
totalPages: response.data?.total_pages || 0,
|
||||
hasNext: response.data?.has_next || false,
|
||||
hasPrev: response.data?.has_prev || false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch login logs:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取登录统计信息
|
||||
*/
|
||||
export const fetchLoginStatistics = async () => {
|
||||
try {
|
||||
// Get token from localStorage
|
||||
const storedUser = localStorage.getItem('user');
|
||||
let headers = {};
|
||||
|
||||
if (storedUser) {
|
||||
const userData = JSON.parse(storedUser);
|
||||
if (userData.token) {
|
||||
headers = {
|
||||
'Authorization': `Bearer ${userData.token}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const response = await getLoginStatisticsApiV1LogsLoginLoginLogsStatisticsGet({
|
||||
headers
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch login statistics:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出登录日志
|
||||
*/
|
||||
export const exportLoginLogs = async (params: LoginLogsQueryParams = {}) => {
|
||||
try {
|
||||
// Get token from localStorage
|
||||
const storedUser = localStorage.getItem('user');
|
||||
let headers = {};
|
||||
|
||||
if (storedUser) {
|
||||
const userData = JSON.parse(storedUser);
|
||||
if (userData.token) {
|
||||
headers = {
|
||||
'Authorization': `Bearer ${userData.token}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const response = await exportLoginLogsApiV1LogsLoginLoginLogsExportGet({
|
||||
headers,
|
||||
query: {
|
||||
start_time: params.start_time,
|
||||
end_time: params.end_time,
|
||||
user_id: params.username, // Map username to user_id for export
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to export login logs:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 转换登录日志数据 - 适配组件使用
|
||||
*/
|
||||
export const transformLoginLogData = (log: any): LoginLog => ({
|
||||
id: log.id,
|
||||
created_at: log.created_at,
|
||||
updated_at: log.updated_at,
|
||||
username: log.username,
|
||||
user_id: log.user_id,
|
||||
status: log.status,
|
||||
method: log.method,
|
||||
ip_address: log.ip_address,
|
||||
user_agent: log.user_agent,
|
||||
location: log.location,
|
||||
failure_reason: log.failure_reason,
|
||||
attempt_count: log.attempt_count,
|
||||
is_suspicious: log.is_suspicious,
|
||||
});
|
||||
|
||||
/**
|
||||
* 批量转换登录日志数据
|
||||
*/
|
||||
export const transformLoginLogsList = (logs: any[]): LoginLog[] => {
|
||||
return logs.map(transformLoginLogData);
|
||||
};
|
||||
@@ -1,202 +1,421 @@
|
||||
'use client'
|
||||
/**
|
||||
* filekorolheader: 登录日志页面 - 用户登录行为监控页面
|
||||
* 功能:登录日志查询、统计、导出、筛选
|
||||
* 路径:/central-config/monitor/login-log
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用SearchFormPagination重构,事件驱动模式
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { LoginLog } from '@/types/monitor'
|
||||
import { Download } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Download, Eye, AlertTriangle, CheckCircle, XCircle } from 'lucide-react';
|
||||
|
||||
// Import modular components
|
||||
import { SearchFormPagination, type SearchFieldConfig, type TableColumnConfig } from '@/components/common/searchFormPagination';
|
||||
import {
|
||||
LoginLogStats,
|
||||
LoginLogFilters,
|
||||
LoginLogTable,
|
||||
LoginLogInfo
|
||||
} from './components'
|
||||
fetchLoginLogs,
|
||||
transformLoginLogsList,
|
||||
LoginLog,
|
||||
PaginationState,
|
||||
LoginLogsQueryParams,
|
||||
fetchLoginStatistics,
|
||||
exportLoginLogs
|
||||
} from './components/loginLogApi';
|
||||
|
||||
export default function LoginLogPage() {
|
||||
const [logs, setLogs] = useState<LoginLog[]>([])
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all')
|
||||
const [dateRange, setDateRange] = useState<string>('all')
|
||||
const [logs, setLogs] = useState<LoginLog[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [statistics, setStatistics] = useState<any>(null);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
});
|
||||
const [searchFilters, setSearchFilters] = useState<Record<string, string>>({
|
||||
search: '',
|
||||
status: 'all'
|
||||
});
|
||||
const isFirstLoad = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadLogs()
|
||||
}, [])
|
||||
// 搜索字段配置
|
||||
const searchFields: SearchFieldConfig[] = [
|
||||
{
|
||||
key: 'search',
|
||||
label: '搜索',
|
||||
type: 'text',
|
||||
placeholder: '搜索用户名、IP地址...',
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: '登录状态',
|
||||
type: 'select',
|
||||
defaultValue: 'all',
|
||||
options: [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'success', label: '成功' },
|
||||
{ value: 'failed', label: '失败' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'dateRange',
|
||||
label: '时间范围',
|
||||
type: 'select',
|
||||
defaultValue: 'all',
|
||||
options: [
|
||||
{ value: 'all', label: '全部时间' },
|
||||
{ value: 'today', label: '今天' },
|
||||
{ value: 'week', label: '最近7天' },
|
||||
{ value: 'month', label: '最近30天' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const loadLogs = () => {
|
||||
// 强制重新加载mock数据以解决显示问题
|
||||
localStorage.removeItem('smart_agriculture_login_logs')
|
||||
// 表格列配置
|
||||
const columns: TableColumnConfig[] = [
|
||||
{
|
||||
key: 'created_at',
|
||||
label: '登录时间',
|
||||
render: (value: string) => (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{value ? new Date(value).toLocaleString('zh-CN') : '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
label: '用户名',
|
||||
render: (value: string) => (
|
||||
<div className="font-medium text-foreground">{value || '-'}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'ip_address',
|
||||
label: 'IP地址',
|
||||
render: (value: string) => (
|
||||
<div className="font-mono text-sm">{value || '-'}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'user_agent',
|
||||
label: '设备信息',
|
||||
render: (value: string) => (
|
||||
<div className="text-sm text-muted-foreground max-w-[200px] truncate" title={value}>
|
||||
{value || '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'location',
|
||||
label: '登录地点',
|
||||
render: (value: string) => (
|
||||
<div className="text-sm text-muted-foreground">{value || '-'}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: '状态',
|
||||
render: (value: string) => {
|
||||
if (value === 'success') {
|
||||
return (
|
||||
<Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
成功
|
||||
</Badge>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Badge className="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
|
||||
<XCircle className="w-3 h-3 mr-1" />
|
||||
失败
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'is_suspicious',
|
||||
label: '安全状态',
|
||||
render: (value: boolean) => {
|
||||
if (value) {
|
||||
return (
|
||||
<Badge className="bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200">
|
||||
<AlertTriangle className="w-3 h-3 mr-1" />
|
||||
可疑
|
||||
</Badge>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Badge className="bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
正常
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'failure_reason',
|
||||
label: '失败原因',
|
||||
render: (value: string) => (
|
||||
<div className="text-sm text-muted-foreground max-w-[150px] truncate" title={value}>
|
||||
{value || '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '操作',
|
||||
render: (_: any, row: LoginLog) => (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleViewDetail(row)}
|
||||
className="h-8 px-2"
|
||||
title="查看详情"
|
||||
>
|
||||
<Eye className="w-3 h-3 mr-1" />
|
||||
查看
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const mockLogs: LoginLog[] = [
|
||||
{
|
||||
id: 'log-1',
|
||||
userId: 'user-1',
|
||||
username: 'admin',
|
||||
loginTime: '2024-10-21T09:30:00',
|
||||
ipAddress: '192.168.1.100',
|
||||
device: 'Windows 11',
|
||||
browser: 'Chrome 118',
|
||||
os: 'Windows',
|
||||
location: '北京市海淀区',
|
||||
status: 'success',
|
||||
sessionId: 'sess-001',
|
||||
},
|
||||
{
|
||||
id: 'log-2',
|
||||
userId: 'user-2',
|
||||
username: 'zhangsan',
|
||||
loginTime: '2024-10-21T10:15:00',
|
||||
ipAddress: '192.168.1.101',
|
||||
device: 'macOS 14',
|
||||
browser: 'Safari 17',
|
||||
os: 'macOS',
|
||||
location: '上海市浦东新区',
|
||||
status: 'success',
|
||||
sessionId: 'sess-002',
|
||||
},
|
||||
{
|
||||
id: 'log-3',
|
||||
userId: 'user-3',
|
||||
username: 'lisi',
|
||||
loginTime: '2024-10-21T11:20:00',
|
||||
ipAddress: '192.168.1.102',
|
||||
device: 'Android 13',
|
||||
browser: 'Chrome Mobile 118',
|
||||
os: 'Android',
|
||||
location: '广州市天河区',
|
||||
status: 'failed',
|
||||
failReason: '密码错误',
|
||||
},
|
||||
{
|
||||
id: 'log-4',
|
||||
userId: 'user-1',
|
||||
username: 'admin',
|
||||
loginTime: '2024-10-21T14:45:00',
|
||||
ipAddress: '192.168.1.100',
|
||||
device: 'Windows 11',
|
||||
browser: 'Chrome 118',
|
||||
os: 'Windows',
|
||||
location: '北京市海淀区',
|
||||
status: 'success',
|
||||
sessionId: 'sess-003',
|
||||
},
|
||||
{
|
||||
id: 'log-5',
|
||||
userId: 'user-4',
|
||||
username: 'wangwu',
|
||||
loginTime: '2024-10-21T15:30:00',
|
||||
ipAddress: '192.168.1.103',
|
||||
device: 'iOS 17',
|
||||
browser: 'Safari 17',
|
||||
os: 'iOS',
|
||||
location: '深圳市南山区',
|
||||
status: 'failed',
|
||||
failReason: '账号被锁定',
|
||||
},
|
||||
{
|
||||
id: 'log-6',
|
||||
userId: 'user-5',
|
||||
username: 'zhaoliu',
|
||||
loginTime: '2024-10-21T16:20:00',
|
||||
ipAddress: '192.168.1.104',
|
||||
device: 'Windows 10',
|
||||
browser: 'Edge 118',
|
||||
os: 'Windows',
|
||||
location: '杭州市西湖区',
|
||||
status: 'success',
|
||||
sessionId: 'sess-004',
|
||||
},
|
||||
{
|
||||
id: 'log-7',
|
||||
userId: 'user-6',
|
||||
username: 'chenqi',
|
||||
loginTime: '2024-10-21T17:10:00',
|
||||
ipAddress: '192.168.1.105',
|
||||
device: 'Ubuntu 22.04',
|
||||
browser: 'Firefox 119',
|
||||
os: 'Linux',
|
||||
location: '成都市武侯区',
|
||||
status: 'success',
|
||||
sessionId: 'sess-005',
|
||||
},
|
||||
]
|
||||
localStorage.setItem('smart_agriculture_login_logs', JSON.stringify(mockLogs))
|
||||
setLogs(mockLogs)
|
||||
}
|
||||
// 加载登录日志数据 - 事件驱动模式
|
||||
const loadLoginLogs = useCallback(async (params?: {
|
||||
filters?: Record<string, string>;
|
||||
pagination?: { page: number; size: number };
|
||||
resetPage?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const filteredLogs = logs.filter(log => {
|
||||
const matchKeyword = !searchKeyword ||
|
||||
log.username.includes(searchKeyword) ||
|
||||
log.ipAddress.includes(searchKeyword) ||
|
||||
(log.location && log.location.includes(searchKeyword))
|
||||
const queryParams: LoginLogsQueryParams = {
|
||||
page: params?.resetPage ? 1 : (params?.pagination?.page || pagination.page),
|
||||
size: params?.pagination?.size || pagination.size,
|
||||
sort_order: 'desc',
|
||||
order_by: 'created_at',
|
||||
};
|
||||
|
||||
const matchStatus = statusFilter === 'all' || log.status === statusFilter
|
||||
|
||||
let matchDate = true
|
||||
if (dateRange !== 'all') {
|
||||
const logDate = new Date(log.loginTime)
|
||||
const now = new Date()
|
||||
const diffDays = Math.floor((now.getTime() - logDate.getTime()) / (1000 * 60 * 60 * 24))
|
||||
|
||||
switch (dateRange) {
|
||||
case 'today':
|
||||
matchDate = diffDays === 0
|
||||
break
|
||||
case 'week':
|
||||
matchDate = diffDays <= 7
|
||||
break
|
||||
case 'month':
|
||||
matchDate = diffDays <= 30
|
||||
break
|
||||
// 处理搜索条件
|
||||
const searchKeyword = params?.filters?.search ?? searchFilters.search;
|
||||
if (searchKeyword) {
|
||||
queryParams.username = searchKeyword;
|
||||
queryParams.ip_address = searchKeyword;
|
||||
}
|
||||
|
||||
// 处理状态筛选
|
||||
const status = params?.filters?.status ?? searchFilters.status;
|
||||
if (status !== 'all') {
|
||||
queryParams.status = status;
|
||||
}
|
||||
|
||||
// 处理时间范围
|
||||
const dateRange = params?.filters?.dateRange ?? searchFilters.dateRange;
|
||||
if (dateRange !== 'all') {
|
||||
const now = new Date();
|
||||
let startTime: string;
|
||||
|
||||
switch (dateRange) {
|
||||
case 'today':
|
||||
startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0).toISOString();
|
||||
break;
|
||||
case 'week':
|
||||
startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
||||
break;
|
||||
case 'month':
|
||||
startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
||||
break;
|
||||
}
|
||||
|
||||
if (startTime) {
|
||||
queryParams.start_time = startTime;
|
||||
queryParams.end_time = now.toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetchLoginLogs(queryParams);
|
||||
const transformedLogs = transformLoginLogsList(response.data);
|
||||
|
||||
setLogs(transformedLogs);
|
||||
setPagination({
|
||||
page: response.page,
|
||||
size: response.size,
|
||||
total: response.total,
|
||||
totalPages: response.totalPages,
|
||||
hasNext: response.hasNext,
|
||||
hasPrev: response.hasPrev,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load login logs:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '加载登录日志失败';
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [pagination.page, pagination.size, searchFilters]);
|
||||
|
||||
return matchKeyword && matchStatus && matchDate
|
||||
})
|
||||
// 加载统计数据
|
||||
const loadStatistics = useCallback(async () => {
|
||||
try {
|
||||
const stats = await fetchLoginStatistics();
|
||||
setStatistics(stats);
|
||||
} catch (error) {
|
||||
console.error('Failed to load login statistics:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleExport = () => {
|
||||
const dataStr = JSON.stringify(filteredLogs, null, 2)
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(dataBlob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `login_logs_${new Date().getTime()}.json`
|
||||
link.click()
|
||||
toast.success('导出成功')
|
||||
}
|
||||
// 初始化数据 - 只在组件挂载时执行一次
|
||||
useEffect(() => {
|
||||
if (isFirstLoad.current) {
|
||||
isFirstLoad.current = false;
|
||||
loadLoginLogs({ resetPage: true });
|
||||
loadStatistics();
|
||||
}
|
||||
}, [loadLoginLogs, loadStatistics]);
|
||||
|
||||
// 事件处理器 - 事件驱动模式
|
||||
const handleSearch = useCallback((filters: Record<string, string>) => {
|
||||
setSearchFilters(filters);
|
||||
loadLoginLogs({
|
||||
filters,
|
||||
pagination: { page: 1, size: pagination.size }
|
||||
});
|
||||
}, [loadLoginLogs, pagination.size]);
|
||||
|
||||
const handlePageChange = useCallback((page: number) => {
|
||||
setPagination(prev => ({ ...prev, page }));
|
||||
loadLoginLogs({
|
||||
filters: searchFilters,
|
||||
pagination: { page, size: pagination.size }
|
||||
});
|
||||
}, [loadLoginLogs, searchFilters, pagination.size]);
|
||||
|
||||
const handleSizeChange = useCallback((size: number) => {
|
||||
setPagination(prev => ({ ...prev, size, page: 1 }));
|
||||
loadLoginLogs({
|
||||
filters: searchFilters,
|
||||
pagination: { page: 1, size }
|
||||
});
|
||||
}, [loadLoginLogs, searchFilters]);
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = (log: LoginLog) => {
|
||||
toast.info(`查看登录日志详情: ${log.username} - ${log.ip_address}`);
|
||||
};
|
||||
|
||||
// 导出日志
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const exportData = await exportLoginLogs({
|
||||
username: searchFilters.search,
|
||||
status: searchFilters.status === 'all' ? undefined : searchFilters.status,
|
||||
start_time: searchFilters.dateRange === 'all' ? undefined :
|
||||
searchFilters.dateRange === 'today' ? new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate(), 0, 0, 0).toISOString() :
|
||||
searchFilters.dateRange === 'week' ? new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString() :
|
||||
searchFilters.dateRange === 'month' ? new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString() : undefined,
|
||||
end_time: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// 创建下载链接
|
||||
const dataStr = JSON.stringify(exportData, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `login_logs_${new Date().getTime()}.json`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast.success('导出成功');
|
||||
} catch (error) {
|
||||
console.error('Failed to export login logs:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '导出失败';
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800">登录日志</h2>
|
||||
<p className="text-muted-foreground">全面记录所有用户的登录行为</p>
|
||||
</div>
|
||||
<Button onClick={handleExport}>
|
||||
<Button onClick={handleExport} disabled={loading}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出日志
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<LoginLogStats logs={logs} />
|
||||
{statistics && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="p-6 bg-card hover:bg-muted transition-colors border rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">总登录次数</div>
|
||||
<div className="text-2xl font-bold text-blue-600">{statistics.total_logins}</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
系统总登录次数
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 bg-card hover:bg-muted transition-colors border rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">成功登录</div>
|
||||
<div className="text-2xl font-bold text-green-600">{statistics.successful_logins}</div>
|
||||
</div>
|
||||
<div className="text-xs text-green-600">
|
||||
登录成功次数
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 bg-card hover:bg-muted transition-colors border rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">失败登录</div>
|
||||
<div className="text-2xl font-bold text-red-600">{statistics.failed_logins}</div>
|
||||
</div>
|
||||
<div className="text-xs text-red-600">
|
||||
登录失败次数
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 bg-card hover:bg-muted transition-colors border rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">成功率</div>
|
||||
<div className="text-2xl font-bold text-purple-600">{Math.round(statistics.success_rate * 100)}%</div>
|
||||
</div>
|
||||
<div className="text-xs text-purple-600">
|
||||
登录成功率
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 搜索和筛选 */}
|
||||
<LoginLogFilters
|
||||
searchKeyword={searchKeyword}
|
||||
onSearchChange={setSearchKeyword}
|
||||
statusFilter={statusFilter}
|
||||
onStatusFilterChange={setStatusFilter}
|
||||
dateRange={dateRange}
|
||||
onDateRangeChange={setDateRange}
|
||||
{/* 搜索、表格和分页 */}
|
||||
<SearchFormPagination
|
||||
formTitle="登录日志列表"
|
||||
searchFields={searchFields}
|
||||
columns={columns}
|
||||
data={logs}
|
||||
loading={loading}
|
||||
error={null}
|
||||
pagination={pagination}
|
||||
onPageChange={handlePageChange}
|
||||
onSizeChange={handleSizeChange}
|
||||
onSearch={handleSearch}
|
||||
emptyText="暂无登录日志数据"
|
||||
showSizeSelector={true}
|
||||
showPageInfo={true}
|
||||
sizeOptions={[10, 20, 50, 100]}
|
||||
/>
|
||||
|
||||
{/* 日志列表 */}
|
||||
<LoginLogTable logs={filteredLogs} />
|
||||
|
||||
{/* 使用说明 */}
|
||||
<LoginLogInfo />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useContext, useState, ReactNode, useRef } from 'react';
|
||||
import { getCurrentUserInfoApiV1AuthMeGet, refreshTokenApiV1AuthRefreshPost } from '@/lib/api/sdk.gen';
|
||||
import { setAuthUser, getAuthUser, AuthUser } from '@/stores/modules/auth';
|
||||
import { getCurrentUserInfoApiV1AuthMeGet, refreshTokenApiV1AuthRefreshPost, listAdminSettingsApiV1AdminSettingsGet } from '@/lib/api/sdk.gen';
|
||||
import { setAuthUser, getAuthUser, setSettings } from '@/stores/modules/auth';
|
||||
|
||||
// Cookie 操作工具
|
||||
const setTokenCookie = (token: string) => {
|
||||
@@ -203,26 +203,47 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
|
||||
const userData = JSON.parse(storedUser);
|
||||
|
||||
// 使用 SDK 调用 /api/v1/auth/me 验证用户信息
|
||||
const response = await getCurrentUserInfoApiV1AuthMeGet({
|
||||
headers: {
|
||||
'Authorization': `Bearer ${userData.token}`,
|
||||
},
|
||||
});
|
||||
// 使用 Promise.all 并行发起两个请求
|
||||
const [userResponse, settingsResponse] = await Promise.all([
|
||||
// 请求1: 调用 /api/v1/auth/me 验证用户信息
|
||||
getCurrentUserInfoApiV1AuthMeGet({
|
||||
headers: {
|
||||
'Authorization': `Bearer ${userData.token}`,
|
||||
},
|
||||
}),
|
||||
// 请求2: 调用 /api/v1/admin/settings 获取设置信息
|
||||
listAdminSettingsApiV1AdminSettingsGet({
|
||||
headers: {
|
||||
'Authorization': `Bearer ${userData.token}`,
|
||||
},
|
||||
query: {
|
||||
page: 1,
|
||||
size: 100,
|
||||
order_by: '',
|
||||
sort_order: 'desc'
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
if (response.data) {
|
||||
if (userResponse.data) {
|
||||
// 更新用户信息(可能包含最新的权限、角色等)
|
||||
const updatedUserData = {
|
||||
...userData,
|
||||
...response.data, // 合并最新的用户信息
|
||||
...userResponse.data, // 合并最新的用户信息
|
||||
};
|
||||
setUser(updatedUserData);
|
||||
|
||||
// 存储到 Zustand store
|
||||
setAuthUser(response.data);
|
||||
console.log('✅ 用户验证成功,最新用户信息:', response.data);
|
||||
setAuthUser(userResponse.data);
|
||||
console.log('✅ 用户验证成功,最新用户信息:', userResponse.data);
|
||||
console.log('📦 从 Zustand store 取出的用户数据:', getAuthUser());
|
||||
|
||||
// 存储设置数据到 Zustand store
|
||||
if (settingsResponse && settingsResponse.data) {
|
||||
setSettings(settingsResponse.data);
|
||||
console.log('✅ 设置数据获取成功:', settingsResponse.data);
|
||||
}
|
||||
|
||||
// 验证成功后,启动 token 自动刷新定时器
|
||||
startTokenRefresh();
|
||||
} else {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -4,6 +4,72 @@ export type ClientOptions = {
|
||||
baseUrl: 'https://gitea-admin-hm-smart-agri-app.dev.maimaiag.com' | (string & {});
|
||||
};
|
||||
|
||||
/**
|
||||
* AdminSettingsCreateRequest
|
||||
*
|
||||
* 创建管理员设置请求
|
||||
*/
|
||||
export type AdminSettingsCreateRequest = {
|
||||
/**
|
||||
* Key
|
||||
*
|
||||
* 设置键名
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* Json Value
|
||||
*
|
||||
* 设置值JSON数据
|
||||
*/
|
||||
json_value: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* AdminSettingsResponse
|
||||
*
|
||||
* 管理员设置响应
|
||||
*/
|
||||
export type AdminSettingsResponse = {
|
||||
/**
|
||||
* Id
|
||||
*
|
||||
* 设置ID
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* Key
|
||||
*
|
||||
* 设置键名
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* Json Value
|
||||
*
|
||||
* 设置值JSON数据
|
||||
*/
|
||||
json_value?: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* AdminSettingsUpdateRequest
|
||||
*
|
||||
* 更新管理员设置请求
|
||||
*/
|
||||
export type AdminSettingsUpdateRequest = {
|
||||
/**
|
||||
* Json Value
|
||||
*
|
||||
* 设置值JSON数据
|
||||
*/
|
||||
json_value: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ApplicationMetrics
|
||||
*
|
||||
@@ -152,6 +218,72 @@ export type CaptchaResponse = {
|
||||
*/
|
||||
export type CompanyScale = '小型(50人以下)' | '中型(50-300人)' | '大型(300人以上)';
|
||||
|
||||
/**
|
||||
* ConnectionConfig
|
||||
*/
|
||||
export type ConnectionConfig = {
|
||||
/**
|
||||
* Mail Username
|
||||
*/
|
||||
MAIL_USERNAME: string;
|
||||
/**
|
||||
* Mail Port
|
||||
*/
|
||||
MAIL_PORT: number;
|
||||
/**
|
||||
* Mail Server
|
||||
*/
|
||||
MAIL_SERVER: string;
|
||||
/**
|
||||
* Mail Starttls
|
||||
*/
|
||||
MAIL_STARTTLS: boolean;
|
||||
/**
|
||||
* Mail Ssl Tls
|
||||
*/
|
||||
MAIL_SSL_TLS: boolean;
|
||||
/**
|
||||
* Mail Debug
|
||||
*/
|
||||
MAIL_DEBUG?: number;
|
||||
/**
|
||||
* Mail From
|
||||
*/
|
||||
MAIL_FROM: string;
|
||||
/**
|
||||
* Mail From Name
|
||||
*/
|
||||
MAIL_FROM_NAME?: string | null;
|
||||
/**
|
||||
* Template Folder
|
||||
*/
|
||||
TEMPLATE_FOLDER?: string | null;
|
||||
/**
|
||||
* Suppress Send
|
||||
*/
|
||||
SUPPRESS_SEND?: number;
|
||||
/**
|
||||
* Use Credentials
|
||||
*/
|
||||
USE_CREDENTIALS?: boolean;
|
||||
/**
|
||||
* Validate Certs
|
||||
*/
|
||||
VALIDATE_CERTS?: boolean;
|
||||
/**
|
||||
* Timeout
|
||||
*/
|
||||
TIMEOUT?: number;
|
||||
/**
|
||||
* Local Hostname
|
||||
*/
|
||||
LOCAL_HOSTNAME?: string | null;
|
||||
/**
|
||||
* Cert Bundle
|
||||
*/
|
||||
CERT_BUNDLE?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* DepartmentCreate
|
||||
*
|
||||
@@ -658,6 +790,50 @@ export type DiskPartitionInfo = {
|
||||
usage_percent: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* EmailConfigTestResponse
|
||||
*
|
||||
* 邮件配置测试响应
|
||||
*/
|
||||
export type EmailConfigTestResponse = {
|
||||
/**
|
||||
* Success
|
||||
*
|
||||
* 测试是否成功
|
||||
*/
|
||||
success: boolean;
|
||||
/**
|
||||
* Message
|
||||
*
|
||||
* 测试结果消息
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Config Valid
|
||||
*
|
||||
* 邮件配置是否有效
|
||||
*/
|
||||
config_valid: boolean;
|
||||
/**
|
||||
* Connection Test
|
||||
*
|
||||
* 连接测试是否通过
|
||||
*/
|
||||
connection_test: boolean;
|
||||
/**
|
||||
* Delivery Test
|
||||
*
|
||||
* 投递测试是否通过
|
||||
*/
|
||||
delivery_test: boolean;
|
||||
/**
|
||||
* Error Details
|
||||
*
|
||||
* 错误详情(如果有)
|
||||
*/
|
||||
error_details?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* ExternalServiceConfigCreate
|
||||
*
|
||||
@@ -827,6 +1003,94 @@ export type HttpValidationError = {
|
||||
detail?: Array<ValidationError>;
|
||||
};
|
||||
|
||||
/**
|
||||
* InboxMessageResponse
|
||||
*
|
||||
* 站内信响应
|
||||
*/
|
||||
export type InboxMessageResponse = {
|
||||
/**
|
||||
* Id
|
||||
*
|
||||
* 消息ID
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Subject
|
||||
*
|
||||
* 消息主题
|
||||
*/
|
||||
subject?: string | null;
|
||||
/**
|
||||
* Content
|
||||
*
|
||||
* 消息内容
|
||||
*/
|
||||
content: string;
|
||||
/**
|
||||
* 消息优先级
|
||||
*/
|
||||
priority: MessagePriority;
|
||||
/**
|
||||
* Is Read
|
||||
*
|
||||
* 是否已读
|
||||
*/
|
||||
is_read: boolean;
|
||||
/**
|
||||
* Read At
|
||||
*
|
||||
* 阅读时间
|
||||
*/
|
||||
read_at?: string | null;
|
||||
/**
|
||||
* Sent At
|
||||
*
|
||||
* 发送时间
|
||||
*/
|
||||
sent_at?: string | null;
|
||||
/**
|
||||
* Created At
|
||||
*
|
||||
* 创建时间
|
||||
*/
|
||||
created_at: string;
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
message_type: MessageType;
|
||||
/**
|
||||
* Recipient
|
||||
*
|
||||
* 接收者号码
|
||||
*/
|
||||
recipient: string;
|
||||
/**
|
||||
* Recipient Name
|
||||
*
|
||||
* 接收者名字
|
||||
*/
|
||||
recipient_name?: string | null;
|
||||
/**
|
||||
* Retry Count
|
||||
*
|
||||
* 重试次数
|
||||
*/
|
||||
retry_count: number;
|
||||
/**
|
||||
* Failure Reason
|
||||
*
|
||||
* 失败原因
|
||||
*/
|
||||
failure_reason?: string | null;
|
||||
/**
|
||||
* Scheduled At
|
||||
*
|
||||
* 计划发送时间
|
||||
*/
|
||||
scheduled_at?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* InvoiceType
|
||||
*
|
||||
@@ -1635,6 +1899,54 @@ export type OperationLogStatistics = {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* PagedResponse[AdminSettingsResponse]
|
||||
*/
|
||||
export type PagedResponseAdminSettingsResponse = {
|
||||
/**
|
||||
* Data
|
||||
*
|
||||
* 数据列表
|
||||
*/
|
||||
data?: Array<AdminSettingsResponse>;
|
||||
/**
|
||||
* Total
|
||||
*
|
||||
* 数据总数
|
||||
*/
|
||||
total: number;
|
||||
/**
|
||||
* Page
|
||||
*
|
||||
* 当前页数
|
||||
*/
|
||||
page: number;
|
||||
/**
|
||||
* Size
|
||||
*
|
||||
* 当前数量
|
||||
*/
|
||||
size: number;
|
||||
/**
|
||||
* Total Pages
|
||||
*
|
||||
* 总页数
|
||||
*/
|
||||
total_pages: number;
|
||||
/**
|
||||
* Has Next
|
||||
*
|
||||
* 是否有下一页
|
||||
*/
|
||||
has_next: boolean;
|
||||
/**
|
||||
* Has Prev
|
||||
*
|
||||
* 是否有上一页
|
||||
*/
|
||||
has_prev: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* PagedResponse[DepartmentResponse]
|
||||
*/
|
||||
@@ -1683,6 +1995,54 @@ export type PagedResponseDepartmentResponse = {
|
||||
has_prev: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* PagedResponse[InboxMessageResponse]
|
||||
*/
|
||||
export type PagedResponseInboxMessageResponse = {
|
||||
/**
|
||||
* Data
|
||||
*
|
||||
* 数据列表
|
||||
*/
|
||||
data?: Array<InboxMessageResponse>;
|
||||
/**
|
||||
* Total
|
||||
*
|
||||
* 数据总数
|
||||
*/
|
||||
total: number;
|
||||
/**
|
||||
* Page
|
||||
*
|
||||
* 当前页数
|
||||
*/
|
||||
page: number;
|
||||
/**
|
||||
* Size
|
||||
*
|
||||
* 当前数量
|
||||
*/
|
||||
size: number;
|
||||
/**
|
||||
* Total Pages
|
||||
*
|
||||
* 总页数
|
||||
*/
|
||||
total_pages: number;
|
||||
/**
|
||||
* Has Next
|
||||
*
|
||||
* 是否有下一页
|
||||
*/
|
||||
has_next: boolean;
|
||||
/**
|
||||
* Has Prev
|
||||
*
|
||||
* 是否有上一页
|
||||
*/
|
||||
has_prev: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* PagedResponse[MessageLogResponse]
|
||||
*/
|
||||
@@ -3251,6 +3611,12 @@ export type UserCreateWithCompany = {
|
||||
* 用户列表响应模型 - 用于系统用户管理接口
|
||||
*/
|
||||
export type UserListResponse = {
|
||||
/**
|
||||
* Id
|
||||
*
|
||||
* 用户ID
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Username
|
||||
*
|
||||
@@ -3595,6 +3961,76 @@ export type ValidationError = {
|
||||
type: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* ConnectionConfig
|
||||
*/
|
||||
export type ConnectionConfigWritable = {
|
||||
/**
|
||||
* Mail Username
|
||||
*/
|
||||
MAIL_USERNAME: string;
|
||||
/**
|
||||
* Mail Password
|
||||
*/
|
||||
MAIL_PASSWORD: string;
|
||||
/**
|
||||
* Mail Port
|
||||
*/
|
||||
MAIL_PORT: number;
|
||||
/**
|
||||
* Mail Server
|
||||
*/
|
||||
MAIL_SERVER: string;
|
||||
/**
|
||||
* Mail Starttls
|
||||
*/
|
||||
MAIL_STARTTLS: boolean;
|
||||
/**
|
||||
* Mail Ssl Tls
|
||||
*/
|
||||
MAIL_SSL_TLS: boolean;
|
||||
/**
|
||||
* Mail Debug
|
||||
*/
|
||||
MAIL_DEBUG?: number;
|
||||
/**
|
||||
* Mail From
|
||||
*/
|
||||
MAIL_FROM: string;
|
||||
/**
|
||||
* Mail From Name
|
||||
*/
|
||||
MAIL_FROM_NAME?: string | null;
|
||||
/**
|
||||
* Template Folder
|
||||
*/
|
||||
TEMPLATE_FOLDER?: string | null;
|
||||
/**
|
||||
* Suppress Send
|
||||
*/
|
||||
SUPPRESS_SEND?: number;
|
||||
/**
|
||||
* Use Credentials
|
||||
*/
|
||||
USE_CREDENTIALS?: boolean;
|
||||
/**
|
||||
* Validate Certs
|
||||
*/
|
||||
VALIDATE_CERTS?: boolean;
|
||||
/**
|
||||
* Timeout
|
||||
*/
|
||||
TIMEOUT?: number;
|
||||
/**
|
||||
* Local Hostname
|
||||
*/
|
||||
LOCAL_HOSTNAME?: string | null;
|
||||
/**
|
||||
* Cert Bundle
|
||||
*/
|
||||
CERT_BUNDLE?: string | null;
|
||||
};
|
||||
|
||||
export type RegisterApiV1AuthRegisterPostData = {
|
||||
body: UserCreate;
|
||||
path?: never;
|
||||
@@ -6681,6 +7117,158 @@ export type RetryFailedMessageApiV1MessagesMessageIdRetryPostResponses = {
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type GetUserInboxMessagesApiV1MessagesInboxGetData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: {
|
||||
/**
|
||||
* Is Read
|
||||
*/
|
||||
is_read?: boolean | null;
|
||||
/**
|
||||
* Page
|
||||
*
|
||||
* 页码
|
||||
*/
|
||||
page?: number;
|
||||
/**
|
||||
* Page Size
|
||||
*/
|
||||
page_size?: number;
|
||||
/**
|
||||
* Size
|
||||
*
|
||||
* 每页数量
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
* Order By
|
||||
*
|
||||
* 排序字段
|
||||
*/
|
||||
order_by?: string;
|
||||
/**
|
||||
* Sort Order
|
||||
*
|
||||
* 排序方向 (asc/desc)
|
||||
*/
|
||||
sort_order?: string;
|
||||
};
|
||||
url: '/api/v1/messages/inbox';
|
||||
};
|
||||
|
||||
export type GetUserInboxMessagesApiV1MessagesInboxGetErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetUserInboxMessagesApiV1MessagesInboxGetError = GetUserInboxMessagesApiV1MessagesInboxGetErrors[keyof GetUserInboxMessagesApiV1MessagesInboxGetErrors];
|
||||
|
||||
export type GetUserInboxMessagesApiV1MessagesInboxGetResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: PagedResponseInboxMessageResponse;
|
||||
};
|
||||
|
||||
export type GetUserInboxMessagesApiV1MessagesInboxGetResponse = GetUserInboxMessagesApiV1MessagesInboxGetResponses[keyof GetUserInboxMessagesApiV1MessagesInboxGetResponses];
|
||||
|
||||
export type MarkMessageAsReadApiV1MessagesInboxMessageIdReadPostData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Message Id
|
||||
*/
|
||||
message_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/messages/inbox/{message_id}/read';
|
||||
};
|
||||
|
||||
export type MarkMessageAsReadApiV1MessagesInboxMessageIdReadPostErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type MarkMessageAsReadApiV1MessagesInboxMessageIdReadPostError = MarkMessageAsReadApiV1MessagesInboxMessageIdReadPostErrors[keyof MarkMessageAsReadApiV1MessagesInboxMessageIdReadPostErrors];
|
||||
|
||||
export type MarkMessageAsReadApiV1MessagesInboxMessageIdReadPostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type GetUnreadMessageCountApiV1MessagesInboxUnreadCountGetData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/v1/messages/inbox/unread-count';
|
||||
};
|
||||
|
||||
export type GetUnreadMessageCountApiV1MessagesInboxUnreadCountGetResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type MarkAllMessagesAsReadApiV1MessagesInboxReadAllPostData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/v1/messages/inbox/read-all';
|
||||
};
|
||||
|
||||
export type MarkAllMessagesAsReadApiV1MessagesInboxReadAllPostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type TestEmailConfigApiV1MessagesTestEmailConfigPostData = {
|
||||
body: ConnectionConfigWritable;
|
||||
path?: never;
|
||||
query: {
|
||||
/**
|
||||
* Recipient
|
||||
*
|
||||
* 测试邮件接收地址
|
||||
*/
|
||||
recipient: string;
|
||||
/**
|
||||
* Is Html
|
||||
*
|
||||
* 是否HTML
|
||||
*/
|
||||
is_html?: boolean;
|
||||
};
|
||||
url: '/api/v1/messages/test-email-config';
|
||||
};
|
||||
|
||||
export type TestEmailConfigApiV1MessagesTestEmailConfigPostErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type TestEmailConfigApiV1MessagesTestEmailConfigPostError = TestEmailConfigApiV1MessagesTestEmailConfigPostErrors[keyof TestEmailConfigApiV1MessagesTestEmailConfigPostErrors];
|
||||
|
||||
export type TestEmailConfigApiV1MessagesTestEmailConfigPostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: EmailConfigTestResponse;
|
||||
};
|
||||
|
||||
export type TestEmailConfigApiV1MessagesTestEmailConfigPostResponse = TestEmailConfigApiV1MessagesTestEmailConfigPostResponses[keyof TestEmailConfigApiV1MessagesTestEmailConfigPostResponses];
|
||||
|
||||
export type ListTemplatesApiV1MessagesTemplatesGetData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
@@ -7554,6 +8142,171 @@ export type GetPerformanceSummaryApiV1SystemMetricsSummaryGetResponses = {
|
||||
|
||||
export type GetPerformanceSummaryApiV1SystemMetricsSummaryGetResponse = GetPerformanceSummaryApiV1SystemMetricsSummaryGetResponses[keyof GetPerformanceSummaryApiV1SystemMetricsSummaryGetResponses];
|
||||
|
||||
export type ListAdminSettingsApiV1AdminSettingsGetData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: {
|
||||
/**
|
||||
* Page
|
||||
*
|
||||
* 页码
|
||||
*/
|
||||
page?: number;
|
||||
/**
|
||||
* Size
|
||||
*
|
||||
* 每页数量
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
* Order By
|
||||
*
|
||||
* 排序字段
|
||||
*/
|
||||
order_by?: string;
|
||||
/**
|
||||
* Sort Order
|
||||
*
|
||||
* 排序方向 (asc/desc)
|
||||
*/
|
||||
sort_order?: string;
|
||||
};
|
||||
url: '/api/v1/admin/settings';
|
||||
};
|
||||
|
||||
export type ListAdminSettingsApiV1AdminSettingsGetErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ListAdminSettingsApiV1AdminSettingsGetError = ListAdminSettingsApiV1AdminSettingsGetErrors[keyof ListAdminSettingsApiV1AdminSettingsGetErrors];
|
||||
|
||||
export type ListAdminSettingsApiV1AdminSettingsGetResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: PagedResponseAdminSettingsResponse;
|
||||
};
|
||||
|
||||
export type ListAdminSettingsApiV1AdminSettingsGetResponse = ListAdminSettingsApiV1AdminSettingsGetResponses[keyof ListAdminSettingsApiV1AdminSettingsGetResponses];
|
||||
|
||||
export type CreateAdminSettingApiV1AdminSettingsPostData = {
|
||||
body: AdminSettingsCreateRequest;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/v1/admin/settings';
|
||||
};
|
||||
|
||||
export type CreateAdminSettingApiV1AdminSettingsPostErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type CreateAdminSettingApiV1AdminSettingsPostError = CreateAdminSettingApiV1AdminSettingsPostErrors[keyof CreateAdminSettingApiV1AdminSettingsPostErrors];
|
||||
|
||||
export type CreateAdminSettingApiV1AdminSettingsPostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
201: AdminSettingsResponse;
|
||||
};
|
||||
|
||||
export type CreateAdminSettingApiV1AdminSettingsPostResponse = CreateAdminSettingApiV1AdminSettingsPostResponses[keyof CreateAdminSettingApiV1AdminSettingsPostResponses];
|
||||
|
||||
export type DeleteAdminSettingApiV1AdminSettingsKeyDeleteData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Key
|
||||
*/
|
||||
key: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/admin/settings/{key}';
|
||||
};
|
||||
|
||||
export type DeleteAdminSettingApiV1AdminSettingsKeyDeleteErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type DeleteAdminSettingApiV1AdminSettingsKeyDeleteError = DeleteAdminSettingApiV1AdminSettingsKeyDeleteErrors[keyof DeleteAdminSettingApiV1AdminSettingsKeyDeleteErrors];
|
||||
|
||||
export type DeleteAdminSettingApiV1AdminSettingsKeyDeleteResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
204: void;
|
||||
};
|
||||
|
||||
export type DeleteAdminSettingApiV1AdminSettingsKeyDeleteResponse = DeleteAdminSettingApiV1AdminSettingsKeyDeleteResponses[keyof DeleteAdminSettingApiV1AdminSettingsKeyDeleteResponses];
|
||||
|
||||
export type GetAdminSettingApiV1AdminSettingsKeyGetData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Key
|
||||
*/
|
||||
key: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/admin/settings/{key}';
|
||||
};
|
||||
|
||||
export type GetAdminSettingApiV1AdminSettingsKeyGetErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetAdminSettingApiV1AdminSettingsKeyGetError = GetAdminSettingApiV1AdminSettingsKeyGetErrors[keyof GetAdminSettingApiV1AdminSettingsKeyGetErrors];
|
||||
|
||||
export type GetAdminSettingApiV1AdminSettingsKeyGetResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: AdminSettingsResponse;
|
||||
};
|
||||
|
||||
export type GetAdminSettingApiV1AdminSettingsKeyGetResponse = GetAdminSettingApiV1AdminSettingsKeyGetResponses[keyof GetAdminSettingApiV1AdminSettingsKeyGetResponses];
|
||||
|
||||
export type UpdateAdminSettingApiV1AdminSettingsKeyPutData = {
|
||||
body: AdminSettingsUpdateRequest;
|
||||
path: {
|
||||
/**
|
||||
* Key
|
||||
*/
|
||||
key: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/admin/settings/{key}';
|
||||
};
|
||||
|
||||
export type UpdateAdminSettingApiV1AdminSettingsKeyPutErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type UpdateAdminSettingApiV1AdminSettingsKeyPutError = UpdateAdminSettingApiV1AdminSettingsKeyPutErrors[keyof UpdateAdminSettingApiV1AdminSettingsKeyPutErrors];
|
||||
|
||||
export type UpdateAdminSettingApiV1AdminSettingsKeyPutResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: AdminSettingsResponse;
|
||||
};
|
||||
|
||||
export type UpdateAdminSettingApiV1AdminSettingsKeyPutResponse = UpdateAdminSettingApiV1AdminSettingsKeyPutResponses[keyof UpdateAdminSettingApiV1AdminSettingsKeyPutResponses];
|
||||
|
||||
export type HealthCheckHealthGetData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { create } from 'zustand';
|
||||
import { getCurrentUserInfoApiV1AuthMeGet } from '@/lib/api/sdk.gen';
|
||||
|
||||
// Auth user interface definition
|
||||
export interface AuthUser {
|
||||
email: string;
|
||||
username: string;
|
||||
full_name: string | null;
|
||||
full_name?: string | null;
|
||||
phone: string;
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
@@ -22,16 +21,38 @@ export interface AuthUser {
|
||||
department_name: string | null;
|
||||
}
|
||||
|
||||
// Settings item interface
|
||||
export interface SettingItem {
|
||||
id: string;
|
||||
key: string;
|
||||
json_value: Record<string, any>;
|
||||
}
|
||||
|
||||
// Settings response interface
|
||||
export interface SettingsResponse {
|
||||
data: SettingItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
total_pages: number;
|
||||
has_next: boolean;
|
||||
has_prev: boolean;
|
||||
}
|
||||
|
||||
// Auth state interface
|
||||
export interface AuthState {
|
||||
user: AuthUser | null;
|
||||
settings: SettingsResponse | null;
|
||||
setAuthUser: (user: AuthUser | null) => void;
|
||||
getAuthUser: () => AuthUser | null;
|
||||
setSettings: (settings: SettingsResponse | null) => void;
|
||||
getSettings: () => SettingsResponse | null;
|
||||
}
|
||||
|
||||
// Create auth store
|
||||
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
user: null,
|
||||
settings: null,
|
||||
|
||||
setAuthUser: (user: AuthUser | null) => {
|
||||
set({ user });
|
||||
@@ -40,6 +61,14 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
getAuthUser: () => {
|
||||
return get().user;
|
||||
},
|
||||
|
||||
setSettings: (settings: SettingsResponse | null) => {
|
||||
set({ settings });
|
||||
},
|
||||
|
||||
getSettings: () => {
|
||||
return get().settings;
|
||||
},
|
||||
}));
|
||||
|
||||
// Export functions for direct usage
|
||||
@@ -51,36 +80,13 @@ export const getAuthUser = (): AuthUser | null => {
|
||||
return useAuthStore.getState().getAuthUser();
|
||||
};
|
||||
|
||||
// Validate and update user info from API
|
||||
export const validateAndUpdateUser = async (token: string): Promise<boolean> => {
|
||||
try {
|
||||
// 使用 SDK 调用 /api/v1/auth/me 验证用户信息
|
||||
const response = await getCurrentUserInfoApiV1AuthMeGet({
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
// 更新用户信息(可能包含最新的权限、角色等)
|
||||
const currentUser = getAuthUser();
|
||||
if (currentUser) {
|
||||
const updatedUserData = {
|
||||
...currentUser,
|
||||
...response.data, // 合并最新的用户信息
|
||||
};
|
||||
setAuthUser(updatedUserData);
|
||||
|
||||
// 打印存储后的用户数据,用于调试
|
||||
console.log('从Zustand取出的用户数据:', getAuthUser());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Failed to validate user info:', error);
|
||||
return false;
|
||||
}
|
||||
export const setSettings = (settings: SettingsResponse | null) => {
|
||||
useAuthStore.getState().setSettings(settings);
|
||||
};
|
||||
|
||||
export const getSettings = (): SettingsResponse | null => {
|
||||
return useAuthStore.getState().getSettings();
|
||||
};
|
||||
|
||||
|
||||
export default useAuthStore;
|
||||
Reference in New Issue
Block a user