mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
48 Commits
v0.18.3
...
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 | ||
|
|
1db6ba94f4 | ||
|
|
afd3d2eea3 | ||
|
|
8bd72a8a34 | ||
|
|
fafc238e70 | ||
|
|
c04bf3c7e0 |
@@ -45,7 +45,7 @@ const baseApp: ApplicationNested = {
|
||||
previewWildcard: "",
|
||||
project: {
|
||||
env: "",
|
||||
adminId: "",
|
||||
organizationId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
|
||||
@@ -5,7 +5,7 @@ vi.mock("node:fs", () => ({
|
||||
default: fs,
|
||||
}));
|
||||
|
||||
import type { Admin, FileConfig } from "@dokploy/server";
|
||||
import type { FileConfig, User } from "@dokploy/server";
|
||||
import {
|
||||
createDefaultServerTraefikConfig,
|
||||
loadOrCreateConfig,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
|
||||
const baseAdmin: Admin = {
|
||||
const baseAdmin: User = {
|
||||
enablePaidFeatures: false,
|
||||
metricsConfig: {
|
||||
containers: {
|
||||
@@ -40,9 +40,7 @@ const baseAdmin: Admin = {
|
||||
cleanupCacheApplications: false,
|
||||
cleanupCacheOnCompose: false,
|
||||
cleanupCacheOnPreviews: false,
|
||||
createdAt: "",
|
||||
authId: "",
|
||||
adminId: "string",
|
||||
createdAt: new Date(),
|
||||
serverIp: null,
|
||||
certificateType: "none",
|
||||
host: null,
|
||||
@@ -53,6 +51,31 @@ const baseAdmin: Admin = {
|
||||
serversQuantity: 0,
|
||||
stripeCustomerId: "",
|
||||
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(() => {
|
||||
|
||||
@@ -26,7 +26,7 @@ const baseApp: ApplicationNested = {
|
||||
previewWildcard: "",
|
||||
project: {
|
||||
env: "",
|
||||
adminId: "",
|
||||
organizationId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
|
||||
@@ -79,7 +79,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
|
||||
data,
|
||||
isLoading,
|
||||
error: queryError,
|
||||
} = api.admin.getContainerMetrics.useQuery(
|
||||
} = api.user.getContainerMetrics.useQuery(
|
||||
{
|
||||
url: baseUrl,
|
||||
token,
|
||||
|
||||
@@ -73,7 +73,7 @@ export const ShowPaidMonitoring = ({
|
||||
data,
|
||||
isLoading,
|
||||
error: queryError,
|
||||
} = api.admin.getServerMetrics.useQuery(
|
||||
} = api.user.getServerMetrics.useQuery(
|
||||
{
|
||||
url: BASE_URL,
|
||||
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 { Textarea } from "@/components/ui/textarea";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
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 (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
|
||||
@@ -51,15 +51,7 @@ import { ProjectEnvironment } from "./project-environment";
|
||||
export const ShowProjects = () => {
|
||||
const utils = api.useUtils();
|
||||
const { data, isLoading } = api.project.all.useQuery();
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { mutateAsync } = api.project.remove.useMutation();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
@@ -91,7 +83,7 @@ export const ShowProjects = () => {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
{(auth?.rol === "admin" || user?.canCreateProjects) && (
|
||||
{(auth?.role === "owner" || auth?.user?.canCreateProjects) && (
|
||||
<div className="">
|
||||
<HandleProject />
|
||||
</div>
|
||||
@@ -293,8 +285,8 @@ export const ShowProjects = () => {
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{(auth?.rol === "admin" ||
|
||||
user?.canDeleteProjects) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteProjects) && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@/components/ui/command";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import {
|
||||
type Services,
|
||||
extractServices,
|
||||
@@ -35,8 +36,10 @@ export const SearchCommand = () => {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [search, setSearch] = React.useState("");
|
||||
|
||||
const { data } = api.project.all.useQuery();
|
||||
const { data: session } = authClient.useSession();
|
||||
const { data } = api.project.all.useQuery(undefined, {
|
||||
enabled: !!session,
|
||||
});
|
||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -39,7 +39,7 @@ export const calculatePrice = (count: number, isAnnual = false) => {
|
||||
};
|
||||
export const ShowBilling = () => {
|
||||
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 { mutateAsync: createCheckoutSession } =
|
||||
api.stripe.createCheckoutSession.useMutation();
|
||||
@@ -70,7 +70,7 @@ export const ShowBilling = () => {
|
||||
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 safePercentage = Math.min(percentage, 100);
|
||||
|
||||
@@ -98,17 +98,17 @@ export const ShowBilling = () => {
|
||||
<TabsTrigger value="annual">Annual</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{admin?.stripeSubscriptionId && (
|
||||
{admin?.user.stripeSubscriptionId && (
|
||||
<div className="space-y-2 flex flex-col">
|
||||
<h3 className="text-lg font-medium">Servers Plan</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You have {servers?.length} server on your plan of{" "}
|
||||
{admin?.serversQuantity} servers
|
||||
{admin?.user.serversQuantity} servers
|
||||
</p>
|
||||
<div>
|
||||
<Progress value={safePercentage} className="max-w-lg" />
|
||||
</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">
|
||||
<AlertTriangle className="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",
|
||||
)}
|
||||
>
|
||||
{admin?.stripeCustomerId && (
|
||||
{admin?.user.stripeCustomerId && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
|
||||
@@ -10,12 +10,12 @@ import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const ShowWelcomeDokploy = () => {
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const { data } = api.user.get.useQuery();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||
|
||||
if (!isCloud || data?.rol !== "admin") {
|
||||
if (!isCloud || data?.role !== "admin") {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ export const ShowWelcomeDokploy = () => {
|
||||
!isLoading &&
|
||||
isCloud &&
|
||||
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
|
||||
data?.rol === "admin"
|
||||
data?.role === "owner"
|
||||
) {
|
||||
setOpen(true);
|
||||
}
|
||||
}, [isCloud, isLoading]);
|
||||
|
||||
const handleClose = (isOpen: boolean) => {
|
||||
if (data?.rol === "admin") {
|
||||
if (data?.role === "owner") {
|
||||
setOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
localStorage.setItem("hasSeenCloudWelcomeModal", "true"); // Establece el flag al cerrar el modal
|
||||
|
||||
@@ -86,6 +86,7 @@ export const AddCertificate = () => {
|
||||
privateKey: data.privateKey,
|
||||
autoRenew: data.autoRenew,
|
||||
serverId: data.serverId,
|
||||
organizationId: "",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Certificate Created");
|
||||
|
||||
@@ -53,7 +53,7 @@ export const AddBitbucketProvider = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const url = useUrl();
|
||||
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 form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
|
||||
@@ -10,13 +10,15 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { format } from "date-fns";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const AddGithubProvider = () => {
|
||||
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 [isOrganization, setIsOrganization] = useState(false);
|
||||
const [organizationName, setOrganization] = useState("");
|
||||
@@ -25,7 +27,7 @@ export const AddGithubProvider = () => {
|
||||
const url = document.location.origin;
|
||||
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")}`,
|
||||
url: origin,
|
||||
hook_attributes: {
|
||||
@@ -93,8 +95,8 @@ export const AddGithubProvider = () => {
|
||||
<form
|
||||
action={
|
||||
isOrganization
|
||||
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${data?.id}`
|
||||
: `https://github.com/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:${activeOrganization?.id}`
|
||||
}
|
||||
method="post"
|
||||
>
|
||||
|
||||
@@ -55,7 +55,7 @@ export const AddGitlabProvider = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
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 webhookUrl = `${url}/api/providers/gitlab/callback`;
|
||||
|
||||
|
||||
@@ -1,52 +1,134 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
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 { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
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 = () => {
|
||||
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 (
|
||||
<AlertDialog>
|
||||
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
Disable 2FA
|
||||
</Button>
|
||||
<Button variant="destructive">Disable 2FA</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<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>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync()
|
||||
.then(() => {
|
||||
utils.auth.get.invalidate();
|
||||
toast.success("2FA Disabled");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error disabling 2FA");
|
||||
});
|
||||
}}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
<FormField
|
||||
control={form.control}
|
||||
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>
|
||||
</AlertDialog>
|
||||
);
|
||||
|
||||
@@ -17,144 +17,315 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/components/ui/input-otp";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, Fingerprint } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { Fingerprint, QrCode } from "lucide-react";
|
||||
import QRCode from "qrcode";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const Enable2FASchema = z.object({
|
||||
const PasswordSchema = z.object({
|
||||
password: z.string().min(8, {
|
||||
message: "Password is required",
|
||||
}),
|
||||
});
|
||||
|
||||
const PinSchema = z.object({
|
||||
pin: z.string().min(6, {
|
||||
message: "Pin is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type Enable2FA = z.infer<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 = () => {
|
||||
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, {
|
||||
refetchOnWindowFocus: false,
|
||||
const handlePasswordSubmit = async (formData: PasswordForm) => {
|
||||
setIsPasswordLoading(true);
|
||||
try {
|
||||
const { data: enableData } = await authClient.twoFactor.enable({
|
||||
password: formData.password,
|
||||
});
|
||||
|
||||
if (!enableData) {
|
||||
throw new Error("No data received from server");
|
||||
}
|
||||
|
||||
if (enableData.backupCodes) {
|
||||
setBackupCodes(enableData.backupCodes);
|
||||
}
|
||||
|
||||
if (enableData.totpURI) {
|
||||
const qrCodeUrl = await QRCode.toDataURL(enableData.totpURI);
|
||||
|
||||
setData({
|
||||
qrCodeUrl,
|
||||
secret: enableData.totpURI.split("secret=")[1]?.split("&")[0] || "",
|
||||
totpURI: enableData.totpURI,
|
||||
});
|
||||
|
||||
setStep("verify");
|
||||
toast.success("Scan the QR code with your authenticator app");
|
||||
} else {
|
||||
throw new Error("No TOTP URI received from server");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Error setting up 2FA",
|
||||
);
|
||||
passwordForm.setError("password", {
|
||||
message: "Error verifying password",
|
||||
});
|
||||
} finally {
|
||||
setIsPasswordLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifySubmit = async (formData: PinForm) => {
|
||||
try {
|
||||
const result = await authClient.twoFactor.verifyTotp({
|
||||
code: formData.pin,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
|
||||
pinForm.setError("pin", {
|
||||
message: "Invalid code. Please try again.",
|
||||
});
|
||||
toast.error("Invalid verification code");
|
||||
return;
|
||||
}
|
||||
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
if (!result.data) {
|
||||
throw new Error("No response received from server");
|
||||
}
|
||||
|
||||
toast.success("2FA configured successfully");
|
||||
utils.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 } =
|
||||
api.auth.verify2FASetup.useMutation();
|
||||
|
||||
const form = useForm<Enable2FA>({
|
||||
const pinForm = useForm<PinForm>({
|
||||
resolver: zodResolver(PinSchema),
|
||||
defaultValues: {
|
||||
pin: "",
|
||||
},
|
||||
resolver: zodResolver(Enable2FASchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
pin: "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
if (!isDialogOpen) {
|
||||
setStep("password");
|
||||
setData(null);
|
||||
setBackupCodes([]);
|
||||
passwordForm.reset();
|
||||
pinForm.reset();
|
||||
}
|
||||
}, [isDialogOpen, passwordForm, pinForm]);
|
||||
|
||||
const onSubmit = async (formData: Enable2FA) => {
|
||||
await mutateAsync({
|
||||
pin: formData.pin,
|
||||
secret: data?.secret || "",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("2FA Verified");
|
||||
utils.auth.get.invalidate();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error verifying the 2FA");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<Fingerprint className="size-4 text-muted-foreground" />
|
||||
Enable 2FA
|
||||
</Button>
|
||||
</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>
|
||||
<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>
|
||||
{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
|
||||
control={form.control}
|
||||
name="pin"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-center max-sm:items-center">
|
||||
<FormLabel>Pin</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 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"
|
||||
{step === "password" ? (
|
||||
<Form {...passwordForm}>
|
||||
<form
|
||||
id="password-form"
|
||||
onSubmit={passwordForm.handleSubmit(handlePasswordSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
Submit 2FA
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
<FormField
|
||||
control={passwordForm.control}
|
||||
name="password"
|
||||
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>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const GenerateToken = () => {
|
||||
const { data, refetch } = api.auth.get.useQuery();
|
||||
const { data, refetch } = api.user.get.useQuery();
|
||||
|
||||
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
|
||||
api.auth.generateToken.useMutation();
|
||||
@@ -51,7 +51,7 @@ export const GenerateToken = () => {
|
||||
<Label>Token</Label>
|
||||
<ToggleVisibilityInput
|
||||
placeholder="Token"
|
||||
value={data?.token || ""}
|
||||
value={data?.user?.token || ""}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { generateSHA256Hash } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -54,7 +56,10 @@ const randomImages = [
|
||||
];
|
||||
|
||||
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 {
|
||||
mutateAsync,
|
||||
isLoading: isUpdating,
|
||||
@@ -73,9 +78,9 @@ export const ProfileForm = () => {
|
||||
|
||||
const form = useForm<Profile>({
|
||||
defaultValues: {
|
||||
email: data?.email || "",
|
||||
email: data?.user?.email || "",
|
||||
password: "",
|
||||
image: data?.image || "",
|
||||
image: data?.user?.image || "",
|
||||
currentPassword: "",
|
||||
},
|
||||
resolver: zodResolver(profileSchema),
|
||||
@@ -84,14 +89,14 @@ export const ProfileForm = () => {
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
email: data?.email || "",
|
||||
email: data?.user?.email || "",
|
||||
password: "",
|
||||
image: data?.image || "",
|
||||
image: data?.user?.image || "",
|
||||
currentPassword: "",
|
||||
});
|
||||
|
||||
if (data.email) {
|
||||
generateSHA256Hash(data.email).then((hash) => {
|
||||
if (data.user.email) {
|
||||
generateSHA256Hash(data.user.email).then((hash) => {
|
||||
setGravatarHash(hash);
|
||||
});
|
||||
}
|
||||
@@ -130,7 +135,7 @@ export const ProfileForm = () => {
|
||||
{t("settings.profile.description")}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
|
||||
{!data?.user.twoFactorEnabled ? <Enable2FA /> : <Disable2FA />}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
|
||||
@@ -35,7 +35,7 @@ const profileSchema = z.object({
|
||||
type Profile = z.infer<typeof profileSchema>;
|
||||
|
||||
export const RemoveSelfAccount = () => {
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.auth.removeSelfAccount.useMutation();
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
@@ -7,7 +7,7 @@ interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||
const { data, refetch } = api.admin.one.useQuery(undefined, {
|
||||
const { data, refetch } = api.user.get.useQuery(undefined, {
|
||||
enabled: !serverId,
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||
},
|
||||
);
|
||||
|
||||
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
|
||||
const enabled = data?.user.enableDockerCleanup || server?.enableDockerCleanup;
|
||||
|
||||
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
||||
enabled: !!serverId,
|
||||
},
|
||||
)
|
||||
: api.admin.one.useQuery();
|
||||
: api.user.get.useQuery();
|
||||
|
||||
const url = useUrl();
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export const CreateSSHKey = () => {
|
||||
description: "Used on Dokploy Cloud",
|
||||
privateKey: keys.privateKey,
|
||||
publicKey: keys.publicKey,
|
||||
organizationId: "",
|
||||
});
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
|
||||
@@ -78,6 +78,7 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
||||
const onSubmit = async (data: SSHKey) => {
|
||||
await mutateAsync({
|
||||
...data,
|
||||
organizationId: "",
|
||||
sshKeyId: sshKeyId || "",
|
||||
})
|
||||
.then(async () => {
|
||||
|
||||
@@ -19,6 +19,14 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
@@ -27,62 +35,70 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const addUser = z.object({
|
||||
const addInvitation = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, "Email is required")
|
||||
.email({ message: "Invalid email" }),
|
||||
role: z.enum(["member", "admin"]),
|
||||
});
|
||||
|
||||
type AddUser = z.infer<typeof addUser>;
|
||||
type AddInvitation = z.infer<typeof addInvitation>;
|
||||
|
||||
export const AddUser = () => {
|
||||
export const AddInvitation = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
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 } =
|
||||
api.admin.createUserInvitation.useMutation();
|
||||
|
||||
const form = useForm<AddUser>({
|
||||
const form = useForm<AddInvitation>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
role: "member",
|
||||
},
|
||||
resolver: zodResolver(addUser),
|
||||
resolver: zodResolver(addInvitation),
|
||||
});
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||
|
||||
const onSubmit = async (data: AddUser) => {
|
||||
await mutateAsync({
|
||||
const onSubmit = async (data: AddInvitation) => {
|
||||
setIsLoading(true);
|
||||
const result = await authClient.organization.inviteMember({
|
||||
email: data.email.toLowerCase(),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Invitation created");
|
||||
await utils.user.all.invalidate();
|
||||
setOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error creating the invitation");
|
||||
});
|
||||
role: data.role,
|
||||
organizationId: activeOrganization?.id,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message || "");
|
||||
} else {
|
||||
toast.success("Invitation created");
|
||||
setError(null);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
utils.organization.allInvitations.invalidate();
|
||||
setIsLoading(false);
|
||||
};
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button>
|
||||
<PlusIcon className="h-4 w-4" /> Add User
|
||||
<PlusIcon className="h-4 w-4" /> Add Invitation
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add User</DialogTitle>
|
||||
<DialogTitle>Add Invitation</DialogTitle>
|
||||
<DialogDescription>Invite a new user</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
{error && <AlertBlock type="error">{error}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-user"
|
||||
id="hook-form-add-invitation"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
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">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-add-user"
|
||||
form="hook-form-add-invitation"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
@@ -52,7 +52,7 @@ interface Props {
|
||||
export const AddUserPermissions = ({ userId }: Props) => {
|
||||
const { data: projects } = api.project.all.useQuery();
|
||||
|
||||
const { data, refetch } = api.user.byUserId.useQuery(
|
||||
const { data, refetch } = api.auth.one.useQuery(
|
||||
{
|
||||
userId,
|
||||
},
|
||||
@@ -62,7 +62,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
);
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.admin.assignPermissions.useMutation();
|
||||
api.user.assignPermissions.useMutation();
|
||||
|
||||
const form = useForm<AddPermissions>({
|
||||
defaultValues: {
|
||||
@@ -92,7 +92,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
|
||||
const onSubmit = async (data: AddPermissions) => {
|
||||
await mutateAsync({
|
||||
userId,
|
||||
id: userId,
|
||||
canCreateServices: data.canCreateServices,
|
||||
canCreateProjects: data.canCreateProjects,
|
||||
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 { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -23,22 +24,19 @@ import {
|
||||
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 } from "date-fns";
|
||||
import { MoreHorizontal, Users } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
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 = () => {
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data, isLoading, refetch } = api.user.all.useQuery();
|
||||
const { mutateAsync, isLoading: isRemoving } =
|
||||
api.admin.removeUser.useMutation();
|
||||
const { mutateAsync, isLoading: isRemoving } = api.user.remove.useMutation();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -67,7 +65,6 @@ export const ShowUsers = () => {
|
||||
<span className="text-base text-muted-foreground">
|
||||
Invite users to your Dokploy account
|
||||
</span>
|
||||
<AddUser />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
@@ -76,43 +73,41 @@ export const ShowUsers = () => {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<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">
|
||||
Expiration
|
||||
Created At
|
||||
</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.map((user) => {
|
||||
{data?.map((member) => {
|
||||
return (
|
||||
<TableRow key={user.userId}>
|
||||
<TableRow key={member.id}>
|
||||
<TableCell className="w-[100px]">
|
||||
{user.auth.email}
|
||||
{member.user.email}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge
|
||||
variant={
|
||||
user.isRegistered ? "default" : "secondary"
|
||||
member.role === "owner"
|
||||
? "default"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
{user.isRegistered
|
||||
? "Registered"
|
||||
: "Not Registered"}
|
||||
{member.role}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{user.auth.is2FAEnabled
|
||||
? "2FA Enabled"
|
||||
: "2FA Not Enabled"}
|
||||
{member.user.twoFactorEnabled
|
||||
? "Enabled"
|
||||
: "Disabled"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<TableCell className="text-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{format(
|
||||
new Date(user.expirationDate),
|
||||
"PPpp",
|
||||
)}
|
||||
{format(new Date(member.createdAt), "PPpp")}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -131,56 +126,63 @@ export const ShowUsers = () => {
|
||||
<DropdownMenuLabel>
|
||||
Actions
|
||||
</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
|
||||
userId={user.userId}
|
||||
userId={member.user.id}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DialogAction
|
||||
title="Delete User"
|
||||
description="Are you sure you want to delete this user?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
authId: user.authId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"User deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting destination",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
{member.role !== "owner" && (
|
||||
<DialogAction
|
||||
title="Delete User"
|
||||
description="Are you sure you want to delete this user?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
if (isCloud) {
|
||||
const { error } =
|
||||
await authClient.organization.removeMember(
|
||||
{
|
||||
memberIdOrEmail: member.id,
|
||||
},
|
||||
);
|
||||
|
||||
if (!error) {
|
||||
toast.success(
|
||||
"User deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
} else {
|
||||
toast.error(
|
||||
"Error deleting user",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await mutateAsync({
|
||||
userId: member.user.id,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"User deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting destination",
|
||||
);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete User
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Delete User
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
@@ -189,10 +191,6 @@ export const ShowUsers = () => {
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<AddUser />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -52,7 +52,7 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
|
||||
|
||||
export const WebDomain = () => {
|
||||
const { t } = useTranslation("settings");
|
||||
const { data: user, refetch } = api.admin.one.useQuery();
|
||||
const { data, refetch } = api.user.get.useQuery();
|
||||
const { mutateAsync, isLoading } =
|
||||
api.settings.assignDomainServer.useMutation();
|
||||
|
||||
@@ -65,14 +65,14 @@ export const WebDomain = () => {
|
||||
resolver: zodResolver(addServerDomain),
|
||||
});
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
if (data) {
|
||||
form.reset({
|
||||
domain: user?.host || "",
|
||||
certificateType: user?.certificateType,
|
||||
letsEncryptEmail: user?.letsEncryptEmail || "",
|
||||
domain: data?.user?.host || "",
|
||||
certificateType: data?.user?.certificateType,
|
||||
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, user]);
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: AddServerDomain) => {
|
||||
await mutateAsync({
|
||||
|
||||
@@ -21,7 +21,7 @@ interface Props {
|
||||
}
|
||||
export const WebServer = ({ className }: Props) => {
|
||||
const { t } = useTranslation("settings");
|
||||
const { data } = api.admin.one.useQuery();
|
||||
const { data } = api.user.get.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">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Server IP: {data?.serverIp}
|
||||
Server IP: {data?.user.serverIp}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Version: {dokployVersion}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -47,15 +46,15 @@ interface Props {
|
||||
export const UpdateServerIp = ({ children, serverId }: Props) => {
|
||||
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 { mutateAsync, isLoading, error, isError } =
|
||||
api.admin.update.useMutation();
|
||||
api.user.update.useMutation();
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
serverIp: data?.serverIp || "",
|
||||
serverIp: data?.user.serverIp || "",
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
@@ -63,7 +62,7 @@ export const UpdateServerIp = ({ children, serverId }: Props) => {
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
serverIp: data.serverIp || "",
|
||||
serverIp: data.user.serverIp || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
import {
|
||||
Activity,
|
||||
AudioWaveform,
|
||||
BarChartHorizontalBigIcon,
|
||||
Bell,
|
||||
BlocksIcon,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
Boxes,
|
||||
ChevronRight,
|
||||
CircleHelp,
|
||||
Command,
|
||||
CreditCard,
|
||||
Database,
|
||||
Folder,
|
||||
@@ -16,11 +18,13 @@ import {
|
||||
GitBranch,
|
||||
HeartIcon,
|
||||
KeyRound,
|
||||
Loader2,
|
||||
type LucideIcon,
|
||||
Package,
|
||||
PieChart,
|
||||
Server,
|
||||
ShieldCheck,
|
||||
Trash2,
|
||||
User,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
@@ -74,7 +78,6 @@ import { UserNav } from "./user-nav";
|
||||
|
||||
// The types of the queries we are going to use
|
||||
type AuthQueryOutput = inferRouterOutputs<AppRouter>["auth"]["get"];
|
||||
type UserQueryOutput = inferRouterOutputs<AppRouter>["user"]["byAuthId"];
|
||||
|
||||
type SingleNavItem = {
|
||||
isSingle?: true;
|
||||
@@ -83,7 +86,6 @@ type SingleNavItem = {
|
||||
icon?: LucideIcon;
|
||||
isEnabled?: (opts: {
|
||||
auth?: AuthQueryOutput;
|
||||
user?: UserQueryOutput;
|
||||
isCloud: boolean;
|
||||
}) => boolean;
|
||||
};
|
||||
@@ -101,7 +103,6 @@ type NavItem =
|
||||
items: SingleNavItem[];
|
||||
isEnabled?: (opts: {
|
||||
auth?: AuthQueryOutput;
|
||||
user?: UserQueryOutput;
|
||||
isCloud: boolean;
|
||||
}) => boolean;
|
||||
};
|
||||
@@ -114,7 +115,6 @@ type ExternalLink = {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
isEnabled?: (opts: {
|
||||
auth?: AuthQueryOutput;
|
||||
user?: UserQueryOutput;
|
||||
isCloud: boolean;
|
||||
}) => boolean;
|
||||
};
|
||||
@@ -145,7 +145,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/monitoring",
|
||||
icon: BarChartHorizontalBigIcon,
|
||||
// Only enabled in non-cloud environments
|
||||
isEnabled: ({ auth, user, isCloud }) => !isCloud,
|
||||
isEnabled: ({ auth, isCloud }) => !isCloud,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -153,9 +153,9 @@ const MENU: Menu = {
|
||||
url: "/dashboard/traefik",
|
||||
icon: GalleryVerticalEnd,
|
||||
// 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
|
||||
),
|
||||
},
|
||||
@@ -165,8 +165,11 @@ const MENU: Menu = {
|
||||
url: "/dashboard/docker",
|
||||
icon: BlocksIcon,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, user, isCloud }) =>
|
||||
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -174,8 +177,11 @@ const MENU: Menu = {
|
||||
url: "/dashboard/swarm",
|
||||
icon: PieChart,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, user, isCloud }) =>
|
||||
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -183,8 +189,11 @@ const MENU: Menu = {
|
||||
url: "/dashboard/requests",
|
||||
icon: Forward,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, user, isCloud }) =>
|
||||
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
|
||||
// Legacy unused menu, adjusted to the new structure
|
||||
@@ -251,8 +260,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/server",
|
||||
icon: Activity,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth, user, isCloud }) =>
|
||||
!!(auth?.rol === "admin" && !isCloud),
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -266,7 +274,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/servers",
|
||||
icon: Server,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -274,7 +282,7 @@ const MENU: Menu = {
|
||||
icon: Users,
|
||||
url: "/dashboard/settings/users",
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -282,8 +290,8 @@ const MENU: Menu = {
|
||||
icon: KeyRound,
|
||||
url: "/dashboard/settings/ssh-keys",
|
||||
// Only enabled for admins and users with access to SSH keys
|
||||
isEnabled: ({ auth, user }) =>
|
||||
!!(auth?.rol === "admin" || user?.canAccessToSSHKeys),
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.user?.canAccessToSSHKeys),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -291,8 +299,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/git-providers",
|
||||
icon: GitBranch,
|
||||
// Only enabled for admins and users with access to Git providers
|
||||
isEnabled: ({ auth, user }) =>
|
||||
!!(auth?.rol === "admin" || user?.canAccessToGitProviders),
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.user?.canAccessToGitProviders),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -300,7 +308,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/registry",
|
||||
icon: Package,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -308,7 +316,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/destinations",
|
||||
icon: Database,
|
||||
// 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",
|
||||
icon: ShieldCheck,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -325,8 +333,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/cluster",
|
||||
icon: Boxes,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth, user, isCloud }) =>
|
||||
!!(auth?.rol === "admin" && !isCloud),
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -334,7 +341,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/notifications",
|
||||
icon: Bell,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -342,8 +349,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/billing",
|
||||
icon: CreditCard,
|
||||
// Only enabled for admins in cloud environments
|
||||
isEnabled: ({ auth, user, isCloud }) =>
|
||||
!!(auth?.rol === "admin" && isCloud),
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -379,7 +385,6 @@ const MENU: Menu = {
|
||||
*/
|
||||
function createMenuForAuthUser(opts: {
|
||||
auth?: AuthQueryOutput;
|
||||
user?: UserQueryOutput;
|
||||
isCloud: boolean;
|
||||
}): Menu {
|
||||
return {
|
||||
@@ -390,7 +395,6 @@ function createMenuForAuthUser(opts: {
|
||||
? true
|
||||
: item.isEnabled({
|
||||
auth: opts.auth,
|
||||
user: opts.user,
|
||||
isCloud: opts.isCloud,
|
||||
}),
|
||||
),
|
||||
@@ -401,7 +405,6 @@ function createMenuForAuthUser(opts: {
|
||||
? true
|
||||
: item.isEnabled({
|
||||
auth: opts.auth,
|
||||
user: opts.user,
|
||||
isCloud: opts.isCloud,
|
||||
}),
|
||||
),
|
||||
@@ -412,7 +415,6 @@ function createMenuForAuthUser(opts: {
|
||||
? true
|
||||
: item.isEnabled({
|
||||
auth: opts.auth,
|
||||
user: opts.user,
|
||||
isCloud: opts.isCloud,
|
||||
}),
|
||||
),
|
||||
@@ -480,37 +482,218 @@ interface Props {
|
||||
function LogoWrapper() {
|
||||
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() {
|
||||
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: 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 (
|
||||
<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 "
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex aspect-square items-center justify-center rounded-lg transition-all",
|
||||
state === "collapsed" ? "size-6" : "size-10",
|
||||
)}
|
||||
<>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[5vh] pt-4">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<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(
|
||||
"transition-all",
|
||||
"flex aspect-square items-center justify-center rounded-lg transition-all",
|
||||
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">
|
||||
<p className="truncate font-semibold">Dokploy</p>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{dokployVersion}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
<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 text-xs text-muted-foreground">
|
||||
{dokployVersion}
|
||||
</p>
|
||||
</div>
|
||||
</Link> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -531,15 +714,7 @@ export default function Page({ children }: Props) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const currentPath = router.pathname;
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const includesProjects = pathname?.includes("/dashboard/project");
|
||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||
@@ -548,7 +723,7 @@ export default function Page({ children }: Props) {
|
||||
home: filteredHome,
|
||||
settings: filteredSettings,
|
||||
help,
|
||||
} = createMenuForAuthUser({ auth, user, isCloud: !!isCloud });
|
||||
} = createMenuForAuthUser({ auth, isCloud: !!isCloud });
|
||||
|
||||
const activeItem = findActiveNavItem(
|
||||
[...filteredHome, ...filteredSettings],
|
||||
@@ -557,7 +732,7 @@ export default function Page({ children }: Props) {
|
||||
|
||||
// const showProjectsButton =
|
||||
// currentPath === "/dashboard/projects" &&
|
||||
// (auth?.rol === "admin" || user?.canCreateProjects);
|
||||
// (auth?.rol === "owner" || user?.canCreateProjects);
|
||||
|
||||
return (
|
||||
<SidebarProvider
|
||||
@@ -577,12 +752,12 @@ export default function Page({ children }: Props) {
|
||||
>
|
||||
<Sidebar collapsible="icon" variant="floating">
|
||||
<SidebarHeader>
|
||||
<SidebarMenuButton
|
||||
{/* <SidebarMenuButton
|
||||
className="group-data-[collapsible=icon]:!p-0"
|
||||
size="lg"
|
||||
>
|
||||
<LogoWrapper />
|
||||
</SidebarMenuButton>
|
||||
> */}
|
||||
<LogoWrapper />
|
||||
{/* </SidebarMenuButton> */}
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
@@ -783,7 +958,7 @@ export default function Page({ children }: Props) {
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
{!isCloud && auth?.rol === "admin" && (
|
||||
{!isCloud && auth?.role === "owner" && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<UpdateServerButton />
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { Languages } from "@/lib/languages";
|
||||
import { api } from "@/utils/api";
|
||||
import useLocale from "@/utils/hooks/use-locale";
|
||||
@@ -29,18 +30,11 @@ const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
||||
|
||||
export const UserNav = () => {
|
||||
const router = useRouter();
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: data?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!data?.id && data?.rol === "user",
|
||||
},
|
||||
);
|
||||
|
||||
const { locale, setLocale } = useLocale();
|
||||
const { mutateAsync } = api.auth.logout.useMutation();
|
||||
// const { mutateAsync } = api.auth.logout.useMutation();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@@ -50,12 +44,15 @@ export const UserNav = () => {
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<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>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<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>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
@@ -70,7 +67,7 @@ export const UserNav = () => {
|
||||
<DropdownMenuLabel className="flex flex-col">
|
||||
My Account
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
{data?.email}
|
||||
{data?.user?.email}
|
||||
</span>
|
||||
</DropdownMenuLabel>
|
||||
<ModeToggle />
|
||||
@@ -95,7 +92,8 @@ export const UserNav = () => {
|
||||
>
|
||||
Monitoring
|
||||
</DropdownMenuItem>
|
||||
{(data?.rol === "admin" || user?.canAccessToTraefikFiles) && (
|
||||
{(data?.role === "owner" ||
|
||||
data?.user?.canAccessToTraefikFiles) && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -105,7 +103,7 @@ export const UserNav = () => {
|
||||
Traefik
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{(data?.rol === "admin" || user?.canAccessToDocker) && (
|
||||
{(data?.role === "owner" || data?.user?.canAccessToDocker) && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -118,7 +116,7 @@ export const UserNav = () => {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{data?.rol === "admin" && (
|
||||
{data?.role === "owner" && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -139,7 +137,7 @@ export const UserNav = () => {
|
||||
>
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
{data?.rol === "admin" && (
|
||||
{data?.role === "owner" && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -150,7 +148,7 @@ export const UserNav = () => {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{data?.rol === "admin" && (
|
||||
{data?.role === "owner" && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -163,7 +161,7 @@ export const UserNav = () => {
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
{isCloud && data?.rol === "admin" && (
|
||||
{isCloud && data?.role === "owner" && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -178,9 +176,12 @@ export const UserNav = () => {
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={async () => {
|
||||
await mutateAsync().then(() => {
|
||||
await authClient.signOut().then(() => {
|
||||
router.push("/");
|
||||
});
|
||||
// await mutateAsync().then(() => {
|
||||
// router.push("/");
|
||||
// });
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
|
||||
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;
|
||||
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
@@ -463,6 +463,76 @@
|
||||
"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);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.18.3",
|
||||
"version": "v0.18.2",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -16,6 +16,7 @@
|
||||
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
||||
"migration:run": "tsx -r dotenv/config migration.ts",
|
||||
"manual-migration:run": "tsx -r dotenv/config migrate.ts",
|
||||
"migration:up": "drizzle-kit up --config ./server/db/drizzle.config.ts",
|
||||
"migration:drop": "drizzle-kit drop --config ./server/db/drizzle.config.ts",
|
||||
"db:push": "drizzle-kit push --config ./server/db/drizzle.config.ts",
|
||||
@@ -35,6 +36,7 @@
|
||||
"test": "vitest --config __test__/vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-auth": "1.1.16",
|
||||
"bl": "6.0.11",
|
||||
"rotating-file-stream": "3.2.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 { github } from "@/server/db/schema";
|
||||
import {
|
||||
auth,
|
||||
createGithub,
|
||||
findAdminByAuthId,
|
||||
findAuthById,
|
||||
findUserByAuthId,
|
||||
findUserById,
|
||||
} from "@dokploy/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
@@ -28,7 +30,7 @@ export default async function handler(
|
||||
return res.status(400).json({ error: "Missing code parameter" });
|
||||
}
|
||||
const [action, value] = state?.split(":");
|
||||
// Value could be the authId or the githubProviderId
|
||||
// Value could be the organizationId or the githubProviderId
|
||||
|
||||
if (action === "gh_init") {
|
||||
const octokit = new Octokit({});
|
||||
@@ -39,17 +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(
|
||||
{
|
||||
name: data.name,
|
||||
@@ -60,7 +51,7 @@ export default async function handler(
|
||||
githubWebhookSecret: data.webhook_secret,
|
||||
githubPrivateKey: data.pem,
|
||||
},
|
||||
adminId,
|
||||
value as string,
|
||||
);
|
||||
} else if (action === "gh_setup") {
|
||||
await db
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { buffer } from "node:stream/consumers";
|
||||
import { db } from "@/server/db";
|
||||
import { admins, server } from "@/server/db/schema";
|
||||
import { findAdminById } from "@dokploy/server";
|
||||
import { server, users_temp } from "@/server/db/schema";
|
||||
import { findAdminById, findUserById } from "@dokploy/server";
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import Stripe from "stripe";
|
||||
@@ -64,33 +64,35 @@ export default async function handler(
|
||||
session.subscription as string,
|
||||
);
|
||||
await db
|
||||
.update(admins)
|
||||
.update(users_temp)
|
||||
.set({
|
||||
stripeCustomerId: session.customer as string,
|
||||
stripeSubscriptionId: session.subscription as string,
|
||||
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
||||
})
|
||||
.where(eq(admins.adminId, adminId))
|
||||
.where(eq(users_temp.id, adminId))
|
||||
.returning();
|
||||
|
||||
const admin = await findAdminById(adminId);
|
||||
const admin = await findUserById(adminId);
|
||||
if (!admin) {
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
const newServersQuantity = admin.serversQuantity;
|
||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.created": {
|
||||
const newSubscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db
|
||||
.update(admins)
|
||||
.update(users_temp)
|
||||
.set({
|
||||
stripeSubscriptionId: newSubscription.id,
|
||||
stripeCustomerId: newSubscription.customer as string,
|
||||
})
|
||||
.where(eq(admins.stripeCustomerId, newSubscription.customer as string))
|
||||
.where(
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
)
|
||||
.returning();
|
||||
|
||||
break;
|
||||
@@ -100,14 +102,16 @@ export default async function handler(
|
||||
const newSubscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db
|
||||
.update(admins)
|
||||
.update(users_temp)
|
||||
.set({
|
||||
stripeSubscriptionId: null,
|
||||
serversQuantity: 0,
|
||||
})
|
||||
.where(eq(admins.stripeCustomerId, newSubscription.customer as string));
|
||||
.where(
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
);
|
||||
|
||||
const admin = await findAdminByStripeCustomerId(
|
||||
const admin = await findUserByStripeCustomerId(
|
||||
newSubscription.customer as string,
|
||||
);
|
||||
|
||||
@@ -115,13 +119,13 @@ export default async function handler(
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
|
||||
await disableServers(admin.adminId);
|
||||
await disableServers(admin.id);
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.updated": {
|
||||
const newSubscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
const admin = await findAdminByStripeCustomerId(
|
||||
const admin = await findUserByStripeCustomerId(
|
||||
newSubscription.customer as string,
|
||||
);
|
||||
|
||||
@@ -131,23 +135,23 @@ export default async function handler(
|
||||
|
||||
if (newSubscription.status === "active") {
|
||||
await db
|
||||
.update(admins)
|
||||
.update(users_temp)
|
||||
.set({
|
||||
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
||||
})
|
||||
.where(
|
||||
eq(admins.stripeCustomerId, newSubscription.customer as string),
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
);
|
||||
|
||||
const newServersQuantity = admin.serversQuantity;
|
||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||
} else {
|
||||
await disableServers(admin.adminId);
|
||||
await disableServers(admin.id);
|
||||
await db
|
||||
.update(admins)
|
||||
.update(users_temp)
|
||||
.set({ serversQuantity: 0 })
|
||||
.where(
|
||||
eq(admins.stripeCustomerId, newSubscription.customer as string),
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -174,7 +178,7 @@ export default async function handler(
|
||||
})
|
||||
.where(eq(admins.stripeCustomerId, suscription.customer as string));
|
||||
|
||||
const admin = await findAdminByStripeCustomerId(
|
||||
const admin = await findUserByStripeCustomerId(
|
||||
suscription.customer as string,
|
||||
);
|
||||
|
||||
@@ -182,7 +186,7 @@ export default async function handler(
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
const newServersQuantity = admin.serversQuantity;
|
||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||
break;
|
||||
}
|
||||
case "invoice.payment_failed": {
|
||||
@@ -193,7 +197,7 @@ export default async function handler(
|
||||
);
|
||||
|
||||
if (subscription.status !== "active") {
|
||||
const admin = await findAdminByStripeCustomerId(
|
||||
const admin = await findUserByStripeCustomerId(
|
||||
newInvoice.customer as string,
|
||||
);
|
||||
|
||||
@@ -207,7 +211,7 @@ export default async function handler(
|
||||
})
|
||||
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
|
||||
|
||||
await disableServers(admin.adminId);
|
||||
await disableServers(admin.id);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -216,20 +220,20 @@ export default async function handler(
|
||||
case "customer.deleted": {
|
||||
const customer = event.data.object as Stripe.Customer;
|
||||
|
||||
const admin = await findAdminByStripeCustomerId(customer.id);
|
||||
const admin = await findUserByStripeCustomerId(customer.id);
|
||||
if (!admin) {
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
|
||||
await disableServers(admin.adminId);
|
||||
await disableServers(admin.id);
|
||||
await db
|
||||
.update(admins)
|
||||
.update(users_temp)
|
||||
.set({
|
||||
stripeCustomerId: null,
|
||||
stripeSubscriptionId: null,
|
||||
serversQuantity: 0,
|
||||
})
|
||||
.where(eq(admins.stripeCustomerId, customer.id));
|
||||
.where(eq(users_temp.stripeCustomerId, customer.id));
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -240,20 +244,20 @@ export default async function handler(
|
||||
return res.status(200).json({ received: true });
|
||||
}
|
||||
|
||||
const disableServers = async (adminId: string) => {
|
||||
const disableServers = async (userId: string) => {
|
||||
await db
|
||||
.update(server)
|
||||
.set({
|
||||
serverStatus: "inactive",
|
||||
})
|
||||
.where(eq(server.adminId, adminId));
|
||||
.where(eq(server.userId, userId));
|
||||
};
|
||||
|
||||
const findAdminByStripeCustomerId = async (stripeCustomerId: string) => {
|
||||
const admin = db.query.admins.findFirst({
|
||||
where: eq(admins.stripeCustomerId, stripeCustomerId),
|
||||
const findUserByStripeCustomerId = async (stripeCustomerId: string) => {
|
||||
const user = db.query.users_temp.findFirst({
|
||||
where: eq(users_temp.stripeCustomerId, stripeCustomerId),
|
||||
});
|
||||
return admin;
|
||||
return user;
|
||||
};
|
||||
|
||||
const activateServer = async (serverId: string) => {
|
||||
@@ -270,19 +274,19 @@ const deactivateServer = async (serverId: string) => {
|
||||
.where(eq(server.serverId, serverId));
|
||||
};
|
||||
|
||||
export const findServersByAdminIdSorted = async (adminId: string) => {
|
||||
export const findServersByUserIdSorted = async (userId: string) => {
|
||||
const servers = await db.query.server.findMany({
|
||||
where: eq(server.adminId, adminId),
|
||||
where: eq(server.userId, userId),
|
||||
orderBy: asc(server.createdAt),
|
||||
});
|
||||
|
||||
return servers;
|
||||
};
|
||||
export const updateServersBasedOnQuantity = async (
|
||||
adminId: string,
|
||||
userId: string,
|
||||
newServersQuantity: number,
|
||||
) => {
|
||||
const servers = await findServersByAdminIdSorted(adminId);
|
||||
const servers = await findServersByUserIdSorted(userId);
|
||||
|
||||
if (servers.length > newServersQuantity) {
|
||||
for (const [index, server] of servers.entries()) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
@@ -27,7 +28,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -44,21 +45,20 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
authId: auth.id,
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!user.canAccessToDocker) {
|
||||
if (!userR.canAccessToDocker) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
import { api } from "@/utils/api";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/index";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type React from "react";
|
||||
@@ -25,7 +25,7 @@ const Dashboard = () => {
|
||||
false,
|
||||
);
|
||||
|
||||
const { data: monitoring, isLoading } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: monitoring, isLoading } = api.user.getMetricsToken.useQuery();
|
||||
return (
|
||||
<div className="space-y-4 pb-10">
|
||||
{/* <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) {
|
||||
return {
|
||||
redirect: {
|
||||
|
||||
@@ -49,7 +49,7 @@ import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import type { findProjectById } from "@dokploy/server";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import {
|
||||
Ban,
|
||||
@@ -70,9 +70,9 @@ import type {
|
||||
} from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useMemo, useState, type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import { type ReactElement, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import superjson from "superjson";
|
||||
|
||||
export type Services = {
|
||||
appName: string;
|
||||
@@ -200,15 +200,8 @@ const Project = (
|
||||
) => {
|
||||
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
||||
const { projectId } = props;
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data, isLoading, refetch } = api.project.one.useQuery({ projectId });
|
||||
const router = useRouter();
|
||||
|
||||
@@ -335,7 +328,7 @@ const Project = (
|
||||
</CardTitle>
|
||||
<CardDescription>{data?.description}</CardDescription>
|
||||
</CardHeader>
|
||||
{(auth?.rol === "admin" || user?.canCreateServices) && (
|
||||
{(auth?.role === "owner" || auth?.user?.canCreateServices) && (
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
<ProjectEnvironment projectId={projectId}>
|
||||
<Button variant="outline">Project Environment</Button>
|
||||
@@ -658,7 +651,7 @@ export async function getServerSideProps(
|
||||
const { params } = ctx;
|
||||
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -674,8 +667,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { GlobeIcon, HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||
@@ -86,16 +86,8 @@ const Service = (
|
||||
);
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
@@ -186,7 +178,8 @@ const Service = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateApplication applicationId={applicationId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteServices) && (
|
||||
<DeleteService id={applicationId} type="application" />
|
||||
)}
|
||||
</div>
|
||||
@@ -370,7 +363,7 @@ export async function getServerSideProps(
|
||||
const { query, params, req, res } = ctx;
|
||||
|
||||
const activeTab = query.tab;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -386,8 +379,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CircuitBoard, ServerOff } from "lucide-react";
|
||||
@@ -79,17 +79,9 @@ const Service = (
|
||||
},
|
||||
);
|
||||
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: monitoring } = api.user.getMetricsToken.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 (
|
||||
<div className="pb-10">
|
||||
@@ -181,7 +173,8 @@ const Service = (
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateCompose composeId={composeId} />
|
||||
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteServices) && (
|
||||
<DeleteService id={composeId} type="compose" />
|
||||
)}
|
||||
</div>
|
||||
@@ -366,7 +359,7 @@ export async function getServerSideProps(
|
||||
const { query, params, req, res } = ctx;
|
||||
|
||||
const activeTab = query.tab;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -382,8 +375,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||
import type {
|
||||
@@ -61,16 +61,9 @@ const Mariadb = (
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
@@ -154,7 +147,8 @@ const Mariadb = (
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMariadb mariadbId={mariadbId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteServices) && (
|
||||
<DeleteService id={mariadbId} type="mariadb" />
|
||||
)}
|
||||
</div>
|
||||
@@ -316,7 +310,7 @@ export async function getServerSideProps(
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -332,8 +326,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||
import type {
|
||||
@@ -61,16 +61,8 @@ const Mongo = (
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mongo.one.useQuery({ mongoId });
|
||||
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -156,7 +148,8 @@ const Mongo = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMongo mongoId={mongoId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteServices) && (
|
||||
<DeleteService id={mongoId} type="mongo" />
|
||||
)}
|
||||
</div>
|
||||
@@ -318,7 +311,7 @@ export async function getServerSideProps(
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -334,8 +327,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||
import type {
|
||||
@@ -60,16 +60,8 @@ const MySql = (
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mysql.one.useQuery({ mysqlId });
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -156,7 +148,8 @@ const MySql = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMysql mysqlId={mysqlId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteServices) && (
|
||||
<DeleteService id={mysqlId} type="mysql" />
|
||||
)}
|
||||
</div>
|
||||
@@ -323,7 +316,7 @@ export async function getServerSideProps(
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -339,8 +332,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||
import type {
|
||||
@@ -60,16 +60,9 @@ const Postgresql = (
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
@@ -154,7 +147,8 @@ const Postgresql = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdatePostgres postgresId={postgresId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteServices) && (
|
||||
<DeleteService id={postgresId} type="postgres" />
|
||||
)}
|
||||
</div>
|
||||
@@ -319,7 +313,7 @@ export async function getServerSideProps(
|
||||
) {
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -335,8 +329,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
@@ -60,16 +60,8 @@ const Redis = (
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.redis.one.useQuery({ redisId });
|
||||
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -155,7 +147,8 @@ const Redis = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateRedis redisId={redisId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteServices) && (
|
||||
<DeleteService id={redisId} type="redis" />
|
||||
)}
|
||||
</div>
|
||||
@@ -311,7 +304,7 @@ export async function getServerSideProps(
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -327,8 +320,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ShowProjects } from "@/components/dashboard/projects/show";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
@@ -38,7 +38,7 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
@@ -46,8 +46,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ShowRequests } from "@/components/dashboard/requests/show-requests";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import * as React from "react";
|
||||
@@ -22,7 +23,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
const { user } = await validateRequest(ctx.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
|
||||
@@ -2,7 +2,8 @@ import { ShowBilling } from "@/components/dashboard/settings/billing/show-billin
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
@@ -29,8 +30,8 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -45,8 +46,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -24,8 +24,8 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -40,8 +40,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -33,8 +33,8 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user || user.rol === "user") {
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -48,8 +48,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -25,8 +25,8 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -41,8 +41,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -40,8 +40,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
@@ -49,14 +49,12 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
authId: auth.id,
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!user.canAccessToGitProviders) {
|
||||
if (!userR.canAccessToGitProviders) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -42,9 +42,9 @@ const settings = z.object({
|
||||
type SettingsType = z.infer<typeof settings>;
|
||||
|
||||
const Page = () => {
|
||||
const { data, refetch } = api.admin.one.useQuery();
|
||||
const { data, refetch } = api.user.get.useQuery();
|
||||
const { mutateAsync, isLoading, isError, error } =
|
||||
api.admin.update.useMutation();
|
||||
api.user.update.useMutation();
|
||||
const form = useForm<SettingsType>({
|
||||
defaultValues: {
|
||||
cleanCacheOnApplications: false,
|
||||
@@ -55,9 +55,9 @@ const Page = () => {
|
||||
});
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
cleanCacheOnApplications: data?.cleanupCacheApplications || false,
|
||||
cleanCacheOnCompose: data?.cleanupCacheOnCompose || false,
|
||||
cleanCacheOnPreviews: data?.cleanupCacheOnPreviews || false,
|
||||
cleanCacheOnApplications: data?.user.cleanupCacheApplications || false,
|
||||
cleanCacheOnCompose: data?.user.cleanupCacheOnCompose || false,
|
||||
cleanCacheOnPreviews: data?.user.cleanupCacheOnPreviews || false,
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
@@ -181,7 +181,7 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -190,7 +190,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.rol === "user") {
|
||||
if (user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -205,8 +205,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -25,8 +25,8 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -41,8 +41,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -13,22 +13,16 @@ import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: data?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!data?.id && data?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { data } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<ProfileForm />
|
||||
{(user?.canAccessToAPI || data?.rol === "admin") && <GenerateToken />}
|
||||
{(data?.user?.canAccessToAPI || data?.role === "owner") && (
|
||||
<GenerateToken />
|
||||
)}
|
||||
|
||||
{isCloud && <RemoveSelfAccount />}
|
||||
</div>
|
||||
@@ -46,7 +40,7 @@ export async function getServerSideProps(
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const locale = getLocale(req.cookies);
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
@@ -54,18 +48,21 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
await helpers.auth.get.prefetch();
|
||||
if (user?.rol === "user") {
|
||||
await helpers.user.byAuthId.prefetch({
|
||||
authId: user.authId,
|
||||
});
|
||||
if (user?.role === "member") {
|
||||
// const userR = await helpers.user.one.fetch({
|
||||
// userId: user.id,
|
||||
// });
|
||||
// await helpers.user.byAuthId.prefetch({
|
||||
// authId: user.authId,
|
||||
// });
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
|
||||
@@ -25,8 +25,8 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -40,8 +40,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -2,19 +2,7 @@ import { SetupMonitoring } from "@/components/dashboard/settings/servers/setup-m
|
||||
import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
||||
import { WebServer } from "@/components/dashboard/settings/web-server";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
@@ -25,8 +13,6 @@ import { toast } from "sonner";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
const { data, refetch } = api.admin.one.useQuery();
|
||||
const { mutateAsync: update } = api.admin.update.useMutation();
|
||||
return (
|
||||
<div className="w-full">
|
||||
<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) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -107,7 +93,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.rol === "user") {
|
||||
if (user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -122,8 +108,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function getServerSideProps(
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const locale = await getLocale(req.cookies);
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -36,7 +36,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.rol === "user") {
|
||||
if (user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -51,8 +51,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -40,23 +40,22 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
authId: auth.id,
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!user.canAccessToSSHKeys) {
|
||||
if (!userR.canAccessToSSHKeys) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations";
|
||||
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
|
||||
@@ -12,6 +13,7 @@ const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowUsers />
|
||||
<ShowInvitations />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -25,8 +27,10 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
console.log("user", user, session);
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -41,8 +45,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
@@ -27,7 +28,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -44,21 +45,20 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
authId: auth.id,
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!user.canAccessToDocker) {
|
||||
if (!userR.canAccessToDocker) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ShowTraefikSystem } from "@/components/dashboard/file-system/show-traefik-system";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
@@ -27,7 +28,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -44,21 +45,20 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
authId: auth.id,
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!user.canAccessToTraefikFiles) {
|
||||
if (!userR.canAccessToTraefikFiles) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -3,99 +3,177 @@ import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Logo } from "@/components/shared/logo";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import { CardContent, CardDescription } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
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 { 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 base32 from "hi-base32";
|
||||
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { TOTP } from "otpauth";
|
||||
import { type ReactElement, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Email is required",
|
||||
})
|
||||
.email({
|
||||
message: "Email must be a valid email",
|
||||
}),
|
||||
|
||||
password: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Password is required",
|
||||
})
|
||||
.min(8, {
|
||||
message: "Password must be at least 8 characters",
|
||||
}),
|
||||
const LoginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
|
||||
type Login = z.infer<typeof loginSchema>;
|
||||
const TwoFactorSchema = z.object({
|
||||
code: z.string().min(6),
|
||||
});
|
||||
|
||||
type AuthResponse = {
|
||||
is2FAEnabled: boolean;
|
||||
authId: string;
|
||||
};
|
||||
const BackupCodeSchema = z.object({
|
||||
code: z.string().min(8, {
|
||||
message: "Backup code must be at least 8 characters",
|
||||
}),
|
||||
});
|
||||
|
||||
type LoginForm = z.infer<typeof LoginSchema>;
|
||||
type BackupCodeForm = z.infer<typeof BackupCodeSchema>;
|
||||
|
||||
interface Props {
|
||||
IS_CLOUD: boolean;
|
||||
}
|
||||
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 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: {
|
||||
email: "",
|
||||
password: "",
|
||||
email: "siumauricio@hotmail.com",
|
||||
password: "Password123",
|
||||
},
|
||||
resolver: zodResolver(loginSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (values: Login) => {
|
||||
await mutateAsync({
|
||||
email: values.email.toLowerCase(),
|
||||
password: values.password,
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.is2FAEnabled) {
|
||||
setTemp(data);
|
||||
} else {
|
||||
toast.success("Successfully signed in", {
|
||||
duration: 2000,
|
||||
});
|
||||
router.push("/dashboard/projects");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Signin failed", {
|
||||
duration: 2000,
|
||||
});
|
||||
const onSubmit = async (values: LoginForm) => {
|
||||
setIsLoginLoading(true);
|
||||
try {
|
||||
const { data, error } = await authClient.signIn.email({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
setError(error.message || "An error occurred while logging in");
|
||||
return;
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
{isError && (
|
||||
{error && (
|
||||
<AlertBlock type="error" className="my-2">
|
||||
<span>{error?.message}</span>
|
||||
<span>{error}</span>
|
||||
</AlertBlock>
|
||||
)}
|
||||
<CardContent className="p-0">
|
||||
{!temp.is2FAEnabled ? (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-4">
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input 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>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" isLoading={isLoading} className="w-full">
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
{!isTwoFactor ? (
|
||||
<Form {...loginForm}>
|
||||
<form
|
||||
onSubmit={loginForm.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="login-form"
|
||||
>
|
||||
<FormField
|
||||
control={loginForm.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="john@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={loginForm.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
className="w-full"
|
||||
type="submit"
|
||||
isLoading={isLoginLoading}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</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">
|
||||
@@ -203,8 +395,7 @@ Home.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
if (IS_CLOUD) {
|
||||
try {
|
||||
const { user } = await validateRequest(context.req, context.res);
|
||||
|
||||
const { user } = await validateRequest(context.req);
|
||||
if (user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -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) {
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Logo } from "@/components/shared/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -16,10 +17,11 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { IS_CLOUD, getUserByToken } from "@dokploy/server";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { AlertCircle, AlertTriangle } from "lucide-react";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -30,6 +32,9 @@ import { z } from "zod";
|
||||
|
||||
const registerSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, {
|
||||
@@ -38,7 +43,6 @@ const registerSchema = z
|
||||
.email({
|
||||
message: "Email must be a valid email",
|
||||
}),
|
||||
|
||||
password: z
|
||||
.string()
|
||||
.min(1, {
|
||||
@@ -71,11 +75,17 @@ interface Props {
|
||||
token: string;
|
||||
invitation: Awaited<ReturnType<typeof getUserByToken>>;
|
||||
isCloud: boolean;
|
||||
userAlreadyExists: boolean;
|
||||
}
|
||||
|
||||
const Invitation = ({ token, invitation, isCloud }: Props) => {
|
||||
const Invitation = ({
|
||||
token,
|
||||
invitation,
|
||||
isCloud,
|
||||
userAlreadyExists,
|
||||
}: Props) => {
|
||||
const router = useRouter();
|
||||
const { data } = api.admin.getUserByToken.useQuery(
|
||||
const { data } = api.user.getUserByToken.useQuery(
|
||||
{
|
||||
token,
|
||||
},
|
||||
@@ -90,6 +100,7 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
|
||||
|
||||
const form = useForm<Register>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
@@ -98,9 +109,9 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.auth?.email) {
|
||||
if (data?.email) {
|
||||
form.reset({
|
||||
email: data?.auth?.email || "",
|
||||
email: data?.email || "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
@@ -108,20 +119,32 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
const onSubmit = async (values: Register) => {
|
||||
await mutateAsync({
|
||||
id: data?.authId,
|
||||
password: values.password,
|
||||
token: token,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("User registered successfuly", {
|
||||
description:
|
||||
"Please check your inbox or spam folder to confirm your account.",
|
||||
duration: 100000,
|
||||
});
|
||||
router.push("/dashboard/projects");
|
||||
})
|
||||
.catch((e) => e);
|
||||
try {
|
||||
const { data, error } = await authClient.signUp.email({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
name: values.name,
|
||||
fetchOptions: {
|
||||
headers: {
|
||||
"x-dokploy-token": token,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await authClient.organization.acceptInvitation({
|
||||
invitationId: token,
|
||||
});
|
||||
|
||||
toast.success("Account created successfully");
|
||||
router.push("/dashboard/projects");
|
||||
} catch (error) {
|
||||
toast.error("An error occurred while creating your account");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -138,114 +161,155 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
|
||||
</Link>
|
||||
Invitation
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Fill the form below to create your account
|
||||
</CardDescription>
|
||||
<div className="w-full">
|
||||
<div className="p-3" />
|
||||
{userAlreadyExists ? (
|
||||
<div className="flex flex-col gap-4 justify-center items-center">
|
||||
<AlertBlock type="success">
|
||||
<div className="flex flex-col gap-2">
|
||||
<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 && (
|
||||
<div className="mx-5 my-2 flex flex-row items-center gap-2 rounded-lg 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>
|
||||
)}
|
||||
<Button asChild variant="default" className="w-full">
|
||||
<Link href="/">Sign In</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<CardDescription>
|
||||
Fill the form below to create your account
|
||||
</CardDescription>
|
||||
<div className="w-full">
|
||||
<div className="p-3" />
|
||||
|
||||
<CardContent className="p-0">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid gap-4"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
{isError && (
|
||||
<div className="mx-5 my-2 flex flex-row items-center gap-2 rounded-lg 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>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<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"
|
||||
<CardContent className="p-0">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid gap-4"
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
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">
|
||||
{isCloud && (
|
||||
<>
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/"
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<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
|
||||
</Link>
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/send-reset-password"
|
||||
>
|
||||
Lost your password?
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</div>
|
||||
Register
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm flex flex-row justify-between gap-2 w-full">
|
||||
{isCloud && (
|
||||
<>
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/send-reset-password"
|
||||
>
|
||||
Lost your password?
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// http://localhost:3000/invitation?token=CZK4BLrUdMa32RVkAdZiLsPDdvnPiAgZ
|
||||
// /f7af93acc1a99eae864972ab4c92fee089f0d83473d415ede8e821e5dbabe79c
|
||||
export default Invitation;
|
||||
Invitation.getLayout = (page: ReactElement) => {
|
||||
return <OnboardingLayout>{page}</OnboardingLayout>;
|
||||
@@ -254,6 +318,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
const { query } = ctx;
|
||||
|
||||
const token = query.token;
|
||||
console.log("query", query);
|
||||
|
||||
if (typeof token !== "string") {
|
||||
return {
|
||||
@@ -267,6 +332,17 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
try {
|
||||
const invitation = await getUserByToken(token);
|
||||
|
||||
if (invitation.userAlreadyExists) {
|
||||
return {
|
||||
props: {
|
||||
isCloud: IS_CLOUD,
|
||||
token: token,
|
||||
invitation: invitation,
|
||||
userAlreadyExists: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (invitation.isExpired) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -284,6 +360,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -31,6 +32,9 @@ import { z } from "zod";
|
||||
|
||||
const registerSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, {
|
||||
@@ -79,9 +83,10 @@ const Register = ({ isCloud }: Props) => {
|
||||
|
||||
const form = useForm<Register>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
name: "Mauricio Siu",
|
||||
email: "user5@yopmail.com",
|
||||
password: "Password1234",
|
||||
confirmPassword: "Password1234",
|
||||
},
|
||||
resolver: zodResolver(registerSchema),
|
||||
});
|
||||
@@ -91,19 +96,33 @@ const Register = ({ isCloud }: Props) => {
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (values: Register) => {
|
||||
await mutateAsync({
|
||||
email: values.email.toLowerCase(),
|
||||
const { data, error } = await authClient.signUp.email({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("User registered successfuly", {
|
||||
duration: 2000,
|
||||
});
|
||||
if (!isCloud) {
|
||||
router.push("/");
|
||||
}
|
||||
})
|
||||
.catch((e) => e);
|
||||
name: values.name,
|
||||
});
|
||||
|
||||
// const { data, error } = await authClient.admin.createUser({
|
||||
// name: values.name,
|
||||
// email: values.email,
|
||||
// password: values.password,
|
||||
// role: "superAdmin",
|
||||
// });
|
||||
|
||||
// consol/e.log(data, error);
|
||||
// await mutateAsync({
|
||||
// email: values.email.toLowerCase(),
|
||||
// password: values.password,
|
||||
// })
|
||||
// .then(() => {
|
||||
// toast.success("User registered successfuly", {
|
||||
// duration: 2000,
|
||||
// });
|
||||
// if (!isCloud) {
|
||||
// router.push("/");
|
||||
// }
|
||||
// })
|
||||
// .catch((e) => e);
|
||||
};
|
||||
return (
|
||||
<div className="">
|
||||
@@ -147,6 +166,19 @@ const Register = ({ isCloud }: Props) => {
|
||||
className="grid gap-4"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="name" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
@@ -242,7 +274,7 @@ Register.getLayout = (page: ReactElement) => {
|
||||
};
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
if (IS_CLOUD) {
|
||||
const { user } = await validateRequest(context.req, context.res);
|
||||
const { user } = await validateRequest(context.req);
|
||||
|
||||
if (user) {
|
||||
return {
|
||||
|
||||
@@ -38,7 +38,7 @@ const Home: NextPage = () => {
|
||||
export default Home;
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { req, res } = context;
|
||||
const { user, session } = await validateRequest(context.req, context.res);
|
||||
const { user, session } = await validateRequest(context.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -53,17 +53,17 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
if (user.rol === "user") {
|
||||
const result = await helpers.user.byAuthId.fetch({
|
||||
authId: user.id,
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!result.canAccessToAPI) {
|
||||
if (!userR.canAccessToAPI) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -19,6 +19,7 @@ import { mongoRouter } from "./routers/mongo";
|
||||
import { mountRouter } from "./routers/mount";
|
||||
import { mysqlRouter } from "./routers/mysql";
|
||||
import { notificationRouter } from "./routers/notification";
|
||||
import { organizationRouter } from "./routers/organization";
|
||||
import { portRouter } from "./routers/port";
|
||||
import { postgresRouter } from "./routers/postgres";
|
||||
import { previewDeploymentRouter } from "./routers/preview-deployment";
|
||||
@@ -33,7 +34,6 @@ import { sshRouter } from "./routers/ssh-key";
|
||||
import { stripeRouter } from "./routers/stripe";
|
||||
import { swarmRouter } from "./routers/swarm";
|
||||
import { userRouter } from "./routers/user";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
*
|
||||
@@ -75,6 +75,7 @@ export const appRouter = createTRPCRouter({
|
||||
server: serverRouter,
|
||||
stripe: stripeRouter,
|
||||
swarm: swarmRouter,
|
||||
organization: organizationRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiAssignPermissions,
|
||||
apiCreateUserInvitation,
|
||||
apiFindOneToken,
|
||||
apiRemoveUser,
|
||||
apiUpdateAdmin,
|
||||
apiUpdateWebServerMonitoring,
|
||||
users,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
createInvitation,
|
||||
findAdminById,
|
||||
findUserByAuthId,
|
||||
findOrganizationById,
|
||||
findUserById,
|
||||
getUserByToken,
|
||||
removeUserByAuthId,
|
||||
removeUserById,
|
||||
setupWebMonitoring,
|
||||
updateAdmin,
|
||||
updateAdminById,
|
||||
updateUser,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
adminProcedure,
|
||||
@@ -32,30 +26,33 @@ import {
|
||||
|
||||
export const adminRouter = createTRPCRouter({
|
||||
one: adminProcedure.query(async ({ ctx }) => {
|
||||
const { sshPrivateKey, ...rest } = await findAdminById(ctx.user.adminId);
|
||||
const { sshPrivateKey, ...rest } = await findUserById(ctx.user.id);
|
||||
return {
|
||||
haveSSH: !!sshPrivateKey,
|
||||
...rest,
|
||||
};
|
||||
}),
|
||||
update: adminProcedure
|
||||
.input(apiUpdateAdmin)
|
||||
.input(
|
||||
z.object({
|
||||
enableDockerCleanup: z.boolean(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
if (ctx.user.rol === "member") {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to update this admin",
|
||||
});
|
||||
}
|
||||
const { authId } = await findAdminById(ctx.user.adminId);
|
||||
// @ts-ignore
|
||||
return updateAdmin(authId, input);
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
return updateUser(user.id, {});
|
||||
}),
|
||||
createUserInvitation: adminProcedure
|
||||
.input(apiCreateUserInvitation)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
await createInvitation(input, ctx.user.adminId);
|
||||
await createInvitation(input, ctx.user.id);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -69,15 +66,16 @@ export const adminRouter = createTRPCRouter({
|
||||
.input(apiRemoveUser)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const user = await findUserByAuthId(input.authId);
|
||||
const user = await findUserById(input.id);
|
||||
|
||||
if (user.adminId !== ctx.user.adminId) {
|
||||
if (user.id !== ctx.user.ownerId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to delete this user",
|
||||
});
|
||||
}
|
||||
return await removeUserByAuthId(input.authId);
|
||||
|
||||
return await removeUserById(input.id);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -95,20 +93,22 @@ export const adminRouter = createTRPCRouter({
|
||||
.input(apiAssignPermissions)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const user = await findUserById(input.userId);
|
||||
const user = await findUserById(input.id);
|
||||
|
||||
if (user.adminId !== ctx.user.adminId) {
|
||||
const organization = await findOrganizationById(
|
||||
ctx.session?.activeOrganizationId || "",
|
||||
);
|
||||
|
||||
if (organization?.ownerId !== ctx.user.ownerId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to assign permissions",
|
||||
});
|
||||
}
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
...input,
|
||||
})
|
||||
.where(eq(users.userId, input.userId));
|
||||
|
||||
await updateUser(user.id, {
|
||||
...input,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -124,15 +124,15 @@ export const adminRouter = createTRPCRouter({
|
||||
message: "Feature disabled on cloud",
|
||||
});
|
||||
}
|
||||
const admin = await findAdminById(ctx.user.adminId);
|
||||
if (admin.adminId !== ctx.user.adminId) {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
if (user.id !== ctx.user.ownerId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to setup this server",
|
||||
message: "You are not authorized to setup the monitoring",
|
||||
});
|
||||
}
|
||||
|
||||
await updateAdminById(admin.adminId, {
|
||||
await updateUser(user.id, {
|
||||
metricsConfig: {
|
||||
server: {
|
||||
type: "Dokploy",
|
||||
@@ -156,18 +156,19 @@ export const adminRouter = createTRPCRouter({
|
||||
},
|
||||
},
|
||||
});
|
||||
const currentServer = await setupWebMonitoring(admin.adminId);
|
||||
|
||||
const currentServer = await setupWebMonitoring(user.id);
|
||||
return currentServer;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
|
||||
const admin = await findAdminById(ctx.user.adminId);
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
return {
|
||||
serverIp: admin.serverIp,
|
||||
enabledFeatures: admin.enablePaidFeatures,
|
||||
metricsConfig: admin?.metricsConfig,
|
||||
serverIp: user.serverIp,
|
||||
enabledFeatures: user.enablePaidFeatures,
|
||||
metricsConfig: user?.metricsConfig,
|
||||
};
|
||||
}),
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiCreateApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
||||
if (ctx.user.rol === "member") {
|
||||
await checkServiceAccess(ctx.user.id, input.projectId, "create");
|
||||
}
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
@@ -72,7 +72,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const project = await findProjectById(input.projectId);
|
||||
if (project.adminId !== ctx.user.adminId) {
|
||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this project",
|
||||
@@ -80,8 +80,8 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
const newApplication = await createApplication(input);
|
||||
|
||||
if (ctx.user.rol === "user") {
|
||||
await addNewService(ctx.user.authId, newApplication.applicationId);
|
||||
if (ctx.user.rol === "member") {
|
||||
await addNewService(ctx.user.id, newApplication.applicationId);
|
||||
}
|
||||
return newApplication;
|
||||
} catch (error: unknown) {
|
||||
@@ -98,15 +98,13 @@ export const applicationRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.authId,
|
||||
input.applicationId,
|
||||
"access",
|
||||
);
|
||||
if (ctx.user.rol === "member") {
|
||||
await checkServiceAccess(ctx.user.id, input.applicationId, "access");
|
||||
}
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
@@ -119,7 +117,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiReloadApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this application",
|
||||
@@ -144,16 +144,14 @@ export const applicationRouter = createTRPCRouter({
|
||||
delete: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.authId,
|
||||
input.applicationId,
|
||||
"delete",
|
||||
);
|
||||
if (ctx.user.rol === "member") {
|
||||
await checkServiceAccess(ctx.user.id, input.applicationId, "delete");
|
||||
}
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this application",
|
||||
@@ -194,7 +192,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const service = await findApplicationById(input.applicationId);
|
||||
if (service.project.adminId !== ctx.user.adminId) {
|
||||
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this application",
|
||||
@@ -214,7 +212,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const service = await findApplicationById(input.applicationId);
|
||||
if (service.project.adminId !== ctx.user.adminId) {
|
||||
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this application",
|
||||
@@ -235,7 +233,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to redeploy this application",
|
||||
@@ -268,7 +268,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiSaveEnvironmentVariables)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
@@ -284,7 +286,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiSaveBuildType)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this build type",
|
||||
@@ -305,7 +309,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiSaveGithubProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this github provider",
|
||||
@@ -327,7 +333,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiSaveGitlabProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this gitlab provider",
|
||||
@@ -351,7 +359,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiSaveBitbucketProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this bitbucket provider",
|
||||
@@ -373,7 +383,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiSaveDockerProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this docker provider",
|
||||
@@ -394,7 +406,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiSaveGitProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this git provider",
|
||||
@@ -415,7 +429,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to mark this application as running",
|
||||
@@ -427,7 +443,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiUpdateApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this application",
|
||||
@@ -451,7 +469,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to refresh this application",
|
||||
@@ -466,7 +486,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this application",
|
||||
@@ -500,7 +522,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to clean this application",
|
||||
@@ -513,7 +537,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to read this application",
|
||||
@@ -548,7 +574,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
|
||||
const app = await findApplicationById(input.applicationId as string);
|
||||
|
||||
if (app.project.adminId !== ctx.user.adminId) {
|
||||
if (app.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this application",
|
||||
@@ -590,7 +616,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this application",
|
||||
|
||||
@@ -1,37 +1,30 @@
|
||||
import {
|
||||
apiCreateAdmin,
|
||||
apiCreateUser,
|
||||
apiFindOneAuth,
|
||||
apiLogin,
|
||||
apiUpdateAuth,
|
||||
apiVerify2FA,
|
||||
apiVerifyLogin2FA,
|
||||
auth,
|
||||
// apiCreateAdmin,
|
||||
// apiCreateUser,
|
||||
// apiFindOneAuth,
|
||||
// apiLogin,
|
||||
// apiUpdateAuth,
|
||||
// apiVerify2FA,
|
||||
// apiVerifyLogin2FA,
|
||||
// auth,
|
||||
member,
|
||||
} from "@/server/db/schema";
|
||||
import { WEBSITE_URL } from "@/server/utils/stripe";
|
||||
import {
|
||||
type Auth,
|
||||
IS_CLOUD,
|
||||
createAdmin,
|
||||
createUser,
|
||||
findAuthByEmail,
|
||||
findAuthById,
|
||||
findUserById,
|
||||
generate2FASecret,
|
||||
getUserByToken,
|
||||
lucia,
|
||||
luciaToken,
|
||||
removeAdminByAuthId,
|
||||
removeUserByAuthId,
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
updateAuthById,
|
||||
validateRequest,
|
||||
verify2FA,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { isBefore } from "date-fns";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { db } from "../../db";
|
||||
@@ -43,81 +36,77 @@ import {
|
||||
} from "../trpc";
|
||||
|
||||
export const authRouter = createTRPCRouter({
|
||||
createAdmin: publicProcedure
|
||||
.input(apiCreateAdmin)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (!IS_CLOUD) {
|
||||
const admin = await db.query.admins.findFirst({});
|
||||
if (admin) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Admin already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
const newAdmin = await createAdmin(input);
|
||||
|
||||
if (IS_CLOUD) {
|
||||
await sendDiscordNotificationWelcome(newAdmin);
|
||||
await sendVerificationEmail(newAdmin.id);
|
||||
return {
|
||||
status: "success",
|
||||
type: "cloud",
|
||||
};
|
||||
}
|
||||
const session = await lucia.createSession(newAdmin.id || "", {});
|
||||
ctx.res.appendHeader(
|
||||
"Set-Cookie",
|
||||
lucia.createSessionCookie(session.id).serialize(),
|
||||
);
|
||||
return {
|
||||
status: "success",
|
||||
type: "selfhosted",
|
||||
};
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
// @ts-ignore
|
||||
message: `Error: ${error?.code === "23505" ? "Email already exists" : "Error creating admin"}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
createUser: publicProcedure
|
||||
.input(apiCreateUser)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
const token = await getUserByToken(input.token);
|
||||
if (token.isExpired) {
|
||||
createAdmin: publicProcedure.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (!IS_CLOUD) {
|
||||
const admin = await db.query.admins.findFirst({});
|
||||
if (admin) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Invalid token",
|
||||
message: "Admin already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newUser = await createUser(input);
|
||||
|
||||
if (IS_CLOUD) {
|
||||
await sendVerificationEmail(token.authId);
|
||||
return true;
|
||||
}
|
||||
const session = await lucia.createSession(newUser?.authId || "", {});
|
||||
ctx.res.appendHeader(
|
||||
"Set-Cookie",
|
||||
lucia.createSessionCookie(session.id).serialize(),
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the user",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
const newAdmin = await createAdmin(input);
|
||||
|
||||
login: publicProcedure.input(apiLogin).mutation(async ({ ctx, input }) => {
|
||||
if (IS_CLOUD) {
|
||||
await sendDiscordNotificationWelcome(newAdmin);
|
||||
await sendVerificationEmail(newAdmin.id);
|
||||
return {
|
||||
status: "success",
|
||||
type: "cloud",
|
||||
};
|
||||
}
|
||||
// const session = await lucia.createSession(newAdmin.id || "", {});
|
||||
// ctx.res.appendHeader(
|
||||
// "Set-Cookie",
|
||||
// lucia.createSessionCookie(session.id).serialize(),
|
||||
// );
|
||||
return {
|
||||
status: "success",
|
||||
type: "selfhosted",
|
||||
};
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
// @ts-ignore
|
||||
message: `Error: ${error?.code === "23505" ? "Email already exists" : "Error creating admin"}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
createUser: publicProcedure.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
const token = await getUserByToken(input.token);
|
||||
// if (token.isExpired) {
|
||||
// throw new TRPCError({
|
||||
// code: "BAD_REQUEST",
|
||||
// message: "Invalid token",
|
||||
// });
|
||||
// }
|
||||
|
||||
// const newUser = await createUser(input);
|
||||
|
||||
// if (IS_CLOUD) {
|
||||
// await sendVerificationEmail(token.authId);
|
||||
// return true;
|
||||
// }
|
||||
// const session = await lucia.createSession(newUser?.authId || "", {});
|
||||
// ctx.res.appendHeader(
|
||||
// "Set-Cookie",
|
||||
// lucia.createSessionCookie(session.id).serialize(),
|
||||
// );
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the user",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
login: publicProcedure.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
const auth = await findAuthByEmail(input.email);
|
||||
|
||||
@@ -149,12 +138,12 @@ export const authRouter = createTRPCRouter({
|
||||
};
|
||||
}
|
||||
|
||||
const session = await lucia.createSession(auth?.id || "", {});
|
||||
// const session = await lucia.createSession(auth?.id || "", {});
|
||||
|
||||
ctx.res.appendHeader(
|
||||
"Set-Cookie",
|
||||
lucia.createSessionCookie(session.id).serialize(),
|
||||
);
|
||||
// ctx.res.appendHeader(
|
||||
// "Set-Cookie",
|
||||
// lucia.createSessionCookie(session.id).serialize(),
|
||||
// );
|
||||
return {
|
||||
is2FAEnabled: false,
|
||||
authId: auth?.id,
|
||||
@@ -169,47 +158,54 @@ export const authRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
get: protectedProcedure.query(async ({ ctx }) => {
|
||||
const auth = await findAuthById(ctx.user.authId);
|
||||
return auth;
|
||||
const memberResult = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, ctx.user.id),
|
||||
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
||||
),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
return memberResult;
|
||||
}),
|
||||
|
||||
logout: protectedProcedure.mutation(async ({ ctx }) => {
|
||||
const { req, res } = ctx;
|
||||
const { session } = await validateRequest(req, res);
|
||||
const { session } = await validateRequest(req);
|
||||
if (!session) return false;
|
||||
|
||||
await lucia.invalidateSession(session.id);
|
||||
res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
|
||||
// await lucia.invalidateSession(session.id);
|
||||
// res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
|
||||
return true;
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateAuth)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const currentAuth = await findAuthByEmail(ctx.user.email);
|
||||
update: protectedProcedure.mutation(async ({ ctx, input }) => {
|
||||
const currentAuth = await findAuthByEmail(ctx.user.email);
|
||||
|
||||
if (input.currentPassword || input.password) {
|
||||
const correctPassword = bcrypt.compareSync(
|
||||
input.currentPassword || "",
|
||||
currentAuth?.password || "",
|
||||
);
|
||||
if (!correctPassword) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Current password is incorrect",
|
||||
});
|
||||
}
|
||||
if (input.currentPassword || input.password) {
|
||||
const correctPassword = bcrypt.compareSync(
|
||||
input.currentPassword || "",
|
||||
currentAuth?.password || "",
|
||||
);
|
||||
if (!correctPassword) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Current password is incorrect",
|
||||
});
|
||||
}
|
||||
const auth = await updateAuthById(ctx.user.authId, {
|
||||
...(input.email && { email: input.email.toLowerCase() }),
|
||||
...(input.password && {
|
||||
password: bcrypt.hashSync(input.password, 10),
|
||||
}),
|
||||
...(input.image && { image: input.image }),
|
||||
});
|
||||
}
|
||||
// const auth = await updateAuthById(ctx.user.authId, {
|
||||
// ...(input.email && { email: input.email.toLowerCase() }),
|
||||
// ...(input.password && {
|
||||
// password: bcrypt.hashSync(input.password, 10),
|
||||
// }),
|
||||
// ...(input.image && { image: input.image }),
|
||||
// });
|
||||
|
||||
return auth;
|
||||
}),
|
||||
return auth;
|
||||
}),
|
||||
removeSelfAccount: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
@@ -237,84 +233,81 @@ export const authRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const { req, res } = ctx;
|
||||
const { session } = await validateRequest(req, res);
|
||||
const { session } = await validateRequest(req);
|
||||
if (!session) return false;
|
||||
|
||||
await lucia.invalidateSession(session.id);
|
||||
res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
|
||||
// await lucia.invalidateSession(session.id);
|
||||
// res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
|
||||
|
||||
if (ctx.user.rol === "admin") {
|
||||
await removeAdminByAuthId(ctx.user.authId);
|
||||
} else {
|
||||
await removeUserByAuthId(ctx.user.authId);
|
||||
}
|
||||
// if (ctx.user.rol === "owner") {
|
||||
// await removeAdminByAuthId(ctx.user.authId);
|
||||
// } else {
|
||||
// await removeUserByAuthId(ctx.user.authId);
|
||||
// }
|
||||
|
||||
return true;
|
||||
}),
|
||||
|
||||
generateToken: protectedProcedure.mutation(async ({ ctx, input }) => {
|
||||
const auth = await findAuthById(ctx.user.authId);
|
||||
|
||||
if (auth.token) {
|
||||
await luciaToken.invalidateSession(auth.token);
|
||||
}
|
||||
const session = await luciaToken.createSession(auth?.id || "", {
|
||||
expiresIn: 60 * 60 * 24 * 30,
|
||||
});
|
||||
|
||||
await updateAuthById(auth.id, {
|
||||
token: session.id,
|
||||
});
|
||||
const auth = await findUserById(ctx.user.id);
|
||||
console.log(auth);
|
||||
|
||||
// if (auth.token) {
|
||||
// await luciaToken.invalidateSession(auth.token);
|
||||
// }
|
||||
// const session = await luciaToken.createSession(auth?.id || "", {
|
||||
// expiresIn: 60 * 60 * 24 * 30,
|
||||
// });
|
||||
// await updateUser(auth.id, {
|
||||
// token: session.id,
|
||||
// });
|
||||
return auth;
|
||||
}),
|
||||
verifyToken: protectedProcedure.mutation(async () => {
|
||||
return true;
|
||||
}),
|
||||
one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => {
|
||||
const auth = await findAuthById(input.id);
|
||||
return auth;
|
||||
}),
|
||||
one: adminProcedure
|
||||
.input(z.object({ userId: z.string().min(1) }))
|
||||
.query(async ({ input }) => {
|
||||
// TODO: Check if the user is admin or member
|
||||
const user = await findUserById(input.userId);
|
||||
return user;
|
||||
}),
|
||||
|
||||
generate2FASecret: protectedProcedure.query(async ({ ctx }) => {
|
||||
return await generate2FASecret(ctx.user.authId);
|
||||
return await generate2FASecret(ctx.user.id);
|
||||
}),
|
||||
verify2FASetup: protectedProcedure.mutation(async ({ ctx, input }) => {
|
||||
// const auth = await findAuthById(ctx.user.authId);
|
||||
// await verify2FA(auth, input.secret, input.pin);
|
||||
// await updateAuthById(auth.id, {
|
||||
// is2FAEnabled: true,
|
||||
// secret: input.secret,
|
||||
// });
|
||||
// return auth;
|
||||
}),
|
||||
verify2FASetup: protectedProcedure
|
||||
.input(apiVerify2FA)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const auth = await findAuthById(ctx.user.authId);
|
||||
|
||||
await verify2FA(auth, input.secret, input.pin);
|
||||
await updateAuthById(auth.id, {
|
||||
is2FAEnabled: true,
|
||||
secret: input.secret,
|
||||
});
|
||||
return auth;
|
||||
}),
|
||||
verifyLogin2FA: publicProcedure.mutation(async ({ ctx, input }) => {
|
||||
// const auth = await findAuthById(input.id);
|
||||
|
||||
verifyLogin2FA: publicProcedure
|
||||
.input(apiVerifyLogin2FA)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const auth = await findAuthById(input.id);
|
||||
// await verify2FA(auth, auth.secret || "", input.pin);
|
||||
|
||||
await verify2FA(auth, auth.secret || "", input.pin);
|
||||
// const session = await lucia.createSession(auth.id, {});
|
||||
|
||||
const session = await lucia.createSession(auth.id, {});
|
||||
// ctx.res.appendHeader(
|
||||
// "Set-Cookie",
|
||||
// lucia.createSessionCookie(session.id).serialize(),
|
||||
// );
|
||||
|
||||
ctx.res.appendHeader(
|
||||
"Set-Cookie",
|
||||
lucia.createSessionCookie(session.id).serialize(),
|
||||
);
|
||||
|
||||
return true;
|
||||
}),
|
||||
return true;
|
||||
}),
|
||||
disable2FA: protectedProcedure.mutation(async ({ ctx }) => {
|
||||
const auth = await findAuthById(ctx.user.authId);
|
||||
await updateAuthById(auth.id, {
|
||||
is2FAEnabled: false,
|
||||
secret: null,
|
||||
});
|
||||
return auth;
|
||||
// const auth = await findAuthById(ctx.user.authId);
|
||||
// await updateAuthById(auth.id, {
|
||||
// is2FAEnabled: false,
|
||||
// secret: null,
|
||||
// });
|
||||
// return auth;
|
||||
}),
|
||||
sendResetPasswordEmail: publicProcedure
|
||||
.input(
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
apiUpdateBitbucket,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
createBitbucket,
|
||||
findBitbucketById,
|
||||
getBitbucketBranches,
|
||||
@@ -23,7 +22,7 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
.input(apiCreateBitbucket)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createBitbucket(input, ctx.user.adminId);
|
||||
return await createBitbucket(input, ctx.session.activeOrganizationId);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -37,10 +36,9 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
.query(async ({ input, ctx }) => {
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
if (
|
||||
IS_CLOUD &&
|
||||
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
//TODO: Remove this line when the cloud version is ready
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this bitbucket provider",
|
||||
@@ -58,12 +56,11 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
|
||||
if (IS_CLOUD) {
|
||||
// TODO: mAyBe a rEfaCtoR 🤫
|
||||
result = result.filter(
|
||||
(provider) => provider.gitProvider.adminId === ctx.user.adminId,
|
||||
);
|
||||
}
|
||||
result = result.filter(
|
||||
(provider) =>
|
||||
provider.gitProvider.organizationId ===
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
return result;
|
||||
}),
|
||||
|
||||
@@ -72,10 +69,9 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
.query(async ({ input, ctx }) => {
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
if (
|
||||
IS_CLOUD &&
|
||||
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
//TODO: Remove this line when the cloud version is ready
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this bitbucket provider",
|
||||
@@ -90,10 +86,9 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
input.bitbucketId || "",
|
||||
);
|
||||
if (
|
||||
IS_CLOUD &&
|
||||
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
//TODO: Remove this line when the cloud version is ready
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this bitbucket provider",
|
||||
@@ -107,10 +102,9 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
try {
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
if (
|
||||
IS_CLOUD &&
|
||||
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
//TODO: Remove this line when the cloud version is ready
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this bitbucket provider",
|
||||
@@ -131,10 +125,9 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
if (
|
||||
IS_CLOUD &&
|
||||
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
//TODO: Remove this line when the cloud version is ready
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this bitbucket provider",
|
||||
@@ -142,7 +135,7 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
}
|
||||
return await updateBitbucket(input.bitbucketId, {
|
||||
...input,
|
||||
adminId: ctx.user.adminId,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -25,14 +25,14 @@ export const certificateRouter = createTRPCRouter({
|
||||
message: "Please set a server to create a certificate",
|
||||
});
|
||||
}
|
||||
return await createCertificate(input, ctx.user.adminId);
|
||||
return await createCertificate(input, ctx.session.activeOrganizationId);
|
||||
}),
|
||||
|
||||
one: adminProcedure
|
||||
.input(apiFindCertificate)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const certificates = await findCertificateById(input.certificateId);
|
||||
if (IS_CLOUD && certificates.adminId !== ctx.user.adminId) {
|
||||
if (certificates.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this certificate",
|
||||
@@ -44,7 +44,7 @@ export const certificateRouter = createTRPCRouter({
|
||||
.input(apiFindCertificate)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const certificates = await findCertificateById(input.certificateId);
|
||||
if (IS_CLOUD && certificates.adminId !== ctx.user.adminId) {
|
||||
if (certificates.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to delete this certificate",
|
||||
@@ -55,8 +55,7 @@ export const certificateRouter = createTRPCRouter({
|
||||
}),
|
||||
all: adminProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.certificates.findMany({
|
||||
// TODO: Remove this line when the cloud version is ready
|
||||
...(IS_CLOUD && { where: eq(certificates.adminId, ctx.user.adminId) }),
|
||||
where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -39,11 +39,11 @@ import {
|
||||
createComposeByTemplate,
|
||||
createDomain,
|
||||
createMount,
|
||||
findAdminById,
|
||||
findComposeById,
|
||||
findDomainsByComposeId,
|
||||
findProjectById,
|
||||
findServerById,
|
||||
findUserById,
|
||||
loadServices,
|
||||
randomizeComposeFile,
|
||||
randomizeIsolatedDeploymentComposeFile,
|
||||
@@ -60,8 +60,8 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiCreateCompose)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
||||
if (ctx.user.rol === "member") {
|
||||
await checkServiceAccess(ctx.user.id, input.projectId, "create");
|
||||
}
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
@@ -71,7 +71,7 @@ export const composeRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const project = await findProjectById(input.projectId);
|
||||
if (project.adminId !== ctx.user.adminId) {
|
||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this project",
|
||||
@@ -79,8 +79,8 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
const newService = await createCompose(input);
|
||||
|
||||
if (ctx.user.rol === "user") {
|
||||
await addNewService(ctx.user.authId, newService.composeId);
|
||||
if (ctx.user.rol === "member") {
|
||||
await addNewService(ctx.user.id, newService.composeId);
|
||||
}
|
||||
|
||||
return newService;
|
||||
@@ -92,12 +92,12 @@ export const composeRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.composeId, "access");
|
||||
if (ctx.user.rol === "member") {
|
||||
await checkServiceAccess(ctx.user.id, input.composeId, "access");
|
||||
}
|
||||
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
@@ -110,7 +110,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiUpdateCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this compose",
|
||||
@@ -121,12 +121,15 @@ export const composeRouter = createTRPCRouter({
|
||||
delete: protectedProcedure
|
||||
.input(apiDeleteCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.composeId, "delete");
|
||||
if (ctx.user.rol === "member") {
|
||||
await checkServiceAccess(ctx.user.id, input.composeId, "delete");
|
||||
}
|
||||
const composeResult = await findComposeById(input.composeId);
|
||||
|
||||
if (composeResult.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
composeResult.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this compose",
|
||||
@@ -157,7 +160,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to clean this compose",
|
||||
@@ -170,7 +173,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFetchServices)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to load this compose",
|
||||
@@ -184,7 +187,9 @@ export const composeRouter = createTRPCRouter({
|
||||
try {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
compose.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to fetch this compose",
|
||||
@@ -209,7 +214,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to randomize this compose",
|
||||
@@ -221,7 +226,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to randomize this compose",
|
||||
@@ -236,7 +241,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to get this compose",
|
||||
@@ -254,7 +259,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this compose",
|
||||
@@ -287,7 +292,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to redeploy this compose",
|
||||
@@ -319,7 +324,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this compose",
|
||||
@@ -333,7 +338,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this compose",
|
||||
@@ -348,7 +353,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to get this compose",
|
||||
@@ -361,7 +366,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to refresh this compose",
|
||||
@@ -375,8 +380,8 @@ export const composeRouter = createTRPCRouter({
|
||||
deployTemplate: protectedProcedure
|
||||
.input(apiCreateComposeByTemplate)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
||||
if (ctx.user.rol === "member") {
|
||||
await checkServiceAccess(ctx.user.id, input.projectId, "create");
|
||||
}
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
@@ -390,7 +395,7 @@ export const composeRouter = createTRPCRouter({
|
||||
|
||||
const generate = await loadTemplateModule(input.id as TemplatesKeys);
|
||||
|
||||
const admin = await findAdminById(ctx.user.adminId);
|
||||
const admin = await findUserById(ctx.user.ownerId);
|
||||
let serverIp = admin.serverIp || "127.0.0.1";
|
||||
|
||||
const project = await findProjectById(input.projectId);
|
||||
@@ -418,8 +423,8 @@ export const composeRouter = createTRPCRouter({
|
||||
isolatedDeployment: true,
|
||||
});
|
||||
|
||||
if (ctx.user.rol === "user") {
|
||||
await addNewService(ctx.user.authId, compose.composeId);
|
||||
if (ctx.user.rol === "member") {
|
||||
await addNewService(ctx.user.id, compose.composeId);
|
||||
}
|
||||
|
||||
if (mounts && mounts?.length > 0) {
|
||||
|
||||
@@ -19,7 +19,9 @@ export const deploymentRouter = createTRPCRouter({
|
||||
.input(apiFindAllByApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (application.project.adminId !== ctx.user.adminId) {
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
@@ -32,7 +34,7 @@ export const deploymentRouter = createTRPCRouter({
|
||||
.input(apiFindAllByCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
@@ -44,7 +46,7 @@ export const deploymentRouter = createTRPCRouter({
|
||||
.input(apiFindAllByServer)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.adminId !== ctx.user.adminId) {
|
||||
if (server.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this server",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user