mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-03 13:05:23 +02:00
Compare commits
79 Commits
v0.18.2
...
feat/bette
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
790894ab93 | ||
|
|
5a1145996d | ||
|
|
a9e12c2b18 | ||
|
|
b73e4102dd | ||
|
|
c7d47a6003 | ||
|
|
8c28223343 | ||
|
|
7abe060fcf | ||
|
|
0e8e92c715 | ||
|
|
e1632cbdb3 | ||
|
|
90156da570 | ||
|
|
9856502ece | ||
|
|
a8d1471b16 | ||
|
|
27736c7c97 | ||
|
|
e7db0ccb70 | ||
|
|
4a1a14aeb4 | ||
|
|
ed62b4e1a3 | ||
|
|
515d65d993 | ||
|
|
78c72b6337 | ||
|
|
e3e35ce792 | ||
|
|
6d0e195a4d | ||
|
|
53ce5e57fa | ||
|
|
87b12ff6e9 | ||
|
|
8b71f963cc | ||
|
|
1c5cc5a0db | ||
|
|
d233f2c764 | ||
|
|
1bbb4c9b64 | ||
|
|
6ec60b6bab | ||
|
|
55abac3f2f | ||
|
|
b6c29ccf05 | ||
|
|
ca217affe6 | ||
|
|
5c24281f72 | ||
|
|
bc901bcb25 | ||
|
|
7c0d223e17 | ||
|
|
74ee024cf9 | ||
|
|
140a871275 | ||
|
|
d1f72a2e20 | ||
|
|
0d525398a8 | ||
|
|
7c62408070 | ||
|
|
23f1ce17de | ||
|
|
60eee55f2d | ||
|
|
8f562eefc1 | ||
|
|
6179cef1ee | ||
|
|
b7112b89fd | ||
|
|
030c8a312d | ||
|
|
1db6ba94f4 | ||
|
|
afd3d2eea3 | ||
|
|
8bd72a8a34 | ||
|
|
fafc238e70 | ||
|
|
c04bf3c7e0 | ||
|
|
6b9fd596e5 | ||
|
|
7e36433144 | ||
|
|
0a6554c275 | ||
|
|
fcc55355f2 | ||
|
|
78e606876a | ||
|
|
7e99baa267 | ||
|
|
92c03bb7cc | ||
|
|
3a5ecb2f64 | ||
|
|
c0a00f4957 | ||
|
|
a8f94540f9 | ||
|
|
3e2cfe6eb8 | ||
|
|
b2d5090b36 | ||
|
|
0a0f53e9de | ||
|
|
17ce03e529 | ||
|
|
f44512a437 | ||
|
|
8379068fe3 | ||
|
|
a71de72a3c | ||
|
|
b024060eed | ||
|
|
56b26ce0d5 | ||
|
|
a9e3a65782 | ||
|
|
7a472df753 | ||
|
|
bd809c8dca | ||
|
|
48642979c5 | ||
|
|
46411a5f4e | ||
|
|
82cf0643d7 | ||
|
|
65780ee852 | ||
|
|
d424524d69 | ||
|
|
6f2148c060 | ||
|
|
79fca72d06 | ||
|
|
62a3707c10 |
BIN
.github/sponsors/openalternative.png
vendored
Normal file
BIN
.github/sponsors/openalternative.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -93,6 +93,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
|
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
|
||||||
<a href="https://startupfa.me/?ref=dokploy "><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
|
<a href="https://startupfa.me/?ref=dokploy "><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
|
||||||
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
|
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
|
||||||
|
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Community Backers 🤝
|
### Community Backers 🤝
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const baseApp: ApplicationNested = {
|
|||||||
previewWildcard: "",
|
previewWildcard: "",
|
||||||
project: {
|
project: {
|
||||||
env: "",
|
env: "",
|
||||||
adminId: "",
|
organizationId: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ vi.mock("node:fs", () => ({
|
|||||||
default: fs,
|
default: fs,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import type { Admin, FileConfig } from "@dokploy/server";
|
import type { FileConfig, User } from "@dokploy/server";
|
||||||
import {
|
import {
|
||||||
createDefaultServerTraefikConfig,
|
createDefaultServerTraefikConfig,
|
||||||
loadOrCreateConfig,
|
loadOrCreateConfig,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { beforeEach, expect, test, vi } from "vitest";
|
import { beforeEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
const baseAdmin: Admin = {
|
const baseAdmin: User = {
|
||||||
enablePaidFeatures: false,
|
enablePaidFeatures: false,
|
||||||
metricsConfig: {
|
metricsConfig: {
|
||||||
containers: {
|
containers: {
|
||||||
@@ -40,9 +40,7 @@ const baseAdmin: Admin = {
|
|||||||
cleanupCacheApplications: false,
|
cleanupCacheApplications: false,
|
||||||
cleanupCacheOnCompose: false,
|
cleanupCacheOnCompose: false,
|
||||||
cleanupCacheOnPreviews: false,
|
cleanupCacheOnPreviews: false,
|
||||||
createdAt: "",
|
createdAt: new Date(),
|
||||||
authId: "",
|
|
||||||
adminId: "string",
|
|
||||||
serverIp: null,
|
serverIp: null,
|
||||||
certificateType: "none",
|
certificateType: "none",
|
||||||
host: null,
|
host: null,
|
||||||
@@ -53,6 +51,31 @@ const baseAdmin: Admin = {
|
|||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
stripeCustomerId: "",
|
stripeCustomerId: "",
|
||||||
stripeSubscriptionId: "",
|
stripeSubscriptionId: "",
|
||||||
|
accessedProjects: [],
|
||||||
|
accessedServices: [],
|
||||||
|
banExpires: new Date(),
|
||||||
|
banned: true,
|
||||||
|
banReason: "",
|
||||||
|
canAccessToAPI: false,
|
||||||
|
canCreateProjects: false,
|
||||||
|
canDeleteProjects: false,
|
||||||
|
canDeleteServices: false,
|
||||||
|
canAccessToDocker: false,
|
||||||
|
canAccessToSSHKeys: false,
|
||||||
|
canCreateServices: false,
|
||||||
|
canAccessToTraefikFiles: false,
|
||||||
|
canAccessToGitProviders: false,
|
||||||
|
email: "",
|
||||||
|
expirationDate: "",
|
||||||
|
id: "",
|
||||||
|
isRegistered: false,
|
||||||
|
name: "",
|
||||||
|
createdAt2: new Date().toISOString(),
|
||||||
|
emailVerified: false,
|
||||||
|
image: "",
|
||||||
|
token: "",
|
||||||
|
updatedAt: new Date(),
|
||||||
|
twoFactorEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const baseApp: ApplicationNested = {
|
|||||||
previewWildcard: "",
|
previewWildcard: "",
|
||||||
project: {
|
project: {
|
||||||
env: "",
|
env: "",
|
||||||
adminId: "",
|
organizationId: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||||
import { RandomizeCompose } from "./randomize-compose";
|
import { ShowUtilities } from "./show-utilities";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
composeId: string;
|
composeId: string;
|
||||||
@@ -125,7 +125,7 @@ services:
|
|||||||
</Form>
|
</Form>
|
||||||
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
||||||
<div className="w-full flex flex-col lg:flex-row gap-4 items-end">
|
<div className="w-full flex flex-col lg:flex-row gap-4 items-end">
|
||||||
<RandomizeCompose composeId={composeId} />
|
<ShowUtilities composeId={composeId} />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { AlertTriangle } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
isolatedDeployment: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const IsolatedDeployment = ({ composeId }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [compose, setCompose] = useState<string>("");
|
||||||
|
const { mutateAsync, error, isError } =
|
||||||
|
api.compose.isolatedDeployment.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
|
||||||
|
|
||||||
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
|
{ composeId },
|
||||||
|
{ enabled: !!composeId },
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
defaultValues: {
|
||||||
|
isolatedDeployment: false,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
randomizeCompose();
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
isolatedDeployment: data?.isolatedDeployment || false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: Schema) => {
|
||||||
|
await updateCompose({
|
||||||
|
composeId,
|
||||||
|
isolatedDeployment: formData?.isolatedDeployment || false,
|
||||||
|
})
|
||||||
|
.then(async (data) => {
|
||||||
|
randomizeCompose();
|
||||||
|
refetch();
|
||||||
|
toast.success("Compose updated");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error updating the compose");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const randomizeCompose = async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
composeId,
|
||||||
|
suffix: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(async (data) => {
|
||||||
|
await utils.project.all.invalidate();
|
||||||
|
setCompose(data);
|
||||||
|
toast.success("Compose Isolated");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error isolating the compose");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Isolate Deployment</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Use this option to isolate the deployment of this compose file.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
||||||
|
<span>
|
||||||
|
This feature creates an isolated environment for your deployment by
|
||||||
|
adding unique prefixes to all resources. It establishes a dedicated
|
||||||
|
network based on your compose file's name, ensuring your services run
|
||||||
|
in isolation. This prevents conflicts when running multiple instances
|
||||||
|
of the same template or services with identical names.
|
||||||
|
</span>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">
|
||||||
|
Resources that will be isolated:
|
||||||
|
</h4>
|
||||||
|
<ul className="list-disc list-inside">
|
||||||
|
<li>Docker volumes</li>
|
||||||
|
<li>Docker networks</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
id="hook-form-add-project"
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
{isError && (
|
||||||
|
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
||||||
|
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||||
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
|
{error?.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-col gap-4 w-full ">
|
||||||
|
<div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isolatedDeployment"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Enable Randomize ({data?.appName})</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Enable randomize to the compose file.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
||||||
|
<Button
|
||||||
|
form="hook-form-add-project"
|
||||||
|
type="submit"
|
||||||
|
className="lg:w-fit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Label>Preview</Label>
|
||||||
|
<pre>
|
||||||
|
<CodeEditor
|
||||||
|
value={compose || ""}
|
||||||
|
language="yaml"
|
||||||
|
readOnly
|
||||||
|
height="50rem"
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { CardTitle } from "@/components/ui/card";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -20,11 +16,6 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
|
||||||
InputOTP,
|
|
||||||
InputOTPGroup,
|
|
||||||
InputOTPSlot,
|
|
||||||
} from "@/components/ui/input-otp";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -70,6 +61,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
|||||||
const suffix = form.watch("suffix");
|
const suffix = form.watch("suffix");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
randomizeCompose();
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
suffix: data?.suffix || "",
|
suffix: data?.suffix || "",
|
||||||
@@ -110,126 +102,117 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<div className="w-full">
|
||||||
<DialogTrigger asChild onClick={() => randomizeCompose()}>
|
<DialogHeader>
|
||||||
<Button className="max-lg:w-full" variant="outline">
|
<DialogTitle>Randomize Compose (Experimental)</DialogTitle>
|
||||||
<Dices className="h-4 w-4" />
|
<DialogDescription>
|
||||||
Randomize Compose
|
Use this in case you want to deploy the same compose file and you have
|
||||||
</Button>
|
conflicts with some property like volumes, networks, etc.
|
||||||
</DialogTrigger>
|
</DialogDescription>
|
||||||
<DialogContent className="sm:max-w-6xl max-h-[50rem] overflow-y-auto">
|
</DialogHeader>
|
||||||
<DialogHeader>
|
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
||||||
<DialogTitle>Randomize Compose (Experimental)</DialogTitle>
|
<span>
|
||||||
<DialogDescription>
|
This will randomize the compose file and will add a suffix to the
|
||||||
Use this in case you want to deploy the same compose file and you
|
property to avoid conflicts
|
||||||
have conflicts with some property like volumes, networks, etc.
|
</span>
|
||||||
</DialogDescription>
|
<ul className="list-disc list-inside">
|
||||||
</DialogHeader>
|
<li>volumes</li>
|
||||||
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
<li>networks</li>
|
||||||
<span>
|
<li>services</li>
|
||||||
This will randomize the compose file and will add a suffix to the
|
<li>configs</li>
|
||||||
property to avoid conflicts
|
<li>secrets</li>
|
||||||
</span>
|
</ul>
|
||||||
<ul className="list-disc list-inside">
|
<AlertBlock type="info">
|
||||||
<li>volumes</li>
|
When you activate this option, we will include a env `COMPOSE_PREFIX`
|
||||||
<li>networks</li>
|
variable to the compose file so you can use it in your compose file.
|
||||||
<li>services</li>
|
</AlertBlock>
|
||||||
<li>configs</li>
|
</div>
|
||||||
<li>secrets</li>
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
</ul>
|
<Form {...form}>
|
||||||
<AlertBlock type="info">
|
<form
|
||||||
When you activate this option, we will include a env
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
`COMPOSE_PREFIX` variable to the compose file so you can use it in
|
id="hook-form-add-project"
|
||||||
your compose file.
|
className="grid w-full gap-4"
|
||||||
</AlertBlock>
|
>
|
||||||
</div>
|
{isError && (
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
||||||
<Form {...form}>
|
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||||
<form
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
{error?.message}
|
||||||
id="hook-form-add-project"
|
</span>
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
{isError && (
|
|
||||||
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-col gap-4 w-full ">
|
|
||||||
<div>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="suffix"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-col justify-center max-sm:items-center w-full">
|
|
||||||
<FormLabel>Suffix</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter a suffix (Optional, example: prod)"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="randomize"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Apply Randomize</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Apply randomize to the compose file.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
|
||||||
<Button
|
|
||||||
form="hook-form-add-project"
|
|
||||||
type="submit"
|
|
||||||
className="lg:w-fit"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={async () => {
|
|
||||||
await randomizeCompose();
|
|
||||||
}}
|
|
||||||
className="lg:w-fit"
|
|
||||||
>
|
|
||||||
Random
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<pre>
|
)}
|
||||||
<CodeEditor
|
|
||||||
value={compose || ""}
|
<div className="flex flex-col lg:flex-col gap-4 w-full ">
|
||||||
language="yaml"
|
<div>
|
||||||
readOnly
|
<FormField
|
||||||
height="50rem"
|
control={form.control}
|
||||||
|
name="suffix"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col justify-center max-sm:items-center w-full mt-4">
|
||||||
|
<FormLabel>Suffix</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a suffix (Optional, example: prod)"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</pre>
|
<FormField
|
||||||
</form>
|
control={form.control}
|
||||||
</Form>
|
name="randomize"
|
||||||
</DialogContent>
|
render={({ field }) => (
|
||||||
</Dialog>
|
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Apply Randomize</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Apply randomize to the compose file.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
||||||
|
<Button
|
||||||
|
form="hook-form-add-project"
|
||||||
|
type="submit"
|
||||||
|
className="lg:w-fit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
await randomizeCompose();
|
||||||
|
}}
|
||||||
|
className="lg:w-fit"
|
||||||
|
>
|
||||||
|
Random
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre>
|
||||||
|
<CodeEditor
|
||||||
|
value={compose || ""}
|
||||||
|
language="yaml"
|
||||||
|
readOnly
|
||||||
|
height="50rem"
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { IsolatedDeployment } from "./isolated-deployment";
|
||||||
|
import { RandomizeCompose } from "./randomize-compose";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowUtilities = ({ composeId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost">Show Utilities</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Utilities </DialogTitle>
|
||||||
|
<DialogDescription>Modify the application data</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Tabs defaultValue="isolated">
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="isolated">Isolated Deployment</TabsTrigger>
|
||||||
|
<TabsTrigger value="randomize">Randomize Compose</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="randomize" className="pt-5">
|
||||||
|
<RandomizeCompose composeId={composeId} />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="isolated" className="pt-5">
|
||||||
|
<IsolatedDeployment composeId={composeId} />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -79,7 +79,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
|
|||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
error: queryError,
|
error: queryError,
|
||||||
} = api.admin.getContainerMetrics.useQuery(
|
} = api.user.getContainerMetrics.useQuery(
|
||||||
{
|
{
|
||||||
url: baseUrl,
|
url: baseUrl,
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const ShowPaidMonitoring = ({
|
|||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
error: queryError,
|
error: queryError,
|
||||||
} = api.admin.getServerMetrics.useQuery(
|
} = api.user.getServerMetrics.useQuery(
|
||||||
{
|
{
|
||||||
url: BASE_URL,
|
url: BASE_URL,
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
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 { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { PenBoxIcon, Plus, SquarePen } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
organizationId?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
export function AddOrganization({ organizationId, children }: Props) {
|
||||||
|
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 [open, setOpen] = useState(false);
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (organization) {
|
||||||
|
setName(organization.name);
|
||||||
|
}
|
||||||
|
}, [organization]);
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
await mutateAsync({ name, organizationId: organizationId ?? "" })
|
||||||
|
.then(() => {
|
||||||
|
setOpen(false);
|
||||||
|
toast.success(
|
||||||
|
`Organization ${organizationId ? "updated" : "created"} successfully`,
|
||||||
|
);
|
||||||
|
utils.organization.all.invalidate();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(
|
||||||
|
`Failed to ${organizationId ? "update" : "create"} organization`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{organizationId ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="group cursor-pointer hover:bg-blue-500/10"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="gap-2 p-2"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
|
||||||
|
<Plus className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div className="font-medium text-muted-foreground">
|
||||||
|
Add organization
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{organizationId ? "Update organization" : "Add organization"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{organizationId
|
||||||
|
? "Update the organization name"
|
||||||
|
: "Create a new organization to manage your projects."}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="name" className="text-right">
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" onClick={handleSubmit} isLoading={isLoading}>
|
||||||
|
{organizationId ? "Update organization" : "Create organization"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon, SquarePen } from "lucide-react";
|
import { PlusIcon, SquarePen } from "lucide-react";
|
||||||
@@ -97,6 +98,18 @@ export const HandleProject = ({ projectId }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
// useEffect(() => {
|
||||||
|
// const getUsers = async () => {
|
||||||
|
// const users = await authClient.admin.listUsers({
|
||||||
|
// query: {
|
||||||
|
// limit: 100,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// console.log(users);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// getUsers();
|
||||||
|
// });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -50,15 +51,7 @@ import { ProjectEnvironment } from "./project-environment";
|
|||||||
export const ShowProjects = () => {
|
export const ShowProjects = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data, isLoading } = api.project.all.useQuery();
|
const { data, isLoading } = api.project.all.useQuery();
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
@@ -90,7 +83,7 @@ export const ShowProjects = () => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{(auth?.rol === "admin" || user?.canCreateProjects) && (
|
{(auth?.role === "owner" || auth?.user?.canCreateProjects) && (
|
||||||
<div className="">
|
<div className="">
|
||||||
<HandleProject />
|
<HandleProject />
|
||||||
</div>
|
</div>
|
||||||
@@ -176,8 +169,11 @@ export const ShowProjects = () => {
|
|||||||
<div key={app.applicationId}>
|
<div key={app.applicationId}>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuLabel className="font-normal capitalize text-xs">
|
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
||||||
{app.name}
|
{app.name}
|
||||||
|
<StatusTooltip
|
||||||
|
status={app.applicationStatus}
|
||||||
|
/>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{app.domains.map((domain) => (
|
{app.domains.map((domain) => (
|
||||||
@@ -209,8 +205,11 @@ export const ShowProjects = () => {
|
|||||||
<div key={comp.composeId}>
|
<div key={comp.composeId}>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuLabel className="font-normal capitalize text-xs">
|
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
||||||
{comp.name}
|
{comp.name}
|
||||||
|
<StatusTooltip
|
||||||
|
status={comp.composeStatus}
|
||||||
|
/>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{comp.domains.map((domain) => (
|
{comp.domains.map((domain) => (
|
||||||
@@ -286,8 +285,8 @@ export const ShowProjects = () => {
|
|||||||
<div
|
<div
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{(auth?.rol === "admin" ||
|
{(auth?.role === "owner" ||
|
||||||
user?.canDeleteProjects) && (
|
auth?.user?.canDeleteProjects) && (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger className="w-full">
|
<AlertDialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import {
|
import {
|
||||||
type Services,
|
type Services,
|
||||||
extractServices,
|
extractServices,
|
||||||
@@ -35,8 +36,10 @@ export const SearchCommand = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [search, setSearch] = React.useState("");
|
const [search, setSearch] = React.useState("");
|
||||||
|
const { data: session } = authClient.useSession();
|
||||||
const { data } = api.project.all.useQuery();
|
const { data } = api.project.all.useQuery(undefined, {
|
||||||
|
enabled: !!session,
|
||||||
|
});
|
||||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const calculatePrice = (count: number, isAnnual = false) => {
|
|||||||
};
|
};
|
||||||
export const ShowBilling = () => {
|
export const ShowBilling = () => {
|
||||||
const { data: servers } = api.server.all.useQuery(undefined);
|
const { data: servers } = api.server.all.useQuery(undefined);
|
||||||
const { data: admin } = api.admin.one.useQuery();
|
const { data: admin } = api.user.get.useQuery();
|
||||||
const { data, isLoading } = api.stripe.getProducts.useQuery();
|
const { data, isLoading } = api.stripe.getProducts.useQuery();
|
||||||
const { mutateAsync: createCheckoutSession } =
|
const { mutateAsync: createCheckoutSession } =
|
||||||
api.stripe.createCheckoutSession.useMutation();
|
api.stripe.createCheckoutSession.useMutation();
|
||||||
@@ -70,7 +70,7 @@ export const ShowBilling = () => {
|
|||||||
return isAnnual ? interval === "year" : interval === "month";
|
return isAnnual ? interval === "year" : interval === "month";
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxServers = admin?.serversQuantity ?? 1;
|
const maxServers = admin?.user.serversQuantity ?? 1;
|
||||||
const percentage = ((servers?.length ?? 0) / maxServers) * 100;
|
const percentage = ((servers?.length ?? 0) / maxServers) * 100;
|
||||||
const safePercentage = Math.min(percentage, 100);
|
const safePercentage = Math.min(percentage, 100);
|
||||||
|
|
||||||
@@ -98,17 +98,17 @@ export const ShowBilling = () => {
|
|||||||
<TabsTrigger value="annual">Annual</TabsTrigger>
|
<TabsTrigger value="annual">Annual</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{admin?.stripeSubscriptionId && (
|
{admin?.user.stripeSubscriptionId && (
|
||||||
<div className="space-y-2 flex flex-col">
|
<div className="space-y-2 flex flex-col">
|
||||||
<h3 className="text-lg font-medium">Servers Plan</h3>
|
<h3 className="text-lg font-medium">Servers Plan</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
You have {servers?.length} server on your plan of{" "}
|
You have {servers?.length} server on your plan of{" "}
|
||||||
{admin?.serversQuantity} servers
|
{admin?.user.serversQuantity} servers
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<Progress value={safePercentage} className="max-w-lg" />
|
<Progress value={safePercentage} className="max-w-lg" />
|
||||||
</div>
|
</div>
|
||||||
{admin && admin.serversQuantity! <= servers?.length! && (
|
{admin && admin.user.serversQuantity! <= servers?.length! && (
|
||||||
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
|
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
|
||||||
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||||
@@ -279,7 +279,7 @@ export const ShowBilling = () => {
|
|||||||
"flex flex-row items-center gap-2 mt-4",
|
"flex flex-row items-center gap-2 mt-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{admin?.stripeCustomerId && (
|
{admin?.user.stripeCustomerId && (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import type React from "react";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const ShowWelcomeDokploy = () => {
|
export const ShowWelcomeDokploy = () => {
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
if (!isCloud || data?.rol !== "admin") {
|
if (!isCloud || data?.role !== "admin") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,14 +24,14 @@ export const ShowWelcomeDokploy = () => {
|
|||||||
!isLoading &&
|
!isLoading &&
|
||||||
isCloud &&
|
isCloud &&
|
||||||
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
|
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
|
||||||
data?.rol === "admin"
|
data?.role === "owner"
|
||||||
) {
|
) {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}
|
}
|
||||||
}, [isCloud, isLoading]);
|
}, [isCloud, isLoading]);
|
||||||
|
|
||||||
const handleClose = (isOpen: boolean) => {
|
const handleClose = (isOpen: boolean) => {
|
||||||
if (data?.rol === "admin") {
|
if (data?.role === "owner") {
|
||||||
setOpen(isOpen);
|
setOpen(isOpen);
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
localStorage.setItem("hasSeenCloudWelcomeModal", "true"); // Establece el flag al cerrar el modal
|
localStorage.setItem("hasSeenCloudWelcomeModal", "true"); // Establece el flag al cerrar el modal
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export const AddCertificate = () => {
|
|||||||
privateKey: data.privateKey,
|
privateKey: data.privateKey,
|
||||||
autoRenew: data.autoRenew,
|
autoRenew: data.autoRenew,
|
||||||
serverId: data.serverId,
|
serverId: data.serverId,
|
||||||
|
organizationId: "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Certificate Created");
|
toast.success("Certificate Created");
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const AddBitbucketProvider = () => {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const url = useUrl();
|
const url = useUrl();
|
||||||
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
|
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const AddGithubProvider = () => {
|
export const AddGithubProvider = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||||
|
const { data } = api.user.get.useQuery();
|
||||||
const [manifest, setManifest] = useState("");
|
const [manifest, setManifest] = useState("");
|
||||||
const [isOrganization, setIsOrganization] = useState(false);
|
const [isOrganization, setIsOrganization] = useState(false);
|
||||||
const [organizationName, setOrganization] = useState("");
|
const [organizationName, setOrganization] = useState("");
|
||||||
@@ -25,7 +27,7 @@ export const AddGithubProvider = () => {
|
|||||||
const url = document.location.origin;
|
const url = document.location.origin;
|
||||||
const manifest = JSON.stringify(
|
const manifest = JSON.stringify(
|
||||||
{
|
{
|
||||||
redirect_url: `${origin}/api/providers/github/setup?authId=${data?.id}`,
|
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}`,
|
||||||
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
|
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
|
||||||
url: origin,
|
url: origin,
|
||||||
hook_attributes: {
|
hook_attributes: {
|
||||||
@@ -93,8 +95,8 @@ export const AddGithubProvider = () => {
|
|||||||
<form
|
<form
|
||||||
action={
|
action={
|
||||||
isOrganization
|
isOrganization
|
||||||
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${data?.id}`
|
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${activeOrganization?.id}`
|
||||||
: `https://github.com/settings/apps/new?state=gh_init:${data?.id}`
|
: `https://github.com/settings/apps/new?state=gh_init:${activeOrganization?.id}`
|
||||||
}
|
}
|
||||||
method="post"
|
method="post"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const AddGitlabProvider = () => {
|
|||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const url = useUrl();
|
const url = useUrl();
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { mutateAsync, error, isError } = api.gitlab.create.useMutation();
|
const { mutateAsync, error, isError } = api.gitlab.create.useMutation();
|
||||||
const webhookUrl = `${url}/api/providers/gitlab/callback`;
|
const webhookUrl = `${url}/api/providers/gitlab/callback`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,52 +1,134 @@
|
|||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const PasswordSchema = z.object({
|
||||||
|
password: z.string().min(8, {
|
||||||
|
message: "Password is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type PasswordForm = z.infer<typeof PasswordSchema>;
|
||||||
|
|
||||||
export const Disable2FA = () => {
|
export const Disable2FA = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, isLoading } = api.auth.disable2FA.useMutation();
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<PasswordForm>({
|
||||||
|
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.auth.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 (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive" isLoading={isLoading}>
|
<Button variant="destructive">Disable 2FA</Button>
|
||||||
Disable 2FA
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
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.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<Form {...form}>
|
||||||
<AlertDialogAction
|
<form
|
||||||
onClick={async () => {
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
await mutateAsync()
|
className="space-y-4"
|
||||||
.then(() => {
|
|
||||||
utils.auth.get.invalidate();
|
|
||||||
toast.success("2FA Disabled");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error disabling 2FA");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Confirm
|
<FormField
|
||||||
</AlertDialogAction>
|
control={form.control}
|
||||||
</AlertDialogFooter>
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Enter your password to disable 2FA
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
form.reset();
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" variant="destructive" isLoading={isLoading}>
|
||||||
|
Disable 2FA
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,144 +17,315 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
InputOTP,
|
InputOTP,
|
||||||
InputOTPGroup,
|
InputOTPGroup,
|
||||||
InputOTPSlot,
|
InputOTPSlot,
|
||||||
} from "@/components/ui/input-otp";
|
} from "@/components/ui/input-otp";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, Fingerprint } from "lucide-react";
|
import { Fingerprint, QrCode } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import QRCode from "qrcode";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
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, {
|
pin: z.string().min(6, {
|
||||||
message: "Pin is required",
|
message: "Pin is required",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Enable2FA = z.infer<typeof Enable2FASchema>;
|
type PasswordForm = z.infer<typeof PasswordSchema>;
|
||||||
|
type PinForm = z.infer<typeof PinSchema>;
|
||||||
|
|
||||||
|
type TwoFactorEnableResponse = {
|
||||||
|
totpURI: string;
|
||||||
|
backupCodes: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type TwoFactorSetupData = {
|
||||||
|
qrCodeUrl: string;
|
||||||
|
secret: string;
|
||||||
|
totpURI: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const Enable2FA = () => {
|
export const Enable2FA = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { data: session } = authClient.useSession();
|
||||||
|
const [data, setData] = useState<TwoFactorSetupData | null>(null);
|
||||||
|
const [backupCodes, setBackupCodes] = useState<string[]>([]);
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const [step, setStep] = useState<"password" | "verify">("password");
|
||||||
|
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
||||||
|
|
||||||
const { data } = api.auth.generate2FASecret.useQuery(undefined, {
|
const handlePasswordSubmit = async (formData: PasswordForm) => {
|
||||||
refetchOnWindowFocus: false,
|
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.auth.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<PasswordForm>({
|
||||||
|
resolver: zodResolver(PasswordSchema),
|
||||||
|
defaultValues: {
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const pinForm = useForm<PinForm>({
|
||||||
api.auth.verify2FASetup.useMutation();
|
resolver: zodResolver(PinSchema),
|
||||||
|
|
||||||
const form = useForm<Enable2FA>({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
pin: "",
|
pin: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(Enable2FASchema),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
if (!isDialogOpen) {
|
||||||
pin: "",
|
setStep("password");
|
||||||
});
|
setData(null);
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
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 (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<Fingerprint className="size-4 text-muted-foreground" />
|
<Fingerprint className="size-4 text-muted-foreground" />
|
||||||
Enable 2FA
|
Enable 2FA
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen max-sm:overflow-y-auto sm:max-w-xl ">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>2FA Setup</DialogTitle>
|
<DialogTitle>2FA Setup</DialogTitle>
|
||||||
<DialogDescription>Add a 2FA to your account</DialogDescription>
|
<DialogDescription>
|
||||||
|
{step === "password"
|
||||||
|
? "Enter your password to begin 2FA setup"
|
||||||
|
: "Scan the QR code and verify with your authenticator app"}
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
|
||||||
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form-add-2FA"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid sm:grid-cols-2 w-full gap-4"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4 justify-center items-center">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{data?.qrCodeUrl ? "Scan the QR code to add 2FA" : ""}
|
|
||||||
</span>
|
|
||||||
<img
|
|
||||||
src={data?.qrCodeUrl}
|
|
||||||
alt="qrCode"
|
|
||||||
className="rounded-lg w-fit"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<span className="text-sm text-muted-foreground text-center">
|
|
||||||
{data?.secret ? `Secret: ${data?.secret}` : ""}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField
|
{step === "password" ? (
|
||||||
control={form.control}
|
<Form {...passwordForm}>
|
||||||
name="pin"
|
<form
|
||||||
render={({ field }) => (
|
id="password-form"
|
||||||
<FormItem className="flex flex-col justify-center max-sm:items-center">
|
onSubmit={passwordForm.handleSubmit(handlePasswordSubmit)}
|
||||||
<FormLabel>Pin</FormLabel>
|
className="space-y-4"
|
||||||
<FormControl>
|
|
||||||
<InputOTP maxLength={6} {...field}>
|
|
||||||
<InputOTPGroup>
|
|
||||||
<InputOTPSlot index={0} />
|
|
||||||
<InputOTPSlot index={1} />
|
|
||||||
<InputOTPSlot index={2} />
|
|
||||||
<InputOTPSlot index={3} />
|
|
||||||
<InputOTPSlot index={4} />
|
|
||||||
<InputOTPSlot index={5} />
|
|
||||||
</InputOTPGroup>
|
|
||||||
</InputOTP>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription className="max-md:text-center">
|
|
||||||
Please enter the 6 digits code provided by your
|
|
||||||
authenticator app.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
form="hook-form-add-2FA"
|
|
||||||
type="submit"
|
|
||||||
>
|
>
|
||||||
Submit 2FA
|
<FormField
|
||||||
</Button>
|
control={passwordForm.control}
|
||||||
</DialogFooter>
|
name="password"
|
||||||
</Form>
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Enter your password to enable 2FA
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full"
|
||||||
|
isLoading={isPasswordLoading}
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
) : (
|
||||||
|
<Form {...pinForm}>
|
||||||
|
<form
|
||||||
|
id="pin-form"
|
||||||
|
onSubmit={pinForm.handleSubmit(handleVerifySubmit)}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-6 justify-center items-center">
|
||||||
|
{data?.qrCodeUrl ? (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center gap-4 p-6 border rounded-lg">
|
||||||
|
<QrCode className="size-5 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
Scan this QR code with your authenticator app
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
src={data.qrCodeUrl}
|
||||||
|
alt="2FA QR Code"
|
||||||
|
className="rounded-lg w-48 h-48"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-2 text-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Can't scan the QR code?
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-mono bg-muted p-2 rounded">
|
||||||
|
{data.secret}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{backupCodes && backupCodes.length > 0 && (
|
||||||
|
<div className="w-full space-y-3 border rounded-lg p-4">
|
||||||
|
<h4 className="font-medium">Backup Codes</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{backupCodes.map((code, index) => (
|
||||||
|
<code
|
||||||
|
key={index}
|
||||||
|
className="bg-muted p-2 rounded text-sm font-mono"
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</code>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Save these backup codes in a secure place. You can use
|
||||||
|
them to access your account if you lose access to your
|
||||||
|
authenticator device.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center w-full h-48 bg-muted rounded-lg">
|
||||||
|
<QrCode className="size-8 text-muted-foreground animate-pulse" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={pinForm.control}
|
||||||
|
name="pin"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col justify-center items-center">
|
||||||
|
<FormLabel>Verification Code</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<InputOTP maxLength={6} {...field}>
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot index={0} />
|
||||||
|
<InputOTPSlot index={1} />
|
||||||
|
<InputOTPSlot index={2} />
|
||||||
|
<InputOTPSlot index={3} />
|
||||||
|
<InputOTPSlot index={4} />
|
||||||
|
<InputOTPSlot index={5} />
|
||||||
|
</InputOTPGroup>
|
||||||
|
</InputOTP>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Enter the 6-digit code from your authenticator app
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full"
|
||||||
|
isLoading={isPasswordLoading}
|
||||||
|
>
|
||||||
|
Enable 2FA
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import Link from "next/link";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export const GenerateToken = () => {
|
export const GenerateToken = () => {
|
||||||
const { data, refetch } = api.auth.get.useQuery();
|
const { data, refetch } = api.user.get.useQuery();
|
||||||
|
|
||||||
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
|
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
|
||||||
api.auth.generateToken.useMutation();
|
api.auth.generateToken.useMutation();
|
||||||
@@ -51,7 +51,7 @@ export const GenerateToken = () => {
|
|||||||
<Label>Token</Label>
|
<Label>Token</Label>
|
||||||
<ToggleVisibilityInput
|
<ToggleVisibilityInput
|
||||||
placeholder="Token"
|
placeholder="Token"
|
||||||
value={data?.token || ""}
|
value={data?.user?.token || ""}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { generateSHA256Hash } from "@/lib/utils";
|
import { generateSHA256Hash } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -54,7 +56,10 @@ const randomImages = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const ProfileForm = () => {
|
export const ProfileForm = () => {
|
||||||
const { data, refetch, isLoading } = api.auth.get.useQuery();
|
const utils = api.useUtils();
|
||||||
|
const { mutateAsync: disable2FA, isLoading: isDisabling } =
|
||||||
|
api.auth.disable2FA.useMutation();
|
||||||
|
const { data, refetch, isLoading } = api.user.get.useQuery();
|
||||||
const {
|
const {
|
||||||
mutateAsync,
|
mutateAsync,
|
||||||
isLoading: isUpdating,
|
isLoading: isUpdating,
|
||||||
@@ -73,9 +78,9 @@ export const ProfileForm = () => {
|
|||||||
|
|
||||||
const form = useForm<Profile>({
|
const form = useForm<Profile>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: data?.email || "",
|
email: data?.user?.email || "",
|
||||||
password: "",
|
password: "",
|
||||||
image: data?.image || "",
|
image: data?.user?.image || "",
|
||||||
currentPassword: "",
|
currentPassword: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(profileSchema),
|
resolver: zodResolver(profileSchema),
|
||||||
@@ -84,14 +89,14 @@ export const ProfileForm = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
email: data?.email || "",
|
email: data?.user?.email || "",
|
||||||
password: "",
|
password: "",
|
||||||
image: data?.image || "",
|
image: data?.user?.image || "",
|
||||||
currentPassword: "",
|
currentPassword: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.email) {
|
if (data.user.email) {
|
||||||
generateSHA256Hash(data.email).then((hash) => {
|
generateSHA256Hash(data.user.email).then((hash) => {
|
||||||
setGravatarHash(hash);
|
setGravatarHash(hash);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -130,7 +135,7 @@ export const ProfileForm = () => {
|
|||||||
{t("settings.profile.description")}
|
{t("settings.profile.description")}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
|
{!data?.user.twoFactorEnabled ? <Enable2FA /> : <Disable2FA />}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const profileSchema = z.object({
|
|||||||
type Profile = z.infer<typeof profileSchema>;
|
type Profile = z.infer<typeof profileSchema>;
|
||||||
|
|
||||||
export const RemoveSelfAccount = () => {
|
export const RemoveSelfAccount = () => {
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.auth.removeSelfAccount.useMutation();
|
api.auth.removeSelfAccount.useMutation();
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface Props {
|
|||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||||
const { data, refetch } = api.admin.one.useQuery(undefined, {
|
const { data, refetch } = api.user.get.useQuery(undefined, {
|
||||||
enabled: !serverId,
|
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();
|
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
|||||||
enabled: !!serverId,
|
enabled: !!serverId,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: api.admin.one.useQuery();
|
: api.user.get.useQuery();
|
||||||
|
|
||||||
const url = useUrl();
|
const url = useUrl();
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const CreateSSHKey = () => {
|
|||||||
description: "Used on Dokploy Cloud",
|
description: "Used on Dokploy Cloud",
|
||||||
privateKey: keys.privateKey,
|
privateKey: keys.privateKey,
|
||||||
publicKey: keys.publicKey,
|
publicKey: keys.publicKey,
|
||||||
|
organizationId: "",
|
||||||
});
|
});
|
||||||
await refetch();
|
await refetch();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
|||||||
const onSubmit = async (data: SSHKey) => {
|
const onSubmit = async (data: SSHKey) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
...data,
|
...data,
|
||||||
|
organizationId: "",
|
||||||
sshKeyId: sshKeyId || "",
|
sshKeyId: sshKeyId || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
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 { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
@@ -27,62 +35,70 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const addUser = z.object({
|
const addInvitation = z.object({
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "Email is required")
|
.min(1, "Email is required")
|
||||||
.email({ message: "Invalid email" }),
|
.email({ message: "Invalid email" }),
|
||||||
|
role: z.enum(["member", "admin"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddUser = z.infer<typeof addUser>;
|
type AddInvitation = z.infer<typeof addInvitation>;
|
||||||
|
|
||||||
export const AddUser = () => {
|
export const AddInvitation = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
const form = useForm<AddInvitation>({
|
||||||
api.admin.createUserInvitation.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<AddUser>({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "",
|
||||||
|
role: "member",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addUser),
|
resolver: zodResolver(addInvitation),
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset();
|
form.reset();
|
||||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddUser) => {
|
const onSubmit = async (data: AddInvitation) => {
|
||||||
await mutateAsync({
|
setIsLoading(true);
|
||||||
|
const result = await authClient.organization.inviteMember({
|
||||||
email: data.email.toLowerCase(),
|
email: data.email.toLowerCase(),
|
||||||
})
|
role: data.role,
|
||||||
.then(async () => {
|
organizationId: activeOrganization?.id,
|
||||||
toast.success("Invitation created");
|
});
|
||||||
await utils.user.all.invalidate();
|
|
||||||
setOpen(false);
|
if (result.error) {
|
||||||
})
|
setError(result.error.message || "");
|
||||||
.catch(() => {
|
} else {
|
||||||
toast.error("Error creating the invitation");
|
toast.success("Invitation created");
|
||||||
});
|
setError(null);
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.organization.allInvitations.invalidate();
|
||||||
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<Button>
|
<Button>
|
||||||
<PlusIcon className="h-4 w-4" /> Add User
|
<PlusIcon className="h-4 w-4" /> Add Invitation
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add User</DialogTitle>
|
<DialogTitle>Add Invitation</DialogTitle>
|
||||||
<DialogDescription>Invite a new user</DialogDescription>
|
<DialogDescription>Invite a new user</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{error && <AlertBlock type="error">{error}</AlertBlock>}
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form-add-user"
|
id="hook-form-add-invitation"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 "
|
className="grid w-full gap-4 "
|
||||||
>
|
>
|
||||||
@@ -104,10 +120,39 @@ export const AddUser = () => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="role"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Role</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a role" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="member">Member</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
Select the role for the new user
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<DialogFooter className="flex w-full flex-row">
|
<DialogFooter className="flex w-full flex-row">
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
form="hook-form-add-user"
|
form="hook-form-add-invitation"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
@@ -52,7 +52,7 @@ interface Props {
|
|||||||
export const AddUserPermissions = ({ userId }: Props) => {
|
export const AddUserPermissions = ({ userId }: Props) => {
|
||||||
const { data: projects } = api.project.all.useQuery();
|
const { data: projects } = api.project.all.useQuery();
|
||||||
|
|
||||||
const { data, refetch } = api.user.byUserId.useQuery(
|
const { data, refetch } = api.auth.one.useQuery(
|
||||||
{
|
{
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
@@ -62,7 +62,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
api.admin.assignPermissions.useMutation();
|
api.user.assignPermissions.useMutation();
|
||||||
|
|
||||||
const form = useForm<AddPermissions>({
|
const form = useForm<AddPermissions>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -92,7 +92,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
|
|
||||||
const onSubmit = async (data: AddPermissions) => {
|
const onSubmit = async (data: AddPermissions) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
userId,
|
id: userId,
|
||||||
canCreateServices: data.canCreateServices,
|
canCreateServices: data.canCreateServices,
|
||||||
canCreateProjects: data.canCreateProjects,
|
canCreateProjects: data.canCreateProjects,
|
||||||
canDeleteServices: data.canDeleteServices,
|
canDeleteServices: data.canDeleteServices,
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { format, isPast } from "date-fns";
|
||||||
|
import { Mail, MoreHorizontal, Users } from "lucide-react";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { AddInvitation } from "./add-invitation";
|
||||||
|
|
||||||
|
export const ShowInvitations = () => {
|
||||||
|
const { data, isLoading, refetch } =
|
||||||
|
api.organization.allInvitations.useQuery();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||||
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
|
<CardHeader className="">
|
||||||
|
<CardTitle className="text-xl flex flex-row gap-2">
|
||||||
|
<Mail className="size-6 text-muted-foreground self-center" />
|
||||||
|
Invitations
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Create invitations to your organization.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
|
||||||
|
<span>Loading...</span>
|
||||||
|
<Loader2 className="animate-spin size-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{data?.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
|
<Users className="size-8 self-center text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
Invite users to your organization
|
||||||
|
</span>
|
||||||
|
<AddInvitation />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
|
<Table>
|
||||||
|
<TableCaption>See all invitations</TableCaption>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[100px]">Email</TableHead>
|
||||||
|
<TableHead className="text-center">Role</TableHead>
|
||||||
|
<TableHead className="text-center">Status</TableHead>
|
||||||
|
<TableHead className="text-center">
|
||||||
|
Expires At
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data?.map((invitation) => {
|
||||||
|
const isExpired = isPast(
|
||||||
|
new Date(invitation.expiresAt),
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<TableRow key={invitation.id}>
|
||||||
|
<TableCell className="w-[100px]">
|
||||||
|
{invitation.email}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
invitation.role === "owner"
|
||||||
|
? "default"
|
||||||
|
: "secondary"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{invitation.role}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
invitation.status === "pending"
|
||||||
|
? "secondary"
|
||||||
|
: invitation.status === "canceled"
|
||||||
|
? "destructive"
|
||||||
|
: "default"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{invitation.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{format(new Date(invitation.expiresAt), "PPpp")}{" "}
|
||||||
|
{isExpired ? (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
(Expired)
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="text-right flex justify-end">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
Actions
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{!isExpired && (
|
||||||
|
<>
|
||||||
|
{invitation.status === "pending" && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onSelect={(e) => {
|
||||||
|
copy(
|
||||||
|
`${origin}/invitation?token=${invitation.id}`,
|
||||||
|
);
|
||||||
|
toast.success(
|
||||||
|
"Invitation Copied to clipboard",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy Invitation
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{invitation.status === "pending" && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onSelect={async (e) => {
|
||||||
|
const result =
|
||||||
|
await authClient.organization.cancelInvitation(
|
||||||
|
{
|
||||||
|
invitationId: invitation.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
toast.error(
|
||||||
|
result.error.message,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.success(
|
||||||
|
"Invitation deleted",
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel Invitation
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
|
<AddInvitation />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -23,22 +24,19 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { MoreHorizontal, Users } from "lucide-react";
|
import { MoreHorizontal, Users } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AddUserPermissions } from "./add-permissions";
|
import { AddUserPermissions } from "./add-permissions";
|
||||||
import { AddUser } from "./add-user";
|
|
||||||
|
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
|
|
||||||
export const ShowUsers = () => {
|
export const ShowUsers = () => {
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isLoading, refetch } = api.user.all.useQuery();
|
const { data, isLoading, refetch } = api.user.all.useQuery();
|
||||||
const { mutateAsync, isLoading: isRemoving } =
|
const { mutateAsync, isLoading: isRemoving } = api.user.remove.useMutation();
|
||||||
api.admin.removeUser.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -67,7 +65,6 @@ export const ShowUsers = () => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
Invite users to your Dokploy account
|
Invite users to your Dokploy account
|
||||||
</span>
|
</span>
|
||||||
<AddUser />
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -76,43 +73,41 @@ export const ShowUsers = () => {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[100px]">Email</TableHead>
|
<TableHead className="w-[100px]">Email</TableHead>
|
||||||
<TableHead className="text-center">Status</TableHead>
|
<TableHead className="text-center">Role</TableHead>
|
||||||
<TableHead className="text-center">2FA</TableHead>
|
<TableHead className="text-center">2FA</TableHead>
|
||||||
|
|
||||||
<TableHead className="text-center">
|
<TableHead className="text-center">
|
||||||
Expiration
|
Created At
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data?.map((user) => {
|
{data?.map((member) => {
|
||||||
return (
|
return (
|
||||||
<TableRow key={user.userId}>
|
<TableRow key={member.id}>
|
||||||
<TableCell className="w-[100px]">
|
<TableCell className="w-[100px]">
|
||||||
{user.auth.email}
|
{member.user.email}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge
|
<Badge
|
||||||
variant={
|
variant={
|
||||||
user.isRegistered ? "default" : "secondary"
|
member.role === "owner"
|
||||||
|
? "default"
|
||||||
|
: "secondary"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{user.isRegistered
|
{member.role}
|
||||||
? "Registered"
|
|
||||||
: "Not Registered"}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{user.auth.is2FAEnabled
|
{member.user.twoFactorEnabled
|
||||||
? "2FA Enabled"
|
? "Enabled"
|
||||||
: "2FA Not Enabled"}
|
: "Disabled"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-center">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{format(
|
{format(new Date(member.createdAt), "PPpp")}
|
||||||
new Date(user.expirationDate),
|
|
||||||
"PPpp",
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
@@ -131,56 +126,63 @@ export const ShowUsers = () => {
|
|||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
Actions
|
Actions
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{!user.isRegistered && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
onSelect={(e) => {
|
|
||||||
copy(
|
|
||||||
`${origin}/invitation?token=${user.token}`,
|
|
||||||
);
|
|
||||||
toast.success(
|
|
||||||
"Invitation Copied to clipboard",
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Copy Invitation
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{user.isRegistered && (
|
{member.role !== "owner" && (
|
||||||
<AddUserPermissions
|
<AddUserPermissions
|
||||||
userId={user.userId}
|
userId={member.user.id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogAction
|
{member.role !== "owner" && (
|
||||||
title="Delete User"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this user?"
|
title="Delete User"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this user?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
authId: user.authId,
|
if (isCloud) {
|
||||||
})
|
const { error } =
|
||||||
.then(() => {
|
await authClient.organization.removeMember(
|
||||||
toast.success(
|
{
|
||||||
"User deleted successfully",
|
memberIdOrEmail: member.id,
|
||||||
);
|
},
|
||||||
refetch();
|
);
|
||||||
})
|
|
||||||
.catch(() => {
|
if (!error) {
|
||||||
toast.error(
|
toast.success(
|
||||||
"Error deleting destination",
|
"User deleted successfully",
|
||||||
);
|
);
|
||||||
});
|
refetch();
|
||||||
}}
|
} else {
|
||||||
>
|
toast.error(
|
||||||
<DropdownMenuItem
|
"Error deleting user",
|
||||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
);
|
||||||
onSelect={(e) => e.preventDefault()}
|
}
|
||||||
|
} else {
|
||||||
|
await mutateAsync({
|
||||||
|
userId: member.user.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
"User deleted successfully",
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
"Error deleting destination",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Delete User
|
<DropdownMenuItem
|
||||||
</DropdownMenuItem>
|
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||||
</DialogAction>
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Delete User
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -189,10 +191,6 @@ export const ShowUsers = () => {
|
|||||||
})}
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
|
||||||
<AddUser />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
|
|||||||
|
|
||||||
export const WebDomain = () => {
|
export const WebDomain = () => {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
const { data: user, refetch } = api.admin.one.useQuery();
|
const { data, refetch } = api.user.get.useQuery();
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.settings.assignDomainServer.useMutation();
|
api.settings.assignDomainServer.useMutation();
|
||||||
|
|
||||||
@@ -65,14 +65,14 @@ export const WebDomain = () => {
|
|||||||
resolver: zodResolver(addServerDomain),
|
resolver: zodResolver(addServerDomain),
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
domain: user?.host || "",
|
domain: data?.user?.host || "",
|
||||||
certificateType: user?.certificateType,
|
certificateType: data?.user?.certificateType,
|
||||||
letsEncryptEmail: user?.letsEncryptEmail || "",
|
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, user]);
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddServerDomain) => {
|
const onSubmit = async (data: AddServerDomain) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
export const WebServer = ({ className }: Props) => {
|
export const WebServer = ({ className }: Props) => {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
const { data } = api.admin.one.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
|
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ export const WebServer = ({ className }: Props) => {
|
|||||||
|
|
||||||
<div className="flex items-center flex-wrap justify-between gap-4">
|
<div className="flex items-center flex-wrap justify-between gap-4">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Server IP: {data?.serverIp}
|
Server IP: {data?.user.serverIp}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Version: {dokployVersion}
|
Version: {dokployVersion}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -47,15 +46,15 @@ interface Props {
|
|||||||
export const UpdateServerIp = ({ children, serverId }: Props) => {
|
export const UpdateServerIp = ({ children, serverId }: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const { data } = api.admin.one.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
const { data: ip } = api.server.publicIp.useQuery();
|
const { data: ip } = api.server.publicIp.useQuery();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.admin.update.useMutation();
|
api.user.update.useMutation();
|
||||||
|
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
serverIp: data?.serverIp || "",
|
serverIp: data?.user.serverIp || "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
@@ -63,7 +62,7 @@ export const UpdateServerIp = ({ children, serverId }: Props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
serverIp: data.serverIp || "",
|
serverIp: data.user.serverIp || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
|
AudioWaveform,
|
||||||
BarChartHorizontalBigIcon,
|
BarChartHorizontalBigIcon,
|
||||||
Bell,
|
Bell,
|
||||||
BlocksIcon,
|
BlocksIcon,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
Boxes,
|
Boxes,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
|
Command,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Database,
|
Database,
|
||||||
Folder,
|
Folder,
|
||||||
@@ -16,11 +18,13 @@ import {
|
|||||||
GitBranch,
|
GitBranch,
|
||||||
HeartIcon,
|
HeartIcon,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
|
Loader2,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
Package,
|
Package,
|
||||||
PieChart,
|
PieChart,
|
||||||
Server,
|
Server,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
Trash2,
|
||||||
User,
|
User,
|
||||||
Users,
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -74,7 +78,6 @@ import { UserNav } from "./user-nav";
|
|||||||
|
|
||||||
// The types of the queries we are going to use
|
// The types of the queries we are going to use
|
||||||
type AuthQueryOutput = inferRouterOutputs<AppRouter>["auth"]["get"];
|
type AuthQueryOutput = inferRouterOutputs<AppRouter>["auth"]["get"];
|
||||||
type UserQueryOutput = inferRouterOutputs<AppRouter>["user"]["byAuthId"];
|
|
||||||
|
|
||||||
type SingleNavItem = {
|
type SingleNavItem = {
|
||||||
isSingle?: true;
|
isSingle?: true;
|
||||||
@@ -83,7 +86,6 @@ type SingleNavItem = {
|
|||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
isEnabled?: (opts: {
|
isEnabled?: (opts: {
|
||||||
auth?: AuthQueryOutput;
|
auth?: AuthQueryOutput;
|
||||||
user?: UserQueryOutput;
|
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
}) => boolean;
|
}) => boolean;
|
||||||
};
|
};
|
||||||
@@ -101,7 +103,6 @@ type NavItem =
|
|||||||
items: SingleNavItem[];
|
items: SingleNavItem[];
|
||||||
isEnabled?: (opts: {
|
isEnabled?: (opts: {
|
||||||
auth?: AuthQueryOutput;
|
auth?: AuthQueryOutput;
|
||||||
user?: UserQueryOutput;
|
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
}) => boolean;
|
}) => boolean;
|
||||||
};
|
};
|
||||||
@@ -114,7 +115,6 @@ type ExternalLink = {
|
|||||||
icon: React.ComponentType<{ className?: string }>;
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
isEnabled?: (opts: {
|
isEnabled?: (opts: {
|
||||||
auth?: AuthQueryOutput;
|
auth?: AuthQueryOutput;
|
||||||
user?: UserQueryOutput;
|
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
}) => boolean;
|
}) => boolean;
|
||||||
};
|
};
|
||||||
@@ -145,7 +145,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/monitoring",
|
url: "/dashboard/monitoring",
|
||||||
icon: BarChartHorizontalBigIcon,
|
icon: BarChartHorizontalBigIcon,
|
||||||
// Only enabled in non-cloud environments
|
// Only enabled in non-cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) => !isCloud,
|
isEnabled: ({ auth, isCloud }) => !isCloud,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -153,9 +153,9 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/traefik",
|
url: "/dashboard/traefik",
|
||||||
icon: GalleryVerticalEnd,
|
icon: GalleryVerticalEnd,
|
||||||
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!(
|
!!(
|
||||||
(auth?.rol === "admin" || user?.canAccessToTraefikFiles) &&
|
(auth?.role === "owner" || auth?.user?.canAccessToTraefikFiles) &&
|
||||||
!isCloud
|
!isCloud
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -165,8 +165,11 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/docker",
|
url: "/dashboard/docker",
|
||||||
icon: BlocksIcon,
|
icon: BlocksIcon,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
!!(
|
||||||
|
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -174,8 +177,11 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/swarm",
|
url: "/dashboard/swarm",
|
||||||
icon: PieChart,
|
icon: PieChart,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
!!(
|
||||||
|
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -183,8 +189,11 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/requests",
|
url: "/dashboard/requests",
|
||||||
icon: Forward,
|
icon: Forward,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
!!(
|
||||||
|
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Legacy unused menu, adjusted to the new structure
|
// Legacy unused menu, adjusted to the new structure
|
||||||
@@ -251,8 +260,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/server",
|
url: "/dashboard/settings/server",
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for admins in non-cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||||
!!(auth?.rol === "admin" && !isCloud),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -266,7 +274,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/servers",
|
url: "/dashboard/settings/servers",
|
||||||
icon: Server,
|
icon: Server,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -274,7 +282,7 @@ const MENU: Menu = {
|
|||||||
icon: Users,
|
icon: Users,
|
||||||
url: "/dashboard/settings/users",
|
url: "/dashboard/settings/users",
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -282,8 +290,8 @@ const MENU: Menu = {
|
|||||||
icon: KeyRound,
|
icon: KeyRound,
|
||||||
url: "/dashboard/settings/ssh-keys",
|
url: "/dashboard/settings/ssh-keys",
|
||||||
// Only enabled for admins and users with access to SSH keys
|
// Only enabled for admins and users with access to SSH keys
|
||||||
isEnabled: ({ auth, user }) =>
|
isEnabled: ({ auth }) =>
|
||||||
!!(auth?.rol === "admin" || user?.canAccessToSSHKeys),
|
!!(auth?.role === "owner" || auth?.user?.canAccessToSSHKeys),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -291,8 +299,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/git-providers",
|
url: "/dashboard/settings/git-providers",
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
// Only enabled for admins and users with access to Git providers
|
// Only enabled for admins and users with access to Git providers
|
||||||
isEnabled: ({ auth, user }) =>
|
isEnabled: ({ auth }) =>
|
||||||
!!(auth?.rol === "admin" || user?.canAccessToGitProviders),
|
!!(auth?.role === "owner" || auth?.user?.canAccessToGitProviders),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -300,7 +308,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/registry",
|
url: "/dashboard/settings/registry",
|
||||||
icon: Package,
|
icon: Package,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -308,7 +316,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/destinations",
|
url: "/dashboard/settings/destinations",
|
||||||
icon: Database,
|
icon: Database,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -317,7 +325,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/certificates",
|
url: "/dashboard/settings/certificates",
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -325,8 +333,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/cluster",
|
url: "/dashboard/settings/cluster",
|
||||||
icon: Boxes,
|
icon: Boxes,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for admins in non-cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||||
!!(auth?.rol === "admin" && !isCloud),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -334,7 +341,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/notifications",
|
url: "/dashboard/settings/notifications",
|
||||||
icon: Bell,
|
icon: Bell,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -342,8 +349,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/billing",
|
url: "/dashboard/settings/billing",
|
||||||
icon: CreditCard,
|
icon: CreditCard,
|
||||||
// Only enabled for admins in cloud environments
|
// Only enabled for admins in cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
||||||
!!(auth?.rol === "admin" && isCloud),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -379,7 +385,6 @@ const MENU: Menu = {
|
|||||||
*/
|
*/
|
||||||
function createMenuForAuthUser(opts: {
|
function createMenuForAuthUser(opts: {
|
||||||
auth?: AuthQueryOutput;
|
auth?: AuthQueryOutput;
|
||||||
user?: UserQueryOutput;
|
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
}): Menu {
|
}): Menu {
|
||||||
return {
|
return {
|
||||||
@@ -390,7 +395,6 @@ function createMenuForAuthUser(opts: {
|
|||||||
? true
|
? true
|
||||||
: item.isEnabled({
|
: item.isEnabled({
|
||||||
auth: opts.auth,
|
auth: opts.auth,
|
||||||
user: opts.user,
|
|
||||||
isCloud: opts.isCloud,
|
isCloud: opts.isCloud,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -401,7 +405,6 @@ function createMenuForAuthUser(opts: {
|
|||||||
? true
|
? true
|
||||||
: item.isEnabled({
|
: item.isEnabled({
|
||||||
auth: opts.auth,
|
auth: opts.auth,
|
||||||
user: opts.user,
|
|
||||||
isCloud: opts.isCloud,
|
isCloud: opts.isCloud,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -412,7 +415,6 @@ function createMenuForAuthUser(opts: {
|
|||||||
? true
|
? true
|
||||||
: item.isEnabled({
|
: item.isEnabled({
|
||||||
auth: opts.auth,
|
auth: opts.auth,
|
||||||
user: opts.user,
|
|
||||||
isCloud: opts.isCloud,
|
isCloud: opts.isCloud,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -480,37 +482,218 @@ interface Props {
|
|||||||
function LogoWrapper() {
|
function LogoWrapper() {
|
||||||
return <SidebarLogo />;
|
return <SidebarLogo />;
|
||||||
}
|
}
|
||||||
|
import { ChevronsUpDown, Plus } from "lucide-react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { AddOrganization } from "../dashboard/organization/handle-organization";
|
||||||
|
import { DialogAction } from "../shared/dialog-action";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
const data = {
|
||||||
|
user: {
|
||||||
|
name: "shadcn",
|
||||||
|
email: "m@example.com",
|
||||||
|
avatar: "/avatars/shadcn.jpg",
|
||||||
|
},
|
||||||
|
teams: [
|
||||||
|
{
|
||||||
|
name: "Acme Inc",
|
||||||
|
logo: GalleryVerticalEnd,
|
||||||
|
plan: "Enterprise",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Acme Corp.",
|
||||||
|
logo: AudioWaveform,
|
||||||
|
plan: "Startup",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Evil Corp.",
|
||||||
|
logo: Command,
|
||||||
|
plan: "Free",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
function SidebarLogo() {
|
function SidebarLogo() {
|
||||||
const { state } = useSidebar();
|
const { state } = useSidebar();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
const { data: user } = api.user.get.useQuery();
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
|
const { data: session } = authClient.useSession();
|
||||||
|
const {
|
||||||
|
data: organizations,
|
||||||
|
refetch,
|
||||||
|
isLoading,
|
||||||
|
} = api.organization.all.useQuery();
|
||||||
|
const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
|
||||||
|
api.organization.delete.useMutation();
|
||||||
|
const { isMobile } = useSidebar();
|
||||||
|
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||||
|
|
||||||
|
const [activeTeam, setActiveTeam] = useState<
|
||||||
|
typeof activeOrganization | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeOrganization) {
|
||||||
|
setActiveTeam(activeOrganization);
|
||||||
|
}
|
||||||
|
}, [activeOrganization]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<>
|
||||||
href="/dashboard/projects"
|
{isLoading ? (
|
||||||
className="flex items-center gap-2 p-1 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]/35 rounded-lg "
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[5vh] pt-4">
|
||||||
>
|
<span>Loading...</span>
|
||||||
<div
|
<Loader2 className="animate-spin size-4" />
|
||||||
className={cn(
|
</div>
|
||||||
"flex aspect-square items-center justify-center rounded-lg transition-all",
|
) : (
|
||||||
state === "collapsed" ? "size-6" : "size-10",
|
<SidebarMenu>
|
||||||
)}
|
<SidebarMenuItem>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<SidebarMenuButton
|
||||||
|
size="lg"
|
||||||
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
|
>
|
||||||
|
{/* <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex aspect-square items-center justify-center rounded-lg transition-all",
|
||||||
|
state === "collapsed" ? "size-6" : "size-10",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Logo
|
||||||
|
className={cn(
|
||||||
|
"transition-all",
|
||||||
|
state === "collapsed" ? "size-6" : "size-10",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span className="truncate font-semibold">
|
||||||
|
{activeTeam?.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDown className="ml-auto" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||||
|
align="start"
|
||||||
|
side={isMobile ? "bottom" : "right"}
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||||
|
Organizations
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{organizations?.map((org, index) => (
|
||||||
|
<div className="flex flex-row justify-between" key={org.name}>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={async () => {
|
||||||
|
await authClient.organization.setActive({
|
||||||
|
organizationId: org.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
className="w-full gap-2 p-2"
|
||||||
|
>
|
||||||
|
<div className="flex size-6 items-center justify-center rounded-sm border">
|
||||||
|
<Logo
|
||||||
|
className={cn(
|
||||||
|
"transition-all",
|
||||||
|
state === "collapsed" ? "size-6" : "size-10",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{org.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{(org.ownerId === session?.user?.id || isCloud) && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AddOrganization organizationId={org.id} />
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Organization"
|
||||||
|
description="Are you sure you want to delete this organization?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteOrganization({
|
||||||
|
organizationId: org.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success(
|
||||||
|
"Organization deleted successfully",
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(
|
||||||
|
error?.message ||
|
||||||
|
"Error deleting organization",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!isCloud && user?.role === "owner" && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<AddOrganization />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* <Link
|
||||||
|
href="/dashboard/projects"
|
||||||
|
className="flex items-center gap-2 p-1 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]/35 rounded-lg "
|
||||||
>
|
>
|
||||||
<Logo
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-all",
|
"flex aspect-square items-center justify-center rounded-lg transition-all",
|
||||||
state === "collapsed" ? "size-6" : "size-10",
|
state === "collapsed" ? "size-6" : "size-10",
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
</div>
|
<Logo
|
||||||
|
className={cn(
|
||||||
|
"transition-all",
|
||||||
|
state === "collapsed" ? "size-6" : "size-10",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="text-left text-sm leading-tight group-data-[state=open]/collapsible:rotate-90">
|
<div className="text-left text-sm leading-tight group-data-[state=open]/collapsible:rotate-90">
|
||||||
<p className="truncate font-semibold">Dokploy</p>
|
<p className="truncate font-semibold">Dokploy</p>
|
||||||
<p className="truncate text-xs text-muted-foreground">
|
<p className="truncate text-xs text-muted-foreground">
|
||||||
{dokployVersion}
|
{dokployVersion}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link> */}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,15 +714,7 @@ export default function Page({ children }: Props) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const currentPath = router.pathname;
|
const currentPath = router.pathname;
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const includesProjects = pathname?.includes("/dashboard/project");
|
const includesProjects = pathname?.includes("/dashboard/project");
|
||||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||||
@@ -548,7 +723,7 @@ export default function Page({ children }: Props) {
|
|||||||
home: filteredHome,
|
home: filteredHome,
|
||||||
settings: filteredSettings,
|
settings: filteredSettings,
|
||||||
help,
|
help,
|
||||||
} = createMenuForAuthUser({ auth, user, isCloud: !!isCloud });
|
} = createMenuForAuthUser({ auth, isCloud: !!isCloud });
|
||||||
|
|
||||||
const activeItem = findActiveNavItem(
|
const activeItem = findActiveNavItem(
|
||||||
[...filteredHome, ...filteredSettings],
|
[...filteredHome, ...filteredSettings],
|
||||||
@@ -557,7 +732,7 @@ export default function Page({ children }: Props) {
|
|||||||
|
|
||||||
// const showProjectsButton =
|
// const showProjectsButton =
|
||||||
// currentPath === "/dashboard/projects" &&
|
// currentPath === "/dashboard/projects" &&
|
||||||
// (auth?.rol === "admin" || user?.canCreateProjects);
|
// (auth?.rol === "owner" || user?.canCreateProjects);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<SidebarProvider
|
||||||
@@ -577,12 +752,12 @@ export default function Page({ children }: Props) {
|
|||||||
>
|
>
|
||||||
<Sidebar collapsible="icon" variant="floating">
|
<Sidebar collapsible="icon" variant="floating">
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<SidebarMenuButton
|
{/* <SidebarMenuButton
|
||||||
className="group-data-[collapsible=icon]:!p-0"
|
className="group-data-[collapsible=icon]:!p-0"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
> */}
|
||||||
<LogoWrapper />
|
<LogoWrapper />
|
||||||
</SidebarMenuButton>
|
{/* </SidebarMenuButton> */}
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
@@ -783,7 +958,7 @@ export default function Page({ children }: Props) {
|
|||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
{!isCloud && auth?.rol === "admin" && (
|
{!isCloud && auth?.role === "owner" && (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton asChild>
|
<SidebarMenuButton asChild>
|
||||||
<UpdateServerButton />
|
<UpdateServerButton />
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { Languages } from "@/lib/languages";
|
import { Languages } from "@/lib/languages";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import useLocale from "@/utils/hooks/use-locale";
|
import useLocale from "@/utils/hooks/use-locale";
|
||||||
@@ -29,18 +30,11 @@ const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
|||||||
|
|
||||||
export const UserNav = () => {
|
export const UserNav = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.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 { locale, setLocale } = useLocale();
|
||||||
const { mutateAsync } = api.auth.logout.useMutation();
|
// const { mutateAsync } = api.auth.logout.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -50,12 +44,15 @@ export const UserNav = () => {
|
|||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
>
|
>
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
<AvatarImage src={data?.image || ""} alt={data?.image || ""} />
|
<AvatarImage
|
||||||
|
src={data?.user?.image || ""}
|
||||||
|
alt={data?.user?.image || ""}
|
||||||
|
/>
|
||||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span className="truncate font-semibold">Account</span>
|
<span className="truncate font-semibold">Account</span>
|
||||||
<span className="truncate text-xs">{data?.email}</span>
|
<span className="truncate text-xs">{data?.user?.email}</span>
|
||||||
</div>
|
</div>
|
||||||
<ChevronsUpDown className="ml-auto size-4" />
|
<ChevronsUpDown className="ml-auto size-4" />
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
@@ -70,7 +67,7 @@ export const UserNav = () => {
|
|||||||
<DropdownMenuLabel className="flex flex-col">
|
<DropdownMenuLabel className="flex flex-col">
|
||||||
My Account
|
My Account
|
||||||
<span className="text-xs font-normal text-muted-foreground">
|
<span className="text-xs font-normal text-muted-foreground">
|
||||||
{data?.email}
|
{data?.user?.email}
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
@@ -95,7 +92,8 @@ export const UserNav = () => {
|
|||||||
>
|
>
|
||||||
Monitoring
|
Monitoring
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{(data?.rol === "admin" || user?.canAccessToTraefikFiles) && (
|
{(data?.role === "owner" ||
|
||||||
|
data?.user?.canAccessToTraefikFiles) && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -105,7 +103,7 @@ export const UserNav = () => {
|
|||||||
Traefik
|
Traefik
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{(data?.rol === "admin" || user?.canAccessToDocker) && (
|
{(data?.role === "owner" || data?.user?.canAccessToDocker) && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -118,7 +116,7 @@ export const UserNav = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data?.rol === "admin" && (
|
{data?.role === "owner" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -139,7 +137,7 @@ export const UserNav = () => {
|
|||||||
>
|
>
|
||||||
Profile
|
Profile
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{data?.rol === "admin" && (
|
{data?.role === "owner" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -150,7 +148,7 @@ export const UserNav = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data?.rol === "admin" && (
|
{data?.role === "owner" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -163,7 +161,7 @@ export const UserNav = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
{isCloud && data?.rol === "admin" && (
|
{isCloud && data?.role === "owner" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -178,9 +176,12 @@ export const UserNav = () => {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync().then(() => {
|
await authClient.signOut().then(() => {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
});
|
});
|
||||||
|
// await mutateAsync().then(() => {
|
||||||
|
// router.push("/");
|
||||||
|
// });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Log out
|
Log out
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0064_previous_agent_brand.sql
Normal file
1
apps/dokploy/drizzle/0064_previous_agent_brand.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "compose" ADD COLUMN "deployable" boolean DEFAULT false NOT NULL;
|
||||||
1
apps/dokploy/drizzle/0065_daily_zaladane.sql
Normal file
1
apps/dokploy/drizzle/0065_daily_zaladane.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "compose" RENAME COLUMN "deployable" TO "isolatedDeployment";
|
||||||
136
apps/dokploy/drizzle/0066_yielding_echo.sql
Normal file
136
apps/dokploy/drizzle/0066_yielding_echo.sql
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
CREATE TABLE "user_temp" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"name" text DEFAULT '' NOT NULL,
|
||||||
|
"token" text NOT NULL,
|
||||||
|
"isRegistered" boolean DEFAULT false NOT NULL,
|
||||||
|
"expirationDate" text NOT NULL,
|
||||||
|
"createdAt" text 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,
|
||||||
|
"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
|
||||||
|
);
|
||||||
|
--> 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
|
||||||
|
);
|
||||||
|
--> 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
|
||||||
|
);
|
||||||
|
--> 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
|
||||||
211
apps/dokploy/drizzle/0067_migrate-data.sql
Normal file
211
apps/dokploy/drizzle/0067_migrate-data.sql
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
-- Custom SQL migration file, put your code below! --
|
||||||
|
|
||||||
|
WITH inserted_users AS (
|
||||||
|
-- Insertar usuarios desde admins
|
||||||
|
INSERT INTO user_temp (
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
"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,
|
||||||
|
COALESCE(auth.token, ''),
|
||||||
|
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,
|
||||||
|
token,
|
||||||
|
"email_verified",
|
||||||
|
"updated_at",
|
||||||
|
image,
|
||||||
|
"createdAt",
|
||||||
|
"canAccessToAPI",
|
||||||
|
"canAccessToDocker",
|
||||||
|
"canAccessToGitProviders",
|
||||||
|
"canAccessToSSHKeys",
|
||||||
|
"canAccessToTraefikFiles",
|
||||||
|
"canCreateProjects",
|
||||||
|
"canCreateServices",
|
||||||
|
"canDeleteProjects",
|
||||||
|
"canDeleteServices",
|
||||||
|
"accesedProjects",
|
||||||
|
"accesedServices",
|
||||||
|
"expirationDate",
|
||||||
|
"isRegistered"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
u."userId",
|
||||||
|
auth.email,
|
||||||
|
COALESCE(u.token, ''),
|
||||||
|
true,
|
||||||
|
CURRENT_TIMESTAMP,
|
||||||
|
auth.image,
|
||||||
|
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", '{}'),
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
gen_random_uuid(),
|
||||||
|
o.id,
|
||||||
|
a."adminId",
|
||||||
|
'owner',
|
||||||
|
NOW()
|
||||||
|
FROM admin a
|
||||||
|
JOIN inserted_orgs o ON o."owner_id" = a."adminId"
|
||||||
|
RETURNING *
|
||||||
|
)
|
||||||
|
-- Insertar miembros regulares en las organizaciones
|
||||||
|
INSERT INTO member (
|
||||||
|
id,
|
||||||
|
"organization_id",
|
||||||
|
"user_id",
|
||||||
|
role,
|
||||||
|
"created_at"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
gen_random_uuid(),
|
||||||
|
o.id,
|
||||||
|
u."userId",
|
||||||
|
'member',
|
||||||
|
NOW()
|
||||||
|
FROM "user" u
|
||||||
|
JOIN admin a ON u."adminId" = a."adminId"
|
||||||
|
JOIN inserted_orgs o ON o."owner_id" = a."adminId";
|
||||||
32
apps/dokploy/drizzle/0068_sour_professor_monster.sql
Normal file
32
apps/dokploy/drizzle/0068_sour_professor_monster.sql
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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;
|
||||||
2
apps/dokploy/drizzle/0069_broad_ken_ellis.sql
Normal file
2
apps/dokploy/drizzle/0069_broad_ken_ellis.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "user_temp" ALTER COLUMN "token" SET DEFAULT '';--> statement-breakpoint
|
||||||
|
ALTER TABLE "user_temp" ADD COLUMN "created_at" timestamp DEFAULT now();
|
||||||
16
apps/dokploy/drizzle/0070_nervous_vivisector.sql
Normal file
16
apps/dokploy/drizzle/0070_nervous_vivisector.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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;
|
||||||
142
apps/dokploy/drizzle/0071_migrate-data-projects.sql
Normal file
142
apps/dokploy/drizzle/0071_migrate-data-projects.sql
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
-- 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");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
32
apps/dokploy/drizzle/0072_lazy_pixie.sql
Normal file
32
apps/dokploy/drizzle/0072_lazy_pixie.sql
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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";
|
||||||
6
apps/dokploy/drizzle/0073_polite_miss_america.sql
Normal file
6
apps/dokploy/drizzle/0073_polite_miss_america.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
--> statement-breakpoint
|
||||||
|
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";
|
||||||
18
apps/dokploy/drizzle/0074_lowly_jack_power.sql
Normal file
18
apps/dokploy/drizzle/0074_lowly_jack_power.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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;
|
||||||
3
apps/dokploy/drizzle/0075_heavy_metal_master.sql
Normal file
3
apps/dokploy/drizzle/0075_heavy_metal_master.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
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;
|
||||||
4485
apps/dokploy/drizzle/meta/0064_snapshot.json
Normal file
4485
apps/dokploy/drizzle/meta/0064_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
4485
apps/dokploy/drizzle/meta/0065_snapshot.json
Normal file
4485
apps/dokploy/drizzle/meta/0065_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5329
apps/dokploy/drizzle/meta/0066_snapshot.json
Normal file
5329
apps/dokploy/drizzle/meta/0066_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5329
apps/dokploy/drizzle/meta/0067_snapshot.json
Normal file
5329
apps/dokploy/drizzle/meta/0067_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5329
apps/dokploy/drizzle/meta/0068_snapshot.json
Normal file
5329
apps/dokploy/drizzle/meta/0068_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5337
apps/dokploy/drizzle/meta/0069_snapshot.json
Normal file
5337
apps/dokploy/drizzle/meta/0069_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5489
apps/dokploy/drizzle/meta/0070_snapshot.json
Normal file
5489
apps/dokploy/drizzle/meta/0070_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5489
apps/dokploy/drizzle/meta/0071_snapshot.json
Normal file
5489
apps/dokploy/drizzle/meta/0071_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5337
apps/dokploy/drizzle/meta/0072_snapshot.json
Normal file
5337
apps/dokploy/drizzle/meta/0072_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
4878
apps/dokploy/drizzle/meta/0073_snapshot.json
Normal file
4878
apps/dokploy/drizzle/meta/0073_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
4878
apps/dokploy/drizzle/meta/0074_snapshot.json
Normal file
4878
apps/dokploy/drizzle/meta/0074_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
4878
apps/dokploy/drizzle/meta/0075_snapshot.json
Normal file
4878
apps/dokploy/drizzle/meta/0075_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -449,6 +449,90 @@
|
|||||||
"when": 1738522845992,
|
"when": 1738522845992,
|
||||||
"tag": "0063_panoramic_dreadnoughts",
|
"tag": "0063_panoramic_dreadnoughts",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 64,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1738564387043,
|
||||||
|
"tag": "0064_previous_agent_brand",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 65,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1739087857244,
|
||||||
|
"tag": "0065_daily_zaladane",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 66,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1739426913392,
|
||||||
|
"tag": "0066_yielding_echo",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 67,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1739427057545,
|
||||||
|
"tag": "0067_migrate-data",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 68,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1739428942964,
|
||||||
|
"tag": "0068_sour_professor_monster",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 69,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1739664410814,
|
||||||
|
"tag": "0069_broad_ken_ellis",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 70,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1739671869809,
|
||||||
|
"tag": "0070_nervous_vivisector",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 71,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1739671878698,
|
||||||
|
"tag": "0071_migrate-data-projects",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 72,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1739672367223,
|
||||||
|
"tag": "0072_lazy_pixie",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 73,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1739740193879,
|
||||||
|
"tag": "0073_polite_miss_america",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 74,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1739773539709,
|
||||||
|
"tag": "0074_lowly_jack_power",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 75,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1739781534192,
|
||||||
|
"tag": "0075_heavy_metal_master",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
8
apps/dokploy/lib/auth-client.ts
Normal file
8
apps/dokploy/lib/auth-client.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { organizationClient } from "better-auth/client/plugins";
|
||||||
|
import { twoFactorClient } 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()],
|
||||||
|
});
|
||||||
150
apps/dokploy/migrate.ts
Normal file
150
apps/dokploy/migrate.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||||
|
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);
|
||||||
|
});
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||||
"migration:generate": "drizzle-kit generate --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",
|
"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:up": "drizzle-kit up --config ./server/db/drizzle.config.ts",
|
||||||
"migration:drop": "drizzle-kit drop --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",
|
"db:push": "drizzle-kit push --config ./server/db/drizzle.config.ts",
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"test": "vitest --config __test__/vitest.config.ts"
|
"test": "vitest --config __test__/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"better-auth": "1.1.16",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"rotating-file-stream": "3.2.3",
|
"rotating-file-stream": "3.2.3",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
|
|||||||
30
apps/dokploy/pages/accept-invitation/[accept-invitation].tsx
Normal file
30
apps/dokploy/pages/accept-invitation/[accept-invitation].tsx
Normal file
@@ -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 (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
const result = await authClient.organization.acceptInvitation({
|
||||||
|
invitationId: invitationId as string,
|
||||||
|
});
|
||||||
|
console.log(result);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Accept Invitation
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AcceptInvitation;
|
||||||
7
apps/dokploy/pages/api/auth/[...all].ts
Normal file
7
apps/dokploy/pages/api/auth/[...all].ts
Normal file
@@ -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);
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { github } from "@/server/db/schema";
|
import { github } from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
|
auth,
|
||||||
createGithub,
|
createGithub,
|
||||||
findAdminByAuthId,
|
findAdminByAuthId,
|
||||||
findAuthById,
|
findAuthById,
|
||||||
findUserByAuthId,
|
findUserByAuthId,
|
||||||
|
findUserById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
@@ -28,7 +30,7 @@ export default async function handler(
|
|||||||
return res.status(400).json({ error: "Missing code parameter" });
|
return res.status(400).json({ error: "Missing code parameter" });
|
||||||
}
|
}
|
||||||
const [action, value] = state?.split(":");
|
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") {
|
if (action === "gh_init") {
|
||||||
const octokit = new Octokit({});
|
const octokit = new Octokit({});
|
||||||
@@ -39,17 +41,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(
|
await createGithub(
|
||||||
{
|
{
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -60,7 +51,7 @@ export default async function handler(
|
|||||||
githubWebhookSecret: data.webhook_secret,
|
githubWebhookSecret: data.webhook_secret,
|
||||||
githubPrivateKey: data.pem,
|
githubPrivateKey: data.pem,
|
||||||
},
|
},
|
||||||
adminId,
|
value as string,
|
||||||
);
|
);
|
||||||
} else if (action === "gh_setup") {
|
} else if (action === "gh_setup") {
|
||||||
await db
|
await db
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { buffer } from "node:stream/consumers";
|
import { buffer } from "node:stream/consumers";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { admins, server } from "@/server/db/schema";
|
import { server, users_temp } from "@/server/db/schema";
|
||||||
import { findAdminById } from "@dokploy/server";
|
import { findAdminById, findUserById } from "@dokploy/server";
|
||||||
import { asc, eq } from "drizzle-orm";
|
import { asc, eq } from "drizzle-orm";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
@@ -64,33 +64,35 @@ export default async function handler(
|
|||||||
session.subscription as string,
|
session.subscription as string,
|
||||||
);
|
);
|
||||||
await db
|
await db
|
||||||
.update(admins)
|
.update(users_temp)
|
||||||
.set({
|
.set({
|
||||||
stripeCustomerId: session.customer as string,
|
stripeCustomerId: session.customer as string,
|
||||||
stripeSubscriptionId: session.subscription as string,
|
stripeSubscriptionId: session.subscription as string,
|
||||||
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
})
|
})
|
||||||
.where(eq(admins.adminId, adminId))
|
.where(eq(users_temp.id, adminId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const admin = await findAdminById(adminId);
|
const admin = await findUserById(adminId);
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
const newServersQuantity = admin.serversQuantity;
|
const newServersQuantity = admin.serversQuantity;
|
||||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "customer.subscription.created": {
|
case "customer.subscription.created": {
|
||||||
const newSubscription = event.data.object as Stripe.Subscription;
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(admins)
|
.update(users_temp)
|
||||||
.set({
|
.set({
|
||||||
stripeSubscriptionId: newSubscription.id,
|
stripeSubscriptionId: newSubscription.id,
|
||||||
stripeCustomerId: newSubscription.customer as string,
|
stripeCustomerId: newSubscription.customer as string,
|
||||||
})
|
})
|
||||||
.where(eq(admins.stripeCustomerId, newSubscription.customer as string))
|
.where(
|
||||||
|
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||||
|
)
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -100,14 +102,16 @@ export default async function handler(
|
|||||||
const newSubscription = event.data.object as Stripe.Subscription;
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(admins)
|
.update(users_temp)
|
||||||
.set({
|
.set({
|
||||||
stripeSubscriptionId: null,
|
stripeSubscriptionId: null,
|
||||||
serversQuantity: 0,
|
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,
|
newSubscription.customer as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -115,13 +119,13 @@ export default async function handler(
|
|||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
await disableServers(admin.adminId);
|
await disableServers(admin.id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "customer.subscription.updated": {
|
case "customer.subscription.updated": {
|
||||||
const newSubscription = event.data.object as Stripe.Subscription;
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
const admin = await findAdminByStripeCustomerId(
|
const admin = await findUserByStripeCustomerId(
|
||||||
newSubscription.customer as string,
|
newSubscription.customer as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -131,23 +135,23 @@ export default async function handler(
|
|||||||
|
|
||||||
if (newSubscription.status === "active") {
|
if (newSubscription.status === "active") {
|
||||||
await db
|
await db
|
||||||
.update(admins)
|
.update(users_temp)
|
||||||
.set({
|
.set({
|
||||||
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
eq(admins.stripeCustomerId, newSubscription.customer as string),
|
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||||
);
|
);
|
||||||
|
|
||||||
const newServersQuantity = admin.serversQuantity;
|
const newServersQuantity = admin.serversQuantity;
|
||||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||||
} else {
|
} else {
|
||||||
await disableServers(admin.adminId);
|
await disableServers(admin.id);
|
||||||
await db
|
await db
|
||||||
.update(admins)
|
.update(users_temp)
|
||||||
.set({ serversQuantity: 0 })
|
.set({ serversQuantity: 0 })
|
||||||
.where(
|
.where(
|
||||||
eq(admins.stripeCustomerId, newSubscription.customer as string),
|
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +178,7 @@ export default async function handler(
|
|||||||
})
|
})
|
||||||
.where(eq(admins.stripeCustomerId, suscription.customer as string));
|
.where(eq(admins.stripeCustomerId, suscription.customer as string));
|
||||||
|
|
||||||
const admin = await findAdminByStripeCustomerId(
|
const admin = await findUserByStripeCustomerId(
|
||||||
suscription.customer as string,
|
suscription.customer as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -182,7 +186,7 @@ export default async function handler(
|
|||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
const newServersQuantity = admin.serversQuantity;
|
const newServersQuantity = admin.serversQuantity;
|
||||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "invoice.payment_failed": {
|
case "invoice.payment_failed": {
|
||||||
@@ -193,7 +197,7 @@ export default async function handler(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (subscription.status !== "active") {
|
if (subscription.status !== "active") {
|
||||||
const admin = await findAdminByStripeCustomerId(
|
const admin = await findUserByStripeCustomerId(
|
||||||
newInvoice.customer as string,
|
newInvoice.customer as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -207,7 +211,7 @@ export default async function handler(
|
|||||||
})
|
})
|
||||||
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
|
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
|
||||||
|
|
||||||
await disableServers(admin.adminId);
|
await disableServers(admin.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -216,20 +220,20 @@ export default async function handler(
|
|||||||
case "customer.deleted": {
|
case "customer.deleted": {
|
||||||
const customer = event.data.object as Stripe.Customer;
|
const customer = event.data.object as Stripe.Customer;
|
||||||
|
|
||||||
const admin = await findAdminByStripeCustomerId(customer.id);
|
const admin = await findUserByStripeCustomerId(customer.id);
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
await disableServers(admin.adminId);
|
await disableServers(admin.id);
|
||||||
await db
|
await db
|
||||||
.update(admins)
|
.update(users_temp)
|
||||||
.set({
|
.set({
|
||||||
stripeCustomerId: null,
|
stripeCustomerId: null,
|
||||||
stripeSubscriptionId: null,
|
stripeSubscriptionId: null,
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
})
|
})
|
||||||
.where(eq(admins.stripeCustomerId, customer.id));
|
.where(eq(users_temp.stripeCustomerId, customer.id));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -240,20 +244,20 @@ export default async function handler(
|
|||||||
return res.status(200).json({ received: true });
|
return res.status(200).json({ received: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const disableServers = async (adminId: string) => {
|
const disableServers = async (userId: string) => {
|
||||||
await db
|
await db
|
||||||
.update(server)
|
.update(server)
|
||||||
.set({
|
.set({
|
||||||
serverStatus: "inactive",
|
serverStatus: "inactive",
|
||||||
})
|
})
|
||||||
.where(eq(server.adminId, adminId));
|
.where(eq(server.userId, userId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const findAdminByStripeCustomerId = async (stripeCustomerId: string) => {
|
const findUserByStripeCustomerId = async (stripeCustomerId: string) => {
|
||||||
const admin = db.query.admins.findFirst({
|
const user = db.query.users_temp.findFirst({
|
||||||
where: eq(admins.stripeCustomerId, stripeCustomerId),
|
where: eq(users_temp.stripeCustomerId, stripeCustomerId),
|
||||||
});
|
});
|
||||||
return admin;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
const activateServer = async (serverId: string) => {
|
const activateServer = async (serverId: string) => {
|
||||||
@@ -270,19 +274,19 @@ const deactivateServer = async (serverId: string) => {
|
|||||||
.where(eq(server.serverId, serverId));
|
.where(eq(server.serverId, serverId));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findServersByAdminIdSorted = async (adminId: string) => {
|
export const findServersByUserIdSorted = async (userId: string) => {
|
||||||
const servers = await db.query.server.findMany({
|
const servers = await db.query.server.findMany({
|
||||||
where: eq(server.adminId, adminId),
|
where: eq(server.userId, userId),
|
||||||
orderBy: asc(server.createdAt),
|
orderBy: asc(server.createdAt),
|
||||||
});
|
});
|
||||||
|
|
||||||
return servers;
|
return servers;
|
||||||
};
|
};
|
||||||
export const updateServersBasedOnQuantity = async (
|
export const updateServersBasedOnQuantity = async (
|
||||||
adminId: string,
|
userId: string,
|
||||||
newServersQuantity: number,
|
newServersQuantity: number,
|
||||||
) => {
|
) => {
|
||||||
const servers = await findServersByAdminIdSorted(adminId);
|
const servers = await findServersByUserIdSorted(userId);
|
||||||
|
|
||||||
if (servers.length > newServersQuantity) {
|
if (servers.length > newServersQuantity) {
|
||||||
for (const [index, server] of servers.entries()) {
|
for (const [index, server] of servers.entries()) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
|
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
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 { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { 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) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -44,21 +45,20 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
const auth = await helpers.auth.get.fetch();
|
|
||||||
|
|
||||||
if (auth.rol === "user") {
|
if (user.role === "member") {
|
||||||
const user = await helpers.user.byAuthId.fetch({
|
const userR = await helpers.user.one.fetch({
|
||||||
authId: auth.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user.canAccessToDocker) {
|
if (!userR.canAccessToDocker) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
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 { Loader2 } from "lucide-react";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
@@ -25,7 +25,7 @@ const Dashboard = () => {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: monitoring, isLoading } = api.admin.getMetricsToken.useQuery();
|
const { data: monitoring, isLoading } = api.user.getMetricsToken.useQuery();
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 pb-10">
|
<div className="space-y-4 pb-10">
|
||||||
{/* <AlertBlock>
|
{/* <AlertBlock>
|
||||||
@@ -104,7 +104,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
const { user } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -47,24 +49,29 @@ import { cn } from "@/lib/utils";
|
|||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import type { findProjectById } from "@dokploy/server";
|
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 { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import {
|
import {
|
||||||
|
Ban,
|
||||||
|
Check,
|
||||||
|
CheckCircle2,
|
||||||
|
ChevronsUpDown,
|
||||||
CircuitBoard,
|
CircuitBoard,
|
||||||
FolderInput,
|
FolderInput,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
Search,
|
Search,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Check, ChevronsUpDown, X } from "lucide-react";
|
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
} from "next";
|
} from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { useMemo, useState, type ReactElement } from "react";
|
import { type ReactElement, useMemo, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
@@ -191,17 +198,11 @@ export const extractServices = (data: Project | undefined) => {
|
|||||||
const Project = (
|
const Project = (
|
||||||
props: InferGetServerSidePropsType<typeof getServerSideProps>,
|
props: InferGetServerSidePropsType<typeof getServerSideProps>,
|
||||||
) => {
|
) => {
|
||||||
|
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
||||||
const { projectId } = props;
|
const { projectId } = props;
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
const { data, isLoading, refetch } = api.project.one.useQuery({ projectId });
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const { data, isLoading } = api.project.one.useQuery({ projectId });
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const emptyServices =
|
const emptyServices =
|
||||||
@@ -228,6 +229,70 @@ const Project = (
|
|||||||
|
|
||||||
const [selectedTypes, setSelectedTypes] = useState<string[]>([]);
|
const [selectedTypes, setSelectedTypes] = useState<string[]>([]);
|
||||||
const [openCombobox, setOpenCombobox] = useState(false);
|
const [openCombobox, setOpenCombobox] = useState(false);
|
||||||
|
const [selectedServices, setSelectedServices] = useState<string[]>([]);
|
||||||
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
if (selectedServices.length === filteredServices.length) {
|
||||||
|
setSelectedServices([]);
|
||||||
|
} else {
|
||||||
|
setSelectedServices(filteredServices.map((service) => service.id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleServiceSelect = (serviceId: string, event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setSelectedServices((prev) =>
|
||||||
|
prev.includes(serviceId)
|
||||||
|
? prev.filter((id) => id !== serviceId)
|
||||||
|
: [...prev, serviceId],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const composeActions = {
|
||||||
|
start: api.compose.start.useMutation(),
|
||||||
|
stop: api.compose.stop.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkStart = async () => {
|
||||||
|
let success = 0;
|
||||||
|
setIsBulkActionLoading(true);
|
||||||
|
for (const serviceId of selectedServices) {
|
||||||
|
try {
|
||||||
|
await composeActions.start.mutateAsync({ composeId: serviceId });
|
||||||
|
success++;
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(`Error starting service ${serviceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (success > 0) {
|
||||||
|
toast.success(`${success} services started successfully`);
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
setIsBulkActionLoading(false);
|
||||||
|
setSelectedServices([]);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkStop = async () => {
|
||||||
|
let success = 0;
|
||||||
|
setIsBulkActionLoading(true);
|
||||||
|
for (const serviceId of selectedServices) {
|
||||||
|
try {
|
||||||
|
await composeActions.stop.mutateAsync({ composeId: serviceId });
|
||||||
|
success++;
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(`Error stopping service ${serviceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (success > 0) {
|
||||||
|
toast.success(`${success} services stopped successfully`);
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
setSelectedServices([]);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
setIsBulkActionLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
const filteredServices = useMemo(() => {
|
const filteredServices = useMemo(() => {
|
||||||
if (!applications) return [];
|
if (!applications) return [];
|
||||||
@@ -263,7 +328,7 @@ const Project = (
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>{data?.description}</CardDescription>
|
<CardDescription>{data?.description}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{(auth?.rol === "admin" || user?.canCreateServices) && (
|
{(auth?.role === "owner" || auth?.user?.canCreateServices) && (
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<ProjectEnvironment projectId={projectId}>
|
<ProjectEnvironment projectId={projectId}>
|
||||||
<Button variant="outline">Project Environment</Button>
|
<Button variant="outline">Project Environment</Button>
|
||||||
@@ -309,78 +374,151 @@ const Project = (
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-row gap-2 items-center">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="w-full relative">
|
<div className="flex items-center gap-4">
|
||||||
<Input
|
<div className="flex items-center gap-2">
|
||||||
placeholder="Filter services..."
|
<Checkbox
|
||||||
value={searchQuery}
|
checked={selectedServices.length > 0}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
className={cn(
|
||||||
className="pr-10"
|
"data-[state=checked]:bg-primary",
|
||||||
/>
|
selectedServices.length > 0 &&
|
||||||
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
selectedServices.length <
|
||||||
|
filteredServices.length &&
|
||||||
|
"bg-primary/50",
|
||||||
|
)}
|
||||||
|
onCheckedChange={handleSelectAll}
|
||||||
|
/>
|
||||||
|
<span className="text-sm">
|
||||||
|
Select All{" "}
|
||||||
|
{selectedServices.length > 0 &&
|
||||||
|
`(${selectedServices.length}/${filteredServices.length})`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenu
|
||||||
|
open={isDropdownOpen}
|
||||||
|
onOpenChange={setIsDropdownOpen}
|
||||||
|
>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled={selectedServices.length === 0}
|
||||||
|
isLoading={isBulkActionLoading}
|
||||||
|
>
|
||||||
|
Bulk Actions
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DialogAction
|
||||||
|
title="Start Services"
|
||||||
|
description={`Are you sure you want to start ${selectedServices.length} services?`}
|
||||||
|
type="default"
|
||||||
|
onClick={handleBulkStart}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-start"
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Services"
|
||||||
|
description={`Are you sure you want to stop ${selectedServices.length} services?`}
|
||||||
|
type="destructive"
|
||||||
|
onClick={handleBulkStop}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-start text-destructive"
|
||||||
|
>
|
||||||
|
<Ban className="mr-2 h-4 w-4" />
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<Popover open={openCombobox} onOpenChange={setOpenCombobox}>
|
|
||||||
<PopoverTrigger asChild>
|
<div className="flex flex-col gap-2 sm:flex-row sm:gap-4 sm:items-center">
|
||||||
<Button
|
<div className="w-full relative">
|
||||||
variant="outline"
|
<Input
|
||||||
aria-expanded={openCombobox}
|
placeholder="Filter services..."
|
||||||
className="min-w-[200px] justify-between"
|
value={searchQuery}
|
||||||
>
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
{selectedTypes.length === 0
|
className="pr-10"
|
||||||
? "Select types..."
|
/>
|
||||||
: `${selectedTypes.length} selected`}
|
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
</div>
|
||||||
</Button>
|
<Popover
|
||||||
</PopoverTrigger>
|
open={openCombobox}
|
||||||
<PopoverContent className="w-[200px] p-0">
|
onOpenChange={setOpenCombobox}
|
||||||
<Command>
|
>
|
||||||
<CommandInput placeholder="Search type..." />
|
<PopoverTrigger asChild>
|
||||||
<CommandEmpty>No type found.</CommandEmpty>
|
<Button
|
||||||
<CommandGroup>
|
variant="outline"
|
||||||
{serviceTypes.map((type) => (
|
aria-expanded={openCombobox}
|
||||||
|
className="min-w-[200px] justify-between"
|
||||||
|
>
|
||||||
|
{selectedTypes.length === 0
|
||||||
|
? "Select types..."
|
||||||
|
: `${selectedTypes.length} selected`}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search type..." />
|
||||||
|
<CommandEmpty>No type found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{serviceTypes.map((type) => (
|
||||||
|
<CommandItem
|
||||||
|
key={type.value}
|
||||||
|
onSelect={() => {
|
||||||
|
setSelectedTypes((prev) =>
|
||||||
|
prev.includes(type.value)
|
||||||
|
? prev.filter((t) => t !== type.value)
|
||||||
|
: [...prev, type.value],
|
||||||
|
);
|
||||||
|
setOpenCombobox(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
selectedTypes.includes(type.value)
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{type.icon && (
|
||||||
|
<type.icon className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{type.label}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={type.value}
|
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setSelectedTypes((prev) =>
|
setSelectedTypes([]);
|
||||||
prev.includes(type.value)
|
|
||||||
? prev.filter((t) => t !== type.value)
|
|
||||||
: [...prev, type.value],
|
|
||||||
);
|
|
||||||
setOpenCombobox(false);
|
setOpenCombobox(false);
|
||||||
}}
|
}}
|
||||||
|
className="border-t"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row items-center">
|
||||||
<Check
|
<X className="mr-2 h-4 w-4" />
|
||||||
className={cn(
|
Clear filters
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
selectedTypes.includes(type.value)
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{type.icon && (
|
|
||||||
<type.icon className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
{type.label}
|
|
||||||
</div>
|
</div>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
</CommandGroup>
|
||||||
<CommandItem
|
</Command>
|
||||||
onSelect={() => {
|
</PopoverContent>
|
||||||
setSelectedTypes([]);
|
</Popover>
|
||||||
setOpenCombobox(false);
|
</div>
|
||||||
}}
|
|
||||||
className="border-t"
|
|
||||||
>
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<X className="mr-2 h-4 w-4" />
|
|
||||||
Clear filters
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
</CommandGroup>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full gap-8">
|
<div className="flex w-full gap-8">
|
||||||
@@ -418,6 +556,27 @@ const Project = (
|
|||||||
<StatusTooltip status={service.status} />
|
<StatusTooltip status={service.status} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute -left-3 -bottom-3 size-9 translate-y-1 rounded-full p-0 transition-all duration-200 z-10 bg-background border",
|
||||||
|
selectedServices.includes(service.id)
|
||||||
|
? "opacity-100 translate-y-0"
|
||||||
|
: "opacity-0 group-hover:translate-y-0 group-hover:opacity-100",
|
||||||
|
)}
|
||||||
|
onClick={(e) =>
|
||||||
|
handleServiceSelect(service.id, e)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="h-full w-full flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedServices.includes(
|
||||||
|
service.id,
|
||||||
|
)}
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center justify-between">
|
<CardTitle className="flex items-center justify-between">
|
||||||
<div className="flex flex-row items-center gap-2 justify-between w-full">
|
<div className="flex flex-row items-center gap-2 justify-between w-full">
|
||||||
@@ -492,7 +651,7 @@ export async function getServerSideProps(
|
|||||||
const { params } = ctx;
|
const { params } = ctx;
|
||||||
|
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -508,8 +667,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
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 { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { GlobeIcon, HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
import { GlobeIcon, HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||||
@@ -86,16 +86,8 @@ const Service = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
@@ -186,7 +178,8 @@ const Service = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateApplication applicationId={applicationId} />
|
<UpdateApplication applicationId={applicationId} />
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.user?.canDeleteServices) && (
|
||||||
<DeleteService id={applicationId} type="application" />
|
<DeleteService id={applicationId} type="application" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -370,7 +363,7 @@ export async function getServerSideProps(
|
|||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
|
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -386,8 +379,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
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 { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { CircuitBoard, ServerOff } from "lucide-react";
|
import { CircuitBoard, ServerOff } from "lucide-react";
|
||||||
@@ -79,17 +79,9 @@ const Service = (
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.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 (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
@@ -181,7 +173,8 @@ const Service = (
|
|||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateCompose composeId={composeId} />
|
<UpdateCompose composeId={composeId} />
|
||||||
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.user?.canDeleteServices) && (
|
||||||
<DeleteService id={composeId} type="compose" />
|
<DeleteService id={composeId} type="compose" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -366,7 +359,7 @@ export async function getServerSideProps(
|
|||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
|
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -382,8 +375,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
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 { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
@@ -61,16 +61,9 @@ const Mariadb = (
|
|||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -154,7 +147,8 @@ const Mariadb = (
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateMariadb mariadbId={mariadbId} />
|
<UpdateMariadb mariadbId={mariadbId} />
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.user?.canDeleteServices) && (
|
||||||
<DeleteService id={mariadbId} type="mariadb" />
|
<DeleteService id={mariadbId} type="mariadb" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -316,7 +310,7 @@ export async function getServerSideProps(
|
|||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
|
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -332,8 +326,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
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 { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
@@ -61,16 +61,8 @@ const Mongo = (
|
|||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mongo.one.useQuery({ mongoId });
|
const { data } = api.mongo.one.useQuery({ mongoId });
|
||||||
|
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
@@ -156,7 +148,8 @@ const Mongo = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateMongo mongoId={mongoId} />
|
<UpdateMongo mongoId={mongoId} />
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.user?.canDeleteServices) && (
|
||||||
<DeleteService id={mongoId} type="mongo" />
|
<DeleteService id={mongoId} type="mongo" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -318,7 +311,7 @@ export async function getServerSideProps(
|
|||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
|
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -334,8 +327,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
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 { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
@@ -60,16 +60,8 @@ const MySql = (
|
|||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mysql.one.useQuery({ mysqlId });
|
const { data } = api.mysql.one.useQuery({ mysqlId });
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
@@ -156,7 +148,8 @@ const MySql = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateMysql mysqlId={mysqlId} />
|
<UpdateMysql mysqlId={mysqlId} />
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.user?.canDeleteServices) && (
|
||||||
<DeleteService id={mysqlId} type="mysql" />
|
<DeleteService id={mysqlId} type="mysql" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -323,7 +316,7 @@ export async function getServerSideProps(
|
|||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
|
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -339,8 +332,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
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 { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
@@ -60,16 +60,9 @@ const Postgresql = (
|
|||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -154,7 +147,8 @@ const Postgresql = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdatePostgres postgresId={postgresId} />
|
<UpdatePostgres postgresId={postgresId} />
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.user?.canDeleteServices) && (
|
||||||
<DeleteService id={postgresId} type="postgres" />
|
<DeleteService id={postgresId} type="postgres" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -319,7 +313,7 @@ export async function getServerSideProps(
|
|||||||
) {
|
) {
|
||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -335,8 +329,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
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 { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff } from "lucide-react";
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
@@ -60,16 +60,8 @@ const Redis = (
|
|||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.redis.one.useQuery({ redisId });
|
const { data } = api.redis.one.useQuery({ redisId });
|
||||||
|
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
@@ -155,7 +147,8 @@ const Redis = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateRedis redisId={redisId} />
|
<UpdateRedis redisId={redisId} />
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.user?.canDeleteServices) && (
|
||||||
<DeleteService id={redisId} type="redis" />
|
<DeleteService id={redisId} type="redis" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -311,7 +304,7 @@ export async function getServerSideProps(
|
|||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
|
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -327,8 +320,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ShowProjects } from "@/components/dashboard/projects/show";
|
|||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
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 { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
@@ -38,7 +38,7 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
|
|
||||||
const helpers = createServerSideHelpers({
|
const helpers = createServerSideHelpers({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
@@ -46,8 +46,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ShowRequests } from "@/components/dashboard/requests/show-requests";
|
import { ShowRequests } from "@/components/dashboard/requests/show-requests";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
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 { GetServerSidePropsContext } from "next";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@@ -22,7 +23,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
const { user } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { ShowBilling } from "@/components/dashboard/settings/billing/show-billin
|
|||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
|
||||||
import { appRouter } from "@/server/api/root";
|
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 { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
@@ -29,8 +30,8 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -45,8 +46,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -40,8 +40,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -48,8 +48,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -41,8 +41,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -40,8 +40,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
@@ -49,14 +49,12 @@ export async function getServerSideProps(
|
|||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
await helpers.settings.isCloud.prefetch();
|
await helpers.settings.isCloud.prefetch();
|
||||||
const auth = await helpers.auth.get.fetch();
|
if (user.role === "member") {
|
||||||
|
const userR = await helpers.user.one.fetch({
|
||||||
if (auth.rol === "user") {
|
userId: user.id,
|
||||||
const user = await helpers.user.byAuthId.fetch({
|
|
||||||
authId: auth.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user.canAccessToGitProviders) {
|
if (!userR.canAccessToGitProviders) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ const settings = z.object({
|
|||||||
type SettingsType = z.infer<typeof settings>;
|
type SettingsType = z.infer<typeof settings>;
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const { data, refetch } = api.admin.one.useQuery();
|
const { data, refetch } = api.user.get.useQuery();
|
||||||
const { mutateAsync, isLoading, isError, error } =
|
const { mutateAsync, isLoading, isError, error } =
|
||||||
api.admin.update.useMutation();
|
api.user.update.useMutation();
|
||||||
const form = useForm<SettingsType>({
|
const form = useForm<SettingsType>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
cleanCacheOnApplications: false,
|
cleanCacheOnApplications: false,
|
||||||
@@ -55,9 +55,9 @@ const Page = () => {
|
|||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
cleanCacheOnApplications: data?.cleanupCacheApplications || false,
|
cleanCacheOnApplications: data?.user.cleanupCacheApplications || false,
|
||||||
cleanCacheOnCompose: data?.cleanupCacheOnCompose || false,
|
cleanCacheOnCompose: data?.user.cleanupCacheOnCompose || false,
|
||||||
cleanCacheOnPreviews: data?.cleanupCacheOnPreviews || false,
|
cleanCacheOnPreviews: data?.user.cleanupCacheOnPreviews || false,
|
||||||
});
|
});
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -190,7 +190,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (user.rol === "user") {
|
if (user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -205,8 +205,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -41,8 +41,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,22 +13,16 @@ import React, { type ReactElement } from "react";
|
|||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: data?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!data?.id && data?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||||
<ProfileForm />
|
<ProfileForm />
|
||||||
{(user?.canAccessToAPI || data?.rol === "admin") && <GenerateToken />}
|
{(data?.user?.canAccessToAPI || data?.role === "owner") && (
|
||||||
|
<GenerateToken />
|
||||||
|
)}
|
||||||
|
|
||||||
{isCloud && <RemoveSelfAccount />}
|
{isCloud && <RemoveSelfAccount />}
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +40,7 @@ export async function getServerSideProps(
|
|||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const locale = getLocale(req.cookies);
|
const locale = getLocale(req.cookies);
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
|
|
||||||
const helpers = createServerSideHelpers({
|
const helpers = createServerSideHelpers({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
@@ -54,18 +48,21 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|
||||||
await helpers.settings.isCloud.prefetch();
|
await helpers.settings.isCloud.prefetch();
|
||||||
await helpers.auth.get.prefetch();
|
await helpers.auth.get.prefetch();
|
||||||
if (user?.rol === "user") {
|
if (user?.role === "member") {
|
||||||
await helpers.user.byAuthId.prefetch({
|
// const userR = await helpers.user.one.fetch({
|
||||||
authId: user.authId,
|
// userId: user.id,
|
||||||
});
|
// });
|
||||||
|
// await helpers.user.byAuthId.prefetch({
|
||||||
|
// authId: user.authId,
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -40,8 +40,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,19 +2,7 @@ import { SetupMonitoring } from "@/components/dashboard/settings/servers/setup-m
|
|||||||
import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
||||||
import { WebServer } from "@/components/dashboard/settings/web-server";
|
import { WebServer } from "@/components/dashboard/settings/web-server";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
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 { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
@@ -25,8 +13,6 @@ import { toast } from "sonner";
|
|||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const { data, refetch } = api.admin.one.useQuery();
|
|
||||||
const { mutateAsync: update } = api.admin.update.useMutation();
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||||
@@ -98,7 +84,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -107,7 +93,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (user.rol === "user") {
|
if (user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -122,8 +108,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export async function getServerSideProps(
|
|||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const locale = await getLocale(req.cookies);
|
const locale = await getLocale(req.cookies);
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -36,7 +36,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (user.rol === "user") {
|
if (user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -51,8 +51,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -40,23 +40,22 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
const auth = await helpers.auth.get.fetch();
|
|
||||||
await helpers.settings.isCloud.prefetch();
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
if (auth.rol === "user") {
|
if (user.role === "member") {
|
||||||
const user = await helpers.user.byAuthId.fetch({
|
const userR = await helpers.user.one.fetch({
|
||||||
authId: auth.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user.canAccessToSSHKeys) {
|
if (!userR.canAccessToSSHKeys) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations";
|
||||||
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ const Page = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<ShowUsers />
|
<ShowUsers />
|
||||||
|
<ShowInvitations />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -25,8 +27,10 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user || user.rol === "user") {
|
|
||||||
|
console.log("user", user, session);
|
||||||
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -41,8 +45,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
|
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
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 { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import type { ReactElement } from "react";
|
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) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -44,21 +45,20 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
const auth = await helpers.auth.get.fetch();
|
|
||||||
|
|
||||||
if (auth.rol === "user") {
|
if (user.role === "member") {
|
||||||
const user = await helpers.user.byAuthId.fetch({
|
const userR = await helpers.user.one.fetch({
|
||||||
authId: auth.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user.canAccessToDocker) {
|
if (!userR.canAccessToDocker) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { ShowTraefikSystem } from "@/components/dashboard/file-system/show-traefik-system";
|
import { ShowTraefikSystem } from "@/components/dashboard/file-system/show-traefik-system";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
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 { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { 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) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -44,21 +45,20 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
const auth = await helpers.auth.get.fetch();
|
|
||||||
|
|
||||||
if (auth.rol === "user") {
|
if (user.role === "member") {
|
||||||
const user = await helpers.user.byAuthId.fetch({
|
const userR = await helpers.user.one.fetch({
|
||||||
authId: auth.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user.canAccessToTraefikFiles) {
|
if (!userR.canAccessToTraefikFiles) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -3,99 +3,177 @@ import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Logo } from "@/components/shared/logo";
|
import { Logo } from "@/components/shared/logo";
|
||||||
import { Button, buttonVariants } from "@/components/ui/button";
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
import { CardContent } from "@/components/ui/card";
|
import { CardContent, CardDescription } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
InputOTP,
|
||||||
|
InputOTPGroup,
|
||||||
|
InputOTPSlot,
|
||||||
|
} from "@/components/ui/input-otp";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD, auth, isAdminPresent } from "@dokploy/server";
|
||||||
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import base32 from "hi-base32";
|
||||||
|
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
||||||
|
import { AlertTriangle } from "lucide-react";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { TOTP } from "otpauth";
|
||||||
import { type ReactElement, useEffect, useState } from "react";
|
import { type ReactElement, useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const loginSchema = z.object({
|
const LoginSchema = z.object({
|
||||||
email: z
|
email: z.string().email(),
|
||||||
.string()
|
password: z.string().min(8),
|
||||||
.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",
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type Login = z.infer<typeof loginSchema>;
|
const TwoFactorSchema = z.object({
|
||||||
|
code: z.string().min(6),
|
||||||
|
});
|
||||||
|
|
||||||
type AuthResponse = {
|
const BackupCodeSchema = z.object({
|
||||||
is2FAEnabled: boolean;
|
code: z.string().min(8, {
|
||||||
authId: string;
|
message: "Backup code must be at least 8 characters",
|
||||||
};
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type LoginForm = z.infer<typeof LoginSchema>;
|
||||||
|
type BackupCodeForm = z.infer<typeof BackupCodeSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
IS_CLOUD: boolean;
|
IS_CLOUD: boolean;
|
||||||
}
|
}
|
||||||
export default function Home({ IS_CLOUD }: Props) {
|
export default function Home({ IS_CLOUD }: Props) {
|
||||||
const [temp, setTemp] = useState<AuthResponse>({
|
|
||||||
is2FAEnabled: false,
|
|
||||||
authId: "",
|
|
||||||
});
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
|
||||||
api.auth.login.useMutation();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<Login>({
|
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
||||||
|
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
|
||||||
|
const [isBackupCodeLoading, setIsBackupCodeLoading] = useState(false);
|
||||||
|
const [isTwoFactor, setIsTwoFactor] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [twoFactorCode, setTwoFactorCode] = useState("");
|
||||||
|
const [isBackupCodeModalOpen, setIsBackupCodeModalOpen] = useState(false);
|
||||||
|
const [backupCode, setBackupCode] = useState("");
|
||||||
|
|
||||||
|
const loginForm = useForm<LoginForm>({
|
||||||
|
resolver: zodResolver(LoginSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "siumauricio@hotmail.com",
|
||||||
password: "",
|
password: "Password123",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(loginSchema),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const onSubmit = async (values: LoginForm) => {
|
||||||
form.reset();
|
setIsLoginLoading(true);
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
try {
|
||||||
|
const { data, error } = await authClient.signIn.email({
|
||||||
const onSubmit = async (values: Login) => {
|
email: values.email,
|
||||||
await mutateAsync({
|
password: values.password,
|
||||||
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,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error(error.message);
|
||||||
|
setError(error.message || "An error occurred while logging in");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 { data, 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 { data, 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col space-y-2 text-center">
|
<div className="flex flex-col space-y-2 text-center">
|
||||||
@@ -109,55 +187,169 @@ export default function Home({ IS_CLOUD }: Props) {
|
|||||||
Enter your email and password to sign in
|
Enter your email and password to sign in
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{isError && (
|
{error && (
|
||||||
<AlertBlock type="error" className="my-2">
|
<AlertBlock type="error" className="my-2">
|
||||||
<span>{error?.message}</span>
|
<span>{error}</span>
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
{!temp.is2FAEnabled ? (
|
{!isTwoFactor ? (
|
||||||
<Form {...form}>
|
<Form {...loginForm}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-4">
|
<form
|
||||||
<div className="space-y-4">
|
onSubmit={loginForm.handleSubmit(onSubmit)}
|
||||||
<FormField
|
className="space-y-4"
|
||||||
control={form.control}
|
id="login-form"
|
||||||
name="email"
|
>
|
||||||
render={({ field }) => (
|
<FormField
|
||||||
<FormItem>
|
control={loginForm.control}
|
||||||
<FormLabel>Email</FormLabel>
|
name="email"
|
||||||
<FormControl>
|
render={({ field }) => (
|
||||||
<Input placeholder="Email" {...field} />
|
<FormItem>
|
||||||
</FormControl>
|
<FormLabel>Email</FormLabel>
|
||||||
<FormMessage />
|
<FormControl>
|
||||||
</FormItem>
|
<Input placeholder="john@example.com" {...field} />
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
<FormMessage />
|
||||||
<FormField
|
</FormItem>
|
||||||
control={form.control}
|
)}
|
||||||
name="password"
|
/>
|
||||||
render={({ field }) => (
|
<FormField
|
||||||
<FormItem>
|
control={loginForm.control}
|
||||||
<FormLabel>Password</FormLabel>
|
name="password"
|
||||||
<FormControl>
|
render={({ field }) => (
|
||||||
<Input
|
<FormItem>
|
||||||
type="password"
|
<FormLabel>Password</FormLabel>
|
||||||
placeholder="Password"
|
<FormControl>
|
||||||
{...field}
|
<Input
|
||||||
/>
|
type="password"
|
||||||
</FormControl>
|
placeholder="Enter your password"
|
||||||
<FormMessage />
|
{...field}
|
||||||
</FormItem>
|
/>
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
<Button type="submit" isLoading={isLoading} className="w-full">
|
)}
|
||||||
Login
|
/>
|
||||||
</Button>
|
<Button
|
||||||
</div>
|
className="w-full"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoginLoading}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
) : (
|
) : (
|
||||||
<Login2FA authId={temp.authId} />
|
<>
|
||||||
|
<form
|
||||||
|
onSubmit={onTwoFactorSubmit}
|
||||||
|
className="space-y-4"
|
||||||
|
id="two-factor-form"
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>2FA Code</Label>
|
||||||
|
<InputOTP
|
||||||
|
value={twoFactorCode}
|
||||||
|
onChange={setTwoFactorCode}
|
||||||
|
maxLength={6}
|
||||||
|
pattern={REGEXP_ONLY_DIGITS}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot index={0} className="border-border" />
|
||||||
|
<InputOTPSlot index={1} className="border-border" />
|
||||||
|
<InputOTPSlot index={2} className="border-border" />
|
||||||
|
<InputOTPSlot index={3} className="border-border" />
|
||||||
|
<InputOTPSlot index={4} className="border-border" />
|
||||||
|
<InputOTPSlot index={5} className="border-border" />
|
||||||
|
</InputOTPGroup>
|
||||||
|
</InputOTP>
|
||||||
|
<CardDescription>
|
||||||
|
Enter the 6-digit code from your authenticator app
|
||||||
|
</CardDescription>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsBackupCodeModalOpen(true)}
|
||||||
|
className="text-sm text-muted-foreground hover:underline self-start mt-2"
|
||||||
|
>
|
||||||
|
Lost access to your authenticator app?
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsTwoFactor(false);
|
||||||
|
setTwoFactorCode("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isTwoFactorLoading}
|
||||||
|
>
|
||||||
|
Verify
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={isBackupCodeModalOpen}
|
||||||
|
onOpenChange={setIsBackupCodeModalOpen}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Enter Backup Code</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Enter one of your backup codes to access your account
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form onSubmit={onBackupCodeSubmit} className="space-y-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>Backup Code</Label>
|
||||||
|
<Input
|
||||||
|
value={backupCode}
|
||||||
|
onChange={(e) => setBackupCode(e.target.value)}
|
||||||
|
placeholder="Enter your backup code"
|
||||||
|
className="font-mono"
|
||||||
|
/>
|
||||||
|
<CardDescription>
|
||||||
|
Enter one of the backup codes you received when setting up
|
||||||
|
2FA
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsBackupCodeModalOpen(false);
|
||||||
|
setBackupCode("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isBackupCodeLoading}
|
||||||
|
>
|
||||||
|
Verify
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-row justify-between flex-wrap">
|
<div className="flex flex-row justify-between flex-wrap">
|
||||||
@@ -203,8 +395,7 @@ Home.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
try {
|
try {
|
||||||
const { user } = await validateRequest(context.req, context.res);
|
const { user } = await validateRequest(context.req);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -232,7 +423,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user } = await validateRequest(context.req, context.res);
|
const { user } = await validateRequest(context.req);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Logo } from "@/components/shared/logo";
|
import { Logo } from "@/components/shared/logo";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -16,10 +17,11 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { IS_CLOUD, getUserByToken } from "@dokploy/server";
|
import { IS_CLOUD, getUserByToken } from "@dokploy/server";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertCircle, AlertTriangle } from "lucide-react";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -30,6 +32,9 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const registerSchema = z
|
const registerSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
name: z.string().min(1, {
|
||||||
|
message: "Name is required",
|
||||||
|
}),
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, {
|
.min(1, {
|
||||||
@@ -38,7 +43,6 @@ const registerSchema = z
|
|||||||
.email({
|
.email({
|
||||||
message: "Email must be a valid email",
|
message: "Email must be a valid email",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, {
|
.min(1, {
|
||||||
@@ -71,11 +75,17 @@ interface Props {
|
|||||||
token: string;
|
token: string;
|
||||||
invitation: Awaited<ReturnType<typeof getUserByToken>>;
|
invitation: Awaited<ReturnType<typeof getUserByToken>>;
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
|
userAlreadyExists: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Invitation = ({ token, invitation, isCloud }: Props) => {
|
const Invitation = ({
|
||||||
|
token,
|
||||||
|
invitation,
|
||||||
|
isCloud,
|
||||||
|
userAlreadyExists,
|
||||||
|
}: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data } = api.admin.getUserByToken.useQuery(
|
const { data } = api.user.getUserByToken.useQuery(
|
||||||
{
|
{
|
||||||
token,
|
token,
|
||||||
},
|
},
|
||||||
@@ -90,6 +100,7 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
|
|||||||
|
|
||||||
const form = useForm<Register>({
|
const form = useForm<Register>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
@@ -98,9 +109,9 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.auth?.email) {
|
if (data?.email) {
|
||||||
form.reset({
|
form.reset({
|
||||||
email: data?.auth?.email || "",
|
email: data?.email || "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
});
|
});
|
||||||
@@ -108,20 +119,32 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
|
|||||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
const onSubmit = async (values: Register) => {
|
const onSubmit = async (values: Register) => {
|
||||||
await mutateAsync({
|
try {
|
||||||
id: data?.authId,
|
const { data, error } = await authClient.signUp.email({
|
||||||
password: values.password,
|
email: values.email,
|
||||||
token: token,
|
password: values.password,
|
||||||
})
|
name: values.name,
|
||||||
.then(() => {
|
fetchOptions: {
|
||||||
toast.success("User registered successfuly", {
|
headers: {
|
||||||
description:
|
"x-dokploy-token": token,
|
||||||
"Please check your inbox or spam folder to confirm your account.",
|
},
|
||||||
duration: 100000,
|
},
|
||||||
});
|
});
|
||||||
router.push("/dashboard/projects");
|
|
||||||
})
|
if (error) {
|
||||||
.catch((e) => e);
|
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 (
|
return (
|
||||||
@@ -138,114 +161,155 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
|
|||||||
</Link>
|
</Link>
|
||||||
Invitation
|
Invitation
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
{userAlreadyExists ? (
|
||||||
Fill the form below to create your account
|
<div className="flex flex-col gap-4 justify-center items-center">
|
||||||
</CardDescription>
|
<AlertBlock type="success">
|
||||||
<div className="w-full">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="p-3" />
|
<span className="font-medium">Valid Invitation!</span>
|
||||||
|
<span className="text-sm text-green-600 dark:text-green-400">
|
||||||
|
We detected that you already have an account with this
|
||||||
|
email. Please sign in to accept the invitation.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</AlertBlock>
|
||||||
|
|
||||||
{isError && (
|
<Button asChild variant="default" className="w-full">
|
||||||
<div className="mx-5 my-2 flex flex-row items-center gap-2 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
<Link href="/">Sign In</Link>
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
</Button>
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
</div>
|
||||||
{error?.message}
|
) : (
|
||||||
</span>
|
<>
|
||||||
</div>
|
<CardDescription>
|
||||||
)}
|
Fill the form below to create your account
|
||||||
|
</CardDescription>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="p-3" />
|
||||||
|
|
||||||
<CardContent className="p-0">
|
{isError && (
|
||||||
<Form {...form}>
|
<div className="mx-5 my-2 flex flex-row items-center gap-2 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||||
<form
|
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
className="grid gap-4"
|
{error?.message}
|
||||||
>
|
</span>
|
||||||
<div className="space-y-4">
|
</div>
|
||||||
<FormField
|
)}
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Email</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input disabled placeholder="Email" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="Password"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<CardContent className="p-0">
|
||||||
control={form.control}
|
<Form {...form}>
|
||||||
name="confirmPassword"
|
<form
|
||||||
render={({ field }) => (
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
<FormItem>
|
className="grid gap-4"
|
||||||
<FormLabel>Confirm Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="Confirm Password"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
isLoading={form.formState.isSubmitting}
|
|
||||||
className="w-full"
|
|
||||||
>
|
>
|
||||||
Register
|
<div className="space-y-4">
|
||||||
</Button>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter your name"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
disabled
|
||||||
|
placeholder="Email"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="mt-4 text-sm flex flex-row justify-between gap-2 w-full">
|
<FormField
|
||||||
{isCloud && (
|
control={form.control}
|
||||||
<>
|
name="confirmPassword"
|
||||||
<Link
|
render={({ field }) => (
|
||||||
className="hover:underline text-muted-foreground"
|
<FormItem>
|
||||||
href="/"
|
<FormLabel>Confirm Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm Password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
className="w-full"
|
||||||
>
|
>
|
||||||
Login
|
Register
|
||||||
</Link>
|
</Button>
|
||||||
<Link
|
</div>
|
||||||
className="hover:underline text-muted-foreground"
|
|
||||||
href="/send-reset-password"
|
<div className="mt-4 text-sm flex flex-row justify-between gap-2 w-full">
|
||||||
>
|
{isCloud && (
|
||||||
Lost your password?
|
<>
|
||||||
</Link>
|
<Link
|
||||||
</>
|
className="hover:underline text-muted-foreground"
|
||||||
)}
|
href="/"
|
||||||
</div>
|
>
|
||||||
</form>
|
Login
|
||||||
</Form>
|
</Link>
|
||||||
</CardContent>
|
<Link
|
||||||
</div>
|
className="hover:underline text-muted-foreground"
|
||||||
|
href="/send-reset-password"
|
||||||
|
>
|
||||||
|
Lost your password?
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
// http://localhost:3000/invitation?token=CZK4BLrUdMa32RVkAdZiLsPDdvnPiAgZ
|
||||||
|
// /f7af93acc1a99eae864972ab4c92fee089f0d83473d415ede8e821e5dbabe79c
|
||||||
export default Invitation;
|
export default Invitation;
|
||||||
Invitation.getLayout = (page: ReactElement) => {
|
Invitation.getLayout = (page: ReactElement) => {
|
||||||
return <OnboardingLayout>{page}</OnboardingLayout>;
|
return <OnboardingLayout>{page}</OnboardingLayout>;
|
||||||
@@ -254,6 +318,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
const { query } = ctx;
|
const { query } = ctx;
|
||||||
|
|
||||||
const token = query.token;
|
const token = query.token;
|
||||||
|
console.log("query", query);
|
||||||
|
|
||||||
if (typeof token !== "string") {
|
if (typeof token !== "string") {
|
||||||
return {
|
return {
|
||||||
@@ -267,6 +332,17 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
try {
|
try {
|
||||||
const invitation = await getUserByToken(token);
|
const invitation = await getUserByToken(token);
|
||||||
|
|
||||||
|
if (invitation.userAlreadyExists) {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
isCloud: IS_CLOUD,
|
||||||
|
token: token,
|
||||||
|
invitation: invitation,
|
||||||
|
userAlreadyExists: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (invitation.isExpired) {
|
if (invitation.isExpired) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -284,6 +360,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log("error", error);
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user