feat: add board-claim flow for local_trusted -> authenticated migration
One-time high-entropy claim URL printed at startup when the only instance admin is local-board. Signed-in user claims ownership, gets promoted to instance_admin, and receives active memberships across all existing companies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import { forbidden, conflict, notFound, unauthorized, badRequest } from "../erro
|
||||
import { validate } from "../middleware/validate.js";
|
||||
import { accessService, agentService, logActivity } from "../services/index.js";
|
||||
import { assertCompanyAccess } from "./authz.js";
|
||||
import { claimBoardOwnership, inspectBoardClaimChallenge } from "../board-claim.js";
|
||||
|
||||
function hashToken(token: string) {
|
||||
return createHash("sha256").update(token).digest("hex");
|
||||
@@ -101,6 +102,40 @@ export function accessRoutes(db: Db) {
|
||||
if (!allowed) throw forbidden("Instance admin required");
|
||||
}
|
||||
|
||||
router.get("/board-claim/:token", async (req, res) => {
|
||||
const token = (req.params.token as string).trim();
|
||||
const code = typeof req.query.code === "string" ? req.query.code.trim() : undefined;
|
||||
if (!token) throw notFound("Board claim challenge not found");
|
||||
const challenge = inspectBoardClaimChallenge(token, code);
|
||||
if (challenge.status === "invalid") throw notFound("Board claim challenge not found");
|
||||
res.json(challenge);
|
||||
});
|
||||
|
||||
router.post("/board-claim/:token/claim", async (req, res) => {
|
||||
const token = (req.params.token as string).trim();
|
||||
const code = typeof req.body?.code === "string" ? req.body.code.trim() : undefined;
|
||||
if (!token) throw notFound("Board claim challenge not found");
|
||||
if (!code) throw badRequest("Claim code is required");
|
||||
if (req.actor.type !== "board" || req.actor.source !== "session" || !req.actor.userId) {
|
||||
throw unauthorized("Sign in before claiming board ownership");
|
||||
}
|
||||
|
||||
const claimed = await claimBoardOwnership(db, {
|
||||
token,
|
||||
code,
|
||||
userId: req.actor.userId,
|
||||
});
|
||||
|
||||
if (claimed.status === "invalid") throw notFound("Board claim challenge not found");
|
||||
if (claimed.status === "expired") throw conflict("Board claim challenge expired. Restart server to generate a new one.");
|
||||
if (claimed.status === "claimed") {
|
||||
res.json({ claimed: true, userId: claimed.claimedByUserId ?? req.actor.userId });
|
||||
return;
|
||||
}
|
||||
|
||||
throw conflict("Board claim challenge is no longer available");
|
||||
});
|
||||
|
||||
async function assertCompanyPermission(req: Request, companyId: string, permissionKey: any) {
|
||||
assertCompanyAccess(req, companyId);
|
||||
if (req.actor.type === "agent") {
|
||||
|
||||
Reference in New Issue
Block a user