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