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:
kappa
2026-02-05 19:08:54 +09:00
parent ec2d4da517
commit 0b24cbdad0
2 changed files with 298 additions and 0 deletions

View File

@@ -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
View 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 };