添加actions文件移除route文件
This commit is contained in:
115
src/actions/auth.ts
Normal file
115
src/actions/auth.ts
Normal 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: '退出失败',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,196 +1,18 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
'use server'
|
||||||
import { prisma } from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
|
|
||||||
// 处理 BigInt 序列化
|
export type AllocationStatus = {
|
||||||
function safeSerialize(data: unknown) {
|
city: string
|
||||||
return JSON.parse(JSON.stringify(data, (key, value) =>
|
count: bigint
|
||||||
typeof value === 'bigint' ? value.toString() : value,
|
assigned: bigint
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
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 })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 城市分配状态
|
// 城市分配状态
|
||||||
async function getAllocationStatus(request: NextRequest) {
|
export async function getAllocationStatus(hours: number) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url)
|
const hoursNum = hours || 24
|
||||||
const hours = searchParams.get('hours') || '24'
|
|
||||||
const hoursNum = parseInt(hours) || 24
|
|
||||||
|
|
||||||
// 使用参数化查询防止SQL注入
|
// 使用参数化查询防止SQL注入
|
||||||
const result = await prisma.$queryRaw`
|
const result: AllocationStatus[] = await prisma.$queryRaw`
|
||||||
SELECT
|
SELECT
|
||||||
city,
|
city,
|
||||||
c1.count AS count,
|
c1.count AS count,
|
||||||
@@ -222,24 +44,185 @@ async function getAllocationStatus(request: NextRequest) {
|
|||||||
WHERE
|
WHERE
|
||||||
cityhash.macaddr IS NOT NULL;
|
cityhash.macaddr IS NOT NULL;
|
||||||
`
|
`
|
||||||
return NextResponse.json(safeSerialize(result))
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('Allocation status query error:', error)
|
console.error('Allocation status query error:', error)
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||||
return NextResponse.json(
|
return {
|
||||||
{ error: '查询分配状态失败: ' + errorMessage },
|
success: false,
|
||||||
{ status: 500 },
|
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 {
|
try {
|
||||||
const { searchParams } = new URL(request.url)
|
const offset = off || 0
|
||||||
const offset = parseInt(searchParams.get('offset') || '0')
|
const limit = itemsPerPage || 100
|
||||||
const limit = parseInt(searchParams.get('limit') || '100')
|
|
||||||
// 获取总数 - 使用类型断言
|
// 获取总数 - 使用类型断言
|
||||||
const totalCountResult = await prisma.$queryRaw<[{ total: bigint }]>`
|
const totalCountResult = await prisma.$queryRaw<[{ total: bigint }]>`
|
||||||
SELECT COUNT(*) as total
|
SELECT COUNT(*) as total
|
||||||
@@ -257,15 +240,19 @@ async function getEdgeNodes(request: NextRequest) {
|
|||||||
ORDER BY edge.id
|
ORDER BY edge.id
|
||||||
LIMIT ${limit} OFFSET ${offset}
|
LIMIT ${limit} OFFSET ${offset}
|
||||||
`
|
`
|
||||||
return NextResponse.json({
|
return {
|
||||||
data: safeSerialize(result),
|
data: result,
|
||||||
totalCount: totalCount,
|
totalCount: totalCount,
|
||||||
currentPage: Math.floor(offset / limit) + 1,
|
currentPage: Math.floor(offset / limit) + 1,
|
||||||
totalPages: Math.ceil(totalCount / limit),
|
totalPages: Math.ceil(totalCount / limit),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('Edge nodes query error:', 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
145
src/actions/user.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,10 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import { formatNumber, validateNumber } from '@/lib/formatters'
|
|
||||||
import LoadingCard from '@/components/ui/loadingCard'
|
import LoadingCard from '@/components/ui/loadingCard'
|
||||||
import ErrorCard from '@/components/ui/errorCard'
|
import ErrorCard from '@/components/ui/errorCard'
|
||||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||||
|
import { getAllocationStatus, type AllocationStatus } from '@/actions/stats'
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AllocationStatus({ detailed = false }: { detailed?: boolean }) {
|
export default function AllocationStatus({ detailed = false }: { detailed?: boolean }) {
|
||||||
const [data, setData] = useState<AllocationStatus[]>([])
|
const [data, setData] = useState<AllocationStatus[]>([])
|
||||||
@@ -48,21 +35,16 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
const hours = getTimeHours()
|
const hours = getTimeHours()
|
||||||
|
const result = await getAllocationStatus(hours)
|
||||||
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 validatedData = (result as ApiAllocationStatus[]).map(item => ({
|
const validatedData = (result.data).map(item => ({
|
||||||
city: item.city || '未知',
|
city: item.city || '未知',
|
||||||
count: validateNumber(item.count),
|
count: item.count || BigInt(0),
|
||||||
assigned: validateNumber(item.assigned),
|
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)
|
setData(sortedData)
|
||||||
}
|
}
|
||||||
@@ -140,18 +122,18 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((item, index) => {
|
{data.map((item, index) => {
|
||||||
const overage = calculateOverage(item.assigned, item.count)
|
const overage = calculateOverage(Number(item.assigned), Number(item.count))
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={index}
|
key={index}
|
||||||
className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
|
className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
|
||||||
>
|
>
|
||||||
<TableCell className="px-4 py-2">{item.city}</TableCell>
|
<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">{item.count}</TableCell>
|
||||||
<TableCell className="px-4 py-2">{formatNumber(item.assigned)}</TableCell>
|
<TableCell className="px-4 py-2">{item.assigned}</TableCell>
|
||||||
<TableCell className="px-4 py-2">
|
<TableCell className="px-4 py-2">
|
||||||
<span className={overage > 0 ? 'text-red-600 font-medium' : ''}>
|
<span className={overage > 0 ? 'text-red-600 font-medium' : ''}>
|
||||||
{formatNumber(overage)}
|
{overage}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -2,14 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||||
|
import { getCityNodeCount, type CityNode } from '@/actions/stats'
|
||||||
interface CityNode {
|
|
||||||
city: string
|
|
||||||
count: number
|
|
||||||
hash: string
|
|
||||||
label: string
|
|
||||||
offset: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CityNodeStats() {
|
export default function CityNodeStats() {
|
||||||
const [data, setData] = useState<CityNode[]>([])
|
const [data, setData] = useState<CityNode[]>([])
|
||||||
@@ -21,9 +14,8 @@ export default function CityNodeStats() {
|
|||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/stats?type=city_node_count')
|
const result = await getCityNodeCount()
|
||||||
const result = await response.json()
|
setData(result.data)
|
||||||
setData(result)
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('获取城市节点数据失败:', error)
|
console.error('获取城市节点数据失败:', error)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import { validateNumber } from '@/lib/formatters'
|
import { validateNumber } from '@/lib/formatters'
|
||||||
import { Pagination } from '@/components/ui/pagination'
|
import { Pagination } from '@/components/ui/pagination'
|
||||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||||
|
import { getEdgeNodes } from '@/actions/stats'
|
||||||
|
|
||||||
interface Edge {
|
interface Edge {
|
||||||
id: number
|
id: number
|
||||||
@@ -39,10 +40,9 @@ export default function Edge() {
|
|||||||
// 计算偏移量
|
// 计算偏移量
|
||||||
const offset = (currentPage - 1) * itemsPerPage
|
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 = {
|
type ResultEdge = {
|
||||||
id: number
|
id: number
|
||||||
macaddr: string
|
macaddr: string
|
||||||
|
|||||||
@@ -3,23 +3,17 @@ import { useEffect, useState, Suspense } from 'react'
|
|||||||
import { useSearchParams } from 'next/navigation'
|
import { useSearchParams } from 'next/navigation'
|
||||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||||
import { Pagination } from '@/components/ui/pagination'
|
import { Pagination } from '@/components/ui/pagination'
|
||||||
|
import { getGatewayConfig, type GatewayConfig } from '@/actions/stats'
|
||||||
|
|
||||||
interface GatewayConfig {
|
// interface GatewayConfig {
|
||||||
city: string
|
// city: string
|
||||||
edge: string
|
// edge: string
|
||||||
user: string
|
// user: string
|
||||||
public: string
|
// public: string
|
||||||
inner_ip: string
|
// inner_ip: string
|
||||||
ischange: number
|
// ischange: number
|
||||||
isonline: number
|
// isonline: number
|
||||||
}
|
// }
|
||||||
|
|
||||||
interface ApiResponse {
|
|
||||||
data: GatewayConfig[]
|
|
||||||
totalCount: number
|
|
||||||
currentPage: number
|
|
||||||
totalPages: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function GatewayConfigContent() {
|
function GatewayConfigContent() {
|
||||||
const [data, setData] = useState<GatewayConfig[]>([])
|
const [data, setData] = useState<GatewayConfig[]>([])
|
||||||
@@ -59,18 +53,7 @@ function GatewayConfigContent() {
|
|||||||
// 计算偏移量
|
// 计算偏移量
|
||||||
const offset = (page - 1) * limit
|
const offset = (page - 1) * limit
|
||||||
|
|
||||||
// 构建API URL
|
const result = await getGatewayConfig(offset, limit, mac)
|
||||||
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()
|
|
||||||
|
|
||||||
// 检查返回的数据是否有效
|
// 检查返回的数据是否有效
|
||||||
if (!result.data || result.data.length === 0) {
|
if (!result.data || result.data.length === 0) {
|
||||||
|
|||||||
@@ -8,13 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { getGatewayInfo, type GatewayInfo } from '@/actions/stats'
|
||||||
interface GatewayInfo {
|
|
||||||
macaddr: string
|
|
||||||
inner_ip: string
|
|
||||||
setid: string
|
|
||||||
enable: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterSchema = z.object({
|
const filterSchema = z.object({
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
@@ -75,13 +69,9 @@ export default function Gatewayinfo() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
const response = await fetch('/api/stats?type=gateway_info')
|
const result = await getGatewayInfo()
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('获取网关信息失败')
|
const sortedData = result.data.sort((a: GatewayInfo, b: GatewayInfo) =>
|
||||||
}
|
|
||||||
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) =>
|
|
||||||
sortByIpAddress(a.inner_ip, b.inner_ip),
|
sortByIpAddress(a.inner_ip, b.inner_ip),
|
||||||
)
|
)
|
||||||
setData(sortedData)
|
setData(sortedData)
|
||||||
|
|||||||
@@ -10,12 +10,15 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '
|
|||||||
import { User, Lock, Search, Trash2, Plus, X } from 'lucide-react'
|
import { User, Lock, Search, Trash2, Plus, X } from 'lucide-react'
|
||||||
import { toast, Toaster } from 'sonner'
|
import { toast, Toaster } from 'sonner'
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
|
import { findUsers, createUser, removeUser } from '@/actions/user'
|
||||||
|
|
||||||
// 用户类型定义
|
// 用户类型定义
|
||||||
interface UserData {
|
interface UserData {
|
||||||
id: string
|
id: number
|
||||||
account: string
|
account: string
|
||||||
createdAt: string
|
createdAt: Date
|
||||||
|
name: string | null
|
||||||
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
@@ -45,15 +48,10 @@ export default function Settings() {
|
|||||||
// 获取用户列表
|
// 获取用户列表
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/users')
|
const data = await findUsers()
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.error || '获取用户列表失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setUsers(data.users)
|
setUsers(data.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -72,23 +70,12 @@ export default function Settings() {
|
|||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/users', {
|
const data = await createUser({
|
||||||
method: 'POST',
|
account: values.account,
|
||||||
headers: {
|
password: values.password,
|
||||||
'Content-Type': 'application/json',
|
name: '',
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
account: values.account,
|
|
||||||
password: values.password,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.error || '创建用户失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toast.success('用户创建成功', {
|
toast.success('用户创建成功', {
|
||||||
description: '新账户已成功添加',
|
description: '新账户已成功添加',
|
||||||
@@ -115,17 +102,7 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用查询参数传递ID
|
const data = await removeUser(userId)
|
||||||
const response = await fetch(`/api/users?id=${userId}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.error || '删除用户失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toast.success('用户删除成功', {
|
toast.success('用户删除成功', {
|
||||||
description: '用户账户已删除',
|
description: '用户账户已删除',
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import AllocationStatus from './components/allocationStatus'
|
|||||||
import Settings from './components/settings'
|
import Settings from './components/settings'
|
||||||
import Edge from './components/edge'
|
import Edge from './components/edge'
|
||||||
import { LogOut } from 'lucide-react'
|
import { LogOut } from 'lucide-react'
|
||||||
|
import { logout } from '@/actions/auth'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'gatewayInfo', label: '网关信息' },
|
{ id: 'gatewayInfo', label: '网关信息' },
|
||||||
@@ -37,11 +39,9 @@ function DashboardContent() {
|
|||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/auth/logout', {
|
const response = await logout()
|
||||||
method: 'POST',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
if (response) {
|
||||||
// 退出成功后跳转到登录页
|
// 退出成功后跳转到登录页
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
router.refresh()
|
router.refresh()
|
||||||
|
|||||||
@@ -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 => {
|
export const validateNumber = (value: unknown): number => {
|
||||||
if (typeof value === 'number') return value
|
if (typeof value === 'number') return value
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ const globalForPrisma = global as unknown as {
|
|||||||
prisma: PrismaClient | undefined
|
prisma: PrismaClient | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
globalForPrisma.prisma = prisma
|
globalForPrisma.prisma = prisma
|
||||||
}
|
}
|
||||||
|
|
||||||
export default prisma
|
export default prisma
|
||||||
|
export * from '@/generated/prisma/client'
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export const config = {
|
|||||||
|
|
||||||
const isIgnored = [
|
const isIgnored = [
|
||||||
'/login',
|
'/login',
|
||||||
'/api/auth/login',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export async function middleware(request: NextRequest) {
|
export async function middleware(request: NextRequest) {
|
||||||
|
|||||||
Reference in New Issue
Block a user