Allow CEO agents to update company branding (name, description, logo, color)
- Add updateCompanyBrandingSchema restricting agent-updatable fields to name, description, brandColor, and logoAssetId - Update PATCH /api/companies/:companyId to allow CEO agents with branding-only fields while keeping admin fields (status, budget, etc.) board-only - Allow agents to GET /api/companies/:companyId for reading company info - issuePrefix (company slug) remains protected — not in any update schema - Document branding APIs in SKILL.md quick reference and api-reference.md Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
035cb8aec2
commit
9c5a31ed45
6 changed files with 95 additions and 7 deletions
|
|
@ -253,8 +253,10 @@ export {
|
||||||
export {
|
export {
|
||||||
createCompanySchema,
|
createCompanySchema,
|
||||||
updateCompanySchema,
|
updateCompanySchema,
|
||||||
|
updateCompanyBrandingSchema,
|
||||||
type CreateCompany,
|
type CreateCompany,
|
||||||
type UpdateCompany,
|
type UpdateCompany,
|
||||||
|
type UpdateCompanyBranding,
|
||||||
createAgentSchema,
|
createAgentSchema,
|
||||||
createAgentHireSchema,
|
createAgentHireSchema,
|
||||||
updateAgentSchema,
|
updateAgentSchema,
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,13 @@ export const updateCompanySchema = createCompanySchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UpdateCompany = z.infer<typeof updateCompanySchema>;
|
export type UpdateCompany = z.infer<typeof updateCompanySchema>;
|
||||||
|
|
||||||
|
/** Branding-only subset that CEO agents may update. */
|
||||||
|
export const updateCompanyBrandingSchema = z.object({
|
||||||
|
name: z.string().min(1).optional(),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
|
brandColor: z.string().regex(/^#[0-9a-fA-F]{6}$/).nullable().optional(),
|
||||||
|
logoAssetId: logoAssetIdSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UpdateCompanyBranding = z.infer<typeof updateCompanyBrandingSchema>;
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,10 @@ export {
|
||||||
export {
|
export {
|
||||||
createCompanySchema,
|
createCompanySchema,
|
||||||
updateCompanySchema,
|
updateCompanySchema,
|
||||||
|
updateCompanyBrandingSchema,
|
||||||
type CreateCompany,
|
type CreateCompany,
|
||||||
type UpdateCompany,
|
type UpdateCompany,
|
||||||
|
type UpdateCompanyBranding,
|
||||||
} from "./company.js";
|
} from "./company.js";
|
||||||
export {
|
export {
|
||||||
portabilityIncludeSchema,
|
portabilityIncludeSchema,
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,13 @@ import {
|
||||||
companyPortabilityPreviewSchema,
|
companyPortabilityPreviewSchema,
|
||||||
createCompanySchema,
|
createCompanySchema,
|
||||||
updateCompanySchema,
|
updateCompanySchema,
|
||||||
|
updateCompanyBrandingSchema,
|
||||||
} from "@paperclipai/shared";
|
} from "@paperclipai/shared";
|
||||||
import { forbidden } from "../errors.js";
|
import { forbidden } from "../errors.js";
|
||||||
import { validate } from "../middleware/validate.js";
|
import { validate } from "../middleware/validate.js";
|
||||||
import {
|
import {
|
||||||
accessService,
|
accessService,
|
||||||
|
agentService,
|
||||||
budgetService,
|
budgetService,
|
||||||
companyPortabilityService,
|
companyPortabilityService,
|
||||||
companyService,
|
companyService,
|
||||||
|
|
@ -58,9 +60,12 @@ export function companyRoutes(db: Db) {
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/:companyId", async (req, res) => {
|
router.get("/:companyId", async (req, res) => {
|
||||||
assertBoard(req);
|
|
||||||
const companyId = req.params.companyId as string;
|
const companyId = req.params.companyId as string;
|
||||||
assertCompanyAccess(req, companyId);
|
assertCompanyAccess(req, companyId);
|
||||||
|
// Allow agents (CEO) to read their own company; board always allowed
|
||||||
|
if (req.actor.type !== "agent") {
|
||||||
|
assertBoard(req);
|
||||||
|
}
|
||||||
const company = await svc.getById(companyId);
|
const company = await svc.getById(companyId);
|
||||||
if (!company) {
|
if (!company) {
|
||||||
res.status(404).json({ error: "Company not found" });
|
res.status(404).json({ error: "Company not found" });
|
||||||
|
|
@ -144,23 +149,44 @@ export function companyRoutes(db: Db) {
|
||||||
res.status(201).json(company);
|
res.status(201).json(company);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.patch("/:companyId", validate(updateCompanySchema), async (req, res) => {
|
router.patch("/:companyId", async (req, res) => {
|
||||||
assertBoard(req);
|
|
||||||
const companyId = req.params.companyId as string;
|
const companyId = req.params.companyId as string;
|
||||||
assertCompanyAccess(req, companyId);
|
assertCompanyAccess(req, companyId);
|
||||||
const company = await svc.update(companyId, req.body);
|
|
||||||
|
const actor = getActorInfo(req);
|
||||||
|
let body: Record<string, unknown>;
|
||||||
|
|
||||||
|
if (req.actor.type === "agent") {
|
||||||
|
// Only CEO agents may update company branding fields
|
||||||
|
const agentSvc = agentService(db);
|
||||||
|
const actorAgent = req.actor.agentId ? await agentSvc.getById(req.actor.agentId) : null;
|
||||||
|
if (!actorAgent || actorAgent.role !== "ceo") {
|
||||||
|
throw forbidden("Only CEO agents or board users may update company settings");
|
||||||
|
}
|
||||||
|
if (actorAgent.companyId !== companyId) {
|
||||||
|
throw forbidden("Agent key cannot access another company");
|
||||||
|
}
|
||||||
|
body = updateCompanyBrandingSchema.parse(req.body);
|
||||||
|
} else {
|
||||||
|
assertBoard(req);
|
||||||
|
body = updateCompanySchema.parse(req.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const company = await svc.update(companyId, body);
|
||||||
if (!company) {
|
if (!company) {
|
||||||
res.status(404).json({ error: "Company not found" });
|
res.status(404).json({ error: "Company not found" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await logActivity(db, {
|
await logActivity(db, {
|
||||||
companyId,
|
companyId,
|
||||||
actorType: "user",
|
actorType: actor.actorType,
|
||||||
actorId: req.actor.userId ?? "board",
|
actorId: actor.actorId,
|
||||||
|
agentId: actor.agentId,
|
||||||
|
runId: actor.runId,
|
||||||
action: "company.updated",
|
action: "company.updated",
|
||||||
entityType: "company",
|
entityType: "company",
|
||||||
entityId: companyId,
|
entityId: companyId,
|
||||||
details: req.body,
|
details: body,
|
||||||
});
|
});
|
||||||
res.json(company);
|
res.json(company);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,34 @@ PATCH /api/agents/{agentId}/instructions-path
|
||||||
| List agents | `GET /api/companies/:companyId/agents` |
|
| List agents | `GET /api/companies/:companyId/agents` |
|
||||||
| Dashboard | `GET /api/companies/:companyId/dashboard` |
|
| Dashboard | `GET /api/companies/:companyId/dashboard` |
|
||||||
| Search issues | `GET /api/companies/:companyId/issues?q=search+term` |
|
| Search issues | `GET /api/companies/:companyId/issues?q=search+term` |
|
||||||
|
| Get company details (CEO/board) | `GET /api/companies/:companyId` |
|
||||||
|
| Update company branding (CEO/board) | `PATCH /api/companies/:companyId` |
|
||||||
|
| Upload company logo | `POST /api/companies/:companyId/logo` (multipart file upload) |
|
||||||
|
|
||||||
|
## Company Branding (CEO)
|
||||||
|
|
||||||
|
CEO agents can read and update their company's branding. Board users have full access to all company fields.
|
||||||
|
|
||||||
|
**Readable fields** (via `GET /api/companies/:companyId`):
|
||||||
|
|
||||||
|
All company fields including `name`, `description`, `brandColor`, `logoUrl`, `issuePrefix`.
|
||||||
|
|
||||||
|
**Updatable fields** (CEO agents, via `PATCH /api/companies/:companyId`):
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
| ------------- | ------------------------ | ----------------------------------------- |
|
||||||
|
| `name` | string | Company display name |
|
||||||
|
| `description` | string \| null | Company description |
|
||||||
|
| `brandColor` | string \| null | Hex color, e.g. `#FF5733` |
|
||||||
|
| `logoAssetId` | UUID string \| null | Set after uploading via the logo endpoint |
|
||||||
|
|
||||||
|
**Protected fields** (board-only): `status`, `budgetMonthlyCents`, `spentMonthlyCents`, `requireBoardApprovalForNewAgents`. The `issuePrefix` (company slug) cannot be changed via API.
|
||||||
|
|
||||||
|
**Logo upload flow:**
|
||||||
|
|
||||||
|
1. Upload: `POST /api/companies/:companyId/logo` with `Content-Type: multipart/form-data`, field name `file`. Accepts PNG, JPEG, WebP, GIF, SVG (max 10 MB).
|
||||||
|
2. Set: `PATCH /api/companies/:companyId` with `{ "logoAssetId": "<asset-id-from-step-1>" }`.
|
||||||
|
3. Clear: `PATCH /api/companies/:companyId` with `{ "logoAssetId": null }`.
|
||||||
|
|
||||||
## Searching Issues
|
## Searching Issues
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,26 @@ GET /api/companies/{companyId}/dashboard — health summary: agent/task counts,
|
||||||
|
|
||||||
Use the dashboard for situational awareness, especially if you're a manager or CEO.
|
Use the dashboard for situational awareness, especially if you're a manager or CEO.
|
||||||
|
|
||||||
|
## Company Branding (CEO / Board)
|
||||||
|
|
||||||
|
CEO agents can update branding fields on their own company. Board users can update all fields.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/companies/{companyId} — read company (CEO agents + board)
|
||||||
|
PATCH /api/companies/{companyId} — update company fields
|
||||||
|
POST /api/companies/{companyId}/logo — upload logo (multipart, field: "file")
|
||||||
|
```
|
||||||
|
|
||||||
|
**CEO-allowed fields:** `name`, `description`, `brandColor` (hex e.g. `#FF5733` or null), `logoAssetId` (UUID or null).
|
||||||
|
|
||||||
|
**Board-only fields:** `status`, `budgetMonthlyCents`, `spentMonthlyCents`, `requireBoardApprovalForNewAgents`.
|
||||||
|
|
||||||
|
**Not updateable:** `issuePrefix` (used as company slug/identifier — protected from changes).
|
||||||
|
|
||||||
|
**Logo workflow:**
|
||||||
|
1. `POST /api/companies/{companyId}/logo` with file upload → returns `{ assetId }`.
|
||||||
|
2. `PATCH /api/companies/{companyId}` with `{ "logoAssetId": "<assetId>" }`.
|
||||||
|
|
||||||
## OpenClaw Invite Prompt (CEO)
|
## OpenClaw Invite Prompt (CEO)
|
||||||
|
|
||||||
Use this endpoint to generate a short-lived OpenClaw onboarding invite prompt:
|
Use this endpoint to generate a short-lived OpenClaw onboarding invite prompt:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue