mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
Rename USER to PROJECT in permissions (#576)
<!-- ELLIPSIS_HIDDEN -->
> [!IMPORTANT]
> Renamed user-related permissions to project-related permissions across
the codebase, affecting enums, schemas, APIs, models, and tests.
>
> - **Behavior**:
> - Renamed `USER` to `PROJECT` in `PermissionScope` enum in
`schema.prisma` and `migration.sql`.
> - Updated `isDefaultUserPermission` to `isDefaultProjectPermission` in
`schema.prisma` and `migration.sql`.
> - Removed `jwks.json/route.ts` file.
> - **API Changes**:
> - Renamed `user-permission-definitions` and `user-permissions`
endpoints to `project-permission-definitions` and `project-permissions`
in `route.tsx` files.
> - Updated CRUD handlers in `crud.tsx` files to reflect new naming.
> - **Models**:
> - Updated models in `permissions.tsx` to use `ProjectPermission` and
`AdminProjectPermission`.
> - Updated `KnownErrors` to use `ProjectPermissionRequired`.
> - **Tests**:
> - Renamed test files and updated test cases in
`e2e/tests/backend/endpoints/api/v1` to reflect new naming.
> - **Misc**:
> - Updated `admin-app-impl.ts`, `client-app-impl.ts`, and
`server-app-impl.ts` to use new project permission naming.
> - Updated `schema-fields.ts` to reflect new permission ID schema.
>
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup>
for 08924f5241. It will automatically
update as commits are pushed.</sup>
<!-- ELLIPSIS_HIDDEN -->
---------
Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
This commit is contained in:
parent
bba3859449
commit
793272c8c5
@ -1,5 +1,20 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- The values [USER] on the enum `PermissionScope` will be removed. If these variants are still used in the database, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterEnum
|
||||
BEGIN;
|
||||
CREATE TYPE "PermissionScope_new" AS ENUM ('PROJECT', 'TEAM');
|
||||
ALTER TABLE "Permission" ALTER COLUMN "scope" TYPE "PermissionScope_new" USING ("scope"::text::"PermissionScope_new");
|
||||
ALTER TYPE "PermissionScope" RENAME TO "PermissionScope_old";
|
||||
ALTER TYPE "PermissionScope_new" RENAME TO "PermissionScope";
|
||||
DROP TYPE "PermissionScope_old";
|
||||
COMMIT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Permission" ADD COLUMN "isDefaultUserPermission" BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE "Permission" ADD COLUMN "isDefaultProjectPermission" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ProjectUserDirectPermission" (
|
||||
@ -224,14 +224,14 @@ model Permission {
|
||||
|
||||
isDefaultTeamCreatorPermission Boolean @default(false)
|
||||
isDefaultTeamMemberPermission Boolean @default(false)
|
||||
isDefaultUserPermission Boolean @default(false)
|
||||
isDefaultProjectPermission Boolean @default(false)
|
||||
|
||||
@@unique([projectConfigId, queryableId])
|
||||
@@unique([tenancyId, teamId, queryableId])
|
||||
}
|
||||
|
||||
enum PermissionScope {
|
||||
USER
|
||||
PROJECT
|
||||
TEAM
|
||||
}
|
||||
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import { yupArray, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { getPublicJwkSet } from "@stackframe/stack-shared/dist/utils/jwt";
|
||||
import { createSmartRouteHandler } from "../../../route-handlers/smart-route-handler";
|
||||
|
||||
export const GET = createSmartRouteHandler({
|
||||
metadata: {
|
||||
summary: "JWKS Endpoint",
|
||||
description: "Returns information about the JSON Web Key Set (JWKS) used to sign and verify JWTs.",
|
||||
tags: [],
|
||||
hidden: true,
|
||||
},
|
||||
request: yupObject({}),
|
||||
response: yupObject({
|
||||
statusCode: yupNumber().oneOf([200]).defined(),
|
||||
bodyType: yupString().oneOf(["json"]).defined(),
|
||||
body: yupObject({
|
||||
keys: yupArray().defined(),
|
||||
message: yupString().optional(),
|
||||
}).defined(),
|
||||
}),
|
||||
async handler() {
|
||||
return {
|
||||
statusCode: 200,
|
||||
bodyType: "json",
|
||||
body: {
|
||||
...await getPublicJwkSet(getEnvVariable("STACK_SERVER_SECRET")),
|
||||
message: "This is deprecated. Please disable the legacy JWT signing in the tenancy setting page, and move to /api/v1/projects/<tenancy-id>/.well-known/jwks.json",
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,4 @@
|
||||
import { projectPermissionDefinitionsCrudHandlers } from "../crud";
|
||||
|
||||
export const PATCH = projectPermissionDefinitionsCrudHandlers.updateHandler;
|
||||
export const DELETE = projectPermissionDefinitionsCrudHandlers.deleteHandler;
|
||||
@ -1,18 +1,18 @@
|
||||
import { createPermissionDefinition, deletePermissionDefinition, listPermissionDefinitions, updatePermissionDefinitions } from "@/lib/permissions";
|
||||
import { retryTransaction } from "@/prisma-client";
|
||||
import { createCrudHandlers } from "@/route-handlers/crud-handler";
|
||||
import { userPermissionDefinitionsCrud } from '@stackframe/stack-shared/dist/interface/crud/user-permissions';
|
||||
import { teamPermissionDefinitionIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { projectPermissionDefinitionsCrud } from '@stackframe/stack-shared/dist/interface/crud/project-permissions';
|
||||
import { permissionDefinitionIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
|
||||
|
||||
export const userPermissionDefinitionsCrudHandlers = createLazyProxy(() => createCrudHandlers(userPermissionDefinitionsCrud, {
|
||||
export const projectPermissionDefinitionsCrudHandlers = createLazyProxy(() => createCrudHandlers(projectPermissionDefinitionsCrud, {
|
||||
paramsSchema: yupObject({
|
||||
permission_id: teamPermissionDefinitionIdSchema.defined(),
|
||||
permission_id: permissionDefinitionIdSchema.defined(),
|
||||
}),
|
||||
async onCreate({ auth, data }) {
|
||||
return await retryTransaction(async (tx) => {
|
||||
return await createPermissionDefinition(tx, {
|
||||
scope: "USER",
|
||||
scope: "PROJECT",
|
||||
tenancy: auth.tenancy,
|
||||
data,
|
||||
});
|
||||
@ -21,7 +21,7 @@ export const userPermissionDefinitionsCrudHandlers = createLazyProxy(() => creat
|
||||
async onUpdate({ auth, data, params }) {
|
||||
return await retryTransaction(async (tx) => {
|
||||
return await updatePermissionDefinitions(tx, {
|
||||
scope: "USER",
|
||||
scope: "PROJECT",
|
||||
tenancy: auth.tenancy,
|
||||
permissionId: params.permission_id,
|
||||
data,
|
||||
@ -39,7 +39,7 @@ export const userPermissionDefinitionsCrudHandlers = createLazyProxy(() => creat
|
||||
async onList({ auth }) {
|
||||
return await retryTransaction(async (tx) => {
|
||||
return {
|
||||
items: await listPermissionDefinitions(tx, "USER", auth.tenancy),
|
||||
items: await listPermissionDefinitions(tx, "PROJECT", auth.tenancy),
|
||||
is_paginated: false,
|
||||
};
|
||||
});
|
||||
@ -0,0 +1,4 @@
|
||||
import { projectPermissionDefinitionsCrudHandlers } from "./crud";
|
||||
|
||||
export const POST = projectPermissionDefinitionsCrudHandlers.createHandler;
|
||||
export const GET = projectPermissionDefinitionsCrudHandlers.listHandler;
|
||||
@ -0,0 +1,4 @@
|
||||
import { projectPermissionsCrudHandlers } from "../../crud";
|
||||
|
||||
export const POST = projectPermissionsCrudHandlers.createHandler;
|
||||
export const DELETE = projectPermissionsCrudHandlers.deleteHandler;
|
||||
@ -1,37 +1,37 @@
|
||||
import { grantUserPermission, listUserPermissions, revokeUserPermission } from "@/lib/permissions";
|
||||
import { ensureUserExists, ensureUserPermissionExists } from "@/lib/request-checks";
|
||||
import { sendUserPermissionCreatedWebhook, sendUserPermissionDeletedWebhook } from "@/lib/webhooks";
|
||||
import { grantProjectPermission, listProjectPermissions, revokeProjectPermission } from "@/lib/permissions";
|
||||
import { ensureUserExists, ensureProjectPermissionExists } from "@/lib/request-checks";
|
||||
import { sendProjectPermissionCreatedWebhook, sendProjectPermissionDeletedWebhook } from "@/lib/webhooks";
|
||||
import { retryTransaction } from "@/prisma-client";
|
||||
import { createCrudHandlers } from "@/route-handlers/crud-handler";
|
||||
import { runAsynchronouslyAndWaitUntil } from "@/utils/vercel";
|
||||
import { KnownErrors } from "@stackframe/stack-shared";
|
||||
import { userPermissionsCrud } from '@stackframe/stack-shared/dist/interface/crud/user-permissions';
|
||||
import { teamPermissionDefinitionIdSchema, userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { projectPermissionsCrud } from '@stackframe/stack-shared/dist/interface/crud/project-permissions';
|
||||
import { permissionDefinitionIdSchema, userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
|
||||
|
||||
export const userPermissionsCrudHandlers = createLazyProxy(() => createCrudHandlers(userPermissionsCrud, {
|
||||
export const projectPermissionsCrudHandlers = createLazyProxy(() => createCrudHandlers(projectPermissionsCrud, {
|
||||
querySchema: yupObject({
|
||||
user_id: userIdOrMeSchema.optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Filter with the user ID. If set, only the permissions this user has will be returned. Client request must set `user_id=me`', exampleValue: 'me' } }),
|
||||
permission_id: teamPermissionDefinitionIdSchema.optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Filter with the permission ID. If set, only the permissions with this specific ID will be returned', exampleValue: '16399452-c4f3-4554-8e44-c2d67bb60360' } }),
|
||||
permission_id: permissionDefinitionIdSchema.optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Filter with the permission ID. If set, only the permissions with this specific ID will be returned', exampleValue: '16399452-c4f3-4554-8e44-c2d67bb60360' } }),
|
||||
recursive: yupString().oneOf(['true', 'false']).optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Whether to list permissions recursively. If set to `false`, only the permission the users directly have will be listed. If set to `true` all the direct and indirect permissions will be listed.', exampleValue: 'true' } }),
|
||||
}),
|
||||
paramsSchema: yupObject({
|
||||
user_id: userIdOrMeSchema.defined(),
|
||||
permission_id: teamPermissionDefinitionIdSchema.defined(),
|
||||
permission_id: permissionDefinitionIdSchema.defined(),
|
||||
}),
|
||||
async onCreate({ auth, params }) {
|
||||
const result = await retryTransaction(async (tx) => {
|
||||
await ensureUserExists(tx, { tenancyId: auth.tenancy.id, userId: params.user_id });
|
||||
|
||||
return await grantUserPermission(tx, {
|
||||
return await grantProjectPermission(tx, {
|
||||
tenancy: auth.tenancy,
|
||||
userId: params.user_id,
|
||||
permissionId: params.permission_id
|
||||
});
|
||||
});
|
||||
|
||||
runAsynchronouslyAndWaitUntil(sendUserPermissionCreatedWebhook({
|
||||
runAsynchronouslyAndWaitUntil(sendProjectPermissionCreatedWebhook({
|
||||
projectId: auth.project.id,
|
||||
data: {
|
||||
id: params.permission_id,
|
||||
@ -43,7 +43,7 @@ export const userPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl
|
||||
},
|
||||
async onDelete({ auth, params }) {
|
||||
const result = await retryTransaction(async (tx) => {
|
||||
await ensureUserPermissionExists(tx, {
|
||||
await ensureProjectPermissionExists(tx, {
|
||||
tenancy: auth.tenancy,
|
||||
userId: params.user_id,
|
||||
permissionId: params.permission_id,
|
||||
@ -51,14 +51,14 @@ export const userPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl
|
||||
recursive: false,
|
||||
});
|
||||
|
||||
return await revokeUserPermission(tx, {
|
||||
return await revokeProjectPermission(tx, {
|
||||
tenancy: auth.tenancy,
|
||||
userId: params.user_id,
|
||||
permissionId: params.permission_id
|
||||
});
|
||||
});
|
||||
|
||||
runAsynchronouslyAndWaitUntil(sendUserPermissionDeletedWebhook({
|
||||
runAsynchronouslyAndWaitUntil(sendProjectPermissionDeletedWebhook({
|
||||
projectId: auth.project.id,
|
||||
data: {
|
||||
id: params.permission_id,
|
||||
@ -79,7 +79,7 @@ export const userPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl
|
||||
|
||||
return await retryTransaction(async (tx) => {
|
||||
return {
|
||||
items: await listUserPermissions(tx, {
|
||||
items: await listProjectPermissions(tx, {
|
||||
tenancy: auth.tenancy,
|
||||
permissionId: query.permission_id,
|
||||
userId: query.user_id,
|
||||
@ -0,0 +1,3 @@
|
||||
import { projectPermissionsCrudHandlers } from "./crud";
|
||||
|
||||
export const GET = projectPermissionsCrudHandlers.listHandler;
|
||||
@ -34,27 +34,27 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro
|
||||
] as const;
|
||||
|
||||
const teamPermissions = await listPermissionDefinitions(tx, "TEAM", auth.tenancy);
|
||||
const userPermissions = await listPermissionDefinitions(tx, "USER", auth.tenancy);
|
||||
const projectPermissions = await listPermissionDefinitions(tx, "PROJECT", auth.tenancy);
|
||||
|
||||
// Handle user default permissions
|
||||
const userDefaultPerms = data.config?.user_default_permissions?.map((p) => p.id);
|
||||
if (userDefaultPerms) {
|
||||
if (!userDefaultPerms.every((id) => userPermissions.some((perm) => perm.id === id))) {
|
||||
if (!userDefaultPerms.every((id) => projectPermissions.some((perm) => perm.id === id))) {
|
||||
throw new StatusError(StatusError.BadRequest,
|
||||
`Invalid user default permission ids: ${userDefaultPerms.filter(id => !userPermissions.some(perm => perm.id === id)).join(', ')}`);
|
||||
`Invalid user default permission ids: ${userDefaultPerms.filter(id => !projectPermissions.some(perm => perm.id === id)).join(', ')}`);
|
||||
}
|
||||
|
||||
// Remove existing default user permissions
|
||||
// Remove existing default project permissions
|
||||
await tx.permission.updateMany({
|
||||
where: {
|
||||
projectConfigId: oldProject.config.id,
|
||||
},
|
||||
data: {
|
||||
isDefaultUserPermission: false,
|
||||
isDefaultProjectPermission: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Add new default user permissions
|
||||
// Add new default project permissions
|
||||
await tx.permission.updateMany({
|
||||
where: {
|
||||
projectConfigId: oldProject.config.id,
|
||||
@ -63,7 +63,7 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro
|
||||
},
|
||||
},
|
||||
data: {
|
||||
isDefaultUserPermission: true,
|
||||
isDefaultProjectPermission: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -2,12 +2,12 @@ import { createPermissionDefinition, deletePermissionDefinition, listPermissionD
|
||||
import { retryTransaction } from "@/prisma-client";
|
||||
import { createCrudHandlers } from "@/route-handlers/crud-handler";
|
||||
import { teamPermissionDefinitionsCrud } from '@stackframe/stack-shared/dist/interface/crud/team-permissions';
|
||||
import { teamPermissionDefinitionIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { permissionDefinitionIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
|
||||
|
||||
export const teamPermissionDefinitionsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamPermissionDefinitionsCrud, {
|
||||
paramsSchema: yupObject({
|
||||
permission_id: teamPermissionDefinitionIdSchema.defined(),
|
||||
permission_id: permissionDefinitionIdSchema.defined(),
|
||||
}),
|
||||
async onCreate({ auth, data }) {
|
||||
return await retryTransaction(async (tx) => {
|
||||
|
||||
@ -6,7 +6,7 @@ import { createCrudHandlers } from "@/route-handlers/crud-handler";
|
||||
import { runAsynchronouslyAndWaitUntil } from "@/utils/vercel";
|
||||
import { KnownErrors } from "@stackframe/stack-shared";
|
||||
import { teamPermissionsCrud } from '@stackframe/stack-shared/dist/interface/crud/team-permissions';
|
||||
import { teamPermissionDefinitionIdSchema, userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { permissionDefinitionIdSchema, userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
|
||||
|
||||
@ -14,13 +14,13 @@ export const teamPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl
|
||||
querySchema: yupObject({
|
||||
team_id: yupString().uuid().optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Filter with the team ID. If set, only the permissions of the members in a specific team will be returned.', exampleValue: 'cce084a3-28b7-418e-913e-c8ee6d802ea4' } }),
|
||||
user_id: userIdOrMeSchema.optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Filter with the user ID. If set, only the permissions this user has will be returned. Client request must set `user_id=me`', exampleValue: 'me' } }),
|
||||
permission_id: teamPermissionDefinitionIdSchema.optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Filter with the permission ID. If set, only the permissions with this specific ID will be returned', exampleValue: '16399452-c4f3-4554-8e44-c2d67bb60360' } }),
|
||||
permission_id: permissionDefinitionIdSchema.optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Filter with the permission ID. If set, only the permissions with this specific ID will be returned', exampleValue: '16399452-c4f3-4554-8e44-c2d67bb60360' } }),
|
||||
recursive: yupString().oneOf(['true', 'false']).optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Whether to list permissions recursively. If set to `false`, only the permission the users directly have will be listed. If set to `true` all the direct and indirect permissions will be listed.', exampleValue: 'true' } }),
|
||||
}),
|
||||
paramsSchema: yupObject({
|
||||
team_id: yupString().uuid().defined(),
|
||||
user_id: userIdOrMeSchema.defined(),
|
||||
permission_id: teamPermissionDefinitionIdSchema.defined(),
|
||||
permission_id: permissionDefinitionIdSchema.defined(),
|
||||
}),
|
||||
async onCreate({ auth, params }) {
|
||||
const result = await retryTransaction(async (tx) => {
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
import { userPermissionDefinitionsCrudHandlers } from "../crud";
|
||||
|
||||
export const PATCH = userPermissionDefinitionsCrudHandlers.updateHandler;
|
||||
export const DELETE = userPermissionDefinitionsCrudHandlers.deleteHandler;
|
||||
@ -1,4 +0,0 @@
|
||||
import { userPermissionDefinitionsCrudHandlers } from "./crud";
|
||||
|
||||
export const POST = userPermissionDefinitionsCrudHandlers.createHandler;
|
||||
export const GET = userPermissionDefinitionsCrudHandlers.listHandler;
|
||||
@ -1,4 +0,0 @@
|
||||
import { userPermissionsCrudHandlers } from "../../crud";
|
||||
|
||||
export const POST = userPermissionsCrudHandlers.createHandler;
|
||||
export const DELETE = userPermissionsCrudHandlers.deleteHandler;
|
||||
@ -1,3 +0,0 @@
|
||||
import { userPermissionsCrudHandlers } from "./crud";
|
||||
|
||||
export const GET = userPermissionsCrudHandlers.listHandler;
|
||||
@ -1,5 +1,5 @@
|
||||
import { grantDefaultProjectPermissions } from "@/lib/permissions";
|
||||
import { ensureTeamMembershipExists, ensureUserExists } from "@/lib/request-checks";
|
||||
import { grantDefaultUserPermissions } from "@/lib/permissions";
|
||||
import { getSoleTenancyFromProject, getTenancy } from "@/lib/tenancies";
|
||||
import { PrismaTransaction } from "@/lib/types";
|
||||
import { sendTeamMembershipDeletedWebhook, sendUserCreatedWebhook, sendUserDeletedWebhook, sendUserUpdatedWebhook } from "@/lib/webhooks";
|
||||
@ -712,7 +712,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
|
||||
}
|
||||
|
||||
// Grant default user permissions
|
||||
await grantDefaultUserPermissions(tx, {
|
||||
await grantDefaultProjectPermissions(tx, {
|
||||
tenancy: auth.tenancy,
|
||||
userId: newUser.projectUserId
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { TeamSystemPermission as DBTeamSystemPermission, Prisma } from "@prisma/client";
|
||||
import { KnownErrors } from "@stackframe/stack-shared";
|
||||
import { TeamPermissionDefinitionsCrud, TeamPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/team-permissions";
|
||||
import { UserPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/user-permissions";
|
||||
import { ProjectPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
|
||||
import { groupBy } from "@stackframe/stack-shared/dist/utils/arrays";
|
||||
import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { stringCompare, typedToLowercase, typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings";
|
||||
@ -42,7 +42,7 @@ type ExtendedTeamPermissionDefinition = TeamPermissionDefinitionsCrud["Admin"]["
|
||||
__database_id: string,
|
||||
__is_default_team_member_permission?: boolean,
|
||||
__is_default_team_creator_permission?: boolean,
|
||||
__is_default_user_permission?: boolean,
|
||||
__is_default_project_permission?: boolean,
|
||||
};
|
||||
|
||||
export function teamPermissionDefinitionJsonFromDbType(db: Prisma.PermissionGetPayload<{ include: typeof fullPermissionInclude }>): ExtendedTeamPermissionDefinition {
|
||||
@ -54,13 +54,13 @@ export function teamPermissionDefinitionJsonFromDbType(db: Prisma.PermissionGetP
|
||||
export function teamPermissionDefinitionJsonFromRawDbType(db: any | Prisma.PermissionGetPayload<{ include: typeof fullPermissionInclude }>): ExtendedTeamPermissionDefinition {
|
||||
if (!db.projectConfigId && !db.teamId) throw new StackAssertionError(`Permission DB object should have either projectConfigId or teamId`, { db });
|
||||
if (db.projectConfigId && db.teamId) throw new StackAssertionError(`Permission DB object should have either projectConfigId or teamId, not both`, { db });
|
||||
if (db.scope === "USER" && db.teamId) throw new StackAssertionError(`Permission DB object should not have teamId when scope is USER`, { db });
|
||||
if (db.scope === "PROJECT" && db.teamId) throw new StackAssertionError(`Permission DB object should not have teamId when scope is PROJECT`, { db });
|
||||
|
||||
return {
|
||||
__database_id: db.dbId,
|
||||
__is_default_team_member_permission: db.isDefaultTeamMemberPermission,
|
||||
__is_default_team_creator_permission: db.isDefaultTeamCreatorPermission,
|
||||
__is_default_user_permission: db.isDefaultUserPermission,
|
||||
__is_default_project_permission: db.isDefaultProjectPermission,
|
||||
id: db.queryableId,
|
||||
description: db.description || undefined,
|
||||
contained_permission_ids: db.parentEdges?.map((edge: any) => {
|
||||
@ -78,7 +78,7 @@ export function teamPermissionDefinitionJsonFromRawDbType(db: any | Prisma.Permi
|
||||
export function teamPermissionDefinitionJsonFromTeamSystemDbType(db: DBTeamSystemPermission, projectConfig: {
|
||||
teamCreateDefaultSystemPermissions: string[] | null,
|
||||
teamMemberDefaultSystemPermissions: string[] | null,
|
||||
userDefaultPermissions?: string[] | null,
|
||||
projectDefaultPermissions?: string[] | null,
|
||||
}): ExtendedTeamPermissionDefinition {
|
||||
if ((["teamMemberDefaultSystemPermissions", "teamCreateDefaultSystemPermissions"] as const).some(key => projectConfig[key] !== null && !Array.isArray(projectConfig[key]))) {
|
||||
throw new StackAssertionError(`Project config should have (nullable) array values for teamMemberDefaultSystemPermissions and teamCreateDefaultSystemPermissions`, { projectConfig });
|
||||
@ -88,7 +88,7 @@ export function teamPermissionDefinitionJsonFromTeamSystemDbType(db: DBTeamSyste
|
||||
__database_id: '$' + typedToLowercase(db),
|
||||
__is_default_team_member_permission: projectConfig.teamMemberDefaultSystemPermissions?.includes(db) ?? false,
|
||||
__is_default_team_creator_permission: projectConfig.teamCreateDefaultSystemPermissions?.includes(db) ?? false,
|
||||
__is_default_user_permission: projectConfig.userDefaultPermissions?.includes(db) ?? false,
|
||||
__is_default_project_permission: projectConfig.projectDefaultPermissions?.includes(db) ?? false,
|
||||
id: '$' + typedToLowercase(db),
|
||||
description: descriptionMap[db],
|
||||
contained_permission_ids: [] as string[],
|
||||
@ -99,7 +99,7 @@ async function getParentDbIds(
|
||||
tx: PrismaTransaction,
|
||||
options: {
|
||||
tenancy: Tenancy,
|
||||
scope: "TEAM" | "USER",
|
||||
scope: "TEAM" | "PROJECT",
|
||||
containedPermissionIds?: string[],
|
||||
}
|
||||
) {
|
||||
@ -324,7 +324,7 @@ export async function revokeTeamPermission(
|
||||
|
||||
export async function listPermissionDefinitions(
|
||||
tx: PrismaTransaction,
|
||||
scope: "TEAM" | "USER",
|
||||
scope: "TEAM" | "PROJECT",
|
||||
tenancy: Tenancy
|
||||
): Promise<(TeamPermissionDefinitionsCrud["Admin"]["Read"] & { __database_id: string })[]> {
|
||||
const projectConfig = await tx.projectConfig.findUnique({
|
||||
@ -356,7 +356,7 @@ export async function listPermissionDefinitions(
|
||||
export async function createPermissionDefinition(
|
||||
tx: PrismaTransaction,
|
||||
options: {
|
||||
scope: "TEAM" | "USER",
|
||||
scope: "TEAM" | "PROJECT",
|
||||
tenancy: Tenancy,
|
||||
data: {
|
||||
id: string,
|
||||
@ -402,7 +402,7 @@ export async function createPermissionDefinition(
|
||||
export async function updatePermissionDefinitions(
|
||||
tx: PrismaTransaction,
|
||||
options: {
|
||||
scope: "TEAM" | "USER",
|
||||
scope: "TEAM" | "PROJECT",
|
||||
tenancy: Tenancy,
|
||||
permissionId: string,
|
||||
data: {
|
||||
@ -477,7 +477,7 @@ export async function deletePermissionDefinition(
|
||||
|
||||
// User permission functions
|
||||
|
||||
export async function listUserPermissions(
|
||||
export async function listProjectPermissions(
|
||||
tx: PrismaTransaction,
|
||||
options: {
|
||||
tenancy: Tenancy,
|
||||
@ -485,8 +485,8 @@ export async function listUserPermissions(
|
||||
permissionId?: string,
|
||||
recursive: boolean,
|
||||
}
|
||||
): Promise<UserPermissionsCrud["Admin"]["Read"][]> {
|
||||
const permissionDefs = await listPermissionDefinitions(tx, "USER", options.tenancy);
|
||||
): Promise<ProjectPermissionsCrud["Admin"]["Read"][]> {
|
||||
const permissionDefs = await listPermissionDefinitions(tx, "PROJECT", options.tenancy);
|
||||
const permissionsMap = new Map(permissionDefs.map(p => [p.id, p]));
|
||||
const results = await tx.projectUserDirectPermission.findMany({
|
||||
where: {
|
||||
@ -528,7 +528,7 @@ export async function listUserPermissions(
|
||||
.filter(p => options.permissionId ? p.id === options.permissionId : true);
|
||||
}
|
||||
|
||||
export async function grantUserPermission(
|
||||
export async function grantProjectPermission(
|
||||
tx: PrismaTransaction,
|
||||
options: {
|
||||
tenancy: Tenancy,
|
||||
@ -579,7 +579,7 @@ export async function grantUserPermission(
|
||||
};
|
||||
}
|
||||
|
||||
export async function revokeUserPermission(
|
||||
export async function revokeProjectPermission(
|
||||
tx: PrismaTransaction,
|
||||
options: {
|
||||
tenancy: Tenancy,
|
||||
@ -610,10 +610,10 @@ export async function revokeUserPermission(
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants default user permissions to a user
|
||||
* Grants default project permissions to a user
|
||||
* This function should be called when a new user is created
|
||||
*/
|
||||
export async function grantDefaultUserPermissions(
|
||||
export async function grantDefaultProjectPermissions(
|
||||
tx: PrismaTransaction,
|
||||
options: {
|
||||
tenancy: Tenancy,
|
||||
@ -623,7 +623,7 @@ export async function grantDefaultUserPermissions(
|
||||
const defaultPermissions = await tx.permission.findMany({
|
||||
where: {
|
||||
projectConfigId: options.tenancy.config.id,
|
||||
isDefaultUserPermission: true,
|
||||
isDefaultProjectPermission: true,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -173,7 +173,7 @@ export function projectPrismaToCrud(
|
||||
.concat(prisma.config.teamMemberDefaultSystemPermissions.map(db => teamPermissionDefinitionJsonFromTeamSystemDbType(db, prisma.config)))
|
||||
.sort((a, b) => stringCompare(a.id, b.id))
|
||||
.map(perm => ({ id: perm.id })),
|
||||
user_default_permissions: prisma.config.permissions.filter(perm => perm.isDefaultUserPermission)
|
||||
user_default_permissions: prisma.config.permissions.filter(perm => perm.isDefaultProjectPermission)
|
||||
.map(teamPermissionDefinitionJsonFromDbType)
|
||||
.sort((a, b) => stringCompare(a.id, b.id))
|
||||
.map(perm => ({ id: perm.id })),
|
||||
@ -464,7 +464,7 @@ export function getProjectQuery(projectId: string): RawQuery<ProjectsCrud["Admin
|
||||
.filter(perm => perm.__is_default_team_member_permission)
|
||||
.map(perm => ({ id: perm.id })),
|
||||
user_default_permissions: teamPermissions
|
||||
.filter(perm => perm.__is_default_user_permission)
|
||||
.filter(perm => perm.__is_default_project_permission)
|
||||
.map(perm => ({ id: perm.id })),
|
||||
},
|
||||
};
|
||||
|
||||
@ -3,7 +3,7 @@ import { KnownErrors } from "@stackframe/stack-shared";
|
||||
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { ProviderType, sharedProviders, standardProviders } from "@stackframe/stack-shared/dist/utils/oauth";
|
||||
import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings";
|
||||
import { listUserPermissions, listUserTeamPermissions } from "./permissions";
|
||||
import { listProjectPermissions, listUserTeamPermissions } from "./permissions";
|
||||
import { Tenancy } from "./tenancies";
|
||||
import { PrismaTransaction } from "./types";
|
||||
|
||||
@ -113,7 +113,7 @@ export async function ensureUserTeamPermissionExists(
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureUserPermissionExists(
|
||||
export async function ensureProjectPermissionExists(
|
||||
tx: PrismaTransaction,
|
||||
options: {
|
||||
tenancy: Tenancy,
|
||||
@ -128,7 +128,7 @@ export async function ensureUserPermissionExists(
|
||||
userId: options.userId,
|
||||
});
|
||||
|
||||
const result = await listUserPermissions(tx, {
|
||||
const result = await listProjectPermissions(tx, {
|
||||
tenancy: options.tenancy,
|
||||
userId: options.userId,
|
||||
permissionId: options.permissionId,
|
||||
@ -139,7 +139,7 @@ export async function ensureUserPermissionExists(
|
||||
if (options.errorType === 'not-exist') {
|
||||
throw new KnownErrors.PermissionNotFound(options.permissionId);
|
||||
} else {
|
||||
throw new KnownErrors.UserPermissionRequired(options.userId, options.permissionId);
|
||||
throw new KnownErrors.ProjectPermissionRequired(options.userId, options.permissionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { projectPermissionCreatedWebhookEvent, projectPermissionDeletedWebhookEvent } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
|
||||
import { teamMembershipCreatedWebhookEvent, teamMembershipDeletedWebhookEvent } from "@stackframe/stack-shared/dist/interface/crud/team-memberships";
|
||||
import { teamPermissionCreatedWebhookEvent, teamPermissionDeletedWebhookEvent } from "@stackframe/stack-shared/dist/interface/crud/team-permissions";
|
||||
import { teamCreatedWebhookEvent, teamDeletedWebhookEvent, teamUpdatedWebhookEvent } from "@stackframe/stack-shared/dist/interface/crud/teams";
|
||||
import { userCreatedWebhookEvent, userDeletedWebhookEvent, userUpdatedWebhookEvent } from "@stackframe/stack-shared/dist/interface/crud/users";
|
||||
import { userPermissionCreatedWebhookEvent, userPermissionDeletedWebhookEvent } from "@stackframe/stack-shared/dist/interface/crud/user-permissions";
|
||||
import { WebhookEvent } from "@stackframe/stack-shared/dist/interface/webhooks";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
@ -74,5 +74,5 @@ export const sendTeamMembershipCreatedWebhook = createWebhookSender(teamMembersh
|
||||
export const sendTeamMembershipDeletedWebhook = createWebhookSender(teamMembershipDeletedWebhookEvent);
|
||||
export const sendTeamPermissionCreatedWebhook = createWebhookSender(teamPermissionCreatedWebhookEvent);
|
||||
export const sendTeamPermissionDeletedWebhook = createWebhookSender(teamPermissionDeletedWebhookEvent);
|
||||
export const sendUserPermissionCreatedWebhook = createWebhookSender(userPermissionCreatedWebhookEvent);
|
||||
export const sendUserPermissionDeletedWebhook = createWebhookSender(userPermissionDeletedWebhookEvent);
|
||||
export const sendProjectPermissionCreatedWebhook = createWebhookSender(projectPermissionCreatedWebhookEvent);
|
||||
export const sendProjectPermissionDeletedWebhook = createWebhookSender(projectPermissionDeletedWebhookEvent);
|
||||
|
||||
@ -10,12 +10,12 @@ import { useAdminApp } from "../use-admin-app";
|
||||
|
||||
export default function PageClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const permissions = stackAdminApp.useUserPermissionDefinitions();
|
||||
const permissions = stackAdminApp.useProjectPermissionDefinitions();
|
||||
const [createPermissionModalOpen, setCreatePermissionModalOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title="User Permissions"
|
||||
title="Project Permissions"
|
||||
actions={
|
||||
<Button onClick={() => setCreatePermissionModalOpen(true)}>
|
||||
Create Permission
|
||||
@ -24,7 +24,7 @@ export default function PageClient() {
|
||||
|
||||
<PermissionTable
|
||||
permissions={permissions}
|
||||
permissionType="user"
|
||||
permissionType="project"
|
||||
/>
|
||||
|
||||
<CreateDialog
|
||||
@ -40,7 +40,7 @@ function CreateDialog(props: {
|
||||
onOpenChange: (open: boolean) => void,
|
||||
}) {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const permissions = stackAdminApp.useUserPermissionDefinitions();
|
||||
const permissions = stackAdminApp.useProjectPermissionDefinitions();
|
||||
|
||||
const formSchema = yup.object({
|
||||
id: yup.string().defined()
|
||||
@ -62,7 +62,7 @@ function CreateDialog(props: {
|
||||
formSchema={formSchema}
|
||||
okButton={{ label: "Create" }}
|
||||
onSubmit={async (values) => {
|
||||
await stackAdminApp.createUserPermissionDefinition({
|
||||
await stackAdminApp.createProjectPermissionDefinition({
|
||||
id: values.id,
|
||||
description: values.description,
|
||||
containedPermissionIds: values.containedPermissionIds,
|
||||
@ -112,9 +112,9 @@ const navigationItems: (Label | Item | Hidden)[] = [
|
||||
type: 'item'
|
||||
},
|
||||
{
|
||||
name: "User Permissions",
|
||||
href: "/user-permissions",
|
||||
regex: /^\/projects\/[^\/]+\/user-permissions$/,
|
||||
name: "Project Permissions",
|
||||
href: "/project-permissions",
|
||||
regex: /^\/projects\/[^\/]+\/project-permissions$/,
|
||||
icon: LockKeyhole,
|
||||
type: 'item'
|
||||
},
|
||||
|
||||
@ -14,7 +14,7 @@ type AdminPermissionDefinition = {
|
||||
containedPermissionIds: string[],
|
||||
};
|
||||
|
||||
type PermissionType = 'user' | 'team';
|
||||
type PermissionType = 'project' | 'team';
|
||||
|
||||
function toolbarRender<TData>(table: Table<TData>) {
|
||||
return (
|
||||
@ -31,8 +31,8 @@ function EditDialog(props: {
|
||||
permissionType: PermissionType,
|
||||
}) {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const permissions = props.permissionType === 'user'
|
||||
? stackAdminApp.useUserPermissionDefinitions()
|
||||
const permissions = props.permissionType === 'project'
|
||||
? stackAdminApp.useProjectPermissionDefinitions()
|
||||
: stackAdminApp.useTeamPermissionDefinitions();
|
||||
const currentPermission = permissions.find((p) => p.id === props.selectedPermissionId);
|
||||
if (!currentPermission) {
|
||||
@ -62,10 +62,6 @@ function EditDialog(props: {
|
||||
})
|
||||
}).default(currentPermission);
|
||||
|
||||
const updatePermission = props.permissionType === 'user'
|
||||
? stackAdminApp.updateUserPermissionDefinition
|
||||
: stackAdminApp.updateTeamPermissionDefinition;
|
||||
|
||||
return <SmartFormDialog
|
||||
open={props.open}
|
||||
onOpenChange={props.onOpenChange}
|
||||
@ -74,7 +70,11 @@ function EditDialog(props: {
|
||||
okButton={{ label: "Save" }}
|
||||
onSubmit={(values) => {
|
||||
runAsynchronously(async () => {
|
||||
await updatePermission(props.selectedPermissionId, values);
|
||||
if (props.permissionType === 'project') {
|
||||
await stackAdminApp.updateProjectPermissionDefinition(props.selectedPermissionId, values);
|
||||
} else {
|
||||
await stackAdminApp.updateTeamPermissionDefinition(props.selectedPermissionId, values);
|
||||
}
|
||||
});
|
||||
}}
|
||||
cancelButton
|
||||
@ -87,10 +87,7 @@ function DeleteDialog<T extends AdminPermissionDefinition>(props: {
|
||||
onOpenChange: (open: boolean) => void,
|
||||
permissionType: PermissionType,
|
||||
}) {
|
||||
const stackApp = useAdminApp();
|
||||
const deletePermission = props.permissionType === 'user'
|
||||
? stackApp.deleteUserPermissionDefinition
|
||||
: stackApp.deleteTeamPermissionDefinition;
|
||||
const stackAdminApp = useAdminApp();
|
||||
|
||||
return <ActionDialog
|
||||
open={props.open}
|
||||
@ -98,7 +95,13 @@ function DeleteDialog<T extends AdminPermissionDefinition>(props: {
|
||||
title="Delete Permission"
|
||||
danger
|
||||
cancelButton
|
||||
okButton={{ label: "Delete Permission", onClick: async () => { await deletePermission(props.permission.id); } }}
|
||||
okButton={{ label: "Delete Permission", onClick: async () => {
|
||||
if (props.permissionType === 'project') {
|
||||
await stackAdminApp.deleteProjectPermissionDefinition(props.permission.id);
|
||||
} else {
|
||||
await stackAdminApp.deleteTeamPermissionDefinition(props.permission.id);
|
||||
}
|
||||
} }}
|
||||
confirmText="I understand this will remove the permission from all users and other permissions that contain it."
|
||||
>
|
||||
{`Are you sure you want to delete the permission "${props.permission.id}"?`}
|
||||
|
||||
@ -6,7 +6,7 @@ it("lists all the user permissions", async ({ expect }) => {
|
||||
backendContext.set({ projectKeys: InternalProjectKeys });
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
|
||||
const response = await niceBackendFetch(`/api/v1/user-permission-definitions`, {
|
||||
const response = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
|
||||
accessType: "admin",
|
||||
method: "GET",
|
||||
headers: {
|
||||
@ -29,7 +29,7 @@ it("creates, updates, and deletes a new user permission", async ({ expect }) =>
|
||||
backendContext.set({ projectKeys: InternalProjectKeys });
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
|
||||
const response1 = await niceBackendFetch(`/api/v1/user-permission-definitions`, {
|
||||
const response1 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
|
||||
accessType: "admin",
|
||||
method: "POST",
|
||||
body: {
|
||||
@ -51,7 +51,7 @@ it("creates, updates, and deletes a new user permission", async ({ expect }) =>
|
||||
`);
|
||||
|
||||
// create another permission with contained permissions
|
||||
const response2 = await niceBackendFetch(`/api/v1/user-permission-definitions`, {
|
||||
const response2 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
|
||||
accessType: "admin",
|
||||
method: "POST",
|
||||
body: {
|
||||
@ -74,7 +74,7 @@ it("creates, updates, and deletes a new user permission", async ({ expect }) =>
|
||||
`);
|
||||
|
||||
// test recursive case
|
||||
const response3 = await niceBackendFetch(`/api/v1/user-permission-definitions`, {
|
||||
const response3 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
|
||||
accessType: "admin",
|
||||
method: "POST",
|
||||
body: {
|
||||
@ -98,7 +98,7 @@ it("creates, updates, and deletes a new user permission", async ({ expect }) =>
|
||||
`);
|
||||
|
||||
// list all permissions again
|
||||
const response4 = await niceBackendFetch(`/api/v1/user-permission-definitions`, {
|
||||
const response4 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
|
||||
accessType: "admin",
|
||||
method: "GET",
|
||||
headers: {
|
||||
@ -130,7 +130,7 @@ it("creates, updates, and deletes a new user permission", async ({ expect }) =>
|
||||
`);
|
||||
|
||||
// delete the permission
|
||||
const response5 = await niceBackendFetch(`/api/v1/user-permission-definitions/p1`, {
|
||||
const response5 = await niceBackendFetch(`/api/v1/project-permission-definitions/p1`, {
|
||||
accessType: "admin",
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@ -146,7 +146,7 @@ it("creates, updates, and deletes a new user permission", async ({ expect }) =>
|
||||
`);
|
||||
|
||||
// list all permissions again
|
||||
const response6 = await niceBackendFetch(`/api/v1/user-permission-definitions`, {
|
||||
const response6 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
|
||||
accessType: "admin",
|
||||
method: "GET",
|
||||
headers: {
|
||||
@ -1,11 +1,11 @@
|
||||
import { wait } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { it } from "../../../../helpers";
|
||||
import { ApiKey, Auth, InternalProjectKeys, Project, Team, Webhook, backendContext, niceBackendFetch } from "../../../backend-helpers";
|
||||
import { ApiKey, Auth, InternalProjectKeys, Project, Webhook, backendContext, niceBackendFetch } from "../../../backend-helpers";
|
||||
|
||||
it("is not allowed to list permissions from the other users on the client", async ({ expect }) => {
|
||||
await Auth.Otp.signIn();
|
||||
|
||||
const response = await niceBackendFetch(`/api/v1/user-permissions`, {
|
||||
const response = await niceBackendFetch(`/api/v1/project-permissions`, {
|
||||
accessType: "client",
|
||||
method: "GET",
|
||||
});
|
||||
@ -21,7 +21,7 @@ it("is not allowed to list permissions from the other users on the client", asyn
|
||||
it("is not allowed to grant non-existing permission to a user on the server", async ({ expect }) => {
|
||||
const { userId } = await Auth.Otp.signIn();
|
||||
|
||||
const response = await niceBackendFetch(`/api/v1/user-permissions/${userId}/does_not_exist`, {
|
||||
const response = await niceBackendFetch(`/api/v1/project-permissions/${userId}/does_not_exist`, {
|
||||
accessType: "server",
|
||||
method: "POST",
|
||||
body: {},
|
||||
@ -47,7 +47,7 @@ it("can create a new permission and grant it to a user on the server", async ({
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken({ config: { magic_link_enabled: true } });
|
||||
|
||||
// create a permission child
|
||||
await niceBackendFetch(`/api/v1/user-permission-definitions`, {
|
||||
await niceBackendFetch(`/api/v1/project-permission-definitions`, {
|
||||
accessType: "admin",
|
||||
method: "POST",
|
||||
body: {
|
||||
@ -60,7 +60,7 @@ it("can create a new permission and grant it to a user on the server", async ({
|
||||
});
|
||||
|
||||
// create a permission parent
|
||||
await niceBackendFetch(`/api/v1/user-permission-definitions`, {
|
||||
await niceBackendFetch(`/api/v1/project-permission-definitions`, {
|
||||
accessType: "admin",
|
||||
method: "POST",
|
||||
body: {
|
||||
@ -78,7 +78,7 @@ it("can create a new permission and grant it to a user on the server", async ({
|
||||
const { userId } = await Auth.Password.signUpWithEmail({ password: 'test1234' });
|
||||
|
||||
// list current permissions
|
||||
const response1 = await niceBackendFetch(`/api/v1/user-permissions?user_id=me`, {
|
||||
const response1 = await niceBackendFetch(`/api/v1/project-permissions?user_id=me`, {
|
||||
accessType: "client",
|
||||
method: "GET",
|
||||
});
|
||||
@ -94,7 +94,7 @@ it("can create a new permission and grant it to a user on the server", async ({
|
||||
`);
|
||||
|
||||
// grant new permission
|
||||
const response2 = await niceBackendFetch(`/api/v1/user-permissions/${userId}/parent`, {
|
||||
const response2 = await niceBackendFetch(`/api/v1/project-permissions/${userId}/parent`, {
|
||||
accessType: "server",
|
||||
method: "POST",
|
||||
body: {},
|
||||
@ -111,7 +111,7 @@ it("can create a new permission and grant it to a user on the server", async ({
|
||||
`);
|
||||
|
||||
// list current permissions (should have the new permission)
|
||||
const response3 = await niceBackendFetch(`/api/v1/user-permissions?user_id=me`, {
|
||||
const response3 = await niceBackendFetch(`/api/v1/project-permissions?user_id=me`, {
|
||||
accessType: "client",
|
||||
method: "GET",
|
||||
});
|
||||
@ -136,7 +136,7 @@ it("can customize default user permissions", async ({ expect }) => {
|
||||
await Auth.Otp.signIn();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
|
||||
const response1 = await niceBackendFetch(`/api/v1/user-permission-definitions`, {
|
||||
const response1 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
|
||||
accessType: "admin",
|
||||
method: "POST",
|
||||
body: {
|
||||
@ -202,7 +202,7 @@ it("can customize default user permissions", async ({ expect }) => {
|
||||
// sign up a new user
|
||||
const { userId } = await Auth.Password.signUpWithEmail({ password: 'test1234' });
|
||||
// list permissions for the new user
|
||||
const response3 = await niceBackendFetch(`/api/v1/user-permissions?user_id=${userId}`, {
|
||||
const response3 = await niceBackendFetch(`/api/v1/project-permissions?user_id=${userId}`, {
|
||||
accessType: "client",
|
||||
method: "GET",
|
||||
});
|
||||
@ -223,18 +223,18 @@ it("can customize default user permissions", async ({ expect }) => {
|
||||
`);
|
||||
});
|
||||
|
||||
it("should trigger user permission webhook when a permission is granted to a user", async ({ expect }) => {
|
||||
it("should trigger project permission webhook when a permission is granted to a user", async ({ expect }) => {
|
||||
const { projectId, svixToken, endpointId } = await Webhook.createProjectWithEndpoint();
|
||||
|
||||
const { userId } = await Auth.Otp.signIn();
|
||||
|
||||
await niceBackendFetch(`/api/v1/user-permission-definitions`, {
|
||||
await niceBackendFetch(`/api/v1/project-permission-definitions`, {
|
||||
accessType: "admin",
|
||||
method: "POST",
|
||||
body: { id: 'test_permission' },
|
||||
});
|
||||
|
||||
const grantPermissionResponse = await niceBackendFetch(`/api/v1/user-permissions/${userId}/test_permission`, {
|
||||
const grantPermissionResponse = await niceBackendFetch(`/api/v1/project-permissions/${userId}/test_permission`, {
|
||||
accessType: "server",
|
||||
method: "POST",
|
||||
body: {},
|
||||
@ -245,39 +245,39 @@ it("should trigger user permission webhook when a permission is granted to a use
|
||||
await wait(3000);
|
||||
|
||||
const attemptResponse = await Webhook.listWebhookAttempts(projectId, endpointId, svixToken);
|
||||
const userPermissionCreatedEvent = attemptResponse.find(event => event.eventType === "user_permission.created");
|
||||
const projectPermissionCreatedEvent = attemptResponse.find(event => event.eventType === "project_permission.created");
|
||||
|
||||
expect(userPermissionCreatedEvent).toMatchInlineSnapshot(`
|
||||
expect(projectPermissionCreatedEvent).toMatchInlineSnapshot(`
|
||||
{
|
||||
"channels": null,
|
||||
"eventId": null,
|
||||
"eventType": "user_permission.created",
|
||||
"eventType": "project_permission.created",
|
||||
"id": "<stripped svix message id>",
|
||||
"payload": {
|
||||
"data": {
|
||||
"id": "test_permission",
|
||||
"user_id": "<stripped UUID>",
|
||||
},
|
||||
"type": "user_permission.created",
|
||||
"type": "project_permission.created",
|
||||
},
|
||||
"timestamp": <stripped field 'timestamp'>,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("should trigger user permission webhook when a permission is revoked from a user", async ({ expect }) => {
|
||||
it("should trigger project permission webhook when a permission is revoked from a user", async ({ expect }) => {
|
||||
const { projectId, svixToken, endpointId } = await Webhook.createProjectWithEndpoint();
|
||||
|
||||
const { userId } = await Auth.Otp.signIn();
|
||||
|
||||
await niceBackendFetch(`/api/v1/user-permission-definitions`, {
|
||||
await niceBackendFetch(`/api/v1/project-permission-definitions`, {
|
||||
accessType: "admin",
|
||||
method: "POST",
|
||||
body: { id: 'test_permission' },
|
||||
});
|
||||
|
||||
// First grant the permission
|
||||
const grantPermissionResponse = await niceBackendFetch(`/api/v1/user-permissions/${userId}/test_permission`, {
|
||||
const grantPermissionResponse = await niceBackendFetch(`/api/v1/project-permissions/${userId}/test_permission`, {
|
||||
accessType: "server",
|
||||
method: "POST",
|
||||
body: {},
|
||||
@ -286,7 +286,7 @@ it("should trigger user permission webhook when a permission is revoked from a u
|
||||
expect(grantPermissionResponse.status).toBe(201);
|
||||
|
||||
// Then revoke the permission
|
||||
const revokePermissionResponse = await niceBackendFetch(`/api/v1/user-permissions/${userId}/test_permission`, {
|
||||
const revokePermissionResponse = await niceBackendFetch(`/api/v1/project-permissions/${userId}/test_permission`, {
|
||||
accessType: "server",
|
||||
method: "DELETE",
|
||||
});
|
||||
@ -296,20 +296,20 @@ it("should trigger user permission webhook when a permission is revoked from a u
|
||||
await wait(3000);
|
||||
|
||||
const attemptResponse = await Webhook.listWebhookAttempts(projectId, endpointId, svixToken);
|
||||
const userPermissionDeletedEvent = attemptResponse.find(event => event.eventType === "user_permission.deleted");
|
||||
const projectPermissionDeletedEvent = attemptResponse.find(event => event.eventType === "project_permission.deleted");
|
||||
|
||||
expect(userPermissionDeletedEvent).toMatchInlineSnapshot(`
|
||||
expect(projectPermissionDeletedEvent).toMatchInlineSnapshot(`
|
||||
{
|
||||
"channels": null,
|
||||
"eventId": null,
|
||||
"eventType": "user_permission.deleted",
|
||||
"eventType": "project_permission.deleted",
|
||||
"id": "<stripped svix message id>",
|
||||
"payload": {
|
||||
"data": {
|
||||
"id": "test_permission",
|
||||
"user_id": "<stripped UUID>",
|
||||
},
|
||||
"type": "user_permission.deleted",
|
||||
"type": "project_permission.deleted",
|
||||
},
|
||||
"timestamp": <stripped field 'timestamp'>,
|
||||
}
|
||||
@ -2,10 +2,10 @@ import { InternalSession } from "../sessions";
|
||||
import { ApiKeysCrud } from "./crud/api-keys";
|
||||
import { EmailTemplateCrud, EmailTemplateType } from "./crud/email-templates";
|
||||
import { InternalEmailsCrud } from "./crud/emails";
|
||||
import { ProjectPermissionDefinitionsCrud } from "./crud/project-permissions";
|
||||
import { ProjectsCrud } from "./crud/projects";
|
||||
import { SvixTokenCrud } from "./crud/svix-token";
|
||||
import { TeamPermissionDefinitionsCrud } from "./crud/team-permissions";
|
||||
import { UserPermissionDefinitionsCrud } from "./crud/user-permissions";
|
||||
import { ServerAuthApplicationOptions, StackServerInterface } from "./serverInterface";
|
||||
|
||||
export type AdminAuthApplicationOptions = ServerAuthApplicationOptions &(
|
||||
@ -194,15 +194,15 @@ export class StackAdminInterface extends StackServerInterface {
|
||||
);
|
||||
}
|
||||
|
||||
async listUserPermissionDefinitions(): Promise<UserPermissionDefinitionsCrud['Admin']['Read'][]> {
|
||||
const response = await this.sendAdminRequest(`/user-permission-definitions`, {}, null);
|
||||
const result = await response.json() as UserPermissionDefinitionsCrud['Admin']['List'];
|
||||
async listProjectPermissionDefinitions(): Promise<ProjectPermissionDefinitionsCrud['Admin']['Read'][]> {
|
||||
const response = await this.sendAdminRequest(`/project-permission-definitions`, {}, null);
|
||||
const result = await response.json() as ProjectPermissionDefinitionsCrud['Admin']['List'];
|
||||
return result.items;
|
||||
}
|
||||
|
||||
async createUserPermissionDefinition(data: UserPermissionDefinitionsCrud['Admin']['Create']): Promise<UserPermissionDefinitionsCrud['Admin']['Read']> {
|
||||
async createProjectPermissionDefinition(data: ProjectPermissionDefinitionsCrud['Admin']['Create']): Promise<ProjectPermissionDefinitionsCrud['Admin']['Read']> {
|
||||
const response = await this.sendAdminRequest(
|
||||
"/user-permission-definitions",
|
||||
"/project-permission-definitions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -215,9 +215,9 @@ export class StackAdminInterface extends StackServerInterface {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async updateUserPermissionDefinition(permissionId: string, data: UserPermissionDefinitionsCrud['Admin']['Update']): Promise<UserPermissionDefinitionsCrud['Admin']['Read']> {
|
||||
async updateProjectPermissionDefinition(permissionId: string, data: ProjectPermissionDefinitionsCrud['Admin']['Update']): Promise<ProjectPermissionDefinitionsCrud['Admin']['Read']> {
|
||||
const response = await this.sendAdminRequest(
|
||||
`/user-permission-definitions/${permissionId}`,
|
||||
`/project-permission-definitions/${permissionId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
@ -230,9 +230,9 @@ export class StackAdminInterface extends StackServerInterface {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async deleteUserPermissionDefinition(permissionId: string): Promise<void> {
|
||||
async deleteProjectPermissionDefinition(permissionId: string): Promise<void> {
|
||||
await this.sendAdminRequest(
|
||||
`/user-permission-definitions/${permissionId}`,
|
||||
`/project-permission-definitions/${permissionId}`,
|
||||
{ method: "DELETE" },
|
||||
null,
|
||||
);
|
||||
|
||||
@ -19,6 +19,7 @@ import { InternalProjectsCrud, ProjectsCrud } from './crud/projects';
|
||||
import { SessionsCrud } from './crud/sessions';
|
||||
import { TeamInvitationCrud } from './crud/team-invitation';
|
||||
import { TeamMemberProfilesCrud } from './crud/team-member-profiles';
|
||||
import { ProjectPermissionsCrud } from './crud/project-permissions';
|
||||
import { TeamPermissionsCrud } from './crud/team-permissions';
|
||||
import { TeamsCrud } from './crud/teams';
|
||||
|
||||
@ -1183,6 +1184,21 @@ export class StackClientInterface {
|
||||
return result.items;
|
||||
}
|
||||
|
||||
async listCurrentUserProjectPermissions(
|
||||
options: {
|
||||
recursive: boolean,
|
||||
},
|
||||
session: InternalSession
|
||||
): Promise<ProjectPermissionsCrud['Client']['Read'][]> {
|
||||
const response = await this.sendClientRequest(
|
||||
`/project-permissions?user_id=me&recursive=${options.recursive}`,
|
||||
{},
|
||||
session,
|
||||
);
|
||||
const result = await response.json() as ProjectPermissionsCrud['Client']['List'];
|
||||
return result.items;
|
||||
}
|
||||
|
||||
async listCurrentUserTeams(session: InternalSession): Promise<TeamsCrud["Client"]["Read"][]> {
|
||||
const response = await this.sendClientRequest(
|
||||
"/teams?user_id=me",
|
||||
|
||||
118
packages/stack-shared/src/interface/crud/project-permissions.ts
Normal file
118
packages/stack-shared/src/interface/crud/project-permissions.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { CrudTypeOf, createCrud } from "../../crud";
|
||||
import * as schemaFields from "../../schema-fields";
|
||||
import { yupMixed, yupObject } from "../../schema-fields";
|
||||
import { WebhookEvent } from "../webhooks";
|
||||
|
||||
// =============== Project permissions =================
|
||||
|
||||
export const projectPermissionsCrudClientReadSchema = yupObject({
|
||||
id: schemaFields.permissionDefinitionIdSchema.defined(),
|
||||
user_id: schemaFields.userIdSchema.defined(),
|
||||
}).defined();
|
||||
|
||||
export const projectPermissionsCrudServerCreateSchema = yupObject({
|
||||
}).defined();
|
||||
|
||||
export const projectPermissionsCrudServerDeleteSchema = yupMixed();
|
||||
|
||||
export const projectPermissionsCrud = createCrud({
|
||||
clientReadSchema: projectPermissionsCrudClientReadSchema,
|
||||
serverCreateSchema: projectPermissionsCrudServerCreateSchema,
|
||||
serverDeleteSchema: projectPermissionsCrudServerDeleteSchema,
|
||||
docs: {
|
||||
clientList: {
|
||||
summary: "List project permissions",
|
||||
description: "List global permissions of the current user. `user_id=me` must be set for client requests. `(user_id, permission_id)` together uniquely identify a permission.",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
serverList: {
|
||||
summary: "List project permissions",
|
||||
description: "Query and filter the permission with `user_id` and `permission_id`. `(user_id, permission_id)` together uniquely identify a permission.",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
serverCreate: {
|
||||
summary: "Grant a global permission to a user",
|
||||
description: "Grant a global permission to a user (the permission must be created first on the Stack dashboard)",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
serverDelete: {
|
||||
summary: "Revoke a global permission from a user",
|
||||
description: "Revoke a global permission from a user",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
},
|
||||
});
|
||||
export type ProjectPermissionsCrud = CrudTypeOf<typeof projectPermissionsCrud>;
|
||||
|
||||
export const projectPermissionCreatedWebhookEvent = {
|
||||
type: "project_permission.created",
|
||||
schema: projectPermissionsCrud.server.readSchema,
|
||||
metadata: {
|
||||
summary: "Project Permission Created",
|
||||
description: "This event is triggered when a project permission is created.",
|
||||
tags: ["Users"],
|
||||
},
|
||||
} satisfies WebhookEvent<typeof projectPermissionsCrud.server.readSchema>;
|
||||
|
||||
export const projectPermissionDeletedWebhookEvent = {
|
||||
type: "project_permission.deleted",
|
||||
schema: projectPermissionsCrud.server.readSchema,
|
||||
metadata: {
|
||||
summary: "Project Permission Deleted",
|
||||
description: "This event is triggered when a project permission is deleted.",
|
||||
tags: ["Users"],
|
||||
},
|
||||
} satisfies WebhookEvent<typeof projectPermissionsCrud.server.readSchema>;
|
||||
|
||||
// =============== Project permission definitions =================
|
||||
|
||||
export const projectPermissionDefinitionsCrudAdminReadSchema = yupObject({
|
||||
id: schemaFields.permissionDefinitionIdSchema.defined(),
|
||||
description: schemaFields.teamPermissionDescriptionSchema.optional(),
|
||||
contained_permission_ids: schemaFields.containedPermissionIdsSchema.defined(),
|
||||
}).defined();
|
||||
|
||||
export const projectPermissionDefinitionsCrudAdminCreateSchema = yupObject({
|
||||
id: schemaFields.customPermissionDefinitionIdSchema.defined(),
|
||||
description: schemaFields.teamPermissionDescriptionSchema.optional(),
|
||||
contained_permission_ids: schemaFields.containedPermissionIdsSchema.optional(),
|
||||
}).defined();
|
||||
|
||||
export const projectPermissionDefinitionsCrudAdminUpdateSchema = yupObject({
|
||||
id: schemaFields.customPermissionDefinitionIdSchema.optional(),
|
||||
description: schemaFields.teamPermissionDescriptionSchema.optional(),
|
||||
contained_permission_ids: schemaFields.containedPermissionIdsSchema.optional(),
|
||||
}).defined();
|
||||
|
||||
export const projectPermissionDefinitionsCrudAdminDeleteSchema = yupMixed();
|
||||
|
||||
export const projectPermissionDefinitionsCrud = createCrud({
|
||||
adminReadSchema: projectPermissionDefinitionsCrudAdminReadSchema,
|
||||
adminCreateSchema: projectPermissionDefinitionsCrudAdminCreateSchema,
|
||||
adminUpdateSchema: projectPermissionDefinitionsCrudAdminUpdateSchema,
|
||||
adminDeleteSchema: projectPermissionDefinitionsCrudAdminDeleteSchema,
|
||||
docs: {
|
||||
adminList: {
|
||||
summary: "List project permission definitions",
|
||||
description: "Query and filter project permission definitions (the equivalent of listing permissions on the Stack dashboard)",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
adminCreate: {
|
||||
summary: "Create a new project permission definition",
|
||||
description: "Create a new project permission definition (the equivalent of creating a new permission on the Stack dashboard)",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
adminUpdate: {
|
||||
summary: "Update a project permission definition",
|
||||
description: "Update a project permission definition (the equivalent of updating a permission on the Stack dashboard)",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
adminDelete: {
|
||||
summary: "Delete a project permission definition",
|
||||
description: "Delete a project permission definition (the equivalent of deleting a permission on the Stack dashboard)",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type ProjectPermissionDefinitionsCrud = CrudTypeOf<typeof projectPermissionDefinitionsCrud>;
|
||||
@ -6,7 +6,7 @@ import { WebhookEvent } from "../webhooks";
|
||||
// =============== Team permissions =================
|
||||
|
||||
export const teamPermissionsCrudClientReadSchema = yupObject({
|
||||
id: schemaFields.teamPermissionDefinitionIdSchema.defined(),
|
||||
id: schemaFields.permissionDefinitionIdSchema.defined(),
|
||||
user_id: schemaFields.userIdSchema.defined(),
|
||||
team_id: schemaFields.teamIdSchema.defined(),
|
||||
}).defined();
|
||||
@ -68,19 +68,19 @@ export const teamPermissionDeletedWebhookEvent = {
|
||||
// =============== Team permission definitions =================
|
||||
|
||||
export const teamPermissionDefinitionsCrudAdminReadSchema = yupObject({
|
||||
id: schemaFields.teamPermissionDefinitionIdSchema.defined(),
|
||||
id: schemaFields.permissionDefinitionIdSchema.defined(),
|
||||
description: schemaFields.teamPermissionDescriptionSchema.optional(),
|
||||
contained_permission_ids: schemaFields.containedPermissionIdsSchema.defined(),
|
||||
}).defined();
|
||||
|
||||
export const teamPermissionDefinitionsCrudAdminCreateSchema = yupObject({
|
||||
id: schemaFields.customTeamPermissionDefinitionIdSchema.defined(),
|
||||
id: schemaFields.customPermissionDefinitionIdSchema.defined(),
|
||||
description: schemaFields.teamPermissionDescriptionSchema.optional(),
|
||||
contained_permission_ids: schemaFields.containedPermissionIdsSchema.optional(),
|
||||
}).defined();
|
||||
|
||||
export const teamPermissionDefinitionsCrudAdminUpdateSchema = yupObject({
|
||||
id: schemaFields.customTeamPermissionDefinitionIdSchema.optional(),
|
||||
id: schemaFields.customPermissionDefinitionIdSchema.optional(),
|
||||
description: schemaFields.teamPermissionDescriptionSchema.optional(),
|
||||
contained_permission_ids: schemaFields.containedPermissionIdsSchema.optional(),
|
||||
}).defined();
|
||||
|
||||
@ -1,118 +0,0 @@
|
||||
import { CrudTypeOf, createCrud } from "../../crud";
|
||||
import * as schemaFields from "../../schema-fields";
|
||||
import { yupMixed, yupObject } from "../../schema-fields";
|
||||
import { WebhookEvent } from "../webhooks";
|
||||
|
||||
// =============== User permissions =================
|
||||
|
||||
export const userPermissionsCrudClientReadSchema = yupObject({
|
||||
id: schemaFields.teamPermissionDefinitionIdSchema.defined(),
|
||||
user_id: schemaFields.userIdSchema.defined(),
|
||||
}).defined();
|
||||
|
||||
export const userPermissionsCrudServerCreateSchema = yupObject({
|
||||
}).defined();
|
||||
|
||||
export const userPermissionsCrudServerDeleteSchema = yupMixed();
|
||||
|
||||
export const userPermissionsCrud = createCrud({
|
||||
clientReadSchema: userPermissionsCrudClientReadSchema,
|
||||
serverCreateSchema: userPermissionsCrudServerCreateSchema,
|
||||
serverDeleteSchema: userPermissionsCrudServerDeleteSchema,
|
||||
docs: {
|
||||
clientList: {
|
||||
summary: "List user permissions",
|
||||
description: "List global permissions of the current user. `user_id=me` must be set for client requests. `(user_id, permission_id)` together uniquely identify a permission.",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
serverList: {
|
||||
summary: "List user permissions",
|
||||
description: "Query and filter the permission with `user_id` and `permission_id`. `(user_id, permission_id)` together uniquely identify a permission.",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
serverCreate: {
|
||||
summary: "Grant a global permission to a user",
|
||||
description: "Grant a global permission to a user (the permission must be created first on the Stack dashboard)",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
serverDelete: {
|
||||
summary: "Revoke a global permission from a user",
|
||||
description: "Revoke a global permission from a user",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
},
|
||||
});
|
||||
export type UserPermissionsCrud = CrudTypeOf<typeof userPermissionsCrud>;
|
||||
|
||||
export const userPermissionCreatedWebhookEvent = {
|
||||
type: "user_permission.created",
|
||||
schema: userPermissionsCrud.server.readSchema,
|
||||
metadata: {
|
||||
summary: "User Permission Created",
|
||||
description: "This event is triggered when a user permission is created.",
|
||||
tags: ["Users"],
|
||||
},
|
||||
} satisfies WebhookEvent<typeof userPermissionsCrud.server.readSchema>;
|
||||
|
||||
export const userPermissionDeletedWebhookEvent = {
|
||||
type: "user_permission.deleted",
|
||||
schema: userPermissionsCrud.server.readSchema,
|
||||
metadata: {
|
||||
summary: "User Permission Deleted",
|
||||
description: "This event is triggered when a user permission is deleted.",
|
||||
tags: ["Users"],
|
||||
},
|
||||
} satisfies WebhookEvent<typeof userPermissionsCrud.server.readSchema>;
|
||||
|
||||
// =============== User permission definitions =================
|
||||
|
||||
export const userPermissionDefinitionsCrudAdminReadSchema = yupObject({
|
||||
id: schemaFields.teamPermissionDefinitionIdSchema.defined(),
|
||||
description: schemaFields.teamPermissionDescriptionSchema.optional(),
|
||||
contained_permission_ids: schemaFields.containedPermissionIdsSchema.defined(),
|
||||
}).defined();
|
||||
|
||||
export const userPermissionDefinitionsCrudAdminCreateSchema = yupObject({
|
||||
id: schemaFields.customTeamPermissionDefinitionIdSchema.defined(),
|
||||
description: schemaFields.teamPermissionDescriptionSchema.optional(),
|
||||
contained_permission_ids: schemaFields.containedPermissionIdsSchema.optional(),
|
||||
}).defined();
|
||||
|
||||
export const userPermissionDefinitionsCrudAdminUpdateSchema = yupObject({
|
||||
id: schemaFields.customTeamPermissionDefinitionIdSchema.optional(),
|
||||
description: schemaFields.teamPermissionDescriptionSchema.optional(),
|
||||
contained_permission_ids: schemaFields.containedPermissionIdsSchema.optional(),
|
||||
}).defined();
|
||||
|
||||
export const userPermissionDefinitionsCrudAdminDeleteSchema = yupMixed();
|
||||
|
||||
export const userPermissionDefinitionsCrud = createCrud({
|
||||
adminReadSchema: userPermissionDefinitionsCrudAdminReadSchema,
|
||||
adminCreateSchema: userPermissionDefinitionsCrudAdminCreateSchema,
|
||||
adminUpdateSchema: userPermissionDefinitionsCrudAdminUpdateSchema,
|
||||
adminDeleteSchema: userPermissionDefinitionsCrudAdminDeleteSchema,
|
||||
docs: {
|
||||
adminList: {
|
||||
summary: "List user permission definitions",
|
||||
description: "Query and filter user permission definitions (the equivalent of listing permissions on the Stack dashboard)",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
adminCreate: {
|
||||
summary: "Create a new user permission definition",
|
||||
description: "Create a new user permission definition (the equivalent of creating a new permission on the Stack dashboard)",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
adminUpdate: {
|
||||
summary: "Update a user permission definition",
|
||||
description: "Update a user permission definition (the equivalent of updating a permission on the Stack dashboard)",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
adminDelete: {
|
||||
summary: "Delete a user permission definition",
|
||||
description: "Delete a user permission definition (the equivalent of deleting a permission on the Stack dashboard)",
|
||||
tags: ["Permissions"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type UserPermissionDefinitionsCrud = CrudTypeOf<typeof userPermissionDefinitionsCrud>;
|
||||
@ -15,6 +15,7 @@ import { SessionsCrud } from "./crud/sessions";
|
||||
import { TeamInvitationCrud } from "./crud/team-invitation";
|
||||
import { TeamMemberProfilesCrud } from "./crud/team-member-profiles";
|
||||
import { TeamMembershipsCrud } from "./crud/team-memberships";
|
||||
import { ProjectPermissionsCrud } from "./crud/project-permissions";
|
||||
import { TeamPermissionsCrud } from "./crud/team-permissions";
|
||||
import { TeamsCrud } from "./crud/teams";
|
||||
import { UsersCrud } from "./crud/users";
|
||||
@ -195,6 +196,25 @@ export class StackServerInterface extends StackClientInterface {
|
||||
return result.items;
|
||||
}
|
||||
|
||||
async listServerProjectPermissions(
|
||||
options: {
|
||||
userId?: string,
|
||||
recursive: boolean,
|
||||
},
|
||||
session: InternalSession | null,
|
||||
): Promise<ProjectPermissionsCrud['Server']['Read'][]> {
|
||||
const response = await this.sendServerRequest(
|
||||
`/project-permissions?${new URLSearchParams(filterUndefined({
|
||||
user_id: options.userId,
|
||||
recursive: options.recursive.toString(),
|
||||
}))}`,
|
||||
{},
|
||||
session,
|
||||
);
|
||||
const result = await response.json() as ProjectPermissionsCrud['Server']['List'];
|
||||
return result.items;
|
||||
}
|
||||
|
||||
async listServerUsers(options: {
|
||||
cursor?: string,
|
||||
limit?: number,
|
||||
|
||||
@ -1104,9 +1104,9 @@ const TeamMembershipAlreadyExists = createKnownErrorConstructor(
|
||||
() => [] as const,
|
||||
);
|
||||
|
||||
const UserPermissionRequired = createKnownErrorConstructor(
|
||||
const ProjectPermissionRequired = createKnownErrorConstructor(
|
||||
KnownError,
|
||||
"USER_PERMISSION_REQUIRED",
|
||||
"PROJECT_PERMISSION_REQUIRED",
|
||||
(userId, permissionId) => [
|
||||
401,
|
||||
`User ${userId} does not have permission ${permissionId}.`,
|
||||
@ -1308,7 +1308,7 @@ export const KnownErrors = {
|
||||
InvalidTotpCode,
|
||||
UserAuthenticationRequired,
|
||||
TeamMembershipAlreadyExists,
|
||||
UserPermissionRequired,
|
||||
ProjectPermissionRequired,
|
||||
TeamPermissionRequired,
|
||||
InvalidSharedOAuthProviderId,
|
||||
InvalidStandardOAuthProviderId,
|
||||
|
||||
@ -359,7 +359,7 @@ export const teamSystemPermissions = [
|
||||
'$remove_members',
|
||||
'$invite_members',
|
||||
] as const;
|
||||
export const teamPermissionDefinitionIdSchema = yupString()
|
||||
export const permissionDefinitionIdSchema = yupString()
|
||||
.matches(/^\$?[a-z0-9_:]+$/, 'Only lowercase letters, numbers, ":", "_" and optional "$" at the beginning are allowed')
|
||||
.test('is-system-permission', 'System permissions must start with a dollar sign', (value, ctx) => {
|
||||
if (!value) return true;
|
||||
@ -369,11 +369,11 @@ export const teamPermissionDefinitionIdSchema = yupString()
|
||||
return true;
|
||||
})
|
||||
.meta({ openapiField: { description: `The permission ID used to uniquely identify a permission. Can either be a custom permission with lowercase letters, numbers, \`:\`, and \`_\` characters, or one of the system permissions: ${teamSystemPermissions.map(x => `\`${x}\``).join(', ')}`, exampleValue: 'read_secret_info' } });
|
||||
export const customTeamPermissionDefinitionIdSchema = yupString()
|
||||
export const customPermissionDefinitionIdSchema = yupString()
|
||||
.matches(/^[a-z0-9_:]+$/, 'Only lowercase letters, numbers, ":", "_" are allowed')
|
||||
.meta({ openapiField: { description: 'The permission ID used to uniquely identify a permission. Can only contain lowercase letters, numbers, ":", and "_" characters', exampleValue: 'read_secret_info' } });
|
||||
export const teamPermissionDescriptionSchema = yupString().meta({ openapiField: { description: 'A human-readable description of the permission', exampleValue: 'Read secret information' } });
|
||||
export const containedPermissionIdsSchema = yupArray(teamPermissionDefinitionIdSchema.defined()).meta({ openapiField: { description: 'The IDs of the permissions that are contained in this permission', exampleValue: ['read_public_info'] } });
|
||||
export const containedPermissionIdsSchema = yupArray(permissionDefinitionIdSchema.defined()).meta({ openapiField: { description: 'The IDs of the permissions that are contained in this permission', exampleValue: ['read_public_info'] } });
|
||||
|
||||
// Teams
|
||||
export const teamIdSchema = yupString().uuid().meta({ openapiField: { description: _idDescription('team'), exampleValue: 'ad962777-8244-496a-b6a2-e0c6a449c79e' } });
|
||||
|
||||
@ -12,7 +12,7 @@ import { AdminSentEmail } from "../..";
|
||||
import { ApiKey, ApiKeyBase, ApiKeyBaseCrudRead, ApiKeyCreateOptions, ApiKeyFirstView, apiKeyCreateOptionsToCrud } from "../../api-keys";
|
||||
import { EmailConfig, stackAppInternalsSymbol } from "../../common";
|
||||
import { AdminEmailTemplate, AdminEmailTemplateUpdateOptions, adminEmailTemplateUpdateOptionsToCrud } from "../../email-templates";
|
||||
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions, AdminUserPermission, AdminUserPermissionDefinition, AdminUserPermissionDefinitionCreateOptions, AdminUserPermissionDefinitionUpdateOptions, adminTeamPermissionDefinitionCreateOptionsToCrud, adminTeamPermissionDefinitionUpdateOptionsToCrud, adminUserPermissionDefinitionCreateOptionsToCrud, adminUserPermissionDefinitionUpdateOptionsToCrud } from "../../permissions";
|
||||
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions, AdminProjectPermission, AdminProjectPermissionDefinition, AdminProjectPermissionDefinitionCreateOptions, AdminProjectPermissionDefinitionUpdateOptions, adminTeamPermissionDefinitionCreateOptionsToCrud, adminTeamPermissionDefinitionUpdateOptionsToCrud, adminProjectPermissionDefinitionCreateOptionsToCrud, adminProjectPermissionDefinitionUpdateOptionsToCrud } from "../../permissions";
|
||||
import { AdminOwnedProject, AdminProject, AdminProjectUpdateOptions, adminProjectUpdateOptionsToCrud } from "../../projects";
|
||||
import { StackAdminApp, StackAdminAppConstructorOptions } from "../interfaces/admin-app";
|
||||
import { clientVersion, createCache, getBaseUrl, getDefaultProjectId, getDefaultPublishableClientKey, getDefaultSecretServerKey, getDefaultSuperSecretAdminKey } from "./common";
|
||||
@ -37,8 +37,8 @@ export class _StackAdminAppImplIncomplete<HasTokenStore extends boolean, Project
|
||||
private readonly _adminTeamPermissionDefinitionsCache = createCache(async () => {
|
||||
return await this._interface.listTeamPermissionDefinitions();
|
||||
});
|
||||
private readonly _adminUserPermissionDefinitionsCache = createCache(async () => {
|
||||
return await this._interface.listUserPermissionDefinitions();
|
||||
private readonly _adminProjectPermissionDefinitionsCache = createCache(async () => {
|
||||
return await this._interface.listProjectPermissionDefinitions();
|
||||
});
|
||||
private readonly _svixTokenCache = createCache(async () => {
|
||||
return await this._interface.getSvixToken();
|
||||
@ -296,32 +296,32 @@ export class _StackAdminAppImplIncomplete<HasTokenStore extends boolean, Project
|
||||
}
|
||||
// END_PLATFORM
|
||||
|
||||
async createUserPermissionDefinition(data: AdminUserPermissionDefinitionCreateOptions): Promise<AdminUserPermission> {
|
||||
const crud = await this._interface.createUserPermissionDefinition(adminUserPermissionDefinitionCreateOptionsToCrud(data));
|
||||
await this._adminUserPermissionDefinitionsCache.refresh([]);
|
||||
return this._serverUserPermissionDefinitionFromCrud(crud);
|
||||
async createProjectPermissionDefinition(data: AdminProjectPermissionDefinitionCreateOptions): Promise<AdminProjectPermission> {
|
||||
const crud = await this._interface.createProjectPermissionDefinition(adminProjectPermissionDefinitionCreateOptionsToCrud(data));
|
||||
await this._adminProjectPermissionDefinitionsCache.refresh([]);
|
||||
return this._serverProjectPermissionDefinitionFromCrud(crud);
|
||||
}
|
||||
|
||||
async updateUserPermissionDefinition(permissionId: string, data: AdminUserPermissionDefinitionUpdateOptions) {
|
||||
await this._interface.updateUserPermissionDefinition(permissionId, adminUserPermissionDefinitionUpdateOptionsToCrud(data));
|
||||
await this._adminUserPermissionDefinitionsCache.refresh([]);
|
||||
async updateProjectPermissionDefinition(permissionId: string, data: AdminProjectPermissionDefinitionUpdateOptions) {
|
||||
await this._interface.updateProjectPermissionDefinition(permissionId, adminProjectPermissionDefinitionUpdateOptionsToCrud(data));
|
||||
await this._adminProjectPermissionDefinitionsCache.refresh([]);
|
||||
}
|
||||
|
||||
async deleteUserPermissionDefinition(permissionId: string): Promise<void> {
|
||||
await this._interface.deleteUserPermissionDefinition(permissionId);
|
||||
await this._adminUserPermissionDefinitionsCache.refresh([]);
|
||||
async deleteProjectPermissionDefinition(permissionId: string): Promise<void> {
|
||||
await this._interface.deleteProjectPermissionDefinition(permissionId);
|
||||
await this._adminProjectPermissionDefinitionsCache.refresh([]);
|
||||
}
|
||||
|
||||
async listUserPermissionDefinitions(): Promise<AdminUserPermissionDefinition[]> {
|
||||
const crud = Result.orThrow(await this._adminUserPermissionDefinitionsCache.getOrWait([], "write-only"));
|
||||
return crud.map((p) => this._serverUserPermissionDefinitionFromCrud(p));
|
||||
async listProjectPermissionDefinitions(): Promise<AdminProjectPermissionDefinition[]> {
|
||||
const crud = Result.orThrow(await this._adminProjectPermissionDefinitionsCache.getOrWait([], "write-only"));
|
||||
return crud.map((p) => this._serverProjectPermissionDefinitionFromCrud(p));
|
||||
}
|
||||
|
||||
// IF_PLATFORM react-like
|
||||
useUserPermissionDefinitions(): AdminUserPermissionDefinition[] {
|
||||
const crud = useAsyncCache(this._adminUserPermissionDefinitionsCache, [], "useUserPermissions()");
|
||||
useProjectPermissionDefinitions(): AdminProjectPermissionDefinition[] {
|
||||
const crud = useAsyncCache(this._adminProjectPermissionDefinitionsCache, [], "useProjectPermissions()");
|
||||
return useMemo(() => {
|
||||
return crud.map((p) => this._serverUserPermissionDefinitionFromCrud(p));
|
||||
return crud.map((p) => this._serverProjectPermissionDefinitionFromCrud(p));
|
||||
}, [crud]);
|
||||
}
|
||||
// END_PLATFORM
|
||||
|
||||
@ -2,6 +2,7 @@ import { WebAuthnError, startAuthentication, startRegistration } from "@simplewe
|
||||
import { KnownErrors, StackClientInterface } from "@stackframe/stack-shared";
|
||||
import { ContactChannelsCrud } from "@stackframe/stack-shared/dist/interface/crud/contact-channels";
|
||||
import { CurrentUserCrud } from "@stackframe/stack-shared/dist/interface/crud/current-user";
|
||||
import { ProjectPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
|
||||
import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects";
|
||||
import { SessionsCrud } from "@stackframe/stack-shared/dist/interface/crud/sessions";
|
||||
import { TeamInvitationCrud } from "@stackframe/stack-shared/dist/interface/crud/team-invitation";
|
||||
@ -24,8 +25,6 @@ import { deindent, mergeScopeStrings } from "@stackframe/stack-shared/dist/utils
|
||||
import { getRelativePart, isRelative } from "@stackframe/stack-shared/dist/utils/urls";
|
||||
import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";
|
||||
import * as cookie from "cookie";
|
||||
import * as NextNavigationUnscrambled from "next/navigation"; // import the entire module to get around some static compiler warnings emitted by Next.js in some cases | THIS_LINE_PLATFORM next
|
||||
import React, { useCallback, useMemo } from "react"; // THIS_LINE_PLATFORM react-like
|
||||
import { constructRedirectUrl } from "../../../../utils/url";
|
||||
import { addNewOAuthProviderOrScope, callOAuthCallback, signInWithOAuth } from "../../../auth";
|
||||
import { CookieHelper, createBrowserCookieHelper, createCookieHelper, createPlaceholderCookieHelper, deleteCookieClient, getCookieClient, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie";
|
||||
@ -38,8 +37,10 @@ import { EditableTeamMemberProfile, Team, TeamCreateOptions, TeamInvitation, Tea
|
||||
import { ActiveSession, Auth, BaseUser, CurrentUser, InternalUserExtra, ProjectCurrentUser, UserExtra, UserUpdateOptions, userUpdateOptionsToCrud } from "../../users";
|
||||
import { StackClientApp, StackClientAppConstructorOptions, StackClientAppJson } from "../interfaces/client-app";
|
||||
import { _StackAdminAppImplIncomplete } from "./admin-app-impl";
|
||||
import { TokenObject, clientVersion, createCache, createCacheBySession, createEmptyTokenStore, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getUrls } from "./common";
|
||||
import { TokenObject, clientVersion, createCache, createCacheBySession, createEmptyTokenStore, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getUrls, } from "./common";
|
||||
|
||||
import * as NextNavigationUnscrambled from "next/navigation"; // import the entire module to get around some static compiler warnings emitted by Next.js in some cases | THIS_LINE_PLATFORM next
|
||||
import React, { useCallback, useMemo } from "react"; // THIS_LINE_PLATFORM react-like
|
||||
import { useAsyncCache } from "./common"; // THIS_LINE_PLATFORM react-like
|
||||
|
||||
let isReactServer = false;
|
||||
@ -115,6 +116,12 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
>(async (session, [teamId, recursive]) => {
|
||||
return await this._interface.listCurrentUserTeamPermissions({ teamId, recursive }, session);
|
||||
});
|
||||
private readonly _currentUserProjectPermissionsCache = createCacheBySession<
|
||||
[boolean],
|
||||
ProjectPermissionsCrud['Client']['Read'][]
|
||||
>(async (session, [recursive]) => {
|
||||
return await this._interface.listCurrentUserProjectPermissions({ recursive }, session);
|
||||
});
|
||||
private readonly _currentUserTeamsCache = createCacheBySession(async (session) => {
|
||||
return await this._interface.listCurrentUserTeams(session);
|
||||
});
|
||||
@ -584,7 +591,7 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
};
|
||||
}
|
||||
|
||||
protected _clientTeamPermissionFromCrud(crud: TeamPermissionsCrud['Client']['Read']): TeamPermission {
|
||||
protected _clientPermissionFromCrud(crud: TeamPermissionsCrud['Client']['Read'] | ProjectPermissionsCrud['Client']['Read']): TeamPermission {
|
||||
return {
|
||||
id: crud.id,
|
||||
};
|
||||
@ -863,30 +870,66 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
await app._interface.leaveTeam(team.id, session);
|
||||
// TODO: refresh cache
|
||||
},
|
||||
async listPermissions(scope: Team, options?: { recursive?: boolean }): Promise<TeamPermission[]> {
|
||||
const recursive = options?.recursive ?? true;
|
||||
const permissions = Result.orThrow(await app._currentUserPermissionsCache.getOrWait([session, scope.id, recursive], "write-only"));
|
||||
return permissions.map((crud) => app._clientTeamPermissionFromCrud(crud));
|
||||
async listPermissions(scopeOrOptions?: Team | { recursive?: boolean }, options?: { recursive?: boolean }): Promise<TeamPermission[]> {
|
||||
if (scopeOrOptions && 'id' in scopeOrOptions) {
|
||||
const scope = scopeOrOptions;
|
||||
const recursive = options?.recursive ?? true;
|
||||
const permissions = Result.orThrow(await app._currentUserPermissionsCache.getOrWait([session, scope.id, recursive], "write-only"));
|
||||
return permissions.map((crud) => app._clientPermissionFromCrud(crud));
|
||||
} else {
|
||||
const opts = scopeOrOptions;
|
||||
const recursive = opts?.recursive ?? true;
|
||||
const permissions = Result.orThrow(await app._currentUserProjectPermissionsCache.getOrWait([session, recursive], "write-only"));
|
||||
return permissions.map((crud) => app._clientPermissionFromCrud(crud));
|
||||
}
|
||||
},
|
||||
// IF_PLATFORM react-like
|
||||
usePermissions(scope: Team, options?: { recursive?: boolean }): TeamPermission[] {
|
||||
const recursive = options?.recursive ?? true;
|
||||
const permissions = useAsyncCache(app._currentUserPermissionsCache, [session, scope.id, recursive] as const, "user.usePermissions()");
|
||||
return useMemo(() => permissions.map((crud) => app._clientTeamPermissionFromCrud(crud)), [permissions]);
|
||||
usePermissions(scopeOrOptions?: Team | { recursive?: boolean }, options?: { recursive?: boolean }): TeamPermission[] {
|
||||
if (scopeOrOptions && 'id' in scopeOrOptions) {
|
||||
const scope = scopeOrOptions;
|
||||
const recursive = options?.recursive ?? true;
|
||||
const permissions = useAsyncCache(app._currentUserPermissionsCache, [session, scope.id, recursive] as const, "user.usePermissions()");
|
||||
return useMemo(() => permissions.map((crud) => app._clientPermissionFromCrud(crud)), [permissions]);
|
||||
} else {
|
||||
const opts = scopeOrOptions;
|
||||
const recursive = opts?.recursive ?? true;
|
||||
const permissions = useAsyncCache(app._currentUserProjectPermissionsCache, [session, recursive] as const, "user.usePermissions()");
|
||||
return useMemo(() => permissions.map((crud) => app._clientPermissionFromCrud(crud)), [permissions]);
|
||||
}
|
||||
},
|
||||
// END_PLATFORM
|
||||
// IF_PLATFORM react-like
|
||||
usePermission(scope: Team, permissionId: string): TeamPermission | null {
|
||||
const permissions = this.usePermissions(scope);
|
||||
return useMemo(() => permissions.find((p) => p.id === permissionId) ?? null, [permissions, permissionId]);
|
||||
usePermission(scopeOrPermissionId: Team | string, permissionId?: string): TeamPermission | null {
|
||||
if (scopeOrPermissionId && typeof scopeOrPermissionId !== 'string') {
|
||||
const scope = scopeOrPermissionId;
|
||||
const permissions = this.usePermissions(scope);
|
||||
return useMemo(() => permissions.find((p) => p.id === permissionId) ?? null, [permissions, permissionId]);
|
||||
} else {
|
||||
const pid = scopeOrPermissionId;
|
||||
const permissions = this.usePermissions();
|
||||
return useMemo(() => permissions.find((p) => p.id === pid) ?? null, [permissions, pid]);
|
||||
}
|
||||
},
|
||||
// END_PLATFORM
|
||||
async getPermission(scope: Team, permissionId: string): Promise<TeamPermission | null> {
|
||||
const permissions = await this.listPermissions(scope);
|
||||
return permissions.find((p) => p.id === permissionId) ?? null;
|
||||
async getPermission(scopeOrPermissionId: Team | string, permissionId?: string): Promise<TeamPermission | null> {
|
||||
if (scopeOrPermissionId && typeof scopeOrPermissionId !== 'string') {
|
||||
const scope = scopeOrPermissionId;
|
||||
const permissions = await this.listPermissions(scope);
|
||||
return permissions.find((p) => p.id === permissionId) ?? null;
|
||||
} else {
|
||||
const pid = scopeOrPermissionId;
|
||||
const permissions = await this.listPermissions();
|
||||
return permissions.find((p) => p.id === pid) ?? null;
|
||||
}
|
||||
},
|
||||
async hasPermission(scope: Team, permissionId: string): Promise<boolean> {
|
||||
return (await this.getPermission(scope, permissionId)) !== null;
|
||||
async hasPermission(scopeOrPermissionId: Team | string, permissionId?: string): Promise<boolean> {
|
||||
if (scopeOrPermissionId && typeof scopeOrPermissionId !== 'string') {
|
||||
const scope = scopeOrPermissionId;
|
||||
return (await this.getPermission(scope, permissionId as string)) !== null;
|
||||
} else {
|
||||
const pid = scopeOrPermissionId;
|
||||
return (await this.getPermission(pid)) !== null;
|
||||
}
|
||||
},
|
||||
async update(update) {
|
||||
return await app._updateClientUser(update, session);
|
||||
|
||||
@ -3,7 +3,7 @@ import { ContactChannelsCrud } from "@stackframe/stack-shared/dist/interface/cru
|
||||
import { TeamInvitationCrud } from "@stackframe/stack-shared/dist/interface/crud/team-invitation";
|
||||
import { TeamMemberProfilesCrud } from "@stackframe/stack-shared/dist/interface/crud/team-member-profiles";
|
||||
import { TeamPermissionDefinitionsCrud, TeamPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/team-permissions";
|
||||
import { UserPermissionDefinitionsCrud } from "@stackframe/stack-shared/dist/interface/crud/user-permissions";
|
||||
import { ProjectPermissionDefinitionsCrud, ProjectPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
|
||||
import { TeamsCrud } from "@stackframe/stack-shared/dist/interface/crud/teams";
|
||||
import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
|
||||
import { InternalSession } from "@stackframe/stack-shared/dist/sessions";
|
||||
@ -17,7 +17,7 @@ import { constructRedirectUrl } from "../../../../utils/url";
|
||||
import { GetUserOptions, HandlerUrls, OAuthScopesOnSignIn, TokenStoreInit } from "../../common";
|
||||
import { OAuthConnection } from "../../connected-accounts";
|
||||
import { ServerContactChannel, ServerContactChannelCreateOptions, ServerContactChannelUpdateOptions, serverContactChannelCreateOptionsToCrud, serverContactChannelUpdateOptionsToCrud } from "../../contact-channels";
|
||||
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminUserPermissionDefinition } from "../../permissions";
|
||||
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminProjectPermissionDefinition } from "../../permissions";
|
||||
import { EditableTeamMemberProfile, ServerListUsersOptions, ServerTeam, ServerTeamCreateOptions, ServerTeamUpdateOptions, ServerTeamUser, Team, TeamInvitation, serverTeamCreateOptionsToCrud, serverTeamUpdateOptionsToCrud } from "../../teams";
|
||||
import { ProjectCurrentServerUser, ServerUser, ServerUserCreateOptions, ServerUserUpdateOptions, serverUserCreateOptionsToCrud, serverUserUpdateOptionsToCrud } from "../../users";
|
||||
import { StackServerAppConstructorOptions } from "../interfaces/server-app";
|
||||
@ -61,6 +61,12 @@ export class _StackServerAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
>(async ([teamId, userId, recursive]) => {
|
||||
return await this._interface.listServerTeamPermissions({ teamId, userId, recursive }, null);
|
||||
});
|
||||
private readonly _serverUserProjectPermissionsCache = createCache<
|
||||
[string, boolean],
|
||||
ProjectPermissionsCrud['Server']['Read'][]
|
||||
>(async ([userId, recursive]) => {
|
||||
return await this._interface.listServerProjectPermissions({ userId, recursive }, null);
|
||||
});
|
||||
private readonly _serverUserOAuthConnectionAccessTokensCache = createCache<[string, string, string], { accessToken: string } | null>(
|
||||
async ([userId, providerId, scope]) => {
|
||||
try {
|
||||
@ -316,30 +322,66 @@ export class _StackServerAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
await app._interface.leaveServerTeam({ teamId: team.id, userId: crud.id });
|
||||
// TODO: refresh cache
|
||||
},
|
||||
async listPermissions(scope: Team, options?: { recursive?: boolean }): Promise<AdminTeamPermission[]> {
|
||||
const recursive = options?.recursive ?? true;
|
||||
const permissions = Result.orThrow(await app._serverTeamUserPermissionsCache.getOrWait([scope.id, crud.id, recursive], "write-only"));
|
||||
return permissions.map((crud) => app._serverPermissionFromCrud(crud));
|
||||
async listPermissions(scopeOrOptions?: Team | { recursive?: boolean }, options?: { recursive?: boolean }): Promise<AdminTeamPermission[]> {
|
||||
if (scopeOrOptions && 'id' in scopeOrOptions) {
|
||||
const scope = scopeOrOptions;
|
||||
const recursive = options?.recursive ?? true;
|
||||
const permissions = Result.orThrow(await app._serverTeamUserPermissionsCache.getOrWait([scope.id, crud.id, recursive], "write-only"));
|
||||
return permissions.map((crud) => app._serverPermissionFromCrud(crud));
|
||||
} else {
|
||||
const opts = scopeOrOptions;
|
||||
const recursive = opts?.recursive ?? true;
|
||||
const permissions = Result.orThrow(await app._serverUserProjectPermissionsCache.getOrWait([crud.id, recursive], "write-only"));
|
||||
return permissions.map((crud) => app._serverPermissionFromCrud(crud));
|
||||
}
|
||||
},
|
||||
// IF_PLATFORM react-like
|
||||
usePermissions(scope: Team, options?: { recursive?: boolean }): AdminTeamPermission[] {
|
||||
const recursive = options?.recursive ?? true;
|
||||
const permissions = useAsyncCache(app._serverTeamUserPermissionsCache, [scope.id, crud.id, recursive] as const, "user.usePermissions()");
|
||||
return useMemo(() => permissions.map((crud) => app._serverPermissionFromCrud(crud)), [permissions]);
|
||||
usePermissions(scopeOrOptions?: Team | { recursive?: boolean }, options?: { recursive?: boolean }): AdminTeamPermission[] {
|
||||
if (scopeOrOptions && 'id' in scopeOrOptions) {
|
||||
const scope = scopeOrOptions;
|
||||
const recursive = options?.recursive ?? true;
|
||||
const permissions = useAsyncCache(app._serverTeamUserPermissionsCache, [scope.id, crud.id, recursive] as const, "user.usePermissions()");
|
||||
return useMemo(() => permissions.map((crud) => app._serverPermissionFromCrud(crud)), [permissions]);
|
||||
} else {
|
||||
const opts = scopeOrOptions;
|
||||
const recursive = opts?.recursive ?? true;
|
||||
const permissions = useAsyncCache(app._serverUserProjectPermissionsCache, [crud.id, recursive] as const, "user.usePermissions()");
|
||||
return useMemo(() => permissions.map((crud) => app._serverPermissionFromCrud(crud)), [permissions]);
|
||||
}
|
||||
},
|
||||
// END_PLATFORM
|
||||
async getPermission(scope: Team, permissionId: string): Promise<AdminTeamPermission | null> {
|
||||
const permissions = await this.listPermissions(scope);
|
||||
return permissions.find((p) => p.id === permissionId) ?? null;
|
||||
async getPermission(scopeOrPermissionId: Team | string, permissionId?: string): Promise<AdminTeamPermission | null> {
|
||||
if (scopeOrPermissionId && typeof scopeOrPermissionId !== 'string') {
|
||||
const scope = scopeOrPermissionId;
|
||||
const permissions = await this.listPermissions(scope);
|
||||
return permissions.find((p) => p.id === permissionId) ?? null;
|
||||
} else {
|
||||
const pid = scopeOrPermissionId;
|
||||
const permissions = await this.listPermissions();
|
||||
return permissions.find((p) => p.id === pid) ?? null;
|
||||
}
|
||||
},
|
||||
// IF_PLATFORM react-like
|
||||
usePermission(scope: Team, permissionId: string): AdminTeamPermission | null {
|
||||
const permissions = this.usePermissions(scope);
|
||||
return useMemo(() => permissions.find((p) => p.id === permissionId) ?? null, [permissions, permissionId]);
|
||||
usePermission(scopeOrPermissionId: Team | string, permissionId?: string): AdminTeamPermission | null {
|
||||
if (scopeOrPermissionId && typeof scopeOrPermissionId !== 'string') {
|
||||
const scope = scopeOrPermissionId;
|
||||
const permissions = this.usePermissions(scope);
|
||||
return useMemo(() => permissions.find((p) => p.id === permissionId) ?? null, [permissions, permissionId]);
|
||||
} else {
|
||||
const pid = scopeOrPermissionId;
|
||||
const permissions = this.usePermissions();
|
||||
return useMemo(() => permissions.find((p) => p.id === pid) ?? null, [permissions, pid]);
|
||||
}
|
||||
},
|
||||
// END_PLATFORM
|
||||
async hasPermission(scope: Team, permissionId: string): Promise<boolean> {
|
||||
return await this.getPermission(scope, permissionId) !== null;
|
||||
async hasPermission(scopeOrPermissionId: Team | string, permissionId?: string): Promise<boolean> {
|
||||
if (scopeOrPermissionId && typeof scopeOrPermissionId !== 'string') {
|
||||
const scope = scopeOrPermissionId;
|
||||
return (await this.getPermission(scope, permissionId as string)) !== null;
|
||||
} else {
|
||||
const pid = scopeOrPermissionId;
|
||||
return (await this.getPermission(pid)) !== null;
|
||||
}
|
||||
},
|
||||
async update(update: ServerUserUpdateOptions) {
|
||||
await app._updateServerUser(crud.id, update);
|
||||
@ -626,7 +668,7 @@ export class _StackServerAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
}
|
||||
// END_PLATFORM
|
||||
|
||||
_serverPermissionFromCrud(crud: TeamPermissionsCrud['Server']['Read']): AdminTeamPermission {
|
||||
_serverPermissionFromCrud(crud: TeamPermissionsCrud['Server']['Read'] | ProjectPermissionsCrud['Server']['Read']): AdminTeamPermission {
|
||||
return {
|
||||
id: crud.id,
|
||||
};
|
||||
@ -640,7 +682,7 @@ export class _StackServerAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
};
|
||||
}
|
||||
|
||||
_serverUserPermissionDefinitionFromCrud(crud: UserPermissionDefinitionsCrud['Admin']['Read']): AdminUserPermissionDefinition {
|
||||
_serverProjectPermissionDefinitionFromCrud(crud: ProjectPermissionDefinitionsCrud['Admin']['Read']): AdminProjectPermissionDefinition {
|
||||
return {
|
||||
id: crud.id,
|
||||
description: crud.description,
|
||||
|
||||
@ -5,7 +5,7 @@ import { ApiKey, ApiKeyCreateOptions, ApiKeyFirstView } from "../../api-keys";
|
||||
import { AsyncStoreProperty, EmailConfig } from "../../common";
|
||||
import { AdminSentEmail } from "../../email";
|
||||
import { AdminEmailTemplate, AdminEmailTemplateUpdateOptions } from "../../email-templates";
|
||||
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions, AdminUserPermission, AdminUserPermissionDefinition, AdminUserPermissionDefinitionCreateOptions, AdminUserPermissionDefinitionUpdateOptions } from "../../permissions";
|
||||
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions, AdminProjectPermission, AdminProjectPermissionDefinition, AdminProjectPermissionDefinitionCreateOptions, AdminProjectPermissionDefinitionUpdateOptions } from "../../permissions";
|
||||
import { AdminProject } from "../../projects";
|
||||
import { _StackAdminAppImpl } from "../implementations";
|
||||
import { StackServerApp, StackServerAppConstructorOptions } from "./server-app";
|
||||
@ -31,7 +31,7 @@ export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId ext
|
||||
& AsyncStoreProperty<"project", [], AdminProject, false>
|
||||
& AsyncStoreProperty<"apiKeys", [], ApiKey[], true>
|
||||
& AsyncStoreProperty<"teamPermissionDefinitions", [], AdminTeamPermissionDefinition[], true>
|
||||
& AsyncStoreProperty<"userPermissionDefinitions", [], AdminUserPermissionDefinition[], true>
|
||||
& AsyncStoreProperty<"projectPermissionDefinitions", [], AdminProjectPermissionDefinition[], true>
|
||||
& {
|
||||
useEmailTemplates(): AdminEmailTemplate[], // THIS_LINE_PLATFORM react-like
|
||||
listEmailTemplates(): Promise<AdminEmailTemplate[]>,
|
||||
@ -44,9 +44,9 @@ export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId ext
|
||||
updateTeamPermissionDefinition(permissionId: string, data: AdminTeamPermissionDefinitionUpdateOptions): Promise<void>,
|
||||
deleteTeamPermissionDefinition(permissionId: string): Promise<void>,
|
||||
|
||||
createUserPermissionDefinition(data: AdminUserPermissionDefinitionCreateOptions): Promise<AdminUserPermission>,
|
||||
updateUserPermissionDefinition(permissionId: string, data: AdminUserPermissionDefinitionUpdateOptions): Promise<void>,
|
||||
deleteUserPermissionDefinition(permissionId: string): Promise<void>,
|
||||
createProjectPermissionDefinition(data: AdminProjectPermissionDefinitionCreateOptions): Promise<AdminProjectPermission>,
|
||||
updateProjectPermissionDefinition(permissionId: string, data: AdminProjectPermissionDefinitionUpdateOptions): Promise<void>,
|
||||
deleteProjectPermissionDefinition(permissionId: string): Promise<void>,
|
||||
|
||||
useSvixToken(): string, // THIS_LINE_PLATFORM react-like
|
||||
|
||||
|
||||
@ -52,10 +52,10 @@ export type {
|
||||
AdminTeamPermissionDefinition,
|
||||
AdminTeamPermissionDefinitionCreateOptions,
|
||||
AdminTeamPermissionDefinitionUpdateOptions,
|
||||
AdminUserPermission,
|
||||
AdminUserPermissionDefinition,
|
||||
AdminUserPermissionDefinitionCreateOptions,
|
||||
AdminUserPermissionDefinitionUpdateOptions,
|
||||
AdminProjectPermission,
|
||||
AdminProjectPermissionDefinition,
|
||||
AdminProjectPermissionDefinitionCreateOptions,
|
||||
AdminProjectPermissionDefinitionUpdateOptions,
|
||||
} from "./permissions";
|
||||
|
||||
export type {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { TeamPermissionDefinitionsCrud } from "@stackframe/stack-shared/dist/interface/crud/team-permissions";
|
||||
import { UserPermissionDefinitionsCrud } from "@stackframe/stack-shared/dist/interface/crud/user-permissions";
|
||||
import { ProjectPermissionDefinitionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
|
||||
|
||||
|
||||
export type TeamPermission = {
|
||||
@ -38,24 +38,24 @@ export function adminTeamPermissionDefinitionUpdateOptionsToCrud(options: AdminT
|
||||
};
|
||||
}
|
||||
|
||||
export type UserPermission = {
|
||||
export type ProjectPermission = {
|
||||
id: string,
|
||||
};
|
||||
|
||||
export type AdminUserPermission = UserPermission;
|
||||
export type AdminProjectPermission = ProjectPermission;
|
||||
|
||||
export type AdminUserPermissionDefinition = {
|
||||
export type AdminProjectPermissionDefinition = {
|
||||
id: string,
|
||||
description?: string,
|
||||
containedPermissionIds: string[],
|
||||
};
|
||||
|
||||
export type AdminUserPermissionDefinitionCreateOptions = {
|
||||
export type AdminProjectPermissionDefinitionCreateOptions = {
|
||||
id: string,
|
||||
description?: string,
|
||||
containedPermissionIds: string[],
|
||||
};
|
||||
export function adminUserPermissionDefinitionCreateOptionsToCrud(options: AdminUserPermissionDefinitionCreateOptions): UserPermissionDefinitionsCrud["Admin"]["Create"] {
|
||||
export function adminProjectPermissionDefinitionCreateOptionsToCrud(options: AdminProjectPermissionDefinitionCreateOptions): ProjectPermissionDefinitionsCrud["Admin"]["Create"] {
|
||||
return {
|
||||
id: options.id,
|
||||
description: options.description,
|
||||
@ -63,8 +63,8 @@ export function adminUserPermissionDefinitionCreateOptionsToCrud(options: AdminU
|
||||
};
|
||||
}
|
||||
|
||||
export type AdminUserPermissionDefinitionUpdateOptions = Partial<AdminUserPermissionDefinitionCreateOptions>;
|
||||
export function adminUserPermissionDefinitionUpdateOptionsToCrud(options: AdminUserPermissionDefinitionUpdateOptions): UserPermissionDefinitionsCrud["Admin"]["Update"] {
|
||||
export type AdminProjectPermissionDefinitionUpdateOptions = Partial<AdminProjectPermissionDefinitionCreateOptions>;
|
||||
export function adminProjectPermissionDefinitionUpdateOptionsToCrud(options: AdminProjectPermissionDefinitionUpdateOptions): ProjectPermissionDefinitionsCrud["Admin"]["Update"] {
|
||||
return {
|
||||
id: options.id,
|
||||
description: options.description,
|
||||
|
||||
@ -187,6 +187,21 @@ export type UserExtra = {
|
||||
// END_PLATFORM
|
||||
|
||||
hasPermission(scope: Team, permissionId: string): Promise<boolean>,
|
||||
hasPermission(permissionId: string): Promise<boolean>,
|
||||
|
||||
getPermission(scope: Team, permissionId: string): Promise<TeamPermission | null>,
|
||||
getPermission(permissionId: string): Promise<TeamPermission | null>,
|
||||
|
||||
listPermissions(scope: Team, options?: { recursive?: boolean }): Promise<TeamPermission[]>,
|
||||
listPermissions(options?: { recursive?: boolean }): Promise<TeamPermission[]>,
|
||||
|
||||
// IF_PLATFORM react-like
|
||||
usePermissions(scope: Team, options?: { recursive?: boolean }): TeamPermission[],
|
||||
usePermissions(options?: { recursive?: boolean }): TeamPermission[],
|
||||
|
||||
usePermission(scope: Team, permissionId: string): TeamPermission | null,
|
||||
usePermission(permissionId: string): TeamPermission | null,
|
||||
// END_PLATFORM
|
||||
|
||||
readonly selectedTeam: Team | null,
|
||||
setSelectedTeam(team: Team | null): Promise<void>,
|
||||
@ -270,6 +285,23 @@ export type ServerBaseUser = {
|
||||
grantPermission(scope: Team, permissionId: string): Promise<void>,
|
||||
revokePermission(scope: Team, permissionId: string): Promise<void>,
|
||||
|
||||
getPermission(scope: Team, permissionId: string): Promise<TeamPermission | null>,
|
||||
getPermission(permissionId: string): Promise<TeamPermission | null>,
|
||||
|
||||
hasPermission(scope: Team, permissionId: string): Promise<boolean>,
|
||||
hasPermission(permissionId: string): Promise<boolean>,
|
||||
|
||||
listPermissions(scope: Team, options?: { recursive?: boolean }): Promise<TeamPermission[]>,
|
||||
listPermissions(options?: { recursive?: boolean }): Promise<TeamPermission[]>,
|
||||
|
||||
// IF_PLATFORM react-like
|
||||
usePermissions(scope: Team, options?: { recursive?: boolean }): TeamPermission[],
|
||||
usePermissions(options?: { recursive?: boolean }): TeamPermission[],
|
||||
|
||||
usePermission(scope: Team, permissionId: string): TeamPermission | null,
|
||||
usePermission(permissionId: string): TeamPermission | null,
|
||||
// END_PLATFORM
|
||||
|
||||
/**
|
||||
* Creates a new session object with a refresh token for this user. Can be used to impersonate them.
|
||||
*/
|
||||
|
||||
Loading…
Reference in New Issue
Block a user