feat: customisable workspace role permissions (#345)
* feat: setup schema for workspace roles * chore: regen migration * feat: add publicId to workspace roles * feat: setup default permissions * feat: add repo funcs * feat: setup basic router interactions * feat: add card permissions * feat: assert permissions for lists * feat: assert board permissions * feat: assert permission for remaining routes * feat: add permissions page to settings * feat: enable updating member roles * feat: order members by role and createdAt * feat: allow editing individual permissions * feat: reset role defaults * feat: clear all permission overrides * feat: allow users to delete entities they have created * feat: set roleId when inviting new members * feat: disable UI elements if user does not have permissions * feat: allow admins to assign the admin role to other users * feat: allow delete:list as default * refactor: centre permissions modal * chore: translations
This commit is contained in:
@@ -518,6 +518,7 @@ export const getWithListIdsByPublicId = (
|
||||
columns: {
|
||||
id: true,
|
||||
workspaceId: true,
|
||||
createdBy: true,
|
||||
},
|
||||
with: {
|
||||
lists: {
|
||||
@@ -671,6 +672,7 @@ export const getWorkspaceAndBoardIdByBoardPublicId = async (
|
||||
columns: {
|
||||
id: true,
|
||||
workspaceId: true,
|
||||
createdBy: true,
|
||||
},
|
||||
where: eq(boards.publicId, boardPublicId),
|
||||
});
|
||||
|
||||
@@ -424,6 +424,7 @@ export const getWithListAndMembersByPublicId = async (
|
||||
title: true,
|
||||
description: true,
|
||||
dueDate: true,
|
||||
createdBy: true,
|
||||
},
|
||||
with: {
|
||||
labels: {
|
||||
@@ -938,7 +939,7 @@ export const getWorkspaceAndCardIdByCardPublicId = async (
|
||||
cardPublicId: string,
|
||||
) => {
|
||||
const result = await db.query.cards.findFirst({
|
||||
columns: { id: true },
|
||||
columns: { id: true, createdBy: true },
|
||||
where: and(eq(cards.publicId, cardPublicId), isNull(cards.deletedAt)),
|
||||
with: {
|
||||
list: {
|
||||
@@ -958,6 +959,7 @@ export const getWorkspaceAndCardIdByCardPublicId = async (
|
||||
return result
|
||||
? {
|
||||
id: result.id,
|
||||
createdBy: result.createdBy,
|
||||
workspaceId: result.list.board.workspaceId,
|
||||
workspaceVisibility: result.list.board.visibility,
|
||||
}
|
||||
|
||||
@@ -419,7 +419,7 @@ export const getWorkspaceAndListIdByListPublicId = async (
|
||||
listPublicId: string,
|
||||
) => {
|
||||
const result = await db.query.lists.findFirst({
|
||||
columns: { id: true },
|
||||
columns: { id: true, createdBy: true },
|
||||
where: and(eq(lists.publicId, listPublicId), isNull(lists.deletedAt)),
|
||||
with: {
|
||||
board: {
|
||||
@@ -431,6 +431,10 @@ export const getWorkspaceAndListIdByListPublicId = async (
|
||||
});
|
||||
|
||||
return result
|
||||
? { id: result.id, workspaceId: result.board.workspaceId }
|
||||
? {
|
||||
id: result.id,
|
||||
createdBy: result.createdBy,
|
||||
workspaceId: result.board.workspaceId,
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ export const create = async (
|
||||
workspaceId: number;
|
||||
createdBy: string;
|
||||
role: MemberRole;
|
||||
roleId?: number | null;
|
||||
status: MemberStatus;
|
||||
},
|
||||
) => {
|
||||
@@ -39,6 +40,7 @@ export const create = async (
|
||||
workspaceId: memberInput.workspaceId,
|
||||
createdBy: memberInput.createdBy,
|
||||
role: memberInput.role,
|
||||
roleId: memberInput.roleId ?? null,
|
||||
status: memberInput.status,
|
||||
})
|
||||
.returning({
|
||||
@@ -55,6 +57,12 @@ export const getByPublicId = async (db: dbClient, publicId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getById = async (db: dbClient, memberId: number) => {
|
||||
return db.query.workspaceMembers.findFirst({
|
||||
where: eq(workspaceMembers.id, memberId),
|
||||
});
|
||||
};
|
||||
|
||||
export const getByEmailAndStatus = async (
|
||||
db: dbClient,
|
||||
email: string,
|
||||
@@ -133,3 +141,28 @@ export const pauseAllMembers = async (db: dbClient, workspaceId: number) => {
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const updateRole = async (
|
||||
db: dbClient,
|
||||
args: {
|
||||
memberId: number;
|
||||
role: MemberRole;
|
||||
roleId: number | null;
|
||||
},
|
||||
) => {
|
||||
const [result] = await db
|
||||
.update(workspaceMembers)
|
||||
.set({
|
||||
role: args.role,
|
||||
roleId: args.roleId,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(workspaceMembers.id, args.memberId))
|
||||
.returning({
|
||||
id: workspaceMembers.id,
|
||||
publicId: workspaceMembers.publicId,
|
||||
role: workspaceMembers.role,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
494
packages/db/src/repository/permission.repo.ts
Normal file
494
packages/db/src/repository/permission.repo.ts
Normal file
@@ -0,0 +1,494 @@
|
||||
import { and, eq, isNull, inArray } from "drizzle-orm";
|
||||
|
||||
import type { dbClient } from "@kan/db/client";
|
||||
import {
|
||||
workspaceMemberPermissions,
|
||||
workspaceMembers,
|
||||
workspaceRolePermissions,
|
||||
workspaceRoles,
|
||||
} from "@kan/db/schema";
|
||||
import type { Permission, Role } from "@kan/shared";
|
||||
import { generateUID, getDefaultPermissions } from "@kan/shared";
|
||||
|
||||
/**
|
||||
* Get permissions by role ID
|
||||
*/
|
||||
export const getPermissionsByRoleId = async (
|
||||
db: dbClient,
|
||||
roleId: number,
|
||||
): Promise<Permission[]> => {
|
||||
const permissions = await db
|
||||
.select({ permission: workspaceRolePermissions.permission })
|
||||
.from(workspaceRolePermissions)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceRolePermissions.workspaceRoleId, roleId),
|
||||
eq(workspaceRolePermissions.granted, true),
|
||||
),
|
||||
);
|
||||
|
||||
return permissions.map((p) => p.permission as Permission);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get role by workspace ID and name
|
||||
*/
|
||||
export const getRoleByWorkspaceIdAndName = async (
|
||||
db: dbClient,
|
||||
workspaceId: number,
|
||||
name: string,
|
||||
) => {
|
||||
const [role] = await db
|
||||
.select()
|
||||
.from(workspaceRoles)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceRoles.workspaceId, workspaceId),
|
||||
eq(workspaceRoles.name, name),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return role;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get role by workspace ID and publicId
|
||||
*/
|
||||
export const getRoleByWorkspaceIdAndPublicId = async (
|
||||
db: dbClient,
|
||||
workspaceId: number,
|
||||
rolePublicId: string,
|
||||
) => {
|
||||
const [role] = await db
|
||||
.select()
|
||||
.from(workspaceRoles)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceRoles.workspaceId, workspaceId),
|
||||
eq(workspaceRoles.publicId, rolePublicId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return role;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all custom permission overrides for a workspace member
|
||||
*/
|
||||
export const getMemberPermissionOverrides = async (
|
||||
db: dbClient,
|
||||
workspaceMemberId: number,
|
||||
) => {
|
||||
return db
|
||||
.select()
|
||||
.from(workspaceMemberPermissions)
|
||||
.where(eq(workspaceMemberPermissions.workspaceMemberId, workspaceMemberId));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a single permission override for a member
|
||||
*/
|
||||
export const getMemberPermissionOverride = async (
|
||||
db: dbClient,
|
||||
workspaceMemberId: number,
|
||||
permission: string,
|
||||
) => {
|
||||
const [override] = await db
|
||||
.select()
|
||||
.from(workspaceMemberPermissions)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMemberPermissions.workspaceMemberId, workspaceMemberId),
|
||||
eq(workspaceMemberPermissions.permission, permission),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return override;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get effective permissions for a workspace member
|
||||
* Combines role template (from DB) with custom overrides
|
||||
*/
|
||||
export const getMemberEffectivePermissions = async (
|
||||
db: dbClient,
|
||||
workspaceMemberId: number,
|
||||
roleId: number | null,
|
||||
roleName: string,
|
||||
): Promise<Permission[]> => {
|
||||
let roleDefaults: Set<Permission>;
|
||||
|
||||
// Try to get role permissions from database first
|
||||
if (roleId) {
|
||||
const dbPermissions = await getPermissionsByRoleId(db, roleId);
|
||||
roleDefaults = new Set<Permission>(dbPermissions);
|
||||
} else {
|
||||
// Fallback to code-based defaults if roleId not set
|
||||
const codeDefaults = getDefaultPermissions(roleName as Role);
|
||||
roleDefaults = new Set<Permission>([...codeDefaults]);
|
||||
}
|
||||
|
||||
// Get custom overrides
|
||||
const overrides = await getMemberPermissionOverrides(db, workspaceMemberId);
|
||||
|
||||
// Apply overrides
|
||||
for (const override of overrides) {
|
||||
if (override.granted) {
|
||||
roleDefaults.add(override.permission as Permission);
|
||||
} else {
|
||||
roleDefaults.delete(override.permission as Permission);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(roleDefaults);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a member has a specific permission
|
||||
*/
|
||||
export const memberHasPermission = async (
|
||||
db: dbClient,
|
||||
workspaceMemberId: number,
|
||||
roleId: number | null,
|
||||
roleName: string,
|
||||
permission: Permission,
|
||||
): Promise<boolean> => {
|
||||
let hasRoleDefault: boolean;
|
||||
|
||||
// Check role permission from database first
|
||||
if (roleId) {
|
||||
const dbPermissions = await getPermissionsByRoleId(db, roleId);
|
||||
hasRoleDefault = dbPermissions.includes(permission);
|
||||
} else {
|
||||
// Fallback to code-based defaults
|
||||
const codeDefaults = getDefaultPermissions(roleName as Role);
|
||||
hasRoleDefault = codeDefaults.includes(permission);
|
||||
}
|
||||
|
||||
// Check for override
|
||||
const [override] = await db
|
||||
.select()
|
||||
.from(workspaceMemberPermissions)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMemberPermissions.workspaceMemberId, workspaceMemberId),
|
||||
eq(workspaceMemberPermissions.permission, permission),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
// Override takes precedence
|
||||
if (override) {
|
||||
return override.granted;
|
||||
}
|
||||
|
||||
return hasRoleDefault;
|
||||
};
|
||||
|
||||
/**
|
||||
* Grant a permission to a member
|
||||
*/
|
||||
export const grantPermission = async (
|
||||
db: dbClient,
|
||||
workspaceMemberId: number,
|
||||
permission: Permission,
|
||||
) => {
|
||||
const [result] = await db
|
||||
.insert(workspaceMemberPermissions)
|
||||
.values({
|
||||
workspaceMemberId,
|
||||
permission,
|
||||
granted: true,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
workspaceMemberPermissions.workspaceMemberId,
|
||||
workspaceMemberPermissions.permission,
|
||||
],
|
||||
set: {
|
||||
granted: true,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Revoke a permission from a member
|
||||
*/
|
||||
export const revokePermission = async (
|
||||
db: dbClient,
|
||||
workspaceMemberId: number,
|
||||
permission: Permission,
|
||||
) => {
|
||||
const [result] = await db
|
||||
.insert(workspaceMemberPermissions)
|
||||
.values({
|
||||
workspaceMemberId,
|
||||
permission,
|
||||
granted: false,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
workspaceMemberPermissions.workspaceMemberId,
|
||||
workspaceMemberPermissions.permission,
|
||||
],
|
||||
set: {
|
||||
granted: false,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear all permission overrides for a workspace member
|
||||
*/
|
||||
export const clearMemberPermissionOverrides = async (
|
||||
db: dbClient,
|
||||
workspaceMemberId: number,
|
||||
) => {
|
||||
await db
|
||||
.delete(workspaceMemberPermissions)
|
||||
.where(eq(workspaceMemberPermissions.workspaceMemberId, workspaceMemberId));
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear all permission overrides for all members in a workspace
|
||||
*/
|
||||
export const clearAllMemberPermissionOverridesForWorkspace = async (
|
||||
db: dbClient,
|
||||
workspaceId: number,
|
||||
) => {
|
||||
const memberIds = await db
|
||||
.select({ id: workspaceMembers.id })
|
||||
.from(workspaceMembers)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMembers.workspaceId, workspaceId),
|
||||
isNull(workspaceMembers.deletedAt),
|
||||
),
|
||||
);
|
||||
|
||||
if (memberIds.length === 0) return;
|
||||
|
||||
const ids = memberIds.map((m) => m.id);
|
||||
|
||||
await db
|
||||
.delete(workspaceMemberPermissions)
|
||||
.where(inArray(workspaceMemberPermissions.workspaceMemberId, ids));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get member with their role by userId and workspaceId
|
||||
*/
|
||||
export const getMemberWithRole = async (
|
||||
db: dbClient,
|
||||
userId: string,
|
||||
workspaceId: number,
|
||||
) => {
|
||||
const [member] = await db
|
||||
.select({
|
||||
id: workspaceMembers.id,
|
||||
publicId: workspaceMembers.publicId,
|
||||
role: workspaceMembers.role,
|
||||
roleId: workspaceMembers.roleId,
|
||||
})
|
||||
.from(workspaceMembers)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMembers.userId, userId),
|
||||
eq(workspaceMembers.workspaceId, workspaceId),
|
||||
isNull(workspaceMembers.deletedAt),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return member;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if user has permission in workspace
|
||||
*/
|
||||
export const userHasPermissionInWorkspace = async (
|
||||
db: dbClient,
|
||||
userId: string,
|
||||
workspaceId: number,
|
||||
permission: Permission,
|
||||
): Promise<boolean> => {
|
||||
const member = await getMemberWithRole(db, userId, workspaceId);
|
||||
|
||||
if (!member) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return memberHasPermission(
|
||||
db,
|
||||
member.id,
|
||||
member.roleId,
|
||||
member.role,
|
||||
permission,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all permissions for a user in a workspace
|
||||
*/
|
||||
export const getUserPermissionsInWorkspace = async (
|
||||
db: dbClient,
|
||||
userId: string,
|
||||
workspaceId: number,
|
||||
): Promise<{
|
||||
permissions: Permission[];
|
||||
role: string;
|
||||
roleId: number | null;
|
||||
} | null> => {
|
||||
const member = await getMemberWithRole(db, userId, workspaceId);
|
||||
|
||||
if (!member) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const permissions = await getMemberEffectivePermissions(
|
||||
db,
|
||||
member.id,
|
||||
member.roleId,
|
||||
member.role,
|
||||
);
|
||||
|
||||
return {
|
||||
permissions,
|
||||
role: member.role,
|
||||
roleId: member.roleId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a role for a workspace
|
||||
*/
|
||||
export const createRole = async (
|
||||
db: dbClient,
|
||||
args: {
|
||||
workspaceId: number;
|
||||
name: string;
|
||||
description: string;
|
||||
hierarchyLevel: number;
|
||||
isSystem: boolean;
|
||||
permissions: Permission[];
|
||||
},
|
||||
) => {
|
||||
const [role] = await db
|
||||
.insert(workspaceRoles)
|
||||
.values({
|
||||
publicId: generateUID(),
|
||||
workspaceId: args.workspaceId,
|
||||
name: args.name,
|
||||
description: args.description,
|
||||
hierarchyLevel: args.hierarchyLevel,
|
||||
isSystem: args.isSystem,
|
||||
})
|
||||
.returning({
|
||||
id: workspaceRoles.id,
|
||||
name: workspaceRoles.name,
|
||||
publicId: workspaceRoles.publicId,
|
||||
});
|
||||
|
||||
if (role && args.permissions.length > 0) {
|
||||
await db.insert(workspaceRolePermissions).values(
|
||||
args.permissions.map((perm) => ({
|
||||
workspaceRoleId: role.id,
|
||||
permission: perm,
|
||||
granted: true,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
return role;
|
||||
};
|
||||
|
||||
/**
|
||||
* Grant a permission to a role
|
||||
*/
|
||||
export const grantRolePermission = async (
|
||||
db: dbClient,
|
||||
roleId: number,
|
||||
permission: Permission,
|
||||
) => {
|
||||
const [result] = await db
|
||||
.insert(workspaceRolePermissions)
|
||||
.values({
|
||||
workspaceRoleId: roleId,
|
||||
permission,
|
||||
granted: true,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
workspaceRolePermissions.workspaceRoleId,
|
||||
workspaceRolePermissions.permission,
|
||||
],
|
||||
set: {
|
||||
granted: true,
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Revoke a permission from a role
|
||||
*/
|
||||
export const revokeRolePermission = async (
|
||||
db: dbClient,
|
||||
roleId: number,
|
||||
permission: Permission,
|
||||
) => {
|
||||
const [result] = await db
|
||||
.insert(workspaceRolePermissions)
|
||||
.values({
|
||||
workspaceRoleId: roleId,
|
||||
permission,
|
||||
granted: false,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
workspaceRolePermissions.workspaceRoleId,
|
||||
workspaceRolePermissions.permission,
|
||||
],
|
||||
set: {
|
||||
granted: false,
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all roles for a workspace
|
||||
*/
|
||||
export const getRolesByWorkspaceId = async (
|
||||
db: dbClient,
|
||||
workspaceId: number,
|
||||
) => {
|
||||
return db
|
||||
.select({
|
||||
id: workspaceRoles.id,
|
||||
publicId: workspaceRoles.publicId,
|
||||
name: workspaceRoles.name,
|
||||
description: workspaceRoles.description,
|
||||
hierarchyLevel: workspaceRoles.hierarchyLevel,
|
||||
isSystem: workspaceRoles.isSystem,
|
||||
})
|
||||
.from(workspaceRoles)
|
||||
.where(eq(workspaceRoles.workspaceId, workspaceId));
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,33 @@ import {
|
||||
workspaceMembers,
|
||||
workspaces,
|
||||
} from "@kan/db/schema";
|
||||
import { generateUID } from "@kan/shared/utils";
|
||||
import type { Permission, Role } from "@kan/shared";
|
||||
import { generateUID, getDefaultPermissions } from "@kan/shared";
|
||||
|
||||
import * as permissionRepo from "./permission.repo";
|
||||
|
||||
// System role definitions
|
||||
const SYSTEM_ROLES: {
|
||||
name: Role;
|
||||
description: string;
|
||||
hierarchyLevel: number;
|
||||
}[] = [
|
||||
{
|
||||
name: "admin",
|
||||
description: "Full access to all workspace features",
|
||||
hierarchyLevel: 100,
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
description: "Standard member with create and edit permissions",
|
||||
hierarchyLevel: 50,
|
||||
},
|
||||
{
|
||||
name: "guest",
|
||||
description: "View-only access",
|
||||
hierarchyLevel: 10,
|
||||
},
|
||||
];
|
||||
|
||||
export const getCount = async (db: dbClient) => {
|
||||
const result = await db
|
||||
@@ -57,6 +83,22 @@ export const create = async (
|
||||
});
|
||||
|
||||
if (workspace) {
|
||||
// Create system roles for the workspace
|
||||
let adminRoleId: number | null = null;
|
||||
for (const roleData of SYSTEM_ROLES) {
|
||||
const role = await permissionRepo.createRole(db, {
|
||||
workspaceId: workspace.id,
|
||||
name: roleData.name,
|
||||
description: roleData.description,
|
||||
hierarchyLevel: roleData.hierarchyLevel,
|
||||
isSystem: true,
|
||||
permissions: [...getDefaultPermissions(roleData.name)] as Permission[],
|
||||
});
|
||||
if (roleData.name === "admin" && role) {
|
||||
adminRoleId = role.id;
|
||||
}
|
||||
}
|
||||
|
||||
await db.insert(workspaceMembers).values({
|
||||
publicId: generateUID(),
|
||||
userId: workspaceInput.createdBy,
|
||||
@@ -64,6 +106,7 @@ export const create = async (
|
||||
workspaceId: workspace.id,
|
||||
createdBy: workspaceInput.createdBy,
|
||||
role: "admin",
|
||||
roleId: adminRoleId,
|
||||
status: "active",
|
||||
});
|
||||
}
|
||||
@@ -151,8 +194,13 @@ export const getByPublicIdWithMembers = (
|
||||
email: true,
|
||||
role: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
},
|
||||
where: isNull(workspaceMembers.deletedAt),
|
||||
orderBy: (member, { desc }) => [
|
||||
desc(sql`CASE WHEN ${member.role} = 'admin' THEN 1 ELSE 0 END`),
|
||||
desc(member.createdAt),
|
||||
],
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from "./integrations";
|
||||
export * from "./workspaces";
|
||||
export * from "./subscriptions";
|
||||
export * from "./workspaceInviteLinks";
|
||||
export * from "./permissions";
|
||||
|
||||
98
packages/db/src/schema/permissions.ts
Normal file
98
packages/db/src/schema/permissions.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
bigserial,
|
||||
boolean,
|
||||
index,
|
||||
integer,
|
||||
pgTable,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
import { workspaces } from "./workspaces";
|
||||
|
||||
export const workspaceRoles = pgTable(
|
||||
"workspace_roles",
|
||||
{
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
|
||||
workspaceId: bigint("workspaceId", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => workspaces.id, { onDelete: "cascade" }),
|
||||
name: varchar("name", { length: 64 }).notNull(),
|
||||
description: varchar("description", { length: 255 }),
|
||||
hierarchyLevel: integer("hierarchyLevel").notNull(),
|
||||
isSystem: boolean("isSystem").notNull().default(false),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt"),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("unique_role_per_workspace").on(table.workspaceId, table.name),
|
||||
index("workspace_roles_workspace_idx").on(table.workspaceId),
|
||||
],
|
||||
).enableRLS();
|
||||
|
||||
export const workspaceRolesRelations = relations(
|
||||
workspaceRoles,
|
||||
({ one, many }) => ({
|
||||
workspace: one(workspaces, {
|
||||
fields: [workspaceRoles.workspaceId],
|
||||
references: [workspaces.id],
|
||||
relationName: "workspaceRoles",
|
||||
}),
|
||||
permissions: many(workspaceRolePermissions),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
export const workspaceRolePermissions = pgTable(
|
||||
"workspace_role_permissions",
|
||||
{
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
workspaceRoleId: bigint("workspaceRoleId", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => workspaceRoles.id, { onDelete: "cascade" }),
|
||||
permission: varchar("permission", { length: 64 }).notNull(),
|
||||
granted: boolean("granted").notNull().default(true),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("unique_role_permission").on(
|
||||
table.workspaceRoleId,
|
||||
table.permission,
|
||||
),
|
||||
index("role_permissions_role_idx").on(table.workspaceRoleId),
|
||||
],
|
||||
).enableRLS();
|
||||
|
||||
export const workspaceRolePermissionsRelations = relations(
|
||||
workspaceRolePermissions,
|
||||
({ one }) => ({
|
||||
role: one(workspaceRoles, {
|
||||
fields: [workspaceRolePermissions.workspaceRoleId],
|
||||
references: [workspaceRoles.id],
|
||||
relationName: "rolePermissions",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const workspaceMemberPermissions = pgTable(
|
||||
"workspace_member_permissions",
|
||||
{
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
workspaceMemberId: bigint("workspaceMemberId", { mode: "number" }).notNull(),
|
||||
permission: varchar("permission", { length: 64 }).notNull(),
|
||||
granted: boolean("granted").notNull().default(true),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt"),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("unique_member_permission").on(
|
||||
table.workspaceMemberId,
|
||||
table.permission,
|
||||
),
|
||||
index("permission_member_idx").on(table.workspaceMemberId),
|
||||
],
|
||||
).enableRLS();
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
import { boards } from "./boards";
|
||||
import { workspaceMemberPermissions, workspaceRoles } from "./permissions";
|
||||
import { subscription } from "./subscriptions";
|
||||
import { users } from "./users";
|
||||
|
||||
@@ -69,6 +70,7 @@ export const workspaceRelations = relations(workspaces, ({ one, many }) => ({
|
||||
members: many(workspaceMembers),
|
||||
boards: many(boards),
|
||||
subscriptions: many(subscription),
|
||||
roles: many(workspaceRoles),
|
||||
}));
|
||||
|
||||
export const workspaceMembers = pgTable("workspace_members", {
|
||||
@@ -86,13 +88,18 @@ export const workspaceMembers = pgTable("workspace_members", {
|
||||
deletedBy: uuid("deletedBy").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
// Legacy role enum
|
||||
role: memberRoleEnum("role").notNull(),
|
||||
roleId: bigint("roleId", { mode: "number" }).references(
|
||||
() => workspaceRoles.id,
|
||||
{ onDelete: "restrict" },
|
||||
),
|
||||
status: memberStatusEnum("status").default("invited").notNull(),
|
||||
}).enableRLS();
|
||||
|
||||
export const workspaceMembersRelations = relations(
|
||||
workspaceMembers,
|
||||
({ one }) => ({
|
||||
({ one, many }) => ({
|
||||
user: one(users, {
|
||||
fields: [workspaceMembers.userId],
|
||||
references: [users.id],
|
||||
@@ -103,6 +110,23 @@ export const workspaceMembersRelations = relations(
|
||||
references: [workspaces.id],
|
||||
relationName: "workspaceMembersWorkspace",
|
||||
}),
|
||||
workspaceRole: one(workspaceRoles, {
|
||||
fields: [workspaceMembers.roleId],
|
||||
references: [workspaceRoles.id],
|
||||
relationName: "workspaceMemberRole",
|
||||
}),
|
||||
permissions: many(workspaceMemberPermissions),
|
||||
}),
|
||||
);
|
||||
|
||||
export const workspaceMemberPermissionsRelations = relations(
|
||||
workspaceMemberPermissions,
|
||||
({ one }) => ({
|
||||
member: one(workspaceMembers, {
|
||||
fields: [workspaceMemberPermissions.workspaceMemberId],
|
||||
references: [workspaceMembers.id],
|
||||
relationName: "memberPermissions",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user