mirror of
https://github.com/d0zingcat/alert-message-center.git
synced 2026-05-13 23:16:48 +00:00
335 lines
9.3 KiB
TypeScript
335 lines
9.3 KiB
TypeScript
import { Hono } from 'hono';
|
|
import { eq, and, desc, sql, gt, sum, count } from 'drizzle-orm';
|
|
import { z } from 'zod';
|
|
import { zValidator } from '@hono/zod-validator';
|
|
import { db } from './db';
|
|
import { topics, users, subscriptions, alertTasks, topicGroupChats, knownGroupChats } from './db/schema';
|
|
import { requireAuth, requireAdmin, AuthSession } from './middleware';
|
|
|
|
const api = new Hono<{ Variables: { session: AuthSession } }>();
|
|
|
|
const topicSchema = z.object({
|
|
name: z.string().min(1),
|
|
slug: z.string().min(1),
|
|
description: z.string().optional(),
|
|
});
|
|
|
|
const groupBindingSchema = z.object({
|
|
chatId: z.string().min(1),
|
|
name: z.string().min(1),
|
|
});
|
|
|
|
const userSchema = z.object({
|
|
name: z.string().min(1),
|
|
feishuUserId: z.string().min(1),
|
|
email: z.string().email().optional().or(z.literal('')),
|
|
});
|
|
|
|
// --- Topics ---
|
|
|
|
// --- Topics ---
|
|
|
|
api.get('/topics', requireAuth, async (c) => {
|
|
const session = c.get('session');
|
|
const isAdmin = session.isAdmin;
|
|
const currentUserId = session.id;
|
|
|
|
const allTopics = await db.query.topics.findMany({
|
|
where: eq(topics.status, 'approved'),
|
|
with: {
|
|
creator: true,
|
|
approver: true,
|
|
subscriptions: {
|
|
where: (subscriptions, { eq }) =>
|
|
isAdmin ? undefined : (currentUserId ? eq(subscriptions.userId, currentUserId) : undefined),
|
|
with: {
|
|
user: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return c.json(allTopics);
|
|
});
|
|
|
|
api.get('/topics/requests', requireAdmin, async (c) => {
|
|
const requests = await db.query.topics.findMany({
|
|
where: eq(topics.status, 'pending'),
|
|
with: {
|
|
creator: true
|
|
}
|
|
});
|
|
return c.json(requests);
|
|
});
|
|
|
|
api.get('/topics/all', requireAdmin, async (c) => {
|
|
const allTopics = await db.query.topics.findMany({
|
|
with: {
|
|
creator: true,
|
|
approver: true,
|
|
subscriptions: true
|
|
},
|
|
orderBy: [desc(topics.createdAt)]
|
|
});
|
|
return c.json(allTopics);
|
|
});
|
|
|
|
api.get('/topics/my-requests', requireAuth, async (c) => {
|
|
const session = c.get('session');
|
|
const requests = await db.query.topics.findMany({
|
|
where: eq(topics.createdBy, session.id),
|
|
orderBy: [desc(topics.createdAt)],
|
|
with: {
|
|
approver: true,
|
|
}
|
|
});
|
|
return c.json(requests);
|
|
});
|
|
|
|
api.post('/topics/:id/approve', requireAdmin, async (c) => {
|
|
const id = c.req.param('id');
|
|
const session = c.get('session');
|
|
const result = await db.update(topics)
|
|
.set({ status: 'approved', approvedBy: session.id })
|
|
.where(eq(topics.id, id))
|
|
.returning();
|
|
return c.json(result[0]);
|
|
});
|
|
|
|
api.post('/topics/:id/reject', requireAdmin, async (c) => {
|
|
const id = c.req.param('id');
|
|
const result = await db.update(topics)
|
|
.set({ status: 'rejected' })
|
|
.where(eq(topics.id, id))
|
|
.returning();
|
|
return c.json(result[0]);
|
|
});
|
|
|
|
// Only admins can create topics
|
|
// Authenticated users can create topics (requests)
|
|
api.post('/topics', requireAuth, zValidator('json', topicSchema), async (c) => {
|
|
const body = c.req.valid('json');
|
|
const session = c.get('session');
|
|
|
|
const status = session.isAdmin ? 'approved' : 'pending';
|
|
|
|
const result = await db.insert(topics).values({
|
|
...body,
|
|
status,
|
|
createdBy: session.id,
|
|
approvedBy: session.isAdmin ? session.id : null,
|
|
}).returning();
|
|
return c.json(result[0]);
|
|
});
|
|
|
|
// Only admins can update topics
|
|
api.put('/topics/:id', requireAdmin, zValidator('json', topicSchema.partial()), async (c) => {
|
|
const id = c.req.param('id');
|
|
const body = c.req.valid('json');
|
|
const result = await db.update(topics).set(body).where(eq(topics.id, id)).returning();
|
|
return c.json(result[0]);
|
|
});
|
|
|
|
// Only admins can delete topics
|
|
api.delete('/topics/:id', requireAdmin, async (c) => {
|
|
const id = c.req.param('id');
|
|
await db.delete(topics).where(eq(topics.id, id));
|
|
return c.json({ success: true });
|
|
});
|
|
|
|
// --- Users ---
|
|
|
|
api.get('/users', requireAdmin, async (c) => {
|
|
const allUsers = await db.query.users.findMany({
|
|
with: {
|
|
subscriptions: {
|
|
with: {
|
|
topic: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return c.json(allUsers);
|
|
});
|
|
|
|
api.post('/users', requireAdmin, zValidator('json', userSchema), async (c) => {
|
|
const body = c.req.valid('json');
|
|
const result = await db.insert(users).values(body).returning();
|
|
return c.json(result[0]);
|
|
});
|
|
|
|
api.put('/users/:id', requireAdmin, zValidator('json', userSchema.partial()), async (c) => {
|
|
const id = c.req.param('id');
|
|
const body = c.req.valid('json');
|
|
const result = await db.update(users).set(body).where(eq(users.id, id)).returning();
|
|
return c.json(result[0]);
|
|
});
|
|
|
|
api.delete('/users/:id', requireAdmin, async (c) => {
|
|
const id = c.req.param('id');
|
|
await db.delete(users).where(eq(users.id, id));
|
|
return c.json({ success: true });
|
|
});
|
|
|
|
// --- Subscriptions ---
|
|
|
|
// --- Subscriptions ---
|
|
|
|
// Users can subscribe themselves or admins can subscribe anyone
|
|
api.post('/topics/:topicId/subscribe/:userId', requireAuth, async (c) => {
|
|
const { topicId, userId } = c.req.param();
|
|
const session = c.get('session');
|
|
|
|
// Check if user is subscribing themselves or is an admin
|
|
if (session.id !== userId && !session.isAdmin) {
|
|
return c.json({ error: 'You can only subscribe yourself' }, 403);
|
|
}
|
|
|
|
const result = await db.insert(subscriptions).values({ topicId, userId }).returning();
|
|
return c.json(result[0]);
|
|
});
|
|
|
|
// Users can unsubscribe themselves or admins can unsubscribe anyone
|
|
api.delete('/topics/:topicId/subscribe/:userId', requireAuth, async (c) => {
|
|
const { topicId, userId } = c.req.param();
|
|
const session = c.get('session');
|
|
|
|
// Check if user is unsubscribing themselves or is an admin
|
|
if (session.id !== userId && !session.isAdmin) {
|
|
return c.json({ error: 'You can only unsubscribe yourself' }, 403);
|
|
}
|
|
|
|
await db.delete(subscriptions)
|
|
.where(and(
|
|
eq(subscriptions.topicId, topicId),
|
|
eq(subscriptions.userId, userId)
|
|
));
|
|
return c.json({ success: true });
|
|
});
|
|
|
|
// --- Group Bindings (App Bot) ---
|
|
|
|
// Get list of known groups (for selection)
|
|
api.get('/groups', requireAuth, async (c) => {
|
|
// Return recent active groups
|
|
const groups = await db.select().from(knownGroupChats)
|
|
.orderBy(desc(knownGroupChats.lastActiveAt))
|
|
.limit(50);
|
|
return c.json(groups);
|
|
});
|
|
|
|
// Get bindings for a topic
|
|
api.get('/topics/:id/groups', requireAuth, async (c) => {
|
|
const topicId = c.req.param('id');
|
|
const groups = await db.select().from(topicGroupChats)
|
|
.where(eq(topicGroupChats.topicId, topicId))
|
|
.orderBy(desc(topicGroupChats.createdAt));
|
|
return c.json(groups);
|
|
});
|
|
|
|
// Bind a group to a topic
|
|
api.post('/topics/:id/groups', requireAuth, zValidator('json', groupBindingSchema), async (c) => {
|
|
const topicId = c.req.param('id');
|
|
const body = c.req.valid('json');
|
|
const session = c.get('session');
|
|
|
|
const result = await db.insert(topicGroupChats).values({
|
|
topicId,
|
|
chatId: body.chatId,
|
|
name: body.name,
|
|
createdBy: session.id,
|
|
}).returning();
|
|
|
|
return c.json(result[0]);
|
|
});
|
|
|
|
// Unbind a group
|
|
api.delete('/topics/:id/groups/:bindingId', requireAuth, async (c) => {
|
|
const { id: topicId, bindingId } = c.req.param();
|
|
|
|
await db.delete(topicGroupChats)
|
|
.where(and(
|
|
eq(topicGroupChats.id, bindingId),
|
|
eq(topicGroupChats.topicId, topicId)
|
|
));
|
|
|
|
return c.json({ success: true });
|
|
});
|
|
|
|
// --- Alert Tasks ---
|
|
|
|
api.get('/alerts/tasks', requireAdmin, async (c) => {
|
|
const limit = Math.min(Number(c.req.query('limit') || 50), 100);
|
|
const tasks = await db.query.alertTasks.findMany({
|
|
orderBy: [desc(alertTasks.createdAt)],
|
|
limit,
|
|
with: {
|
|
sender: true,
|
|
logs: {
|
|
limit: 10, // Only show first 10 logs inline
|
|
}
|
|
}
|
|
});
|
|
return c.json(tasks);
|
|
});
|
|
|
|
api.get('/alerts/tasks/:id', requireAuth, async (c) => {
|
|
const id = c.req.param('id');
|
|
const task = await db.query.alertTasks.findFirst({
|
|
where: eq(alertTasks.id, id),
|
|
with: {
|
|
sender: true,
|
|
logs: true // Show all logs for detail view
|
|
}
|
|
});
|
|
|
|
if (!task) {
|
|
return c.json({ error: 'Task not found' }, 404);
|
|
}
|
|
|
|
return c.json(task);
|
|
});
|
|
|
|
// --- Stats ---
|
|
|
|
api.get('/stats', requireAdmin, async (c) => {
|
|
// 1. Message count per topic
|
|
const topicStats = await db.select({
|
|
topicSlug: alertTasks.topicSlug,
|
|
totalTasks: count(),
|
|
totalRecipients: sql<number>`cast(${sum(alertTasks.recipientCount)} as int)`,
|
|
totalSuccess: sql<number>`cast(${sum(alertTasks.successCount)} as int)`,
|
|
})
|
|
.from(alertTasks)
|
|
.groupBy(alertTasks.topicSlug);
|
|
|
|
// 2. Recent metrics (last 24h)
|
|
const last24h = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
const recentStats = await db.select({
|
|
totalRecipients: sql<number>`cast(${sum(alertTasks.recipientCount)} as int)`,
|
|
totalSuccess: sql<number>`cast(${sum(alertTasks.successCount)} as int)`,
|
|
taskCount: count(),
|
|
})
|
|
.from(alertTasks)
|
|
.where(gt(alertTasks.createdAt, last24h));
|
|
|
|
const recent = recentStats[0] || { totalRecipients: 0, totalSuccess: 0, taskCount: 0 };
|
|
const totalRecipients = Number(recent.totalRecipients || 0);
|
|
const totalSuccess = Number(recent.totalSuccess || 0);
|
|
const failedCount = totalRecipients - totalSuccess;
|
|
const successRate = totalRecipients > 0 ? (totalSuccess / totalRecipients) * 100 : 100;
|
|
|
|
return c.json({
|
|
topics: topicStats,
|
|
recent: {
|
|
alertsReceived: Number(recent.taskCount || 0),
|
|
plannedMessages: totalRecipients,
|
|
successCount: totalSuccess,
|
|
failedCount: failedCount,
|
|
successRate: successRate,
|
|
}
|
|
});
|
|
});
|
|
|
|
export default api;
|