添加actions文件移除route文件

This commit is contained in:
wmp
2025-09-24 19:15:22 +08:00
parent 02fc0676bf
commit ebf50c15f1
17 changed files with 495 additions and 727 deletions

115
src/actions/auth.ts Normal file
View File

@@ -0,0 +1,115 @@
'use server'
import prisma, { User } from '@/lib/prisma' // 使用统一的prisma实例
import { compare } from 'bcryptjs'
import { cookies } from 'next/headers'
import { z } from 'zod'
const loginSchema = z.object({
account: z.string().min(3, '账号至少需要3个字符'),
password: z.string().min(6, '密码至少需要6个字符'),
})
export async function login(props: {
account: string
password: string
}) {
try {
const result = loginSchema.parse(props)
const user: User | null = await prisma.user.findFirst({
where: {
OR: [
{ account: result.account.trim() },
{ password: result.password.trim() },
],
},
})
if (!user) {
return {
success: false,
error: '用户不存在或密码未设置',
}
}
// 验证密码
const passwordMatch = await compare(result.password, user.password || '')
if (!passwordMatch) {
return {
success: false,
error: '密码错误',
}
}
// 创建会话
const sessionToken = crypto.randomUUID()
await prisma.session.create({
data: {
id: sessionToken,
userId: user.id,
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
})
// 设置cookie
const response = {
success: true,
user: {
id: user.id,
account: user.account,
name: user.name,
},
}
const cookieStore = await cookies()
cookieStore.set('session', sessionToken, {
httpOnly: true,
// secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7,
})
return response
}
catch (error) {
console.error('登录错误:', error)
return {
success: false,
error: '服务器错误,请稍后重试',
}
}
}
export async function logout() {
try {
const cookieStore = await cookies()
const sessionToken = cookieStore.get('session')?.value
// 删除数据库中的session如果存在
if (sessionToken) {
await prisma.session.deleteMany({
where: { id: sessionToken },
}).catch(() => {
// 忽略删除错误确保cookie被清除
})
}
// 清除cookie
const response = { success: true }
cookieStore.set('session', '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 0, // 立即过期
path: '/',
})
return response
}
catch (error) {
console.error('退出错误:', error)
return {
success: false,
error: '退出失败',
}
}
}

View File

@@ -1,196 +1,18 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
'use server'
import prisma from '@/lib/prisma'
// 处理 BigInt 序列化
function safeSerialize(data: unknown) {
return JSON.parse(JSON.stringify(data, (key, value) =>
typeof value === 'bigint' ? value.toString() : value,
))
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const reportType = searchParams.get('type')
switch (reportType) {
case 'gateway_info':
return await getGatewayInfo()
case 'gateway_config':
return await getGatewayConfig(request)
case 'city_config_count':
return await getCityConfigCount()
case 'city_node_count':
return await getCityNodeCount()
case 'allocation_status':
return await getAllocationStatus(request)
case 'edge_nodes':
return await getEdgeNodes(request)
default:
return NextResponse.json({ error: 'Invalid report type' }, { status: 400 })
}
}
catch (error) {
console.error('API Error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
// 获取网关基本信息
async function getGatewayInfo() {
try {
const result = await prisma.$queryRaw`
SELECT macaddr, inner_ip, setid, enable
FROM token
ORDER BY macaddr
`
return NextResponse.json(safeSerialize(result))
}
catch (error) {
console.error('Gateway info query error:', error)
return NextResponse.json({ error: '查询网关信息失败' }, { status: 500 })
}
}
// 网关配置
async function getGatewayConfig(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const macAddress = searchParams.get('mac') || ''
const offset = parseInt(searchParams.get('offset') || '0')
const limit = parseInt(searchParams.get('limit') || '100')
// 定义类型接口
interface GatewayRecord {
edge: string | null
city: string | null
user: string | null
public: string | null
inner_ip: string | null
ischange: boolean | number | null
isonline: boolean | number | null
}
// 获取总数
let totalCountQuery = ''
let totalCountParams: (string | number)[] = []
if (macAddress) {
totalCountQuery = `
SELECT COUNT(*) as total
FROM gateway
LEFT JOIN cityhash ON cityhash.hash = gateway.cityhash
LEFT JOIN edge ON edge.macaddr = gateway.edge
WHERE gateway.macaddr = ?
`
totalCountParams = [macAddress]
}
else {
totalCountQuery = `
SELECT COUNT(*) as total
FROM gateway
`
}
const totalCountResult = await prisma.$queryRawUnsafe<[{ total: bigint }]>(
totalCountQuery,
...totalCountParams,
)
const totalCount = Number(totalCountResult[0]?.total || 0)
// 获取分页数据
let query = `
select edge, city, user, public, inner_ip, ischange, isonline
from
gateway
left join cityhash
on cityhash.hash = gateway.cityhash
left join edge
on edge.macaddr = gateway.edge
`
let params: (string | number)[] = []
if (macAddress) {
query += ' WHERE gateway.macaddr = ?'
params = [macAddress]
}
else {
query += ' LIMIT ? OFFSET ?'
params.push(limit, offset)
}
// 指定返回类型
const result = await prisma.$queryRawUnsafe<GatewayRecord[]>(query, ...params)
return NextResponse.json({
data: safeSerialize(result),
totalCount: totalCount,
currentPage: Math.floor(offset / limit) + 1,
totalPages: Math.ceil(totalCount / limit),
})
}
catch (error) {
console.error('Gateway config query error:', error)
return NextResponse.json({ error: '查询网关配置失败' }, { status: 500 })
}
}
// 城市节点配置数量统计
async function getCityConfigCount() {
try {
const result = await prisma.$queryRaw`
SELECT c.city, COUNT(e.id) as node_count
FROM cityhash c
LEFT JOIN edge e ON c.id = e.city_id
GROUP BY c.city
`
return NextResponse.json(safeSerialize(result))
}
catch (error) {
console.error('City config count query error:', error)
return NextResponse.json({ error: '查询城市配置失败' }, { status: 500 })
}
}
// 城市节点数量分布
async function getCityNodeCount() {
try {
const result = await prisma.$queryRaw`
select c.city, c.hash, c.label, e.count, c.\`offset\`
from
cityhash c
left join (
select city_id, count(*) as count
from
edge
where
edge.active is true
group by
city_id
) e
on c.id = e.city_id
group by
c.hash
order by
count desc
`
return NextResponse.json(safeSerialize(result))
}
catch (error) {
console.error('City node count query error:', error)
return NextResponse.json({ error: '查询城市节点失败' }, { status: 500 })
}
export type AllocationStatus = {
city: string
count: bigint
assigned: bigint
}
// 城市分配状态
async function getAllocationStatus(request: NextRequest) {
export async function getAllocationStatus(hours: number) {
try {
const { searchParams } = new URL(request.url)
const hours = searchParams.get('hours') || '24'
const hoursNum = parseInt(hours) || 24
const hoursNum = hours || 24
// 使用参数化查询防止SQL注入
const result = await prisma.$queryRaw`
const result: AllocationStatus[] = await prisma.$queryRaw`
SELECT
city,
c1.count AS count,
@@ -222,24 +44,185 @@ async function getAllocationStatus(request: NextRequest) {
WHERE
cityhash.macaddr IS NOT NULL;
`
return NextResponse.json(safeSerialize(result))
return {
success: true,
data: result,
}
}
catch (error) {
console.error('Allocation status query error:', error)
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
return NextResponse.json(
{ error: '查询分配状态失败: ' + errorMessage },
{ status: 500 },
return {
success: false,
data: [],
error: '查询分配状态失败: ' + errorMessage,
}
}
}
export type GatewayInfo = {
macaddr: string
inner_ip: string
setid: string
enable: number
}
// 获取网关基本信息
export async function getGatewayInfo() {
try {
const result: GatewayInfo[] = await prisma.$queryRaw`
SELECT macaddr, inner_ip, setid, enable
FROM token
ORDER BY macaddr
`
return {
success: true,
data: result,
}
}
catch (error) {
console.error('Gateway info query error:', error)
return {
success: false,
data: [],
error: '查询网关信息失败',
}
}
}
export type GatewayConfig = {
city: string
edge: string
user: string
public: string
inner_ip: string
ischange: number
isonline: number
}
// 网关配置
export async function getGatewayConfig(off: number, itemsPerPage: number, mac?: string) {
try {
const offset = off || 0
const limit = itemsPerPage || 100
// 获取总数
let totalCountQuery = ''
let totalCountParams: (string | number)[] = []
if (mac) {
totalCountQuery = `
SELECT COUNT(*) as total
FROM gateway
LEFT JOIN cityhash ON cityhash.hash = gateway.cityhash
LEFT JOIN edge ON edge.macaddr = gateway.edge
WHERE gateway.macaddr = ?
`
totalCountParams = [mac]
}
else {
totalCountQuery = `
SELECT COUNT(*) as total
FROM gateway
`
}
const totalCountResult = await prisma.$queryRawUnsafe<[{ total: bigint }]>(
totalCountQuery,
...totalCountParams,
)
const totalCount = Number(totalCountResult[0]?.total || 0)
// 获取分页数据
let query = `
select edge, city, user, public, inner_ip, ischange, isonline
from
gateway
left join cityhash
on cityhash.hash = gateway.cityhash
left join edge
on edge.macaddr = gateway.edge
`
let params: (string | number)[] = []
if (mac) {
query += ' WHERE gateway.macaddr = ?'
params = [mac]
}
else {
query += ' LIMIT ? OFFSET ?'
params.push(limit, offset)
}
// 指定返回类型
const result: GatewayConfig[] = await prisma.$queryRawUnsafe<GatewayConfig[]>(query, ...params)
return {
data: result,
totalCount: totalCount,
currentPage: Math.floor(offset / limit) + 1,
totalPages: Math.ceil(totalCount / limit),
}
}
catch (error) {
console.error('Gateway config query error:', error)
return {
success: false,
data: [],
error: '查询网关配置失败',
}
}
}
export type CityNode = {
city: string
count: number
hash: string
label: string
offset: string
}
// 城市节点数量分布
export async function getCityNodeCount() {
try {
const result: CityNode[] = await prisma.$queryRaw`
select c.city, c.hash, c.label, e.count, c.\`offset\`
from
cityhash c
left join (
select city_id, count(*) as count
from
edge
where
edge.active is true
group by
city_id
) e
on c.id = e.city_id
group by
c.hash
order by
count desc
`
return {
success: true,
data: result,
}
}
catch (error) {
console.error('City node count query error:', error)
return {
success: false,
data: [],
error: '查询城市节点失败',
}
}
}
// 获取节点信息
async function getEdgeNodes(request: NextRequest) {
export async function getEdgeNodes(off: number, itemsPerPage: number) {
try {
const { searchParams } = new URL(request.url)
const offset = parseInt(searchParams.get('offset') || '0')
const limit = parseInt(searchParams.get('limit') || '100')
const offset = off || 0
const limit = itemsPerPage || 100
// 获取总数 - 使用类型断言
const totalCountResult = await prisma.$queryRaw<[{ total: bigint }]>`
SELECT COUNT(*) as total
@@ -257,15 +240,19 @@ async function getEdgeNodes(request: NextRequest) {
ORDER BY edge.id
LIMIT ${limit} OFFSET ${offset}
`
return NextResponse.json({
data: safeSerialize(result),
return {
data: result,
totalCount: totalCount,
currentPage: Math.floor(offset / limit) + 1,
totalPages: Math.ceil(totalCount / limit),
})
}
}
catch (error) {
console.error('Edge nodes query error:', error)
return NextResponse.json({ error: '查询边缘节点失败' }, { status: 500 })
return {
success: false,
data: [],
error: '查询边缘节点失败',
}
}
}

145
src/actions/user.ts Normal file
View File

@@ -0,0 +1,145 @@
'use server'
import prisma from '@/lib/prisma'
import { hash } from 'bcryptjs'
type User = {
id: number
account: string
name: string | null
createdAt: Date
updatedAt: Date
}
// 获取所有用户
export async function findUsers(): Promise<{
success: boolean
data: User[]
error: string
}> {
try {
const users = await prisma.user.findMany({
select: {
id: true,
account: true,
name: true,
createdAt: true,
updatedAt: true,
},
orderBy: {
createdAt: 'desc',
},
})
return {
success: true,
data: users,
error: '',
}
}
catch (error) {
console.error('获取用户列表错误:', error)
return {
success: false,
data: [],
error: '服务器错误,请稍后重试',
}
}
}
// 创建用户
export async function createUser(params: {
account: string
password: string
name: string | ''
}) {
try {
// 检查用户是否已存在
const existingUser = await prisma.user.findUnique({
where: { account: params.account },
})
if (existingUser) {
return {
success: false,
error: '用户账号已存在',
}
}
// 加密密码
const hashedPassword = await hash(params.password, 10)
// 创建用户
const user = await prisma.user.create({
data: {
account: params.account,
password: hashedPassword,
name: params.name || params.account,
},
})
// 不返回密码字段
const { password: _, ...userWithoutPassword } = user
return {
success: true,
user: userWithoutPassword,
}
}
catch (error) {
console.error('创建用户错误:', error)
return {
success: false,
error: '服务器错误,请稍后重试',
}
}
}
// 删除用户
export async function removeUser(id: number) {
try {
if (!id) {
return {
success: false,
error: '用户ID不能为空',
}
}
// const userId = parseInt(id)
if (isNaN(id)) {
return {
success: false,
error: '无效的用户ID',
}
}
// 检查用户是否存在
const existingUser = await prisma.user.findUnique({
where: { id: id },
})
if (!existingUser) {
return {
status: 404,
success: false,
error: '用户不存在',
}
}
// 删除用户
await prisma.user.delete({
where: { id: id },
})
return {
success: true,
message: '用户删除成功',
}
}
catch (error) {
console.error('删除用户错误:', error)
return {
success: false,
error: '服务器错误,请稍后重试',
status: 500,
}
}
}

View File

@@ -1,139 +0,0 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Lock, User } from 'lucide-react'
import { useAuthStore } from '@/store/auth'
import { toast, Toaster } from 'sonner'
const formSchema = z.object({
account: z.string().min(3, '账号至少需要3个字符'),
password: z.string().min(6, '密码至少需要6个字符'),
})
export default function LoginPage() {
const router = useRouter()
const [loading, setLoading] = useState(false)
const setAuth = useAuthStore(state => state.setAuth)
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
account: '',
password: '',
},
})
async function onSubmit(values: z.infer<typeof formSchema>) {
setLoading(true)
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || '登录失败')
}
if (data.success) {
toast.success('登录成功', {
description: '正在跳转到仪表盘...',
})
setAuth(true)
await new Promise(resolve => setTimeout(resolve, 1000))
router.push('/dashboard')
router.refresh()
}
}
catch (error) {
toast.error('登录失败', {
description: error instanceof Error ? error.message : '服务器连接失败,请稍后重试',
})
}
finally {
setLoading(false)
}
}
return (
<>
<div className="flex h-screen items-center justify-center bg-gray-50">
<Card className="w-[350px] shadow-lg">
<CardHeader className="space-y-1">
<CardTitle className="text-2xl font-bold text-center"></CardTitle>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="account"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<div className="relative">
<User className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="请输入您的账号"
className="pl-8"
{...field}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<div className="relative">
<Lock className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input type="password" placeholder="请输入密码" className="pl-8" {...field} />
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"
disabled={loading}
size="lg"
>
{loading
? (
<div className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
...
</div>
)
: (
'登录'
)}
</Button>
</form>
</Form>
</CardContent>
</Card>
</div>
<Toaster richColors />
</>
)
}

View File

@@ -1,77 +0,0 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma' // 使用统一的prisma实例
import { compare } from 'bcryptjs'
import { z } from 'zod'
const loginSchema = z.object({
account: z.string().min(3, '账号至少需要3个字符'),
password: z.string().min(6, '密码至少需要6个字符'),
})
export async function POST(request: Request) {
try {
const body = await request.json()
const { account, password } = loginSchema.parse(body)
const user = await prisma.user.findFirst({
where: {
OR: [
{ account: account.trim() },
{ password: account.trim() },
],
},
})
if (!user) {
return NextResponse.json(
{ success: false, error: '用户不存在' },
{ status: 401 },
)
}
// 验证密码
const passwordMatch = await compare(password, user.password || '')
if (!passwordMatch) {
return NextResponse.json({
success: false,
error: '密码错误',
}, { status: 401 })
}
// 创建会话
const sessionToken = crypto.randomUUID()
await prisma.session.create({
data: {
id: sessionToken,
userId: user.id,
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
})
// 设置cookie
const response = NextResponse.json({
success: true,
user: {
id: user.id,
account: user.account,
name: user.name,
},
})
response.cookies.set('session', sessionToken, {
httpOnly: true,
// secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7,
})
return response
}
catch (error) {
console.error('登录错误:', error)
return NextResponse.json(
{ success: false, error: '服务器错误,请稍后重试' },
{ status: 500 },
)
}
}

View File

@@ -1,38 +0,0 @@
import { NextResponse } from 'next/server'
import { cookies } from 'next/headers'
import { prisma } from '@/lib/prisma'
export async function POST() {
try {
const cookieStore = await cookies()
const sessionToken = cookieStore.get('session')?.value
// 删除数据库中的session如果存在
if (sessionToken) {
await prisma.session.deleteMany({
where: { id: sessionToken },
}).catch(() => {
// 忽略删除错误确保cookie被清除
})
}
// 清除cookie
const response = NextResponse.json({ success: true })
response.cookies.set('session', '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 0, // 立即过期
path: '/',
})
return response
}
catch (error) {
console.error('退出错误:', error)
return NextResponse.json(
{ success: false, error: '退出失败' },
{ status: 500 },
)
}
}

View File

@@ -1,132 +0,0 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { hash } from 'bcryptjs'
// 获取所有用户
export async function GET() {
try {
const users = await prisma.user.findMany({
select: {
id: true,
account: true,
name: true,
createdAt: true,
updatedAt: true,
},
orderBy: {
createdAt: 'desc',
},
})
return NextResponse.json({
success: true,
users,
})
}
catch (error) {
console.error('获取用户列表错误:', error)
return NextResponse.json(
{ success: false, error: '服务器错误,请稍后重试' },
{ status: 500 },
)
}
}
// 创建用户
export async function POST(request: Request) {
try {
const { account, password, name } = await request.json()
// 检查用户是否已存在
const existingUser = await prisma.user.findUnique({
where: { account },
})
if (existingUser) {
return NextResponse.json(
{ success: false, error: '用户账号已存在' },
{ status: 400 },
)
}
// 加密密码
const hashedPassword = await hash(password, 10)
// 创建用户
const user = await prisma.user.create({
data: {
account,
password: hashedPassword,
name: name || account,
},
})
// 不返回密码字段
const { password: _, ...userWithoutPassword } = user
return NextResponse.json({
success: true,
user: userWithoutPassword,
})
}
catch (error) {
console.error('创建用户错误:', error)
return NextResponse.json(
{ success: false, error: '服务器错误,请稍后重试' },
{ status: 500 },
)
}
}
// 删除用户
export async function DELETE(request: Request) {
try {
// 从URL中获取查询参数
const url = new URL(request.url)
const id = url.searchParams.get('id')
if (!id) {
return NextResponse.json(
{ success: false, error: '用户ID不能为空' },
{ status: 400 },
)
}
const userId = parseInt(id)
if (isNaN(userId)) {
return NextResponse.json(
{ success: false, error: '无效的用户ID' },
{ status: 400 },
)
}
// 检查用户是否存在
const existingUser = await prisma.user.findUnique({
where: { id: userId },
})
if (!existingUser) {
return NextResponse.json(
{ success: false, error: '用户不存在' },
{ status: 404 },
)
}
// 删除用户
await prisma.user.delete({
where: { id: userId },
})
return NextResponse.json({
success: true,
message: '用户删除成功',
})
}
catch (error) {
console.error('删除用户错误:', error)
return NextResponse.json(
{ success: false, error: '服务器错误,请稍后重试' },
{ status: 500 },
)
}
}

View File

@@ -1,23 +1,10 @@
'use client'
import { useEffect, useState, useCallback } from 'react'
import { formatNumber, validateNumber } from '@/lib/formatters'
import LoadingCard from '@/components/ui/loadingCard'
import ErrorCard from '@/components/ui/errorCard'
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
interface AllocationStatus {
city: string
count: number
assigned: number
}
interface ApiAllocationStatus {
city?: string
count: number | string | bigint
assigned: number | string | bigint
unique_allocated_ips: number | string | bigint
}
import { getAllocationStatus, type AllocationStatus } from '@/actions/stats'
export default function AllocationStatus({ detailed = false }: { detailed?: boolean }) {
const [data, setData] = useState<AllocationStatus[]>([])
@@ -48,21 +35,16 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
setLoading(true)
const hours = getTimeHours()
const response = await fetch(`/api/stats?type=allocation_status&hours=${hours}`)
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const result = await response.json()
const result = await getAllocationStatus(hours)
// 数据验证
const validatedData = (result as ApiAllocationStatus[]).map(item => ({
const validatedData = (result.data).map(item => ({
city: item.city || '未知',
count: validateNumber(item.count),
assigned: validateNumber(item.assigned),
count: item.count || BigInt(0),
assigned: item.assigned || BigInt(0),
}))
const sortedData = validatedData.sort((a, b) => b.count - a.count)
const sortedData = validatedData.sort((a, b) => Number(b.count - a.count))
setData(sortedData)
}
@@ -140,18 +122,18 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
</TableHeader>
<TableBody>
{data.map((item, index) => {
const overage = calculateOverage(item.assigned, item.count)
const overage = calculateOverage(Number(item.assigned), Number(item.count))
return (
<TableRow
key={index}
className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
>
<TableCell className="px-4 py-2">{item.city}</TableCell>
<TableCell className="px-4 py-2">{formatNumber(item.count)}</TableCell>
<TableCell className="px-4 py-2">{formatNumber(item.assigned)}</TableCell>
<TableCell className="px-4 py-2">{item.count}</TableCell>
<TableCell className="px-4 py-2">{item.assigned}</TableCell>
<TableCell className="px-4 py-2">
<span className={overage > 0 ? 'text-red-600 font-medium' : ''}>
{formatNumber(overage)}
{overage}
</span>
</TableCell>
</TableRow>

View File

@@ -2,14 +2,7 @@
import { useEffect, useState } from 'react'
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
interface CityNode {
city: string
count: number
hash: string
label: string
offset: string
}
import { getCityNodeCount, type CityNode } from '@/actions/stats'
export default function CityNodeStats() {
const [data, setData] = useState<CityNode[]>([])
@@ -21,9 +14,8 @@ export default function CityNodeStats() {
const fetchData = async () => {
try {
const response = await fetch('/api/stats?type=city_node_count')
const result = await response.json()
setData(result)
const result = await getCityNodeCount()
setData(result.data)
}
catch (error) {
console.error('获取城市节点数据失败:', error)

View File

@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
import { validateNumber } from '@/lib/formatters'
import { Pagination } from '@/components/ui/pagination'
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
import { getEdgeNodes } from '@/actions/stats'
interface Edge {
id: number
@@ -39,10 +40,9 @@ export default function Edge() {
// 计算偏移量
const offset = (currentPage - 1) * itemsPerPage
const response = await fetch(`/api/stats?type=edge_nodes&offset=${offset}&limit=${itemsPerPage}`)
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
// if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const result = await response.json()
const result = await getEdgeNodes(offset, itemsPerPage)
type ResultEdge = {
id: number
macaddr: string

View File

@@ -3,23 +3,17 @@ import { useEffect, useState, Suspense } from 'react'
import { useSearchParams } from 'next/navigation'
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
import { Pagination } from '@/components/ui/pagination'
import { getGatewayConfig, type GatewayConfig } from '@/actions/stats'
interface GatewayConfig {
city: string
edge: string
user: string
public: string
inner_ip: string
ischange: number
isonline: number
}
interface ApiResponse {
data: GatewayConfig[]
totalCount: number
currentPage: number
totalPages: number
}
// interface GatewayConfig {
// city: string
// edge: string
// user: string
// public: string
// inner_ip: string
// ischange: number
// isonline: number
// }
function GatewayConfigContent() {
const [data, setData] = useState<GatewayConfig[]>([])
@@ -59,18 +53,7 @@ function GatewayConfigContent() {
// 计算偏移量
const offset = (page - 1) * limit
// 构建API URL
let apiUrl = `/api/stats?type=gateway_config&offset=${offset}&limit=${limit}`
if (mac.trim()) {
apiUrl += `&mac=${encodeURIComponent(mac)}`
}
const response = await fetch(apiUrl)
if (!response.ok) {
throw new Error(`HTTP错误! 状态: ${response.status}`)
}
const result: ApiResponse = await response.json()
const result = await getGatewayConfig(offset, limit, mac)
// 检查返回的数据是否有效
if (!result.data || result.data.length === 0) {

View File

@@ -8,13 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
interface GatewayInfo {
macaddr: string
inner_ip: string
setid: string
enable: number
}
import { getGatewayInfo, type GatewayInfo } from '@/actions/stats'
const filterSchema = z.object({
status: z.string(),
@@ -75,13 +69,9 @@ export default function Gatewayinfo() {
try {
setLoading(true)
setError('')
const response = await fetch('/api/stats?type=gateway_info')
if (!response.ok) {
throw new Error('获取网关信息失败')
}
const result = await response.json()
// const sortedData = result.sort(( a, b) => Number(a.inner_ip) - Number(b.inner_ip))
const sortedData = result.sort((a: GatewayInfo, b: GatewayInfo) =>
const result = await getGatewayInfo()
const sortedData = result.data.sort((a: GatewayInfo, b: GatewayInfo) =>
sortByIpAddress(a.inner_ip, b.inner_ip),
)
setData(sortedData)

View File

@@ -10,12 +10,15 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '
import { User, Lock, Search, Trash2, Plus, X } from 'lucide-react'
import { toast, Toaster } from 'sonner'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { findUsers, createUser, removeUser } from '@/actions/user'
// 用户类型定义
interface UserData {
id: string
id: number
account: string
createdAt: string
createdAt: Date
name: string | null
updatedAt: Date
}
const formSchema = z.object({
@@ -45,15 +48,10 @@ export default function Settings() {
// 获取用户列表
const fetchUsers = async () => {
try {
const response = await fetch('/api/users')
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || '获取用户列表失败')
}
const data = await findUsers()
if (data.success) {
setUsers(data.users)
setUsers(data.data)
}
}
catch (error) {
@@ -72,23 +70,12 @@ export default function Settings() {
async function onSubmit(values: z.infer<typeof formSchema>) {
setLoading(true)
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
account: values.account,
password: values.password,
}),
const data = await createUser({
account: values.account,
password: values.password,
name: '',
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || '创建用户失败')
}
if (data.success) {
toast.success('用户创建成功', {
description: '新账户已成功添加',
@@ -115,17 +102,7 @@ export default function Settings() {
}
try {
// 使用查询参数传递ID
const response = await fetch(`/api/users?id=${userId}`, {
method: 'DELETE',
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || '删除用户失败')
}
const data = await removeUser(userId)
if (data.success) {
toast.success('用户删除成功', {
description: '用户账户已删除',

View File

@@ -9,6 +9,8 @@ import AllocationStatus from './components/allocationStatus'
import Settings from './components/settings'
import Edge from './components/edge'
import { LogOut } from 'lucide-react'
import { logout } from '@/actions/auth'
import { toast } from 'sonner'
const tabs = [
{ id: 'gatewayInfo', label: '网关信息' },
@@ -37,11 +39,9 @@ function DashboardContent() {
const handleLogout = async () => {
setIsLoading(true)
try {
const response = await fetch('/api/auth/logout', {
method: 'POST',
})
const response = await logout()
if (response.ok) {
if (response) {
// 退出成功后跳转到登录页
router.push('/login')
router.refresh()

View File

@@ -1,20 +1,3 @@
// 数字格式化工具函数
export const formatNumber = (num: number | string): string => {
const numberValue = typeof num === 'string' ? parseInt(num) || 0 : num
return numberValue.toLocaleString('zh-CN')
}
export const formatLargeNumber = (num: number | string): string => {
const numberValue = typeof num === 'string' ? parseInt(num) || 0 : num
if (numberValue > 1e9) return `${(numberValue / 1e9).toFixed(1)}亿`
if (numberValue > 1e6) return `${(numberValue / 1e6).toFixed(1)}百万`
if (numberValue > 1e4) return `${(numberValue / 1e4).toFixed(1)}`
if (numberValue > 1e3) return `${(numberValue / 1e3).toFixed(1)}`
return numberValue.toLocaleString('zh-CN')
}
// 数据验证函数
export const validateNumber = (value: unknown): number => {
if (typeof value === 'number') return value

View File

@@ -4,10 +4,11 @@ const globalForPrisma = global as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
export default prisma
export * from '@/generated/prisma/client'

View File

@@ -9,7 +9,6 @@ export const config = {
const isIgnored = [
'/login',
'/api/auth/login',
]
export async function middleware(request: NextRequest) {