diff --git a/.github/sponsors/synexa.png b/.github/sponsors/synexa.png new file mode 100644 index 00000000..737ccd57 Binary files /dev/null and b/.github/sponsors/synexa.png differ diff --git a/.github/workflows/dokploy.yml b/.github/workflows/dokploy.yml index 9914811e..adcb1bb5 100644 --- a/.github/workflows/dokploy.yml +++ b/.github/workflows/dokploy.yml @@ -2,7 +2,7 @@ name: Dokploy Docker Build on: push: - branches: [main, canary, "feat/monitoring"] + branches: [main, canary, "feat/better-auth-2"] env: IMAGE_NAME: dokploy/dokploy diff --git a/README.md b/README.md index 8d375549..9246cf55 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,8 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). Lightnode + + ### Premium Supporters 🥇 @@ -94,8 +96,10 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). Startupfame Itsdb-center Openalternative +Synexa + ### Community Backers 🤝
diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 4b405e9c..0db56599 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -28,7 +28,7 @@ app.use(async (c, next) => { app.post("/deploy", zValidator("json", deployJobSchema), (c) => { const data = c.req.valid("json"); - const res = queue.add(data, { groupName: data.serverId }); + queue.add(data, { groupName: data.serverId }); return c.json( { message: "Deployment Added", diff --git a/apps/api/src/utils.ts b/apps/api/src/utils.ts index d919f29e..3f3c9698 100644 --- a/apps/api/src/utils.ts +++ b/apps/api/src/utils.ts @@ -64,7 +64,7 @@ export const deploy = async (job: DeployJob) => { } } } - } catch (error) { + } catch (_) { if (job.applicationType === "application") { await updateApplicationStatus(job.applicationId, "error"); } else if (job.applicationType === "compose") { diff --git a/apps/dokploy/__test__/compose/config/config.test.ts b/apps/dokploy/__test__/compose/config/config.test.ts index 3f98525a..aed3350f 100644 --- a/apps/dokploy/__test__/compose/config/config.test.ts +++ b/apps/dokploy/__test__/compose/config/config.test.ts @@ -1,5 +1,5 @@ import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server"; +import { addSuffixToAllConfigs } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/network/network-root.test.ts b/apps/dokploy/__test__/compose/network/network-root.test.ts index 7e06a9f0..980502ff 100644 --- a/apps/dokploy/__test__/compose/network/network-root.test.ts +++ b/apps/dokploy/__test__/compose/network/network-root.test.ts @@ -293,29 +293,6 @@ networks: dokploy-network: `; -const expectedComposeFile7 = ` -version: "3.8" - -services: - web: - image: nginx:latest - networks: - - dokploy-network - -networks: - dokploy-network: - driver: bridge - driver_opts: - com.docker.network.driver.mtu: 1200 - - backend: - driver: bridge - attachable: true - - external_network: - external: true - name: dokploy-network -`; test("It shoudn't add suffix to dokploy-network", () => { const composeData = load(composeFile7) as ComposeSpecification; diff --git a/apps/dokploy/__test__/compose/secrets/secret-root.test.ts b/apps/dokploy/__test__/compose/secrets/secret-root.test.ts index 2bd91b58..1b1898c5 100644 --- a/apps/dokploy/__test__/compose/secrets/secret-root.test.ts +++ b/apps/dokploy/__test__/compose/secrets/secret-root.test.ts @@ -1,7 +1,7 @@ import { generateRandomHash } from "@dokploy/server"; import { addSuffixToSecretsRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { dump, load } from "js-yaml"; +import { load } from "js-yaml"; import { expect, test } from "vitest"; test("Generate random hash with 8 characters", () => { diff --git a/apps/dokploy/__test__/compose/volume/volume.test.ts b/apps/dokploy/__test__/compose/volume/volume.test.ts index d4623aeb..6c434476 100644 --- a/apps/dokploy/__test__/compose/volume/volume.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume.test.ts @@ -1,8 +1,4 @@ -import { generateRandomHash } from "@dokploy/server"; -import { - addSuffixToAllVolumes, - addSuffixToVolumesInServices, -} from "@dokploy/server"; +import { addSuffixToAllVolumes } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/drop/drop.test.test.ts b/apps/dokploy/__test__/drop/drop.test.test.ts index c4b2ba8d..4e6f20d3 100644 --- a/apps/dokploy/__test__/drop/drop.test.test.ts +++ b/apps/dokploy/__test__/drop/drop.test.test.ts @@ -45,7 +45,7 @@ const baseApp: ApplicationNested = { previewWildcard: "", project: { env: "", - adminId: "", + organizationId: "", name: "", description: "", createdAt: "", diff --git a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts index e38c19e4..c72d7254 100644 --- a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts +++ b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts @@ -5,7 +5,7 @@ vi.mock("node:fs", () => ({ default: fs, })); -import type { Admin, FileConfig } from "@dokploy/server"; +import type { FileConfig, User } from "@dokploy/server"; import { createDefaultServerTraefikConfig, loadOrCreateConfig, @@ -13,7 +13,7 @@ import { } from "@dokploy/server"; import { beforeEach, expect, test, vi } from "vitest"; -const baseAdmin: Admin = { +const baseAdmin: User = { enablePaidFeatures: false, metricsConfig: { containers: { @@ -40,9 +40,7 @@ const baseAdmin: Admin = { cleanupCacheApplications: false, cleanupCacheOnCompose: false, cleanupCacheOnPreviews: false, - createdAt: "", - authId: "", - adminId: "string", + createdAt: new Date(), serverIp: null, certificateType: "none", host: null, @@ -53,6 +51,19 @@ const baseAdmin: Admin = { serversQuantity: 0, stripeCustomerId: "", stripeSubscriptionId: "", + banExpires: new Date(), + banned: true, + banReason: "", + email: "", + expirationDate: "", + id: "", + isRegistered: false, + name: "", + createdAt2: new Date().toISOString(), + emailVerified: false, + image: "", + updatedAt: new Date(), + twoFactorEnabled: false, }; beforeEach(() => { @@ -103,8 +114,6 @@ test("Should not touch config without host", () => { }); test("Should remove websecure if https rollback to http", () => { - const originalConfig: FileConfig = loadOrCreateConfig("dokploy"); - updateServerTraefik( { ...baseAdmin, certificateType: "letsencrypt" }, "example.com", diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index d05dda81..955103de 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -26,7 +26,7 @@ const baseApp: ApplicationNested = { previewWildcard: "", project: { env: "", - adminId: "", + organizationId: "", name: "", description: "", createdAt: "", diff --git a/apps/dokploy/components/auth/login-2fa.tsx b/apps/dokploy/components/auth/login-2fa.tsx deleted file mode 100644 index 6a11268e..00000000 --- a/apps/dokploy/components/auth/login-2fa.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; - -import { CardTitle } from "@/components/ui/card"; -import { - InputOTP, - InputOTPGroup, - InputOTPSeparator, - InputOTPSlot, -} from "@/components/ui/input-otp"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { REGEXP_ONLY_DIGITS } from "input-otp"; -import { AlertTriangle } from "lucide-react"; -import { useRouter } from "next/router"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const Login2FASchema = z.object({ - pin: z.string().min(6, { - message: "Pin is required", - }), -}); - -type Login2FA = z.infer; - -interface Props { - authId: string; -} - -export const Login2FA = ({ authId }: Props) => { - const { push } = useRouter(); - - const { mutateAsync, isLoading, isError, error } = - api.auth.verifyLogin2FA.useMutation(); - - const form = useForm({ - defaultValues: { - pin: "", - }, - resolver: zodResolver(Login2FASchema), - }); - - useEffect(() => { - form.reset({ - pin: "", - }); - }, [form, form.reset, form.formState.isSubmitSuccessful]); - - const onSubmit = async (data: Login2FA) => { - await mutateAsync({ - pin: data.pin, - id: authId, - }) - .then(() => { - toast.success("Signin successfully", { - duration: 2000, - }); - - push("/dashboard/projects"); - }) - .catch(() => { - toast.error("Signin failed", { - duration: 2000, - }); - }); - }; - return ( -
- - {isError && ( -
- - - {error?.message} - -
- )} - 2FA Login - - ( - - Pin - -
- - - - - - - - - - -
-
- - Please enter the 6 digits code provided by your authenticator - app. - - -
- )} - /> - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx index 9b71a042..95a559f6 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -130,7 +130,7 @@ const createStringToJSONSchema = (schema: z.ZodTypeAny) => { } try { return JSON.parse(str); - } catch (e) { + } catch (_e) { ctx.addIssue({ code: "custom", message: "Invalid JSON format" }); return z.NEVER; } diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx index cf7314cf..1eadf8ba 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx @@ -29,7 +29,6 @@ import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { Server } from "lucide-react"; import Link from "next/link"; -import React from "react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; diff --git a/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx b/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx index 4cd839a1..50e36ad7 100644 --- a/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx @@ -17,7 +17,6 @@ import { import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import React from "react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; diff --git a/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx b/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx index a2c6ddcf..4cd29a36 100644 --- a/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx @@ -10,7 +10,6 @@ import { } from "@/components/ui/card"; import { api } from "@/utils/api"; import { Rss, Trash2 } from "lucide-react"; -import React from "react"; import { toast } from "sonner"; import { HandlePorts } from "./handle-ports"; interface Props { diff --git a/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx b/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx index 4ee59791..5c2c5943 100644 --- a/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx @@ -9,7 +9,6 @@ import { } from "@/components/ui/card"; import { api } from "@/utils/api"; import { Split, Trash2 } from "lucide-react"; -import React from "react"; import { toast } from "sonner"; import { HandleRedirect } from "./handle-redirect"; diff --git a/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx b/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx index 33022c09..92439f51 100644 --- a/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx @@ -9,7 +9,6 @@ import { } from "@/components/ui/card"; import { api } from "@/utils/api"; import { LockKeyhole, Trash2 } from "lucide-react"; -import React from "react"; import { toast } from "sonner"; import { HandleSecurity } from "./handle-security"; diff --git a/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx b/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx index 227bca55..3d26716f 100644 --- a/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx @@ -25,7 +25,7 @@ import { import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { InfoIcon } from "lucide-react"; -import React, { useEffect } from "react"; +import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; diff --git a/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx b/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx index fb6fc0c1..58601fb4 100644 --- a/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx @@ -8,7 +8,6 @@ import { } from "@/components/ui/card"; import { api } from "@/utils/api"; import { File, Loader2 } from "lucide-react"; -import React from "react"; import { UpdateTraefikConfig } from "./update-traefik-config"; interface Props { applicationId: string; diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx index e0f842ce..2a2d2c03 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx @@ -10,7 +10,6 @@ import { } from "@/components/ui/card"; import { api } from "@/utils/api"; import { Package, Trash2 } from "lucide-react"; -import React from "react"; import { toast } from "sonner"; import type { ServiceType } from "../show-resources"; import { AddVolumes } from "./add-volumes"; diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx index d8481d65..8da09b58 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx @@ -21,7 +21,7 @@ import { import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon, Pencil } from "lucide-react"; +import { PenBoxIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -77,7 +77,7 @@ export const UpdateVolume = ({ serviceType, }: Props) => { const [isOpen, setIsOpen] = useState(false); - const utils = api.useUtils(); + const _utils = api.useUtils(); const { data } = api.mounts.one.useQuery( { mountId, diff --git a/apps/dokploy/components/dashboard/application/deployments/refresh-token.tsx b/apps/dokploy/components/dashboard/application/deployments/refresh-token.tsx index c268e6d5..b80450f9 100644 --- a/apps/dokploy/components/dashboard/application/deployments/refresh-token.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/refresh-token.tsx @@ -11,7 +11,6 @@ import { } from "@/components/ui/alert-dialog"; import { api } from "@/utils/api"; import { RefreshCcw } from "lucide-react"; -import React from "react"; import { toast } from "sonner"; interface Props { diff --git a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx index b65a1816..ba20db31 100644 --- a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx +++ b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx @@ -18,7 +18,7 @@ import { Toggle } from "@/components/ui/toggle"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { EyeIcon, EyeOffIcon } from "lucide-react"; -import React, { type CSSProperties, useEffect, useState } from "react"; +import { type CSSProperties, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; diff --git a/apps/dokploy/components/dashboard/application/environment/show.tsx b/apps/dokploy/components/dashboard/application/environment/show.tsx index 7200f2a7..d97c39e2 100644 --- a/apps/dokploy/components/dashboard/application/environment/show.tsx +++ b/apps/dokploy/components/dashboard/application/environment/show.tsx @@ -1,5 +1,5 @@ import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; +import { Card } from "@/components/ui/card"; import { Form } from "@/components/ui/form"; import { Secrets } from "@/components/ui/secrets"; import { api } from "@/utils/api"; diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx index 9b207d63..9af040b7 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx @@ -84,7 +84,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { data: repositories, isLoading: isLoadingRepositories, error, - isError, } = api.bitbucket.getBitbucketRepositories.useQuery( { bitbucketId, diff --git a/apps/dokploy/components/dashboard/application/general/generic/show.tsx b/apps/dokploy/components/dashboard/application/general/generic/show.tsx index 73f5e8f8..b00a3495 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/show.tsx @@ -11,7 +11,7 @@ import { import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { api } from "@/utils/api"; -import { GitBranch, LockIcon, UploadCloud } from "lucide-react"; +import { GitBranch, UploadCloud } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import { SaveBitbucketProvider } from "./save-bitbucket-provider"; diff --git a/apps/dokploy/components/dashboard/application/general/show.tsx b/apps/dokploy/components/dashboard/application/general/show.tsx index 83e4b6f0..8989ca19 100644 --- a/apps/dokploy/components/dashboard/application/general/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/show.tsx @@ -7,7 +7,6 @@ import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { Ban, CheckCircle2, Hammer, RefreshCcw, Terminal } from "lucide-react"; import { useRouter } from "next/router"; -import React from "react"; import { toast } from "sonner"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; interface Props { @@ -28,8 +27,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => { const { mutateAsync: stop, isLoading: isStopping } = api.application.stop.useMutation(); - const { mutateAsync: deploy, isLoading: isDeploying } = - api.application.deploy.useMutation(); + const { mutateAsync: deploy } = api.application.deploy.useMutation(); const { mutateAsync: reload, isLoading: isReloading } = api.application.reload.useMutation(); diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx index 55b31f3f..90800f75 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx @@ -5,7 +5,6 @@ import { Dialog, DialogContent, DialogDescription, - DialogFooter, DialogHeader, DialogTitle, DialogTrigger, diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx index 371276bd..ec3680f1 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -22,7 +22,6 @@ import { RocketIcon, Trash2, } from "lucide-react"; -import React from "react"; import { toast } from "sonner"; import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; import { AddPreviewDomain } from "./add-preview-domain"; diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx index fec61ca6..9d53f31d 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx @@ -279,7 +279,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { ( + render={() => ( { compose: () => api.compose.one.useQuery({ composeId: id }, { enabled: !!id }), }; - const { data, refetch } = queryMap[type] + const { data } = queryMap[type] ? queryMap[type]() : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); diff --git a/apps/dokploy/components/dashboard/compose/deployments/refresh-token-compose.tsx b/apps/dokploy/components/dashboard/compose/deployments/refresh-token-compose.tsx index 95fafaab..b062b099 100644 --- a/apps/dokploy/components/dashboard/compose/deployments/refresh-token-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/deployments/refresh-token-compose.tsx @@ -11,7 +11,6 @@ import { } from "@/components/ui/alert-dialog"; import { api } from "@/utils/api"; import { RefreshCcw } from "lucide-react"; -import React from "react"; import { toast } from "sonner"; interface Props { diff --git a/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx b/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx index 7bc451e0..e6468d6f 100644 --- a/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx @@ -118,7 +118,7 @@ export const ShowDomainsCompose = ({ composeId }: Props) => { await deleteDomain({ domainId: item.domainId, }) - .then((data) => { + .then((_data) => { refetch(); toast.success("Domain deleted successfully"); }) diff --git a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx index c4ce44e5..72589582 100644 --- a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx +++ b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx @@ -35,8 +35,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => { { enabled: !!composeId }, ); - const { mutateAsync, isLoading, error, isError } = - api.compose.update.useMutation(); + const { mutateAsync, isLoading } = api.compose.update.useMutation(); const form = useForm({ defaultValues: { @@ -76,7 +75,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => { composeId, }); }) - .catch((e) => { + .catch((_e) => { toast.error("Error updating the Compose config"); }); }; diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx index 1c06fe88..87584133 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx @@ -84,7 +84,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { data: repositories, isLoading: isLoadingRepositories, error, - isError, } = api.bitbucket.getBitbucketRepositories.useQuery( { bitbucketId, diff --git a/apps/dokploy/components/dashboard/compose/general/generic/show.tsx b/apps/dokploy/components/dashboard/compose/general/generic/show.tsx index 1681039c..347c134e 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/show.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/show.tsx @@ -7,7 +7,7 @@ import { import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { api } from "@/utils/api"; -import { CodeIcon, GitBranch, LockIcon } from "lucide-react"; +import { CodeIcon, GitBranch } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import { ComposeFileEditor } from "../compose-file-editor"; diff --git a/apps/dokploy/components/dashboard/compose/general/isolated-deployment.tsx b/apps/dokploy/components/dashboard/compose/general/isolated-deployment.tsx index 70d685ef..3ae2e9fe 100644 --- a/apps/dokploy/components/dashboard/compose/general/isolated-deployment.tsx +++ b/apps/dokploy/components/dashboard/compose/general/isolated-deployment.tsx @@ -70,7 +70,7 @@ export const IsolatedDeployment = ({ composeId }: Props) => { composeId, isolatedDeployment: formData?.isolatedDeployment || false, }) - .then(async (data) => { + .then(async (_data) => { randomizeCompose(); refetch(); toast.success("Compose updated"); diff --git a/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx b/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx index b1a00985..4cc877fd 100644 --- a/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx @@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, Dices } from "lucide-react"; +import { AlertTriangle } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -39,7 +39,7 @@ type Schema = z.infer; export const RandomizeCompose = ({ composeId }: Props) => { const utils = api.useUtils(); const [compose, setCompose] = useState(""); - const [isOpen, setIsOpen] = useState(false); + const [_isOpen, _setIsOpen] = useState(false); const { mutateAsync, error, isError } = api.compose.randomizeCompose.useMutation(); @@ -76,7 +76,7 @@ export const RandomizeCompose = ({ composeId }: Props) => { suffix: formData?.suffix || "", randomize: formData?.randomize || false, }) - .then(async (data) => { + .then(async (_data) => { randomizeCompose(); refetch(); toast.success("Compose updated"); diff --git a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx index 8a2186d9..49606645 100644 --- a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx @@ -40,7 +40,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => { .then(() => { refetch(); }) - .catch((err) => {}); + .catch((_err) => {}); } }, [isOpen]); diff --git a/apps/dokploy/components/dashboard/compose/general/show.tsx b/apps/dokploy/components/dashboard/compose/general/show.tsx index d002b409..71752525 100644 --- a/apps/dokploy/components/dashboard/compose/general/show.tsx +++ b/apps/dokploy/components/dashboard/compose/general/show.tsx @@ -7,7 +7,6 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import React from "react"; import { ComposeActions } from "./actions"; import { ShowProviderFormCompose } from "./generic/show"; interface Props { diff --git a/apps/dokploy/components/dashboard/compose/logs/show.tsx b/apps/dokploy/components/dashboard/compose/logs/show.tsx index 4530e0dd..57119054 100644 --- a/apps/dokploy/components/dashboard/compose/logs/show.tsx +++ b/apps/dokploy/components/dashboard/compose/logs/show.tsx @@ -18,7 +18,7 @@ import { SelectValue, } from "@/components/ui/select"; import { api } from "@/utils/api"; -import { Loader, Loader2 } from "lucide-react"; +import { Loader2 } from "lucide-react"; import dynamic from "next/dynamic"; import { useEffect, useState } from "react"; export const DockerLogs = dynamic( diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index 21fe28d4..6619ceae 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -16,7 +16,6 @@ import { import { api } from "@/utils/api"; import { DatabaseBackup, Play, Trash2 } from "lucide-react"; import Link from "next/link"; -import React from "react"; import { toast } from "sonner"; import type { ServiceType } from "../../application/advanced/show-resources"; import { AddBackup } from "./add-backup"; diff --git a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx b/apps/dokploy/components/dashboard/database/backups/update-backup.tsx index 0083bb1d..99f7692a 100644 --- a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/update-backup.tsx @@ -35,7 +35,7 @@ import { Switch } from "@/components/ui/switch"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { CheckIcon, ChevronsUpDown, PenBoxIcon, Pencil } from "lucide-react"; +import { CheckIcon, ChevronsUpDown, PenBoxIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; diff --git a/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx index b7caafe7..44f2cdfc 100644 --- a/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx @@ -15,7 +15,6 @@ import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { cn } from "@/lib/utils"; import { CheckIcon } from "lucide-react"; -import React from "react"; export type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index c25acc67..48ec4557 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -9,7 +9,6 @@ import { import { cn } from "@/lib/utils"; import { FancyAnsi } from "fancy-ansi"; import { escapeRegExp } from "lodash"; -import React from "react"; import { type LogLine, getLogType } from "./utils"; interface LogLineProps { @@ -48,23 +47,12 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { } const htmlContent = fancyAnsi.toHtml(text); + const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi"); + const modifiedContent = htmlContent.replace( - /]*)>([^<]*)<\/span>/g, - (match, attrs, content) => { - const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi"); - if (!content.match(searchRegex)) return match; - - const segments = content.split(searchRegex); - const wrappedSegments = segments - .map((segment: string) => - segment.toLowerCase() === term.toLowerCase() - ? `${segment}` - : segment, - ) - .join(""); - - return `${wrappedSegments}`; - }, + searchRegex, + (match) => + `${match}`, ); return ( diff --git a/apps/dokploy/components/dashboard/docker/show/colums.tsx b/apps/dokploy/components/dashboard/docker/show/colums.tsx index 3feae176..1cf0200f 100644 --- a/apps/dokploy/components/dashboard/docker/show/colums.tsx +++ b/apps/dokploy/components/dashboard/docker/show/colums.tsx @@ -1,6 +1,5 @@ import type { ColumnDef } from "@tanstack/react-table"; import { ArrowUpDown, MoreHorizontal } from "lucide-react"; -import * as React from "react"; import { Button } from "@/components/ui/button"; import { diff --git a/apps/dokploy/components/dashboard/docker/show/show-containers.tsx b/apps/dokploy/components/dashboard/docker/show/show-containers.tsx index c66c9b9b..024b0061 100644 --- a/apps/dokploy/components/dashboard/docker/show/show-containers.tsx +++ b/apps/dokploy/components/dashboard/docker/show/show-containers.tsx @@ -1,18 +1,3 @@ -import { - type ColumnFiltersState, - type SortingState, - type VisibilityState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; -import { ChevronDown, Container } from "lucide-react"; -import * as React from "react"; - -import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { Card, @@ -37,6 +22,19 @@ import { TableRow, } from "@/components/ui/table"; import { type RouterOutputs, api } from "@/utils/api"; +import { + type ColumnFiltersState, + type SortingState, + type VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { ChevronDown, Container } from "lucide-react"; +import * as React from "react"; import { columns } from "./colums"; export type Container = NonNullable< RouterOutputs["docker"]["getContainers"] diff --git a/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx b/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx index ed2ed197..c9272f29 100644 --- a/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx +++ b/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx @@ -7,9 +7,8 @@ import { CardTitle, } from "@/components/ui/card"; import { Tree } from "@/components/ui/file-tree"; -import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import { FileIcon, Folder, Link, Loader2, Workflow } from "lucide-react"; +import { FileIcon, Folder, Loader2, Workflow } from "lucide-react"; import React from "react"; import { ShowTraefikFile } from "./show-traefik-file"; diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx index f2044917..4a5c43a2 100644 --- a/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx +++ b/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx @@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx index 98773685..ad6b1164 100644 --- a/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx +++ b/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx @@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { api } from "@/utils/api"; import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react"; -import React, { useState } from "react"; +import { useState } from "react"; import { toast } from "sonner"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx index b409ac4d..17026926 100644 --- a/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx +++ b/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx @@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; -import React from "react"; interface Props { mariadbId: string; diff --git a/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx index 4c9be090..48d94489 100644 --- a/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx +++ b/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx @@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react"; +import { PenBoxIcon } from "lucide-react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; diff --git a/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx b/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx index 6dd2e919..9fe6e713 100644 --- a/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx +++ b/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx @@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; diff --git a/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx b/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx index df01e36d..a20d4637 100644 --- a/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx +++ b/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx @@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { api } from "@/utils/api"; import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react"; -import React, { useState } from "react"; +import { useState } from "react"; import { toast } from "sonner"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; diff --git a/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx b/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx index 6636688d..e66ea8c3 100644 --- a/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx +++ b/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx @@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; -import React from "react"; interface Props { mongoId: string; diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-compose-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-compose-monitoring.tsx index 99be6d9d..84510154 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-compose-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-compose-monitoring.tsx @@ -1,6 +1,7 @@ +import { badgeStateColor } from "@/components/dashboard/application/logs/show"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { - Card, CardContent, CardDescription, CardHeader, @@ -96,7 +97,10 @@ export const ComposeFreeMonitoring = ({ key={container.containerId} value={container.name} > - {container.name} ({container.containerId}) {container.state} + {container.name} ({container.containerId}){" "} + + {container.state} + ))} Containers ({data?.length}) diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx index 64a46bdb..278e0936 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx @@ -1,13 +1,7 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { api } from "@/utils/api"; -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { DockerBlockChart } from "./docker-block-chart"; import { DockerCpuChart } from "./docker-cpu-chart"; import { DockerDiskChart } from "./docker-disk-chart"; diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/container-block-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/container-block-chart.tsx index 9150dbcd..12af6b91 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/container-block-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/container-block-chart.tsx @@ -29,14 +29,6 @@ interface Props { data: ContainerMetric[]; } -interface FormattedMetric { - timestamp: string; - read: number; - write: number; - readUnit: string; - writeUnit: string; -} - const chartConfig = { read: { label: "Read", diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring.tsx index 580d7ea1..4ca461c2 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring.tsx @@ -1,3 +1,5 @@ +import { badgeStateColor } from "@/components/dashboard/application/logs/show"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, @@ -102,7 +104,9 @@ export const ComposePaidMonitoring = ({ value={container.name} > {container.name} ({container.containerId}){" "} - {container.state} + + {container.state} + ))} Containers ({data?.length}) diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx index 3636a391..3b189c2a 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx @@ -79,7 +79,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => { data, isLoading, error: queryError, - } = api.admin.getContainerMetrics.useQuery( + } = api.user.getContainerMetrics.useQuery( { url: baseUrl, token, diff --git a/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx index 043b5c62..e92ce03f 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx @@ -7,7 +7,6 @@ import { } from "@/components/ui/select"; import { api } from "@/utils/api"; import { Clock, Cpu, HardDrive, Loader2, MemoryStick } from "lucide-react"; -import type React from "react"; import { useEffect, useState } from "react"; import { CPUChart } from "./cpu-chart"; import { DiskChart } from "./disk-chart"; @@ -73,7 +72,7 @@ export const ShowPaidMonitoring = ({ data, isLoading, error: queryError, - } = api.admin.getServerMetrics.useQuery( + } = api.server.getServerMetrics.useQuery( { url: BASE_URL, token, diff --git a/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx b/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx index dc1ca3a7..7a0527b1 100644 --- a/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx +++ b/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx @@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; diff --git a/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx b/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx index 56a191ce..13f46cae 100644 --- a/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx +++ b/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx @@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { api } from "@/utils/api"; import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react"; -import React, { useState } from "react"; +import { useState } from "react"; import { toast } from "sonner"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; diff --git a/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx b/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx index 2c09efb8..3f187237 100644 --- a/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx +++ b/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx @@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; -import React from "react"; interface Props { mysqlId: string; diff --git a/apps/dokploy/components/dashboard/mysql/update-mysql.tsx b/apps/dokploy/components/dashboard/mysql/update-mysql.tsx index 645575cd..efe1eb11 100644 --- a/apps/dokploy/components/dashboard/mysql/update-mysql.tsx +++ b/apps/dokploy/components/dashboard/mysql/update-mysql.tsx @@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react"; +import { PenBoxIcon } from "lucide-react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; diff --git a/apps/dokploy/components/dashboard/organization/handle-organization.tsx b/apps/dokploy/components/dashboard/organization/handle-organization.tsx new file mode 100644 index 00000000..014c37df --- /dev/null +++ b/apps/dokploy/components/dashboard/organization/handle-organization.tsx @@ -0,0 +1,182 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { PenBoxIcon, Plus } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const organizationSchema = z.object({ + name: z.string().min(1, { + message: "Organization name is required", + }), + logo: z.string().optional(), +}); + +type OrganizationFormValues = z.infer; + +interface Props { + organizationId?: string; + children?: React.ReactNode; +} + +export function AddOrganization({ organizationId }: Props) { + const [open, setOpen] = useState(false); + const utils = api.useUtils(); + const { data: organization } = api.organization.one.useQuery( + { + organizationId: organizationId ?? "", + }, + { + enabled: !!organizationId, + }, + ); + const { mutateAsync, isLoading } = organizationId + ? api.organization.update.useMutation() + : api.organization.create.useMutation(); + + const form = useForm({ + resolver: zodResolver(organizationSchema), + defaultValues: { + name: "", + logo: "", + }, + }); + + useEffect(() => { + if (organization) { + form.reset({ + name: organization.name, + logo: organization.logo || "", + }); + } + }, [organization, form]); + + const onSubmit = async (values: OrganizationFormValues) => { + await mutateAsync({ + name: values.name, + logo: values.logo, + organizationId: organizationId ?? "", + }) + .then(() => { + form.reset(); + toast.success( + `Organization ${organizationId ? "updated" : "created"} successfully`, + ); + utils.organization.all.invalidate(); + setOpen(false); + }) + .catch((error) => { + console.error(error); + toast.error( + `Failed to ${organizationId ? "update" : "create"} organization`, + ); + }); + }; + + return ( + + + {organizationId ? ( + e.preventDefault()} + > + + + ) : ( + e.preventDefault()} + > +
+ +
+
+ Add organization +
+
+ )} +
+ + + + {organizationId ? "Update organization" : "Add organization"} + + + {organizationId + ? "Update the organization name and logo" + : "Create a new organization to manage your projects."} + + +
+ + ( + + Name + + + + + + )} + /> + ( + + Logo URL + + + + + + )} + /> + + + + + +
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx b/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx index 6e912db9..40e84844 100644 --- a/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx +++ b/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx @@ -11,7 +11,7 @@ import { import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect } from "react"; +import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; @@ -53,7 +53,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => { mongo: () => api.mongo.update.useMutation(), }; - const { mutateAsync, isLoading } = mutationMap[type] + const { mutateAsync } = mutationMap[type] ? mutationMap[type]() : api.mongo.update.useMutation(); diff --git a/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx b/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx index e8fff7dc..dbd57d0b 100644 --- a/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx +++ b/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx @@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; diff --git a/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx b/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx index 43c3f432..89d27523 100644 --- a/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx +++ b/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx @@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { api } from "@/utils/api"; import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react"; -import React, { useState } from "react"; +import { useState } from "react"; import { toast } from "sonner"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; diff --git a/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx b/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx index e0122610..545150f8 100644 --- a/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx +++ b/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx @@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; -import React from "react"; interface Props { postgresId: string; diff --git a/apps/dokploy/components/dashboard/postgres/update-postgres.tsx b/apps/dokploy/components/dashboard/postgres/update-postgres.tsx index 54ad5bce..7be6908f 100644 --- a/apps/dokploy/components/dashboard/postgres/update-postgres.tsx +++ b/apps/dokploy/components/dashboard/postgres/update-postgres.tsx @@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react"; +import { PenBoxIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; diff --git a/apps/dokploy/components/dashboard/project/add-application.tsx b/apps/dokploy/components/dashboard/project/add-application.tsx index da30cfee..16c56917 100644 --- a/apps/dokploy/components/dashboard/project/add-application.tsx +++ b/apps/dokploy/components/dashboard/project/add-application.tsx @@ -103,7 +103,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => { projectId, }); }) - .catch((e) => { + .catch((_e) => { toast.error("Error creating the service"); }); }; diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx index 1ca0d6a5..b14e2cfa 100644 --- a/apps/dokploy/components/dashboard/project/add-database.tsx +++ b/apps/dokploy/components/dashboard/project/add-database.tsx @@ -18,7 +18,6 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { Form, FormControl, - FormDescription, FormField, FormItem, FormLabel, @@ -49,7 +48,6 @@ import { z } from "zod"; type DbType = typeof mySchema._type.type; -// TODO: Change to a real docker images const dockerImageDefaultPlaceholder: Record = { mongo: "mongo:6", mariadb: "mariadb:11", diff --git a/apps/dokploy/components/dashboard/project/add-template.tsx b/apps/dokploy/components/dashboard/project/add-template.tsx index cc6962aa..5363e6f3 100644 --- a/apps/dokploy/components/dashboard/project/add-template.tsx +++ b/apps/dokploy/components/dashboard/project/add-template.tsx @@ -57,7 +57,6 @@ import { BookText, CheckIcon, ChevronsUpDown, - Code, Github, Globe, HelpCircle, @@ -435,14 +434,14 @@ export const AddTemplate = ({ projectId }: Props) => { }); toast.promise(promise, { loading: "Setting up...", - success: (data) => { + success: (_data) => { utils.project.one.invalidate({ projectId, }); setOpen(false); return `${template.name} template created successfully`; }, - error: (err) => { + error: (_err) => { return `An error ocurred deploying ${template.name} template`; }, }); diff --git a/apps/dokploy/components/dashboard/projects/handle-project.tsx b/apps/dokploy/components/dashboard/projects/handle-project.tsx index 08e3e0a8..f5d62dfc 100644 --- a/apps/dokploy/components/dashboard/projects/handle-project.tsx +++ b/apps/dokploy/components/dashboard/projects/handle-project.tsx @@ -97,6 +97,18 @@ export const HandleProject = ({ projectId }: Props) => { ); }); }; + // useEffect(() => { + // const getUsers = async () => { + // const users = await authClient.admin.listUsers({ + // query: { + // limit: 100, + // }, + // }); + // console.log(users); + // }; + + // getUsers(); + // }); return ( diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 8d058b6c..188ee60d 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -51,15 +51,7 @@ import { ProjectEnvironment } from "./project-environment"; export const ShowProjects = () => { const utils = api.useUtils(); const { data, isLoading } = api.project.all.useQuery(); - const { data: auth } = api.auth.get.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: auth?.id || "", - }, - { - enabled: !!auth?.id && auth?.rol === "user", - }, - ); + const { data: auth } = api.user.get.useQuery(); const { mutateAsync } = api.project.remove.useMutation(); const [searchQuery, setSearchQuery] = useState(""); @@ -91,7 +83,7 @@ export const ShowProjects = () => { - {(auth?.rol === "admin" || user?.canCreateProjects) && ( + {(auth?.role === "owner" || auth?.canCreateProjects) && (
@@ -293,8 +285,8 @@ export const ShowProjects = () => {
e.stopPropagation()} > - {(auth?.rol === "admin" || - user?.canDeleteProjects) && ( + {(auth?.role === "owner" || + auth?.canDeleteProjects) && ( { useEffect(() => { const buildConnectionUrl = () => { - const hostname = window.location.hostname; + const _hostname = window.location.hostname; const port = form.watch("externalPort") || data?.externalPort; return `redis://default:${data?.databasePassword}@${getIp}:${port}`; diff --git a/apps/dokploy/components/dashboard/redis/general/show-general-redis.tsx b/apps/dokploy/components/dashboard/redis/general/show-general-redis.tsx index ec4aeb6c..e309ef49 100644 --- a/apps/dokploy/components/dashboard/redis/general/show-general-redis.tsx +++ b/apps/dokploy/components/dashboard/redis/general/show-general-redis.tsx @@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { api } from "@/utils/api"; import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react"; -import React, { useState } from "react"; +import { useState } from "react"; import { toast } from "sonner"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; diff --git a/apps/dokploy/components/dashboard/redis/general/show-internal-redis-credentials.tsx b/apps/dokploy/components/dashboard/redis/general/show-internal-redis-credentials.tsx index 09200674..47ad0df0 100644 --- a/apps/dokploy/components/dashboard/redis/general/show-internal-redis-credentials.tsx +++ b/apps/dokploy/components/dashboard/redis/general/show-internal-redis-credentials.tsx @@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; -import React from "react"; interface Props { redisId: string; diff --git a/apps/dokploy/components/dashboard/redis/update-redis.tsx b/apps/dokploy/components/dashboard/redis/update-redis.tsx index c3557bee..193aec3b 100644 --- a/apps/dokploy/components/dashboard/redis/update-redis.tsx +++ b/apps/dokploy/components/dashboard/redis/update-redis.tsx @@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react"; +import { PenBoxIcon } from "lucide-react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; diff --git a/apps/dokploy/components/dashboard/requests/columns.tsx b/apps/dokploy/components/dashboard/requests/columns.tsx index 529d64a4..2c0391f8 100644 --- a/apps/dokploy/components/dashboard/requests/columns.tsx +++ b/apps/dokploy/components/dashboard/requests/columns.tsx @@ -3,7 +3,6 @@ import { Button } from "@/components/ui/button"; import type { ColumnDef } from "@tanstack/react-table"; import { format } from "date-fns"; import { ArrowUpDown } from "lucide-react"; -import * as React from "react"; import type { LogEntry } from "./show-requests"; export const getStatusColor = (status: number) => { @@ -25,7 +24,7 @@ export const getStatusColor = (status: number) => { export const columns: ColumnDef[] = [ { accessorKey: "level", - header: ({ column }) => { + header: () => { return ; }, cell: ({ row }) => { diff --git a/apps/dokploy/components/dashboard/requests/requests-table.tsx b/apps/dokploy/components/dashboard/requests/requests-table.tsx index cd2949c3..4926ce4e 100644 --- a/apps/dokploy/components/dashboard/requests/requests-table.tsx +++ b/apps/dokploy/components/dashboard/requests/requests-table.tsx @@ -92,7 +92,7 @@ export const RequestsTable = () => { pageSize: 10, }); - const { data: statsLogs, isLoading } = api.settings.readStatsLogs.useQuery( + const { data: statsLogs } = api.settings.readStatsLogs.useQuery( { sort: sorting[0], page: pagination, @@ -300,7 +300,7 @@ export const RequestsTable = () => {
setSelectedRow(undefined)} + onOpenChange={(_open) => setSelectedRow(undefined)} > diff --git a/apps/dokploy/components/dashboard/requests/show-requests.tsx b/apps/dokploy/components/dashboard/requests/show-requests.tsx index 05ba6e51..c3d92dd6 100644 --- a/apps/dokploy/components/dashboard/requests/show-requests.tsx +++ b/apps/dokploy/components/dashboard/requests/show-requests.tsx @@ -11,7 +11,6 @@ import { import { type RouterOutputs, api } from "@/utils/api"; import { ArrowDownUp } from "lucide-react"; import Link from "next/link"; -import * as React from "react"; import { toast } from "sonner"; import { RequestDistributionChart } from "./request-distribution-chart"; import { RequestsTable } from "./requests-table"; diff --git a/apps/dokploy/components/dashboard/search-command.tsx b/apps/dokploy/components/dashboard/search-command.tsx index 4d3c75f9..b3670303 100644 --- a/apps/dokploy/components/dashboard/search-command.tsx +++ b/apps/dokploy/components/dashboard/search-command.tsx @@ -7,9 +7,7 @@ import { PostgresqlIcon, RedisIcon, } from "@/components/icons/data-tools-icons"; -import { Badge } from "@/components/ui/badge"; import { - Command, CommandDialog, CommandEmpty, CommandGroup, @@ -18,26 +16,26 @@ import { CommandList, CommandSeparator, } from "@/components/ui/command"; +import { authClient } from "@/lib/auth-client"; import { type Services, extractServices, } from "@/pages/dashboard/project/[projectId]"; import { api } from "@/utils/api"; -import type { findProjectById } from "@dokploy/server/services/project"; import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react"; import { useRouter } from "next/router"; import React from "react"; import { StatusTooltip } from "../shared/status-tooltip"; -type Project = Awaited>; - export const SearchCommand = () => { const router = useRouter(); const [open, setOpen] = React.useState(false); const [search, setSearch] = React.useState(""); - - const { data } = api.project.all.useQuery(); - const { data: isCloud, isLoading } = api.settings.isCloud.useQuery(); + const { data: session } = authClient.useSession(); + const { data } = api.project.all.useQuery(undefined, { + enabled: !!session, + }); + const { data: isCloud } = api.settings.isCloud.useQuery(); React.useEffect(() => { const down = (e: KeyboardEvent) => { diff --git a/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx b/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx new file mode 100644 index 00000000..a82a9b35 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx @@ -0,0 +1,468 @@ +import { Button } from "@/components/ui/button"; +import { api } from "@/utils/api"; +import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogDescription, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form"; +import { Switch } from "@/components/ui/switch"; + +const formSchema = z.object({ + name: z.string().min(1, "Name is required"), + prefix: z.string().optional(), + expiresIn: z.number().nullable(), + organizationId: z.string().min(1, "Organization is required"), + // Rate limiting fields + rateLimitEnabled: z.boolean().optional(), + rateLimitTimeWindow: z.number().nullable(), + rateLimitMax: z.number().nullable(), + // Request limiting fields + remaining: z.number().nullable().optional(), + refillAmount: z.number().nullable().optional(), + refillInterval: z.number().nullable().optional(), +}); + +type FormValues = z.infer; + +const EXPIRATION_OPTIONS = [ + { label: "Never", value: "0" }, + { label: "1 day", value: String(60 * 60 * 24) }, + { label: "7 days", value: String(60 * 60 * 24 * 7) }, + { label: "30 days", value: String(60 * 60 * 24 * 30) }, + { label: "90 days", value: String(60 * 60 * 24 * 90) }, + { label: "1 year", value: String(60 * 60 * 24 * 365) }, +]; + +const TIME_WINDOW_OPTIONS = [ + { label: "1 minute", value: String(60 * 1000) }, + { label: "5 minutes", value: String(5 * 60 * 1000) }, + { label: "15 minutes", value: String(15 * 60 * 1000) }, + { label: "30 minutes", value: String(30 * 60 * 1000) }, + { label: "1 hour", value: String(60 * 60 * 1000) }, + { label: "1 day", value: String(24 * 60 * 60 * 1000) }, +]; + +const REFILL_INTERVAL_OPTIONS = [ + { label: "1 hour", value: String(60 * 60 * 1000) }, + { label: "6 hours", value: String(6 * 60 * 60 * 1000) }, + { label: "12 hours", value: String(12 * 60 * 60 * 1000) }, + { label: "1 day", value: String(24 * 60 * 60 * 1000) }, + { label: "7 days", value: String(7 * 24 * 60 * 60 * 1000) }, + { label: "30 days", value: String(30 * 24 * 60 * 60 * 1000) }, +]; + +export const AddApiKey = () => { + const [open, setOpen] = useState(false); + const [showSuccessModal, setShowSuccessModal] = useState(false); + const [newApiKey, setNewApiKey] = useState(""); + const { refetch } = api.user.get.useQuery(); + const { data: organizations } = api.organization.all.useQuery(); + const createApiKey = api.user.createApiKey.useMutation({ + onSuccess: (data) => { + if (!data) return; + + setNewApiKey(data.key); + setOpen(false); + setShowSuccessModal(true); + form.reset(); + void refetch(); + }, + onError: () => { + toast.error("Failed to generate API key"); + }, + }); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + prefix: "", + expiresIn: null, + organizationId: "", + rateLimitEnabled: false, + rateLimitTimeWindow: null, + rateLimitMax: null, + remaining: null, + refillAmount: null, + refillInterval: null, + }, + }); + + const rateLimitEnabled = form.watch("rateLimitEnabled"); + + const onSubmit = async (values: FormValues) => { + createApiKey.mutate({ + name: values.name, + expiresIn: values.expiresIn || undefined, + prefix: values.prefix || undefined, + metadata: { + organizationId: values.organizationId, + }, + // Rate limiting + rateLimitEnabled: values.rateLimitEnabled, + rateLimitTimeWindow: values.rateLimitTimeWindow || undefined, + rateLimitMax: values.rateLimitMax || undefined, + // Request limiting + remaining: values.remaining || undefined, + refillAmount: values.refillAmount || undefined, + refillInterval: values.refillInterval || undefined, + }); + }; + + return ( + <> + + + + + + + Generate API Key + + Create a new API key for accessing the API. You can set an + expiration date and a custom prefix for better organization. + + +
+ + ( + + Name + + + + + + )} + /> + ( + + Prefix + + + + + + )} + /> + ( + + Expiration + + + + )} + /> + ( + + Organization + + + + )} + /> + + {/* Rate Limiting Section */} +
+

Rate Limiting

+ ( + +
+ Enable Rate Limiting + + Limit the number of requests within a time window + +
+ + + +
+ )} + /> + + {rateLimitEnabled && ( + <> + ( + + Time Window + + + The duration in which requests are counted + + + + )} + /> + ( + + Maximum Requests + + + field.onChange( + e.target.value + ? Number.parseInt(e.target.value, 10) + : null, + ) + } + /> + + + Maximum number of requests allowed within the time + window + + + + )} + /> + + )} +
+ + {/* Request Limiting Section */} +
+

Request Limiting

+ ( + + Total Request Limit + + + field.onChange( + e.target.value + ? Number.parseInt(e.target.value, 10) + : null, + ) + } + /> + + + Total number of requests allowed (leave empty for + unlimited) + + + + )} + /> + + ( + + Refill Amount + + + field.onChange( + e.target.value + ? Number.parseInt(e.target.value, 10) + : null, + ) + } + /> + + + Number of requests to add on each refill + + + + )} + /> + + ( + + Refill Interval + + + How often to refill the request limit + + + + )} + /> +
+ +
+ + +
+ + +
+
+ + + + + API Key Generated Successfully + + Please copy your API key now. You won't be able to see it again! + + +
+
+ {newApiKey} +
+
+ + +
+
+
+
+ + ); +}; diff --git a/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx b/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx new file mode 100644 index 00000000..6744f1de --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx @@ -0,0 +1,142 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { ExternalLinkIcon, KeyIcon, Trash2, Clock, Tag } from "lucide-react"; +import Link from "next/link"; +import { toast } from "sonner"; +import { formatDistanceToNow } from "date-fns"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { AddApiKey } from "./add-api-key"; +import { Badge } from "@/components/ui/badge"; + +export const ShowApiKeys = () => { + const { data, refetch } = api.user.get.useQuery(); + const { mutateAsync: deleteApiKey, isLoading: isLoadingDelete } = + api.user.deleteApiKey.useMutation(); + + return ( +
+ +
+ +
+ + + API/CLI Keys + + + Generate and manage API keys to access the API/CLI + +
+
+ + Swagger API: + + + View + + +
+
+ +
+ {data?.user.apiKeys && data.user.apiKeys.length > 0 ? ( + data.user.apiKeys.map((apiKey) => ( +
+
+
+ {apiKey.name} +
+ + + Created{" "} + {formatDistanceToNow(new Date(apiKey.createdAt))}{" "} + ago + + {apiKey.prefix && ( + + + {apiKey.prefix} + + )} + {apiKey.expiresAt && ( + + + Expires in{" "} + {formatDistanceToNow( + new Date(apiKey.expiresAt), + )}{" "} + + )} +
+
+ { + try { + await deleteApiKey({ + apiKeyId: apiKey.id, + }); + await refetch(); + toast.success("API key deleted successfully"); + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Error deleting API key", + ); + } + }} + > + + +
+
+ )) + ) : ( +
+ + + No API keys found + +
+ )} +
+ + {/* Generate new API key */} +
+ +
+
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx index 9f3430de..2c20bb81 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx @@ -23,7 +23,7 @@ import { PlusIcon, } from "lucide-react"; import Link from "next/link"; -import React, { useState } from "react"; +import { useState } from "react"; const stripePromise = loadStripe( process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!, @@ -38,8 +38,8 @@ export const calculatePrice = (count: number, isAnnual = false) => { return count * 3.5; }; export const ShowBilling = () => { - const { data: servers } = api.server.all.useQuery(undefined); - const { data: admin } = api.admin.one.useQuery(); + const { data: servers } = api.server.count.useQuery(); + const { data: admin } = api.user.get.useQuery(); const { data, isLoading } = api.stripe.getProducts.useQuery(); const { mutateAsync: createCheckoutSession } = api.stripe.createCheckoutSession.useMutation(); @@ -70,8 +70,8 @@ export const ShowBilling = () => { return isAnnual ? interval === "year" : interval === "month"; }); - const maxServers = admin?.serversQuantity ?? 1; - const percentage = ((servers?.length ?? 0) / maxServers) * 100; + const maxServers = admin?.user.serversQuantity ?? 1; + const percentage = ((servers ?? 0) / maxServers) * 100; const safePercentage = Math.min(percentage, 100); return ( @@ -98,17 +98,17 @@ export const ShowBilling = () => { Annual - {admin?.stripeSubscriptionId && ( + {admin?.user.stripeSubscriptionId && (

Servers Plan

- You have {servers?.length} server on your plan of{" "} - {admin?.serversQuantity} servers + You have {servers} server on your plan of{" "} + {admin?.user.serversQuantity} servers

- {admin && admin.serversQuantity! <= servers?.length! && ( + {admin && admin.user.serversQuantity! <= (servers ?? 0) && (
@@ -279,7 +279,7 @@ export const ShowBilling = () => { "flex flex-row items-center gap-2 mt-4", )} > - {admin?.stripeCustomerId && ( + {admin?.user.stripeCustomerId && (
- {data?.map((gitProvider, index) => { + {data?.map((gitProvider, _index) => { const isGithub = gitProvider.providerType === "github"; const isGitlab = gitProvider.providerType === "gitlab"; const isBitbucket = diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index 55c56fd7..7a3e286e 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -137,7 +137,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { const [visible, setVisible] = useState(false); const { data: isCloud } = api.settings.isCloud.useQuery(); - const { data: notification, refetch } = api.notification.one.useQuery( + const { data: notification } = api.notification.one.useQuery( { notificationId: notificationId || "", }, @@ -1060,7 +1060,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { }); } toast.success("Connection Success"); - } catch (err) { + } catch (_err) { toast.error("Error testing the provider"); } }} diff --git a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx index d65069d4..782b9241 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx @@ -56,7 +56,7 @@ export const ShowNotifications = () => { ) : (
- {data?.map((notification, index) => ( + {data?.map((notification, _index) => (
; export const Disable2FA = () => { const utils = api.useUtils(); - const { mutateAsync, isLoading } = api.auth.disable2FA.useMutation(); + const [isOpen, setIsOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const form = useForm({ + resolver: zodResolver(PasswordSchema), + defaultValues: { + password: "", + }, + }); + + const handleSubmit = async (formData: PasswordForm) => { + setIsLoading(true); + try { + const result = await authClient.twoFactor.disable({ + password: formData.password, + }); + + if (result.error) { + form.setError("password", { + message: result.error.message, + }); + toast.error(result.error.message); + return; + } + + toast.success("2FA disabled successfully"); + utils.user.get.invalidate(); + setIsOpen(false); + } catch (_error) { + form.setError("password", { + message: "Connection error. Please try again.", + }); + toast.error("Connection error. Please try again."); + } finally { + setIsLoading(false); + } + }; + return ( - + - + Are you absolutely sure? - This action cannot be undone. This will permanently delete the 2FA + This action cannot be undone. This will permanently disable + Two-Factor Authentication for your account. - - Cancel - { - await mutateAsync() - .then(() => { - utils.auth.get.invalidate(); - toast.success("2FA Disabled"); - }) - .catch(() => { - toast.error("Error disabling 2FA"); - }); - }} + + + - Confirm - - + ( + + Password + + + + + Enter your password to disable 2FA + + + + )} + /> +
+ + +
+ +
); diff --git a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx index cf5910b8..f47c8d9c 100644 --- a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx @@ -3,7 +3,6 @@ import { Dialog, DialogContent, DialogDescription, - DialogFooter, DialogHeader, DialogTitle, DialogTrigger, @@ -17,144 +16,309 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; import { InputOTP, InputOTPGroup, InputOTPSlot, } from "@/components/ui/input-otp"; +import { authClient } from "@/lib/auth-client"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, Fingerprint } from "lucide-react"; -import { useEffect } from "react"; +import { Fingerprint, QrCode } from "lucide-react"; +import QRCode from "qrcode"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; -const Enable2FASchema = z.object({ +const PasswordSchema = z.object({ + password: z.string().min(8, { + message: "Password is required", + }), +}); + +const PinSchema = z.object({ pin: z.string().min(6, { message: "Pin is required", }), }); -type Enable2FA = z.infer; +type TwoFactorSetupData = { + qrCodeUrl: string; + secret: string; + totpURI: string; +}; + +type PasswordForm = z.infer; +type PinForm = z.infer; export const Enable2FA = () => { const utils = api.useUtils(); + const [data, setData] = useState(null); + const [backupCodes, setBackupCodes] = useState([]); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [step, setStep] = useState<"password" | "verify">("password"); + const [isPasswordLoading, setIsPasswordLoading] = useState(false); - const { data } = api.auth.generate2FASecret.useQuery(undefined, { - refetchOnWindowFocus: false, + const handlePasswordSubmit = async (formData: PasswordForm) => { + setIsPasswordLoading(true); + try { + const { data: enableData } = await authClient.twoFactor.enable({ + password: formData.password, + }); + + if (!enableData) { + throw new Error("No data received from server"); + } + + if (enableData.backupCodes) { + setBackupCodes(enableData.backupCodes); + } + + if (enableData.totpURI) { + const qrCodeUrl = await QRCode.toDataURL(enableData.totpURI); + + setData({ + qrCodeUrl, + secret: enableData.totpURI.split("secret=")[1]?.split("&")[0] || "", + totpURI: enableData.totpURI, + }); + + setStep("verify"); + toast.success("Scan the QR code with your authenticator app"); + } else { + throw new Error("No TOTP URI received from server"); + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error setting up 2FA", + ); + passwordForm.setError("password", { + message: "Error verifying password", + }); + } finally { + setIsPasswordLoading(false); + } + }; + + const handleVerifySubmit = async (formData: PinForm) => { + try { + const result = await authClient.twoFactor.verifyTotp({ + code: formData.pin, + }); + + if (result.error) { + if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") { + pinForm.setError("pin", { + message: "Invalid code. Please try again.", + }); + toast.error("Invalid verification code"); + return; + } + + throw result.error; + } + + if (!result.data) { + throw new Error("No response received from server"); + } + + toast.success("2FA configured successfully"); + utils.user.get.invalidate(); + setIsDialogOpen(false); + } catch (error) { + if (error instanceof Error) { + const errorMessage = + error.message === "Failed to fetch" + ? "Connection error. Please check your internet connection." + : error.message; + + pinForm.setError("pin", { + message: errorMessage, + }); + toast.error(errorMessage); + } else { + pinForm.setError("pin", { + message: "Error verifying code", + }); + toast.error("Error verifying 2FA code"); + } + } + }; + + const passwordForm = useForm({ + resolver: zodResolver(PasswordSchema), + defaultValues: { + password: "", + }, }); - const { mutateAsync, isLoading, error, isError } = - api.auth.verify2FASetup.useMutation(); - - const form = useForm({ + const pinForm = useForm({ + resolver: zodResolver(PinSchema), defaultValues: { pin: "", }, - resolver: zodResolver(Enable2FASchema), }); useEffect(() => { - form.reset({ - pin: "", - }); - }, [form, form.reset, form.formState.isSubmitSuccessful]); + if (!isDialogOpen) { + setStep("password"); + setData(null); + setBackupCodes([]); + passwordForm.reset(); + pinForm.reset(); + } + }, [isDialogOpen, passwordForm, pinForm]); - const onSubmit = async (formData: Enable2FA) => { - await mutateAsync({ - pin: formData.pin, - secret: data?.secret || "", - }) - .then(async () => { - toast.success("2FA Verified"); - utils.auth.get.invalidate(); - }) - .catch(() => { - toast.error("Error verifying the 2FA"); - }); - }; return ( - + - + 2FA Setup - Add a 2FA to your account + + {step === "password" + ? "Enter your password to begin 2FA setup" + : "Scan the QR code and verify with your authenticator app"} + - {isError && ( -
- - - {error?.message} - -
- )} -
- -
- - {data?.qrCodeUrl ? "Scan the QR code to add 2FA" : ""} - - qrCode -
- - {data?.secret ? `Secret: ${data?.secret}` : ""} - -
-
- ( - - Pin - - - - - - - - - - - - - - Please enter the 6 digits code provided by your - authenticator app. - - - - )} - /> - - - - - - + ( + + Password + + + + + Enter your password to enable 2FA + + + + )} + /> + + + + ) : ( +
+ +
+ {data?.qrCodeUrl ? ( + <> +
+ + + Scan this QR code with your authenticator app + + 2FA QR Code +
+ + Can't scan the QR code? + + + {data.secret} + +
+
+ + {backupCodes && backupCodes.length > 0 && ( +
+

Backup Codes

+
+ {backupCodes.map((code, index) => ( + + {code} + + ))} +
+

+ Save these backup codes in a secure place. You can use + them to access your account if you lose access to your + authenticator device. +

+
+ )} + + ) : ( +
+ +
+ )} +
+ + ( + + Verification Code + + + + + + + + + + + + + + Enter the 6-digit code from your authenticator app + + + + )} + /> + + + + + )}
); diff --git a/apps/dokploy/components/dashboard/settings/profile/generate-token.tsx b/apps/dokploy/components/dashboard/settings/profile/generate-token.tsx deleted file mode 100644 index 66486c33..00000000 --- a/apps/dokploy/components/dashboard/settings/profile/generate-token.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Label } from "@/components/ui/label"; -import { api } from "@/utils/api"; -import { ExternalLinkIcon } from "lucide-react"; -import Link from "next/link"; -import { toast } from "sonner"; - -export const GenerateToken = () => { - const { data, refetch } = api.auth.get.useQuery(); - - const { mutateAsync: generateToken, isLoading: isLoadingToken } = - api.auth.generateToken.useMutation(); - - return ( -
- -
- -
- API/CLI - - Generate a token to access the API/CLI - -
-
- - Swagger API: - - - View - - -
-
- -
-
-
- - -
-
- -
-
-
-
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 9ae140c6..32179378 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -54,13 +54,14 @@ const randomImages = [ ]; export const ProfileForm = () => { - const { data, refetch, isLoading } = api.auth.get.useQuery(); + const _utils = api.useUtils(); + const { data, refetch, isLoading } = api.user.get.useQuery(); const { mutateAsync, isLoading: isUpdating, isError, error, - } = api.auth.update.useMutation(); + } = api.user.update.useMutation(); const { t } = useTranslation("settings"); const [gravatarHash, setGravatarHash] = useState(null); @@ -73,9 +74,9 @@ export const ProfileForm = () => { const form = useForm({ defaultValues: { - email: data?.email || "", + email: data?.user?.email || "", password: "", - image: data?.image || "", + image: data?.user?.image || "", currentPassword: "", }, resolver: zodResolver(profileSchema), @@ -84,14 +85,14 @@ export const ProfileForm = () => { useEffect(() => { if (data) { form.reset({ - email: data?.email || "", + email: data?.user?.email || "", password: "", - image: data?.image || "", + image: data?.user?.image || "", currentPassword: "", }); - if (data.email) { - generateSHA256Hash(data.email).then((hash) => { + if (data.user.email) { + generateSHA256Hash(data.user.email).then((hash) => { setGravatarHash(hash); }); } @@ -102,9 +103,9 @@ export const ProfileForm = () => { const onSubmit = async (values: Profile) => { await mutateAsync({ email: values.email.toLowerCase(), - password: values.password, + password: values.password || undefined, image: values.image, - currentPassword: values.currentPassword, + currentPassword: values.currentPassword || undefined, }) .then(async () => { await refetch(); @@ -130,7 +131,7 @@ export const ProfileForm = () => { {t("settings.profile.description")}
- {!data?.is2FAEnabled ? : } + {!data?.user.twoFactorEnabled ? : } diff --git a/apps/dokploy/components/dashboard/settings/profile/remove-self-account.tsx b/apps/dokploy/components/dashboard/settings/profile/remove-self-account.tsx deleted file mode 100644 index cff9db54..00000000 --- a/apps/dokploy/components/dashboard/settings/profile/remove-self-account.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { DialogAction } from "@/components/shared/dialog-action"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useTranslation } from "next-i18next"; -import { useRouter } from "next/router"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const profileSchema = z.object({ - password: z.string().min(1, { - message: "Password is required", - }), -}); - -type Profile = z.infer; - -export const RemoveSelfAccount = () => { - const { data } = api.auth.get.useQuery(); - const { mutateAsync, isLoading, error, isError } = - api.auth.removeSelfAccount.useMutation(); - const { t } = useTranslation("settings"); - const router = useRouter(); - - const form = useForm({ - defaultValues: { - password: "", - }, - resolver: zodResolver(profileSchema), - }); - - useEffect(() => { - if (data) { - form.reset({ - password: "", - }); - } - form.reset(); - }, [form, form.reset, data]); - - const onSubmit = async (values: Profile) => { - await mutateAsync({ - password: values.password, - }) - .then(async () => { - toast.success("Profile Deleted"); - router.push("/"); - }) - .catch(() => {}); - }; - - return ( -
- -
- -
- Remove Self Account - - If you want to remove your account, you can do it here - -
-
- - {isError && {error?.message}} - -
- e.preventDefault()} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - } - }} - className="grid gap-4" - > -
- ( - - {t("settings.profile.password")} - - - - - - )} - /> -
-
- -
- form.handleSubmit(onSubmit)()} - > - - -
-
-
-
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-dokploy-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-dokploy-actions.tsx index 3a1af206..f57dad3c 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/show-dokploy-actions.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-dokploy-actions.tsx @@ -1,5 +1,4 @@ import { Button } from "@/components/ui/button"; -import React from "react"; import { UpdateServerIp } from "@/components/dashboard/settings/web-server/update-server-ip"; import { diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-storage-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-storage-actions.tsx index c45c0c8b..3492ba7c 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/show-storage-actions.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-storage-actions.tsx @@ -1,5 +1,4 @@ import { Button } from "@/components/ui/button"; -import React from "react"; import { DropdownMenu, @@ -27,7 +26,7 @@ export const ShowStorageActions = ({ serverId }: Props) => { isLoading: cleanDockerBuilderIsLoading, } = api.settings.cleanDockerBuilder.useMutation(); - const { mutateAsync: cleanMonitoring, isLoading: cleanMonitoringIsLoading } = + const { mutateAsync: cleanMonitoring } = api.settings.cleanMonitoring.useMutation(); const { mutateAsync: cleanUnusedImages, diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx index 17a6ae75..0968931d 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx @@ -1,14 +1,4 @@ import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import React from "react"; import { DropdownMenu, @@ -20,10 +10,8 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { api } from "@/utils/api"; -import { toast } from "sonner"; - -import { cn } from "@/lib/utils"; import { useTranslation } from "next-i18next"; +import { toast } from "sonner"; import { EditTraefikEnv } from "../../web-server/edit-traefik-env"; import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports"; import { ShowModalLogs } from "../../web-server/show-modal-logs"; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/toggle-docker-cleanup.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-docker-cleanup.tsx index 78ad1236..12e27942 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/toggle-docker-cleanup.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-docker-cleanup.tsx @@ -7,7 +7,7 @@ interface Props { serverId?: string; } export const ToggleDockerCleanup = ({ serverId }: Props) => { - const { data, refetch } = api.admin.one.useQuery(undefined, { + const { data, refetch } = api.user.get.useQuery(undefined, { enabled: !serverId, }); @@ -20,7 +20,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => { }, ); - const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup; + const enabled = data?.user.enableDockerCleanup || server?.enableDockerCleanup; const { mutateAsync } = api.settings.updateDockerCleanup.useMutation(); @@ -36,7 +36,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => { await refetch(); } toast.success("Docker Cleanup updated"); - } catch (error) { + } catch (_error) { toast.error("Docker Cleanup Error"); } }; diff --git a/apps/dokploy/components/dashboard/settings/servers/edit-script.tsx b/apps/dokploy/components/dashboard/settings/servers/edit-script.tsx index 0a22220e..6225ee77 100644 --- a/apps/dokploy/components/dashboard/settings/servers/edit-script.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/edit-script.tsx @@ -82,7 +82,7 @@ export const EditScript = ({ serverId }: Props) => { command: formData.command || "", serverId, }) - .then((data) => { + .then((_data) => { toast.success("Script modified successfully"); }) .catch(() => { diff --git a/apps/dokploy/components/dashboard/settings/servers/gpu-support.tsx b/apps/dokploy/components/dashboard/settings/servers/gpu-support.tsx index 3cda7e80..c24440a6 100644 --- a/apps/dokploy/components/dashboard/settings/servers/gpu-support.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/gpu-support.tsx @@ -9,7 +9,6 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { TRPCClientError } from "@trpc/client"; import { CheckCircle2, Cpu, Loader2, RefreshCw, XCircle } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; @@ -57,7 +56,7 @@ export function GPUSupport({ serverId }: GPUSupportProps) { try { await utils.settings.checkGPUStatus.invalidate({ serverId }); await refetch(); - } catch (error) { + } catch (_error) { toast.error("Failed to refresh GPU status"); } finally { setIsRefreshing(false); @@ -75,7 +74,7 @@ export function GPUSupport({ serverId }: GPUSupportProps) { try { await setupGPU.mutateAsync({ serverId }); - } catch (error) { + } catch (_error) { // Error handling is done in mutation's onError } }; diff --git a/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx index be71d836..97994145 100644 --- a/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx @@ -118,7 +118,7 @@ export const HandleServers = ({ serverId }: Props) => { sshKeyId: data.sshKeyId || "", serverId: serverId || "", }) - .then(async (data) => { + .then(async (_data) => { await utils.server.all.invalidate(); refetchServer(); toast.success(serverId ? "Server Updated" : "Server Created"); diff --git a/apps/dokploy/components/dashboard/settings/servers/security-audit.tsx b/apps/dokploy/components/dashboard/settings/servers/security-audit.tsx index 475f2b8f..8cce306a 100644 --- a/apps/dokploy/components/dashboard/settings/servers/security-audit.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/security-audit.tsx @@ -25,7 +25,7 @@ export const SecurityAudit = ({ serverId }: Props) => { enabled: !!serverId, }, ); - const utils = api.useUtils(); + const _utils = api.useUtils(); return (
@@ -145,15 +145,6 @@ export const SecurityAudit = ({ serverId }: Props) => { : "Enabled (Password Authentication should be disabled)" } /> - ; export const SetupMonitoring = ({ serverId }: Props) => { - const { data, isLoading } = serverId + const { data } = serverId ? api.server.one.useQuery( { serverId: serverId || "", @@ -91,7 +89,7 @@ export const SetupMonitoring = ({ serverId }: Props) => { enabled: !!serverId, }, ) - : api.admin.one.useQuery(); + : api.user.getServerMetrics.useQuery(); const url = useUrl(); diff --git a/apps/dokploy/components/dashboard/settings/servers/show-docker-containers-modal.tsx b/apps/dokploy/components/dashboard/settings/servers/show-docker-containers-modal.tsx index 1cdab23b..ad82085c 100644 --- a/apps/dokploy/components/dashboard/settings/servers/show-docker-containers-modal.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/show-docker-containers-modal.tsx @@ -1,10 +1,4 @@ -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { useState } from "react"; import { ShowContainers } from "../../docker/show/show-containers"; diff --git a/apps/dokploy/components/dashboard/settings/servers/show-swarm-overview-modal.tsx b/apps/dokploy/components/dashboard/settings/servers/show-swarm-overview-modal.tsx index a47005d0..b8631184 100644 --- a/apps/dokploy/components/dashboard/settings/servers/show-swarm-overview-modal.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/show-swarm-overview-modal.tsx @@ -1,12 +1,5 @@ -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; -import { ContainerIcon } from "lucide-react"; import { useState } from "react"; import SwarmMonitorCard from "../../swarm/monitoring-card"; diff --git a/apps/dokploy/components/dashboard/settings/servers/validate-server.tsx b/apps/dokploy/components/dashboard/settings/servers/validate-server.tsx index db4f17b7..0632b97c 100644 --- a/apps/dokploy/components/dashboard/settings/servers/validate-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/validate-server.tsx @@ -25,7 +25,7 @@ export const ValidateServer = ({ serverId }: Props) => { enabled: !!serverId, }, ); - const utils = api.useUtils(); + const _utils = api.useUtils(); return (
diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-server.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-server.tsx index a025ad37..24d01553 100644 --- a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-server.tsx @@ -52,10 +52,10 @@ interface Props { export const CreateServer = ({ stepper }: Props) => { const { data: sshKeys } = api.sshKey.all.useQuery(); - const [isOpen, setIsOpen] = useState(false); + const [isOpen, _setIsOpen] = useState(false); const { data: canCreateMoreServers, refetch } = api.stripe.canCreateMoreServers.useQuery(); - const { mutateAsync, error, isError } = api.server.create.useMutation(); + const { mutateAsync } = api.server.create.useMutation(); const cloudSSHKey = sshKeys?.find( (sshKey) => sshKey.name === "dokploy-cloud-ssh-key", ); @@ -96,7 +96,7 @@ export const CreateServer = ({ stepper }: Props) => { username: data.username || "root", sshKeyId: data.sshKeyId || "", }) - .then(async (data) => { + .then(async (_data) => { toast.success("Server Created"); stepper.next(); }) diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx index 37f8e017..b1a3f2d0 100644 --- a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx @@ -35,6 +35,7 @@ export const CreateSSHKey = () => { description: "Used on Dokploy Cloud", privateKey: keys.privateKey, publicKey: keys.publicKey, + organizationId: "", }); await refetch(); } catch (error) { diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/verify.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/verify.tsx index fe8c36c2..f7c2a987 100644 --- a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/verify.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/verify.tsx @@ -37,15 +37,6 @@ export const Verify = () => { ); const [isRefreshing, setIsRefreshing] = useState(false); - const { data: server } = api.server.one.useQuery( - { - serverId, - }, - { - enabled: !!serverId, - }, - ); - return (
diff --git a/apps/dokploy/components/dashboard/settings/ssh-keys/handle-ssh-keys.tsx b/apps/dokploy/components/dashboard/settings/ssh-keys/handle-ssh-keys.tsx index fc48134a..1a8fe918 100644 --- a/apps/dokploy/components/dashboard/settings/ssh-keys/handle-ssh-keys.tsx +++ b/apps/dokploy/components/dashboard/settings/ssh-keys/handle-ssh-keys.tsx @@ -78,6 +78,7 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => { const onSubmit = async (data: SSHKey) => { await mutateAsync({ ...data, + organizationId: "", sshKeyId: sshKeyId || "", }) .then(async () => { diff --git a/apps/dokploy/components/dashboard/settings/users/add-user.tsx b/apps/dokploy/components/dashboard/settings/users/add-invitation.tsx similarity index 52% rename from apps/dokploy/components/dashboard/settings/users/add-user.tsx rename to apps/dokploy/components/dashboard/settings/users/add-invitation.tsx index 8fb6de27..d05409fb 100644 --- a/apps/dokploy/components/dashboard/settings/users/add-user.tsx +++ b/apps/dokploy/components/dashboard/settings/users/add-invitation.tsx @@ -19,6 +19,14 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { authClient } from "@/lib/auth-client"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { PlusIcon } from "lucide-react"; @@ -27,62 +35,70 @@ import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; -const addUser = z.object({ +const addInvitation = z.object({ email: z .string() .min(1, "Email is required") .email({ message: "Invalid email" }), + role: z.enum(["member", "admin"]), }); -type AddUser = z.infer; +type AddInvitation = z.infer; -export const AddUser = () => { +export const AddInvitation = () => { const [open, setOpen] = useState(false); const utils = api.useUtils(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const { data: activeOrganization } = authClient.useActiveOrganization(); - const { mutateAsync, isError, error, isLoading } = - api.admin.createUserInvitation.useMutation(); - - const form = useForm({ + const form = useForm({ defaultValues: { email: "", + role: "member", }, - resolver: zodResolver(addUser), + resolver: zodResolver(addInvitation), }); useEffect(() => { form.reset(); }, [form, form.formState.isSubmitSuccessful, form.reset]); - const onSubmit = async (data: AddUser) => { - await mutateAsync({ + const onSubmit = async (data: AddInvitation) => { + setIsLoading(true); + const result = await authClient.organization.inviteMember({ email: data.email.toLowerCase(), - }) - .then(async () => { - toast.success("Invitation created"); - await utils.user.all.invalidate(); - setOpen(false); - }) - .catch(() => { - toast.error("Error creating the invitation"); - }); + role: data.role, + organizationId: activeOrganization?.id, + }); + + if (result.error) { + setError(result.error.message || ""); + } else { + toast.success("Invitation created"); + setError(null); + setOpen(false); + } + + utils.organization.allInvitations.invalidate(); + setIsLoading(false); }; return ( - Add User + Add Invitation Invite a new user - {isError && {error?.message}} + {error && {error}}
@@ -104,10 +120,39 @@ export const AddUser = () => { ); }} /> + + { + return ( + + Role + + + Select the role for the new user + + + + ); + }} + /> + +
+ )} +
+ ))} + {(user?.role === "owner" || isCloud) && ( + <> + + + + )} + + + -
-

Dokploy

-

- {dokployVersion} -

-
- + {/* Notification Bell */} + + + + + + + Pending Invitations +
+ {invitations && invitations.length > 0 ? ( + invitations.map((invitation) => ( +
+ e.preventDefault()} + > +
+ {invitation?.organization?.name} +
+
+ Expires:{" "} + {new Date(invitation.expiresAt).toLocaleString()} +
+
+ Role: {invitation.role} +
+
+ { + const { error } = + await authClient.organization.acceptInvitation({ + invitationId: invitation.id, + }); + + if (error) { + toast.error( + error.message || "Error accepting invitation", + ); + } else { + toast.success("Invitation accepted successfully"); + await refetchInvitations(); + await refetch(); + } + }} + > + + +
+ )) + ) : ( + + No pending invitations + + )} +
+
+
+
+ + )} + ); } @@ -518,6 +750,7 @@ export default function Page({ children }: Props) { const [defaultOpen, setDefaultOpen] = useState( undefined, ); + const [isLoaded, setIsLoaded] = useState(false); useEffect(() => { const cookieValue = document.cookie @@ -526,38 +759,31 @@ export default function Page({ children }: Props) { ?.split("=")[1]; setDefaultOpen(cookieValue === undefined ? true : cookieValue === "true"); + setIsLoaded(true); }, []); const router = useRouter(); const pathname = usePathname(); - const currentPath = router.pathname; - const { data: auth } = api.auth.get.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: auth?.id || "", - }, - { - enabled: !!auth?.id && auth?.rol === "user", - }, - ); + const _currentPath = router.pathname; + const { data: auth } = api.user.get.useQuery(); const includesProjects = pathname?.includes("/dashboard/project"); - const { data: isCloud, isLoading } = api.settings.isCloud.useQuery(); + const { data: isCloud } = api.settings.isCloud.useQuery(); const { home: filteredHome, settings: filteredSettings, help, - } = createMenuForAuthUser({ auth, user, isCloud: !!isCloud }); + } = createMenuForAuthUser({ auth, isCloud: !!isCloud }); const activeItem = findActiveNavItem( [...filteredHome, ...filteredSettings], pathname, ); - // const showProjectsButton = - // currentPath === "/dashboard/projects" && - // (auth?.rol === "admin" || user?.canCreateProjects); + if (!isLoaded) { + return
; // Placeholder mientras se carga + } return ( - - - + > */} + + {/* */} @@ -783,7 +1009,7 @@ export default function Page({ children }: Props) { ))} - {!isCloud && auth?.rol === "admin" && ( + {!isCloud && auth?.role === "owner" && ( diff --git a/apps/dokploy/components/layouts/update-server.tsx b/apps/dokploy/components/layouts/update-server.tsx index fa748f84..5d797885 100644 --- a/apps/dokploy/components/layouts/update-server.tsx +++ b/apps/dokploy/components/layouts/update-server.tsx @@ -11,7 +11,7 @@ export const UpdateServerButton = () => { latestVersion: null, updateAvailable: false, }); - const router = useRouter(); + const _router = useRouter(); const { data: isCloud } = api.settings.isCloud.useQuery(); const { mutateAsync: getUpdateData } = api.settings.getUpdateData.useMutation(); diff --git a/apps/dokploy/components/layouts/user-nav.tsx b/apps/dokploy/components/layouts/user-nav.tsx index 67c5cbfd..4a9624de 100644 --- a/apps/dokploy/components/layouts/user-nav.tsx +++ b/apps/dokploy/components/layouts/user-nav.tsx @@ -15,32 +15,24 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { authClient } from "@/lib/auth-client"; import { Languages } from "@/lib/languages"; import { api } from "@/utils/api"; import useLocale from "@/utils/hooks/use-locale"; import { ChevronsUpDown } from "lucide-react"; -import { useTranslation } from "next-i18next"; import { useRouter } from "next/router"; -import { useEffect, useRef, useState } from "react"; import { ModeToggle } from "../ui/modeToggle"; import { SidebarMenuButton } from "../ui/sidebar"; -const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7; +const _AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7; export const UserNav = () => { const router = useRouter(); - const { data } = api.auth.get.useQuery(); + const { data } = api.user.get.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: data?.id || "", - }, - { - enabled: !!data?.id && data?.rol === "user", - }, - ); + const { locale, setLocale } = useLocale(); - const { mutateAsync } = api.auth.logout.useMutation(); + // const { mutateAsync } = api.auth.logout.useMutation(); return ( @@ -50,12 +42,15 @@ export const UserNav = () => { className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" > - + CN
Account - {data?.email} + {data?.user?.email}
@@ -70,7 +65,7 @@ export const UserNav = () => { My Account - {data?.email} + {data?.user?.email} @@ -95,7 +90,7 @@ export const UserNav = () => { > Monitoring - {(data?.rol === "admin" || user?.canAccessToTraefikFiles) && ( + {(data?.role === "owner" || data?.canAccessToTraefikFiles) && ( { @@ -105,7 +100,7 @@ export const UserNav = () => { Traefik )} - {(data?.rol === "admin" || user?.canAccessToDocker) && ( + {(data?.role === "owner" || data?.canAccessToDocker) && ( { @@ -118,7 +113,7 @@ export const UserNav = () => { )} - {data?.rol === "admin" && ( + {data?.role === "owner" && ( { @@ -139,7 +134,7 @@ export const UserNav = () => { > Profile - {data?.rol === "admin" && ( + {data?.role === "owner" && ( { @@ -150,7 +145,7 @@ export const UserNav = () => { )} - {data?.rol === "admin" && ( + {data?.role === "owner" && ( { @@ -163,7 +158,7 @@ export const UserNav = () => { )} - {isCloud && data?.rol === "admin" && ( + {isCloud && data?.role === "owner" && ( { @@ -178,9 +173,12 @@ export const UserNav = () => { { - await mutateAsync().then(() => { + await authClient.signOut().then(() => { router.push("/"); }); + // await mutateAsync().then(() => { + // router.push("/"); + // }); }} > Log out diff --git a/apps/dokploy/components/shared/breadcrumb-sidebar.tsx b/apps/dokploy/components/shared/breadcrumb-sidebar.tsx index 60730c96..74e9fdf6 100644 --- a/apps/dokploy/components/shared/breadcrumb-sidebar.tsx +++ b/apps/dokploy/components/shared/breadcrumb-sidebar.tsx @@ -26,7 +26,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => { - {list.map((item, index) => ( + {list.map((item, _index) => ( diff --git a/apps/dokploy/components/shared/dialog-action.tsx b/apps/dokploy/components/shared/dialog-action.tsx index 3724242d..444440a2 100644 --- a/apps/dokploy/components/shared/dialog-action.tsx +++ b/apps/dokploy/components/shared/dialog-action.tsx @@ -9,7 +9,6 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Button } from "../ui/button"; interface Props { title?: string | React.ReactNode; diff --git a/apps/dokploy/components/shared/drawer-logs.tsx b/apps/dokploy/components/shared/drawer-logs.tsx index f5a56cd6..d8d1affb 100644 --- a/apps/dokploy/components/shared/drawer-logs.tsx +++ b/apps/dokploy/components/shared/drawer-logs.tsx @@ -1,6 +1,3 @@ -import { DialogAction } from "@/components/shared/dialog-action"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Sheet, SheetContent, @@ -8,10 +5,8 @@ import { SheetHeader, SheetTitle, } from "@/components/ui/sheet"; -import { api } from "@/utils/api"; -import { Ban, CheckCircle2, Loader2, RefreshCcw, Terminal } from "lucide-react"; -import React, { useState, useEffect, useRef } from "react"; -import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; import { TerminalLine } from "../dashboard/docker/logs/terminal-line"; import type { LogLine } from "../dashboard/docker/logs/utils"; @@ -48,7 +43,7 @@ export const DrawerLogs = ({ isOpen, onClose, filteredLogs }: Props) => { return ( { + onOpenChange={(_open) => { onClose(); }} > diff --git a/apps/dokploy/components/shared/logo.tsx b/apps/dokploy/components/shared/logo.tsx index 5d192cfd..a1c3acb7 100644 --- a/apps/dokploy/components/shared/logo.tsx +++ b/apps/dokploy/components/shared/logo.tsx @@ -1,10 +1,21 @@ -import React from "react"; +import { cn } from "@/lib/utils"; interface Props { className?: string; + logoUrl?: string; } -export const Logo = ({ className = "size-14" }: Props) => { +export const Logo = ({ className = "size-14", logoUrl }: Props) => { + if (logoUrl) { + return ( + Organization Logo + ); + } + return ( ( return ids; }, [data, initialSlelectedItemId]); - const { ref: refRoot, width, height } = useResizeObserver(); + const { ref: refRoot } = useResizeObserver(); return (
diff --git a/apps/dokploy/components/ui/modeToggle.tsx b/apps/dokploy/components/ui/modeToggle.tsx index 7965a339..9b6ba27b 100644 --- a/apps/dokploy/components/ui/modeToggle.tsx +++ b/apps/dokploy/components/ui/modeToggle.tsx @@ -3,7 +3,6 @@ import { Button } from "@/components/ui/button"; import { Moon, Sun } from "lucide-react"; import { useTheme } from "next-themes"; -import * as React from "react"; export function ModeToggle() { const { theme, setTheme } = useTheme(); diff --git a/apps/dokploy/drizzle/0066_yielding_echo.sql b/apps/dokploy/drizzle/0066_yielding_echo.sql new file mode 100644 index 00000000..bb5c2511 --- /dev/null +++ b/apps/dokploy/drizzle/0066_yielding_echo.sql @@ -0,0 +1,688 @@ +CREATE TABLE "user_temp" ( + "id" text PRIMARY KEY NOT NULL, + "name" text DEFAULT '' NOT NULL, + "isRegistered" boolean DEFAULT false NOT NULL, + "expirationDate" text NOT NULL, + "createdAt" text NOT NULL, + "two_factor_enabled" boolean DEFAULT false NOT NULL, + "email" text NOT NULL, + "email_verified" boolean NOT NULL, + "image" text, + "banned" boolean, + "ban_reason" text, + "ban_expires" timestamp, + "updated_at" timestamp NOT NULL, + "serverIp" text, + "certificateType" "certificateType" DEFAULT 'none' NOT NULL, + "host" text, + "letsEncryptEmail" text, + "sshPrivateKey" text, + "enableDockerCleanup" boolean DEFAULT false NOT NULL, + "enableLogRotation" boolean DEFAULT false NOT NULL, + "enablePaidFeatures" boolean DEFAULT false NOT NULL, + "metricsConfig" jsonb DEFAULT '{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb NOT NULL, + "cleanupCacheApplications" boolean DEFAULT false NOT NULL, + "cleanupCacheOnPreviews" boolean DEFAULT false NOT NULL, + "cleanupCacheOnCompose" boolean DEFAULT false NOT NULL, + "stripeCustomerId" text, + "stripeSubscriptionId" text, + "serversQuantity" integer DEFAULT 0 NOT NULL, + CONSTRAINT "user_temp_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE "session_temp" ( + "id" text PRIMARY KEY NOT NULL, + "expires_at" timestamp NOT NULL, + "token" text NOT NULL, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + "ip_address" text, + "user_agent" text, + "user_id" text NOT NULL, + "impersonated_by" text, + "active_organization_id" text, + CONSTRAINT "session_temp_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "account" ( + "id" text PRIMARY KEY NOT NULL, + "account_id" text NOT NULL, + "provider_id" text NOT NULL, + "user_id" text NOT NULL, + "access_token" text, + "refresh_token" text, + "id_token" text, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "scope" text, + "password" text, + "is2FAEnabled" boolean DEFAULT false NOT NULL, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + "resetPasswordToken" text, + "resetPasswordExpiresAt" text, + "confirmationToken" text, + "confirmationExpiresAt" text +); +--> statement-breakpoint +CREATE TABLE "invitation" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "email" text NOT NULL, + "role" text, + "status" text NOT NULL, + "expires_at" timestamp NOT NULL, + "inviter_id" text NOT NULL, + "team_id" text +); +--> statement-breakpoint +CREATE TABLE "member" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "user_id" text NOT NULL, + "role" text NOT NULL, + "created_at" timestamp NOT NULL, + "canCreateProjects" boolean DEFAULT false NOT NULL, + "canAccessToSSHKeys" boolean DEFAULT false NOT NULL, + "canCreateServices" boolean DEFAULT false NOT NULL, + "canDeleteProjects" boolean DEFAULT false NOT NULL, + "canDeleteServices" boolean DEFAULT false NOT NULL, + "canAccessToDocker" boolean DEFAULT false NOT NULL, + "canAccessToAPI" boolean DEFAULT false NOT NULL, + "canAccessToGitProviders" boolean DEFAULT false NOT NULL, + "canAccessToTraefikFiles" boolean DEFAULT false NOT NULL, + "accesedProjects" text[] DEFAULT ARRAY[]::text[] NOT NULL, + "accesedServices" text[] DEFAULT ARRAY[]::text[] NOT NULL, + "team_id" text +); +--> statement-breakpoint +CREATE TABLE "organization" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "slug" text, + "logo" text, + "created_at" timestamp NOT NULL, + "metadata" text, + "owner_id" text NOT NULL, + CONSTRAINT "organization_slug_unique" UNIQUE("slug") +); +--> statement-breakpoint +CREATE TABLE "verification" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp, + "updated_at" timestamp +); + +CREATE TABLE "two_factor" ( + "id" text PRIMARY KEY NOT NULL, + "secret" text NOT NULL, + "backup_codes" text NOT NULL, + "user_id" text NOT NULL +); + +CREATE TABLE "apikey" ( + "id" text PRIMARY KEY NOT NULL, + "name" text, + "start" text, + "prefix" text, + "key" text NOT NULL, + "user_id" text NOT NULL, + "refill_interval" integer, + "refill_amount" integer, + "last_refill_at" timestamp, + "enabled" boolean, + "rate_limit_enabled" boolean, + "rate_limit_time_window" integer, + "rate_limit_max" integer, + "request_count" integer, + "remaining" integer, + "last_request" timestamp, + "expires_at" timestamp, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + "permissions" text, + "metadata" text +); +--> statement-breakpoint +ALTER TABLE "certificate" ALTER COLUMN "adminId" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "notification" ALTER COLUMN "adminId" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "ssh-key" ALTER COLUMN "adminId" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "git_provider" ALTER COLUMN "adminId" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "session_temp" ADD CONSTRAINT "session_temp_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_temp_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "organization" ADD CONSTRAINT "organization_owner_id_user_temp_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action; +ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "apikey" ADD CONSTRAINT "apikey_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint + + +-- Data Migration + +-- Custom SQL migration file, put your code below! -- + +WITH inserted_users AS ( + -- Insertar usuarios desde admins + INSERT INTO user_temp ( + id, + email, + "email_verified", + "updated_at", + "serverIp", + image, + "certificateType", + host, + "letsEncryptEmail", + "sshPrivateKey", + "enableDockerCleanup", + "enableLogRotation", + "enablePaidFeatures", + "metricsConfig", + "cleanupCacheApplications", + "cleanupCacheOnPreviews", + "cleanupCacheOnCompose", + "stripeCustomerId", + "stripeSubscriptionId", + "serversQuantity", + "expirationDate", + "createdAt", + "isRegistered" + ) + SELECT + a."adminId", + auth.email, + true, + CURRENT_TIMESTAMP, + a."serverIp", + auth.image, + a."certificateType", + a.host, + a."letsEncryptEmail", + a."sshPrivateKey", + a."enableDockerCleanup", + a."enableLogRotation", + a."enablePaidFeatures", + a."metricsConfig", + a."cleanupCacheApplications", + a."cleanupCacheOnPreviews", + a."cleanupCacheOnCompose", + a."stripeCustomerId", + a."stripeSubscriptionId", + a."serversQuantity", + NOW() + INTERVAL '1 year', + NOW(), + true + FROM admin a + JOIN auth ON auth.id = a."authId" + RETURNING * +), +inserted_accounts AS ( + -- Insertar cuentas para los admins + INSERT INTO account ( + id, + "account_id", + "provider_id", + "user_id", + password, + "created_at", + "updated_at" + ) + SELECT + gen_random_uuid(), + gen_random_uuid(), + 'credential', + a."adminId", + auth.password, + NOW(), + NOW() + FROM admin a + JOIN auth ON auth.id = a."authId" + RETURNING * +), +inserted_orgs AS ( + -- Crear organizaciones para cada admin + INSERT INTO organization ( + id, + name, + slug, + "owner_id", + "created_at" + ) + SELECT + gen_random_uuid(), + 'My Organization', + -- Generamos un slug único usando una función de hash + encode(sha256((a."adminId" || CURRENT_TIMESTAMP)::bytea), 'hex'), + a."adminId", + NOW() + FROM admin a + RETURNING * +), +inserted_members AS ( + -- Insertar usuarios miembros + INSERT INTO user_temp ( + id, + email, + "email_verified", + "updated_at", + image, + "createdAt", + "expirationDate", + "isRegistered" + ) + SELECT + u."userId", + auth.email, + true, + CURRENT_TIMESTAMP, + auth.image, + NOW(), + NOW() + INTERVAL '1 year', + COALESCE(u."isRegistered", false) + FROM "user" u + JOIN admin a ON u."adminId" = a."adminId" + JOIN auth ON auth.id = u."authId" + RETURNING * +), +inserted_member_accounts AS ( + -- Insertar cuentas para los usuarios miembros + INSERT INTO account ( + id, + "account_id", + "provider_id", + "user_id", + password, + "created_at", + "updated_at" + ) + SELECT + gen_random_uuid(), + gen_random_uuid(), + 'credential', + u."userId", + auth.password, + NOW(), + NOW() + FROM "user" u + JOIN admin a ON u."adminId" = a."adminId" + JOIN auth ON auth.id = u."authId" + RETURNING * +), +inserted_admin_members AS ( + -- Insertar miembros en las organizaciones (admins como owners) + INSERT INTO member ( + id, + "organization_id", + "user_id", + role, + "created_at", + "canAccessToAPI", + "canAccessToDocker", + "canAccessToGitProviders", + "canAccessToSSHKeys", + "canAccessToTraefikFiles", + "canCreateProjects", + "canCreateServices", + "canDeleteProjects", + "canDeleteServices", + "accesedProjects", + "accesedServices" + ) + SELECT + gen_random_uuid(), + o.id, + a."adminId", + 'owner', + NOW(), + true, -- Los admins tienen todos los permisos por defecto + true, + true, + true, + true, + true, + true, + true, + true, + '{}', + '{}' + FROM admin a + JOIN inserted_orgs o ON o."owner_id" = a."adminId" + JOIN auth ON auth.id = a."authId" + RETURNING * +) +-- Insertar miembros regulares en las organizaciones +INSERT INTO member ( + id, + "organization_id", + "user_id", + role, + "created_at", + "canAccessToAPI", + "canAccessToDocker", + "canAccessToGitProviders", + "canAccessToSSHKeys", + "canAccessToTraefikFiles", + "canCreateProjects", + "canCreateServices", + "canDeleteProjects", + "canDeleteServices", + "accesedProjects", + "accesedServices" +) +SELECT + gen_random_uuid(), + o.id, + u."userId", + 'member', + NOW(), + COALESCE(u."canAccessToAPI", false), + COALESCE(u."canAccessToDocker", false), + COALESCE(u."canAccessToGitProviders", false), + COALESCE(u."canAccessToSSHKeys", false), + COALESCE(u."canAccessToTraefikFiles", false), + COALESCE(u."canCreateProjects", false), + COALESCE(u."canCreateServices", false), + COALESCE(u."canDeleteProjects", false), + COALESCE(u."canDeleteServices", false), + COALESCE(u."accesedProjects", '{}'), + COALESCE(u."accesedServices", '{}') +FROM "user" u +JOIN admin a ON u."adminId" = a."adminId" +JOIN inserted_orgs o ON o."owner_id" = a."adminId" +JOIN auth ON auth.id = u."authId"; + +-- Migrar tokens de auth a apikey +INSERT INTO apikey ( + id, + name, + key, + user_id, + enabled, + created_at, + updated_at +) +SELECT + gen_random_uuid(), + 'Legacy Token', + auth.token, +user_temp.id, + true, + NOW(), + NOW() +FROM auth +JOIN admin ON auth.id = admin."authId" +JOIN user_temp ON user_temp.id = admin."adminId" +WHERE auth.token IS NOT NULL AND auth.token != ''; + +-- Migration tables foreign keys + +ALTER TABLE "project" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint +ALTER TABLE "destination" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint +ALTER TABLE "certificate" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint +ALTER TABLE "registry" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint +ALTER TABLE "notification" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint +ALTER TABLE "ssh-key" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint +ALTER TABLE "git_provider" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint +ALTER TABLE "server" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint +ALTER TABLE "project" DROP CONSTRAINT "project_adminId_admin_adminId_fk"; +--> statement-breakpoint +ALTER TABLE "destination" DROP CONSTRAINT "destination_adminId_admin_adminId_fk"; +--> statement-breakpoint +ALTER TABLE "certificate" DROP CONSTRAINT "certificate_adminId_admin_adminId_fk"; +--> statement-breakpoint +ALTER TABLE "registry" DROP CONSTRAINT "registry_adminId_admin_adminId_fk"; +--> statement-breakpoint +ALTER TABLE "notification" DROP CONSTRAINT "notification_adminId_admin_adminId_fk"; +--> statement-breakpoint +ALTER TABLE "ssh-key" DROP CONSTRAINT "ssh-key_adminId_admin_adminId_fk"; +--> statement-breakpoint +ALTER TABLE "git_provider" DROP CONSTRAINT "git_provider_adminId_admin_adminId_fk"; +--> statement-breakpoint +ALTER TABLE "server" DROP CONSTRAINT "server_adminId_admin_adminId_fk"; +--> statement-breakpoint +ALTER TABLE "project" ADD CONSTRAINT "project_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "destination" ADD CONSTRAINT "destination_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "certificate" ADD CONSTRAINT "certificate_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "registry" ADD CONSTRAINT "registry_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "notification" ADD CONSTRAINT "notification_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ssh-key" ADD CONSTRAINT "ssh-key_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "server" ADD CONSTRAINT "server_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action; + + +ALTER TABLE "user_temp" ADD COLUMN "created_at" timestamp DEFAULT now(); + + +-- Add properties + +ALTER TABLE "project" ADD COLUMN "organizationId" text;--> statement-breakpoint +ALTER TABLE "destination" ADD COLUMN "organizationId" text;--> statement-breakpoint +ALTER TABLE "certificate" ADD COLUMN "organizationId" text;--> statement-breakpoint +ALTER TABLE "registry" ADD COLUMN "organizationId" text;--> statement-breakpoint +ALTER TABLE "notification" ADD COLUMN "organizationId" text;--> statement-breakpoint +ALTER TABLE "ssh-key" ADD COLUMN "organizationId" text;--> statement-breakpoint +ALTER TABLE "git_provider" ADD COLUMN "organizationId" text;--> statement-breakpoint +ALTER TABLE "server" ADD COLUMN "organizationId" text;--> statement-breakpoint +ALTER TABLE "project" ADD CONSTRAINT "project_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "destination" ADD CONSTRAINT "destination_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "certificate" ADD CONSTRAINT "certificate_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "registry" ADD CONSTRAINT "registry_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "notification" ADD CONSTRAINT "notification_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ssh-key" ADD CONSTRAINT "ssh-key_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "server" ADD CONSTRAINT "server_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action; + + +-- Update tables to use organizationId + +-- Custom SQL migration file + +-- Actualizar projects +UPDATE "project" p +SET "organizationId" = ( + SELECT m."organization_id" + FROM "member" m + WHERE m."user_id" = p."userId" + AND m."role" = 'owner' + LIMIT 1 +) +WHERE p."organizationId" IS NULL; + +-- Actualizar servers +UPDATE "server" s +SET "organizationId" = ( + SELECT m."organization_id" + FROM "member" m + WHERE m."user_id" = s."userId" + AND m."role" = 'owner' + LIMIT 1 +) +WHERE s."organizationId" IS NULL; + +-- Actualizar ssh-keys +UPDATE "ssh-key" k +SET "organizationId" = ( + SELECT m."organization_id" + FROM "member" m + WHERE m."user_id" = k."userId" + AND m."role" = 'owner' + LIMIT 1 +) +WHERE k."organizationId" IS NULL; + +-- Actualizar destinations +UPDATE "destination" d +SET "organizationId" = ( + SELECT m."organization_id" + FROM "member" m + WHERE m."user_id" = d."userId" + AND m."role" = 'owner' + LIMIT 1 +) +WHERE d."organizationId" IS NULL; + +-- Actualizar registry +UPDATE "registry" r +SET "organizationId" = ( + SELECT m."organization_id" + FROM "member" m + WHERE m."user_id" = r."userId" + AND m."role" = 'owner' + LIMIT 1 +) +WHERE r."organizationId" IS NULL; + +-- Actualizar notifications +UPDATE "notification" n +SET "organizationId" = ( + SELECT m."organization_id" + FROM "member" m + WHERE m."user_id" = n."userId" + AND m."role" = 'owner' + LIMIT 1 +) +WHERE n."organizationId" IS NULL; + +-- Actualizar certificates +UPDATE "certificate" c +SET "organizationId" = ( + SELECT m."organization_id" + FROM "member" m + WHERE m."user_id" = c."userId" + AND m."role" = 'owner' + LIMIT 1 +) +WHERE c."organizationId" IS NULL; + +-- Actualizar git_provider +UPDATE "git_provider" g +SET "organizationId" = ( + SELECT m."organization_id" + FROM "member" m + WHERE m."user_id" = g."userId" + AND m."role" = 'owner' + LIMIT 1 +) +WHERE g."organizationId" IS NULL; + +-- Verificar que todos los recursos tengan una organización +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM "project" WHERE "organizationId" IS NULL + UNION ALL + SELECT 1 FROM "server" WHERE "organizationId" IS NULL + UNION ALL + SELECT 1 FROM "ssh-key" WHERE "organizationId" IS NULL + UNION ALL + SELECT 1 FROM "destination" WHERE "organizationId" IS NULL + UNION ALL + SELECT 1 FROM "registry" WHERE "organizationId" IS NULL + UNION ALL + SELECT 1 FROM "notification" WHERE "organizationId" IS NULL + UNION ALL + SELECT 1 FROM "certificate" WHERE "organizationId" IS NULL + UNION ALL + SELECT 1 FROM "git_provider" WHERE "organizationId" IS NULL + ) THEN + RAISE EXCEPTION 'Hay recursos sin organización asignada'; + END IF; +END $$; + +-- Hacer organization_id NOT NULL en todas las tablas +ALTER TABLE "project" ALTER COLUMN "organizationId" SET NOT NULL; +ALTER TABLE "server" ALTER COLUMN "organizationId" SET NOT NULL; +ALTER TABLE "ssh-key" ALTER COLUMN "organizationId" SET NOT NULL; +ALTER TABLE "destination" ALTER COLUMN "organizationId" SET NOT NULL; +ALTER TABLE "registry" ALTER COLUMN "organizationId" SET NOT NULL; +ALTER TABLE "notification" ALTER COLUMN "organizationId" SET NOT NULL; +ALTER TABLE "certificate" ALTER COLUMN "organizationId" SET NOT NULL; +ALTER TABLE "git_provider" ALTER COLUMN "organizationId" SET NOT NULL; + +-- Crear índices para mejorar el rendimiento de búsquedas por organización +CREATE INDEX IF NOT EXISTS "idx_project_organization" ON "project" ("organizationId"); +CREATE INDEX IF NOT EXISTS "idx_server_organization" ON "server" ("organizationId"); +CREATE INDEX IF NOT EXISTS "idx_sshkey_organization" ON "ssh-key" ("organizationId"); +CREATE INDEX IF NOT EXISTS "idx_destination_organization" ON "destination" ("organizationId"); +CREATE INDEX IF NOT EXISTS "idx_registry_organization" ON "registry" ("organizationId"); +CREATE INDEX IF NOT EXISTS "idx_notification_organization" ON "notification" ("organizationId"); +CREATE INDEX IF NOT EXISTS "idx_certificate_organization" ON "certificate" ("organizationId"); +CREATE INDEX IF NOT EXISTS "idx_git_provider_organization" ON "git_provider" ("organizationId"); + + + + + +-- Botar tablas de migración +ALTER TABLE "project" DROP CONSTRAINT "project_userId_user_temp_id_fk"; +--> statement-breakpoint +ALTER TABLE "destination" DROP CONSTRAINT "destination_userId_user_temp_id_fk"; +--> statement-breakpoint +ALTER TABLE "certificate" DROP CONSTRAINT "certificate_userId_user_temp_id_fk"; +--> statement-breakpoint +ALTER TABLE "registry" DROP CONSTRAINT "registry_userId_user_temp_id_fk"; +--> statement-breakpoint +ALTER TABLE "notification" DROP CONSTRAINT "notification_userId_user_temp_id_fk"; +--> statement-breakpoint +ALTER TABLE "ssh-key" DROP CONSTRAINT "ssh-key_userId_user_temp_id_fk"; +--> statement-breakpoint +ALTER TABLE "git_provider" DROP CONSTRAINT "git_provider_userId_user_temp_id_fk"; +--> statement-breakpoint +ALTER TABLE "server" DROP CONSTRAINT "server_userId_user_temp_id_fk"; +--> statement-breakpoint +ALTER TABLE "project" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "destination" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "certificate" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "registry" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "notification" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "ssh-key" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "git_provider" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "server" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "project" DROP COLUMN "userId";--> statement-breakpoint +ALTER TABLE "destination" DROP COLUMN "userId";--> statement-breakpoint +ALTER TABLE "certificate" DROP COLUMN "userId";--> statement-breakpoint +ALTER TABLE "registry" DROP COLUMN "userId";--> statement-breakpoint +ALTER TABLE "notification" DROP COLUMN "userId";--> statement-breakpoint +ALTER TABLE "ssh-key" DROP COLUMN "userId";--> statement-breakpoint +ALTER TABLE "git_provider" DROP COLUMN "userId";--> statement-breakpoint +ALTER TABLE "server" DROP COLUMN "userId"; + +-- Drop tables +DROP TABLE "user" CASCADE;--> statement-breakpoint +DROP TABLE "admin" CASCADE;--> statement-breakpoint +DROP TABLE "auth" CASCADE;--> statement-breakpoint +DROP TABLE "session" CASCADE;--> statement-breakpoint +DROP TYPE "public"."Roles"; + + +-- Drop tables +ALTER TABLE "account" DROP CONSTRAINT "account_user_id_user_temp_id_fk"; +--> statement-breakpoint +ALTER TABLE "invitation" DROP CONSTRAINT "invitation_organization_id_organization_id_fk"; +--> statement-breakpoint +ALTER TABLE "invitation" DROP CONSTRAINT "invitation_inviter_id_user_temp_id_fk"; +--> statement-breakpoint +ALTER TABLE "member" DROP CONSTRAINT "member_organization_id_organization_id_fk"; +--> statement-breakpoint +ALTER TABLE "member" DROP CONSTRAINT "member_user_id_user_temp_id_fk"; +--> statement-breakpoint +ALTER TABLE "organization" DROP CONSTRAINT "organization_owner_id_user_temp_id_fk"; +--> statement-breakpoint +ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_temp_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "organization" ADD CONSTRAINT "organization_owner_id_user_temp_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action; + + +-- Update references + +ALTER TABLE "session_temp" DROP CONSTRAINT "session_temp_user_id_user_temp_id_fk"; +--> statement-breakpoint +ALTER TABLE "session_temp" ADD CONSTRAINT "session_temp_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/0066_snapshot.json b/apps/dokploy/drizzle/meta/0066_snapshot.json index 9d95d70e..55804f0c 100644 --- a/apps/dokploy/drizzle/meta/0066_snapshot.json +++ b/apps/dokploy/drizzle/meta/0066_snapshot.json @@ -1,6 +1,6 @@ { - "id": "ddd0c05a-3bcb-41b8-8bf7-6b7647eaa09d", - "prevId": "1240ec96-1751-4de3-b64f-cef9cb716786", + "id": "c5e17a87-0aa3-4178-be24-cfa7cde0f75d", + "prevId": "9cb79f1e-14c2-4deb-b1ab-a1d038f72356", "version": "7", "dialect": "postgresql", "tables": { @@ -731,21 +731,22 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.user": { - "name": "user", + "public.user_temp": { + "name": "user_temp", "schema": "", "columns": { - "userId": { - "name": "userId", + "id": { + "name": "id", "type": "text", "primaryKey": true, "notNull": true }, - "token": { - "name": "token", + "name": { + "name": "name", "type": "text", "primaryKey": false, - "notNull": true + "notNull": true, + "default": "''" }, "isRegistered": { "name": "isRegistered", @@ -756,7 +757,7 @@ }, "expirationDate": { "name": "expirationDate", - "type": "timestamp(3)", + "type": "text", "primaryKey": false, "notNull": true }, @@ -766,139 +767,59 @@ "primaryKey": false, "notNull": true }, - "canCreateProjects": { - "name": "canCreateProjects", + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "two_factor_enabled": { + "name": "two_factor_enabled", "type": "boolean", "primaryKey": false, - "notNull": true, - "default": false + "notNull": false }, - "canAccessToSSHKeys": { - "name": "canAccessToSSHKeys", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "canCreateServices": { - "name": "canCreateServices", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "canDeleteProjects": { - "name": "canDeleteProjects", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "canDeleteServices": { - "name": "canDeleteServices", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "canAccessToDocker": { - "name": "canAccessToDocker", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "canAccessToAPI": { - "name": "canAccessToAPI", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "canAccessToGitProviders": { - "name": "canAccessToGitProviders", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "canAccessToTraefikFiles": { - "name": "canAccessToTraefikFiles", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "accesedProjects": { - "name": "accesedProjects", - "type": "text[]", - "primaryKey": false, - "notNull": true, - "default": "ARRAY[]::text[]" - }, - "accesedServices": { - "name": "accesedServices", - "type": "text[]", - "primaryKey": false, - "notNull": true, - "default": "ARRAY[]::text[]" - }, - "adminId": { - "name": "adminId", + "email": { + "name": "email", "type": "text", "primaryKey": false, "notNull": true }, - "authId": { - "name": "authId", - "type": "text", + "email_verified": { + "name": "email_verified", + "type": "boolean", "primaryKey": false, "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "user_adminId_admin_adminId_fk": { - "name": "user_adminId_admin_adminId_fk", - "tableFrom": "user", - "tableTo": "admin", - "columnsFrom": [ - "adminId" - ], - "columnsTo": [ - "adminId" - ], - "onDelete": "cascade", - "onUpdate": "no action" }, - "user_authId_auth_id_fk": { - "name": "user_authId_auth_id_fk", - "tableFrom": "user", - "tableTo": "auth", - "columnsFrom": [ - "authId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.admin": { - "name": "admin", - "schema": "", - "columns": { - "adminId": { - "name": "adminId", + "image": { + "name": "image", "type": "text", - "primaryKey": true, + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, "notNull": true }, "serverIp": { @@ -947,37 +868,6 @@ "notNull": true, "default": false }, - "authId": { - "name": "authId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "stripeCustomerId": { - "name": "stripeCustomerId", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stripeSubscriptionId": { - "name": "stripeSubscriptionId", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "serversQuantity": { - "name": "serversQuantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, "enablePaidFeatures": { "name": "enablePaidFeatures", "type": "boolean", @@ -1012,121 +902,33 @@ "primaryKey": false, "notNull": true, "default": false - } - }, - "indexes": {}, - "foreignKeys": { - "admin_authId_auth_id_fk": { - "name": "admin_authId_auth_id_fk", - "tableFrom": "admin", - "tableTo": "auth", - "columnsFrom": [ - "authId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.auth": { - "name": "auth", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "rol": { - "name": "rol", - "type": "Roles", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "image": { - "name": "image", + "stripeCustomerId": { + "name": "stripeCustomerId", "type": "text", "primaryKey": false, "notNull": false }, - "secret": { - "name": "secret", + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", "type": "text", "primaryKey": false, "notNull": false }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "is2FAEnabled": { - "name": "is2FAEnabled", - "type": "boolean", + "serversQuantity": { + "name": "serversQuantity", + "type": "integer", "primaryKey": false, "notNull": true, - "default": false - }, - "createdAt": { - "name": "createdAt", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "resetPasswordToken": { - "name": "resetPasswordToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "resetPasswordExpiresAt": { - "name": "resetPasswordExpiresAt", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "confirmationToken": { - "name": "confirmationToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "confirmationExpiresAt": { - "name": "confirmationExpiresAt", - "type": "text", - "primaryKey": false, - "notNull": false + "default": 0 } }, "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": { - "auth_email_unique": { - "name": "auth_email_unique", + "user_temp_email_unique": { + "name": "user_temp_email_unique", "nullsNotDistinct": false, "columns": [ "email" @@ -1165,8 +967,8 @@ "primaryKey": false, "notNull": true }, - "adminId": { - "name": "adminId", + "organizationId": { + "name": "organizationId", "type": "text", "primaryKey": false, "notNull": true @@ -1181,15 +983,15 @@ }, "indexes": {}, "foreignKeys": { - "project_adminId_admin_adminId_fk": { - "name": "project_adminId_admin_adminId_fk", + "project_organizationId_organization_id_fk": { + "name": "project_organizationId_organization_id_fk", "tableFrom": "project", - "tableTo": "admin", + "tableTo": "organization", "columnsFrom": [ - "adminId" + "organizationId" ], "columnsTo": [ - "adminId" + "id" ], "onDelete": "cascade", "onUpdate": "no action" @@ -2042,8 +1844,8 @@ "primaryKey": false, "notNull": true }, - "adminId": { - "name": "adminId", + "organizationId": { + "name": "organizationId", "type": "text", "primaryKey": false, "notNull": true @@ -2051,15 +1853,15 @@ }, "indexes": {}, "foreignKeys": { - "destination_adminId_admin_adminId_fk": { - "name": "destination_adminId_admin_adminId_fk", + "destination_organizationId_organization_id_fk": { + "name": "destination_organizationId_organization_id_fk", "tableFrom": "destination", - "tableTo": "admin", + "tableTo": "organization", "columnsFrom": [ - "adminId" + "organizationId" ], "columnsTo": [ - "adminId" + "id" ], "onDelete": "cascade", "onUpdate": "no action" @@ -2450,11 +2252,11 @@ "primaryKey": false, "notNull": false }, - "adminId": { - "name": "adminId", + "organizationId": { + "name": "organizationId", "type": "text", "primaryKey": false, - "notNull": false + "notNull": true }, "serverId": { "name": "serverId", @@ -2465,15 +2267,15 @@ }, "indexes": {}, "foreignKeys": { - "certificate_adminId_admin_adminId_fk": { - "name": "certificate_adminId_admin_adminId_fk", + "certificate_organizationId_organization_id_fk": { + "name": "certificate_organizationId_organization_id_fk", "tableFrom": "certificate", - "tableTo": "admin", + "tableTo": "organization", "columnsFrom": [ - "adminId" + "organizationId" ], "columnsTo": [ - "adminId" + "id" ], "onDelete": "cascade", "onUpdate": "no action" @@ -2506,8 +2308,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.session": { - "name": "session", + "public.session_temp": { + "name": "session_temp", "schema": "", "columns": { "id": { @@ -2516,25 +2318,67 @@ "primaryKey": true, "notNull": true }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, "user_id": { "name": "user_id", "type": "text", "primaryKey": false, "notNull": true }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", + "impersonated_by": { + "name": "impersonated_by", + "type": "text", "primaryKey": false, - "notNull": true + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false } }, "indexes": {}, "foreignKeys": { - "session_user_id_auth_id_fk": { - "name": "session_user_id_auth_id_fk", - "tableFrom": "session", - "tableTo": "auth", + "session_temp_user_id_user_temp_id_fk": { + "name": "session_temp_user_id_user_temp_id_fk", + "tableFrom": "session_temp", + "tableTo": "user_temp", "columnsFrom": [ "user_id" ], @@ -2546,7 +2390,15 @@ } }, "compositePrimaryKeys": {}, - "uniqueConstraints": {}, + "uniqueConstraints": { + "session_temp_token_unique": { + "name": "session_temp_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false @@ -3272,8 +3124,8 @@ "notNull": true, "default": "'cloud'" }, - "adminId": { - "name": "adminId", + "organizationId": { + "name": "organizationId", "type": "text", "primaryKey": false, "notNull": true @@ -3281,15 +3133,15 @@ }, "indexes": {}, "foreignKeys": { - "registry_adminId_admin_adminId_fk": { - "name": "registry_adminId_admin_adminId_fk", + "registry_organizationId_organization_id_fk": { + "name": "registry_organizationId_organization_id_fk", "tableFrom": "registry", - "tableTo": "admin", + "tableTo": "organization", "columnsFrom": [ - "adminId" + "organizationId" ], "columnsTo": [ - "adminId" + "id" ], "onDelete": "cascade", "onUpdate": "no action" @@ -3532,11 +3384,11 @@ "primaryKey": false, "notNull": false }, - "adminId": { - "name": "adminId", + "organizationId": { + "name": "organizationId", "type": "text", "primaryKey": false, - "notNull": false + "notNull": true } }, "indexes": {}, @@ -3606,15 +3458,15 @@ "onDelete": "cascade", "onUpdate": "no action" }, - "notification_adminId_admin_adminId_fk": { - "name": "notification_adminId_admin_adminId_fk", + "notification_organizationId_organization_id_fk": { + "name": "notification_organizationId_organization_id_fk", "tableFrom": "notification", - "tableTo": "admin", + "tableTo": "organization", "columnsFrom": [ - "adminId" + "organizationId" ], "columnsTo": [ - "adminId" + "id" ], "onDelete": "cascade", "onUpdate": "no action" @@ -3678,12 +3530,6 @@ "type": "text", "primaryKey": false, "notNull": true - }, - "messageThreadId": { - "name": "messageThreadId", - "type": "text", - "primaryKey": false, - "notNull": false } }, "indexes": {}, @@ -3741,24 +3587,24 @@ "primaryKey": false, "notNull": false }, - "adminId": { - "name": "adminId", + "organizationId": { + "name": "organizationId", "type": "text", "primaryKey": false, - "notNull": false + "notNull": true } }, "indexes": {}, "foreignKeys": { - "ssh-key_adminId_admin_adminId_fk": { - "name": "ssh-key_adminId_admin_adminId_fk", + "ssh-key_organizationId_organization_id_fk": { + "name": "ssh-key_organizationId_organization_id_fk", "tableFrom": "ssh-key", - "tableTo": "admin", + "tableTo": "organization", "columnsFrom": [ - "adminId" + "organizationId" ], "columnsTo": [ - "adminId" + "id" ], "onDelete": "cascade", "onUpdate": "no action" @@ -3800,24 +3646,24 @@ "primaryKey": false, "notNull": true }, - "adminId": { - "name": "adminId", + "organizationId": { + "name": "organizationId", "type": "text", "primaryKey": false, - "notNull": false + "notNull": true } }, "indexes": {}, "foreignKeys": { - "git_provider_adminId_admin_adminId_fk": { - "name": "git_provider_adminId_admin_adminId_fk", + "git_provider_organizationId_organization_id_fk": { + "name": "git_provider_organizationId_organization_id_fk", "tableFrom": "git_provider", - "tableTo": "admin", + "tableTo": "organization", "columnsFrom": [ - "adminId" + "organizationId" ], "columnsTo": [ - "adminId" + "id" ], "onDelete": "cascade", "onUpdate": "no action" @@ -4115,8 +3961,8 @@ "primaryKey": false, "notNull": true }, - "adminId": { - "name": "adminId", + "organizationId": { + "name": "organizationId", "type": "text", "primaryKey": false, "notNull": true @@ -4152,15 +3998,15 @@ }, "indexes": {}, "foreignKeys": { - "server_adminId_admin_adminId_fk": { - "name": "server_adminId_admin_adminId_fk", + "server_organizationId_organization_id_fk": { + "name": "server_organizationId_organization_id_fk", "tableFrom": "server", - "tableTo": "admin", + "tableTo": "organization", "columnsFrom": [ - "adminId" + "organizationId" ], "columnsTo": [ - "adminId" + "id" ], "onDelete": "cascade", "onUpdate": "no action" @@ -4312,6 +4158,714 @@ "policies": {}, "checkConstraints": {}, "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "resetPasswordToken": { + "name": "resetPasswordToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resetPasswordExpiresAt": { + "name": "resetPasswordExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationToken": { + "name": "confirmationToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationExpiresAt": { + "name": "confirmationExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_temp_id_fk": { + "name": "account_user_id_user_temp_id_fk", + "tableFrom": "account", + "tableTo": "user_temp", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_user_id_user_temp_id_fk": { + "name": "apikey_user_id_user_temp_id_fk", + "tableFrom": "apikey", + "tableTo": "user_temp", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_temp_id_fk": { + "name": "invitation_inviter_id_user_temp_id_fk", + "tableFrom": "invitation", + "tableTo": "user_temp", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToSSHKeys": { + "name": "canAccessToSSHKeys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToGitProviders": { + "name": "canAccessToGitProviders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_temp_id_fk": { + "name": "member_user_id_user_temp_id_fk", + "tableFrom": "member", + "tableTo": "user_temp", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_owner_id_user_temp_id_fk": { + "name": "organization_owner_id_user_temp_id_fk", + "tableFrom": "organization", + "tableTo": "user_temp", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_temp_id_fk": { + "name": "two_factor_user_id_user_temp_id_fk", + "tableFrom": "two_factor", + "tableTo": "user_temp", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false } }, "enums": { @@ -4338,14 +4892,6 @@ "drop" ] }, - "public.Roles": { - "name": "Roles", - "schema": "public", - "values": [ - "admin", - "user" - ] - }, "public.domainType": { "name": "domainType", "schema": "public", diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index 8490f648..8bf49b3a 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -467,8 +467,8 @@ { "idx": 66, "version": "7", - "when": 1739458568322, - "tag": "0066_petite_scarecrow", + "when": 1739426913392, + "tag": "0066_yielding_echo", "breakpoints": true } ] diff --git a/apps/dokploy/lib/auth-client.ts b/apps/dokploy/lib/auth-client.ts new file mode 100644 index 00000000..f1088e73 --- /dev/null +++ b/apps/dokploy/lib/auth-client.ts @@ -0,0 +1,9 @@ +import { organizationClient } from "better-auth/client/plugins"; +import { twoFactorClient } from "better-auth/client/plugins"; +import { apiKeyClient } from "better-auth/client/plugins"; +import { createAuthClient } from "better-auth/react"; + +export const authClient = createAuthClient({ + // baseURL: "http://localhost:3000", // the base url of your auth server + plugins: [organizationClient(), twoFactorClient(), apiKeyClient()], +}); diff --git a/apps/dokploy/migrate.ts b/apps/dokploy/migrate.ts new file mode 100644 index 00000000..e1f52c9a --- /dev/null +++ b/apps/dokploy/migrate.ts @@ -0,0 +1,149 @@ +// import { drizzle } from "drizzle-orm/postgres-js"; +// import { nanoid } from "nanoid"; +// import postgres from "postgres"; +// import * as schema from "./server/db/schema"; + +// const connectionString = process.env.DATABASE_URL!; + +// const sql = postgres(connectionString, { max: 1 }); +// const db = drizzle(sql, { +// schema, +// }); + +// await db +// .transaction(async (db) => { +// const admins = await db.query.admins.findMany({ +// with: { +// auth: true, +// users: { +// with: { +// auth: true, +// }, +// }, +// }, +// }); +// for (const admin of admins) { +// const user = await db +// .insert(schema.users_temp) +// .values({ +// id: admin.adminId, +// email: admin.auth.email, +// token: admin.auth.token || "", +// emailVerified: true, +// updatedAt: new Date(), +// role: "admin", +// serverIp: admin.serverIp, +// image: admin.auth.image, +// certificateType: admin.certificateType, +// host: admin.host, +// letsEncryptEmail: admin.letsEncryptEmail, +// sshPrivateKey: admin.sshPrivateKey, +// enableDockerCleanup: admin.enableDockerCleanup, +// enableLogRotation: admin.enableLogRotation, +// enablePaidFeatures: admin.enablePaidFeatures, +// metricsConfig: admin.metricsConfig, +// cleanupCacheApplications: admin.cleanupCacheApplications, +// cleanupCacheOnPreviews: admin.cleanupCacheOnPreviews, +// cleanupCacheOnCompose: admin.cleanupCacheOnCompose, +// stripeCustomerId: admin.stripeCustomerId, +// stripeSubscriptionId: admin.stripeSubscriptionId, +// serversQuantity: admin.serversQuantity, +// }) +// .returning() +// .then((user) => user[0]); + +// await db.insert(schema.account).values({ +// providerId: "credential", +// userId: user?.id || "", +// password: admin.auth.password, +// is2FAEnabled: admin.auth.is2FAEnabled || false, +// createdAt: new Date(admin.auth.createdAt) || new Date(), +// updatedAt: new Date(admin.auth.createdAt) || new Date(), +// }); + +// const organization = await db +// .insert(schema.organization) +// .values({ +// name: "My Organization", +// slug: nanoid(), +// ownerId: user?.id || "", +// createdAt: new Date(admin.createdAt) || new Date(), +// }) +// .returning() +// .then((organization) => organization[0]); + +// for (const member of admin.users) { +// const userTemp = await db +// .insert(schema.users_temp) +// .values({ +// id: member.userId, +// email: member.auth.email, +// token: member.token || "", +// emailVerified: true, +// updatedAt: new Date(admin.createdAt) || new Date(), +// role: "user", +// image: member.auth.image, +// createdAt: admin.createdAt, +// canAccessToAPI: member.canAccessToAPI || false, +// canAccessToDocker: member.canAccessToDocker || false, +// canAccessToGitProviders: member.canAccessToGitProviders || false, +// canAccessToSSHKeys: member.canAccessToSSHKeys || false, +// canAccessToTraefikFiles: member.canAccessToTraefikFiles || false, +// canCreateProjects: member.canCreateProjects || false, +// canCreateServices: member.canCreateServices || false, +// canDeleteProjects: member.canDeleteProjects || false, +// canDeleteServices: member.canDeleteServices || false, +// accessedProjects: member.accessedProjects || [], +// accessedServices: member.accessedServices || [], +// }) +// .returning() +// .then((userTemp) => userTemp[0]); + +// await db.insert(schema.account).values({ +// providerId: "credential", +// userId: member?.userId || "", +// password: member.auth.password, +// is2FAEnabled: member.auth.is2FAEnabled || false, +// createdAt: new Date(member.auth.createdAt) || new Date(), +// updatedAt: new Date(member.auth.createdAt) || new Date(), +// }); + +// await db.insert(schema.member).values({ +// organizationId: organization?.id || "", +// userId: userTemp?.id || "", +// role: "admin", +// createdAt: new Date(member.createdAt) || new Date(), +// }); +// } +// } +// }) +// .then(() => { +// console.log("Migration finished"); +// }) +// .catch((error) => { +// console.error(error); +// }); + +// await db +// .transaction(async (db) => { +// const projects = await db.query.projects.findMany({ +// with: { +// user: { +// with: { +// organizations: true, +// }, +// }, +// }, +// }); +// for (const project of projects) { +// const _user = await db.update(schema.projects).set({ +// organizationId: project.user.organizations[0]?.id || "", +// }); +// } +// }) +// .then(() => { +// console.log("Migration finished"); +// }) +// .catch((error) => { +// console.error(error); +// }); diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 30666f83..3c7b0528 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.18.3", + "version": "v0.18.4", "private": true, "license": "Apache-2.0", "type": "module", @@ -16,6 +16,7 @@ "studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts", "migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts", "migration:run": "tsx -r dotenv/config migration.ts", + "manual-migration:run": "tsx -r dotenv/config migrate.ts", "migration:up": "drizzle-kit up --config ./server/db/drizzle.config.ts", "migration:drop": "drizzle-kit drop --config ./server/db/drizzle.config.ts", "db:push": "drizzle-kit push --config ./server/db/drizzle.config.ts", @@ -35,6 +36,7 @@ "test": "vitest --config __test__/vitest.config.ts" }, "dependencies": { + "better-auth": "1.2.0", "bl": "6.0.11", "rotating-file-stream": "3.2.3", "qrcode": "^1.5.3", diff --git a/apps/dokploy/pages/_error.tsx b/apps/dokploy/pages/_error.tsx index 958e1740..d28e2cb0 100644 --- a/apps/dokploy/pages/_error.tsx +++ b/apps/dokploy/pages/_error.tsx @@ -90,7 +90,7 @@ export default function Custom404({ statusCode, error }: Props) { } // @ts-ignore -Error.getInitialProps = ({ res, err, ...rest }: NextPageContext) => { +Error.getInitialProps = ({ res, err }: NextPageContext) => { const statusCode = res ? res.statusCode : err ? err.statusCode : 404; return { statusCode, error: err }; }; diff --git a/apps/dokploy/pages/accept-invitation/[accept-invitation].tsx b/apps/dokploy/pages/accept-invitation/[accept-invitation].tsx new file mode 100644 index 00000000..bc60d970 --- /dev/null +++ b/apps/dokploy/pages/accept-invitation/[accept-invitation].tsx @@ -0,0 +1,30 @@ +import { Button } from "@/components/ui/button"; +import { authClient } from "@/lib/auth-client"; +import { useRouter } from "next/router"; + +export const AcceptInvitation = () => { + const { query } = useRouter(); + + const invitationId = query["accept-invitation"]; + + // const { data: organization } = api.organization.getById.useQuery({ + // id: id as string + // }) + + return ( +
+ +
+ ); +}; + +export default AcceptInvitation; diff --git a/apps/dokploy/pages/api/[...trpc].ts b/apps/dokploy/pages/api/[...trpc].ts index df85440b..85ddbb28 100644 --- a/apps/dokploy/pages/api/[...trpc].ts +++ b/apps/dokploy/pages/api/[...trpc].ts @@ -1,17 +1,11 @@ import { appRouter } from "@/server/api/root"; import { createTRPCContext } from "@/server/api/trpc"; -import { validateBearerToken, validateRequest } from "@dokploy/server"; +import { validateRequest } from "@dokploy/server"; import { createOpenApiNextHandler } from "@dokploy/trpc-openapi"; import type { NextApiRequest, NextApiResponse } from "next"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { - let { session, user } = await validateBearerToken(req); - - if (!session) { - const cookieResult = await validateRequest(req, res); - session = cookieResult.session; - user = cookieResult.user; - } + const { session, user } = await validateRequest(req); if (!user || !session) { res.status(401).json({ message: "Unauthorized" }); diff --git a/apps/dokploy/pages/api/auth/[...all].ts b/apps/dokploy/pages/api/auth/[...all].ts new file mode 100644 index 00000000..48aa0370 --- /dev/null +++ b/apps/dokploy/pages/api/auth/[...all].ts @@ -0,0 +1,7 @@ +import { auth } from "@dokploy/server/index"; +import { toNodeHandler } from "better-auth/node"; + +// Disallow body parsing, we will parse it manually +export const config = { api: { bodyParser: false } }; + +export default toNodeHandler(auth.handler); diff --git a/apps/dokploy/pages/api/deploy/github.ts b/apps/dokploy/pages/api/deploy/github.ts index 761c3866..5e64d8b2 100644 --- a/apps/dokploy/pages/api/deploy/github.ts +++ b/apps/dokploy/pages/api/deploy/github.ts @@ -3,9 +3,7 @@ import { applications, compose, github } from "@/server/db/schema"; import type { DeploymentJob } from "@/server/queues/queue-types"; import { myQueue } from "@/server/queues/queueSetup"; import { deploy } from "@/server/utils/deploy"; -import { generateRandomDomain } from "@/templates/utils"; import { - type Domain, IS_CLOUD, createPreviewDeployment, findPreviewDeploymentByApplicationId, diff --git a/apps/dokploy/pages/api/health.ts b/apps/dokploy/pages/api/health.ts index 9dc8101e..57875a19 100644 --- a/apps/dokploy/pages/api/health.ts +++ b/apps/dokploy/pages/api/health.ts @@ -1,7 +1,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; export default async function handler( - req: NextApiRequest, + _req: NextApiRequest, res: NextApiResponse, ) { return res.status(200).json({ ok: true }); diff --git a/apps/dokploy/pages/api/providers/github/setup.ts b/apps/dokploy/pages/api/providers/github/setup.ts index a1ce98d4..32712250 100644 --- a/apps/dokploy/pages/api/providers/github/setup.ts +++ b/apps/dokploy/pages/api/providers/github/setup.ts @@ -1,11 +1,6 @@ import { db } from "@/server/db"; import { github } from "@/server/db/schema"; -import { - createGithub, - findAdminByAuthId, - findAuthById, - findUserByAuthId, -} from "@dokploy/server"; +import { createGithub } from "@dokploy/server"; import { eq } from "drizzle-orm"; import type { NextApiRequest, NextApiResponse } from "next"; import { Octokit } from "octokit"; @@ -21,14 +16,13 @@ export default async function handler( req: NextApiRequest, res: NextApiResponse, ) { - const { code, state, installation_id, setup_action }: Query = - req.query as Query; + const { code, state, installation_id }: Query = req.query as Query; if (!code) { return res.status(400).json({ error: "Missing code parameter" }); } const [action, value] = state?.split(":"); - // Value could be the authId or the githubProviderId + // Value could be the organizationId or the githubProviderId if (action === "gh_init") { const octokit = new Octokit({}); @@ -39,17 +33,6 @@ export default async function handler( }, ); - const auth = await findAuthById(value as string); - - let adminId = ""; - if (auth.rol === "admin") { - const admin = await findAdminByAuthId(auth.id); - adminId = admin.adminId; - } else { - const user = await findUserByAuthId(auth.id); - adminId = user.adminId; - } - await createGithub( { name: data.name, @@ -60,7 +43,7 @@ export default async function handler( githubWebhookSecret: data.webhook_secret, githubPrivateKey: data.pem, }, - adminId, + value as string, ); } else if (action === "gh_setup") { await db diff --git a/apps/dokploy/pages/api/stripe/webhook.ts b/apps/dokploy/pages/api/stripe/webhook.ts index d4599f78..9e8c9da5 100644 --- a/apps/dokploy/pages/api/stripe/webhook.ts +++ b/apps/dokploy/pages/api/stripe/webhook.ts @@ -1,7 +1,7 @@ import { buffer } from "node:stream/consumers"; import { db } from "@/server/db"; -import { admins, server } from "@/server/db/schema"; -import { findAdminById } from "@dokploy/server"; +import { organization, server, users_temp } from "@/server/db/schema"; +import { type Server, findUserById } from "@dokploy/server"; import { asc, eq } from "drizzle-orm"; import type { NextApiRequest, NextApiResponse } from "next"; import Stripe from "stripe"; @@ -64,33 +64,35 @@ export default async function handler( session.subscription as string, ); await db - .update(admins) + .update(users_temp) .set({ stripeCustomerId: session.customer as string, stripeSubscriptionId: session.subscription as string, serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0, }) - .where(eq(admins.adminId, adminId)) + .where(eq(users_temp.id, adminId)) .returning(); - const admin = await findAdminById(adminId); + const admin = await findUserById(adminId); if (!admin) { return res.status(400).send("Webhook Error: Admin not found"); } const newServersQuantity = admin.serversQuantity; - await updateServersBasedOnQuantity(admin.adminId, newServersQuantity); + await updateServersBasedOnQuantity(admin.id, newServersQuantity); break; } case "customer.subscription.created": { const newSubscription = event.data.object as Stripe.Subscription; await db - .update(admins) + .update(users_temp) .set({ stripeSubscriptionId: newSubscription.id, stripeCustomerId: newSubscription.customer as string, }) - .where(eq(admins.stripeCustomerId, newSubscription.customer as string)) + .where( + eq(users_temp.stripeCustomerId, newSubscription.customer as string), + ) .returning(); break; @@ -100,14 +102,16 @@ export default async function handler( const newSubscription = event.data.object as Stripe.Subscription; await db - .update(admins) + .update(users_temp) .set({ stripeSubscriptionId: null, serversQuantity: 0, }) - .where(eq(admins.stripeCustomerId, newSubscription.customer as string)); + .where( + eq(users_temp.stripeCustomerId, newSubscription.customer as string), + ); - const admin = await findAdminByStripeCustomerId( + const admin = await findUserByStripeCustomerId( newSubscription.customer as string, ); @@ -115,13 +119,13 @@ export default async function handler( return res.status(400).send("Webhook Error: Admin not found"); } - await disableServers(admin.adminId); + await disableServers(admin.id); break; } case "customer.subscription.updated": { const newSubscription = event.data.object as Stripe.Subscription; - const admin = await findAdminByStripeCustomerId( + const admin = await findUserByStripeCustomerId( newSubscription.customer as string, ); @@ -131,23 +135,23 @@ export default async function handler( if (newSubscription.status === "active") { await db - .update(admins) + .update(users_temp) .set({ serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0, }) .where( - eq(admins.stripeCustomerId, newSubscription.customer as string), + eq(users_temp.stripeCustomerId, newSubscription.customer as string), ); const newServersQuantity = admin.serversQuantity; - await updateServersBasedOnQuantity(admin.adminId, newServersQuantity); + await updateServersBasedOnQuantity(admin.id, newServersQuantity); } else { - await disableServers(admin.adminId); + await disableServers(admin.id); await db - .update(admins) + .update(users_temp) .set({ serversQuantity: 0 }) .where( - eq(admins.stripeCustomerId, newSubscription.customer as string), + eq(users_temp.stripeCustomerId, newSubscription.customer as string), ); } @@ -168,13 +172,13 @@ export default async function handler( } await db - .update(admins) + .update(users_temp) .set({ serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0, }) - .where(eq(admins.stripeCustomerId, suscription.customer as string)); + .where(eq(users_temp.stripeCustomerId, suscription.customer as string)); - const admin = await findAdminByStripeCustomerId( + const admin = await findUserByStripeCustomerId( suscription.customer as string, ); @@ -182,7 +186,7 @@ export default async function handler( return res.status(400).send("Webhook Error: Admin not found"); } const newServersQuantity = admin.serversQuantity; - await updateServersBasedOnQuantity(admin.adminId, newServersQuantity); + await updateServersBasedOnQuantity(admin.id, newServersQuantity); break; } case "invoice.payment_failed": { @@ -193,7 +197,7 @@ export default async function handler( ); if (subscription.status !== "active") { - const admin = await findAdminByStripeCustomerId( + const admin = await findUserByStripeCustomerId( newInvoice.customer as string, ); @@ -201,13 +205,15 @@ export default async function handler( return res.status(400).send("Webhook Error: Admin not found"); } await db - .update(admins) + .update(users_temp) .set({ serversQuantity: 0, }) - .where(eq(admins.stripeCustomerId, newInvoice.customer as string)); + .where( + eq(users_temp.stripeCustomerId, newInvoice.customer as string), + ); - await disableServers(admin.adminId); + await disableServers(admin.id); } break; @@ -216,20 +222,20 @@ export default async function handler( case "customer.deleted": { const customer = event.data.object as Stripe.Customer; - const admin = await findAdminByStripeCustomerId(customer.id); + const admin = await findUserByStripeCustomerId(customer.id); if (!admin) { return res.status(400).send("Webhook Error: Admin not found"); } - await disableServers(admin.adminId); + await disableServers(admin.id); await db - .update(admins) + .update(users_temp) .set({ stripeCustomerId: null, stripeSubscriptionId: null, serversQuantity: 0, }) - .where(eq(admins.stripeCustomerId, customer.id)); + .where(eq(users_temp.stripeCustomerId, customer.id)); break; } @@ -240,20 +246,26 @@ export default async function handler( return res.status(200).json({ received: true }); } -const disableServers = async (adminId: string) => { - await db - .update(server) - .set({ - serverStatus: "inactive", - }) - .where(eq(server.adminId, adminId)); +const disableServers = async (userId: string) => { + const organizations = await db.query.organization.findMany({ + where: eq(organization.ownerId, userId), + }); + + for (const org of organizations) { + await db + .update(server) + .set({ + serverStatus: "inactive", + }) + .where(eq(server.organizationId, org.id)); + } }; -const findAdminByStripeCustomerId = async (stripeCustomerId: string) => { - const admin = db.query.admins.findFirst({ - where: eq(admins.stripeCustomerId, stripeCustomerId), +const findUserByStripeCustomerId = async (stripeCustomerId: string) => { + const user = db.query.users_temp.findFirst({ + where: eq(users_temp.stripeCustomerId, stripeCustomerId), }); - return admin; + return user; }; const activateServer = async (serverId: string) => { @@ -270,19 +282,27 @@ const deactivateServer = async (serverId: string) => { .where(eq(server.serverId, serverId)); }; -export const findServersByAdminIdSorted = async (adminId: string) => { - const servers = await db.query.server.findMany({ - where: eq(server.adminId, adminId), - orderBy: asc(server.createdAt), +export const findServersByUserIdSorted = async (userId: string) => { + const organizations = await db.query.organization.findMany({ + where: eq(organization.ownerId, userId), }); + const servers: Server[] = []; + for (const org of organizations) { + const serversByOrg = await db.query.server.findMany({ + where: eq(server.organizationId, org.id), + orderBy: asc(server.createdAt), + }); + servers.push(...serversByOrg); + } + return servers; }; export const updateServersBasedOnQuantity = async ( - adminId: string, + userId: string, newServersQuantity: number, ) => { - const servers = await findServersByAdminIdSorted(adminId); + const servers = await findServersByUserIdSorted(userId); if (servers.length > newServersQuantity) { for (const [index, server] of servers.entries()) { diff --git a/apps/dokploy/pages/confirm-email.tsx b/apps/dokploy/pages/confirm-email.tsx deleted file mode 100644 index 2910a267..00000000 --- a/apps/dokploy/pages/confirm-email.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { OnboardingLayout } from "@/components/layouts/onboarding-layout"; -import { Logo } from "@/components/shared/logo"; -import { CardDescription, CardTitle } from "@/components/ui/card"; -import { db } from "@/server/db"; -import { auth } from "@/server/db/schema"; -import { IS_CLOUD, updateAuthById } from "@dokploy/server"; -import { isBefore } from "date-fns"; -import { eq } from "drizzle-orm"; -import type { GetServerSidePropsContext } from "next"; -import Link from "next/link"; -import type { ReactElement } from "react"; - -export default function Home() { - return ( -
-
- - - Dokploy - - Email Confirmed - - Congratulations, your email is confirmed. - -
- - Click here to login - -
-
-
- ); -} - -Home.getLayout = (page: ReactElement) => { - return {page}; -}; -export async function getServerSideProps(context: GetServerSidePropsContext) { - if (!IS_CLOUD) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - const { token } = context.query; - - if (typeof token !== "string") { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - - const authR = await db.query.auth.findFirst({ - where: eq(auth.confirmationToken, token), - }); - - if ( - !authR || - authR?.confirmationToken === null || - authR?.confirmationExpiresAt === null - ) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - - const isExpired = isBefore(new Date(authR.confirmationExpiresAt), new Date()); - - if (isExpired) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - - await updateAuthById(authR.id, { - confirmationToken: null, - confirmationExpiresAt: null, - }); - - return { - props: { - token: authR.confirmationToken, - }, - }; -} diff --git a/apps/dokploy/pages/dashboard/docker.tsx b/apps/dokploy/pages/dashboard/docker.tsx index 96387c07..e01a763b 100644 --- a/apps/dokploy/pages/dashboard/docker.tsx +++ b/apps/dokploy/pages/dashboard/docker.tsx @@ -1,10 +1,11 @@ import { ShowContainers } from "@/components/dashboard/docker/show/show-containers"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { appRouter } from "@/server/api/root"; -import { IS_CLOUD, validateRequest } from "@dokploy/server"; +import { IS_CLOUD } from "@dokploy/server/constants"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Dashboard = () => { @@ -27,7 +28,7 @@ export async function getServerSideProps( }, }; } - const { user, session } = await validateRequest(ctx.req, ctx.res); + const { user, session } = await validateRequest(ctx.req); if (!user) { return { redirect: { @@ -44,21 +45,20 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); try { await helpers.project.all.prefetch(); - const auth = await helpers.auth.get.fetch(); - if (auth.rol === "user") { - const user = await helpers.user.byAuthId.fetch({ - authId: auth.id, + if (user.role === "member") { + const userR = await helpers.user.one.fetch({ + userId: user.id, }); - if (!user.canAccessToDocker) { + if (!userR?.canAccessToDocker) { return { redirect: { permanent: true, @@ -72,7 +72,7 @@ export async function getServerSideProps( trpcState: helpers.dehydrate(), }, }; - } catch (error) { + } catch (_error) { return { props: {}, }; diff --git a/apps/dokploy/pages/dashboard/monitoring.tsx b/apps/dokploy/pages/dashboard/monitoring.tsx index a9f712f3..4272c453 100644 --- a/apps/dokploy/pages/dashboard/monitoring.tsx +++ b/apps/dokploy/pages/dashboard/monitoring.tsx @@ -1,17 +1,13 @@ import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring"; import { ShowPaidMonitoring } from "@/components/dashboard/monitoring/paid/servers/show-paid-monitoring"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { AlertBlock } from "@/components/shared/alert-block"; import { Card } from "@/components/ui/card"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; import { useLocalStorage } from "@/hooks/useLocalStorage"; import { api } from "@/utils/api"; import { IS_CLOUD } from "@dokploy/server/constants"; -import { validateRequest } from "@dokploy/server/index"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { Loader2 } from "lucide-react"; import type { GetServerSidePropsContext } from "next"; -import type React from "react"; import type { ReactElement } from "react"; const BASE_URL = "http://localhost:3001/metrics"; @@ -19,13 +15,12 @@ const BASE_URL = "http://localhost:3001/metrics"; const DEFAULT_TOKEN = "metrics"; const Dashboard = () => { - const { data: isCloud } = api.settings.isCloud.useQuery(); - const [toggleMonitoring, setToggleMonitoring] = useLocalStorage( + const [toggleMonitoring, _setToggleMonitoring] = useLocalStorage( "monitoring-enabled", false, ); - const { data: monitoring, isLoading } = api.admin.getMetricsToken.useQuery(); + const { data: monitoring, isLoading } = api.user.getMetricsToken.useQuery(); return (
{/* @@ -104,7 +99,7 @@ export async function getServerSideProps( }, }; } - const { user } = await validateRequest(ctx.req, ctx.res); + const { user } = await validateRequest(ctx.req); if (!user) { return { redirect: { diff --git a/apps/dokploy/pages/dashboard/project/[projectId].tsx b/apps/dokploy/pages/dashboard/project/[projectId].tsx index ea23ad3a..62de98c1 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId].tsx @@ -49,7 +49,7 @@ import { cn } from "@/lib/utils"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; import type { findProjectById } from "@dokploy/server"; -import { validateRequest } from "@dokploy/server"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; import { Ban, @@ -200,15 +200,8 @@ const Project = ( ) => { const [isBulkActionLoading, setIsBulkActionLoading] = useState(false); const { projectId } = props; - const { data: auth } = api.auth.get.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: auth?.id || "", - }, - { - enabled: !!auth?.id && auth?.rol === "user", - }, - ); + const { data: auth } = api.user.get.useQuery(); + const { data, isLoading, refetch } = api.project.one.useQuery({ projectId }); const router = useRouter(); @@ -268,7 +261,7 @@ const Project = ( try { await composeActions.start.mutateAsync({ composeId: serviceId }); success++; - } catch (error) { + } catch (_error) { toast.error(`Error starting service ${serviceId}`); } } @@ -288,7 +281,7 @@ const Project = ( try { await composeActions.stop.mutateAsync({ composeId: serviceId }); success++; - } catch (error) { + } catch (_error) { toast.error(`Error stopping service ${serviceId}`); } } @@ -335,7 +328,7 @@ const Project = ( {data?.description} - {(auth?.rol === "admin" || user?.canCreateServices) && ( + {(auth?.role === "owner" || auth?.canCreateServices) && (
@@ -658,7 +651,7 @@ export async function getServerSideProps( const { params } = ctx; const { req, res } = ctx; - const { user, session } = await validateRequest(req, res); + const { user, session } = await validateRequest(req); if (!user) { return { redirect: { @@ -674,8 +667,8 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); @@ -692,7 +685,7 @@ export async function getServerSideProps( projectId: params?.projectId, }, }; - } catch (error) { + } catch (_error) { return { redirect: { permanent: false, diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx index 9d3ffe4f..94b8f5f5 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx @@ -28,7 +28,6 @@ import { CardTitle, } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, @@ -39,10 +38,10 @@ import { import { cn } from "@/lib/utils"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; -import { validateRequest } from "@dokploy/server"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; import copy from "copy-to-clipboard"; -import { GlobeIcon, HelpCircle, ServerOff, Trash2 } from "lucide-react"; +import { GlobeIcon, HelpCircle, ServerOff } from "lucide-react"; import type { GetServerSidePropsContext, InferGetServerSidePropsType, @@ -50,7 +49,7 @@ import type { import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; -import React, { useState, useEffect, type ReactElement } from "react"; +import { type ReactElement, useEffect, useState } from "react"; import { toast } from "sonner"; import superjson from "superjson"; @@ -66,7 +65,7 @@ type TabState = const Service = ( props: InferGetServerSidePropsType, ) => { - const [toggleMonitoring, setToggleMonitoring] = useState(false); + const [_toggleMonitoring, _setToggleMonitoring] = useState(false); const { applicationId, activeTab } = props; const router = useRouter(); const { projectId } = router.query; @@ -86,16 +85,7 @@ const Service = ( ); const { data: isCloud } = api.settings.isCloud.useQuery(); - const { data: auth } = api.auth.get.useQuery(); - const { data: monitoring } = api.admin.getMetricsToken.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: auth?.id || "", - }, - { - enabled: !!auth?.id && auth?.rol === "user", - }, - ); + const { data: auth } = api.user.get.useQuery(); return (
@@ -186,7 +176,7 @@ const Service = (
- {(auth?.rol === "admin" || user?.canDeleteServices) && ( + {(auth?.role === "owner" || auth?.canDeleteServices) && ( )}
@@ -370,7 +360,7 @@ export async function getServerSideProps( const { query, params, req, res } = ctx; const activeTab = query.tab; - const { user, session } = await validateRequest(req, res); + const { user, session } = await validateRequest(req); if (!user) { return { redirect: { @@ -386,8 +376,8 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); @@ -408,7 +398,7 @@ export async function getServerSideProps( activeTab: (activeTab || "general") as TabState, }, }; - } catch (error) { + } catch (_error) { return { redirect: { permanent: false, diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index c6840214..46b727d2 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -22,7 +22,6 @@ import { CardTitle, } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, @@ -33,7 +32,7 @@ import { import { cn } from "@/lib/utils"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; -import { validateRequest } from "@dokploy/server"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; import copy from "copy-to-clipboard"; import { CircuitBoard, ServerOff } from "lucide-react"; @@ -45,7 +44,7 @@ import type { import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; -import React, { useState, useEffect, type ReactElement } from "react"; +import { type ReactElement, useEffect, useState } from "react"; import { toast } from "sonner"; import superjson from "superjson"; @@ -60,7 +59,7 @@ type TabState = const Service = ( props: InferGetServerSidePropsType, ) => { - const [toggleMonitoring, setToggleMonitoring] = useState(false); + const [_toggleMonitoring, _setToggleMonitoring] = useState(false); const { composeId, activeTab } = props; const router = useRouter(); const { projectId } = router.query; @@ -79,17 +78,8 @@ const Service = ( }, ); - const { data: auth } = api.auth.get.useQuery(); - const { data: monitoring } = api.admin.getMetricsToken.useQuery(); + const { data: auth } = api.user.get.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: auth?.id || "", - }, - { - enabled: !!auth?.id && auth?.rol === "user", - }, - ); return (
@@ -181,7 +171,7 @@ const Service = (
- {(auth?.rol === "admin" || user?.canDeleteServices) && ( + {(auth?.role === "owner" || auth?.canDeleteServices) && ( )}
@@ -366,7 +356,7 @@ export async function getServerSideProps( const { query, params, req, res } = ctx; const activeTab = query.tab; - const { user, session } = await validateRequest(req, res); + const { user, session } = await validateRequest(req); if (!user) { return { redirect: { @@ -382,8 +372,8 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); @@ -402,7 +392,7 @@ export async function getServerSideProps( activeTab: (activeTab || "general") as TabState, }, }; - } catch (error) { + } catch (_error) { return { redirect: { permanent: false, diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx index a2ee9051..e91e0978 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx @@ -24,7 +24,6 @@ import { CardTitle, } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, @@ -35,9 +34,9 @@ import { import { cn } from "@/lib/utils"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; -import { validateRequest } from "@dokploy/server"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; -import { HelpCircle, ServerOff, Trash2 } from "lucide-react"; +import { HelpCircle, ServerOff } from "lucide-react"; import type { GetServerSidePropsContext, InferGetServerSidePropsType, @@ -45,8 +44,7 @@ import type { import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; -import React, { useState, type ReactElement } from "react"; -import { toast } from "sonner"; +import { type ReactElement, useState } from "react"; import superjson from "superjson"; type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced"; @@ -54,23 +52,15 @@ type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced"; const Mariadb = ( props: InferGetServerSidePropsType, ) => { - const [toggleMonitoring, setToggleMonitoring] = useState(false); + const [_toggleMonitoring, _setToggleMonitoring] = useState(false); const { mariadbId, activeTab } = props; const router = useRouter(); const { projectId } = router.query; const [tab, setSab] = useState(activeTab); const { data } = api.mariadb.one.useQuery({ mariadbId }); - const { data: auth } = api.auth.get.useQuery(); - const { data: monitoring } = api.admin.getMetricsToken.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: auth?.id || "", - }, - { - enabled: !!auth?.id && auth?.rol === "user", - }, - ); + const { data: auth } = api.user.get.useQuery(); + const { data: isCloud } = api.settings.isCloud.useQuery(); return ( @@ -154,7 +144,7 @@ const Mariadb = (
- {(auth?.rol === "admin" || user?.canDeleteServices) && ( + {(auth?.role === "owner" || auth?.canDeleteServices) && ( )}
@@ -316,7 +306,7 @@ export async function getServerSideProps( const { query, params, req, res } = ctx; const activeTab = query.tab; - const { user, session } = await validateRequest(req, res); + const { user, session } = await validateRequest(req); if (!user) { return { redirect: { @@ -332,8 +322,8 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); @@ -351,7 +341,7 @@ export async function getServerSideProps( activeTab: (activeTab || "general") as TabState, }, }; - } catch (error) { + } catch (_error) { return { redirect: { permanent: false, diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx index 4f3947c2..b10b7b93 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx @@ -24,7 +24,6 @@ import { CardTitle, } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, @@ -35,9 +34,9 @@ import { import { cn } from "@/lib/utils"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; -import { validateRequest } from "@dokploy/server"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; -import { HelpCircle, ServerOff, Trash2 } from "lucide-react"; +import { HelpCircle, ServerOff } from "lucide-react"; import type { GetServerSidePropsContext, InferGetServerSidePropsType, @@ -45,8 +44,7 @@ import type { import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; -import React, { useState, type ReactElement } from "react"; -import { toast } from "sonner"; +import { type ReactElement, useState } from "react"; import superjson from "superjson"; type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced"; @@ -54,23 +52,14 @@ type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced"; const Mongo = ( props: InferGetServerSidePropsType, ) => { - const [toggleMonitoring, setToggleMonitoring] = useState(false); + const [_toggleMonitoring, _setToggleMonitoring] = useState(false); const { mongoId, activeTab } = props; const router = useRouter(); const { projectId } = router.query; const [tab, setSab] = useState(activeTab); const { data } = api.mongo.one.useQuery({ mongoId }); - const { data: auth } = api.auth.get.useQuery(); - const { data: monitoring } = api.admin.getMetricsToken.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: auth?.id || "", - }, - { - enabled: !!auth?.id && auth?.rol === "user", - }, - ); + const { data: auth } = api.user.get.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); @@ -156,7 +145,7 @@ const Mongo = (
- {(auth?.rol === "admin" || user?.canDeleteServices) && ( + {(auth?.role === "owner" || auth?.canDeleteServices) && ( )}
@@ -318,7 +307,7 @@ export async function getServerSideProps( const { query, params, req, res } = ctx; const activeTab = query.tab; - const { user, session } = await validateRequest(req, res); + const { user, session } = await validateRequest(req); if (!user) { return { redirect: { @@ -334,8 +323,8 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); @@ -353,7 +342,7 @@ export async function getServerSideProps( activeTab: (activeTab || "general") as TabState, }, }; - } catch (error) { + } catch (_error) { return { redirect: { permanent: false, diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx index baf7e6f8..261a2762 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx @@ -24,7 +24,6 @@ import { CardTitle, } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, @@ -35,9 +34,9 @@ import { import { cn } from "@/lib/utils"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; -import { validateRequest } from "@dokploy/server"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; -import { HelpCircle, ServerOff, Trash2 } from "lucide-react"; +import { HelpCircle, ServerOff } from "lucide-react"; import type { GetServerSidePropsContext, InferGetServerSidePropsType, @@ -45,8 +44,7 @@ import type { import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; -import React, { useState, type ReactElement } from "react"; -import { toast } from "sonner"; +import { type ReactElement, useState } from "react"; import superjson from "superjson"; type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced"; @@ -54,22 +52,13 @@ type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced"; const MySql = ( props: InferGetServerSidePropsType, ) => { - const [toggleMonitoring, setToggleMonitoring] = useState(false); + const [_toggleMonitoring, _setToggleMonitoring] = useState(false); const { mysqlId, activeTab } = props; const router = useRouter(); const { projectId } = router.query; const [tab, setSab] = useState(activeTab); const { data } = api.mysql.one.useQuery({ mysqlId }); - const { data: auth } = api.auth.get.useQuery(); - const { data: monitoring } = api.admin.getMetricsToken.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: auth?.id || "", - }, - { - enabled: !!auth?.id && auth?.rol === "user", - }, - ); + const { data: auth } = api.user.get.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); @@ -156,7 +145,7 @@ const MySql = (
- {(auth?.rol === "admin" || user?.canDeleteServices) && ( + {(auth?.role === "owner" || auth?.canDeleteServices) && ( )}
@@ -323,7 +312,7 @@ export async function getServerSideProps( const { query, params, req, res } = ctx; const activeTab = query.tab; - const { user, session } = await validateRequest(req, res); + const { user, session } = await validateRequest(req); if (!user) { return { redirect: { @@ -339,8 +328,8 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); @@ -358,7 +347,7 @@ export async function getServerSideProps( activeTab: (activeTab || "general") as TabState, }, }; - } catch (error) { + } catch (_error) { return { redirect: { permanent: false, diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx index e3fd8b44..5d8fd3b1 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx @@ -24,7 +24,6 @@ import { CardTitle, } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, @@ -35,9 +34,9 @@ import { import { cn } from "@/lib/utils"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; -import { validateRequest } from "@dokploy/server"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; -import { HelpCircle, ServerOff, Trash2 } from "lucide-react"; +import { HelpCircle, ServerOff } from "lucide-react"; import type { GetServerSidePropsContext, InferGetServerSidePropsType, @@ -45,8 +44,7 @@ import type { import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; -import React, { useState, type ReactElement } from "react"; -import { toast } from "sonner"; +import { type ReactElement, useState } from "react"; import superjson from "superjson"; type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced"; @@ -54,22 +52,14 @@ type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced"; const Postgresql = ( props: InferGetServerSidePropsType, ) => { - const [toggleMonitoring, setToggleMonitoring] = useState(false); + const [_toggleMonitoring, _setToggleMonitoring] = useState(false); const { postgresId, activeTab } = props; const router = useRouter(); const { projectId } = router.query; const [tab, setSab] = useState(activeTab); const { data } = api.postgres.one.useQuery({ postgresId }); - const { data: auth } = api.auth.get.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: auth?.id || "", - }, - { - enabled: !!auth?.id && auth?.rol === "user", - }, - ); - const { data: monitoring } = api.admin.getMetricsToken.useQuery(); + const { data: auth } = api.user.get.useQuery(); + const { data: isCloud } = api.settings.isCloud.useQuery(); return ( @@ -154,7 +144,7 @@ const Postgresql = (
- {(auth?.rol === "admin" || user?.canDeleteServices) && ( + {(auth?.role === "owner" || auth?.canDeleteServices) && ( )}
@@ -319,7 +309,7 @@ export async function getServerSideProps( ) { const { query, params, req, res } = ctx; const activeTab = query.tab; - const { user, session } = await validateRequest(req, res); + const { user, session } = await validateRequest(req); if (!user) { return { redirect: { @@ -335,8 +325,8 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); @@ -354,7 +344,7 @@ export async function getServerSideProps( activeTab: (activeTab || "general") as TabState, }, }; - } catch (error) { + } catch (_error) { return { redirect: { permanent: false, diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx index 3421e759..c4f40281 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx @@ -23,7 +23,6 @@ import { CardTitle, } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, @@ -34,7 +33,7 @@ import { import { cn } from "@/lib/utils"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; -import { validateRequest } from "@dokploy/server"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; import { HelpCircle, ServerOff } from "lucide-react"; import type { @@ -44,8 +43,7 @@ import type { import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; -import React, { useState, type ReactElement } from "react"; -import { toast } from "sonner"; +import { type ReactElement, useState } from "react"; import superjson from "superjson"; type TabState = "projects" | "monitoring" | "settings" | "advanced"; @@ -53,23 +51,14 @@ type TabState = "projects" | "monitoring" | "settings" | "advanced"; const Redis = ( props: InferGetServerSidePropsType, ) => { - const [toggleMonitoring, setToggleMonitoring] = useState(false); + const [_toggleMonitoring, _setToggleMonitoring] = useState(false); const { redisId, activeTab } = props; const router = useRouter(); const { projectId } = router.query; const [tab, setSab] = useState(activeTab); const { data } = api.redis.one.useQuery({ redisId }); - const { data: auth } = api.auth.get.useQuery(); - const { data: monitoring } = api.admin.getMetricsToken.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: auth?.id || "", - }, - { - enabled: !!auth?.id && auth?.rol === "user", - }, - ); + const { data: auth } = api.user.get.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); @@ -155,7 +144,7 @@ const Redis = (
- {(auth?.rol === "admin" || user?.canDeleteServices) && ( + {(auth?.role === "owner" || auth?.canDeleteServices) && ( )}
@@ -311,7 +300,7 @@ export async function getServerSideProps( const { query, params, req, res } = ctx; const activeTab = query.tab; - const { user, session } = await validateRequest(req, res); + const { user, session } = await validateRequest(req); if (!user) { return { redirect: { @@ -327,8 +316,8 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); @@ -345,7 +334,7 @@ export async function getServerSideProps( activeTab: (activeTab || "general") as TabState, }, }; - } catch (error) { + } catch (_error) { return { redirect: { permanent: false, diff --git a/apps/dokploy/pages/dashboard/projects.tsx b/apps/dokploy/pages/dashboard/projects.tsx index af4d0f1e..5434163a 100644 --- a/apps/dokploy/pages/dashboard/projects.tsx +++ b/apps/dokploy/pages/dashboard/projects.tsx @@ -2,11 +2,10 @@ import { ShowProjects } from "@/components/dashboard/projects/show"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; -import { validateRequest } from "@dokploy/server"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; import dynamic from "next/dynamic"; -import type React from "react"; import type { ReactElement } from "react"; import superjson from "superjson"; @@ -38,7 +37,7 @@ export async function getServerSideProps( ctx: GetServerSidePropsContext<{ serviceId: string }>, ) { const { req, res } = ctx; - const { user, session } = await validateRequest(req, res); + const { user, session } = await validateRequest(req); const helpers = createServerSideHelpers({ router: appRouter, @@ -46,14 +45,14 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); await helpers.settings.isCloud.prefetch(); - await helpers.auth.get.prefetch(); + await helpers.user.get.prefetch(); if (!user) { return { redirect: { diff --git a/apps/dokploy/pages/dashboard/requests.tsx b/apps/dokploy/pages/dashboard/requests.tsx index d1a927e3..cb454587 100644 --- a/apps/dokploy/pages/dashboard/requests.tsx +++ b/apps/dokploy/pages/dashboard/requests.tsx @@ -1,9 +1,9 @@ import { ShowRequests } from "@/components/dashboard/requests/show-requests"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { IS_CLOUD, validateRequest } from "@dokploy/server"; +import { IS_CLOUD } from "@dokploy/server/constants"; +import { validateRequest } from "@dokploy/server/lib/auth"; import type { GetServerSidePropsContext } from "next"; import type { ReactElement } from "react"; -import * as React from "react"; export default function Requests() { return ; @@ -22,7 +22,7 @@ export async function getServerSideProps( }, }; } - const { user } = await validateRequest(ctx.req, ctx.res); + const { user } = await validateRequest(ctx.req); if (!user) { return { redirect: { diff --git a/apps/dokploy/pages/dashboard/settings/billing.tsx b/apps/dokploy/pages/dashboard/settings/billing.tsx index 5c58e25a..7ba5717e 100644 --- a/apps/dokploy/pages/dashboard/settings/billing.tsx +++ b/apps/dokploy/pages/dashboard/settings/billing.tsx @@ -2,10 +2,11 @@ import { ShowBilling } from "@/components/dashboard/settings/billing/show-billin import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { appRouter } from "@/server/api/root"; -import { IS_CLOUD, validateRequest } from "@dokploy/server"; +import { IS_CLOUD } from "@dokploy/server/constants"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Page = () => { @@ -29,8 +30,8 @@ export async function getServerSideProps( }; } const { req, res } = ctx; - const { user, session } = await validateRequest(req, res); - if (!user || user.rol === "user") { + const { user, session } = await validateRequest(req); + if (!user || user.role === "member") { return { redirect: { permanent: true, @@ -45,13 +46,13 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); - await helpers.auth.get.prefetch(); + await helpers.user.get.prefetch(); await helpers.settings.isCloud.prefetch(); diff --git a/apps/dokploy/pages/dashboard/settings/certificates.tsx b/apps/dokploy/pages/dashboard/settings/certificates.tsx index 732b6622..0c82ed4f 100644 --- a/apps/dokploy/pages/dashboard/settings/certificates.tsx +++ b/apps/dokploy/pages/dashboard/settings/certificates.tsx @@ -5,7 +5,7 @@ import { appRouter } from "@/server/api/root"; import { validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Page = () => { return ( @@ -24,8 +24,8 @@ export async function getServerSideProps( ctx: GetServerSidePropsContext<{ serviceId: string }>, ) { const { req, res } = ctx; - const { user, session } = await validateRequest(req, res); - if (!user || user.rol === "user") { + const { user, session } = await validateRequest(req); + if (!user || user.role === "member") { return { redirect: { permanent: true, @@ -40,12 +40,12 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); - await helpers.auth.get.prefetch(); + await helpers.user.get.prefetch(); await helpers.settings.isCloud.prefetch(); return { diff --git a/apps/dokploy/pages/dashboard/settings/cluster.tsx b/apps/dokploy/pages/dashboard/settings/cluster.tsx index a6605c57..a1a46bb6 100644 --- a/apps/dokploy/pages/dashboard/settings/cluster.tsx +++ b/apps/dokploy/pages/dashboard/settings/cluster.tsx @@ -5,7 +5,7 @@ import { appRouter } from "@/server/api/root"; import { IS_CLOUD, validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Page = () => { @@ -33,8 +33,8 @@ export async function getServerSideProps( }, }; } - const { user, session } = await validateRequest(ctx.req, ctx.res); - if (!user || user.rol === "user") { + const { user, session } = await validateRequest(ctx.req); + if (!user || user.role === "member") { return { redirect: { permanent: true, @@ -48,12 +48,12 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); - await helpers.auth.get.prefetch(); + await helpers.user.get.prefetch(); return { props: { diff --git a/apps/dokploy/pages/dashboard/settings/destinations.tsx b/apps/dokploy/pages/dashboard/settings/destinations.tsx index c5c6f2f8..3c906b55 100644 --- a/apps/dokploy/pages/dashboard/settings/destinations.tsx +++ b/apps/dokploy/pages/dashboard/settings/destinations.tsx @@ -5,7 +5,7 @@ import { appRouter } from "@/server/api/root"; import { validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Page = () => { @@ -25,8 +25,8 @@ export async function getServerSideProps( ctx: GetServerSidePropsContext<{ serviceId: string }>, ) { const { req, res } = ctx; - const { user, session } = await validateRequest(req, res); - if (!user || user.rol === "user") { + const { user, session } = await validateRequest(req); + if (!user || user.role === "member") { return { redirect: { permanent: true, @@ -41,12 +41,12 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); - await helpers.auth.get.prefetch(); + await helpers.user.get.prefetch(); await helpers.settings.isCloud.prefetch(); return { diff --git a/apps/dokploy/pages/dashboard/settings/git-providers.tsx b/apps/dokploy/pages/dashboard/settings/git-providers.tsx index dc37522a..7a9b08df 100644 --- a/apps/dokploy/pages/dashboard/settings/git-providers.tsx +++ b/apps/dokploy/pages/dashboard/settings/git-providers.tsx @@ -5,7 +5,7 @@ import { appRouter } from "@/server/api/root"; import { validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Page = () => { @@ -24,7 +24,7 @@ Page.getLayout = (page: ReactElement) => { export async function getServerSideProps( ctx: GetServerSidePropsContext<{ serviceId: string }>, ) { - const { user, session } = await validateRequest(ctx.req, ctx.res); + const { user, session } = await validateRequest(ctx.req); if (!user) { return { redirect: { @@ -40,23 +40,21 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); - await helpers.auth.get.prefetch(); + await helpers.user.get.prefetch(); try { await helpers.project.all.prefetch(); await helpers.settings.isCloud.prefetch(); - const auth = await helpers.auth.get.fetch(); - - if (auth.rol === "user") { - const user = await helpers.user.byAuthId.fetch({ - authId: auth.id, + if (user.role === "member") { + const userR = await helpers.user.one.fetch({ + userId: user.id, }); - if (!user.canAccessToGitProviders) { + if (!userR?.canAccessToGitProviders) { return { redirect: { permanent: true, @@ -70,7 +68,7 @@ export async function getServerSideProps( trpcState: helpers.dehydrate(), }, }; - } catch (error) { + } catch (_error) { return { props: {}, }; diff --git a/apps/dokploy/pages/dashboard/settings/index.tsx b/apps/dokploy/pages/dashboard/settings/index.tsx index bf76607b..4c060cbb 100644 --- a/apps/dokploy/pages/dashboard/settings/index.tsx +++ b/apps/dokploy/pages/dashboard/settings/index.tsx @@ -17,7 +17,6 @@ import { FormField, FormItem, FormLabel, - FormMessage, } from "@/components/ui/form"; import { Switch } from "@/components/ui/switch"; import { appRouter } from "@/server/api/root"; @@ -27,7 +26,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { createServerSideHelpers } from "@trpc/react-query/server"; import { Settings } from "lucide-react"; import type { GetServerSidePropsContext } from "next"; -import React, { useEffect, type ReactElement } from "react"; +import { type ReactElement, useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import superjson from "superjson"; @@ -42,9 +41,9 @@ const settings = z.object({ type SettingsType = z.infer; const Page = () => { - const { data, refetch } = api.admin.one.useQuery(); + const { data, refetch } = api.user.get.useQuery(); const { mutateAsync, isLoading, isError, error } = - api.admin.update.useMutation(); + api.user.update.useMutation(); const form = useForm({ defaultValues: { cleanCacheOnApplications: false, @@ -55,9 +54,9 @@ const Page = () => { }); useEffect(() => { form.reset({ - cleanCacheOnApplications: data?.cleanupCacheApplications || false, - cleanCacheOnCompose: data?.cleanupCacheOnCompose || false, - cleanCacheOnPreviews: data?.cleanupCacheOnPreviews || false, + cleanCacheOnApplications: data?.user.cleanupCacheApplications || false, + cleanCacheOnCompose: data?.user.cleanupCacheOnCompose || false, + cleanCacheOnPreviews: data?.user.cleanupCacheOnPreviews || false, }); }, [form, form.reset, form.formState.isSubmitSuccessful, data]); @@ -181,7 +180,7 @@ export async function getServerSideProps( ctx: GetServerSidePropsContext<{ serviceId: string }>, ) { const { req, res } = ctx; - const { user, session } = await validateRequest(ctx.req, ctx.res); + const { user, session } = await validateRequest(ctx.req); if (!user) { return { redirect: { @@ -190,7 +189,7 @@ export async function getServerSideProps( }, }; } - if (user.rol === "user") { + if (user.role === "member") { return { redirect: { permanent: true, @@ -205,12 +204,12 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); - await helpers.auth.get.prefetch(); + await helpers.user.get.prefetch(); return { props: { diff --git a/apps/dokploy/pages/dashboard/settings/notifications.tsx b/apps/dokploy/pages/dashboard/settings/notifications.tsx index 0b75fc67..fbdc2e20 100644 --- a/apps/dokploy/pages/dashboard/settings/notifications.tsx +++ b/apps/dokploy/pages/dashboard/settings/notifications.tsx @@ -5,7 +5,7 @@ import { appRouter } from "@/server/api/root"; import { validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Page = () => { @@ -25,8 +25,8 @@ export async function getServerSideProps( ctx: GetServerSidePropsContext<{ serviceId: string }>, ) { const { req, res } = ctx; - const { user, session } = await validateRequest(req, res); - if (!user || user.rol === "user") { + const { user, session } = await validateRequest(req); + if (!user || user.role === "member") { return { redirect: { permanent: true, @@ -41,12 +41,12 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); - await helpers.auth.get.prefetch(); + await helpers.user.get.prefetch(); await helpers.settings.isCloud.prefetch(); return { diff --git a/apps/dokploy/pages/dashboard/settings/profile.tsx b/apps/dokploy/pages/dashboard/settings/profile.tsx index 44e007f1..83ff5624 100644 --- a/apps/dokploy/pages/dashboard/settings/profile.tsx +++ b/apps/dokploy/pages/dashboard/settings/profile.tsx @@ -1,6 +1,5 @@ -import { GenerateToken } from "@/components/dashboard/settings/profile/generate-token"; +import { ShowApiKeys } from "@/components/dashboard/settings/api/show-api-keys"; import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form"; -import { RemoveSelfAccount } from "@/components/dashboard/settings/profile/remove-self-account"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { appRouter } from "@/server/api/root"; @@ -9,28 +8,20 @@ import { getLocale, serverSideTranslations } from "@/utils/i18n"; import { validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Page = () => { - const { data } = api.auth.get.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: data?.id || "", - }, - { - enabled: !!data?.id && data?.rol === "user", - }, - ); + const { data } = api.user.get.useQuery(); - const { data: isCloud } = api.settings.isCloud.useQuery(); + // const { data: isCloud } = api.settings.isCloud.useQuery(); return (
- {(user?.canAccessToAPI || data?.rol === "admin") && } + {(data?.canAccessToAPI || data?.role === "owner") && } - {isCloud && } + {/* {isCloud && } */}
); @@ -46,7 +37,7 @@ export async function getServerSideProps( ) { const { req, res } = ctx; const locale = getLocale(req.cookies); - const { user, session } = await validateRequest(req, res); + const { user, session } = await validateRequest(req); const helpers = createServerSideHelpers({ router: appRouter, @@ -54,19 +45,14 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); await helpers.settings.isCloud.prefetch(); - await helpers.auth.get.prefetch(); - if (user?.rol === "user") { - await helpers.user.byAuthId.prefetch({ - authId: user.authId, - }); - } + await helpers.user.get.prefetch(); if (!user) { return { diff --git a/apps/dokploy/pages/dashboard/settings/registry.tsx b/apps/dokploy/pages/dashboard/settings/registry.tsx index 49c9ec20..42f0627f 100644 --- a/apps/dokploy/pages/dashboard/settings/registry.tsx +++ b/apps/dokploy/pages/dashboard/settings/registry.tsx @@ -5,7 +5,7 @@ import { appRouter } from "@/server/api/root"; import { validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Page = () => { @@ -25,8 +25,8 @@ export async function getServerSideProps( ctx: GetServerSidePropsContext<{ serviceId: string }>, ) { const { req, res } = ctx; - const { user, session } = await validateRequest(req, res); - if (!user || user.rol === "user") { + const { user, session } = await validateRequest(req); + if (!user || user.role === "member") { return { redirect: { permanent: true, @@ -40,12 +40,12 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); - await helpers.auth.get.prefetch(); + await helpers.user.get.prefetch(); await helpers.settings.isCloud.prefetch(); return { diff --git a/apps/dokploy/pages/dashboard/settings/server.tsx b/apps/dokploy/pages/dashboard/settings/server.tsx index a229d0bc..0c5e36dc 100644 --- a/apps/dokploy/pages/dashboard/settings/server.tsx +++ b/apps/dokploy/pages/dashboard/settings/server.tsx @@ -1,32 +1,15 @@ -import { SetupMonitoring } from "@/components/dashboard/settings/servers/setup-monitoring"; import { WebDomain } from "@/components/dashboard/settings/web-domain"; import { WebServer } from "@/components/dashboard/settings/web-server"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { DialogAction } from "@/components/shared/dialog-action"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Switch } from "@/components/ui/switch"; - import { appRouter } from "@/server/api/root"; -import { api } from "@/utils/api"; import { getLocale, serverSideTranslations } from "@/utils/i18n"; import { IS_CLOUD, validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; -import { LayoutDashboardIcon } from "lucide-react"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; -import { toast } from "sonner"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Page = () => { - const { data, refetch } = api.admin.one.useQuery(); - const { mutateAsync: update } = api.admin.update.useMutation(); return (
@@ -98,7 +81,7 @@ export async function getServerSideProps( }, }; } - const { user, session } = await validateRequest(ctx.req, ctx.res); + const { user, session } = await validateRequest(ctx.req); if (!user) { return { redirect: { @@ -107,7 +90,7 @@ export async function getServerSideProps( }, }; } - if (user.rol === "user") { + if (user.role === "member") { return { redirect: { permanent: true, @@ -122,12 +105,12 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); - await helpers.auth.get.prefetch(); + await helpers.user.get.prefetch(); return { props: { diff --git a/apps/dokploy/pages/dashboard/settings/servers.tsx b/apps/dokploy/pages/dashboard/settings/servers.tsx index 36fde983..5cc30b83 100644 --- a/apps/dokploy/pages/dashboard/settings/servers.tsx +++ b/apps/dokploy/pages/dashboard/settings/servers.tsx @@ -6,7 +6,7 @@ import { getLocale, serverSideTranslations } from "@/utils/i18n"; import { validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Page = () => { @@ -27,7 +27,7 @@ export async function getServerSideProps( ) { const { req, res } = ctx; const locale = await getLocale(req.cookies); - const { user, session } = await validateRequest(req, res); + const { user, session } = await validateRequest(req); if (!user) { return { redirect: { @@ -36,7 +36,7 @@ export async function getServerSideProps( }, }; } - if (user.rol === "user") { + if (user.role === "member") { return { redirect: { permanent: true, @@ -51,12 +51,12 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); - await helpers.auth.get.prefetch(); + await helpers.user.get.prefetch(); await helpers.settings.isCloud.prefetch(); return { diff --git a/apps/dokploy/pages/dashboard/settings/ssh-keys.tsx b/apps/dokploy/pages/dashboard/settings/ssh-keys.tsx index 239edac6..2472feab 100644 --- a/apps/dokploy/pages/dashboard/settings/ssh-keys.tsx +++ b/apps/dokploy/pages/dashboard/settings/ssh-keys.tsx @@ -5,7 +5,7 @@ import { appRouter } from "@/server/api/root"; import { validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Page = () => { @@ -24,7 +24,7 @@ Page.getLayout = (page: ReactElement) => { export async function getServerSideProps( ctx: GetServerSidePropsContext<{ serviceId: string }>, ) { - const { user, session } = await validateRequest(ctx.req, ctx.res); + const { user, session } = await validateRequest(ctx.req); if (!user) { return { redirect: { @@ -33,30 +33,29 @@ export async function getServerSideProps( }, }; } - const { req, res, resolvedUrl } = ctx; + const { req, res } = ctx; const helpers = createServerSideHelpers({ router: appRouter, ctx: { req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); try { await helpers.project.all.prefetch(); - const auth = await helpers.auth.get.fetch(); await helpers.settings.isCloud.prefetch(); - if (auth.rol === "user") { - const user = await helpers.user.byAuthId.fetch({ - authId: auth.id, + if (user.role === "member") { + const userR = await helpers.user.one.fetch({ + userId: user.id, }); - if (!user.canAccessToSSHKeys) { + if (!userR?.canAccessToSSHKeys) { return { redirect: { permanent: true, @@ -70,7 +69,7 @@ export async function getServerSideProps( trpcState: helpers.dehydrate(), }, }; - } catch (error) { + } catch (_error) { return { props: {}, }; diff --git a/apps/dokploy/pages/dashboard/settings/users.tsx b/apps/dokploy/pages/dashboard/settings/users.tsx index e7072890..16f90abb 100644 --- a/apps/dokploy/pages/dashboard/settings/users.tsx +++ b/apps/dokploy/pages/dashboard/settings/users.tsx @@ -1,3 +1,4 @@ +import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations"; import { ShowUsers } from "@/components/dashboard/settings/users/show-users"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; @@ -5,13 +6,14 @@ import { appRouter } from "@/server/api/root"; import { validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Page = () => { return (
+
); }; @@ -25,8 +27,9 @@ export async function getServerSideProps( ctx: GetServerSidePropsContext<{ serviceId: string }>, ) { const { req, res } = ctx; - const { user, session } = await validateRequest(req, res); - if (!user || user.rol === "user") { + const { user, session } = await validateRequest(req); + + if (!user || user.role === "member") { return { redirect: { permanent: true, @@ -41,12 +44,12 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); - await helpers.auth.get.prefetch(); + await helpers.user.get.prefetch(); await helpers.settings.isCloud.prefetch(); return { diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index 3a8a60b2..15553116 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,7 +1,8 @@ import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { appRouter } from "@/server/api/root"; -import { IS_CLOUD, validateRequest } from "@dokploy/server"; +import { IS_CLOUD } from "@dokploy/server/constants"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; import type { ReactElement } from "react"; @@ -27,7 +28,7 @@ export async function getServerSideProps( }, }; } - const { user, session } = await validateRequest(ctx.req, ctx.res); + const { user, session } = await validateRequest(ctx.req); if (!user) { return { redirect: { @@ -44,21 +45,20 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); try { await helpers.project.all.prefetch(); - const auth = await helpers.auth.get.fetch(); - if (auth.rol === "user") { - const user = await helpers.user.byAuthId.fetch({ - authId: auth.id, + if (user.role === "member") { + const userR = await helpers.user.one.fetch({ + userId: user.id, }); - if (!user.canAccessToDocker) { + if (!userR?.canAccessToDocker) { return { redirect: { permanent: true, @@ -72,7 +72,7 @@ export async function getServerSideProps( trpcState: helpers.dehydrate(), }, }; - } catch (error) { + } catch (_error) { return { props: {}, }; diff --git a/apps/dokploy/pages/dashboard/traefik.tsx b/apps/dokploy/pages/dashboard/traefik.tsx index 9cd7eefc..ce8208be 100644 --- a/apps/dokploy/pages/dashboard/traefik.tsx +++ b/apps/dokploy/pages/dashboard/traefik.tsx @@ -1,10 +1,11 @@ import { ShowTraefikSystem } from "@/components/dashboard/file-system/show-traefik-system"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { appRouter } from "@/server/api/root"; -import { IS_CLOUD, validateRequest } from "@dokploy/server"; +import { IS_CLOUD } from "@dokploy/server/constants"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Dashboard = () => { @@ -27,7 +28,7 @@ export async function getServerSideProps( }, }; } - const { user, session } = await validateRequest(ctx.req, ctx.res); + const { user, session } = await validateRequest(ctx.req); if (!user) { return { redirect: { @@ -44,21 +45,20 @@ export async function getServerSideProps( req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); try { await helpers.project.all.prefetch(); - const auth = await helpers.auth.get.fetch(); - if (auth.rol === "user") { - const user = await helpers.user.byAuthId.fetch({ - authId: auth.id, + if (user.role === "member") { + const userR = await helpers.user.one.fetch({ + userId: user.id, }); - if (!user.canAccessToTraefikFiles) { + if (!userR?.canAccessToTraefikFiles) { return { redirect: { permanent: true, @@ -72,7 +72,7 @@ export async function getServerSideProps( trpcState: helpers.dehydrate(), }, }; - } catch (error) { + } catch (_error) { return { props: {}, }; diff --git a/apps/dokploy/pages/index.tsx b/apps/dokploy/pages/index.tsx index 2c5ab0bb..70aa2f10 100644 --- a/apps/dokploy/pages/index.tsx +++ b/apps/dokploy/pages/index.tsx @@ -1,9 +1,15 @@ -import { Login2FA } from "@/components/auth/login-2fa"; import { OnboardingLayout } from "@/components/layouts/onboarding-layout"; import { AlertBlock } from "@/components/shared/alert-block"; import { Logo } from "@/components/shared/logo"; -import { Button, buttonVariants } from "@/components/ui/button"; -import { CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { CardContent, CardDescription } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Form, FormControl, @@ -13,88 +19,186 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { cn } from "@/lib/utils"; -import { api } from "@/utils/api"; -import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server"; +import { + InputOTP, + InputOTPGroup, + InputOTPSlot, +} from "@/components/ui/input-otp"; +import { Label } from "@/components/ui/label"; +import { authClient } from "@/lib/auth-client"; +import { IS_CLOUD, isAdminPresent } from "@dokploy/server"; +import { validateRequest } from "@dokploy/server/lib/auth"; import { zodResolver } from "@hookform/resolvers/zod"; +import { REGEXP_ONLY_DIGITS } from "input-otp"; import type { GetServerSidePropsContext } from "next"; import Link from "next/link"; import { useRouter } from "next/router"; -import { type ReactElement, useEffect, useState } from "react"; +import { type ReactElement, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; -const loginSchema = z.object({ - email: z - .string() - .min(1, { - message: "Email is required", - }) - .email({ - message: "Email must be a valid email", - }), - - password: z - .string() - .min(1, { - message: "Password is required", - }) - .min(8, { - message: "Password must be at least 8 characters", - }), +const LoginSchema = z.object({ + email: z.string().email(), + password: z.string().min(8), }); -type Login = z.infer; +const _TwoFactorSchema = z.object({ + code: z.string().min(6), +}); -type AuthResponse = { - is2FAEnabled: boolean; - authId: string; -}; +type LoginForm = z.infer; interface Props { IS_CLOUD: boolean; } export default function Home({ IS_CLOUD }: Props) { - const [temp, setTemp] = useState({ - is2FAEnabled: false, - authId: "", - }); - const { mutateAsync, isLoading, error, isError } = - api.auth.login.useMutation(); const router = useRouter(); - const form = useForm({ + const [isLoginLoading, setIsLoginLoading] = useState(false); + const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false); + const [isBackupCodeLoading, setIsBackupCodeLoading] = useState(false); + const [isTwoFactor, setIsTwoFactor] = useState(false); + const [error, setError] = useState(null); + const [twoFactorCode, setTwoFactorCode] = useState(""); + const [isBackupCodeModalOpen, setIsBackupCodeModalOpen] = useState(false); + const [backupCode, setBackupCode] = useState(""); + const [isGithubLoading, setIsGithubLoading] = useState(false); + const [isGoogleLoading, setIsGoogleLoading] = useState(false); + const loginForm = useForm({ + resolver: zodResolver(LoginSchema), defaultValues: { - email: "", - password: "", + email: "siumauricio@hotmail.com", + password: "Password123", }, - resolver: zodResolver(loginSchema), }); - useEffect(() => { - form.reset(); - }, [form, form.reset, form.formState.isSubmitSuccessful]); - - const onSubmit = async (values: Login) => { - await mutateAsync({ - email: values.email.toLowerCase(), - password: values.password, - }) - .then((data) => { - if (data.is2FAEnabled) { - setTemp(data); - } else { - toast.success("Successfully signed in", { - duration: 2000, - }); - router.push("/dashboard/projects"); - } - }) - .catch(() => { - toast.error("Signin failed", { - duration: 2000, - }); + const onSubmit = async (values: LoginForm) => { + setIsLoginLoading(true); + try { + const { data, error } = await authClient.signIn.email({ + email: values.email, + password: values.password, }); + + if (error) { + toast.error(error.message); + setError(error.message || "An error occurred while logging in"); + return; + } + + // @ts-ignore + if (data?.twoFactorRedirect as boolean) { + setTwoFactorCode(""); + setIsTwoFactor(true); + toast.info("Please enter your 2FA code"); + return; + } + + toast.success("Logged in successfully"); + router.push("/dashboard/projects"); + } catch (_error) { + toast.error("An error occurred while logging in"); + } finally { + setIsLoginLoading(false); + } + }; + + const onTwoFactorSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (twoFactorCode.length !== 6) { + toast.error("Please enter a valid 6-digit code"); + return; + } + + setIsTwoFactorLoading(true); + try { + const { error } = await authClient.twoFactor.verifyTotp({ + code: twoFactorCode.replace(/\s/g, ""), + }); + + if (error) { + toast.error(error.message); + setError(error.message || "An error occurred while verifying 2FA code"); + return; + } + + toast.success("Logged in successfully"); + router.push("/dashboard/projects"); + } catch (_error) { + toast.error("An error occurred while verifying 2FA code"); + } finally { + setIsTwoFactorLoading(false); + } + }; + + const onBackupCodeSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (backupCode.length < 8) { + toast.error("Please enter a valid backup code"); + return; + } + + setIsBackupCodeLoading(true); + try { + const { error } = await authClient.twoFactor.verifyBackupCode({ + code: backupCode.trim(), + }); + + if (error) { + toast.error(error.message); + setError( + error.message || "An error occurred while verifying backup code", + ); + return; + } + + toast.success("Logged in successfully"); + router.push("/dashboard/projects"); + } catch (_error) { + toast.error("An error occurred while verifying backup code"); + } finally { + setIsBackupCodeLoading(false); + } + }; + + const handleGithubSignIn = async () => { + setIsGithubLoading(true); + try { + const { error } = await authClient.signIn.social({ + provider: "github", + }); + + if (error) { + toast.error(error.message); + return; + } + } catch (error) { + toast.error("An error occurred while signing in with GitHub", { + description: error instanceof Error ? error.message : "Unknown error", + }); + } finally { + setIsGithubLoading(false); + } + }; + + const handleGoogleSignIn = async () => { + setIsGoogleLoading(true); + try { + const { error } = await authClient.signIn.social({ + provider: "google", + }); + + if (error) { + toast.error(error.message); + return; + } + } catch (error) { + toast.error("An error occurred while signing in with Google", { + description: error instanceof Error ? error.message : "Unknown error", + }); + } finally { + setIsGoogleLoading(false); + } }; return ( <> @@ -109,31 +213,81 @@ export default function Home({ IS_CLOUD }: Props) { Enter your email and password to sign in

- {isError && ( + {error && ( - {error?.message} + {error} )} - {!temp.is2FAEnabled ? ( - - -
+ {!isTwoFactor ? ( + <> + {IS_CLOUD && ( + + )} + {IS_CLOUD && ( + + )} + + ( Email - + )} /> ( @@ -141,7 +295,7 @@ export default function Home({ IS_CLOUD }: Props) { @@ -149,15 +303,127 @@ export default function Home({ IS_CLOUD }: Props) { )} /> - - + + + + ) : ( + <> +
+
+ + + + + + + + + + + + + Enter the 6-digit code from your authenticator app + + +
+ +
+ +
- - ) : ( - + + + + + Enter Backup Code + + Enter one of your backup codes to access your account + + + +
+
+ + setBackupCode(e.target.value)} + placeholder="Enter your backup code" + className="font-mono" + /> + + Enter one of the backup codes you received when setting up + 2FA + +
+ +
+ + +
+
+
+
+ )}
@@ -203,8 +469,7 @@ Home.getLayout = (page: ReactElement) => { export async function getServerSideProps(context: GetServerSidePropsContext) { if (IS_CLOUD) { try { - const { user } = await validateRequest(context.req, context.res); - + const { user } = await validateRequest(context.req); if (user) { return { redirect: { @@ -213,7 +478,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }, }; } - } catch (error) {} + } catch (_error) {} return { props: { @@ -232,7 +497,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; } - const { user } = await validateRequest(context.req, context.res); + const { user } = await validateRequest(context.req); if (user) { return { diff --git a/apps/dokploy/pages/invitation.tsx b/apps/dokploy/pages/invitation.tsx index 77f9f249..32026c0e 100644 --- a/apps/dokploy/pages/invitation.tsx +++ b/apps/dokploy/pages/invitation.tsx @@ -1,12 +1,8 @@ import { OnboardingLayout } from "@/components/layouts/onboarding-layout"; +import { AlertBlock } from "@/components/shared/alert-block"; import { Logo } from "@/components/shared/logo"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardTitle, -} from "@/components/ui/card"; +import { CardContent, CardDescription, CardTitle } from "@/components/ui/card"; import { Form, FormControl, @@ -16,10 +12,10 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { authClient } from "@/lib/auth-client"; import { api } from "@/utils/api"; import { IS_CLOUD, getUserByToken } from "@dokploy/server"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle } from "lucide-react"; import type { GetServerSidePropsContext } from "next"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -30,6 +26,9 @@ import { z } from "zod"; const registerSchema = z .object({ + name: z.string().min(1, { + message: "Name is required", + }), email: z .string() .min(1, { @@ -38,7 +37,6 @@ const registerSchema = z .email({ message: "Email must be a valid email", }), - password: z .string() .min(1, { @@ -71,11 +69,17 @@ interface Props { token: string; invitation: Awaited>; isCloud: boolean; + userAlreadyExists: boolean; } -const Invitation = ({ token, invitation, isCloud }: Props) => { +const Invitation = ({ + token, + invitation, + isCloud, + userAlreadyExists, +}: Props) => { const router = useRouter(); - const { data } = api.admin.getUserByToken.useQuery( + const { data } = api.user.getUserByToken.useQuery( { token, }, @@ -85,11 +89,9 @@ const Invitation = ({ token, invitation, isCloud }: Props) => { }, ); - const { mutateAsync, error, isError, isSuccess } = - api.auth.createUser.useMutation(); - const form = useForm({ defaultValues: { + name: "", email: "", password: "", confirmPassword: "", @@ -98,9 +100,9 @@ const Invitation = ({ token, invitation, isCloud }: Props) => { }); useEffect(() => { - if (data?.auth?.email) { + if (data?.email) { form.reset({ - email: data?.auth?.email || "", + email: data?.email || "", password: "", confirmPassword: "", }); @@ -108,20 +110,32 @@ const Invitation = ({ token, invitation, isCloud }: Props) => { }, [form, form.reset, form.formState.isSubmitSuccessful, data]); const onSubmit = async (values: Register) => { - await mutateAsync({ - id: data?.authId, - password: values.password, - token: token, - }) - .then(() => { - toast.success("User registered successfuly", { - description: - "Please check your inbox or spam folder to confirm your account.", - duration: 100000, - }); - router.push("/dashboard/projects"); - }) - .catch((e) => e); + try { + const { error } = await authClient.signUp.email({ + email: values.email, + password: values.password, + name: values.name, + fetchOptions: { + headers: { + "x-dokploy-token": token, + }, + }, + }); + + if (error) { + toast.error(error.message); + return; + } + + const _result = await authClient.organization.acceptInvitation({ + invitationId: token, + }); + + toast.success("Account created successfully"); + router.push("/dashboard/projects"); + } catch (_error) { + toast.error("An error occurred while creating your account"); + } }; return ( @@ -138,114 +152,155 @@ const Invitation = ({ token, invitation, isCloud }: Props) => { Invitation - - Fill the form below to create your account - -
-
+ {userAlreadyExists ? ( +
+ +
+ Valid Invitation! + + We detected that you already have an account with this + email. Please sign in to accept the invitation. + +
+
- {isError && ( -
- - - {error?.message} - -
- )} + +
+ ) : ( + <> + + Fill the form below to create your account + +
+
- -
- -
- ( - - Email - - - - - - )} - /> - ( - - Password - - - - - - )} - /> + {/* {isError && ( +
+ + + {error?.message} + +
+ )} */} - ( - - Confirm Password - - - - - - )} - /> - - -
+
+ ( + + Name + + + + + + )} + /> + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> -
- {isCloud && ( - <> - ( + + Confirm Password + + + + + + )} + /> + +
- - - -
+ Register + +
+ +
+ {isCloud && ( + <> + + Login + + + Lost your password? + + + )} +
+ + + +
+ + )}
); }; - +// http://localhost:3000/invitation?token=CZK4BLrUdMa32RVkAdZiLsPDdvnPiAgZ +// /f7af93acc1a99eae864972ab4c92fee089f0d83473d415ede8e821e5dbabe79c export default Invitation; Invitation.getLayout = (page: ReactElement) => { return {page}; @@ -255,6 +310,15 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { const token = query.token; + // if (IS_CLOUD) { + // return { + // redirect: { + // permanent: true, + // destination: "/", + // }, + // }; + // } + if (typeof token !== "string") { return { redirect: { @@ -267,6 +331,17 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { try { const invitation = await getUserByToken(token); + if (invitation.userAlreadyExists) { + return { + props: { + isCloud: IS_CLOUD, + token: token, + invitation: invitation, + userAlreadyExists: true, + }, + }; + } + if (invitation.isExpired) { return { redirect: { @@ -284,6 +359,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { }, }; } catch (error) { + console.log("error", error); return { redirect: { permanent: true, diff --git a/apps/dokploy/pages/register.tsx b/apps/dokploy/pages/register.tsx index e42231a2..980f641f 100644 --- a/apps/dokploy/pages/register.tsx +++ b/apps/dokploy/pages/register.tsx @@ -2,12 +2,7 @@ import { OnboardingLayout } from "@/components/layouts/onboarding-layout"; import { AlertBlock } from "@/components/shared/alert-block"; import { Logo } from "@/components/shared/logo"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardTitle, -} from "@/components/ui/card"; +import { CardContent, CardDescription, CardTitle } from "@/components/ui/card"; import { Form, FormControl, @@ -17,20 +12,23 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; +import { authClient } from "@/lib/auth-client"; import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server"; import { zodResolver } from "@hookform/resolvers/zod"; import { AlertTriangle } from "lucide-react"; import type { GetServerSidePropsContext } from "next"; import Link from "next/link"; import { useRouter } from "next/router"; -import { type ReactElement, useEffect } from "react"; +import { type ReactElement, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; const registerSchema = z .object({ + name: z.string().min(1, { + message: "Name is required", + }), email: z .string() .min(1, { @@ -74,14 +72,16 @@ interface Props { const Register = ({ isCloud }: Props) => { const router = useRouter(); - const { mutateAsync, error, isError, data } = - api.auth.createAdmin.useMutation(); + const [isError, setIsError] = useState(false); + const [error, setError] = useState(null); + const [data, setData] = useState(null); const form = useForm({ defaultValues: { - email: "", - password: "", - confirmPassword: "", + name: "Mauricio Siu", + email: "user5@yopmail.com", + password: "Password123", + confirmPassword: "Password123", }, resolver: zodResolver(registerSchema), }); @@ -91,19 +91,25 @@ const Register = ({ isCloud }: Props) => { }, [form, form.reset, form.formState.isSubmitSuccessful]); const onSubmit = async (values: Register) => { - await mutateAsync({ - email: values.email.toLowerCase(), + const { data, error } = await authClient.signUp.email({ + email: values.email, password: values.password, - }) - .then(() => { - toast.success("User registered successfuly", { - duration: 2000, - }); - if (!isCloud) { - router.push("/"); - } - }) - .catch((e) => e); + name: values.name, + }); + + if (error) { + setIsError(true); + setError(error.message || "An error occurred"); + } else { + toast.success("User registered successfuly", { + duration: 2000, + }); + if (!isCloud) { + router.push("/"); + } else { + setData(data); + } + } }; return (
@@ -125,15 +131,15 @@ const Register = ({ isCloud }: Props) => {
{isError && ( -
+
- {error?.message} + {error}
)} - {data?.type === "cloud" && ( - + {isCloud && data && ( + Registered successfully, please check your inbox or spam folder to confirm your account. @@ -147,6 +153,19 @@ const Register = ({ isCloud }: Props) => { className="grid gap-4" >
+ ( + + Name + + + + + + )} + /> { }; export async function getServerSideProps(context: GetServerSidePropsContext) { if (IS_CLOUD) { - const { user } = await validateRequest(context.req, context.res); + const { user } = await validateRequest(context.req); if (user) { return { diff --git a/apps/dokploy/pages/reset-password.tsx b/apps/dokploy/pages/reset-password.tsx index 42e2ce00..0f6cf0b3 100644 --- a/apps/dokploy/pages/reset-password.tsx +++ b/apps/dokploy/pages/reset-password.tsx @@ -12,17 +12,13 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { db } from "@/server/db"; -import { auth } from "@/server/db/schema"; -import { api } from "@/utils/api"; +import { authClient } from "@/lib/auth-client"; import { IS_CLOUD } from "@dokploy/server"; import { zodResolver } from "@hookform/resolvers/zod"; -import { isBefore } from "date-fns"; -import { eq } from "drizzle-orm"; import type { GetServerSidePropsContext } from "next"; import Link from "next/link"; import { useRouter } from "next/router"; -import { type ReactElement, useEffect } from "react"; +import { type ReactElement, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; @@ -54,11 +50,12 @@ const loginSchema = z type Login = z.infer; interface Props { - token: string; + tokenResetPassword: string; } -export default function Home({ token }: Props) { - const { mutateAsync, isLoading, isError, error } = - api.auth.resetPassword.useMutation(); +export default function Home({ tokenResetPassword }: Props) { + const [token, setToken] = useState(tokenResetPassword); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); const router = useRouter(); const form = useForm({ defaultValues: { @@ -68,26 +65,32 @@ export default function Home({ token }: Props) { resolver: zodResolver(loginSchema), }); + useEffect(() => { + const token = new URLSearchParams(window.location.search).get("token"); + + if (token) { + setToken(token); + } + }, [token]); + useEffect(() => { form.reset(); }, [form, form.reset, form.formState.isSubmitSuccessful]); const onSubmit = async (values: Login) => { - await mutateAsync({ - resetPasswordToken: token, - password: values.password, - }) - .then((data) => { - toast.success("Password reset successfully", { - duration: 2000, - }); - router.push("/"); - }) - .catch(() => { - toast.error("Error resetting password", { - duration: 2000, - }); - }); + setIsLoading(true); + const { error } = await authClient.resetPassword({ + newPassword: values.password, + token: token || "", + }); + + if (error) { + setError(error.message || "An error occurred"); + } else { + toast.success("Password reset successfully"); + router.push("/"); + } + setIsLoading(false); }; return (
@@ -104,9 +107,9 @@ export default function Home({ token }: Props) {
- {isError && ( + {error && ( - {error?.message} + {error} )}
@@ -194,35 +197,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; } - const authR = await db.query.auth.findFirst({ - where: eq(auth.resetPasswordToken, token), - }); - - if (!authR || authR?.resetPasswordExpiresAt === null) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - const isExpired = isBefore( - new Date(authR.resetPasswordExpiresAt), - new Date(), - ); - - if (isExpired) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - return { props: { - token: authR.resetPasswordToken, + tokenResetPassword: token, }, }; } diff --git a/apps/dokploy/pages/send-reset-password.tsx b/apps/dokploy/pages/send-reset-password.tsx index c4cd851c..10d1058a 100644 --- a/apps/dokploy/pages/send-reset-password.tsx +++ b/apps/dokploy/pages/send-reset-password.tsx @@ -1,14 +1,8 @@ -import { Login2FA } from "@/components/auth/login-2fa"; import { OnboardingLayout } from "@/components/layouts/onboarding-layout"; import { AlertBlock } from "@/components/shared/alert-block"; import { Logo } from "@/components/shared/logo"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardTitle, -} from "@/components/ui/card"; +import { CardContent, CardDescription, CardTitle } from "@/components/ui/card"; import { Form, FormControl, @@ -18,7 +12,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; +import { authClient } from "@/lib/auth-client"; import { IS_CLOUD } from "@dokploy/server"; import { zodResolver } from "@hookform/resolvers/zod"; import type { GetServerSidePropsContext } from "next"; @@ -48,13 +42,14 @@ type AuthResponse = { }; export default function Home() { - const [temp, setTemp] = useState({ + const [temp, _setTemp] = useState({ is2FAEnabled: false, authId: "", }); - const { mutateAsync, isLoading, isError, error } = - api.auth.sendResetPasswordEmail.useMutation(); - const router = useRouter(); + + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const _router = useRouter(); const form = useForm({ defaultValues: { email: "", @@ -67,19 +62,20 @@ export default function Home() { }, [form, form.reset, form.formState.isSubmitSuccessful]); const onSubmit = async (values: Login) => { - await mutateAsync({ + setIsLoading(true); + const { error } = await authClient.forgetPassword({ email: values.email, - }) - .then((data) => { - toast.success("Email sent", { - duration: 2000, - }); - }) - .catch(() => { - toast.error("Error sending email", { - duration: 2000, - }); + redirectTo: "/reset-password", + }); + if (error) { + setError(error.message || "An error occurred"); + setIsLoading(false); + } else { + toast.success("Email sent", { + duration: 2000, }); + } + setIsLoading(false); }; return (
@@ -95,9 +91,9 @@ export default function Home() {
- {isError && ( + {error && ( - {error?.message} + {error} )} {!temp.is2FAEnabled ? ( @@ -131,9 +127,7 @@ export default function Home() {
- ) : ( - - )} + ) : null}
@@ -155,7 +149,7 @@ export default function Home() { Home.getLayout = (page: ReactElement) => { return {page}; }; -export async function getServerSideProps(context: GetServerSidePropsContext) { +export async function getServerSideProps(_context: GetServerSidePropsContext) { if (!IS_CLOUD) { return { redirect: { diff --git a/apps/dokploy/pages/swagger.tsx b/apps/dokploy/pages/swagger.tsx index b5deb2ac..11ea0731 100644 --- a/apps/dokploy/pages/swagger.tsx +++ b/apps/dokploy/pages/swagger.tsx @@ -30,7 +30,41 @@ const Home: NextPage = () => { return (
- + (args: any) => { + const result = ori(args); + const apiKey = args?.apiKey?.value; + if (apiKey) { + localStorage.setItem("swagger_api_key", apiKey); + } + return result; + }, + logout: (ori: any) => (args: any) => { + const result = ori(args); + localStorage.removeItem("swagger_api_key"); + return result; + }, + }, + }, + }, + }, + ]} + requestInterceptor={(request: any) => { + const apiKey = localStorage.getItem("swagger_api_key"); + if (apiKey) { + request.headers = request.headers || {}; + request.headers["x-api-key"] = apiKey; + } + return request; + }} + />
); }; @@ -38,7 +72,7 @@ const Home: NextPage = () => { export default Home; export async function getServerSideProps(context: GetServerSidePropsContext) { const { req, res } = context; - const { user, session } = await validateRequest(context.req, context.res); + const { user, session } = await validateRequest(context.req); if (!user) { return { redirect: { @@ -53,17 +87,17 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { req: req as any, res: res as any, db: null as any, - session: session, - user: user, + session: session as any, + user: user as any, }, transformer: superjson, }); - if (user.rol === "user") { - const result = await helpers.user.byAuthId.fetch({ - authId: user.id, + if (user.role === "member") { + const userR = await helpers.user.one.fetch({ + userId: user.id, }); - if (!result.canAccessToAPI) { + if (!userR?.canAccessToAPI) { return { redirect: { permanent: true, diff --git a/apps/dokploy/public/templates/appwrite.svg b/apps/dokploy/public/templates/appwrite.svg new file mode 100644 index 00000000..2034a812 --- /dev/null +++ b/apps/dokploy/public/templates/appwrite.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/apps/dokploy/public/templates/convex.svg b/apps/dokploy/public/templates/convex.svg new file mode 100644 index 00000000..8622c4c0 --- /dev/null +++ b/apps/dokploy/public/templates/convex.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/dokploy/public/templates/linkwarden.png b/apps/dokploy/public/templates/linkwarden.png new file mode 100644 index 00000000..843f681e Binary files /dev/null and b/apps/dokploy/public/templates/linkwarden.png differ diff --git a/apps/dokploy/public/templates/mailpit.svg b/apps/dokploy/public/templates/mailpit.svg new file mode 100644 index 00000000..58675a26 --- /dev/null +++ b/apps/dokploy/public/templates/mailpit.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apps/dokploy/public/templates/outline.png b/apps/dokploy/public/templates/outline.png new file mode 100644 index 00000000..b241f01d Binary files /dev/null and b/apps/dokploy/public/templates/outline.png differ diff --git a/apps/dokploy/public/templates/pocket-id.svg b/apps/dokploy/public/templates/pocket-id.svg new file mode 100644 index 00000000..0ee89b14 --- /dev/null +++ b/apps/dokploy/public/templates/pocket-id.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/dokploy/public/templates/registry.png b/apps/dokploy/public/templates/registry.png new file mode 100644 index 00000000..39418022 Binary files /dev/null and b/apps/dokploy/public/templates/registry.png differ diff --git a/apps/dokploy/public/templates/trilium.png b/apps/dokploy/public/templates/trilium.png new file mode 100644 index 00000000..f6afe82f Binary files /dev/null and b/apps/dokploy/public/templates/trilium.png differ diff --git a/apps/dokploy/public/templates/wikijs.svg b/apps/dokploy/public/templates/wikijs.svg new file mode 100644 index 00000000..78073b23 --- /dev/null +++ b/apps/dokploy/public/templates/wikijs.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/dokploy/reset-password.ts b/apps/dokploy/reset-password.ts index 43b11fdf..32cab433 100644 --- a/apps/dokploy/reset-password.ts +++ b/apps/dokploy/reset-password.ts @@ -1,6 +1,8 @@ import { findAdmin } from "@dokploy/server"; -import { updateAuthById } from "@dokploy/server"; import { generateRandomPassword } from "@dokploy/server"; +import { db } from "@dokploy/server/db"; +import { account } from "@dokploy/server/db/schema"; +import { eq } from "drizzle-orm"; (async () => { try { @@ -8,9 +10,12 @@ import { generateRandomPassword } from "@dokploy/server"; const result = await findAdmin(); - const update = await updateAuthById(result.authId, { - password: randomPassword.hashedPassword, - }); + const update = await db + .update(account) + .set({ + password: randomPassword.hashedPassword, + }) + .where(eq(account.userId, result.userId)); if (update) { console.log("Password reset successful"); diff --git a/apps/dokploy/server/api/root.ts b/apps/dokploy/server/api/root.ts index 68f5e4e0..5f5d0bcc 100644 --- a/apps/dokploy/server/api/root.ts +++ b/apps/dokploy/server/api/root.ts @@ -19,6 +19,7 @@ import { mongoRouter } from "./routers/mongo"; import { mountRouter } from "./routers/mount"; import { mysqlRouter } from "./routers/mysql"; import { notificationRouter } from "./routers/notification"; +import { organizationRouter } from "./routers/organization"; import { portRouter } from "./routers/port"; import { postgresRouter } from "./routers/postgres"; import { previewDeploymentRouter } from "./routers/preview-deployment"; @@ -33,7 +34,6 @@ import { sshRouter } from "./routers/ssh-key"; import { stripeRouter } from "./routers/stripe"; import { swarmRouter } from "./routers/swarm"; import { userRouter } from "./routers/user"; - /** * This is the primary router for your server. * @@ -75,6 +75,7 @@ export const appRouter = createTRPCRouter({ server: serverRouter, stripe: stripeRouter, swarm: swarmRouter, + organization: organizationRouter, }); // export type definition of API diff --git a/apps/dokploy/server/api/routers/admin.ts b/apps/dokploy/server/api/routers/admin.ts index 31612fe0..47bd9cd9 100644 --- a/apps/dokploy/server/api/routers/admin.ts +++ b/apps/dokploy/server/api/routers/admin.ts @@ -1,119 +1,14 @@ -import { db } from "@/server/db"; -import { - apiAssignPermissions, - apiCreateUserInvitation, - apiFindOneToken, - apiRemoveUser, - apiUpdateAdmin, - apiUpdateWebServerMonitoring, - users, -} from "@/server/db/schema"; +import { apiUpdateWebServerMonitoring } from "@/server/db/schema"; import { IS_CLOUD, - createInvitation, - findAdminById, - findUserByAuthId, findUserById, - getUserByToken, - removeUserByAuthId, setupWebMonitoring, - updateAdmin, - updateAdminById, + updateUser, } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; -import { eq } from "drizzle-orm"; -import { z } from "zod"; -import { - adminProcedure, - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "../trpc"; +import { adminProcedure, createTRPCRouter } from "../trpc"; export const adminRouter = createTRPCRouter({ - one: adminProcedure.query(async ({ ctx }) => { - const { sshPrivateKey, ...rest } = await findAdminById(ctx.user.adminId); - return { - haveSSH: !!sshPrivateKey, - ...rest, - }; - }), - update: adminProcedure - .input(apiUpdateAdmin) - .mutation(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to update this admin", - }); - } - const { authId } = await findAdminById(ctx.user.adminId); - // @ts-ignore - return updateAdmin(authId, input); - }), - createUserInvitation: adminProcedure - .input(apiCreateUserInvitation) - .mutation(async ({ input, ctx }) => { - try { - await createInvitation(input, ctx.user.adminId); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: - "Error creating this user\ncheck if the email is not registered", - cause: error, - }); - } - }), - removeUser: adminProcedure - .input(apiRemoveUser) - .mutation(async ({ input, ctx }) => { - try { - const user = await findUserByAuthId(input.authId); - - if (user.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to delete this user", - }); - } - return await removeUserByAuthId(input.authId); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error deleting this user", - cause: error, - }); - } - }), - getUserByToken: publicProcedure - .input(apiFindOneToken) - .query(async ({ input }) => { - return await getUserByToken(input.token); - }), - assignPermissions: adminProcedure - .input(apiAssignPermissions) - .mutation(async ({ input, ctx }) => { - try { - const user = await findUserById(input.userId); - - if (user.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to assign permissions", - }); - } - await db - .update(users) - .set({ - ...input, - }) - .where(eq(users.userId, input.userId)); - } catch (error) { - throw error; - } - }), - setupMonitoring: adminProcedure .input(apiUpdateWebServerMonitoring) .mutation(async ({ input, ctx }) => { @@ -124,15 +19,15 @@ export const adminRouter = createTRPCRouter({ message: "Feature disabled on cloud", }); } - const admin = await findAdminById(ctx.user.adminId); - if (admin.adminId !== ctx.user.adminId) { + const user = await findUserById(ctx.user.ownerId); + if (user.id !== ctx.user.ownerId) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "You are not authorized to setup this server", + message: "You are not authorized to setup the monitoring", }); } - await updateAdminById(admin.adminId, { + await updateUser(user.id, { metricsConfig: { server: { type: "Dokploy", @@ -156,135 +51,11 @@ export const adminRouter = createTRPCRouter({ }, }, }); - const currentServer = await setupWebMonitoring(admin.adminId); + + const currentServer = await setupWebMonitoring(user.id); return currentServer; } catch (error) { throw error; } }), - getMetricsToken: protectedProcedure.query(async ({ ctx }) => { - const admin = await findAdminById(ctx.user.adminId); - return { - serverIp: admin.serverIp, - enabledFeatures: admin.enablePaidFeatures, - metricsConfig: admin?.metricsConfig, - }; - }), - - getServerMetrics: protectedProcedure - .input( - z.object({ - url: z.string(), - token: z.string(), - dataPoints: z.string(), - }), - ) - .query(async ({ ctx, input }) => { - try { - const url = new URL(input.url); - url.searchParams.append("limit", input.dataPoints); - const response = await fetch(url.toString(), { - headers: { - Authorization: `Bearer ${input.token}`, - }, - }); - if (!response.ok) { - throw new Error( - `Error ${response.status}: ${response.statusText}. Ensure the container is running and this service is included in the monitoring configuration.`, - ); - } - - const data = await response.json(); - if (!Array.isArray(data) || data.length === 0) { - throw new Error( - [ - "No monitoring data available. This could be because:", - "", - "1. You don't have setup the monitoring service, you can do in web server section.", - "2. If you already have setup the monitoring service, wait a few minutes and refresh the page.", - ].join("\n"), - ); - } - return data as { - cpu: string; - cpuModel: string; - cpuCores: number; - cpuPhysicalCores: number; - cpuSpeed: number; - os: string; - distro: string; - kernel: string; - arch: string; - memUsed: string; - memUsedGB: string; - memTotal: string; - uptime: number; - diskUsed: string; - totalDisk: string; - networkIn: string; - networkOut: string; - timestamp: string; - }[]; - } catch (error) { - throw error; - } - }), - getContainerMetrics: protectedProcedure - .input( - z.object({ - url: z.string(), - token: z.string(), - appName: z.string(), - dataPoints: z.string(), - }), - ) - .query(async ({ ctx, input }) => { - try { - if (!input.appName) { - throw new Error( - [ - "No Application Selected:", - "", - "Make Sure to select an application to monitor.", - ].join("\n"), - ); - } - const url = new URL(`${input.url}/metrics/containers`); - url.searchParams.append("limit", input.dataPoints); - url.searchParams.append("appName", input.appName); - const response = await fetch(url.toString(), { - headers: { - Authorization: `Bearer ${input.token}`, - }, - }); - if (!response.ok) { - throw new Error( - `Error ${response.status}: ${response.statusText}. Please verify that the application "${input.appName}" is running and this service is included in the monitoring configuration.`, - ); - } - - const data = await response.json(); - if (!Array.isArray(data) || data.length === 0) { - throw new Error( - [ - `No monitoring data available for "${input.appName}". This could be because:`, - "", - "1. The container was recently started - wait a few minutes for data to be collected", - "2. The container is not running - verify its status", - "3. The service is not included in your monitoring configuration", - ].join("\n"), - ); - } - return data as { - containerId: string; - containerName: string; - containerImage: string; - containerLabels: string; - containerCommand: string; - containerCreated: string; - }[]; - } catch (error) { - throw error; - } - }), }); diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 13b7c80d..e1629b4c 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -60,8 +60,13 @@ export const applicationRouter = createTRPCRouter({ .input(apiCreateApplication) .mutation(async ({ input, ctx }) => { try { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.projectId, + ctx.session.activeOrganizationId, + "create", + ); } if (IS_CLOUD && !input.serverId) { @@ -72,7 +77,7 @@ export const applicationRouter = createTRPCRouter({ } const project = await findProjectById(input.projectId); - if (project.adminId !== ctx.user.adminId) { + if (project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this project", @@ -80,8 +85,12 @@ export const applicationRouter = createTRPCRouter({ } const newApplication = await createApplication(input); - if (ctx.user.rol === "user") { - await addNewService(ctx.user.authId, newApplication.applicationId); + if (ctx.user.rol === "member") { + await addNewService( + ctx.user.id, + newApplication.applicationId, + project.organizationId, + ); } return newApplication; } catch (error: unknown) { @@ -98,15 +107,18 @@ export const applicationRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOneApplication) .query(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { + if (ctx.user.rol === "member") { await checkServiceAccess( - ctx.user.authId, + ctx.user.id, input.applicationId, + ctx.session.activeOrganizationId, "access", ); } const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", @@ -119,7 +131,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiReloadApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to reload this application", @@ -144,16 +158,19 @@ export const applicationRouter = createTRPCRouter({ delete: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { + if (ctx.user.rol === "member") { await checkServiceAccess( - ctx.user.authId, + ctx.user.id, input.applicationId, + ctx.session.activeOrganizationId, "delete", ); } const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this application", @@ -184,7 +201,7 @@ export const applicationRouter = createTRPCRouter({ for (const operation of cleanupOperations) { try { await operation(); - } catch (error) {} + } catch (_) {} } return result[0]; @@ -194,7 +211,7 @@ export const applicationRouter = createTRPCRouter({ .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const service = await findApplicationById(input.applicationId); - if (service.project.adminId !== ctx.user.adminId) { + if (service.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to stop this application", @@ -214,7 +231,7 @@ export const applicationRouter = createTRPCRouter({ .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const service = await findApplicationById(input.applicationId); - if (service.project.adminId !== ctx.user.adminId) { + if (service.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to start this application", @@ -235,7 +252,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to redeploy this application", @@ -268,7 +287,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiSaveEnvironmentVariables) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this environment", @@ -284,7 +305,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiSaveBuildType) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this build type", @@ -305,7 +328,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiSaveGithubProvider) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this github provider", @@ -327,7 +352,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiSaveGitlabProvider) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this gitlab provider", @@ -351,7 +378,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiSaveBitbucketProvider) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this bitbucket provider", @@ -373,7 +402,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiSaveDockerProvider) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this docker provider", @@ -394,7 +425,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiSaveGitProvider) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this git provider", @@ -415,7 +448,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to mark this application as running", @@ -427,7 +462,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiUpdateApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this application", @@ -451,7 +488,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to refresh this application", @@ -466,7 +505,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this application", @@ -500,7 +541,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to clean this application", @@ -513,7 +556,9 @@ export const applicationRouter = createTRPCRouter({ .input(apiFindOneApplication) .query(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to read this application", @@ -548,7 +593,7 @@ export const applicationRouter = createTRPCRouter({ const app = await findApplicationById(input.applicationId as string); - if (app.project.adminId !== ctx.user.adminId) { + if (app.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this application", @@ -590,7 +635,9 @@ export const applicationRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this application", @@ -610,7 +657,7 @@ export const applicationRouter = createTRPCRouter({ }), readAppMonitoring: protectedProcedure .input(apiFindMonitoringStats) - .query(async ({ input, ctx }) => { + .query(async ({ input }) => { if (IS_CLOUD) { throw new TRPCError({ code: "UNAUTHORIZED", diff --git a/apps/dokploy/server/api/routers/auth.ts b/apps/dokploy/server/api/routers/auth.ts index f8bbfa9b..2c7469c3 100644 --- a/apps/dokploy/server/api/routers/auth.ts +++ b/apps/dokploy/server/api/routers/auth.ts @@ -1,523 +1,326 @@ -import { - apiCreateAdmin, - apiCreateUser, - apiFindOneAuth, - apiLogin, - apiUpdateAuth, - apiVerify2FA, - apiVerifyLogin2FA, - auth, -} from "@/server/db/schema"; -import { WEBSITE_URL } from "@/server/utils/stripe"; -import { - type Auth, - IS_CLOUD, - createAdmin, - createUser, - findAuthByEmail, - findAuthById, - generate2FASecret, - getUserByToken, - lucia, - luciaToken, - removeAdminByAuthId, - removeUserByAuthId, - sendDiscordNotification, - sendEmailNotification, - updateAuthById, - validateRequest, - verify2FA, -} from "@dokploy/server"; -import { TRPCError } from "@trpc/server"; -import * as bcrypt from "bcrypt"; -import { isBefore } from "date-fns"; -import { eq } from "drizzle-orm"; -import { nanoid } from "nanoid"; -import { z } from "zod"; -import { db } from "../../db"; -import { - adminProcedure, - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "../trpc"; +import { createTRPCRouter } from "../trpc"; export const authRouter = createTRPCRouter({ - createAdmin: publicProcedure - .input(apiCreateAdmin) - .mutation(async ({ ctx, input }) => { - try { - if (!IS_CLOUD) { - const admin = await db.query.admins.findFirst({}); - if (admin) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Admin already exists", - }); - } - } - const newAdmin = await createAdmin(input); - - if (IS_CLOUD) { - await sendDiscordNotificationWelcome(newAdmin); - await sendVerificationEmail(newAdmin.id); - return { - status: "success", - type: "cloud", - }; - } - const session = await lucia.createSession(newAdmin.id || "", {}); - ctx.res.appendHeader( - "Set-Cookie", - lucia.createSessionCookie(session.id).serialize(), - ); - return { - status: "success", - type: "selfhosted", - }; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - // @ts-ignore - message: `Error: ${error?.code === "23505" ? "Email already exists" : "Error creating admin"}`, - cause: error, - }); - } - }), - createUser: publicProcedure - .input(apiCreateUser) - .mutation(async ({ ctx, input }) => { - try { - const token = await getUserByToken(input.token); - if (token.isExpired) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Invalid token", - }); - } - - const newUser = await createUser(input); - - if (IS_CLOUD) { - await sendVerificationEmail(token.authId); - return true; - } - const session = await lucia.createSession(newUser?.authId || "", {}); - ctx.res.appendHeader( - "Set-Cookie", - lucia.createSessionCookie(session.id).serialize(), - ); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the user", - cause: error, - }); - } - }), - - login: publicProcedure.input(apiLogin).mutation(async ({ ctx, input }) => { - try { - const auth = await findAuthByEmail(input.email); - - const correctPassword = bcrypt.compareSync( - input.password, - auth?.password || "", - ); - - if (!correctPassword) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Credentials do not match", - }); - } - - if (auth?.confirmationToken && IS_CLOUD) { - await sendVerificationEmail(auth.id); - throw new TRPCError({ - code: "BAD_REQUEST", - message: - "Email not confirmed, we have sent you a confirmation email please check your inbox.", - }); - } - - if (auth?.is2FAEnabled) { - return { - is2FAEnabled: true, - authId: auth.id, - }; - } - - const session = await lucia.createSession(auth?.id || "", {}); - - ctx.res.appendHeader( - "Set-Cookie", - lucia.createSessionCookie(session.id).serialize(), - ); - return { - is2FAEnabled: false, - authId: auth?.id, - }; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `Error: ${error instanceof Error ? error.message : "Login error"}`, - cause: error, - }); - } - }), - - get: protectedProcedure.query(async ({ ctx }) => { - const auth = await findAuthById(ctx.user.authId); - return auth; - }), - - logout: protectedProcedure.mutation(async ({ ctx }) => { - const { req, res } = ctx; - const { session } = await validateRequest(req, res); - if (!session) return false; - - await lucia.invalidateSession(session.id); - res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()); - return true; - }), - - update: protectedProcedure - .input(apiUpdateAuth) - .mutation(async ({ ctx, input }) => { - const currentAuth = await findAuthByEmail(ctx.user.email); - - if (input.currentPassword || input.password) { - const correctPassword = bcrypt.compareSync( - input.currentPassword || "", - currentAuth?.password || "", - ); - if (!correctPassword) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Current password is incorrect", - }); - } - } - const auth = await updateAuthById(ctx.user.authId, { - ...(input.email && { email: input.email.toLowerCase() }), - ...(input.password && { - password: bcrypt.hashSync(input.password, 10), - }), - ...(input.image && { image: input.image }), - }); - - return auth; - }), - removeSelfAccount: protectedProcedure - .input( - z.object({ - password: z.string().min(1), - }), - ) - .mutation(async ({ ctx, input }) => { - if (!IS_CLOUD) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "This feature is only available in the cloud version", - }); - } - const currentAuth = await findAuthByEmail(ctx.user.email); - - const correctPassword = bcrypt.compareSync( - input.password, - currentAuth?.password || "", - ); - - if (!correctPassword) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Password is incorrect", - }); - } - const { req, res } = ctx; - const { session } = await validateRequest(req, res); - if (!session) return false; - - await lucia.invalidateSession(session.id); - res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()); - - if (ctx.user.rol === "admin") { - await removeAdminByAuthId(ctx.user.authId); - } else { - await removeUserByAuthId(ctx.user.authId); - } - - return true; - }), - - generateToken: protectedProcedure.mutation(async ({ ctx, input }) => { - const auth = await findAuthById(ctx.user.authId); - - if (auth.token) { - await luciaToken.invalidateSession(auth.token); - } - const session = await luciaToken.createSession(auth?.id || "", { - expiresIn: 60 * 60 * 24 * 30, - }); - - await updateAuthById(auth.id, { - token: session.id, - }); - - return auth; - }), - verifyToken: protectedProcedure.mutation(async () => { - return true; - }), - one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => { - const auth = await findAuthById(input.id); - return auth; - }), - - generate2FASecret: protectedProcedure.query(async ({ ctx }) => { - return await generate2FASecret(ctx.user.authId); - }), - verify2FASetup: protectedProcedure - .input(apiVerify2FA) - .mutation(async ({ ctx, input }) => { - const auth = await findAuthById(ctx.user.authId); - - await verify2FA(auth, input.secret, input.pin); - await updateAuthById(auth.id, { - is2FAEnabled: true, - secret: input.secret, - }); - return auth; - }), - - verifyLogin2FA: publicProcedure - .input(apiVerifyLogin2FA) - .mutation(async ({ ctx, input }) => { - const auth = await findAuthById(input.id); - - await verify2FA(auth, auth.secret || "", input.pin); - - const session = await lucia.createSession(auth.id, {}); - - ctx.res.appendHeader( - "Set-Cookie", - lucia.createSessionCookie(session.id).serialize(), - ); - - return true; - }), - disable2FA: protectedProcedure.mutation(async ({ ctx }) => { - const auth = await findAuthById(ctx.user.authId); - await updateAuthById(auth.id, { - is2FAEnabled: false, - secret: null, - }); - return auth; - }), - sendResetPasswordEmail: publicProcedure - .input( - z.object({ - email: z.string().min(1).email(), - }), - ) - .mutation(async ({ ctx, input }) => { - if (!IS_CLOUD) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "This feature is only available in the cloud version", - }); - } - const authR = await db.query.auth.findFirst({ - where: eq(auth.email, input.email), - }); - if (!authR) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "User not found", - }); - } - const token = nanoid(); - await updateAuthById(authR.id, { - resetPasswordToken: token, - // Make resetPassword in 24 hours - resetPasswordExpiresAt: new Date( - new Date().getTime() + 24 * 60 * 60 * 1000, - ).toISOString(), - }); - - await sendEmailNotification( - { - fromAddress: process.env.SMTP_FROM_ADDRESS!, - toAddresses: [authR.email], - smtpServer: process.env.SMTP_SERVER!, - smtpPort: Number(process.env.SMTP_PORT), - username: process.env.SMTP_USERNAME!, - password: process.env.SMTP_PASSWORD!, - }, - "Reset Password", - ` - Reset your password by clicking the link below: - The link will expire in 24 hours. - - Reset Password - - - `, - ); - }), - - resetPassword: publicProcedure - .input( - z.object({ - resetPasswordToken: z.string().min(1), - password: z.string().min(1), - }), - ) - .mutation(async ({ ctx, input }) => { - if (!IS_CLOUD) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "This feature is only available in the cloud version", - }); - } - const authR = await db.query.auth.findFirst({ - where: eq(auth.resetPasswordToken, input.resetPasswordToken), - }); - - if (!authR || authR.resetPasswordExpiresAt === null) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Token not found", - }); - } - - const isExpired = isBefore( - new Date(authR.resetPasswordExpiresAt), - new Date(), - ); - - if (isExpired) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Token expired", - }); - } - - await updateAuthById(authR.id, { - resetPasswordExpiresAt: null, - resetPasswordToken: null, - password: bcrypt.hashSync(input.password, 10), - }); - - return true; - }), - confirmEmail: adminProcedure - .input( - z.object({ - confirmationToken: z.string().min(1), - }), - ) - .mutation(async ({ ctx, input }) => { - if (!IS_CLOUD) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Functionality not available in cloud version", - }); - } - const authR = await db.query.auth.findFirst({ - where: eq(auth.confirmationToken, input.confirmationToken), - }); - if (!authR || authR.confirmationExpiresAt === null) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Token not found", - }); - } - if (authR.confirmationToken !== input.confirmationToken) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Confirmation Token not found", - }); - } - - const isExpired = isBefore( - new Date(authR.confirmationExpiresAt), - new Date(), - ); - - if (isExpired) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Confirmation Token expired", - }); - } - 1; - await updateAuthById(authR.id, { - confirmationToken: null, - confirmationExpiresAt: null, - }); - return true; - }), + // createAdmin: publicProcedure.mutation(async ({ input }) => { + // try { + // if (!IS_CLOUD) { + // const admin = await db.query.admins.findFirst({}); + // if (admin) { + // throw new TRPCError({ + // code: "BAD_REQUEST", + // message: "Admin already exists", + // }); + // } + // } + // const newAdmin = await createAdmin(input); + // if (IS_CLOUD) { + // await sendDiscordNotificationWelcome(newAdmin); + // await sendVerificationEmail(newAdmin.id); + // return { + // status: "success", + // type: "cloud", + // }; + // } + // // const session = await lucia.createSession(newAdmin.id || "", {}); + // // ctx.res.appendHeader( + // // "Set-Cookie", + // // lucia.createSessionCookie(session.id).serialize(), + // // ); + // return { + // status: "success", + // type: "selfhosted", + // }; + // } catch (error) { + // throw new TRPCError({ + // code: "BAD_REQUEST", + // // @ts-ignore + // message: `Error: ${error?.code === "23505" ? "Email already exists" : "Error creating admin"}`, + // cause: error, + // }); + // } + // }), + // createUser: publicProcedure.mutation(async ({ input }) => { + // try { + // const _token = await getUserByToken(input.token); + // // if (token.isExpired) { + // // throw new TRPCError({ + // // code: "BAD_REQUEST", + // // message: "Invalid token", + // // }); + // // } + // // const newUser = await createUser(input); + // // if (IS_CLOUD) { + // // await sendVerificationEmail(token.authId); + // // return true; + // // } + // // const session = await lucia.createSession(newUser?.authId || "", {}); + // // ctx.res.appendHeader( + // // "Set-Cookie", + // // lucia.createSessionCookie(session.id).serialize(), + // // ); + // return true; + // } catch (error) { + // throw new TRPCError({ + // code: "BAD_REQUEST", + // message: "Error creating the user", + // cause: error, + // }); + // } + // }), + // login: publicProcedure.mutation(async ({ input }) => { + // try { + // const auth = await findAuthByEmail(input.email); + // const correctPassword = bcrypt.compareSync( + // input.password, + // auth?.password || "", + // ); + // if (!correctPassword) { + // throw new TRPCError({ + // code: "BAD_REQUEST", + // message: "Credentials do not match", + // }); + // } + // if (auth?.confirmationToken && IS_CLOUD) { + // await sendVerificationEmail(auth.id); + // throw new TRPCError({ + // code: "BAD_REQUEST", + // message: + // "Email not confirmed, we have sent you a confirmation email please check your inbox.", + // }); + // } + // if (auth?.is2FAEnabled) { + // return { + // is2FAEnabled: true, + // authId: auth.id, + // }; + // } + // // const session = await lucia.createSession(auth?.id || "", {}); + // // ctx.res.appendHeader( + // // "Set-Cookie", + // // lucia.createSessionCookie(session.id).serialize(), + // // ); + // return { + // is2FAEnabled: false, + // authId: auth?.id, + // }; + // } catch (error) { + // throw new TRPCError({ + // code: "BAD_REQUEST", + // message: `Error: ${error instanceof Error ? error.message : "Login error"}`, + // cause: error, + // }); + // } + // }), + // get: protectedProcedure.query(async ({ ctx }) => { + // const memberResult = await db.query.member.findFirst({ + // where: and( + // eq(member.userId, ctx.user.id), + // eq(member.organizationId, ctx.session?.activeOrganizationId || ""), + // ), + // with: { + // user: true, + // }, + // }); + // return memberResult; + // }), + // logout: protectedProcedure.mutation(async ({ ctx }) => { + // const { req } = ctx; + // const { session } = await validateRequest(req); + // if (!session) return false; + // // await lucia.invalidateSession(session.id); + // // res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()); + // return true; + // }), + // update: protectedProcedure.mutation(async ({ ctx, input }) => { + // const currentAuth = await findAuthByEmail(ctx.user.email); + // if (input.currentPassword || input.password) { + // const correctPassword = bcrypt.compareSync( + // input.currentPassword || "", + // currentAuth?.password || "", + // ); + // if (!correctPassword) { + // throw new TRPCError({ + // code: "BAD_REQUEST", + // message: "Current password is incorrect", + // }); + // } + // } + // // const auth = await updateAuthById(ctx.user.authId, { + // // ...(input.email && { email: input.email.toLowerCase() }), + // // ...(input.password && { + // // password: bcrypt.hashSync(input.password, 10), + // // }), + // // ...(input.image && { image: input.image }), + // // }); + // return auth; + // }), + // removeSelfAccount: protectedProcedure + // .input( + // z.object({ + // password: z.string().min(1), + // }), + // ) + // .mutation(async ({ ctx, input }) => { + // if (!IS_CLOUD) { + // throw new TRPCError({ + // code: "NOT_FOUND", + // message: "This feature is only available in the cloud version", + // }); + // } + // const currentAuth = await findAuthByEmail(ctx.user.email); + // const correctPassword = bcrypt.compareSync( + // input.password, + // currentAuth?.password || "", + // ); + // if (!correctPassword) { + // throw new TRPCError({ + // code: "BAD_REQUEST", + // message: "Password is incorrect", + // }); + // } + // const { req } = ctx; + // const { session } = await validateRequest(req); + // if (!session) return false; + // // await lucia.invalidateSession(session.id); + // // res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()); + // // if (ctx.user.rol === "owner") { + // // await removeAdminByAuthId(ctx.user.authId); + // // } else { + // // await removeUserByAuthId(ctx.user.authId); + // // } + // return true; + // }), + // generateToken: protectedProcedure.mutation(async ({ ctx }) => { + // const auth = await findUserById(ctx.user.id); + // console.log(auth); + // // if (auth.token) { + // // await luciaToken.invalidateSession(auth.token); + // // } + // // const session = await luciaToken.createSession(auth?.id || "", { + // // expiresIn: 60 * 60 * 24 * 30, + // // }); + // // await updateUser(auth.id, { + // // token: session.id, + // // }); + // return auth; + // }), + // verifyToken: protectedProcedure.mutation(async () => { + // return true; + // }), + // one: adminProcedure + // .input(z.object({ userId: z.string().min(1) })) + // .query(async ({ input }) => { + // // TODO: Check if the user is admin or member + // const user = await findUserById(input.userId); + // return user; + // }), + // sendResetPasswordEmail: publicProcedure + // .input( + // z.object({ + // email: z.string().min(1).email(), + // }), + // ) + // .mutation(async ({ input }) => { + // if (!IS_CLOUD) { + // throw new TRPCError({ + // code: "NOT_FOUND", + // message: "This feature is only available in the cloud version", + // }); + // } + // const authR = await db.query.auth.findFirst({ + // where: eq(auth.email, input.email), + // }); + // if (!authR) { + // throw new TRPCError({ + // code: "NOT_FOUND", + // message: "User not found", + // }); + // } + // const token = nanoid(); + // await updateAuthById(authR.id, { + // resetPasswordToken: token, + // // Make resetPassword in 24 hours + // resetPasswordExpiresAt: new Date( + // new Date().getTime() + 24 * 60 * 60 * 1000, + // ).toISOString(), + // }); + // await sendEmailNotification( + // { + // fromAddress: process.env.SMTP_FROM_ADDRESS!, + // toAddresses: [authR.email], + // smtpServer: process.env.SMTP_SERVER!, + // smtpPort: Number(process.env.SMTP_PORT), + // username: process.env.SMTP_USERNAME!, + // password: process.env.SMTP_PASSWORD!, + // }, + // "Reset Password", + // ` + // Reset your password by clicking the link below: + // The link will expire in 24 hours. + // + // Reset Password + // + // `, + // ); + // }), }); -export const sendVerificationEmail = async (authId: string) => { - const token = nanoid(); - const result = await updateAuthById(authId, { - confirmationToken: token, - confirmationExpiresAt: new Date( - new Date().getTime() + 24 * 60 * 60 * 1000, - ).toISOString(), - }); +// export const sendVerificationEmail = async (authId: string) => { +// const token = nanoid(); +// const result = await updateAuthById(authId, { +// confirmationToken: token, +// confirmationExpiresAt: new Date( +// new Date().getTime() + 24 * 60 * 60 * 1000, +// ).toISOString(), +// }); - if (!result) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "User not found", - }); - } - await sendEmailNotification( - { - fromAddress: process.env.SMTP_FROM_ADDRESS || "", - toAddresses: [result?.email], - smtpServer: process.env.SMTP_SERVER || "", - smtpPort: Number(process.env.SMTP_PORT), - username: process.env.SMTP_USERNAME || "", - password: process.env.SMTP_PASSWORD || "", - }, - "Confirm your email | Dokploy", - ` - Welcome to Dokploy! - Please confirm your email by clicking the link below: - - Confirm Email - - `, - ); +// if (!result) { +// throw new TRPCError({ +// code: "BAD_REQUEST", +// message: "User not found", +// }); +// } +// await sendEmailNotification( +// { +// fromAddress: process.env.SMTP_FROM_ADDRESS || "", +// toAddresses: [result?.email], +// smtpServer: process.env.SMTP_SERVER || "", +// smtpPort: Number(process.env.SMTP_PORT), +// username: process.env.SMTP_USERNAME || "", +// password: process.env.SMTP_PASSWORD || "", +// }, +// "Confirm your email | Dokploy", +// ` +// Welcome to Dokploy! +// Please confirm your email by clicking the link below: +// +// Confirm Email +// +// `, +// ); - return true; -}; +// return true; +// }; -export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => { - await sendDiscordNotification( - { - webhookUrl: process.env.DISCORD_WEBHOOK_URL || "", - }, - { - title: "New User Registered", - color: 0x00ff00, - fields: [ - { - name: "Email", - value: newAdmin.email, - inline: true, - }, - ], - timestamp: newAdmin.createdAt, - footer: { - text: "Dokploy User Registration Notification", - }, - }, - ); -}; +// export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => { +// await sendDiscordNotification( +// { +// webhookUrl: process.env.DISCORD_WEBHOOK_URL || "", +// }, +// { +// title: "New User Registered", +// color: 0x00ff00, +// fields: [ +// { +// name: "Email", +// value: newAdmin.email, +// inline: true, +// }, +// ], +// timestamp: newAdmin.createdAt, +// footer: { +// text: "Dokploy User Registration Notification", +// }, +// }, +// ); +// }; diff --git a/apps/dokploy/server/api/routers/backup.ts b/apps/dokploy/server/api/routers/backup.ts index 0b8d7ab1..8a7a5f22 100644 --- a/apps/dokploy/server/api/routers/backup.ts +++ b/apps/dokploy/server/api/routers/backup.ts @@ -30,7 +30,7 @@ import { TRPCError } from "@trpc/server"; export const backupRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateBackup) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input }) => { try { const newBackup = await createBackup(input); @@ -74,16 +74,14 @@ export const backupRouter = createTRPCRouter({ }); } }), - one: protectedProcedure - .input(apiFindOneBackup) - .query(async ({ input, ctx }) => { - const backup = await findBackupById(input.backupId); + one: protectedProcedure.input(apiFindOneBackup).query(async ({ input }) => { + const backup = await findBackupById(input.backupId); - return backup; - }), + return backup; + }), update: protectedProcedure .input(apiUpdateBackup) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input }) => { try { await updateBackupById(input.backupId, input); const backup = await findBackupById(input.backupId); @@ -111,15 +109,17 @@ export const backupRouter = createTRPCRouter({ } } } catch (error) { + const message = + error instanceof Error ? error.message : "Error updating this Backup"; throw new TRPCError({ code: "BAD_REQUEST", - message: "Error updating this Backup", + message, }); } }), remove: protectedProcedure .input(apiRemoveBackup) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input }) => { try { const value = await removeBackupById(input.backupId); if (IS_CLOUD && value) { @@ -133,10 +133,11 @@ export const backupRouter = createTRPCRouter({ } return value; } catch (error) { + const message = + error instanceof Error ? error.message : "Error deleting this Backup"; throw new TRPCError({ code: "BAD_REQUEST", - message: "Error deleting this Backup", - cause: error, + message, }); } }), @@ -149,11 +150,13 @@ export const backupRouter = createTRPCRouter({ await runPostgresBackup(postgres, backup); return true; } catch (error) { - console.log(error); + const message = + error instanceof Error + ? error.message + : "Error running manual Postgres backup "; throw new TRPCError({ code: "BAD_REQUEST", - message: "Error running manual Postgres backup ", - cause: error, + message, }); } }), diff --git a/apps/dokploy/server/api/routers/bitbucket.ts b/apps/dokploy/server/api/routers/bitbucket.ts index c66716d3..fa02be8d 100644 --- a/apps/dokploy/server/api/routers/bitbucket.ts +++ b/apps/dokploy/server/api/routers/bitbucket.ts @@ -8,7 +8,6 @@ import { apiUpdateBitbucket, } from "@/server/db/schema"; import { - IS_CLOUD, createBitbucket, findBitbucketById, getBitbucketBranches, @@ -23,7 +22,7 @@ export const bitbucketRouter = createTRPCRouter({ .input(apiCreateBitbucket) .mutation(async ({ input, ctx }) => { try { - return await createBitbucket(input, ctx.user.adminId); + return await createBitbucket(input, ctx.session.activeOrganizationId); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -37,10 +36,9 @@ export const bitbucketRouter = createTRPCRouter({ .query(async ({ input, ctx }) => { const bitbucketProvider = await findBitbucketById(input.bitbucketId); if ( - IS_CLOUD && - bitbucketProvider.gitProvider.adminId !== ctx.user.adminId + bitbucketProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId ) { - //TODO: Remove this line when the cloud version is ready throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this bitbucket provider", @@ -58,12 +56,11 @@ export const bitbucketRouter = createTRPCRouter({ }, }); - if (IS_CLOUD) { - // TODO: mAyBe a rEfaCtoR 🤫 - result = result.filter( - (provider) => provider.gitProvider.adminId === ctx.user.adminId, - ); - } + result = result.filter( + (provider) => + provider.gitProvider.organizationId === + ctx.session.activeOrganizationId, + ); return result; }), @@ -72,10 +69,9 @@ export const bitbucketRouter = createTRPCRouter({ .query(async ({ input, ctx }) => { const bitbucketProvider = await findBitbucketById(input.bitbucketId); if ( - IS_CLOUD && - bitbucketProvider.gitProvider.adminId !== ctx.user.adminId + bitbucketProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId ) { - //TODO: Remove this line when the cloud version is ready throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this bitbucket provider", @@ -90,10 +86,9 @@ export const bitbucketRouter = createTRPCRouter({ input.bitbucketId || "", ); if ( - IS_CLOUD && - bitbucketProvider.gitProvider.adminId !== ctx.user.adminId + bitbucketProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId ) { - //TODO: Remove this line when the cloud version is ready throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this bitbucket provider", @@ -107,10 +102,9 @@ export const bitbucketRouter = createTRPCRouter({ try { const bitbucketProvider = await findBitbucketById(input.bitbucketId); if ( - IS_CLOUD && - bitbucketProvider.gitProvider.adminId !== ctx.user.adminId + bitbucketProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId ) { - //TODO: Remove this line when the cloud version is ready throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this bitbucket provider", @@ -131,10 +125,9 @@ export const bitbucketRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const bitbucketProvider = await findBitbucketById(input.bitbucketId); if ( - IS_CLOUD && - bitbucketProvider.gitProvider.adminId !== ctx.user.adminId + bitbucketProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId ) { - //TODO: Remove this line when the cloud version is ready throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this bitbucket provider", @@ -142,7 +135,7 @@ export const bitbucketRouter = createTRPCRouter({ } return await updateBitbucket(input.bitbucketId, { ...input, - adminId: ctx.user.adminId, + organizationId: ctx.session.activeOrganizationId, }); }), }); diff --git a/apps/dokploy/server/api/routers/certificate.ts b/apps/dokploy/server/api/routers/certificate.ts index 0f8d6fd9..3dc944ac 100644 --- a/apps/dokploy/server/api/routers/certificate.ts +++ b/apps/dokploy/server/api/routers/certificate.ts @@ -25,14 +25,14 @@ export const certificateRouter = createTRPCRouter({ message: "Please set a server to create a certificate", }); } - return await createCertificate(input, ctx.user.adminId); + return await createCertificate(input, ctx.session.activeOrganizationId); }), one: adminProcedure .input(apiFindCertificate) .query(async ({ input, ctx }) => { const certificates = await findCertificateById(input.certificateId); - if (IS_CLOUD && certificates.adminId !== ctx.user.adminId) { + if (certificates.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this certificate", @@ -44,7 +44,7 @@ export const certificateRouter = createTRPCRouter({ .input(apiFindCertificate) .mutation(async ({ input, ctx }) => { const certificates = await findCertificateById(input.certificateId); - if (IS_CLOUD && certificates.adminId !== ctx.user.adminId) { + if (certificates.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to delete this certificate", @@ -55,8 +55,7 @@ export const certificateRouter = createTRPCRouter({ }), all: adminProcedure.query(async ({ ctx }) => { return await db.query.certificates.findMany({ - // TODO: Remove this line when the cloud version is ready - ...(IS_CLOUD && { where: eq(certificates.adminId, ctx.user.adminId) }), + where: eq(certificates.organizationId, ctx.session.activeOrganizationId), }); }), }); diff --git a/apps/dokploy/server/api/routers/cluster.ts b/apps/dokploy/server/api/routers/cluster.ts index 7ded632c..0d840757 100644 --- a/apps/dokploy/server/api/routers/cluster.ts +++ b/apps/dokploy/server/api/routers/cluster.ts @@ -40,7 +40,7 @@ export const clusterRouter = createTRPCRouter({ }); } }), - addWorker: protectedProcedure.query(async ({ input }) => { + addWorker: protectedProcedure.query(async () => { if (IS_CLOUD) { return { command: "", @@ -57,7 +57,7 @@ export const clusterRouter = createTRPCRouter({ version: docker_version.Version, }; }), - addManager: protectedProcedure.query(async ({ input }) => { + addManager: protectedProcedure.query(async () => { if (IS_CLOUD) { return { command: "", diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index 463d0398..bae926d0 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -39,11 +39,11 @@ import { createComposeByTemplate, createDomain, createMount, - findAdminById, findComposeById, findDomainsByComposeId, findProjectById, findServerById, + findUserById, loadServices, randomizeComposeFile, randomizeIsolatedDeploymentComposeFile, @@ -60,8 +60,13 @@ export const composeRouter = createTRPCRouter({ .input(apiCreateCompose) .mutation(async ({ ctx, input }) => { try { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.projectId, + ctx.session.activeOrganizationId, + "create", + ); } if (IS_CLOUD && !input.serverId) { @@ -71,7 +76,7 @@ export const composeRouter = createTRPCRouter({ }); } const project = await findProjectById(input.projectId); - if (project.adminId !== ctx.user.adminId) { + if (project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this project", @@ -79,8 +84,12 @@ export const composeRouter = createTRPCRouter({ } const newService = await createCompose(input); - if (ctx.user.rol === "user") { - await addNewService(ctx.user.authId, newService.composeId); + if (ctx.user.rol === "member") { + await addNewService( + ctx.user.id, + newService.composeId, + project.organizationId, + ); } return newService; @@ -92,12 +101,17 @@ export const composeRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindCompose) .query(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.composeId, "access"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.composeId, + ctx.session.activeOrganizationId, + "access", + ); } const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this compose", @@ -110,7 +124,7 @@ export const composeRouter = createTRPCRouter({ .input(apiUpdateCompose) .mutation(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this compose", @@ -121,12 +135,20 @@ export const composeRouter = createTRPCRouter({ delete: protectedProcedure .input(apiDeleteCompose) .mutation(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.composeId, "delete"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.composeId, + ctx.session.activeOrganizationId, + "delete", + ); } const composeResult = await findComposeById(input.composeId); - if (composeResult.project.adminId !== ctx.user.adminId) { + if ( + composeResult.project.organizationId !== + ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this compose", @@ -148,7 +170,7 @@ export const composeRouter = createTRPCRouter({ for (const operation of cleanupOperations) { try { await operation(); - } catch (error) {} + } catch (_) {} } return result[0]; @@ -157,7 +179,7 @@ export const composeRouter = createTRPCRouter({ .input(apiFindCompose) .mutation(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to clean this compose", @@ -170,7 +192,7 @@ export const composeRouter = createTRPCRouter({ .input(apiFetchServices) .query(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to load this compose", @@ -184,7 +206,9 @@ export const composeRouter = createTRPCRouter({ try { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if ( + compose.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to fetch this compose", @@ -209,7 +233,7 @@ export const composeRouter = createTRPCRouter({ .input(apiRandomizeCompose) .mutation(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to randomize this compose", @@ -221,7 +245,7 @@ export const composeRouter = createTRPCRouter({ .input(apiRandomizeCompose) .mutation(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to randomize this compose", @@ -236,7 +260,7 @@ export const composeRouter = createTRPCRouter({ .input(apiFindCompose) .query(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to get this compose", @@ -254,7 +278,7 @@ export const composeRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this compose", @@ -287,7 +311,7 @@ export const composeRouter = createTRPCRouter({ .input(apiFindCompose) .mutation(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to redeploy this compose", @@ -319,7 +343,7 @@ export const composeRouter = createTRPCRouter({ .input(apiFindCompose) .mutation(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to stop this compose", @@ -333,7 +357,7 @@ export const composeRouter = createTRPCRouter({ .input(apiFindCompose) .mutation(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to stop this compose", @@ -348,7 +372,7 @@ export const composeRouter = createTRPCRouter({ .query(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to get this compose", @@ -361,7 +385,7 @@ export const composeRouter = createTRPCRouter({ .input(apiFindCompose) .mutation(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to refresh this compose", @@ -375,8 +399,13 @@ export const composeRouter = createTRPCRouter({ deployTemplate: protectedProcedure .input(apiCreateComposeByTemplate) .mutation(async ({ ctx, input }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.projectId, + ctx.session.activeOrganizationId, + "create", + ); } if (IS_CLOUD && !input.serverId) { @@ -390,7 +419,7 @@ export const composeRouter = createTRPCRouter({ const generate = await loadTemplateModule(input.id as TemplatesKeys); - const admin = await findAdminById(ctx.user.adminId); + const admin = await findUserById(ctx.user.ownerId); let serverIp = admin.serverIp || "127.0.0.1"; const project = await findProjectById(input.projectId); @@ -418,8 +447,12 @@ export const composeRouter = createTRPCRouter({ isolatedDeployment: true, }); - if (ctx.user.rol === "user") { - await addNewService(ctx.user.authId, compose.composeId); + if (ctx.user.rol === "member") { + await addNewService( + ctx.user.id, + compose.composeId, + project.organizationId, + ); } if (mounts && mounts?.length > 0) { @@ -463,7 +496,7 @@ export const composeRouter = createTRPCRouter({ return templatesData; }), - getTags: protectedProcedure.query(async ({ input }) => { + getTags: protectedProcedure.query(async () => { const allTags = templates.flatMap((template) => template.tags); const uniqueTags = _.uniq(allTags); return uniqueTags; diff --git a/apps/dokploy/server/api/routers/deployment.ts b/apps/dokploy/server/api/routers/deployment.ts index bf981c6d..8d95c121 100644 --- a/apps/dokploy/server/api/routers/deployment.ts +++ b/apps/dokploy/server/api/routers/deployment.ts @@ -19,7 +19,9 @@ export const deploymentRouter = createTRPCRouter({ .input(apiFindAllByApplication) .query(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", @@ -32,7 +34,7 @@ export const deploymentRouter = createTRPCRouter({ .input(apiFindAllByCompose) .query(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this compose", @@ -44,7 +46,7 @@ export const deploymentRouter = createTRPCRouter({ .input(apiFindAllByServer) .query(async ({ input, ctx }) => { const server = await findServerById(input.serverId); - if (server.adminId !== ctx.user.adminId) { + if (server.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this server", diff --git a/apps/dokploy/server/api/routers/destination.ts b/apps/dokploy/server/api/routers/destination.ts index d13928be..f1d582c5 100644 --- a/apps/dokploy/server/api/routers/destination.ts +++ b/apps/dokploy/server/api/routers/destination.ts @@ -28,7 +28,10 @@ export const destinationRouter = createTRPCRouter({ .input(apiCreateDestination) .mutation(async ({ input, ctx }) => { try { - return await createDestintation(input, ctx.user.adminId); + return await createDestintation( + input, + ctx.session.activeOrganizationId, + ); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -84,7 +87,7 @@ export const destinationRouter = createTRPCRouter({ .input(apiFindOneDestination) .query(async ({ input, ctx }) => { const destination = await findDestinationById(input.destinationId); - if (destination.adminId !== ctx.user.adminId) { + if (destination.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this destination", @@ -94,7 +97,7 @@ export const destinationRouter = createTRPCRouter({ }), all: protectedProcedure.query(async ({ ctx }) => { return await db.query.destinations.findMany({ - where: eq(destinations.adminId, ctx.user.adminId), + where: eq(destinations.organizationId, ctx.session.activeOrganizationId), }); }), remove: adminProcedure @@ -103,7 +106,7 @@ export const destinationRouter = createTRPCRouter({ try { const destination = await findDestinationById(input.destinationId); - if (destination.adminId !== ctx.user.adminId) { + if (destination.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to delete this destination", @@ -111,7 +114,7 @@ export const destinationRouter = createTRPCRouter({ } return await removeDestinationById( input.destinationId, - ctx.user.adminId, + ctx.session.activeOrganizationId, ); } catch (error) { throw error; @@ -122,7 +125,7 @@ export const destinationRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const destination = await findDestinationById(input.destinationId); - if (destination.adminId !== ctx.user.adminId) { + if (destination.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to update this destination", @@ -130,7 +133,7 @@ export const destinationRouter = createTRPCRouter({ } return await updateDestinationById(input.destinationId, { ...input, - adminId: ctx.user.adminId, + organizationId: ctx.session.activeOrganizationId, }); } catch (error) { throw error; diff --git a/apps/dokploy/server/api/routers/domain.ts b/apps/dokploy/server/api/routers/domain.ts index f122cf86..aac2a016 100644 --- a/apps/dokploy/server/api/routers/domain.ts +++ b/apps/dokploy/server/api/routers/domain.ts @@ -30,7 +30,9 @@ export const domainRouter = createTRPCRouter({ try { if (input.domainType === "compose" && input.composeId) { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if ( + compose.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this compose", @@ -38,7 +40,10 @@ export const domainRouter = createTRPCRouter({ } } else if (input.domainType === "application" && input.applicationId) { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== + ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", @@ -58,7 +63,9 @@ export const domainRouter = createTRPCRouter({ .input(apiFindOneApplication) .query(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", @@ -70,7 +77,7 @@ export const domainRouter = createTRPCRouter({ .input(apiFindCompose) .query(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this compose", @@ -83,7 +90,7 @@ export const domainRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { return generateTraefikMeDomain( input.appName, - ctx.user.adminId, + ctx.user.ownerId, input.serverId, ); }), @@ -95,7 +102,9 @@ export const domainRouter = createTRPCRouter({ if (currentDomain.applicationId) { const newApp = await findApplicationById(currentDomain.applicationId); - if (newApp.project.adminId !== ctx.user.adminId) { + if ( + newApp.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", @@ -103,7 +112,9 @@ export const domainRouter = createTRPCRouter({ } } else if (currentDomain.composeId) { const newCompose = await findComposeById(currentDomain.composeId); - if (newCompose.project.adminId !== ctx.user.adminId) { + if ( + newCompose.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this compose", @@ -114,7 +125,8 @@ export const domainRouter = createTRPCRouter({ currentDomain.previewDeploymentId, ); if ( - newPreviewDeployment.application.project.adminId !== ctx.user.adminId + newPreviewDeployment.application.project.organizationId !== + ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", @@ -143,7 +155,9 @@ export const domainRouter = createTRPCRouter({ const domain = await findDomainById(input.domainId); if (domain.applicationId) { const application = await findApplicationById(domain.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", @@ -151,7 +165,7 @@ export const domainRouter = createTRPCRouter({ } } else if (domain.composeId) { const compose = await findComposeById(domain.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this compose", @@ -166,7 +180,10 @@ export const domainRouter = createTRPCRouter({ const domain = await findDomainById(input.domainId); if (domain.applicationId) { const application = await findApplicationById(domain.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== + ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", @@ -174,7 +191,9 @@ export const domainRouter = createTRPCRouter({ } } else if (domain.composeId) { const compose = await findComposeById(domain.composeId); - if (compose.project.adminId !== ctx.user.adminId) { + if ( + compose.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this compose", diff --git a/apps/dokploy/server/api/routers/git-provider.ts b/apps/dokploy/server/api/routers/git-provider.ts index abd93392..ed37869d 100644 --- a/apps/dokploy/server/api/routers/git-provider.ts +++ b/apps/dokploy/server/api/routers/git-provider.ts @@ -1,11 +1,7 @@ import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { db } from "@/server/db"; import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema"; -import { - IS_CLOUD, - findGitProviderById, - removeGitProvider, -} from "@dokploy/server"; +import { findGitProviderById, removeGitProvider } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import { desc, eq } from "drizzle-orm"; @@ -18,8 +14,7 @@ export const gitProviderRouter = createTRPCRouter({ github: true, }, orderBy: desc(gitProvider.createdAt), - ...(IS_CLOUD && { where: eq(gitProvider.adminId, ctx.user.adminId) }), - //TODO: Remove this line when the cloud version is ready + where: eq(gitProvider.organizationId, ctx.session.activeOrganizationId), }); }), remove: protectedProcedure @@ -28,8 +23,7 @@ export const gitProviderRouter = createTRPCRouter({ try { const gitProvider = await findGitProviderById(input.gitProviderId); - if (IS_CLOUD && gitProvider.adminId !== ctx.user.adminId) { - // TODO: Remove isCloud in the next versions of dokploy + if (gitProvider.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to delete this Git provider", @@ -37,9 +31,13 @@ export const gitProviderRouter = createTRPCRouter({ } return await removeGitProvider(input.gitProviderId); } catch (error) { + const message = + error instanceof Error + ? error.message + : "Error deleting this Git provider"; throw new TRPCError({ code: "BAD_REQUEST", - message: "Error deleting this Git provider", + message, }); } }), diff --git a/apps/dokploy/server/api/routers/github.ts b/apps/dokploy/server/api/routers/github.ts index 56222577..691030e2 100644 --- a/apps/dokploy/server/api/routers/github.ts +++ b/apps/dokploy/server/api/routers/github.ts @@ -6,7 +6,6 @@ import { apiUpdateGithub, } from "@/server/db/schema"; import { - IS_CLOUD, findGithubById, getGithubBranches, getGithubRepositories, @@ -20,8 +19,10 @@ export const githubRouter = createTRPCRouter({ .input(apiFindOneGithub) .query(async ({ input, ctx }) => { const githubProvider = await findGithubById(input.githubId); - if (IS_CLOUD && githubProvider.gitProvider.adminId !== ctx.user.adminId) { - //TODO: Remove this line when the cloud version is ready + if ( + githubProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this github provider", @@ -33,8 +34,10 @@ export const githubRouter = createTRPCRouter({ .input(apiFindOneGithub) .query(async ({ input, ctx }) => { const githubProvider = await findGithubById(input.githubId); - if (IS_CLOUD && githubProvider.gitProvider.adminId !== ctx.user.adminId) { - //TODO: Remove this line when the cloud version is ready + if ( + githubProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this github provider", @@ -46,7 +49,10 @@ export const githubRouter = createTRPCRouter({ .input(apiFindGithubBranches) .query(async ({ input, ctx }) => { const githubProvider = await findGithubById(input.githubId || ""); - if (IS_CLOUD && githubProvider.gitProvider.adminId !== ctx.user.adminId) { + if ( + githubProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId + ) { //TODO: Remove this line when the cloud version is ready throw new TRPCError({ code: "UNAUTHORIZED", @@ -62,12 +68,11 @@ export const githubRouter = createTRPCRouter({ }, }); - if (IS_CLOUD) { - // TODO: mAyBe a rEfaCtoR 🤫 - result = result.filter( - (provider) => provider.gitProvider.adminId === ctx.user.adminId, - ); - } + result = result.filter( + (provider) => + provider.gitProvider.organizationId === + ctx.session.activeOrganizationId, + ); const filtered = result .filter((provider) => haveGithubRequirements(provider)) @@ -89,10 +94,9 @@ export const githubRouter = createTRPCRouter({ try { const githubProvider = await findGithubById(input.githubId); if ( - IS_CLOUD && - githubProvider.gitProvider.adminId !== ctx.user.adminId + githubProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId ) { - //TODO: Remove this line when the cloud version is ready throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this github provider", @@ -111,8 +115,10 @@ export const githubRouter = createTRPCRouter({ .input(apiUpdateGithub) .mutation(async ({ input, ctx }) => { const githubProvider = await findGithubById(input.githubId); - if (IS_CLOUD && githubProvider.gitProvider.adminId !== ctx.user.adminId) { - //TODO: Remove this line when the cloud version is ready + if ( + githubProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this github provider", @@ -120,7 +126,7 @@ export const githubRouter = createTRPCRouter({ } await updateGitProvider(input.gitProviderId, { name: input.name, - adminId: ctx.user.adminId, + organizationId: ctx.session.activeOrganizationId, }); }), }); diff --git a/apps/dokploy/server/api/routers/gitlab.ts b/apps/dokploy/server/api/routers/gitlab.ts index 6d35f4a2..daae68a5 100644 --- a/apps/dokploy/server/api/routers/gitlab.ts +++ b/apps/dokploy/server/api/routers/gitlab.ts @@ -9,7 +9,6 @@ import { import { db } from "@/server/db"; import { - IS_CLOUD, createGitlab, findGitlabById, getGitlabBranches, @@ -26,7 +25,7 @@ export const gitlabRouter = createTRPCRouter({ .input(apiCreateGitlab) .mutation(async ({ input, ctx }) => { try { - return await createGitlab(input, ctx.user.adminId); + return await createGitlab(input, ctx.session.activeOrganizationId); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -39,8 +38,10 @@ export const gitlabRouter = createTRPCRouter({ .input(apiFindOneGitlab) .query(async ({ input, ctx }) => { const gitlabProvider = await findGitlabById(input.gitlabId); - if (IS_CLOUD && gitlabProvider.gitProvider.adminId !== ctx.user.adminId) { - //TODO: Remove this line when the cloud version is ready + if ( + gitlabProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this Gitlab provider", @@ -55,12 +56,11 @@ export const gitlabRouter = createTRPCRouter({ }, }); - if (IS_CLOUD) { - // TODO: mAyBe a rEfaCtoR 🤫 - result = result.filter( - (provider) => provider.gitProvider.adminId === ctx.user.adminId, - ); - } + result = result.filter( + (provider) => + provider.gitProvider.organizationId === + ctx.session.activeOrganizationId, + ); const filtered = result .filter((provider) => haveGitlabRequirements(provider)) .map((provider) => { @@ -78,8 +78,10 @@ export const gitlabRouter = createTRPCRouter({ .input(apiFindOneGitlab) .query(async ({ input, ctx }) => { const gitlabProvider = await findGitlabById(input.gitlabId); - if (IS_CLOUD && gitlabProvider.gitProvider.adminId !== ctx.user.adminId) { - //TODO: Remove this line when the cloud version is ready + if ( + gitlabProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this Gitlab provider", @@ -92,8 +94,10 @@ export const gitlabRouter = createTRPCRouter({ .input(apiFindGitlabBranches) .query(async ({ input, ctx }) => { const gitlabProvider = await findGitlabById(input.gitlabId || ""); - if (IS_CLOUD && gitlabProvider.gitProvider.adminId !== ctx.user.adminId) { - //TODO: Remove this line when the cloud version is ready + if ( + gitlabProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this Gitlab provider", @@ -107,10 +111,9 @@ export const gitlabRouter = createTRPCRouter({ try { const gitlabProvider = await findGitlabById(input.gitlabId || ""); if ( - IS_CLOUD && - gitlabProvider.gitProvider.adminId !== ctx.user.adminId + gitlabProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId ) { - //TODO: Remove this line when the cloud version is ready throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this Gitlab provider", @@ -130,8 +133,10 @@ export const gitlabRouter = createTRPCRouter({ .input(apiUpdateGitlab) .mutation(async ({ input, ctx }) => { const gitlabProvider = await findGitlabById(input.gitlabId); - if (IS_CLOUD && gitlabProvider.gitProvider.adminId !== ctx.user.adminId) { - //TODO: Remove this line when the cloud version is ready + if ( + gitlabProvider.gitProvider.organizationId !== + ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this Gitlab provider", @@ -140,7 +145,7 @@ export const gitlabRouter = createTRPCRouter({ if (input.name) { await updateGitProvider(input.gitProviderId, { name: input.name, - adminId: ctx.user.adminId, + organizationId: ctx.session.activeOrganizationId, }); await updateGitlab(input.gitlabId, { diff --git a/apps/dokploy/server/api/routers/mariadb.ts b/apps/dokploy/server/api/routers/mariadb.ts index 6e85d274..be0ffd39 100644 --- a/apps/dokploy/server/api/routers/mariadb.ts +++ b/apps/dokploy/server/api/routers/mariadb.ts @@ -20,7 +20,6 @@ import { findBackupsByDbId, findMariadbById, findProjectById, - findServerById, removeMariadbById, removeService, startService, @@ -37,8 +36,13 @@ export const mariadbRouter = createTRPCRouter({ .input(apiCreateMariaDB) .mutation(async ({ input, ctx }) => { try { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.projectId, + ctx.session.activeOrganizationId, + "create", + ); } if (IS_CLOUD && !input.serverId) { @@ -49,15 +53,19 @@ export const mariadbRouter = createTRPCRouter({ } const project = await findProjectById(input.projectId); - if (project.adminId !== ctx.user.adminId) { + if (project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this project", }); } const newMariadb = await createMariadb(input); - if (ctx.user.rol === "user") { - await addNewService(ctx.user.authId, newMariadb.mariadbId); + if (ctx.user.rol === "member") { + await addNewService( + ctx.user.id, + newMariadb.mariadbId, + project.organizationId, + ); } await createMount({ @@ -79,11 +87,16 @@ export const mariadbRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOneMariaDB) .query(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.mariadbId, "access"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.mariadbId, + ctx.session.activeOrganizationId, + "access", + ); } const mariadb = await findMariadbById(input.mariadbId); - if (mariadb.project.adminId !== ctx.user.adminId) { + if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this Mariadb", @@ -96,7 +109,7 @@ export const mariadbRouter = createTRPCRouter({ .input(apiFindOneMariaDB) .mutation(async ({ input, ctx }) => { const service = await findMariadbById(input.mariadbId); - if (service.project.adminId !== ctx.user.adminId) { + if (service.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to start this Mariadb", @@ -133,7 +146,7 @@ export const mariadbRouter = createTRPCRouter({ .input(apiSaveExternalPortMariaDB) .mutation(async ({ input, ctx }) => { const mongo = await findMariadbById(input.mariadbId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this external port", @@ -149,7 +162,7 @@ export const mariadbRouter = createTRPCRouter({ .input(apiDeployMariaDB) .mutation(async ({ input, ctx }) => { const mariadb = await findMariadbById(input.mariadbId); - if (mariadb.project.adminId !== ctx.user.adminId) { + if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this Mariadb", @@ -170,7 +183,7 @@ export const mariadbRouter = createTRPCRouter({ .input(apiDeployMariaDB) .subscription(async ({ input, ctx }) => { const mariadb = await findMariadbById(input.mariadbId); - if (mariadb.project.adminId !== ctx.user.adminId) { + if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this Mariadb", @@ -187,7 +200,7 @@ export const mariadbRouter = createTRPCRouter({ .input(apiChangeMariaDBStatus) .mutation(async ({ input, ctx }) => { const mongo = await findMariadbById(input.mariadbId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to change this Mariadb status", @@ -201,12 +214,17 @@ export const mariadbRouter = createTRPCRouter({ remove: protectedProcedure .input(apiFindOneMariaDB) .mutation(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.mariadbId, "delete"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.mariadbId, + ctx.session.activeOrganizationId, + "delete", + ); } const mongo = await findMariadbById(input.mariadbId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this Mariadb", @@ -223,7 +241,7 @@ export const mariadbRouter = createTRPCRouter({ for (const operation of cleanupOperations) { try { await operation(); - } catch (error) {} + } catch (_) {} } return mongo; @@ -232,7 +250,7 @@ export const mariadbRouter = createTRPCRouter({ .input(apiSaveEnvironmentVariablesMariaDB) .mutation(async ({ input, ctx }) => { const mariadb = await findMariadbById(input.mariadbId); - if (mariadb.project.adminId !== ctx.user.adminId) { + if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this environment", @@ -255,7 +273,7 @@ export const mariadbRouter = createTRPCRouter({ .input(apiResetMariadb) .mutation(async ({ input, ctx }) => { const mariadb = await findMariadbById(input.mariadbId); - if (mariadb.project.adminId !== ctx.user.adminId) { + if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to reload this Mariadb", @@ -285,7 +303,7 @@ export const mariadbRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const { mariadbId, ...rest } = input; const mariadb = await findMariadbById(mariadbId); - if (mariadb.project.adminId !== ctx.user.adminId) { + if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this Mariadb", diff --git a/apps/dokploy/server/api/routers/mongo.ts b/apps/dokploy/server/api/routers/mongo.ts index 2bca3ec5..1c3ba6bb 100644 --- a/apps/dokploy/server/api/routers/mongo.ts +++ b/apps/dokploy/server/api/routers/mongo.ts @@ -36,8 +36,13 @@ export const mongoRouter = createTRPCRouter({ .input(apiCreateMongo) .mutation(async ({ input, ctx }) => { try { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.projectId, + ctx.session.activeOrganizationId, + "create", + ); } if (IS_CLOUD && !input.serverId) { @@ -48,15 +53,19 @@ export const mongoRouter = createTRPCRouter({ } const project = await findProjectById(input.projectId); - if (project.adminId !== ctx.user.adminId) { + if (project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this project", }); } const newMongo = await createMongo(input); - if (ctx.user.rol === "user") { - await addNewService(ctx.user.authId, newMongo.mongoId); + if (ctx.user.rol === "member") { + await addNewService( + ctx.user.id, + newMongo.mongoId, + project.organizationId, + ); } await createMount({ @@ -82,12 +91,17 @@ export const mongoRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOneMongo) .query(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.mongoId, "access"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.mongoId, + ctx.session.activeOrganizationId, + "access", + ); } const mongo = await findMongoById(input.mongoId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this mongo", @@ -101,7 +115,7 @@ export const mongoRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const service = await findMongoById(input.mongoId); - if (service.project.adminId !== ctx.user.adminId) { + if (service.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to start this mongo", @@ -124,7 +138,7 @@ export const mongoRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const mongo = await findMongoById(input.mongoId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to stop this mongo", @@ -146,7 +160,7 @@ export const mongoRouter = createTRPCRouter({ .input(apiSaveExternalPortMongo) .mutation(async ({ input, ctx }) => { const mongo = await findMongoById(input.mongoId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this external port", @@ -162,7 +176,7 @@ export const mongoRouter = createTRPCRouter({ .input(apiDeployMongo) .mutation(async ({ input, ctx }) => { const mongo = await findMongoById(input.mongoId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this mongo", @@ -182,7 +196,7 @@ export const mongoRouter = createTRPCRouter({ .input(apiDeployMongo) .subscription(async ({ input, ctx }) => { const mongo = await findMongoById(input.mongoId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this mongo", @@ -199,7 +213,7 @@ export const mongoRouter = createTRPCRouter({ .input(apiChangeMongoStatus) .mutation(async ({ input, ctx }) => { const mongo = await findMongoById(input.mongoId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to change this mongo status", @@ -214,7 +228,7 @@ export const mongoRouter = createTRPCRouter({ .input(apiResetMongo) .mutation(async ({ input, ctx }) => { const mongo = await findMongoById(input.mongoId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to reload this mongo", @@ -242,13 +256,18 @@ export const mongoRouter = createTRPCRouter({ remove: protectedProcedure .input(apiFindOneMongo) .mutation(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.mongoId, "delete"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.mongoId, + ctx.session.activeOrganizationId, + "delete", + ); } const mongo = await findMongoById(input.mongoId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this mongo", @@ -265,7 +284,7 @@ export const mongoRouter = createTRPCRouter({ for (const operation of cleanupOperations) { try { await operation(); - } catch (error) {} + } catch (_) {} } return mongo; @@ -274,7 +293,7 @@ export const mongoRouter = createTRPCRouter({ .input(apiSaveEnvironmentVariablesMongo) .mutation(async ({ input, ctx }) => { const mongo = await findMongoById(input.mongoId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this environment", @@ -298,7 +317,7 @@ export const mongoRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const { mongoId, ...rest } = input; const mongo = await findMongoById(mongoId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this mongo", diff --git a/apps/dokploy/server/api/routers/mysql.ts b/apps/dokploy/server/api/routers/mysql.ts index 7ebf4623..594403f2 100644 --- a/apps/dokploy/server/api/routers/mysql.ts +++ b/apps/dokploy/server/api/routers/mysql.ts @@ -38,8 +38,13 @@ export const mysqlRouter = createTRPCRouter({ .input(apiCreateMySql) .mutation(async ({ input, ctx }) => { try { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.projectId, + ctx.session.activeOrganizationId, + "create", + ); } if (IS_CLOUD && !input.serverId) { @@ -50,7 +55,7 @@ export const mysqlRouter = createTRPCRouter({ } 1; const project = await findProjectById(input.projectId); - if (project.adminId !== ctx.user.adminId) { + if (project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this project", @@ -58,8 +63,12 @@ export const mysqlRouter = createTRPCRouter({ } const newMysql = await createMysql(input); - if (ctx.user.rol === "user") { - await addNewService(ctx.user.authId, newMysql.mysqlId); + if (ctx.user.rol === "member") { + await addNewService( + ctx.user.id, + newMysql.mysqlId, + project.organizationId, + ); } await createMount({ @@ -85,11 +94,16 @@ export const mysqlRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOneMySql) .query(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.mysqlId, "access"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.mysqlId, + ctx.session.activeOrganizationId, + "access", + ); } const mysql = await findMySqlById(input.mysqlId); - if (mysql.project.adminId !== ctx.user.adminId) { + if (mysql.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this MySQL", @@ -102,7 +116,7 @@ export const mysqlRouter = createTRPCRouter({ .input(apiFindOneMySql) .mutation(async ({ input, ctx }) => { const service = await findMySqlById(input.mysqlId); - if (service.project.adminId !== ctx.user.adminId) { + if (service.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to start this MySQL", @@ -124,7 +138,7 @@ export const mysqlRouter = createTRPCRouter({ .input(apiFindOneMySql) .mutation(async ({ input, ctx }) => { const mongo = await findMySqlById(input.mysqlId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to stop this MySQL", @@ -145,7 +159,7 @@ export const mysqlRouter = createTRPCRouter({ .input(apiSaveExternalPortMySql) .mutation(async ({ input, ctx }) => { const mongo = await findMySqlById(input.mysqlId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this external port", @@ -161,7 +175,7 @@ export const mysqlRouter = createTRPCRouter({ .input(apiDeployMySql) .mutation(async ({ input, ctx }) => { const mysql = await findMySqlById(input.mysqlId); - if (mysql.project.adminId !== ctx.user.adminId) { + if (mysql.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this MySQL", @@ -181,7 +195,7 @@ export const mysqlRouter = createTRPCRouter({ .input(apiDeployMySql) .subscription(async ({ input, ctx }) => { const mysql = await findMySqlById(input.mysqlId); - if (mysql.project.adminId !== ctx.user.adminId) { + if (mysql.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this MySQL", @@ -198,7 +212,7 @@ export const mysqlRouter = createTRPCRouter({ .input(apiChangeMySqlStatus) .mutation(async ({ input, ctx }) => { const mongo = await findMySqlById(input.mysqlId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to change this MySQL status", @@ -213,7 +227,7 @@ export const mysqlRouter = createTRPCRouter({ .input(apiResetMysql) .mutation(async ({ input, ctx }) => { const mysql = await findMySqlById(input.mysqlId); - if (mysql.project.adminId !== ctx.user.adminId) { + if (mysql.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to reload this MySQL", @@ -240,11 +254,16 @@ export const mysqlRouter = createTRPCRouter({ remove: protectedProcedure .input(apiFindOneMySql) .mutation(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.mysqlId, "delete"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.mysqlId, + ctx.session.activeOrganizationId, + "delete", + ); } const mongo = await findMySqlById(input.mysqlId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this MySQL", @@ -261,7 +280,7 @@ export const mysqlRouter = createTRPCRouter({ for (const operation of cleanupOperations) { try { await operation(); - } catch (error) {} + } catch (_) {} } return mongo; @@ -270,7 +289,7 @@ export const mysqlRouter = createTRPCRouter({ .input(apiSaveEnvironmentVariablesMySql) .mutation(async ({ input, ctx }) => { const mysql = await findMySqlById(input.mysqlId); - if (mysql.project.adminId !== ctx.user.adminId) { + if (mysql.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this environment", @@ -294,7 +313,7 @@ export const mysqlRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const { mysqlId, ...rest } = input; const mysql = await findMySqlById(mysqlId); - if (mysql.project.adminId !== ctx.user.adminId) { + if (mysql.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this MySQL", diff --git a/apps/dokploy/server/api/routers/notification.ts b/apps/dokploy/server/api/routers/notification.ts index ea346973..23283d97 100644 --- a/apps/dokploy/server/api/routers/notification.ts +++ b/apps/dokploy/server/api/routers/notification.ts @@ -6,7 +6,6 @@ import { } from "@/server/api/trpc"; import { db } from "@/server/db"; import { - admins, apiCreateDiscord, apiCreateEmail, apiCreateGotify, @@ -25,6 +24,7 @@ import { apiUpdateTelegram, notifications, server, + users_temp, } from "@/server/db/schema"; import { IS_CLOUD, @@ -51,13 +51,15 @@ import { TRPCError } from "@trpc/server"; import { desc, eq, sql } from "drizzle-orm"; import { z } from "zod"; -// TODO: Uncomment the validations when is cloud ready export const notificationRouter = createTRPCRouter({ createSlack: adminProcedure .input(apiCreateSlack) .mutation(async ({ input, ctx }) => { try { - return await createSlackNotification(input, ctx.user.adminId); + return await createSlackNotification( + input, + ctx.session.activeOrganizationId, + ); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -71,8 +73,7 @@ export const notificationRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const notification = await findNotificationById(input.notificationId); - if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { - // TODO: Remove isCloud in the next versions of dokploy + if (notification.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this notification", @@ -80,7 +81,7 @@ export const notificationRouter = createTRPCRouter({ } return await updateSlackNotification({ ...input, - adminId: ctx.user.adminId, + organizationId: ctx.session.activeOrganizationId, }); } catch (error) { throw error; @@ -107,7 +108,10 @@ export const notificationRouter = createTRPCRouter({ .input(apiCreateTelegram) .mutation(async ({ input, ctx }) => { try { - return await createTelegramNotification(input, ctx.user.adminId); + return await createTelegramNotification( + input, + ctx.session.activeOrganizationId, + ); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -122,8 +126,7 @@ export const notificationRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const notification = await findNotificationById(input.notificationId); - if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { - // TODO: Remove isCloud in the next versions of dokploy + if (notification.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this notification", @@ -131,7 +134,7 @@ export const notificationRouter = createTRPCRouter({ } return await updateTelegramNotification({ ...input, - adminId: ctx.user.adminId, + organizationId: ctx.session.activeOrganizationId, }); } catch (error) { throw new TRPCError({ @@ -159,7 +162,10 @@ export const notificationRouter = createTRPCRouter({ .input(apiCreateDiscord) .mutation(async ({ input, ctx }) => { try { - return await createDiscordNotification(input, ctx.user.adminId); + return await createDiscordNotification( + input, + ctx.session.activeOrganizationId, + ); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -174,8 +180,7 @@ export const notificationRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const notification = await findNotificationById(input.notificationId); - if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { - // TODO: Remove isCloud in the next versions of dokploy + if (notification.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this notification", @@ -183,7 +188,7 @@ export const notificationRouter = createTRPCRouter({ } return await updateDiscordNotification({ ...input, - adminId: ctx.user.adminId, + organizationId: ctx.session.activeOrganizationId, }); } catch (error) { throw new TRPCError({ @@ -220,7 +225,10 @@ export const notificationRouter = createTRPCRouter({ .input(apiCreateEmail) .mutation(async ({ input, ctx }) => { try { - return await createEmailNotification(input, ctx.user.adminId); + return await createEmailNotification( + input, + ctx.session.activeOrganizationId, + ); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -234,8 +242,7 @@ export const notificationRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const notification = await findNotificationById(input.notificationId); - if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { - // TODO: Remove isCloud in the next versions of dokploy + if (notification.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this notification", @@ -243,7 +250,7 @@ export const notificationRouter = createTRPCRouter({ } return await updateEmailNotification({ ...input, - adminId: ctx.user.adminId, + organizationId: ctx.session.activeOrganizationId, }); } catch (error) { throw new TRPCError({ @@ -276,8 +283,7 @@ export const notificationRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const notification = await findNotificationById(input.notificationId); - if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { - // TODO: Remove isCloud in the next versions of dokploy + if (notification.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this notification", @@ -285,9 +291,13 @@ export const notificationRouter = createTRPCRouter({ } return await removeNotificationById(input.notificationId); } catch (error) { + const message = + error instanceof Error + ? error.message + : "Error deleting this notification"; throw new TRPCError({ code: "BAD_REQUEST", - message: "Error deleting this notification", + message, }); } }), @@ -295,8 +305,7 @@ export const notificationRouter = createTRPCRouter({ .input(apiFindOneNotification) .query(async ({ input, ctx }) => { const notification = await findNotificationById(input.notificationId); - if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { - // TODO: Remove isCloud in the next versions of dokploy + if (notification.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this notification", @@ -314,8 +323,7 @@ export const notificationRouter = createTRPCRouter({ gotify: true, }, orderBy: desc(notifications.createdAt), - ...(IS_CLOUD && { where: eq(notifications.adminId, ctx.user.adminId) }), - // TODO: Remove this line when the cloud version is ready + where: eq(notifications.organizationId, ctx.session.activeOrganizationId), }); }), receiveNotification: publicProcedure @@ -332,24 +340,24 @@ export const notificationRouter = createTRPCRouter({ ) .mutation(async ({ input }) => { try { - let adminId = ""; + let organizationId = ""; let ServerName = ""; if (input.ServerType === "Dokploy") { const result = await db .select() - .from(admins) + .from(users_temp) .where( - sql`${admins.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`, + sql`${users_temp.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`, ); - if (!result?.[0]?.adminId) { + if (!result?.[0]?.id) { throw new TRPCError({ code: "BAD_REQUEST", message: "Token not found", }); } - adminId = result?.[0]?.adminId; + organizationId = result?.[0]?.id; ServerName = "Dokploy"; } else { const result = await db @@ -359,18 +367,18 @@ export const notificationRouter = createTRPCRouter({ sql`${server.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`, ); - if (!result?.[0]?.adminId) { + if (!result?.[0]?.organizationId) { throw new TRPCError({ code: "BAD_REQUEST", message: "Token not found", }); } - adminId = result?.[0]?.adminId; + organizationId = result?.[0]?.organizationId; ServerName = "Remote"; } - await sendServerThresholdNotifications(adminId, { + await sendServerThresholdNotifications(organizationId, { ...input, ServerName, }); @@ -386,7 +394,10 @@ export const notificationRouter = createTRPCRouter({ .input(apiCreateGotify) .mutation(async ({ input, ctx }) => { try { - return await createGotifyNotification(input, ctx.user.adminId); + return await createGotifyNotification( + input, + ctx.session.activeOrganizationId, + ); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -400,7 +411,10 @@ export const notificationRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const notification = await findNotificationById(input.notificationId); - if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { + if ( + IS_CLOUD && + notification.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this notification", @@ -408,7 +422,7 @@ export const notificationRouter = createTRPCRouter({ } return await updateGotifyNotification({ ...input, - adminId: ctx.user.adminId, + organizationId: ctx.session.activeOrganizationId, }); } catch (error) { throw error; diff --git a/apps/dokploy/server/api/routers/organization.ts b/apps/dokploy/server/api/routers/organization.ts new file mode 100644 index 00000000..6f7a9c67 --- /dev/null +++ b/apps/dokploy/server/api/routers/organization.ts @@ -0,0 +1,148 @@ +import { db } from "@/server/db"; +import { invitation, member, organization } from "@/server/db/schema"; +import { IS_CLOUD } from "@dokploy/server/index"; +import { TRPCError } from "@trpc/server"; +import { and, desc, eq, exists } from "drizzle-orm"; +import { nanoid } from "nanoid"; +import { z } from "zod"; +import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc"; +export const organizationRouter = createTRPCRouter({ + create: protectedProcedure + .input( + z.object({ + name: z.string(), + logo: z.string().optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + if (ctx.user.rol !== "owner" && !IS_CLOUD) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Only the organization owner can create an organization", + }); + } + const result = await db + .insert(organization) + .values({ + ...input, + slug: nanoid(), + createdAt: new Date(), + ownerId: ctx.user.id, + }) + .returning() + .then((res) => res[0]); + + console.log("result", result); + + if (!result) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to create organization", + }); + } + + await db.insert(member).values({ + organizationId: result.id, + role: "owner", + createdAt: new Date(), + userId: ctx.user.id, + }); + return result; + }), + all: protectedProcedure.query(async ({ ctx }) => { + const memberResult = await db.query.organization.findMany({ + where: (organization) => + exists( + db + .select() + .from(member) + .where( + and( + eq(member.organizationId, organization.id), + eq(member.userId, ctx.user.id), + ), + ), + ), + }); + return memberResult; + }), + one: protectedProcedure + .input( + z.object({ + organizationId: z.string(), + }), + ) + .query(async ({ input }) => { + return await db.query.organization.findFirst({ + where: eq(organization.id, input.organizationId), + }); + }), + update: protectedProcedure + .input( + z.object({ + organizationId: z.string(), + name: z.string(), + logo: z.string().optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + if (ctx.user.rol !== "owner" && !IS_CLOUD) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Only the organization owner can update it", + }); + } + const result = await db + .update(organization) + .set({ + name: input.name, + logo: input.logo, + }) + .where(eq(organization.id, input.organizationId)) + .returning(); + return result[0]; + }), + delete: protectedProcedure + .input( + z.object({ + organizationId: z.string(), + }), + ) + .mutation(async ({ ctx, input }) => { + if (ctx.user.rol !== "owner" && !IS_CLOUD) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Only the organization owner can delete it", + }); + } + const org = await db.query.organization.findFirst({ + where: eq(organization.id, input.organizationId), + }); + + if (!org) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Organization not found", + }); + } + + if (org.ownerId !== ctx.user.id) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Only the organization owner can delete it", + }); + } + + const result = await db + .delete(organization) + .where(eq(organization.id, input.organizationId)); + + return result; + }), + allInvitations: adminProcedure.query(async ({ ctx }) => { + return await db.query.invitation.findMany({ + where: eq(invitation.organizationId, ctx.session.activeOrganizationId), + orderBy: [desc(invitation.status), desc(invitation.expiresAt)], + }); + }), +}); diff --git a/apps/dokploy/server/api/routers/port.ts b/apps/dokploy/server/api/routers/port.ts index bfbc9863..923fea57 100644 --- a/apps/dokploy/server/api/routers/port.ts +++ b/apps/dokploy/server/api/routers/port.ts @@ -44,9 +44,11 @@ export const portRouter = createTRPCRouter({ try { return removePortById(input.portId); } catch (error) { + const message = + error instanceof Error ? error.message : "Error input: Deleting port"; throw new TRPCError({ code: "BAD_REQUEST", - message: "Error input: Deleting port", + message, }); } }), @@ -56,9 +58,11 @@ export const portRouter = createTRPCRouter({ try { return updatePortById(input.portId, input); } catch (error) { + const message = + error instanceof Error ? error.message : "Error updating the port"; throw new TRPCError({ code: "BAD_REQUEST", - message: "Error updating the port", + message, }); } }), diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index 92603a61..cf3221b4 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -1,9 +1,4 @@ -import { EventEmitter } from "node:events"; -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "@/server/api/trpc"; +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { apiChangePostgresStatus, apiCreatePostgres, @@ -35,17 +30,19 @@ import { } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import { observable } from "@trpc/server/observable"; -import { z } from "zod"; - -const ee = new EventEmitter(); export const postgresRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreatePostgres) .mutation(async ({ input, ctx }) => { try { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.projectId, + ctx.session.activeOrganizationId, + "create", + ); } if (IS_CLOUD && !input.serverId) { @@ -56,15 +53,19 @@ export const postgresRouter = createTRPCRouter({ } const project = await findProjectById(input.projectId); - if (project.adminId !== ctx.user.adminId) { + if (project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this project", }); } const newPostgres = await createPostgres(input); - if (ctx.user.rol === "user") { - await addNewService(ctx.user.authId, newPostgres.postgresId); + if (ctx.user.rol === "member") { + await addNewService( + ctx.user.id, + newPostgres.postgresId, + project.organizationId, + ); } await createMount({ @@ -90,12 +91,19 @@ export const postgresRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOnePostgres) .query(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.postgresId, "access"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.postgresId, + ctx.session.activeOrganizationId, + "access", + ); } const postgres = await findPostgresById(input.postgresId); - if (postgres.project.adminId !== ctx.user.adminId) { + if ( + postgres.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this Postgres", @@ -109,7 +117,7 @@ export const postgresRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const service = await findPostgresById(input.postgresId); - if (service.project.adminId !== ctx.user.adminId) { + if (service.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to start this Postgres", @@ -131,7 +139,9 @@ export const postgresRouter = createTRPCRouter({ .input(apiFindOnePostgres) .mutation(async ({ input, ctx }) => { const postgres = await findPostgresById(input.postgresId); - if (postgres.project.adminId !== ctx.user.adminId) { + if ( + postgres.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to stop this Postgres", @@ -153,7 +163,9 @@ export const postgresRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const postgres = await findPostgresById(input.postgresId); - if (postgres.project.adminId !== ctx.user.adminId) { + if ( + postgres.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this external port", @@ -169,7 +181,9 @@ export const postgresRouter = createTRPCRouter({ .input(apiDeployPostgres) .mutation(async ({ input, ctx }) => { const postgres = await findPostgresById(input.postgresId); - if (postgres.project.adminId !== ctx.user.adminId) { + if ( + postgres.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this Postgres", @@ -190,7 +204,9 @@ export const postgresRouter = createTRPCRouter({ .input(apiDeployPostgres) .subscription(async ({ input, ctx }) => { const postgres = await findPostgresById(input.postgresId); - if (postgres.project.adminId !== ctx.user.adminId) { + if ( + postgres.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this Postgres", @@ -207,7 +223,9 @@ export const postgresRouter = createTRPCRouter({ .input(apiChangePostgresStatus) .mutation(async ({ input, ctx }) => { const postgres = await findPostgresById(input.postgresId); - if (postgres.project.adminId !== ctx.user.adminId) { + if ( + postgres.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to change this Postgres status", @@ -221,12 +239,19 @@ export const postgresRouter = createTRPCRouter({ remove: protectedProcedure .input(apiFindOnePostgres) .mutation(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.postgresId, "delete"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.postgresId, + ctx.session.activeOrganizationId, + "delete", + ); } const postgres = await findPostgresById(input.postgresId); - if (postgres.project.adminId !== ctx.user.adminId) { + if ( + postgres.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this Postgres", @@ -249,7 +274,9 @@ export const postgresRouter = createTRPCRouter({ .input(apiSaveEnvironmentVariablesPostgres) .mutation(async ({ input, ctx }) => { const postgres = await findPostgresById(input.postgresId); - if (postgres.project.adminId !== ctx.user.adminId) { + if ( + postgres.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this environment", @@ -272,7 +299,9 @@ export const postgresRouter = createTRPCRouter({ .input(apiResetPostgres) .mutation(async ({ input, ctx }) => { const postgres = await findPostgresById(input.postgresId); - if (postgres.project.adminId !== ctx.user.adminId) { + if ( + postgres.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to reload this Postgres", @@ -302,7 +331,9 @@ export const postgresRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const { postgresId, ...rest } = input; const postgres = await findPostgresById(postgresId); - if (postgres.project.adminId !== ctx.user.adminId) { + if ( + postgres.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this Postgres", diff --git a/apps/dokploy/server/api/routers/preview-deployment.ts b/apps/dokploy/server/api/routers/preview-deployment.ts index 74b8461a..f833e9f9 100644 --- a/apps/dokploy/server/api/routers/preview-deployment.ts +++ b/apps/dokploy/server/api/routers/preview-deployment.ts @@ -14,7 +14,9 @@ export const previewDeploymentRouter = createTRPCRouter({ .input(apiFindAllByApplication) .query(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", @@ -28,7 +30,10 @@ export const previewDeploymentRouter = createTRPCRouter({ const previewDeployment = await findPreviewDeploymentById( input.previewDeploymentId, ); - if (previewDeployment.application.project.adminId !== ctx.user.adminId) { + if ( + previewDeployment.application.project.organizationId !== + ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this preview deployment", @@ -43,7 +48,10 @@ export const previewDeploymentRouter = createTRPCRouter({ const previewDeployment = await findPreviewDeploymentById( input.previewDeploymentId, ); - if (previewDeployment.application.project.adminId !== ctx.user.adminId) { + if ( + previewDeployment.application.project.organizationId !== + ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this preview deployment", diff --git a/apps/dokploy/server/api/routers/project.ts b/apps/dokploy/server/api/routers/project.ts index 35b2669b..438a3f07 100644 --- a/apps/dokploy/server/api/routers/project.ts +++ b/apps/dokploy/server/api/routers/project.ts @@ -15,32 +15,34 @@ import { redis, } from "@/server/db/schema"; -import { TRPCError } from "@trpc/server"; -import { and, desc, eq, sql } from "drizzle-orm"; -import type { AnyPgColumn } from "drizzle-orm/pg-core"; - import { IS_CLOUD, addNewProject, checkProjectAccess, createProject, deleteProject, - findAdminById, + findMemberById, findProjectById, - findUserByAuthId, + findUserById, updateProjectById, } from "@dokploy/server"; - +import { TRPCError } from "@trpc/server"; +import { and, desc, eq, sql } from "drizzle-orm"; +import type { AnyPgColumn } from "drizzle-orm/pg-core"; export const projectRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateProject) .mutation(async ({ ctx, input }) => { try { - if (ctx.user.rol === "user") { - await checkProjectAccess(ctx.user.authId, "create"); + if (ctx.user.rol === "member") { + await checkProjectAccess( + ctx.user.id, + "create", + ctx.session.activeOrganizationId, + ); } - const admin = await findAdminById(ctx.user.adminId); + const admin = await findUserById(ctx.user.ownerId); if (admin.serversQuantity === 0 && IS_CLOUD) { throw new TRPCError({ @@ -49,9 +51,16 @@ export const projectRouter = createTRPCRouter({ }); } - const project = await createProject(input, ctx.user.adminId); - if (ctx.user.rol === "user") { - await addNewProject(ctx.user.authId, project.projectId); + const project = await createProject( + input, + ctx.session.activeOrganizationId, + ); + if (ctx.user.rol === "member") { + await addNewProject( + ctx.user.id, + project.projectId, + ctx.session.activeOrganizationId, + ); } return project; @@ -67,15 +76,23 @@ export const projectRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOneProject) .query(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - const { accessedServices } = await findUserByAuthId(ctx.user.authId); + if (ctx.user.rol === "member") { + const { accessedServices } = await findMemberById( + ctx.user.id, + ctx.session.activeOrganizationId, + ); - await checkProjectAccess(ctx.user.authId, "access", input.projectId); + await checkProjectAccess( + ctx.user.id, + "access", + ctx.session.activeOrganizationId, + input.projectId, + ); const project = await db.query.projects.findFirst({ where: and( eq(projects.projectId, input.projectId), - eq(projects.adminId, ctx.user.adminId), + eq(projects.organizationId, ctx.session.activeOrganizationId), ), with: { compose: { @@ -115,7 +132,7 @@ export const projectRouter = createTRPCRouter({ } const project = await findProjectById(input.projectId); - if (project.adminId !== ctx.user.adminId) { + if (project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this project", @@ -124,9 +141,11 @@ export const projectRouter = createTRPCRouter({ return project; }), all: protectedProcedure.query(async ({ ctx }) => { - if (ctx.user.rol === "user") { - const { accessedProjects, accessedServices } = await findUserByAuthId( - ctx.user.authId, + // console.log(ctx.user); + if (ctx.user.rol === "member") { + const { accessedProjects, accessedServices } = await findMemberById( + ctx.user.id, + ctx.session.activeOrganizationId, ); if (accessedProjects.length === 0) { @@ -139,7 +158,7 @@ export const projectRouter = createTRPCRouter({ accessedProjects.map((projectId) => sql`${projectId}`), sql`, `, )})`, - eq(projects.adminId, ctx.user.adminId), + eq(projects.organizationId, ctx.session.activeOrganizationId), ), with: { applications: { @@ -193,7 +212,7 @@ export const projectRouter = createTRPCRouter({ }, }, }, - where: eq(projects.adminId, ctx.user.adminId), + where: eq(projects.organizationId, ctx.session.activeOrganizationId), orderBy: desc(projects.createdAt), }); }), @@ -202,11 +221,17 @@ export const projectRouter = createTRPCRouter({ .input(apiRemoveProject) .mutation(async ({ input, ctx }) => { try { - if (ctx.user.rol === "user") { - await checkProjectAccess(ctx.user.authId, "delete"); + if (ctx.user.rol === "member") { + await checkProjectAccess( + ctx.user.id, + "delete", + ctx.session.activeOrganizationId, + ); } const currentProject = await findProjectById(input.projectId); - if (currentProject.adminId !== ctx.user.adminId) { + if ( + currentProject.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this project", @@ -224,7 +249,9 @@ export const projectRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const currentProject = await findProjectById(input.projectId); - if (currentProject.adminId !== ctx.user.adminId) { + if ( + currentProject.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this project", diff --git a/apps/dokploy/server/api/routers/redirects.ts b/apps/dokploy/server/api/routers/redirects.ts index bcd7962a..2d520cc4 100644 --- a/apps/dokploy/server/api/routers/redirects.ts +++ b/apps/dokploy/server/api/routers/redirects.ts @@ -18,7 +18,9 @@ export const redirectsRouter = createTRPCRouter({ .input(apiCreateRedirect) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", @@ -31,7 +33,9 @@ export const redirectsRouter = createTRPCRouter({ .query(async ({ input, ctx }) => { const redirect = await findRedirectById(input.redirectId); const application = await findApplicationById(redirect.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", @@ -44,7 +48,9 @@ export const redirectsRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const redirect = await findRedirectById(input.redirectId); const application = await findApplicationById(redirect.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", @@ -57,7 +63,9 @@ export const redirectsRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const redirect = await findRedirectById(input.redirectId); const application = await findApplicationById(redirect.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", diff --git a/apps/dokploy/server/api/routers/redis.ts b/apps/dokploy/server/api/routers/redis.ts index 967fb51a..a80660bf 100644 --- a/apps/dokploy/server/api/routers/redis.ts +++ b/apps/dokploy/server/api/routers/redis.ts @@ -36,8 +36,13 @@ export const redisRouter = createTRPCRouter({ .input(apiCreateRedis) .mutation(async ({ input, ctx }) => { try { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.projectId, + ctx.session.activeOrganizationId, + "create", + ); } if (IS_CLOUD && !input.serverId) { @@ -48,15 +53,19 @@ export const redisRouter = createTRPCRouter({ } const project = await findProjectById(input.projectId); - if (project.adminId !== ctx.user.adminId) { + if (project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this project", }); } const newRedis = await createRedis(input); - if (ctx.user.rol === "user") { - await addNewService(ctx.user.authId, newRedis.redisId); + if (ctx.user.rol === "member") { + await addNewService( + ctx.user.id, + newRedis.redisId, + project.organizationId, + ); } await createMount({ @@ -75,12 +84,17 @@ export const redisRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOneRedis) .query(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.redisId, "access"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.redisId, + ctx.session.activeOrganizationId, + "access", + ); } const redis = await findRedisById(input.redisId); - if (redis.project.adminId !== ctx.user.adminId) { + if (redis.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this Redis", @@ -93,7 +107,7 @@ export const redisRouter = createTRPCRouter({ .input(apiFindOneRedis) .mutation(async ({ input, ctx }) => { const redis = await findRedisById(input.redisId); - if (redis.project.adminId !== ctx.user.adminId) { + if (redis.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to start this Redis", @@ -115,7 +129,7 @@ export const redisRouter = createTRPCRouter({ .input(apiResetRedis) .mutation(async ({ input, ctx }) => { const redis = await findRedisById(input.redisId); - if (redis.project.adminId !== ctx.user.adminId) { + if (redis.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to reload this Redis", @@ -145,7 +159,7 @@ export const redisRouter = createTRPCRouter({ .input(apiFindOneRedis) .mutation(async ({ input, ctx }) => { const redis = await findRedisById(input.redisId); - if (redis.project.adminId !== ctx.user.adminId) { + if (redis.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to stop this Redis", @@ -166,7 +180,7 @@ export const redisRouter = createTRPCRouter({ .input(apiSaveExternalPortRedis) .mutation(async ({ input, ctx }) => { const mongo = await findRedisById(input.redisId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this external port", @@ -182,7 +196,7 @@ export const redisRouter = createTRPCRouter({ .input(apiDeployRedis) .mutation(async ({ input, ctx }) => { const redis = await findRedisById(input.redisId); - if (redis.project.adminId !== ctx.user.adminId) { + if (redis.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this Redis", @@ -202,7 +216,7 @@ export const redisRouter = createTRPCRouter({ .input(apiDeployRedis) .subscription(async ({ input, ctx }) => { const redis = await findRedisById(input.redisId); - if (redis.project.adminId !== ctx.user.adminId) { + if (redis.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this Redis", @@ -218,7 +232,7 @@ export const redisRouter = createTRPCRouter({ .input(apiChangeRedisStatus) .mutation(async ({ input, ctx }) => { const mongo = await findRedisById(input.redisId); - if (mongo.project.adminId !== ctx.user.adminId) { + if (mongo.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to change this Redis status", @@ -232,13 +246,18 @@ export const redisRouter = createTRPCRouter({ remove: protectedProcedure .input(apiFindOneRedis) .mutation(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.redisId, "delete"); + if (ctx.user.rol === "member") { + await checkServiceAccess( + ctx.user.id, + input.redisId, + ctx.session.activeOrganizationId, + "delete", + ); } const redis = await findRedisById(input.redisId); - if (redis.project.adminId !== ctx.user.adminId) { + if (redis.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this Redis", @@ -252,7 +271,7 @@ export const redisRouter = createTRPCRouter({ for (const operation of cleanupOperations) { try { await operation(); - } catch (error) {} + } catch (_) {} } return redis; @@ -261,7 +280,7 @@ export const redisRouter = createTRPCRouter({ .input(apiSaveEnvironmentVariablesRedis) .mutation(async ({ input, ctx }) => { const redis = await findRedisById(input.redisId); - if (redis.project.adminId !== ctx.user.adminId) { + if (redis.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this environment", diff --git a/apps/dokploy/server/api/routers/registry.ts b/apps/dokploy/server/api/routers/registry.ts index f66ed4ae..a9a6be89 100644 --- a/apps/dokploy/server/api/routers/registry.ts +++ b/apps/dokploy/server/api/routers/registry.ts @@ -1,34 +1,35 @@ +import { db } from "@/server/db"; import { apiCreateRegistry, apiFindOneRegistry, apiRemoveRegistry, apiTestRegistry, apiUpdateRegistry, + registry, } from "@/server/db/schema"; import { IS_CLOUD, createRegistry, execAsync, execAsyncRemote, - findAllRegistryByAdminId, findRegistryById, removeRegistry, updateRegistry, } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc"; - export const registryRouter = createTRPCRouter({ create: adminProcedure .input(apiCreateRegistry) .mutation(async ({ ctx, input }) => { - return await createRegistry(input, ctx.user.adminId); + return await createRegistry(input, ctx.session.activeOrganizationId); }), remove: adminProcedure .input(apiRemoveRegistry) .mutation(async ({ ctx, input }) => { const registry = await findRegistryById(input.registryId); - if (registry.adminId !== ctx.user.adminId) { + if (registry.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to delete this registry", @@ -41,7 +42,7 @@ export const registryRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const { registryId, ...rest } = input; const registry = await findRegistryById(registryId); - if (registry.adminId !== ctx.user.adminId) { + if (registry.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to update this registry", @@ -61,13 +62,16 @@ export const registryRouter = createTRPCRouter({ return true; }), all: protectedProcedure.query(async ({ ctx }) => { - return await findAllRegistryByAdminId(ctx.user.adminId); + const registryResponse = await db.query.registry.findMany({ + where: eq(registry.organizationId, ctx.session.activeOrganizationId), + }); + return registryResponse; }), one: adminProcedure .input(apiFindOneRegistry) .query(async ({ input, ctx }) => { const registry = await findRegistryById(input.registryId); - if (registry.adminId !== ctx.user.adminId) { + if (registry.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this registry", diff --git a/apps/dokploy/server/api/routers/security.ts b/apps/dokploy/server/api/routers/security.ts index 5318a293..b8e70bbb 100644 --- a/apps/dokploy/server/api/routers/security.ts +++ b/apps/dokploy/server/api/routers/security.ts @@ -18,7 +18,9 @@ export const securityRouter = createTRPCRouter({ .input(apiCreateSecurity) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", @@ -31,7 +33,9 @@ export const securityRouter = createTRPCRouter({ .query(async ({ input, ctx }) => { const security = await findSecurityById(input.securityId); const application = await findApplicationById(security.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", @@ -44,7 +48,9 @@ export const securityRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const security = await findSecurityById(input.securityId); const application = await findApplicationById(security.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", @@ -57,7 +63,9 @@ export const securityRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const security = await findSecurityById(input.securityId); const application = await findApplicationById(security.applicationId); - if (application.project.adminId !== ctx.user.adminId) { + if ( + application.project.organizationId !== ctx.session.activeOrganizationId + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts index 0094b675..1a9ebc0a 100644 --- a/apps/dokploy/server/api/routers/server.ts +++ b/apps/dokploy/server/api/routers/server.ts @@ -12,6 +12,7 @@ import { mariadb, mongo, mysql, + organization, postgres, redis, server, @@ -21,9 +22,9 @@ import { createServer, defaultCommand, deleteServer, - findAdminById, findServerById, - findServersByAdminId, + findServersByUserId, + findUserById, getPublicIpWithFallback, haveActiveServices, removeDeploymentsByServerId, @@ -36,21 +37,25 @@ import { import { TRPCError } from "@trpc/server"; import { observable } from "@trpc/server/observable"; import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm"; +import { z } from "zod"; export const serverRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateServer) .mutation(async ({ ctx, input }) => { try { - const admin = await findAdminById(ctx.user.adminId); - const servers = await findServersByAdminId(admin.adminId); - if (IS_CLOUD && servers.length >= admin.serversQuantity) { + const user = await findUserById(ctx.user.ownerId); + const servers = await findServersByUserId(user.id); + if (IS_CLOUD && servers.length >= user.serversQuantity) { throw new TRPCError({ code: "BAD_REQUEST", message: "You cannot create more servers", }); } - const project = await createServer(input, ctx.user.adminId); + const project = await createServer( + input, + ctx.session.activeOrganizationId, + ); return project; } catch (error) { throw new TRPCError({ @@ -65,7 +70,7 @@ export const serverRouter = createTRPCRouter({ .input(apiFindOneServer) .query(async ({ input, ctx }) => { const server = await findServerById(input.serverId); - if (server.adminId !== ctx.user.adminId) { + if (server.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this server", @@ -76,7 +81,7 @@ export const serverRouter = createTRPCRouter({ }), getDefaultCommand: protectedProcedure .input(apiFindOneServer) - .query(async ({ input, ctx }) => { + .query(async () => { return defaultCommand(); }), all: protectedProcedure.query(async ({ ctx }) => { @@ -93,22 +98,37 @@ export const serverRouter = createTRPCRouter({ .leftJoin(mongo, eq(mongo.serverId, server.serverId)) .leftJoin(mysql, eq(mysql.serverId, server.serverId)) .leftJoin(postgres, eq(postgres.serverId, server.serverId)) - .where(eq(server.adminId, ctx.user.adminId)) + .where(eq(server.organizationId, ctx.session.activeOrganizationId)) .orderBy(desc(server.createdAt)) .groupBy(server.serverId); return result; }), + count: protectedProcedure.query(async ({ ctx }) => { + const organizations = await db.query.organization.findMany({ + where: eq(organization.ownerId, ctx.user.id), + with: { + servers: true, + }, + }); + + const servers = organizations.flatMap((org) => org.servers); + + return servers.length ?? 0; + }), withSSHKey: protectedProcedure.query(async ({ ctx }) => { const result = await db.query.server.findMany({ orderBy: desc(server.createdAt), where: IS_CLOUD ? and( isNotNull(server.sshKeyId), - eq(server.adminId, ctx.user.adminId), + eq(server.organizationId, ctx.session.activeOrganizationId), eq(server.serverStatus, "active"), ) - : and(isNotNull(server.sshKeyId), eq(server.adminId, ctx.user.adminId)), + : and( + isNotNull(server.sshKeyId), + eq(server.organizationId, ctx.session.activeOrganizationId), + ), }); return result; }), @@ -117,7 +137,7 @@ export const serverRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const server = await findServerById(input.serverId); - if (server.adminId !== ctx.user.adminId) { + if (server.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to setup this server", @@ -142,7 +162,7 @@ export const serverRouter = createTRPCRouter({ .subscription(async ({ input, ctx }) => { try { const server = await findServerById(input.serverId); - if (server.adminId !== ctx.user.adminId) { + if (server.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to setup this server", @@ -162,7 +182,7 @@ export const serverRouter = createTRPCRouter({ .query(async ({ input, ctx }) => { try { const server = await findServerById(input.serverId); - if (server.adminId !== ctx.user.adminId) { + if (server.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to validate this server", @@ -204,7 +224,7 @@ export const serverRouter = createTRPCRouter({ .query(async ({ input, ctx }) => { try { const server = await findServerById(input.serverId); - if (server.adminId !== ctx.user.adminId) { + if (server.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to validate this server", @@ -254,7 +274,7 @@ export const serverRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const server = await findServerById(input.serverId); - if (server.adminId !== ctx.user.adminId) { + if (server.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to setup this server", @@ -296,7 +316,7 @@ export const serverRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const server = await findServerById(input.serverId); - if (server.adminId !== ctx.user.adminId) { + if (server.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this server", @@ -315,12 +335,9 @@ export const serverRouter = createTRPCRouter({ await deleteServer(input.serverId); if (IS_CLOUD) { - const admin = await findAdminById(ctx.user.adminId); + const admin = await findUserById(ctx.user.ownerId); - await updateServersBasedOnQuantity( - admin.adminId, - admin.serversQuantity, - ); + await updateServersBasedOnQuantity(admin.id, admin.serversQuantity); } return currentServer; @@ -333,7 +350,7 @@ export const serverRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const server = await findServerById(input.serverId); - if (server.adminId !== ctx.user.adminId) { + if (server.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this server", @@ -355,11 +372,69 @@ export const serverRouter = createTRPCRouter({ throw error; } }), - publicIp: protectedProcedure.query(async ({ ctx }) => { + publicIp: protectedProcedure.query(async () => { if (IS_CLOUD) { return ""; } const ip = await getPublicIpWithFallback(); return ip; }), + getServerMetrics: protectedProcedure + .input( + z.object({ + url: z.string(), + token: z.string(), + dataPoints: z.string(), + }), + ) + .query(async ({ input }) => { + try { + const url = new URL(input.url); + url.searchParams.append("limit", input.dataPoints); + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${input.token}`, + }, + }); + if (!response.ok) { + throw new Error( + `Error ${response.status}: ${response.statusText}. Ensure the container is running and this service is included in the monitoring configuration.`, + ); + } + + const data = await response.json(); + if (!Array.isArray(data) || data.length === 0) { + throw new Error( + [ + "No monitoring data available. This could be because:", + "", + "1. You don't have setup the monitoring service, you can do in web server section.", + "2. If you already have setup the monitoring service, wait a few minutes and refresh the page.", + ].join("\n"), + ); + } + return data as { + cpu: string; + cpuModel: string; + cpuCores: number; + cpuPhysicalCores: number; + cpuSpeed: number; + os: string; + distro: string; + kernel: string; + arch: string; + memUsed: string; + memUsedGB: string; + memTotal: string; + uptime: number; + diskUsed: string; + totalDisk: string; + networkIn: string; + networkOut: string; + timestamp: string; + }[]; + } catch (error) { + throw error; + } + }), }); diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index cb0e32d9..fc1255fc 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -22,9 +22,8 @@ import { cleanUpUnusedVolumes, execAsync, execAsyncRemote, - findAdmin, - findAdminById, findServerById, + findUserById, getDokployImage, getDokployImageTag, getUpdateData, @@ -47,10 +46,10 @@ import { startServiceRemote, stopService, stopServiceRemote, - updateAdmin, updateLetsEncryptEmail, updateServerById, updateServerTraefik, + updateUser, writeConfig, writeMainConfig, writeTraefikConfigInPath, @@ -164,7 +163,7 @@ export const settingsRouter = createTRPCRouter({ if (IS_CLOUD) { return true; } - await updateAdmin(ctx.user.authId, { + await updateUser(ctx.user.id, { sshPrivateKey: input.sshPrivateKey, }); @@ -176,7 +175,7 @@ export const settingsRouter = createTRPCRouter({ if (IS_CLOUD) { return true; } - const admin = await updateAdmin(ctx.user.authId, { + const user = await updateUser(ctx.user.id, { host: input.host, ...(input.letsEncryptEmail && { letsEncryptEmail: input.letsEncryptEmail, @@ -184,25 +183,25 @@ export const settingsRouter = createTRPCRouter({ certificateType: input.certificateType, }); - if (!admin) { + if (!user) { throw new TRPCError({ code: "NOT_FOUND", - message: "Admin not found", + message: "User not found", }); } - updateServerTraefik(admin, input.host); + updateServerTraefik(user, input.host); if (input.letsEncryptEmail) { updateLetsEncryptEmail(input.letsEncryptEmail); } - return admin; + return user; }), cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => { if (IS_CLOUD) { return true; } - await updateAdmin(ctx.user.authId, { + await updateUser(ctx.user.id, { sshPrivateKey: null, }); return true; @@ -217,7 +216,7 @@ export const settingsRouter = createTRPCRouter({ const server = await findServerById(input.serverId); - if (server.adminId !== ctx.user.adminId) { + if (server.organizationId !== ctx.session?.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this server", @@ -246,7 +245,7 @@ export const settingsRouter = createTRPCRouter({ await cleanUpUnusedImages(server.serverId); await cleanUpDockerBuilder(server.serverId); await cleanUpSystemPrune(server.serverId); - await sendDockerCleanupNotifications(server.adminId); + await sendDockerCleanupNotifications(server.organizationId); }); } } else { @@ -262,19 +261,11 @@ export const settingsRouter = createTRPCRouter({ } } } else if (!IS_CLOUD) { - const admin = await findAdminById(ctx.user.adminId); - - if (admin.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this admin", - }); - } - const adminUpdated = await updateAdmin(ctx.user.authId, { + const userUpdated = await updateUser(ctx.user.id, { enableDockerCleanup: input.enableDockerCleanup, }); - if (adminUpdated?.enableDockerCleanup) { + if (userUpdated?.enableDockerCleanup) { scheduleJob("docker-cleanup", "0 0 * * *", async () => { console.log( `Docker Cleanup ${new Date().toLocaleString()}] Running...`, @@ -282,7 +273,9 @@ export const settingsRouter = createTRPCRouter({ await cleanUpUnusedImages(); await cleanUpDockerBuilder(); await cleanUpSystemPrune(); - await sendDockerCleanupNotifications(admin.adminId); + await sendDockerCleanupNotifications( + ctx.session.activeOrganizationId, + ); }); } else { const currentJob = scheduledJobs["docker-cleanup"]; @@ -383,8 +376,11 @@ export const settingsRouter = createTRPCRouter({ .input(apiServerSchema) .query(async ({ ctx, input }) => { try { - if (ctx.user.rol === "user") { - const canAccess = await canAccessToTraefikFiles(ctx.user.authId); + if (ctx.user.rol === "member") { + const canAccess = await canAccessToTraefikFiles( + ctx.user.id, + ctx.session.activeOrganizationId, + ); if (!canAccess) { throw new TRPCError({ code: "UNAUTHORIZED" }); @@ -401,8 +397,11 @@ export const settingsRouter = createTRPCRouter({ updateTraefikFile: protectedProcedure .input(apiModifyTraefikConfig) .mutation(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - const canAccess = await canAccessToTraefikFiles(ctx.user.authId); + if (ctx.user.rol === "member") { + const canAccess = await canAccessToTraefikFiles( + ctx.user.id, + ctx.session.activeOrganizationId, + ); if (!canAccess) { throw new TRPCError({ code: "UNAUTHORIZED" }); @@ -419,8 +418,11 @@ export const settingsRouter = createTRPCRouter({ readTraefikFile: protectedProcedure .input(apiReadTraefikConfig) .query(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - const canAccess = await canAccessToTraefikFiles(ctx.user.authId); + if (ctx.user.rol === "member") { + const canAccess = await canAccessToTraefikFiles( + ctx.user.id, + ctx.session.activeOrganizationId, + ); if (!canAccess) { throw new TRPCError({ code: "UNAUTHORIZED" }); @@ -428,12 +430,12 @@ export const settingsRouter = createTRPCRouter({ } return readConfigInPath(input.path, input.serverId); }), - getIp: protectedProcedure.query(async () => { + getIp: protectedProcedure.query(async ({ ctx }) => { if (IS_CLOUD) { return true; } - const admin = await findAdmin(); - return admin.serverIp; + const user = await findUserById(ctx.user.ownerId); + return user.serverIp; }), getOpenApiDocument: protectedProcedure.query( @@ -480,10 +482,28 @@ export const settingsRouter = createTRPCRouter({ openApiDocument.info = { title: "Dokploy API", description: "Endpoints for dokploy", - // TODO: get version from package.json version: "1.0.0", }; + // Add security schemes configuration + openApiDocument.components = { + ...openApiDocument.components, + securitySchemes: { + apiKey: { + type: "apiKey", + in: "header", + name: "x-api-key", + description: "API key authentication", + }, + }, + }; + + // Apply security globally to all endpoints + openApiDocument.security = [ + { + apiKey: [], + }, + ]; return openApiDocument; }, ), @@ -655,7 +675,7 @@ export const settingsRouter = createTRPCRouter({ return true; }), - isCloud: protectedProcedure.query(async () => { + isCloud: publicProcedure.query(async () => { return IS_CLOUD; }), health: publicProcedure.query(async () => { @@ -715,7 +735,12 @@ export const settingsRouter = createTRPCRouter({ try { return await checkGPUStatus(input.serverId || ""); } catch (error) { - throw new Error("Failed to check GPU status"); + const message = + error instanceof Error ? error.message : "Failed to check GPU status"; + throw new TRPCError({ + code: "BAD_REQUEST", + message, + }); } }), updateTraefikPorts: adminProcedure diff --git a/apps/dokploy/server/api/routers/ssh-key.ts b/apps/dokploy/server/api/routers/ssh-key.ts index fe2f24f9..4663af8f 100644 --- a/apps/dokploy/server/api/routers/ssh-key.ts +++ b/apps/dokploy/server/api/routers/ssh-key.ts @@ -9,7 +9,6 @@ import { sshKeys, } from "@/server/db/schema"; import { - IS_CLOUD, createSshKey, findSSHKeyById, generateSSHKey, @@ -26,7 +25,7 @@ export const sshRouter = createTRPCRouter({ try { await createSshKey({ ...input, - adminId: ctx.user.adminId, + organizationId: ctx.session.activeOrganizationId, }); } catch (error) { throw new TRPCError({ @@ -41,8 +40,7 @@ export const sshRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const sshKey = await findSSHKeyById(input.sshKeyId); - if (IS_CLOUD && sshKey.adminId !== ctx.user.adminId) { - // TODO: Remove isCloud in the next versions of dokploy + if (sshKey.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to delete this SSH key", @@ -59,8 +57,7 @@ export const sshRouter = createTRPCRouter({ .query(async ({ input, ctx }) => { const sshKey = await findSSHKeyById(input.sshKeyId); - if (IS_CLOUD && sshKey.adminId !== ctx.user.adminId) { - // TODO: Remove isCloud in the next versions of dokploy + if (sshKey.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to access this SSH key", @@ -70,10 +67,9 @@ export const sshRouter = createTRPCRouter({ }), all: protectedProcedure.query(async ({ ctx }) => { return await db.query.sshKeys.findMany({ - ...(IS_CLOUD && { where: eq(sshKeys.adminId, ctx.user.adminId) }), + where: eq(sshKeys.organizationId, ctx.session.activeOrganizationId), orderBy: desc(sshKeys.createdAt), }); - // TODO: Remove this line when the cloud version is ready }), generate: protectedProcedure .input(apiGenerateSSHKey) @@ -85,8 +81,7 @@ export const sshRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const sshKey = await findSSHKeyById(input.sshKeyId); - if (IS_CLOUD && sshKey.adminId !== ctx.user.adminId) { - // TODO: Remove isCloud in the next versions of dokploy + if (sshKey.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not allowed to update this SSH key", diff --git a/apps/dokploy/server/api/routers/stripe.ts b/apps/dokploy/server/api/routers/stripe.ts index 7a8a537c..a226eeac 100644 --- a/apps/dokploy/server/api/routers/stripe.ts +++ b/apps/dokploy/server/api/routers/stripe.ts @@ -1,9 +1,9 @@ import { WEBSITE_URL, getStripeItems } from "@/server/utils/stripe"; import { IS_CLOUD, - findAdminById, - findServersByAdminId, - updateAdmin, + findServersByUserId, + findUserById, + updateUser, } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import Stripe from "stripe"; @@ -12,8 +12,8 @@ import { adminProcedure, createTRPCRouter } from "../trpc"; export const stripeRouter = createTRPCRouter({ getProducts: adminProcedure.query(async ({ ctx }) => { - const admin = await findAdminById(ctx.user.adminId); - const stripeCustomerId = admin.stripeCustomerId; + const user = await findUserById(ctx.user.ownerId); + const stripeCustomerId = user.stripeCustomerId; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2024-09-30.acacia", @@ -56,15 +56,15 @@ export const stripeRouter = createTRPCRouter({ }); const items = getStripeItems(input.serverQuantity, input.isAnnual); - const admin = await findAdminById(ctx.user.adminId); + const user = await findUserById(ctx.user.id); - let stripeCustomerId = admin.stripeCustomerId; + let stripeCustomerId = user.stripeCustomerId; if (stripeCustomerId) { const customer = await stripe.customers.retrieve(stripeCustomerId); if (customer.deleted) { - await updateAdmin(admin.authId, { + await updateUser(user.id, { stripeCustomerId: null, }); stripeCustomerId = null; @@ -78,7 +78,7 @@ export const stripeRouter = createTRPCRouter({ customer: stripeCustomerId, }), metadata: { - adminId: admin.adminId, + adminId: user.id, }, allow_promotion_codes: true, success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`, @@ -87,45 +87,43 @@ export const stripeRouter = createTRPCRouter({ return { sessionId: session.id }; }), - createCustomerPortalSession: adminProcedure.mutation( - async ({ ctx, input }) => { - const admin = await findAdminById(ctx.user.adminId); + createCustomerPortalSession: adminProcedure.mutation(async ({ ctx }) => { + const user = await findUserById(ctx.user.id); - if (!admin.stripeCustomerId) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Stripe Customer ID not found", - }); - } - const stripeCustomerId = admin.stripeCustomerId; + if (!user.stripeCustomerId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Stripe Customer ID not found", + }); + } + const stripeCustomerId = user.stripeCustomerId; - const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { - apiVersion: "2024-09-30.acacia", + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: "2024-09-30.acacia", + }); + + try { + const session = await stripe.billingPortal.sessions.create({ + customer: stripeCustomerId, + return_url: `${WEBSITE_URL}/dashboard/settings/billing`, }); - try { - const session = await stripe.billingPortal.sessions.create({ - customer: stripeCustomerId, - return_url: `${WEBSITE_URL}/dashboard/settings/billing`, - }); - - return { url: session.url }; - } catch (error) { - return { - url: "", - }; - } - }, - ), + return { url: session.url }; + } catch (_) { + return { + url: "", + }; + } + }), canCreateMoreServers: adminProcedure.query(async ({ ctx }) => { - const admin = await findAdminById(ctx.user.adminId); - const servers = await findServersByAdminId(admin.adminId); + const user = await findUserById(ctx.user.ownerId); + const servers = await findServersByUserId(user.id); if (!IS_CLOUD) { return true; } - return servers.length < admin.serversQuantity; + return servers.length < user.serversQuantity; }), }); diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index 91db9826..0b740ab7 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -1,34 +1,330 @@ -import { apiFindOneUser, apiFindOneUserByAuth } from "@/server/db/schema"; -import { findUserByAuthId, findUserById, findUsers } from "@dokploy/server"; +import { + IS_CLOUD, + findOrganizationById, + findUserById, + getUserByToken, + removeUserById, + updateUser, + createApiKey, +} from "@dokploy/server"; +import { db } from "@dokploy/server/db"; +import { + account, + apiAssignPermissions, + apiFindOneToken, + apiUpdateUser, + invitation, + member, + apikey, +} from "@dokploy/server/db/schema"; +import * as bcrypt from "bcrypt"; import { TRPCError } from "@trpc/server"; -import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc"; +import { and, asc, eq, gt } from "drizzle-orm"; +import { z } from "zod"; +import { + adminProcedure, + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "../trpc"; + +const apiCreateApiKey = z.object({ + name: z.string().min(1), + prefix: z.string().optional(), + expiresIn: z.number().optional(), + metadata: z.object({ + organizationId: z.string(), + }), + // Rate limiting + rateLimitEnabled: z.boolean().optional(), + rateLimitTimeWindow: z.number().optional(), + rateLimitMax: z.number().optional(), + // Request limiting + remaining: z.number().optional(), + refillAmount: z.number().optional(), + refillInterval: z.number().optional(), +}); export const userRouter = createTRPCRouter({ all: adminProcedure.query(async ({ ctx }) => { - return await findUsers(ctx.user.adminId); + return await db.query.member.findMany({ + where: eq(member.organizationId, ctx.session.activeOrganizationId), + with: { + user: true, + }, + orderBy: [asc(member.createdAt)], + }); }), - byAuthId: protectedProcedure - .input(apiFindOneUserByAuth) + one: protectedProcedure + .input( + z.object({ + userId: z.string(), + }), + ) .query(async ({ input, ctx }) => { - const user = await findUserByAuthId(input.authId); - if (user.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this user", - }); - } - return user; + const memberResult = await db.query.member.findFirst({ + where: and( + eq(member.userId, input.userId), + eq(member.organizationId, ctx.session?.activeOrganizationId || ""), + ), + with: { + user: true, + }, + }); + + return memberResult; }), - byUserId: protectedProcedure - .input(apiFindOneUser) - .query(async ({ input, ctx }) => { - const user = await findUserById(input.userId); - if (user.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this user", + get: protectedProcedure.query(async ({ ctx }) => { + const memberResult = await db.query.member.findFirst({ + where: and( + eq(member.userId, ctx.user.id), + eq(member.organizationId, ctx.session?.activeOrganizationId || ""), + ), + with: { + user: { + with: { + apiKeys: true, + }, + }, + }, + }); + + return memberResult; + }), + getServerMetrics: protectedProcedure.query(async ({ ctx }) => { + const memberResult = await db.query.member.findFirst({ + where: and( + eq(member.userId, ctx.user.id), + eq(member.organizationId, ctx.session?.activeOrganizationId || ""), + ), + with: { + user: true, + }, + }); + + return memberResult?.user; + }), + update: protectedProcedure + .input(apiUpdateUser) + .mutation(async ({ input, ctx }) => { + if (input.password || input.currentPassword) { + const currentAuth = await db.query.account.findFirst({ + where: eq(account.userId, ctx.user.id), }); + const correctPassword = bcrypt.compareSync( + input.currentPassword || "", + currentAuth?.password || "", + ); + + if (!correctPassword) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Current password is incorrect", + }); + } + + if (!input.password) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "New password is required", + }); + } + await db + .update(account) + .set({ + password: bcrypt.hashSync(input.password, 10), + }) + .where(eq(account.userId, ctx.user.id)); } - return user; + return await updateUser(ctx.user.id, input); + }), + getUserByToken: publicProcedure + .input(apiFindOneToken) + .query(async ({ input }) => { + return await getUserByToken(input.token); + }), + getMetricsToken: protectedProcedure.query(async ({ ctx }) => { + const user = await findUserById(ctx.user.ownerId); + return { + serverIp: user.serverIp, + enabledFeatures: user.enablePaidFeatures, + metricsConfig: user?.metricsConfig, + }; + }), + remove: protectedProcedure + .input( + z.object({ + userId: z.string(), + }), + ) + .mutation(async ({ input }) => { + if (IS_CLOUD) { + return true; + } + return await removeUserById(input.userId); + }), + assignPermissions: adminProcedure + .input(apiAssignPermissions) + .mutation(async ({ input, ctx }) => { + try { + const organization = await findOrganizationById( + ctx.session?.activeOrganizationId || "", + ); + + if (organization?.ownerId !== ctx.user.ownerId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to assign permissions", + }); + } + + const { id, ...rest } = input; + + await db + .update(member) + .set({ + ...rest, + }) + .where( + and( + eq(member.userId, input.id), + eq( + member.organizationId, + ctx.session?.activeOrganizationId || "", + ), + ), + ); + } catch (error) { + throw error; + } + }), + getInvitations: protectedProcedure.query(async ({ ctx }) => { + return await db.query.invitation.findMany({ + where: and( + eq(invitation.email, ctx.user.email), + gt(invitation.expiresAt, new Date()), + eq(invitation.status, "pending"), + ), + with: { + organization: true, + }, + }); + }), + + getContainerMetrics: protectedProcedure + .input( + z.object({ + url: z.string(), + token: z.string(), + appName: z.string(), + dataPoints: z.string(), + }), + ) + .query(async ({ input }) => { + try { + if (!input.appName) { + throw new Error( + [ + "No Application Selected:", + "", + "Make Sure to select an application to monitor.", + ].join("\n"), + ); + } + const url = new URL(`${input.url}/metrics/containers`); + url.searchParams.append("limit", input.dataPoints); + url.searchParams.append("appName", input.appName); + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${input.token}`, + }, + }); + if (!response.ok) { + throw new Error( + `Error ${response.status}: ${response.statusText}. Please verify that the application "${input.appName}" is running and this service is included in the monitoring configuration.`, + ); + } + + const data = await response.json(); + if (!Array.isArray(data) || data.length === 0) { + throw new Error( + [ + `No monitoring data available for "${input.appName}". This could be because:`, + "", + "1. The container was recently started - wait a few minutes for data to be collected", + "2. The container is not running - verify its status", + "3. The service is not included in your monitoring configuration", + ].join("\n"), + ); + } + return data as { + containerId: string; + containerName: string; + containerImage: string; + containerLabels: string; + containerCommand: string; + containerCreated: string; + }[]; + } catch (error) { + throw error; + } + }), + + generateToken: protectedProcedure.mutation(async () => { + return "token"; + }), + + deleteApiKey: protectedProcedure + .input( + z.object({ + apiKeyId: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + try { + const apiKeyToDelete = await db.query.apikey.findFirst({ + where: eq(apikey.id, input.apiKeyId), + }); + + if (!apiKeyToDelete) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "API key not found", + }); + } + + if (apiKeyToDelete.userId !== ctx.user.id) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this API key", + }); + } + + await db.delete(apikey).where(eq(apikey.id, input.apiKeyId)); + return true; + } catch (error) { + throw error; + } + }), + + createApiKey: protectedProcedure + .input(apiCreateApiKey) + .mutation(async ({ input, ctx }) => { + const apiKey = await createApiKey(ctx.user.id, input); + return apiKey; + }), + + checkUserOrganizations: protectedProcedure + .input( + z.object({ + userId: z.string(), + }), + ) + .query(async ({ input }) => { + const organizations = await db.query.member.findMany({ + where: eq(member.userId, input.userId), + }); + + return organizations.length; }), }); diff --git a/apps/dokploy/server/api/trpc.ts b/apps/dokploy/server/api/trpc.ts index db4f7adf..4c88eb22 100644 --- a/apps/dokploy/server/api/trpc.ts +++ b/apps/dokploy/server/api/trpc.ts @@ -9,7 +9,7 @@ // import { getServerAuthSession } from "@/server/auth"; import { db } from "@/server/db"; -import { validateBearerToken, validateRequest } from "@dokploy/server"; +import { validateRequest } from "@dokploy/server/lib/auth"; import type { OpenApiMeta } from "@dokploy/trpc-openapi"; import { TRPCError, initTRPC } from "@trpc/server"; import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; @@ -18,7 +18,7 @@ import { experimental_isMultipartFormDataRequest, experimental_parseMultipartFormData, } from "@trpc/server/adapters/node-http/content-type/form-data"; -import type { Session, User } from "lucia"; +import type { Session, User } from "better-auth"; import superjson from "superjson"; import { ZodError } from "zod"; /** @@ -30,8 +30,8 @@ import { ZodError } from "zod"; */ interface CreateContextOptions { - user: (User & { authId: string; adminId: string }) | null; - session: Session | null; + user: (User & { rol: "member" | "admin" | "owner"; ownerId: string }) | null; + session: (Session & { activeOrganizationId: string }) | null; req: CreateNextContextOptions["req"]; res: CreateNextContextOptions["res"]; } @@ -65,30 +65,29 @@ const createInnerTRPCContext = (opts: CreateContextOptions) => { export const createTRPCContext = async (opts: CreateNextContextOptions) => { const { req, res } = opts; - let { session, user } = await validateBearerToken(req); - - if (!session) { - const cookieResult = await validateRequest(req, res); - session = cookieResult.session; - user = cookieResult.user; - } + // Get from the request + const { session, user } = await validateRequest(req); return createInnerTRPCContext({ req, res, - session: session, - ...((user && { - user: { - authId: user.id, - email: user.email, - rol: user.rol, - id: user.id, - secret: user.secret, - adminId: user.adminId, - }, - }) || { - user: null, - }), + // @ts-ignore + session: session + ? { + ...session, + activeOrganizationId: session.activeOrganizationId || "", + } + : null, + // @ts-ignore + user: user + ? { + ...user, + email: user.email, + rol: user.role as "owner" | "member" | "admin", + id: user.id, + ownerId: user.ownerId, + } + : null, }); }; @@ -181,7 +180,7 @@ export const uploadProcedure = async (opts: any) => { }; export const cliProcedure = t.procedure.use(({ ctx, next }) => { - if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") { + if (!ctx.session || !ctx.user || ctx.user.rol !== "owner") { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({ @@ -195,7 +194,7 @@ export const cliProcedure = t.procedure.use(({ ctx, next }) => { }); export const adminProcedure = t.procedure.use(({ ctx, next }) => { - if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") { + if (!ctx.session || !ctx.user || ctx.user.rol !== "owner") { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({ diff --git a/apps/dokploy/server/db/seed.ts b/apps/dokploy/server/db/seed.ts index b7935079..5b3eb6c6 100644 --- a/apps/dokploy/server/db/seed.ts +++ b/apps/dokploy/server/db/seed.ts @@ -1,16 +1,10 @@ -import bc from "bcrypt"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; -import { users } from "./schema"; const connectionString = process.env.DATABASE_URL!; const pg = postgres(connectionString, { max: 1 }); -const db = drizzle(pg); - -function password(txt: string) { - return bc.hashSync(txt, 10); -} +const _db = drizzle(pg); async function seed() { console.log("> Seed:", process.env.DATABASE_PATH, "\n"); diff --git a/apps/dokploy/server/utils/backup.ts b/apps/dokploy/server/utils/backup.ts index 2f141971..4fc9db93 100644 --- a/apps/dokploy/server/utils/backup.ts +++ b/apps/dokploy/server/utils/backup.ts @@ -2,7 +2,6 @@ import { type BackupScheduleList, IS_CLOUD, removeScheduleBackup, - scheduleBackup, } from "@dokploy/server/index"; type QueueJob = diff --git a/apps/dokploy/server/utils/docker.ts b/apps/dokploy/server/utils/docker.ts index 92008678..3314eb62 100644 --- a/apps/dokploy/server/utils/docker.ts +++ b/apps/dokploy/server/utils/docker.ts @@ -6,7 +6,7 @@ export const isWSL = async () => { const { stdout } = await execAsync("uname -r"); const isWSL = stdout.includes("microsoft"); return isWSL; - } catch (error) { + } catch (_error) { return false; } }; diff --git a/apps/dokploy/server/wss/docker-container-logs.ts b/apps/dokploy/server/wss/docker-container-logs.ts index 092f3973..8d08ebd4 100644 --- a/apps/dokploy/server/wss/docker-container-logs.ts +++ b/apps/dokploy/server/wss/docker-container-logs.ts @@ -1,5 +1,5 @@ import type http from "node:http"; -import { findServerById, validateWebSocketRequest } from "@dokploy/server"; +import { findServerById, validateRequest } from "@dokploy/server"; import { spawn } from "node-pty"; import { Client } from "ssh2"; import { WebSocketServer } from "ws"; @@ -35,7 +35,7 @@ export const setupDockerContainerLogsWebSocketServer = ( const since = url.searchParams.get("since"); const serverId = url.searchParams.get("serverId"); const runType = url.searchParams.get("runType"); - const { user, session } = await validateWebSocketRequest(req); + const { user, session } = await validateRequest(req); if (!containerId) { ws.close(4000, "containerId no provided"); diff --git a/apps/dokploy/server/wss/docker-container-terminal.ts b/apps/dokploy/server/wss/docker-container-terminal.ts index 8981ccbc..2f25edb1 100644 --- a/apps/dokploy/server/wss/docker-container-terminal.ts +++ b/apps/dokploy/server/wss/docker-container-terminal.ts @@ -1,5 +1,5 @@ import type http from "node:http"; -import { findServerById, validateWebSocketRequest } from "@dokploy/server"; +import { findServerById, validateRequest } from "@dokploy/server"; import { spawn } from "node-pty"; import { Client } from "ssh2"; import { WebSocketServer } from "ws"; @@ -32,7 +32,7 @@ export const setupDockerContainerTerminalWebSocketServer = ( const containerId = url.searchParams.get("containerId"); const activeWay = url.searchParams.get("activeWay"); const serverId = url.searchParams.get("serverId"); - const { user, session } = await validateWebSocketRequest(req); + const { user, session } = await validateRequest(req); if (!containerId) { ws.close(4000, "containerId no provided"); @@ -50,8 +50,8 @@ export const setupDockerContainerTerminalWebSocketServer = ( throw new Error("No SSH key available for this server"); const conn = new Client(); - let stdout = ""; - let stderr = ""; + let _stdout = ""; + let _stderr = ""; conn .once("ready", () => { conn.exec( @@ -61,16 +61,16 @@ export const setupDockerContainerTerminalWebSocketServer = ( if (err) throw err; stream - .on("close", (code: number, signal: string) => { + .on("close", (code: number, _signal: string) => { ws.send(`\nContainer closed with code: ${code}\n`); conn.end(); }) .on("data", (data: string) => { - stdout += data.toString(); + _stdout += data.toString(); ws.send(data.toString()); }) .stderr.on("data", (data) => { - stderr += data.toString(); + _stderr += data.toString(); ws.send(data.toString()); console.error("Error: ", data.toString()); }); diff --git a/apps/dokploy/server/wss/docker-stats.ts b/apps/dokploy/server/wss/docker-stats.ts index b1e4585c..99e993dc 100644 --- a/apps/dokploy/server/wss/docker-stats.ts +++ b/apps/dokploy/server/wss/docker-stats.ts @@ -4,7 +4,7 @@ import { execAsync, getLastAdvancedStatsFile, recordAdvancedStats, - validateWebSocketRequest, + validateRequest, } from "@dokploy/server"; import { WebSocketServer } from "ws"; @@ -36,7 +36,7 @@ export const setupDockerStatsMonitoringSocketServer = ( | "application" | "stack" | "docker-compose"; - const { user, session } = await validateWebSocketRequest(req); + const { user, session } = await validateRequest(req); if (!appName) { ws.close(4000, "appName no provided"); diff --git a/apps/dokploy/server/wss/drawer-logs.ts b/apps/dokploy/server/wss/drawer-logs.ts index c1dec315..404dfeee 100644 --- a/apps/dokploy/server/wss/drawer-logs.ts +++ b/apps/dokploy/server/wss/drawer-logs.ts @@ -1,4 +1,5 @@ import type http from "node:http"; +import { validateRequest } from "@dokploy/server/index"; import { applyWSSHandler } from "@trpc/server/adapters/ws"; import { WebSocketServer } from "ws"; import { appRouter } from "../api/root"; @@ -31,6 +32,12 @@ export const setupDrawerLogsWebSocketServer = ( }); wssTerm.on("connection", async (ws, req) => { - const url = new URL(req.url || "", `http://${req.headers.host}`); + const _url = new URL(req.url || "", `http://${req.headers.host}`); + const { user, session } = await validateRequest(req); + + if (!user || !session) { + ws.close(); + return; + } }); }; diff --git a/apps/dokploy/server/wss/listen-deployment.ts b/apps/dokploy/server/wss/listen-deployment.ts index df77ceb4..4a25c6f0 100644 --- a/apps/dokploy/server/wss/listen-deployment.ts +++ b/apps/dokploy/server/wss/listen-deployment.ts @@ -1,6 +1,6 @@ import { spawn } from "node:child_process"; import type http from "node:http"; -import { findServerById, validateWebSocketRequest } from "@dokploy/server"; +import { findServerById, validateRequest } from "@dokploy/server"; import { Client } from "ssh2"; import { WebSocketServer } from "ws"; @@ -29,7 +29,7 @@ export const setupDeploymentLogsWebSocketServer = ( const url = new URL(req.url || "", `http://${req.headers.host}`); const logPath = url.searchParams.get("logPath"); const serverId = url.searchParams.get("serverId"); - const { user, session } = await validateWebSocketRequest(req); + const { user, session } = await validateRequest(req); if (!logPath) { console.log("logPath no provided"); @@ -103,7 +103,7 @@ export const setupDeploymentLogsWebSocketServer = ( ws.close(); }); } - } catch (error) { + } catch (_error) { // @ts-ignore // const errorMessage = error?.message as unknown as string; // ws.send(errorMessage); diff --git a/apps/dokploy/server/wss/terminal.ts b/apps/dokploy/server/wss/terminal.ts index 5fa1accc..094c5e15 100644 --- a/apps/dokploy/server/wss/terminal.ts +++ b/apps/dokploy/server/wss/terminal.ts @@ -1,9 +1,5 @@ import type http from "node:http"; -import { - IS_CLOUD, - findServerById, - validateWebSocketRequest, -} from "@dokploy/server"; +import { IS_CLOUD, findServerById, validateRequest } from "@dokploy/server"; import { publicIpv4, publicIpv6 } from "public-ip"; import { Client, type ConnectConfig } from "ssh2"; import { WebSocketServer } from "ws"; @@ -71,7 +67,7 @@ export const setupTerminalWebSocketServer = ( wssTerm.on("connection", async (ws, req) => { const url = new URL(req.url || "", `http://${req.headers.host}`); const serverId = url.searchParams.get("serverId"); - const { user, session } = await validateWebSocketRequest(req); + const { user, session } = await validateRequest(req); if (!user || !session || !serverId) { ws.close(); return; @@ -148,8 +144,8 @@ export const setupTerminalWebSocketServer = ( } const conn = new Client(); - let stdout = ""; - let stderr = ""; + let _stdout = ""; + let _stderr = ""; ws.send("Connecting...\n"); @@ -162,16 +158,16 @@ export const setupTerminalWebSocketServer = ( if (err) throw err; stream - .on("close", (code: number, signal: string) => { + .on("close", (code: number, _signal: string) => { ws.send(`\nContainer closed with code: ${code}\n`); conn.end(); }) .on("data", (data: string) => { - stdout += data.toString(); + _stdout += data.toString(); ws.send(data.toString()); }) .stderr.on("data", (data) => { - stderr += data.toString(); + _stderr += data.toString(); ws.send(data.toString()); console.error("Error: ", data.toString()); }); diff --git a/apps/dokploy/templates/appsmith/index.ts b/apps/dokploy/templates/appsmith/index.ts index ff744a24..73279e91 100644 --- a/apps/dokploy/templates/appsmith/index.ts +++ b/apps/dokploy/templates/appsmith/index.ts @@ -7,7 +7,7 @@ import { } from "../utils"; export function generate(schema: Schema): Template { - const mainServiceHash = generateHash(schema.projectName); + const _mainServiceHash = generateHash(schema.projectName); const domains: DomainSchema[] = [ { diff --git a/apps/dokploy/templates/appwrite/docker-compose.yml b/apps/dokploy/templates/appwrite/docker-compose.yml new file mode 100644 index 00000000..163cb3d0 --- /dev/null +++ b/apps/dokploy/templates/appwrite/docker-compose.yml @@ -0,0 +1,887 @@ +version: "3.8" + +x-logging: &x-logging + logging: + driver: "json-file" + options: + max-file: "5" + max-size: "10m" + +services: + appwrite: + image: appwrite/appwrite:1.6.0 + container_name: appwrite + <<: *x-logging + restart: unless-stopped + networks: + - dokploy-network + labels: + - traefik.enable=true + - traefik.constraint-label-stack=appwrite + volumes: + - appwrite-uploads:/storage/uploads:rw + - appwrite-cache:/storage/cache:rw + - appwrite-config:/storage/config:rw + - appwrite-certificates:/storage/certificates:rw + - appwrite-functions:/storage/functions:rw + depends_on: + - mariadb + - redis + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_LOCALE + - _APP_CONSOLE_WHITELIST_ROOT + - _APP_CONSOLE_WHITELIST_EMAILS + - _APP_CONSOLE_SESSION_ALERTS + - _APP_CONSOLE_WHITELIST_IPS + - _APP_CONSOLE_HOSTNAMES + - _APP_SYSTEM_EMAIL_NAME + - _APP_SYSTEM_EMAIL_ADDRESS + - _APP_EMAIL_SECURITY + - _APP_SYSTEM_RESPONSE_FORMAT + - _APP_OPTIONS_ABUSE + - _APP_OPTIONS_ROUTER_PROTECTION + - _APP_OPTIONS_FORCE_HTTPS + - _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS + - _APP_OPENSSL_KEY_V1 + - _APP_DOMAIN + - _APP_DOMAIN_TARGET + - _APP_DOMAIN_FUNCTIONS + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_SMTP_HOST + - _APP_SMTP_PORT + - _APP_SMTP_SECURE + - _APP_SMTP_USERNAME + - _APP_SMTP_PASSWORD + - _APP_USAGE_STATS + - _APP_STORAGE_LIMIT + - _APP_STORAGE_PREVIEW_LIMIT + - _APP_STORAGE_ANTIVIRUS + - _APP_STORAGE_ANTIVIRUS_HOST + - _APP_STORAGE_ANTIVIRUS_PORT + - _APP_STORAGE_DEVICE + - _APP_STORAGE_S3_ACCESS_KEY + - _APP_STORAGE_S3_SECRET + - _APP_STORAGE_S3_REGION + - _APP_STORAGE_S3_BUCKET + - _APP_STORAGE_DO_SPACES_ACCESS_KEY + - _APP_STORAGE_DO_SPACES_SECRET + - _APP_STORAGE_DO_SPACES_REGION + - _APP_STORAGE_DO_SPACES_BUCKET + - _APP_STORAGE_BACKBLAZE_ACCESS_KEY + - _APP_STORAGE_BACKBLAZE_SECRET + - _APP_STORAGE_BACKBLAZE_REGION + - _APP_STORAGE_BACKBLAZE_BUCKET + - _APP_STORAGE_LINODE_ACCESS_KEY + - _APP_STORAGE_LINODE_SECRET + - _APP_STORAGE_LINODE_REGION + - _APP_STORAGE_LINODE_BUCKET + - _APP_STORAGE_WASABI_ACCESS_KEY + - _APP_STORAGE_WASABI_SECRET + - _APP_STORAGE_WASABI_REGION + - _APP_STORAGE_WASABI_BUCKET + - _APP_FUNCTIONS_SIZE_LIMIT + - _APP_FUNCTIONS_TIMEOUT + - _APP_FUNCTIONS_BUILD_TIMEOUT + - _APP_FUNCTIONS_CPUS + - _APP_FUNCTIONS_MEMORY + - _APP_FUNCTIONS_RUNTIMES + - _APP_EXECUTOR_SECRET + - _APP_EXECUTOR_HOST + - _APP_LOGGING_CONFIG + - _APP_MAINTENANCE_INTERVAL + - _APP_MAINTENANCE_DELAY + - _APP_MAINTENANCE_RETENTION_EXECUTION + - _APP_MAINTENANCE_RETENTION_CACHE + - _APP_MAINTENANCE_RETENTION_ABUSE + - _APP_MAINTENANCE_RETENTION_AUDIT + - _APP_MAINTENANCE_RETENTION_USAGE_HOURLY + - _APP_MAINTENANCE_RETENTION_SCHEDULES + - _APP_SMS_PROVIDER + - _APP_SMS_FROM + - _APP_GRAPHQL_MAX_BATCH_SIZE + - _APP_GRAPHQL_MAX_COMPLEXITY + - _APP_GRAPHQL_MAX_DEPTH + - _APP_VCS_GITHUB_APP_NAME + - _APP_VCS_GITHUB_PRIVATE_KEY + - _APP_VCS_GITHUB_APP_ID + - _APP_VCS_GITHUB_WEBHOOK_SECRET + - _APP_VCS_GITHUB_CLIENT_SECRET + - _APP_VCS_GITHUB_CLIENT_ID + - _APP_MIGRATIONS_FIREBASE_CLIENT_ID + - _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET + - _APP_ASSISTANT_OPENAI_API_KEY + + appwrite-console: + image: appwrite/console:5.0.12 + container_name: appwrite-console + <<: *x-logging + restart: unless-stopped + networks: + - dokploy-network + labels: + - "traefik.enable=true" + - "traefik.constraint-label-stack=appwrite" + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_LOCALE + - _APP_CONSOLE_WHITELIST_ROOT + - _APP_CONSOLE_WHITELIST_EMAILS + - _APP_CONSOLE_SESSION_ALERTS + - _APP_CONSOLE_WHITELIST_IPS + - _APP_CONSOLE_HOSTNAMES + - _APP_SYSTEM_EMAIL_NAME + - _APP_SYSTEM_EMAIL_ADDRESS + - _APP_EMAIL_SECURITY + - _APP_SYSTEM_RESPONSE_FORMAT + - _APP_OPTIONS_ABUSE + - _APP_OPTIONS_ROUTER_PROTECTION + - _APP_OPTIONS_FORCE_HTTPS + - _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS + - _APP_OPENSSL_KEY_V1 + - _APP_DOMAIN + - _APP_DOMAIN_TARGET + - _APP_DOMAIN_FUNCTIONS + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_SMTP_HOST + - _APP_SMTP_PORT + - _APP_SMTP_SECURE + - _APP_SMTP_USERNAME + - _APP_SMTP_PASSWORD + - _APP_USAGE_STATS + - _APP_STORAGE_LIMIT + - _APP_STORAGE_PREVIEW_LIMIT + - _APP_STORAGE_ANTIVIRUS + - _APP_STORAGE_ANTIVIRUS_HOST + - _APP_STORAGE_ANTIVIRUS_PORT + - _APP_STORAGE_DEVICE + - _APP_STORAGE_S3_ACCESS_KEY + - _APP_STORAGE_S3_SECRET + - _APP_STORAGE_S3_REGION + - _APP_STORAGE_S3_BUCKET + - _APP_STORAGE_DO_SPACES_ACCESS_KEY + - _APP_STORAGE_DO_SPACES_SECRET + - _APP_STORAGE_DO_SPACES_REGION + - _APP_STORAGE_DO_SPACES_BUCKET + - _APP_STORAGE_BACKBLAZE_ACCESS_KEY + - _APP_STORAGE_BACKBLAZE_SECRET + - _APP_STORAGE_BACKBLAZE_REGION + - _APP_STORAGE_BACKBLAZE_BUCKET + - _APP_STORAGE_LINODE_ACCESS_KEY + - _APP_STORAGE_LINODE_SECRET + - _APP_STORAGE_LINODE_REGION + - _APP_STORAGE_LINODE_BUCKET + - _APP_STORAGE_WASABI_ACCESS_KEY + - _APP_STORAGE_WASABI_SECRET + - _APP_STORAGE_WASABI_REGION + - _APP_STORAGE_WASABI_BUCKET + + appwrite-realtime: + image: appwrite/appwrite:1.6.0 + entrypoint: realtime + container_name: appwrite-realtime + <<: *x-logging + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - mariadb + - redis + labels: + - "traefik.enable=true" + - "traefik.constraint-label-stack=appwrite" + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPTIONS_ABUSE + - _APP_OPTIONS_ROUTER_PROTECTION + - _APP_OPENSSL_KEY_V1 + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_USAGE_STATS + - _APP_LOGGING_CONFIG + + appwrite-worker-audits: + image: appwrite/appwrite:1.6.0 + entrypoint: worker-audits + <<: *x-logging + container_name: appwrite-worker-audits + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_LOGGING_CONFIG + + appwrite-worker-webhooks: + image: appwrite/appwrite:1.6.0 + entrypoint: worker-webhooks + <<: *x-logging + container_name: appwrite-worker-webhooks + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_EMAIL_SECURITY + - _APP_SYSTEM_SECURITY_EMAIL_ADDRESS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_LOGGING_CONFIG + + appwrite-worker-deletes: + image: appwrite/appwrite:1.6.0 + entrypoint: worker-deletes + <<: *x-logging + container_name: appwrite-worker-deletes + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - redis + - mariadb + volumes: + - appwrite-uploads:/storage/uploads:rw + - appwrite-cache:/storage/cache:rw + - appwrite-functions:/storage/functions:rw + - appwrite-builds:/storage/builds:rw + - appwrite-certificates:/storage/certificates:rw + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_STORAGE_DEVICE + - _APP_STORAGE_S3_ACCESS_KEY + - _APP_STORAGE_S3_SECRET + - _APP_STORAGE_S3_REGION + - _APP_STORAGE_S3_BUCKET + - _APP_STORAGE_DO_SPACES_ACCESS_KEY + - _APP_STORAGE_DO_SPACES_SECRET + - _APP_STORAGE_DO_SPACES_REGION + - _APP_STORAGE_DO_SPACES_BUCKET + - _APP_STORAGE_BACKBLAZE_ACCESS_KEY + - _APP_STORAGE_BACKBLAZE_SECRET + - _APP_STORAGE_BACKBLAZE_REGION + - _APP_STORAGE_BACKBLAZE_BUCKET + - _APP_STORAGE_LINODE_ACCESS_KEY + - _APP_STORAGE_LINODE_SECRET + - _APP_STORAGE_LINODE_REGION + - _APP_STORAGE_LINODE_BUCKET + - _APP_STORAGE_WASABI_ACCESS_KEY + - _APP_STORAGE_WASABI_SECRET + - _APP_STORAGE_WASABI_REGION + - _APP_STORAGE_WASABI_BUCKET + - _APP_LOGGING_CONFIG + - _APP_EXECUTOR_SECRET + - _APP_EXECUTOR_HOST + - _APP_MAINTENANCE_RETENTION_ABUSE + - _APP_MAINTENANCE_RETENTION_AUDIT + - _APP_MAINTENANCE_RETENTION_EXECUTION + + appwrite-worker-databases: + image: appwrite/appwrite:1.6.0 + entrypoint: worker-databases + <<: *x-logging + container_name: appwrite-worker-databases + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_LOGGING_CONFIG + + appwrite-worker-builds: + image: appwrite/appwrite:1.6.0 + entrypoint: worker-builds + <<: *x-logging + container_name: appwrite-worker-builds + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - redis + - mariadb + volumes: + - appwrite-functions:/storage/functions:rw + - appwrite-builds:/storage/builds:rw + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_EXECUTOR_SECRET + - _APP_EXECUTOR_HOST + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_LOGGING_CONFIG + - _APP_VCS_GITHUB_APP_NAME + - _APP_VCS_GITHUB_PRIVATE_KEY + - _APP_VCS_GITHUB_APP_ID + - _APP_FUNCTIONS_TIMEOUT + - _APP_FUNCTIONS_BUILD_TIMEOUT + - _APP_FUNCTIONS_CPUS + - _APP_FUNCTIONS_MEMORY + - _APP_FUNCTIONS_SIZE_LIMIT + - _APP_OPTIONS_FORCE_HTTPS + - _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS + - _APP_DOMAIN + - _APP_STORAGE_DEVICE + - _APP_STORAGE_S3_ACCESS_KEY + - _APP_STORAGE_S3_SECRET + - _APP_STORAGE_S3_REGION + - _APP_STORAGE_S3_BUCKET + - _APP_STORAGE_DO_SPACES_ACCESS_KEY + - _APP_STORAGE_DO_SPACES_SECRET + - _APP_STORAGE_DO_SPACES_REGION + - _APP_STORAGE_DO_SPACES_BUCKET + - _APP_STORAGE_BACKBLAZE_ACCESS_KEY + - _APP_STORAGE_BACKBLAZE_SECRET + - _APP_STORAGE_BACKBLAZE_REGION + - _APP_STORAGE_BACKBLAZE_BUCKET + - _APP_STORAGE_LINODE_ACCESS_KEY + - _APP_STORAGE_LINODE_SECRET + - _APP_STORAGE_LINODE_REGION + - _APP_STORAGE_LINODE_BUCKET + - _APP_STORAGE_WASABI_ACCESS_KEY + - _APP_STORAGE_WASABI_SECRET + - _APP_STORAGE_WASABI_REGION + - _APP_STORAGE_WASABI_BUCKET + + appwrite-worker-certificates: + image: appwrite/appwrite:1.6.0 + entrypoint: worker-certificates + <<: *x-logging + container_name: appwrite-worker-certificates + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - redis + - mariadb + volumes: + - appwrite-config:/storage/config:rw + - appwrite-certificates:/storage/certificates:rw + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_DOMAIN + - _APP_DOMAIN_TARGET + - _APP_DOMAIN_FUNCTIONS + - _APP_EMAIL_CERTIFICATES + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_LOGGING_CONFIG + + appwrite-worker-functions: + image: appwrite/appwrite:1.6.0 + entrypoint: worker-functions + <<: *x-logging + container_name: appwrite-worker-functions + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - redis + - mariadb + - openruntimes-executor + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_DOMAIN + - _APP_OPTIONS_FORCE_HTTPS + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_FUNCTIONS_TIMEOUT + - _APP_FUNCTIONS_BUILD_TIMEOUT + - _APP_FUNCTIONS_CPUS + - _APP_FUNCTIONS_MEMORY + - _APP_EXECUTOR_SECRET + - _APP_EXECUTOR_HOST + - _APP_USAGE_STATS + - _APP_DOCKER_HUB_USERNAME + - _APP_DOCKER_HUB_PASSWORD + - _APP_LOGGING_CONFIG + + appwrite-worker-mails: + image: appwrite/appwrite:1.6.0 + entrypoint: worker-mails + <<: *x-logging + container_name: appwrite-worker-mails + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - redis + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_SYSTEM_EMAIL_NAME + - _APP_SYSTEM_EMAIL_ADDRESS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_SMTP_HOST + - _APP_SMTP_PORT + - _APP_SMTP_SECURE + - _APP_SMTP_USERNAME + - _APP_SMTP_PASSWORD + - _APP_LOGGING_CONFIG + + appwrite-worker-messaging: + image: appwrite/appwrite:1.6.0 + entrypoint: worker-messaging + container_name: appwrite-worker-messaging + <<: *x-logging + restart: unless-stopped + networks: + - dokploy-network + volumes: + - appwrite-uploads:/storage/uploads:rw + depends_on: + - redis + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_LOGGING_CONFIG + - _APP_SMS_FROM + - _APP_SMS_PROVIDER + - _APP_STORAGE_DEVICE + - _APP_STORAGE_S3_ACCESS_KEY + - _APP_STORAGE_S3_SECRET + - _APP_STORAGE_S3_REGION + - _APP_STORAGE_S3_BUCKET + - _APP_STORAGE_DO_SPACES_ACCESS_KEY + - _APP_STORAGE_DO_SPACES_SECRET + - _APP_STORAGE_DO_SPACES_REGION + - _APP_STORAGE_DO_SPACES_BUCKET + - _APP_STORAGE_BACKBLAZE_ACCESS_KEY + - _APP_STORAGE_BACKBLAZE_SECRET + - _APP_STORAGE_BACKBLAZE_REGION + - _APP_STORAGE_BACKBLAZE_BUCKET + - _APP_STORAGE_LINODE_ACCESS_KEY + - _APP_STORAGE_LINODE_SECRET + - _APP_STORAGE_LINODE_REGION + - _APP_STORAGE_LINODE_BUCKET + - _APP_STORAGE_WASABI_ACCESS_KEY + - _APP_STORAGE_WASABI_SECRET + - _APP_STORAGE_WASABI_REGION + - _APP_STORAGE_WASABI_BUCKET + + appwrite-worker-migrations: + image: appwrite/appwrite:1.6.0 + entrypoint: worker-migrations + <<: *x-logging + container_name: appwrite-worker-migrations + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - mariadb + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_DOMAIN + - _APP_DOMAIN_TARGET + - _APP_EMAIL_SECURITY + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_LOGGING_CONFIG + - _APP_MIGRATIONS_FIREBASE_CLIENT_ID + - _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET + + appwrite-task-maintenance: + image: appwrite/appwrite:1.6.0 + entrypoint: maintenance + <<: *x-logging + container_name: appwrite-task-maintenance + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - redis + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_DOMAIN + - _APP_DOMAIN_TARGET + - _APP_DOMAIN_FUNCTIONS + - _APP_OPENSSL_KEY_V1 + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_MAINTENANCE_INTERVAL + - _APP_MAINTENANCE_RETENTION_EXECUTION + - _APP_MAINTENANCE_RETENTION_CACHE + - _APP_MAINTENANCE_RETENTION_ABUSE + - _APP_MAINTENANCE_RETENTION_AUDIT + - _APP_MAINTENANCE_RETENTION_USAGE_HOURLY + - _APP_MAINTENANCE_RETENTION_SCHEDULES + + appwrite-worker-usage: + image: appwrite/appwrite:1.6.0 + entrypoint: worker-usage + container_name: appwrite-worker-usage + <<: *x-logging + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_USAGE_STATS + - _APP_LOGGING_CONFIG + - _APP_USAGE_AGGREGATION_INTERVAL + + appwrite-worker-usage-dump: + image: appwrite/appwrite:1.6.0 + entrypoint: worker-usage-dump + container_name: appwrite-worker-usage-dump + <<: *x-logging + networks: + - dokploy-network + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_USAGE_STATS + - _APP_LOGGING_CONFIG + - _APP_USAGE_AGGREGATION_INTERVAL + + appwrite-task-scheduler-functions: + image: appwrite/appwrite:1.6.0 + entrypoint: schedule-functions + container_name: appwrite-task-scheduler-functions + <<: *x-logging + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - mariadb + - redis + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-task-scheduler-executions: + image: appwrite/appwrite:1.6.0 + entrypoint: schedule-executions + container_name: appwrite-task-scheduler-executions + <<: *x-logging + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - mariadb + - redis + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-task-scheduler-messages: + image: appwrite/appwrite:1.6.0 + entrypoint: schedule-messages + container_name: appwrite-task-scheduler-messages + <<: *x-logging + restart: unless-stopped + networks: + - dokploy-network + depends_on: + - mariadb + - redis + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-assistant: + image: appwrite/assistant:0.4.0 + container_name: appwrite-assistant + <<: *x-logging + restart: unless-stopped + networks: + - dokploy-network + environment: + - _APP_ASSISTANT_OPENAI_API_KEY + + openruntimes-executor: + container_name: openruntimes-executor + hostname: exc1 + <<: *x-logging + restart: unless-stopped + stop_signal: SIGINT + image: openruntimes/executor:0.6.11 + networks: + - dokploy-network + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - appwrite-builds:/storage/builds:rw + - appwrite-functions:/storage/functions:rw + - /tmp:/tmp:rw + environment: + - OPR_EXECUTOR_INACTIVE_TRESHOLD=$_APP_FUNCTIONS_INACTIVE_THRESHOLD + - OPR_EXECUTOR_MAINTENANCE_INTERVAL=$_APP_FUNCTIONS_MAINTENANCE_INTERVAL + - OPR_EXECUTOR_NETWORK=$_APP_FUNCTIONS_RUNTIMES_NETWORK + - OPR_EXECUTOR_DOCKER_HUB_USERNAME=$_APP_DOCKER_HUB_USERNAME + - OPR_EXECUTOR_DOCKER_HUB_PASSWORD=$_APP_DOCKER_HUB_PASSWORD + - OPR_EXECUTOR_ENV=$_APP_ENV + - OPR_EXECUTOR_RUNTIMES=$_APP_FUNCTIONS_RUNTIMES + - OPR_EXECUTOR_SECRET=$_APP_EXECUTOR_SECRET + - OPR_EXECUTOR_LOGGING_CONFIG=$_APP_LOGGING_CONFIG + - OPR_EXECUTOR_STORAGE_DEVICE=$_APP_STORAGE_DEVICE + - OPR_EXECUTOR_STORAGE_S3_ACCESS_KEY=$_APP_STORAGE_S3_ACCESS_KEY + - OPR_EXECUTOR_STORAGE_S3_SECRET=$_APP_STORAGE_S3_SECRET + - OPR_EXECUTOR_STORAGE_S3_REGION=$_APP_STORAGE_S3_REGION + - OPR_EXECUTOR_STORAGE_S3_BUCKET=$_APP_STORAGE_S3_BUCKET + - OPR_EXECUTOR_STORAGE_DO_SPACES_ACCESS_KEY=$_APP_STORAGE_DO_SPACES_ACCESS_KEY + - OPR_EXECUTOR_STORAGE_DO_SPACES_SECRET=$_APP_STORAGE_DO_SPACES_SECRET + - OPR_EXECUTOR_STORAGE_DO_SPACES_REGION=$_APP_STORAGE_DO_SPACES_REGION + - OPR_EXECUTOR_STORAGE_DO_SPACES_BUCKET=$_APP_STORAGE_DO_SPACES_BUCKET + - OPR_EXECUTOR_STORAGE_BACKBLAZE_ACCESS_KEY=$_APP_STORAGE_BACKBLAZE_ACCESS_KEY + - OPR_EXECUTOR_STORAGE_BACKBLAZE_SECRET=$_APP_STORAGE_BACKBLAZE_SECRET + - OPR_EXECUTOR_STORAGE_BACKBLAZE_REGION=$_APP_STORAGE_BACKBLAZE_REGION + - OPR_EXECUTOR_STORAGE_BACKBLAZE_BUCKET=$_APP_STORAGE_BACKBLAZE_BUCKET + - OPR_EXECUTOR_STORAGE_LINODE_ACCESS_KEY=$_APP_STORAGE_LINODE_ACCESS_KEY + - OPR_EXECUTOR_STORAGE_LINODE_SECRET=$_APP_STORAGE_LINODE_SECRET + - OPR_EXECUTOR_STORAGE_LINODE_REGION=$_APP_STORAGE_LINODE_REGION + - OPR_EXECUTOR_STORAGE_LINODE_BUCKET=$_APP_STORAGE_LINODE_BUCKET + - OPR_EXECUTOR_STORAGE_WASABI_ACCESS_KEY=$_APP_STORAGE_WASABI_ACCESS_KEY + - OPR_EXECUTOR_STORAGE_WASABI_SECRET=$_APP_STORAGE_WASABI_SECRET + - OPR_EXECUTOR_STORAGE_WASABI_REGION=$_APP_STORAGE_WASABI_REGION + - OPR_EXECUTOR_STORAGE_WASABI_BUCKET=$_APP_STORAGE_WASABI_BUCKET + + mariadb: + image: mariadb:10.11 + container_name: appwrite-mariadb + <<: *x-logging + restart: unless-stopped + networks: + - dokploy-network + volumes: + - appwrite-mariadb:/var/lib/mysql:rw + environment: + - MYSQL_ROOT_PASSWORD=${_APP_DB_ROOT_PASS} + - MYSQL_DATABASE=${_APP_DB_SCHEMA} + - MYSQL_USER=${_APP_DB_USER} + - MYSQL_PASSWORD=${_APP_DB_PASS} + - MARIADB_AUTO_UPGRADE=1 + command: "mysqld --innodb-flush-method=fsync" + + redis: + image: redis:7.2.4-alpine + container_name: appwrite-redis + <<: *x-logging + restart: unless-stopped + command: > + redis-server + --maxmemory 512mb + --maxmemory-policy allkeys-lru + --maxmemory-samples 5 + networks: + - dokploy-network + volumes: + - appwrite-redis:/data:rw + +# Uncomment and configure if ClamAV is needed +# clamav: +# image: appwrite/clamav:1.2.0 +# container_name: appwrite-clamav +# restart: unless-stopped +# networks: +# - dokploy-network +# volumes: +# - appwrite-uploads:/storage/uploads + +volumes: + appwrite-mariadb: + appwrite-redis: + appwrite-cache: + appwrite-uploads: + appwrite-certificates: + appwrite-functions: + appwrite-builds: + appwrite-config: + +networks: + dokploy-network: + external: true diff --git a/apps/dokploy/templates/appwrite/index.ts b/apps/dokploy/templates/appwrite/index.ts new file mode 100644 index 00000000..4e671324 --- /dev/null +++ b/apps/dokploy/templates/appwrite/index.ts @@ -0,0 +1,153 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { host: mainDomain, port: 80, serviceName: "appwrite", path: "/" }, + { + host: mainDomain, + port: 80, + serviceName: "appwrite-console", + path: "/console", + }, + { + host: mainDomain, + port: 80, + serviceName: "appwrite-realtime", + path: "/v1/realtime", + }, + ]; + + const envs = [ + "_APP_ENV=production", + "_APP_LOCALE=en", + "_APP_OPTIONS_ABUSE=enabled", + "_APP_OPTIONS_FORCE_HTTPS=disabled", + "_APP_OPTIONS_FUNCTIONS_FORCE_HTTPS=disabled", + "_APP_OPTIONS_ROUTER_PROTECTION=disabled", + "_APP_OPENSSL_KEY_V1=your-secret-key", + `_APP_DOMAIN=${mainDomain}`, + `_APP_DOMAIN_FUNCTIONS=${mainDomain}`, + `_APP_DOMAIN_TARGET=${mainDomain}`, + "_APP_CONSOLE_WHITELIST_ROOT=enabled", + "_APP_CONSOLE_WHITELIST_EMAILS=", + "_APP_CONSOLE_WHITELIST_IPS=", + "_APP_CONSOLE_HOSTNAMES=", + "_APP_SYSTEM_EMAIL_NAME=Appwrite", + "_APP_SYSTEM_EMAIL_ADDRESS=noreply@appwrite.io", + "_APP_SYSTEM_TEAM_EMAIL=team@appwrite.io", + "_APP_SYSTEM_RESPONSE_FORMAT=", + "_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=certs@appwrite.io", + "_APP_EMAIL_SECURITY=", + "_APP_EMAIL_CERTIFICATES=", + "_APP_USAGE_STATS=enabled", + "_APP_LOGGING_PROVIDER=", + "_APP_LOGGING_CONFIG=", + "_APP_USAGE_AGGREGATION_INTERVAL=30", + "_APP_USAGE_TIMESERIES_INTERVAL=30", + "_APP_USAGE_DATABASE_INTERVAL=900", + "_APP_WORKER_PER_CORE=6", + "_APP_CONSOLE_SESSION_ALERTS=disabled", + "_APP_REDIS_HOST=redis", + "_APP_REDIS_PORT=6379", + "_APP_REDIS_USER=", + "_APP_REDIS_PASS=", + "_APP_DB_HOST=mariadb", + "_APP_DB_PORT=3306", + "_APP_DB_SCHEMA=appwrite", + "_APP_DB_USER=user", + "_APP_DB_PASS=password", + "_APP_DB_ROOT_PASS=rootsecretpassword", + "_APP_INFLUXDB_HOST=influxdb", + "_APP_INFLUXDB_PORT=8086", + "_APP_STATSD_HOST=telegraf", + "_APP_STATSD_PORT=8125", + "_APP_SMTP_HOST=", + "_APP_SMTP_PORT=", + "_APP_SMTP_SECURE=", + "_APP_SMTP_USERNAME=", + "_APP_SMTP_PASSWORD=", + "_APP_SMS_PROVIDER=", + "_APP_SMS_FROM=", + "_APP_STORAGE_LIMIT=30000000", + "_APP_STORAGE_PREVIEW_LIMIT=20000000", + "_APP_STORAGE_ANTIVIRUS=disabled", + "_APP_STORAGE_ANTIVIRUS_HOST=clamav", + "_APP_STORAGE_ANTIVIRUS_PORT=3310", + "_APP_STORAGE_DEVICE=local", + "_APP_STORAGE_S3_ACCESS_KEY=", + "_APP_STORAGE_S3_SECRET=", + "_APP_STORAGE_S3_REGION=us-east-1", + "_APP_STORAGE_S3_BUCKET=", + "_APP_STORAGE_DO_SPACES_ACCESS_KEY=", + "_APP_STORAGE_DO_SPACES_SECRET=", + "_APP_STORAGE_DO_SPACES_REGION=us-east-1", + "_APP_STORAGE_DO_SPACES_BUCKET=", + "_APP_STORAGE_BACKBLAZE_ACCESS_KEY=", + "_APP_STORAGE_BACKBLAZE_SECRET=", + "_APP_STORAGE_BACKBLAZE_REGION=us-west-004", + "_APP_STORAGE_BACKBLAZE_BUCKET=", + "_APP_STORAGE_LINODE_ACCESS_KEY=", + "_APP_STORAGE_LINODE_SECRET=", + "_APP_STORAGE_LINODE_REGION=eu-central-1", + "_APP_STORAGE_LINODE_BUCKET=", + "_APP_STORAGE_WASABI_ACCESS_KEY=", + "_APP_STORAGE_WASABI_SECRET=", + "_APP_STORAGE_WASABI_REGION=eu-central-1", + "_APP_STORAGE_WASABI_BUCKET=", + "_APP_FUNCTIONS_SIZE_LIMIT=30000000", + "_APP_FUNCTIONS_BUILD_SIZE_LIMIT=2000000000", + "_APP_FUNCTIONS_TIMEOUT=900", + "_APP_FUNCTIONS_BUILD_TIMEOUT=900", + "_APP_FUNCTIONS_CONTAINERS=10", + "_APP_FUNCTIONS_CPUS=0", + "_APP_FUNCTIONS_MEMORY=0", + "_APP_FUNCTIONS_MEMORY_SWAP=0", + "_APP_FUNCTIONS_RUNTIMES=node-16.0,php-8.0,python-3.9,ruby-3.0", + "_APP_EXECUTOR_SECRET=your-secret-key", + "_APP_EXECUTOR_HOST=http://exc1/v1", + "_APP_EXECUTOR_RUNTIME_NETWORK=appwrite_runtimes", + "_APP_FUNCTIONS_ENVS=node-16.0,php-7.4,python-3.9,ruby-3.0", + "_APP_FUNCTIONS_INACTIVE_THRESHOLD=60", + "DOCKERHUB_PULL_USERNAME=", + "DOCKERHUB_PULL_PASSWORD=", + "DOCKERHUB_PULL_EMAIL=", + "OPEN_RUNTIMES_NETWORK=appwrite_runtimes", + "_APP_FUNCTIONS_RUNTIMES_NETWORK=runtimes", + "_APP_DOCKER_HUB_USERNAME=", + "_APP_DOCKER_HUB_PASSWORD=", + "_APP_FUNCTIONS_MAINTENANCE_INTERVAL=3600", + "_APP_VCS_GITHUB_APP_NAME=", + "_APP_VCS_GITHUB_PRIVATE_KEY=", + "_APP_VCS_GITHUB_APP_ID=", + "_APP_VCS_GITHUB_CLIENT_ID=", + "_APP_VCS_GITHUB_CLIENT_SECRET=", + "_APP_VCS_GITHUB_WEBHOOK_SECRET=", + "_APP_MAINTENANCE_INTERVAL=86400", + "_APP_MAINTENANCE_DELAY=0", + "_APP_MAINTENANCE_RETENTION_CACHE=2592000", + "_APP_MAINTENANCE_RETENTION_EXECUTION=1209600", + "_APP_MAINTENANCE_RETENTION_AUDIT=1209600", + "_APP_MAINTENANCE_RETENTION_ABUSE=86400", + "_APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000", + "_APP_MAINTENANCE_RETENTION_SCHEDULES=86400", + "_APP_GRAPHQL_MAX_BATCH_SIZE=10", + "_APP_GRAPHQL_MAX_COMPLEXITY=250", + "_APP_GRAPHQL_MAX_DEPTH=3", + "_APP_MIGRATIONS_FIREBASE_CLIENT_ID=", + "_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=", + "_APP_ASSISTANT_OPENAI_API_KEY=", + ]; + + return { + domains, + envs, + mounts: [], + }; +} diff --git a/apps/dokploy/templates/blender/index.ts b/apps/dokploy/templates/blender/index.ts index 84e52755..79508bed 100644 --- a/apps/dokploy/templates/blender/index.ts +++ b/apps/dokploy/templates/blender/index.ts @@ -7,7 +7,7 @@ import { } from "../utils"; export function generate(schema: Schema): Template { - const mainServiceHash = generateHash(schema.projectName); + const _mainServiceHash = generateHash(schema.projectName); const mainDomain = generateRandomDomain(schema); const domains: DomainSchema[] = [ diff --git a/apps/dokploy/templates/cloudflared/index.ts b/apps/dokploy/templates/cloudflared/index.ts index 661fa31d..93ea091c 100644 --- a/apps/dokploy/templates/cloudflared/index.ts +++ b/apps/dokploy/templates/cloudflared/index.ts @@ -1,6 +1,6 @@ import type { Schema, Template } from "../utils"; -export function generate(schema: Schema): Template { +export function generate(_schema: Schema): Template { const envs = [`CLOUDFLARE_TUNNEL_TOKEN=""`]; return { diff --git a/apps/dokploy/templates/convex/docker-compose.yml b/apps/dokploy/templates/convex/docker-compose.yml new file mode 100644 index 00000000..12e2b5ad --- /dev/null +++ b/apps/dokploy/templates/convex/docker-compose.yml @@ -0,0 +1,37 @@ +services: + backend: + image: ghcr.io/get-convex/convex-backend:6c974d219776b753cd23d26f4a296629ff7c2cad + ports: + - "${PORT:-3210}:3210" + - "${SITE_PROXY_PORT:-3211}:3211" + volumes: + - data:/convex/data + environment: + - INSTANCE_NAME=${INSTANCE_NAME:-} + - INSTANCE_SECRET=${INSTANCE_SECRET:-} + - CONVEX_RELEASE_VERSION_DEV=${CONVEX_RELEASE_VERSION_DEV:-} + - ACTIONS_USER_TIMEOUT_SECS=${ACTIONS_USER_TIMEOUT_SECS:-} + - CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://127.0.0.1:3210} + - CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://127.0.0.1:3211} + - DATABASE_URL=${DATABASE_URL:-} + - DISABLE_BEACON=${DISABLE_BEACON:-} + - REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:-} + - RUST_LOG=${RUST_LOG:-info} + - RUST_BACKTRACE=${RUST_BACKTRACE:-} + healthcheck: + test: curl -f http://localhost:3210/version + interval: 5s + start_period: 5s + + dashboard: + image: ghcr.io/get-convex/convex-dashboard:4499dd4fd7f2148687a7774599c613d052950f46 + ports: + - "${DASHBOARD_PORT:-6791}:6791" + environment: + - NEXT_PUBLIC_DEPLOYMENT_URL=${NEXT_PUBLIC_DEPLOYMENT_URL:-http://127.0.0.1:3210} + depends_on: + backend: + condition: service_healthy + +volumes: + data: diff --git a/apps/dokploy/templates/convex/index.ts b/apps/dokploy/templates/convex/index.ts new file mode 100644 index 00000000..badfe732 --- /dev/null +++ b/apps/dokploy/templates/convex/index.ts @@ -0,0 +1,38 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const dashboardDomain = generateRandomDomain(schema); + const backendDomain = generateRandomDomain(schema); + const actionsDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: dashboardDomain, + port: 6791, + serviceName: "dashboard", + }, + { + host: backendDomain, + port: 3210, + serviceName: "backend", + }, + { + host: actionsDomain, + port: 3211, + serviceName: "backend", + }, + ]; + + const envs = [ + `NEXT_PUBLIC_DEPLOYMENT_URL=http://${backendDomain}`, + `CONVEX_CLOUD_ORIGIN=http://${backendDomain}`, + `CONVEX_SITE_ORIGIN=http://${actionsDomain}`, + ]; + + return { envs, domains }; +} diff --git a/apps/dokploy/templates/drawio/index.ts b/apps/dokploy/templates/drawio/index.ts index e3c57c5a..701283c8 100644 --- a/apps/dokploy/templates/drawio/index.ts +++ b/apps/dokploy/templates/drawio/index.ts @@ -8,7 +8,7 @@ import { export function generate(schema: Schema): Template { const mainDomain = generateRandomDomain(schema); - const secretKeyBase = generateBase64(64); + const _secretKeyBase = generateBase64(64); const domains: DomainSchema[] = [ { diff --git a/apps/dokploy/templates/excalidraw/index.ts b/apps/dokploy/templates/excalidraw/index.ts index 13a43c44..7f73f395 100644 --- a/apps/dokploy/templates/excalidraw/index.ts +++ b/apps/dokploy/templates/excalidraw/index.ts @@ -2,7 +2,6 @@ import { type DomainSchema, type Schema, type Template, - generateHash, generateRandomDomain, } from "../utils"; diff --git a/apps/dokploy/templates/ghost/index.ts b/apps/dokploy/templates/ghost/index.ts index 1a88c362..052b7c6b 100644 --- a/apps/dokploy/templates/ghost/index.ts +++ b/apps/dokploy/templates/ghost/index.ts @@ -2,7 +2,6 @@ import { type DomainSchema, type Schema, type Template, - generateHash, generateRandomDomain, } from "../utils"; diff --git a/apps/dokploy/templates/glance/docker-compose.yml b/apps/dokploy/templates/glance/docker-compose.yml index e931d6e4..ace8bc94 100644 --- a/apps/dokploy/templates/glance/docker-compose.yml +++ b/apps/dokploy/templates/glance/docker-compose.yml @@ -2,7 +2,10 @@ services: glance: image: glanceapp/glance volumes: - - ../files/app/glance.yml:/app/glance.yml + - ../files/app/config/:/app/config + - ../files/app/assets:/app/assets + # Optionally, also mount docker socket if you want to use the docker containers widget + # - /var/run/docker.sock:/var/run/docker.sock:ro ports: - 8080 - restart: unless-stopped + env_file: .env \ No newline at end of file diff --git a/apps/dokploy/templates/glance/index.ts b/apps/dokploy/templates/glance/index.ts index 4b229786..a0ab1b67 100644 --- a/apps/dokploy/templates/glance/index.ts +++ b/apps/dokploy/templates/glance/index.ts @@ -17,7 +17,7 @@ export function generate(schema: Schema): Template { const mounts: Template["mounts"] = [ { - filePath: "/app/glance.yml", + filePath: "/app/config/glance.yml", content: ` branding: hide-footer: true diff --git a/apps/dokploy/templates/immich/index.ts b/apps/dokploy/templates/immich/index.ts index b1b11afb..4beca87d 100644 --- a/apps/dokploy/templates/immich/index.ts +++ b/apps/dokploy/templates/immich/index.ts @@ -11,7 +11,7 @@ export function generate(schema: Schema): Template { const mainDomain = generateRandomDomain(schema); const dbPassword = generatePassword(); const dbUser = "immich"; - const appSecret = generateBase64(32); + const _appSecret = generateBase64(32); const domains: DomainSchema[] = [ { diff --git a/apps/dokploy/templates/linkwarden/docker-compose.yml b/apps/dokploy/templates/linkwarden/docker-compose.yml new file mode 100644 index 00000000..05ffb8a0 --- /dev/null +++ b/apps/dokploy/templates/linkwarden/docker-compose.yml @@ -0,0 +1,40 @@ +services: + linkwarden: + environment: + - NEXTAUTH_SECRET + - NEXTAUTH_URL + - DATABASE_URL=postgresql://linkwarden:${POSTGRES_PASSWORD}@postgres:5432/linkwarden + restart: unless-stopped + image: ghcr.io/linkwarden/linkwarden:v2.9.3 + ports: + - 3000 + volumes: + - linkwarden-data:/data/data + depends_on: + - postgres + healthcheck: + test: curl --fail http://localhost:3000 || exit 1 + interval: 60s + retries: 2 + start_period: 60s + timeout: 15s + + postgres: + image: postgres:17-alpine + restart: unless-stopped + user: postgres + environment: + POSTGRES_USER: linkwarden + POSTGRES_DB: linkwarden + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + linkwarden-data: + postgres-data: diff --git a/apps/dokploy/templates/linkwarden/index.ts b/apps/dokploy/templates/linkwarden/index.ts new file mode 100644 index 00000000..86025035 --- /dev/null +++ b/apps/dokploy/templates/linkwarden/index.ts @@ -0,0 +1,33 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const postgresPassword = generatePassword(); + const nextSecret = generateBase64(32); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 3000, + serviceName: "linkwarden", + }, + ]; + + const envs = [ + `POSTGRES_PASSWORD=${postgresPassword}`, + `NEXTAUTH_SECRET=${nextSecret}`, + `NEXTAUTH_URL=http://${mainDomain}/api/v1/auth`, + ]; + + return { + domains, + envs, + }; +} diff --git a/apps/dokploy/templates/mailpit/docker-compose.yml b/apps/dokploy/templates/mailpit/docker-compose.yml new file mode 100644 index 00000000..d0dbdb8e --- /dev/null +++ b/apps/dokploy/templates/mailpit/docker-compose.yml @@ -0,0 +1,25 @@ +services: + mailpit: + image: axllent/mailpit:v1.22.3 + restart: unless-stopped + ports: + - '1025:1025' + volumes: + - 'mailpit-data:/data' + environment: + - MP_SMTP_AUTH_ALLOW_INSECURE=true + - MP_MAX_MESSAGES=5000 + - MP_DATABASE=/data/mailpit.db + - MP_UI_AUTH=${MP_UI_AUTH} + - MP_SMTP_AUTH=${MP_SMTP_AUTH} + healthcheck: + test: + - CMD + - /mailpit + - readyz + interval: 5s + timeout: 20s + retries: 10 + +volumes: + mailpit-data: \ No newline at end of file diff --git a/apps/dokploy/templates/mailpit/index.ts b/apps/dokploy/templates/mailpit/index.ts new file mode 100644 index 00000000..25f18f7e --- /dev/null +++ b/apps/dokploy/templates/mailpit/index.ts @@ -0,0 +1,31 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const domains: DomainSchema[] = [ + { + host: generateRandomDomain(schema), + port: 8025, + serviceName: "mailpit", + }, + ]; + + const defaultPassword = generatePassword(); + + const envs = [ + "# Uncomment below if you want basic auth on UI and SMTP", + `#MP_UI_AUTH=mailpit:${defaultPassword}`, + `#MP_SMTP_AUTH=mailpit:${defaultPassword}`, + ]; + + return { + domains, + envs, + }; +} diff --git a/apps/dokploy/templates/outline/docker-compose.yml b/apps/dokploy/templates/outline/docker-compose.yml new file mode 100644 index 00000000..aaf98ac0 --- /dev/null +++ b/apps/dokploy/templates/outline/docker-compose.yml @@ -0,0 +1,57 @@ +services: + outline: + image: outlinewiki/outline:0.82.0 + restart: always + depends_on: + - postgres + - redis + - dex + ports: + - 3000 + environment: + NODE_ENV: production + URL: ${URL} + FORCE_HTTPS: 'false' + SECRET_KEY: ${SECRET_KEY} + UTILS_SECRET: ${UTILS_SECRET} + DATABASE_URL: postgres://outline:${POSTGRES_PASSWORD}@postgres:5432/outline + PGSSLMODE: disable + REDIS_URL: redis://redis:6379 + OIDC_CLIENT_ID: outline + OIDC_CLIENT_SECRET: ${CLIENT_SECRET} + OIDC_AUTH_URI: ${DEX_URL}/auth + OIDC_TOKEN_URI: ${DEX_URL}/token + OIDC_USERINFO_URI: ${DEX_URL}/userinfo + + dex: + image: ghcr.io/dexidp/dex:v2.37.0 + restart: always + volumes: + - ../files/etc/dex/config.yaml:/etc/dex/config.yaml + command: + - dex + - serve + - /etc/dex/config.yaml + ports: + - 5556 + + postgres: + image: postgres:15 + restart: always + environment: + POSTGRES_DB: outline + POSTGRES_USER: outline + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres_data-test-outline-khufpx:/var/lib/postgresql/data + + redis: + image: redis:latest + restart: always + command: redis-server --appendonly yes + volumes: + - redis_data-test-outline-khufpx:/data + +volumes: + postgres_data-test-outline-khufpx: + redis_data-test-outline-khufpx: \ No newline at end of file diff --git a/apps/dokploy/templates/outline/index.ts b/apps/dokploy/templates/outline/index.ts new file mode 100644 index 00000000..8431e568 --- /dev/null +++ b/apps/dokploy/templates/outline/index.ts @@ -0,0 +1,90 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const dexDomain = generateRandomDomain(schema); + const SECRET_KEY = generateBase64(32); + const UTILS_SECRET = generateBase64(32); + const CLIENT_SECRET = generateBase64(32); + const POSTGRES_PASSWORD = generatePassword(); + + const mainURL = `http://${mainDomain}`; + const dexURL = `http://${dexDomain}`; + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 3000, + serviceName: "outline", + }, + { + host: dexDomain, + port: 5556, + serviceName: "dex", + }, + ]; + + const mounts: Template["mounts"] = [ + { + filePath: "/etc/dex/config.yaml", + content: `issuer: ${dexURL} + +web: + http: 0.0.0.0:5556 + +storage: + type: memory + +enablePasswordDB: true + +frontend: + issuer: Outline + +logger: + level: debug + +staticPasswords: + - email: "admin@example.com" + # bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2) + hash: "$2y$10$jsRWHw54uxTUIfhjgUrB9u8HSzPk7TUuQri9sXZrKzRXcScvwYor." + username: "admin" + userID: "1" + + +oauth2: + skipApprovalScreen: true + alwaysShowLoginScreen: false + passwordConnector: local + +staticClients: + - id: "outline" + redirectURIs: + - ${mainURL}/auth/oidc.callback + name: "Outline" + secret: "${CLIENT_SECRET}"`, + }, + ]; + + const envs = [ + `URL=${mainURL}`, + `DEX_URL=${dexURL}`, + `DOMAIN_NAME=${mainDomain}`, + `POSTGRES_PASSWORD=${POSTGRES_PASSWORD}`, + `SECRET_KEY=${SECRET_KEY}`, + `UTILS_SECRET=${UTILS_SECRET}`, + `CLIENT_SECRET=${CLIENT_SECRET}`, + ]; + + return { + domains, + envs, + mounts, + }; +} diff --git a/apps/dokploy/templates/penpot/index.ts b/apps/dokploy/templates/penpot/index.ts index f657c698..a3e90e8a 100644 --- a/apps/dokploy/templates/penpot/index.ts +++ b/apps/dokploy/templates/penpot/index.ts @@ -2,8 +2,6 @@ import { type DomainSchema, type Schema, type Template, - generateBase64, - generatePassword, generateRandomDomain, } from "../utils"; diff --git a/apps/dokploy/templates/photoprism/index.ts b/apps/dokploy/templates/photoprism/index.ts index d20ac29c..4a103a62 100644 --- a/apps/dokploy/templates/photoprism/index.ts +++ b/apps/dokploy/templates/photoprism/index.ts @@ -2,7 +2,6 @@ import { type DomainSchema, type Schema, type Template, - generateHash, generatePassword, generateRandomDomain, } from "../utils"; diff --git a/apps/dokploy/templates/plausible/docker-compose.yml b/apps/dokploy/templates/plausible/docker-compose.yml index f17bdfa3..ad483ecf 100644 --- a/apps/dokploy/templates/plausible/docker-compose.yml +++ b/apps/dokploy/templates/plausible/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3.8" services: plausible_db: image: postgres:16-alpine @@ -24,7 +23,7 @@ services: hard: 262144 plausible: - image: ghcr.io/plausible/community-edition:v2.1.4 + image: ghcr.io/plausible/community-edition:v2.1.5 restart: always command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run" depends_on: diff --git a/apps/dokploy/templates/pocket-id/docker-compose.yml b/apps/dokploy/templates/pocket-id/docker-compose.yml new file mode 100644 index 00000000..f9385143 --- /dev/null +++ b/apps/dokploy/templates/pocket-id/docker-compose.yml @@ -0,0 +1,21 @@ +services: + pocket-id: + image: ghcr.io/pocket-id/pocket-id:v0.35.1 + restart: unless-stopped + environment: + - PUBLIC_UI_CONFIG_DISABLED + - PUBLIC_APP_URL + - TRUST_PROXY + ports: + - 80 + volumes: + - pocket-id-data:/app/backend/data + healthcheck: + test: "curl -f http://localhost/health" + interval: 1m30s + timeout: 5s + retries: 2 + start_period: 10s + +volumes: + pocket-id-data: diff --git a/apps/dokploy/templates/pocket-id/index.ts b/apps/dokploy/templates/pocket-id/index.ts new file mode 100644 index 00000000..9a9faa2a --- /dev/null +++ b/apps/dokploy/templates/pocket-id/index.ts @@ -0,0 +1,29 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 80, + serviceName: "pocket-id", + }, + ]; + + const envs = [ + "PUBLIC_UI_CONFIG_DISABLED=false", + `PUBLIC_APP_URL=http://${mainDomain}`, + "TRUST_PROXY=true", + ]; + + return { + domains, + envs, + }; +} diff --git a/apps/dokploy/templates/registry/docker-compose.yml b/apps/dokploy/templates/registry/docker-compose.yml new file mode 100644 index 00000000..08c5c368 --- /dev/null +++ b/apps/dokploy/templates/registry/docker-compose.yml @@ -0,0 +1,19 @@ +services: + registry: + restart: always + image: registry:2 + ports: + - 5000 + volumes: + - ../files/auth/registry.password:/auth/registry.password + - registry-data:/var/lib/registry + environment: + REGISTRY_STORAGE_DELETE_ENABLED: true + REGISTRY_HEALTH_STORAGEDRIVER_ENABLED: false + REGISTRY_HTTP_SECRET: ${REGISTRY_HTTP_SECRET} + REGISTRY_AUTH: htpasswd + REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm + REGISTRY_AUTH_HTPASSWD_PATH: /auth/registry.password + +volumes: + registry-data: \ No newline at end of file diff --git a/apps/dokploy/templates/registry/index.ts b/apps/dokploy/templates/registry/index.ts new file mode 100644 index 00000000..81965e6e --- /dev/null +++ b/apps/dokploy/templates/registry/index.ts @@ -0,0 +1,35 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const domains: DomainSchema[] = [ + { + host: generateRandomDomain(schema), + port: 5000, + serviceName: "registry", + }, + ]; + + const registryHttpSecret = generatePassword(30); + + const envs = [`REGISTRY_HTTP_SECRET=${registryHttpSecret}`]; + + const mounts: Template["mounts"] = [ + { + filePath: "/auth/registry.password", + content: + "# from: docker run --rm --entrypoint htpasswd httpd:2 -Bbn docker password\ndocker:$2y$10$qWZoWev/u5PV7WneFoRAMuoGpRcAQOgUuIIdLnU7pJXogrBSY23/2\n", + }, + ]; + + return { + domains, + envs, + mounts, + }; +} diff --git a/apps/dokploy/templates/superset/docker-compose.yml b/apps/dokploy/templates/superset/docker-compose.yml index 8dd1cd2d..b73bf55e 100644 --- a/apps/dokploy/templates/superset/docker-compose.yml +++ b/apps/dokploy/templates/superset/docker-compose.yml @@ -1,5 +1,8 @@ -# Note: this is an UNOFFICIAL production docker image build for Superset: +# This is an UNOFFICIAL production docker image build for Superset: # - https://github.com/amancevice/docker-superset + + +# ## SETUP INSTRUCTIONS # # After deploying this image, you will need to run one of the two # commands below in a terminal within the superset container: @@ -7,11 +10,30 @@ # $ superset-init # Initialise database only # # You will be prompted to enter the credentials for the admin user. + + +# ## NETWORK INSTRUCTIONS +# +# If you want to connect superset with other internal databases managed by +# Dokploy (on dokploy-network) using internal hostnames, you will need to +# uncomment the `networks` section, both for the superset container and +# at the very bottom of this docker-compose template. +# +# Note that the `superset` service name/hostname will not be unique on the +# global `dokploy-network`. If you plan to: +# +# 1. deploy a second instance of superset on dokploy-network, and +# 2. have other containers on dokploy-network utilise the second instance's +# Superset API (https://superset.apache.org/docs/api) +# +# Please change the service name of the second instance. services: superset: image: amancevice/superset restart: always + #networks: + # - dokploy-network depends_on: - superset_postgres - superset_redis @@ -44,8 +66,7 @@ services: timeout: 10s retries: 3 - - superset_redis: +superset_redis: image: redis restart: always volumes: @@ -57,6 +78,9 @@ services: timeout: 10s retries: 3 +#networks: +# dokploy-network: +# external: true volumes: superset_postgres_data: diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index 8143bbb2..d39465a8 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -1,6 +1,37 @@ import type { TemplateData } from "./types/templates-data.type"; export const templates: TemplateData[] = [ + { + id: "appwrite", + name: "Appwrite", + version: "1.6.0", + description: + "Appwrite is an end-to-end backend server for Web, Mobile, Native, or Backend apps. Appwrite abstracts the complexity and repetitiveness required to build a modern backend API from scratch and allows you to build secure apps faster.\n" + + "Using Appwrite, you can easily integrate your app with user authentication and multiple sign-in methods, a database for storing and querying users and team data, storage and file management, image manipulation, Cloud Functions, messaging, and more services.", + links: { + github: "https://github.com/appwrite/appwrite", + website: "https://appwrite.io/", + docs: "https://appwrite.io/docs", + }, + logo: "appwrite.svg", + tags: ["database", "firebase", "postgres"], + load: () => import("./appwrite/index").then((m) => m.generate), + }, + { + id: "outline", + name: "Outline", + version: "0.82.0", + description: + "Outline is a self-hosted knowledge base and documentation platform that allows you to build and manage your own knowledge base applications.", + links: { + github: "https://github.com/outline/outline", + website: "https://getoutline.com/", + docs: "https://docs.getoutline.com/s/guide", + }, + logo: "outline.png", + load: () => import("./outline/index").then((m) => m.generate), + tags: ["documentation", "knowledge-base", "self-hosted"], + }, { id: "supabase", name: "SupaBase", @@ -34,7 +65,7 @@ export const templates: TemplateData[] = [ { id: "plausible", name: "Plausible", - version: "v2.1.4", + version: "v2.1.5", description: "Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.", logo: "plausible.svg", @@ -362,6 +393,21 @@ export const templates: TemplateData[] = [ tags: ["chat"], load: () => import("./open-webui/index").then((m) => m.generate), }, + { + id: "mailpit", + name: "Mailpit", + version: "v1.22.3", + description: + "Mailpit is a tiny, self-contained, and secure email & SMTP testing tool with API for developers.", + logo: "mailpit.svg", + links: { + github: "https://github.com/axllent/mailpit", + website: "https://mailpit.axllent.org/", + docs: "https://mailpit.axllent.org/docs/", + }, + tags: ["email", "smtp"], + load: () => import("./mailpit/index").then((m) => m.generate), + }, { id: "listmonk", name: "Listmonk", @@ -395,7 +441,7 @@ export const templates: TemplateData[] = [ { id: "umami", name: "Umami", - version: "v2.14.0", + version: "v2.16.1", description: "Umami is a simple, fast, privacy-focused alternative to Google Analytics.", logo: "umami.png", @@ -631,6 +677,21 @@ export const templates: TemplateData[] = [ tags: ["open-source"], load: () => import("./vaultwarden/index").then((m) => m.generate), }, + { + id: "linkwarden", + name: "Linkwarden", + version: "2.9.3", + description: + "Self-hosted, open-source collaborative bookmark manager to collect, organize and archive webpages.", + logo: "linkwarden.png", + links: { + github: "https://github.com/linkwarden/linkwarden", + website: "https://linkwarden.app/", + docs: "https://docs.linkwarden.app/", + }, + tags: ["bookmarks", "link-sharing"], + load: () => import("./linkwarden/index").then((m) => m.generate), + }, { id: "hi-events", name: "Hi.events", @@ -1062,6 +1123,21 @@ export const templates: TemplateData[] = [ tags: ["identity", "auth"], load: () => import("./logto/index").then((m) => m.generate), }, + { + id: "pocket-id", + name: "Pocket ID", + version: "0.35.1", + description: + "A simple and easy-to-use OIDC provider that allows users to authenticate with their passkeys to your services.", + logo: "pocket-id.svg", + links: { + github: "https://github.com/pocket-id/pocket-id", + website: "https://pocket-id.org/", + docs: "https://pocket-id.org/docs", + }, + tags: ["identity", "auth"], + load: () => import("./pocket-id/index").then((m) => m.generate), + }, { id: "penpot", name: "Penpot", @@ -1408,6 +1484,21 @@ export const templates: TemplateData[] = [ tags: ["file-manager", "vdfs", "storage"], load: () => import("./spacedrive/index").then((m) => m.generate), }, + { + id: "registry", + name: "Docker Registry", + version: "2", + description: + "Distribution implementation for storing and distributing of Docker container images and artifacts.", + links: { + github: "https://github.com/distribution/distribution", + website: "https://hub.docker.com/_/registry", + docs: "https://distribution.github.io/distribution/", + }, + logo: "registry.png", + tags: ["registry", "docker", "self-hosted"], + load: () => import("./registry/index").then((m) => m.generate), + }, { id: "alist", name: "AList", @@ -1483,4 +1574,48 @@ export const templates: TemplateData[] = [ tags: ["forms", "analytics"], load: () => import("./formbricks/index").then((m) => m.generate), }, + { + id: "trilium", + name: "Trilium", + description: + "Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases.", + logo: "trilium.png", + version: "latest", + links: { + github: "https://github.com/zadam/trilium", + website: "https://github.com/zadam/trilium", + docs: "https://github.com/zadam/trilium/wiki/", + }, + tags: ["self-hosted", "productivity", "personal-use"], + load: () => import("./trilium/index").then((m) => m.generate), + }, + { + id: "convex", + name: "Convex", + version: "latest", + description: + "Convex is an open-source reactive database designed to make life easy for web app developers.", + logo: "convex.svg", + links: { + github: "https://github.com/get-convex/convex", + website: "https://www.convex.dev/", + docs: "https://www.convex.dev/docs", + }, + tags: ["backend", "database", "api"], + load: () => import("./convex/index").then((m) => m.generate), + }, + { + id: "wikijs", + name: "Wiki.js", + version: "2.5", + description: "The most powerful and extensible open source Wiki software.", + logo: "wikijs.svg", + links: { + github: "https://github.com/requarks/wiki", + website: "https://js.wiki/", + docs: "https://docs.requarks.io/", + }, + tags: ["knowledge-base", "self-hosted", "documentation"], + load: () => import("./wikijs/index").then((m) => m.generate), + }, ]; diff --git a/apps/dokploy/templates/triggerdotdev/index.ts b/apps/dokploy/templates/triggerdotdev/index.ts index 7b894acb..c11c708b 100644 --- a/apps/dokploy/templates/triggerdotdev/index.ts +++ b/apps/dokploy/templates/triggerdotdev/index.ts @@ -1,4 +1,3 @@ -import { Secrets } from "@/components/ui/secrets"; import { type DomainSchema, type Schema, diff --git a/apps/dokploy/templates/trilium/docker-compose.yml b/apps/dokploy/templates/trilium/docker-compose.yml new file mode 100644 index 00000000..f549d820 --- /dev/null +++ b/apps/dokploy/templates/trilium/docker-compose.yml @@ -0,0 +1,14 @@ +services: + trilium: + image: zadam/trilium:latest + ports: + - 8080 + networks: + - dokploy-network + restart: always + volumes: + - /root/trilium-backups:/home/node/trilium-data/backup + +networks: + dokploy-network: + external: true diff --git a/apps/dokploy/templates/trilium/index.ts b/apps/dokploy/templates/trilium/index.ts new file mode 100644 index 00000000..acac9841 --- /dev/null +++ b/apps/dokploy/templates/trilium/index.ts @@ -0,0 +1,22 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const triliumDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: triliumDomain, + port: 8080, + serviceName: "trilium", + }, + ]; + + return { + domains, + }; +} diff --git a/apps/dokploy/templates/umami/docker-compose.yml b/apps/dokploy/templates/umami/docker-compose.yml index 87568165..26efd337 100644 --- a/apps/dokploy/templates/umami/docker-compose.yml +++ b/apps/dokploy/templates/umami/docker-compose.yml @@ -1,6 +1,6 @@ services: umami: - image: ghcr.io/umami-software/umami:postgresql-v2.14.0 + image: ghcr.io/umami-software/umami:postgresql-v2.16.1 restart: always healthcheck: test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"] diff --git a/apps/dokploy/templates/unifi/index.ts b/apps/dokploy/templates/unifi/index.ts index 975ce63d..ea67b0fa 100644 --- a/apps/dokploy/templates/unifi/index.ts +++ b/apps/dokploy/templates/unifi/index.ts @@ -1,6 +1,6 @@ import type { Schema, Template } from "../utils"; -export function generate(schema: Schema): Template { +export function generate(_schema: Schema): Template { const mounts: Template["mounts"] = [ { filePath: "init-mongo.sh", diff --git a/apps/dokploy/templates/unsend/index.ts b/apps/dokploy/templates/unsend/index.ts index 1c4c9c71..dcc80f66 100644 --- a/apps/dokploy/templates/unsend/index.ts +++ b/apps/dokploy/templates/unsend/index.ts @@ -3,7 +3,6 @@ import { type Schema, type Template, generateBase64, - generateHash, generateRandomDomain, } from "../utils"; diff --git a/apps/dokploy/templates/utils/index.ts b/apps/dokploy/templates/utils/index.ts index b5369b91..941afc80 100644 --- a/apps/dokploy/templates/utils/index.ts +++ b/apps/dokploy/templates/utils/index.ts @@ -12,7 +12,9 @@ export interface Schema { projectName: string; } -export type DomainSchema = Pick; +export type DomainSchema = Pick & { + path?: string; +}; export interface Template { envs?: string[]; diff --git a/apps/dokploy/templates/wikijs/docker-compose.yml b/apps/dokploy/templates/wikijs/docker-compose.yml new file mode 100644 index 00000000..6b21423d --- /dev/null +++ b/apps/dokploy/templates/wikijs/docker-compose.yml @@ -0,0 +1,31 @@ +version: '3.5' +services: + wiki: + image: ghcr.io/requarks/wiki:2.5 + restart: unless-stopped + environment: + - DB_TYPE + - DB_HOST + - DB_PORT + - DB_USER + - DB_PASS + - DB_NAME + depends_on: + - db + labels: + - traefik.enable=true + - traefik.constraint-label-stack=wikijs + db: + image: postgres:14 + restart: unless-stopped + environment: + - POSTGRES_USER + - POSTGRES_PASSWORD + - POSTGRES_DB + volumes: + - wiki-db-data:/var/lib/postgresql/data +networks: + dokploy-network: + external: true +volumes: + wiki-db-data: diff --git a/apps/dokploy/templates/wikijs/index.ts b/apps/dokploy/templates/wikijs/index.ts new file mode 100644 index 00000000..ff6c234d --- /dev/null +++ b/apps/dokploy/templates/wikijs/index.ts @@ -0,0 +1,35 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const domains: DomainSchema[] = [ + { + host: generateRandomDomain(schema), + port: 3000, + serviceName: "wiki", + }, + ]; + + const envs = [ + "# Database Setup", + "POSTGRES_USER=wikijs", + "POSTGRES_PASSWORD=wikijsrocks", + "POSTGRES_DB=wiki", + "# WikiJS Database Connection", + "DB_TYPE=postgres", + "DB_HOST=db", + "DB_PORT=5432", + "DB_USER=wikijs", + "DB_PASS=wikijsrocks", + "DB_NAME=wiki", + ]; + + return { + domains, + envs, + }; +} diff --git a/apps/schedules/tsconfig.json b/apps/schedules/tsconfig.json index 3c0b02bc..3d4adb16 100644 --- a/apps/schedules/tsconfig.json +++ b/apps/schedules/tsconfig.json @@ -7,7 +7,8 @@ "skipLibCheck": true, "outDir": "dist", "jsx": "react-jsx", - "jsxImportSource": "hono/jsx" + "jsxImportSource": "hono/jsx", + "declaration": false }, "exclude": ["node_modules", "dist"] } diff --git a/biome.json b/biome.json index f5a6c232..cf677ec4 100644 --- a/biome.json +++ b/biome.json @@ -24,7 +24,10 @@ }, "correctness": { "useExhaustiveDependencies": "off", - "noUnsafeOptionalChaining": "off" + "noUnsafeOptionalChaining": "off", + "noUnusedImports": "error", + "noUnusedFunctionParameters": "error", + "noUnusedVariables": "error" }, "style": { "noNonNullAssertion": "off" diff --git a/lefthook.yml b/lefthook.yml index 1a491cd8..3f5a6d09 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -37,9 +37,9 @@ commit-msg: commands: commitlint: - run: "npx commitlint --edit $1" + # run: "npx commitlint --edit $1" pre-commit: commands: check: - run: "pnpm check" + # run: "pnpm check" diff --git a/packages/server/auth-schema.ts b/packages/server/auth-schema.ts new file mode 100644 index 00000000..a5829046 --- /dev/null +++ b/packages/server/auth-schema.ts @@ -0,0 +1,133 @@ +// import { +// pgTable, +// text, +// integer, +// timestamp, +// boolean, +// } from "drizzle-orm/pg-core"; + +// export const users_temp = pgTable("users_temp", { +// id: text("id").primaryKey(), +// name: text("name").notNull(), +// email: text("email").notNull().unique(), +// emailVerified: boolean("email_verified").notNull(), +// image: text("image"), +// createdAt: timestamp("created_at").notNull(), +// updatedAt: timestamp("updated_at").notNull(), +// twoFactorEnabled: boolean("two_factor_enabled"), +// role: text("role"), +// ownerId: text("owner_id"), +// }); + +// export const session = pgTable("session", { +// id: text("id").primaryKey(), +// expiresAt: timestamp("expires_at").notNull(), +// token: text("token").notNull().unique(), +// createdAt: timestamp("created_at").notNull(), +// updatedAt: timestamp("updated_at").notNull(), +// ipAddress: text("ip_address"), +// userAgent: text("user_agent"), +// userId: text("user_id") +// .notNull() +// .references(() => users_temp.id, { onDelete: "cascade" }), +// activeOrganizationId: text("active_organization_id"), +// }); + +// export const account = pgTable("account", { +// id: text("id").primaryKey(), +// accountId: text("account_id").notNull(), +// providerId: text("provider_id").notNull(), +// userId: text("user_id") +// .notNull() +// .references(() => users_temp.id, { onDelete: "cascade" }), +// accessToken: text("access_token"), +// refreshToken: text("refresh_token"), +// idToken: text("id_token"), +// accessTokenExpiresAt: timestamp("access_token_expires_at"), +// refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), +// scope: text("scope"), +// password: text("password"), +// createdAt: timestamp("created_at").notNull(), +// updatedAt: timestamp("updated_at").notNull(), +// }); + +// export const verification = pgTable("verification", { +// id: text("id").primaryKey(), +// identifier: text("identifier").notNull(), +// value: text("value").notNull(), +// expiresAt: timestamp("expires_at").notNull(), +// createdAt: timestamp("created_at"), +// updatedAt: timestamp("updated_at"), +// }); + +// export const apikey = pgTable("apikey", { +// id: text("id").primaryKey(), +// name: text("name"), +// start: text("start"), +// prefix: text("prefix"), +// key: text("key").notNull(), +// userId: text("user_id") +// .notNull() +// .references(() => user.id, { onDelete: "cascade" }), +// refillInterval: integer("refill_interval"), +// refillAmount: integer("refill_amount"), +// lastRefillAt: timestamp("last_refill_at"), +// enabled: boolean("enabled"), +// rateLimitEnabled: boolean("rate_limit_enabled"), +// rateLimitTimeWindow: integer("rate_limit_time_window"), +// rateLimitMax: integer("rate_limit_max"), +// requestCount: integer("request_count"), +// remaining: integer("remaining"), +// lastRequest: timestamp("last_request"), +// expiresAt: timestamp("expires_at"), +// createdAt: timestamp("created_at").notNull(), +// updatedAt: timestamp("updated_at").notNull(), +// permissions: text("permissions"), +// metadata: text("metadata"), +// }); + +// export const twoFactor = pgTable("two_factor", { +// id: text("id").primaryKey(), +// secret: text("secret").notNull(), +// backupCodes: text("backup_codes").notNull(), +// userId: text("user_id") +// .notNull() +// .references(() => user.id, { onDelete: "cascade" }), +// }); + +// export const organization = pgTable("organization", { +// id: text("id").primaryKey(), +// name: text("name").notNull(), +// slug: text("slug").unique(), +// logo: text("logo"), +// createdAt: timestamp("created_at").notNull(), +// metadata: text("metadata"), +// }); + +// export const member = pgTable("member", { +// id: text("id").primaryKey(), +// organizationId: text("organization_id") +// .notNull() +// .references(() => organization.id, { onDelete: "cascade" }), +// userId: text("user_id") +// .notNull() +// .references(() => user.id, { onDelete: "cascade" }), +// role: text("role").notNull(), +// teamId: text("team_id"), +// createdAt: timestamp("created_at").notNull(), +// }); + +// export const invitation = pgTable("invitation", { +// id: text("id").primaryKey(), +// organizationId: text("organization_id") +// .notNull() +// .references(() => organization.id, { onDelete: "cascade" }), +// email: text("email").notNull(), +// role: text("role"), +// teamId: text("team_id"), +// status: text("status").notNull(), +// expiresAt: timestamp("expires_at").notNull(), +// inviterId: text("inviter_id") +// .notNull() +// .references(() => user.id, { onDelete: "cascade" }), +// }); diff --git a/packages/server/package.json b/packages/server/package.json index cfff36fe..d99f5c24 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -28,6 +28,11 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@better-auth/utils":"0.2.3", + "@oslojs/encoding":"1.1.0", + "@oslojs/crypto":"1.0.1", + "drizzle-dbml-generator":"0.10.0", + "better-auth":"1.2.0", "rotating-file-stream": "3.2.3", "@faker-js/faker": "^8.4.1", "@lucia-auth/adapter-drizzle": "1.0.7", diff --git a/packages/server/src/auth/auth.ts b/packages/server/src/auth/auth.ts deleted file mode 100644 index ab340d0a..00000000 --- a/packages/server/src/auth/auth.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { IncomingMessage, ServerResponse } from "node:http"; -import { findAdminByAuthId } from "@dokploy/server/services/admin"; -import { findUserByAuthId } from "@dokploy/server/services/user"; -import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle"; -import { TimeSpan } from "lucia"; -import { Lucia } from "lucia/dist/core.js"; -import type { Session, User } from "lucia/dist/core.js"; -import { db } from "../db"; -import { type DatabaseUser, auth, sessionTable } from "../db/schema"; - -export const adapter = new DrizzlePostgreSQLAdapter(db, sessionTable, auth); - -export const lucia = new Lucia(adapter, { - sessionCookie: { - attributes: { - secure: false, - }, - }, - sessionExpiresIn: new TimeSpan(1, "d"), - getUserAttributes: (attributes) => { - return { - email: attributes.email, - rol: attributes.rol, - secret: attributes.secret !== null, - adminId: attributes.adminId, - }; - }, -}); - -declare module "lucia" { - interface Register { - Lucia: typeof lucia; - DatabaseUserAttributes: Omit & { - authId: string; - adminId: string; - }; - } -} - -export type ReturnValidateToken = Promise<{ - user: (User & { authId: string; adminId: string }) | null; - session: Session | null; -}>; - -export async function validateRequest( - req: IncomingMessage, - res: ServerResponse, -): ReturnValidateToken { - const sessionId = lucia.readSessionCookie(req.headers.cookie ?? ""); - - if (!sessionId) { - return { - user: null, - session: null, - }; - } - const result = await lucia.validateSession(sessionId); - if (result?.session?.fresh) { - res.appendHeader( - "Set-Cookie", - lucia.createSessionCookie(result.session.id).serialize(), - ); - } - if (!result.session) { - res.appendHeader( - "Set-Cookie", - lucia.createBlankSessionCookie().serialize(), - ); - } - if (result.user) { - try { - if (result.user?.rol === "admin") { - const admin = await findAdminByAuthId(result.user.id); - result.user.adminId = admin.adminId; - } else if (result.user?.rol === "user") { - const userResult = await findUserByAuthId(result.user.id); - result.user.adminId = userResult.adminId; - } - } catch (error) { - return { - user: null, - session: null, - }; - } - } - - return { - session: result.session, - ...((result.user && { - user: { - authId: result.user.id, - email: result.user.email, - rol: result.user.rol, - id: result.user.id, - secret: result.user.secret, - adminId: result.user.adminId, - }, - }) || { - user: null, - }), - }; -} - -export async function validateWebSocketRequest( - req: IncomingMessage, -): Promise<{ user: User; session: Session } | { user: null; session: null }> { - const sessionId = lucia.readSessionCookie(req.headers.cookie ?? ""); - - if (!sessionId) { - return { - user: null, - session: null, - }; - } - const result = await lucia.validateSession(sessionId); - return result; -} diff --git a/packages/server/src/auth/token.ts b/packages/server/src/auth/token.ts deleted file mode 100644 index f29d4dbd..00000000 --- a/packages/server/src/auth/token.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { IncomingMessage } from "node:http"; -import { TimeSpan } from "lucia"; -import { Lucia } from "lucia/dist/core.js"; -import { findAdminByAuthId } from "../services/admin"; -import { findUserByAuthId } from "../services/user"; -import { type ReturnValidateToken, adapter } from "./auth"; - -export const luciaToken = new Lucia(adapter, { - sessionCookie: { - attributes: { - secure: false, - }, - }, - sessionExpiresIn: new TimeSpan(365, "d"), - getUserAttributes: (attributes) => { - return { - email: attributes.email, - rol: attributes.rol, - secret: attributes.secret !== null, - }; - }, -}); - -export const validateBearerToken = async ( - req: IncomingMessage, -): ReturnValidateToken => { - const authorizationHeader = req.headers.authorization; - const sessionId = luciaToken.readBearerToken(authorizationHeader ?? ""); - if (!sessionId) { - return { - user: null, - session: null, - }; - } - const result = await luciaToken.validateSession(sessionId); - - if (result.user) { - if (result.user?.rol === "admin") { - const admin = await findAdminByAuthId(result.user.id); - result.user.adminId = admin.adminId; - } else if (result.user?.rol === "user") { - const userResult = await findUserByAuthId(result.user.id); - result.user.adminId = userResult.adminId; - } - } - return { - session: result.session, - ...((result.user && { - user: { - adminId: result.user.adminId, - authId: result.user.id, - email: result.user.email, - rol: result.user.rol, - id: result.user.id, - secret: result.user.secret, - }, - }) || { - user: null, - }), - }; -}; - -export const validateBearerTokenAPI = async ( - authorizationHeader: string, -): ReturnValidateToken => { - const sessionId = luciaToken.readBearerToken(authorizationHeader ?? ""); - if (!sessionId) { - return { - user: null, - session: null, - }; - } - const result = await luciaToken.validateSession(sessionId); - - if (result.user) { - if (result.user?.rol === "admin") { - const admin = await findAdminByAuthId(result.user.id); - result.user.adminId = admin.adminId; - } else if (result.user?.rol === "user") { - const userResult = await findUserByAuthId(result.user.id); - result.user.adminId = userResult.adminId; - } - } - return { - session: result.session, - ...((result.user && { - user: { - adminId: result.user.adminId, - authId: result.user.id, - email: result.user.email, - rol: result.user.rol, - id: result.user.id, - secret: result.user.secret, - }, - }) || { - user: null, - }), - }; -}; diff --git a/packages/server/src/db/schema/account.ts b/packages/server/src/db/schema/account.ts new file mode 100644 index 00000000..8291ea4d --- /dev/null +++ b/packages/server/src/db/schema/account.ts @@ -0,0 +1,194 @@ +import { relations, sql } from "drizzle-orm"; +import { + boolean, + integer, + pgTable, + text, + timestamp, +} from "drizzle-orm/pg-core"; +import { nanoid } from "nanoid"; +import { projects } from "./project"; +import { server } from "./server"; +import { users_temp } from "./user"; + +export const account = pgTable("account", { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + accountId: text("account_id") + .notNull() + .$defaultFn(() => nanoid()), + providerId: text("provider_id").notNull(), + userId: text("user_id") + .notNull() + .references(() => users_temp.id, { onDelete: "cascade" }), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: timestamp("access_token_expires_at"), + refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), + scope: text("scope"), + password: text("password"), + is2FAEnabled: boolean("is2FAEnabled").notNull().default(false), + createdAt: timestamp("created_at").notNull(), + updatedAt: timestamp("updated_at").notNull(), + resetPasswordToken: text("resetPasswordToken"), + resetPasswordExpiresAt: text("resetPasswordExpiresAt"), + confirmationToken: text("confirmationToken"), + confirmationExpiresAt: text("confirmationExpiresAt"), +}); + +export const accountRelations = relations(account, ({ one }) => ({ + user: one(users_temp, { + fields: [account.userId], + references: [users_temp.id], + }), +})); + +export const verification = pgTable("verification", { + id: text("id").primaryKey(), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at"), + updatedAt: timestamp("updated_at"), +}); + +export const organization = pgTable("organization", { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + name: text("name").notNull(), + slug: text("slug").unique(), + logo: text("logo"), + createdAt: timestamp("created_at").notNull(), + metadata: text("metadata"), + ownerId: text("owner_id") + .notNull() + .references(() => users_temp.id, { onDelete: "cascade" }), +}); + +export const organizationRelations = relations( + organization, + ({ one, many }) => ({ + owner: one(users_temp, { + fields: [organization.ownerId], + references: [users_temp.id], + }), + servers: many(server), + projects: many(projects), + members: many(member), + }), +); + +export const member = pgTable("member", { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + userId: text("user_id") + .notNull() + .references(() => users_temp.id, { onDelete: "cascade" }), + role: text("role").notNull().$type<"owner" | "member" | "admin">(), + createdAt: timestamp("created_at").notNull(), + teamId: text("team_id"), + // Permissions + canCreateProjects: boolean("canCreateProjects").notNull().default(false), + canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false), + canCreateServices: boolean("canCreateServices").notNull().default(false), + canDeleteProjects: boolean("canDeleteProjects").notNull().default(false), + canDeleteServices: boolean("canDeleteServices").notNull().default(false), + canAccessToDocker: boolean("canAccessToDocker").notNull().default(false), + canAccessToAPI: boolean("canAccessToAPI").notNull().default(false), + canAccessToGitProviders: boolean("canAccessToGitProviders") + .notNull() + .default(false), + canAccessToTraefikFiles: boolean("canAccessToTraefikFiles") + .notNull() + .default(false), + accessedProjects: text("accesedProjects") + .array() + .notNull() + .default(sql`ARRAY[]::text[]`), + accessedServices: text("accesedServices") + .array() + .notNull() + .default(sql`ARRAY[]::text[]`), +}); + +export const memberRelations = relations(member, ({ one }) => ({ + organization: one(organization, { + fields: [member.organizationId], + references: [organization.id], + }), + user: one(users_temp, { + fields: [member.userId], + references: [users_temp.id], + }), +})); + +export const invitation = pgTable("invitation", { + id: text("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + email: text("email").notNull(), + role: text("role").$type<"owner" | "member" | "admin">(), + status: text("status").notNull(), + expiresAt: timestamp("expires_at").notNull(), + inviterId: text("inviter_id") + .notNull() + .references(() => users_temp.id, { onDelete: "cascade" }), + teamId: text("team_id"), +}); + +export const invitationRelations = relations(invitation, ({ one }) => ({ + organization: one(organization, { + fields: [invitation.organizationId], + references: [organization.id], + }), +})); + +export const twoFactor = pgTable("two_factor", { + id: text("id").primaryKey(), + secret: text("secret").notNull(), + backupCodes: text("backup_codes").notNull(), + userId: text("user_id") + .notNull() + .references(() => users_temp.id, { onDelete: "cascade" }), +}); + +export const apikey = pgTable("apikey", { + id: text("id").primaryKey(), + name: text("name"), + start: text("start"), + prefix: text("prefix"), + key: text("key").notNull(), + userId: text("user_id") + .notNull() + .references(() => users_temp.id, { onDelete: "cascade" }), + refillInterval: integer("refill_interval"), + refillAmount: integer("refill_amount"), + lastRefillAt: timestamp("last_refill_at"), + enabled: boolean("enabled"), + rateLimitEnabled: boolean("rate_limit_enabled"), + rateLimitTimeWindow: integer("rate_limit_time_window"), + rateLimitMax: integer("rate_limit_max"), + requestCount: integer("request_count"), + remaining: integer("remaining"), + lastRequest: timestamp("last_request"), + expiresAt: timestamp("expires_at"), + createdAt: timestamp("created_at").notNull(), + updatedAt: timestamp("updated_at").notNull(), + permissions: text("permissions"), + metadata: text("metadata"), +}); + +export const apikeyRelations = relations(apikey, ({ one }) => ({ + user: one(users_temp, { + fields: [apikey.userId], + references: [users_temp.id], + }), +})); diff --git a/packages/server/src/db/schema/admin.ts b/packages/server/src/db/schema/admin.ts deleted file mode 100644 index 983f99fd..00000000 --- a/packages/server/src/db/schema/admin.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { relations } from "drizzle-orm"; -import { - boolean, - integer, - json, - jsonb, - pgTable, - text, -} from "drizzle-orm/pg-core"; -import { createInsertSchema } from "drizzle-zod"; -import { nanoid } from "nanoid"; -import { z } from "zod"; -import { auth } from "./auth"; -import { certificates } from "./certificate"; -import { registry } from "./registry"; -import { certificateType } from "./shared"; -import { sshKeys } from "./ssh-key"; -import { users } from "./user"; - -export const admins = pgTable("admin", { - adminId: text("adminId") - .notNull() - .primaryKey() - .$defaultFn(() => nanoid()), - serverIp: text("serverIp"), - certificateType: certificateType("certificateType").notNull().default("none"), - host: text("host"), - letsEncryptEmail: text("letsEncryptEmail"), - sshPrivateKey: text("sshPrivateKey"), - enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false), - enableLogRotation: boolean("enableLogRotation").notNull().default(false), - authId: text("authId") - .notNull() - .references(() => auth.id, { onDelete: "cascade" }), - createdAt: text("createdAt") - .notNull() - .$defaultFn(() => new Date().toISOString()), - stripeCustomerId: text("stripeCustomerId"), - stripeSubscriptionId: text("stripeSubscriptionId"), - serversQuantity: integer("serversQuantity").notNull().default(0), - - // Metrics - enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false), - metricsConfig: jsonb("metricsConfig") - .$type<{ - server: { - type: "Dokploy" | "Remote"; - refreshRate: number; - port: number; - token: string; - urlCallback: string; - retentionDays: number; - cronJob: string; - thresholds: { - cpu: number; - memory: number; - }; - }; - containers: { - refreshRate: number; - services: { - include: string[]; - exclude: string[]; - }; - }; - }>() - .notNull() - .default({ - server: { - type: "Dokploy", - refreshRate: 60, - port: 4500, - token: "", - retentionDays: 2, - cronJob: "", - urlCallback: "", - thresholds: { - cpu: 0, - memory: 0, - }, - }, - containers: { - refreshRate: 60, - services: { - include: [], - exclude: [], - }, - }, - }), - cleanupCacheApplications: boolean("cleanupCacheApplications") - .notNull() - .default(false), - cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews") - .notNull() - .default(false), - cleanupCacheOnCompose: boolean("cleanupCacheOnCompose") - .notNull() - .default(false), -}); - -export const adminsRelations = relations(admins, ({ one, many }) => ({ - auth: one(auth, { - fields: [admins.authId], - references: [auth.id], - }), - users: many(users), - registry: many(registry), - sshKeys: many(sshKeys), - certificates: many(certificates), -})); - -const createSchema = createInsertSchema(admins, { - adminId: z.string(), - enableDockerCleanup: z.boolean().optional(), - sshPrivateKey: z.string().optional(), - certificateType: z.enum(["letsencrypt", "none"]).default("none"), - serverIp: z.string().optional(), - letsEncryptEmail: z.string().optional(), -}); - -export const apiUpdateAdmin = createSchema.partial(); - -export const apiSaveSSHKey = createSchema - .pick({ - sshPrivateKey: true, - }) - .required(); - -export const apiAssignDomain = createSchema - .pick({ - host: true, - certificateType: true, - letsEncryptEmail: true, - }) - .required() - .partial({ - letsEncryptEmail: true, - }); - -export const apiUpdateDockerCleanup = createSchema - .pick({ - enableDockerCleanup: true, - }) - .required() - .extend({ - serverId: z.string().optional(), - }); - -export const apiTraefikConfig = z.object({ - traefikConfig: z.string().min(1), -}); - -export const apiModifyTraefikConfig = z.object({ - path: z.string().min(1), - traefikConfig: z.string().min(1), - serverId: z.string().optional(), -}); -export const apiReadTraefikConfig = z.object({ - path: z.string().min(1), - serverId: z.string().optional(), -}); - -export const apiEnableDashboard = z.object({ - enableDashboard: z.boolean().optional(), - serverId: z.string().optional(), -}); - -export const apiServerSchema = z - .object({ - serverId: z.string().optional(), - }) - .optional(); - -export const apiReadStatsLogs = z.object({ - page: z - .object({ - pageIndex: z.number(), - pageSize: z.number(), - }) - .optional(), - status: z.string().array().optional(), - search: z.string().optional(), - sort: z.object({ id: z.string(), desc: z.boolean() }).optional(), -}); - -export const apiUpdateWebServerMonitoring = z.object({ - metricsConfig: z - .object({ - server: z.object({ - refreshRate: z.number().min(2), - port: z.number().min(1), - token: z.string(), - urlCallback: z.string().url(), - retentionDays: z.number().min(1), - cronJob: z.string().min(1), - thresholds: z.object({ - cpu: z.number().min(0), - memory: z.number().min(0), - }), - }), - containers: z.object({ - refreshRate: z.number().min(2), - services: z.object({ - include: z.array(z.string()).optional(), - exclude: z.array(z.string()).optional(), - }), - }), - }) - .required(), -}); diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts index 2437f59d..e670e2e2 100644 --- a/packages/server/src/db/schema/application.ts +++ b/packages/server/src/db/schema/application.ts @@ -44,7 +44,6 @@ export const buildType = pgEnum("buildType", [ "static", ]); -// TODO: refactor this types export interface HealthCheckSwarm { Test?: string[] | undefined; Interval?: number | undefined; diff --git a/packages/server/src/db/schema/auth.ts b/packages/server/src/db/schema/auth.ts deleted file mode 100644 index 3e16c68e..00000000 --- a/packages/server/src/db/schema/auth.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { getRandomValues } from "node:crypto"; -import { relations } from "drizzle-orm"; -import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core"; -import { createInsertSchema } from "drizzle-zod"; -import { nanoid } from "nanoid"; -import { z } from "zod"; -import { admins } from "./admin"; -import { users } from "./user"; - -const randomImages = [ - "/avatars/avatar-1.png", - "/avatars/avatar-2.png", - "/avatars/avatar-3.png", - "/avatars/avatar-4.png", - "/avatars/avatar-5.png", - "/avatars/avatar-6.png", - "/avatars/avatar-7.png", - "/avatars/avatar-8.png", - "/avatars/avatar-9.png", - "/avatars/avatar-10.png", - "/avatars/avatar-11.png", - "/avatars/avatar-12.png", -]; - -const generateRandomImage = () => { - return ( - randomImages[ - // @ts-ignore - getRandomValues(new Uint32Array(1))[0] % randomImages.length - ] || "/avatars/avatar-1.png" - ); -}; -export type DatabaseUser = typeof auth.$inferSelect; -export const roles = pgEnum("Roles", ["admin", "user"]); - -export const auth = pgTable("auth", { - id: text("id") - .notNull() - .primaryKey() - .$defaultFn(() => nanoid()), - email: text("email").notNull().unique(), - password: text("password").notNull(), - rol: roles("rol").notNull(), - image: text("image").$defaultFn(() => generateRandomImage()), - secret: text("secret"), - token: text("token"), - is2FAEnabled: boolean("is2FAEnabled").notNull().default(false), - createdAt: text("createdAt") - .notNull() - .$defaultFn(() => new Date().toISOString()), - resetPasswordToken: text("resetPasswordToken"), - resetPasswordExpiresAt: text("resetPasswordExpiresAt"), - confirmationToken: text("confirmationToken"), - confirmationExpiresAt: text("confirmationExpiresAt"), -}); - -export const authRelations = relations(auth, ({ many }) => ({ - admins: many(admins), - users: many(users), -})); -const createSchema = createInsertSchema(auth, { - email: z.string().email(), - password: z.string().min(8), - rol: z.enum(["admin", "user"]), - image: z.string().optional(), -}); - -export const apiCreateAdmin = createSchema.pick({ - email: true, - password: true, -}); - -export const apiCreateUser = createSchema - .pick({ - password: true, - id: true, - token: true, - }) - .required() - .extend({ - token: z.string().min(1), - }); - -export const apiLogin = createSchema - .pick({ - email: true, - password: true, - }) - .required(); - -export const apiUpdateAuth = createSchema.partial().extend({ - email: z.string().nullable(), - password: z.string().nullable(), - image: z.string().optional(), - currentPassword: z.string().nullable(), -}); - -export const apiUpdateAuthByAdmin = createSchema.partial().extend({ - email: z.string().nullable(), - password: z.string().nullable(), - image: z.string().optional(), - id: z.string().min(1), -}); - -export const apiFindOneAuth = createSchema - .pick({ - id: true, - }) - .required(); - -export const apiVerify2FA = createSchema - .extend({ - pin: z.string().min(6), - secret: z.string().min(1), - }) - .pick({ - pin: true, - secret: true, - }) - .required(); - -export const apiVerifyLogin2FA = createSchema - .extend({ - pin: z.string().min(6), - }) - .pick({ - pin: true, - id: true, - }) - .required(); diff --git a/packages/server/src/db/schema/bitbucket.ts b/packages/server/src/db/schema/bitbucket.ts index 393cb1e7..0311202d 100644 --- a/packages/server/src/db/schema/bitbucket.ts +++ b/packages/server/src/db/schema/bitbucket.ts @@ -61,5 +61,5 @@ export const apiUpdateBitbucket = createSchema.extend({ name: z.string().min(1), bitbucketUsername: z.string().optional(), bitbucketWorkspaceName: z.string().optional(), - adminId: z.string().optional(), + organizationId: z.string().optional(), }); diff --git a/packages/server/src/db/schema/certificate.ts b/packages/server/src/db/schema/certificate.ts index 1df61be8..bf72f7db 100644 --- a/packages/server/src/db/schema/certificate.ts +++ b/packages/server/src/db/schema/certificate.ts @@ -3,7 +3,7 @@ import { boolean, pgTable, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { admins } from "./admin"; +import { organization } from "./account"; import { server } from "./server"; import { generateAppName } from "./utils"; @@ -20,27 +20,24 @@ export const certificates = pgTable("certificate", { .$defaultFn(() => generateAppName("certificate")) .unique(), autoRenew: boolean("autoRenew"), - adminId: text("adminId").references(() => admins.adminId, { - onDelete: "cascade", - }), + organizationId: text("organizationId") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), serverId: text("serverId").references(() => server.serverId, { onDelete: "cascade", }), }); -export const certificatesRelations = relations( - certificates, - ({ one, many }) => ({ - server: one(server, { - fields: [certificates.serverId], - references: [server.serverId], - }), - admin: one(admins, { - fields: [certificates.adminId], - references: [admins.adminId], - }), +export const certificatesRelations = relations(certificates, ({ one }) => ({ + server: one(server, { + fields: [certificates.serverId], + references: [server.serverId], }), -); + organization: one(organization, { + fields: [certificates.organizationId], + references: [organization.id], + }), +})); export const apiCreateCertificate = createInsertSchema(certificates, { name: z.string().min(1), diff --git a/packages/server/src/db/schema/dbml.ts b/packages/server/src/db/schema/dbml.ts new file mode 100644 index 00000000..72a75814 --- /dev/null +++ b/packages/server/src/db/schema/dbml.ts @@ -0,0 +1,7 @@ +import { pgGenerate } from "drizzle-dbml-generator"; // Using Postgres for this example +import * as schema from "./index"; + +const out = "./schema.dbml"; +const relational = true; + +pgGenerate({ schema, out, relational }); diff --git a/packages/server/src/db/schema/deployment.ts b/packages/server/src/db/schema/deployment.ts index 1be5db5e..4dfed76b 100644 --- a/packages/server/src/db/schema/deployment.ts +++ b/packages/server/src/db/schema/deployment.ts @@ -1,4 +1,4 @@ -import { is, relations } from "drizzle-orm"; +import { relations } from "drizzle-orm"; import { type AnyPgColumn, boolean, diff --git a/packages/server/src/db/schema/destination.ts b/packages/server/src/db/schema/destination.ts index 7d7be614..0aeb1490 100644 --- a/packages/server/src/db/schema/destination.ts +++ b/packages/server/src/db/schema/destination.ts @@ -3,7 +3,7 @@ import { pgTable, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { admins } from "./admin"; +import { organization } from "./account"; import { backups } from "./backups"; export const destinations = pgTable("destination", { @@ -17,20 +17,19 @@ export const destinations = pgTable("destination", { secretAccessKey: text("secretAccessKey").notNull(), bucket: text("bucket").notNull(), region: text("region").notNull(), - // maybe it can be null endpoint: text("endpoint").notNull(), - adminId: text("adminId") + organizationId: text("organizationId") .notNull() - .references(() => admins.adminId, { onDelete: "cascade" }), + .references(() => organization.id, { onDelete: "cascade" }), }); export const destinationsRelations = relations( destinations, ({ many, one }) => ({ backups: many(backups), - admin: one(admins, { - fields: [destinations.adminId], - references: [admins.adminId], + organization: one(organization, { + fields: [destinations.organizationId], + references: [organization.id], }), }), ); diff --git a/packages/server/src/db/schema/git-provider.ts b/packages/server/src/db/schema/git-provider.ts index dbbfc183..92230737 100644 --- a/packages/server/src/db/schema/git-provider.ts +++ b/packages/server/src/db/schema/git-provider.ts @@ -3,7 +3,7 @@ import { pgEnum, pgTable, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { admins } from "./admin"; +import { organization } from "./account"; import { bitbucket } from "./bitbucket"; import { github } from "./github"; import { gitlab } from "./gitlab"; @@ -24,12 +24,12 @@ export const gitProvider = pgTable("git_provider", { createdAt: text("createdAt") .notNull() .$defaultFn(() => new Date().toISOString()), - adminId: text("adminId").references(() => admins.adminId, { - onDelete: "cascade", - }), + organizationId: text("organizationId") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), }); -export const gitProviderRelations = relations(gitProvider, ({ one, many }) => ({ +export const gitProviderRelations = relations(gitProvider, ({ one }) => ({ github: one(github, { fields: [gitProvider.gitProviderId], references: [github.gitProviderId], @@ -42,9 +42,9 @@ export const gitProviderRelations = relations(gitProvider, ({ one, many }) => ({ fields: [gitProvider.gitProviderId], references: [bitbucket.gitProviderId], }), - admin: one(admins, { - fields: [gitProvider.adminId], - references: [admins.adminId], + organization: one(organization, { + fields: [gitProvider.organizationId], + references: [organization.id], }), })); diff --git a/packages/server/src/db/schema/index.ts b/packages/server/src/db/schema/index.ts index 9c7a079c..5eb7f369 100644 --- a/packages/server/src/db/schema/index.ts +++ b/packages/server/src/db/schema/index.ts @@ -1,8 +1,6 @@ export * from "./application"; export * from "./postgres"; export * from "./user"; -export * from "./admin"; -export * from "./auth"; export * from "./project"; export * from "./domain"; export * from "./mariadb"; @@ -30,3 +28,4 @@ export * from "./gitlab"; export * from "./server"; export * from "./utils"; export * from "./preview-deployments"; +export * from "./account"; diff --git a/packages/server/src/db/schema/notification.ts b/packages/server/src/db/schema/notification.ts index 3e33bfd8..1c8a2d8f 100644 --- a/packages/server/src/db/schema/notification.ts +++ b/packages/server/src/db/schema/notification.ts @@ -3,7 +3,7 @@ import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { admins } from "./admin"; +import { organization } from "./account"; export const notificationType = pgEnum("notificationType", [ "slack", @@ -44,9 +44,9 @@ export const notifications = pgTable("notification", { gotifyId: text("gotifyId").references(() => gotify.gotifyId, { onDelete: "cascade", }), - adminId: text("adminId").references(() => admins.adminId, { - onDelete: "cascade", - }), + organizationId: text("organizationId") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), }); export const slack = pgTable("slack", { @@ -122,9 +122,9 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({ fields: [notifications.gotifyId], references: [gotify.gotifyId], }), - admin: one(admins, { - fields: [notifications.adminId], - references: [admins.adminId], + organization: one(organization, { + fields: [notifications.organizationId], + references: [organization.id], }), })); @@ -149,7 +149,7 @@ export const apiCreateSlack = notificationsSchema export const apiUpdateSlack = apiCreateSlack.partial().extend({ notificationId: z.string().min(1), slackId: z.string(), - adminId: z.string().optional(), + organizationId: z.string().optional(), }); export const apiTestSlackConnection = apiCreateSlack.pick({ @@ -177,7 +177,7 @@ export const apiCreateTelegram = notificationsSchema export const apiUpdateTelegram = apiCreateTelegram.partial().extend({ notificationId: z.string().min(1), telegramId: z.string().min(1), - adminId: z.string().optional(), + organizationId: z.string().optional(), }); export const apiTestTelegramConnection = apiCreateTelegram.pick({ @@ -205,7 +205,7 @@ export const apiCreateDiscord = notificationsSchema export const apiUpdateDiscord = apiCreateDiscord.partial().extend({ notificationId: z.string().min(1), discordId: z.string().min(1), - adminId: z.string().optional(), + organizationId: z.string().optional(), }); export const apiTestDiscordConnection = apiCreateDiscord @@ -239,7 +239,7 @@ export const apiCreateEmail = notificationsSchema export const apiUpdateEmail = apiCreateEmail.partial().extend({ notificationId: z.string().min(1), emailId: z.string().min(1), - adminId: z.string().optional(), + organizationId: z.string().optional(), }); export const apiTestEmailConnection = apiCreateEmail.pick({ @@ -271,7 +271,7 @@ export const apiCreateGotify = notificationsSchema export const apiUpdateGotify = apiCreateGotify.partial().extend({ notificationId: z.string().min(1), gotifyId: z.string().min(1), - adminId: z.string().optional(), + organizationId: z.string().optional(), }); export const apiTestGotifyConnection = apiCreateGotify diff --git a/packages/server/src/db/schema/project.ts b/packages/server/src/db/schema/project.ts index 7ed140d6..deeba4ac 100644 --- a/packages/server/src/db/schema/project.ts +++ b/packages/server/src/db/schema/project.ts @@ -1,10 +1,9 @@ import { relations } from "drizzle-orm"; - import { pgTable, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { admins } from "./admin"; +import { organization } from "./account"; import { applications } from "./application"; import { compose } from "./compose"; import { mariadb } from "./mariadb"; @@ -23,9 +22,10 @@ export const projects = pgTable("project", { createdAt: text("createdAt") .notNull() .$defaultFn(() => new Date().toISOString()), - adminId: text("adminId") + + organizationId: text("organizationId") .notNull() - .references(() => admins.adminId, { onDelete: "cascade" }), + .references(() => organization.id, { onDelete: "cascade" }), env: text("env").notNull().default(""), }); @@ -37,9 +37,9 @@ export const projectRelations = relations(projects, ({ many, one }) => ({ mongo: many(mongo), redis: many(redis), compose: many(compose), - admin: one(admins, { - fields: [projects.adminId], - references: [admins.adminId], + organization: one(organization, { + fields: [projects.organizationId], + references: [organization.id], }), })); diff --git a/packages/server/src/db/schema/registry.ts b/packages/server/src/db/schema/registry.ts index 20544a58..b1874709 100644 --- a/packages/server/src/db/schema/registry.ts +++ b/packages/server/src/db/schema/registry.ts @@ -3,7 +3,7 @@ import { pgEnum, pgTable, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { admins } from "./admin"; +import { organization } from "./account"; import { applications } from "./application"; /** * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same @@ -27,16 +27,12 @@ export const registry = pgTable("registry", { .notNull() .$defaultFn(() => new Date().toISOString()), registryType: registryType("selfHosted").notNull().default("cloud"), - adminId: text("adminId") + organizationId: text("organizationId") .notNull() - .references(() => admins.adminId, { onDelete: "cascade" }), + .references(() => organization.id, { onDelete: "cascade" }), }); -export const registryRelations = relations(registry, ({ one, many }) => ({ - admin: one(admins, { - fields: [registry.adminId], - references: [admins.adminId], - }), +export const registryRelations = relations(registry, ({ many }) => ({ applications: many(applications), })); @@ -45,7 +41,7 @@ const createSchema = createInsertSchema(registry, { username: z.string().min(1), password: z.string().min(1), registryUrl: z.string(), - adminId: z.string().min(1), + organizationId: z.string().min(1), registryId: z.string().min(1), registryType: z.enum(["cloud"]), imagePrefix: z.string().nullable().optional(), diff --git a/packages/server/src/db/schema/schema.dbml b/packages/server/src/db/schema/schema.dbml new file mode 100644 index 00000000..ce2b5abc --- /dev/null +++ b/packages/server/src/db/schema/schema.dbml @@ -0,0 +1,872 @@ +enum applicationStatus { + idle + running + done + error +} + +enum buildType { + dockerfile + heroku_buildpacks + paketo_buildpacks + nixpacks + static +} + +enum certificateType { + letsencrypt + none +} + +enum composeType { + "docker-compose" + stack +} + +enum databaseType { + postgres + mariadb + mysql + mongo +} + +enum deploymentStatus { + running + done + error +} + +enum domainType { + compose + application + preview +} + +enum gitProviderType { + github + gitlab + bitbucket +} + +enum mountType { + bind + volume + file +} + +enum notificationType { + slack + telegram + discord + email + gotify +} + +enum protocolType { + tcp + udp +} + +enum RegistryType { + selfHosted + cloud +} + +enum Roles { + admin + user +} + +enum serverStatus { + active + inactive +} + +enum serviceType { + application + postgres + mysql + mariadb + mongo + redis + compose +} + +enum sourceType { + docker + git + github + gitlab + bitbucket + drop +} + +enum sourceTypeCompose { + git + github + gitlab + bitbucket + raw +} + +table account { + id text [pk, not null] + account_id text [not null] + provider_id text [not null] + user_id text [not null] + access_token text + refresh_token text + id_token text + access_token_expires_at timestamp + refresh_token_expires_at timestamp + scope text + password text + is2FAEnabled boolean [not null, default: false] + created_at timestamp [not null] + updated_at timestamp [not null] + resetPasswordToken text + resetPasswordExpiresAt text + confirmationToken text + confirmationExpiresAt text +} + +table admin { +} + +table application { + applicationId text [pk, not null] + name text [not null] + appName text [not null, unique] + description text + env text + previewEnv text + previewBuildArgs text + previewWildcard text + previewPort integer [default: 3000] + previewHttps boolean [not null, default: false] + previewPath text [default: '/'] + certificateType certificateType [not null, default: 'none'] + previewLimit integer [default: 3] + isPreviewDeploymentsActive boolean [default: false] + buildArgs text + memoryReservation text + memoryLimit text + cpuReservation text + cpuLimit text + title text + enabled boolean + subtitle text + command text + refreshToken text + sourceType sourceType [not null, default: 'github'] + repository text + owner text + branch text + buildPath text [default: '/'] + autoDeploy boolean + gitlabProjectId integer + gitlabRepository text + gitlabOwner text + gitlabBranch text + gitlabBuildPath text [default: '/'] + gitlabPathNamespace text + bitbucketRepository text + bitbucketOwner text + bitbucketBranch text + bitbucketBuildPath text [default: '/'] + username text + password text + dockerImage text + registryUrl text + customGitUrl text + customGitBranch text + customGitBuildPath text + customGitSSHKeyId text + dockerfile text + dockerContextPath text + dockerBuildStage text + dropBuildPath text + healthCheckSwarm json + restartPolicySwarm json + placementSwarm json + updateConfigSwarm json + rollbackConfigSwarm json + modeSwarm json + labelsSwarm json + networkSwarm json + replicas integer [not null, default: 1] + applicationStatus applicationStatus [not null, default: 'idle'] + buildType buildType [not null, default: 'nixpacks'] + herokuVersion text [default: '24'] + publishDirectory text + createdAt text [not null] + registryId text + projectId text [not null] + githubId text + gitlabId text + bitbucketId text + serverId text +} + +table auth { + id text [pk, not null] + email text [not null, unique] + password text [not null] + rol Roles [not null] + image text + secret text + token text + is2FAEnabled boolean [not null, default: false] + createdAt text [not null] + resetPasswordToken text + resetPasswordExpiresAt text + confirmationToken text + confirmationExpiresAt text +} + +table backup { + backupId text [pk, not null] + schedule text [not null] + enabled boolean + database text [not null] + prefix text [not null] + destinationId text [not null] + databaseType databaseType [not null] + postgresId text + mariadbId text + mysqlId text + mongoId text +} + +table bitbucket { + bitbucketId text [pk, not null] + bitbucketUsername text + appPassword text + bitbucketWorkspaceName text + gitProviderId text [not null] +} + +table certificate { + certificateId text [pk, not null] + name text [not null] + certificateData text [not null] + privateKey text [not null] + certificatePath text [not null, unique] + autoRenew boolean + userId text + serverId text +} + +table compose { + composeId text [pk, not null] + name text [not null] + appName text [not null] + description text + env text + composeFile text [not null, default: ''] + refreshToken text + sourceType sourceTypeCompose [not null, default: 'github'] + composeType composeType [not null, default: 'docker-compose'] + repository text + owner text + branch text + autoDeploy boolean + gitlabProjectId integer + gitlabRepository text + gitlabOwner text + gitlabBranch text + gitlabPathNamespace text + bitbucketRepository text + bitbucketOwner text + bitbucketBranch text + customGitUrl text + customGitBranch text + customGitSSHKeyId text + command text [not null, default: ''] + composePath text [not null, default: './docker-compose.yml'] + suffix text [not null, default: ''] + randomize boolean [not null, default: false] + isolatedDeployment boolean [not null, default: false] + composeStatus applicationStatus [not null, default: 'idle'] + projectId text [not null] + createdAt text [not null] + githubId text + gitlabId text + bitbucketId text + serverId text +} + +table deployment { + deploymentId text [pk, not null] + title text [not null] + description text + status deploymentStatus [default: 'running'] + logPath text [not null] + applicationId text + composeId text + serverId text + isPreviewDeployment boolean [default: false] + previewDeploymentId text + createdAt text [not null] + errorMessage text +} + +table destination { + destinationId text [pk, not null] + name text [not null] + provider text + accessKey text [not null] + secretAccessKey text [not null] + bucket text [not null] + region text [not null] + endpoint text [not null] + userId text [not null] +} + +table discord { + discordId text [pk, not null] + webhookUrl text [not null] + decoration boolean +} + +table domain { + domainId text [pk, not null] + host text [not null] + https boolean [not null, default: false] + port integer [default: 3000] + path text [default: '/'] + serviceName text + domainType domainType [default: 'application'] + uniqueConfigKey serial [not null, increment] + createdAt text [not null] + composeId text + applicationId text + previewDeploymentId text + certificateType certificateType [not null, default: 'none'] +} + +table email { + emailId text [pk, not null] + smtpServer text [not null] + smtpPort integer [not null] + username text [not null] + password text [not null] + fromAddress text [not null] + toAddress text[] [not null] +} + +table git_provider { + gitProviderId text [pk, not null] + name text [not null] + providerType gitProviderType [not null, default: 'github'] + createdAt text [not null] + userId text +} + +table github { + githubId text [pk, not null] + githubAppName text + githubAppId integer + githubClientId text + githubClientSecret text + githubInstallationId text + githubPrivateKey text + githubWebhookSecret text + gitProviderId text [not null] +} + +table gitlab { + gitlabId text [pk, not null] + gitlabUrl text [not null, default: 'https://gitlab.com'] + application_id text + redirect_uri text + secret text + access_token text + refresh_token text + group_name text + expires_at integer + gitProviderId text [not null] +} + +table gotify { + gotifyId text [pk, not null] + serverUrl text [not null] + appToken text [not null] + priority integer [not null, default: 5] + decoration boolean +} + +table invitation { + id text [pk, not null] + organization_id text [not null] + email text [not null] + role text + status text [not null] + expires_at timestamp [not null] + inviter_id text [not null] +} + +table mariadb { + mariadbId text [pk, not null] + name text [not null] + appName text [not null, unique] + description text + databaseName text [not null] + databaseUser text [not null] + databasePassword text [not null] + rootPassword text [not null] + dockerImage text [not null] + command text + env text + memoryReservation text + memoryLimit text + cpuReservation text + cpuLimit text + externalPort integer + applicationStatus applicationStatus [not null, default: 'idle'] + createdAt text [not null] + projectId text [not null] + serverId text +} + +table member { + id text [pk, not null] + organization_id text [not null] + user_id text [not null] + role text [not null] + created_at timestamp [not null] +} + +table mongo { + mongoId text [pk, not null] + name text [not null] + appName text [not null, unique] + description text + databaseUser text [not null] + databasePassword text [not null] + dockerImage text [not null] + command text + env text + memoryReservation text + memoryLimit text + cpuReservation text + cpuLimit text + externalPort integer + applicationStatus applicationStatus [not null, default: 'idle'] + createdAt text [not null] + projectId text [not null] + serverId text + replicaSets boolean [default: false] +} + +table mount { + mountId text [pk, not null] + type mountType [not null] + hostPath text + volumeName text + filePath text + content text + serviceType serviceType [not null, default: 'application'] + mountPath text [not null] + applicationId text + postgresId text + mariadbId text + mongoId text + mysqlId text + redisId text + composeId text +} + +table mysql { + mysqlId text [pk, not null] + name text [not null] + appName text [not null, unique] + description text + databaseName text [not null] + databaseUser text [not null] + databasePassword text [not null] + rootPassword text [not null] + dockerImage text [not null] + command text + env text + memoryReservation text + memoryLimit text + cpuReservation text + cpuLimit text + externalPort integer + applicationStatus applicationStatus [not null, default: 'idle'] + createdAt text [not null] + projectId text [not null] + serverId text +} + +table notification { + notificationId text [pk, not null] + name text [not null] + appDeploy boolean [not null, default: false] + appBuildError boolean [not null, default: false] + databaseBackup boolean [not null, default: false] + dokployRestart boolean [not null, default: false] + dockerCleanup boolean [not null, default: false] + serverThreshold boolean [not null, default: false] + notificationType notificationType [not null] + createdAt text [not null] + slackId text + telegramId text + discordId text + emailId text + gotifyId text + userId text +} + +table organization { + id text [pk, not null] + name text [not null] + slug text [unique] + logo text + created_at timestamp [not null] + metadata text + owner_id text [not null] +} + +table port { + portId text [pk, not null] + publishedPort integer [not null] + targetPort integer [not null] + protocol protocolType [not null] + applicationId text [not null] +} + +table postgres { + postgresId text [pk, not null] + name text [not null] + appName text [not null, unique] + databaseName text [not null] + databaseUser text [not null] + databasePassword text [not null] + description text + dockerImage text [not null] + command text + env text + memoryReservation text + externalPort integer + memoryLimit text + cpuReservation text + cpuLimit text + applicationStatus applicationStatus [not null, default: 'idle'] + createdAt text [not null] + projectId text [not null] + serverId text +} + +table preview_deployments { + previewDeploymentId text [pk, not null] + branch text [not null] + pullRequestId text [not null] + pullRequestNumber text [not null] + pullRequestURL text [not null] + pullRequestTitle text [not null] + pullRequestCommentId text [not null] + previewStatus applicationStatus [not null, default: 'idle'] + appName text [not null, unique] + applicationId text [not null] + domainId text + createdAt text [not null] + expiresAt text +} + +table project { + projectId text [pk, not null] + name text [not null] + description text + createdAt text [not null] + userId text [not null] + env text [not null, default: ''] +} + +table redirect { + redirectId text [pk, not null] + regex text [not null] + replacement text [not null] + permanent boolean [not null, default: false] + uniqueConfigKey serial [not null, increment] + createdAt text [not null] + applicationId text [not null] +} + +table redis { + redisId text [pk, not null] + name text [not null] + appName text [not null, unique] + description text + password text [not null] + dockerImage text [not null] + command text + env text + memoryReservation text + memoryLimit text + cpuReservation text + cpuLimit text + externalPort integer + createdAt text [not null] + applicationStatus applicationStatus [not null, default: 'idle'] + projectId text [not null] + serverId text +} + +table registry { + registryId text [pk, not null] + registryName text [not null] + imagePrefix text + username text [not null] + password text [not null] + registryUrl text [not null, default: ''] + createdAt text [not null] + selfHosted RegistryType [not null, default: 'cloud'] + userId text [not null] +} + +table security { + securityId text [pk, not null] + username text [not null] + password text [not null] + createdAt text [not null] + applicationId text [not null] + + indexes { + (username, applicationId) [name: 'security_username_applicationId_unique', unique] + } +} + +table server { + serverId text [pk, not null] + name text [not null] + description text + ipAddress text [not null] + port integer [not null] + username text [not null, default: 'root'] + appName text [not null] + enableDockerCleanup boolean [not null, default: false] + createdAt text [not null] + userId text [not null] + serverStatus serverStatus [not null, default: 'active'] + command text [not null, default: ''] + sshKeyId text + metricsConfig jsonb [not null, default: `{"server":{"type":"Remote","refreshRate":60,"port":4500,"token":"","urlCallback":"","cronJob":"","retentionDays":2,"thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}`] +} + +table session { + id text [pk, not null] + expires_at timestamp [not null] + token text [not null, unique] + created_at timestamp [not null] + updated_at timestamp [not null] + ip_address text + user_agent text + user_id text [not null] + impersonated_by text + active_organization_id text +} + +table slack { + slackId text [pk, not null] + webhookUrl text [not null] + channel text +} + +table "ssh-key" { + sshKeyId text [pk, not null] + privateKey text [not null, default: ''] + publicKey text [not null] + name text [not null] + description text + createdAt text [not null] + lastUsedAt text + userId text +} + +table telegram { + telegramId text [pk, not null] + botToken text [not null] + chatId text [not null] +} + +table user { + id text [pk, not null] + name text [not null, default: ''] + token text [not null] + isRegistered boolean [not null, default: false] + expirationDate text [not null] + createdAt text [not null] + canCreateProjects boolean [not null, default: false] + canAccessToSSHKeys boolean [not null, default: false] + canCreateServices boolean [not null, default: false] + canDeleteProjects boolean [not null, default: false] + canDeleteServices boolean [not null, default: false] + canAccessToDocker boolean [not null, default: false] + canAccessToAPI boolean [not null, default: false] + canAccessToGitProviders boolean [not null, default: false] + canAccessToTraefikFiles boolean [not null, default: false] + accesedProjects text[] [not null, default: `ARRAY[]::text[]`] + accesedServices text[] [not null, default: `ARRAY[]::text[]`] + email text [not null, unique] + email_verified boolean [not null] + image text + role text + banned boolean + ban_reason text + ban_expires timestamp + updated_at timestamp [not null] + serverIp text + certificateType certificateType [not null, default: 'none'] + host text + letsEncryptEmail text + sshPrivateKey text + enableDockerCleanup boolean [not null, default: false] + enableLogRotation boolean [not null, default: false] + enablePaidFeatures boolean [not null, default: false] + metricsConfig jsonb [not null, default: `{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}`] + cleanupCacheApplications boolean [not null, default: false] + cleanupCacheOnPreviews boolean [not null, default: false] + cleanupCacheOnCompose boolean [not null, default: false] + stripeCustomerId text + stripeSubscriptionId text + serversQuantity integer [not null, default: 0] +} + +table verification { + id text [pk, not null] + identifier text [not null] + value text [not null] + expires_at timestamp [not null] + created_at timestamp + updated_at timestamp +} + +ref: mount.applicationId > application.applicationId + +ref: mount.postgresId > postgres.postgresId + +ref: mount.mariadbId > mariadb.mariadbId + +ref: mount.mongoId > mongo.mongoId + +ref: mount.mysqlId > mysql.mysqlId + +ref: mount.redisId > redis.redisId + +ref: mount.composeId > compose.composeId + +ref: application.projectId > project.projectId + +ref: application.customGitSSHKeyId > "ssh-key".sshKeyId + +ref: application.registryId > registry.registryId + +ref: application.githubId - github.githubId + +ref: application.gitlabId - gitlab.gitlabId + +ref: application.bitbucketId - bitbucket.bitbucketId + +ref: application.serverId > server.serverId + +ref: backup.destinationId > destination.destinationId + +ref: backup.postgresId > postgres.postgresId + +ref: backup.mariadbId > mariadb.mariadbId + +ref: backup.mysqlId > mysql.mysqlId + +ref: backup.mongoId > mongo.mongoId + +ref: git_provider.gitProviderId - bitbucket.gitProviderId + +ref: certificate.serverId > server.serverId + +ref: certificate.userId - user.id + +ref: compose.projectId > project.projectId + +ref: compose.customGitSSHKeyId > "ssh-key".sshKeyId + +ref: compose.githubId - github.githubId + +ref: compose.gitlabId - gitlab.gitlabId + +ref: compose.bitbucketId - bitbucket.bitbucketId + +ref: compose.serverId > server.serverId + +ref: deployment.applicationId > application.applicationId + +ref: deployment.composeId > compose.composeId + +ref: deployment.serverId > server.serverId + +ref: deployment.previewDeploymentId > preview_deployments.previewDeploymentId + +ref: destination.userId - user.id + +ref: domain.applicationId > application.applicationId + +ref: domain.composeId > compose.composeId + +ref: preview_deployments.domainId - domain.domainId + +ref: github.gitProviderId - git_provider.gitProviderId + +ref: gitlab.gitProviderId - git_provider.gitProviderId + +ref: git_provider.userId - user.id + +ref: mariadb.projectId > project.projectId + +ref: mariadb.serverId > server.serverId + +ref: mongo.projectId > project.projectId + +ref: mongo.serverId > server.serverId + +ref: mysql.projectId > project.projectId + +ref: mysql.serverId > server.serverId + +ref: notification.slackId - slack.slackId + +ref: notification.telegramId - telegram.telegramId + +ref: notification.discordId - discord.discordId + +ref: notification.emailId - email.emailId + +ref: notification.gotifyId - gotify.gotifyId + +ref: notification.userId - user.id + +ref: port.applicationId > application.applicationId + +ref: postgres.projectId > project.projectId + +ref: postgres.serverId > server.serverId + +ref: preview_deployments.applicationId > application.applicationId + +ref: project.userId - user.id + +ref: redirect.applicationId > application.applicationId + +ref: redis.projectId > project.projectId + +ref: redis.serverId > server.serverId + +ref: registry.userId - user.id + +ref: security.applicationId > application.applicationId + +ref: server.userId - user.id + +ref: server.sshKeyId > "ssh-key".sshKeyId + +ref: "ssh-key".userId - user.id \ No newline at end of file diff --git a/packages/server/src/db/schema/server.ts b/packages/server/src/db/schema/server.ts index c4ec6a09..26bb4632 100644 --- a/packages/server/src/db/schema/server.ts +++ b/packages/server/src/db/schema/server.ts @@ -10,8 +10,7 @@ import { import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; - -import { admins } from "./admin"; +import { organization } from "./account"; import { applications } from "./application"; import { certificates } from "./certificate"; import { compose } from "./compose"; @@ -40,12 +39,10 @@ export const server = pgTable("server", { .notNull() .$defaultFn(() => generateAppName("server")), enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false), - createdAt: text("createdAt") + createdAt: text("createdAt").notNull(), + organizationId: text("organizationId") .notNull() - .$defaultFn(() => new Date().toISOString()), - adminId: text("adminId") - .notNull() - .references(() => admins.adminId, { onDelete: "cascade" }), + .references(() => organization.id, { onDelete: "cascade" }), serverStatus: serverStatus("serverStatus").notNull().default("active"), command: text("command").notNull().default(""), sshKeyId: text("sshKeyId").references(() => sshKeys.sshKeyId, { @@ -100,10 +97,6 @@ export const server = pgTable("server", { }); export const serverRelations = relations(server, ({ one, many }) => ({ - admin: one(admins, { - fields: [server.adminId], - references: [admins.adminId], - }), deployments: many(deployments), sshKey: one(sshKeys, { fields: [server.sshKeyId], @@ -117,6 +110,10 @@ export const serverRelations = relations(server, ({ one, many }) => ({ mysql: many(mysql), postgres: many(postgres), certificates: many(certificates), + organization: one(organization, { + fields: [server.organizationId], + references: [organization.id], + }), })); const createSchema = createInsertSchema(server, { diff --git a/packages/server/src/db/schema/session.ts b/packages/server/src/db/schema/session.ts index 1b6d8cc1..f7c12dae 100644 --- a/packages/server/src/db/schema/session.ts +++ b/packages/server/src/db/schema/session.ts @@ -1,13 +1,18 @@ import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; -import { auth } from "./auth"; +import { users_temp } from "./user"; -export const sessionTable = pgTable("session", { +// OLD TABLE +export const session = pgTable("session_temp", { id: text("id").primaryKey(), + expiresAt: timestamp("expires_at").notNull(), + token: text("token").notNull().unique(), + createdAt: timestamp("created_at").notNull(), + updatedAt: timestamp("updated_at").notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), userId: text("user_id") .notNull() - .references(() => auth.id, { onDelete: "cascade" }), - expiresAt: timestamp("expires_at", { - withTimezone: true, - mode: "date", - }).notNull(), + .references(() => users_temp.id, { onDelete: "cascade" }), + impersonatedBy: text("impersonated_by"), + activeOrganizationId: text("active_organization_id"), }); diff --git a/packages/server/src/db/schema/source.ts b/packages/server/src/db/schema/source.ts deleted file mode 100644 index 6618ced7..00000000 --- a/packages/server/src/db/schema/source.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { pgTable, text } from "drizzle-orm/pg-core"; -import { createInsertSchema } from "drizzle-zod"; -import { nanoid } from "nanoid"; -import { z } from "zod"; - -export const source = pgTable("project", { - projectId: text("projectId") - .notNull() - .primaryKey() - .$defaultFn(() => nanoid()), - name: text("name").notNull(), - description: text("description"), - createdAt: text("createdAt") - .notNull() - .$defaultFn(() => new Date().toISOString()), -}); - -const createSchema = createInsertSchema(source, { - name: z.string().min(1), - description: z.string(), - projectId: z.string(), -}); - -export const apiCreate = createSchema.pick({ - name: true, - description: true, -}); diff --git a/packages/server/src/db/schema/ssh-key.ts b/packages/server/src/db/schema/ssh-key.ts index e4842851..8a66d6d9 100644 --- a/packages/server/src/db/schema/ssh-key.ts +++ b/packages/server/src/db/schema/ssh-key.ts @@ -3,7 +3,7 @@ import { pgTable, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { sshKeyCreate, sshKeyType } from "../validations"; -import { admins } from "./admin"; +import { organization } from "./account"; import { applications } from "./application"; import { compose } from "./compose"; import { server } from "./server"; @@ -21,18 +21,18 @@ export const sshKeys = pgTable("ssh-key", { .notNull() .$defaultFn(() => new Date().toISOString()), lastUsedAt: text("lastUsedAt"), - adminId: text("adminId").references(() => admins.adminId, { - onDelete: "cascade", - }), + organizationId: text("organizationId") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), }); export const sshKeysRelations = relations(sshKeys, ({ many, one }) => ({ applications: many(applications), compose: many(compose), servers: many(server), - admin: one(admins, { - fields: [sshKeys.adminId], - references: [admins.adminId], + organization: one(organization, { + fields: [sshKeys.organizationId], + references: [organization.id], }), })); @@ -48,7 +48,7 @@ export const apiCreateSshKey = createSchema description: true, privateKey: true, publicKey: true, - adminId: true, + organizationId: true, }) .merge(sshKeyCreate.pick({ privateKey: true })); diff --git a/packages/server/src/db/schema/user.ts b/packages/server/src/db/schema/user.ts index 735898f9..9307127a 100644 --- a/packages/server/src/db/schema/user.ts +++ b/packages/server/src/db/schema/user.ts @@ -1,10 +1,18 @@ -import { relations, sql } from "drizzle-orm"; -import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { relations } from "drizzle-orm"; +import { + boolean, + integer, + jsonb, + pgTable, + text, + timestamp, +} from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { admins } from "./admin"; -import { auth } from "./auth"; +import { account, organization, apikey } from "./account"; +import { projects } from "./project"; +import { certificateType } from "./shared"; /** * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same * database instance for multiple projects. @@ -12,75 +20,115 @@ import { auth } from "./auth"; * @see https://orm.drizzle.team/docs/goodies#multi-project-schema */ -export const users = pgTable("user", { - userId: text("userId") +// OLD TABLE + +// TEMP +export const users_temp = pgTable("user_temp", { + id: text("id") .notNull() .primaryKey() .$defaultFn(() => nanoid()), - - token: text("token").notNull(), + name: text("name").notNull().default(""), isRegistered: boolean("isRegistered").notNull().default(false), - expirationDate: timestamp("expirationDate", { - precision: 3, - mode: "string", - }).notNull(), - createdAt: text("createdAt") + expirationDate: text("expirationDate") .notNull() .$defaultFn(() => new Date().toISOString()), - canCreateProjects: boolean("canCreateProjects").notNull().default(false), - canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false), - canCreateServices: boolean("canCreateServices").notNull().default(false), - canDeleteProjects: boolean("canDeleteProjects").notNull().default(false), - canDeleteServices: boolean("canDeleteServices").notNull().default(false), - canAccessToDocker: boolean("canAccessToDocker").notNull().default(false), - canAccessToAPI: boolean("canAccessToAPI").notNull().default(false), - canAccessToGitProviders: boolean("canAccessToGitProviders") + createdAt2: text("createdAt") + .notNull() + .$defaultFn(() => new Date().toISOString()), + createdAt: timestamp("created_at").defaultNow(), + // Auth + twoFactorEnabled: boolean("two_factor_enabled"), + email: text("email").notNull().unique(), + emailVerified: boolean("email_verified").notNull(), + image: text("image"), + banned: boolean("banned"), + banReason: text("ban_reason"), + banExpires: timestamp("ban_expires"), + updatedAt: timestamp("updated_at").notNull(), + // Admin + serverIp: text("serverIp"), + certificateType: certificateType("certificateType").notNull().default("none"), + host: text("host"), + letsEncryptEmail: text("letsEncryptEmail"), + sshPrivateKey: text("sshPrivateKey"), + enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false), + enableLogRotation: boolean("enableLogRotation").notNull().default(false), + // Metrics + enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false), + metricsConfig: jsonb("metricsConfig") + .$type<{ + server: { + type: "Dokploy" | "Remote"; + refreshRate: number; + port: number; + token: string; + urlCallback: string; + retentionDays: number; + cronJob: string; + thresholds: { + cpu: number; + memory: number; + }; + }; + containers: { + refreshRate: number; + services: { + include: string[]; + exclude: string[]; + }; + }; + }>() + .notNull() + .default({ + server: { + type: "Dokploy", + refreshRate: 60, + port: 4500, + token: "", + retentionDays: 2, + cronJob: "", + urlCallback: "", + thresholds: { + cpu: 0, + memory: 0, + }, + }, + containers: { + refreshRate: 60, + services: { + include: [], + exclude: [], + }, + }, + }), + cleanupCacheApplications: boolean("cleanupCacheApplications") .notNull() .default(false), - canAccessToTraefikFiles: boolean("canAccessToTraefikFiles") + cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews") .notNull() .default(false), - accessedProjects: text("accesedProjects") - .array() + cleanupCacheOnCompose: boolean("cleanupCacheOnCompose") .notNull() - .default(sql`ARRAY[]::text[]`), - accessedServices: text("accesedServices") - .array() - .notNull() - .default(sql`ARRAY[]::text[]`), - adminId: text("adminId") - .notNull() - .references(() => admins.adminId, { onDelete: "cascade" }), - authId: text("authId") - .notNull() - .references(() => auth.id, { onDelete: "cascade" }), + .default(false), + stripeCustomerId: text("stripeCustomerId"), + stripeSubscriptionId: text("stripeSubscriptionId"), + serversQuantity: integer("serversQuantity").notNull().default(0), }); -export const usersRelations = relations(users, ({ one }) => ({ - auth: one(auth, { - fields: [users.authId], - references: [auth.id], - }), - admin: one(admins, { - fields: [users.adminId], - references: [admins.adminId], +export const usersRelations = relations(users_temp, ({ one, many }) => ({ + account: one(account, { + fields: [users_temp.id], + references: [account.userId], }), + organizations: many(organization), + projects: many(projects), + apiKeys: many(apikey), })); -const createSchema = createInsertSchema(users, { - userId: z.string().min(1), - authId: z.string().min(1), - token: z.string().min(1), +const createSchema = createInsertSchema(users_temp, { + id: z.string().min(1), isRegistered: z.boolean().optional(), - adminId: z.string(), - accessedProjects: z.array(z.string()).optional(), - accessedServices: z.array(z.string()).optional(), - canCreateProjects: z.boolean().optional(), - canCreateServices: z.boolean().optional(), - canDeleteProjects: z.boolean().optional(), - canDeleteServices: z.boolean().optional(), - canAccessToDocker: z.boolean().optional(), - canAccessToTraefikFiles: z.boolean().optional(), }); export const apiCreateUserInvitation = createSchema.pick({}).extend({ @@ -89,41 +137,172 @@ export const apiCreateUserInvitation = createSchema.pick({}).extend({ export const apiRemoveUser = createSchema .pick({ - authId: true, + id: true, }) .required(); export const apiFindOneToken = createSchema - .pick({ - token: true, - }) - .required(); + .pick({}) + .required() + .extend({ + token: z.string().min(1), + }); export const apiAssignPermissions = createSchema .pick({ - userId: true, - canCreateProjects: true, - canCreateServices: true, - canDeleteProjects: true, - canDeleteServices: true, - accessedProjects: true, - accessedServices: true, - canAccessToTraefikFiles: true, - canAccessToDocker: true, - canAccessToAPI: true, - canAccessToSSHKeys: true, - canAccessToGitProviders: true, + id: true, + // canCreateProjects: true, + // canCreateServices: true, + // canDeleteProjects: true, + // canDeleteServices: true, + // accessedProjects: true, + // accessedServices: true, + // canAccessToTraefikFiles: true, + // canAccessToDocker: true, + // canAccessToAPI: true, + // canAccessToSSHKeys: true, + // canAccessToGitProviders: true, + }) + .extend({ + accessedProjects: z.array(z.string()).optional(), + accessedServices: z.array(z.string()).optional(), + canCreateProjects: z.boolean().optional(), + canCreateServices: z.boolean().optional(), + canDeleteProjects: z.boolean().optional(), + canDeleteServices: z.boolean().optional(), + canAccessToDocker: z.boolean().optional(), + canAccessToTraefikFiles: z.boolean().optional(), + canAccessToAPI: z.boolean().optional(), + canAccessToSSHKeys: z.boolean().optional(), + canAccessToGitProviders: z.boolean().optional(), }) .required(); export const apiFindOneUser = createSchema .pick({ - userId: true, + id: true, }) .required(); export const apiFindOneUserByAuth = createSchema .pick({ - authId: true, + // authId: true, }) .required(); +export const apiSaveSSHKey = createSchema + .pick({ + sshPrivateKey: true, + }) + .required(); + +export const apiAssignDomain = createSchema + .pick({ + host: true, + certificateType: true, + letsEncryptEmail: true, + }) + .required() + .partial({ + letsEncryptEmail: true, + }); + +export const apiUpdateDockerCleanup = createSchema + .pick({ + enableDockerCleanup: true, + }) + .required() + .extend({ + serverId: z.string().optional(), + }); + +export const apiTraefikConfig = z.object({ + traefikConfig: z.string().min(1), +}); + +export const apiModifyTraefikConfig = z.object({ + path: z.string().min(1), + traefikConfig: z.string().min(1), + serverId: z.string().optional(), +}); +export const apiReadTraefikConfig = z.object({ + path: z.string().min(1), + serverId: z.string().optional(), +}); + +export const apiEnableDashboard = z.object({ + enableDashboard: z.boolean().optional(), + serverId: z.string().optional(), +}); + +export const apiServerSchema = z + .object({ + serverId: z.string().optional(), + }) + .optional(); + +export const apiReadStatsLogs = z.object({ + page: z + .object({ + pageIndex: z.number(), + pageSize: z.number(), + }) + .optional(), + status: z.string().array().optional(), + search: z.string().optional(), + sort: z.object({ id: z.string(), desc: z.boolean() }).optional(), +}); + +export const apiUpdateWebServerMonitoring = z.object({ + metricsConfig: z + .object({ + server: z.object({ + refreshRate: z.number().min(2), + port: z.number().min(1), + token: z.string(), + urlCallback: z.string().url(), + retentionDays: z.number().min(1), + cronJob: z.string().min(1), + thresholds: z.object({ + cpu: z.number().min(0), + memory: z.number().min(0), + }), + }), + containers: z.object({ + refreshRate: z.number().min(2), + services: z.object({ + include: z.array(z.string()).optional(), + exclude: z.array(z.string()).optional(), + }), + }), + }) + .required(), +}); + +export const apiUpdateUser = createSchema.partial().extend({ + password: z.string().optional(), + currentPassword: z.string().optional(), + metricsConfig: z + .object({ + server: z.object({ + type: z.enum(["Dokploy", "Remote"]), + refreshRate: z.number(), + port: z.number(), + token: z.string(), + urlCallback: z.string(), + retentionDays: z.number(), + cronJob: z.string(), + thresholds: z.object({ + cpu: z.number(), + memory: z.number(), + }), + }), + containers: z.object({ + refreshRate: z.number(), + services: z.object({ + include: z.array(z.string()), + exclude: z.array(z.string()), + }), + }), + }) + .optional(), +}); diff --git a/packages/server/src/emails/emails/build-failed.tsx b/packages/server/src/emails/emails/build-failed.tsx index b3d99919..79e7b718 100644 --- a/packages/server/src/emails/emails/build-failed.tsx +++ b/packages/server/src/emails/emails/build-failed.tsx @@ -12,7 +12,6 @@ import { Tailwind, Text, } from "@react-email/components"; -import * as React from "react"; export type TemplateProps = { projectName: string; diff --git a/packages/server/src/emails/emails/build-success.tsx b/packages/server/src/emails/emails/build-success.tsx index eadf7c44..d9e500ab 100644 --- a/packages/server/src/emails/emails/build-success.tsx +++ b/packages/server/src/emails/emails/build-success.tsx @@ -12,7 +12,6 @@ import { Tailwind, Text, } from "@react-email/components"; -import * as React from "react"; export type TemplateProps = { projectName: string; diff --git a/packages/server/src/emails/emails/database-backup.tsx b/packages/server/src/emails/emails/database-backup.tsx index 2bdf944c..754d4d98 100644 --- a/packages/server/src/emails/emails/database-backup.tsx +++ b/packages/server/src/emails/emails/database-backup.tsx @@ -10,7 +10,6 @@ import { Tailwind, Text, } from "@react-email/components"; -import * as React from "react"; export type TemplateProps = { projectName: string; diff --git a/packages/server/src/emails/emails/docker-cleanup.tsx b/packages/server/src/emails/emails/docker-cleanup.tsx index 05d93ed7..985406ae 100644 --- a/packages/server/src/emails/emails/docker-cleanup.tsx +++ b/packages/server/src/emails/emails/docker-cleanup.tsx @@ -1,6 +1,5 @@ import { Body, - Button, Container, Head, Heading, @@ -11,7 +10,6 @@ import { Tailwind, Text, } from "@react-email/components"; -import * as React from "react"; export type TemplateProps = { message: string; diff --git a/packages/server/src/emails/emails/dokploy-restart.tsx b/packages/server/src/emails/emails/dokploy-restart.tsx index 1ad3d600..db4edd69 100644 --- a/packages/server/src/emails/emails/dokploy-restart.tsx +++ b/packages/server/src/emails/emails/dokploy-restart.tsx @@ -10,7 +10,6 @@ import { Tailwind, Text, } from "@react-email/components"; -import * as React from "react"; export type TemplateProps = { date: string; diff --git a/packages/server/src/emails/emails/notion-magic-link.tsx b/packages/server/src/emails/emails/notion-magic-link.tsx index b2286c34..f4071ce0 100644 --- a/packages/server/src/emails/emails/notion-magic-link.tsx +++ b/packages/server/src/emails/emails/notion-magic-link.tsx @@ -9,7 +9,6 @@ import { Preview, Text, } from "@react-email/components"; -import * as React from "react"; interface NotionMagicLinkEmailProps { loginCode?: string; diff --git a/packages/server/src/emails/emails/plaid-verify-identity.tsx b/packages/server/src/emails/emails/plaid-verify-identity.tsx index 650ab486..88cf893d 100644 --- a/packages/server/src/emails/emails/plaid-verify-identity.tsx +++ b/packages/server/src/emails/emails/plaid-verify-identity.tsx @@ -9,7 +9,6 @@ import { Section, Text, } from "@react-email/components"; -import * as React from "react"; interface PlaidVerifyIdentityEmailProps { validationCode?: string; diff --git a/packages/server/src/emails/emails/stripe-welcome.tsx b/packages/server/src/emails/emails/stripe-welcome.tsx index 9377853b..dbf02ea0 100644 --- a/packages/server/src/emails/emails/stripe-welcome.tsx +++ b/packages/server/src/emails/emails/stripe-welcome.tsx @@ -11,7 +11,6 @@ import { Section, Text, } from "@react-email/components"; -import * as React from "react"; const baseUrl = process.env.VERCEL_URL!; diff --git a/packages/server/src/emails/emails/vercel-invite-user.tsx b/packages/server/src/emails/emails/vercel-invite-user.tsx index 53b31987..79f50cd7 100644 --- a/packages/server/src/emails/emails/vercel-invite-user.tsx +++ b/packages/server/src/emails/emails/vercel-invite-user.tsx @@ -15,7 +15,6 @@ import { Tailwind, Text, } from "@react-email/components"; -import * as React from "react"; interface VercelInviteUserEmailProps { username?: string; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 71549a38..f74b8d9d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,7 +1,4 @@ -export * from "./auth/auth"; -export * from "./auth/token"; export * from "./auth/random-password"; -// export * from "./db"; export * from "./services/admin"; export * from "./services/user"; export * from "./services/project"; @@ -30,7 +27,6 @@ export * from "./services/ssh-key"; export * from "./services/git-provider"; export * from "./services/bitbucket"; export * from "./services/github"; -export * from "./services/auth"; export * from "./services/gitlab"; export * from "./services/server"; export * from "./services/application"; @@ -118,3 +114,5 @@ export * from "./monitoring/utils"; export * from "./db/validations/domain"; export * from "./db/validations/index"; export * from "./utils/gpu-setup"; + +export * from "./lib/auth"; diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts new file mode 100644 index 00000000..1efa1730 --- /dev/null +++ b/packages/server/src/lib/auth.ts @@ -0,0 +1,304 @@ +import type { IncomingMessage } from "node:http"; +import * as bcrypt from "bcrypt"; +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { organization, twoFactor, apiKey } from "better-auth/plugins"; +import { and, desc, eq } from "drizzle-orm"; +import { db } from "../db"; +import * as schema from "../db/schema"; +import { sendEmail } from "../verification/send-verification-email"; +import { IS_CLOUD } from "../constants"; + +const { handler, api } = betterAuth({ + database: drizzleAdapter(db, { + provider: "pg", + schema: schema, + }), + logger: { + disabled: process.env.NODE_ENV === "production", + }, + appName: "Dokploy", + socialProviders: { + github: { + clientId: process.env.GITHUB_CLIENT_ID as string, + clientSecret: process.env.GITHUB_CLIENT_SECRET as string, + }, + google: { + clientId: process.env.GOOGLE_CLIENT_ID as string, + clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, + }, + }, + emailVerification: { + sendOnSignUp: true, + autoSignInAfterVerification: true, + sendVerificationEmail: async ({ user, url }) => { + if (IS_CLOUD) { + await sendEmail({ + email: user.email, + subject: "Verify your email", + text: ` +

Click the link to verify your email: Verify Email

+ `, + }); + } + }, + }, + emailAndPassword: { + enabled: true, + autoSignIn: !IS_CLOUD, + requireEmailVerification: IS_CLOUD, + password: { + async hash(password) { + return bcrypt.hashSync(password, 10); + }, + async verify({ hash, password }) { + return bcrypt.compareSync(password, hash); + }, + }, + sendResetPassword: async ({ user, url }) => { + await sendEmail({ + email: user.email, + subject: "Reset your password", + text: ` +

Click the link to reset your password: Reset Password

+ `, + }); + }, + }, + databaseHooks: { + user: { + create: { + after: async (user) => { + const isAdminPresent = await db.query.member.findFirst({ + where: eq(schema.member.role, "owner"), + }); + + if (IS_CLOUD || !isAdminPresent) { + await db.transaction(async (tx) => { + const organization = await tx + .insert(schema.organization) + .values({ + name: "My Organization", + ownerId: user.id, + createdAt: new Date(), + }) + .returning() + .then((res) => res[0]); + + await tx.insert(schema.member).values({ + userId: user.id, + organizationId: organization?.id || "", + role: "owner", + createdAt: new Date(), + }); + }); + } + }, + }, + }, + session: { + create: { + before: async (session) => { + const member = await db.query.member.findFirst({ + where: eq(schema.member.userId, session.userId), + orderBy: desc(schema.member.createdAt), + with: { + organization: true, + }, + }); + + return { + data: { + ...session, + activeOrganizationId: member?.organization.id, + }, + }; + }, + }, + }, + }, + user: { + modelName: "users_temp", + additionalFields: { + role: { + type: "string", + // required: true, + input: false, + }, + ownerId: { + type: "string", + // required: true, + input: false, + }, + }, + }, + + plugins: [ + apiKey({ + enableMetadata: true, + }), + twoFactor(), + organization({ + async sendInvitationEmail(data, _request) { + if (IS_CLOUD) { + const host = + process.env.NODE_ENV === "development" + ? "http://localhost:3000" + : "https://dokploy.com"; + const inviteLink = `${host}/invitation?token=${data.id}`; + + await sendEmail({ + email: data.email, + subject: "Invitation to join organization", + text: ` +

You are invited to join ${data.organization.name} on Dokploy. Click the link to accept the invitation: Accept Invitation

+ `, + }); + } + }, + }), + ], +}); + +export const auth = { + handler, + createApiKey: api.createApiKey, +}; + +export const validateRequest = async (request: IncomingMessage) => { + const apiKey = request.headers["x-api-key"] as string; + if (apiKey) { + try { + const { valid, key, error } = await api.verifyApiKey({ + body: { + key: apiKey, + }, + }); + + if (error) { + throw new Error(error.message || "Error verifying API key"); + } + if (!valid || !key) { + return { + session: null, + user: null, + }; + } + + const apiKeyRecord = await db.query.apikey.findFirst({ + where: eq(schema.apikey.id, key.id), + with: { + user: true, + }, + }); + + if (!apiKeyRecord) { + return { + session: null, + user: null, + }; + } + + const organizationId = JSON.parse( + apiKeyRecord.metadata || "{}", + ).organizationId; + + if (!organizationId) { + return { + session: null, + user: null, + }; + } + + const member = await db.query.member.findFirst({ + where: and( + eq(schema.member.userId, apiKeyRecord.user.id), + eq(schema.member.organizationId, organizationId), + ), + with: { + organization: true, + }, + }); + + const { + id, + name, + email, + emailVerified, + image, + createdAt, + updatedAt, + twoFactorEnabled, + } = apiKeyRecord.user; + + const mockSession = { + session: { + user: { + id: apiKeyRecord.user.id, + email: apiKeyRecord.user.email, + name: apiKeyRecord.user.name, + }, + activeOrganizationId: organizationId || "", + }, + user: { + id, + name, + email, + emailVerified, + image, + createdAt, + updatedAt, + twoFactorEnabled, + role: member?.role || "member", + ownerId: member?.organization.ownerId || apiKeyRecord.user.id, + }, + }; + + return mockSession; + } catch (error) { + console.error("Error verifying API key", error); + return { + session: null, + user: null, + }; + } + } + + // If no API key, proceed with normal session validation + const session = await api.getSession({ + headers: new Headers({ + cookie: request.headers.cookie || "", + }), + }); + + if (!session?.session || !session.user) { + return { + session: null, + user: null, + }; + } + + if (session?.user) { + const member = await db.query.member.findFirst({ + where: and( + eq(schema.member.userId, session.user.id), + eq( + schema.member.organizationId, + session.session.activeOrganizationId || "", + ), + ), + with: { + organization: true, + }, + }); + + session.user.role = member?.role || "member"; + if (member) { + session.user.ownerId = member.organization.ownerId; + } else { + session.user.ownerId = session.user.id; + } + } + + return session; +}; diff --git a/packages/server/src/monitoring/utils.ts b/packages/server/src/monitoring/utils.ts index 561cc835..147ade0a 100644 --- a/packages/server/src/monitoring/utils.ts +++ b/packages/server/src/monitoring/utils.ts @@ -73,7 +73,7 @@ export const readStatsFile = async ( const filePath = `${MONITORING_PATH}/${appName}/${statType}.json`; const data = await promises.readFile(filePath, "utf-8"); return JSON.parse(data); - } catch (error) { + } catch (_error) { return []; } }; @@ -108,7 +108,7 @@ export const readLastValueStatsFile = async ( const data = await promises.readFile(filePath, "utf-8"); const stats = JSON.parse(data); return stats[stats.length - 1] || null; - } catch (error) { + } catch (_error) { return null; } }; diff --git a/packages/server/src/services/admin.ts b/packages/server/src/services/admin.ts index b6d9c7fb..3509868b 100644 --- a/packages/server/src/services/admin.ts +++ b/packages/server/src/services/admin.ts @@ -1,124 +1,56 @@ -import { randomBytes } from "node:crypto"; import { db } from "@dokploy/server/db"; import { - admins, - type apiCreateUserInvitation, - auth, - users, + invitation, + member, + organization, + users_temp, } from "@dokploy/server/db/schema"; import { TRPCError } from "@trpc/server"; -import * as bcrypt from "bcrypt"; import { eq } from "drizzle-orm"; import { IS_CLOUD } from "../constants"; -export type Admin = typeof admins.$inferSelect; -export const createInvitation = async ( - input: typeof apiCreateUserInvitation._type, - adminId: string, -) => { - await db.transaction(async (tx) => { - const result = await tx - .insert(auth) - .values({ - email: input.email.toLowerCase(), - rol: "user", - password: bcrypt.hashSync("01231203012312", 10), - }) - .returning() - .then((res) => res[0]); - - if (!result) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the user", - }); - } - const expiresIn24Hours = new Date(); - expiresIn24Hours.setDate(expiresIn24Hours.getDate() + 1); - const token = randomBytes(32).toString("hex"); - await tx - .insert(users) - .values({ - adminId: adminId, - authId: result.id, - token, - expirationDate: expiresIn24Hours.toISOString(), - }) - .returning(); +export const findUserById = async (userId: string) => { + const user = await db.query.users_temp.findFirst({ + where: eq(users_temp.id, userId), + // with: { + // account: true, + // }, }); -}; - -export const findAdminById = async (adminId: string) => { - const admin = await db.query.admins.findFirst({ - where: eq(admins.adminId, adminId), - }); - if (!admin) { + if (!user) { throw new TRPCError({ code: "NOT_FOUND", - message: "Admin not found", + message: "User not found", }); } - return admin; + return user; }; -export const updateAdmin = async ( - authId: string, - adminData: Partial, -) => { - const admin = await db - .update(admins) - .set({ - ...adminData, - }) - .where(eq(admins.authId, authId)) - .returning() - .then((res) => res[0]); - - return admin; -}; - -export const updateAdminById = async ( - adminId: string, - adminData: Partial, -) => { - const admin = await db - .update(admins) - .set({ - ...adminData, - }) - .where(eq(admins.adminId, adminId)) - .returning() - .then((res) => res[0]); - - return admin; +export const findOrganizationById = async (organizationId: string) => { + const organizationResult = await db.query.organization.findFirst({ + where: eq(organization.id, organizationId), + }); + return organizationResult; }; export const isAdminPresent = async () => { - const admin = await db.query.admins.findFirst(); + const admin = await db.query.member.findFirst({ + where: eq(member.role, "owner"), + }); + if (!admin) { return false; } return true; }; -export const findAdminByAuthId = async (authId: string) => { - const admin = await db.query.admins.findFirst({ - where: eq(admins.authId, authId), +export const findAdmin = async () => { + const admin = await db.query.member.findFirst({ + where: eq(member.role, "owner"), with: { - users: true, + user: true, }, }); - if (!admin) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Admin not found", - }); - } - return admin; -}; -export const findAdmin = async () => { - const admin = await db.query.admins.findFirst({}); if (!admin) { throw new TRPCError({ code: "NOT_FOUND", @@ -129,14 +61,15 @@ export const findAdmin = async () => { }; export const getUserByToken = async (token: string) => { - const user = await db.query.users.findFirst({ - where: eq(users.token, token), - with: { - auth: { - columns: { - password: false, - }, - }, + const user = await db.query.invitation.findFirst({ + where: eq(invitation.id, token), + columns: { + id: true, + email: true, + status: true, + expiresAt: true, + role: true, + inviterId: true, }, }); @@ -146,34 +79,23 @@ export const getUserByToken = async (token: string) => { message: "Invitation not found", }); } + + const userAlreadyExists = await db.query.users_temp.findFirst({ + where: eq(users_temp.email, user?.email || ""), + }); + + const { expiresAt, ...rest } = user; return { - ...user, - isExpired: user.isRegistered, + ...rest, + isExpired: user.expiresAt < new Date(), + userAlreadyExists: !!userAlreadyExists, }; }; -export const removeUserByAuthId = async (authId: string) => { +export const removeUserById = async (userId: string) => { await db - .delete(auth) - .where(eq(auth.id, authId)) - .returning() - .then((res) => res[0]); -}; - -export const removeAdminByAuthId = async (authId: string) => { - const admin = await findAdminByAuthId(authId); - if (!admin) return null; - - // First delete all associated users - const users = admin.users; - - for (const user of users) { - await removeUserByAuthId(user.authId); - } - // Then delete the auth record which will cascade delete the admin - return await db - .delete(auth) - .where(eq(auth.id, authId)) + .delete(users_temp) + .where(eq(users_temp.id, userId)) .returning() .then((res) => res[0]); }; @@ -184,8 +106,8 @@ export const getDokployUrl = async () => { } const admin = await findAdmin(); - if (admin.host) { - return `https://${admin.host}`; + if (admin.user.host) { + return `https://${admin.user.host}`; } - return `http://${admin.serverIp}:${process.env.PORT}`; + return `http://${admin.user.serverIp}:${process.env.PORT}`; }; diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index c102e8ed..425a6adb 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -4,7 +4,6 @@ import { type apiCreateApplication, applications, buildAppName, - cleanAppName, } from "@dokploy/server/db/schema"; import { getAdvancedStats } from "@dokploy/server/monitoring/utils"; import { @@ -28,7 +27,6 @@ import { getCustomGitCloneCommand, } from "@dokploy/server/utils/providers/git"; import { - authGithub, cloneGithubRepository, getGithubCloneCommand, } from "@dokploy/server/utils/providers/github"; @@ -40,7 +38,7 @@ import { createTraefikConfig } from "@dokploy/server/utils/traefik/application"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { encodeBase64 } from "../utils/docker/utils"; -import { findAdminById, getDokployUrl } from "./admin"; +import { getDokployUrl } from "./admin"; import { createDeployment, createDeploymentPreview, @@ -58,7 +56,6 @@ import { updatePreviewDeployment, } from "./preview-deployment"; import { validUniqueServerAppName } from "./project"; -import { cleanupFullDocker } from "./settings"; export type Application = typeof applications.$inferSelect; export const createApplication = async ( @@ -185,11 +182,11 @@ export const deployApplication = async ({ }); try { - const admin = await findAdminById(application.project.adminId); + // const admin = await findUserById(application.project.userId); - if (admin.cleanupCacheApplications) { - await cleanupFullDocker(application?.serverId); - } + // if (admin.cleanupCacheApplications) { + // await cleanupFullDocker(application?.serverId); + // } if (application.sourceType === "github") { await cloneGithubRepository({ @@ -220,7 +217,7 @@ export const deployApplication = async ({ applicationName: application.name, applicationType: "application", buildLink, - adminId: application.project.adminId, + organizationId: application.project.organizationId, domains: application.domains, }); } catch (error) { @@ -233,7 +230,7 @@ export const deployApplication = async ({ // @ts-ignore errorMessage: error?.message || "Error building", buildLink, - adminId: application.project.adminId, + organizationId: application.project.organizationId, }); throw error; @@ -260,11 +257,11 @@ export const rebuildApplication = async ({ }); try { - const admin = await findAdminById(application.project.adminId); + // const admin = await findUserById(application.project.userId); - if (admin.cleanupCacheApplications) { - await cleanupFullDocker(application?.serverId); - } + // if (admin.cleanupCacheApplications) { + // await cleanupFullDocker(application?.serverId); + // } if (application.sourceType === "github") { await buildApplication(application, deployment.logPath); } else if (application.sourceType === "gitlab") { @@ -309,11 +306,11 @@ export const deployRemoteApplication = async ({ try { if (application.serverId) { - const admin = await findAdminById(application.project.adminId); + // const admin = await findUserById(application.project.userId); - if (admin.cleanupCacheApplications) { - await cleanupFullDocker(application?.serverId); - } + // if (admin.cleanupCacheApplications) { + // await cleanupFullDocker(application?.serverId); + // } let command = "set -e;"; if (application.sourceType === "github") { command += await getGithubCloneCommand({ @@ -352,7 +349,7 @@ export const deployRemoteApplication = async ({ applicationName: application.name, applicationType: "application", buildLink, - adminId: application.project.adminId, + organizationId: application.project.organizationId, domains: application.domains, }); } catch (error) { @@ -376,7 +373,7 @@ export const deployRemoteApplication = async ({ // @ts-ignore errorMessage: error?.message || "Error building", buildLink, - adminId: application.project.adminId, + organizationId: application.project.organizationId, }); throw error; @@ -454,11 +451,11 @@ export const deployPreviewApplication = async ({ application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`; application.buildArgs = application.previewBuildArgs; - const admin = await findAdminById(application.project.adminId); + // const admin = await findUserById(application.project.userId); - if (admin.cleanupCacheOnPreviews) { - await cleanupFullDocker(application?.serverId); - } + // if (admin.cleanupCacheOnPreviews) { + // await cleanupFullDocker(application?.serverId); + // } if (application.sourceType === "github") { await cloneGithubRepository({ @@ -568,11 +565,11 @@ export const deployRemotePreviewApplication = async ({ application.buildArgs = application.previewBuildArgs; if (application.serverId) { - const admin = await findAdminById(application.project.adminId); + // const admin = await findUserById(application.project.userId); - if (admin.cleanupCacheOnPreviews) { - await cleanupFullDocker(application?.serverId); - } + // if (admin.cleanupCacheOnPreviews) { + // await cleanupFullDocker(application?.serverId); + // } let command = "set -e;"; if (application.sourceType === "github") { command += await getGithubCloneCommand({ @@ -637,11 +634,11 @@ export const rebuildRemoteApplication = async ({ try { if (application.serverId) { - const admin = await findAdminById(application.project.adminId); + // const admin = await findUserById(application.project.userId); - if (admin.cleanupCacheApplications) { - await cleanupFullDocker(application?.serverId); - } + // if (admin.cleanupCacheApplications) { + // await cleanupFullDocker(application?.serverId); + // } if (application.sourceType !== "docker") { let command = "set -e;"; command += getBuildCommand(application, deployment.logPath); diff --git a/packages/server/src/services/auth.ts b/packages/server/src/services/auth.ts deleted file mode 100644 index 65a01c41..00000000 --- a/packages/server/src/services/auth.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { randomBytes } from "node:crypto"; -import { db } from "@dokploy/server/db"; -import { - admins, - type apiCreateAdmin, - type apiCreateUser, - auth, - users, -} from "@dokploy/server/db/schema"; -import { getPublicIpWithFallback } from "@dokploy/server/wss/utils"; -import { TRPCError } from "@trpc/server"; -import * as bcrypt from "bcrypt"; -import { eq } from "drizzle-orm"; -import encode from "hi-base32"; -import { TOTP } from "otpauth"; -import QRCode from "qrcode"; -import { IS_CLOUD } from "../constants"; - -export type Auth = typeof auth.$inferSelect; - -export const createAdmin = async (input: typeof apiCreateAdmin._type) => { - return await db.transaction(async (tx) => { - const hashedPassword = bcrypt.hashSync(input.password, 10); - const newAuth = await tx - .insert(auth) - .values({ - email: input.email.toLowerCase(), - password: hashedPassword, - rol: "admin", - }) - .returning() - .then((res) => res[0]); - - if (!newAuth) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the user", - }); - } - - await tx - .insert(admins) - .values({ - authId: newAuth.id, - ...(!IS_CLOUD && { - serverIp: - process.env.ADVERTISE_ADDR || (await getPublicIpWithFallback()), - }), - }) - .returning(); - - return newAuth; - }); -}; - -export const createUser = async (input: typeof apiCreateUser._type) => { - return await db.transaction(async (tx) => { - const hashedPassword = bcrypt.hashSync(input.password, 10); - const res = await tx - .update(auth) - .set({ - password: hashedPassword, - }) - .where(eq(auth.id, input.id)) - .returning() - .then((res) => res[0]); - - if (!res) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the user", - }); - } - - const user = await tx - .update(users) - .set({ - isRegistered: true, - expirationDate: undefined, - }) - .where(eq(users.token, input.token)) - .returning() - .then((res) => res[0]); - - return user; - }); -}; - -export const findAuthByEmail = async (email: string) => { - const result = await db.query.auth.findFirst({ - where: eq(auth.email, email), - }); - if (!result) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "User not found", - }); - } - return result; -}; - -export const findAuthById = async (authId: string) => { - const result = await db.query.auth.findFirst({ - where: eq(auth.id, authId), - columns: { - password: false, - }, - }); - if (!result) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Auth not found", - }); - } - return result; -}; - -export const updateAuthById = async ( - authId: string, - authData: Partial, -) => { - const result = await db - .update(auth) - .set({ - ...authData, - }) - .where(eq(auth.id, authId)) - .returning(); - - return result[0]; -}; - -export const generate2FASecret = async (authId: string) => { - const auth = await findAuthById(authId); - - const base32_secret = generateBase32Secret(); - - const totp = new TOTP({ - issuer: "Dokploy", - label: `${auth?.email}`, - algorithm: "SHA1", - digits: 6, - secret: base32_secret, - }); - - const otpauth_url = totp.toString(); - - const qrUrl = await QRCode.toDataURL(otpauth_url); - - return { - qrCodeUrl: qrUrl, - secret: base32_secret, - }; -}; - -export const verify2FA = async ( - auth: Omit, - secret: string, - pin: string, -) => { - const totp = new TOTP({ - issuer: "Dokploy", - label: `${auth?.email}`, - algorithm: "SHA1", - digits: 6, - secret: secret, - }); - - const delta = totp.validate({ token: pin }); - - if (delta === null) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Invalid 2FA code", - }); - } - return auth; -}; - -const generateBase32Secret = () => { - const buffer = randomBytes(15); - const base32 = encode.encode(buffer).replace(/=/g, "").substring(0, 24); - return base32; -}; diff --git a/packages/server/src/services/backup.ts b/packages/server/src/services/backup.ts index ef3d0446..32705786 100644 --- a/packages/server/src/services/backup.ts +++ b/packages/server/src/services/backup.ts @@ -2,8 +2,6 @@ import { db } from "@dokploy/server/db"; import { type apiCreateBackup, backups } from "@dokploy/server/db/schema"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; -import { IS_CLOUD } from "../constants"; -import { removeScheduleBackup, scheduleBackup } from "../utils/backups/utils"; export type Backup = typeof backups.$inferSelect; diff --git a/packages/server/src/services/bitbucket.ts b/packages/server/src/services/bitbucket.ts index 21807156..7b5be7d6 100644 --- a/packages/server/src/services/bitbucket.ts +++ b/packages/server/src/services/bitbucket.ts @@ -12,14 +12,14 @@ export type Bitbucket = typeof bitbucket.$inferSelect; export const createBitbucket = async ( input: typeof apiCreateBitbucket._type, - adminId: string, + organizationId: string, ) => { return await db.transaction(async (tx) => { const newGitProvider = await tx .insert(gitProvider) .values({ providerType: "bitbucket", - adminId: adminId, + organizationId: organizationId, name: input.name, }) .returning() @@ -74,12 +74,12 @@ export const updateBitbucket = async ( .where(eq(bitbucket.bitbucketId, bitbucketId)) .returning(); - if (input.name || input.adminId) { + if (input.name || input.organizationId) { await tx .update(gitProvider) .set({ name: input.name, - adminId: input.adminId, + organizationId: input.organizationId, }) .where(eq(gitProvider.gitProviderId, input.gitProviderId)) .returning(); diff --git a/packages/server/src/services/certificate.ts b/packages/server/src/services/certificate.ts index 23177862..f59f1c2a 100644 --- a/packages/server/src/services/certificate.ts +++ b/packages/server/src/services/certificate.ts @@ -33,13 +33,13 @@ export const findCertificateById = async (certificateId: string) => { export const createCertificate = async ( certificateData: z.infer, - adminId: string, + organizationId: string, ) => { const certificate = await db .insert(certificates) .values({ ...certificateData, - adminId: adminId, + organizationId: organizationId, }) .returning(); diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 39bf423f..a3ebc26c 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -44,10 +44,9 @@ import { import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { encodeBase64 } from "../utils/docker/utils"; -import { findAdminById, getDokployUrl } from "./admin"; +import { getDokployUrl } from "./admin"; import { createDeploymentCompose, updateDeploymentStatus } from "./deployment"; import { validUniqueServerAppName } from "./project"; -import { cleanupFullDocker } from "./settings"; export type Compose = typeof compose.$inferSelect; @@ -217,10 +216,10 @@ export const deployCompose = async ({ }); try { - const admin = await findAdminById(compose.project.adminId); - if (admin.cleanupCacheOnCompose) { - await cleanupFullDocker(compose?.serverId); - } + // const admin = await findUserById(compose.project.userId); + // if (admin.cleanupCacheOnCompose) { + // await cleanupFullDocker(compose?.serverId); + // } if (compose.sourceType === "github") { await cloneGithubRepository({ ...compose, @@ -247,7 +246,7 @@ export const deployCompose = async ({ applicationName: compose.name, applicationType: "compose", buildLink, - adminId: compose.project.adminId, + organizationId: compose.project.organizationId, domains: compose.domains, }); } catch (error) { @@ -262,7 +261,7 @@ export const deployCompose = async ({ // @ts-ignore errorMessage: error?.message || "Error building", buildLink, - adminId: compose.project.adminId, + organizationId: compose.project.organizationId, }); throw error; } @@ -286,10 +285,10 @@ export const rebuildCompose = async ({ }); try { - const admin = await findAdminById(compose.project.adminId); - if (admin.cleanupCacheOnCompose) { - await cleanupFullDocker(compose?.serverId); - } + // const admin = await findUserById(compose.project.userId); + // if (admin.cleanupCacheOnCompose) { + // await cleanupFullDocker(compose?.serverId); + // } if (compose.serverId) { await getBuildComposeCommand(compose, deployment.logPath); } else { @@ -332,10 +331,10 @@ export const deployRemoteCompose = async ({ }); try { if (compose.serverId) { - const admin = await findAdminById(compose.project.adminId); - if (admin.cleanupCacheOnCompose) { - await cleanupFullDocker(compose?.serverId); - } + // const admin = await findUserById(compose.project.userId); + // if (admin.cleanupCacheOnCompose) { + // await cleanupFullDocker(compose?.serverId); + // } let command = "set -e;"; if (compose.sourceType === "github") { @@ -381,7 +380,7 @@ export const deployRemoteCompose = async ({ applicationName: compose.name, applicationType: "compose", buildLink, - adminId: compose.project.adminId, + organizationId: compose.project.organizationId, domains: compose.domains, }); } catch (error) { @@ -406,7 +405,7 @@ export const deployRemoteCompose = async ({ // @ts-ignore errorMessage: error?.message || "Error building", buildLink, - adminId: compose.project.adminId, + organizationId: compose.project.organizationId, }); throw error; } @@ -430,10 +429,10 @@ export const rebuildRemoteCompose = async ({ }); try { - const admin = await findAdminById(compose.project.adminId); - if (admin.cleanupCacheOnCompose) { - await cleanupFullDocker(compose?.serverId); - } + // const admin = await findUserById(compose.project.userId); + // if (admin.cleanupCacheOnCompose) { + // await cleanupFullDocker(compose?.serverId); + // } if (compose.serverId) { await getBuildComposeCommand(compose, deployment.logPath); } diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index 096bdf19..86d6c88e 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -12,7 +12,7 @@ import { import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory"; import { TRPCError } from "@trpc/server"; import { format } from "date-fns"; -import { and, desc, eq, isNull } from "drizzle-orm"; +import { desc, eq } from "drizzle-orm"; import { type Application, findApplicationById, @@ -278,9 +278,11 @@ export const removeDeployment = async (deploymentId: string) => { .returning(); return deployment[0]; } catch (error) { + const message = + error instanceof Error ? error.message : "Error creating the deployment"; throw new TRPCError({ code: "BAD_REQUEST", - message: "Error deleting this deployment", + message, }); } }; @@ -535,9 +537,11 @@ export const createServerDeployment = async ( } return deploymentCreate[0]; } catch (error) { + const message = + error instanceof Error ? error.message : "Error creating the deployment"; throw new TRPCError({ code: "BAD_REQUEST", - message: "Error creating the deployment", + message, }); } }; diff --git a/packages/server/src/services/destination.ts b/packages/server/src/services/destination.ts index 892c9354..e66f8695 100644 --- a/packages/server/src/services/destination.ts +++ b/packages/server/src/services/destination.ts @@ -10,13 +10,13 @@ export type Destination = typeof destinations.$inferSelect; export const createDestintation = async ( input: typeof apiCreateDestination._type, - adminId: string, + organizationId: string, ) => { const newDestination = await db .insert(destinations) .values({ ...input, - adminId: adminId, + organizationId: organizationId, }) .returning() .then((value) => value[0]); @@ -46,14 +46,14 @@ export const findDestinationById = async (destinationId: string) => { export const removeDestinationById = async ( destinationId: string, - adminId: string, + organizationId: string, ) => { const result = await db .delete(destinations) .where( and( eq(destinations.destinationId, destinationId), - eq(destinations.adminId, adminId), + eq(destinations.organizationId, organizationId), ), ) .returning(); @@ -73,7 +73,7 @@ export const updateDestinationById = async ( .where( and( eq(destinations.destinationId, destinationId), - eq(destinations.adminId, destinationData.adminId || ""), + eq(destinations.organizationId, destinationData.organizationId || ""), ), ) .returning(); diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 597c03fa..a4a3b0b5 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -98,7 +98,7 @@ export const getConfig = async ( const config = JSON.parse(stdout); return config; - } catch (error) {} + } catch (_error) {} }; export const getContainersByAppNameMatch = async ( @@ -156,7 +156,7 @@ export const getContainersByAppNameMatch = async ( }); return containers || []; - } catch (error) {} + } catch (_error) {} return []; }; @@ -214,7 +214,7 @@ export const getStackContainersByAppName = async ( }); return containers || []; - } catch (error) {} + } catch (_error) {} return []; }; @@ -274,7 +274,7 @@ export const getServiceContainersByAppName = async ( }); return containers || []; - } catch (error) {} + } catch (_error) {} return []; }; @@ -325,7 +325,7 @@ export const getContainersByAppLabel = async ( }); return containers || []; - } catch (error) {} + } catch (_error) {} return []; }; @@ -344,7 +344,7 @@ export const containerRestart = async (containerId: string) => { const config = JSON.parse(stdout); return config; - } catch (error) {} + } catch (_error) {} }; export const getSwarmNodes = async (serverId?: string) => { @@ -373,7 +373,7 @@ export const getSwarmNodes = async (serverId?: string) => { .split("\n") .map((line) => JSON.parse(line)); return nodesArray; - } catch (error) {} + } catch (_error) {} }; export const getNodeInfo = async (nodeId: string, serverId?: string) => { @@ -399,7 +399,7 @@ export const getNodeInfo = async (nodeId: string, serverId?: string) => { const nodeInfo = JSON.parse(stdout); return nodeInfo; - } catch (error) {} + } catch (_error) {} }; export const getNodeApplications = async (serverId?: string) => { @@ -431,7 +431,7 @@ export const getNodeApplications = async (serverId?: string) => { .filter((service) => !service.Name.startsWith("dokploy-")); return appArray; - } catch (error) {} + } catch (_error) {} }; export const getApplicationInfo = async ( @@ -464,5 +464,5 @@ export const getApplicationInfo = async ( .map((line) => JSON.parse(line)); return appArray; - } catch (error) {} + } catch (_error) {} }; diff --git a/packages/server/src/services/domain.ts b/packages/server/src/services/domain.ts index b99c4869..d2e23c06 100644 --- a/packages/server/src/services/domain.ts +++ b/packages/server/src/services/domain.ts @@ -4,7 +4,7 @@ import { manageDomain } from "@dokploy/server/utils/traefik/domain"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { type apiCreateDomain, domains } from "../db/schema"; -import { findAdmin, findAdminById } from "./admin"; +import { findUserById } from "./admin"; import { findApplicationById } from "./application"; import { findServerById } from "./server"; @@ -40,7 +40,7 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => { export const generateTraefikMeDomain = async ( appName: string, - adminId: string, + userId: string, serverId?: string, ) => { if (serverId) { @@ -57,7 +57,7 @@ export const generateTraefikMeDomain = async ( projectName: appName, }); } - const admin = await findAdminById(adminId); + const admin = await findUserById(userId); return generateRandomDomain({ serverIp: admin?.serverIp || "", projectName: appName, @@ -126,7 +126,6 @@ export const updateDomainById = async ( export const removeDomainById = async (domainId: string) => { await findDomainById(domainId); - // TODO: fix order const result = await db .delete(domains) .where(eq(domains.domainId, domainId)) diff --git a/packages/server/src/services/github.ts b/packages/server/src/services/github.ts index a5d9d863..19deb2b2 100644 --- a/packages/server/src/services/github.ts +++ b/packages/server/src/services/github.ts @@ -12,14 +12,14 @@ import { updatePreviewDeployment } from "./preview-deployment"; export type Github = typeof github.$inferSelect; export const createGithub = async ( input: typeof apiCreateGithub._type, - adminId: string, + organizationId: string, ) => { return await db.transaction(async (tx) => { const newGitProvider = await tx .insert(gitProvider) .values({ providerType: "github", - adminId: adminId, + organizationId: organizationId, name: input.name, }) .returning() @@ -119,7 +119,7 @@ export const issueCommentExists = async ({ comment_id: comment_id, }); return true; - } catch (error) { + } catch (_error) { return false; } }; diff --git a/packages/server/src/services/gitlab.ts b/packages/server/src/services/gitlab.ts index 8e1362c9..fdca2775 100644 --- a/packages/server/src/services/gitlab.ts +++ b/packages/server/src/services/gitlab.ts @@ -1,9 +1,7 @@ import { db } from "@dokploy/server/db"; import { type apiCreateGitlab, - type bitbucket, gitProvider, - type github, gitlab, } from "@dokploy/server/db/schema"; import { TRPCError } from "@trpc/server"; @@ -13,14 +11,14 @@ export type Gitlab = typeof gitlab.$inferSelect; export const createGitlab = async ( input: typeof apiCreateGitlab._type, - adminId: string, + organizationId: string, ) => { return await db.transaction(async (tx) => { const newGitProvider = await tx .insert(gitProvider) .values({ providerType: "gitlab", - adminId: adminId, + organizationId: organizationId, name: input.name, }) .returning() diff --git a/packages/server/src/services/mariadb.ts b/packages/server/src/services/mariadb.ts index 8257b587..00be29d6 100644 --- a/packages/server/src/services/mariadb.ts +++ b/packages/server/src/services/mariadb.ts @@ -4,7 +4,7 @@ import { backups, mariadb, } from "@dokploy/server/db/schema"; -import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; +import { buildAppName } from "@dokploy/server/db/schema"; import { generatePassword } from "@dokploy/server/templates/utils"; import { buildMariadb } from "@dokploy/server/utils/databases/mariadb"; import { pullImage } from "@dokploy/server/utils/docker/utils"; diff --git a/packages/server/src/services/mongo.ts b/packages/server/src/services/mongo.ts index 031a6013..0ac4cc63 100644 --- a/packages/server/src/services/mongo.ts +++ b/packages/server/src/services/mongo.ts @@ -1,6 +1,6 @@ import { db } from "@dokploy/server/db"; import { type apiCreateMongo, backups, mongo } from "@dokploy/server/db/schema"; -import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; +import { buildAppName } from "@dokploy/server/db/schema"; import { generatePassword } from "@dokploy/server/templates/utils"; import { buildMongo } from "@dokploy/server/utils/databases/mongo"; import { pullImage } from "@dokploy/server/utils/docker/utils"; diff --git a/packages/server/src/services/mount.ts b/packages/server/src/services/mount.ts index 38e82d1a..836feace 100644 --- a/packages/server/src/services/mount.ts +++ b/packages/server/src/services/mount.ts @@ -123,8 +123,8 @@ export const updateMount = async ( mountId: string, mountData: Partial, ) => { - return await db.transaction(async (transaction) => { - const mount = await db + return await db.transaction(async (tx) => { + const mount = await tx .update(mounts) .set({ ...mountData, @@ -211,7 +211,7 @@ export const deleteFileMount = async (mountId: string) => { } else { await removeFileOrDirectory(fullPath); } - } catch (error) {} + } catch (_error) {} }; export const getBaseFilesPath = async (mountId: string) => { diff --git a/packages/server/src/services/notification.ts b/packages/server/src/services/notification.ts index f30d77ae..16ba2085 100644 --- a/packages/server/src/services/notification.ts +++ b/packages/server/src/services/notification.ts @@ -24,7 +24,7 @@ export type Notification = typeof notifications.$inferSelect; export const createSlackNotification = async ( input: typeof apiCreateSlack._type, - adminId: string, + organizationId: string, ) => { await db.transaction(async (tx) => { const newSlack = await tx @@ -54,7 +54,7 @@ export const createSlackNotification = async ( dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, notificationType: "slack", - adminId: adminId, + organizationId: organizationId, serverThreshold: input.serverThreshold, }) .returning() @@ -84,7 +84,7 @@ export const updateSlackNotification = async ( databaseBackup: input.databaseBackup, dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, - adminId: input.adminId, + organizationId: input.organizationId, serverThreshold: input.serverThreshold, }) .where(eq(notifications.notificationId, input.notificationId)) @@ -114,7 +114,7 @@ export const updateSlackNotification = async ( export const createTelegramNotification = async ( input: typeof apiCreateTelegram._type, - adminId: string, + organizationId: string, ) => { await db.transaction(async (tx) => { const newTelegram = await tx @@ -145,7 +145,7 @@ export const createTelegramNotification = async ( dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, notificationType: "telegram", - adminId: adminId, + organizationId: organizationId, serverThreshold: input.serverThreshold, }) .returning() @@ -175,7 +175,7 @@ export const updateTelegramNotification = async ( databaseBackup: input.databaseBackup, dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, - adminId: input.adminId, + organizationId: input.organizationId, serverThreshold: input.serverThreshold, }) .where(eq(notifications.notificationId, input.notificationId)) @@ -206,7 +206,7 @@ export const updateTelegramNotification = async ( export const createDiscordNotification = async ( input: typeof apiCreateDiscord._type, - adminId: string, + organizationId: string, ) => { await db.transaction(async (tx) => { const newDiscord = await tx @@ -236,7 +236,7 @@ export const createDiscordNotification = async ( dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, notificationType: "discord", - adminId: adminId, + organizationId: organizationId, serverThreshold: input.serverThreshold, }) .returning() @@ -266,7 +266,7 @@ export const updateDiscordNotification = async ( databaseBackup: input.databaseBackup, dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, - adminId: input.adminId, + organizationId: input.organizationId, serverThreshold: input.serverThreshold, }) .where(eq(notifications.notificationId, input.notificationId)) @@ -296,7 +296,7 @@ export const updateDiscordNotification = async ( export const createEmailNotification = async ( input: typeof apiCreateEmail._type, - adminId: string, + organizationId: string, ) => { await db.transaction(async (tx) => { const newEmail = await tx @@ -330,7 +330,7 @@ export const createEmailNotification = async ( dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, notificationType: "email", - adminId: adminId, + organizationId: organizationId, serverThreshold: input.serverThreshold, }) .returning() @@ -360,7 +360,7 @@ export const updateEmailNotification = async ( databaseBackup: input.databaseBackup, dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, - adminId: input.adminId, + organizationId: input.organizationId, serverThreshold: input.serverThreshold, }) .where(eq(notifications.notificationId, input.notificationId)) @@ -394,7 +394,7 @@ export const updateEmailNotification = async ( export const createGotifyNotification = async ( input: typeof apiCreateGotify._type, - adminId: string, + organizationId: string, ) => { await db.transaction(async (tx) => { const newGotify = await tx @@ -426,7 +426,7 @@ export const createGotifyNotification = async ( dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, notificationType: "gotify", - adminId: adminId, + organizationId: organizationId, }) .returning() .then((value) => value[0]); @@ -455,7 +455,7 @@ export const updateGotifyNotification = async ( databaseBackup: input.databaseBackup, dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, - adminId: input.adminId, + organizationId: input.organizationId, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() diff --git a/packages/server/src/services/postgres.ts b/packages/server/src/services/postgres.ts index 682d3f78..75b81c50 100644 --- a/packages/server/src/services/postgres.ts +++ b/packages/server/src/services/postgres.ts @@ -4,7 +4,7 @@ import { backups, postgres, } from "@dokploy/server/db/schema"; -import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; +import { buildAppName } from "@dokploy/server/db/schema"; import { generatePassword } from "@dokploy/server/templates/utils"; import { buildPostgres } from "@dokploy/server/utils/databases/postgres"; import { pullImage } from "@dokploy/server/utils/docker/utils"; diff --git a/packages/server/src/services/preview-deployment.ts b/packages/server/src/services/preview-deployment.ts index ab38c17c..a1ffca4b 100644 --- a/packages/server/src/services/preview-deployment.ts +++ b/packages/server/src/services/preview-deployment.ts @@ -2,23 +2,20 @@ import { db } from "@dokploy/server/db"; import { type apiCreatePreviewDeployment, deployments, + organization, previewDeployments, } from "@dokploy/server/db/schema"; import { TRPCError } from "@trpc/server"; import { and, desc, eq } from "drizzle-orm"; -import { slugify } from "../setup/server-setup"; -import { generatePassword, generateRandomDomain } from "../templates/utils"; +import { generatePassword } from "../templates/utils"; import { removeService } from "../utils/docker/utils"; import { removeDirectoryCode } from "../utils/filesystem/directory"; import { authGithub } from "../utils/providers/github"; import { removeTraefikConfig } from "../utils/traefik/application"; import { manageDomain } from "../utils/traefik/domain"; -import { findAdminById } from "./admin"; +import { findUserById } from "./admin"; import { findApplicationById } from "./application"; -import { - removeDeployments, - removeDeploymentsByPreviewDeploymentId, -} from "./deployment"; +import { removeDeploymentsByPreviewDeploymentId } from "./deployment"; import { createDomain } from "./domain"; import { type Github, getIssueComment } from "./github"; @@ -106,13 +103,17 @@ export const removePreviewDeployment = async (previewDeploymentId: string) => { for (const operation of cleanupOperations) { try { await operation(); - } catch (error) {} + } catch (_error) {} } return deployment[0]; } catch (error) { + const message = + error instanceof Error + ? error.message + : "Error deleting this preview deployment"; throw new TRPCError({ code: "BAD_REQUEST", - message: "Error deleting this preview deployment", + message, }); } }; @@ -154,11 +155,14 @@ export const createPreviewDeployment = async ( const application = await findApplicationById(schema.applicationId); const appName = `preview-${application.appName}-${generatePassword(6)}`; + const org = await db.query.organization.findFirst({ + where: eq(organization.id, application.project.organizationId), + }); const generateDomain = await generateWildcardDomain( application.previewWildcard || "*.traefik.me", appName, application.server?.ipAddress || "", - application.project.adminId, + org?.ownerId || "", ); const octokit = authGithub(application?.github as Github); @@ -250,7 +254,7 @@ const generateWildcardDomain = async ( baseDomain: string, appName: string, serverIp: string, - adminId: string, + userId: string, ): Promise => { if (!baseDomain.startsWith("*.")) { throw new Error('The base domain must start with "*."'); @@ -268,7 +272,7 @@ const generateWildcardDomain = async ( } if (!ip) { - const admin = await findAdminById(adminId); + const admin = await findUserById(userId); ip = admin?.serverIp || ""; } diff --git a/packages/server/src/services/project.ts b/packages/server/src/services/project.ts index adaa07ea..b740834b 100644 --- a/packages/server/src/services/project.ts +++ b/packages/server/src/services/project.ts @@ -16,13 +16,13 @@ export type Project = typeof projects.$inferSelect; export const createProject = async ( input: typeof apiCreateProject._type, - adminId: string, + organizationId: string, ) => { const newProject = await db .insert(projects) .values({ ...input, - adminId: adminId, + organizationId: organizationId, }) .returning() .then((value) => value[0]); diff --git a/packages/server/src/services/redirect.ts b/packages/server/src/services/redirect.ts index f16dbe42..1896105f 100644 --- a/packages/server/src/services/redirect.ts +++ b/packages/server/src/services/redirect.ts @@ -6,7 +6,7 @@ import { updateRedirectMiddleware, } from "@dokploy/server/utils/traefik/redirect"; import { TRPCError } from "@trpc/server"; -import { desc, eq } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import type { z } from "zod"; import { findApplicationById } from "./application"; export type Redirect = typeof redirects.$inferSelect; @@ -114,9 +114,11 @@ export const updateRedirectById = async ( return redirect; } catch (error) { + const message = + error instanceof Error ? error.message : "Error updating this redirect"; throw new TRPCError({ code: "BAD_REQUEST", - message: "Error updating this redirect", + message, }); } }; diff --git a/packages/server/src/services/redis.ts b/packages/server/src/services/redis.ts index e0dbbe02..9f4a1f9e 100644 --- a/packages/server/src/services/redis.ts +++ b/packages/server/src/services/redis.ts @@ -1,6 +1,6 @@ import { db } from "@dokploy/server/db"; import { type apiCreateRedis, redis } from "@dokploy/server/db/schema"; -import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; +import { buildAppName } from "@dokploy/server/db/schema"; import { generatePassword } from "@dokploy/server/templates/utils"; import { buildRedis } from "@dokploy/server/utils/databases/redis"; import { pullImage } from "@dokploy/server/utils/docker/utils"; diff --git a/packages/server/src/services/registry.ts b/packages/server/src/services/registry.ts index 2bcf3a4a..6468cd97 100644 --- a/packages/server/src/services/registry.ts +++ b/packages/server/src/services/registry.ts @@ -12,14 +12,14 @@ export type Registry = typeof registry.$inferSelect; export const createRegistry = async ( input: typeof apiCreateRegistry._type, - adminId: string, + organizationId: string, ) => { return await db.transaction(async (tx) => { const newRegistry = await tx .insert(registry) .values({ ...input, - adminId: adminId, + organizationId: organizationId, }) .returning() .then((value) => value[0]); @@ -112,9 +112,11 @@ export const updateRegistry = async ( return response; } catch (error) { + const message = + error instanceof Error ? error.message : "Error updating this registry"; throw new TRPCError({ code: "BAD_REQUEST", - message: "Error updating this registry", + message, }); } }; @@ -135,9 +137,11 @@ export const findRegistryById = async (registryId: string) => { return registryResponse; }; -export const findAllRegistryByAdminId = async (adminId: string) => { +export const findAllRegistryByOrganizationId = async ( + organizationId: string, +) => { const registryResponse = await db.query.registry.findMany({ - where: eq(registry.adminId, adminId), + where: eq(registry.organizationId, organizationId), }); return registryResponse; }; diff --git a/packages/server/src/services/security.ts b/packages/server/src/services/security.ts index 5efca19f..d6947b88 100644 --- a/packages/server/src/services/security.ts +++ b/packages/server/src/services/security.ts @@ -76,9 +76,11 @@ export const deleteSecurityById = async (securityId: string) => { await removeSecurityMiddleware(application, result); return result; } catch (error) { + const message = + error instanceof Error ? error.message : "Error removing this security"; throw new TRPCError({ code: "BAD_REQUEST", - message: "Error removing this security", + message, }); } }; @@ -98,9 +100,11 @@ export const updateSecurityById = async ( return response[0]; } catch (error) { + const message = + error instanceof Error ? error.message : "Error updating this security"; throw new TRPCError({ code: "BAD_REQUEST", - message: "Error updating this security", + message, }); } }; diff --git a/packages/server/src/services/server.ts b/packages/server/src/services/server.ts index 081b19fa..a4d5c5d8 100644 --- a/packages/server/src/services/server.ts +++ b/packages/server/src/services/server.ts @@ -1,19 +1,24 @@ import { db } from "@dokploy/server/db"; -import { type apiCreateServer, server } from "@dokploy/server/db/schema"; +import { + type apiCreateServer, + organization, + server, +} from "@dokploy/server/db/schema"; import { TRPCError } from "@trpc/server"; -import { desc, eq } from "drizzle-orm"; +import { eq } from "drizzle-orm"; export type Server = typeof server.$inferSelect; export const createServer = async ( input: typeof apiCreateServer._type, - adminId: string, + organizationId: string, ) => { const newServer = await db .insert(server) .values({ ...input, - adminId: adminId, + organizationId: organizationId, + createdAt: new Date().toISOString(), }) .returning() .then((value) => value[0]); @@ -45,12 +50,16 @@ export const findServerById = async (serverId: string) => { return currentServer; }; -export const findServersByAdminId = async (adminId: string) => { - const servers = await db.query.server.findMany({ - where: eq(server.adminId, adminId), - orderBy: desc(server.createdAt), +export const findServersByUserId = async (userId: string) => { + const orgs = await db.query.organization.findMany({ + where: eq(organization.ownerId, userId), + with: { + servers: true, + }, }); + const servers = orgs.flatMap((org) => org.servers); + return servers; }; diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index dd5b4e18..75613be0 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -5,8 +5,6 @@ import { execAsync, execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; -import { findAdminById } from "./admin"; -// import packageInfo from "../../../package.json"; export interface IUpdateData { latestVersion: string | null; @@ -171,7 +169,6 @@ echo "$json_output" const result = JSON.parse(stdout); return result; } - const items = readdirSync(dirPath, { withFileTypes: true }); const stack = [dirPath]; const result: TreeDataItem[] = []; diff --git a/packages/server/src/services/user.ts b/packages/server/src/services/user.ts index d8d9862c..39ac95ce 100644 --- a/packages/server/src/services/user.ts +++ b/packages/server/src/services/user.ts @@ -1,80 +1,53 @@ import { db } from "@dokploy/server/db"; -import { users } from "@dokploy/server/db/schema"; +import { apikey, member, users_temp } from "@dokploy/server/db/schema"; import { TRPCError } from "@trpc/server"; -import { eq } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; +import { auth } from "../lib/auth"; -export type User = typeof users.$inferSelect; +export type User = typeof users_temp.$inferSelect; -export const findUserById = async (userId: string) => { - const user = await db.query.users.findFirst({ - where: eq(users.userId, userId), - }); - if (!user) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "User not found", - }); - } - return user; -}; - -export const findUserByAuthId = async (authId: string) => { - const user = await db.query.users.findFirst({ - where: eq(users.authId, authId), - with: { - auth: true, - }, - }); - if (!user) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "User not found", - }); - } - return user; -}; - -export const findUsers = async (adminId: string) => { - const currentUsers = await db.query.users.findMany({ - where: eq(users.adminId, adminId), - with: { - auth: { - columns: { - secret: false, - }, - }, - }, - }); - return currentUsers; -}; - -export const addNewProject = async (authId: string, projectId: string) => { - const user = await findUserByAuthId(authId); +export const addNewProject = async ( + userId: string, + projectId: string, + organizationId: string, +) => { + const userR = await findMemberById(userId, organizationId); await db - .update(users) + .update(member) .set({ - accessedProjects: [...user.accessedProjects, projectId], + accessedProjects: [...userR.accessedProjects, projectId], }) - .where(eq(users.authId, authId)); + .where( + and(eq(member.id, userR.id), eq(member.organizationId, organizationId)), + ); }; -export const addNewService = async (authId: string, serviceId: string) => { - const user = await findUserByAuthId(authId); +export const addNewService = async ( + userId: string, + serviceId: string, + organizationId: string, +) => { + const userR = await findMemberById(userId, organizationId); await db - .update(users) + .update(member) .set({ - accessedServices: [...user.accessedServices, serviceId], + accessedServices: [...userR.accessedServices, serviceId], }) - .where(eq(users.authId, authId)); + .where( + and(eq(member.id, userR.id), eq(member.organizationId, organizationId)), + ); }; export const canPerformCreationService = async ( userId: string, projectId: string, + organizationId: string, ) => { - const { accessedProjects, canCreateServices } = - await findUserByAuthId(userId); + const { accessedProjects, canCreateServices } = await findMemberById( + userId, + organizationId, + ); const haveAccessToProject = accessedProjects.includes(projectId); if (canCreateServices && haveAccessToProject) { @@ -87,8 +60,9 @@ export const canPerformCreationService = async ( export const canPerformAccessService = async ( userId: string, serviceId: string, + organizationId: string, ) => { - const { accessedServices } = await findUserByAuthId(userId); + const { accessedServices } = await findMemberById(userId, organizationId); const haveAccessToService = accessedServices.includes(serviceId); if (haveAccessToService) { @@ -99,11 +73,14 @@ export const canPerformAccessService = async ( }; export const canPeformDeleteService = async ( - authId: string, + userId: string, serviceId: string, + organizationId: string, ) => { - const { accessedServices, canDeleteServices } = - await findUserByAuthId(authId); + const { accessedServices, canDeleteServices } = await findMemberById( + userId, + organizationId, + ); const haveAccessToService = accessedServices.includes(serviceId); if (canDeleteServices && haveAccessToService) { @@ -113,8 +90,11 @@ export const canPeformDeleteService = async ( return false; }; -export const canPerformCreationProject = async (authId: string) => { - const { canCreateProjects } = await findUserByAuthId(authId); +export const canPerformCreationProject = async ( + userId: string, + organizationId: string, +) => { + const { canCreateProjects } = await findMemberById(userId, organizationId); if (canCreateProjects) { return true; @@ -123,8 +103,11 @@ export const canPerformCreationProject = async (authId: string) => { return false; }; -export const canPerformDeleteProject = async (authId: string) => { - const { canDeleteProjects } = await findUserByAuthId(authId); +export const canPerformDeleteProject = async ( + userId: string, + organizationId: string, +) => { + const { canDeleteProjects } = await findMemberById(userId, organizationId); if (canDeleteProjects) { return true; @@ -134,10 +117,11 @@ export const canPerformDeleteProject = async (authId: string) => { }; export const canPerformAccessProject = async ( - authId: string, + userId: string, projectId: string, + organizationId: string, ) => { - const { accessedProjects } = await findUserByAuthId(authId); + const { accessedProjects } = await findMemberById(userId, organizationId); const haveAccessToProject = accessedProjects.includes(projectId); @@ -147,26 +131,45 @@ export const canPerformAccessProject = async ( return false; }; -export const canAccessToTraefikFiles = async (authId: string) => { - const { canAccessToTraefikFiles } = await findUserByAuthId(authId); +export const canAccessToTraefikFiles = async ( + userId: string, + organizationId: string, +) => { + const { canAccessToTraefikFiles } = await findMemberById( + userId, + organizationId, + ); return canAccessToTraefikFiles; }; export const checkServiceAccess = async ( - authId: string, + userId: string, serviceId: string, + organizationId: string, action = "access" as "access" | "create" | "delete", ) => { let hasPermission = false; switch (action) { case "create": - hasPermission = await canPerformCreationService(authId, serviceId); + hasPermission = await canPerformCreationService( + userId, + serviceId, + organizationId, + ); break; case "access": - hasPermission = await canPerformAccessService(authId, serviceId); + hasPermission = await canPerformAccessService( + userId, + serviceId, + organizationId, + ); break; case "delete": - hasPermission = await canPeformDeleteService(authId, serviceId); + hasPermission = await canPeformDeleteService( + userId, + serviceId, + organizationId, + ); break; default: hasPermission = false; @@ -182,6 +185,7 @@ export const checkServiceAccess = async ( export const checkProjectAccess = async ( authId: string, action: "create" | "delete" | "access", + organizationId: string, projectId?: string, ) => { let hasPermission = false; @@ -190,13 +194,14 @@ export const checkProjectAccess = async ( hasPermission = await canPerformAccessProject( authId, projectId as string, + organizationId, ); break; case "create": - hasPermission = await canPerformCreationProject(authId); + hasPermission = await canPerformCreationProject(authId, organizationId); break; case "delete": - hasPermission = await canPerformDeleteProject(authId); + hasPermission = await canPerformDeleteProject(authId, organizationId); break; default: hasPermission = false; @@ -208,3 +213,82 @@ export const checkProjectAccess = async ( }); } }; + +export const findMemberById = async ( + userId: string, + organizationId: string, +) => { + const result = await db.query.member.findFirst({ + where: and( + eq(member.userId, userId), + eq(member.organizationId, organizationId), + ), + with: { + user: true, + }, + }); + + if (!result) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Permission denied", + }); + } + return result; +}; + +export const updateUser = async (userId: string, userData: Partial) => { + const user = await db + .update(users_temp) + .set({ + ...userData, + }) + .where(eq(users_temp.id, userId)) + .returning() + .then((res) => res[0]); + + return user; +}; + +export const createApiKey = async ( + userId: string, + input: { + name: string; + prefix?: string; + expiresIn?: number; + metadata: { + organizationId: string; + }; + rateLimitEnabled?: boolean; + rateLimitTimeWindow?: number; + rateLimitMax?: number; + remaining?: number; + refillAmount?: number; + refillInterval?: number; + }, +) => { + const apiKey = await auth.createApiKey({ + body: { + name: input.name, + expiresIn: input.expiresIn, + prefix: input.prefix, + rateLimitEnabled: input.rateLimitEnabled, + rateLimitTimeWindow: input.rateLimitTimeWindow, + rateLimitMax: input.rateLimitMax, + remaining: input.remaining, + refillAmount: input.refillAmount, + refillInterval: input.refillInterval, + userId, + }, + }); + + if (input.metadata) { + await db + .update(apikey) + .set({ + metadata: JSON.stringify(input.metadata), + }) + .where(eq(apikey.id, apiKey.id)); + } + return apiKey; +}; diff --git a/packages/server/src/setup/monitoring-setup.ts b/packages/server/src/setup/monitoring-setup.ts index f72b2244..75b9a928 100644 --- a/packages/server/src/setup/monitoring-setup.ts +++ b/packages/server/src/setup/monitoring-setup.ts @@ -1,7 +1,7 @@ import { findServerById } from "@dokploy/server/services/server"; import type { ContainerCreateOptions } from "dockerode"; import { IS_CLOUD } from "../constants"; -import { findAdminById } from "../services/admin"; +import { findUserById } from "../services/admin"; import { getDokployImageTag } from "../services/settings"; import { pullImage, pullRemoteImage } from "../utils/docker/utils"; import { execAsync, execAsyncRemote } from "../utils/process/execAsync"; @@ -66,7 +66,7 @@ export const setupMonitoring = async (serverId: string) => { await container.inspect(); await container.remove({ force: true }); console.log("Removed existing container"); - } catch (error) { + } catch (_error) { // Container doesn't exist, continue } @@ -80,8 +80,8 @@ export const setupMonitoring = async (serverId: string) => { } }; -export const setupWebMonitoring = async (adminId: string) => { - const admin = await findAdminById(adminId); +export const setupWebMonitoring = async (userId: string) => { + const user = await findUserById(userId); const containerName = "dokploy-monitoring"; let imageName = "dokploy/monitoring:latest"; @@ -96,7 +96,7 @@ export const setupWebMonitoring = async (adminId: string) => { const settings: ContainerCreateOptions = { name: containerName, - Env: [`METRICS_CONFIG=${JSON.stringify(admin?.metricsConfig)}`], + Env: [`METRICS_CONFIG=${JSON.stringify(user?.metricsConfig)}`], Image: imageName, HostConfig: { // Memory: 100 * 1024 * 1024, // 100MB en bytes @@ -104,9 +104,9 @@ export const setupWebMonitoring = async (adminId: string) => { // CapAdd: ["NET_ADMIN", "SYS_ADMIN"], // Privileged: true, PortBindings: { - [`${admin.metricsConfig.server.port}/tcp`]: [ + [`${user?.metricsConfig?.server?.port}/tcp`]: [ { - HostPort: admin.metricsConfig.server.port.toString(), + HostPort: user?.metricsConfig?.server?.port.toString(), }, ], }, @@ -120,7 +120,7 @@ export const setupWebMonitoring = async (adminId: string) => { // NetworkMode: "host", }, ExposedPorts: { - [`${admin.metricsConfig.server.port}/tcp`]: {}, + [`${user?.metricsConfig?.server?.port}/tcp`]: {}, }, }; const docker = await getRemoteDocker(); @@ -135,7 +135,7 @@ export const setupWebMonitoring = async (adminId: string) => { await container.inspect(); await container.remove({ force: true }); console.log("Removed existing container"); - } catch (error) {} + } catch (_error) {} await docker.createContainer(settings); const newContainer = docker.getContainer(containerName); diff --git a/packages/server/src/setup/postgres-setup.ts b/packages/server/src/setup/postgres-setup.ts index b5794c2b..cf162f1e 100644 --- a/packages/server/src/setup/postgres-setup.ts +++ b/packages/server/src/setup/postgres-setup.ts @@ -54,10 +54,16 @@ export const initializePostgres = async () => { version: Number.parseInt(inspect.Version.Index), ...settings, }); - console.log("Postgres Started ✅"); - } catch (error) { - await docker.createService(settings); + } catch (_) { + try { + await docker.createService(settings); + } catch (error: any) { + if (error?.statusCode !== 409) { + throw error; + } + console.log("Postgres service already exists, continuing..."); + } console.log("Postgres Not Found: Starting ✅"); } }; diff --git a/packages/server/src/setup/redis-setup.ts b/packages/server/src/setup/redis-setup.ts index 1c3b545a..7366546d 100644 --- a/packages/server/src/setup/redis-setup.ts +++ b/packages/server/src/setup/redis-setup.ts @@ -52,8 +52,15 @@ export const initializeRedis = async () => { ...settings, }); console.log("Redis Started ✅"); - } catch (error) { - await docker.createService(settings); + } catch (_) { + try { + await docker.createService(settings); + } catch (error: any) { + if (error?.statusCode !== 409) { + throw error; + } + console.log("Redis service already exists, continuing..."); + } console.log("Redis Not Found: Starting ✅"); } }; diff --git a/packages/server/src/setup/server-audit.ts b/packages/server/src/setup/server-audit.ts index df00e9a7..b9283c31 100644 --- a/packages/server/src/setup/server-audit.ts +++ b/packages/server/src/setup/server-audit.ts @@ -89,7 +89,7 @@ export const serverAudit = async (serverId: string) => { .on("data", (data: string) => { output += data; }) - .stderr.on("data", (data) => {}); + .stderr.on("data", (_data) => {}); }); }) .on("error", (err) => { diff --git a/packages/server/src/setup/server-validate.ts b/packages/server/src/setup/server-validate.ts index 4ca21df8..c86206b6 100644 --- a/packages/server/src/setup/server-validate.ts +++ b/packages/server/src/setup/server-validate.ts @@ -128,7 +128,7 @@ export const serverValidate = async (serverId: string) => { .on("data", (data: string) => { output += data; }) - .stderr.on("data", (data) => {}); + .stderr.on("data", (_data) => {}); }); }) .on("error", (err) => { diff --git a/packages/server/src/setup/setup.ts b/packages/server/src/setup/setup.ts index c5987702..eeef32dd 100644 --- a/packages/server/src/setup/setup.ts +++ b/packages/server/src/setup/setup.ts @@ -18,7 +18,7 @@ export const dockerSwarmInitialized = async () => { await docker.swarmInspect(); return true; - } catch (e) { + } catch (_e) { return false; } }; @@ -41,7 +41,7 @@ export const dockerNetworkInitialized = async () => { try { await docker.getNetwork("dokploy-network").inspect(); return true; - } catch (e) { + } catch (_e) { return false; } }; diff --git a/packages/server/src/setup/traefik-setup.ts b/packages/server/src/setup/traefik-setup.ts index 1d60e577..e8d01942 100644 --- a/packages/server/src/setup/traefik-setup.ts +++ b/packages/server/src/setup/traefik-setup.ts @@ -127,8 +127,15 @@ export const initializeTraefik = async ({ }); console.log("Traefik Started ✅"); - } catch (error) { - await docker.createService(settings); + } catch (_) { + try { + await docker.createService(settings); + } catch (error: any) { + if (error?.statusCode !== 409) { + throw error; + } + console.log("Traefik service already exists, continuing..."); + } console.log("Traefik Not Found: Starting ✅"); } }; diff --git a/packages/server/src/types/with.ts b/packages/server/src/types/with.ts index c4826f73..467020a2 100644 --- a/packages/server/src/types/with.ts +++ b/packages/server/src/types/with.ts @@ -36,7 +36,7 @@ type AnyObj = Record; type ZodObj = { [key in keyof T]: z.ZodType; }; -const zObject = (arg: ZodObj) => z.object(arg); +const _zObject = (arg: ZodObj) => z.object(arg); // const goodDogScheme = zObject({ // // prueba: schema.selectDatabaseSchema, diff --git a/packages/server/src/utils/access-log/handler.ts b/packages/server/src/utils/access-log/handler.ts index b1fd925c..69d0cc68 100644 --- a/packages/server/src/utils/access-log/handler.ts +++ b/packages/server/src/utils/access-log/handler.ts @@ -1,8 +1,8 @@ import { IS_CLOUD, paths } from "@dokploy/server/constants"; -import { updateAdmin } from "@dokploy/server/services/admin"; import { type RotatingFileStream, createStream } from "rotating-file-stream"; -import { db } from "../../db"; import { execAsync } from "../process/execAsync"; +import { findAdmin } from "@dokploy/server/services/admin"; +import { updateUser } from "@dokploy/server/services/user"; class LogRotationManager { private static instance: LogRotationManager; @@ -30,17 +30,16 @@ class LogRotationManager { } private async getStateFromDB(): Promise { - const setting = await db.query.admins.findFirst({}); - return setting?.enableLogRotation ?? false; + const admin = await findAdmin(); + return admin?.user.enableLogRotation ?? false; } private async setStateInDB(active: boolean): Promise { - const admin = await db.query.admins.findFirst({}); - + const admin = await findAdmin(); if (!admin) { return; } - await updateAdmin(admin?.authId, { + await updateUser(admin.user.id, { enableLogRotation: active, }); } diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index 922232a0..7699a42e 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -1,4 +1,3 @@ -import { findAdmin } from "@dokploy/server/services/admin"; import { getAllServers } from "@dokploy/server/services/server"; import { scheduleJob } from "node-schedule"; import { db } from "../../db/index"; @@ -12,13 +11,14 @@ import { runMariadbBackup } from "./mariadb"; import { runMongoBackup } from "./mongo"; import { runMySqlBackup } from "./mysql"; import { runPostgresBackup } from "./postgres"; +import { findAdmin } from "../../services/admin"; export const initCronJobs = async () => { console.log("Setting up cron jobs...."); const admin = await findAdmin(); - if (admin?.enableDockerCleanup) { + if (admin?.user.enableDockerCleanup) { scheduleJob("docker-cleanup", "0 0 * * *", async () => { console.log( `Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`, @@ -26,7 +26,7 @@ export const initCronJobs = async () => { await cleanUpUnusedImages(); await cleanUpDockerBuilder(); await cleanUpSystemPrune(); - await sendDockerCleanupNotifications(admin.adminId); + await sendDockerCleanupNotifications(admin.user.id); }); } @@ -43,7 +43,7 @@ export const initCronJobs = async () => { await cleanUpDockerBuilder(serverId); await cleanUpSystemPrune(serverId); await sendDockerCleanupNotifications( - admin.adminId, + admin.user.id, `Docker cleanup for Server ${name} (${serverId})`, ); }); diff --git a/packages/server/src/utils/backups/mariadb.ts b/packages/server/src/utils/backups/mariadb.ts index 79cba9c5..56c2919c 100644 --- a/packages/server/src/utils/backups/mariadb.ts +++ b/packages/server/src/utils/backups/mariadb.ts @@ -49,7 +49,7 @@ export const runMariadbBackup = async ( projectName: project.name, databaseType: "mariadb", type: "success", - adminId: project.adminId, + organizationId: project.organizationId, }); } catch (error) { console.log(error); @@ -60,7 +60,7 @@ export const runMariadbBackup = async ( type: "error", // @ts-ignore errorMessage: error?.message || "Error message not provided", - adminId: project.adminId, + organizationId: project.organizationId, }); throw error; } diff --git a/packages/server/src/utils/backups/mongo.ts b/packages/server/src/utils/backups/mongo.ts index ddd1b889..a40ec4f4 100644 --- a/packages/server/src/utils/backups/mongo.ts +++ b/packages/server/src/utils/backups/mongo.ts @@ -46,7 +46,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { projectName: project.name, databaseType: "mongodb", type: "success", - adminId: project.adminId, + organizationId: project.organizationId, }); } catch (error) { console.log(error); @@ -57,7 +57,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { type: "error", // @ts-ignore errorMessage: error?.message || "Error message not provided", - adminId: project.adminId, + organizationId: project.organizationId, }); throw error; } diff --git a/packages/server/src/utils/backups/mysql.ts b/packages/server/src/utils/backups/mysql.ts index b505204c..1272fc3e 100644 --- a/packages/server/src/utils/backups/mysql.ts +++ b/packages/server/src/utils/backups/mysql.ts @@ -1,4 +1,3 @@ -import { unlink } from "node:fs/promises"; import path from "node:path"; import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { MySql } from "@dokploy/server/services/mysql"; @@ -46,7 +45,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => { projectName: project.name, databaseType: "mysql", type: "success", - adminId: project.adminId, + organizationId: project.organizationId, }); } catch (error) { console.log(error); @@ -57,7 +56,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => { type: "error", // @ts-ignore errorMessage: error?.message || "Error message not provided", - adminId: project.adminId, + organizationId: project.organizationId, }); throw error; } diff --git a/packages/server/src/utils/backups/postgres.ts b/packages/server/src/utils/backups/postgres.ts index e9609fc8..5ada2aa9 100644 --- a/packages/server/src/utils/backups/postgres.ts +++ b/packages/server/src/utils/backups/postgres.ts @@ -49,7 +49,7 @@ export const runPostgresBackup = async ( projectName: project.name, databaseType: "postgres", type: "success", - adminId: project.adminId, + organizationId: project.organizationId, }); } catch (error) { await sendDatabaseBackupNotifications({ @@ -59,7 +59,7 @@ export const runPostgresBackup = async ( type: "error", // @ts-ignore errorMessage: error?.message || "Error message not provided", - adminId: project.adminId, + organizationId: project.organizationId, }); throw error; diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index 0d78ff96..c76f7962 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -28,7 +28,7 @@ export const removeScheduleBackup = (backupId: string) => { }; export const getS3Credentials = (destination: Destination) => { - const { accessKey, secretAccessKey, bucket, region, endpoint, provider } = + const { accessKey, secretAccessKey, region, endpoint, provider } = destination; const rcloneFlags = [ `--s3-access-key-id=${accessKey}`, diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 838cf74e..19e7d152 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -2,7 +2,6 @@ import { createWriteStream, existsSync, mkdirSync, - readFileSync, writeFileSync, } from "node:fs"; import { dirname, join } from "node:path"; @@ -99,8 +98,7 @@ export const getBuildComposeCommand = async ( logPath: string, ) => { const { COMPOSE_PATH } = paths(true); - const { sourceType, appName, mounts, composeType, domains, composePath } = - compose; + const { sourceType, appName, mounts, composeType, domains } = compose; const command = createCommand(compose); const envCommand = getCreateEnvFileCommand(compose); const projectPath = join(COMPOSE_PATH, compose.appName, "code"); diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index d6748275..d777b1a3 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -197,7 +197,7 @@ export const mechanizeDockerContainer = async ( ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1, }, }); - } catch (error) { + } catch (_error) { await docker.createService(settings); } }; diff --git a/packages/server/src/utils/builders/nixpacks.ts b/packages/server/src/utils/builders/nixpacks.ts index 56560e4e..c13f82a5 100644 --- a/packages/server/src/utils/builders/nixpacks.ts +++ b/packages/server/src/utils/builders/nixpacks.ts @@ -91,7 +91,7 @@ export const getNixpacksCommand = ( application: ApplicationNested, logPath: string, ) => { - const { env, appName, publishDirectory, serverId } = application; + const { env, appName, publishDirectory } = application; const buildAppDirectory = getBuildAppDirectory(application); const buildContainerId = `${appName}-${nanoid(10)}`; diff --git a/packages/server/src/utils/databases/mariadb.ts b/packages/server/src/utils/databases/mariadb.ts index d1b41fc3..ead5a618 100644 --- a/packages/server/src/utils/databases/mariadb.ts +++ b/packages/server/src/utils/databases/mariadb.ts @@ -98,7 +98,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => { version: Number.parseInt(inspect.Version.Index), ...settings, }); - } catch (error) { + } catch (_error) { await docker.createService(settings); } }; diff --git a/packages/server/src/utils/databases/mongo.ts b/packages/server/src/utils/databases/mongo.ts index 5af58eef..ace9c972 100644 --- a/packages/server/src/utils/databases/mongo.ts +++ b/packages/server/src/utils/databases/mongo.ts @@ -152,7 +152,7 @@ ${command ?? "wait $MONGOD_PID"}`; version: Number.parseInt(inspect.Version.Index), ...settings, }); - } catch (error) { + } catch (_error) { await docker.createService(settings); } }; diff --git a/packages/server/src/utils/databases/mysql.ts b/packages/server/src/utils/databases/mysql.ts index 5a691177..de28cfe6 100644 --- a/packages/server/src/utils/databases/mysql.ts +++ b/packages/server/src/utils/databases/mysql.ts @@ -104,7 +104,7 @@ export const buildMysql = async (mysql: MysqlNested) => { version: Number.parseInt(inspect.Version.Index), ...settings, }); - } catch (error) { + } catch (_error) { await docker.createService(settings); } }; diff --git a/packages/server/src/utils/databases/redis.ts b/packages/server/src/utils/databases/redis.ts index 724069a1..aef86280 100644 --- a/packages/server/src/utils/databases/redis.ts +++ b/packages/server/src/utils/databases/redis.ts @@ -95,7 +95,7 @@ export const buildRedis = async (redis: RedisNested) => { version: Number.parseInt(inspect.Version.Index), ...settings, }); - } catch (error) { + } catch (_error) { await docker.createService(settings); } }; diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index 8a1b0608..c4ced3f4 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -109,7 +109,7 @@ export const loadDockerComposeRemote = async ( if (!stdout) return null; const parsedConfig = load(stdout) as ComposeSpecification; return parsedConfig; - } catch (err) { + } catch (_err) { return null; } }; diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index 062e0722..71b7e4aa 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -100,7 +100,7 @@ export const containerExists = async (containerName: string) => { try { await container.inspect(); return true; - } catch (error) { + } catch (_error) { return false; } }; @@ -240,7 +240,7 @@ export const startServiceRemote = async (serverId: string, appName: string) => { export const removeService = async ( appName: string, serverId?: string | null, - deleteVolumes = false, + _deleteVolumes = false, ) => { try { const command = `docker service rm ${appName}`; diff --git a/packages/server/src/utils/gpu-setup.ts b/packages/server/src/utils/gpu-setup.ts index 6a6611b4..a815a00c 100644 --- a/packages/server/src/utils/gpu-setup.ts +++ b/packages/server/src/utils/gpu-setup.ts @@ -34,7 +34,7 @@ export async function checkGPUStatus(serverId?: string): Promise { ...gpuInfo, ...cudaInfo, }; - } catch (error) { + } catch (_error) { return { driverInstalled: false, driverVersion: undefined, @@ -315,7 +315,7 @@ const setupLocalServer = async (daemonConfig: any) => { try { await execAsync(setupCommands); - } catch (error) { + } catch (_error) { throw new Error( "Failed to configure GPU support. Please ensure you have sudo privileges and try again.", ); diff --git a/packages/server/src/utils/notifications/build-error.ts b/packages/server/src/utils/notifications/build-error.ts index 95936652..c873c9ab 100644 --- a/packages/server/src/utils/notifications/build-error.ts +++ b/packages/server/src/utils/notifications/build-error.ts @@ -18,7 +18,7 @@ interface Props { applicationType: string; errorMessage: string; buildLink: string; - adminId: string; + organizationId: string; } export const sendBuildErrorNotifications = async ({ @@ -27,14 +27,14 @@ export const sendBuildErrorNotifications = async ({ applicationType, errorMessage, buildLink, - adminId, + organizationId, }: Props) => { const date = new Date(); const unixDate = ~~(Number(date) / 1000); const notificationList = await db.query.notifications.findMany({ where: and( eq(notifications.appBuildError, true), - eq(notifications.adminId, adminId), + eq(notifications.organizationId, organizationId), ), with: { email: true, diff --git a/packages/server/src/utils/notifications/build-success.ts b/packages/server/src/utils/notifications/build-success.ts index 960f7a6a..ac470c49 100644 --- a/packages/server/src/utils/notifications/build-success.ts +++ b/packages/server/src/utils/notifications/build-success.ts @@ -18,7 +18,7 @@ interface Props { applicationName: string; applicationType: string; buildLink: string; - adminId: string; + organizationId: string; domains: Domain[]; } @@ -27,7 +27,7 @@ export const sendBuildSuccessNotifications = async ({ applicationName, applicationType, buildLink, - adminId, + organizationId, domains, }: Props) => { const date = new Date(); @@ -35,7 +35,7 @@ export const sendBuildSuccessNotifications = async ({ const notificationList = await db.query.notifications.findMany({ where: and( eq(notifications.appDeploy, true), - eq(notifications.adminId, adminId), + eq(notifications.organizationId, organizationId), ), with: { email: true, diff --git a/packages/server/src/utils/notifications/database-backup.ts b/packages/server/src/utils/notifications/database-backup.ts index 0b1d61f7..37a4a1ff 100644 --- a/packages/server/src/utils/notifications/database-backup.ts +++ b/packages/server/src/utils/notifications/database-backup.ts @@ -1,4 +1,3 @@ -import { error } from "node:console"; import { db } from "@dokploy/server/db"; import { notifications } from "@dokploy/server/db/schema"; import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup"; @@ -19,13 +18,13 @@ export const sendDatabaseBackupNotifications = async ({ databaseType, type, errorMessage, - adminId, + organizationId, }: { projectName: string; applicationName: string; databaseType: "postgres" | "mysql" | "mongodb" | "mariadb"; type: "error" | "success"; - adminId: string; + organizationId: string; errorMessage?: string; }) => { const date = new Date(); @@ -33,7 +32,7 @@ export const sendDatabaseBackupNotifications = async ({ const notificationList = await db.query.notifications.findMany({ where: and( eq(notifications.databaseBackup, true), - eq(notifications.adminId, adminId), + eq(notifications.organizationId, organizationId), ), with: { email: true, diff --git a/packages/server/src/utils/notifications/docker-cleanup.ts b/packages/server/src/utils/notifications/docker-cleanup.ts index b60e3b0a..b3959ccc 100644 --- a/packages/server/src/utils/notifications/docker-cleanup.ts +++ b/packages/server/src/utils/notifications/docker-cleanup.ts @@ -13,7 +13,7 @@ import { } from "./utils"; export const sendDockerCleanupNotifications = async ( - adminId: string, + organizationId: string, message = "Docker cleanup for dokploy", ) => { const date = new Date(); @@ -21,7 +21,7 @@ export const sendDockerCleanupNotifications = async ( const notificationList = await db.query.notifications.findMany({ where: and( eq(notifications.dockerCleanup, true), - eq(notifications.adminId, adminId), + eq(notifications.organizationId, organizationId), ), with: { email: true, diff --git a/packages/server/src/utils/notifications/server-threshold.ts b/packages/server/src/utils/notifications/server-threshold.ts index 32ff9b55..2e63ba25 100644 --- a/packages/server/src/utils/notifications/server-threshold.ts +++ b/packages/server/src/utils/notifications/server-threshold.ts @@ -18,7 +18,7 @@ interface ServerThresholdPayload { } export const sendServerThresholdNotifications = async ( - adminId: string, + organizationId: string, payload: ServerThresholdPayload, ) => { const date = new Date(payload.Timestamp); @@ -27,7 +27,7 @@ export const sendServerThresholdNotifications = async ( const notificationList = await db.query.notifications.findMany({ where: and( eq(notifications.serverThreshold, true), - eq(notifications.adminId, adminId), + eq(notifications.organizationId, organizationId), ), with: { email: true, diff --git a/packages/server/src/utils/process/execAsync.ts b/packages/server/src/utils/process/execAsync.ts index 19a16ac1..aee1e821 100644 --- a/packages/server/src/utils/process/execAsync.ts +++ b/packages/server/src/utils/process/execAsync.ts @@ -27,7 +27,7 @@ export const execAsyncRemote = async ( throw err; } stream - .on("close", (code: number, signal: string) => { + .on("close", (code: number, _signal: string) => { conn.end(); if (code === 0) { resolve({ stdout, stderr }); diff --git a/packages/server/src/utils/providers/bitbucket.ts b/packages/server/src/utils/providers/bitbucket.ts index 7059e65f..dd98a93b 100644 --- a/packages/server/src/utils/providers/bitbucket.ts +++ b/packages/server/src/utils/providers/bitbucket.ts @@ -176,7 +176,6 @@ export const getBitbucketCloneCommand = async ( bitbucketBranch, bitbucketId, serverId, - bitbucket, } = entity; if (!serverId) { diff --git a/packages/server/src/utils/providers/git.ts b/packages/server/src/utils/providers/git.ts index 8f8a3830..c26af3af 100644 --- a/packages/server/src/utils/providers/git.ts +++ b/packages/server/src/utils/providers/git.ts @@ -320,7 +320,7 @@ export const cloneGitRawRepository = async (entity: { outputPath, "--progress", ], - (data) => {}, + (_data) => {}, { env: { ...process.env, diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index 096f9e28..c380a920 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -162,8 +162,6 @@ export const getGitlabCloneCommand = async ( ) => { const { appName, - gitlabRepository, - gitlabOwner, gitlabPathNamespace, gitlabBranch, gitlabId, @@ -268,7 +266,7 @@ export const getGitlabRepositories = async (gitlabId?: string) => { if (groupName) { return full_path.toLowerCase().includes(groupName) && kind === "group"; } - return kind === "user"; + return kind === "member"; }); const mappedRepositories = filteredRepos.map((repo: any) => { return { @@ -328,14 +326,7 @@ export const getGitlabBranches = async (input: { }; export const cloneRawGitlabRepository = async (entity: Compose) => { - const { - appName, - gitlabRepository, - gitlabOwner, - gitlabBranch, - gitlabId, - gitlabPathNamespace, - } = entity; + const { appName, gitlabBranch, gitlabId, gitlabPathNamespace } = entity; if (!gitlabId) { throw new TRPCError({ @@ -442,7 +433,7 @@ export const testGitlabConnection = async ( if (groupName) { return full_path.toLowerCase().includes(groupName) && kind === "group"; } - return kind === "user"; + return kind === "member"; }); return filteredRepos.length; diff --git a/packages/server/src/utils/traefik/application.ts b/packages/server/src/utils/traefik/application.ts index 4434d858..61150abf 100644 --- a/packages/server/src/utils/traefik/application.ts +++ b/packages/server/src/utils/traefik/application.ts @@ -67,7 +67,7 @@ export const removeTraefikConfig = async ( if (fs.existsSync(configPath)) { await fs.promises.unlink(configPath); } - } catch (error) {} + } catch (_error) {} }; export const removeTraefikConfigRemote = async ( @@ -78,7 +78,7 @@ export const removeTraefikConfigRemote = async ( const { DYNAMIC_TRAEFIK_PATH } = paths(true); const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); await execAsyncRemote(serverId, `rm ${configPath}`); - } catch (error) {} + } catch (_error) {} }; export const loadOrCreateConfig = (appName: string): FileConfig => { @@ -110,7 +110,7 @@ export const loadOrCreateConfigRemote = async ( http: { routers: {}, services: {} }, }; return parsedConfig; - } catch (err) { + } catch (_err) { return fileConfig; } }; @@ -132,7 +132,7 @@ export const readRemoteConfig = async (serverId: string, appName: string) => { const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`); if (!stdout) return null; return stdout; - } catch (err) { + } catch (_err) { return null; } }; diff --git a/packages/server/src/utils/traefik/domain.ts b/packages/server/src/utils/traefik/domain.ts index a6c878e7..1ae3c05a 100644 --- a/packages/server/src/utils/traefik/domain.ts +++ b/packages/server/src/utils/traefik/domain.ts @@ -122,13 +122,25 @@ export const createRouterConfig = async ( if ((entryPoint === "websecure" && https) || !https) { // redirects for (const redirect of redirects) { - const middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`; + let middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`; + if (domain.domainType === "preview") { + middlewareName = `redirect-${appName.replace( + /^preview-(.+)-[^-]+$/, + "$1", + )}-${redirect.uniqueConfigKey}`; + } routerConfig.middlewares?.push(middlewareName); } // security if (security.length > 0) { - const middlewareName = `auth-${appName}`; + let middlewareName = `auth-${appName}`; + if (domain.domainType === "preview") { + middlewareName = `auth-${appName.replace( + /^preview-(.+)-[^-]+$/, + "$1", + )}`; + } routerConfig.middlewares?.push(middlewareName); } } diff --git a/packages/server/src/utils/traefik/middleware.ts b/packages/server/src/utils/traefik/middleware.ts index 60345f66..934d637e 100644 --- a/packages/server/src/utils/traefik/middleware.ts +++ b/packages/server/src/utils/traefik/middleware.ts @@ -95,7 +95,7 @@ export const loadRemoteMiddlewares = async (serverId: string) => { } const config = load(stdout) as FileConfig; return config; - } catch (error) { + } catch (_) { throw new Error(`File not found: ${configPath}`); } }; diff --git a/packages/server/src/utils/traefik/web-server.ts b/packages/server/src/utils/traefik/web-server.ts index 0aa4d35d..78046c67 100644 --- a/packages/server/src/utils/traefik/web-server.ts +++ b/packages/server/src/utils/traefik/web-server.ts @@ -1,14 +1,14 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; -import type { Admin } from "@dokploy/server/services/admin"; +import type { User } from "@dokploy/server/services/user"; import { dump, load } from "js-yaml"; import { loadOrCreateConfig, writeTraefikConfig } from "./application"; import type { FileConfig } from "./file-types"; import type { MainTraefikConfig } from "./types"; export const updateServerTraefik = ( - admin: Admin | null, + user: User | null, newHost: string | null, ) => { const appName = "dokploy"; @@ -22,7 +22,7 @@ export const updateServerTraefik = ( if (currentRouterConfig && newHost) { currentRouterConfig.rule = `Host(\`${newHost}\`)`; - if (admin?.certificateType === "letsencrypt") { + if (user?.certificateType === "letsencrypt") { config.http.routers[`${appName}-router-app-secure`] = { ...currentRouterConfig, entryPoints: ["websecure"], diff --git a/packages/server/src/verification/send-verification-email.tsx b/packages/server/src/verification/send-verification-email.tsx new file mode 100644 index 00000000..c673c0f7 --- /dev/null +++ b/packages/server/src/verification/send-verification-email.tsx @@ -0,0 +1,51 @@ +import { + sendDiscordNotification, + sendEmailNotification, +} from "../utils/notifications/utils"; +export const sendEmail = async ({ + email, + subject, + text, +}: { + email: string; + subject: string; + text: string; +}) => { + await sendEmailNotification( + { + fromAddress: process.env.SMTP_FROM_ADDRESS || "", + toAddresses: [email], + smtpServer: process.env.SMTP_SERVER || "", + smtpPort: Number(process.env.SMTP_PORT), + username: process.env.SMTP_USERNAME || "", + password: process.env.SMTP_PASSWORD || "", + }, + subject, + text, + ); + + return true; +}; + +export const sendDiscordNotificationWelcome = async (email: string) => { + await sendDiscordNotification( + { + webhookUrl: process.env.DISCORD_WEBHOOK_URL || "", + }, + { + title: "New User Registered", + color: 0x00ff00, + fields: [ + { + name: "Email", + value: email, + inline: true, + }, + ], + timestamp: new Date(), + footer: { + text: "Dokploy User Registration Notification", + }, + }, + ); +}; diff --git a/packages/server/tsconfig.server.json b/packages/server/tsconfig.server.json index 7f349eb8..33777c02 100644 --- a/packages/server/tsconfig.server.json +++ b/packages/server/tsconfig.server.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "disableSizeLimit": true, "module": "ESNext", "outDir": "dist/", "target": "ESNext", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c651464..03fb11f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -238,6 +238,9 @@ importers: bcrypt: specifier: 5.1.1 version: 5.1.1(encoding@0.1.13) + better-auth: + specifier: 1.2.0 + version: 1.2.0(typescript@5.5.3) bl: specifier: 6.0.11 version: 6.0.11 @@ -273,10 +276,10 @@ importers: version: 16.4.5 drizzle-orm: specifier: ^0.39.1 - version: 0.39.1(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) + version: 0.39.1(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) drizzle-zod: specifier: 0.5.1 - version: 0.5.1(drizzle-orm@0.39.1(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8) + version: 0.5.1(drizzle-orm@0.39.1(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8) fancy-ansi: specifier: ^0.1.3 version: 0.1.3 @@ -505,7 +508,7 @@ importers: version: 16.4.5 drizzle-orm: specifier: ^0.39.1 - version: 0.39.1(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) + version: 0.39.1(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) hono: specifier: ^4.5.8 version: 4.5.8 @@ -546,6 +549,9 @@ importers: packages/server: dependencies: + '@better-auth/utils': + specifier: 0.2.3 + version: 0.2.3 '@faker-js/faker': specifier: ^8.4.1 version: 8.4.1 @@ -555,6 +561,12 @@ importers: '@octokit/auth-app': specifier: ^6.0.4 version: 6.1.1 + '@oslojs/crypto': + specifier: 1.0.1 + version: 1.0.1 + '@oslojs/encoding': + specifier: 1.1.0 + version: 1.1.0 '@react-email/components': specifier: ^0.0.21 version: 0.0.21(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -567,6 +579,9 @@ importers: bcrypt: specifier: 5.1.1 version: 5.1.1(encoding@0.1.13) + better-auth: + specifier: 1.2.0 + version: 1.2.0(typescript@5.5.3) bl: specifier: 6.0.11 version: 6.0.11 @@ -582,12 +597,15 @@ importers: dotenv: specifier: 16.4.5 version: 16.4.5 + drizzle-dbml-generator: + specifier: 0.10.0 + version: 0.10.0(drizzle-orm@0.39.1(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)) drizzle-orm: specifier: ^0.39.1 - version: 0.39.1(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) + version: 0.39.1(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) drizzle-zod: specifier: 0.5.1 - version: 0.5.1(drizzle-orm@0.39.1(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8) + version: 0.5.1(drizzle-orm@0.39.1(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8) hi-base32: specifier: ^0.5.1 version: 0.5.1 @@ -748,6 +766,12 @@ packages: '@balena/dockerignore@1.0.2': resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + '@better-auth/utils@0.2.3': + resolution: {integrity: sha512-Ap1GaSmo6JYhJhxJOpUB0HobkKPTNzfta+bLV89HfpyCAHN7p8ntCrmNFHNAVD0F6v0mywFVEUg1FUhNCc81Rw==} + + '@better-fetch/fetch@1.1.15': + resolution: {integrity: sha512-0Bl8YYj1f8qCTNHeSn5+1DWv2hy7rLBrQ8rS8Y9XYloiwZEfc3k4yspIG0llRxafxqhGCwlGRg+F8q1HZRCMXA==} + '@biomejs/biome@1.9.4': resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} engines: {node: '>=14.21.3'} @@ -1518,6 +1542,9 @@ packages: '@hapi/bourne@3.0.0': resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} + '@hexagon/base64@1.1.28': + resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} + '@hono/node-server@1.12.1': resolution: {integrity: sha512-C9l+08O8xtXB7Ppmy8DjBFH1hYji7JKzsU32Yt1poIIbdPp6S7aOI8IldDHD9YFJ55lv2c21ovNrmxatlHfhAg==} engines: {node: '>=18.14.1'} @@ -1697,6 +1724,9 @@ packages: '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + '@levischuck/tiny-cbor@0.2.11': + resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} + '@lezer/common@1.2.1': resolution: {integrity: sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==} @@ -1807,10 +1837,17 @@ packages: cpu: [x64] os: [win32] + '@noble/ciphers@0.6.0': + resolution: {integrity: sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==} + '@noble/hashes@1.5.0': resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@1.7.1': + resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} + engines: {node: ^14.21.3 || >=16} + '@node-rs/argon2-android-arm-eabi@1.7.0': resolution: {integrity: sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg==} engines: {node: '>= 10'} @@ -2136,6 +2173,33 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@oslojs/asn1@1.0.0': + resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==} + + '@oslojs/binary@1.0.0': + resolution: {integrity: sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==} + + '@oslojs/crypto@1.0.1': + resolution: {integrity: sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==} + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@peculiar/asn1-android@2.3.15': + resolution: {integrity: sha512-8U2TIj59cRlSXTX2d0mzUKP7whfWGFMzTeC3qPgAbccXFrPNZLaDhpNEdG5U2QZ/tBv/IHlCJ8s+KYXpJeop6w==} + + '@peculiar/asn1-ecc@2.3.15': + resolution: {integrity: sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==} + + '@peculiar/asn1-rsa@2.3.15': + resolution: {integrity: sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==} + + '@peculiar/asn1-schema@2.3.15': + resolution: {integrity: sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==} + + '@peculiar/asn1-x509@2.3.15': + resolution: {integrity: sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -3147,6 +3211,13 @@ packages: '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@simplewebauthn/browser@13.1.0': + resolution: {integrity: sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg==} + + '@simplewebauthn/server@13.1.1': + resolution: {integrity: sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==} + engines: {node: '>=20.0.0'} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -3762,6 +3833,10 @@ packages: asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + asn1js@3.0.5: + resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} + engines: {node: '>=12.0.0'} + assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} @@ -3804,6 +3879,12 @@ packages: before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + better-auth@1.2.0: + resolution: {integrity: sha512-eIRGOXfix25bh4fgs8jslZAZssufpIkxfEeEokQu5G4wICoDee1wPctkFb8v80PvhtI4dPm28SuAoZaAdRc6Wg==} + + better-call@1.0.3: + resolution: {integrity: sha512-DUKImKoDIy5UtCvQbHTg0wuBRse6gu1Yvznn7+1B3I5TeY8sclRPFce0HI+4WF2bcb+9PqmkET8nXZubrHQh9A==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -4389,6 +4470,11 @@ packages: resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} engines: {node: '>=4'} + drizzle-dbml-generator@0.10.0: + resolution: {integrity: sha512-cMZq9E3U3RlmE0uBeXyc6oWJ0royOkC6HiTlc9LDeMe+W87poZTzKoNYUyAxZrs4Q1RQtob+cGKiefV4ZoI8HA==} + peerDependencies: + drizzle-orm: '>=0.36.0' + drizzle-kit@0.30.4: resolution: {integrity: sha512-B2oJN5UkvwwNHscPWXDG5KqAixu7AUzZ3qbe++KU9SsQ+cZWR4DXEPYcvWplyFAno0dhRJECNEhNxiDmFaPGyQ==} hasBin: true @@ -5120,6 +5206,9 @@ packages: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true + jose@5.9.6: + resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -5223,6 +5312,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kysely@0.27.5: + resolution: {integrity: sha512-s7hZHcQeSNKpzCkHRm8yA+0JPLjncSWnjb+2TIElwS2JAqYr+Kv3Ess+9KFfJS0C1xcQ1i9NkNHpWwCYpHMWsA==} + engines: {node: '>=14.0.0'} + leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} @@ -5608,6 +5701,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanostores@0.11.3: + resolution: {integrity: sha512-TUes3xKIX33re4QzdxwZ6tdbodjmn3tWXCEc1uokiEmo14sI1EaGYNs2k3bU2pyyGNmBqFGAVl6jAGWd06AVIg==} + engines: {node: ^18.0.0 || >=20.0.0} + napi-build-utils@1.0.2: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} @@ -6058,6 +6155,13 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.1.3: + resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} + engines: {node: '>=6.0.0'} + qrcode@1.5.4: resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} engines: {node: '>=10.13.0'} @@ -6403,6 +6507,9 @@ packages: resolution: {integrity: sha512-cfmm3tqdnbuYw2FBmRTPBDaohYEbMJ3211T35o6eZdr4d7v69+ZeK1Av84Br7FLj2dlzyeZSbN6qTuXXE6dawQ==} engines: {node: '>=14.0'} + rou3@0.5.1: + resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -6460,6 +6567,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -6872,6 +6982,9 @@ packages: tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.16.2: resolution: {integrity: sha512-C1uWweJDgdtX2x600HjaFaucXTilT7tgUZHbOE4+ypskZ1OP8CRCSDkCxG6Vya9EwaFIVagWwpaVAn5wzypaqQ==} engines: {node: '>=18.0.0'} @@ -7004,6 +7117,14 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + valibot@1.0.0-beta.15: + resolution: {integrity: sha512-BKy8XosZkDHWmYC+cJG74LBzP++Gfntwi33pP3D3RKztz2XV9jmFWnkOi21GoqARP8wAWARwhV6eTr1JcWzjGw==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} @@ -7237,6 +7358,9 @@ packages: zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zod@3.24.1: + resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -7266,6 +7390,12 @@ snapshots: '@balena/dockerignore@1.0.2': {} + '@better-auth/utils@0.2.3': + dependencies: + uncrypto: 0.1.3 + + '@better-fetch/fetch@1.1.15': {} + '@biomejs/biome@1.9.4': optionalDependencies: '@biomejs/cli-darwin-arm64': 1.9.4 @@ -7832,6 +7962,8 @@ snapshots: '@hapi/bourne@3.0.0': {} + '@hexagon/base64@1.1.28': {} + '@hono/node-server@1.12.1': {} '@hono/zod-validator@0.3.0(hono@4.5.8)(zod@3.23.8)': @@ -7981,6 +8113,8 @@ snapshots: '@leichtgewicht/ip-codec@2.0.5': {} + '@levischuck/tiny-cbor@0.2.11': {} + '@lezer/common@1.2.1': {} '@lezer/highlight@1.2.0': @@ -8071,8 +8205,12 @@ snapshots: '@next/swc-win32-x64-msvc@15.0.1': optional: true + '@noble/ciphers@0.6.0': {} + '@noble/hashes@1.5.0': {} + '@noble/hashes@1.7.1': {} + '@node-rs/argon2-android-arm-eabi@1.7.0': optional: true @@ -8401,6 +8539,52 @@ snapshots: '@one-ini/wasm@0.1.1': {} + '@oslojs/asn1@1.0.0': + dependencies: + '@oslojs/binary': 1.0.0 + + '@oslojs/binary@1.0.0': {} + + '@oslojs/crypto@1.0.1': + dependencies: + '@oslojs/asn1': 1.0.0 + '@oslojs/binary': 1.0.0 + + '@oslojs/encoding@1.1.0': {} + + '@peculiar/asn1-android@2.3.15': + dependencies: + '@peculiar/asn1-schema': 2.3.15 + asn1js: 3.0.5 + tslib: 2.8.1 + + '@peculiar/asn1-ecc@2.3.15': + dependencies: + '@peculiar/asn1-schema': 2.3.15 + '@peculiar/asn1-x509': 2.3.15 + asn1js: 3.0.5 + tslib: 2.8.1 + + '@peculiar/asn1-rsa@2.3.15': + dependencies: + '@peculiar/asn1-schema': 2.3.15 + '@peculiar/asn1-x509': 2.3.15 + asn1js: 3.0.5 + tslib: 2.8.1 + + '@peculiar/asn1-schema@2.3.15': + dependencies: + asn1js: 3.0.5 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + '@peculiar/asn1-x509@2.3.15': + dependencies: + '@peculiar/asn1-schema': 2.3.15 + asn1js: 3.0.5 + pvtsutils: 1.3.6 + tslib: 2.8.1 + '@pkgjs/parseargs@0.11.0': optional: true @@ -9394,6 +9578,18 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 + '@simplewebauthn/browser@13.1.0': {} + + '@simplewebauthn/server@13.1.1': + dependencies: + '@hexagon/base64': 1.1.28 + '@levischuck/tiny-cbor': 0.2.11 + '@peculiar/asn1-android': 2.3.15 + '@peculiar/asn1-ecc': 2.3.15 + '@peculiar/asn1-rsa': 2.3.15 + '@peculiar/asn1-schema': 2.3.15 + '@peculiar/asn1-x509': 2.3.15 + '@sinclair/typebox@0.27.8': {} '@sindresorhus/is@5.6.0': {} @@ -10304,6 +10500,12 @@ snapshots: dependencies: safer-buffer: 2.1.2 + asn1js@3.0.5: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.1.3 + tslib: 2.8.1 + assertion-error@1.1.0: {} async-await-queue@2.1.4: {} @@ -10352,6 +10554,31 @@ snapshots: before-after-hook@2.2.3: {} + better-auth@1.2.0(typescript@5.5.3): + dependencies: + '@better-auth/utils': 0.2.3 + '@better-fetch/fetch': 1.1.15 + '@noble/ciphers': 0.6.0 + '@noble/hashes': 1.7.1 + '@simplewebauthn/browser': 13.1.0 + '@simplewebauthn/server': 13.1.1 + better-call: 1.0.3 + defu: 6.1.4 + jose: 5.9.6 + kysely: 0.27.5 + nanostores: 0.11.3 + valibot: 1.0.0-beta.15(typescript@5.5.3) + zod: 3.24.1 + transitivePeerDependencies: + - typescript + + better-call@1.0.3: + dependencies: + '@better-fetch/fetch': 1.1.15 + rou3: 0.5.1 + set-cookie-parser: 2.7.1 + uncrypto: 0.1.3 + binary-extensions@2.3.0: {} bindings@1.5.0: @@ -10948,6 +11175,10 @@ snapshots: drange@1.1.1: {} + drizzle-dbml-generator@0.10.0(drizzle-orm@0.39.1(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)): + dependencies: + drizzle-orm: 0.39.1(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) + drizzle-kit@0.30.4: dependencies: '@drizzle-team/brocli': 0.10.2 @@ -10957,16 +11188,17 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.39.1(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7): + drizzle-orm@0.39.1(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7): optionalDependencies: '@types/react': 18.3.5 + kysely: 0.27.5 postgres: 3.4.4 react: 18.2.0 sqlite3: 5.1.7 - drizzle-zod@0.5.1(drizzle-orm@0.39.1(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8): + drizzle-zod@0.5.1(drizzle-orm@0.39.1(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8): dependencies: - drizzle-orm: 0.39.1(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) + drizzle-orm: 0.39.1(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) zod: 3.23.8 eastasianwidth@0.2.0: {} @@ -11686,6 +11918,8 @@ snapshots: jiti@1.21.6: {} + jose@5.9.6: {} + joycon@3.1.1: {} js-base64@3.7.7: {} @@ -11843,6 +12077,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kysely@0.27.5: {} + leac@0.6.0: {} lefthook-darwin-arm64@1.8.4: @@ -12215,6 +12451,8 @@ snapshots: nanoid@3.3.7: {} + nanostores@0.11.3: {} + napi-build-utils@1.0.2: optional: true @@ -12692,6 +12930,12 @@ snapshots: punycode@2.3.1: {} + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.1.3: {} + qrcode@1.5.4: dependencies: dijkstrajs: 1.0.3 @@ -13054,6 +13298,8 @@ snapshots: rotating-file-stream@3.2.3: {} + rou3@0.5.1: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -13106,6 +13352,8 @@ snapshots: set-blocking@2.0.0: {} + set-cookie-parser@2.7.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -13617,6 +13865,8 @@ snapshots: tslib@2.6.3: {} + tslib@2.8.1: {} + tsx@4.16.2: dependencies: esbuild: 0.21.5 @@ -13736,6 +13986,10 @@ snapshots: v8-compile-cache-lib@3.0.1: optional: true + valibot@1.0.0-beta.15(typescript@5.5.3): + optionalDependencies: + typescript: 5.5.3 + victory-vendor@36.9.2: dependencies: '@types/d3-array': 3.2.1 @@ -13994,3 +14248,5 @@ snapshots: zod: 3.23.8 zod@3.23.8: {} + + zod@3.24.1: {}