first commit

This commit is contained in:
MOHAN 2026-06-30 22:53:29 +05:30
parent 503966e881
commit d532c2e084
12 changed files with 202 additions and 5 deletions

View File

@ -7,4 +7,7 @@ CORS_ORIGINS=*
MAX_UPLOAD_MB=250 MAX_UPLOAD_MB=250
GOOGLE_CREDENTIALS=./credentials.json GOOGLE_CREDENTIALS=./credentials.json
GOOGLE_TOKEN=./token.json GOOGLE_TOKEN=./token.json
GOOGLE_DRIVE_ROOT_FOLDER=Whats-Drive-SaaS GOOGLE_DRIVE_ROOT_FOLDER=Metatron-Drive
ADMIN_USERNAME=admin
ADMIN_PASSWORD=replace-with-a-strong-admin-password
ADMIN_JWT_EXPIRES_IN=8h

1
.gitignore vendored
View File

@ -5,5 +5,6 @@ uploads/
generated/ generated/
credentials.json credentials.json
token.json token.json
.pgdata*/
/generated/prisma /generated/prisma

View File

@ -1,7 +1,7 @@
{ {
"name": "backend", "name": "backend",
"version": "1.0.0", "version": "1.0.0",
"description": "Private organizational Google Drive upload API for Whats Drive", "description": "Private organizational Google Drive upload API for Metatron-Drive",
"main": "src/server.js", "main": "src/server.js",
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@ -0,0 +1,3 @@
ALTER TABLE "User" ADD COLUMN "isSuspended" BOOLEAN NOT NULL DEFAULT false;
CREATE INDEX "User_isSuspended_createdAt_idx" ON "User"("isSuspended", "createdAt");

View File

@ -21,10 +21,13 @@ model User {
gender String gender String
age Int age Int
driveFolderId String @unique driveFolderId String @unique
isSuspended Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
folders Folder[] folders Folder[]
files FileAsset[] files FileAsset[]
@@index([isSuspended, createdAt])
} }
model Folder { model Folder {

View File

@ -10,6 +10,7 @@ import authRoutes from "./routes/auth.js";
import folderRoutes from "./routes/folders.js"; import folderRoutes from "./routes/folders.js";
import fileRoutes from "./routes/files.js"; import fileRoutes from "./routes/files.js";
import retentionRoutes from "./routes/retention.js"; import retentionRoutes from "./routes/retention.js";
import adminRoutes from "./routes/admin.js";
export const app = express(); export const app = express();
app.disable("x-powered-by"); app.disable("x-powered-by");
@ -22,11 +23,13 @@ app.use(express.json({ limit: "1mb" }));
app.use(express.urlencoded({ extended: false, limit: "1mb" })); app.use(express.urlencoded({ extended: false, limit: "1mb" }));
app.use(morgan(config.NODE_ENV === "production" ? "combined" : "dev")); app.use(morgan(config.NODE_ENV === "production" ? "combined" : "dev"));
app.get("/health", (request, response) => response.json({ status: "ok", service: "whats-drive-api" })); app.get("/health", (request, response) => response.json({ status: "ok", service: "metatron-drive-api" }));
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 100, standardHeaders: "draft-8", legacyHeaders: false }); const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 100, standardHeaders: "draft-8", legacyHeaders: false });
const uploadLimiter = rateLimit({ windowMs: 60 * 60 * 1000, limit: 500, standardHeaders: "draft-8", legacyHeaders: false }); const uploadLimiter = rateLimit({ windowMs: 60 * 60 * 1000, limit: 500, standardHeaders: "draft-8", legacyHeaders: false });
const adminLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 50, standardHeaders: "draft-8", legacyHeaders: false });
app.use("/api/auth", authLimiter, authRoutes); app.use("/api/auth", authLimiter, authRoutes);
app.use("/api/admin", adminLimiter, adminRoutes);
app.use("/api/folders", requireAuth, folderRoutes); app.use("/api/folders", requireAuth, folderRoutes);
app.use("/api/files/upload", uploadLimiter); app.use("/api/files/upload", uploadLimiter);
app.use("/api/files", requireAuth, fileRoutes); app.use("/api/files", requireAuth, fileRoutes);

View File

@ -13,7 +13,10 @@ const schema = z.object({
MAX_UPLOAD_MB: z.coerce.number().positive().default(250), MAX_UPLOAD_MB: z.coerce.number().positive().default(250),
GOOGLE_CREDENTIALS: z.string().default("./credentials.json"), GOOGLE_CREDENTIALS: z.string().default("./credentials.json"),
GOOGLE_TOKEN: z.string().default("./token.json"), GOOGLE_TOKEN: z.string().default("./token.json"),
GOOGLE_DRIVE_ROOT_FOLDER: z.string().min(1).default("Whats-Drive-SaaS"), GOOGLE_DRIVE_ROOT_FOLDER: z.string().min(1).default("Metatron-Drive"),
ADMIN_USERNAME: z.string().trim().min(3).max(40).default("admin"),
ADMIN_PASSWORD: z.string().min(12),
ADMIN_JWT_EXPIRES_IN: z.string().default("8h"),
}); });
const parsed = schema.safeParse(process.env); const parsed = schema.safeParse(process.env);

View File

@ -0,0 +1,18 @@
import jwt from "jsonwebtoken";
import { config } from "../config.js";
export function requireAdmin(request, response, next) {
try {
const [scheme, token] = String(request.headers.authorization || "").split(" ");
if (scheme !== "Bearer" || !token) {
return response.status(401).json({ error: "Admin authentication required" });
}
const payload = jwt.verify(token, config.JWT_SECRET);
if (payload.sub !== "admin" || payload.role !== "admin") {
return response.status(403).json({ error: "Administrator access required" });
}
next();
} catch {
response.status(401).json({ error: "Invalid or expired admin session" });
}
}

View File

@ -11,6 +11,7 @@ export async function requireAuth(request, response, next) {
const payload = jwt.verify(token, config.JWT_SECRET); const payload = jwt.verify(token, config.JWT_SECRET);
const user = await prisma.user.findUnique({ where: { id: payload.sub } }); const user = await prisma.user.findUnique({ where: { id: payload.sub } });
if (!user) return response.status(401).json({ error: "Account no longer exists" }); if (!user) return response.status(401).json({ error: "Account no longer exists" });
if (user.isSuspended) return response.status(403).json({ error: "Account is suspended" });
request.user = user; request.user = user;
next(); next();
} catch { } catch {

161
src/routes/admin.js Normal file
View File

@ -0,0 +1,161 @@
import { Router } from "express";
import bcrypt from "bcryptjs";
import crypto from "node:crypto";
import jwt from "jsonwebtoken";
import { z } from "zod";
import { Prisma } from "@prisma/client";
import { config } from "../config.js";
import { prisma } from "../db.js";
import { requireAdmin } from "../middleware/adminAuth.js";
import { asyncHandler } from "../utils/asyncHandler.js";
import { HttpError } from "../utils/httpError.js";
const router = Router();
const usernameRule = z.string().trim().toLowerCase().min(3).max(30).regex(/^[a-z0-9._-]+$/);
const updateSchema = z.object({
name: z.string().trim().min(2).max(80).optional(),
username: usernameRule.optional(),
email: z.string().trim().toLowerCase().email().max(160).optional(),
phone: z.string().trim().min(7).max(20).optional(),
gender: z.enum(["Male", "Female", "Non-binary", "Prefer not to say"]).optional(),
age: z.coerce.number().int().min(13).max(120).optional(),
createdAt: z.coerce.date().optional(),
isSuspended: z.boolean().optional(),
password: z.string().min(8).max(128).optional(),
}).refine((input) => Object.keys(input).length > 0, "At least one field is required");
function secureEqual(left, right) {
const leftBuffer = Buffer.from(String(left));
const rightBuffer = Buffer.from(String(right));
return leftBuffer.length === rightBuffer.length && crypto.timingSafeEqual(leftBuffer, rightBuffer);
}
function adminUser(user) {
return {
id: user.id,
username: user.username,
name: user.name,
phone: user.phone,
email: user.email,
gender: user.gender,
age: user.age,
driveFolderId: user.driveFolderId,
isSuspended: user.isSuspended,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
counts: user._count ? { folders: user._count.folders, files: user._count.files } : undefined,
};
}
router.post("/login", (request, response) => {
const input = z.object({
username: z.string().trim().min(1).max(40),
password: z.string().min(1).max(128),
}).parse(request.body);
if (!secureEqual(input.username, config.ADMIN_USERNAME) || !secureEqual(input.password, config.ADMIN_PASSWORD)) {
throw new HttpError(401, "Invalid administrator credentials");
}
const token = jwt.sign({ role: "admin" }, config.JWT_SECRET, {
subject: "admin",
expiresIn: config.ADMIN_JWT_EXPIRES_IN,
});
response.json({ token, admin: { username: config.ADMIN_USERNAME } });
});
router.use(requireAdmin);
router.get("/me", (request, response) => {
response.json({ admin: { username: config.ADMIN_USERNAME } });
});
router.get("/stats", asyncHandler(async (request, response) => {
const [users, activeUsers, suspendedUsers, files, folders, storage] = await Promise.all([
prisma.user.count(),
prisma.user.count({ where: { isSuspended: false } }),
prisma.user.count({ where: { isSuspended: true } }),
prisma.fileAsset.count(),
prisma.folder.count(),
prisma.fileAsset.aggregate({ _sum: { sizeBytes: true } }),
]);
response.json({
stats: {
users,
activeUsers,
suspendedUsers,
files,
folders,
storageBytes: Number(storage._sum.sizeBytes || 0n),
},
});
}));
router.get("/users", asyncHandler(async (request, response) => {
const query = z.object({
search: z.string().trim().max(100).default(""),
status: z.enum(["all", "active", "suspended"]).default("all"),
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(25),
}).parse(request.query);
const where = {
...(query.status === "active" ? { isSuspended: false } : {}),
...(query.status === "suspended" ? { isSuspended: true } : {}),
...(query.search ? {
OR: ["name", "username", "email", "phone"].map((field) => ({
[field]: { contains: query.search, mode: "insensitive" },
})),
} : {}),
};
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
orderBy: { createdAt: "desc" },
skip: (query.page - 1) * query.limit,
take: query.limit,
include: { _count: { select: { folders: true, files: true } } },
}),
prisma.user.count({ where }),
]);
response.json({
users: users.map(adminUser),
pagination: { page: query.page, limit: query.limit, total, pages: Math.max(1, Math.ceil(total / query.limit)) },
});
}));
router.patch("/users/:id", asyncHandler(async (request, response) => {
const input = updateSchema.parse(request.body);
const { password, ...profile } = input;
try {
const user = await prisma.user.update({
where: { id: request.params.id },
data: {
...profile,
...(password ? { passwordHash: await bcrypt.hash(password, 12) } : {}),
},
include: { _count: { select: { folders: true, files: true } } },
});
response.json({ user: adminUser(user) });
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
throw new HttpError(409, "Username, email, or phone is already registered");
}
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") {
throw new HttpError(404, "User not found");
}
throw error;
}
}));
router.delete("/users/:id", asyncHandler(async (request, response) => {
try {
// Database relations cascade. Google Drive content is deliberately retained.
await prisma.user.delete({ where: { id: request.params.id } });
response.status(204).end();
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") {
throw new HttpError(404, "User not found");
}
throw error;
}
}));
export default router;

View File

@ -55,6 +55,7 @@ router.post("/login", asyncHandler(async (request, response) => {
if (!user || !(await bcrypt.compare(input.password, user.passwordHash))) { if (!user || !(await bcrypt.compare(input.password, user.passwordHash))) {
throw new HttpError(401, "Invalid username or password"); throw new HttpError(401, "Invalid username or password");
} }
if (user.isSuspended) throw new HttpError(403, "Account is suspended. Contact the administrator.");
response.json({ token: issueToken(user.id), user: publicUser(user) }); response.json({ token: issueToken(user.id), user: publicUser(user) });
})); }));

View File

@ -3,7 +3,7 @@ import { config } from "./config.js";
import { prisma } from "./db.js"; import { prisma } from "./db.js";
const server = app.listen(config.PORT, "0.0.0.0", () => { const server = app.listen(config.PORT, "0.0.0.0", () => {
console.log(`Whats Drive API listening on http://0.0.0.0:${config.PORT}`); console.log(`Metatron-Drive API listening on http://0.0.0.0:${config.PORT}`);
}); });
async function shutdown(signal) { async function shutdown(signal) {