feat(den): use Better Auth active org context (#1485)

* feat(den): use Better Auth active org context

* fix(app): switch Better Auth org only on explicit actions

* refactor(den): flatten active org resource routes

---------

Co-authored-by: src-opn <src-opn@users.noreply.github.com>
This commit is contained in:
Source Open
2026-04-17 21:52:42 -07:00
committed by GitHub
parent 58ae294191
commit ac41d58b0b
57 changed files with 521 additions and 273 deletions

View File

@@ -533,6 +533,34 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
}
};
const switchActiveOrg = async (nextId: string) => {
const nextOrg = orgs().find((org) => org.id === nextId) ?? null;
if (!nextOrg || nextId === activeOrgId()) {
return;
}
setOrgsBusy(true);
setOrgsError(null);
try {
await client().setActiveOrganization({ organizationId: nextId });
setActiveOrgId(nextId);
writeDenSettings({
baseUrl: baseUrl(),
authToken: authToken() || null,
activeOrgId: nextId || null,
activeOrgSlug: nextOrg.slug,
activeOrgName: nextOrg.name,
});
setStatusMessage(
t("den.org_switched", currentLocale(), { name: nextOrg.name }),
);
} catch (error) {
setOrgsError(error instanceof Error ? error.message : tr("den.error_load_orgs"));
} finally {
setOrgsBusy(false);
}
};
const refreshWorkers = async (quiet = false) => {
const orgId = activeOrgId().trim();
if (!authToken().trim() || !orgId) {
@@ -1300,18 +1328,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
value={activeOrgId()}
onChange={(event) => {
const nextId = event.currentTarget.value;
const nextOrg = orgs().find((org) => org.id === nextId) ?? null;
setActiveOrgId(nextId);
writeDenSettings({
baseUrl: baseUrl(),
authToken: authToken() || null,
activeOrgId: nextId || null,
activeOrgSlug: nextOrg?.slug ?? null,
activeOrgName: nextOrg?.name ?? null,
});
setStatusMessage(
t("den.org_switched", currentLocale(), { name: nextOrg?.name ?? tr("den.active_org_title") }),
);
void switchActiveOrg(nextId);
}}
disabled={orgsBusy() || orgs().length === 0}
>

View File

@@ -882,11 +882,36 @@ async function requestJson<T>(
return raw.json as T;
}
async function ensureActiveOrganization(
baseUrls: DenBaseUrls,
token: string | null,
input: { organizationId?: string | null; organizationSlug?: string | null },
) {
const organizationId = input.organizationId?.trim() ?? "";
const organizationSlug = input.organizationSlug?.trim() ?? "";
if (!token || (!organizationId && !organizationSlug)) {
return;
}
await requestJson<unknown>(baseUrls, "/api/auth/organization/set-active", {
method: "POST",
token,
body: {
organizationId: organizationId || undefined,
organizationSlug: organizationSlug || undefined,
},
});
}
export function createDenClient(options: { baseUrl: string; token?: string | null }) {
const baseUrls = resolveDenBaseUrls(options.baseUrl);
const token = options.token?.trim() ?? null;
return {
async setActiveOrganization(input: { organizationId?: string | null; organizationSlug?: string | null }): Promise<void> {
await ensureActiveOrganization(baseUrls, token, input);
},
async signInEmail(email: string, password: string): Promise<DenAuthResult> {
const payload = await requestJson<unknown>(baseUrls, "/api/auth/sign-in/email", {
method: "POST",
@@ -949,21 +974,30 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
return { user: getUser(payload), token: getToken(payload) };
},
async listOrgs(): Promise<{ orgs: DenOrgSummary[]; defaultOrgId: string | null }> {
async listOrgs(): Promise<{ orgs: DenOrgSummary[]; activeOrgId: string | null; activeOrgSlug: string | null; defaultOrgId: string | null }> {
const payload = await requestJson<unknown>(baseUrls, "/v1/me/orgs", {
method: "GET",
token,
});
const activeOrgId = isRecord(payload) && typeof payload.activeOrgId === "string"
? payload.activeOrgId
: null;
const activeOrgSlug = isRecord(payload) && typeof payload.activeOrgSlug === "string"
? payload.activeOrgSlug
: null;
return {
orgs: getOrgList(payload),
defaultOrgId: isRecord(payload) && typeof payload.defaultOrgId === "string" ? payload.defaultOrgId : null,
activeOrgId,
activeOrgSlug,
defaultOrgId: activeOrgId,
};
},
async listWorkers(orgId: string, limit = 20): Promise<DenWorkerSummary[]> {
const params = new URLSearchParams();
params.set("limit", String(limit));
params.set("orgId", orgId);
const payload = await requestJson<unknown>(baseUrls, `/v1/workers?${params.toString()}`, {
method: "GET",
token,
@@ -972,9 +1006,7 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
},
async getWorkerTokens(workerId: string, orgId: string): Promise<DenWorkerTokens> {
const params = new URLSearchParams();
params.set("orgId", orgId);
const payload = await requestJson<unknown>(baseUrls, `/v1/workers/${encodeURIComponent(workerId)}/tokens?${params.toString()}`, {
const payload = await requestJson<unknown>(baseUrls, `/v1/workers/${encodeURIComponent(workerId)}/tokens`, {
method: "POST",
token,
body: {},
@@ -989,7 +1021,7 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
async listTemplates(orgSlug: string): Promise<DenTemplate[]> {
const payload = await requestJson<unknown>(
baseUrls,
`/v1/orgs/${encodeURIComponent(orgSlug)}/templates`,
"/v1/templates",
{
method: "GET",
token,
@@ -1004,7 +1036,7 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
): Promise<DenTemplate> {
const payload = await requestJson<unknown>(
baseUrls,
`/v1/orgs/${encodeURIComponent(orgSlug)}/templates`,
"/v1/templates",
{
method: "POST",
token,
@@ -1024,7 +1056,7 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
async deleteTemplate(orgSlug: string, templateId: string): Promise<void> {
const raw = await requestJsonRaw(
baseUrls,
`/v1/orgs/${encodeURIComponent(orgSlug)}/templates/${encodeURIComponent(templateId)}`,
`/v1/templates/${encodeURIComponent(templateId)}`,
{
method: "DELETE",
token,
@@ -1039,7 +1071,7 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
},
async listOrgSkills(orgId: string): Promise<DenOrgSkillCard[]> {
const payload = await requestJson<unknown>(baseUrls, `/v1/orgs/${encodeURIComponent(orgId)}/skills`, {
const payload = await requestJson<unknown>(baseUrls, "/v1/skills", {
method: "GET",
token,
});
@@ -1047,7 +1079,7 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
},
async listOrgSkillHubs(orgId: string): Promise<DenOrgSkillHub[]> {
const payload = await requestJson<unknown>(baseUrls, `/v1/orgs/${encodeURIComponent(orgId)}/skill-hubs`, {
const payload = await requestJson<unknown>(baseUrls, "/v1/skill-hubs", {
method: "GET",
token,
});
@@ -1055,7 +1087,7 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
},
async listOrgSkillHubSummaries(orgId: string): Promise<DenOrgSkillHubSummary[]> {
const payload = await requestJson<unknown>(baseUrls, `/v1/orgs/${encodeURIComponent(orgId)}/skill-hubs`, {
const payload = await requestJson<unknown>(baseUrls, "/v1/skill-hubs", {
method: "GET",
token,
});
@@ -1070,7 +1102,7 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
skillText: input.skillText,
shared: input.shared === undefined ? ("org" as const) : input.shared,
};
const payload = await requestJson<unknown>(baseUrls, `/v1/orgs/${encodeURIComponent(orgId)}/skills`, {
const payload = await requestJson<unknown>(baseUrls, "/v1/skills", {
method: "POST",
token,
body,
@@ -1085,7 +1117,7 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
async addOrgSkillToHub(orgId: string, skillHubId: string, skillId: string): Promise<void> {
await requestJson<unknown>(
baseUrls,
`/v1/orgs/${encodeURIComponent(orgId)}/skill-hubs/${encodeURIComponent(skillHubId)}/skills`,
`/v1/skill-hubs/${encodeURIComponent(skillHubId)}/skills`,
{
method: "POST",
token,
@@ -1095,7 +1127,7 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
},
async listOrgLlmProviders(orgId: string): Promise<DenOrgLlmProvider[]> {
const payload = await requestJson<unknown>(baseUrls, `/v1/orgs/${encodeURIComponent(orgId)}/llm-providers`, {
const payload = await requestJson<unknown>(baseUrls, "/v1/llm-providers", {
method: "GET",
token,
});
@@ -1105,7 +1137,7 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
async getOrgLlmProviderConnection(orgId: string, llmProviderId: string): Promise<DenOrgLlmProviderConnection> {
const payload = await requestJson<unknown>(
baseUrls,
`/v1/orgs/${encodeURIComponent(orgId)}/llm-providers/${encodeURIComponent(llmProviderId)}/connect`,
`/v1/llm-providers/${encodeURIComponent(llmProviderId)}/connect`,
{
method: "GET",
token,

View File

@@ -308,6 +308,26 @@ export default function CreateWorkspaceModal(props: CreateWorkspaceModalProps) {
setCloudSettings(nextSettings);
};
const switchActiveOrg = async (orgId: string) => {
const nextOrg = orgs().find((org) => org.id === orgId) ?? null;
if (!nextOrg || orgId === activeOrgId().trim()) {
return;
}
setOrgsBusy(true);
setOrgsError(null);
try {
await denClient().setActiveOrganization({ organizationId: orgId });
applyActiveOrg(nextOrg);
} catch (error) {
setOrgsError(
error instanceof Error ? error.message : translate("dashboard.error_load_orgs"),
);
} finally {
setOrgsBusy(false);
}
};
const refreshOrgs = async () => {
if (!isSignedIn()) return;
setOrgsBusy(true);
@@ -597,8 +617,7 @@ export default function CreateWorkspaceModal(props: CreateWorkspaceModalProps) {
orgs={orgs()}
activeOrgId={activeOrgId()}
onActiveOrgChange={(orgId) => {
const nextOrg = orgs().find((org) => org.id === orgId) ?? null;
applyActiveOrg(nextOrg);
void switchActiveOrg(orgId);
}}
orgsBusy={orgsBusy()}
orgsError={orgsError()}

View File

@@ -0,0 +1,19 @@
import { asc, eq } from "@openwork-ee/den-db/drizzle"
import { MemberTable } from "@openwork-ee/den-db/schema"
import { normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import { db } from "./db.js"
export async function getInitialActiveOrganizationIdForUser(userId: string) {
const normalizedUserId = normalizeDenTypeId("user", userId)
const rows = await db
.select({
organizationId: MemberTable.organizationId,
})
.from(MemberTable)
.where(eq(MemberTable.userId, normalizedUserId))
.orderBy(asc(MemberTable.createdAt))
.limit(1)
return rows[0]?.organizationId ?? null
}

View File

@@ -1,3 +1,4 @@
import { getInitialActiveOrganizationIdForUser } from "./active-organization.js";
import { db } from "./db.js";
import { env } from "./env.js";
import {
@@ -77,6 +78,22 @@ export const auth = betterAuth({
provider: "mysql",
schema,
}),
databaseHooks: {
session: {
create: {
before: async (session) => {
const activeOrganizationId = await getInitialActiveOrganizationIdForUser(session.userId);
return {
data: {
...session,
activeOrganizationId,
},
};
},
},
},
},
advanced: {
ipAddress: {
ipAddressHeaders: ["x-forwarded-for", "x-real-ip", "cf-connecting-ip"],

View File

@@ -1,45 +1,62 @@
import { normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import type { MiddlewareHandler } from "hono"
import { isScopedApiKeyForOrganization } from "../api-keys.js"
import { getOrganizationContextForUser, type OrganizationContext } from "../orgs.js"
import { getApiKeyScopedOrganizationId, isScopedApiKeyForOrganization } from "../api-keys.js"
import { getOrganizationContextForUser, resolveUserOrganizations, type OrganizationContext } from "../orgs.js"
import type { AuthContextVariables } from "../session.js"
import type { UserOrganizationsContext } from "./user-organizations.js"
export type OrganizationContextVariables = {
organizationContext: OrganizationContext
}
export const resolveOrganizationContextMiddleware: MiddlewareHandler<{
Variables: AuthContextVariables & Partial<OrganizationContextVariables>
Variables: AuthContextVariables & Partial<OrganizationContextVariables> & Partial<UserOrganizationsContext>
}> = async (c, next) => {
const user = c.get("user")
if (!user?.id) {
return c.json({ error: "unauthorized" }, 401) as never
}
const params = (c.req as { valid: (target: "param") => { orgId?: string } }).valid("param")
const organizationIdRaw = params.orgId?.trim()
if (!organizationIdRaw) {
return c.json({ error: "organization_id_required" }, 400) as never
const apiKey = c.get("apiKey")
const scopedOrganizationId = getApiKeyScopedOrganizationId(apiKey)
let organizationId = c.get("activeOrganizationId") ?? null
let organizationSlug = c.get("activeOrganizationSlug") ?? null
if (!organizationId) {
const resolved = await resolveUserOrganizations({
activeOrganizationId: scopedOrganizationId ?? c.get("session")?.activeOrganizationId ?? null,
userId: normalizeDenTypeId("user", user.id),
})
const scopedOrgs = scopedOrganizationId
? resolved.orgs.filter((org) => org.id === scopedOrganizationId)
: resolved.orgs
organizationId = scopedOrganizationId ? scopedOrgs[0]?.id ?? null : resolved.activeOrgId
organizationSlug = scopedOrganizationId ? scopedOrgs[0]?.slug ?? null : resolved.activeOrgSlug
c.set("userOrganizations", scopedOrgs)
c.set("activeOrganizationId", organizationId)
c.set("activeOrganizationSlug", organizationSlug)
}
let organizationId
try {
organizationId = normalizeDenTypeId("organization", organizationIdRaw)
} catch {
if (!organizationId) {
return c.json({ error: "organization_not_found" }, 404) as never
}
const normalizedOrganizationId = normalizeDenTypeId("organization", organizationId)
const context = await getOrganizationContextForUser({
userId: normalizeDenTypeId("user", user.id),
organizationId,
organizationId: normalizedOrganizationId,
})
if (!context) {
return c.json({ error: "organization_not_found" }, 404) as never
}
const apiKey = c.get("apiKey")
if (apiKey && !isScopedApiKeyForOrganization({ apiKey, organizationId })) {
if (apiKey && !isScopedApiKeyForOrganization({ apiKey, organizationId: normalizedOrganizationId })) {
return c.json({
error: "forbidden",
message: "This API key is scoped to a different organization.",
@@ -54,5 +71,7 @@ export const resolveOrganizationContextMiddleware: MiddlewareHandler<{
}
c.set("organizationContext", context)
c.set("activeOrganizationId", context.organization.id)
c.set("activeOrganizationSlug", context.organization.slug)
await next()
}

View File

@@ -30,13 +30,13 @@ export const resolveUserOrganizationsMiddleware: MiddlewareHandler<{
? resolved.orgs.filter((org) => org.id === scopedOrganizationId)
: resolved.orgs
const activeOrganizationId = scopedOrganizationId ? scopedOrgs[0]?.id ?? null : resolved.activeOrgId
const activeOrganizationSlug = scopedOrganizationId
? scopedOrgs[0]?.slug ?? null
: resolved.activeOrgSlug
c.set("userOrganizations", scopedOrgs)
c.set("activeOrganizationId", scopedOrganizationId ? scopedOrgs[0]?.id ?? null : resolved.activeOrgId)
c.set(
"activeOrganizationSlug",
scopedOrganizationId
? scopedOrgs[0]?.slug ?? null
: resolved.activeOrgSlug,
)
c.set("activeOrganizationId", activeOrganizationId)
c.set("activeOrganizationSlug", activeOrganizationSlug)
await next()
}

View File

@@ -12,6 +12,16 @@ This folder owns organization-facing Den API routes.
- `templates.ts`: shared template CRUD
- `shared.ts`: shared route-local helpers, param schemas, and guard helpers
## Active organization model
- `POST /api/auth/organization/set-active` is the only Better Auth endpoint that should switch the user's active org explicitly.
- New sessions should get an initial `activeOrganizationId` from Better Auth session creation hooks in `src/auth.ts`.
- `GET /v1/org` returns the active organization from the current session, including a nested `organization.owner` object plus the current member and team context.
- `POST /v1/org` creates a new organization and switches the session to it. `PATCH /v1/org` updates the active organization.
- Active-org scoped resources should prefer top-level routes like `/v1/templates`, `/v1/skills`, `/v1/teams`, `/v1/roles`, `/v1/api-keys`, `/v1/llm-providers`, and plugin-system `/v1/...` routes. They should not require `:orgId` or `:orgSlug` in the path.
- Routes under `/v1/orgs/**` are reserved for cross-org flows that are not tied to the active workspace yet, such as invitation preview/accept.
- If a client needs to change workspaces, it should call Better Auth set-active first, then use the active-org scoped `/v1/...` resource routes.
## Middleware expectations
- `requireUserMiddleware`: the route requires a signed-in user

View File

@@ -12,7 +12,7 @@ import { jsonValidator, paramValidator, requireUserMiddleware, resolveOrganizati
import { denTypeIdSchema } from "../../openapi.js"
import { auth } from "../../auth.js"
import type { OrgRouteVariables } from "./shared.js"
import { ensureApiKeyManager, idParamSchema, orgIdParamSchema } from "./shared.js"
import { ensureApiKeyManager, idParamSchema } from "./shared.js"
const createOrganizationApiKeySchema = z.object({
name: z.string().trim().min(2).max(64),
@@ -92,12 +92,12 @@ const createOrganizationApiKeyResponseSchema = z.object({
key: z.string().min(1),
}).meta({ ref: "CreateOrganizationApiKeyResponse" })
const apiKeyIdParamSchema = orgIdParamSchema.extend(idParamSchema("apiKeyId").shape)
const apiKeyIdParamSchema = idParamSchema("apiKeyId")
const hideApiKeyGenerationRoute = () => process.env.NODE_ENV === "production"
export function registerOrgApiKeyRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.get(
"/v1/orgs/:orgId/api-keys",
"/v1/api-keys",
describeRoute({
tags: ["API Keys"],
summary: "List organization API keys",
@@ -147,7 +147,6 @@ export function registerOrgApiKeyRoutes<T extends { Variables: OrgRouteVariables
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
async (c) => {
const access = ensureApiKeyManager(c)
@@ -162,7 +161,7 @@ export function registerOrgApiKeyRoutes<T extends { Variables: OrgRouteVariables
)
app.post(
"/v1/orgs/:orgId/api-keys",
"/v1/api-keys",
describeRoute({
tags: ["API Keys"],
summary: "Create an organization API key",
@@ -213,7 +212,6 @@ export function registerOrgApiKeyRoutes<T extends { Variables: OrgRouteVariables
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
jsonValidator(createOrganizationApiKeySchema),
async (c) => {
@@ -259,7 +257,7 @@ export function registerOrgApiKeyRoutes<T extends { Variables: OrgRouteVariables
)
app.delete(
"/v1/orgs/:orgId/api-keys/:apiKeyId",
"/v1/api-keys/:apiKeyId",
describeRoute({
tags: ["API Keys"],
hide: true,

View File

@@ -1,18 +1,19 @@
import { eq } from "@openwork-ee/den-db/drizzle"
import { OrganizationTable } from "@openwork-ee/den-db/schema"
import { normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import { normalizeDenTypeId, type DenTypeId } from "@openwork-ee/utils/typeid"
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { auth } from "../../auth.js"
import { requireCloudWorkerAccess } from "../../billing/polar.js"
import { db } from "../../db.js"
import { env } from "../../env.js"
import { jsonValidator, paramValidator, queryValidator, requireUserMiddleware, resolveMemberTeamsMiddleware, resolveOrganizationContextMiddleware } from "../../middleware/index.js"
import { jsonValidator, queryValidator, requireUserMiddleware, resolveMemberTeamsMiddleware, resolveOrganizationContextMiddleware } from "../../middleware/index.js"
import { denTypeIdSchema, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, unauthorizedSchema } from "../../openapi.js"
import { acceptInvitationForUser, createOrganizationForUser, getInvitationPreview, setSessionActiveOrganization, updateOrganizationName } from "../../orgs.js"
import { getRequiredUserEmail } from "../../user.js"
import type { OrgRouteVariables } from "./shared.js"
import { ensureOwner, orgIdParamSchema } from "./shared.js"
import { ensureOwner } from "./shared.js"
const createOrganizationSchema = z.object({
name: z.string().trim().min(2).max(120),
@@ -34,6 +35,14 @@ const organizationResponseSchema = z.object({
organization: z.object({}).passthrough().nullable(),
}).meta({ ref: "OrganizationResponse" })
const organizationOwnerSchema = z.object({
memberId: denTypeIdSchema("member"),
userId: denTypeIdSchema("user"),
name: z.string().nullable(),
email: z.string().email().nullable(),
image: z.string().nullable().optional(),
}).meta({ ref: "OrganizationOwner" })
const paymentRequiredSchema = z.object({
error: z.literal("payment_required"),
message: z.string(),
@@ -54,6 +63,10 @@ const invitationAcceptedResponseSchema = z.object({
}).meta({ ref: "InvitationAcceptedResponse" })
const organizationContextResponseSchema = z.object({
organization: z.object({
owner: organizationOwnerSchema.nullable().optional(),
}).passthrough(),
currentMember: z.object({}).passthrough(),
currentMemberTeams: z.array(z.object({}).passthrough()),
}).passthrough().meta({ ref: "OrganizationContextResponse" })
@@ -73,9 +86,30 @@ function getStoredSessionId(session: { id?: string | null } | null) {
}
}
async function setRequestActiveOrganization(
c: {
get: (key: "session") => { id?: string | null } | null
req: { raw: Request }
},
organizationId: DenTypeId<"organization"> | null,
) {
try {
await auth.api.setActiveOrganization({
body: { organizationId },
headers: c.req.raw.headers,
})
return
} catch {}
const sessionId = getStoredSessionId(c.get("session"))
if (sessionId) {
await setSessionActiveOrganization(sessionId, organizationId)
}
}
export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.post(
"/v1/orgs",
"/v1/org",
describeRoute({
tags: ["Organizations"],
hide: true,
@@ -100,7 +134,6 @@ export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }
}
const user = c.get("user")
const session = c.get("session")
const input = c.req.valid("json")
const email = getRequiredUserEmail(user)
@@ -131,10 +164,7 @@ export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }
name: input.name,
})
const sessionId = getStoredSessionId(session)
if (sessionId) {
await setSessionActiveOrganization(sessionId, organizationId)
}
await setRequestActiveOrganization(c, organizationId)
const organization = await db
.select()
@@ -196,7 +226,6 @@ export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }
}
const user = c.get("user")
const session = c.get("session")
const input = c.req.valid("json")
const email = getRequiredUserEmail(user)
@@ -214,10 +243,7 @@ export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }
return c.json({ error: "invitation_not_found" }, 404)
}
const sessionId = getStoredSessionId(session)
if (sessionId) {
await setSessionActiveOrganization(sessionId, accepted.member.organizationId)
}
await setRequestActiveOrganization(c, accepted.member.organizationId)
const orgRows = await db
.select({ slug: OrganizationTable.slug })
@@ -235,7 +261,7 @@ export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }
)
app.patch(
"/v1/orgs/:orgId",
"/v1/org",
describeRoute({
tags: ["Organizations"],
summary: "Update organization",
@@ -249,7 +275,6 @@ export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
jsonValidator(updateOrganizationSchema),
async (c) => {
@@ -275,25 +300,38 @@ export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }
)
app.get(
"/v1/orgs/:orgId/context",
"/v1/org",
describeRoute({
tags: ["Organizations"],
summary: "Get organization context",
description: "Returns the resolved organization context for a specific org, including the current member record and their team memberships.",
summary: "Get active organization",
description: "Returns the active organization from the current session, including its owner, the current member record, and their team memberships.",
responses: {
200: jsonResponse("Organization context returned successfully.", organizationContextResponseSchema),
400: jsonResponse("The organization context path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to load organization context.", unauthorizedSchema),
404: jsonResponse("The organization could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
resolveMemberTeamsMiddleware,
(c) => {
const payload = c.get("organizationContext")
const owner = payload.members.find((member: typeof payload.members[number]) => member.isOwner) ?? null
return c.json({
...c.get("organizationContext"),
...payload,
organization: {
...payload.organization,
owner: owner
? {
memberId: owner.id,
userId: owner.user.id,
name: owner.user.name,
email: owner.user.email,
image: owner.user.image,
}
: null,
},
currentMemberTeams: c.get("memberTeams") ?? [],
})
},

View File

@@ -11,7 +11,7 @@ import { denTypeIdSchema, forbiddenSchema, invalidRequestSchema, jsonResponse, n
import { getOrganizationLimitStatus } from "../../organization-limits.js"
import { listAssignableRoles } from "../../orgs.js"
import type { OrgRouteVariables } from "./shared.js"
import { buildInvitationLink, createInvitationId, ensureInviteManager, idParamSchema, normalizeRoleName, orgIdParamSchema } from "./shared.js"
import { buildInvitationLink, createInvitationId, ensureInviteManager, idParamSchema, normalizeRoleName } from "./shared.js"
const inviteMemberSchema = z.object({
email: z.string().email(),
@@ -33,11 +33,11 @@ const invitationEmailFailedSchema = z.object({
}).meta({ ref: "InvitationEmailFailedError" })
type InvitationId = typeof InvitationTable.$inferSelect.id
const orgInvitationParamsSchema = orgIdParamSchema.extend(idParamSchema("invitationId", "invitation").shape)
const orgInvitationParamsSchema = idParamSchema("invitationId", "invitation")
export function registerOrgInvitationRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.post(
"/v1/orgs/:orgId/invitations",
"/v1/invitations",
describeRoute({
tags: ["Invitations"],
summary: "Create organization invitation",
@@ -53,7 +53,6 @@ export function registerOrgInvitationRoutes<T extends { Variables: OrgRouteVaria
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
jsonValidator(inviteMemberSchema),
async (c) => {
@@ -173,7 +172,7 @@ export function registerOrgInvitationRoutes<T extends { Variables: OrgRouteVaria
)
app.post(
"/v1/orgs/:orgId/invitations/:invitationId/cancel",
"/v1/invitations/:invitationId/cancel",
describeRoute({
tags: ["Invitations"],
summary: "Cancel organization invitation",

View File

@@ -23,7 +23,7 @@ import { getModelsDevProvider, listModelsDevProviders } from "../../llm/models-d
import type { MemberTeamsContext } from "../../middleware/member-teams.js"
import { denTypeIdSchema, emptyResponse, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, unauthorizedSchema } from "../../openapi.js"
import type { OrgRouteVariables } from "./shared.js"
import { idParamSchema, memberHasRole, orgIdParamSchema } from "./shared.js"
import { idParamSchema, memberHasRole } from "./shared.js"
type JsonRecord = Record<string, unknown>
type LlmProviderId = typeof LlmProviderTable.$inferSelect.id
@@ -38,11 +38,11 @@ type RouteFailure = {
message?: string
}
const providerCatalogParamsSchema = orgIdParamSchema.extend({
const providerCatalogParamsSchema = z.object({
providerId: z.string().trim().min(1).max(255),
})
const orgLlmProviderParamsSchema = orgIdParamSchema.extend(idParamSchema("llmProviderId", "llmProvider").shape)
const orgLlmProviderParamsSchema = idParamSchema("llmProviderId", "llmProvider")
const customModelSchema = z.object({
id: z.string().trim().min(1).max(255),
@@ -480,7 +480,7 @@ async function loadLlmProviders(input: {
export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVariables & Partial<MemberTeamsContext> }>(app: Hono<T>) {
app.get(
"/v1/orgs/:orgId/llm-provider-catalog",
"/v1/llm-provider-catalog",
describeRoute({
tags: ["LLM Providers"],
summary: "List LLM provider catalog",
@@ -493,7 +493,6 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
async (c) => {
try {
@@ -509,7 +508,7 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
)
app.get(
"/v1/orgs/:orgId/llm-provider-catalog/:providerId",
"/v1/llm-provider-catalog/:providerId",
describeRoute({
tags: ["LLM Providers"],
summary: "Get LLM provider catalog entry",
@@ -556,7 +555,7 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
)
app.get(
"/v1/orgs/:orgId/llm-providers",
"/v1/llm-providers",
describeRoute({
tags: ["LLM Providers"],
summary: "List organization LLM providers",
@@ -568,7 +567,6 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
resolveMemberTeamsMiddleware,
async (c) => {
@@ -592,7 +590,7 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
)
app.get(
"/v1/orgs/:orgId/llm-providers/:llmProviderId/connect",
"/v1/llm-providers/:llmProviderId/connect",
describeRoute({
tags: ["LLM Providers"],
summary: "Get LLM provider connect payload",
@@ -669,7 +667,7 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
)
app.post(
"/v1/orgs/:orgId/llm-providers",
"/v1/llm-providers",
describeRoute({
tags: ["LLM Providers"],
summary: "Create organization LLM provider",
@@ -682,7 +680,6 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
jsonValidator(llmProviderWriteSchema),
async (c) => {
@@ -781,7 +778,7 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
)
app.patch(
"/v1/orgs/:orgId/llm-providers/:llmProviderId",
"/v1/llm-providers/:llmProviderId",
describeRoute({
tags: ["LLM Providers"],
summary: "Update organization LLM provider",
@@ -917,7 +914,7 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
)
app.delete(
"/v1/orgs/:orgId/llm-providers/:llmProviderId",
"/v1/llm-providers/:llmProviderId",
describeRoute({
tags: ["LLM Providers"],
summary: "Delete organization LLM provider",
@@ -973,7 +970,7 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
)
app.delete(
"/v1/orgs/:orgId/llm-providers/:llmProviderId/access/:accessId",
"/v1/llm-providers/:llmProviderId/access/:accessId",
describeRoute({
tags: ["LLM Providers"],
summary: "Remove LLM provider access grant",

View File

@@ -9,18 +9,18 @@ import { jsonValidator, paramValidator, requireUserMiddleware, resolveOrganizati
import { emptyResponse, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, successSchema, unauthorizedSchema } from "../../openapi.js"
import { listAssignableRoles, removeOrganizationMember, roleIncludesOwner } from "../../orgs.js"
import type { OrgRouteVariables } from "./shared.js"
import { ensureOwner, idParamSchema, normalizeRoleName, orgIdParamSchema } from "./shared.js"
import { ensureOwner, idParamSchema, normalizeRoleName } from "./shared.js"
const updateMemberRoleSchema = z.object({
role: z.string().trim().min(1).max(64),
})
type MemberId = typeof MemberTable.$inferSelect.id
const orgMemberParamsSchema = orgIdParamSchema.extend(idParamSchema("memberId", "member").shape)
const orgMemberParamsSchema = idParamSchema("memberId", "member")
export function registerOrgMemberRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.post(
"/v1/orgs/:orgId/members/:memberId/role",
"/v1/members/:memberId/role",
describeRoute({
tags: ["Members"],
summary: "Update member role",
@@ -81,7 +81,7 @@ export function registerOrgMemberRoutes<T extends { Variables: OrgRouteVariables
)
app.delete(
"/v1/orgs/:orgId/members/:memberId",
"/v1/members/:memberId",
describeRoute({
tags: ["Members"],
summary: "Remove organization member",

View File

@@ -87,7 +87,6 @@ import {
pluginUpdateSchema,
resourceAccessGrantWriteSchema,
} from "./schemas.js"
import { orgIdParamSchema } from "../shared.js"
type EndpointMethod = "DELETE" | "GET" | "PATCH" | "POST"
type EndpointAudience = "admin" | "public_webhook"
@@ -120,7 +119,7 @@ type DeferredEndpointContract = {
tag: EndpointTag
}
const orgBasePath = "/v1/orgs/:orgId"
const orgBasePath = "/v1"
export const pluginArchRoutePaths = {
configObjects: `${orgBasePath}/config-objects`,
@@ -191,7 +190,7 @@ export const pluginArchEndpointContracts: Record<string, EndpointContract> = {
description: "List current config object projections with search and connector filters.",
method: "GET",
path: pluginArchRoutePaths.configObjects,
request: { params: orgIdParamSchema, query: configObjectListQuerySchema },
request: { query: configObjectListQuerySchema },
response: { description: "Current config object rows.", schema: configObjectListResponseSchema, status: 200 },
tag: "Config Objects",
},
@@ -209,7 +208,7 @@ export const pluginArchEndpointContracts: Record<string, EndpointContract> = {
description: "Create a cloud or imported config object and optionally attach it to plugins.",
method: "POST",
path: pluginArchRoutePaths.configObjects,
request: { body: configObjectCreateSchema, params: orgIdParamSchema },
request: { body: configObjectCreateSchema },
response: { description: "Config object created successfully.", schema: configObjectMutationResponseSchema, status: 201 },
tag: "Config Objects",
},
@@ -335,7 +334,7 @@ export const pluginArchEndpointContracts: Record<string, EndpointContract> = {
description: "List accessible plugins for the organization.",
method: "GET",
path: pluginArchRoutePaths.plugins,
request: { params: orgIdParamSchema, query: pluginListQuerySchema },
request: { query: pluginListQuerySchema },
response: { description: "Plugin list.", schema: pluginListResponseSchema, status: 200 },
tag: "Plugins",
},
@@ -353,7 +352,7 @@ export const pluginArchEndpointContracts: Record<string, EndpointContract> = {
description: "Create a private-by-default plugin.",
method: "POST",
path: pluginArchRoutePaths.plugins,
request: { body: pluginCreateSchema, params: orgIdParamSchema },
request: { body: pluginCreateSchema },
response: { description: "Plugin created successfully.", schema: pluginMutationResponseSchema, status: 201 },
tag: "Plugins",
},
@@ -452,7 +451,7 @@ export const pluginArchEndpointContracts: Record<string, EndpointContract> = {
description: "List accessible marketplaces for the organization.",
method: "GET",
path: pluginArchRoutePaths.marketplaces,
request: { params: orgIdParamSchema, query: marketplaceListQuerySchema },
request: { query: marketplaceListQuerySchema },
response: { description: "Marketplace list.", schema: marketplaceListResponseSchema, status: 200 },
tag: "Marketplaces",
},
@@ -470,7 +469,7 @@ export const pluginArchEndpointContracts: Record<string, EndpointContract> = {
description: "Create a private-by-default marketplace.",
method: "POST",
path: pluginArchRoutePaths.marketplaces,
request: { body: marketplaceCreateSchema, params: orgIdParamSchema },
request: { body: marketplaceCreateSchema },
response: { description: "Marketplace created successfully.", schema: marketplaceMutationResponseSchema, status: 201 },
tag: "Marketplaces",
},
@@ -560,7 +559,7 @@ export const pluginArchEndpointContracts: Record<string, EndpointContract> = {
description: "List connector accounts such as GitHub App installations available to the org.",
method: "GET",
path: pluginArchRoutePaths.connectorAccounts,
request: { params: orgIdParamSchema, query: connectorAccountListQuerySchema },
request: { query: connectorAccountListQuerySchema },
response: { description: "Connector account list.", schema: connectorAccountListResponseSchema, status: 200 },
tag: "Connectors",
},
@@ -569,7 +568,7 @@ export const pluginArchEndpointContracts: Record<string, EndpointContract> = {
description: "Create a reusable connector account record.",
method: "POST",
path: pluginArchRoutePaths.connectorAccounts,
request: { body: connectorAccountCreateSchema, params: orgIdParamSchema },
request: { body: connectorAccountCreateSchema },
response: { description: "Connector account created successfully.", schema: connectorAccountMutationResponseSchema, status: 201 },
tag: "Connectors",
},
@@ -596,7 +595,7 @@ export const pluginArchEndpointContracts: Record<string, EndpointContract> = {
description: "List configured connector instances for the org.",
method: "GET",
path: pluginArchRoutePaths.connectorInstances,
request: { params: orgIdParamSchema, query: connectorInstanceListQuerySchema },
request: { query: connectorInstanceListQuerySchema },
response: { description: "Connector instance list.", schema: connectorInstanceListResponseSchema, status: 200 },
tag: "Connectors",
},
@@ -605,7 +604,7 @@ export const pluginArchEndpointContracts: Record<string, EndpointContract> = {
description: "Create a connector instance backed by one connector account.",
method: "POST",
path: pluginArchRoutePaths.connectorInstances,
request: { body: connectorInstanceCreateSchema, params: orgIdParamSchema },
request: { body: connectorInstanceCreateSchema },
response: { description: "Connector instance created successfully.", schema: connectorInstanceMutationResponseSchema, status: 201 },
tag: "Connectors",
},
@@ -767,7 +766,7 @@ export const pluginArchEndpointContracts: Record<string, EndpointContract> = {
description: "List connector sync events for inspection and debugging.",
method: "GET",
path: pluginArchRoutePaths.connectorSyncEvents,
request: { params: orgIdParamSchema, query: connectorSyncEventListQuerySchema },
request: { query: connectorSyncEventListQuerySchema },
response: { description: "Connector sync event list.", schema: connectorSyncEventListResponseSchema, status: 200 },
tag: "Connectors",
},
@@ -794,7 +793,7 @@ export const pluginArchEndpointContracts: Record<string, EndpointContract> = {
description: "Create the GitHub connector account, instance, target, and initial mappings in one setup flow.",
method: "POST",
path: pluginArchRoutePaths.githubSetup,
request: { body: githubConnectorSetupSchema, params: orgIdParamSchema },
request: { body: githubConnectorSetupSchema },
response: { description: "GitHub connector setup created successfully.", schema: githubSetupResponseSchema, status: 201 },
tag: "GitHub",
},
@@ -803,7 +802,7 @@ export const pluginArchEndpointContracts: Record<string, EndpointContract> = {
description: "Persist a GitHub App installation as a reusable connector account.",
method: "POST",
path: pluginArchRoutePaths.githubAccounts,
request: { body: githubConnectorAccountCreateSchema, params: orgIdParamSchema },
request: { body: githubConnectorAccountCreateSchema },
response: { description: "GitHub connector account created successfully.", schema: connectorAccountMutationResponseSchema, status: 201 },
tag: "GitHub",
},
@@ -821,7 +820,7 @@ export const pluginArchEndpointContracts: Record<string, EndpointContract> = {
description: "Validate one GitHub repository-branch target before persisting it.",
method: "POST",
path: pluginArchRoutePaths.githubValidateTarget,
request: { body: githubValidateTargetSchema, params: orgIdParamSchema },
request: { body: githubValidateTargetSchema },
response: { description: "GitHub target validation result.", schema: githubValidateTargetResponseSchema, status: 200 },
tag: "GitHub",
},

View File

@@ -200,7 +200,6 @@ export function registerPluginArchRoutes<T extends { Variables: OrgRouteVariable
app,
"get",
pluginArchRoutePaths.configObjects,
paramValidator(configObjectParamsSchema.pick({ orgId: true })),
queryValidator(configObjectListQuerySchema),
describeRoute({
tags: ["Config Objects"],
@@ -233,7 +232,6 @@ export function registerPluginArchRoutes<T extends { Variables: OrgRouteVariable
app,
"post",
pluginArchRoutePaths.configObjects,
paramValidator(configObjectParamsSchema.pick({ orgId: true })),
jsonValidator(configObjectCreateSchema),
describeRoute({
tags: ["Config Objects"],
@@ -549,7 +547,6 @@ export function registerPluginArchRoutes<T extends { Variables: OrgRouteVariable
})
withPluginArchOrgContext(app, "get", pluginArchRoutePaths.plugins,
paramValidator(pluginParamsSchema.pick({ orgId: true })),
queryValidator(pluginListQuerySchema),
describeRoute({
tags: ["Plugins"],
@@ -567,7 +564,6 @@ export function registerPluginArchRoutes<T extends { Variables: OrgRouteVariable
})
withPluginArchOrgContext(app, "post", pluginArchRoutePaths.plugins,
paramValidator(pluginParamsSchema.pick({ orgId: true })),
jsonValidator(pluginCreateSchema),
describeRoute({
tags: ["Plugins"],
@@ -828,7 +824,6 @@ export function registerPluginArchRoutes<T extends { Variables: OrgRouteVariable
})
withPluginArchOrgContext(app, "get", pluginArchRoutePaths.marketplaces,
paramValidator(marketplaceParamsSchema.pick({ orgId: true })),
queryValidator(marketplaceListQuerySchema),
describeRoute({
tags: ["Marketplaces"],
@@ -846,7 +841,6 @@ export function registerPluginArchRoutes<T extends { Variables: OrgRouteVariable
})
withPluginArchOrgContext(app, "post", pluginArchRoutePaths.marketplaces,
paramValidator(marketplaceParamsSchema.pick({ orgId: true })),
jsonValidator(marketplaceCreateSchema),
describeRoute({
tags: ["Marketplaces"],
@@ -1085,7 +1079,6 @@ export function registerPluginArchRoutes<T extends { Variables: OrgRouteVariable
})
withPluginArchOrgContext(app, "get", pluginArchRoutePaths.connectorAccounts,
paramValidator(connectorAccountParamsSchema.pick({ orgId: true })),
queryValidator(connectorAccountListQuerySchema),
describeRoute({
tags: ["Connectors"],
@@ -1103,7 +1096,6 @@ export function registerPluginArchRoutes<T extends { Variables: OrgRouteVariable
})
withPluginArchOrgContext(app, "post", pluginArchRoutePaths.connectorAccounts,
paramValidator(connectorAccountParamsSchema.pick({ orgId: true })),
jsonValidator(connectorAccountCreateSchema),
describeRoute({
tags: ["Connectors"],
@@ -1176,7 +1168,6 @@ export function registerPluginArchRoutes<T extends { Variables: OrgRouteVariable
})
withPluginArchOrgContext(app, "get", pluginArchRoutePaths.connectorInstances,
paramValidator(connectorInstanceParamsSchema.pick({ orgId: true })),
queryValidator(connectorInstanceListQuerySchema),
describeRoute({
tags: ["Connectors"],
@@ -1194,7 +1185,6 @@ export function registerPluginArchRoutes<T extends { Variables: OrgRouteVariable
})
withPluginArchOrgContext(app, "post", pluginArchRoutePaths.connectorInstances,
paramValidator(connectorInstanceParamsSchema.pick({ orgId: true })),
jsonValidator(connectorInstanceCreateSchema),
describeRoute({
tags: ["Connectors"],
@@ -1577,7 +1567,6 @@ export function registerPluginArchRoutes<T extends { Variables: OrgRouteVariable
})
withPluginArchOrgContext(app, "get", pluginArchRoutePaths.connectorSyncEvents,
paramValidator(connectorSyncEventParamsSchema.pick({ orgId: true })),
queryValidator(connectorSyncEventListQuerySchema),
describeRoute({
tags: ["Connectors"],
@@ -1639,7 +1628,6 @@ export function registerPluginArchRoutes<T extends { Variables: OrgRouteVariable
})
withPluginArchOrgContext(app, "post", pluginArchRoutePaths.githubAccounts,
paramValidator(connectorAccountParamsSchema.pick({ orgId: true })),
jsonValidator(githubConnectorAccountCreateSchema),
describeRoute({
tags: ["GitHub"],
@@ -1664,7 +1652,6 @@ export function registerPluginArchRoutes<T extends { Variables: OrgRouteVariable
})
withPluginArchOrgContext(app, "post", pluginArchRoutePaths.githubSetup,
paramValidator(connectorAccountParamsSchema.pick({ orgId: true })),
jsonValidator(githubConnectorSetupSchema),
describeRoute({
tags: ["GitHub"],
@@ -1713,7 +1700,6 @@ export function registerPluginArchRoutes<T extends { Variables: OrgRouteVariable
})
withPluginArchOrgContext(app, "post", pluginArchRoutePaths.githubValidateTarget,
paramValidator(connectorAccountParamsSchema.pick({ orgId: true })),
jsonValidator(githubValidateTargetSchema),
describeRoute({
tags: ["GitHub"],

View File

@@ -17,7 +17,7 @@ import {
} from "@openwork-ee/den-db/schema"
import { z } from "zod"
import { denTypeIdSchema } from "../../../openapi.js"
import { idParamSchema, orgIdParamSchema } from "../shared.js"
import { idParamSchema } from "../shared.js"
const cursorSchema = z.string().trim().min(1).max(255)
const jsonObjectSchema = z.object({}).passthrough()
@@ -132,21 +132,21 @@ export const githubRepositoryListQuerySchema = pluginArchPaginationQuerySchema.e
q: z.string().trim().min(1).max(255).optional(),
})
export const configObjectParamsSchema = orgIdParamSchema.extend(idParamSchema("configObjectId", "configObject").shape)
export const configObjectParamsSchema = idParamSchema("configObjectId", "configObject")
export const configObjectVersionParamsSchema = configObjectParamsSchema.extend(idParamSchema("versionId", "configObjectVersion").shape)
export const configObjectAccessGrantParamsSchema = configObjectParamsSchema.extend(idParamSchema("grantId", "configObjectAccessGrant").shape)
export const pluginParamsSchema = orgIdParamSchema.extend(idParamSchema("pluginId", "plugin").shape)
export const pluginParamsSchema = idParamSchema("pluginId", "plugin")
export const pluginConfigObjectParamsSchema = pluginParamsSchema.extend(idParamSchema("configObjectId", "configObject").shape)
export const pluginAccessGrantParamsSchema = pluginParamsSchema.extend(idParamSchema("grantId", "pluginAccessGrant").shape)
export const marketplaceParamsSchema = orgIdParamSchema.extend(idParamSchema("marketplaceId", "marketplace").shape)
export const marketplaceParamsSchema = idParamSchema("marketplaceId", "marketplace")
export const marketplacePluginParamsSchema = marketplaceParamsSchema.extend(idParamSchema("pluginId", "plugin").shape)
export const marketplaceAccessGrantParamsSchema = marketplaceParamsSchema.extend(idParamSchema("grantId", "marketplaceAccessGrant").shape)
export const connectorAccountParamsSchema = orgIdParamSchema.extend(idParamSchema("connectorAccountId", "connectorAccount").shape)
export const connectorInstanceParamsSchema = orgIdParamSchema.extend(idParamSchema("connectorInstanceId", "connectorInstance").shape)
export const connectorAccountParamsSchema = idParamSchema("connectorAccountId", "connectorAccount")
export const connectorInstanceParamsSchema = idParamSchema("connectorInstanceId", "connectorInstance")
export const connectorInstanceAccessGrantParamsSchema = connectorInstanceParamsSchema.extend(idParamSchema("grantId", "connectorInstanceAccessGrant").shape)
export const connectorTargetParamsSchema = orgIdParamSchema.extend(idParamSchema("connectorTargetId", "connectorTarget").shape)
export const connectorMappingParamsSchema = orgIdParamSchema.extend(idParamSchema("connectorMappingId", "connectorMapping").shape)
export const connectorSyncEventParamsSchema = orgIdParamSchema.extend(idParamSchema("connectorSyncEventId", "connectorSyncEvent").shape)
export const connectorTargetParamsSchema = idParamSchema("connectorTargetId", "connectorTarget")
export const connectorMappingParamsSchema = idParamSchema("connectorMappingId", "connectorMapping")
export const connectorSyncEventParamsSchema = idParamSchema("connectorSyncEventId", "connectorSyncEvent")
export const connectorAccountRepositoryParamsSchema = connectorAccountParamsSchema

View File

@@ -9,7 +9,7 @@ import { jsonValidator, paramValidator, requireUserMiddleware, resolveOrganizati
import { emptyResponse, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, successSchema, unauthorizedSchema } from "../../openapi.js"
import { serializePermissionRecord } from "../../orgs.js"
import type { OrgRouteVariables } from "./shared.js"
import { createRoleId, ensureOwner, idParamSchema, normalizeRoleName, orgIdParamSchema, replaceRoleValue, splitRoles } from "./shared.js"
import { createRoleId, ensureOwner, idParamSchema, normalizeRoleName, replaceRoleValue, splitRoles } from "./shared.js"
const permissionSchema = z.record(z.string(), z.array(z.string()))
@@ -24,11 +24,11 @@ const updateRoleSchema = z.object({
})
type OrganizationRoleId = typeof OrganizationRoleTable.$inferSelect.id
const orgRoleParamsSchema = orgIdParamSchema.extend(idParamSchema("roleId", "organizationRole").shape)
const orgRoleParamsSchema = idParamSchema("roleId", "organizationRole")
export function registerOrgRoleRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.post(
"/v1/orgs/:orgId/roles",
"/v1/roles",
describeRoute({
tags: ["Roles"],
summary: "Create organization role",
@@ -42,7 +42,6 @@ export function registerOrgRoleRoutes<T extends { Variables: OrgRouteVariables }
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
jsonValidator(createRoleSchema),
async (c) => {
@@ -81,7 +80,7 @@ export function registerOrgRoleRoutes<T extends { Variables: OrgRouteVariables }
)
app.patch(
"/v1/orgs/:orgId/roles/:roleId",
"/v1/roles/:roleId",
describeRoute({
tags: ["Roles"],
summary: "Update organization role",
@@ -188,7 +187,7 @@ export function registerOrgRoleRoutes<T extends { Variables: OrgRouteVariables }
)
app.delete(
"/v1/orgs/:orgId/roles/:roleId",
"/v1/roles/:roleId",
describeRoute({
tags: ["Roles"],
summary: "Delete organization role",

View File

@@ -11,10 +11,6 @@ export type OrgRouteVariables =
& Partial<OrganizationContextVariables>
& Partial<MemberTeamsContext>
export const orgIdParamSchema = z.object({
orgId: denTypeIdSchema("organization"),
})
export function idParamSchema<K extends string>(key: K, typeName?: DenTypeIdName) {
if (!typeName) {
return z.object({

View File

@@ -24,7 +24,7 @@ import {
import type { MemberTeamsContext } from "../../middleware/member-teams.js"
import { denTypeIdSchema, emptyResponse, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, successSchema, unauthorizedSchema } from "../../openapi.js"
import type { OrgRouteVariables } from "./shared.js"
import { idParamSchema, memberHasRole, orgIdParamSchema } from "./shared.js"
import { idParamSchema, memberHasRole } from "./shared.js"
const skillTextSchema = z.string().superRefine((value, ctx) => {
if (!value.trim()) {
@@ -105,8 +105,8 @@ type MemberId = typeof MemberTable.$inferSelect.id
type SkillRow = typeof SkillTable.$inferSelect
type SkillHubRow = typeof SkillHubTable.$inferSelect
const orgSkillHubParamsSchema = orgIdParamSchema.extend(idParamSchema("skillHubId", "skillHub").shape)
const orgSkillParamsSchema = orgIdParamSchema.extend(idParamSchema("skillId", "skill").shape)
const orgSkillHubParamsSchema = idParamSchema("skillHubId", "skillHub")
const orgSkillParamsSchema = idParamSchema("skillId", "skill")
const orgSkillHubSkillParamsSchema = orgSkillHubParamsSchema.extend(idParamSchema("skillId", "skill").shape)
const orgSkillHubAccessParamsSchema = orgSkillHubParamsSchema.extend(idParamSchema("accessId", "skillHubMember").shape)
@@ -263,7 +263,7 @@ function canViewSkill(input: {
export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables & Partial<MemberTeamsContext> }>(app: Hono<T>) {
app.post(
"/v1/orgs/:orgId/skills",
"/v1/skills",
describeRoute({
tags: ["Skills"],
summary: "Create skill",
@@ -275,7 +275,6 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
jsonValidator(createSkillSchema),
async (c) => {
@@ -314,7 +313,7 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
)
app.get(
"/v1/orgs/:orgId/skills",
"/v1/skills",
describeRoute({
tags: ["Skills"],
summary: "List skills",
@@ -326,7 +325,6 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
resolveMemberTeamsMiddleware,
async (c) => {
@@ -360,7 +358,7 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
)
app.delete(
"/v1/orgs/:orgId/skills/:skillId",
"/v1/skills/:skillId",
describeRoute({
tags: ["Skills"],
summary: "Delete skill",
@@ -412,7 +410,7 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
)
app.patch(
"/v1/orgs/:orgId/skills/:skillId",
"/v1/skills/:skillId",
describeRoute({
tags: ["Skills"],
summary: "Update skill",
@@ -486,7 +484,7 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
)
app.post(
"/v1/orgs/:orgId/skill-hubs",
"/v1/skill-hubs",
describeRoute({
tags: ["Skill Hubs"],
summary: "Create skill hub",
@@ -498,7 +496,6 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
jsonValidator(createSkillHubSchema),
async (c) => {
@@ -542,7 +539,7 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
)
app.get(
"/v1/orgs/:orgId/skill-hubs",
"/v1/skill-hubs",
describeRoute({
tags: ["Skill Hubs"],
summary: "List skill hubs",
@@ -554,7 +551,6 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
resolveMemberTeamsMiddleware,
async (c) => {
@@ -698,7 +694,7 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
)
app.patch(
"/v1/orgs/:orgId/skill-hubs/:skillHubId",
"/v1/skill-hubs/:skillHubId",
describeRoute({
tags: ["Skill Hubs"],
summary: "Update skill hub",
@@ -767,7 +763,7 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
)
app.delete(
"/v1/orgs/:orgId/skill-hubs/:skillHubId",
"/v1/skill-hubs/:skillHubId",
describeRoute({
tags: ["Skill Hubs"],
summary: "Delete skill hub",
@@ -820,7 +816,7 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
)
app.post(
"/v1/orgs/:orgId/skill-hubs/:skillHubId/skills",
"/v1/skill-hubs/:skillHubId/skills",
describeRoute({
tags: ["Skill Hubs"],
summary: "Add skill to skill hub",
@@ -908,7 +904,7 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
)
app.delete(
"/v1/orgs/:orgId/skill-hubs/:skillHubId/skills/:skillId",
"/v1/skill-hubs/:skillHubId/skills/:skillId",
describeRoute({
tags: ["Skill Hubs"],
summary: "Remove skill from skill hub",
@@ -971,7 +967,7 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
)
app.post(
"/v1/orgs/:orgId/skill-hubs/:skillHubId/access",
"/v1/skill-hubs/:skillHubId/access",
describeRoute({
tags: ["Skill Hubs"],
summary: "Grant skill hub access",
@@ -1082,7 +1078,7 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
)
app.delete(
"/v1/orgs/:orgId/skill-hubs/:skillHubId/access/:accessId",
"/v1/skill-hubs/:skillHubId/access/:accessId",
describeRoute({
tags: ["Skill Hubs"],
summary: "Revoke skill hub access",

View File

@@ -21,7 +21,6 @@ import type { OrgRouteVariables } from "./shared.js"
import {
ensureTeamManager,
idParamSchema,
orgIdParamSchema,
} from "./shared.js"
const createTeamSchema = z.object({
@@ -45,7 +44,7 @@ const updateTeamSchema = z.object({
type TeamId = typeof TeamTable.$inferSelect.id
type MemberId = typeof MemberTable.$inferSelect.id
const orgTeamParamsSchema = orgIdParamSchema.extend(idParamSchema("teamId", "team").shape)
const orgTeamParamsSchema = idParamSchema("teamId", "team")
const teamResponseSchema = z.object({
team: z.object({
@@ -85,7 +84,7 @@ async function ensureMembersBelongToOrganization(input: {
export function registerOrgTeamRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.post(
"/v1/orgs/:orgId/teams",
"/v1/teams",
describeRoute({
tags: ["Teams"],
summary: "Create team",
@@ -99,7 +98,6 @@ export function registerOrgTeamRoutes<T extends { Variables: OrgRouteVariables }
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
jsonValidator(createTeamSchema),
async (c) => {
@@ -174,7 +172,7 @@ export function registerOrgTeamRoutes<T extends { Variables: OrgRouteVariables }
)
app.patch(
"/v1/orgs/:orgId/teams/:teamId",
"/v1/teams/:teamId",
describeRoute({
tags: ["Teams"],
summary: "Update team",
@@ -278,7 +276,7 @@ export function registerOrgTeamRoutes<T extends { Variables: OrgRouteVariables }
)
app.delete(
"/v1/orgs/:orgId/teams/:teamId",
"/v1/teams/:teamId",
describeRoute({
tags: ["Teams"],
summary: "Delete team",

View File

@@ -8,7 +8,7 @@ import { db } from "../../db.js"
import { jsonValidator, paramValidator, requireUserMiddleware, resolveOrganizationContextMiddleware } from "../../middleware/index.js"
import { denTypeIdSchema, emptyResponse, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, unauthorizedSchema } from "../../openapi.js"
import type { OrgRouteVariables } from "./shared.js"
import { idParamSchema, orgIdParamSchema, parseTemplateJson } from "./shared.js"
import { idParamSchema, parseTemplateJson } from "./shared.js"
const createTemplateSchema = z.object({
name: z.string().trim().min(1).max(255),
@@ -41,11 +41,11 @@ const templateListResponseSchema = z.object({
}).meta({ ref: "TemplateListResponse" })
type TemplateSharingId = typeof TempTemplateSharingTable.$inferSelect.id
const orgTemplateParamsSchema = orgIdParamSchema.extend(idParamSchema("templateId", "tempTemplateSharing").shape)
const orgTemplateParamsSchema = idParamSchema("templateId", "tempTemplateSharing")
export function registerOrgTemplateRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.post(
"/v1/orgs/:orgId/templates",
"/v1/templates",
describeRoute({
tags: ["Templates"],
summary: "Create shared template",
@@ -58,7 +58,6 @@ export function registerOrgTemplateRoutes<T extends { Variables: OrgRouteVariabl
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
jsonValidator(createTemplateSchema),
async (c) => {
@@ -101,7 +100,7 @@ export function registerOrgTemplateRoutes<T extends { Variables: OrgRouteVariabl
)
app.get(
"/v1/orgs/:orgId/templates",
"/v1/templates",
describeRoute({
tags: ["Templates"],
summary: "List shared templates",
@@ -114,7 +113,6 @@ export function registerOrgTemplateRoutes<T extends { Variables: OrgRouteVariabl
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
async (c) => {
const payload = c.get("organizationContext")
@@ -168,7 +166,7 @@ export function registerOrgTemplateRoutes<T extends { Variables: OrgRouteVariabl
)
app.delete(
"/v1/orgs/:orgId/templates/:templateId",
"/v1/templates/:templateId",
describeRoute({
tags: ["Templates"],
summary: "Delete shared template",

View File

@@ -153,7 +153,7 @@ export function OrganizationScreen() {
setCreateBusy(true);
setCreateError(null);
try {
const { response, payload } = await requestJson("/v1/orgs", {
const { response, payload } = await requestJson("/v1/org", {
method: "POST",
body: JSON.stringify({ name: trimmed }),
});

View File

@@ -109,6 +109,13 @@ export type DenOrgContext = {
metadata: string | null;
createdAt: string | null;
updatedAt: string | null;
owner: {
memberId: string;
userId: string;
name: string | null;
email: string | null;
image: string | null;
} | null;
};
currentMember: {
id: string;
@@ -196,95 +203,95 @@ export function formatRoleLabel(role: string): string {
.join(" ");
}
export function getOrgDashboardRoute(orgSlug: string): string {
return `/o/${encodeURIComponent(orgSlug)}/dashboard`;
export function getOrgDashboardRoute(_orgSlug?: string | null): string {
return "/dashboard";
}
export function getJoinOrgRoute(invitationId: string): string {
return `/join-org?invite=${encodeURIComponent(invitationId)}`;
}
export function getManageMembersRoute(orgSlug: string): string {
export function getManageMembersRoute(orgSlug?: string | null): string {
return `${getOrgDashboardRoute(orgSlug)}/manage-members`;
}
export function getMembersRoute(orgSlug: string): string {
export function getMembersRoute(orgSlug?: string | null): string {
return `${getOrgDashboardRoute(orgSlug)}/members`;
}
export function getSharedSetupsRoute(orgSlug: string): string {
export function getSharedSetupsRoute(orgSlug?: string | null): string {
return `${getOrgDashboardRoute(orgSlug)}/shared-setups`;
}
export function getBackgroundAgentsRoute(orgSlug: string): string {
export function getBackgroundAgentsRoute(orgSlug?: string | null): string {
return `${getOrgDashboardRoute(orgSlug)}/background-agents`;
}
export function getCustomLlmProvidersRoute(orgSlug: string): string {
export function getCustomLlmProvidersRoute(orgSlug?: string | null): string {
return `${getOrgDashboardRoute(orgSlug)}/custom-llm-providers`;
}
export function getLlmProvidersRoute(orgSlug: string): string {
export function getLlmProvidersRoute(orgSlug?: string | null): string {
return getCustomLlmProvidersRoute(orgSlug);
}
export function getLlmProviderRoute(orgSlug: string, llmProviderId: string): string {
export function getLlmProviderRoute(orgSlug: string | null | undefined, llmProviderId: string): string {
return `${getLlmProvidersRoute(orgSlug)}/${encodeURIComponent(llmProviderId)}`;
}
export function getEditLlmProviderRoute(orgSlug: string, llmProviderId: string): string {
export function getEditLlmProviderRoute(orgSlug: string | null | undefined, llmProviderId: string): string {
return `${getLlmProviderRoute(orgSlug, llmProviderId)}/edit`;
}
export function getNewLlmProviderRoute(orgSlug: string): string {
export function getNewLlmProviderRoute(orgSlug?: string | null): string {
return `${getLlmProvidersRoute(orgSlug)}/new`;
}
export function getBillingRoute(orgSlug: string): string {
export function getBillingRoute(orgSlug?: string | null): string {
return `${getOrgDashboardRoute(orgSlug)}/billing`;
}
export function getApiKeysRoute(orgSlug: string): string {
export function getApiKeysRoute(orgSlug?: string | null): string {
return `${getOrgDashboardRoute(orgSlug)}/api-keys`;
}
export function getSkillHubsRoute(orgSlug: string): string {
export function getSkillHubsRoute(orgSlug?: string | null): string {
return `${getOrgDashboardRoute(orgSlug)}/skill-hubs`;
}
export function getSkillHubRoute(orgSlug: string, skillHubId: string): string {
export function getSkillHubRoute(orgSlug: string | null | undefined, skillHubId: string): string {
return `${getSkillHubsRoute(orgSlug)}/${encodeURIComponent(skillHubId)}`;
}
export function getEditSkillHubRoute(orgSlug: string, skillHubId: string): string {
export function getEditSkillHubRoute(orgSlug: string | null | undefined, skillHubId: string): string {
return `${getSkillHubRoute(orgSlug, skillHubId)}/edit`;
}
export function getNewSkillHubRoute(orgSlug: string): string {
export function getNewSkillHubRoute(orgSlug?: string | null): string {
return `${getSkillHubsRoute(orgSlug)}/new`;
}
export function getSkillDetailRoute(orgSlug: string, skillId: string): string {
export function getSkillDetailRoute(orgSlug: string | null | undefined, skillId: string): string {
return `${getSkillHubsRoute(orgSlug)}/skills/${encodeURIComponent(skillId)}`;
}
export function getEditSkillRoute(orgSlug: string, skillId: string): string {
export function getEditSkillRoute(orgSlug: string | null | undefined, skillId: string): string {
return `${getSkillDetailRoute(orgSlug, skillId)}/edit`;
}
export function getNewSkillRoute(orgSlug: string): string {
export function getNewSkillRoute(orgSlug?: string | null): string {
return `${getSkillHubsRoute(orgSlug)}/skills/new`;
}
export function getPluginsRoute(orgSlug: string): string {
export function getPluginsRoute(orgSlug?: string | null): string {
return `${getOrgDashboardRoute(orgSlug)}/plugins`;
}
export function getPluginRoute(orgSlug: string, pluginId: string): string {
export function getPluginRoute(orgSlug: string | null | undefined, pluginId: string): string {
return `${getPluginsRoute(orgSlug)}/${encodeURIComponent(pluginId)}`;
}
export function getIntegrationsRoute(orgSlug: string): string {
export function getIntegrationsRoute(orgSlug?: string | null): string {
return `${getOrgDashboardRoute(orgSlug)}/integrations`;
}
@@ -346,6 +353,9 @@ export function parseOrgContextPayload(payload: unknown): DenOrgContext | null {
const organizationId = asString(organization.id);
const organizationName = asString(organization.name);
const organizationSlug = asString(organization.slug);
const organizationOwner = isRecord(organization.owner) ? organization.owner : null;
const organizationOwnerMemberId = organizationOwner ? asString(organizationOwner.memberId) : null;
const organizationOwnerUserId = organizationOwner ? asString(organizationOwner.userId) : null;
const currentMemberId = asString(currentMember.id);
const currentMemberUserId = asString(currentMember.userId);
const currentMemberRole = asString(currentMember.role);
@@ -498,6 +508,15 @@ export function parseOrgContextPayload(payload: unknown): DenOrgContext | null {
metadata: asString(organization.metadata),
createdAt: asIsoString(organization.createdAt),
updatedAt: asIsoString(organization.updatedAt),
owner: organizationOwner && organizationOwnerMemberId && organizationOwnerUserId
? {
memberId: organizationOwnerMemberId,
userId: organizationOwnerUserId,
name: asString(organizationOwner.name),
email: asString(organizationOwner.email),
image: asString(organizationOwner.image),
}
: null,
},
currentMember: {
id: currentMemberId,

View File

@@ -0,0 +1 @@
export { default } from "../../o/[orgSlug]/dashboard/api-keys/page";

View File

@@ -0,0 +1 @@
export { default } from "../../o/[orgSlug]/dashboard/background-agents/page";

View File

@@ -0,0 +1 @@
export { default } from "../../o/[orgSlug]/dashboard/billing/page";

View File

@@ -0,0 +1 @@
export { default } from "../../../../o/[orgSlug]/dashboard/custom-llm-providers/[llmProviderId]/edit/page";

View File

@@ -0,0 +1 @@
export { default } from "../../../o/[orgSlug]/dashboard/custom-llm-providers/[llmProviderId]/page";

View File

@@ -0,0 +1 @@
export { default } from "../../../o/[orgSlug]/dashboard/custom-llm-providers/new/page";

View File

@@ -0,0 +1 @@
export { default } from "../../o/[orgSlug]/dashboard/custom-llm-providers/page";

View File

@@ -0,0 +1 @@
export { default } from "../../o/[orgSlug]/dashboard/integrations/page";

View File

@@ -0,0 +1,17 @@
import { OrgDashboardShell } from "../o/[orgSlug]/dashboard/_components/org-dashboard-shell";
import { OrgDashboardProvider } from "../o/[orgSlug]/dashboard/_providers/org-dashboard-provider";
import { DashboardQueryClientProvider } from "../o/[orgSlug]/dashboard/_providers/query-client-provider";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<DashboardQueryClientProvider>
<OrgDashboardProvider>
<OrgDashboardShell>{children}</OrgDashboardShell>
</OrgDashboardProvider>
</DashboardQueryClientProvider>
);
}

View File

@@ -0,0 +1 @@
export { default } from "../../o/[orgSlug]/dashboard/manage-members/page";

View File

@@ -0,0 +1 @@
export { default } from "../../o/[orgSlug]/dashboard/members/page";

View File

@@ -1,5 +1 @@
import { DashboardRedirectScreen } from "../_components/dashboard-redirect-screen";
export default function DashboardPage() {
return <DashboardRedirectScreen />;
}
export { default } from "../o/[orgSlug]/dashboard/page";

View File

@@ -0,0 +1 @@
export { default } from "../../../o/[orgSlug]/dashboard/plugins/[pluginId]/page";

View File

@@ -0,0 +1 @@
export { default } from "../../o/[orgSlug]/dashboard/plugins/page";

View File

@@ -0,0 +1 @@
export { default } from "../../o/[orgSlug]/dashboard/shared-setups/page";

View File

@@ -0,0 +1 @@
export { default } from "../../../../o/[orgSlug]/dashboard/skill-hubs/[skillHubId]/edit/page";

View File

@@ -0,0 +1 @@
export { default } from "../../../o/[orgSlug]/dashboard/skill-hubs/[skillHubId]/page";

View File

@@ -0,0 +1 @@
export { default } from "../../../o/[orgSlug]/dashboard/skill-hubs/new/page";

View File

@@ -0,0 +1 @@
export { default } from "../../o/[orgSlug]/dashboard/skill-hubs/page";

View File

@@ -0,0 +1 @@
export { default } from "../../../../../o/[orgSlug]/dashboard/skill-hubs/skills/[skillId]/edit/page";

View File

@@ -0,0 +1 @@
export { default } from "../../../../o/[orgSlug]/dashboard/skill-hubs/skills/[skillId]/page";

View File

@@ -0,0 +1 @@
export { default } from "../../../../o/[orgSlug]/dashboard/skill-hubs/skills/new/page";

View File

@@ -83,7 +83,7 @@ export function ApiKeysScreen() {
setError(null);
try {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/api-keys`,
`/v1/api-keys`,
{ method: "GET" },
12000,
);
@@ -135,7 +135,7 @@ export function ApiKeysScreen() {
setCopied(false);
try {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/api-keys`,
`/v1/api-keys`,
{
method: "POST",
body: JSON.stringify({ name }),
@@ -203,7 +203,7 @@ export function ApiKeysScreen() {
setError(null);
try {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/api-keys/${encodeURIComponent(apiKey.id)}`,
`/v1/api-keys/${encodeURIComponent(apiKey.id)}`,
{ method: "DELETE" },
12000,
);

View File

@@ -34,7 +34,7 @@ import {
} from "../../../../_lib/den-flow";
import { buildDenFeedbackUrl } from "../../../../_lib/feedback";
import { useDenFlow } from "../../../../_providers/den-flow-provider";
import { getSharedSetupsRoute } from "../../../../_lib/den-org";
import { getBackgroundAgentsRoute, getSharedSetupsRoute } from "../../../../_lib/den-org";
import { useOrgDashboard } from "../_providers/org-dashboard-provider";
type ConnectionDetails = {
@@ -317,7 +317,7 @@ export function BackgroundAgentsScreen() {
renameBusyWorkerId,
} = useDenFlow();
const feedbackHref = buildDenFeedbackUrl({
pathname: `/o/${orgSlug}/dashboard/background-agents`,
pathname: getBackgroundAgentsRoute(orgSlug),
orgSlug,
topic: "workspace-limits",
});

View File

@@ -353,7 +353,7 @@ export function buildEditableCustomProviderText(provider: DenLlmProvider) {
}
export async function requestLlmProviderCatalog(orgId: string) {
const { response, payload } = await requestJson(`/v1/orgs/${encodeURIComponent(orgId)}/llm-provider-catalog`, { method: "GET" }, 20000);
const { response, payload } = await requestJson(`/v1/llm-provider-catalog`, { method: "GET" }, 20000);
if (!response.ok) {
throw new Error(getErrorMessage(payload, `Failed to load the provider catalog (${response.status}).`));
}
@@ -365,7 +365,7 @@ export async function requestLlmProviderCatalog(orgId: string) {
export async function requestLlmProviderCatalogDetail(orgId: string, providerId: string) {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/llm-provider-catalog/${encodeURIComponent(providerId)}`,
`/v1/llm-provider-catalog/${encodeURIComponent(providerId)}`,
{ method: "GET" },
20000,
);
@@ -401,7 +401,7 @@ export function useOrgLlmProviders(orgId: string | null) {
setBusy(true);
setError(null);
try {
const { response, payload } = await requestJson(`/v1/orgs/${encodeURIComponent(orgId)}/llm-providers`, { method: "GET" }, 15000);
const { response, payload } = await requestJson(`/v1/llm-providers`, { method: "GET" }, 15000);
if (!response.ok) {
throw new Error(getErrorMessage(payload, `Failed to load providers (${response.status}).`));
}

View File

@@ -67,7 +67,7 @@ export function LlmProviderDetailScreen({
setDeleteError(null);
try {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/llm-providers/${encodeURIComponent(provider.id)}`,
`/v1/llm-providers/${encodeURIComponent(provider.id)}`,
{ method: "DELETE" },
12000,
);

View File

@@ -298,8 +298,8 @@ export function LlmProviderEditorScreen({
}
const path = provider
? `/v1/orgs/${encodeURIComponent(orgId)}/llm-providers/${encodeURIComponent(provider.id)}`
: `/v1/orgs/${encodeURIComponent(orgId)}/llm-providers`;
? `/v1/llm-providers/${encodeURIComponent(provider.id)}`
: `/v1/llm-providers`;
const method = provider ? "PATCH" : "POST";
const { response, payload } = await requestJson(

View File

@@ -87,7 +87,7 @@ export function useOrgTemplates(orgId: string | null) {
}
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/templates`,
`/v1/templates`,
{ method: "GET" },
12000,
);

View File

@@ -95,7 +95,7 @@ export function SkillEditorScreen({ skillId }: { skillId?: string }) {
const shared = visibility === "private" ? null : visibility;
if (skillId) {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/skills/${encodeURIComponent(skillId)}`,
`/v1/skills/${encodeURIComponent(skillId)}`,
{ method: "PATCH", body: JSON.stringify({ skillText, shared }) },
12000,
);
@@ -103,7 +103,7 @@ export function SkillEditorScreen({ skillId }: { skillId?: string }) {
router.push(getSkillDetailRoute(orgSlug, skillId));
} else {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/skills`,
`/v1/skills`,
{ method: "POST", body: JSON.stringify({ skillText, shared }) },
12000,
);

View File

@@ -363,8 +363,8 @@ export function useOrgSkillLibrary(orgId: string | null) {
setError(null);
try {
const [skillsResult, skillHubsResult] = await Promise.all([
requestJson(`/v1/orgs/${encodeURIComponent(orgId)}/skills`, { method: "GET" }, 12000),
requestJson(`/v1/orgs/${encodeURIComponent(orgId)}/skill-hubs`, { method: "GET" }, 12000),
requestJson(`/v1/skills`, { method: "GET" }, 12000),
requestJson(`/v1/skill-hubs`, { method: "GET" }, 12000),
]);
if (!skillsResult.response.ok) {

View File

@@ -130,7 +130,7 @@ export function SkillHubEditorScreen({ skillHubId }: { skillHubId?: string }) {
if (!nextSkillHubId) {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/skill-hubs`,
`/v1/skill-hubs`,
{
method: "POST",
body: JSON.stringify({
@@ -172,7 +172,7 @@ export function SkillHubEditorScreen({ skillHubId }: { skillHubId?: string }) {
(skillHub.description ?? "") !== description.trim())
) {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/skill-hubs/${encodeURIComponent(nextSkillHubId)}`,
`/v1/skill-hubs/${encodeURIComponent(nextSkillHubId)}`,
{
method: "PATCH",
body: JSON.stringify({
@@ -209,7 +209,7 @@ export function SkillHubEditorScreen({ skillHubId }: { skillHubId?: string }) {
await Promise.all(
teamIdsToAdd.map(async (teamId) => {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/skill-hubs/${encodeURIComponent(nextSkillHubId)}/access`,
`/v1/skill-hubs/${encodeURIComponent(nextSkillHubId)}/access`,
{
method: "POST",
body: JSON.stringify({ teamId }),
@@ -231,7 +231,7 @@ export function SkillHubEditorScreen({ skillHubId }: { skillHubId?: string }) {
await Promise.all(
teamAccessIdsToRemove.map(async (accessId) => {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/skill-hubs/${encodeURIComponent(nextSkillHubId)}/access/${encodeURIComponent(accessId)}`,
`/v1/skill-hubs/${encodeURIComponent(nextSkillHubId)}/access/${encodeURIComponent(accessId)}`,
{ method: "DELETE" },
12000,
);
@@ -250,7 +250,7 @@ export function SkillHubEditorScreen({ skillHubId }: { skillHubId?: string }) {
await Promise.all(
skillIdsToAdd.map(async (entry) => {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/skill-hubs/${encodeURIComponent(nextSkillHubId)}/skills`,
`/v1/skill-hubs/${encodeURIComponent(nextSkillHubId)}/skills`,
{
method: "POST",
body: JSON.stringify({ skillId: entry }),
@@ -272,7 +272,7 @@ export function SkillHubEditorScreen({ skillHubId }: { skillHubId?: string }) {
await Promise.all(
skillIdsToRemove.map(async (entry) => {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/skill-hubs/${encodeURIComponent(nextSkillHubId)}/skills/${encodeURIComponent(entry)}`,
`/v1/skill-hubs/${encodeURIComponent(nextSkillHubId)}/skills/${encodeURIComponent(entry)}`,
{ method: "DELETE" },
12000,
);

View File

@@ -100,7 +100,7 @@ export function SharedSetupsScreen() {
}
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/templates/${encodeURIComponent(templateId)}`,
`/v1/templates/${encodeURIComponent(templateId)}`,
{ method: "DELETE" },
12000,
);

View File

@@ -7,7 +7,7 @@ import {
useMemo,
useState,
} from "react";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { useDenFlow } from "../../../../_providers/den-flow-provider";
import { getErrorMessage, getOrgLimitError, requestJson } from "../../../../_lib/den-flow";
import {
@@ -19,7 +19,7 @@ import {
} from "../../../../_lib/den-org";
type OrgDashboardContextValue = {
orgSlug: string;
orgSlug: string | null;
orgId: string | null;
orgDirectory: DenOrgSummary[];
activeOrg: DenOrgSummary | null;
@@ -49,10 +49,11 @@ export function OrgDashboardProvider({
orgSlug,
children,
}: {
orgSlug: string;
orgSlug?: string | null;
children: React.ReactNode;
}) {
const router = useRouter();
const pathname = usePathname();
const { user, sessionHydrated, signOut, refreshWorkers, workersLoadedOnce } = useDenFlow();
const [orgDirectory, setOrgDirectory] = useState<DenOrgSummary[]>([]);
const [orgContext, setOrgContext] = useState<DenOrgContext | null>(null);
@@ -61,18 +62,20 @@ export function OrgDashboardProvider({
const [mutationBusy, setMutationBusy] = useState<string | null>(null);
const activeOrg = useMemo(
() => orgDirectory.find((entry) => entry.slug === orgSlug) ?? orgDirectory.find((entry) => entry.isActive) ?? null,
() =>
(orgSlug ? orgDirectory.find((entry) => entry.slug === orgSlug) : null) ??
orgDirectory.find((entry) => entry.isActive) ??
orgDirectory[0] ??
null,
[orgDirectory, orgSlug],
);
const activeOrgId = activeOrg?.id ?? orgContext?.organization.id ?? null;
function getRequiredActiveOrgId() {
function ensureActiveOrganizationSelected() {
if (!activeOrgId) {
throw new Error("Organization not found.");
}
return activeOrgId;
}
async function loadOrgDirectory() {
@@ -81,11 +84,26 @@ export function OrgDashboardProvider({
throw new Error(getErrorMessage(payload, `Failed to load organizations (${response.status}).`));
}
return parseOrgListPayload(payload).orgs;
return parseOrgListPayload(payload);
}
async function loadOrgContext(targetOrgId: string) {
const { response, payload } = await requestJson(`/v1/orgs/${encodeURIComponent(targetOrgId)}/context`, { method: "GET" }, 12000);
async function setActiveOrganization(input: { organizationId?: string | null; organizationSlug?: string | null }) {
const { response, payload } = await requestJson(
"/api/auth/organization/set-active",
{
method: "POST",
body: JSON.stringify(input),
},
12000,
);
if (!response.ok) {
throw new Error(getErrorMessage(payload, `Failed to switch organization (${response.status}).`));
}
}
async function loadOrgContext() {
const { response, payload } = await requestJson("/v1/org", { method: "GET" }, 12000);
if (!response.ok) {
throw new Error(getErrorMessage(payload, `Failed to load organization (${response.status}).`));
}
@@ -110,16 +128,25 @@ export function OrgDashboardProvider({
setOrgError(null);
try {
const directory = await loadOrgDirectory();
const targetOrg = directory.find((entry) => entry.slug === orgSlug) ?? null;
let directoryPayload = await loadOrgDirectory();
const fallbackOrg = directoryPayload.orgs.find((entry) => entry.isActive) ?? directoryPayload.orgs[0] ?? null;
const targetOrg = (orgSlug ? directoryPayload.orgs.find((entry) => entry.slug === orgSlug) : null) ?? fallbackOrg;
if (!targetOrg) {
throw new Error("Organization not found.");
setOrgDirectory([]);
setOrgContext(null);
router.replace("/organization");
return;
}
const context = await loadOrgContext(targetOrg.id);
if (!targetOrg.isActive) {
await setActiveOrganization({ organizationId: targetOrg.id });
directoryPayload = await loadOrgDirectory();
}
setOrgDirectory(directory.map((entry) => ({ ...entry, isActive: entry.id === context.organization.id })));
const context = await loadOrgContext();
setOrgDirectory(directoryPayload.orgs.map((entry) => ({ ...entry, isActive: entry.id === context.organization.id })));
setOrgContext(context);
await refreshWorkers({ keepSelection: false, quiet: workersLoadedOnce });
} catch (error) {
@@ -150,7 +177,7 @@ export function OrgDashboardProvider({
setOrgError(null);
try {
const { response, payload } = await requestJson(
"/v1/orgs",
"/v1/org",
{
method: "POST",
body: JSON.stringify({ name: trimmed }),
@@ -183,7 +210,34 @@ export function OrgDashboardProvider({
}
function switchOrganization(nextSlug: string) {
router.push(getOrgDashboardRoute(nextSlug));
const targetOrg = orgDirectory.find((entry) => entry.slug === nextSlug) ?? null;
if (!targetOrg) {
return;
}
void (async () => {
setMutationBusy("switch-organization");
setOrgError(null);
try {
await setActiveOrganization({ organizationId: targetOrg.id });
const context = await loadOrgContext();
setOrgDirectory((current) => current.map((entry) => ({ ...entry, isActive: entry.id === context.organization.id })));
setOrgContext(context);
await refreshWorkers({ keepSelection: false, quiet: workersLoadedOnce });
if (orgSlug && pathname.startsWith("/o/")) {
router.replace(getOrgDashboardRoute(nextSlug));
return;
}
router.refresh();
} catch (error) {
setOrgError(error instanceof Error ? error.message : "Failed to switch organization.");
} finally {
setMutationBusy(null);
}
})();
}
async function updateOrganizationName(name: string) {
@@ -193,8 +247,9 @@ export function OrgDashboardProvider({
}
await runMutation("update-organization-name", async () => {
ensureActiveOrganizationSelected();
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(getRequiredActiveOrgId())}`,
"/v1/org",
{
method: "PATCH",
body: JSON.stringify({ name: trimmed }),
@@ -210,8 +265,9 @@ export function OrgDashboardProvider({
async function inviteMember(input: { email: string; role: string }) {
await runMutation("invite-member", async () => {
ensureActiveOrganizationSelected();
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(getRequiredActiveOrgId())}/invitations`,
"/v1/invitations",
{
method: "POST",
body: JSON.stringify(input),
@@ -231,8 +287,9 @@ export function OrgDashboardProvider({
async function cancelInvitation(invitationId: string) {
await runMutation("cancel-invitation", async () => {
ensureActiveOrganizationSelected();
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(getRequiredActiveOrgId())}/invitations/${encodeURIComponent(invitationId)}/cancel`,
`/v1/invitations/${encodeURIComponent(invitationId)}/cancel`,
{ method: "POST", body: JSON.stringify({}) },
12000,
);
@@ -245,8 +302,9 @@ export function OrgDashboardProvider({
async function updateMemberRole(memberId: string, role: string) {
await runMutation("update-member-role", async () => {
ensureActiveOrganizationSelected();
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(getRequiredActiveOrgId())}/members/${encodeURIComponent(memberId)}/role`,
`/v1/members/${encodeURIComponent(memberId)}/role`,
{
method: "POST",
body: JSON.stringify({ role }),
@@ -262,8 +320,9 @@ export function OrgDashboardProvider({
async function removeMember(memberId: string) {
await runMutation("remove-member", async () => {
ensureActiveOrganizationSelected();
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(getRequiredActiveOrgId())}/members/${encodeURIComponent(memberId)}`,
`/v1/members/${encodeURIComponent(memberId)}`,
{ method: "DELETE" },
12000,
);
@@ -276,8 +335,9 @@ export function OrgDashboardProvider({
async function createRole(input: { roleName: string; permission: Record<string, string[]> }) {
await runMutation("create-role", async () => {
ensureActiveOrganizationSelected();
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(getRequiredActiveOrgId())}/roles`,
"/v1/roles",
{
method: "POST",
body: JSON.stringify(input),
@@ -293,8 +353,9 @@ export function OrgDashboardProvider({
async function createTeam(input: { name: string; memberIds: string[] }) {
await runMutation("create-team", async () => {
ensureActiveOrganizationSelected();
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(getRequiredActiveOrgId())}/teams`,
"/v1/teams",
{
method: "POST",
body: JSON.stringify(input),
@@ -310,8 +371,9 @@ export function OrgDashboardProvider({
async function updateTeam(teamId: string, input: { name?: string; memberIds?: string[] }) {
await runMutation("update-team", async () => {
ensureActiveOrganizationSelected();
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(getRequiredActiveOrgId())}/teams/${encodeURIComponent(teamId)}`,
`/v1/teams/${encodeURIComponent(teamId)}`,
{
method: "PATCH",
body: JSON.stringify(input),
@@ -327,8 +389,9 @@ export function OrgDashboardProvider({
async function deleteTeam(teamId: string) {
await runMutation("delete-team", async () => {
ensureActiveOrganizationSelected();
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(getRequiredActiveOrgId())}/teams/${encodeURIComponent(teamId)}`,
`/v1/teams/${encodeURIComponent(teamId)}`,
{ method: "DELETE" },
12000,
);
@@ -341,8 +404,9 @@ export function OrgDashboardProvider({
async function updateRole(roleId: string, input: { roleName?: string; permission?: Record<string, string[]> }) {
await runMutation("update-role", async () => {
ensureActiveOrganizationSelected();
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(getRequiredActiveOrgId())}/roles/${encodeURIComponent(roleId)}`,
`/v1/roles/${encodeURIComponent(roleId)}`,
{
method: "PATCH",
body: JSON.stringify(input),
@@ -358,8 +422,9 @@ export function OrgDashboardProvider({
async function deleteRole(roleId: string) {
await runMutation("delete-role", async () => {
ensureActiveOrganizationSelected();
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(getRequiredActiveOrgId())}/roles/${encodeURIComponent(roleId)}`,
`/v1/roles/${encodeURIComponent(roleId)}`,
{ method: "DELETE" },
12000,
);
@@ -385,7 +450,7 @@ export function OrgDashboardProvider({
}, [orgSlug, router, sessionHydrated, user?.id]);
const value: OrgDashboardContextValue = {
orgSlug,
orgSlug: activeOrg?.slug ?? orgSlug ?? null,
orgId: activeOrgId,
orgDirectory,
activeOrg,

View File

@@ -1,11 +1,5 @@
import { redirect } from "next/navigation";
export default async function ManageMembersRedirectPage({
params,
}: {
params: Promise<{ orgSlug: string }>;
}) {
const { orgSlug } = await params;
redirect(`/o/${encodeURIComponent(orgSlug)}/dashboard/members`);
export default function ManageMembersRedirectPage() {
redirect("/dashboard/members");
}