{"version":3,"file":"disable.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/admin/users/[id]/disable.ts"],"sourcesContent":["/**\n * User disable endpoint\n *\n * POST /_emdash/api/admin/users/:id/disable - Soft-disable a user\n */\n\nimport { Role } from \"@premium-cms/auth\";\nimport { createKyselyAdapter } from \"@premium-cms/auth/adapters/kysely\";\nimport type { APIRoute } from \"astro\";\n\nimport { requirePerm } from \"#api/authorize.js\";\nimport { apiError, apiSuccess, handleError } from \"#api/error.js\";\nimport { withTransaction } from \"#db/transaction.js\";\n\nexport const prerender = false;\n\nexport const POST: APIRoute = async ({ params, locals }) => {\n\tconst { emdash, user: currentUser } = locals;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"Database not configured\", 500);\n\t}\n\n\tconst denied = requirePerm(currentUser, \"users:manage\");\n\tif (denied) return denied;\n\n\tconst adapter = createKyselyAdapter(emdash.db);\n\n\tconst { id } = params;\n\n\tif (!id) {\n\t\treturn apiError(\"VALIDATION_ERROR\", \"User ID required\", 400);\n\t}\n\n\t// Prevent disabling self\n\tif (id === currentUser.id) {\n\t\treturn apiError(\"VALIDATION_ERROR\", \"Cannot disable your own account\", 400);\n\t}\n\n\ttry {\n\t\t// Get target user\n\t\tconst targetUser = await adapter.getUserById(id);\n\t\tif (!targetUser) {\n\t\t\treturn apiError(\"NOT_FOUND\", \"User not found\", 404);\n\t\t}\n\n\t\t// Wrap admin check + disable in a transaction to prevent two\n\t\t// concurrent requests from both passing the count check.\n\t\tconst lastAdminBlocked = await withTransaction(emdash.db, async (trx) => {\n\t\t\tconst trxAdapter = createKyselyAdapter(trx);\n\t\t\tif (targetUser.role === Role.ADMIN) {\n\t\t\t\tconst adminCount = await trxAdapter.countAdmins();\n\t\t\t\tif (adminCount <= 1) return true;\n\t\t\t}\n\t\t\tawait trxAdapter.updateUser(id, { disabled: true });\n\t\t\treturn false;\n\t\t});\n\n\t\tif (lastAdminBlocked) {\n\t\t\treturn apiError(\n\t\t\t\t\"VALIDATION_ERROR\",\n\t\t\t\t\"Cannot disable the last admin. Promote another user first.\",\n\t\t\t\t400,\n\t\t\t);\n\t\t}\n\n\t\t// SEC-43: Revoke all OAuth tokens for the disabled user.\n\t\t// Without this, existing refresh tokens remain valid for up to 90 days.\n\t\tawait emdash.db.deleteFrom(\"_emdash_oauth_tokens\").where(\"user_id\", \"=\", id).execute();\n\n\t\treturn apiSuccess({ success: true });\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to disable user\", \"USER_DISABLE_ERROR\");\n\t}\n};\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAa,YAAY;AAEzB,MAAa,OAAiB,OAAO,EAAE,QAAQ,aAAa;CAC3D,MAAM,EAAE,QAAQ,MAAM,gBAAgB;AAEtC,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,2BAA2B,IAAI;CAGlE,MAAM,SAAS,YAAY,aAAa,eAAe;AACvD,KAAI,OAAQ,QAAO;CAEnB,MAAM,UAAU,oBAAoB,OAAO,GAAG;CAE9C,MAAM,EAAE,OAAO;AAEf,KAAI,CAAC,GACJ,QAAO,SAAS,oBAAoB,oBAAoB,IAAI;AAI7D,KAAI,OAAO,YAAY,GACtB,QAAO,SAAS,oBAAoB,mCAAmC,IAAI;AAG5E,KAAI;EAEH,MAAM,aAAa,MAAM,QAAQ,YAAY,GAAG;AAChD,MAAI,CAAC,WACJ,QAAO,SAAS,aAAa,kBAAkB,IAAI;AAepD,MAVyB,MAAM,gBAAgB,OAAO,IAAI,OAAO,QAAQ;GACxE,MAAM,aAAa,oBAAoB,IAAI;AAC3C,OAAI,WAAW,SAAS,KAAK,OAE5B;QADmB,MAAM,WAAW,aAAa,IAC/B,EAAG,QAAO;;AAE7B,SAAM,WAAW,WAAW,IAAI,EAAE,UAAU,MAAM,CAAC;AACnD,UAAO;IACN,CAGD,QAAO,SACN,oBACA,8DACA,IACA;AAKF,QAAM,OAAO,GAAG,WAAW,uBAAuB,CAAC,MAAM,WAAW,KAAK,GAAG,CAAC,SAAS;AAEtF,SAAO,WAAW,EAAE,SAAS,MAAM,CAAC;UAC5B,OAAO;AACf,SAAO,YAAY,OAAO,0BAA0B,qBAAqB"}