├── .eslintrc.json ├── public ├── quickstack-icon-dark.png ├── quick-stack-logo-light.png ├── quickstack-repo-heading.png └── template-icons │ ├── wordpress.png │ └── mongodb.svg ├── prisma └── migrations │ ├── 20241229143025_migration │ └── migration.sql │ ├── 20250324085100_delete_roleprojectpermission │ └── migration.sql │ ├── migration_lock.toml │ ├── 20241229131352_migration │ └── migration.sql │ ├── 20241121151959_migration │ └── migration.sql │ ├── 20241025085330_migration │ └── migration.sql │ ├── 20241202170526_migration │ └── migration.sql │ ├── 20241231161704_migration │ └── migration.sql │ ├── 20250107081600_migration │ └── migration.sql │ ├── 20241202160004_migration │ └── migration.sql │ ├── 20241223140802_migration │ └── migration.sql │ ├── 20250102145143_migration │ └── migration.sql │ ├── 20250307150516_migration │ └── migration.sql │ ├── 20241026155705_migration │ └── migration.sql │ ├── 20241106181617_migration │ └── migration.sql │ ├── 20241017064349_migration │ └── migration.sql │ ├── 20241028143028_migration │ └── migration.sql │ ├── 20251209144504_migration │ └── migration.sql │ ├── 20251219105325_add_storage_class_to_app_volume │ └── migration.sql │ ├── 20241024113318_migration │ └── migration.sql │ ├── 20241222123831_migration │ └── migration.sql │ ├── 20241025144850_migration │ └── migration.sql │ ├── 20241024084212_migration │ └── migration.sql │ ├── 20241107155300_migration │ └── migration.sql │ └── 20241021100307_migration │ └── migration.sql ├── github-assets └── app-settings-general.png ├── .devcontainer ├── devcontainer.env_example ├── docker.compose.yml └── devcontainer.json ├── src ├── shared │ ├── model │ │ ├── downloadable-app-logs.model.ts │ │ ├── project-extended.model.ts │ │ ├── totp.model.ts │ │ ├── event-info.model.ts │ │ ├── generated-zod │ │ │ ├── verificationtoken.ts │ │ │ ├── parameter.ts │ │ │ ├── index.ts │ │ │ ├── appport.ts │ │ │ ├── appports.ts │ │ │ ├── session.ts │ │ │ ├── appbasicauth.ts │ │ │ ├── appfilemount.ts │ │ │ ├── appdomain.ts │ │ │ ├── project.ts │ │ │ ├── s3target.ts │ │ │ ├── authenticator.ts │ │ │ ├── role.ts │ │ │ ├── appvolume.ts │ │ │ ├── volumebackup.ts │ │ │ ├── usergroup.ts │ │ │ ├── account.ts │ │ │ ├── roleapppermission.ts │ │ │ ├── roleprojectpermission.ts │ │ │ └── user.ts │ │ ├── env-edit.model.ts │ │ ├── network-policy.model.ts │ │ ├── traefik-ip-propagation.model.ts │ │ ├── volume-upload.model.ts │ │ ├── qs-public-ipv4-settings.model.ts │ │ ├── file-mount-edit.model.ts │ │ ├── app-monitoring-usage.model.ts │ │ ├── default-port.model.ts │ │ ├── app-volume-monitoring-usage.model.ts │ │ ├── system-backup-location-settings.model.ts │ │ ├── service.exception.model.ts │ │ ├── pods-info.model.ts │ │ ├── qs-letsencrypt-settings.model.ts │ │ ├── qs-settings.model.ts │ │ ├── volume-backup-extended.model.ts │ │ ├── update-password.model.ts │ │ ├── user-extended.model.ts │ │ ├── pods-resource-info.model.ts │ │ ├── registry-storage-location-settings.model.ts │ │ ├── basic-auth-edit.model.ts │ │ ├── domain-edit.model.ts │ │ ├── user-edit.model.ts │ │ ├── role-extended.model.ts.ts │ │ ├── terminal-setup-info.model.ts │ │ ├── backup-info.model.ts │ │ ├── app-rate-limits.model.ts │ │ ├── s3-target-edit.model.ts │ │ ├── node-resource.model.ts │ │ ├── build-job.ts │ │ ├── backup-volume-edit.model.ts │ │ ├── volume-edit.model.ts │ │ ├── form-validation-exception.model.ts │ │ ├── auth-form.ts │ │ ├── database-template-info.model.ts │ │ ├── app-extended.model.ts │ │ ├── deployment-info.model.ts │ │ ├── sim-session.model.ts │ │ ├── node-info.model.ts │ │ ├── server-action-error-return.model.ts │ │ ├── role-edit.model.ts │ │ └── app-source-info.model.ts │ ├── utils │ │ ├── date.utils.ts │ │ ├── stream.utils.ts │ │ ├── fancy-console.utils.ts │ │ ├── react-node.utils.ts │ │ ├── constants.ts │ │ └── domain-dns-provider.utils.ts │ └── templates │ │ └── all.templates.ts ├── middleware.ts ├── app │ ├── error │ │ └── page.tsx │ ├── unauthorized │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── print-schedules-jobs │ │ │ └── route.ts │ │ ├── v1 │ │ │ └── webhook │ │ │ │ └── deploy │ │ │ │ └── route.ts │ │ ├── volume-data-download │ │ │ └── route.ts │ │ ├── logs-download │ │ │ └── route.ts │ │ └── build-logs │ │ │ └── route.ts │ ├── page.tsx │ ├── projects │ │ ├── projects-breadcrumbs.tsx │ │ ├── edit-project-dialog.tsx │ │ ├── actions.ts │ │ └── project-page.tsx │ ├── project │ │ ├── [projectId] │ │ │ ├── project-breadcrumbs.tsx │ │ │ ├── edit-app-dialog.tsx │ │ │ ├── create-project-actions.tsx │ │ │ ├── project-overview.tsx │ │ │ └── page.tsx │ │ └── app │ │ │ └── [appId] │ │ │ ├── app-breadcrumbs.tsx │ │ │ ├── environment │ │ │ └── actions.ts │ │ │ ├── credentials │ │ │ └── db-tools.tsx │ │ │ ├── overview │ │ │ ├── terminal-overlay.tsx │ │ │ ├── build-logs-overlay.tsx │ │ │ └── deployment-status-badge.tsx │ │ │ ├── page.tsx │ │ │ ├── layout.tsx │ │ │ └── actions.ts │ ├── auth │ │ └── page.tsx │ ├── sidebar.tsx │ ├── settings │ │ ├── server │ │ │ ├── server-settings-tabs.tsx │ │ │ └── hostname-check.tsx │ │ ├── profile │ │ │ ├── totp-settings.tsx │ │ │ └── page.tsx │ │ ├── s3-targets │ │ │ ├── page.tsx │ │ │ └── actions.ts │ │ └── cluster │ │ │ ├── actions.ts │ │ │ └── page.tsx │ ├── global-error.tsx │ ├── backups │ │ └── backups-table.tsx │ ├── monitoring │ │ └── actions.ts │ └── sidebar-logout-button.tsx ├── frontend │ ├── sockets │ │ └── sockets.ts │ ├── utils │ │ ├── utils.ts │ │ ├── nextjs-actions.utils.ts │ │ ├── format.utils.ts │ │ ├── form.utilts.ts │ │ └── toast.utils.ts │ └── hooks │ │ └── use-mobile.tsx ├── server │ ├── services │ │ ├── standalone-services │ │ │ ├── 00_info.md │ │ │ ├── maintenance.service.ts │ │ │ └── schedule.service.ts │ │ ├── hostname-dns-provider.service.ts │ │ ├── setup-services │ │ │ └── ingress-setup.service.ts │ │ └── namespace.service.ts │ ├── utils │ │ ├── env-var.utils.ts │ │ ├── command-executor.utils.ts │ │ └── cache-tag-generator.utils.ts │ └── adapter │ │ ├── ip-adress-finder.adapter.ts │ │ └── aws-s3.adapter.ts ├── components │ ├── custom │ │ ├── navigate-back.tsx │ │ ├── short-commit-hash.tsx │ │ ├── page-title.tsx │ │ ├── submit-button.tsx │ │ ├── bottom-bar-menu.tsx │ │ ├── text-link.tsx │ │ ├── pods-status-polling-provider.tsx │ │ ├── code.tsx │ │ ├── loading-alert-dialog-action.tsx │ │ ├── hint-box-url.tsx │ │ ├── form-label-with-question.tsx │ │ ├── confirm-dialog.tsx │ │ ├── copy-input-field.tsx │ │ └── logs-overlay.tsx │ ├── ui │ │ ├── full-loading-spinnter.tsx │ │ ├── skeleton.tsx │ │ ├── collapsible.tsx │ │ ├── spinner.tsx │ │ ├── loading-spinner.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── separator.tsx │ │ ├── sonner.tsx │ │ ├── checkbox.tsx │ │ ├── switch.tsx │ │ ├── tooltip.tsx │ │ ├── progress.tsx │ │ ├── hover-card.tsx │ │ ├── popover.tsx │ │ ├── avatar.tsx │ │ ├── alert.tsx │ │ ├── scroll-area.tsx │ │ └── column-toggle.tsx │ └── breadcrumbs-setter.tsx ├── socket-io.server.ts ├── __tests__ │ └── shared │ │ └── utils │ │ ├── stream.utils.test.ts │ │ └── date.utils.test.ts └── websocket.server.ts ├── postcss.config.mjs ├── .dockerignore ├── next.config.mjs ├── prisma.config.ts ├── components.json ├── tsconfig.server.json ├── .vscode ├── settings.json └── launch.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── tests.yml ├── tsconfig.json ├── .gitignore ├── additional-containers ├── mariadb-backup │ ├── Dockerfile.amd64 │ ├── Dockerfile.arm64 │ └── docker-compose.yml ├── mongodb-backup │ ├── Dockerfile.amd64 │ ├── Dockerfile.arm64 │ └── docker-compose.yml └── postgres-backup │ ├── Dockerfile.amd64 │ ├── Dockerfile.arm64 │ └── docker-compose.yml ├── setup └── reset-password.sh ├── fix-wrong-zod-imports.js └── Dockerfile /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/quickstack-icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biersoeckli/QuickStack/HEAD/public/quickstack-icon-dark.png -------------------------------------------------------------------------------- /prisma/migrations/20241229143025_migration/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "App" ADD COLUMN "webhookId" TEXT; 3 | -------------------------------------------------------------------------------- /public/quick-stack-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biersoeckli/QuickStack/HEAD/public/quick-stack-logo-light.png -------------------------------------------------------------------------------- /public/quickstack-repo-heading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biersoeckli/QuickStack/HEAD/public/quickstack-repo-heading.png -------------------------------------------------------------------------------- /public/template-icons/wordpress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biersoeckli/QuickStack/HEAD/public/template-icons/wordpress.png -------------------------------------------------------------------------------- /github-assets/app-settings-general.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biersoeckli/QuickStack/HEAD/github-assets/app-settings-general.png -------------------------------------------------------------------------------- /.devcontainer/devcontainer.env_example: -------------------------------------------------------------------------------- 1 | NEXTAUTH_SECRET=SOME_TOKEN_FOR_NEXTJS_AUTHENTICATION 2 | DATABASE_URL="file:/workspace/storage/db/data.db" -------------------------------------------------------------------------------- /src/shared/model/downloadable-app-logs.model.ts: -------------------------------------------------------------------------------- 1 | export interface DownloadableAppLogsModel { 2 | appId: string; 3 | date: Date; 4 | } 5 | -------------------------------------------------------------------------------- /prisma/migrations/20250324085100_delete_roleprojectpermission/migration.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM RoleProjectPermission; 2 | DELETE FROM RoleAppPermission; 3 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | export { default } from "next-auth/middleware" 2 | 3 | export const config = { 4 | matcher: ["/"], 5 | exclude: ["/auth"], 6 | 7 | } -------------------------------------------------------------------------------- /src/app/error/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | export default function ErrorPage() { 3 | 4 | return ( 5 |
6 | Error Page 7 |
8 | ) 9 | } -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "sqlite" 4 | -------------------------------------------------------------------------------- /src/shared/model/project-extended.model.ts: -------------------------------------------------------------------------------- 1 | import { App, Project } from "@prisma/client"; 2 | 3 | export type ProjectExtendedModel = Project & { 4 | apps: App[]; 5 | } -------------------------------------------------------------------------------- /src/app/unauthorized/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | export default function UnauthorizedPage() { 3 | 4 | return ( 5 |
6 | Unauthorized 7 |
8 | ) 9 | } -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | README.md 6 | .next 7 | !.next/static 8 | !.next/standalone 9 | .git 10 | db 11 | internal 12 | shared 13 | dist -------------------------------------------------------------------------------- /src/shared/model/totp.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const totpZodModel = z.object({ 4 | totp: z.string().trim(), 5 | }) 6 | 7 | export type TotpModel = z.infer; -------------------------------------------------------------------------------- /prisma/migrations/20241229131352_migration/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "App" ADD COLUMN "containerRegistryPassword" TEXT; 3 | ALTER TABLE "App" ADD COLUMN "containerRegistryUsername" TEXT; 4 | -------------------------------------------------------------------------------- /src/frontend/sockets/sockets.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Manager } from "socket.io-client"; 4 | 5 | const manager = new Manager(); 6 | export const podTerminalSocket = manager.socket("/pod-terminal"); -------------------------------------------------------------------------------- /src/frontend/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/model/event-info.model.ts: -------------------------------------------------------------------------------- 1 | export interface EventInfoModel { 2 | podName: string, 3 | action: string, 4 | eventTime: Date, 5 | note: string, 6 | reason: string, 7 | type: string 8 | } 9 | -------------------------------------------------------------------------------- /src/shared/model/generated-zod/verificationtoken.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | 4 | export const VerificationTokenModel = z.object({ 5 | identifier: z.string(), 6 | token: z.string(), 7 | expires: z.date(), 8 | }) 9 | -------------------------------------------------------------------------------- /src/shared/model/env-edit.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const appEnvVariablesZodModel = z.object({ 4 | envVars: z.string(), 5 | }) 6 | 7 | export type AppEnvVariablesModel = z.infer; -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | output: 'standalone', 4 | /* experimental: { 5 | instrumentationHook: true 6 | }*/ 7 | }; 8 | 9 | export default nextConfig; 10 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { } from "next-auth" 2 | import { authOptions } from "@/server/utils/auth-options"; 3 | 4 | 5 | const handler = NextAuth(authOptions) 6 | 7 | export { handler as GET, handler as POST } -------------------------------------------------------------------------------- /src/shared/model/generated-zod/parameter.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | 4 | export const ParameterModel = z.object({ 5 | name: z.string(), 6 | value: z.string(), 7 | createdAt: z.date(), 8 | updatedAt: z.date(), 9 | }) 10 | -------------------------------------------------------------------------------- /src/shared/model/network-policy.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const appNetworkPolicy = z.enum(["ALLOW_ALL", "INTERNET_ONLY", "NAMESPACE_ONLY", "DENY_ALL"]); 4 | export type AppNetworkPolicyType = z.infer; -------------------------------------------------------------------------------- /src/shared/model/traefik-ip-propagation.model.ts: -------------------------------------------------------------------------------- 1 | export type TraefikIpPropagationStatus = { 2 | externalTrafficPolicy?: 'Local' | 'Cluster'; 3 | readyReplicas: number; 4 | replicas: number; 5 | restartedAt?: string | null; 6 | }; 7 | -------------------------------------------------------------------------------- /src/shared/model/volume-upload.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const volumeUploadZodModel = z.object({ 4 | file: z.any(), 5 | volumeId: z.string(), 6 | }) 7 | 8 | export type VolumeUploadModel = z.infer; -------------------------------------------------------------------------------- /src/shared/model/qs-public-ipv4-settings.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const qsPublicIpv4SettingsZodModel = z.object({ 4 | publicIpv4: z.string().trim(), 5 | }) 6 | 7 | export type QsPublicIpv4SettingsModel = z.infer; -------------------------------------------------------------------------------- /src/shared/utils/date.utils.ts: -------------------------------------------------------------------------------- 1 | export class DateUtils { 2 | static isSameDay(date1: Date, date2: Date): boolean { 3 | return date1.getDate() === date2.getDate() && date1.getMonth() === date2.getMonth() && date1.getFullYear() === date2.getFullYear(); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/server/services/standalone-services/00_info.md: -------------------------------------------------------------------------------- 1 | # What are Standalone Services 2 | 3 | Standalone services are service classes wich can be used within a Next.JS request context or in a standalone context (for example at application startup, without any Next.JS specific features). 4 | -------------------------------------------------------------------------------- /prisma/migrations/20241121151959_migration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Parameter" ( 3 | "name" TEXT NOT NULL PRIMARY KEY, 4 | "value" TEXT NOT NULL, 5 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | "updatedAt" DATETIME NOT NULL 7 | ); 8 | -------------------------------------------------------------------------------- /src/shared/model/file-mount-edit.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const fileMountEditZodModel = z.object({ 4 | containerMountPath: z.string().trim().min(1), 5 | content: z.string().min(1), 6 | }) 7 | 8 | export type FileMountEditModel = z.infer; -------------------------------------------------------------------------------- /src/shared/model/app-monitoring-usage.model.ts: -------------------------------------------------------------------------------- 1 | export interface AppMonitoringUsageModel { 2 | projectId: string, 3 | projectName: string, 4 | appName: string, 5 | appId: string, 6 | cpuUsage: number, 7 | cpuUsagePercent: number, 8 | ramUsageBytes: number 9 | } 10 | -------------------------------------------------------------------------------- /src/components/custom/navigate-back.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ArrowLeft } from "lucide-react"; 4 | import { Button } from "../ui/button"; 5 | 6 | export default function NavigateBackButton() { 7 | return ; 8 | } -------------------------------------------------------------------------------- /src/shared/model/default-port.model.ts: -------------------------------------------------------------------------------- 1 | import { stringToNumber, stringToOptionalNumber } from "@/shared/utils/zod.utils"; 2 | import { z } from "zod"; 3 | 4 | export const appPortZodModel = z.object({ 5 | port: stringToNumber, 6 | }); 7 | 8 | export type AppPortModel = z.infer; -------------------------------------------------------------------------------- /src/shared/model/app-volume-monitoring-usage.model.ts: -------------------------------------------------------------------------------- 1 | export interface AppVolumeMonitoringUsageModel { 2 | projectId: string, 3 | projectName: string, 4 | appName: string, 5 | appId: string, 6 | mountPath: string, 7 | usedBytes: number, 8 | capacityBytes: number 9 | } 10 | -------------------------------------------------------------------------------- /src/shared/model/system-backup-location-settings.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const systemBackupLocationSettingsZodModel = z.object({ 4 | systemBackupLocation: z.string(), 5 | }) 6 | 7 | export type SystemBackupLocationSettingsModel = z.infer; 8 | -------------------------------------------------------------------------------- /prisma.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { defineConfig, env } from 'prisma/config' 3 | 4 | export default defineConfig({ 5 | schema: 'prisma/schema.prisma', 6 | migrations: { 7 | path: 'prisma/migrations', 8 | }, 9 | datasource: { 10 | url: env('DATABASE_URL'), 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /prisma/migrations/20241025085330_migration/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[hostname]` on the table `AppDomain` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "AppDomain_hostname_key" ON "AppDomain"("hostname"); 9 | -------------------------------------------------------------------------------- /src/shared/model/service.exception.model.ts: -------------------------------------------------------------------------------- 1 | export class ServiceException extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | this.name = ServiceException.name; 5 | // Optionally, you can capture the stack trace here if needed 6 | Error.captureStackTrace(this, this.constructor); 7 | } 8 | } -------------------------------------------------------------------------------- /src/components/ui/full-loading-spinnter.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/frontend/utils/utils" 2 | import LoadingSpinner from "./loading-spinner"; 3 | 4 | export default function FullLoadingSpinner() { 5 | return
6 | 7 |
; 8 | } -------------------------------------------------------------------------------- /src/shared/model/pods-info.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const podsInfoZodModel = z.object({ 4 | podName: z.string(), 5 | containerName: z.string(), 6 | uid: z.string().optional(), 7 | status: z.string().optional(), 8 | }); 9 | 10 | export type PodsInfoModel = z.infer; 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/shared/model/qs-letsencrypt-settings.model.ts: -------------------------------------------------------------------------------- 1 | import { stringToBoolean } from "@/shared/utils/zod.utils"; 2 | import { z } from "zod"; 3 | 4 | export const qsLetsEncryptSettingsZodModel = z.object({ 5 | letsEncryptMail: z.string().trim().email(), 6 | }) 7 | 8 | export type QsLetsEncryptSettingsModel = z.infer; -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import ProjectPage from "./projects/project-page"; 2 | import paramService, { ParamService } from "@/server/services/param.service"; 3 | import HostnameCheck from "./settings/server/hostname-check"; 4 | 5 | export default async function Home() { 6 | return <> 7 | 8 | 9 | ; 10 | } 11 | -------------------------------------------------------------------------------- /src/shared/model/qs-settings.model.ts: -------------------------------------------------------------------------------- 1 | import { stringToBoolean } from "@/shared/utils/zod.utils"; 2 | import { z } from "zod"; 3 | 4 | export const qsIngressSettingsZodModel = z.object({ 5 | serverUrl: z.string().trim().min(1), 6 | disableNodePortAccess: stringToBoolean, 7 | }) 8 | 9 | export type QsIngressSettingsModel = z.infer; -------------------------------------------------------------------------------- /src/shared/model/volume-backup-extended.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { S3TargetModel, VolumeBackupModel } from "./generated-zod"; 3 | 4 | export const volumeBackupExtendedZodModel = z.lazy(() => VolumeBackupModel.extend({ 5 | target: S3TargetModel 6 | })) 7 | 8 | export type VolumeBackupExtendedModel = z.infer; 9 | -------------------------------------------------------------------------------- /src/shared/model/update-password.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const profilePasswordChangeZodModel = z.object({ 4 | oldPassword: z.string().trim().min(1), 5 | newPassword: z.string().trim().min(6), 6 | confirmNewPassword: z.string().trim().min(6) 7 | }) 8 | 9 | export type ProfilePasswordChangeModel = z.infer; -------------------------------------------------------------------------------- /src/shared/model/user-extended.model.ts: -------------------------------------------------------------------------------- 1 | import { User, UserGroup } from "@prisma/client"; 2 | import { UserGroupExtended } from "./sim-session.model"; 3 | 4 | export type UserExtended = { 5 | id: string; 6 | userGroup: UserGroup | null; 7 | userGroupId: string | null; 8 | email: string; 9 | createdAt: Date; 10 | updatedAt: Date; 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { cn } from "@/frontend/utils/utils" 3 | 4 | function Skeleton({ 5 | className, 6 | ...props 7 | }: React.HTMLAttributes) { 8 | return ( 9 |
13 | ) 14 | } 15 | 16 | export { Skeleton } 17 | -------------------------------------------------------------------------------- /src/shared/model/pods-resource-info.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const podsResourceInfoZodModel = z.object({ 4 | cpuPercent: z.number(), 5 | cpuAbsolutCores: z.number(), 6 | ramPercent: z.number(), 7 | ramAbsolutBytes: z.number(), 8 | }); 9 | 10 | export type PodsResourceInfoModel = z.infer; 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/shared/model/registry-storage-location-settings.model.ts: -------------------------------------------------------------------------------- 1 | import { stringToBoolean } from "@/shared/utils/zod.utils"; 2 | import { z } from "zod"; 3 | 4 | export const registryStorageLocationSettingsZodModel = z.object({ 5 | registryStorageLocation: z.string(), 6 | }) 7 | 8 | export type RegistryStorageLocationSettingsModel = z.infer; -------------------------------------------------------------------------------- /prisma/migrations/20241202170526_migration/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[appId,containerMountPath]` on the table `AppVolume` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "AppVolume_appId_containerMountPath_key" ON "AppVolume"("appId", "containerMountPath"); 9 | -------------------------------------------------------------------------------- /src/components/custom/short-commit-hash.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Code } from './code'; 3 | 4 | export default function ShortCommitHash({ children }: { children?: string }) { 5 | const shortHash = children ? children.slice(0, 7) : ''; 6 | if (!shortHash) { 7 | return <>; 8 | } 9 | return ({shortHash}); 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | const Collapsible = CollapsiblePrimitive.Root 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 12 | -------------------------------------------------------------------------------- /src/shared/model/basic-auth-edit.model.ts: -------------------------------------------------------------------------------- 1 | import { stringToNumber } from "@/shared/utils/zod.utils"; 2 | import { z } from "zod"; 3 | 4 | export const basicAuthEditZodModel = z.object({ 5 | id: z.string().nullish(), 6 | username: z.string().trim().min(1), 7 | password: z.string().trim().min(1), 8 | appId: z.string().min(1), 9 | }); 10 | 11 | export type BasicAuthEditModel = z.infer; -------------------------------------------------------------------------------- /src/shared/model/domain-edit.model.ts: -------------------------------------------------------------------------------- 1 | import { stringToBoolean, stringToNumber } from "@/shared/utils/zod.utils"; 2 | import { z } from "zod"; 3 | 4 | export const appDomainEditZodModel = z.object({ 5 | hostname: z.string().trim().min(1), 6 | useSsl: stringToBoolean, 7 | redirectHttps: stringToBoolean, 8 | port: stringToNumber, 9 | }) 10 | 11 | export type AppDomainEditModel = z.infer; -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /src/shared/model/user-edit.model.ts: -------------------------------------------------------------------------------- 1 | import { stringToNumber } from "@/shared/utils/zod.utils"; 2 | import { z } from "zod"; 3 | 4 | export const userEditZodModel = z.object({ 5 | id: z.string().trim().optional(), 6 | email: z.string().trim().min(1), 7 | newPassword: z.string().optional(), 8 | userGroupId: z.string().trim().nullable(), 9 | }) 10 | 11 | export type UserEditModel = z.infer; 12 | -------------------------------------------------------------------------------- /.devcontainer/docker.compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | vscode-container: 4 | image: mcr.microsoft.com/devcontainers/typescript-node 5 | #image: mcr.microsoft.com/devcontainers/base:debian 6 | #image: mcr.microsoft.com/devcontainers/base:alpine 7 | command: /bin/sh -c "while sleep 1000; do :; done" 8 | volumes: 9 | - ..:/workspace 10 | - ~/.ssh:/home/node/.ssh 11 | env_file: devcontainer.env -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "outDir": "dist", 7 | "lib": [ 8 | "ES2023" 9 | ], 10 | "target": "ES2023", 11 | "isolatedModules": false, 12 | "noEmit": false, 13 | }, 14 | "include": [ 15 | "src/server.ts", 16 | ] 17 | } -------------------------------------------------------------------------------- /src/components/ui/spinner.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/frontend/utils/utils" 2 | import { Loader2Icon } from "lucide-react" 3 | 4 | 5 | function Spinner({ className, ...props }: React.ComponentProps<"svg">) { 6 | return ( 7 | 13 | ) 14 | } 15 | 16 | export { Spinner } 17 | -------------------------------------------------------------------------------- /src/shared/model/role-extended.model.ts.ts: -------------------------------------------------------------------------------- 1 | import { RoleAppPermission, User, UserGroup } from "@prisma/client"; 2 | 3 | export type RoleExtended = UserGroup & { 4 | roleAppPermissions: (RoleAppPermission & { 5 | app: { 6 | name: string; 7 | }; 8 | })[]; 9 | } 10 | 11 | export enum RolePermissionEnum { 12 | READ = 'READ', 13 | READWRITE = 'READWRITE' 14 | } 15 | 16 | 17 | export const adminRoleName = "admin"; -------------------------------------------------------------------------------- /src/shared/utils/stream.utils.ts: -------------------------------------------------------------------------------- 1 | import { TerminalSetupInfoModel } from "../model/terminal-setup-info.model"; 2 | 3 | export class StreamUtils { 4 | 5 | static getInputStreamName(terminalInfo: TerminalSetupInfoModel) { 6 | return `${terminalInfo.terminalSessionKey}_input`; 7 | } 8 | 9 | static getOutputStreamName(terminalInfo: TerminalSetupInfoModel) { 10 | return `${terminalInfo.terminalSessionKey}_output`; 11 | } 12 | } -------------------------------------------------------------------------------- /prisma/migrations/20241231161704_migration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "S3Target" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "name" TEXT NOT NULL, 5 | "bucketName" TEXT NOT NULL, 6 | "endpoint" TEXT NOT NULL, 7 | "region" TEXT NOT NULL, 8 | "accessKeyId" TEXT NOT NULL, 9 | "secretKey" TEXT NOT NULL, 10 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | "updatedAt" DATETIME NOT NULL 12 | ); 13 | -------------------------------------------------------------------------------- /src/server/utils/env-var.utils.ts: -------------------------------------------------------------------------------- 1 | import { AppExtendedModel } from "@/shared/model/app-extended.model"; 2 | 3 | export class EnvVarUtils { 4 | static parseEnvVariables(app: AppExtendedModel) { 5 | return app.envVars ? app.envVars.split('\n').filter(x => !!x).map(env => { 6 | const [name] = env.split('='); 7 | const value = env.replace(`${name}=`, ''); 8 | return { name, value }; 9 | }) : []; 10 | } 11 | } -------------------------------------------------------------------------------- /src/shared/model/terminal-setup-info.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const terminalSetupInfoZodModel = z.object({ 4 | namespace: z.string().min(1), 5 | podName: z.string().min(1), 6 | containerName: z.string().min(1), 7 | terminalType: z.enum(['sh', 'bash']).default('bash').nullish(), 8 | terminalSessionKey: z.string().nullish(), 9 | }); 10 | 11 | export type TerminalSetupInfoModel = z.infer; -------------------------------------------------------------------------------- /prisma/migrations/20250107081600_migration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "AppBasicAuth" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "username" TEXT NOT NULL, 5 | "password" TEXT NOT NULL, 6 | "appId" TEXT NOT NULL, 7 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updatedAt" DATETIME NOT NULL, 9 | CONSTRAINT "AppBasicAuth_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE CASCADE ON UPDATE CASCADE 10 | ); 11 | -------------------------------------------------------------------------------- /src/shared/model/backup-info.model.ts: -------------------------------------------------------------------------------- 1 | export interface BackupInfoModel { 2 | projectId: string; 3 | projectName: string; 4 | appName: string; 5 | appId: string; 6 | backupVolumeId: string; 7 | s3TargetId: string; 8 | volumeId: string; 9 | mountPath: string; 10 | backupRetention: number; 11 | backups: BackupEntry[] 12 | } 13 | 14 | export interface BackupEntry { 15 | key: string; 16 | backupDate: Date; 17 | sizeBytes?: number; 18 | } -------------------------------------------------------------------------------- /src/shared/model/app-rate-limits.model.ts: -------------------------------------------------------------------------------- 1 | import { stringToNumber, stringToOptionalNumber } from "@/shared/utils/zod.utils"; 2 | import { z } from "zod"; 3 | 4 | export const appRateLimitsZodModel = z.object({ 5 | memoryReservation: stringToOptionalNumber, 6 | memoryLimit: stringToOptionalNumber, 7 | cpuReservation: stringToOptionalNumber, 8 | cpuLimit: stringToOptionalNumber, 9 | replicas: stringToNumber, 10 | }) 11 | 12 | export type AppRateLimitsModel = z.infer; -------------------------------------------------------------------------------- /src/components/custom/page-title.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export default function PageTitle({ title, subtitle, children }: { 4 | title: string; 5 | subtitle?: string; 6 | children?: React.ReactNode; 7 | }) { 8 | return
9 |
10 |

{title}

11 |

{subtitle}

12 |
13 | {children} 14 |
15 | } -------------------------------------------------------------------------------- /src/components/custom/submit-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useFormStatus } from "react-dom"; 4 | import LoadingSpinner from "../ui/loading-spinner"; 5 | import { Button } from "../ui/button"; 6 | 7 | export function SubmitButton(props: { children: React.ReactNode, className?: string }) { 8 | const { pending, data, method, action } = useFormStatus(); 9 | return 10 | } -------------------------------------------------------------------------------- /src/server/adapter/ip-adress-finder.adapter.ts: -------------------------------------------------------------------------------- 1 | class IpAddressFinder { 2 | 3 | public async getPublicIpOfServer(): Promise { 4 | // source: https://www.ipify.org 5 | const response = await fetch('https://api.ipify.org?format=json') // ipv6 is on other domain https://api6.ipify.org?format=json 6 | const data = await response.json() 7 | return data?.ip || undefined; 8 | } 9 | } 10 | 11 | const ipAddressFinderAdapter = new IpAddressFinder(); 12 | export default ipAddressFinderAdapter; -------------------------------------------------------------------------------- /prisma/migrations/20241202160004_migration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "AppPorts" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "appId" TEXT NOT NULL, 5 | "port" INTEGER NOT NULL, 6 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "updatedAt" DATETIME NOT NULL, 8 | CONSTRAINT "AppPorts_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE CASCADE ON UPDATE CASCADE 9 | ); 10 | 11 | -- CreateIndex 12 | CREATE UNIQUE INDEX "AppPorts_appId_port_key" ON "AppPorts"("appId", "port"); 13 | -------------------------------------------------------------------------------- /src/components/custom/bottom-bar-menu.tsx: -------------------------------------------------------------------------------- 1 | export default function BottomBarMenu({ children }: { children: React.ReactNode }) { 2 | return (<> 3 |
4 |
5 |
6 | {children} 7 |
8 |
9 |
10 |
11 | 12 | ) 13 | } -------------------------------------------------------------------------------- /src/app/projects/projects-breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; 4 | import { Button } from "@/components/ui/button"; 5 | import { useBreadcrumbs } from "@/frontend/states/zustand.states"; 6 | import { useEffect } from "react"; 7 | 8 | export default function ProjectsBreadcrumbs() { 9 | const { setBreadcrumbs } = useBreadcrumbs(); 10 | useEffect(() => setBreadcrumbs([ 11 | { name: "Projects", url: "/" } 12 | ]), []); 13 | return <>; 14 | } -------------------------------------------------------------------------------- /src/components/ui/loading-spinner.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/frontend/utils/utils" 2 | 3 | export default function LoadingSpinner() { 4 | return 16 | 17 | 18 | } -------------------------------------------------------------------------------- /src/shared/model/s3-target-edit.model.ts: -------------------------------------------------------------------------------- 1 | import { stringToNumber } from "@/shared/utils/zod.utils"; 2 | import { z } from "zod"; 3 | 4 | export const s3TargetEditZodModel = z.object({ 5 | id: z.string().optional(), 6 | name: z.string().trim().min(1), 7 | endpoint: z.string().trim().min(1), 8 | bucketName: z.string().trim().min(1), 9 | region: z.string().trim().min(1), 10 | accessKeyId: z.string().trim().min(1), 11 | secretKey: z.string().trim().min(1), 12 | }) 13 | 14 | export type S3TargetEditModel = z.infer; 15 | -------------------------------------------------------------------------------- /src/components/custom/text-link.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from "lucide-react"; 2 | import Link from "next/link"; 3 | 4 | export default function TextLink({ href, children }: { href: string; children: React.ReactNode }) { 5 | return ( 6 | 7 |
8 |

{children}

9 |
10 |
11 | 12 | ) 13 | } -------------------------------------------------------------------------------- /src/shared/utils/fancy-console.utils.ts: -------------------------------------------------------------------------------- 1 | export class FancyConsoleUtils { 2 | public static printQuickStack() { 3 | console.log(''); 4 | console.log(' ___ _ _ ____ _ _ '); 5 | console.log(' / _ \\ _ _(_) ___| | __/ ___|| |_ __ _ ___| | __'); 6 | console.log('| | | | | | | |/ __| |/ /\\___ \\| __/ _` |/ __| |/ /'); 7 | console.log('| |_| | |_| | | (__| < ___) | || (_| | (__| < '); 8 | console.log(' \\__\\_\\\\__,_|_|\\___|_|\\_\\|____/ \\__\\__,_|\\___|_|\\_\\'); 9 | console.log(''); 10 | } 11 | } -------------------------------------------------------------------------------- /src/socket-io.server.ts: -------------------------------------------------------------------------------- 1 | import type http from "node:http"; 2 | import { Server } from "socket.io"; 3 | import terminalService from "./server/services/terminal.service"; 4 | 5 | class SocketIoServer { 6 | initialize(server: http.Server) { 7 | const io = new Server(server); 8 | const podLogsNamespace = io.of("/pod-terminal"); 9 | podLogsNamespace.on("connection", (socket) => { 10 | terminalService.streamTerminal(socket); 11 | }); 12 | }; 13 | } 14 | const socketIoServer = new SocketIoServer(); 15 | export default socketIoServer; 16 | 17 | -------------------------------------------------------------------------------- /src/shared/model/node-resource.model.ts: -------------------------------------------------------------------------------- 1 | import { stringToNumber, stringToOptionalNumber } from "@/shared/utils/zod.utils"; 2 | import { pid } from "process"; 3 | import { z } from "zod"; 4 | 5 | export const nodeResourceZodModel = z.object({ 6 | name: z.string(), 7 | cpuUsage: z.number(), 8 | cpuCapacity: z.number(), 9 | ramUsage: z.number(), 10 | ramCapacity: z.number(), 11 | diskUsageAbsolut: z.number(), 12 | diskUsageCapacity: z.number(), 13 | diskUsageReserved: z.number(), 14 | diskSpaceSchedulable: z.number(), 15 | }) 16 | 17 | export type NodeResourceModel = z.infer; -------------------------------------------------------------------------------- /src/components/breadcrumbs-setter.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; 4 | import { Button } from "@/components/ui/button"; 5 | import { useEffect } from "react"; 6 | import { Breadcrumb, useBreadcrumbs } from "@/frontend/states/zustand.states"; 7 | 8 | export default function BreadcrumbSetter({ items }: { items: Breadcrumb[] }) { 9 | const { setBreadcrumbs } = useBreadcrumbs(); 10 | useEffect(() => { 11 | setBreadcrumbs(items) 12 | return () => setBreadcrumbs([]); 13 | }, [items]); 14 | return <>; 15 | } -------------------------------------------------------------------------------- /src/shared/model/build-job.ts: -------------------------------------------------------------------------------- 1 | import { GitCommit } from "lucide-react"; 2 | import { z } from "zod"; 3 | 4 | export const buildJobStatusEnumZod = z.union([z.literal('UNKNOWN'), z.literal('RUNNING'), z.literal('FAILED'), z.literal('SUCCEEDED')]); 5 | 6 | export const buildJobSchemaZod = z.object({ 7 | name: z.string(), 8 | startTime: z.date(), 9 | status: buildJobStatusEnumZod, 10 | gitCommit: z.string(), 11 | deploymentId: z.string(), 12 | }); 13 | 14 | export type BuildJobModel = z.infer; 15 | export type BuildJobStatus = z.infer; 16 | 17 | 18 | -------------------------------------------------------------------------------- /prisma/migrations/20241223140802_migration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "AppFileMount" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "containerMountPath" TEXT NOT NULL, 5 | "content" TEXT NOT NULL, 6 | "appId" TEXT NOT NULL, 7 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updatedAt" DATETIME NOT NULL, 9 | CONSTRAINT "AppFileMount_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE CASCADE ON UPDATE CASCADE 10 | ); 11 | 12 | -- CreateIndex 13 | CREATE UNIQUE INDEX "AppFileMount_appId_containerMountPath_key" ON "AppFileMount"("appId", "containerMountPath"); 14 | -------------------------------------------------------------------------------- /src/shared/model/generated-zod/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./account" 2 | export * from "./session" 3 | export * from "./user" 4 | export * from "./verificationtoken" 5 | export * from "./authenticator" 6 | export * from "./usergroup" 7 | export * from "./roleprojectpermission" 8 | export * from "./roleapppermission" 9 | export * from "./project" 10 | export * from "./app" 11 | export * from "./appport" 12 | export * from "./appdomain" 13 | export * from "./appvolume" 14 | export * from "./appfilemount" 15 | export * from "./parameter" 16 | export * from "./s3target" 17 | export * from "./volumebackup" 18 | export * from "./appbasicauth" 19 | -------------------------------------------------------------------------------- /src/shared/utils/react-node.utils.ts: -------------------------------------------------------------------------------- 1 | import { isValidElement } from "react"; 2 | 3 | export class ReactNodeUtils { 4 | static getTextFromReactElement(element: React.ReactNode): string { 5 | if (typeof element === "string" || typeof element === "number") { 6 | return element.toString(); 7 | } 8 | if (isValidElement(element)) { 9 | return this.getTextFromReactElement(element.props.children); 10 | } 11 | if (Array.isArray(element)) { 12 | return element.map(child => this.getTextFromReactElement(child)).join(""); 13 | } 14 | return ""; 15 | } 16 | } -------------------------------------------------------------------------------- /src/app/project/[projectId]/project-breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; 4 | import { Button } from "@/components/ui/button"; 5 | import { useBreadcrumbs } from "@/frontend/states/zustand.states"; 6 | import { useEffect } from "react"; 7 | 8 | export default function ProjectBreadcrumbs({ project }: { project: { name: string } }) { 9 | const { setBreadcrumbs } = useBreadcrumbs(); 10 | useEffect(() => setBreadcrumbs([ 11 | { name: "Projects", url: "/" }, 12 | { name: project.name } 13 | ]), []); 14 | return <>; 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // The path to the `bun` executable. 3 | "bun.runtime": "/home/node/.bun/bin/bun", 4 | // If support for Bun should be added to the default "JavaScript Debug Terminal". 5 | "bun.debugTerminal.enabled": true, 6 | // If the debugger should stop on the first line of the program. 7 | "bun.debugTerminal.stopOnEntry": false, 8 | "java.compile.nullAnalysis.mode": "automatic", 9 | "typescript.tsdk": "node_modules/typescript/lib", 10 | "files.exclude": { 11 | "**/.DS_Store": true, 12 | }, 13 | "search.exclude": { 14 | }, 15 | "files.trimTrailingWhitespace": true, 16 | "prettier.singleQuote": true, 17 | } -------------------------------------------------------------------------------- /prisma/migrations/20250102145143_migration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "VolumeBackup" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "volumeId" TEXT NOT NULL, 5 | "targetId" TEXT NOT NULL, 6 | "cron" TEXT NOT NULL, 7 | "retention" INTEGER NOT NULL, 8 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "updatedAt" DATETIME NOT NULL, 10 | CONSTRAINT "VolumeBackup_volumeId_fkey" FOREIGN KEY ("volumeId") REFERENCES "AppVolume" ("id") ON DELETE CASCADE ON UPDATE CASCADE, 11 | CONSTRAINT "VolumeBackup_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "S3Target" ("id") ON DELETE CASCADE ON UPDATE CASCADE 12 | ); 13 | -------------------------------------------------------------------------------- /src/shared/model/backup-volume-edit.model.ts: -------------------------------------------------------------------------------- 1 | import { stringToNumber } from "@/shared/utils/zod.utils"; 2 | import { z } from "zod"; 3 | 4 | export const volumeBackupEditZodModel = z.object({ 5 | id: z.string().nullish(), 6 | volumeId: z.string(), 7 | targetId: z.string(), 8 | cron: z.string().trim().regex(/(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})/), 9 | //cron: z.string().trim().min(1), 10 | retention: stringToNumber, 11 | useDatabaseBackup: z.boolean().optional(), 12 | }); 13 | 14 | export type VolumeBackupEditModel = z.infer; -------------------------------------------------------------------------------- /src/shared/model/volume-edit.model.ts: -------------------------------------------------------------------------------- 1 | import { stringToNumber } from "@/shared/utils/zod.utils"; 2 | import { z } from "zod"; 3 | 4 | export const appVolumeTypeZodModel = z.enum(["ReadWriteOnce", "ReadWriteMany"]); 5 | export const storageClassNameZodModel = z.enum(["longhorn", "local-path"]); 6 | 7 | export const appVolumeEditZodModel = z.object({ 8 | containerMountPath: z.string().trim().min(1), 9 | size: stringToNumber, 10 | accessMode: appVolumeTypeZodModel.nullish().or(z.string().nullish()), 11 | storageClassName: storageClassNameZodModel.default("longhorn"), 12 | }); 13 | 14 | export type AppVolumeEditModel = z.infer; 15 | -------------------------------------------------------------------------------- /src/frontend/hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /src/shared/model/form-validation-exception.model.ts: -------------------------------------------------------------------------------- 1 | import { FormZodErrorValidationCallback } from "@/frontend/utils/form.utilts"; 2 | import { ServiceException } from "./service.exception.model"; 3 | import { z, ZodType } from "zod"; 4 | 5 | export class FormValidationException> extends ServiceException { 6 | constructor(message: string, public readonly errors: FormZodErrorValidationCallback> | null) { 7 | super(message); 8 | this.name = FormValidationException.name; 9 | // Optionally, you can capture the stack trace here if needed 10 | Error.captureStackTrace(this, this.constructor); 11 | } 12 | } -------------------------------------------------------------------------------- /src/shared/model/auth-form.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const authFormInputSchemaZod = z.object({ 4 | email: z.string().trim().email(), 5 | password: z.string().trim().min(1) 6 | }); 7 | export type AuthFormInputSchema = z.infer; 8 | 9 | export const registgerFormInputSchemaZod = authFormInputSchemaZod.merge(z.object({ 10 | qsHostname: z.string().trim().optional(), 11 | })); 12 | export type RegisterFormInputSchema = z.infer; 13 | 14 | export const twoFaInputSchemaZod = z.object({ 15 | twoFactorCode: z.string().length(6) 16 | }); 17 | export type TwoFaInputSchema = z.infer; 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/components/custom/pods-status-polling-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect } from 'react'; 4 | import { podsStatusPollingService } from '@/frontend/services/pods-status-polling.service'; 5 | 6 | /** 7 | * Client component that initializes and manages the pods status polling service. 8 | * This component should be mounted in the root layout to ensure polling is active 9 | * across all pages of the application. 10 | */ 11 | export default function PodsStatusPollingProvider() { 12 | useEffect(() => { 13 | podsStatusPollingService.start(); 14 | 15 | return () => { 16 | podsStatusPollingService.stop(); 17 | }; 18 | }, []); 19 | 20 | return null; 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules", "fix-wrong-zod-imports.js"] 26 | } 27 | -------------------------------------------------------------------------------- /src/server/adapter/aws-s3.adapter.ts: -------------------------------------------------------------------------------- 1 | import { DeleteObjectCommand, HeadBucketCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; 2 | 3 | import { S3Target } from "@prisma/client"; 4 | import { createReadStream } from "fs"; 5 | 6 | class AwsS3Adapter { 7 | 8 | getS3Client(s3Target: S3Target) { 9 | return new S3Client({ 10 | region: s3Target.region, 11 | credentials: { 12 | accessKeyId: s3Target.accessKeyId, 13 | secretAccessKey: s3Target.secretKey, 14 | }, 15 | endpoint: `https://${s3Target.endpoint}` 16 | }); 17 | } 18 | } 19 | 20 | const s3Adapter = new AwsS3Adapter(); 21 | export default s3Adapter; -------------------------------------------------------------------------------- /src/shared/model/database-template-info.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { AppDomainModel, AppModel, AppPortModel, AppVolumeModel, RelatedAppDomainModel, RelatedAppPortModel, RelatedAppVolumeModel } from "./generated-zod"; 3 | import { appSourceTypeZodModel, appTypeZodModel } from "./app-source-info.model"; 4 | import { appVolumeTypeZodModel } from "./volume-edit.model"; 5 | 6 | export const databaseTemplateInfoZodModel = z.object({ 7 | username: z.string(), 8 | password: z.string(), 9 | port: z.number(), 10 | hostname: z.string(), 11 | databaseName: z.string(), 12 | internalConnectionUrl: z.string(), 13 | }); 14 | 15 | export type DatabaseTemplateInfoModel = z.infer; 16 | -------------------------------------------------------------------------------- /public/template-icons/mongodb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | .devcontainer/devcontainer.env 38 | 39 | db 40 | kube-config.config 41 | kube-config.config_clusteradmin 42 | kube-config.config_old 43 | kube-config.config_restricted 44 | internal 45 | dist 46 | storage/ -------------------------------------------------------------------------------- /src/app/auth/page.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import userService from "@/server/services/user.service"; 4 | import UserRegistrationForm from "./register-from"; 5 | import UserLoginForm from "./login-form"; 6 | import { getUserSession } from "@/server/utils/action-wrapper.utils"; 7 | import { redirect } from "next/navigation"; 8 | 9 | export default async function AuthPage() { 10 | const session = await getUserSession(); 11 | if (session) { 12 | redirect('/'); 13 | } 14 | const allUsers = await userService.getAllUsers(); 15 | return ( 16 |
17 | {allUsers.length === 0 ? : } 18 |
19 | ) 20 | } -------------------------------------------------------------------------------- /src/app/project/app/[appId]/app-breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; 4 | import { Button } from "@/components/ui/button"; 5 | import { useBreadcrumbs } from "@/frontend/states/zustand.states"; 6 | import { useEffect } from "react"; 7 | import { AppExtendedModel } from "@/shared/model/app-extended.model"; 8 | 9 | export default function AppBreadcrumbs({ app }: { app: AppExtendedModel }) { 10 | const { setBreadcrumbs } = useBreadcrumbs(); 11 | useEffect(() => setBreadcrumbs([ 12 | { name: "Projects", url: "/" }, 13 | { name: app.project.name, url: "/project/" + app.projectId }, 14 | { name: app.name }, 15 | ]), []); 16 | return <>; 17 | } -------------------------------------------------------------------------------- /src/shared/model/generated-zod/appport.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | import { CompleteApp, RelatedAppModel } from "./index" 4 | 5 | export const AppPortModel = z.object({ 6 | id: z.string(), 7 | appId: z.string(), 8 | port: z.number().int(), 9 | createdAt: z.date(), 10 | updatedAt: z.date(), 11 | }) 12 | 13 | export interface CompleteAppPort extends z.infer { 14 | app: CompleteApp 15 | } 16 | 17 | /** 18 | * RelatedAppPortModel contains all relations on your model in addition to the scalars 19 | * 20 | * NOTE: Lazy required in case of potential circular dependencies within schema 21 | */ 22 | export const RelatedAppPortModel: z.ZodSchema = z.lazy(() => AppPortModel.extend({ 23 | app: RelatedAppModel, 24 | })) 25 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | env: 10 | # dummy database url for build time --> prisma 11 | DATABASE_URL: file:./dev.db 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: '22.x' 26 | registry-url: 'https://registry.npmjs.org' 27 | 28 | - name: Install dependencies for backend 29 | run: yarn install && yarn run prisma-generate 30 | 31 | - name: Run tests for backend 32 | run: yarn test 33 | -------------------------------------------------------------------------------- /src/shared/model/app-extended.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { AppBasicAuthModel, AppDomainModel, AppFileMountModel, AppModel, AppPortModel, AppVolumeModel, ProjectModel, VolumeBackupModel } from "./generated-zod"; 3 | import { App, Project } from "@prisma/client"; 4 | 5 | export const AppExtendedZodModel= z.lazy(() => AppModel.extend({ 6 | project: ProjectModel, 7 | appDomains: AppDomainModel.array(), 8 | appPorts: AppPortModel.array(), 9 | appFileMounts: AppFileMountModel.array(), 10 | appVolumes: AppVolumeModel.array(), 11 | appBasicAuths: AppBasicAuthModel.array(), 12 | })) 13 | 14 | export type AppExtendedModel = z.infer; 15 | 16 | export type AppWithProjectModel = App & { 17 | project: Project; 18 | } -------------------------------------------------------------------------------- /src/shared/model/generated-zod/appports.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | import { CompleteApp, RelatedAppModel } from "./index" 4 | 5 | export const AppPortsModel = z.object({ 6 | id: z.string(), 7 | appId: z.string(), 8 | port: z.number().int(), 9 | createdAt: z.date(), 10 | updatedAt: z.date(), 11 | }) 12 | 13 | export interface CompleteAppPorts extends z.infer { 14 | app: CompleteApp 15 | } 16 | 17 | /** 18 | * RelatedAppPortsModel contains all relations on your model in addition to the scalars 19 | * 20 | * NOTE: Lazy required in case of potential circular dependencies within schema 21 | */ 22 | export const RelatedAppPortsModel: z.ZodSchema = z.lazy(() => AppPortsModel.extend({ 23 | app: RelatedAppModel, 24 | })) 25 | -------------------------------------------------------------------------------- /src/shared/model/generated-zod/session.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | import { CompleteUser, RelatedUserModel } from "./index" 4 | 5 | export const SessionModel = z.object({ 6 | sessionToken: z.string(), 7 | userId: z.string(), 8 | expires: z.date(), 9 | createdAt: z.date(), 10 | updatedAt: z.date(), 11 | }) 12 | 13 | export interface CompleteSession extends z.infer { 14 | user: CompleteUser 15 | } 16 | 17 | /** 18 | * RelatedSessionModel contains all relations on your model in addition to the scalars 19 | * 20 | * NOTE: Lazy required in case of potential circular dependencies within schema 21 | */ 22 | export const RelatedSessionModel: z.ZodSchema = z.lazy(() => SessionModel.extend({ 23 | user: RelatedUserModel, 24 | })) 25 | -------------------------------------------------------------------------------- /src/server/utils/command-executor.utils.ts: -------------------------------------------------------------------------------- 1 | import util from 'util'; 2 | import child_process from 'child_process'; 3 | 4 | export class CommandExecutorUtils { 5 | static async runCommand(command: string) { 6 | if (!command) { 7 | throw new Error('cannot run an empty command'); 8 | } 9 | try { 10 | //console.log('Running command: "' + command + '"...'); 11 | const exec = util.promisify(child_process.exec); 12 | 13 | const { stdout, stderr } = await exec(command); 14 | console.log('stdout:\n', stdout); 15 | console.log('stderr:\n', stderr); 16 | } catch (err) { 17 | console.error('Error while running command: +' + command + '":'); 18 | console.error(err); 19 | }; 20 | } 21 | } -------------------------------------------------------------------------------- /src/app/project/app/[appId]/environment/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { AppEnvVariablesModel, appEnvVariablesZodModel } from "@/shared/model/env-edit.model"; 4 | import appService from "@/server/services/app.service"; 5 | import { getAuthUserSession, isAuthorizedWriteForApp, saveFormAction } from "@/server/utils/action-wrapper.utils"; 6 | 7 | 8 | export const saveEnvVariables = async (prevState: any, inputData: AppEnvVariablesModel, appId: string) => 9 | saveFormAction(inputData, appEnvVariablesZodModel, async (validatedData) => { 10 | await isAuthorizedWriteForApp(appId); 11 | const existingApp = await appService.getById(appId); 12 | await appService.save({ 13 | ...existingApp, 14 | ...validatedData, 15 | id: appId, 16 | }); 17 | }); -------------------------------------------------------------------------------- /src/components/custom/code.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ReactElement } from "react"; 4 | import { toast } from "sonner"; 5 | 6 | export function Code({ children, copieable = true, copieableValue, className }: { children: string | null | undefined, copieable?: boolean, copieableValue?: string, className?: string }) { 7 | return (children && 8 | { 10 | if (!copieable) return; 11 | navigator.clipboard.writeText(copieableValue || children || ''); 12 | toast.success('Copied to clipboard'); 13 | }}> 14 | {children} 15 | 16 | ) 17 | } -------------------------------------------------------------------------------- /src/shared/model/deployment-info.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const deploymentStatusEnumZod = z.union([ 4 | z.literal('UNKNOWN'), 5 | z.literal('BUILDING'), 6 | z.literal('ERROR'), 7 | z.literal('DEPLOYED'), 8 | z.literal('DEPLOYING'), 9 | z.literal('SHUTDOWN'), 10 | z.literal('SHUTTING_DOWN'), 11 | ]); 12 | 13 | export const deploymentInfoZodModel = z.object({ 14 | replicasetName: z.string().optional(), 15 | buildJobName: z.string().optional(), 16 | createdAt: z.date(), 17 | status: deploymentStatusEnumZod, 18 | gitCommit: z.string().optional(), 19 | deploymentId: z.string(), 20 | }); 21 | 22 | export type DeploymentInfoModel = z.infer; 23 | export type DeplyomentStatus = z.infer; 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/shared/templates/all.templates.ts: -------------------------------------------------------------------------------- 1 | import { AppTemplateModel } from "../model/app-template.model"; 2 | import { wordpressAppTemplate } from "./apps/wordpress.template"; 3 | import { mariadbAppTemplate } from "./databases/mariadb.template"; 4 | import { mongodbAppTemplate } from "./databases/mongodb.template"; 5 | import { mysqlAppTemplate } from "./databases/mysql.template"; 6 | import { postgreAppTemplate } from "./databases/postgres.template"; 7 | 8 | 9 | export const databaseTemplates: AppTemplateModel[] = [ 10 | postgreAppTemplate, 11 | mongodbAppTemplate, 12 | mariadbAppTemplate, 13 | mysqlAppTemplate 14 | ]; 15 | 16 | export const appTemplates: AppTemplateModel[] = [ 17 | wordpressAppTemplate 18 | ]; 19 | 20 | 21 | export const allTemplates: AppTemplateModel[] = databaseTemplates.concat(appTemplates); -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /prisma/migrations/20250307150516_migration/migration.sql: -------------------------------------------------------------------------------- 1 | -- RedefineTables 2 | PRAGMA defer_foreign_keys=ON; 3 | PRAGMA foreign_keys=OFF; 4 | CREATE TABLE "new_Role" ( 5 | "id" TEXT NOT NULL PRIMARY KEY, 6 | "name" TEXT NOT NULL, 7 | "description" TEXT, 8 | "canCreateNewApps" BOOLEAN NOT NULL DEFAULT false, 9 | "canAccessBackups" BOOLEAN NOT NULL DEFAULT false, 10 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | "updatedAt" DATETIME NOT NULL 12 | ); 13 | INSERT INTO "new_Role" ("createdAt", "description", "id", "name", "updatedAt") SELECT "createdAt", "description", "id", "name", "updatedAt" FROM "Role"; 14 | DROP TABLE "Role"; 15 | ALTER TABLE "new_Role" RENAME TO "Role"; 16 | CREATE UNIQUE INDEX "Role_name_key" ON "Role"("name"); 17 | PRAGMA foreign_keys=ON; 18 | PRAGMA defer_foreign_keys=OFF; 19 | -------------------------------------------------------------------------------- /src/frontend/utils/nextjs-actions.utils.ts: -------------------------------------------------------------------------------- 1 | import { ServerActionResult } from "@/shared/model/server-action-error-return.model"; 2 | import { toast } from "sonner"; 3 | 4 | export class Actions { 5 | static async run(action: () => Promise>) { 6 | try { 7 | const retVal = await action(); 8 | if (!retVal || (retVal as ServerActionResult).status !== 'success') { 9 | toast.error(retVal?.message ?? 'An unknown error occurred.'); 10 | throw new Error(retVal?.message ?? 'An unknown error occurred.'); 11 | } 12 | return retVal.data!; 13 | } catch (error) { 14 | toast.error('An unknown error occurred.'); 15 | throw error; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/shared/model/generated-zod/appbasicauth.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | import { CompleteApp, RelatedAppModel } from "./index" 4 | 5 | export const AppBasicAuthModel = z.object({ 6 | id: z.string(), 7 | username: z.string(), 8 | password: z.string(), 9 | appId: z.string(), 10 | createdAt: z.date(), 11 | updatedAt: z.date(), 12 | }) 13 | 14 | export interface CompleteAppBasicAuth extends z.infer { 15 | app: CompleteApp 16 | } 17 | 18 | /** 19 | * RelatedAppBasicAuthModel contains all relations on your model in addition to the scalars 20 | * 21 | * NOTE: Lazy required in case of potential circular dependencies within schema 22 | */ 23 | export const RelatedAppBasicAuthModel: z.ZodSchema = z.lazy(() => AppBasicAuthModel.extend({ 24 | app: RelatedAppModel, 25 | })) 26 | -------------------------------------------------------------------------------- /src/shared/model/sim-session.model.ts: -------------------------------------------------------------------------------- 1 | import { RoleAppPermission } from "@prisma/client"; 2 | import { Session } from "next-auth"; 3 | import { RolePermissionEnum } from "./role-extended.model.ts"; 4 | 5 | export interface UserSession { 6 | email: string; 7 | userGroup?: UserGroupExtended; 8 | } 9 | 10 | export type UserGroupExtended = { 11 | name: string; 12 | id: string; 13 | canAccessBackups: boolean; 14 | roleProjectPermissions: { 15 | projectId: string; 16 | project: { 17 | apps: { 18 | id: string; 19 | name: string; 20 | }[]; 21 | }; 22 | createApps: boolean; 23 | deleteApps: boolean; 24 | writeApps: boolean; 25 | readApps: boolean; 26 | roleAppPermissions: RoleAppPermission[]; 27 | }[]; 28 | }; -------------------------------------------------------------------------------- /src/components/custom/loading-alert-dialog-action.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from "react"; 4 | import { AlertDialogAction } from "../ui/alert-dialog"; 5 | import LoadingSpinner from "../ui/loading-spinner"; 6 | 7 | export default function LoadingAlertDialogAction({ onClick, children }: { onClick: () => Promise, children: React.ReactNode }) { 8 | 9 | const [buttonIsLoading, setButtonIsLoading] = useState(false); 10 | return ( 11 | { 12 | setButtonIsLoading(true); 13 | try { 14 | await onClick(); 15 | } finally { 16 | setButtonIsLoading(false); 17 | } 18 | }} disabled={buttonIsLoading}>{buttonIsLoading ? : children} 19 | ) 20 | } -------------------------------------------------------------------------------- /src/shared/model/generated-zod/appfilemount.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | import { CompleteApp, RelatedAppModel } from "./index" 4 | 5 | export const AppFileMountModel = z.object({ 6 | id: z.string(), 7 | containerMountPath: z.string(), 8 | content: z.string(), 9 | appId: z.string(), 10 | createdAt: z.date(), 11 | updatedAt: z.date(), 12 | }) 13 | 14 | export interface CompleteAppFileMount extends z.infer { 15 | app: CompleteApp 16 | } 17 | 18 | /** 19 | * RelatedAppFileMountModel contains all relations on your model in addition to the scalars 20 | * 21 | * NOTE: Lazy required in case of potential circular dependencies within schema 22 | */ 23 | export const RelatedAppFileMountModel: z.ZodSchema = z.lazy(() => AppFileMountModel.extend({ 24 | app: RelatedAppModel, 25 | })) 26 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/frontend/utils/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/shared/model/generated-zod/appdomain.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | import { CompleteApp, RelatedAppModel } from "./index" 4 | 5 | export const AppDomainModel = z.object({ 6 | id: z.string(), 7 | hostname: z.string(), 8 | port: z.number().int(), 9 | useSsl: z.boolean(), 10 | redirectHttps: z.boolean(), 11 | appId: z.string(), 12 | createdAt: z.date(), 13 | updatedAt: z.date(), 14 | }) 15 | 16 | export interface CompleteAppDomain extends z.infer { 17 | app: CompleteApp 18 | } 19 | 20 | /** 21 | * RelatedAppDomainModel contains all relations on your model in addition to the scalars 22 | * 23 | * NOTE: Lazy required in case of potential circular dependencies within schema 24 | */ 25 | export const RelatedAppDomainModel: z.ZodSchema = z.lazy(() => AppDomainModel.extend({ 26 | app: RelatedAppModel, 27 | })) 28 | -------------------------------------------------------------------------------- /src/server/utils/cache-tag-generator.utils.ts: -------------------------------------------------------------------------------- 1 | export class Tags { 2 | 3 | static users() { 4 | return `users`; 5 | } 6 | 7 | static userGroups() { 8 | return `roles`; 9 | } 10 | 11 | static projects() { 12 | return `projects`; 13 | } 14 | 15 | static s3Targets() { 16 | return `targets`; 17 | } 18 | 19 | static volumeBackups() { 20 | return `volume-backups`; 21 | } 22 | 23 | static apps(projectId: string) { 24 | return `apps-${projectId}`; 25 | } 26 | 27 | static parameter() { 28 | return `parameter`; 29 | } 30 | 31 | static app(appId: string) { 32 | return `app-${appId}`; 33 | } 34 | 35 | static appBuilds(appId: string) { 36 | return `app-build-${appId}`; 37 | } 38 | 39 | static nodeInfos() { 40 | return `node-infos`; 41 | } 42 | } -------------------------------------------------------------------------------- /src/components/custom/hint-box-url.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"; 3 | import { Button } from "../ui/button"; 4 | import { QuestionMarkIcon } from "@radix-ui/react-icons"; 5 | 6 | 7 | 8 | 9 | export function HintBoxUrl({ url }: { url: string }) { 10 | 11 | const uri = new URL(url); 12 | 13 | 14 | return 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

Absprung zu {uri.hostname}

23 |
24 |
25 |
26 | } -------------------------------------------------------------------------------- /src/app/api/print-schedules-jobs/route.ts: -------------------------------------------------------------------------------- 1 | import k3s from "@/server/adapter/kubernetes-api.adapter"; 2 | import appService from "@/server/services/app.service"; 3 | import deploymentService from "@/server/services/deployment.service"; 4 | import scheduleService from "@/server/services/standalone-services/schedule.service"; 5 | import { getAuthUserSession, simpleRoute } from "@/server/utils/action-wrapper.utils"; 6 | import { Informer, V1Pod } from "@kubernetes/client-node"; 7 | import { NextResponse } from "next/server"; 8 | import { z } from "zod"; 9 | 10 | // Prevents this route's response from being cached 11 | export const dynamic = "force-dynamic"; 12 | 13 | 14 | export async function GET(request: Request) { 15 | return simpleRoute(async () => { 16 | scheduleService.printScheduledJobs(); 17 | return NextResponse.json({ 18 | status: "success" 19 | }); 20 | }) 21 | } -------------------------------------------------------------------------------- /src/app/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import projectService from "@/server/services/project.service" 2 | import { getUserSession } from "@/server/utils/action-wrapper.utils" 3 | import { SidebarCient } from "./sidebar-client" 4 | import { UserGroupUtils } from "@/shared/utils/role.utils"; 5 | 6 | export async function AppSidebar() { 7 | 8 | const session = await getUserSession(); 9 | 10 | if (!session) { 11 | return <> 12 | } 13 | 14 | const projects = await projectService.getAllProjects(); 15 | 16 | const relevantProjectsForUser = projects.filter((project) => 17 | UserGroupUtils.sessionHasReadAccessToProject(session, project.id)); 18 | for (const project of relevantProjectsForUser) { 19 | project.apps = project.apps.filter((app) => UserGroupUtils.sessionHasReadAccessForApp(session, app.id)); 20 | } 21 | 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/frontend/utils/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |