Import source: backend/src/routes/admin
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -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<Record<FileStatus, number>>((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 };
|
||||||
|
|
||||||
Reference in New Issue
Block a user