import { computeUrlHash, normalizeUrl } from "@browser-bookmark/shared"; import { httpError } from "../lib/httpErrors.js"; import { requireAdmin, isAdminEmail } from "../lib/admin.js"; import { bookmarkRowToDto, folderRowToDto, userRowToDto } from "../lib/rows.js"; function toUserDtoWithAdminOverride(app, row) { const dto = userRowToDto(row); if (isAdminEmail(app, dto.email)) dto.role = "admin"; return dto; } export async function adminRoutes(app) { app.get( "/admin/users", { preHandler: [async (req) => requireAdmin(app, req)] }, async () => { const res = await app.pg.query( "select id, email, role, created_at, updated_at from users order by created_at desc limit 500" ); return res.rows.map((r) => toUserDtoWithAdminOverride(app, r)); } ); app.get( "/admin/users/:id/folders", { preHandler: [async (req) => requireAdmin(app, req)] }, async (req) => { const userId = req.params?.id; if (!userId) throw httpError(400, "user id required"); const orderBy = app.features?.folderSortOrder ? "parent_id nulls first, sort_order asc, name asc" : "parent_id nulls first, name asc"; const res = await app.pg.query( `select * from bookmark_folders where user_id=$1 order by ${orderBy} limit 1000`, [userId] ); return res.rows.map(folderRowToDto); } ); app.get( "/admin/users/:id/bookmarks", { preHandler: [async (req) => requireAdmin(app, req)] }, async (req) => { const userId = req.params?.id; if (!userId) throw httpError(400, "user id required"); const q = (req.query?.q || "").trim(); const params = [userId]; let where = "where user_id=$1 and deleted_at is null"; if (q) { params.push(`%${q}%`); where += ` and (title ilike $${params.length} or url ilike $${params.length})`; } const orderBy = app.features?.bookmarkSortOrder ? "folder_id nulls first, sort_order asc, updated_at desc" : "updated_at desc"; const res = await app.pg.query( `select * from bookmarks ${where} order by ${orderBy} limit 500`, params ); return res.rows.map(bookmarkRowToDto); } ); app.delete( "/admin/users/:userId/bookmarks/:bookmarkId", { preHandler: [async (req) => requireAdmin(app, req)] }, async (req) => { const userId = req.params?.userId; const bookmarkId = req.params?.bookmarkId; if (!userId || !bookmarkId) throw httpError(400, "userId and bookmarkId required"); const res = await app.pg.query( "update bookmarks set deleted_at=now(), updated_at=now() where id=$1 and user_id=$2 and deleted_at is null returning *", [bookmarkId, userId] ); if (!res.rows[0]) throw httpError(404, "bookmark not found"); return bookmarkRowToDto(res.rows[0]); } ); app.delete( "/admin/users/:userId/folders/:folderId", { preHandler: [async (req) => requireAdmin(app, req)] }, async (req) => { const userId = req.params?.userId; const folderId = req.params?.folderId; if (!userId || !folderId) throw httpError(400, "userId and folderId required"); const res = await app.pg.query( "delete from bookmark_folders where id=$1 and user_id=$2 returning *", [folderId, userId] ); if (!res.rows[0]) throw httpError(404, "folder not found"); return folderRowToDto(res.rows[0]); } ); app.post( "/admin/users/:userId/bookmarks/:bookmarkId/copy-to-me", { preHandler: [async (req) => requireAdmin(app, req)] }, async (req) => { const sourceUserId = req.params?.userId; const bookmarkId = req.params?.bookmarkId; const adminUserId = req.adminUser?.id; if (!sourceUserId || !bookmarkId) throw httpError(400, "userId and bookmarkId required"); if (!adminUserId) throw httpError(401, "unauthorized"); const srcRes = await app.pg.query( "select * from bookmarks where id=$1 and user_id=$2 and deleted_at is null", [bookmarkId, sourceUserId] ); const src = srcRes.rows[0]; if (!src) throw httpError(404, "bookmark not found"); const urlNormalized = normalizeUrl(src.url); const urlHash = computeUrlHash(urlNormalized); const existing = await app.pg.query( "select * from bookmarks where user_id=$1 and url_hash=$2 and deleted_at is null limit 1", [adminUserId, urlHash] ); if (existing.rows[0]) { const merged = await app.pg.query( `update bookmarks set title=$1, url=$2, url_normalized=$3, visibility='private', folder_id=null, source='manual', updated_at=now() where id=$4 returning *`, [src.title, src.url, urlNormalized, existing.rows[0].id] ); return bookmarkRowToDto(merged.rows[0]); } const res = await app.pg.query( `insert into bookmarks (user_id, folder_id, title, url, url_normalized, url_hash, visibility, source) values ($1, null, $2, $3, $4, $5, 'private', 'manual') returning *`, [adminUserId, src.title, src.url, urlNormalized, urlHash] ); return bookmarkRowToDto(res.rows[0]); } ); }