🎉 init: 小龙的工作空间

This commit is contained in:
大海
2026-06-06 10:40:48 +08:00
commit a188ee1426
3201 changed files with 231817 additions and 0 deletions
@@ -0,0 +1,25 @@
{
"name": "@network-tool/control-server",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "tsx watch src/index.ts",
"start": "tsx src/index.ts",
"build": "tsc",
"db:init": "tsx src/db/init.ts"
},
"dependencies": {
"@fastify/cors": "^9.0.1",
"@fastify/websocket": "^10.0.1",
"@network-tool/shared-types": "*",
"@network-tool/shared-crypto": "*",
"fastify": "^4.26.0",
"pino-pretty": "^11.0.0",
"sql.js": "^1.10.0"
},
"devDependencies": {
"@types/node": "^20.11.0",
"tsx": "^4.7.0",
"typescript": "^5.4.0"
}
}
@@ -0,0 +1,196 @@
import initSqlJs, { Database as SqlJsDatabase, Statement } from 'sql.js';
import fs from 'fs';
import path from 'path';
const DB_PATH = process.env.DB_PATH || path.join(__dirname, '..', 'data', 'network.db');
let db: SqlJsDatabase | null = null;
export async function getDb(): Promise<SqlJsDatabase> {
if (!db) {
const SQL = await initSqlJs();
// 确保 data 目录存在
const dir = path.dirname(DB_PATH);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// 尝试从文件加载,否则创建新数据库
if (fs.existsSync(DB_PATH)) {
const buffer = fs.readFileSync(DB_PATH);
db = new SQL.Database(buffer);
} else {
db = new SQL.Database();
}
db.run('PRAGMA journal_mode = WAL');
db.run('PRAGMA foreign_keys = ON');
initSchema(db);
console.log('[DB] 数据库已初始化');
}
return db;
}
/**
* 手动保存数据库到磁盘
*/
export function saveDb(): void {
if (!db) return;
const data = db.export();
const buffer = Buffer.from(data);
fs.writeFileSync(DB_PATH, buffer);
console.log('[DB] 数据库已保存');
}
function initSchema(database: SqlJsDatabase): void {
database.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user' CHECK(role IN ('user', 'admin')),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)
`);
database.run(`
CREATE TABLE IF NOT EXISTS devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
device_name TEXT NOT NULL,
device_fingerprint TEXT UNIQUE NOT NULL,
public_key TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'offline' CHECK(status IN ('online', 'offline')),
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
database.run(`
CREATE TABLE IF NOT EXISTS relay_nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
host TEXT NOT NULL,
port INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'offline' CHECK(status IN ('online', 'offline', 'busy')),
load INTEGER NOT NULL DEFAULT 0,
last_heartbeat_at TEXT NOT NULL DEFAULT (datetime('now'))
)
`);
database.run(`
CREATE TABLE IF NOT EXISTS connection_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
device_id INTEGER NOT NULL,
node_id INTEGER NOT NULL,
token_hash TEXT NOT NULL,
expires_at TEXT NOT NULL,
used_at TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE,
FOREIGN KEY (node_id) REFERENCES relay_nodes(id) ON DELETE CASCADE
)
`);
database.run(`
CREATE TABLE IF NOT EXISTS connection_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
device_id INTEGER,
node_id INTEGER,
action TEXT NOT NULL,
status TEXT NOT NULL CHECK(status IN ('success', 'failed')),
message TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)
`);
// 索引
database.run('CREATE INDEX IF NOT EXISTS idx_devices_user ON devices(user_id)');
database.run('CREATE INDEX IF NOT EXISTS idx_devices_fingerprint ON devices(device_fingerprint)');
database.run('CREATE INDEX IF NOT EXISTS idx_nodes_status ON relay_nodes(status)');
database.run('CREATE INDEX IF NOT EXISTS idx_tokens_hash ON connection_tokens(token_hash)');
database.run('CREATE INDEX IF NOT EXISTS idx_logs_created ON connection_logs(created_at)');
}
/**
* 辅助函数:查询单行
*/
export function queryOne<T = Record<string, any>>(
database: SqlJsDatabase,
sql: string,
params: any[] = []
): T | null {
try {
const stmt = database.prepare(sql);
if (params.length > 0) stmt.bind(params);
if (stmt.step()) {
const row = stmt.getAsObject() as T;
stmt.free();
return row;
}
stmt.free();
return null;
} catch (err) {
console.error('[DB] queryOne error:', sql, err);
return null;
}
}
/**
* 辅助函数:查询多行
*/
export function queryAll<T = Record<string, any>>(
database: SqlJsDatabase,
sql: string,
params: any[] = []
): T[] {
try {
const stmt = database.prepare(sql);
if (params.length > 0) stmt.bind(params);
const results: T[] = [];
while (stmt.step()) {
results.push(stmt.getAsObject() as T);
}
stmt.free();
return results;
} catch (err) {
console.error('[DB] queryAll error:', sql, err);
return [];
}
}
/**
* 辅助函数:执行 INSERT/UPDATE/DELETE
*/
export function execute(
database: SqlJsDatabase,
sql: string,
params: any[] = []
): { changes: number; lastInsertRowid: number } {
try {
database.run(sql, params);
const lastId = database.exec("SELECT last_insert_rowid() as id");
const lastInsertRowid = lastId.length > 0 ? (lastId[0].values[0][0] as number) : 0;
return {
changes: database.getRowsModified(),
lastInsertRowid,
};
} catch (err) {
console.error('[DB] execute error:', sql, err);
throw err;
}
}
/**
* 独立的数据库初始化脚本(用于 npm run db:init
*/
if (require.main === module) {
getDb().then(() => {
saveDb();
console.log('[DB] 数据库初始化完成:', DB_PATH);
process.exit(0);
});
}
@@ -0,0 +1,10 @@
import { getDb, saveDb } from './database';
getDb().then(() => {
saveDb();
console.log('[DB Init] 数据库初始化完成');
process.exit(0);
}).catch(err => {
console.error('[DB Init] 失败:', err);
process.exit(1);
});
@@ -0,0 +1,106 @@
import Fastify from 'fastify';
import cors from '@fastify/cors';
import websocket from '@fastify/websocket';
import { getDb, saveDb } from './db/database';
import { authRoutes } from './routes/auth.routes';
import { deviceRoutes } from './routes/device.routes';
import { nodeRoutes } from './routes/node.routes';
import { connectionRoutes } from './routes/connection.routes';
const PORT = parseInt(process.env.PORT || '3001', 10);
const HOST = process.env.HOST || '0.0.0.0';
async function main() {
// 异步初始化数据库
await getDb();
console.log('[Server] 数据库已初始化');
const app = Fastify({
logger: {
level: 'info',
transport: {
target: 'pino-pretty',
options: { colorize: true },
},
},
});
// 插件
await app.register(cors, { origin: true });
await app.register(websocket);
// WebSocket 端点
const connectedClients = new Set<any>();
app.get('/ws', { websocket: true }, (socket, _req) => {
connectedClients.add(socket);
console.log(`[WS] 客户端连接,当前连接数: ${connectedClients.size}`);
socket.on('message', (data: Buffer) => {
try {
const msg = JSON.parse(data.toString());
for (const client of connectedClients) {
if (client !== socket && client.readyState === 1) {
client.send(JSON.stringify(msg));
}
}
} catch { /* ignore */ }
});
socket.on('close', () => {
connectedClients.delete(socket);
console.log(`[WS] 客户端断开,当前连接数: ${connectedClients.size}`);
});
});
// 注册路由
await app.register(authRoutes);
await app.register(deviceRoutes);
await app.register(nodeRoutes);
await app.register(connectionRoutes);
// 健康检查
app.get('/api/health', async () => ({
success: true,
message: 'Control Server is running',
timestamp: new Date().toISOString(),
}));
// 全局错误处理
app.setErrorHandler((error, _request, reply) => {
console.error('[Server Error]', error);
reply.status(error.statusCode || 500).send({
success: false,
error: 'INTERNAL_ERROR',
message: error.message || 'Internal server error',
});
});
// 定时保存数据库(sql.js 是内存型,需要定期持久化)
setInterval(() => {
try {
saveDb();
} catch { /* ignore */ }
}, 30000);
// 定时清理过期数据
setInterval(async () => {
try {
const db = await getDb();
const { execute } = require('./db/database');
execute(db, "UPDATE relay_nodes SET status = 'offline' WHERE status != 'offline' AND last_heartbeat_at < datetime('now', '-5 minutes')");
execute(db, "DELETE FROM connection_tokens WHERE expires_at < datetime('now') AND used_at IS NULL");
saveDb();
} catch { /* ignore */ }
}, 60000);
// 启动服务
await app.listen({ port: PORT, host: HOST });
console.log(`[Server] Control Server 已启动: http://${HOST}:${PORT}`);
console.log(`[Server] WebSocket: ws://${HOST}:${PORT}/ws`);
}
main().catch((err) => {
console.error('[Server] 启动失败:', err);
process.exit(1);
});
@@ -0,0 +1,45 @@
import { FastifyRequest, FastifyReply } from 'fastify';
import { verifyToken, JwtPayload } from '@network-tool/shared-crypto';
import { JWT_SECRET } from '../services/auth.service';
declare module 'fastify' {
interface FastifyRequest {
user?: JwtPayload;
}
}
/**
* JWT 认证中间件
*/
export async function authMiddleware(
request: FastifyRequest,
reply: FastifyReply
): Promise<void> {
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
reply.status(401).send({ success: false, error: 'UNAUTHORIZED', message: 'Missing or invalid token' });
return;
}
const token = authHeader.substring(7);
const payload = verifyToken(token, JWT_SECRET);
if (!payload) {
reply.status(401).send({ success: false, error: 'TOKEN_EXPIRED', message: 'Token expired or invalid' });
return;
}
request.user = payload;
}
/**
* Admin 权限中间件(需在 authMiddleware 之后使用)
*/
export async function adminMiddleware(
request: FastifyRequest,
reply: FastifyReply
): Promise<void> {
if (!request.user || request.user.role !== 'admin') {
reply.status(403).send({ success: false, error: 'FORBIDDEN', message: 'Admin access required' });
}
}
@@ -0,0 +1,96 @@
import { FastifyInstance } from 'fastify';
import { AuthService } from '../services/auth.service';
import { authMiddleware, adminMiddleware } from '../middleware/auth';
const authService = new AuthService();
export async function authRoutes(app: FastifyInstance): Promise<void> {
/**
* POST /api/auth/register - 用户注册
*/
app.post('/api/auth/register', async (request, reply) => {
try {
const { email, password } = request.body as { email: string; password: string };
if (!email || !password) {
return reply.status(400).send({
success: false,
error: 'VALIDATION_ERROR',
message: 'Email and password are required',
});
}
if (password.length < 6) {
return reply.status(400).send({
success: false,
error: 'VALIDATION_ERROR',
message: 'Password must be at least 6 characters',
});
}
const result = await authService.register({ email, password });
return reply.send({ success: true, data: result });
} catch (err: any) {
if (err.message === 'EMAIL_EXISTS') {
return reply.status(409).send({
success: false,
error: 'EMAIL_EXISTS',
message: 'Email already registered',
});
}
throw err;
}
});
/**
* POST /api/auth/login - 用户登录
*/
app.post('/api/auth/login', async (request, reply) => {
try {
const { email, password } = request.body as { email: string; password: string };
if (!email || !password) {
return reply.status(400).send({
success: false,
error: 'VALIDATION_ERROR',
message: 'Email and password are required',
});
}
const result = await authService.login({ email, password });
return reply.send({ success: true, data: result });
} catch (err: any) {
if (err.message === 'INVALID_CREDENTIALS') {
return reply.status(401).send({
success: false,
error: 'INVALID_CREDENTIALS',
message: 'Invalid email or password',
});
}
throw err;
}
});
/**
* GET /api/auth/me - 获取当前用户信息
*/
app.get('/api/auth/me', { preHandler: [authMiddleware] }, async (request, reply) => {
const user = await authService.getMe(request.user!.user_id);
if (!user) {
return reply.status(404).send({
success: false,
error: 'USER_NOT_FOUND',
message: 'User not found',
});
}
return reply.send({ success: true, data: user });
});
/**
* GET /api/admin/users - 管理所有用户
*/
app.get('/api/admin/users', { preHandler: [authMiddleware, adminMiddleware] }, async (request, reply) => {
const users = await authService.listUsers();
return reply.send({ success: true, data: users });
});
}
@@ -0,0 +1,71 @@
import { FastifyInstance } from 'fastify';
import { ConnectionService } from '../services/connection.service';
import { authMiddleware, adminMiddleware } from '../middleware/auth';
const connectionService = new ConnectionService();
export async function connectionRoutes(app: FastifyInstance): Promise<void> {
// 需要认证的路由
app.post('/api/connections/request', { preHandler: [authMiddleware] }, async (request, reply) => {
try {
const { device_id, node_id } = request.body as { device_id: number; node_id: number };
if (!device_id || !node_id) {
return reply.status(400).send({
success: false, error: 'VALIDATION_ERROR',
message: 'device_id and node_id are required',
});
}
const result = await connectionService.requestConnection(request.user!.user_id, { device_id, node_id });
return reply.send({ success: true, data: result });
} catch (err: any) {
if (err.message === 'DEVICE_NOT_FOUND') {
return reply.status(404).send({
success: false, error: 'DEVICE_NOT_FOUND',
message: 'Device not found or not owned by user',
});
}
if (err.message === 'NODE_NOT_AVAILABLE') {
return reply.status(400).send({
success: false, error: 'NODE_NOT_AVAILABLE',
message: 'Node not available',
});
}
throw err;
}
});
// verify-token 不需要用户认证(由 Relay Node 调用)
app.post('/api/connections/verify-token', async (request, reply) => {
const { token, device_id } = request.body as { token: string; device_id: number };
if (!token || !device_id) {
return reply.status(400).send({
success: false, error: 'VALIDATION_ERROR',
message: 'token and device_id are required',
});
}
const result = await connectionService.verifyToken({ token, device_id });
return reply.send({ success: true, data: result });
});
app.post('/api/connections/disconnect', { preHandler: [authMiddleware] }, async (request, reply) => {
const { device_id } = request.body as { device_id: number };
if (!device_id) {
return reply.status(400).send({
success: false, error: 'VALIDATION_ERROR',
message: 'device_id is required',
});
}
await connectionService.disconnect(request.user!.user_id, device_id);
return reply.send({ success: true, message: 'Disconnected' });
});
app.get('/api/connections/logs', { preHandler: [authMiddleware] }, async (request, reply) => {
const logs = await connectionService.getUserLogs(request.user!.user_id);
return reply.send({ success: true, data: logs });
});
app.get('/api/admin/connections/logs', { preHandler: [authMiddleware, adminMiddleware] }, async (_request, reply) => {
const logs = await connectionService.getLogs();
return reply.send({ success: true, data: logs });
});
}
@@ -0,0 +1,87 @@
import { FastifyInstance } from 'fastify';
import { DeviceService } from '../services/device.service';
import { authMiddleware, adminMiddleware } from '../middleware/auth';
const deviceService = new DeviceService();
export async function deviceRoutes(app: FastifyInstance): Promise<void> {
app.addHook('preHandler', authMiddleware);
app.post('/api/devices/register', async (request, reply) => {
try {
const { device_name, device_fingerprint, public_key } = request.body as {
device_name: string;
device_fingerprint: string;
public_key: string;
};
if (!device_name || !device_fingerprint) {
return reply.status(400).send({
success: false, error: 'VALIDATION_ERROR',
message: 'device_name and device_fingerprint are required',
});
}
const device = await deviceService.registerDevice(request.user!.user_id, {
device_name, device_fingerprint,
public_key: public_key || '',
});
return reply.send({ success: true, data: device });
} catch (err: any) {
if (err.message === 'DEVICE_ALREADY_EXISTS') {
return reply.status(409).send({
success: false, error: 'DEVICE_ALREADY_EXISTS',
message: 'Device already registered',
});
}
throw err;
}
});
app.get('/api/devices', async (request, reply) => {
const devices = await deviceService.getUserDevices(request.user!.user_id);
return reply.send({ success: true, data: devices });
});
app.patch('/api/devices/:id/status', async (request, reply) => {
const { id } = request.params as { id: string };
const { status } = request.body as { status: 'online' | 'offline' };
if (!status || !['online', 'offline'].includes(status)) {
return reply.status(400).send({
success: false, error: 'VALIDATION_ERROR',
message: 'Status must be "online" or "offline"',
});
}
const device = await deviceService.updateDeviceStatus(parseInt(id), status);
if (!device) {
return reply.status(404).send({
success: false, error: 'DEVICE_NOT_FOUND',
message: 'Device not found',
});
}
return reply.send({ success: true, data: device });
});
app.delete('/api/devices/:id', async (request, reply) => {
const { id } = request.params as { id: string };
const deleted = await deviceService.deleteDevice(parseInt(id), request.user!.user_id);
if (!deleted) {
return reply.status(404).send({
success: false, error: 'DEVICE_NOT_FOUND',
message: 'Device not found',
});
}
return reply.send({ success: true, message: 'Device deleted' });
});
app.get('/api/admin/devices', { preHandler: [adminMiddleware] }, async (_request, reply) => {
const devices = await deviceService.getAllDevices();
return reply.send({ success: true, data: devices });
});
}
@@ -0,0 +1,65 @@
import { FastifyInstance } from 'fastify';
import { NodeService } from '../services/node.service';
import { authMiddleware, adminMiddleware } from '../middleware/auth';
const nodeService = new NodeService();
export async function nodeRoutes(app: FastifyInstance): Promise<void> {
app.post('/api/nodes/register', async (request, reply) => {
try {
const { name, host, port } = request.body as { name: string; host: string; port: number };
if (!name || !host || !port) {
return reply.status(400).send({
success: false, error: 'VALIDATION_ERROR',
message: 'name, host, and port are required',
});
}
const node = await nodeService.registerNode({ name, host, port });
return reply.send({ success: true, data: node });
} catch (err) { throw err; }
});
app.post('/api/nodes/heartbeat', async (request, reply) => {
const { node_id, load, status } = request.body as {
node_id: number; load: number; status: 'online' | 'offline' | 'busy';
};
if (!node_id) {
return reply.status(400).send({
success: false, error: 'VALIDATION_ERROR', message: 'node_id is required',
});
}
const node = await nodeService.heartbeat(node_id, { load: load ?? 0, status: status ?? 'online' });
if (!node) {
return reply.status(404).send({
success: false, error: 'NODE_NOT_FOUND', message: 'Node not found',
});
}
return reply.send({ success: true, data: node });
});
app.get('/api/nodes', { preHandler: [authMiddleware] }, async (_request, reply) => {
const nodes = await nodeService.getAllNodes();
return reply.send({ success: true, data: nodes });
});
app.get('/api/nodes/available', { preHandler: [authMiddleware] }, async (_request, reply) => {
const nodes = await nodeService.getAvailableNodes();
return reply.send({ success: true, data: nodes });
});
app.get('/api/admin/nodes', { preHandler: [authMiddleware, adminMiddleware] }, async (_request, reply) => {
const nodes = await nodeService.getAllNodes();
return reply.send({ success: true, data: nodes });
});
app.delete('/api/admin/nodes/:id', { preHandler: [authMiddleware, adminMiddleware] }, async (request, reply) => {
const { id } = request.params as { id: string };
const deleted = await nodeService.deleteNode(parseInt(id));
if (!deleted) {
return reply.status(404).send({
success: false, error: 'NODE_NOT_FOUND', message: 'Node not found',
});
}
return reply.send({ success: true, message: 'Node deleted' });
});
}
@@ -0,0 +1,45 @@
import { getDb, queryOne, queryAll, execute, saveDb } from '../db/database';
import type { User, UserPublic } from '@network-tool/shared-types';
import { hashPassword, verifyPassword, generateToken } from '@network-tool/shared-crypto';
const JWT_SECRET = process.env.JWT_SECRET || 'network-tool-dev-secret-change-in-production';
export { JWT_SECRET };
export class AuthService {
async register(data: { email: string; password: string }) {
const db = await getDb();
if (queryOne(db, 'SELECT id FROM users WHERE email = ?', [data.email])) {
throw new Error('EMAIL_EXISTS');
}
const hash = await hashPassword(data.password);
const result = execute(db, 'INSERT INTO users (email, password_hash) VALUES (?, ?)', [data.email, hash]);
const user = queryOne<User>(db, 'SELECT * FROM users WHERE id = ?', [result.lastInsertRowid])!;
saveDb();
const token = generateToken({ user_id: user.id, email: user.email, role: user.role }, JWT_SECRET);
return { user: this.public(user), token };
}
async login(data: { email: string; password: string }) {
const db = await getDb();
const user = queryOne<User>(db, 'SELECT * FROM users WHERE email = ?', [data.email]);
if (!user) throw new Error('INVALID_CREDENTIALS');
if (!(await verifyPassword(data.password, user.password_hash))) throw new Error('INVALID_CREDENTIALS');
const token = generateToken({ user_id: user.id, email: user.email, role: user.role }, JWT_SECRET);
return { user: this.public(user), token };
}
async getMe(userId: number): Promise<UserPublic | null> {
const db = await getDb();
const user = queryOne<User>(db, 'SELECT * FROM users WHERE id = ?', [userId]);
return user ? this.public(user) : null;
}
async listUsers(): Promise<UserPublic[]> {
const db = await getDb();
return queryAll<User>(db, 'SELECT * FROM users ORDER BY created_at DESC').map(u => this.public(u));
}
private public(u: User): UserPublic {
return { id: u.id, email: u.email, role: u.role, created_at: u.created_at };
}
}
@@ -0,0 +1,106 @@
import { getDb, queryOne, queryAll, execute, saveDb } from '../db/database';
import type { ConnectionTokenResponse, TokenVerifyResponse, ConnectionLog } from '@network-tool/shared-types';
import { generateConnectionToken, sha256, generateNonce } from '@network-tool/shared-crypto';
import { JWT_SECRET } from './auth.service';
export class ConnectionService {
async requestConnection(userId: number, data: {
device_id: number;
node_id: number;
}): Promise<ConnectionTokenResponse> {
const db = await getDb();
const device = queryOne(db, 'SELECT * FROM devices WHERE id = ? AND user_id = ?', [data.device_id, userId]);
if (!device) throw new Error('DEVICE_NOT_FOUND');
const node = queryOne(db, "SELECT * FROM relay_nodes WHERE id = ? AND status = 'online'", [data.node_id]);
if (!node) throw new Error('NODE_NOT_AVAILABLE');
execute(db, "UPDATE devices SET status = 'online', last_seen_at = datetime('now') WHERE id = ?", [data.device_id]);
const nonce = generateNonce();
const token = generateConnectionToken({
user_id: userId,
device_id: data.device_id,
node_id: data.node_id,
nonce,
}, JWT_SECRET, '5m');
const tokenHash = sha256(token);
const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString();
execute(db,
'INSERT INTO connection_tokens (user_id, device_id, node_id, token_hash, expires_at) VALUES (?, ?, ?, ?, ?)',
[userId, data.device_id, data.node_id, tokenHash, expiresAt]
);
this._log(db, userId, data.device_id, data.node_id, 'token_issued', 'success', 'Token issued');
saveDb();
return {
token,
node: node as any,
expires_at: expiresAt,
};
}
async verifyToken(data: { token: string; device_id: number }): Promise<TokenVerifyResponse> {
const db = await getDb();
const tokenHash = sha256(data.token);
const record = queryOne<any>(db,
`SELECT * FROM connection_tokens
WHERE token_hash = ? AND device_id = ? AND used_at IS NULL
AND expires_at > datetime('now')`,
[tokenHash, data.device_id]
);
if (!record) {
this._log(db, null, data.device_id, null, 'verify_token', 'failed', 'Invalid or expired token');
return { valid: false, user_id: 0, device_id: 0, message: 'Invalid or expired token' };
}
execute(db, "UPDATE connection_tokens SET used_at = datetime('now') WHERE id = ?", [record.id]);
execute(db, "UPDATE devices SET status = 'online', last_seen_at = datetime('now') WHERE id = ?", [data.device_id]);
this._log(db, record.user_id, record.device_id, record.node_id, 'verify_token', 'success', 'Token verified');
saveDb();
return { valid: true, user_id: record.user_id, device_id: record.device_id, message: 'Token verified' };
}
async disconnect(userId: number, deviceId: number): Promise<void> {
const db = await getDb();
execute(db, "UPDATE devices SET status = 'offline', last_seen_at = datetime('now') WHERE id = ? AND user_id = ?",
[deviceId, userId]);
this._log(db, userId, deviceId, null, 'disconnect', 'success', 'Disconnected');
saveDb();
}
async getLogs(limit = 100): Promise<ConnectionLog[]> {
const db = await getDb();
return queryAll<ConnectionLog>(db, 'SELECT * FROM connection_logs ORDER BY created_at DESC LIMIT ?', [limit]);
}
async getUserLogs(userId: number, limit = 50): Promise<ConnectionLog[]> {
const db = await getDb();
return queryAll<ConnectionLog>(db,
'SELECT * FROM connection_logs WHERE user_id = ? ORDER BY created_at DESC LIMIT ?',
[userId, limit]
);
}
async cleanupExpiredTokens(): Promise<void> {
const db = await getDb();
execute(db, "DELETE FROM connection_tokens WHERE expires_at < datetime('now') AND used_at IS NULL");
saveDb();
}
private _log(db: any, userId: number | null, deviceId: number | null, nodeId: number | null,
action: string, status: 'success' | 'failed', message: string): void {
execute(db,
'INSERT INTO connection_logs (user_id, device_id, node_id, action, status, message) VALUES (?, ?, ?, ?, ?, ?)',
[userId, deviceId, nodeId, action, status, message]
);
}
}
@@ -0,0 +1,50 @@
import { getDb, queryOne, queryAll, execute } from '../db/database';
import type { Device } from '@network-tool/shared-types';
export class DeviceService {
async registerDevice(userId: number, data: { device_name: string; device_fingerprint: string; public_key: string }): Promise<Device> {
const db = await getDb();
if (queryOne(db, 'SELECT id FROM devices WHERE device_fingerprint = ?', [data.device_fingerprint])) {
throw new Error('DEVICE_ALREADY_EXISTS');
}
const result = execute(db,
'INSERT INTO devices (user_id, device_name, device_fingerprint, public_key, status) VALUES (?, ?, ?, ?, ?)',
[userId, data.device_name, data.device_fingerprint, data.public_key, 'offline']
);
return queryOne<Device>(db, 'SELECT * FROM devices WHERE id = ?', [result.lastInsertRowid])!;
}
async getUserDevices(userId: number): Promise<Device[]> {
const db = await getDb();
return queryAll<Device>(db, 'SELECT * FROM devices WHERE user_id = ? ORDER BY last_seen_at DESC', [userId]);
}
async getAllDevices(): Promise<Device[]> {
const db = await getDb();
return queryAll<Device>(db, 'SELECT * FROM devices ORDER BY last_seen_at DESC');
}
async updateDeviceStatus(deviceId: number, status: 'online' | 'offline'): Promise<Device | null> {
const db = await getDb();
const result = execute(db, "UPDATE devices SET status = ?, last_seen_at = datetime('now') WHERE id = ?", [status, deviceId]);
if (result.changes === 0) return null;
return queryOne<Device>(db, 'SELECT * FROM devices WHERE id = ?', [deviceId]);
}
async deleteDevice(deviceId: number, userId: number): Promise<boolean> {
const db = await getDb();
const result = execute(db, 'DELETE FROM devices WHERE id = ? AND user_id = ?', [deviceId, userId]);
return result.changes > 0;
}
async adminDeleteDevice(deviceId: number): Promise<boolean> {
const db = await getDb();
const result = execute(db, 'DELETE FROM devices WHERE id = ?', [deviceId]);
return result.changes > 0;
}
async getDeviceById(deviceId: number): Promise<Device | null> {
const db = await getDb();
return queryOne<Device>(db, 'SELECT * FROM devices WHERE id = ?', [deviceId]);
}
}
@@ -0,0 +1,54 @@
import { getDb, queryOne, queryAll, execute } from '../db/database';
import type { RelayNode } from '@network-tool/shared-types';
export class NodeService {
async registerNode(data: { name: string; host: string; port: number }): Promise<RelayNode> {
const db = await getDb();
const result = execute(db,
"INSERT INTO relay_nodes (name, host, port, status, load) VALUES (?, ?, ?, 'offline', 0)",
[data.name, data.host, data.port]
);
return queryOne<RelayNode>(db, 'SELECT * FROM relay_nodes WHERE id = ?', [result.lastInsertRowid])!;
}
async heartbeat(nodeId: number, data: { load: number; status: 'online' | 'offline' | 'busy' }): Promise<RelayNode | null> {
const db = await getDb();
const result = execute(db,
"UPDATE relay_nodes SET status = ?, load = ?, last_heartbeat_at = datetime('now') WHERE id = ?",
[data.status, data.load, nodeId]
);
if (result.changes === 0) return null;
return queryOne<RelayNode>(db, 'SELECT * FROM relay_nodes WHERE id = ?', [nodeId]);
}
async getAllNodes(): Promise<RelayNode[]> {
const db = await getDb();
return queryAll<RelayNode>(db, 'SELECT * FROM relay_nodes ORDER BY last_heartbeat_at DESC');
}
async getAvailableNodes(): Promise<RelayNode[]> {
const db = await getDb();
return queryAll<RelayNode>(db,
"SELECT * FROM relay_nodes WHERE status = 'online' AND load < 100 ORDER BY load ASC, last_heartbeat_at DESC"
);
}
async getNodeById(nodeId: number): Promise<RelayNode | null> {
const db = await getDb();
return queryOne<RelayNode>(db, 'SELECT * FROM relay_nodes WHERE id = ?', [nodeId]);
}
async deleteNode(nodeId: number): Promise<boolean> {
const db = await getDb();
const result = execute(db, 'DELETE FROM relay_nodes WHERE id = ?', [nodeId]);
return result.changes > 0;
}
async cleanupStaleNodes(timeoutMinutes = 5): Promise<void> {
const db = await getDb();
execute(db,
"UPDATE relay_nodes SET status = 'offline' WHERE status != 'offline' AND last_heartbeat_at < datetime('now', '-' || ? || ' minutes')",
[timeoutMinutes]
);
}
}
+33
View File
@@ -0,0 +1,33 @@
declare module 'sql.js' {
interface SqlValue {
[columnName: string]: number | string | Uint8Array | null;
}
interface QueryExecResult {
columns: string[];
values: any[][];
}
interface Statement {
bind(params?: any[]): boolean;
step(): boolean;
getAsObject(params?: any): SqlValue;
free(): boolean;
}
interface Database {
run(sql: string, params?: any[]): Database;
exec(sql: string): QueryExecResult[];
prepare(sql: string): Statement;
export(): Uint8Array;
close(): void;
getRowsModified(): number;
}
interface SqlJsStatic {
Database: new (data?: ArrayLike<number> | Buffer | null) => Database;
}
export default function initSqlJs(config?: any): Promise<SqlJsStatic>;
export { Database, Statement, SqlJsStatic };
}
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src"]
}