mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-22 16:35:26 +00:00
Compare commits
48 Commits
| 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: "",
|
previewWildcard: "",
|
||||||
project: {
|
project: {
|
||||||
env: "",
|
env: "",
|
||||||
adminId: "",
|
organizationId: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ vi.mock("node:fs", () => ({
|
|||||||
default: fs,
|
default: fs,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import type { Admin, FileConfig } from "@dokploy/server";
|
import type { FileConfig, User } from "@dokploy/server";
|
||||||
import {
|
import {
|
||||||
createDefaultServerTraefikConfig,
|
createDefaultServerTraefikConfig,
|
||||||
loadOrCreateConfig,
|
loadOrCreateConfig,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { beforeEach, expect, test, vi } from "vitest";
|
import { beforeEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
const baseAdmin: Admin = {
|
const baseAdmin: User = {
|
||||||
enablePaidFeatures: false,
|
enablePaidFeatures: false,
|
||||||
metricsConfig: {
|
metricsConfig: {
|
||||||
containers: {
|
containers: {
|
||||||
@@ -40,9 +40,7 @@ const baseAdmin: Admin = {
|
|||||||
cleanupCacheApplications: false,
|
cleanupCacheApplications: false,
|
||||||
cleanupCacheOnCompose: false,
|
cleanupCacheOnCompose: false,
|
||||||
cleanupCacheOnPreviews: false,
|
cleanupCacheOnPreviews: false,
|
||||||
createdAt: "",
|
createdAt: new Date(),
|
||||||
authId: "",
|
|
||||||
adminId: "string",
|
|
||||||
serverIp: null,
|
serverIp: null,
|
||||||
certificateType: "none",
|
certificateType: "none",
|
||||||
host: null,
|
host: null,
|
||||||
@@ -53,6 +51,31 @@ const baseAdmin: Admin = {
|
|||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
stripeCustomerId: "",
|
stripeCustomerId: "",
|
||||||
stripeSubscriptionId: "",
|
stripeSubscriptionId: "",
|
||||||
|
accessedProjects: [],
|
||||||
|
accessedServices: [],
|
||||||
|
banExpires: new Date(),
|
||||||
|
banned: true,
|
||||||
|
banReason: "",
|
||||||
|
canAccessToAPI: false,
|
||||||
|
canCreateProjects: false,
|
||||||
|
canDeleteProjects: false,
|
||||||
|
canDeleteServices: false,
|
||||||
|
canAccessToDocker: false,
|
||||||
|
canAccessToSSHKeys: false,
|
||||||
|
canCreateServices: false,
|
||||||
|
canAccessToTraefikFiles: false,
|
||||||
|
canAccessToGitProviders: false,
|
||||||
|
email: "",
|
||||||
|
expirationDate: "",
|
||||||
|
id: "",
|
||||||
|
isRegistered: false,
|
||||||
|
name: "",
|
||||||
|
createdAt2: new Date().toISOString(),
|
||||||
|
emailVerified: false,
|
||||||
|
image: "",
|
||||||
|
token: "",
|
||||||
|
updatedAt: new Date(),
|
||||||
|
twoFactorEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const baseApp: ApplicationNested = {
|
|||||||
previewWildcard: "",
|
previewWildcard: "",
|
||||||
project: {
|
project: {
|
||||||
env: "",
|
env: "",
|
||||||
adminId: "",
|
organizationId: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
|
|||||||
+1
-1
@@ -79,7 +79,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
|
|||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
error: queryError,
|
error: queryError,
|
||||||
} = api.admin.getContainerMetrics.useQuery(
|
} = api.user.getContainerMetrics.useQuery(
|
||||||
{
|
{
|
||||||
url: baseUrl,
|
url: baseUrl,
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const ShowPaidMonitoring = ({
|
|||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
error: queryError,
|
error: queryError,
|
||||||
} = api.admin.getServerMetrics.useQuery(
|
} = api.user.getServerMetrics.useQuery(
|
||||||
{
|
{
|
||||||
url: BASE_URL,
|
url: BASE_URL,
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { PenBoxIcon, Plus, SquarePen } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
organizationId?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
export function AddOrganization({ organizationId, children }: Props) {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { data: organization } = api.organization.one.useQuery(
|
||||||
|
{
|
||||||
|
organizationId: organizationId ?? "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!organizationId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { mutateAsync, isLoading } = organizationId
|
||||||
|
? api.organization.update.useMutation()
|
||||||
|
: api.organization.create.useMutation();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (organization) {
|
||||||
|
setName(organization.name);
|
||||||
|
}
|
||||||
|
}, [organization]);
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
await mutateAsync({ name, organizationId: organizationId ?? "" })
|
||||||
|
.then(() => {
|
||||||
|
setOpen(false);
|
||||||
|
toast.success(
|
||||||
|
`Organization ${organizationId ? "updated" : "created"} successfully`,
|
||||||
|
);
|
||||||
|
utils.organization.all.invalidate();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(
|
||||||
|
`Failed to ${organizationId ? "update" : "create"} organization`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{organizationId ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="group cursor-pointer hover:bg-blue-500/10"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="gap-2 p-2"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
|
||||||
|
<Plus className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div className="font-medium text-muted-foreground">
|
||||||
|
Add organization
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{organizationId ? "Update organization" : "Add organization"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{organizationId
|
||||||
|
? "Update the organization name"
|
||||||
|
: "Create a new organization to manage your projects."}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="name" className="text-right">
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" onClick={handleSubmit} isLoading={isLoading}>
|
||||||
|
{organizationId ? "Update organization" : "Create organization"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon, SquarePen } from "lucide-react";
|
import { PlusIcon, SquarePen } from "lucide-react";
|
||||||
@@ -97,6 +98,18 @@ export const HandleProject = ({ projectId }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
// useEffect(() => {
|
||||||
|
// const getUsers = async () => {
|
||||||
|
// const users = await authClient.admin.listUsers({
|
||||||
|
// query: {
|
||||||
|
// limit: 100,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// console.log(users);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// getUsers();
|
||||||
|
// });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
|||||||
@@ -51,15 +51,7 @@ import { ProjectEnvironment } from "./project-environment";
|
|||||||
export const ShowProjects = () => {
|
export const ShowProjects = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data, isLoading } = api.project.all.useQuery();
|
const { data, isLoading } = api.project.all.useQuery();
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
@@ -91,7 +83,7 @@ export const ShowProjects = () => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{(auth?.rol === "admin" || user?.canCreateProjects) && (
|
{(auth?.role === "owner" || auth?.user?.canCreateProjects) && (
|
||||||
<div className="">
|
<div className="">
|
||||||
<HandleProject />
|
<HandleProject />
|
||||||
</div>
|
</div>
|
||||||
@@ -293,8 +285,8 @@ export const ShowProjects = () => {
|
|||||||
<div
|
<div
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{(auth?.rol === "admin" ||
|
{(auth?.role === "owner" ||
|
||||||
user?.canDeleteProjects) && (
|
auth?.user?.canDeleteProjects) && (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger className="w-full">
|
<AlertDialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import {
|
import {
|
||||||
type Services,
|
type Services,
|
||||||
extractServices,
|
extractServices,
|
||||||
@@ -35,8 +36,10 @@ export const SearchCommand = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [search, setSearch] = React.useState("");
|
const [search, setSearch] = React.useState("");
|
||||||
|
const { data: session } = authClient.useSession();
|
||||||
const { data } = api.project.all.useQuery();
|
const { data } = api.project.all.useQuery(undefined, {
|
||||||
|
enabled: !!session,
|
||||||
|
});
|
||||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const calculatePrice = (count: number, isAnnual = false) => {
|
|||||||
};
|
};
|
||||||
export const ShowBilling = () => {
|
export const ShowBilling = () => {
|
||||||
const { data: servers } = api.server.all.useQuery(undefined);
|
const { data: servers } = api.server.all.useQuery(undefined);
|
||||||
const { data: admin } = api.admin.one.useQuery();
|
const { data: admin } = api.user.get.useQuery();
|
||||||
const { data, isLoading } = api.stripe.getProducts.useQuery();
|
const { data, isLoading } = api.stripe.getProducts.useQuery();
|
||||||
const { mutateAsync: createCheckoutSession } =
|
const { mutateAsync: createCheckoutSession } =
|
||||||
api.stripe.createCheckoutSession.useMutation();
|
api.stripe.createCheckoutSession.useMutation();
|
||||||
@@ -70,7 +70,7 @@ export const ShowBilling = () => {
|
|||||||
return isAnnual ? interval === "year" : interval === "month";
|
return isAnnual ? interval === "year" : interval === "month";
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxServers = admin?.serversQuantity ?? 1;
|
const maxServers = admin?.user.serversQuantity ?? 1;
|
||||||
const percentage = ((servers?.length ?? 0) / maxServers) * 100;
|
const percentage = ((servers?.length ?? 0) / maxServers) * 100;
|
||||||
const safePercentage = Math.min(percentage, 100);
|
const safePercentage = Math.min(percentage, 100);
|
||||||
|
|
||||||
@@ -98,17 +98,17 @@ export const ShowBilling = () => {
|
|||||||
<TabsTrigger value="annual">Annual</TabsTrigger>
|
<TabsTrigger value="annual">Annual</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{admin?.stripeSubscriptionId && (
|
{admin?.user.stripeSubscriptionId && (
|
||||||
<div className="space-y-2 flex flex-col">
|
<div className="space-y-2 flex flex-col">
|
||||||
<h3 className="text-lg font-medium">Servers Plan</h3>
|
<h3 className="text-lg font-medium">Servers Plan</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
You have {servers?.length} server on your plan of{" "}
|
You have {servers?.length} server on your plan of{" "}
|
||||||
{admin?.serversQuantity} servers
|
{admin?.user.serversQuantity} servers
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<Progress value={safePercentage} className="max-w-lg" />
|
<Progress value={safePercentage} className="max-w-lg" />
|
||||||
</div>
|
</div>
|
||||||
{admin && admin.serversQuantity! <= servers?.length! && (
|
{admin && admin.user.serversQuantity! <= servers?.length! && (
|
||||||
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
|
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
|
||||||
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||||
@@ -279,7 +279,7 @@ export const ShowBilling = () => {
|
|||||||
"flex flex-row items-center gap-2 mt-4",
|
"flex flex-row items-center gap-2 mt-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{admin?.stripeCustomerId && (
|
{admin?.user.stripeCustomerId && (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import type React from "react";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const ShowWelcomeDokploy = () => {
|
export const ShowWelcomeDokploy = () => {
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
if (!isCloud || data?.rol !== "admin") {
|
if (!isCloud || data?.role !== "admin") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,14 +24,14 @@ export const ShowWelcomeDokploy = () => {
|
|||||||
!isLoading &&
|
!isLoading &&
|
||||||
isCloud &&
|
isCloud &&
|
||||||
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
|
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
|
||||||
data?.rol === "admin"
|
data?.role === "owner"
|
||||||
) {
|
) {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}
|
}
|
||||||
}, [isCloud, isLoading]);
|
}, [isCloud, isLoading]);
|
||||||
|
|
||||||
const handleClose = (isOpen: boolean) => {
|
const handleClose = (isOpen: boolean) => {
|
||||||
if (data?.rol === "admin") {
|
if (data?.role === "owner") {
|
||||||
setOpen(isOpen);
|
setOpen(isOpen);
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
localStorage.setItem("hasSeenCloudWelcomeModal", "true"); // Establece el flag al cerrar el modal
|
localStorage.setItem("hasSeenCloudWelcomeModal", "true"); // Establece el flag al cerrar el modal
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export const AddCertificate = () => {
|
|||||||
privateKey: data.privateKey,
|
privateKey: data.privateKey,
|
||||||
autoRenew: data.autoRenew,
|
autoRenew: data.autoRenew,
|
||||||
serverId: data.serverId,
|
serverId: data.serverId,
|
||||||
|
organizationId: "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Certificate Created");
|
toast.success("Certificate Created");
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const AddBitbucketProvider = () => {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const url = useUrl();
|
const url = useUrl();
|
||||||
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
|
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const AddGithubProvider = () => {
|
export const AddGithubProvider = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||||
|
const { data } = api.user.get.useQuery();
|
||||||
const [manifest, setManifest] = useState("");
|
const [manifest, setManifest] = useState("");
|
||||||
const [isOrganization, setIsOrganization] = useState(false);
|
const [isOrganization, setIsOrganization] = useState(false);
|
||||||
const [organizationName, setOrganization] = useState("");
|
const [organizationName, setOrganization] = useState("");
|
||||||
@@ -25,7 +27,7 @@ export const AddGithubProvider = () => {
|
|||||||
const url = document.location.origin;
|
const url = document.location.origin;
|
||||||
const manifest = JSON.stringify(
|
const manifest = JSON.stringify(
|
||||||
{
|
{
|
||||||
redirect_url: `${origin}/api/providers/github/setup?authId=${data?.id}`,
|
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}`,
|
||||||
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
|
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
|
||||||
url: origin,
|
url: origin,
|
||||||
hook_attributes: {
|
hook_attributes: {
|
||||||
@@ -93,8 +95,8 @@ export const AddGithubProvider = () => {
|
|||||||
<form
|
<form
|
||||||
action={
|
action={
|
||||||
isOrganization
|
isOrganization
|
||||||
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${data?.id}`
|
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${activeOrganization?.id}`
|
||||||
: `https://github.com/settings/apps/new?state=gh_init:${data?.id}`
|
: `https://github.com/settings/apps/new?state=gh_init:${activeOrganization?.id}`
|
||||||
}
|
}
|
||||||
method="post"
|
method="post"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const AddGitlabProvider = () => {
|
|||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const url = useUrl();
|
const url = useUrl();
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { mutateAsync, error, isError } = api.gitlab.create.useMutation();
|
const { mutateAsync, error, isError } = api.gitlab.create.useMutation();
|
||||||
const webhookUrl = `${url}/api/providers/gitlab/callback`;
|
const webhookUrl = `${url}/api/providers/gitlab/callback`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,52 +1,134 @@
|
|||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const PasswordSchema = z.object({
|
||||||
|
password: z.string().min(8, {
|
||||||
|
message: "Password is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type PasswordForm = z.infer<typeof PasswordSchema>;
|
||||||
|
|
||||||
export const Disable2FA = () => {
|
export const Disable2FA = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, isLoading } = api.auth.disable2FA.useMutation();
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<PasswordForm>({
|
||||||
|
resolver: zodResolver(PasswordSchema),
|
||||||
|
defaultValues: {
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (formData: PasswordForm) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await authClient.twoFactor.disable({
|
||||||
|
password: formData.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
form.setError("password", {
|
||||||
|
message: result.error.message,
|
||||||
|
});
|
||||||
|
toast.error(result.error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("2FA disabled successfully");
|
||||||
|
utils.auth.get.invalidate();
|
||||||
|
setIsOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
form.setError("password", {
|
||||||
|
message: "Connection error. Please try again.",
|
||||||
|
});
|
||||||
|
toast.error("Connection error. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive" isLoading={isLoading}>
|
<Button variant="destructive">Disable 2FA</Button>
|
||||||
Disable 2FA
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the 2FA
|
This action cannot be undone. This will permanently disable
|
||||||
|
Two-Factor Authentication for your account.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<Form {...form}>
|
||||||
<AlertDialogAction
|
<form
|
||||||
onClick={async () => {
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
await mutateAsync()
|
className="space-y-4"
|
||||||
.then(() => {
|
|
||||||
utils.auth.get.invalidate();
|
|
||||||
toast.success("2FA Disabled");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error disabling 2FA");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Confirm
|
<FormField
|
||||||
</AlertDialogAction>
|
control={form.control}
|
||||||
</AlertDialogFooter>
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Enter your password to disable 2FA
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
form.reset();
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" variant="destructive" isLoading={isLoading}>
|
||||||
|
Disable 2FA
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,144 +17,315 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
InputOTP,
|
InputOTP,
|
||||||
InputOTPGroup,
|
InputOTPGroup,
|
||||||
InputOTPSlot,
|
InputOTPSlot,
|
||||||
} from "@/components/ui/input-otp";
|
} from "@/components/ui/input-otp";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, Fingerprint } from "lucide-react";
|
import { Fingerprint, QrCode } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import QRCode from "qrcode";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const Enable2FASchema = z.object({
|
const PasswordSchema = z.object({
|
||||||
|
password: z.string().min(8, {
|
||||||
|
message: "Password is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const PinSchema = z.object({
|
||||||
pin: z.string().min(6, {
|
pin: z.string().min(6, {
|
||||||
message: "Pin is required",
|
message: "Pin is required",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Enable2FA = z.infer<typeof Enable2FASchema>;
|
type PasswordForm = z.infer<typeof PasswordSchema>;
|
||||||
|
type PinForm = z.infer<typeof PinSchema>;
|
||||||
|
|
||||||
|
type TwoFactorEnableResponse = {
|
||||||
|
totpURI: string;
|
||||||
|
backupCodes: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type TwoFactorSetupData = {
|
||||||
|
qrCodeUrl: string;
|
||||||
|
secret: string;
|
||||||
|
totpURI: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const Enable2FA = () => {
|
export const Enable2FA = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { data: session } = authClient.useSession();
|
||||||
|
const [data, setData] = useState<TwoFactorSetupData | null>(null);
|
||||||
|
const [backupCodes, setBackupCodes] = useState<string[]>([]);
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const [step, setStep] = useState<"password" | "verify">("password");
|
||||||
|
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
||||||
|
|
||||||
const { data } = api.auth.generate2FASecret.useQuery(undefined, {
|
const handlePasswordSubmit = async (formData: PasswordForm) => {
|
||||||
refetchOnWindowFocus: false,
|
setIsPasswordLoading(true);
|
||||||
|
try {
|
||||||
|
const { data: enableData } = await authClient.twoFactor.enable({
|
||||||
|
password: formData.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!enableData) {
|
||||||
|
throw new Error("No data received from server");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableData.backupCodes) {
|
||||||
|
setBackupCodes(enableData.backupCodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableData.totpURI) {
|
||||||
|
const qrCodeUrl = await QRCode.toDataURL(enableData.totpURI);
|
||||||
|
|
||||||
|
setData({
|
||||||
|
qrCodeUrl,
|
||||||
|
secret: enableData.totpURI.split("secret=")[1]?.split("&")[0] || "",
|
||||||
|
totpURI: enableData.totpURI,
|
||||||
|
});
|
||||||
|
|
||||||
|
setStep("verify");
|
||||||
|
toast.success("Scan the QR code with your authenticator app");
|
||||||
|
} else {
|
||||||
|
throw new Error("No TOTP URI received from server");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Error setting up 2FA",
|
||||||
|
);
|
||||||
|
passwordForm.setError("password", {
|
||||||
|
message: "Error verifying password",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsPasswordLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifySubmit = async (formData: PinForm) => {
|
||||||
|
try {
|
||||||
|
const result = await authClient.twoFactor.verifyTotp({
|
||||||
|
code: formData.pin,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
|
||||||
|
pinForm.setError("pin", {
|
||||||
|
message: "Invalid code. Please try again.",
|
||||||
|
});
|
||||||
|
toast.error("Invalid verification code");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.data) {
|
||||||
|
throw new Error("No response received from server");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("2FA configured successfully");
|
||||||
|
utils.auth.get.invalidate();
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const errorMessage =
|
||||||
|
error.message === "Failed to fetch"
|
||||||
|
? "Connection error. Please check your internet connection."
|
||||||
|
: error.message;
|
||||||
|
|
||||||
|
pinForm.setError("pin", {
|
||||||
|
message: errorMessage,
|
||||||
|
});
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} else {
|
||||||
|
pinForm.setError("pin", {
|
||||||
|
message: "Error verifying code",
|
||||||
|
});
|
||||||
|
toast.error("Error verifying 2FA code");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const passwordForm = useForm<PasswordForm>({
|
||||||
|
resolver: zodResolver(PasswordSchema),
|
||||||
|
defaultValues: {
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const pinForm = useForm<PinForm>({
|
||||||
api.auth.verify2FASetup.useMutation();
|
resolver: zodResolver(PinSchema),
|
||||||
|
|
||||||
const form = useForm<Enable2FA>({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
pin: "",
|
pin: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(Enable2FASchema),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
if (!isDialogOpen) {
|
||||||
pin: "",
|
setStep("password");
|
||||||
});
|
setData(null);
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
setBackupCodes([]);
|
||||||
|
passwordForm.reset();
|
||||||
|
pinForm.reset();
|
||||||
|
}
|
||||||
|
}, [isDialogOpen, passwordForm, pinForm]);
|
||||||
|
|
||||||
const onSubmit = async (formData: Enable2FA) => {
|
|
||||||
await mutateAsync({
|
|
||||||
pin: formData.pin,
|
|
||||||
secret: data?.secret || "",
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("2FA Verified");
|
|
||||||
utils.auth.get.invalidate();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error verifying the 2FA");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<Fingerprint className="size-4 text-muted-foreground" />
|
<Fingerprint className="size-4 text-muted-foreground" />
|
||||||
Enable 2FA
|
Enable 2FA
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen max-sm:overflow-y-auto sm:max-w-xl ">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>2FA Setup</DialogTitle>
|
<DialogTitle>2FA Setup</DialogTitle>
|
||||||
<DialogDescription>Add a 2FA to your account</DialogDescription>
|
<DialogDescription>
|
||||||
|
{step === "password"
|
||||||
|
? "Enter your password to begin 2FA setup"
|
||||||
|
: "Scan the QR code and verify with your authenticator app"}
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
|
||||||
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form-add-2FA"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid sm:grid-cols-2 w-full gap-4"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4 justify-center items-center">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{data?.qrCodeUrl ? "Scan the QR code to add 2FA" : ""}
|
|
||||||
</span>
|
|
||||||
<img
|
|
||||||
src={data?.qrCodeUrl}
|
|
||||||
alt="qrCode"
|
|
||||||
className="rounded-lg w-fit"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<span className="text-sm text-muted-foreground text-center">
|
|
||||||
{data?.secret ? `Secret: ${data?.secret}` : ""}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField
|
{step === "password" ? (
|
||||||
control={form.control}
|
<Form {...passwordForm}>
|
||||||
name="pin"
|
<form
|
||||||
render={({ field }) => (
|
id="password-form"
|
||||||
<FormItem className="flex flex-col justify-center max-sm:items-center">
|
onSubmit={passwordForm.handleSubmit(handlePasswordSubmit)}
|
||||||
<FormLabel>Pin</FormLabel>
|
className="space-y-4"
|
||||||
<FormControl>
|
|
||||||
<InputOTP maxLength={6} {...field}>
|
|
||||||
<InputOTPGroup>
|
|
||||||
<InputOTPSlot index={0} />
|
|
||||||
<InputOTPSlot index={1} />
|
|
||||||
<InputOTPSlot index={2} />
|
|
||||||
<InputOTPSlot index={3} />
|
|
||||||
<InputOTPSlot index={4} />
|
|
||||||
<InputOTPSlot index={5} />
|
|
||||||
</InputOTPGroup>
|
|
||||||
</InputOTP>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription className="max-md:text-center">
|
|
||||||
Please enter the 6 digits code provided by your
|
|
||||||
authenticator app.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
form="hook-form-add-2FA"
|
|
||||||
type="submit"
|
|
||||||
>
|
>
|
||||||
Submit 2FA
|
<FormField
|
||||||
</Button>
|
control={passwordForm.control}
|
||||||
</DialogFooter>
|
name="password"
|
||||||
</Form>
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Enter your password to enable 2FA
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full"
|
||||||
|
isLoading={isPasswordLoading}
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
) : (
|
||||||
|
<Form {...pinForm}>
|
||||||
|
<form
|
||||||
|
id="pin-form"
|
||||||
|
onSubmit={pinForm.handleSubmit(handleVerifySubmit)}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-6 justify-center items-center">
|
||||||
|
{data?.qrCodeUrl ? (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center gap-4 p-6 border rounded-lg">
|
||||||
|
<QrCode className="size-5 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
Scan this QR code with your authenticator app
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
src={data.qrCodeUrl}
|
||||||
|
alt="2FA QR Code"
|
||||||
|
className="rounded-lg w-48 h-48"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-2 text-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Can't scan the QR code?
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-mono bg-muted p-2 rounded">
|
||||||
|
{data.secret}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{backupCodes && backupCodes.length > 0 && (
|
||||||
|
<div className="w-full space-y-3 border rounded-lg p-4">
|
||||||
|
<h4 className="font-medium">Backup Codes</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{backupCodes.map((code, index) => (
|
||||||
|
<code
|
||||||
|
key={index}
|
||||||
|
className="bg-muted p-2 rounded text-sm font-mono"
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</code>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Save these backup codes in a secure place. You can use
|
||||||
|
them to access your account if you lose access to your
|
||||||
|
authenticator device.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center w-full h-48 bg-muted rounded-lg">
|
||||||
|
<QrCode className="size-8 text-muted-foreground animate-pulse" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={pinForm.control}
|
||||||
|
name="pin"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col justify-center items-center">
|
||||||
|
<FormLabel>Verification Code</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<InputOTP maxLength={6} {...field}>
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot index={0} />
|
||||||
|
<InputOTPSlot index={1} />
|
||||||
|
<InputOTPSlot index={2} />
|
||||||
|
<InputOTPSlot index={3} />
|
||||||
|
<InputOTPSlot index={4} />
|
||||||
|
<InputOTPSlot index={5} />
|
||||||
|
</InputOTPGroup>
|
||||||
|
</InputOTP>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Enter the 6-digit code from your authenticator app
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full"
|
||||||
|
isLoading={isPasswordLoading}
|
||||||
|
>
|
||||||
|
Enable 2FA
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import Link from "next/link";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export const GenerateToken = () => {
|
export const GenerateToken = () => {
|
||||||
const { data, refetch } = api.auth.get.useQuery();
|
const { data, refetch } = api.user.get.useQuery();
|
||||||
|
|
||||||
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
|
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
|
||||||
api.auth.generateToken.useMutation();
|
api.auth.generateToken.useMutation();
|
||||||
@@ -51,7 +51,7 @@ export const GenerateToken = () => {
|
|||||||
<Label>Token</Label>
|
<Label>Token</Label>
|
||||||
<ToggleVisibilityInput
|
<ToggleVisibilityInput
|
||||||
placeholder="Token"
|
placeholder="Token"
|
||||||
value={data?.token || ""}
|
value={data?.user?.token || ""}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { generateSHA256Hash } from "@/lib/utils";
|
import { generateSHA256Hash } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -54,7 +56,10 @@ const randomImages = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const ProfileForm = () => {
|
export const ProfileForm = () => {
|
||||||
const { data, refetch, isLoading } = api.auth.get.useQuery();
|
const utils = api.useUtils();
|
||||||
|
const { mutateAsync: disable2FA, isLoading: isDisabling } =
|
||||||
|
api.auth.disable2FA.useMutation();
|
||||||
|
const { data, refetch, isLoading } = api.user.get.useQuery();
|
||||||
const {
|
const {
|
||||||
mutateAsync,
|
mutateAsync,
|
||||||
isLoading: isUpdating,
|
isLoading: isUpdating,
|
||||||
@@ -73,9 +78,9 @@ export const ProfileForm = () => {
|
|||||||
|
|
||||||
const form = useForm<Profile>({
|
const form = useForm<Profile>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: data?.email || "",
|
email: data?.user?.email || "",
|
||||||
password: "",
|
password: "",
|
||||||
image: data?.image || "",
|
image: data?.user?.image || "",
|
||||||
currentPassword: "",
|
currentPassword: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(profileSchema),
|
resolver: zodResolver(profileSchema),
|
||||||
@@ -84,14 +89,14 @@ export const ProfileForm = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
email: data?.email || "",
|
email: data?.user?.email || "",
|
||||||
password: "",
|
password: "",
|
||||||
image: data?.image || "",
|
image: data?.user?.image || "",
|
||||||
currentPassword: "",
|
currentPassword: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.email) {
|
if (data.user.email) {
|
||||||
generateSHA256Hash(data.email).then((hash) => {
|
generateSHA256Hash(data.user.email).then((hash) => {
|
||||||
setGravatarHash(hash);
|
setGravatarHash(hash);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -130,7 +135,7 @@ export const ProfileForm = () => {
|
|||||||
{t("settings.profile.description")}
|
{t("settings.profile.description")}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
|
{!data?.user.twoFactorEnabled ? <Enable2FA /> : <Disable2FA />}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const profileSchema = z.object({
|
|||||||
type Profile = z.infer<typeof profileSchema>;
|
type Profile = z.infer<typeof profileSchema>;
|
||||||
|
|
||||||
export const RemoveSelfAccount = () => {
|
export const RemoveSelfAccount = () => {
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.auth.removeSelfAccount.useMutation();
|
api.auth.removeSelfAccount.useMutation();
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@ interface Props {
|
|||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||||
const { data, refetch } = api.admin.one.useQuery(undefined, {
|
const { data, refetch } = api.user.get.useQuery(undefined, {
|
||||||
enabled: !serverId,
|
enabled: !serverId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
|
const enabled = data?.user.enableDockerCleanup || server?.enableDockerCleanup;
|
||||||
|
|
||||||
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
|||||||
enabled: !!serverId,
|
enabled: !!serverId,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: api.admin.one.useQuery();
|
: api.user.get.useQuery();
|
||||||
|
|
||||||
const url = useUrl();
|
const url = useUrl();
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const CreateSSHKey = () => {
|
|||||||
description: "Used on Dokploy Cloud",
|
description: "Used on Dokploy Cloud",
|
||||||
privateKey: keys.privateKey,
|
privateKey: keys.privateKey,
|
||||||
publicKey: keys.publicKey,
|
publicKey: keys.publicKey,
|
||||||
|
organizationId: "",
|
||||||
});
|
});
|
||||||
await refetch();
|
await refetch();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
|||||||
const onSubmit = async (data: SSHKey) => {
|
const onSubmit = async (data: SSHKey) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
...data,
|
...data,
|
||||||
|
organizationId: "",
|
||||||
sshKeyId: sshKeyId || "",
|
sshKeyId: sshKeyId || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
|||||||
+69
-24
@@ -19,6 +19,14 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
@@ -27,62 +35,70 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const addUser = z.object({
|
const addInvitation = z.object({
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "Email is required")
|
.min(1, "Email is required")
|
||||||
.email({ message: "Invalid email" }),
|
.email({ message: "Invalid email" }),
|
||||||
|
role: z.enum(["member", "admin"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddUser = z.infer<typeof addUser>;
|
type AddInvitation = z.infer<typeof addInvitation>;
|
||||||
|
|
||||||
export const AddUser = () => {
|
export const AddInvitation = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
const form = useForm<AddInvitation>({
|
||||||
api.admin.createUserInvitation.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<AddUser>({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "",
|
||||||
|
role: "member",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addUser),
|
resolver: zodResolver(addInvitation),
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset();
|
form.reset();
|
||||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddUser) => {
|
const onSubmit = async (data: AddInvitation) => {
|
||||||
await mutateAsync({
|
setIsLoading(true);
|
||||||
|
const result = await authClient.organization.inviteMember({
|
||||||
email: data.email.toLowerCase(),
|
email: data.email.toLowerCase(),
|
||||||
})
|
role: data.role,
|
||||||
.then(async () => {
|
organizationId: activeOrganization?.id,
|
||||||
toast.success("Invitation created");
|
});
|
||||||
await utils.user.all.invalidate();
|
|
||||||
setOpen(false);
|
if (result.error) {
|
||||||
})
|
setError(result.error.message || "");
|
||||||
.catch(() => {
|
} else {
|
||||||
toast.error("Error creating the invitation");
|
toast.success("Invitation created");
|
||||||
});
|
setError(null);
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.organization.allInvitations.invalidate();
|
||||||
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<Button>
|
<Button>
|
||||||
<PlusIcon className="h-4 w-4" /> Add User
|
<PlusIcon className="h-4 w-4" /> Add Invitation
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add User</DialogTitle>
|
<DialogTitle>Add Invitation</DialogTitle>
|
||||||
<DialogDescription>Invite a new user</DialogDescription>
|
<DialogDescription>Invite a new user</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{error && <AlertBlock type="error">{error}</AlertBlock>}
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form-add-user"
|
id="hook-form-add-invitation"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 "
|
className="grid w-full gap-4 "
|
||||||
>
|
>
|
||||||
@@ -104,10 +120,39 @@ export const AddUser = () => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="role"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Role</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a role" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="member">Member</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
Select the role for the new user
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<DialogFooter className="flex w-full flex-row">
|
<DialogFooter className="flex w-full flex-row">
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
form="hook-form-add-user"
|
form="hook-form-add-invitation"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
@@ -52,7 +52,7 @@ interface Props {
|
|||||||
export const AddUserPermissions = ({ userId }: Props) => {
|
export const AddUserPermissions = ({ userId }: Props) => {
|
||||||
const { data: projects } = api.project.all.useQuery();
|
const { data: projects } = api.project.all.useQuery();
|
||||||
|
|
||||||
const { data, refetch } = api.user.byUserId.useQuery(
|
const { data, refetch } = api.auth.one.useQuery(
|
||||||
{
|
{
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
@@ -62,7 +62,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
api.admin.assignPermissions.useMutation();
|
api.user.assignPermissions.useMutation();
|
||||||
|
|
||||||
const form = useForm<AddPermissions>({
|
const form = useForm<AddPermissions>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -92,7 +92,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
|
|
||||||
const onSubmit = async (data: AddPermissions) => {
|
const onSubmit = async (data: AddPermissions) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
userId,
|
id: userId,
|
||||||
canCreateServices: data.canCreateServices,
|
canCreateServices: data.canCreateServices,
|
||||||
canCreateProjects: data.canCreateProjects,
|
canCreateProjects: data.canCreateProjects,
|
||||||
canDeleteServices: data.canDeleteServices,
|
canDeleteServices: data.canDeleteServices,
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { format, isPast } from "date-fns";
|
||||||
|
import { Mail, MoreHorizontal, Users } from "lucide-react";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { AddInvitation } from "./add-invitation";
|
||||||
|
|
||||||
|
export const ShowInvitations = () => {
|
||||||
|
const { data, isLoading, refetch } =
|
||||||
|
api.organization.allInvitations.useQuery();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||||
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
|
<CardHeader className="">
|
||||||
|
<CardTitle className="text-xl flex flex-row gap-2">
|
||||||
|
<Mail className="size-6 text-muted-foreground self-center" />
|
||||||
|
Invitations
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Create invitations to your organization.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
|
||||||
|
<span>Loading...</span>
|
||||||
|
<Loader2 className="animate-spin size-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{data?.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
|
<Users className="size-8 self-center text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
Invite users to your organization
|
||||||
|
</span>
|
||||||
|
<AddInvitation />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
|
<Table>
|
||||||
|
<TableCaption>See all invitations</TableCaption>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[100px]">Email</TableHead>
|
||||||
|
<TableHead className="text-center">Role</TableHead>
|
||||||
|
<TableHead className="text-center">Status</TableHead>
|
||||||
|
<TableHead className="text-center">
|
||||||
|
Expires At
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data?.map((invitation) => {
|
||||||
|
const isExpired = isPast(
|
||||||
|
new Date(invitation.expiresAt),
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<TableRow key={invitation.id}>
|
||||||
|
<TableCell className="w-[100px]">
|
||||||
|
{invitation.email}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
invitation.role === "owner"
|
||||||
|
? "default"
|
||||||
|
: "secondary"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{invitation.role}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
invitation.status === "pending"
|
||||||
|
? "secondary"
|
||||||
|
: invitation.status === "canceled"
|
||||||
|
? "destructive"
|
||||||
|
: "default"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{invitation.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{format(new Date(invitation.expiresAt), "PPpp")}{" "}
|
||||||
|
{isExpired ? (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
(Expired)
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="text-right flex justify-end">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
Actions
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{!isExpired && (
|
||||||
|
<>
|
||||||
|
{invitation.status === "pending" && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onSelect={(e) => {
|
||||||
|
copy(
|
||||||
|
`${origin}/invitation?token=${invitation.id}`,
|
||||||
|
);
|
||||||
|
toast.success(
|
||||||
|
"Invitation Copied to clipboard",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy Invitation
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{invitation.status === "pending" && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onSelect={async (e) => {
|
||||||
|
const result =
|
||||||
|
await authClient.organization.cancelInvitation(
|
||||||
|
{
|
||||||
|
invitationId: invitation.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
toast.error(
|
||||||
|
result.error.message,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.success(
|
||||||
|
"Invitation deleted",
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel Invitation
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
|
<AddInvitation />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -23,22 +24,19 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { MoreHorizontal, Users } from "lucide-react";
|
import { MoreHorizontal, Users } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AddUserPermissions } from "./add-permissions";
|
import { AddUserPermissions } from "./add-permissions";
|
||||||
import { AddUser } from "./add-user";
|
|
||||||
|
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
|
|
||||||
export const ShowUsers = () => {
|
export const ShowUsers = () => {
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isLoading, refetch } = api.user.all.useQuery();
|
const { data, isLoading, refetch } = api.user.all.useQuery();
|
||||||
const { mutateAsync, isLoading: isRemoving } =
|
const { mutateAsync, isLoading: isRemoving } = api.user.remove.useMutation();
|
||||||
api.admin.removeUser.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -67,7 +65,6 @@ export const ShowUsers = () => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
Invite users to your Dokploy account
|
Invite users to your Dokploy account
|
||||||
</span>
|
</span>
|
||||||
<AddUser />
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -76,43 +73,41 @@ export const ShowUsers = () => {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[100px]">Email</TableHead>
|
<TableHead className="w-[100px]">Email</TableHead>
|
||||||
<TableHead className="text-center">Status</TableHead>
|
<TableHead className="text-center">Role</TableHead>
|
||||||
<TableHead className="text-center">2FA</TableHead>
|
<TableHead className="text-center">2FA</TableHead>
|
||||||
|
|
||||||
<TableHead className="text-center">
|
<TableHead className="text-center">
|
||||||
Expiration
|
Created At
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data?.map((user) => {
|
{data?.map((member) => {
|
||||||
return (
|
return (
|
||||||
<TableRow key={user.userId}>
|
<TableRow key={member.id}>
|
||||||
<TableCell className="w-[100px]">
|
<TableCell className="w-[100px]">
|
||||||
{user.auth.email}
|
{member.user.email}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge
|
<Badge
|
||||||
variant={
|
variant={
|
||||||
user.isRegistered ? "default" : "secondary"
|
member.role === "owner"
|
||||||
|
? "default"
|
||||||
|
: "secondary"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{user.isRegistered
|
{member.role}
|
||||||
? "Registered"
|
|
||||||
: "Not Registered"}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{user.auth.is2FAEnabled
|
{member.user.twoFactorEnabled
|
||||||
? "2FA Enabled"
|
? "Enabled"
|
||||||
: "2FA Not Enabled"}
|
: "Disabled"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-center">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{format(
|
{format(new Date(member.createdAt), "PPpp")}
|
||||||
new Date(user.expirationDate),
|
|
||||||
"PPpp",
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
@@ -131,56 +126,63 @@ export const ShowUsers = () => {
|
|||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
Actions
|
Actions
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{!user.isRegistered && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
onSelect={(e) => {
|
|
||||||
copy(
|
|
||||||
`${origin}/invitation?token=${user.token}`,
|
|
||||||
);
|
|
||||||
toast.success(
|
|
||||||
"Invitation Copied to clipboard",
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Copy Invitation
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{user.isRegistered && (
|
{member.role !== "owner" && (
|
||||||
<AddUserPermissions
|
<AddUserPermissions
|
||||||
userId={user.userId}
|
userId={member.user.id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogAction
|
{member.role !== "owner" && (
|
||||||
title="Delete User"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this user?"
|
title="Delete User"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this user?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
authId: user.authId,
|
if (isCloud) {
|
||||||
})
|
const { error } =
|
||||||
.then(() => {
|
await authClient.organization.removeMember(
|
||||||
toast.success(
|
{
|
||||||
"User deleted successfully",
|
memberIdOrEmail: member.id,
|
||||||
);
|
},
|
||||||
refetch();
|
);
|
||||||
})
|
|
||||||
.catch(() => {
|
if (!error) {
|
||||||
toast.error(
|
toast.success(
|
||||||
"Error deleting destination",
|
"User deleted successfully",
|
||||||
);
|
);
|
||||||
});
|
refetch();
|
||||||
}}
|
} else {
|
||||||
>
|
toast.error(
|
||||||
<DropdownMenuItem
|
"Error deleting user",
|
||||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
);
|
||||||
onSelect={(e) => e.preventDefault()}
|
}
|
||||||
|
} else {
|
||||||
|
await mutateAsync({
|
||||||
|
userId: member.user.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
"User deleted successfully",
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
"Error deleting destination",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Delete User
|
<DropdownMenuItem
|
||||||
</DropdownMenuItem>
|
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||||
</DialogAction>
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Delete User
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -189,10 +191,6 @@ export const ShowUsers = () => {
|
|||||||
})}
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
|
||||||
<AddUser />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
|
|||||||
|
|
||||||
export const WebDomain = () => {
|
export const WebDomain = () => {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
const { data: user, refetch } = api.admin.one.useQuery();
|
const { data, refetch } = api.user.get.useQuery();
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.settings.assignDomainServer.useMutation();
|
api.settings.assignDomainServer.useMutation();
|
||||||
|
|
||||||
@@ -65,14 +65,14 @@ export const WebDomain = () => {
|
|||||||
resolver: zodResolver(addServerDomain),
|
resolver: zodResolver(addServerDomain),
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
domain: user?.host || "",
|
domain: data?.user?.host || "",
|
||||||
certificateType: user?.certificateType,
|
certificateType: data?.user?.certificateType,
|
||||||
letsEncryptEmail: user?.letsEncryptEmail || "",
|
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, user]);
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddServerDomain) => {
|
const onSubmit = async (data: AddServerDomain) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
export const WebServer = ({ className }: Props) => {
|
export const WebServer = ({ className }: Props) => {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
const { data } = api.admin.one.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
|
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ export const WebServer = ({ className }: Props) => {
|
|||||||
|
|
||||||
<div className="flex items-center flex-wrap justify-between gap-4">
|
<div className="flex items-center flex-wrap justify-between gap-4">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Server IP: {data?.serverIp}
|
Server IP: {data?.user.serverIp}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Version: {dokployVersion}
|
Version: {dokployVersion}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -47,15 +46,15 @@ interface Props {
|
|||||||
export const UpdateServerIp = ({ children, serverId }: Props) => {
|
export const UpdateServerIp = ({ children, serverId }: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const { data } = api.admin.one.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
const { data: ip } = api.server.publicIp.useQuery();
|
const { data: ip } = api.server.publicIp.useQuery();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.admin.update.useMutation();
|
api.user.update.useMutation();
|
||||||
|
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
serverIp: data?.serverIp || "",
|
serverIp: data?.user.serverIp || "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
@@ -63,7 +62,7 @@ export const UpdateServerIp = ({ children, serverId }: Props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
serverIp: data.serverIp || "",
|
serverIp: data.user.serverIp || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
|
AudioWaveform,
|
||||||
BarChartHorizontalBigIcon,
|
BarChartHorizontalBigIcon,
|
||||||
Bell,
|
Bell,
|
||||||
BlocksIcon,
|
BlocksIcon,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
Boxes,
|
Boxes,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
|
Command,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Database,
|
Database,
|
||||||
Folder,
|
Folder,
|
||||||
@@ -16,11 +18,13 @@ import {
|
|||||||
GitBranch,
|
GitBranch,
|
||||||
HeartIcon,
|
HeartIcon,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
|
Loader2,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
Package,
|
Package,
|
||||||
PieChart,
|
PieChart,
|
||||||
Server,
|
Server,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
Trash2,
|
||||||
User,
|
User,
|
||||||
Users,
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -74,7 +78,6 @@ import { UserNav } from "./user-nav";
|
|||||||
|
|
||||||
// The types of the queries we are going to use
|
// The types of the queries we are going to use
|
||||||
type AuthQueryOutput = inferRouterOutputs<AppRouter>["auth"]["get"];
|
type AuthQueryOutput = inferRouterOutputs<AppRouter>["auth"]["get"];
|
||||||
type UserQueryOutput = inferRouterOutputs<AppRouter>["user"]["byAuthId"];
|
|
||||||
|
|
||||||
type SingleNavItem = {
|
type SingleNavItem = {
|
||||||
isSingle?: true;
|
isSingle?: true;
|
||||||
@@ -83,7 +86,6 @@ type SingleNavItem = {
|
|||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
isEnabled?: (opts: {
|
isEnabled?: (opts: {
|
||||||
auth?: AuthQueryOutput;
|
auth?: AuthQueryOutput;
|
||||||
user?: UserQueryOutput;
|
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
}) => boolean;
|
}) => boolean;
|
||||||
};
|
};
|
||||||
@@ -101,7 +103,6 @@ type NavItem =
|
|||||||
items: SingleNavItem[];
|
items: SingleNavItem[];
|
||||||
isEnabled?: (opts: {
|
isEnabled?: (opts: {
|
||||||
auth?: AuthQueryOutput;
|
auth?: AuthQueryOutput;
|
||||||
user?: UserQueryOutput;
|
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
}) => boolean;
|
}) => boolean;
|
||||||
};
|
};
|
||||||
@@ -114,7 +115,6 @@ type ExternalLink = {
|
|||||||
icon: React.ComponentType<{ className?: string }>;
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
isEnabled?: (opts: {
|
isEnabled?: (opts: {
|
||||||
auth?: AuthQueryOutput;
|
auth?: AuthQueryOutput;
|
||||||
user?: UserQueryOutput;
|
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
}) => boolean;
|
}) => boolean;
|
||||||
};
|
};
|
||||||
@@ -145,7 +145,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/monitoring",
|
url: "/dashboard/monitoring",
|
||||||
icon: BarChartHorizontalBigIcon,
|
icon: BarChartHorizontalBigIcon,
|
||||||
// Only enabled in non-cloud environments
|
// Only enabled in non-cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) => !isCloud,
|
isEnabled: ({ auth, isCloud }) => !isCloud,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -153,9 +153,9 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/traefik",
|
url: "/dashboard/traefik",
|
||||||
icon: GalleryVerticalEnd,
|
icon: GalleryVerticalEnd,
|
||||||
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!(
|
!!(
|
||||||
(auth?.rol === "admin" || user?.canAccessToTraefikFiles) &&
|
(auth?.role === "owner" || auth?.user?.canAccessToTraefikFiles) &&
|
||||||
!isCloud
|
!isCloud
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -165,8 +165,11 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/docker",
|
url: "/dashboard/docker",
|
||||||
icon: BlocksIcon,
|
icon: BlocksIcon,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
!!(
|
||||||
|
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -174,8 +177,11 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/swarm",
|
url: "/dashboard/swarm",
|
||||||
icon: PieChart,
|
icon: PieChart,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
!!(
|
||||||
|
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -183,8 +189,11 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/requests",
|
url: "/dashboard/requests",
|
||||||
icon: Forward,
|
icon: Forward,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
!!(
|
||||||
|
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Legacy unused menu, adjusted to the new structure
|
// Legacy unused menu, adjusted to the new structure
|
||||||
@@ -251,8 +260,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/server",
|
url: "/dashboard/settings/server",
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for admins in non-cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||||
!!(auth?.rol === "admin" && !isCloud),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -266,7 +274,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/servers",
|
url: "/dashboard/settings/servers",
|
||||||
icon: Server,
|
icon: Server,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -274,7 +282,7 @@ const MENU: Menu = {
|
|||||||
icon: Users,
|
icon: Users,
|
||||||
url: "/dashboard/settings/users",
|
url: "/dashboard/settings/users",
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -282,8 +290,8 @@ const MENU: Menu = {
|
|||||||
icon: KeyRound,
|
icon: KeyRound,
|
||||||
url: "/dashboard/settings/ssh-keys",
|
url: "/dashboard/settings/ssh-keys",
|
||||||
// Only enabled for admins and users with access to SSH keys
|
// Only enabled for admins and users with access to SSH keys
|
||||||
isEnabled: ({ auth, user }) =>
|
isEnabled: ({ auth }) =>
|
||||||
!!(auth?.rol === "admin" || user?.canAccessToSSHKeys),
|
!!(auth?.role === "owner" || auth?.user?.canAccessToSSHKeys),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -291,8 +299,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/git-providers",
|
url: "/dashboard/settings/git-providers",
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
// Only enabled for admins and users with access to Git providers
|
// Only enabled for admins and users with access to Git providers
|
||||||
isEnabled: ({ auth, user }) =>
|
isEnabled: ({ auth }) =>
|
||||||
!!(auth?.rol === "admin" || user?.canAccessToGitProviders),
|
!!(auth?.role === "owner" || auth?.user?.canAccessToGitProviders),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -300,7 +308,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/registry",
|
url: "/dashboard/settings/registry",
|
||||||
icon: Package,
|
icon: Package,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -308,7 +316,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/destinations",
|
url: "/dashboard/settings/destinations",
|
||||||
icon: Database,
|
icon: Database,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -317,7 +325,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/certificates",
|
url: "/dashboard/settings/certificates",
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -325,8 +333,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/cluster",
|
url: "/dashboard/settings/cluster",
|
||||||
icon: Boxes,
|
icon: Boxes,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for admins in non-cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||||
!!(auth?.rol === "admin" && !isCloud),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -334,7 +341,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/notifications",
|
url: "/dashboard/settings/notifications",
|
||||||
icon: Bell,
|
icon: Bell,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -342,8 +349,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/billing",
|
url: "/dashboard/settings/billing",
|
||||||
icon: CreditCard,
|
icon: CreditCard,
|
||||||
// Only enabled for admins in cloud environments
|
// Only enabled for admins in cloud environments
|
||||||
isEnabled: ({ auth, user, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
||||||
!!(auth?.rol === "admin" && isCloud),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -379,7 +385,6 @@ const MENU: Menu = {
|
|||||||
*/
|
*/
|
||||||
function createMenuForAuthUser(opts: {
|
function createMenuForAuthUser(opts: {
|
||||||
auth?: AuthQueryOutput;
|
auth?: AuthQueryOutput;
|
||||||
user?: UserQueryOutput;
|
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
}): Menu {
|
}): Menu {
|
||||||
return {
|
return {
|
||||||
@@ -390,7 +395,6 @@ function createMenuForAuthUser(opts: {
|
|||||||
? true
|
? true
|
||||||
: item.isEnabled({
|
: item.isEnabled({
|
||||||
auth: opts.auth,
|
auth: opts.auth,
|
||||||
user: opts.user,
|
|
||||||
isCloud: opts.isCloud,
|
isCloud: opts.isCloud,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -401,7 +405,6 @@ function createMenuForAuthUser(opts: {
|
|||||||
? true
|
? true
|
||||||
: item.isEnabled({
|
: item.isEnabled({
|
||||||
auth: opts.auth,
|
auth: opts.auth,
|
||||||
user: opts.user,
|
|
||||||
isCloud: opts.isCloud,
|
isCloud: opts.isCloud,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -412,7 +415,6 @@ function createMenuForAuthUser(opts: {
|
|||||||
? true
|
? true
|
||||||
: item.isEnabled({
|
: item.isEnabled({
|
||||||
auth: opts.auth,
|
auth: opts.auth,
|
||||||
user: opts.user,
|
|
||||||
isCloud: opts.isCloud,
|
isCloud: opts.isCloud,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -480,37 +482,218 @@ interface Props {
|
|||||||
function LogoWrapper() {
|
function LogoWrapper() {
|
||||||
return <SidebarLogo />;
|
return <SidebarLogo />;
|
||||||
}
|
}
|
||||||
|
import { ChevronsUpDown, Plus } from "lucide-react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { AddOrganization } from "../dashboard/organization/handle-organization";
|
||||||
|
import { DialogAction } from "../shared/dialog-action";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
const data = {
|
||||||
|
user: {
|
||||||
|
name: "shadcn",
|
||||||
|
email: "m@example.com",
|
||||||
|
avatar: "/avatars/shadcn.jpg",
|
||||||
|
},
|
||||||
|
teams: [
|
||||||
|
{
|
||||||
|
name: "Acme Inc",
|
||||||
|
logo: GalleryVerticalEnd,
|
||||||
|
plan: "Enterprise",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Acme Corp.",
|
||||||
|
logo: AudioWaveform,
|
||||||
|
plan: "Startup",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Evil Corp.",
|
||||||
|
logo: Command,
|
||||||
|
plan: "Free",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
function SidebarLogo() {
|
function SidebarLogo() {
|
||||||
const { state } = useSidebar();
|
const { state } = useSidebar();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
const { data: user } = api.user.get.useQuery();
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
|
const { data: session } = authClient.useSession();
|
||||||
|
const {
|
||||||
|
data: organizations,
|
||||||
|
refetch,
|
||||||
|
isLoading,
|
||||||
|
} = api.organization.all.useQuery();
|
||||||
|
const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
|
||||||
|
api.organization.delete.useMutation();
|
||||||
|
const { isMobile } = useSidebar();
|
||||||
|
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||||
|
|
||||||
|
const [activeTeam, setActiveTeam] = useState<
|
||||||
|
typeof activeOrganization | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeOrganization) {
|
||||||
|
setActiveTeam(activeOrganization);
|
||||||
|
}
|
||||||
|
}, [activeOrganization]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<>
|
||||||
href="/dashboard/projects"
|
{isLoading ? (
|
||||||
className="flex items-center gap-2 p-1 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]/35 rounded-lg "
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[5vh] pt-4">
|
||||||
>
|
<span>Loading...</span>
|
||||||
<div
|
<Loader2 className="animate-spin size-4" />
|
||||||
className={cn(
|
</div>
|
||||||
"flex aspect-square items-center justify-center rounded-lg transition-all",
|
) : (
|
||||||
state === "collapsed" ? "size-6" : "size-10",
|
<SidebarMenu>
|
||||||
)}
|
<SidebarMenuItem>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<SidebarMenuButton
|
||||||
|
size="lg"
|
||||||
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
|
>
|
||||||
|
{/* <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex aspect-square items-center justify-center rounded-lg transition-all",
|
||||||
|
state === "collapsed" ? "size-6" : "size-10",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Logo
|
||||||
|
className={cn(
|
||||||
|
"transition-all",
|
||||||
|
state === "collapsed" ? "size-6" : "size-10",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span className="truncate font-semibold">
|
||||||
|
{activeTeam?.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDown className="ml-auto" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||||
|
align="start"
|
||||||
|
side={isMobile ? "bottom" : "right"}
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||||
|
Organizations
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{organizations?.map((org, index) => (
|
||||||
|
<div className="flex flex-row justify-between" key={org.name}>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={async () => {
|
||||||
|
await authClient.organization.setActive({
|
||||||
|
organizationId: org.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
className="w-full gap-2 p-2"
|
||||||
|
>
|
||||||
|
<div className="flex size-6 items-center justify-center rounded-sm border">
|
||||||
|
<Logo
|
||||||
|
className={cn(
|
||||||
|
"transition-all",
|
||||||
|
state === "collapsed" ? "size-6" : "size-10",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{org.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{(org.ownerId === session?.user?.id || isCloud) && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AddOrganization organizationId={org.id} />
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Organization"
|
||||||
|
description="Are you sure you want to delete this organization?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteOrganization({
|
||||||
|
organizationId: org.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success(
|
||||||
|
"Organization deleted successfully",
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(
|
||||||
|
error?.message ||
|
||||||
|
"Error deleting organization",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!isCloud && user?.role === "owner" && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<AddOrganization />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* <Link
|
||||||
|
href="/dashboard/projects"
|
||||||
|
className="flex items-center gap-2 p-1 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]/35 rounded-lg "
|
||||||
>
|
>
|
||||||
<Logo
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-all",
|
"flex aspect-square items-center justify-center rounded-lg transition-all",
|
||||||
state === "collapsed" ? "size-6" : "size-10",
|
state === "collapsed" ? "size-6" : "size-10",
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
</div>
|
<Logo
|
||||||
|
className={cn(
|
||||||
|
"transition-all",
|
||||||
|
state === "collapsed" ? "size-6" : "size-10",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="text-left text-sm leading-tight group-data-[state=open]/collapsible:rotate-90">
|
<div className="text-left text-sm leading-tight group-data-[state=open]/collapsible:rotate-90">
|
||||||
<p className="truncate font-semibold">Dokploy</p>
|
<p className="truncate font-semibold">Dokploy</p>
|
||||||
<p className="truncate text-xs text-muted-foreground">
|
<p className="truncate text-xs text-muted-foreground">
|
||||||
{dokployVersion}
|
{dokployVersion}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link> */}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,15 +714,7 @@ export default function Page({ children }: Props) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const currentPath = router.pathname;
|
const currentPath = router.pathname;
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const includesProjects = pathname?.includes("/dashboard/project");
|
const includesProjects = pathname?.includes("/dashboard/project");
|
||||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||||
@@ -548,7 +723,7 @@ export default function Page({ children }: Props) {
|
|||||||
home: filteredHome,
|
home: filteredHome,
|
||||||
settings: filteredSettings,
|
settings: filteredSettings,
|
||||||
help,
|
help,
|
||||||
} = createMenuForAuthUser({ auth, user, isCloud: !!isCloud });
|
} = createMenuForAuthUser({ auth, isCloud: !!isCloud });
|
||||||
|
|
||||||
const activeItem = findActiveNavItem(
|
const activeItem = findActiveNavItem(
|
||||||
[...filteredHome, ...filteredSettings],
|
[...filteredHome, ...filteredSettings],
|
||||||
@@ -557,7 +732,7 @@ export default function Page({ children }: Props) {
|
|||||||
|
|
||||||
// const showProjectsButton =
|
// const showProjectsButton =
|
||||||
// currentPath === "/dashboard/projects" &&
|
// currentPath === "/dashboard/projects" &&
|
||||||
// (auth?.rol === "admin" || user?.canCreateProjects);
|
// (auth?.rol === "owner" || user?.canCreateProjects);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<SidebarProvider
|
||||||
@@ -577,12 +752,12 @@ export default function Page({ children }: Props) {
|
|||||||
>
|
>
|
||||||
<Sidebar collapsible="icon" variant="floating">
|
<Sidebar collapsible="icon" variant="floating">
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<SidebarMenuButton
|
{/* <SidebarMenuButton
|
||||||
className="group-data-[collapsible=icon]:!p-0"
|
className="group-data-[collapsible=icon]:!p-0"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
> */}
|
||||||
<LogoWrapper />
|
<LogoWrapper />
|
||||||
</SidebarMenuButton>
|
{/* </SidebarMenuButton> */}
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
@@ -783,7 +958,7 @@ export default function Page({ children }: Props) {
|
|||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
{!isCloud && auth?.rol === "admin" && (
|
{!isCloud && auth?.role === "owner" && (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton asChild>
|
<SidebarMenuButton asChild>
|
||||||
<UpdateServerButton />
|
<UpdateServerButton />
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { Languages } from "@/lib/languages";
|
import { Languages } from "@/lib/languages";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import useLocale from "@/utils/hooks/use-locale";
|
import useLocale from "@/utils/hooks/use-locale";
|
||||||
@@ -29,18 +30,11 @@ const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
|||||||
|
|
||||||
export const UserNav = () => {
|
export const UserNav = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: data?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!data?.id && data?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const { locale, setLocale } = useLocale();
|
const { locale, setLocale } = useLocale();
|
||||||
const { mutateAsync } = api.auth.logout.useMutation();
|
// const { mutateAsync } = api.auth.logout.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -50,12 +44,15 @@ export const UserNav = () => {
|
|||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
>
|
>
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
<AvatarImage src={data?.image || ""} alt={data?.image || ""} />
|
<AvatarImage
|
||||||
|
src={data?.user?.image || ""}
|
||||||
|
alt={data?.user?.image || ""}
|
||||||
|
/>
|
||||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span className="truncate font-semibold">Account</span>
|
<span className="truncate font-semibold">Account</span>
|
||||||
<span className="truncate text-xs">{data?.email}</span>
|
<span className="truncate text-xs">{data?.user?.email}</span>
|
||||||
</div>
|
</div>
|
||||||
<ChevronsUpDown className="ml-auto size-4" />
|
<ChevronsUpDown className="ml-auto size-4" />
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
@@ -70,7 +67,7 @@ export const UserNav = () => {
|
|||||||
<DropdownMenuLabel className="flex flex-col">
|
<DropdownMenuLabel className="flex flex-col">
|
||||||
My Account
|
My Account
|
||||||
<span className="text-xs font-normal text-muted-foreground">
|
<span className="text-xs font-normal text-muted-foreground">
|
||||||
{data?.email}
|
{data?.user?.email}
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
@@ -95,7 +92,8 @@ export const UserNav = () => {
|
|||||||
>
|
>
|
||||||
Monitoring
|
Monitoring
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{(data?.rol === "admin" || user?.canAccessToTraefikFiles) && (
|
{(data?.role === "owner" ||
|
||||||
|
data?.user?.canAccessToTraefikFiles) && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -105,7 +103,7 @@ export const UserNav = () => {
|
|||||||
Traefik
|
Traefik
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{(data?.rol === "admin" || user?.canAccessToDocker) && (
|
{(data?.role === "owner" || data?.user?.canAccessToDocker) && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -118,7 +116,7 @@ export const UserNav = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data?.rol === "admin" && (
|
{data?.role === "owner" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -139,7 +137,7 @@ export const UserNav = () => {
|
|||||||
>
|
>
|
||||||
Profile
|
Profile
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{data?.rol === "admin" && (
|
{data?.role === "owner" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -150,7 +148,7 @@ export const UserNav = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data?.rol === "admin" && (
|
{data?.role === "owner" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -163,7 +161,7 @@ export const UserNav = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
{isCloud && data?.rol === "admin" && (
|
{isCloud && data?.role === "owner" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -178,9 +176,12 @@ export const UserNav = () => {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync().then(() => {
|
await authClient.signOut().then(() => {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
});
|
});
|
||||||
|
// await mutateAsync().then(() => {
|
||||||
|
// router.push("/");
|
||||||
|
// });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Log out
|
Log out
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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";
|
||||||
@@ -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;
|
||||||
@@ -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();
|
||||||
@@ -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;
|
||||||
@@ -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");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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";
|
||||||
@@ -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";
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -463,6 +463,76 @@
|
|||||||
"when": 1739087857244,
|
"when": 1739087857244,
|
||||||
"tag": "0065_daily_zaladane",
|
"tag": "0065_daily_zaladane",
|
||||||
"breakpoints": true
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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()],
|
||||||
|
});
|
||||||
@@ -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",
|
"name": "dokploy",
|
||||||
"version": "v0.18.3",
|
"version": "v0.18.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||||
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
||||||
"migration:run": "tsx -r dotenv/config migration.ts",
|
"migration:run": "tsx -r dotenv/config migration.ts",
|
||||||
|
"manual-migration:run": "tsx -r dotenv/config migrate.ts",
|
||||||
"migration:up": "drizzle-kit up --config ./server/db/drizzle.config.ts",
|
"migration:up": "drizzle-kit up --config ./server/db/drizzle.config.ts",
|
||||||
"migration:drop": "drizzle-kit drop --config ./server/db/drizzle.config.ts",
|
"migration:drop": "drizzle-kit drop --config ./server/db/drizzle.config.ts",
|
||||||
"db:push": "drizzle-kit push --config ./server/db/drizzle.config.ts",
|
"db:push": "drizzle-kit push --config ./server/db/drizzle.config.ts",
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"test": "vitest --config __test__/vitest.config.ts"
|
"test": "vitest --config __test__/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"better-auth": "1.1.16",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"rotating-file-stream": "3.2.3",
|
"rotating-file-stream": "3.2.3",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { auth } from "@dokploy/server/index";
|
||||||
|
import { toNodeHandler } from "better-auth/node";
|
||||||
|
|
||||||
|
// Disallow body parsing, we will parse it manually
|
||||||
|
export const config = { api: { bodyParser: false } };
|
||||||
|
|
||||||
|
export default toNodeHandler(auth.handler);
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { github } from "@/server/db/schema";
|
import { github } from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
|
auth,
|
||||||
createGithub,
|
createGithub,
|
||||||
findAdminByAuthId,
|
findAdminByAuthId,
|
||||||
findAuthById,
|
findAuthById,
|
||||||
findUserByAuthId,
|
findUserByAuthId,
|
||||||
|
findUserById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
@@ -28,7 +30,7 @@ export default async function handler(
|
|||||||
return res.status(400).json({ error: "Missing code parameter" });
|
return res.status(400).json({ error: "Missing code parameter" });
|
||||||
}
|
}
|
||||||
const [action, value] = state?.split(":");
|
const [action, value] = state?.split(":");
|
||||||
// Value could be the authId or the githubProviderId
|
// Value could be the organizationId or the githubProviderId
|
||||||
|
|
||||||
if (action === "gh_init") {
|
if (action === "gh_init") {
|
||||||
const octokit = new Octokit({});
|
const octokit = new Octokit({});
|
||||||
@@ -39,17 +41,6 @@ export default async function handler(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const auth = await findAuthById(value as string);
|
|
||||||
|
|
||||||
let adminId = "";
|
|
||||||
if (auth.rol === "admin") {
|
|
||||||
const admin = await findAdminByAuthId(auth.id);
|
|
||||||
adminId = admin.adminId;
|
|
||||||
} else {
|
|
||||||
const user = await findUserByAuthId(auth.id);
|
|
||||||
adminId = user.adminId;
|
|
||||||
}
|
|
||||||
|
|
||||||
await createGithub(
|
await createGithub(
|
||||||
{
|
{
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -60,7 +51,7 @@ export default async function handler(
|
|||||||
githubWebhookSecret: data.webhook_secret,
|
githubWebhookSecret: data.webhook_secret,
|
||||||
githubPrivateKey: data.pem,
|
githubPrivateKey: data.pem,
|
||||||
},
|
},
|
||||||
adminId,
|
value as string,
|
||||||
);
|
);
|
||||||
} else if (action === "gh_setup") {
|
} else if (action === "gh_setup") {
|
||||||
await db
|
await db
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { buffer } from "node:stream/consumers";
|
import { buffer } from "node:stream/consumers";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { admins, server } from "@/server/db/schema";
|
import { server, users_temp } from "@/server/db/schema";
|
||||||
import { findAdminById } from "@dokploy/server";
|
import { findAdminById, findUserById } from "@dokploy/server";
|
||||||
import { asc, eq } from "drizzle-orm";
|
import { asc, eq } from "drizzle-orm";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
@@ -64,33 +64,35 @@ export default async function handler(
|
|||||||
session.subscription as string,
|
session.subscription as string,
|
||||||
);
|
);
|
||||||
await db
|
await db
|
||||||
.update(admins)
|
.update(users_temp)
|
||||||
.set({
|
.set({
|
||||||
stripeCustomerId: session.customer as string,
|
stripeCustomerId: session.customer as string,
|
||||||
stripeSubscriptionId: session.subscription as string,
|
stripeSubscriptionId: session.subscription as string,
|
||||||
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
})
|
})
|
||||||
.where(eq(admins.adminId, adminId))
|
.where(eq(users_temp.id, adminId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const admin = await findAdminById(adminId);
|
const admin = await findUserById(adminId);
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
const newServersQuantity = admin.serversQuantity;
|
const newServersQuantity = admin.serversQuantity;
|
||||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "customer.subscription.created": {
|
case "customer.subscription.created": {
|
||||||
const newSubscription = event.data.object as Stripe.Subscription;
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(admins)
|
.update(users_temp)
|
||||||
.set({
|
.set({
|
||||||
stripeSubscriptionId: newSubscription.id,
|
stripeSubscriptionId: newSubscription.id,
|
||||||
stripeCustomerId: newSubscription.customer as string,
|
stripeCustomerId: newSubscription.customer as string,
|
||||||
})
|
})
|
||||||
.where(eq(admins.stripeCustomerId, newSubscription.customer as string))
|
.where(
|
||||||
|
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||||
|
)
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -100,14 +102,16 @@ export default async function handler(
|
|||||||
const newSubscription = event.data.object as Stripe.Subscription;
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(admins)
|
.update(users_temp)
|
||||||
.set({
|
.set({
|
||||||
stripeSubscriptionId: null,
|
stripeSubscriptionId: null,
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
})
|
})
|
||||||
.where(eq(admins.stripeCustomerId, newSubscription.customer as string));
|
.where(
|
||||||
|
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||||
|
);
|
||||||
|
|
||||||
const admin = await findAdminByStripeCustomerId(
|
const admin = await findUserByStripeCustomerId(
|
||||||
newSubscription.customer as string,
|
newSubscription.customer as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -115,13 +119,13 @@ export default async function handler(
|
|||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
await disableServers(admin.adminId);
|
await disableServers(admin.id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "customer.subscription.updated": {
|
case "customer.subscription.updated": {
|
||||||
const newSubscription = event.data.object as Stripe.Subscription;
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
const admin = await findAdminByStripeCustomerId(
|
const admin = await findUserByStripeCustomerId(
|
||||||
newSubscription.customer as string,
|
newSubscription.customer as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -131,23 +135,23 @@ export default async function handler(
|
|||||||
|
|
||||||
if (newSubscription.status === "active") {
|
if (newSubscription.status === "active") {
|
||||||
await db
|
await db
|
||||||
.update(admins)
|
.update(users_temp)
|
||||||
.set({
|
.set({
|
||||||
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
eq(admins.stripeCustomerId, newSubscription.customer as string),
|
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||||
);
|
);
|
||||||
|
|
||||||
const newServersQuantity = admin.serversQuantity;
|
const newServersQuantity = admin.serversQuantity;
|
||||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||||
} else {
|
} else {
|
||||||
await disableServers(admin.adminId);
|
await disableServers(admin.id);
|
||||||
await db
|
await db
|
||||||
.update(admins)
|
.update(users_temp)
|
||||||
.set({ serversQuantity: 0 })
|
.set({ serversQuantity: 0 })
|
||||||
.where(
|
.where(
|
||||||
eq(admins.stripeCustomerId, newSubscription.customer as string),
|
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +178,7 @@ export default async function handler(
|
|||||||
})
|
})
|
||||||
.where(eq(admins.stripeCustomerId, suscription.customer as string));
|
.where(eq(admins.stripeCustomerId, suscription.customer as string));
|
||||||
|
|
||||||
const admin = await findAdminByStripeCustomerId(
|
const admin = await findUserByStripeCustomerId(
|
||||||
suscription.customer as string,
|
suscription.customer as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -182,7 +186,7 @@ export default async function handler(
|
|||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
const newServersQuantity = admin.serversQuantity;
|
const newServersQuantity = admin.serversQuantity;
|
||||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "invoice.payment_failed": {
|
case "invoice.payment_failed": {
|
||||||
@@ -193,7 +197,7 @@ export default async function handler(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (subscription.status !== "active") {
|
if (subscription.status !== "active") {
|
||||||
const admin = await findAdminByStripeCustomerId(
|
const admin = await findUserByStripeCustomerId(
|
||||||
newInvoice.customer as string,
|
newInvoice.customer as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -207,7 +211,7 @@ export default async function handler(
|
|||||||
})
|
})
|
||||||
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
|
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
|
||||||
|
|
||||||
await disableServers(admin.adminId);
|
await disableServers(admin.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -216,20 +220,20 @@ export default async function handler(
|
|||||||
case "customer.deleted": {
|
case "customer.deleted": {
|
||||||
const customer = event.data.object as Stripe.Customer;
|
const customer = event.data.object as Stripe.Customer;
|
||||||
|
|
||||||
const admin = await findAdminByStripeCustomerId(customer.id);
|
const admin = await findUserByStripeCustomerId(customer.id);
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
await disableServers(admin.adminId);
|
await disableServers(admin.id);
|
||||||
await db
|
await db
|
||||||
.update(admins)
|
.update(users_temp)
|
||||||
.set({
|
.set({
|
||||||
stripeCustomerId: null,
|
stripeCustomerId: null,
|
||||||
stripeSubscriptionId: null,
|
stripeSubscriptionId: null,
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
})
|
})
|
||||||
.where(eq(admins.stripeCustomerId, customer.id));
|
.where(eq(users_temp.stripeCustomerId, customer.id));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -240,20 +244,20 @@ export default async function handler(
|
|||||||
return res.status(200).json({ received: true });
|
return res.status(200).json({ received: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const disableServers = async (adminId: string) => {
|
const disableServers = async (userId: string) => {
|
||||||
await db
|
await db
|
||||||
.update(server)
|
.update(server)
|
||||||
.set({
|
.set({
|
||||||
serverStatus: "inactive",
|
serverStatus: "inactive",
|
||||||
})
|
})
|
||||||
.where(eq(server.adminId, adminId));
|
.where(eq(server.userId, userId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const findAdminByStripeCustomerId = async (stripeCustomerId: string) => {
|
const findUserByStripeCustomerId = async (stripeCustomerId: string) => {
|
||||||
const admin = db.query.admins.findFirst({
|
const user = db.query.users_temp.findFirst({
|
||||||
where: eq(admins.stripeCustomerId, stripeCustomerId),
|
where: eq(users_temp.stripeCustomerId, stripeCustomerId),
|
||||||
});
|
});
|
||||||
return admin;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
const activateServer = async (serverId: string) => {
|
const activateServer = async (serverId: string) => {
|
||||||
@@ -270,19 +274,19 @@ const deactivateServer = async (serverId: string) => {
|
|||||||
.where(eq(server.serverId, serverId));
|
.where(eq(server.serverId, serverId));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findServersByAdminIdSorted = async (adminId: string) => {
|
export const findServersByUserIdSorted = async (userId: string) => {
|
||||||
const servers = await db.query.server.findMany({
|
const servers = await db.query.server.findMany({
|
||||||
where: eq(server.adminId, adminId),
|
where: eq(server.userId, userId),
|
||||||
orderBy: asc(server.createdAt),
|
orderBy: asc(server.createdAt),
|
||||||
});
|
});
|
||||||
|
|
||||||
return servers;
|
return servers;
|
||||||
};
|
};
|
||||||
export const updateServersBasedOnQuantity = async (
|
export const updateServersBasedOnQuantity = async (
|
||||||
adminId: string,
|
userId: string,
|
||||||
newServersQuantity: number,
|
newServersQuantity: number,
|
||||||
) => {
|
) => {
|
||||||
const servers = await findServersByAdminIdSorted(adminId);
|
const servers = await findServersByUserIdSorted(userId);
|
||||||
|
|
||||||
if (servers.length > newServersQuantity) {
|
if (servers.length > newServersQuantity) {
|
||||||
for (const [index, server] of servers.entries()) {
|
for (const [index, server] of servers.entries()) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
|
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
@@ -27,7 +28,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -44,21 +45,20 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
const auth = await helpers.auth.get.fetch();
|
|
||||||
|
|
||||||
if (auth.rol === "user") {
|
if (user.role === "member") {
|
||||||
const user = await helpers.user.byAuthId.fetch({
|
const userR = await helpers.user.one.fetch({
|
||||||
authId: auth.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user.canAccessToDocker) {
|
if (!userR.canAccessToDocker) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
import { validateRequest } from "@dokploy/server/index";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
@@ -25,7 +25,7 @@ const Dashboard = () => {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: monitoring, isLoading } = api.admin.getMetricsToken.useQuery();
|
const { data: monitoring, isLoading } = api.user.getMetricsToken.useQuery();
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 pb-10">
|
<div className="space-y-4 pb-10">
|
||||||
{/* <AlertBlock>
|
{/* <AlertBlock>
|
||||||
@@ -104,7 +104,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
const { user } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import type { findProjectById } from "@dokploy/server";
|
import type { findProjectById } from "@dokploy/server";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import {
|
import {
|
||||||
Ban,
|
Ban,
|
||||||
@@ -70,9 +70,9 @@ import type {
|
|||||||
} from "next";
|
} from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useMemo, useState, type ReactElement } from "react";
|
import { type ReactElement, useMemo, useState } from "react";
|
||||||
import superjson from "superjson";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
appName: string;
|
appName: string;
|
||||||
@@ -200,15 +200,8 @@ const Project = (
|
|||||||
) => {
|
) => {
|
||||||
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
||||||
const { projectId } = props;
|
const { projectId } = props;
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const { data, isLoading, refetch } = api.project.one.useQuery({ projectId });
|
const { data, isLoading, refetch } = api.project.one.useQuery({ projectId });
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -335,7 +328,7 @@ const Project = (
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>{data?.description}</CardDescription>
|
<CardDescription>{data?.description}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{(auth?.rol === "admin" || user?.canCreateServices) && (
|
{(auth?.role === "owner" || auth?.user?.canCreateServices) && (
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<ProjectEnvironment projectId={projectId}>
|
<ProjectEnvironment projectId={projectId}>
|
||||||
<Button variant="outline">Project Environment</Button>
|
<Button variant="outline">Project Environment</Button>
|
||||||
@@ -658,7 +651,7 @@ export async function getServerSideProps(
|
|||||||
const { params } = ctx;
|
const { params } = ctx;
|
||||||
|
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -674,8 +667,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
+8
-15
@@ -39,7 +39,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { GlobeIcon, HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
import { GlobeIcon, HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||||
@@ -86,16 +86,8 @@ const Service = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
@@ -186,7 +178,8 @@ const Service = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateApplication applicationId={applicationId} />
|
<UpdateApplication applicationId={applicationId} />
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.user?.canDeleteServices) && (
|
||||||
<DeleteService id={applicationId} type="application" />
|
<DeleteService id={applicationId} type="application" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -370,7 +363,7 @@ export async function getServerSideProps(
|
|||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
|
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -386,8 +379,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { CircuitBoard, ServerOff } from "lucide-react";
|
import { CircuitBoard, ServerOff } from "lucide-react";
|
||||||
@@ -79,17 +79,9 @@ const Service = (
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
@@ -181,7 +173,8 @@ const Service = (
|
|||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateCompose composeId={composeId} />
|
<UpdateCompose composeId={composeId} />
|
||||||
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.user?.canDeleteServices) && (
|
||||||
<DeleteService id={composeId} type="compose" />
|
<DeleteService id={composeId} type="compose" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -366,7 +359,7 @@ export async function getServerSideProps(
|
|||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
|
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -382,8 +375,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
@@ -61,16 +61,9 @@ const Mariadb = (
|
|||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -154,7 +147,8 @@ const Mariadb = (
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateMariadb mariadbId={mariadbId} />
|
<UpdateMariadb mariadbId={mariadbId} />
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.user?.canDeleteServices) && (
|
||||||
<DeleteService id={mariadbId} type="mariadb" />
|
<DeleteService id={mariadbId} type="mariadb" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -316,7 +310,7 @@ export async function getServerSideProps(
|
|||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
|
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -332,8 +326,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
@@ -61,16 +61,8 @@ const Mongo = (
|
|||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mongo.one.useQuery({ mongoId });
|
const { data } = api.mongo.one.useQuery({ mongoId });
|
||||||
|
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
@@ -156,7 +148,8 @@ const Mongo = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateMongo mongoId={mongoId} />
|
<UpdateMongo mongoId={mongoId} />
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.user?.canDeleteServices) && (
|
||||||
<DeleteService id={mongoId} type="mongo" />
|
<DeleteService id={mongoId} type="mongo" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -318,7 +311,7 @@ export async function getServerSideProps(
|
|||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
|
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -334,8 +327,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
@@ -60,16 +60,8 @@ const MySql = (
|
|||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mysql.one.useQuery({ mysqlId });
|
const { data } = api.mysql.one.useQuery({ mysqlId });
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
@@ -156,7 +148,8 @@ const MySql = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateMysql mysqlId={mysqlId} />
|
<UpdateMysql mysqlId={mysqlId} />
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.user?.canDeleteServices) && (
|
||||||
<DeleteService id={mysqlId} type="mysql" />
|
<DeleteService id={mysqlId} type="mysql" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -323,7 +316,7 @@ export async function getServerSideProps(
|
|||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
|
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -339,8 +332,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
+9
-15
@@ -35,7 +35,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
@@ -60,16 +60,9 @@ const Postgresql = (
|
|||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -154,7 +147,8 @@ const Postgresql = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdatePostgres postgresId={postgresId} />
|
<UpdatePostgres postgresId={postgresId} />
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.user?.canDeleteServices) && (
|
||||||
<DeleteService id={postgresId} type="postgres" />
|
<DeleteService id={postgresId} type="postgres" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -319,7 +313,7 @@ export async function getServerSideProps(
|
|||||||
) {
|
) {
|
||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -335,8 +329,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff } from "lucide-react";
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
@@ -60,16 +60,8 @@ const Redis = (
|
|||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.redis.one.useQuery({ redisId });
|
const { data } = api.redis.one.useQuery({ redisId });
|
||||||
|
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
@@ -155,7 +147,8 @@ const Redis = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateRedis redisId={redisId} />
|
<UpdateRedis redisId={redisId} />
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.user?.canDeleteServices) && (
|
||||||
<DeleteService id={redisId} type="redis" />
|
<DeleteService id={redisId} type="redis" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -311,7 +304,7 @@ export async function getServerSideProps(
|
|||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
|
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -327,8 +320,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ShowProjects } from "@/components/dashboard/projects/show";
|
|||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
@@ -38,7 +38,7 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
|
|
||||||
const helpers = createServerSideHelpers({
|
const helpers = createServerSideHelpers({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
@@ -46,8 +46,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ShowRequests } from "@/components/dashboard/requests/show-requests";
|
import { ShowRequests } from "@/components/dashboard/requests/show-requests";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@@ -22,7 +23,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
const { user } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { ShowBilling } from "@/components/dashboard/settings/billing/show-billin
|
|||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
@@ -29,8 +30,8 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -45,8 +46,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -40,8 +40,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -48,8 +48,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -41,8 +41,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -40,8 +40,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
@@ -49,14 +49,12 @@ export async function getServerSideProps(
|
|||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
await helpers.settings.isCloud.prefetch();
|
await helpers.settings.isCloud.prefetch();
|
||||||
const auth = await helpers.auth.get.fetch();
|
if (user.role === "member") {
|
||||||
|
const userR = await helpers.user.one.fetch({
|
||||||
if (auth.rol === "user") {
|
userId: user.id,
|
||||||
const user = await helpers.user.byAuthId.fetch({
|
|
||||||
authId: auth.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user.canAccessToGitProviders) {
|
if (!userR.canAccessToGitProviders) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ const settings = z.object({
|
|||||||
type SettingsType = z.infer<typeof settings>;
|
type SettingsType = z.infer<typeof settings>;
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const { data, refetch } = api.admin.one.useQuery();
|
const { data, refetch } = api.user.get.useQuery();
|
||||||
const { mutateAsync, isLoading, isError, error } =
|
const { mutateAsync, isLoading, isError, error } =
|
||||||
api.admin.update.useMutation();
|
api.user.update.useMutation();
|
||||||
const form = useForm<SettingsType>({
|
const form = useForm<SettingsType>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
cleanCacheOnApplications: false,
|
cleanCacheOnApplications: false,
|
||||||
@@ -55,9 +55,9 @@ const Page = () => {
|
|||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
cleanCacheOnApplications: data?.cleanupCacheApplications || false,
|
cleanCacheOnApplications: data?.user.cleanupCacheApplications || false,
|
||||||
cleanCacheOnCompose: data?.cleanupCacheOnCompose || false,
|
cleanCacheOnCompose: data?.user.cleanupCacheOnCompose || false,
|
||||||
cleanCacheOnPreviews: data?.cleanupCacheOnPreviews || false,
|
cleanCacheOnPreviews: data?.user.cleanupCacheOnPreviews || false,
|
||||||
});
|
});
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -190,7 +190,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (user.rol === "user") {
|
if (user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -205,8 +205,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -41,8 +41,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,22 +13,16 @@ import React, { type ReactElement } from "react";
|
|||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: data?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!data?.id && data?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||||
<ProfileForm />
|
<ProfileForm />
|
||||||
{(user?.canAccessToAPI || data?.rol === "admin") && <GenerateToken />}
|
{(data?.user?.canAccessToAPI || data?.role === "owner") && (
|
||||||
|
<GenerateToken />
|
||||||
|
)}
|
||||||
|
|
||||||
{isCloud && <RemoveSelfAccount />}
|
{isCloud && <RemoveSelfAccount />}
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +40,7 @@ export async function getServerSideProps(
|
|||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const locale = getLocale(req.cookies);
|
const locale = getLocale(req.cookies);
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
|
|
||||||
const helpers = createServerSideHelpers({
|
const helpers = createServerSideHelpers({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
@@ -54,18 +48,21 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|
||||||
await helpers.settings.isCloud.prefetch();
|
await helpers.settings.isCloud.prefetch();
|
||||||
await helpers.auth.get.prefetch();
|
await helpers.auth.get.prefetch();
|
||||||
if (user?.rol === "user") {
|
if (user?.role === "member") {
|
||||||
await helpers.user.byAuthId.prefetch({
|
// const userR = await helpers.user.one.fetch({
|
||||||
authId: user.authId,
|
// userId: user.id,
|
||||||
});
|
// });
|
||||||
|
// await helpers.user.byAuthId.prefetch({
|
||||||
|
// authId: user.authId,
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -40,8 +40,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,19 +2,7 @@ import { SetupMonitoring } from "@/components/dashboard/settings/servers/setup-m
|
|||||||
import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
||||||
import { WebServer } from "@/components/dashboard/settings/web-server";
|
import { WebServer } from "@/components/dashboard/settings/web-server";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
|
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
@@ -25,8 +13,6 @@ import { toast } from "sonner";
|
|||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const { data, refetch } = api.admin.one.useQuery();
|
|
||||||
const { mutateAsync: update } = api.admin.update.useMutation();
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||||
@@ -98,7 +84,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -107,7 +93,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (user.rol === "user") {
|
if (user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -122,8 +108,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export async function getServerSideProps(
|
|||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const locale = await getLocale(req.cookies);
|
const locale = await getLocale(req.cookies);
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -36,7 +36,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (user.rol === "user") {
|
if (user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -51,8 +51,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -40,23 +40,22 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
const auth = await helpers.auth.get.fetch();
|
|
||||||
await helpers.settings.isCloud.prefetch();
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
if (auth.rol === "user") {
|
if (user.role === "member") {
|
||||||
const user = await helpers.user.byAuthId.fetch({
|
const userR = await helpers.user.one.fetch({
|
||||||
authId: auth.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user.canAccessToSSHKeys) {
|
if (!userR.canAccessToSSHKeys) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations";
|
||||||
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ const Page = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<ShowUsers />
|
<ShowUsers />
|
||||||
|
<ShowInvitations />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -25,8 +27,10 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user || user.rol === "user") {
|
|
||||||
|
console.log("user", user, session);
|
||||||
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -41,8 +45,8 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
|
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
@@ -27,7 +28,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -44,21 +45,20 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
const auth = await helpers.auth.get.fetch();
|
|
||||||
|
|
||||||
if (auth.rol === "user") {
|
if (user.role === "member") {
|
||||||
const user = await helpers.user.byAuthId.fetch({
|
const userR = await helpers.user.one.fetch({
|
||||||
authId: auth.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user.canAccessToDocker) {
|
if (!userR.canAccessToDocker) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { ShowTraefikSystem } from "@/components/dashboard/file-system/show-traefik-system";
|
import { ShowTraefikSystem } from "@/components/dashboard/file-system/show-traefik-system";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
@@ -27,7 +28,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -44,21 +45,20 @@ export async function getServerSideProps(
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
const auth = await helpers.auth.get.fetch();
|
|
||||||
|
|
||||||
if (auth.rol === "user") {
|
if (user.role === "member") {
|
||||||
const user = await helpers.user.byAuthId.fetch({
|
const userR = await helpers.user.one.fetch({
|
||||||
authId: auth.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user.canAccessToTraefikFiles) {
|
if (!userR.canAccessToTraefikFiles) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
+294
-103
@@ -3,99 +3,177 @@ import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Logo } from "@/components/shared/logo";
|
import { Logo } from "@/components/shared/logo";
|
||||||
import { Button, buttonVariants } from "@/components/ui/button";
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
import { CardContent } from "@/components/ui/card";
|
import { CardContent, CardDescription } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
InputOTP,
|
||||||
|
InputOTPGroup,
|
||||||
|
InputOTPSlot,
|
||||||
|
} from "@/components/ui/input-otp";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD, auth, isAdminPresent } from "@dokploy/server";
|
||||||
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import base32 from "hi-base32";
|
||||||
|
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
||||||
|
import { AlertTriangle } from "lucide-react";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { TOTP } from "otpauth";
|
||||||
import { type ReactElement, useEffect, useState } from "react";
|
import { type ReactElement, useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const loginSchema = z.object({
|
const LoginSchema = z.object({
|
||||||
email: z
|
email: z.string().email(),
|
||||||
.string()
|
password: z.string().min(8),
|
||||||
.min(1, {
|
|
||||||
message: "Email is required",
|
|
||||||
})
|
|
||||||
.email({
|
|
||||||
message: "Email must be a valid email",
|
|
||||||
}),
|
|
||||||
|
|
||||||
password: z
|
|
||||||
.string()
|
|
||||||
.min(1, {
|
|
||||||
message: "Password is required",
|
|
||||||
})
|
|
||||||
.min(8, {
|
|
||||||
message: "Password must be at least 8 characters",
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type Login = z.infer<typeof loginSchema>;
|
const TwoFactorSchema = z.object({
|
||||||
|
code: z.string().min(6),
|
||||||
|
});
|
||||||
|
|
||||||
type AuthResponse = {
|
const BackupCodeSchema = z.object({
|
||||||
is2FAEnabled: boolean;
|
code: z.string().min(8, {
|
||||||
authId: string;
|
message: "Backup code must be at least 8 characters",
|
||||||
};
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type LoginForm = z.infer<typeof LoginSchema>;
|
||||||
|
type BackupCodeForm = z.infer<typeof BackupCodeSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
IS_CLOUD: boolean;
|
IS_CLOUD: boolean;
|
||||||
}
|
}
|
||||||
export default function Home({ IS_CLOUD }: Props) {
|
export default function Home({ IS_CLOUD }: Props) {
|
||||||
const [temp, setTemp] = useState<AuthResponse>({
|
|
||||||
is2FAEnabled: false,
|
|
||||||
authId: "",
|
|
||||||
});
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
|
||||||
api.auth.login.useMutation();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<Login>({
|
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
||||||
|
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
|
||||||
|
const [isBackupCodeLoading, setIsBackupCodeLoading] = useState(false);
|
||||||
|
const [isTwoFactor, setIsTwoFactor] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [twoFactorCode, setTwoFactorCode] = useState("");
|
||||||
|
const [isBackupCodeModalOpen, setIsBackupCodeModalOpen] = useState(false);
|
||||||
|
const [backupCode, setBackupCode] = useState("");
|
||||||
|
|
||||||
|
const loginForm = useForm<LoginForm>({
|
||||||
|
resolver: zodResolver(LoginSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "siumauricio@hotmail.com",
|
||||||
password: "",
|
password: "Password123",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(loginSchema),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const onSubmit = async (values: LoginForm) => {
|
||||||
form.reset();
|
setIsLoginLoading(true);
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
try {
|
||||||
|
const { data, error } = await authClient.signIn.email({
|
||||||
const onSubmit = async (values: Login) => {
|
email: values.email,
|
||||||
await mutateAsync({
|
password: values.password,
|
||||||
email: values.email.toLowerCase(),
|
|
||||||
password: values.password,
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
if (data.is2FAEnabled) {
|
|
||||||
setTemp(data);
|
|
||||||
} else {
|
|
||||||
toast.success("Successfully signed in", {
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
router.push("/dashboard/projects");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Signin failed", {
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error(error.message);
|
||||||
|
setError(error.message || "An error occurred while logging in");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.twoFactorRedirect as boolean) {
|
||||||
|
setTwoFactorCode("");
|
||||||
|
setIsTwoFactor(true);
|
||||||
|
toast.info("Please enter your 2FA code");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Logged in successfully");
|
||||||
|
router.push("/dashboard/projects");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("An error occurred while logging in");
|
||||||
|
} finally {
|
||||||
|
setIsLoginLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onTwoFactorSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (twoFactorCode.length !== 6) {
|
||||||
|
toast.error("Please enter a valid 6-digit code");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsTwoFactorLoading(true);
|
||||||
|
try {
|
||||||
|
const { data, error } = await authClient.twoFactor.verifyTotp({
|
||||||
|
code: twoFactorCode.replace(/\s/g, ""),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error(error.message);
|
||||||
|
setError(error.message || "An error occurred while verifying 2FA code");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Logged in successfully");
|
||||||
|
router.push("/dashboard/projects");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("An error occurred while verifying 2FA code");
|
||||||
|
} finally {
|
||||||
|
setIsTwoFactorLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBackupCodeSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (backupCode.length < 8) {
|
||||||
|
toast.error("Please enter a valid backup code");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsBackupCodeLoading(true);
|
||||||
|
try {
|
||||||
|
const { data, error } = await authClient.twoFactor.verifyBackupCode({
|
||||||
|
code: backupCode.trim(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error(error.message);
|
||||||
|
setError(
|
||||||
|
error.message || "An error occurred while verifying backup code",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Logged in successfully");
|
||||||
|
router.push("/dashboard/projects");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("An error occurred while verifying backup code");
|
||||||
|
} finally {
|
||||||
|
setIsBackupCodeLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col space-y-2 text-center">
|
<div className="flex flex-col space-y-2 text-center">
|
||||||
@@ -109,55 +187,169 @@ export default function Home({ IS_CLOUD }: Props) {
|
|||||||
Enter your email and password to sign in
|
Enter your email and password to sign in
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{isError && (
|
{error && (
|
||||||
<AlertBlock type="error" className="my-2">
|
<AlertBlock type="error" className="my-2">
|
||||||
<span>{error?.message}</span>
|
<span>{error}</span>
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
{!temp.is2FAEnabled ? (
|
{!isTwoFactor ? (
|
||||||
<Form {...form}>
|
<Form {...loginForm}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-4">
|
<form
|
||||||
<div className="space-y-4">
|
onSubmit={loginForm.handleSubmit(onSubmit)}
|
||||||
<FormField
|
className="space-y-4"
|
||||||
control={form.control}
|
id="login-form"
|
||||||
name="email"
|
>
|
||||||
render={({ field }) => (
|
<FormField
|
||||||
<FormItem>
|
control={loginForm.control}
|
||||||
<FormLabel>Email</FormLabel>
|
name="email"
|
||||||
<FormControl>
|
render={({ field }) => (
|
||||||
<Input placeholder="Email" {...field} />
|
<FormItem>
|
||||||
</FormControl>
|
<FormLabel>Email</FormLabel>
|
||||||
<FormMessage />
|
<FormControl>
|
||||||
</FormItem>
|
<Input placeholder="john@example.com" {...field} />
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
<FormMessage />
|
||||||
<FormField
|
</FormItem>
|
||||||
control={form.control}
|
)}
|
||||||
name="password"
|
/>
|
||||||
render={({ field }) => (
|
<FormField
|
||||||
<FormItem>
|
control={loginForm.control}
|
||||||
<FormLabel>Password</FormLabel>
|
name="password"
|
||||||
<FormControl>
|
render={({ field }) => (
|
||||||
<Input
|
<FormItem>
|
||||||
type="password"
|
<FormLabel>Password</FormLabel>
|
||||||
placeholder="Password"
|
<FormControl>
|
||||||
{...field}
|
<Input
|
||||||
/>
|
type="password"
|
||||||
</FormControl>
|
placeholder="Enter your password"
|
||||||
<FormMessage />
|
{...field}
|
||||||
</FormItem>
|
/>
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
<Button type="submit" isLoading={isLoading} className="w-full">
|
)}
|
||||||
Login
|
/>
|
||||||
</Button>
|
<Button
|
||||||
</div>
|
className="w-full"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoginLoading}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
) : (
|
) : (
|
||||||
<Login2FA authId={temp.authId} />
|
<>
|
||||||
|
<form
|
||||||
|
onSubmit={onTwoFactorSubmit}
|
||||||
|
className="space-y-4"
|
||||||
|
id="two-factor-form"
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>2FA Code</Label>
|
||||||
|
<InputOTP
|
||||||
|
value={twoFactorCode}
|
||||||
|
onChange={setTwoFactorCode}
|
||||||
|
maxLength={6}
|
||||||
|
pattern={REGEXP_ONLY_DIGITS}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot index={0} className="border-border" />
|
||||||
|
<InputOTPSlot index={1} className="border-border" />
|
||||||
|
<InputOTPSlot index={2} className="border-border" />
|
||||||
|
<InputOTPSlot index={3} className="border-border" />
|
||||||
|
<InputOTPSlot index={4} className="border-border" />
|
||||||
|
<InputOTPSlot index={5} className="border-border" />
|
||||||
|
</InputOTPGroup>
|
||||||
|
</InputOTP>
|
||||||
|
<CardDescription>
|
||||||
|
Enter the 6-digit code from your authenticator app
|
||||||
|
</CardDescription>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsBackupCodeModalOpen(true)}
|
||||||
|
className="text-sm text-muted-foreground hover:underline self-start mt-2"
|
||||||
|
>
|
||||||
|
Lost access to your authenticator app?
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsTwoFactor(false);
|
||||||
|
setTwoFactorCode("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isTwoFactorLoading}
|
||||||
|
>
|
||||||
|
Verify
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={isBackupCodeModalOpen}
|
||||||
|
onOpenChange={setIsBackupCodeModalOpen}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Enter Backup Code</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Enter one of your backup codes to access your account
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form onSubmit={onBackupCodeSubmit} className="space-y-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>Backup Code</Label>
|
||||||
|
<Input
|
||||||
|
value={backupCode}
|
||||||
|
onChange={(e) => setBackupCode(e.target.value)}
|
||||||
|
placeholder="Enter your backup code"
|
||||||
|
className="font-mono"
|
||||||
|
/>
|
||||||
|
<CardDescription>
|
||||||
|
Enter one of the backup codes you received when setting up
|
||||||
|
2FA
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsBackupCodeModalOpen(false);
|
||||||
|
setBackupCode("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isBackupCodeLoading}
|
||||||
|
>
|
||||||
|
Verify
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-row justify-between flex-wrap">
|
<div className="flex flex-row justify-between flex-wrap">
|
||||||
@@ -203,8 +395,7 @@ Home.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
try {
|
try {
|
||||||
const { user } = await validateRequest(context.req, context.res);
|
const { user } = await validateRequest(context.req);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -232,7 +423,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user } = await validateRequest(context.req, context.res);
|
const { user } = await validateRequest(context.req);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
+194
-117
@@ -1,4 +1,5 @@
|
|||||||
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Logo } from "@/components/shared/logo";
|
import { Logo } from "@/components/shared/logo";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -16,10 +17,11 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { IS_CLOUD, getUserByToken } from "@dokploy/server";
|
import { IS_CLOUD, getUserByToken } from "@dokploy/server";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertCircle, AlertTriangle } from "lucide-react";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -30,6 +32,9 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const registerSchema = z
|
const registerSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
name: z.string().min(1, {
|
||||||
|
message: "Name is required",
|
||||||
|
}),
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, {
|
.min(1, {
|
||||||
@@ -38,7 +43,6 @@ const registerSchema = z
|
|||||||
.email({
|
.email({
|
||||||
message: "Email must be a valid email",
|
message: "Email must be a valid email",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, {
|
.min(1, {
|
||||||
@@ -71,11 +75,17 @@ interface Props {
|
|||||||
token: string;
|
token: string;
|
||||||
invitation: Awaited<ReturnType<typeof getUserByToken>>;
|
invitation: Awaited<ReturnType<typeof getUserByToken>>;
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
|
userAlreadyExists: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Invitation = ({ token, invitation, isCloud }: Props) => {
|
const Invitation = ({
|
||||||
|
token,
|
||||||
|
invitation,
|
||||||
|
isCloud,
|
||||||
|
userAlreadyExists,
|
||||||
|
}: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data } = api.admin.getUserByToken.useQuery(
|
const { data } = api.user.getUserByToken.useQuery(
|
||||||
{
|
{
|
||||||
token,
|
token,
|
||||||
},
|
},
|
||||||
@@ -90,6 +100,7 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
|
|||||||
|
|
||||||
const form = useForm<Register>({
|
const form = useForm<Register>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
@@ -98,9 +109,9 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.auth?.email) {
|
if (data?.email) {
|
||||||
form.reset({
|
form.reset({
|
||||||
email: data?.auth?.email || "",
|
email: data?.email || "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
});
|
});
|
||||||
@@ -108,20 +119,32 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
|
|||||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
const onSubmit = async (values: Register) => {
|
const onSubmit = async (values: Register) => {
|
||||||
await mutateAsync({
|
try {
|
||||||
id: data?.authId,
|
const { data, error } = await authClient.signUp.email({
|
||||||
password: values.password,
|
email: values.email,
|
||||||
token: token,
|
password: values.password,
|
||||||
})
|
name: values.name,
|
||||||
.then(() => {
|
fetchOptions: {
|
||||||
toast.success("User registered successfuly", {
|
headers: {
|
||||||
description:
|
"x-dokploy-token": token,
|
||||||
"Please check your inbox or spam folder to confirm your account.",
|
},
|
||||||
duration: 100000,
|
},
|
||||||
});
|
});
|
||||||
router.push("/dashboard/projects");
|
|
||||||
})
|
if (error) {
|
||||||
.catch((e) => e);
|
toast.error(error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await authClient.organization.acceptInvitation({
|
||||||
|
invitationId: token,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Account created successfully");
|
||||||
|
router.push("/dashboard/projects");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("An error occurred while creating your account");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -138,114 +161,155 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
|
|||||||
</Link>
|
</Link>
|
||||||
Invitation
|
Invitation
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
{userAlreadyExists ? (
|
||||||
Fill the form below to create your account
|
<div className="flex flex-col gap-4 justify-center items-center">
|
||||||
</CardDescription>
|
<AlertBlock type="success">
|
||||||
<div className="w-full">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="p-3" />
|
<span className="font-medium">Valid Invitation!</span>
|
||||||
|
<span className="text-sm text-green-600 dark:text-green-400">
|
||||||
|
We detected that you already have an account with this
|
||||||
|
email. Please sign in to accept the invitation.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</AlertBlock>
|
||||||
|
|
||||||
{isError && (
|
<Button asChild variant="default" className="w-full">
|
||||||
<div className="mx-5 my-2 flex flex-row items-center gap-2 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
<Link href="/">Sign In</Link>
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
</Button>
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
</div>
|
||||||
{error?.message}
|
) : (
|
||||||
</span>
|
<>
|
||||||
</div>
|
<CardDescription>
|
||||||
)}
|
Fill the form below to create your account
|
||||||
|
</CardDescription>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="p-3" />
|
||||||
|
|
||||||
<CardContent className="p-0">
|
{isError && (
|
||||||
<Form {...form}>
|
<div className="mx-5 my-2 flex flex-row items-center gap-2 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||||
<form
|
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
className="grid gap-4"
|
{error?.message}
|
||||||
>
|
</span>
|
||||||
<div className="space-y-4">
|
</div>
|
||||||
<FormField
|
)}
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Email</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input disabled placeholder="Email" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="Password"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<CardContent className="p-0">
|
||||||
control={form.control}
|
<Form {...form}>
|
||||||
name="confirmPassword"
|
<form
|
||||||
render={({ field }) => (
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
<FormItem>
|
className="grid gap-4"
|
||||||
<FormLabel>Confirm Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="Confirm Password"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
isLoading={form.formState.isSubmitting}
|
|
||||||
className="w-full"
|
|
||||||
>
|
>
|
||||||
Register
|
<div className="space-y-4">
|
||||||
</Button>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter your name"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
disabled
|
||||||
|
placeholder="Email"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="mt-4 text-sm flex flex-row justify-between gap-2 w-full">
|
<FormField
|
||||||
{isCloud && (
|
control={form.control}
|
||||||
<>
|
name="confirmPassword"
|
||||||
<Link
|
render={({ field }) => (
|
||||||
className="hover:underline text-muted-foreground"
|
<FormItem>
|
||||||
href="/"
|
<FormLabel>Confirm Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm Password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
className="w-full"
|
||||||
>
|
>
|
||||||
Login
|
Register
|
||||||
</Link>
|
</Button>
|
||||||
<Link
|
</div>
|
||||||
className="hover:underline text-muted-foreground"
|
|
||||||
href="/send-reset-password"
|
<div className="mt-4 text-sm flex flex-row justify-between gap-2 w-full">
|
||||||
>
|
{isCloud && (
|
||||||
Lost your password?
|
<>
|
||||||
</Link>
|
<Link
|
||||||
</>
|
className="hover:underline text-muted-foreground"
|
||||||
)}
|
href="/"
|
||||||
</div>
|
>
|
||||||
</form>
|
Login
|
||||||
</Form>
|
</Link>
|
||||||
</CardContent>
|
<Link
|
||||||
</div>
|
className="hover:underline text-muted-foreground"
|
||||||
|
href="/send-reset-password"
|
||||||
|
>
|
||||||
|
Lost your password?
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
// http://localhost:3000/invitation?token=CZK4BLrUdMa32RVkAdZiLsPDdvnPiAgZ
|
||||||
|
// /f7af93acc1a99eae864972ab4c92fee089f0d83473d415ede8e821e5dbabe79c
|
||||||
export default Invitation;
|
export default Invitation;
|
||||||
Invitation.getLayout = (page: ReactElement) => {
|
Invitation.getLayout = (page: ReactElement) => {
|
||||||
return <OnboardingLayout>{page}</OnboardingLayout>;
|
return <OnboardingLayout>{page}</OnboardingLayout>;
|
||||||
@@ -254,6 +318,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
const { query } = ctx;
|
const { query } = ctx;
|
||||||
|
|
||||||
const token = query.token;
|
const token = query.token;
|
||||||
|
console.log("query", query);
|
||||||
|
|
||||||
if (typeof token !== "string") {
|
if (typeof token !== "string") {
|
||||||
return {
|
return {
|
||||||
@@ -267,6 +332,17 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
try {
|
try {
|
||||||
const invitation = await getUserByToken(token);
|
const invitation = await getUserByToken(token);
|
||||||
|
|
||||||
|
if (invitation.userAlreadyExists) {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
isCloud: IS_CLOUD,
|
||||||
|
token: token,
|
||||||
|
invitation: invitation,
|
||||||
|
userAlreadyExists: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (invitation.isExpired) {
|
if (invitation.isExpired) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -284,6 +360,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log("error", error);
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -31,6 +32,9 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const registerSchema = z
|
const registerSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
name: z.string().min(1, {
|
||||||
|
message: "Name is required",
|
||||||
|
}),
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, {
|
.min(1, {
|
||||||
@@ -79,9 +83,10 @@ const Register = ({ isCloud }: Props) => {
|
|||||||
|
|
||||||
const form = useForm<Register>({
|
const form = useForm<Register>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
name: "Mauricio Siu",
|
||||||
password: "",
|
email: "user5@yopmail.com",
|
||||||
confirmPassword: "",
|
password: "Password1234",
|
||||||
|
confirmPassword: "Password1234",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(registerSchema),
|
resolver: zodResolver(registerSchema),
|
||||||
});
|
});
|
||||||
@@ -91,19 +96,33 @@ const Register = ({ isCloud }: Props) => {
|
|||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
const onSubmit = async (values: Register) => {
|
const onSubmit = async (values: Register) => {
|
||||||
await mutateAsync({
|
const { data, error } = await authClient.signUp.email({
|
||||||
email: values.email.toLowerCase(),
|
email: values.email,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
})
|
name: values.name,
|
||||||
.then(() => {
|
});
|
||||||
toast.success("User registered successfuly", {
|
|
||||||
duration: 2000,
|
// const { data, error } = await authClient.admin.createUser({
|
||||||
});
|
// name: values.name,
|
||||||
if (!isCloud) {
|
// email: values.email,
|
||||||
router.push("/");
|
// password: values.password,
|
||||||
}
|
// role: "superAdmin",
|
||||||
})
|
// });
|
||||||
.catch((e) => e);
|
|
||||||
|
// 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 (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
@@ -147,6 +166,19 @@ const Register = ({ isCloud }: Props) => {
|
|||||||
className="grid gap-4"
|
className="grid gap-4"
|
||||||
>
|
>
|
||||||
<div className="space-y-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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="email"
|
name="email"
|
||||||
@@ -242,7 +274,7 @@ Register.getLayout = (page: ReactElement) => {
|
|||||||
};
|
};
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
const { user } = await validateRequest(context.req, context.res);
|
const { user } = await validateRequest(context.req);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const Home: NextPage = () => {
|
|||||||
export default Home;
|
export default Home;
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const { req, res } = context;
|
const { req, res } = context;
|
||||||
const { user, session } = await validateRequest(context.req, context.res);
|
const { user, session } = await validateRequest(context.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -53,17 +53,17 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
req: req as any,
|
req: req as any,
|
||||||
res: res as any,
|
res: res as any,
|
||||||
db: null as any,
|
db: null as any,
|
||||||
session: session,
|
session: session as any,
|
||||||
user: user,
|
user: user as any,
|
||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
if (user.rol === "user") {
|
if (user.role === "member") {
|
||||||
const result = await helpers.user.byAuthId.fetch({
|
const userR = await helpers.user.one.fetch({
|
||||||
authId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canAccessToAPI) {
|
if (!userR.canAccessToAPI) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { mongoRouter } from "./routers/mongo";
|
|||||||
import { mountRouter } from "./routers/mount";
|
import { mountRouter } from "./routers/mount";
|
||||||
import { mysqlRouter } from "./routers/mysql";
|
import { mysqlRouter } from "./routers/mysql";
|
||||||
import { notificationRouter } from "./routers/notification";
|
import { notificationRouter } from "./routers/notification";
|
||||||
|
import { organizationRouter } from "./routers/organization";
|
||||||
import { portRouter } from "./routers/port";
|
import { portRouter } from "./routers/port";
|
||||||
import { postgresRouter } from "./routers/postgres";
|
import { postgresRouter } from "./routers/postgres";
|
||||||
import { previewDeploymentRouter } from "./routers/preview-deployment";
|
import { previewDeploymentRouter } from "./routers/preview-deployment";
|
||||||
@@ -33,7 +34,6 @@ import { sshRouter } from "./routers/ssh-key";
|
|||||||
import { stripeRouter } from "./routers/stripe";
|
import { stripeRouter } from "./routers/stripe";
|
||||||
import { swarmRouter } from "./routers/swarm";
|
import { swarmRouter } from "./routers/swarm";
|
||||||
import { userRouter } from "./routers/user";
|
import { userRouter } from "./routers/user";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
*
|
*
|
||||||
@@ -75,6 +75,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
server: serverRouter,
|
server: serverRouter,
|
||||||
stripe: stripeRouter,
|
stripe: stripeRouter,
|
||||||
swarm: swarmRouter,
|
swarm: swarmRouter,
|
||||||
|
organization: organizationRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -1,27 +1,21 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import {
|
import {
|
||||||
apiAssignPermissions,
|
apiAssignPermissions,
|
||||||
apiCreateUserInvitation,
|
apiCreateUserInvitation,
|
||||||
apiFindOneToken,
|
apiFindOneToken,
|
||||||
apiRemoveUser,
|
apiRemoveUser,
|
||||||
apiUpdateAdmin,
|
|
||||||
apiUpdateWebServerMonitoring,
|
apiUpdateWebServerMonitoring,
|
||||||
users,
|
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
createInvitation,
|
createInvitation,
|
||||||
findAdminById,
|
findOrganizationById,
|
||||||
findUserByAuthId,
|
|
||||||
findUserById,
|
findUserById,
|
||||||
getUserByToken,
|
getUserByToken,
|
||||||
removeUserByAuthId,
|
removeUserById,
|
||||||
setupWebMonitoring,
|
setupWebMonitoring,
|
||||||
updateAdmin,
|
updateUser,
|
||||||
updateAdminById,
|
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
adminProcedure,
|
adminProcedure,
|
||||||
@@ -32,30 +26,33 @@ import {
|
|||||||
|
|
||||||
export const adminRouter = createTRPCRouter({
|
export const adminRouter = createTRPCRouter({
|
||||||
one: adminProcedure.query(async ({ ctx }) => {
|
one: adminProcedure.query(async ({ ctx }) => {
|
||||||
const { sshPrivateKey, ...rest } = await findAdminById(ctx.user.adminId);
|
const { sshPrivateKey, ...rest } = await findUserById(ctx.user.id);
|
||||||
return {
|
return {
|
||||||
haveSSH: !!sshPrivateKey,
|
haveSSH: !!sshPrivateKey,
|
||||||
...rest,
|
...rest,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
update: adminProcedure
|
update: adminProcedure
|
||||||
.input(apiUpdateAdmin)
|
.input(
|
||||||
|
z.object({
|
||||||
|
enableDockerCleanup: z.boolean(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "member") {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not allowed to update this admin",
|
message: "You are not allowed to update this admin",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { authId } = await findAdminById(ctx.user.adminId);
|
const user = await findUserById(ctx.user.ownerId);
|
||||||
// @ts-ignore
|
return updateUser(user.id, {});
|
||||||
return updateAdmin(authId, input);
|
|
||||||
}),
|
}),
|
||||||
createUserInvitation: adminProcedure
|
createUserInvitation: adminProcedure
|
||||||
.input(apiCreateUserInvitation)
|
.input(apiCreateUserInvitation)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
await createInvitation(input, ctx.user.adminId);
|
await createInvitation(input, ctx.user.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -69,15 +66,16 @@ export const adminRouter = createTRPCRouter({
|
|||||||
.input(apiRemoveUser)
|
.input(apiRemoveUser)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
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({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not allowed to delete this user",
|
message: "You are not allowed to delete this user",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await removeUserByAuthId(input.authId);
|
|
||||||
|
return await removeUserById(input.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -95,20 +93,22 @@ export const adminRouter = createTRPCRouter({
|
|||||||
.input(apiAssignPermissions)
|
.input(apiAssignPermissions)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
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({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not allowed to assign permissions",
|
message: "You are not allowed to assign permissions",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await db
|
|
||||||
.update(users)
|
await updateUser(user.id, {
|
||||||
.set({
|
...input,
|
||||||
...input,
|
});
|
||||||
})
|
|
||||||
.where(eq(users.userId, input.userId));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -124,15 +124,15 @@ export const adminRouter = createTRPCRouter({
|
|||||||
message: "Feature disabled on cloud",
|
message: "Feature disabled on cloud",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const admin = await findAdminById(ctx.user.adminId);
|
const user = await findUserById(ctx.user.ownerId);
|
||||||
if (admin.adminId !== ctx.user.adminId) {
|
if (user.id !== ctx.user.ownerId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
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: {
|
metricsConfig: {
|
||||||
server: {
|
server: {
|
||||||
type: "Dokploy",
|
type: "Dokploy",
|
||||||
@@ -156,18 +156,19 @@ export const adminRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const currentServer = await setupWebMonitoring(admin.adminId);
|
|
||||||
|
const currentServer = await setupWebMonitoring(user.id);
|
||||||
return currentServer;
|
return currentServer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
|
getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
|
||||||
const admin = await findAdminById(ctx.user.adminId);
|
const user = await findUserById(ctx.user.ownerId);
|
||||||
return {
|
return {
|
||||||
serverIp: admin.serverIp,
|
serverIp: user.serverIp,
|
||||||
enabledFeatures: admin.enablePaidFeatures,
|
enabledFeatures: user.enablePaidFeatures,
|
||||||
metricsConfig: admin?.metricsConfig,
|
metricsConfig: user?.metricsConfig,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiCreateApplication)
|
.input(apiCreateApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "member") {
|
||||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
await checkServiceAccess(ctx.user.id, input.projectId, "create");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
@@ -72,7 +72,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const project = await findProjectById(input.projectId);
|
const project = await findProjectById(input.projectId);
|
||||||
if (project.adminId !== ctx.user.adminId) {
|
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this project",
|
message: "You are not authorized to access this project",
|
||||||
@@ -80,8 +80,8 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
const newApplication = await createApplication(input);
|
const newApplication = await createApplication(input);
|
||||||
|
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "member") {
|
||||||
await addNewService(ctx.user.authId, newApplication.applicationId);
|
await addNewService(ctx.user.id, newApplication.applicationId);
|
||||||
}
|
}
|
||||||
return newApplication;
|
return newApplication;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@@ -98,15 +98,13 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(ctx.user.id, input.applicationId, "access");
|
||||||
ctx.user.authId,
|
|
||||||
input.applicationId,
|
|
||||||
"access",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this application",
|
message: "You are not authorized to access this application",
|
||||||
@@ -119,7 +117,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiReloadApplication)
|
.input(apiReloadApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to reload this application",
|
message: "You are not authorized to reload this application",
|
||||||
@@ -144,16 +144,14 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(ctx.user.id, input.applicationId, "delete");
|
||||||
ctx.user.authId,
|
|
||||||
input.applicationId,
|
|
||||||
"delete",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to delete this application",
|
message: "You are not authorized to delete this application",
|
||||||
@@ -194,7 +192,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const service = await findApplicationById(input.applicationId);
|
const service = await findApplicationById(input.applicationId);
|
||||||
if (service.project.adminId !== ctx.user.adminId) {
|
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to stop this application",
|
message: "You are not authorized to stop this application",
|
||||||
@@ -214,7 +212,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const service = await findApplicationById(input.applicationId);
|
const service = await findApplicationById(input.applicationId);
|
||||||
if (service.project.adminId !== ctx.user.adminId) {
|
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to start this application",
|
message: "You are not authorized to start this application",
|
||||||
@@ -235,7 +233,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to redeploy this application",
|
message: "You are not authorized to redeploy this application",
|
||||||
@@ -268,7 +268,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiSaveEnvironmentVariables)
|
.input(apiSaveEnvironmentVariables)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this environment",
|
message: "You are not authorized to save this environment",
|
||||||
@@ -284,7 +286,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiSaveBuildType)
|
.input(apiSaveBuildType)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this build type",
|
message: "You are not authorized to save this build type",
|
||||||
@@ -305,7 +309,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiSaveGithubProvider)
|
.input(apiSaveGithubProvider)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this github provider",
|
message: "You are not authorized to save this github provider",
|
||||||
@@ -327,7 +333,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiSaveGitlabProvider)
|
.input(apiSaveGitlabProvider)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this gitlab provider",
|
message: "You are not authorized to save this gitlab provider",
|
||||||
@@ -351,7 +359,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiSaveBitbucketProvider)
|
.input(apiSaveBitbucketProvider)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this bitbucket provider",
|
message: "You are not authorized to save this bitbucket provider",
|
||||||
@@ -373,7 +383,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiSaveDockerProvider)
|
.input(apiSaveDockerProvider)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this docker provider",
|
message: "You are not authorized to save this docker provider",
|
||||||
@@ -394,7 +406,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiSaveGitProvider)
|
.input(apiSaveGitProvider)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this git provider",
|
message: "You are not authorized to save this git provider",
|
||||||
@@ -415,7 +429,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to mark this application as running",
|
message: "You are not authorized to mark this application as running",
|
||||||
@@ -427,7 +443,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiUpdateApplication)
|
.input(apiUpdateApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to update this application",
|
message: "You are not authorized to update this application",
|
||||||
@@ -451,7 +469,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to refresh this application",
|
message: "You are not authorized to refresh this application",
|
||||||
@@ -466,7 +486,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to deploy this application",
|
message: "You are not authorized to deploy this application",
|
||||||
@@ -500,7 +522,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to clean this application",
|
message: "You are not authorized to clean this application",
|
||||||
@@ -513,7 +537,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to read this application",
|
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);
|
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({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to deploy this application",
|
message: "You are not authorized to deploy this application",
|
||||||
@@ -590,7 +616,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to update this application",
|
message: "You are not authorized to update this application",
|
||||||
|
|||||||
@@ -1,37 +1,30 @@
|
|||||||
import {
|
import {
|
||||||
apiCreateAdmin,
|
// apiCreateAdmin,
|
||||||
apiCreateUser,
|
// apiCreateUser,
|
||||||
apiFindOneAuth,
|
// apiFindOneAuth,
|
||||||
apiLogin,
|
// apiLogin,
|
||||||
apiUpdateAuth,
|
// apiUpdateAuth,
|
||||||
apiVerify2FA,
|
// apiVerify2FA,
|
||||||
apiVerifyLogin2FA,
|
// apiVerifyLogin2FA,
|
||||||
auth,
|
// auth,
|
||||||
|
member,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { WEBSITE_URL } from "@/server/utils/stripe";
|
import { WEBSITE_URL } from "@/server/utils/stripe";
|
||||||
import {
|
import {
|
||||||
type Auth,
|
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
createAdmin,
|
|
||||||
createUser,
|
|
||||||
findAuthByEmail,
|
|
||||||
findAuthById,
|
findAuthById,
|
||||||
|
findUserById,
|
||||||
generate2FASecret,
|
generate2FASecret,
|
||||||
getUserByToken,
|
getUserByToken,
|
||||||
lucia,
|
|
||||||
luciaToken,
|
|
||||||
removeAdminByAuthId,
|
|
||||||
removeUserByAuthId,
|
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
updateAuthById,
|
|
||||||
validateRequest,
|
validateRequest,
|
||||||
verify2FA,
|
verify2FA,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from "bcrypt";
|
||||||
import { isBefore } from "date-fns";
|
import { isBefore } from "date-fns";
|
||||||
import { eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "../../db";
|
import { db } from "../../db";
|
||||||
@@ -43,81 +36,77 @@ import {
|
|||||||
} from "../trpc";
|
} from "../trpc";
|
||||||
|
|
||||||
export const authRouter = createTRPCRouter({
|
export const authRouter = createTRPCRouter({
|
||||||
createAdmin: publicProcedure
|
createAdmin: publicProcedure.mutation(async ({ ctx, input }) => {
|
||||||
.input(apiCreateAdmin)
|
try {
|
||||||
.mutation(async ({ ctx, input }) => {
|
if (!IS_CLOUD) {
|
||||||
try {
|
const admin = await db.query.admins.findFirst({});
|
||||||
if (!IS_CLOUD) {
|
if (admin) {
|
||||||
const admin = await db.query.admins.findFirst({});
|
|
||||||
if (admin) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Admin already exists",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const newAdmin = await createAdmin(input);
|
|
||||||
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
await sendDiscordNotificationWelcome(newAdmin);
|
|
||||||
await sendVerificationEmail(newAdmin.id);
|
|
||||||
return {
|
|
||||||
status: "success",
|
|
||||||
type: "cloud",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const session = await lucia.createSession(newAdmin.id || "", {});
|
|
||||||
ctx.res.appendHeader(
|
|
||||||
"Set-Cookie",
|
|
||||||
lucia.createSessionCookie(session.id).serialize(),
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
status: "success",
|
|
||||||
type: "selfhosted",
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
// @ts-ignore
|
|
||||||
message: `Error: ${error?.code === "23505" ? "Email already exists" : "Error creating admin"}`,
|
|
||||||
cause: error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
createUser: publicProcedure
|
|
||||||
.input(apiCreateUser)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
try {
|
|
||||||
const token = await getUserByToken(input.token);
|
|
||||||
if (token.isExpired) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
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 {
|
try {
|
||||||
const auth = await findAuthByEmail(input.email);
|
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(
|
// ctx.res.appendHeader(
|
||||||
"Set-Cookie",
|
// "Set-Cookie",
|
||||||
lucia.createSessionCookie(session.id).serialize(),
|
// lucia.createSessionCookie(session.id).serialize(),
|
||||||
);
|
// );
|
||||||
return {
|
return {
|
||||||
is2FAEnabled: false,
|
is2FAEnabled: false,
|
||||||
authId: auth?.id,
|
authId: auth?.id,
|
||||||
@@ -169,47 +158,54 @@ export const authRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
get: protectedProcedure.query(async ({ ctx }) => {
|
get: protectedProcedure.query(async ({ ctx }) => {
|
||||||
const auth = await findAuthById(ctx.user.authId);
|
const memberResult = await db.query.member.findFirst({
|
||||||
return auth;
|
where: and(
|
||||||
|
eq(member.userId, ctx.user.id),
|
||||||
|
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
||||||
|
),
|
||||||
|
with: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return memberResult;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
logout: protectedProcedure.mutation(async ({ ctx }) => {
|
logout: protectedProcedure.mutation(async ({ ctx }) => {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { session } = await validateRequest(req, res);
|
const { session } = await validateRequest(req);
|
||||||
if (!session) return false;
|
if (!session) return false;
|
||||||
|
|
||||||
await lucia.invalidateSession(session.id);
|
// await lucia.invalidateSession(session.id);
|
||||||
res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
|
// res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: protectedProcedure.mutation(async ({ ctx, input }) => {
|
||||||
.input(apiUpdateAuth)
|
const currentAuth = await findAuthByEmail(ctx.user.email);
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const currentAuth = await findAuthByEmail(ctx.user.email);
|
|
||||||
|
|
||||||
if (input.currentPassword || input.password) {
|
if (input.currentPassword || input.password) {
|
||||||
const correctPassword = bcrypt.compareSync(
|
const correctPassword = bcrypt.compareSync(
|
||||||
input.currentPassword || "",
|
input.currentPassword || "",
|
||||||
currentAuth?.password || "",
|
currentAuth?.password || "",
|
||||||
);
|
);
|
||||||
if (!correctPassword) {
|
if (!correctPassword) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Current password is incorrect",
|
message: "Current password is incorrect",
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const auth = await updateAuthById(ctx.user.authId, {
|
}
|
||||||
...(input.email && { email: input.email.toLowerCase() }),
|
// const auth = await updateAuthById(ctx.user.authId, {
|
||||||
...(input.password && {
|
// ...(input.email && { email: input.email.toLowerCase() }),
|
||||||
password: bcrypt.hashSync(input.password, 10),
|
// ...(input.password && {
|
||||||
}),
|
// password: bcrypt.hashSync(input.password, 10),
|
||||||
...(input.image && { image: input.image }),
|
// }),
|
||||||
});
|
// ...(input.image && { image: input.image }),
|
||||||
|
// });
|
||||||
|
|
||||||
return auth;
|
return auth;
|
||||||
}),
|
}),
|
||||||
removeSelfAccount: protectedProcedure
|
removeSelfAccount: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -237,84 +233,81 @@ export const authRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { session } = await validateRequest(req, res);
|
const { session } = await validateRequest(req);
|
||||||
if (!session) return false;
|
if (!session) return false;
|
||||||
|
|
||||||
await lucia.invalidateSession(session.id);
|
// await lucia.invalidateSession(session.id);
|
||||||
res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
|
// res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
|
||||||
|
|
||||||
if (ctx.user.rol === "admin") {
|
// if (ctx.user.rol === "owner") {
|
||||||
await removeAdminByAuthId(ctx.user.authId);
|
// await removeAdminByAuthId(ctx.user.authId);
|
||||||
} else {
|
// } else {
|
||||||
await removeUserByAuthId(ctx.user.authId);
|
// await removeUserByAuthId(ctx.user.authId);
|
||||||
}
|
// }
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
generateToken: protectedProcedure.mutation(async ({ ctx, input }) => {
|
generateToken: protectedProcedure.mutation(async ({ ctx, input }) => {
|
||||||
const auth = await findAuthById(ctx.user.authId);
|
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 updateAuthById(auth.id, {
|
|
||||||
token: session.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// 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;
|
return auth;
|
||||||
}),
|
}),
|
||||||
verifyToken: protectedProcedure.mutation(async () => {
|
verifyToken: protectedProcedure.mutation(async () => {
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => {
|
one: adminProcedure
|
||||||
const auth = await findAuthById(input.id);
|
.input(z.object({ userId: z.string().min(1) }))
|
||||||
return auth;
|
.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 }) => {
|
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);
|
verifyLogin2FA: publicProcedure.mutation(async ({ ctx, input }) => {
|
||||||
await updateAuthById(auth.id, {
|
// const auth = await findAuthById(input.id);
|
||||||
is2FAEnabled: true,
|
|
||||||
secret: input.secret,
|
|
||||||
});
|
|
||||||
return auth;
|
|
||||||
}),
|
|
||||||
|
|
||||||
verifyLogin2FA: publicProcedure
|
// await verify2FA(auth, auth.secret || "", input.pin);
|
||||||
.input(apiVerifyLogin2FA)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const auth = await findAuthById(input.id);
|
|
||||||
|
|
||||||
await verify2FA(auth, auth.secret || "", input.pin);
|
// const session = await lucia.createSession(auth.id, {});
|
||||||
|
|
||||||
const session = await lucia.createSession(auth.id, {});
|
// ctx.res.appendHeader(
|
||||||
|
// "Set-Cookie",
|
||||||
|
// lucia.createSessionCookie(session.id).serialize(),
|
||||||
|
// );
|
||||||
|
|
||||||
ctx.res.appendHeader(
|
return true;
|
||||||
"Set-Cookie",
|
}),
|
||||||
lucia.createSessionCookie(session.id).serialize(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
disable2FA: protectedProcedure.mutation(async ({ ctx }) => {
|
disable2FA: protectedProcedure.mutation(async ({ ctx }) => {
|
||||||
const auth = await findAuthById(ctx.user.authId);
|
// const auth = await findAuthById(ctx.user.authId);
|
||||||
await updateAuthById(auth.id, {
|
// await updateAuthById(auth.id, {
|
||||||
is2FAEnabled: false,
|
// is2FAEnabled: false,
|
||||||
secret: null,
|
// secret: null,
|
||||||
});
|
// });
|
||||||
return auth;
|
// return auth;
|
||||||
}),
|
}),
|
||||||
sendResetPasswordEmail: publicProcedure
|
sendResetPasswordEmail: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
apiUpdateBitbucket,
|
apiUpdateBitbucket,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
|
||||||
createBitbucket,
|
createBitbucket,
|
||||||
findBitbucketById,
|
findBitbucketById,
|
||||||
getBitbucketBranches,
|
getBitbucketBranches,
|
||||||
@@ -23,7 +22,7 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
.input(apiCreateBitbucket)
|
.input(apiCreateBitbucket)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createBitbucket(input, ctx.user.adminId);
|
return await createBitbucket(input, ctx.session.activeOrganizationId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -37,10 +36,9 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||||
if (
|
if (
|
||||||
IS_CLOUD &&
|
bitbucketProvider.gitProvider.organizationId !==
|
||||||
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
|
ctx.session.activeOrganizationId
|
||||||
) {
|
) {
|
||||||
//TODO: Remove this line when the cloud version is ready
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not allowed to access this bitbucket provider",
|
message: "You are not allowed to access this bitbucket provider",
|
||||||
@@ -58,12 +56,11 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (IS_CLOUD) {
|
result = result.filter(
|
||||||
// TODO: mAyBe a rEfaCtoR 🤫
|
(provider) =>
|
||||||
result = result.filter(
|
provider.gitProvider.organizationId ===
|
||||||
(provider) => provider.gitProvider.adminId === ctx.user.adminId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -72,10 +69,9 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||||
if (
|
if (
|
||||||
IS_CLOUD &&
|
bitbucketProvider.gitProvider.organizationId !==
|
||||||
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
|
ctx.session.activeOrganizationId
|
||||||
) {
|
) {
|
||||||
//TODO: Remove this line when the cloud version is ready
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not allowed to access this bitbucket provider",
|
message: "You are not allowed to access this bitbucket provider",
|
||||||
@@ -90,10 +86,9 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
input.bitbucketId || "",
|
input.bitbucketId || "",
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
IS_CLOUD &&
|
bitbucketProvider.gitProvider.organizationId !==
|
||||||
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
|
ctx.session.activeOrganizationId
|
||||||
) {
|
) {
|
||||||
//TODO: Remove this line when the cloud version is ready
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not allowed to access this bitbucket provider",
|
message: "You are not allowed to access this bitbucket provider",
|
||||||
@@ -107,10 +102,9 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||||
if (
|
if (
|
||||||
IS_CLOUD &&
|
bitbucketProvider.gitProvider.organizationId !==
|
||||||
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
|
ctx.session.activeOrganizationId
|
||||||
) {
|
) {
|
||||||
//TODO: Remove this line when the cloud version is ready
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not allowed to access this bitbucket provider",
|
message: "You are not allowed to access this bitbucket provider",
|
||||||
@@ -131,10 +125,9 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||||
if (
|
if (
|
||||||
IS_CLOUD &&
|
bitbucketProvider.gitProvider.organizationId !==
|
||||||
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
|
ctx.session.activeOrganizationId
|
||||||
) {
|
) {
|
||||||
//TODO: Remove this line when the cloud version is ready
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not allowed to access this bitbucket provider",
|
message: "You are not allowed to access this bitbucket provider",
|
||||||
@@ -142,7 +135,7 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return await updateBitbucket(input.bitbucketId, {
|
return await updateBitbucket(input.bitbucketId, {
|
||||||
...input,
|
...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",
|
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
|
one: adminProcedure
|
||||||
.input(apiFindCertificate)
|
.input(apiFindCertificate)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const certificates = await findCertificateById(input.certificateId);
|
const certificates = await findCertificateById(input.certificateId);
|
||||||
if (IS_CLOUD && certificates.adminId !== ctx.user.adminId) {
|
if (certificates.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not allowed to access this certificate",
|
message: "You are not allowed to access this certificate",
|
||||||
@@ -44,7 +44,7 @@ export const certificateRouter = createTRPCRouter({
|
|||||||
.input(apiFindCertificate)
|
.input(apiFindCertificate)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const certificates = await findCertificateById(input.certificateId);
|
const certificates = await findCertificateById(input.certificateId);
|
||||||
if (IS_CLOUD && certificates.adminId !== ctx.user.adminId) {
|
if (certificates.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not allowed to delete this certificate",
|
message: "You are not allowed to delete this certificate",
|
||||||
@@ -55,8 +55,7 @@ export const certificateRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
all: adminProcedure.query(async ({ ctx }) => {
|
all: adminProcedure.query(async ({ ctx }) => {
|
||||||
return await db.query.certificates.findMany({
|
return await db.query.certificates.findMany({
|
||||||
// TODO: Remove this line when the cloud version is ready
|
where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
|
||||||
...(IS_CLOUD && { where: eq(certificates.adminId, ctx.user.adminId) }),
|
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ import {
|
|||||||
createComposeByTemplate,
|
createComposeByTemplate,
|
||||||
createDomain,
|
createDomain,
|
||||||
createMount,
|
createMount,
|
||||||
findAdminById,
|
|
||||||
findComposeById,
|
findComposeById,
|
||||||
findDomainsByComposeId,
|
findDomainsByComposeId,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
findServerById,
|
findServerById,
|
||||||
|
findUserById,
|
||||||
loadServices,
|
loadServices,
|
||||||
randomizeComposeFile,
|
randomizeComposeFile,
|
||||||
randomizeIsolatedDeploymentComposeFile,
|
randomizeIsolatedDeploymentComposeFile,
|
||||||
@@ -60,8 +60,8 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiCreateCompose)
|
.input(apiCreateCompose)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "member") {
|
||||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
await checkServiceAccess(ctx.user.id, input.projectId, "create");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
@@ -71,7 +71,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const project = await findProjectById(input.projectId);
|
const project = await findProjectById(input.projectId);
|
||||||
if (project.adminId !== ctx.user.adminId) {
|
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this project",
|
message: "You are not authorized to access this project",
|
||||||
@@ -79,8 +79,8 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
const newService = await createCompose(input);
|
const newService = await createCompose(input);
|
||||||
|
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "member") {
|
||||||
await addNewService(ctx.user.authId, newService.composeId);
|
await addNewService(ctx.user.id, newService.composeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newService;
|
return newService;
|
||||||
@@ -92,12 +92,12 @@ export const composeRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "member") {
|
||||||
await checkServiceAccess(ctx.user.authId, input.composeId, "access");
|
await checkServiceAccess(ctx.user.id, input.composeId, "access");
|
||||||
}
|
}
|
||||||
|
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this compose",
|
message: "You are not authorized to access this compose",
|
||||||
@@ -110,7 +110,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiUpdateCompose)
|
.input(apiUpdateCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to update this compose",
|
message: "You are not authorized to update this compose",
|
||||||
@@ -121,12 +121,15 @@ export const composeRouter = createTRPCRouter({
|
|||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(apiDeleteCompose)
|
.input(apiDeleteCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "member") {
|
||||||
await checkServiceAccess(ctx.user.authId, input.composeId, "delete");
|
await checkServiceAccess(ctx.user.id, input.composeId, "delete");
|
||||||
}
|
}
|
||||||
const composeResult = await findComposeById(input.composeId);
|
const composeResult = await findComposeById(input.composeId);
|
||||||
|
|
||||||
if (composeResult.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
composeResult.project.organizationId !==
|
||||||
|
ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to delete this compose",
|
message: "You are not authorized to delete this compose",
|
||||||
@@ -157,7 +160,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to clean this compose",
|
message: "You are not authorized to clean this compose",
|
||||||
@@ -170,7 +173,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiFetchServices)
|
.input(apiFetchServices)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to load this compose",
|
message: "You are not authorized to load this compose",
|
||||||
@@ -184,7 +187,9 @@ export const composeRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
|
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
compose.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to fetch this compose",
|
message: "You are not authorized to fetch this compose",
|
||||||
@@ -209,7 +214,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiRandomizeCompose)
|
.input(apiRandomizeCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to randomize this compose",
|
message: "You are not authorized to randomize this compose",
|
||||||
@@ -221,7 +226,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiRandomizeCompose)
|
.input(apiRandomizeCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to randomize this compose",
|
message: "You are not authorized to randomize this compose",
|
||||||
@@ -236,7 +241,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to get this compose",
|
message: "You are not authorized to get this compose",
|
||||||
@@ -254,7 +259,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
|
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to deploy this compose",
|
message: "You are not authorized to deploy this compose",
|
||||||
@@ -287,7 +292,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to redeploy this compose",
|
message: "You are not authorized to redeploy this compose",
|
||||||
@@ -319,7 +324,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to stop this compose",
|
message: "You are not authorized to stop this compose",
|
||||||
@@ -333,7 +338,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to stop this compose",
|
message: "You are not authorized to stop this compose",
|
||||||
@@ -348,7 +353,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
|
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to get this compose",
|
message: "You are not authorized to get this compose",
|
||||||
@@ -361,7 +366,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to refresh this compose",
|
message: "You are not authorized to refresh this compose",
|
||||||
@@ -375,8 +380,8 @@ export const composeRouter = createTRPCRouter({
|
|||||||
deployTemplate: protectedProcedure
|
deployTemplate: protectedProcedure
|
||||||
.input(apiCreateComposeByTemplate)
|
.input(apiCreateComposeByTemplate)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "member") {
|
||||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
await checkServiceAccess(ctx.user.id, input.projectId, "create");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
@@ -390,7 +395,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const generate = await loadTemplateModule(input.id as TemplatesKeys);
|
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";
|
let serverIp = admin.serverIp || "127.0.0.1";
|
||||||
|
|
||||||
const project = await findProjectById(input.projectId);
|
const project = await findProjectById(input.projectId);
|
||||||
@@ -418,8 +423,8 @@ export const composeRouter = createTRPCRouter({
|
|||||||
isolatedDeployment: true,
|
isolatedDeployment: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "member") {
|
||||||
await addNewService(ctx.user.authId, compose.composeId);
|
await addNewService(ctx.user.id, compose.composeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounts && mounts?.length > 0) {
|
if (mounts && mounts?.length > 0) {
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ export const deploymentRouter = createTRPCRouter({
|
|||||||
.input(apiFindAllByApplication)
|
.input(apiFindAllByApplication)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (application.project.adminId !== ctx.user.adminId) {
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this application",
|
message: "You are not authorized to access this application",
|
||||||
@@ -32,7 +34,7 @@ export const deploymentRouter = createTRPCRouter({
|
|||||||
.input(apiFindAllByCompose)
|
.input(apiFindAllByCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this compose",
|
message: "You are not authorized to access this compose",
|
||||||
@@ -44,7 +46,7 @@ export const deploymentRouter = createTRPCRouter({
|
|||||||
.input(apiFindAllByServer)
|
.input(apiFindAllByServer)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const server = await findServerById(input.serverId);
|
const server = await findServerById(input.serverId);
|
||||||
if (server.adminId !== ctx.user.adminId) {
|
if (server.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this server",
|
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