mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-11 05:59:23 -06:00
feat: add new role permissions for app creation and backup access, update related models and utilities
This commit is contained in:
18
prisma/migrations/20250307150516_migration/migration.sql
Normal file
18
prisma/migrations/20250307150516_migration/migration.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Role" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"canCreateNewApps" BOOLEAN NOT NULL DEFAULT false,
|
||||
"canAccessBackups" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
INSERT INTO "new_Role" ("createdAt", "description", "id", "name", "updatedAt") SELECT "createdAt", "description", "id", "name", "updatedAt" FROM "Role";
|
||||
DROP TABLE "Role";
|
||||
ALTER TABLE "new_Role" RENAME TO "Role";
|
||||
CREATE UNIQUE INDEX "Role_name_key" ON "Role"("name");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
@@ -118,14 +118,13 @@ model Authenticator {
|
||||
// *** FROM HERE CUSTOM CLASSES
|
||||
|
||||
model Role {
|
||||
id String @id @default(uuid())
|
||||
name String @unique
|
||||
description String?
|
||||
id String @id @default(uuid())
|
||||
name String @unique
|
||||
description String?
|
||||
canCreateNewApps Boolean @default(false)
|
||||
canAccessBackups Boolean @default(false)
|
||||
|
||||
// A Role can be assigned to multiple users
|
||||
users User[]
|
||||
|
||||
// A Role defines permissions on apps via the join model below
|
||||
users User[]
|
||||
roleAppPermissions RoleAppPermission[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@ -4,7 +4,7 @@ import monitoringService from "@/server/services/monitoring.service";
|
||||
import clusterService from "@/server/services/node.service";
|
||||
import pvcService from "@/server/services/pvc.service";
|
||||
import backupService from "@/server/services/standalone-services/backup.service";
|
||||
import { getAuthUserSession, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { getAuthUserSession, isAuthorizedForBackups, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { AppMonitoringUsageModel } from "@/shared/model/app-monitoring-usage.model";
|
||||
import { AppVolumeMonitoringUsageModel } from "@/shared/model/app-volume-monitoring-usage.model";
|
||||
import { NodeResourceModel } from "@/shared/model/node-resource.model";
|
||||
@@ -13,7 +13,7 @@ import { z } from "zod";
|
||||
|
||||
export const downloadBackup = async (s3TargetId: string, s3Key: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await isAuthorizedForBackups();
|
||||
|
||||
const validatetData = z.object({
|
||||
s3TargetId: z.string(),
|
||||
@@ -29,7 +29,7 @@ export const downloadBackup = async (s3TargetId: string, s3Key: string) =>
|
||||
|
||||
export const deleteBackup = async (s3TargetId: string, s3Key: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await isAuthorizedForBackups();
|
||||
|
||||
const validatetData = z.object({
|
||||
s3TargetId: z.string(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use server'
|
||||
|
||||
import { getAuthUserSession } from "@/server/utils/action-wrapper.utils";
|
||||
import { getAuthUserSession, isAuthorizedForBackups } from "@/server/utils/action-wrapper.utils";
|
||||
import PageTitle from "@/components/custom/page-title";
|
||||
import backupService from "@/server/services/standalone-services/backup.service";
|
||||
import BackupsTable from "./backups-table";
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
|
||||
export default async function BackupsPage() {
|
||||
|
||||
await getAuthUserSession();
|
||||
await isAuthorizedForBackups();
|
||||
const {
|
||||
backupInfoModels,
|
||||
backupsVolumesWithoutActualBackups
|
||||
|
||||
43
src/app/global-error.tsx
Normal file
43
src/app/global-error.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client' // Error boundaries must be Client Components
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/frontend/utils/utils";
|
||||
import { AlertCircle } from "lucide-react"
|
||||
import { Inter } from "next/font/google";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}) {
|
||||
return (
|
||||
<html >
|
||||
<body className={cn(
|
||||
"min-h-screen bg-background font-sans antialiased",
|
||||
inter.variable
|
||||
)}>
|
||||
<div className="h-screen w-fuäll flex flex-col items-center justify-center p-4 space-y-4 bg-background text-foreground">
|
||||
<div className="flex flex-col items-center justify-center space-y-2 text-center max-w-md">
|
||||
<div className="rounded-full bg-destructive/10 p-3">
|
||||
<AlertCircle className="h-8 w-8 text-destructive" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">Something went wrong!</h2>
|
||||
<p className="text-muted-foreground mt-4">
|
||||
An unexpected error occurred. Please check if your authorized for this action and try again.
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-6">
|
||||
Digest: {error.digest}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import monitoringService from "@/server/services/monitoring.service";
|
||||
import clusterService from "@/server/services/node.service";
|
||||
import { getAuthUserSession, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { RoleUtils } from "@/server/utils/role.utils";
|
||||
import { AppMonitoringUsageModel } from "@/shared/model/app-monitoring-usage.model";
|
||||
import { AppVolumeMonitoringUsageModel } from "@/shared/model/app-volume-monitoring-usage.model";
|
||||
import { NodeResourceModel } from "@/shared/model/node-resource.model";
|
||||
@@ -16,12 +17,16 @@ export const getNodeResourceUsage = async () =>
|
||||
|
||||
export const getVolumeMonitoringUsage = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
return await monitoringService.getAllAppVolumesUsage();
|
||||
const session = await getAuthUserSession();
|
||||
let volumesUsage = await monitoringService.getAllAppVolumesUsage();
|
||||
volumesUsage = volumesUsage?.filter((volume) => RoleUtils.sessionHasReadAccessForApp(session, volume.appId));
|
||||
return volumesUsage;
|
||||
}) as Promise<ServerActionResult<unknown, AppVolumeMonitoringUsageModel[]>>;
|
||||
|
||||
export const getMonitoringForAllApps = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
return await monitoringService.getMonitoringForAllApps();
|
||||
const session = await getAuthUserSession();
|
||||
let updatedNodeRessources = await monitoringService.getMonitoringForAllApps();
|
||||
updatedNodeRessources = updatedNodeRessources?.filter((app) => RoleUtils.sessionHasReadAccessForApp(session, app.appId));
|
||||
return updatedNodeRessources;
|
||||
}) as Promise<ServerActionResult<unknown, AppMonitoringUsageModel[]>>;
|
||||
@@ -10,10 +10,11 @@ import monitoringService from "@/server/services/monitoring.service";
|
||||
import AppRessourceMonitoring from "./app-monitoring";
|
||||
import AppVolumeMonitoring from "./app-volumes-monitoring";
|
||||
import { AppMonitoringUsageModel } from "@/shared/model/app-monitoring-usage.model";
|
||||
import { RoleUtils } from "@/server/utils/role.utils";
|
||||
|
||||
export default async function ResourceNodesInfoPage() {
|
||||
|
||||
await getAuthUserSession();
|
||||
const session = await getAuthUserSession();
|
||||
let resourcesNode: NodeResourceModel[] | undefined;
|
||||
let volumesUsage: AppVolumeMonitoringUsageModel[] | undefined;
|
||||
let updatedNodeRessources: AppMonitoringUsageModel[] | undefined;
|
||||
@@ -27,6 +28,10 @@ export default async function ResourceNodesInfoPage() {
|
||||
// do nothing --> if an error occurs, the ResourceNodes will show a loading spinner and error message
|
||||
}
|
||||
|
||||
// filter by role
|
||||
volumesUsage = volumesUsage?.filter((volume) => RoleUtils.sessionHasReadAccessForApp(session, volume.appId));
|
||||
updatedNodeRessources = updatedNodeRessources?.filter((app) => RoleUtils.sessionHasReadAccessForApp(session, app.appId));
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-4 pt-6">
|
||||
<PageTitle
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { SuccessActionResult } from "@/shared/model/server-action-error-return.model";
|
||||
import appService from "@/server/services/app.service";
|
||||
import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { getAuthUserSession, isAuthorizedWriteForApp, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { z } from "zod";
|
||||
import appTemplateService from "@/server/services/app-template.service";
|
||||
import { AppTemplateModel, appTemplateZodModel } from "@/shared/model/app-template.model";
|
||||
@@ -11,6 +11,7 @@ import dbGateService from "@/server/services/db-tool-services/dbgate.service";
|
||||
import fileBrowserService from "@/server/services/file-browser-service";
|
||||
import phpMyAdminService from "@/server/services/db-tool-services/phpmyadmin.service";
|
||||
import pgAdminService from "@/server/services/db-tool-services/pgadmin.service";
|
||||
import { RoleUtils } from "@/server/utils/role.utils";
|
||||
|
||||
const createAppSchema = z.object({
|
||||
appName: z.string().min(1)
|
||||
@@ -18,7 +19,10 @@ const createAppSchema = z.object({
|
||||
|
||||
export const createApp = async (appName: string, projectId: string, appId?: string) =>
|
||||
saveFormAction({ appName }, createAppSchema, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
const session = await getAuthUserSession();
|
||||
if (!RoleUtils.sessionCanCreateNewApps(session)) {
|
||||
throw new ServiceException("You are not allowed to create new apps.");
|
||||
}
|
||||
|
||||
const returnData = await appService.save({
|
||||
id: appId ?? undefined,
|
||||
@@ -31,7 +35,10 @@ export const createApp = async (appName: string, projectId: string, appId?: stri
|
||||
|
||||
export const createAppFromTemplate = async (prevState: any, inputData: AppTemplateModel, projectId: string) =>
|
||||
saveFormAction(inputData, appTemplateZodModel, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
const session = await getAuthUserSession();
|
||||
if (!RoleUtils.sessionCanCreateNewApps(session)) {
|
||||
throw new ServiceException("You are not allowed to create new apps.");
|
||||
}
|
||||
if (validatedData.templates.some(x => x.inputSettings.some(y => !y.randomGeneratedIfEmpty && !y.value))) {
|
||||
throw new ServiceException('Please fill out all required fields.');
|
||||
}
|
||||
@@ -41,7 +48,7 @@ export const createAppFromTemplate = async (prevState: any, inputData: AppTempla
|
||||
|
||||
export const deleteApp = async (appId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await isAuthorizedWriteForApp(appId);
|
||||
const app = await appService.getExtendedById(appId);
|
||||
// First delete external services wich might be running
|
||||
await dbGateService.deleteToolForAppIfExists(appId);
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
import { SuccessActionResult } from "@/shared/model/server-action-error-return.model";
|
||||
import projectService from "@/server/services/project.service";
|
||||
import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { getAdminUserSession, getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { z } from "zod";
|
||||
import { RoleUtils } from "@/server/utils/role.utils";
|
||||
import { ServiceException } from "@/shared/model/service.exception.model";
|
||||
|
||||
const createProjectSchema = z.object({
|
||||
projectName: z.string().min(1),
|
||||
@@ -12,7 +14,10 @@ const createProjectSchema = z.object({
|
||||
|
||||
export const createProject = async (projectName: string, projectId?: string) =>
|
||||
saveFormAction({ projectName, projectId }, createProjectSchema, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
const session = await getAuthUserSession();
|
||||
if (!RoleUtils.sessionCanCreateNewApps(session)) {
|
||||
throw new ServiceException("You are not allowed to create new projects.");
|
||||
}
|
||||
await projectService.save({
|
||||
id: validatedData.projectId ?? undefined,
|
||||
name: validatedData.projectName
|
||||
@@ -22,7 +27,7 @@ export const createProject = async (projectName: string, projectId?: string) =>
|
||||
|
||||
export const deleteProject = async (projectId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
await projectService.deleteById(projectId);
|
||||
return new SuccessActionResult(undefined, "Project deleted successfully.");
|
||||
});
|
||||
@@ -10,12 +10,10 @@ import { Edit2, Eye, MoreHorizontal, Trash } from "lucide-react";
|
||||
import { Toast } from "@/frontend/utils/toast.utils";
|
||||
import { Project } from "@prisma/client";
|
||||
import { deleteProject } from "./actions";
|
||||
import { useBreadcrumbs, useConfirmDialog } from "@/frontend/states/zustand.states";
|
||||
import { useEffect } from "react";
|
||||
import { useConfirmDialog } from "@/frontend/states/zustand.states";
|
||||
import { EditProjectDialog } from "./edit-project-dialog";
|
||||
|
||||
|
||||
|
||||
export default function ProjectsTable({ data }: { data: Project[] }) {
|
||||
|
||||
const { openConfirmDialog: openDialog } = useConfirmDialog();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use server'
|
||||
|
||||
import { getAuthUserSession, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { getAdminUserSession, getAuthUserSession, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { SuccessActionResult } from "@/shared/model/server-action-error-return.model";
|
||||
import clusterService from "@/server/services/node.service";
|
||||
|
||||
export const setNodeStatus = async (nodeName: string, schedulable: boolean) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
await clusterService.setNodeStatus(nodeName, schedulable);
|
||||
return new SuccessActionResult(undefined, 'Successfully updated node status.');
|
||||
});
|
||||
|
||||
@@ -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 clusterService from "@/server/services/node.service";
|
||||
import NodeInfo from "./nodeInfo";
|
||||
@@ -11,7 +11,7 @@ import BreadcrumbSetter from "@/components/breadcrumbs-setter";
|
||||
|
||||
export default async function ClusterInfoPage() {
|
||||
|
||||
const session = await getAuthUserSession();
|
||||
const session = await getAdminUserSession();
|
||||
const nodeInfo = await clusterService.getNodeInfo();
|
||||
const clusterJoinToken = await paramService.getString(ParamService.K3S_JOIN_TOKEN);
|
||||
return (
|
||||
|
||||
@@ -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 paramService, { ParamService } from "@/server/services/param.service";
|
||||
import podService from "@/server/services/pod.service";
|
||||
@@ -13,7 +13,7 @@ import quickStackService from "@/server/services/qs.service";
|
||||
|
||||
export default async function MaintenancePage() {
|
||||
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
const useCanaryChannel = await paramService.getBoolean(ParamService.USE_CANARY_CHANNEL, false);
|
||||
const qsPodInfos = await podService.getPodsForApp(Constants.QS_NAMESPACE, Constants.QS_APP_NAME);
|
||||
const qsPodInfo = qsPodInfos.find(p => !!p);
|
||||
|
||||
@@ -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 { S3TargetEditModel, s3TargetEditZodModel } from "@/shared/model/s3-target-edit.model";
|
||||
import s3TargetService from "@/server/services/s3-target.service";
|
||||
import s3Service from "@/server/services/aws-s3.service";
|
||||
@@ -10,7 +10,7 @@ import { ServiceException } from "@/shared/model/service.exception.model";
|
||||
|
||||
export const saveS3Target = async (prevState: any, inputData: S3TargetEditModel) =>
|
||||
saveFormAction(inputData, s3TargetEditZodModel, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
|
||||
const url = new URL(validatedData.endpoint.includes('://') ? validatedData.endpoint : `https://${validatedData.endpoint}`);
|
||||
validatedData.endpoint = url.hostname;
|
||||
@@ -27,7 +27,7 @@ export const saveS3Target = async (prevState: any, inputData: S3TargetEditModel)
|
||||
|
||||
export const deleteS3Target = async (s3TargetId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
await s3TargetService.deleteById(s3TargetId);
|
||||
return new SuccessActionResult(undefined, 'Successfully deleted S3 Target');
|
||||
});
|
||||
@@ -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 s3TargetService from "@/server/services/s3-target.service";
|
||||
import S3TargetsTable from "./s3-targets-table";
|
||||
@@ -10,7 +10,7 @@ import BreadcrumbSetter from "@/components/breadcrumbs-setter";
|
||||
|
||||
export default async function S3TargetsPage() {
|
||||
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
const data = await s3TargetService.getAll();
|
||||
return (
|
||||
<div className="flex-1 space-y-4 pt-6">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use server'
|
||||
|
||||
import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { getAdminUserSession, getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import paramService, { ParamService } from "@/server/services/param.service";
|
||||
import { QsIngressSettingsModel, qsIngressSettingsZodModel } from "@/shared/model/qs-settings.model";
|
||||
import { QsLetsEncryptSettingsModel, qsLetsEncryptSettingsZodModel } from "@/shared/model/qs-letsencrypt-settings.model";
|
||||
@@ -20,7 +20,7 @@ import appLogsService from "@/server/services/standalone-services/app-logs.servi
|
||||
|
||||
export const updateIngressSettings = async (prevState: any, inputData: QsIngressSettingsModel) =>
|
||||
saveFormAction(inputData, qsIngressSettingsZodModel, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
|
||||
const url = new URL(validatedData.serverUrl.includes('://') ? validatedData.serverUrl : `https://${validatedData.serverUrl}`);
|
||||
|
||||
@@ -41,7 +41,7 @@ export const updateIngressSettings = async (prevState: any, inputData: QsIngress
|
||||
|
||||
export const updatePublicIpv4Settings = async (prevState: any, inputData: QsPublicIpv4SettingsModel) =>
|
||||
saveFormAction(inputData, qsPublicIpv4SettingsZodModel, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
|
||||
await paramService.save({
|
||||
name: ParamService.PUBLIC_IPV4_ADDRESS,
|
||||
@@ -52,7 +52,7 @@ export const updatePublicIpv4Settings = async (prevState: any, inputData: QsPubl
|
||||
|
||||
export const updatePublicIpv4SettingsAutomatically = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
|
||||
const publicIpv4 = await ipAddressFinderAdapter.getPublicIpOfServer();
|
||||
await paramService.save({
|
||||
@@ -63,7 +63,7 @@ export const updatePublicIpv4SettingsAutomatically = async () =>
|
||||
|
||||
export const updateLetsEncryptSettings = async (prevState: any, inputData: QsLetsEncryptSettingsModel) =>
|
||||
saveFormAction(inputData, qsLetsEncryptSettingsZodModel, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
|
||||
await paramService.save({
|
||||
name: ParamService.LETS_ENCRYPT_MAIL,
|
||||
@@ -75,7 +75,7 @@ export const updateLetsEncryptSettings = async (prevState: any, inputData: QsLet
|
||||
|
||||
export const getConfiguredHostname: () => Promise<ServerActionResult<unknown, string | undefined>> = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
|
||||
return await paramService.getString(ParamService.QS_SERVER_HOSTNAME);
|
||||
});
|
||||
@@ -83,21 +83,21 @@ export const getConfiguredHostname: () => Promise<ServerActionResult<unknown, st
|
||||
|
||||
export const cleanupOldTmpFiles = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
await maintenanceService.deleteAllTempFiles();
|
||||
return new SuccessActionResult(undefined, 'Successfully cleaned up temp files.');
|
||||
});
|
||||
|
||||
export const cleanupOldBuildJobs = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
await buildService.deleteAllFailedOrSuccededBuilds();
|
||||
return new SuccessActionResult(undefined, 'Successfully cleaned up old build jobs.');
|
||||
});
|
||||
|
||||
export const updateQuickstack = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
const useCaranyChannel = await paramService.getBoolean(ParamService.USE_CANARY_CHANNEL, false);
|
||||
await quickStackService.updateQuickStack(useCaranyChannel);
|
||||
return new SuccessActionResult(undefined, 'QuickStack will be updated, refresh the page in a few seconds.');
|
||||
@@ -105,7 +105,7 @@ export const updateQuickstack = async () =>
|
||||
|
||||
export const updateRegistry = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
const registryLocation = await paramService.getString(ParamService.REGISTRY_SOTRAGE_LOCATION, Constants.INTERNAL_REGISTRY_LOCATION);
|
||||
await registryService.deployRegistry(registryLocation!, true);
|
||||
return new SuccessActionResult(undefined, 'Registry will be updated, this might take a few seconds.');
|
||||
@@ -113,35 +113,35 @@ export const updateRegistry = async () =>
|
||||
|
||||
export const updateTraefikMeCertificates = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
await traefikMeDomainStandaloneService.updateTraefikMeCertificate();
|
||||
return new SuccessActionResult(undefined, 'Certificates will be updated, this might take a few seconds.');
|
||||
});
|
||||
|
||||
export const deleteAllFailedAndSuccededPods = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
await standalonePodService.deleteAllFailedAndSuccededPods();
|
||||
return new SuccessActionResult(undefined, 'Successfully deleted all failed and succeeded pods.');
|
||||
});
|
||||
|
||||
export const purgeRegistryImages = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
const deletedSize = await registryService.purgeRegistryImages();
|
||||
return new SuccessActionResult(undefined, `Successfully purged ${KubeSizeConverter.convertBytesToReadableSize(deletedSize)} of images.`);
|
||||
});
|
||||
|
||||
export const deleteOldAppLogs = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
await appLogsService.deleteOldAppLogs();
|
||||
return new SuccessActionResult(undefined, `Successfully deletes old app logs.`);
|
||||
});
|
||||
|
||||
export const setCanaryChannel = async (useCanaryChannel: boolean) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
await paramService.save({
|
||||
name: ParamService.USE_CANARY_CHANNEL,
|
||||
value: !!useCanaryChannel ? 'true' : 'false'
|
||||
@@ -151,7 +151,7 @@ export const setCanaryChannel = async (useCanaryChannel: boolean) =>
|
||||
|
||||
export const setRegistryStorageLocation = async (prevState: any, inputData: RegistryStorageLocationSettingsModel) =>
|
||||
saveFormAction(inputData, registryStorageLocationSettingsZodModel, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
await getAdminUserSession();
|
||||
|
||||
await registryService.deployRegistry(validatedData.registryStorageLocation, true);
|
||||
|
||||
|
||||
@@ -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 paramService, { ParamService } from "@/server/services/param.service";
|
||||
import QuickStackIngressSettings from "./qs-ingress-settings";
|
||||
@@ -13,7 +13,7 @@ import BreadcrumbSetter from "@/components/breadcrumbs-setter";
|
||||
|
||||
export default async function ProjectPage() {
|
||||
|
||||
const session = await getAuthUserSession();
|
||||
const session = await getAdminUserSession();
|
||||
const serverUrl = await paramService.getString(ParamService.QS_SERVER_HOSTNAME, '');
|
||||
const disableNodePortAccess = await paramService.getBoolean(ParamService.DISABLE_NODEPORT_ACCESS, false);
|
||||
const letsEncryptMail = await paramService.getString(ParamService.LETS_ENCRYPT_MAIL, session.email);
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function RoleEditOverlay({ children, role, apps }: {
|
||||
|
||||
const [state, formAction] = useFormState((state: ServerActionResult<any, any>,
|
||||
payload: RoleEditModel) =>
|
||||
saveRole(state, {
|
||||
saveRole(state, {
|
||||
...payload,
|
||||
id: role?.id,
|
||||
roleAppPermissions: appPermissions.flatMap(perm => {
|
||||
@@ -154,6 +154,38 @@ export default function RoleEditOverlay({ children, role, apps }: {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canCreateNewApps"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex gap-4">
|
||||
<FormLabel className="pt-2">Can create new apps</FormLabel>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessBackups"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex gap-4">
|
||||
<FormLabel className="pt-2">Can access backups</FormLabel>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="pt-3">
|
||||
<h3 className="text-sm font-medium mb-2">App Permissions</h3>
|
||||
<Table>
|
||||
|
||||
@@ -31,6 +31,7 @@ import { usePathname } from "next/navigation"
|
||||
import { useRouter } from "next/router"
|
||||
import { useEffect, useState } from "react"
|
||||
import QuickStackLogo from "@/components/custom/quickstack-logo"
|
||||
import { RoleUtils } from "@/server/utils/role.utils"
|
||||
|
||||
|
||||
const settingsMenu = [
|
||||
@@ -43,26 +44,28 @@ const settingsMenu = [
|
||||
title: "Users & Roles",
|
||||
url: "/settings/users",
|
||||
icon: User2,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
title: "S3 Targets",
|
||||
url: "/settings/s3-targets",
|
||||
icon: Settings,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
title: "QuickStack Settings",
|
||||
url: "/settings/server",
|
||||
icon: Settings,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
title: "Cluster",
|
||||
url: "/settings/cluster",
|
||||
icon: Server,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
title: "Maintenance",
|
||||
url: "/settings/maintenance",
|
||||
icon: Settings,
|
||||
adminOnly: true,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -118,7 +121,7 @@ export function SidebarCient({
|
||||
<SidebarMenuButton size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-qs-500 text-sidebar-primary-foreground">
|
||||
<QuickStackLogo className="size-5" color="light-all" />
|
||||
<QuickStackLogo className="size-5" color="light-all" />
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight my-4">
|
||||
<span className="truncate font-semibold">QuickStack</span>
|
||||
@@ -231,12 +234,12 @@ export function SidebarCient({
|
||||
</SidebarGroup>
|
||||
|
||||
|
||||
<SidebarGroup>
|
||||
{RoleUtils.sessionHasAccessToBackups(session) && <SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip={{
|
||||
children: 'Monitoring',
|
||||
children: 'Backups',
|
||||
hidden: open,
|
||||
}}
|
||||
isActive={path.startsWith('/backups')}>
|
||||
@@ -248,7 +251,7 @@ export function SidebarCient({
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarGroup>}
|
||||
|
||||
|
||||
<SidebarGroup>
|
||||
@@ -265,16 +268,16 @@ export function SidebarCient({
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuSub>
|
||||
{settingsMenu.map((item) => (
|
||||
<SidebarMenuSubItem key={item.title}>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={item.url}>
|
||||
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
{(RoleUtils.isAdmin(session) ? settingsMenu :
|
||||
settingsMenu.filter(x => !x.adminOnly)).map((item) => (
|
||||
<SidebarMenuSubItem key={item.title}>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={item.url}>
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import projectService from "@/server/services/project.service"
|
||||
import { getUserSession } from "@/server/utils/action-wrapper.utils"
|
||||
import { SidebarCient } from "./sidebar-client"
|
||||
import { RoleUtils } from "@/server/utils/role.utils";
|
||||
|
||||
export async function AppSidebar() {
|
||||
|
||||
@@ -12,5 +13,11 @@ export async function AppSidebar() {
|
||||
|
||||
const projects = await projectService.getAllProjects();
|
||||
|
||||
return <SidebarCient projects={projects} session={session} />
|
||||
const relevantProjectsForUser = projects.filter((project) =>
|
||||
project.apps.some((app) => RoleUtils.sessionHasReadAccessForApp(session, app.id)));
|
||||
for (const project of relevantProjectsForUser) {
|
||||
project.apps = project.apps.filter((app) => RoleUtils.sessionHasReadAccessForApp(session, app.id));
|
||||
}
|
||||
|
||||
return <SidebarCient projects={relevantProjectsForUser} session={session} />
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ export class RoleService {
|
||||
select: {
|
||||
name: true,
|
||||
id: true,
|
||||
canAccessBackups: true,
|
||||
canCreateNewApps: true,
|
||||
roleAppPermissions: {
|
||||
select: {
|
||||
appId: true,
|
||||
@@ -47,6 +49,8 @@ export class RoleService {
|
||||
},
|
||||
data: {
|
||||
name: item.name,
|
||||
canAccessBackups: item.canAccessBackups,
|
||||
canCreateNewApps: item.canCreateNewApps,
|
||||
roleAppPermissions: {
|
||||
deleteMany: {},
|
||||
createMany: {
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 } from "../services/role.service";
|
||||
import roleService from "../services/role.service";
|
||||
import { Role } from "@prisma/client";
|
||||
import { RolePermissionEnum } from "@/shared/model/role-extended.model.ts";
|
||||
import { RoleUtils } from "./role.utils";
|
||||
@@ -28,6 +28,8 @@ export async function getUserSession(): Promise<UserSession | null> {
|
||||
let role: {
|
||||
id: string;
|
||||
name: string;
|
||||
canAccessBackups: boolean,
|
||||
canCreateNewApps: boolean,
|
||||
roleAppPermissions: {
|
||||
appId: string;
|
||||
permission: string;
|
||||
@@ -40,6 +42,8 @@ export async function getUserSession(): Promise<UserSession | null> {
|
||||
email: session?.user?.email as string,
|
||||
roleId: role?.id,
|
||||
roleName: role?.name,
|
||||
canAccessBackups: role?.canAccessBackups,
|
||||
canCreateNewApps: role?.canCreateNewApps,
|
||||
permissions: role?.roleAppPermissions as roleAppPermissions[],
|
||||
};
|
||||
}
|
||||
@@ -62,6 +66,15 @@ export async function getAdminUserSession(): Promise<UserSession> {
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function isAuthorizedForBackups() {
|
||||
const session = await getAuthUserSession();
|
||||
if (!RoleUtils.sessionHasAccessToBackups(session)) {
|
||||
console.error('User is not authorized for backups.');
|
||||
throw new ServiceException('User is not authorized for this action.');
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function isAuthorizedForRoleId(roleId: string) {
|
||||
const session = await getAuthUserSession();
|
||||
if (!RoleUtils.isAdmin(session) && session.roleId !== roleId) {
|
||||
@@ -89,7 +102,7 @@ export async function isAuthorizedReadForApp(appId: string) {
|
||||
console.error('User is not authorized for app: ' + appId);
|
||||
throw new ServiceException('User is not authorized for this action.');
|
||||
}
|
||||
const roleHasReadAccessForApp = RoleUtils.sessionHasReadAccessForApp(session, appId);
|
||||
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.');
|
||||
@@ -106,7 +119,7 @@ export async function isAuthorizedWriteForApp(appId: string) {
|
||||
console.error('User is not authorized for app: ' + appId);
|
||||
throw new ServiceException('User is not authorized for this action.');
|
||||
}
|
||||
const roleHasReadAccessForApp = RoleUtils.sessionHasWriteAccessForApp(session, appId);
|
||||
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.');
|
||||
|
||||
@@ -6,6 +6,20 @@ export class RoleUtils {
|
||||
return (session.permissions?.find(app => app.appId === appId)?.permission ?? null) as RolePermissionEnum | null;
|
||||
}
|
||||
|
||||
static sessionHasAccessToBackups(session: UserSession) {
|
||||
if (this.isAdmin(session)) {
|
||||
return true;
|
||||
}
|
||||
return !!session.canAccessBackups;
|
||||
}
|
||||
|
||||
static sessionCanCreateNewApps(session: UserSession) {
|
||||
if (this.isAdmin(session)) {
|
||||
return true;
|
||||
}
|
||||
return !!session.canCreateNewApps;
|
||||
}
|
||||
|
||||
static sessionIsReadOnlyForApp(session: UserSession, appId: string) {
|
||||
if (this.isAdmin(session)) {
|
||||
return false;
|
||||
|
||||
@@ -6,6 +6,8 @@ export const RoleModel = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullish(),
|
||||
canCreateNewApps: z.boolean(),
|
||||
canAccessBackups: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
})
|
||||
|
||||
@@ -4,6 +4,8 @@ import { z } from "zod";
|
||||
export const roleEditZodModel = z.object({
|
||||
id: z.string().trim().optional(),
|
||||
name: z.string().trim().min(1),
|
||||
canCreateNewApps: z.boolean().optional().default(false),
|
||||
canAccessBackups: z.boolean().optional().default(false),
|
||||
roleAppPermissions: z.array(z.object({
|
||||
appId: z.string(),
|
||||
permission: z.string(),
|
||||
|
||||
@@ -6,6 +6,8 @@ export interface UserSession {
|
||||
email: string;
|
||||
roleName?: string;
|
||||
roleId?: string;
|
||||
canAccessBackups?: boolean;
|
||||
canCreateNewApps?: boolean;
|
||||
permissions?: {
|
||||
appId: string,
|
||||
permission: RolePermissionEnum
|
||||
|
||||
Reference in New Issue
Block a user