feat: add dashboard API endpoints
- POST /api/auth/telegram - Telegram Login Widget verification - GET /api/dashboard - Dashboard stats (servers, domains, deposit) - GET /api/servers - User's server list - GET /api/domains - User's domain list - GET /api/deposit/transactions - Transaction history - POST /api/server/action - Server start/stop/reboot CORS enabled for my.anvil.it.com Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { depositRouter } from './api/deposit';
|
|||||||
import { chatRouter } from './api/chat';
|
import { chatRouter } from './api/chat';
|
||||||
import { contactRouter } from './api/contact';
|
import { contactRouter } from './api/contact';
|
||||||
import { metricsRouter } from './api/metrics';
|
import { metricsRouter } from './api/metrics';
|
||||||
|
import { dashboardRouter } from './api/dashboard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API Router (Hono)
|
* API Router (Hono)
|
||||||
@@ -49,5 +50,6 @@ api.route('/deposit', depositRouter);
|
|||||||
api.route('/', chatRouter); // /test, /chat
|
api.route('/', chatRouter); // /test, /chat
|
||||||
api.route('/contact', contactRouter);
|
api.route('/contact', contactRouter);
|
||||||
api.route('/metrics', metricsRouter);
|
api.route('/metrics', metricsRouter);
|
||||||
|
api.route('/', dashboardRouter); // /auth/telegram, /dashboard, /servers, /domains, /server/action
|
||||||
|
|
||||||
export { api as apiRouter };
|
export { api as apiRouter };
|
||||||
|
|||||||
296
src/routes/api/dashboard.ts
Normal file
296
src/routes/api/dashboard.ts
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import { Hono } from 'hono';
|
||||||
|
import { cors } from 'hono/cors';
|
||||||
|
import { Env } from '../../types';
|
||||||
|
import { createLogger } from '../../utils/logger';
|
||||||
|
|
||||||
|
const logger = createLogger('dashboard-api');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard API Router
|
||||||
|
*
|
||||||
|
* Endpoints:
|
||||||
|
* - POST /auth/telegram - Verify Telegram login
|
||||||
|
* - GET /dashboard - Get dashboard stats
|
||||||
|
* - GET /servers - Get user's servers
|
||||||
|
* - GET /domains - Get user's domains
|
||||||
|
* - GET /deposit/transactions - Get transaction history
|
||||||
|
*/
|
||||||
|
const dashboardRouter = new Hono<{ Bindings: Env }>();
|
||||||
|
|
||||||
|
// CORS for my.anvil.it.com
|
||||||
|
dashboardRouter.use('*', cors({
|
||||||
|
origin: ['https://my.anvil.it.com', 'http://localhost:8788'],
|
||||||
|
allowMethods: ['GET', 'POST', 'OPTIONS'],
|
||||||
|
allowHeaders: ['Content-Type', 'Authorization'],
|
||||||
|
credentials: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify Telegram Login Widget data
|
||||||
|
* https://core.telegram.org/widgets/login#checking-authorization
|
||||||
|
*/
|
||||||
|
dashboardRouter.post('/auth/telegram', async (c) => {
|
||||||
|
try {
|
||||||
|
const body = await c.req.json();
|
||||||
|
const { id, first_name, last_name, username, photo_url, auth_date, hash } = body;
|
||||||
|
|
||||||
|
if (!id || !auth_date || !hash) {
|
||||||
|
return c.json({ error: 'Missing required fields' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify hash using BOT_TOKEN
|
||||||
|
const botToken = c.env.BOT_TOKEN;
|
||||||
|
if (!botToken) {
|
||||||
|
logger.error('BOT_TOKEN not configured');
|
||||||
|
return c.json({ error: 'Server configuration error' }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create data-check-string
|
||||||
|
const dataCheckArr = [];
|
||||||
|
if (auth_date) dataCheckArr.push(`auth_date=${auth_date}`);
|
||||||
|
if (first_name) dataCheckArr.push(`first_name=${first_name}`);
|
||||||
|
if (id) dataCheckArr.push(`id=${id}`);
|
||||||
|
if (last_name) dataCheckArr.push(`last_name=${last_name}`);
|
||||||
|
if (photo_url) dataCheckArr.push(`photo_url=${photo_url}`);
|
||||||
|
if (username) dataCheckArr.push(`username=${username}`);
|
||||||
|
dataCheckArr.sort();
|
||||||
|
const dataCheckString = dataCheckArr.join('\n');
|
||||||
|
|
||||||
|
// Calculate secret key = SHA256(bot_token)
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const secretKey = await crypto.subtle.digest('SHA-256', encoder.encode(botToken));
|
||||||
|
|
||||||
|
// Calculate hash = HMAC-SHA256(data_check_string, secret_key)
|
||||||
|
const key = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
secretKey,
|
||||||
|
{ name: 'HMAC', hash: 'SHA-256' },
|
||||||
|
false,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(dataCheckString));
|
||||||
|
const calculatedHash = Array.from(new Uint8Array(signature))
|
||||||
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
if (calculatedHash !== hash) {
|
||||||
|
logger.warn('Invalid Telegram auth hash', { userId: id });
|
||||||
|
return c.json({ error: 'Invalid authentication' }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check auth_date is not too old (1 hour)
|
||||||
|
const authTime = parseInt(auth_date, 10) * 1000;
|
||||||
|
if (Date.now() - authTime > 60 * 60 * 1000) {
|
||||||
|
return c.json({ error: 'Authentication expired' }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure user exists in DB
|
||||||
|
await c.env.DB.prepare(`
|
||||||
|
INSERT INTO users (telegram_id, username, first_name, last_name)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON CONFLICT(telegram_id) DO UPDATE SET
|
||||||
|
username = excluded.username,
|
||||||
|
first_name = excluded.first_name,
|
||||||
|
last_name = excluded.last_name
|
||||||
|
`).bind(id, username || null, first_name || null, last_name || null).run();
|
||||||
|
|
||||||
|
// Generate simple token (in production, use JWT)
|
||||||
|
const token = btoa(JSON.stringify({ id, ts: Date.now() }));
|
||||||
|
|
||||||
|
logger.info('Telegram auth success', { userId: id, username });
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
token: token,
|
||||||
|
user: { id, first_name, last_name, username, photo_url },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Telegram auth error', error as Error);
|
||||||
|
return c.json({ error: 'Authentication failed' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify Authorization header and extract user_id
|
||||||
|
*/
|
||||||
|
async function verifyAuth(c: any): Promise<number | null> {
|
||||||
|
const authHeader = c.req.header('Authorization');
|
||||||
|
if (!authHeader?.startsWith('Bearer ')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = authHeader.slice(7);
|
||||||
|
const decoded = JSON.parse(atob(token));
|
||||||
|
|
||||||
|
// Check token age (24 hours)
|
||||||
|
if (Date.now() - decoded.ts > 24 * 60 * 60 * 1000) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded.id;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dashboard stats
|
||||||
|
*/
|
||||||
|
dashboardRouter.get('/dashboard', async (c) => {
|
||||||
|
const userId = await verifyAuth(c);
|
||||||
|
if (!userId) {
|
||||||
|
return c.json({ error: 'Unauthorized' }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get server count
|
||||||
|
const servers = await c.env.DB.prepare(`
|
||||||
|
SELECT COUNT(*) as count FROM server_orders
|
||||||
|
WHERE telegram_user_id = ? AND status IN ('active', 'stopped', 'provisioning')
|
||||||
|
`).bind(userId).first<{ count: number }>();
|
||||||
|
|
||||||
|
// Get domain count
|
||||||
|
const domains = await c.env.DB.prepare(`
|
||||||
|
SELECT COUNT(*) as count FROM user_domains
|
||||||
|
WHERE user_id = ? AND verified = 1
|
||||||
|
`).bind(userId).first<{ count: number }>();
|
||||||
|
|
||||||
|
// Get deposit balance
|
||||||
|
const deposit = await c.env.DB.prepare(`
|
||||||
|
SELECT balance FROM user_deposits WHERE user_id = ?
|
||||||
|
`).bind(userId).first<{ balance: number }>();
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
servers: servers?.count || 0,
|
||||||
|
domains: domains?.count || 0,
|
||||||
|
ddos: '미사용',
|
||||||
|
deposit: deposit?.balance || 0,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Dashboard stats error', error as Error);
|
||||||
|
return c.json({ error: 'Failed to load stats' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user's servers
|
||||||
|
*/
|
||||||
|
dashboardRouter.get('/servers', async (c) => {
|
||||||
|
const userId = await verifyAuth(c);
|
||||||
|
if (!userId) {
|
||||||
|
return c.json({ error: 'Unauthorized' }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const servers = await c.env.DB.prepare(`
|
||||||
|
SELECT id, label, status, region, created_at
|
||||||
|
FROM server_orders
|
||||||
|
WHERE telegram_user_id = ? AND status IN ('active', 'stopped', 'provisioning')
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`).bind(userId).all();
|
||||||
|
|
||||||
|
return c.json({ servers: servers.results || [] });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Servers list error', error as Error);
|
||||||
|
return c.json({ error: 'Failed to load servers' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user's domains
|
||||||
|
*/
|
||||||
|
dashboardRouter.get('/domains', async (c) => {
|
||||||
|
const userId = await verifyAuth(c);
|
||||||
|
if (!userId) {
|
||||||
|
return c.json({ error: 'Unauthorized' }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const domains = await c.env.DB.prepare(`
|
||||||
|
SELECT domain, created_at
|
||||||
|
FROM user_domains
|
||||||
|
WHERE user_id = ? AND verified = 1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`).bind(userId).all();
|
||||||
|
|
||||||
|
return c.json({ domains: domains.results || [] });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Domains list error', error as Error);
|
||||||
|
return c.json({ error: 'Failed to load domains' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get transaction history
|
||||||
|
*/
|
||||||
|
dashboardRouter.get('/deposit/transactions', async (c) => {
|
||||||
|
const userId = await verifyAuth(c);
|
||||||
|
if (!userId) {
|
||||||
|
return c.json({ error: 'Unauthorized' }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transactions = await c.env.DB.prepare(`
|
||||||
|
SELECT id, amount, type, status, description, created_at
|
||||||
|
FROM deposit_transactions
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 50
|
||||||
|
`).bind(userId).all();
|
||||||
|
|
||||||
|
return c.json({ transactions: transactions.results || [] });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Transactions list error', error as Error);
|
||||||
|
return c.json({ error: 'Failed to load transactions' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server action (start/stop/reboot)
|
||||||
|
*/
|
||||||
|
dashboardRouter.post('/server/action', async (c) => {
|
||||||
|
const userId = await verifyAuth(c);
|
||||||
|
if (!userId) {
|
||||||
|
return c.json({ error: 'Unauthorized' }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { order_id, action } = await c.req.json();
|
||||||
|
|
||||||
|
if (!order_id || !['start', 'stop', 'reboot'].includes(action)) {
|
||||||
|
return c.json({ error: 'Invalid request' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify ownership
|
||||||
|
const server = await c.env.DB.prepare(`
|
||||||
|
SELECT id FROM server_orders
|
||||||
|
WHERE id = ? AND telegram_user_id = ?
|
||||||
|
`).bind(order_id, userId).first();
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
return c.json({ error: 'Server not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Cloud Orchestrator
|
||||||
|
if (c.env.CLOUD_ORCHESTRATOR) {
|
||||||
|
const response = await c.env.CLOUD_ORCHESTRATOR.fetch(
|
||||||
|
`https://internal/api/provision/orders/${order_id}/${action}`,
|
||||||
|
{ method: 'POST' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Orchestrator error: ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Server action', { userId, orderId: order_id, action });
|
||||||
|
|
||||||
|
return c.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Server action error', error as Error);
|
||||||
|
return c.json({ error: 'Action failed' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export { dashboardRouter };
|
||||||
Reference in New Issue
Block a user