mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-10 21:49:19 -06:00
feat: enhance role-based access control by adding permissions to user session and implementing role utility functions
This commit is contained in:
@@ -8,6 +8,7 @@ import appService from "@/server/services/app.service";
|
||||
import PageTitle from "@/components/custom/page-title";
|
||||
import ProjectBreadcrumbs from "./project-breadcrumbs";
|
||||
import CreateProjectActions from "./create-project-actions";
|
||||
import { RoleUtils } from "@/server/utils/role.utils";
|
||||
|
||||
export default async function AppsPage({
|
||||
searchParams,
|
||||
@@ -16,7 +17,7 @@ export default async function AppsPage({
|
||||
searchParams?: { [key: string]: string | undefined };
|
||||
params: { projectId: string }
|
||||
}) {
|
||||
await getAuthUserSession();
|
||||
const session = await getAuthUserSession();
|
||||
|
||||
const projectId = params?.projectId;
|
||||
if (!projectId) {
|
||||
@@ -24,6 +25,9 @@ export default async function AppsPage({
|
||||
}
|
||||
const project = await projectService.getById(projectId);
|
||||
const data = await appService.getAllAppsByProjectID(projectId);
|
||||
const relevantApps = data.filter((app) =>
|
||||
RoleUtils.sessionHasReadAccessForApp(session, app.id));
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-4 pt-6">
|
||||
<PageTitle
|
||||
@@ -31,7 +35,7 @@ export default async function AppsPage({
|
||||
subtitle={`All Apps for Project "${project.name}"`}>
|
||||
<CreateProjectActions projectId={projectId} />
|
||||
</PageTitle>
|
||||
<AppTable app={data} projectId={project.id} />
|
||||
<AppTable app={relevantApps} projectId={project.id} />
|
||||
<ProjectBreadcrumbs project={project} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -18,11 +18,16 @@ import {
|
||||
import { useBreadcrumbs } from "@/frontend/states/zustand.states";
|
||||
import ProjectsBreadcrumbs from "./projects-breadcrumbs";
|
||||
import { Plus } from "lucide-react";
|
||||
import { RoleUtils } from "@/server/utils/role.utils";
|
||||
|
||||
export default async function ProjectPage() {
|
||||
|
||||
await getAuthUserSession();
|
||||
const session = await getAuthUserSession();
|
||||
const data = await projectService.getAllProjects();
|
||||
const relevantProjectsForUser = data.filter((project) =>
|
||||
project.apps.some((app) => RoleUtils.sessionHasReadAccessForApp(session, app.id)));
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-4 pt-6">
|
||||
@@ -32,7 +37,7 @@ export default async function ProjectPage() {
|
||||
<Button><Plus /> Create Project</Button>
|
||||
</EditProjectDialog>
|
||||
</div>
|
||||
<ProjectsTable data={data} />
|
||||
<ProjectsTable data={relevantProjectsForUser} />
|
||||
<ProjectsBreadcrumbs />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use server'
|
||||
|
||||
import { SuccessActionResult } from "@/shared/model/server-action-error-return.model";
|
||||
import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { getAdminUserSession, getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { ServiceException } from "@/shared/model/service.exception.model";
|
||||
import userService from "@/server/services/user.service";
|
||||
import { UserEditModel, userEditZodModel } from "@/shared/model/user-edit.model";
|
||||
@@ -10,7 +10,7 @@ import { RoleEditModel, roleEditZodModel } from "@/shared/model/role-edit.model"
|
||||
|
||||
export const saveUser = async (prevState: any, inputData: UserEditModel) =>
|
||||
saveFormAction(inputData, userEditZodModel, async (validatedData) => {
|
||||
const { email } = await getAuthUserSession(); // check admin permission
|
||||
const { email } = await getAdminUserSession();
|
||||
if (validatedData.email === email) {
|
||||
throw new ServiceException('Please edit your profile in the profile settings');
|
||||
}
|
||||
@@ -33,22 +33,21 @@ export const saveUser = async (prevState: any, inputData: UserEditModel) =>
|
||||
|
||||
export const saveRole = async (prevState: any, inputData: RoleEditModel) =>
|
||||
saveFormAction(inputData, roleEditZodModel, async (validatedData) => {
|
||||
const { email } = await getAuthUserSession(); // check admin permission
|
||||
|
||||
await roleService.save(validatedData);
|
||||
await getAdminUserSession();
|
||||
await roleService.saveWithPermissions(validatedData);
|
||||
return new SuccessActionResult();
|
||||
});
|
||||
|
||||
export const deleteUser = async (userId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession(); // todo check admin permission
|
||||
await getAdminUserSession();
|
||||
await userService.deleteUserById(userId);
|
||||
return new SuccessActionResult();
|
||||
});
|
||||
|
||||
export const deleteRole = async (roleId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession(); // todo check admin permission
|
||||
await getAdminUserSession();
|
||||
await roleService.deleteById(roleId);
|
||||
return new SuccessActionResult();
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
'use server'
|
||||
|
||||
import { getAuthUserSession } from "@/server/utils/action-wrapper.utils";
|
||||
import { getAdminUserSession, getAuthUserSession } from "@/server/utils/action-wrapper.utils";
|
||||
import PageTitle from "@/components/custom/page-title";
|
||||
import S3TargetEditOverlay from "./user-edit-overlay";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -8,7 +8,6 @@ import BreadcrumbSetter from "@/components/breadcrumbs-setter";
|
||||
import UsersTable from "./users-table";
|
||||
import userService from "@/server/services/user.service";
|
||||
import roleService from "@/server/services/role.service";
|
||||
import UserEditOverlay from "./user-edit-overlay";
|
||||
import { CircleUser, Plus, User, UserRoundCog } from "lucide-react";
|
||||
import {
|
||||
Tabs,
|
||||
@@ -19,9 +18,9 @@ import {
|
||||
import RolesTable from "./roles-table";
|
||||
import appService from "@/server/services/app.service";
|
||||
|
||||
export default async function S3TargetsPage() {
|
||||
export default async function UsersAndRolesPage() {
|
||||
|
||||
await getAuthUserSession(); // todo only admins
|
||||
await getAdminUserSession();
|
||||
const users = await userService.getAllUsers();
|
||||
const roles = await roleService.getAll();
|
||||
const allApps = await appService.getAll();
|
||||
|
||||
@@ -20,16 +20,17 @@ import { ServerActionResult } from "@/shared/model/server-action-error-return.mo
|
||||
import { toast } from "sonner"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { saveRole } from "./actions"
|
||||
import { RoleExtended } from "@/shared/model/role-extended.model.ts"
|
||||
import { RoleExtended, RolePermissionEnum } from "@/shared/model/role-extended.model.ts"
|
||||
import { RoleEditModel, roleEditZodModel } from "@/shared/model/role-edit.model"
|
||||
import { App } from "@prisma/client"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { AppWithProjectModel } from "@/shared/model/app-extended.model"
|
||||
|
||||
export default function RoleEditOverlay({ children, role, apps }: {
|
||||
children: React.ReactNode;
|
||||
role?: RoleExtended;
|
||||
apps: App[]
|
||||
apps: AppWithProjectModel[]
|
||||
}) {
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
@@ -53,7 +54,7 @@ export default function RoleEditOverlay({ children, role, apps }: {
|
||||
if (!perm.read && !perm.readwrite) return [];
|
||||
return [{
|
||||
appId: perm.appId,
|
||||
permission: perm.readwrite ? 'READWRITE' : 'READ'
|
||||
permission: perm.readwrite ? RolePermissionEnum.READWRITE : RolePermissionEnum.READ
|
||||
}];
|
||||
})
|
||||
}), FormUtils.getInitialFormState<typeof roleEditZodModel>());
|
||||
@@ -76,8 +77,8 @@ export default function RoleEditOverlay({ children, role, apps }: {
|
||||
const existingPermission = role.roleAppPermissions?.find(p => p.appId === app.id);
|
||||
return {
|
||||
appId: app.id,
|
||||
read: !!existingPermission && existingPermission.permission === 'READ',
|
||||
readwrite: !!existingPermission && existingPermission.permission === 'READWRITE'
|
||||
read: !!existingPermission && (existingPermission.permission === RolePermissionEnum.READ || existingPermission.permission === RolePermissionEnum.READWRITE),
|
||||
readwrite: !!existingPermission && existingPermission.permission === RolePermissionEnum.READWRITE
|
||||
};
|
||||
});
|
||||
|
||||
@@ -128,7 +129,7 @@ export default function RoleEditOverlay({ children, role, apps }: {
|
||||
{children}
|
||||
</div>
|
||||
<Dialog open={!!isOpen} onOpenChange={(isOpened) => setIsOpen(isOpened)}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogContent className="sm:max-w-[700px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{role?.id ? 'Edit' : 'Create'} Role</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -158,6 +159,7 @@ export default function RoleEditOverlay({ children, role, apps }: {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Project</TableHead>
|
||||
<TableHead>App</TableHead>
|
||||
<TableHead>Read</TableHead>
|
||||
<TableHead>ReadWrite</TableHead>
|
||||
@@ -168,6 +170,7 @@ export default function RoleEditOverlay({ children, role, apps }: {
|
||||
const permission = appPermissions.find(p => p.appId === app.id);
|
||||
return (
|
||||
<TableRow key={app.id}>
|
||||
<TableCell>{app.project.name}</TableCell>
|
||||
<TableCell>{app.name}</TableCell>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
|
||||
@@ -8,13 +8,13 @@ import React from "react";
|
||||
import { SimpleDataTable } from "@/components/custom/simple-data-table";
|
||||
import { formatDateTime } from "@/frontend/utils/format.utils";
|
||||
import { deleteRole } from "./actions";
|
||||
import { RoleExtended } from "@/shared/model/role-extended.model.ts";
|
||||
import { adminRoleName, RoleExtended, RolePermissionEnum } from "@/shared/model/role-extended.model.ts";
|
||||
import RoleEditOverlay from "./role-edit-overlay";
|
||||
import { App } from "@prisma/client";
|
||||
import { AppWithProjectModel } from "@/shared/model/app-extended.model";
|
||||
|
||||
export default function RolesTable({ roles, apps }: {
|
||||
roles: RoleExtended[];
|
||||
apps: App[];
|
||||
apps: AppWithProjectModel[];
|
||||
}) {
|
||||
|
||||
const { openConfirmDialog: openDialog } = useConfirmDialog();
|
||||
@@ -34,8 +34,8 @@ export default function RolesTable({ roles, apps }: {
|
||||
<SimpleDataTable columns={[
|
||||
['id', 'ID', false],
|
||||
['name', 'Name', true],
|
||||
['roleReadPermissions', 'Read Permissions', true],
|
||||
['roleWritePermissions', 'Write Permissions', true],
|
||||
['roleReadPermissions', 'Read Permissions', true, (item) => item.roleAppPermissions.filter(x => x.permission === RolePermissionEnum.READ).map(p => p.app.name).join(', ')],
|
||||
['roleWritePermissions', 'Write Permissions', true, (item) => item.roleAppPermissions.filter(x => x.permission === RolePermissionEnum.READWRITE).map(p => p.app.name).join(', ')],
|
||||
["createdAt", "Created At", true, (item) => formatDateTime(item.createdAt)],
|
||||
["updatedAt", "Updated At", false, (item) => formatDateTime(item.updatedAt)],
|
||||
]}
|
||||
@@ -43,13 +43,15 @@ export default function RolesTable({ roles, apps }: {
|
||||
actionCol={(item) =>
|
||||
<>
|
||||
<div className="flex">
|
||||
<div className="flex-1"></div>
|
||||
<RoleEditOverlay apps={apps} role={item} >
|
||||
<Button variant="ghost"><EditIcon /></Button>
|
||||
</RoleEditOverlay>
|
||||
<Button variant="ghost" onClick={() => asyncDeleteItem(item.id)}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
{item.name !== adminRoleName && <>
|
||||
<div className="flex-1"></div>
|
||||
<RoleEditOverlay apps={apps} role={item} >
|
||||
<Button variant="ghost"><EditIcon /></Button>
|
||||
</RoleEditOverlay>
|
||||
<Button variant="ghost" onClick={() => asyncDeleteItem(item.id)}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</>}
|
||||
</div>
|
||||
</>}
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@ import dataAccess from "../adapter/db.client";
|
||||
import { Tags } from "../utils/cache-tag-generator.utils";
|
||||
import { App, AppBasicAuth, AppDomain, AppFileMount, AppPort, AppVolume, Prisma } from "@prisma/client";
|
||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
||||
import { AppExtendedModel } from "@/shared/model/app-extended.model";
|
||||
import { AppExtendedModel, AppWithProjectModel } from "@/shared/model/app-extended.model";
|
||||
import { ServiceException } from "@/shared/model/service.exception.model";
|
||||
import { KubeObjectNameUtils } from "../utils/kube-object-name.utils";
|
||||
import deploymentService from "./deployment.service";
|
||||
@@ -470,10 +470,29 @@ class AppService {
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
return await dataAccess.client.app.findMany({
|
||||
const apps = await dataAccess.client.app.findMany({
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
}
|
||||
}) as AppWithProjectModel[];
|
||||
|
||||
return apps.toSorted((a, b) => {
|
||||
if (a.project.name.toLocaleLowerCase() < b.project.name.toLocaleLowerCase()) {
|
||||
return -1;
|
||||
}
|
||||
if (a.project.name.toLocaleLowerCase() > b.project.name.toLocaleLowerCase()) {
|
||||
return 1;
|
||||
}
|
||||
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,33 +3,80 @@ import dataAccess from "../adapter/db.client";
|
||||
import { revalidateTag, unstable_cache } from "next/cache";
|
||||
import { Tags } from "../utils/cache-tag-generator.utils";
|
||||
import { ServiceException } from "@/shared/model/service.exception.model";
|
||||
|
||||
|
||||
export const adminRoleName = "admin";
|
||||
export enum RolePersmission {
|
||||
READ = "READ",
|
||||
READWRITE = "READWRITE",
|
||||
}
|
||||
import { RoleEditModel } from "@/shared/model/role-edit.model";
|
||||
import { adminRoleName } from "@/shared/model/role-extended.model.ts";
|
||||
|
||||
export class RoleService {
|
||||
|
||||
async getRoleByUserId(userId: string) {
|
||||
return await unstable_cache(async (uId: string) => await dataAccess.client.role.findFirst({
|
||||
async getRoleByUserMail(email: string) {
|
||||
return await unstable_cache(async (mail: string) => await dataAccess.client.user.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
where: {
|
||||
users: {
|
||||
some: {
|
||||
id: uId
|
||||
role: {
|
||||
select: {
|
||||
name: true,
|
||||
id: true,
|
||||
roleAppPermissions: {
|
||||
select: {
|
||||
appId: true,
|
||||
permission: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
where: {
|
||||
email: mail
|
||||
}
|
||||
}).then(user => {
|
||||
return user?.role ?? null;
|
||||
}),
|
||||
[Tags.roles(), Tags.users()], {
|
||||
tags: [Tags.roles(), Tags.users()]
|
||||
})(userId);
|
||||
})(email);
|
||||
}
|
||||
|
||||
async saveWithPermissions(item: RoleEditModel) {
|
||||
try {
|
||||
if (item.name === adminRoleName) {
|
||||
throw new ServiceException("You cannot assign the name 'admin' to a role");
|
||||
}
|
||||
if (item.id) {
|
||||
await dataAccess.client.role.update({
|
||||
where: {
|
||||
id: item.id as string
|
||||
},
|
||||
data: {
|
||||
name: item.name,
|
||||
roleAppPermissions: {
|
||||
deleteMany: {},
|
||||
createMany: {
|
||||
data: item.roleAppPermissions?.map(p => ({
|
||||
appId: p.appId,
|
||||
permission: p.permission
|
||||
})) || []
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await dataAccess.client.role.create({
|
||||
data: {
|
||||
name: item.name,
|
||||
roleAppPermissions: {
|
||||
createMany: {
|
||||
data: item.roleAppPermissions?.map(p => ({
|
||||
appId: p.appId,
|
||||
permission: p.permission
|
||||
})) || []
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
revalidateTag(Tags.roles());
|
||||
revalidateTag(Tags.users());
|
||||
}
|
||||
}
|
||||
|
||||
async save(item: Prisma.RoleUncheckedCreateInput | Prisma.RoleUncheckedUpdateInput) {
|
||||
@@ -79,7 +126,15 @@ export class RoleService {
|
||||
async getAll() {
|
||||
return await unstable_cache(async () => await dataAccess.client.role.findMany({
|
||||
include: {
|
||||
roleAppPermissions: true
|
||||
roleAppPermissions: {
|
||||
include: {
|
||||
app: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
[Tags.roles()], {
|
||||
|
||||
@@ -7,7 +7,10 @@ import { ServerActionResult } from "@/shared/model/server-action-error-return.mo
|
||||
import { FormValidationException } from "@/shared/model/form-validation-exception.model";
|
||||
import { authOptions } from "@/server/utils/auth-options";
|
||||
import { NextResponse } from "next/server";
|
||||
import roleService, { adminRoleName, RolePersmission } from "../services/role.service";
|
||||
import roleService, { adminRoleName } from "../services/role.service";
|
||||
import { Role } from "@prisma/client";
|
||||
import { RolePermissionEnum } from "@/shared/model/role-extended.model.ts";
|
||||
import { RoleUtils } from "./role.utils";
|
||||
|
||||
/**
|
||||
* THIS FUNCTION RETURNS NULL IF NO USER IS LOGGED IN
|
||||
@@ -18,10 +21,26 @@ export async function getUserSession(): Promise<UserSession | null> {
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
type roleAppPermissions = {
|
||||
appId: string;
|
||||
permission: RolePermissionEnum;
|
||||
};
|
||||
let role: {
|
||||
id: string;
|
||||
name: string;
|
||||
roleAppPermissions: {
|
||||
appId: string;
|
||||
permission: string;
|
||||
}[];
|
||||
} | null = null;
|
||||
if (!!session?.user?.email) {
|
||||
role = await roleService.getRoleByUserMail(session.user.email);
|
||||
}
|
||||
return {
|
||||
email: session?.user?.email as string,
|
||||
roleId: (session?.user as any)?.roleId as string,
|
||||
roleName: (session?.user as any)?.roleName as string
|
||||
roleId: role?.id,
|
||||
roleName: role?.name,
|
||||
permissions: role?.roleAppPermissions as roleAppPermissions[],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,20 +55,16 @@ export async function getAuthUserSession(): Promise<UserSession> {
|
||||
|
||||
export async function getAdminUserSession(): Promise<UserSession> {
|
||||
const session = await getAuthUserSession();
|
||||
if (!isAdmin(session)) {
|
||||
if (!RoleUtils.isAdmin(session)) {
|
||||
console.error('User is not admin.');
|
||||
throw new ServiceException('User is not authorized for this action.');
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
function isAdmin(session: UserSession) {
|
||||
return session.roleName === adminRoleName;
|
||||
}
|
||||
|
||||
export async function isAuthorizedForRoleId(roleId: string) {
|
||||
const session = await getAuthUserSession();
|
||||
if (!isAdmin(session) && session.roleId !== roleId) {
|
||||
if (!RoleUtils.isAdmin(session) && session.roleId !== roleId) {
|
||||
console.error('User is not authorized for role: ' + roleId);
|
||||
throw new ServiceException('User is not authorized for this action.');
|
||||
}
|
||||
@@ -58,7 +73,7 @@ export async function isAuthorizedForRoleId(roleId: string) {
|
||||
|
||||
export async function isAuthorizedForRoleName(roleName: string) {
|
||||
const session = await getAuthUserSession();
|
||||
if (!isAdmin(session) && session.roleName !== roleName) {
|
||||
if (!RoleUtils.isAdmin(session) && session.roleName !== roleName) {
|
||||
console.error('User is not authorized for role: ' + roleName);
|
||||
throw new ServiceException('User is not authorized for this action.');
|
||||
}
|
||||
@@ -67,15 +82,15 @@ export async function isAuthorizedForRoleName(roleName: string) {
|
||||
|
||||
export async function isAuthorizedReadForApp(appId: string) {
|
||||
const session = await getAuthUserSession();
|
||||
if (isAdmin(session)) {
|
||||
if (RoleUtils.isAdmin(session)) {
|
||||
return session;
|
||||
}
|
||||
if (!session.roleId) {
|
||||
console.error('User is not authorized for app: ' + appId);
|
||||
throw new ServiceException('User is not authorized for this action.');
|
||||
}
|
||||
const role = await roleService.getById(session.roleId);
|
||||
if (!isAdmin(session) && !role.roleAppPermissions.some(app => app.appId === appId && app.permission === RolePersmission.READ)) {
|
||||
const roleHasReadAccessForApp = RoleUtils.sessionHasReadAccessForApp(session, appId);
|
||||
if (!roleHasReadAccessForApp) {
|
||||
console.error('User is not authorized for app: ' + appId);
|
||||
throw new ServiceException('User is not authorized for this action.');
|
||||
}
|
||||
@@ -84,21 +99,29 @@ export async function isAuthorizedReadForApp(appId: string) {
|
||||
|
||||
export async function isAuthorizedWriteForApp(appId: string) {
|
||||
const session = await getAuthUserSession();
|
||||
if (isAdmin(session)) {
|
||||
if (RoleUtils.isAdmin(session)) {
|
||||
return session;
|
||||
}
|
||||
if (!session.roleId) {
|
||||
console.error('User is not authorized for app: ' + appId);
|
||||
throw new ServiceException('User is not authorized for this action.');
|
||||
}
|
||||
const role = await roleService.getById(session.roleId);
|
||||
if (!isAdmin(session) && !role.roleAppPermissions.some(app => app.appId === appId && app.permission === RolePersmission.READWRITE)) {
|
||||
const roleHasReadAccessForApp = RoleUtils.sessionHasWriteAccessForApp(session, appId);
|
||||
if (!roleHasReadAccessForApp) {
|
||||
console.error('User is not authorized for app: ' + appId);
|
||||
throw new ServiceException('User is not authorized for this action.');
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function safeGetUserPermissionForApp(appId: string) {
|
||||
const session = await getUserSession();
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
return RoleUtils.getRolePermissionForApp(session, appId);
|
||||
}
|
||||
|
||||
export async function saveFormAction<ReturnType, TInputData, ZodType extends ZodRawShape>(
|
||||
inputData: TInputData,
|
||||
validationModel: ZodObject<ZodType>,
|
||||
|
||||
@@ -55,20 +55,6 @@ export const authOptions: NextAuthOptions = {
|
||||
}
|
||||
})
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
// Initial sign in
|
||||
return token;
|
||||
},
|
||||
async session({ session, token, user }) {
|
||||
if (token?.sub) {
|
||||
const role = await roleService.getRoleByUserId(token.sub);
|
||||
(session.user as any).roleName = role?.name;
|
||||
(session.user as any).roleId = role?.id;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
adapter: PrismaAdapter(dataAccess.client),
|
||||
};
|
||||
|
||||
|
||||
30
src/server/utils/role.utils.ts
Normal file
30
src/server/utils/role.utils.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { adminRoleName, RolePermissionEnum } from "@/shared/model/role-extended.model.ts";
|
||||
import { UserSession } from "@/shared/model/sim-session.model";
|
||||
|
||||
export class RoleUtils {
|
||||
static getRolePermissionForApp(session: UserSession, appId: string) {
|
||||
return (session.permissions?.find(app => app.appId === appId)?.permission ?? null) as RolePermissionEnum | null;
|
||||
}
|
||||
|
||||
static sessionHasReadAccessForApp(session: UserSession, appId: string) {
|
||||
if (this.isAdmin(session)) {
|
||||
return true;
|
||||
}
|
||||
const rolePermission = this.getRolePermissionForApp(session, appId);
|
||||
const roleHasReadAccessForApp = rolePermission === RolePermissionEnum.READ || rolePermission === RolePermissionEnum.READWRITE;
|
||||
return !!roleHasReadAccessForApp;
|
||||
}
|
||||
|
||||
static sessionHasWriteAccessForApp(session: UserSession, appId: string) {
|
||||
if (this.isAdmin(session)) {
|
||||
return true;
|
||||
}
|
||||
const rolePermission = this.getRolePermissionForApp(session, appId);
|
||||
const roleHasReadAccessForApp = rolePermission === RolePermissionEnum.READWRITE;
|
||||
return roleHasReadAccessForApp;
|
||||
}
|
||||
|
||||
static isAdmin(session: UserSession) {
|
||||
return session.roleName === adminRoleName;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { AppBasicAuthModel, AppDomainModel, AppFileMountModel, AppModel, AppPortModel, AppVolumeModel, ProjectModel, VolumeBackupModel } from "./generated-zod";
|
||||
import { App, Project } from "@prisma/client";
|
||||
|
||||
export const AppExtendedZodModel= z.lazy(() => AppModel.extend({
|
||||
project: ProjectModel,
|
||||
@@ -11,3 +12,7 @@ export const AppExtendedZodModel= z.lazy(() => AppModel.extend({
|
||||
}))
|
||||
|
||||
export type AppExtendedModel = z.infer<typeof AppExtendedZodModel>;
|
||||
|
||||
export type AppWithProjectModel = App & {
|
||||
project: Project;
|
||||
}
|
||||
@@ -1,5 +1,17 @@
|
||||
import { Role, RoleAppPermission, User } from "@prisma/client";
|
||||
|
||||
export type RoleExtended = Role & {
|
||||
roleAppPermissions: RoleAppPermission[];
|
||||
roleAppPermissions: (RoleAppPermission & {
|
||||
app: {
|
||||
name: string;
|
||||
};
|
||||
})[];
|
||||
}
|
||||
|
||||
export enum RolePermissionEnum {
|
||||
READ = 'READ',
|
||||
READWRITE = 'READWRITE'
|
||||
}
|
||||
|
||||
|
||||
export const adminRoleName = "admin";
|
||||
@@ -1,7 +1,13 @@
|
||||
import { RoleAppPermission } from "@prisma/client";
|
||||
import { Session } from "next-auth";
|
||||
import { RolePermissionEnum } from "./role-extended.model.ts";
|
||||
|
||||
export interface UserSession {
|
||||
email: string;
|
||||
roleName?: string;
|
||||
roleId?: string;
|
||||
permissions?: {
|
||||
appId: string,
|
||||
permission: RolePermissionEnum
|
||||
}[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user