From 62b82da64fd5ba6c4886ababc8a057b9b57f0fad Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:12:31 +0330 Subject: [PATCH] Import source: backend/src/routes/admin Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- backend/src/routes/admin.ts | 162 ++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 backend/src/routes/admin.ts diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts new file mode 100644 index 0000000..da362b4 --- /dev/null +++ b/backend/src/routes/admin.ts @@ -0,0 +1,162 @@ +import express from 'express'; +import { z } from 'zod'; +import { requireAdmin } from '../middleware/adminAuth.js'; +import { HttpError } from '../middleware/errorHandler.js'; +import { store } from '../services/store.js'; +import type { FileStatus, Plan } from '../types.js'; +import { fileToApi, sanitizeFilename } from '../utils/files.js'; +import * as gdrive from '../utils/gdrive.js'; + +const router = express.Router(); + +router.use(requireAdmin); + +const filePatchSchema = z.object({ + originalName: z.string().trim().min(1).max(255).optional(), + uploader: z.string().trim().min(1).max(80).optional(), + expiresAt: z.string().datetime().optional(), + status: z.enum(['uploaded', 'deleted', 'expired']).optional(), +}); + +const userPatchSchema = z.object({ + plan: z.enum(['free', 'pro', 'team']).optional(), + verified: z.boolean().optional(), +}); + +function publicUser(user: { + id: string; + email: string; + plan: Plan; + verified: boolean; + createdAt: string; + updatedAt: string; +}) { + return { + id: user.id, + email: user.email, + plan: user.plan, + verified: user.verified, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }; +} + +async function unlinkStoredFile(fileId: string) { + await gdrive.deleteFile(fileId).catch(() => {}); +} + +router.get('/overview', async (_req, res) => { + const db = await store.read(); + const filesByStatus = db.files.reduce>((acc, file) => { + acc[file.status] += 1; + return acc; + }, { uploaded: 0, deleted: 0, expired: 0 }); + + res.json({ + data: { + users: db.users.length, + files: db.files.length, + filesByStatus, + events: db.events.length, + totalBytes: db.files.reduce((sum, file) => sum + file.sizeBytes, 0), + }, + }); +}); + +router.get('/files', async (req, res) => { + const status = String(req.query.status ?? 'all'); + const db = await store.read(); + const files = db.files + .filter((file) => status === 'all' || file.status === status) + .map(fileToApi); + res.json({ data: files, meta: { total: files.length } }); +}); + +router.get('/files/:id', async (req, res) => { + const file = (await store.read()).files.find((row) => row.id === req.params.id); + if (!file) throw new HttpError(404, 'FILE_NOT_FOUND', 'File not found'); + res.json({ data: fileToApi(file) }); +}); + +router.patch('/files/:id', async (req, res) => { + const body = filePatchSchema.parse(req.body); + const file = await store.update((db) => { + const row = db.files.find((item) => item.id === req.params.id); + if (!row) return null; + if (body.originalName) row.originalName = sanitizeFilename(body.originalName); + if (body.uploader) row.uploader = body.uploader; + if (body.expiresAt) row.expiresAt = body.expiresAt; + if (body.status) { + row.status = body.status; + row.deletedAt = body.status === 'uploaded' ? undefined : new Date().toISOString(); + } + return row; + }); + + if (!file) throw new HttpError(404, 'FILE_NOT_FOUND', 'File not found'); + res.json({ data: fileToApi(file) }); +}); + +router.delete('/files/:id', async (req, res) => { + const purge = req.query.purge !== 'false'; + const deleted = await store.update(async (db) => { + const row = db.files.find((item) => item.id === req.params.id); + if (!row) return null; + row.status = 'deleted'; + row.deletedAt = new Date().toISOString(); + if (purge) await unlinkStoredFile(row.gdriveId); + return row; + }); + + if (!deleted) throw new HttpError(404, 'FILE_NOT_FOUND', 'File not found'); + res.status(204).send(); +}); + +router.get('/users', async (_req, res) => { + const users = (await store.read()).users.map(publicUser); + res.json({ data: users, meta: { total: users.length } }); +}); + +router.patch('/users/:id', async (req, res) => { + const body = userPatchSchema.parse(req.body); + const user = await store.update((db) => { + const row = db.users.find((item) => item.id === req.params.id); + if (!row) return null; + if (body.plan) row.plan = body.plan; + if (typeof body.verified === 'boolean') row.verified = body.verified; + row.updatedAt = new Date().toISOString(); + return row; + }); + + if (!user) throw new HttpError(404, 'USER_NOT_FOUND', 'User not found'); + res.json({ data: publicUser(user) }); +}); + +router.delete('/users/:id', async (req, res) => { + const removeFiles = req.query.files === 'delete'; + const deleted = await store.update(async (db) => { + const index = db.users.findIndex((item) => item.id === req.params.id); + if (index === -1) return null; + const [user] = db.users.splice(index, 1); + db.refreshTokens = db.refreshTokens.filter((token) => token.userId !== user.id); + if (removeFiles) { + for (const file of db.files.filter((item) => item.uploader === user.email)) { + file.status = 'deleted'; + file.deletedAt = new Date().toISOString(); + await unlinkStoredFile(file.gdriveId); + } + } + return user; + }); + + if (!deleted) throw new HttpError(404, 'USER_NOT_FOUND', 'User not found'); + res.status(204).send(); +}); + +router.get('/events', async (_req, res) => { + const events = (await store.read()).events.slice().reverse(); + res.json({ data: events, meta: { total: events.length } }); +}); + +export { router as adminRouter }; +