feat(server): integrate Better Auth, access control, and deployment mode startup
Wire up Better Auth for session-based authentication. Add actor middleware that resolves local_trusted mode to an implicit board actor and authenticated mode to Better Auth sessions. Add access service with membership, permission, invite, and join-request management. Register access routes for member/invite/ join-request CRUD. Update health endpoint to report deployment mode and bootstrap status. Enforce tasks:assign and agents:create permissions in issue and agent routes. Add deployment mode validation at startup with guardrails (loopback-only for local_trusted, auth config required for authenticated). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +1,45 @@
|
||||
import { Router } from "express";
|
||||
import type { Db } from "@paperclip/db";
|
||||
import { createCompanySchema, updateCompanySchema } from "@paperclip/shared";
|
||||
import { forbidden } from "../errors.js";
|
||||
import { validate } from "../middleware/validate.js";
|
||||
import { companyService, logActivity } from "../services/index.js";
|
||||
import { assertBoard } from "./authz.js";
|
||||
import { accessService, companyService, logActivity } from "../services/index.js";
|
||||
import { assertBoard, assertCompanyAccess } from "./authz.js";
|
||||
|
||||
export function companyRoutes(db: Db) {
|
||||
const router = Router();
|
||||
const svc = companyService(db);
|
||||
const access = accessService(db);
|
||||
|
||||
router.get("/", async (_req, res) => {
|
||||
router.get("/", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const result = await svc.list();
|
||||
res.json(result);
|
||||
if (req.actor.source === "local_implicit" || req.actor.isInstanceAdmin) {
|
||||
res.json(result);
|
||||
return;
|
||||
}
|
||||
const allowed = new Set(req.actor.companyIds ?? []);
|
||||
res.json(result.filter((company) => allowed.has(company.id)));
|
||||
});
|
||||
|
||||
router.get("/stats", async (_req, res) => {
|
||||
router.get("/stats", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const allowed = req.actor.source === "local_implicit" || req.actor.isInstanceAdmin
|
||||
? null
|
||||
: new Set(req.actor.companyIds ?? []);
|
||||
const stats = await svc.stats();
|
||||
res.json(stats);
|
||||
if (!allowed) {
|
||||
res.json(stats);
|
||||
return;
|
||||
}
|
||||
const filtered = Object.fromEntries(Object.entries(stats).filter(([companyId]) => allowed.has(companyId)));
|
||||
res.json(filtered);
|
||||
});
|
||||
|
||||
router.get("/:companyId", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const company = await svc.getById(companyId);
|
||||
if (!company) {
|
||||
res.status(404).json({ error: "Company not found" });
|
||||
@@ -31,7 +50,11 @@ export function companyRoutes(db: Db) {
|
||||
|
||||
router.post("/", validate(createCompanySchema), async (req, res) => {
|
||||
assertBoard(req);
|
||||
if (!(req.actor.source === "local_implicit" || req.actor.isInstanceAdmin)) {
|
||||
throw forbidden("Instance admin required");
|
||||
}
|
||||
const company = await svc.create(req.body);
|
||||
await access.ensureMembership(company.id, "user", req.actor.userId ?? "local-board", "owner", "active");
|
||||
await logActivity(db, {
|
||||
companyId: company.id,
|
||||
actorType: "user",
|
||||
@@ -47,6 +70,7 @@ export function companyRoutes(db: Db) {
|
||||
router.patch("/:companyId", validate(updateCompanySchema), async (req, res) => {
|
||||
assertBoard(req);
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const company = await svc.update(companyId, req.body);
|
||||
if (!company) {
|
||||
res.status(404).json({ error: "Company not found" });
|
||||
@@ -67,6 +91,7 @@ export function companyRoutes(db: Db) {
|
||||
router.post("/:companyId/archive", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const company = await svc.archive(companyId);
|
||||
if (!company) {
|
||||
res.status(404).json({ error: "Company not found" });
|
||||
@@ -86,6 +111,7 @@ export function companyRoutes(db: Db) {
|
||||
router.delete("/:companyId", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const company = await svc.remove(companyId);
|
||||
if (!company) {
|
||||
res.status(404).json({ error: "Company not found" });
|
||||
|
||||
Reference in New Issue
Block a user