mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
35 Commits
feat/bette
...
v0.18.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56af89468a | ||
|
|
b1502f5f82 | ||
|
|
c78b8cfead | ||
|
|
48c4ec55f9 | ||
|
|
2ebb02dc20 | ||
|
|
9ca3ab3845 | ||
|
|
0baf9d8962 | ||
|
|
71c609df0e | ||
|
|
450302b2c2 | ||
|
|
71a007a4b3 | ||
|
|
871931b460 | ||
|
|
3a7bb5016c | ||
|
|
23b59076b8 | ||
|
|
2ddfc83f36 | ||
|
|
ba54124a56 | ||
|
|
64bdf07dbd | ||
|
|
8366219266 | ||
|
|
f38fb96eaf | ||
|
|
3b1ade804f | ||
|
|
fd69b45e5e | ||
|
|
12034e460e | ||
|
|
2b5a1d90b0 | ||
|
|
e7195c8acf | ||
|
|
9ace0f38cd | ||
|
|
796e50ed5f | ||
|
|
ed54df9bd2 | ||
|
|
bf6c2698d4 | ||
|
|
bbdda014d8 | ||
|
|
d9c83b7010 | ||
|
|
209029214e | ||
|
|
9de3bf3c32 | ||
|
|
2c65fc22b0 | ||
|
|
6688b14753 | ||
|
|
6ea2a82bb0 | ||
|
|
e01347673e |
@@ -45,7 +45,7 @@ const baseApp: ApplicationNested = {
|
||||
previewWildcard: "",
|
||||
project: {
|
||||
env: "",
|
||||
organizationId: "",
|
||||
adminId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
|
||||
@@ -5,7 +5,7 @@ vi.mock("node:fs", () => ({
|
||||
default: fs,
|
||||
}));
|
||||
|
||||
import type { FileConfig, User } from "@dokploy/server";
|
||||
import type { Admin, FileConfig } from "@dokploy/server";
|
||||
import {
|
||||
createDefaultServerTraefikConfig,
|
||||
loadOrCreateConfig,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
|
||||
const baseAdmin: User = {
|
||||
const baseAdmin: Admin = {
|
||||
enablePaidFeatures: false,
|
||||
metricsConfig: {
|
||||
containers: {
|
||||
@@ -40,7 +40,9 @@ const baseAdmin: User = {
|
||||
cleanupCacheApplications: false,
|
||||
cleanupCacheOnCompose: false,
|
||||
cleanupCacheOnPreviews: false,
|
||||
createdAt: new Date(),
|
||||
createdAt: "",
|
||||
authId: "",
|
||||
adminId: "string",
|
||||
serverIp: null,
|
||||
certificateType: "none",
|
||||
host: null,
|
||||
@@ -51,31 +53,6 @@ const baseAdmin: User = {
|
||||
serversQuantity: 0,
|
||||
stripeCustomerId: "",
|
||||
stripeSubscriptionId: "",
|
||||
accessedProjects: [],
|
||||
accessedServices: [],
|
||||
banExpires: new Date(),
|
||||
banned: true,
|
||||
banReason: "",
|
||||
canAccessToAPI: false,
|
||||
canCreateProjects: false,
|
||||
canDeleteProjects: false,
|
||||
canDeleteServices: false,
|
||||
canAccessToDocker: false,
|
||||
canAccessToSSHKeys: false,
|
||||
canCreateServices: false,
|
||||
canAccessToTraefikFiles: false,
|
||||
canAccessToGitProviders: false,
|
||||
email: "",
|
||||
expirationDate: "",
|
||||
id: "",
|
||||
isRegistered: false,
|
||||
name: "",
|
||||
createdAt2: new Date().toISOString(),
|
||||
emailVerified: false,
|
||||
image: "",
|
||||
token: "",
|
||||
updatedAt: new Date(),
|
||||
twoFactorEnabled: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -26,7 +26,7 @@ const baseApp: ApplicationNested = {
|
||||
previewWildcard: "",
|
||||
project: {
|
||||
env: "",
|
||||
organizationId: "",
|
||||
adminId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -96,7 +98,10 @@ export const ComposeFreeMonitoring = ({
|
||||
key={container.containerId}
|
||||
value={container.name}
|
||||
>
|
||||
{container.name} ({container.containerId}) {container.state}
|
||||
{container.name} ({container.containerId}){" "}
|
||||
<Badge variant={badgeStateColor(container.state)}>
|
||||
{container.state}
|
||||
</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -102,7 +104,9 @@ export const ComposePaidMonitoring = ({
|
||||
value={container.name}
|
||||
>
|
||||
{container.name} ({container.containerId}){" "}
|
||||
{container.state}
|
||||
<Badge variant={badgeStateColor(container.state)}>
|
||||
{container.state}
|
||||
</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||
|
||||
@@ -79,7 +79,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
|
||||
data,
|
||||
isLoading,
|
||||
error: queryError,
|
||||
} = api.user.getContainerMetrics.useQuery(
|
||||
} = api.admin.getContainerMetrics.useQuery(
|
||||
{
|
||||
url: baseUrl,
|
||||
token,
|
||||
|
||||
@@ -73,7 +73,7 @@ export const ShowPaidMonitoring = ({
|
||||
data,
|
||||
isLoading,
|
||||
error: queryError,
|
||||
} = api.user.getServerMetrics.useQuery(
|
||||
} = api.admin.getServerMetrics.useQuery(
|
||||
{
|
||||
url: BASE_URL,
|
||||
token,
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
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,7 +21,6 @@ import {
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon, SquarePen } from "lucide-react";
|
||||
@@ -98,18 +97,6 @@ export const HandleProject = ({ projectId }: Props) => {
|
||||
);
|
||||
});
|
||||
};
|
||||
// useEffect(() => {
|
||||
// const getUsers = async () => {
|
||||
// const users = await authClient.admin.listUsers({
|
||||
// query: {
|
||||
// limit: 100,
|
||||
// },
|
||||
// });
|
||||
// console.log(users);
|
||||
// };
|
||||
|
||||
// getUsers();
|
||||
// });
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
|
||||
@@ -51,7 +51,15 @@ import { ProjectEnvironment } from "./project-environment";
|
||||
export const ShowProjects = () => {
|
||||
const utils = api.useUtils();
|
||||
const { data, isLoading } = api.project.all.useQuery();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { mutateAsync } = api.project.remove.useMutation();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
@@ -83,7 +91,7 @@ export const ShowProjects = () => {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
{(auth?.role === "owner" || auth?.user?.canCreateProjects) && (
|
||||
{(auth?.rol === "admin" || user?.canCreateProjects) && (
|
||||
<div className="">
|
||||
<HandleProject />
|
||||
</div>
|
||||
@@ -285,8 +293,8 @@ export const ShowProjects = () => {
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteProjects) && (
|
||||
{(auth?.rol === "admin" ||
|
||||
user?.canDeleteProjects) && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@/components/ui/command";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import {
|
||||
type Services,
|
||||
extractServices,
|
||||
@@ -36,10 +35,8 @@ export const SearchCommand = () => {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [search, setSearch] = React.useState("");
|
||||
const { data: session } = authClient.useSession();
|
||||
const { data } = api.project.all.useQuery(undefined, {
|
||||
enabled: !!session,
|
||||
});
|
||||
|
||||
const { data } = api.project.all.useQuery();
|
||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -39,7 +39,7 @@ export const calculatePrice = (count: number, isAnnual = false) => {
|
||||
};
|
||||
export const ShowBilling = () => {
|
||||
const { data: servers } = api.server.all.useQuery(undefined);
|
||||
const { data: admin } = api.user.get.useQuery();
|
||||
const { data: admin } = api.admin.one.useQuery();
|
||||
const { data, isLoading } = api.stripe.getProducts.useQuery();
|
||||
const { mutateAsync: createCheckoutSession } =
|
||||
api.stripe.createCheckoutSession.useMutation();
|
||||
@@ -70,7 +70,7 @@ export const ShowBilling = () => {
|
||||
return isAnnual ? interval === "year" : interval === "month";
|
||||
});
|
||||
|
||||
const maxServers = admin?.user.serversQuantity ?? 1;
|
||||
const maxServers = admin?.serversQuantity ?? 1;
|
||||
const percentage = ((servers?.length ?? 0) / maxServers) * 100;
|
||||
const safePercentage = Math.min(percentage, 100);
|
||||
|
||||
@@ -98,17 +98,17 @@ export const ShowBilling = () => {
|
||||
<TabsTrigger value="annual">Annual</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{admin?.user.stripeSubscriptionId && (
|
||||
{admin?.stripeSubscriptionId && (
|
||||
<div className="space-y-2 flex flex-col">
|
||||
<h3 className="text-lg font-medium">Servers Plan</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You have {servers?.length} server on your plan of{" "}
|
||||
{admin?.user.serversQuantity} servers
|
||||
{admin?.serversQuantity} servers
|
||||
</p>
|
||||
<div>
|
||||
<Progress value={safePercentage} className="max-w-lg" />
|
||||
</div>
|
||||
{admin && admin.user.serversQuantity! <= servers?.length! && (
|
||||
{admin && admin.serversQuantity! <= servers?.length! && (
|
||||
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
|
||||
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
@@ -279,7 +279,7 @@ export const ShowBilling = () => {
|
||||
"flex flex-row items-center gap-2 mt-4",
|
||||
)}
|
||||
>
|
||||
{admin?.user.stripeCustomerId && (
|
||||
{admin?.stripeCustomerId && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
|
||||
@@ -10,12 +10,12 @@ import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const ShowWelcomeDokploy = () => {
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||
|
||||
if (!isCloud || data?.role !== "admin") {
|
||||
if (!isCloud || data?.rol !== "admin") {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ export const ShowWelcomeDokploy = () => {
|
||||
!isLoading &&
|
||||
isCloud &&
|
||||
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
|
||||
data?.role === "owner"
|
||||
data?.rol === "admin"
|
||||
) {
|
||||
setOpen(true);
|
||||
}
|
||||
}, [isCloud, isLoading]);
|
||||
|
||||
const handleClose = (isOpen: boolean) => {
|
||||
if (data?.role === "owner") {
|
||||
if (data?.rol === "admin") {
|
||||
setOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
localStorage.setItem("hasSeenCloudWelcomeModal", "true"); // Establece el flag al cerrar el modal
|
||||
|
||||
@@ -86,7 +86,6 @@ export const AddCertificate = () => {
|
||||
privateKey: data.privateKey,
|
||||
autoRenew: data.autoRenew,
|
||||
serverId: data.serverId,
|
||||
organizationId: "",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Certificate Created");
|
||||
|
||||
@@ -53,7 +53,7 @@ export const AddBitbucketProvider = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const url = useUrl();
|
||||
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const router = useRouter();
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
|
||||
@@ -10,15 +10,13 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { format } from "date-fns";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const AddGithubProvider = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const [manifest, setManifest] = useState("");
|
||||
const [isOrganization, setIsOrganization] = useState(false);
|
||||
const [organizationName, setOrganization] = useState("");
|
||||
@@ -27,7 +25,7 @@ export const AddGithubProvider = () => {
|
||||
const url = document.location.origin;
|
||||
const manifest = JSON.stringify(
|
||||
{
|
||||
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}`,
|
||||
redirect_url: `${origin}/api/providers/github/setup?authId=${data?.id}`,
|
||||
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
|
||||
url: origin,
|
||||
hook_attributes: {
|
||||
@@ -95,8 +93,8 @@ export const AddGithubProvider = () => {
|
||||
<form
|
||||
action={
|
||||
isOrganization
|
||||
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${activeOrganization?.id}`
|
||||
: `https://github.com/settings/apps/new?state=gh_init:${activeOrganization?.id}`
|
||||
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${data?.id}`
|
||||
: `https://github.com/settings/apps/new?state=gh_init:${data?.id}`
|
||||
}
|
||||
method="post"
|
||||
>
|
||||
|
||||
@@ -55,7 +55,7 @@ export const AddGitlabProvider = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const url = useUrl();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { mutateAsync, error, isError } = api.gitlab.create.useMutation();
|
||||
const webhookUrl = `${url}/api/providers/gitlab/callback`;
|
||||
|
||||
|
||||
@@ -1,134 +1,52 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const PasswordSchema = z.object({
|
||||
password: z.string().min(8, {
|
||||
message: "Password is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type PasswordForm = z.infer<typeof PasswordSchema>;
|
||||
|
||||
export const Disable2FA = () => {
|
||||
const utils = api.useUtils();
|
||||
const [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);
|
||||
}
|
||||
};
|
||||
|
||||
const { mutateAsync, isLoading } = api.auth.disable2FA.useMutation();
|
||||
return (
|
||||
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive">Disable 2FA</Button>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
Disable 2FA
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently disable
|
||||
Two-Factor Authentication for your account.
|
||||
This action cannot be undone. This will permanently delete the 2FA
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="space-y-4"
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync()
|
||||
.then(() => {
|
||||
utils.auth.get.invalidate();
|
||||
toast.success("2FA Disabled");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error disabling 2FA");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Enter your password to disable 2FA
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
form.reset();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="destructive" isLoading={isLoading}>
|
||||
Disable 2FA
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
|
||||
@@ -17,315 +17,144 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/components/ui/input-otp";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Fingerprint, QrCode } from "lucide-react";
|
||||
import QRCode from "qrcode";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AlertTriangle, Fingerprint } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const PasswordSchema = z.object({
|
||||
password: z.string().min(8, {
|
||||
message: "Password is required",
|
||||
}),
|
||||
});
|
||||
|
||||
const PinSchema = z.object({
|
||||
const Enable2FASchema = z.object({
|
||||
pin: z.string().min(6, {
|
||||
message: "Pin is required",
|
||||
}),
|
||||
});
|
||||
|
||||
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;
|
||||
};
|
||||
type Enable2FA = z.infer<typeof Enable2FASchema>;
|
||||
|
||||
export const Enable2FA = () => {
|
||||
const utils = api.useUtils();
|
||||
const { data: session } = authClient.useSession();
|
||||
const [data, setData] = useState<TwoFactorSetupData | null>(null);
|
||||
const [backupCodes, setBackupCodes] = useState<string[]>([]);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [step, setStep] = useState<"password" | "verify">("password");
|
||||
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
||||
|
||||
const handlePasswordSubmit = async (formData: PasswordForm) => {
|
||||
setIsPasswordLoading(true);
|
||||
try {
|
||||
const { data: enableData } = await authClient.twoFactor.enable({
|
||||
password: formData.password,
|
||||
});
|
||||
|
||||
if (!enableData) {
|
||||
throw new Error("No data received from server");
|
||||
}
|
||||
|
||||
if (enableData.backupCodes) {
|
||||
setBackupCodes(enableData.backupCodes);
|
||||
}
|
||||
|
||||
if (enableData.totpURI) {
|
||||
const qrCodeUrl = await QRCode.toDataURL(enableData.totpURI);
|
||||
|
||||
setData({
|
||||
qrCodeUrl,
|
||||
secret: enableData.totpURI.split("secret=")[1]?.split("&")[0] || "",
|
||||
totpURI: enableData.totpURI,
|
||||
});
|
||||
|
||||
setStep("verify");
|
||||
toast.success("Scan the QR code with your authenticator app");
|
||||
} else {
|
||||
throw new Error("No TOTP URI received from server");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Error setting up 2FA",
|
||||
);
|
||||
passwordForm.setError("password", {
|
||||
message: "Error verifying password",
|
||||
});
|
||||
} finally {
|
||||
setIsPasswordLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifySubmit = async (formData: PinForm) => {
|
||||
try {
|
||||
const result = await authClient.twoFactor.verifyTotp({
|
||||
code: formData.pin,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
|
||||
pinForm.setError("pin", {
|
||||
message: "Invalid code. Please try again.",
|
||||
});
|
||||
toast.error("Invalid verification code");
|
||||
return;
|
||||
}
|
||||
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
if (!result.data) {
|
||||
throw new Error("No response received from server");
|
||||
}
|
||||
|
||||
toast.success("2FA configured successfully");
|
||||
utils.auth.get.invalidate();
|
||||
setIsDialogOpen(false);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
const errorMessage =
|
||||
error.message === "Failed to fetch"
|
||||
? "Connection error. Please check your internet connection."
|
||||
: error.message;
|
||||
|
||||
pinForm.setError("pin", {
|
||||
message: errorMessage,
|
||||
});
|
||||
toast.error(errorMessage);
|
||||
} else {
|
||||
pinForm.setError("pin", {
|
||||
message: "Error verifying code",
|
||||
});
|
||||
toast.error("Error verifying 2FA code");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const passwordForm = useForm<PasswordForm>({
|
||||
resolver: zodResolver(PasswordSchema),
|
||||
defaultValues: {
|
||||
password: "",
|
||||
},
|
||||
const { data } = api.auth.generate2FASecret.useQuery(undefined, {
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const pinForm = useForm<PinForm>({
|
||||
resolver: zodResolver(PinSchema),
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.auth.verify2FASetup.useMutation();
|
||||
|
||||
const form = useForm<Enable2FA>({
|
||||
defaultValues: {
|
||||
pin: "",
|
||||
},
|
||||
resolver: zodResolver(Enable2FASchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDialogOpen) {
|
||||
setStep("password");
|
||||
setData(null);
|
||||
setBackupCodes([]);
|
||||
passwordForm.reset();
|
||||
pinForm.reset();
|
||||
}
|
||||
}, [isDialogOpen, passwordForm, pinForm]);
|
||||
form.reset({
|
||||
pin: "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (formData: Enable2FA) => {
|
||||
await mutateAsync({
|
||||
pin: formData.pin,
|
||||
secret: data?.secret || "",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("2FA Verified");
|
||||
utils.auth.get.invalidate();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error verifying the 2FA");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<Fingerprint className="size-4 text-muted-foreground" />
|
||||
Enable 2FA
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-xl">
|
||||
<DialogContent className="max-h-screen max-sm:overflow-y-auto sm:max-w-xl ">
|
||||
<DialogHeader>
|
||||
<DialogTitle>2FA Setup</DialogTitle>
|
||||
<DialogDescription>
|
||||
{step === "password"
|
||||
? "Enter your password to begin 2FA setup"
|
||||
: "Scan the QR code and verify with your authenticator app"}
|
||||
</DialogDescription>
|
||||
<DialogDescription>Add a 2FA to your account</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{step === "password" ? (
|
||||
<Form {...passwordForm}>
|
||||
<form
|
||||
id="password-form"
|
||||
onSubmit={passwordForm.handleSubmit(handlePasswordSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={passwordForm.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Enter your password to enable 2FA
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
isLoading={isPasswordLoading}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
) : (
|
||||
<Form {...pinForm}>
|
||||
<form
|
||||
id="pin-form"
|
||||
onSubmit={pinForm.handleSubmit(handleVerifySubmit)}
|
||||
className="space-y-6"
|
||||
>
|
||||
<div className="flex flex-col gap-6 justify-center items-center">
|
||||
{data?.qrCodeUrl ? (
|
||||
<>
|
||||
<div className="flex flex-col items-center gap-4 p-6 border rounded-lg">
|
||||
<QrCode className="size-5 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">
|
||||
Scan this QR code with your authenticator app
|
||||
</span>
|
||||
<img
|
||||
src={data.qrCodeUrl}
|
||||
alt="2FA QR Code"
|
||||
className="rounded-lg w-48 h-48"
|
||||
/>
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Can't scan the QR code?
|
||||
</span>
|
||||
<span className="text-xs font-mono bg-muted p-2 rounded">
|
||||
{data.secret}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{backupCodes && backupCodes.length > 0 && (
|
||||
<div className="w-full space-y-3 border rounded-lg p-4">
|
||||
<h4 className="font-medium">Backup Codes</h4>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{backupCodes.map((code, index) => (
|
||||
<code
|
||||
key={index}
|
||||
className="bg-muted p-2 rounded text-sm font-mono"
|
||||
>
|
||||
{code}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Save these backup codes in a secure place. You can use
|
||||
them to access your account if you lose access to your
|
||||
authenticator device.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center w-full h-48 bg-muted rounded-lg">
|
||||
<QrCode className="size-8 text-muted-foreground animate-pulse" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={pinForm.control}
|
||||
name="pin"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-center items-center">
|
||||
<FormLabel>Verification Code</FormLabel>
|
||||
<FormControl>
|
||||
<InputOTP maxLength={6} {...field}>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Enter the 6-digit code from your authenticator app
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
isLoading={isPasswordLoading}
|
||||
>
|
||||
Enable 2FA
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
{isError && (
|
||||
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-2FA"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid sm:grid-cols-2 w-full gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-4 justify-center items-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{data?.qrCodeUrl ? "Scan the QR code to add 2FA" : ""}
|
||||
</span>
|
||||
<img
|
||||
src={data?.qrCodeUrl}
|
||||
alt="qrCode"
|
||||
className="rounded-lg w-fit"
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm text-muted-foreground text-center">
|
||||
{data?.secret ? `Secret: ${data?.secret}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pin"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-center max-sm:items-center">
|
||||
<FormLabel>Pin</FormLabel>
|
||||
<FormControl>
|
||||
<InputOTP maxLength={6} {...field}>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</FormControl>
|
||||
<FormDescription className="max-md:text-center">
|
||||
Please enter the 6 digits code provided by your
|
||||
authenticator app.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-add-2FA"
|
||||
type="submit"
|
||||
>
|
||||
Submit 2FA
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const GenerateToken = () => {
|
||||
const { data, refetch } = api.user.get.useQuery();
|
||||
const { data, refetch } = api.auth.get.useQuery();
|
||||
|
||||
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
|
||||
api.auth.generateToken.useMutation();
|
||||
@@ -51,7 +51,7 @@ export const GenerateToken = () => {
|
||||
<Label>Token</Label>
|
||||
<ToggleVisibilityInput
|
||||
placeholder="Token"
|
||||
value={data?.user?.token || ""}
|
||||
value={data?.token || ""}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { generateSHA256Hash } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -56,10 +54,7 @@ const randomImages = [
|
||||
];
|
||||
|
||||
export const ProfileForm = () => {
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync: disable2FA, isLoading: isDisabling } =
|
||||
api.auth.disable2FA.useMutation();
|
||||
const { data, refetch, isLoading } = api.user.get.useQuery();
|
||||
const { data, refetch, isLoading } = api.auth.get.useQuery();
|
||||
const {
|
||||
mutateAsync,
|
||||
isLoading: isUpdating,
|
||||
@@ -78,9 +73,9 @@ export const ProfileForm = () => {
|
||||
|
||||
const form = useForm<Profile>({
|
||||
defaultValues: {
|
||||
email: data?.user?.email || "",
|
||||
email: data?.email || "",
|
||||
password: "",
|
||||
image: data?.user?.image || "",
|
||||
image: data?.image || "",
|
||||
currentPassword: "",
|
||||
},
|
||||
resolver: zodResolver(profileSchema),
|
||||
@@ -89,14 +84,14 @@ export const ProfileForm = () => {
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
email: data?.user?.email || "",
|
||||
email: data?.email || "",
|
||||
password: "",
|
||||
image: data?.user?.image || "",
|
||||
image: data?.image || "",
|
||||
currentPassword: "",
|
||||
});
|
||||
|
||||
if (data.user.email) {
|
||||
generateSHA256Hash(data.user.email).then((hash) => {
|
||||
if (data.email) {
|
||||
generateSHA256Hash(data.email).then((hash) => {
|
||||
setGravatarHash(hash);
|
||||
});
|
||||
}
|
||||
@@ -135,7 +130,7 @@ export const ProfileForm = () => {
|
||||
{t("settings.profile.description")}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{!data?.user.twoFactorEnabled ? <Enable2FA /> : <Disable2FA />}
|
||||
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
|
||||
@@ -35,7 +35,7 @@ const profileSchema = z.object({
|
||||
type Profile = z.infer<typeof profileSchema>;
|
||||
|
||||
export const RemoveSelfAccount = () => {
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.auth.removeSelfAccount.useMutation();
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
@@ -7,7 +7,7 @@ interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||
const { data, refetch } = api.user.get.useQuery(undefined, {
|
||||
const { data, refetch } = api.admin.one.useQuery(undefined, {
|
||||
enabled: !serverId,
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||
},
|
||||
);
|
||||
|
||||
const enabled = data?.user.enableDockerCleanup || server?.enableDockerCleanup;
|
||||
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
|
||||
|
||||
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
||||
enabled: !!serverId,
|
||||
},
|
||||
)
|
||||
: api.user.get.useQuery();
|
||||
: api.admin.one.useQuery();
|
||||
|
||||
const url = useUrl();
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ export const CreateSSHKey = () => {
|
||||
description: "Used on Dokploy Cloud",
|
||||
privateKey: keys.privateKey,
|
||||
publicKey: keys.publicKey,
|
||||
organizationId: "",
|
||||
});
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
|
||||
@@ -78,7 +78,6 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
||||
const onSubmit = async (data: SSHKey) => {
|
||||
await mutateAsync({
|
||||
...data,
|
||||
organizationId: "",
|
||||
sshKeyId: sshKeyId || "",
|
||||
})
|
||||
.then(async () => {
|
||||
|
||||
@@ -52,7 +52,7 @@ interface Props {
|
||||
export const AddUserPermissions = ({ userId }: Props) => {
|
||||
const { data: projects } = api.project.all.useQuery();
|
||||
|
||||
const { data, refetch } = api.auth.one.useQuery(
|
||||
const { data, refetch } = api.user.byUserId.useQuery(
|
||||
{
|
||||
userId,
|
||||
},
|
||||
@@ -62,7 +62,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
);
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.user.assignPermissions.useMutation();
|
||||
api.admin.assignPermissions.useMutation();
|
||||
|
||||
const form = useForm<AddPermissions>({
|
||||
defaultValues: {
|
||||
@@ -92,7 +92,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
|
||||
const onSubmit = async (data: AddPermissions) => {
|
||||
await mutateAsync({
|
||||
id: userId,
|
||||
userId,
|
||||
canCreateServices: data.canCreateServices,
|
||||
canCreateProjects: data.canCreateProjects,
|
||||
canDeleteServices: data.canDeleteServices,
|
||||
|
||||
@@ -19,14 +19,6 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
@@ -35,70 +27,62 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const addInvitation = z.object({
|
||||
const addUser = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, "Email is required")
|
||||
.email({ message: "Invalid email" }),
|
||||
role: z.enum(["member", "admin"]),
|
||||
});
|
||||
|
||||
type AddInvitation = z.infer<typeof addInvitation>;
|
||||
type AddUser = z.infer<typeof addUser>;
|
||||
|
||||
export const AddInvitation = () => {
|
||||
export const AddUser = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
|
||||
const form = useForm<AddInvitation>({
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.admin.createUserInvitation.useMutation();
|
||||
|
||||
const form = useForm<AddUser>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
role: "member",
|
||||
},
|
||||
resolver: zodResolver(addInvitation),
|
||||
resolver: zodResolver(addUser),
|
||||
});
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||
|
||||
const onSubmit = async (data: AddInvitation) => {
|
||||
setIsLoading(true);
|
||||
const result = await authClient.organization.inviteMember({
|
||||
const onSubmit = async (data: AddUser) => {
|
||||
await mutateAsync({
|
||||
email: data.email.toLowerCase(),
|
||||
role: data.role,
|
||||
organizationId: activeOrganization?.id,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message || "");
|
||||
} else {
|
||||
toast.success("Invitation created");
|
||||
setError(null);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
utils.organization.allInvitations.invalidate();
|
||||
setIsLoading(false);
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Invitation created");
|
||||
await utils.user.all.invalidate();
|
||||
setOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error creating the invitation");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button>
|
||||
<PlusIcon className="h-4 w-4" /> Add Invitation
|
||||
<PlusIcon className="h-4 w-4" /> Add User
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Invitation</DialogTitle>
|
||||
<DialogTitle>Add User</DialogTitle>
|
||||
<DialogDescription>Invite a new user</DialogDescription>
|
||||
</DialogHeader>
|
||||
{error && <AlertBlock type="error">{error}</AlertBlock>}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-invitation"
|
||||
id="hook-form-add-user"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
>
|
||||
@@ -120,39 +104,10 @@ export const AddInvitation = () => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="role"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a role" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Select the role for the new user
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<DialogFooter className="flex w-full flex-row">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-add-invitation"
|
||||
form="hook-form-add-user"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
@@ -1,208 +0,0 @@
|
||||
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,4 +1,3 @@
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -24,19 +23,22 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { format } from "date-fns";
|
||||
import { MoreHorizontal, Users } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { AddUserPermissions } from "./add-permissions";
|
||||
import { AddUser } from "./add-user";
|
||||
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export const ShowUsers = () => {
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data, isLoading, refetch } = api.user.all.useQuery();
|
||||
const { mutateAsync, isLoading: isRemoving } = api.user.remove.useMutation();
|
||||
const { mutateAsync, isLoading: isRemoving } =
|
||||
api.admin.removeUser.useMutation();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -65,6 +67,7 @@ export const ShowUsers = () => {
|
||||
<span className="text-base text-muted-foreground">
|
||||
Invite users to your Dokploy account
|
||||
</span>
|
||||
<AddUser />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
@@ -73,41 +76,43 @@ export const ShowUsers = () => {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">Email</TableHead>
|
||||
<TableHead className="text-center">Role</TableHead>
|
||||
<TableHead className="text-center">Status</TableHead>
|
||||
<TableHead className="text-center">2FA</TableHead>
|
||||
|
||||
<TableHead className="text-center">
|
||||
Created At
|
||||
Expiration
|
||||
</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.map((member) => {
|
||||
{data?.map((user) => {
|
||||
return (
|
||||
<TableRow key={member.id}>
|
||||
<TableRow key={user.userId}>
|
||||
<TableCell className="w-[100px]">
|
||||
{member.user.email}
|
||||
{user.auth.email}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge
|
||||
variant={
|
||||
member.role === "owner"
|
||||
? "default"
|
||||
: "secondary"
|
||||
user.isRegistered ? "default" : "secondary"
|
||||
}
|
||||
>
|
||||
{member.role}
|
||||
{user.isRegistered
|
||||
? "Registered"
|
||||
: "Not Registered"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{member.user.twoFactorEnabled
|
||||
? "Enabled"
|
||||
: "Disabled"}
|
||||
{user.auth.is2FAEnabled
|
||||
? "2FA Enabled"
|
||||
: "2FA Not Enabled"}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{format(new Date(member.createdAt), "PPpp")}
|
||||
{format(
|
||||
new Date(user.expirationDate),
|
||||
"PPpp",
|
||||
)}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -126,63 +131,56 @@ export const ShowUsers = () => {
|
||||
<DropdownMenuLabel>
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
{!user.isRegistered && (
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={(e) => {
|
||||
copy(
|
||||
`${origin}/invitation?token=${user.token}`,
|
||||
);
|
||||
toast.success(
|
||||
"Invitation Copied to clipboard",
|
||||
);
|
||||
}}
|
||||
>
|
||||
Copy Invitation
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{member.role !== "owner" && (
|
||||
{user.isRegistered && (
|
||||
<AddUserPermissions
|
||||
userId={member.user.id}
|
||||
userId={user.userId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{member.role !== "owner" && (
|
||||
<DialogAction
|
||||
title="Delete User"
|
||||
description="Are you sure you want to delete this user?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
if (isCloud) {
|
||||
const { error } =
|
||||
await authClient.organization.removeMember(
|
||||
{
|
||||
memberIdOrEmail: member.id,
|
||||
},
|
||||
);
|
||||
|
||||
if (!error) {
|
||||
toast.success(
|
||||
"User deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
} else {
|
||||
toast.error(
|
||||
"Error deleting user",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await mutateAsync({
|
||||
userId: member.user.id,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"User deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting destination",
|
||||
);
|
||||
});
|
||||
}
|
||||
}}
|
||||
<DialogAction
|
||||
title="Delete User"
|
||||
description="Are you sure you want to delete this user?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
authId: user.authId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"User deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting destination",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Delete User
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
)}
|
||||
Delete User
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
@@ -191,6 +189,10 @@ export const ShowUsers = () => {
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<AddUser />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -52,7 +52,7 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
|
||||
|
||||
export const WebDomain = () => {
|
||||
const { t } = useTranslation("settings");
|
||||
const { data, refetch } = api.user.get.useQuery();
|
||||
const { data: user, refetch } = api.admin.one.useQuery();
|
||||
const { mutateAsync, isLoading } =
|
||||
api.settings.assignDomainServer.useMutation();
|
||||
|
||||
@@ -65,14 +65,14 @@ export const WebDomain = () => {
|
||||
resolver: zodResolver(addServerDomain),
|
||||
});
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (user) {
|
||||
form.reset({
|
||||
domain: data?.user?.host || "",
|
||||
certificateType: data?.user?.certificateType,
|
||||
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
|
||||
domain: user?.host || "",
|
||||
certificateType: user?.certificateType,
|
||||
letsEncryptEmail: user?.letsEncryptEmail || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
}, [form, form.reset, user]);
|
||||
|
||||
const onSubmit = async (data: AddServerDomain) => {
|
||||
await mutateAsync({
|
||||
|
||||
@@ -21,7 +21,7 @@ interface Props {
|
||||
}
|
||||
export const WebServer = ({ className }: Props) => {
|
||||
const { t } = useTranslation("settings");
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data } = api.admin.one.useQuery();
|
||||
|
||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||
|
||||
@@ -58,7 +58,7 @@ export const WebServer = ({ className }: Props) => {
|
||||
|
||||
<div className="flex items-center flex-wrap justify-between gap-4">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Server IP: {data?.user.serverIp}
|
||||
Server IP: {data?.serverIp}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Version: {dokployVersion}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -46,15 +47,15 @@ interface Props {
|
||||
export const UpdateServerIp = ({ children, serverId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data } = api.admin.one.useQuery();
|
||||
const { data: ip } = api.server.publicIp.useQuery();
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.user.update.useMutation();
|
||||
api.admin.update.useMutation();
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
serverIp: data?.user.serverIp || "",
|
||||
serverIp: data?.serverIp || "",
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
@@ -62,7 +63,7 @@ export const UpdateServerIp = ({ children, serverId }: Props) => {
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
serverIp: data.user.serverIp || "",
|
||||
serverIp: data.serverIp || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
import {
|
||||
Activity,
|
||||
AudioWaveform,
|
||||
BarChartHorizontalBigIcon,
|
||||
Bell,
|
||||
BlocksIcon,
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
Boxes,
|
||||
ChevronRight,
|
||||
CircleHelp,
|
||||
Command,
|
||||
CreditCard,
|
||||
Database,
|
||||
Folder,
|
||||
@@ -18,13 +16,11 @@ import {
|
||||
GitBranch,
|
||||
HeartIcon,
|
||||
KeyRound,
|
||||
Loader2,
|
||||
type LucideIcon,
|
||||
Package,
|
||||
PieChart,
|
||||
Server,
|
||||
ShieldCheck,
|
||||
Trash2,
|
||||
User,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
@@ -78,6 +74,7 @@ import { UserNav } from "./user-nav";
|
||||
|
||||
// The types of the queries we are going to use
|
||||
type AuthQueryOutput = inferRouterOutputs<AppRouter>["auth"]["get"];
|
||||
type UserQueryOutput = inferRouterOutputs<AppRouter>["user"]["byAuthId"];
|
||||
|
||||
type SingleNavItem = {
|
||||
isSingle?: true;
|
||||
@@ -86,6 +83,7 @@ type SingleNavItem = {
|
||||
icon?: LucideIcon;
|
||||
isEnabled?: (opts: {
|
||||
auth?: AuthQueryOutput;
|
||||
user?: UserQueryOutput;
|
||||
isCloud: boolean;
|
||||
}) => boolean;
|
||||
};
|
||||
@@ -103,6 +101,7 @@ type NavItem =
|
||||
items: SingleNavItem[];
|
||||
isEnabled?: (opts: {
|
||||
auth?: AuthQueryOutput;
|
||||
user?: UserQueryOutput;
|
||||
isCloud: boolean;
|
||||
}) => boolean;
|
||||
};
|
||||
@@ -115,6 +114,7 @@ type ExternalLink = {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
isEnabled?: (opts: {
|
||||
auth?: AuthQueryOutput;
|
||||
user?: UserQueryOutput;
|
||||
isCloud: boolean;
|
||||
}) => boolean;
|
||||
};
|
||||
@@ -145,7 +145,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/monitoring",
|
||||
icon: BarChartHorizontalBigIcon,
|
||||
// Only enabled in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) => !isCloud,
|
||||
isEnabled: ({ auth, user, isCloud }) => !isCloud,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -153,9 +153,9 @@ const MENU: Menu = {
|
||||
url: "/dashboard/traefik",
|
||||
icon: GalleryVerticalEnd,
|
||||
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
isEnabled: ({ auth, user, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" || auth?.user?.canAccessToTraefikFiles) &&
|
||||
(auth?.rol === "admin" || user?.canAccessToTraefikFiles) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
@@ -165,11 +165,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/docker",
|
||||
icon: BlocksIcon,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
|
||||
!isCloud
|
||||
),
|
||||
isEnabled: ({ auth, user, isCloud }) =>
|
||||
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -177,11 +174,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/swarm",
|
||||
icon: PieChart,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
|
||||
!isCloud
|
||||
),
|
||||
isEnabled: ({ auth, user, isCloud }) =>
|
||||
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -189,11 +183,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/requests",
|
||||
icon: Forward,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
|
||||
!isCloud
|
||||
),
|
||||
isEnabled: ({ auth, user, isCloud }) =>
|
||||
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
||||
},
|
||||
|
||||
// Legacy unused menu, adjusted to the new structure
|
||||
@@ -260,7 +251,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/server",
|
||||
icon: Activity,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||
isEnabled: ({ auth, user, isCloud }) =>
|
||||
!!(auth?.rol === "admin" && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -274,7 +266,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/servers",
|
||||
icon: Server,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -282,7 +274,7 @@ const MENU: Menu = {
|
||||
icon: Users,
|
||||
url: "/dashboard/settings/users",
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -290,8 +282,8 @@ const MENU: Menu = {
|
||||
icon: KeyRound,
|
||||
url: "/dashboard/settings/ssh-keys",
|
||||
// Only enabled for admins and users with access to SSH keys
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.user?.canAccessToSSHKeys),
|
||||
isEnabled: ({ auth, user }) =>
|
||||
!!(auth?.rol === "admin" || user?.canAccessToSSHKeys),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -299,8 +291,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/git-providers",
|
||||
icon: GitBranch,
|
||||
// Only enabled for admins and users with access to Git providers
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.user?.canAccessToGitProviders),
|
||||
isEnabled: ({ auth, user }) =>
|
||||
!!(auth?.rol === "admin" || user?.canAccessToGitProviders),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -308,7 +300,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/registry",
|
||||
icon: Package,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -316,7 +308,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/destinations",
|
||||
icon: Database,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||
},
|
||||
|
||||
{
|
||||
@@ -325,7 +317,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/certificates",
|
||||
icon: ShieldCheck,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -333,7 +325,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/cluster",
|
||||
icon: Boxes,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||
isEnabled: ({ auth, user, isCloud }) =>
|
||||
!!(auth?.rol === "admin" && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -341,7 +334,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/notifications",
|
||||
icon: Bell,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -349,7 +342,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/billing",
|
||||
icon: CreditCard,
|
||||
// Only enabled for admins in cloud environments
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
||||
isEnabled: ({ auth, user, isCloud }) =>
|
||||
!!(auth?.rol === "admin" && isCloud),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -385,6 +379,7 @@ const MENU: Menu = {
|
||||
*/
|
||||
function createMenuForAuthUser(opts: {
|
||||
auth?: AuthQueryOutput;
|
||||
user?: UserQueryOutput;
|
||||
isCloud: boolean;
|
||||
}): Menu {
|
||||
return {
|
||||
@@ -395,6 +390,7 @@ function createMenuForAuthUser(opts: {
|
||||
? true
|
||||
: item.isEnabled({
|
||||
auth: opts.auth,
|
||||
user: opts.user,
|
||||
isCloud: opts.isCloud,
|
||||
}),
|
||||
),
|
||||
@@ -405,6 +401,7 @@ function createMenuForAuthUser(opts: {
|
||||
? true
|
||||
: item.isEnabled({
|
||||
auth: opts.auth,
|
||||
user: opts.user,
|
||||
isCloud: opts.isCloud,
|
||||
}),
|
||||
),
|
||||
@@ -415,6 +412,7 @@ function createMenuForAuthUser(opts: {
|
||||
? true
|
||||
: item.isEnabled({
|
||||
auth: opts.auth,
|
||||
user: opts.user,
|
||||
isCloud: opts.isCloud,
|
||||
}),
|
||||
),
|
||||
@@ -482,218 +480,37 @@ interface Props {
|
||||
function LogoWrapper() {
|
||||
return <SidebarLogo />;
|
||||
}
|
||||
import { ChevronsUpDown, Plus } from "lucide-react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { toast } from "sonner";
|
||||
import { AddOrganization } from "../dashboard/organization/handle-organization";
|
||||
import { DialogAction } from "../shared/dialog-action";
|
||||
import { Button } from "../ui/button";
|
||||
const data = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg",
|
||||
},
|
||||
teams: [
|
||||
{
|
||||
name: "Acme Inc",
|
||||
logo: GalleryVerticalEnd,
|
||||
plan: "Enterprise",
|
||||
},
|
||||
{
|
||||
name: "Acme Corp.",
|
||||
logo: AudioWaveform,
|
||||
plan: "Startup",
|
||||
},
|
||||
{
|
||||
name: "Evil Corp.",
|
||||
logo: Command,
|
||||
plan: "Free",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function SidebarLogo() {
|
||||
const { state } = useSidebar();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: user } = api.user.get.useQuery();
|
||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||
const { data: session } = authClient.useSession();
|
||||
const {
|
||||
data: organizations,
|
||||
refetch,
|
||||
isLoading,
|
||||
} = api.organization.all.useQuery();
|
||||
const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
|
||||
api.organization.delete.useMutation();
|
||||
const { isMobile } = useSidebar();
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
|
||||
const [activeTeam, setActiveTeam] = useState<
|
||||
typeof activeOrganization | null
|
||||
>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeOrganization) {
|
||||
setActiveTeam(activeOrganization);
|
||||
}
|
||||
}, [activeOrganization]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[5vh] pt-4">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
{/* <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex aspect-square items-center justify-center rounded-lg transition-all",
|
||||
state === "collapsed" ? "size-6" : "size-10",
|
||||
)}
|
||||
>
|
||||
<Logo
|
||||
className={cn(
|
||||
"transition-all",
|
||||
state === "collapsed" ? "size-6" : "size-10",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{activeTeam?.name}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
align="start"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
Organizations
|
||||
</DropdownMenuLabel>
|
||||
{organizations?.map((org, index) => (
|
||||
<div className="flex flex-row justify-between" key={org.name}>
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await authClient.organization.setActive({
|
||||
organizationId: org.id,
|
||||
});
|
||||
|
||||
window.location.reload();
|
||||
}}
|
||||
className="w-full gap-2 p-2"
|
||||
>
|
||||
<div className="flex size-6 items-center justify-center rounded-sm border">
|
||||
<Logo
|
||||
className={cn(
|
||||
"transition-all",
|
||||
state === "collapsed" ? "size-6" : "size-10",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{org.name}
|
||||
</DropdownMenuItem>
|
||||
{(org.ownerId === session?.user?.id || isCloud) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<AddOrganization organizationId={org.id} />
|
||||
<DialogAction
|
||||
title="Delete Organization"
|
||||
description="Are you sure you want to delete this organization?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await deleteOrganization({
|
||||
organizationId: org.id,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success(
|
||||
"Organization deleted successfully",
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(
|
||||
error?.message ||
|
||||
"Error deleting organization",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!isCloud && user?.role === "owner" && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<AddOrganization />
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)}
|
||||
|
||||
{/* <Link
|
||||
href="/dashboard/projects"
|
||||
className="flex items-center gap-2 p-1 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]/35 rounded-lg "
|
||||
<Link
|
||||
href="/dashboard/projects"
|
||||
className="flex items-center gap-2 p-1 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]/35 rounded-lg "
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex aspect-square items-center justify-center rounded-lg transition-all",
|
||||
state === "collapsed" ? "size-6" : "size-10",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
<Logo
|
||||
className={cn(
|
||||
"flex aspect-square items-center justify-center rounded-lg transition-all",
|
||||
"transition-all",
|
||||
state === "collapsed" ? "size-6" : "size-10",
|
||||
)}
|
||||
>
|
||||
<Logo
|
||||
className={cn(
|
||||
"transition-all",
|
||||
state === "collapsed" ? "size-6" : "size-10",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-left text-sm leading-tight group-data-[state=open]/collapsible:rotate-90">
|
||||
<p className="truncate font-semibold">Dokploy</p>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{dokployVersion}
|
||||
</p>
|
||||
</div>
|
||||
</Link> */}
|
||||
</>
|
||||
<div className="text-left text-sm leading-tight group-data-[state=open]/collapsible:rotate-90">
|
||||
<p className="truncate font-semibold">Dokploy</p>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{dokployVersion}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -714,7 +531,15 @@ export default function Page({ children }: Props) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const currentPath = router.pathname;
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
|
||||
const includesProjects = pathname?.includes("/dashboard/project");
|
||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||
@@ -723,7 +548,7 @@ export default function Page({ children }: Props) {
|
||||
home: filteredHome,
|
||||
settings: filteredSettings,
|
||||
help,
|
||||
} = createMenuForAuthUser({ auth, isCloud: !!isCloud });
|
||||
} = createMenuForAuthUser({ auth, user, isCloud: !!isCloud });
|
||||
|
||||
const activeItem = findActiveNavItem(
|
||||
[...filteredHome, ...filteredSettings],
|
||||
@@ -732,7 +557,7 @@ export default function Page({ children }: Props) {
|
||||
|
||||
// const showProjectsButton =
|
||||
// currentPath === "/dashboard/projects" &&
|
||||
// (auth?.rol === "owner" || user?.canCreateProjects);
|
||||
// (auth?.rol === "admin" || user?.canCreateProjects);
|
||||
|
||||
return (
|
||||
<SidebarProvider
|
||||
@@ -752,12 +577,12 @@ export default function Page({ children }: Props) {
|
||||
>
|
||||
<Sidebar collapsible="icon" variant="floating">
|
||||
<SidebarHeader>
|
||||
{/* <SidebarMenuButton
|
||||
<SidebarMenuButton
|
||||
className="group-data-[collapsible=icon]:!p-0"
|
||||
size="lg"
|
||||
> */}
|
||||
<LogoWrapper />
|
||||
{/* </SidebarMenuButton> */}
|
||||
>
|
||||
<LogoWrapper />
|
||||
</SidebarMenuButton>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
@@ -958,7 +783,7 @@ export default function Page({ children }: Props) {
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
{!isCloud && auth?.role === "owner" && (
|
||||
{!isCloud && auth?.rol === "admin" && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<UpdateServerButton />
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { Languages } from "@/lib/languages";
|
||||
import { api } from "@/utils/api";
|
||||
import useLocale from "@/utils/hooks/use-locale";
|
||||
@@ -30,11 +29,18 @@ const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
||||
|
||||
export const UserNav = () => {
|
||||
const router = useRouter();
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: data?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!data?.id && data?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { locale, setLocale } = useLocale();
|
||||
// const { mutateAsync } = api.auth.logout.useMutation();
|
||||
const { mutateAsync } = api.auth.logout.useMutation();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@@ -44,15 +50,12 @@ export const UserNav = () => {
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage
|
||||
src={data?.user?.image || ""}
|
||||
alt={data?.user?.image || ""}
|
||||
/>
|
||||
<AvatarImage src={data?.image || ""} alt={data?.image || ""} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">Account</span>
|
||||
<span className="truncate text-xs">{data?.user?.email}</span>
|
||||
<span className="truncate text-xs">{data?.email}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
@@ -67,7 +70,7 @@ export const UserNav = () => {
|
||||
<DropdownMenuLabel className="flex flex-col">
|
||||
My Account
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
{data?.user?.email}
|
||||
{data?.email}
|
||||
</span>
|
||||
</DropdownMenuLabel>
|
||||
<ModeToggle />
|
||||
@@ -92,8 +95,7 @@ export const UserNav = () => {
|
||||
>
|
||||
Monitoring
|
||||
</DropdownMenuItem>
|
||||
{(data?.role === "owner" ||
|
||||
data?.user?.canAccessToTraefikFiles) && (
|
||||
{(data?.rol === "admin" || user?.canAccessToTraefikFiles) && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -103,7 +105,7 @@ export const UserNav = () => {
|
||||
Traefik
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{(data?.role === "owner" || data?.user?.canAccessToDocker) && (
|
||||
{(data?.rol === "admin" || user?.canAccessToDocker) && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -116,7 +118,7 @@ export const UserNav = () => {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{data?.role === "owner" && (
|
||||
{data?.rol === "admin" && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -137,7 +139,7 @@ export const UserNav = () => {
|
||||
>
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
{data?.role === "owner" && (
|
||||
{data?.rol === "admin" && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -148,7 +150,7 @@ export const UserNav = () => {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{data?.role === "owner" && (
|
||||
{data?.rol === "admin" && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -161,7 +163,7 @@ export const UserNav = () => {
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
{isCloud && data?.role === "owner" && (
|
||||
{isCloud && data?.rol === "admin" && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -176,12 +178,9 @@ export const UserNav = () => {
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={async () => {
|
||||
await authClient.signOut().then(() => {
|
||||
await mutateAsync().then(() => {
|
||||
router.push("/");
|
||||
});
|
||||
// await mutateAsync().then(() => {
|
||||
// router.push("/");
|
||||
// });
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
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
|
||||
@@ -1,211 +0,0 @@
|
||||
-- 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";
|
||||
@@ -1,32 +0,0 @@
|
||||
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;
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE "user_temp" ALTER COLUMN "token" SET DEFAULT '';--> statement-breakpoint
|
||||
ALTER TABLE "user_temp" ADD COLUMN "created_at" timestamp DEFAULT now();
|
||||
@@ -1,16 +0,0 @@
|
||||
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;
|
||||
@@ -1,142 +0,0 @@
|
||||
-- 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");
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
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";
|
||||
@@ -1,6 +0,0 @@
|
||||
--> 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";
|
||||
@@ -1,18 +0,0 @@
|
||||
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;
|
||||
@@ -1,3 +0,0 @@
|
||||
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,76 +463,6 @@
|
||||
"when": 1739087857244,
|
||||
"tag": "0065_daily_zaladane",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 66,
|
||||
"version": "7",
|
||||
"when": 1739426913392,
|
||||
"tag": "0066_yielding_echo",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 67,
|
||||
"version": "7",
|
||||
"when": 1739427057545,
|
||||
"tag": "0067_migrate-data",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 68,
|
||||
"version": "7",
|
||||
"when": 1739428942964,
|
||||
"tag": "0068_sour_professor_monster",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 69,
|
||||
"version": "7",
|
||||
"when": 1739664410814,
|
||||
"tag": "0069_broad_ken_ellis",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 70,
|
||||
"version": "7",
|
||||
"when": 1739671869809,
|
||||
"tag": "0070_nervous_vivisector",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 71,
|
||||
"version": "7",
|
||||
"when": 1739671878698,
|
||||
"tag": "0071_migrate-data-projects",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 72,
|
||||
"version": "7",
|
||||
"when": 1739672367223,
|
||||
"tag": "0072_lazy_pixie",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 73,
|
||||
"version": "7",
|
||||
"when": 1739740193879,
|
||||
"tag": "0073_polite_miss_america",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 74,
|
||||
"version": "7",
|
||||
"when": 1739773539709,
|
||||
"tag": "0074_lowly_jack_power",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 75,
|
||||
"version": "7",
|
||||
"when": 1739781534192,
|
||||
"tag": "0075_heavy_metal_master",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
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()],
|
||||
});
|
||||
@@ -1,150 +0,0 @@
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||
import { nanoid } from "nanoid";
|
||||
import postgres from "postgres";
|
||||
import * as schema from "./server/db/schema";
|
||||
|
||||
const connectionString = process.env.DATABASE_URL!;
|
||||
|
||||
const sql = postgres(connectionString, { max: 1 });
|
||||
const db = drizzle(sql, {
|
||||
schema,
|
||||
});
|
||||
|
||||
await db
|
||||
.transaction(async (db) => {
|
||||
const admins = await db.query.admins.findMany({
|
||||
with: {
|
||||
auth: true,
|
||||
users: {
|
||||
with: {
|
||||
auth: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
for (const admin of admins) {
|
||||
const user = await db
|
||||
.insert(schema.users_temp)
|
||||
.values({
|
||||
id: admin.adminId,
|
||||
email: admin.auth.email,
|
||||
token: admin.auth.token || "",
|
||||
emailVerified: true,
|
||||
updatedAt: new Date(),
|
||||
role: "admin",
|
||||
serverIp: admin.serverIp,
|
||||
image: admin.auth.image,
|
||||
certificateType: admin.certificateType,
|
||||
host: admin.host,
|
||||
letsEncryptEmail: admin.letsEncryptEmail,
|
||||
sshPrivateKey: admin.sshPrivateKey,
|
||||
enableDockerCleanup: admin.enableDockerCleanup,
|
||||
enableLogRotation: admin.enableLogRotation,
|
||||
enablePaidFeatures: admin.enablePaidFeatures,
|
||||
metricsConfig: admin.metricsConfig,
|
||||
cleanupCacheApplications: admin.cleanupCacheApplications,
|
||||
cleanupCacheOnPreviews: admin.cleanupCacheOnPreviews,
|
||||
cleanupCacheOnCompose: admin.cleanupCacheOnCompose,
|
||||
stripeCustomerId: admin.stripeCustomerId,
|
||||
stripeSubscriptionId: admin.stripeSubscriptionId,
|
||||
serversQuantity: admin.serversQuantity,
|
||||
})
|
||||
.returning()
|
||||
.then((user) => user[0]);
|
||||
|
||||
await db.insert(schema.account).values({
|
||||
providerId: "credential",
|
||||
userId: user?.id || "",
|
||||
password: admin.auth.password,
|
||||
is2FAEnabled: admin.auth.is2FAEnabled || false,
|
||||
createdAt: new Date(admin.auth.createdAt) || new Date(),
|
||||
updatedAt: new Date(admin.auth.createdAt) || new Date(),
|
||||
});
|
||||
|
||||
const organization = await db
|
||||
.insert(schema.organization)
|
||||
.values({
|
||||
name: "My Organization",
|
||||
slug: nanoid(),
|
||||
ownerId: user?.id || "",
|
||||
createdAt: new Date(admin.createdAt) || new Date(),
|
||||
})
|
||||
.returning()
|
||||
.then((organization) => organization[0]);
|
||||
|
||||
for (const member of admin.users) {
|
||||
const userTemp = await db
|
||||
.insert(schema.users_temp)
|
||||
.values({
|
||||
id: member.userId,
|
||||
email: member.auth.email,
|
||||
token: member.token || "",
|
||||
emailVerified: true,
|
||||
updatedAt: new Date(admin.createdAt) || new Date(),
|
||||
role: "user",
|
||||
image: member.auth.image,
|
||||
createdAt: admin.createdAt,
|
||||
canAccessToAPI: member.canAccessToAPI || false,
|
||||
canAccessToDocker: member.canAccessToDocker || false,
|
||||
canAccessToGitProviders: member.canAccessToGitProviders || false,
|
||||
canAccessToSSHKeys: member.canAccessToSSHKeys || false,
|
||||
canAccessToTraefikFiles: member.canAccessToTraefikFiles || false,
|
||||
canCreateProjects: member.canCreateProjects || false,
|
||||
canCreateServices: member.canCreateServices || false,
|
||||
canDeleteProjects: member.canDeleteProjects || false,
|
||||
canDeleteServices: member.canDeleteServices || false,
|
||||
accessedProjects: member.accessedProjects || [],
|
||||
accessedServices: member.accessedServices || [],
|
||||
})
|
||||
.returning()
|
||||
.then((userTemp) => userTemp[0]);
|
||||
|
||||
await db.insert(schema.account).values({
|
||||
providerId: "credential",
|
||||
userId: member?.userId || "",
|
||||
password: member.auth.password,
|
||||
is2FAEnabled: member.auth.is2FAEnabled || false,
|
||||
createdAt: new Date(member.auth.createdAt) || new Date(),
|
||||
updatedAt: new Date(member.auth.createdAt) || new Date(),
|
||||
});
|
||||
|
||||
await db.insert(schema.member).values({
|
||||
organizationId: organization?.id || "",
|
||||
userId: userTemp?.id || "",
|
||||
role: "admin",
|
||||
createdAt: new Date(member.createdAt) || new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Migration finished");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
await db
|
||||
.transaction(async (db) => {
|
||||
const projects = await db.query.projects.findMany({
|
||||
with: {
|
||||
user: {
|
||||
with: {
|
||||
organizations: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
for (const project of projects) {
|
||||
const user = await db.update(schema.projects).set({
|
||||
organizationId: project.user.organizations[0]?.id || "",
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Migration finished");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.18.2",
|
||||
"version": "v0.18.4",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -16,7 +16,6 @@
|
||||
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
||||
"migration:run": "tsx -r dotenv/config migration.ts",
|
||||
"manual-migration:run": "tsx -r dotenv/config migrate.ts",
|
||||
"migration:up": "drizzle-kit up --config ./server/db/drizzle.config.ts",
|
||||
"migration:drop": "drizzle-kit drop --config ./server/db/drizzle.config.ts",
|
||||
"db:push": "drizzle-kit push --config ./server/db/drizzle.config.ts",
|
||||
@@ -36,7 +35,6 @@
|
||||
"test": "vitest --config __test__/vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-auth": "1.1.16",
|
||||
"bl": "6.0.11",
|
||||
"rotating-file-stream": "3.2.3",
|
||||
"qrcode": "^1.5.3",
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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;
|
||||
@@ -1,7 +0,0 @@
|
||||
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,12 +1,10 @@
|
||||
import { db } from "@/server/db";
|
||||
import { github } from "@/server/db/schema";
|
||||
import {
|
||||
auth,
|
||||
createGithub,
|
||||
findAdminByAuthId,
|
||||
findAuthById,
|
||||
findUserByAuthId,
|
||||
findUserById,
|
||||
} from "@dokploy/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
@@ -30,7 +28,7 @@ export default async function handler(
|
||||
return res.status(400).json({ error: "Missing code parameter" });
|
||||
}
|
||||
const [action, value] = state?.split(":");
|
||||
// Value could be the organizationId or the githubProviderId
|
||||
// Value could be the authId or the githubProviderId
|
||||
|
||||
if (action === "gh_init") {
|
||||
const octokit = new Octokit({});
|
||||
@@ -41,6 +39,17 @@ export default async function handler(
|
||||
},
|
||||
);
|
||||
|
||||
const auth = await findAuthById(value as string);
|
||||
|
||||
let adminId = "";
|
||||
if (auth.rol === "admin") {
|
||||
const admin = await findAdminByAuthId(auth.id);
|
||||
adminId = admin.adminId;
|
||||
} else {
|
||||
const user = await findUserByAuthId(auth.id);
|
||||
adminId = user.adminId;
|
||||
}
|
||||
|
||||
await createGithub(
|
||||
{
|
||||
name: data.name,
|
||||
@@ -51,7 +60,7 @@ export default async function handler(
|
||||
githubWebhookSecret: data.webhook_secret,
|
||||
githubPrivateKey: data.pem,
|
||||
},
|
||||
value as string,
|
||||
adminId,
|
||||
);
|
||||
} else if (action === "gh_setup") {
|
||||
await db
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { buffer } from "node:stream/consumers";
|
||||
import { db } from "@/server/db";
|
||||
import { server, users_temp } from "@/server/db/schema";
|
||||
import { findAdminById, findUserById } from "@dokploy/server";
|
||||
import { admins, server } from "@/server/db/schema";
|
||||
import { findAdminById } from "@dokploy/server";
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import Stripe from "stripe";
|
||||
@@ -64,35 +64,33 @@ export default async function handler(
|
||||
session.subscription as string,
|
||||
);
|
||||
await db
|
||||
.update(users_temp)
|
||||
.update(admins)
|
||||
.set({
|
||||
stripeCustomerId: session.customer as string,
|
||||
stripeSubscriptionId: session.subscription as string,
|
||||
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
||||
})
|
||||
.where(eq(users_temp.id, adminId))
|
||||
.where(eq(admins.adminId, adminId))
|
||||
.returning();
|
||||
|
||||
const admin = await findUserById(adminId);
|
||||
const admin = await findAdminById(adminId);
|
||||
if (!admin) {
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
const newServersQuantity = admin.serversQuantity;
|
||||
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.created": {
|
||||
const newSubscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db
|
||||
.update(users_temp)
|
||||
.update(admins)
|
||||
.set({
|
||||
stripeSubscriptionId: newSubscription.id,
|
||||
stripeCustomerId: newSubscription.customer as string,
|
||||
})
|
||||
.where(
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
)
|
||||
.where(eq(admins.stripeCustomerId, newSubscription.customer as string))
|
||||
.returning();
|
||||
|
||||
break;
|
||||
@@ -102,16 +100,14 @@ export default async function handler(
|
||||
const newSubscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db
|
||||
.update(users_temp)
|
||||
.update(admins)
|
||||
.set({
|
||||
stripeSubscriptionId: null,
|
||||
serversQuantity: 0,
|
||||
})
|
||||
.where(
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
);
|
||||
.where(eq(admins.stripeCustomerId, newSubscription.customer as string));
|
||||
|
||||
const admin = await findUserByStripeCustomerId(
|
||||
const admin = await findAdminByStripeCustomerId(
|
||||
newSubscription.customer as string,
|
||||
);
|
||||
|
||||
@@ -119,13 +115,13 @@ export default async function handler(
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
|
||||
await disableServers(admin.id);
|
||||
await disableServers(admin.adminId);
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.updated": {
|
||||
const newSubscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
const admin = await findUserByStripeCustomerId(
|
||||
const admin = await findAdminByStripeCustomerId(
|
||||
newSubscription.customer as string,
|
||||
);
|
||||
|
||||
@@ -135,23 +131,23 @@ export default async function handler(
|
||||
|
||||
if (newSubscription.status === "active") {
|
||||
await db
|
||||
.update(users_temp)
|
||||
.update(admins)
|
||||
.set({
|
||||
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
||||
})
|
||||
.where(
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
eq(admins.stripeCustomerId, newSubscription.customer as string),
|
||||
);
|
||||
|
||||
const newServersQuantity = admin.serversQuantity;
|
||||
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||
} else {
|
||||
await disableServers(admin.id);
|
||||
await disableServers(admin.adminId);
|
||||
await db
|
||||
.update(users_temp)
|
||||
.update(admins)
|
||||
.set({ serversQuantity: 0 })
|
||||
.where(
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
eq(admins.stripeCustomerId, newSubscription.customer as string),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -178,7 +174,7 @@ export default async function handler(
|
||||
})
|
||||
.where(eq(admins.stripeCustomerId, suscription.customer as string));
|
||||
|
||||
const admin = await findUserByStripeCustomerId(
|
||||
const admin = await findAdminByStripeCustomerId(
|
||||
suscription.customer as string,
|
||||
);
|
||||
|
||||
@@ -186,7 +182,7 @@ export default async function handler(
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
const newServersQuantity = admin.serversQuantity;
|
||||
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||
break;
|
||||
}
|
||||
case "invoice.payment_failed": {
|
||||
@@ -197,7 +193,7 @@ export default async function handler(
|
||||
);
|
||||
|
||||
if (subscription.status !== "active") {
|
||||
const admin = await findUserByStripeCustomerId(
|
||||
const admin = await findAdminByStripeCustomerId(
|
||||
newInvoice.customer as string,
|
||||
);
|
||||
|
||||
@@ -211,7 +207,7 @@ export default async function handler(
|
||||
})
|
||||
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
|
||||
|
||||
await disableServers(admin.id);
|
||||
await disableServers(admin.adminId);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -220,20 +216,20 @@ export default async function handler(
|
||||
case "customer.deleted": {
|
||||
const customer = event.data.object as Stripe.Customer;
|
||||
|
||||
const admin = await findUserByStripeCustomerId(customer.id);
|
||||
const admin = await findAdminByStripeCustomerId(customer.id);
|
||||
if (!admin) {
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
|
||||
await disableServers(admin.id);
|
||||
await disableServers(admin.adminId);
|
||||
await db
|
||||
.update(users_temp)
|
||||
.update(admins)
|
||||
.set({
|
||||
stripeCustomerId: null,
|
||||
stripeSubscriptionId: null,
|
||||
serversQuantity: 0,
|
||||
})
|
||||
.where(eq(users_temp.stripeCustomerId, customer.id));
|
||||
.where(eq(admins.stripeCustomerId, customer.id));
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -244,20 +240,20 @@ export default async function handler(
|
||||
return res.status(200).json({ received: true });
|
||||
}
|
||||
|
||||
const disableServers = async (userId: string) => {
|
||||
const disableServers = async (adminId: string) => {
|
||||
await db
|
||||
.update(server)
|
||||
.set({
|
||||
serverStatus: "inactive",
|
||||
})
|
||||
.where(eq(server.userId, userId));
|
||||
.where(eq(server.adminId, adminId));
|
||||
};
|
||||
|
||||
const findUserByStripeCustomerId = async (stripeCustomerId: string) => {
|
||||
const user = db.query.users_temp.findFirst({
|
||||
where: eq(users_temp.stripeCustomerId, stripeCustomerId),
|
||||
const findAdminByStripeCustomerId = async (stripeCustomerId: string) => {
|
||||
const admin = db.query.admins.findFirst({
|
||||
where: eq(admins.stripeCustomerId, stripeCustomerId),
|
||||
});
|
||||
return user;
|
||||
return admin;
|
||||
};
|
||||
|
||||
const activateServer = async (serverId: string) => {
|
||||
@@ -274,19 +270,19 @@ const deactivateServer = async (serverId: string) => {
|
||||
.where(eq(server.serverId, serverId));
|
||||
};
|
||||
|
||||
export const findServersByUserIdSorted = async (userId: string) => {
|
||||
export const findServersByAdminIdSorted = async (adminId: string) => {
|
||||
const servers = await db.query.server.findMany({
|
||||
where: eq(server.userId, userId),
|
||||
where: eq(server.adminId, adminId),
|
||||
orderBy: asc(server.createdAt),
|
||||
});
|
||||
|
||||
return servers;
|
||||
};
|
||||
export const updateServersBasedOnQuantity = async (
|
||||
userId: string,
|
||||
adminId: string,
|
||||
newServersQuantity: number,
|
||||
) => {
|
||||
const servers = await findServersByUserIdSorted(userId);
|
||||
const servers = await findServersByAdminIdSorted(adminId);
|
||||
|
||||
if (servers.length > newServersQuantity) {
|
||||
for (const [index, server] of servers.entries()) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
@@ -28,7 +27,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -45,20 +44,21 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
authId: auth.id,
|
||||
});
|
||||
|
||||
if (!userR.canAccessToDocker) {
|
||||
if (!user.canAccessToDocker) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
import { api } from "@/utils/api";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { validateRequest } from "@dokploy/server/index";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type React from "react";
|
||||
@@ -25,7 +25,7 @@ const Dashboard = () => {
|
||||
false,
|
||||
);
|
||||
|
||||
const { data: monitoring, isLoading } = api.user.getMetricsToken.useQuery();
|
||||
const { data: monitoring, isLoading } = api.admin.getMetricsToken.useQuery();
|
||||
return (
|
||||
<div className="space-y-4 pb-10">
|
||||
{/* <AlertBlock>
|
||||
@@ -104,7 +104,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user } = await validateRequest(ctx.req);
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
|
||||
@@ -49,7 +49,7 @@ import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import type { findProjectById } from "@dokploy/server";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import {
|
||||
Ban,
|
||||
@@ -200,8 +200,15 @@ const Project = (
|
||||
) => {
|
||||
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
||||
const { projectId } = props;
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { data, isLoading, refetch } = api.project.one.useQuery({ projectId });
|
||||
const router = useRouter();
|
||||
|
||||
@@ -328,7 +335,7 @@ const Project = (
|
||||
</CardTitle>
|
||||
<CardDescription>{data?.description}</CardDescription>
|
||||
</CardHeader>
|
||||
{(auth?.role === "owner" || auth?.user?.canCreateServices) && (
|
||||
{(auth?.rol === "admin" || user?.canCreateServices) && (
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
<ProjectEnvironment projectId={projectId}>
|
||||
<Button variant="outline">Project Environment</Button>
|
||||
@@ -651,7 +658,7 @@ export async function getServerSideProps(
|
||||
const { params } = ctx;
|
||||
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -667,8 +674,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { GlobeIcon, HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||
@@ -86,8 +86,16 @@ const Service = (
|
||||
);
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
@@ -178,8 +186,7 @@ const Service = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateApplication applicationId={applicationId} />
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteServices) && (
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteService id={applicationId} type="application" />
|
||||
)}
|
||||
</div>
|
||||
@@ -363,7 +370,7 @@ export async function getServerSideProps(
|
||||
const { query, params, req, res } = ctx;
|
||||
|
||||
const activeTab = query.tab;
|
||||
const { user, session } = await validateRequest(req);
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -379,8 +386,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CircuitBoard, ServerOff } from "lucide-react";
|
||||
@@ -79,9 +79,17 @@ const Service = (
|
||||
},
|
||||
);
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
@@ -173,8 +181,7 @@ const Service = (
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateCompose composeId={composeId} />
|
||||
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteServices) && (
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteService id={composeId} type="compose" />
|
||||
)}
|
||||
</div>
|
||||
@@ -359,7 +366,7 @@ export async function getServerSideProps(
|
||||
const { query, params, req, res } = ctx;
|
||||
|
||||
const activeTab = query.tab;
|
||||
const { user, session } = await validateRequest(req);
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -375,8 +382,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||
import type {
|
||||
@@ -61,9 +61,16 @@ const Mariadb = (
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
@@ -147,8 +154,7 @@ const Mariadb = (
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMariadb mariadbId={mariadbId} />
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteServices) && (
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteService id={mariadbId} type="mariadb" />
|
||||
)}
|
||||
</div>
|
||||
@@ -310,7 +316,7 @@ export async function getServerSideProps(
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
|
||||
const { user, session } = await validateRequest(req);
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -326,8 +332,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||
import type {
|
||||
@@ -61,8 +61,16 @@ const Mongo = (
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mongo.one.useQuery({ mongoId });
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -148,8 +156,7 @@ const Mongo = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMongo mongoId={mongoId} />
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteServices) && (
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteService id={mongoId} type="mongo" />
|
||||
)}
|
||||
</div>
|
||||
@@ -311,7 +318,7 @@ export async function getServerSideProps(
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
|
||||
const { user, session } = await validateRequest(req);
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -327,8 +334,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||
import type {
|
||||
@@ -60,8 +60,16 @@ const MySql = (
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mysql.one.useQuery({ mysqlId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -148,8 +156,7 @@ const MySql = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMysql mysqlId={mysqlId} />
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteServices) && (
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteService id={mysqlId} type="mysql" />
|
||||
)}
|
||||
</div>
|
||||
@@ -316,7 +323,7 @@ export async function getServerSideProps(
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
|
||||
const { user, session } = await validateRequest(req);
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -332,8 +339,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
|
||||
import type {
|
||||
@@ -60,9 +60,16 @@ const Postgresql = (
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
@@ -147,8 +154,7 @@ const Postgresql = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdatePostgres postgresId={postgresId} />
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteServices) && (
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteService id={postgresId} type="postgres" />
|
||||
)}
|
||||
</div>
|
||||
@@ -313,7 +319,7 @@ export async function getServerSideProps(
|
||||
) {
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
const { user, session } = await validateRequest(req);
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -329,8 +335,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
@@ -60,8 +60,16 @@ const Redis = (
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.redis.one.useQuery({ redisId });
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: auth?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!auth?.id && auth?.rol === "user",
|
||||
},
|
||||
);
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -147,8 +155,7 @@ const Redis = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateRedis redisId={redisId} />
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.user?.canDeleteServices) && (
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteService id={redisId} type="redis" />
|
||||
)}
|
||||
</div>
|
||||
@@ -304,7 +311,7 @@ export async function getServerSideProps(
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
|
||||
const { user, session } = await validateRequest(req);
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -320,8 +327,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ShowProjects } from "@/components/dashboard/projects/show";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
@@ -38,7 +38,7 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
@@ -46,8 +46,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ShowRequests } from "@/components/dashboard/requests/show-requests";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import * as React from "react";
|
||||
@@ -23,7 +22,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user } = await validateRequest(ctx.req);
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
|
||||
@@ -2,8 +2,7 @@ import { ShowBilling } from "@/components/dashboard/settings/billing/show-billin
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
@@ -30,8 +29,8 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || user.role === "member") {
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -46,8 +45,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -24,8 +24,8 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || user.role === "member") {
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -40,8 +40,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -33,8 +33,8 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user || user.role === "member") {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -48,8 +48,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -25,8 +25,8 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || user.role === "member") {
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -41,8 +41,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -40,8 +40,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
@@ -49,12 +49,14 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
authId: auth.id,
|
||||
});
|
||||
|
||||
if (!userR.canAccessToGitProviders) {
|
||||
if (!user.canAccessToGitProviders) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -42,9 +42,9 @@ const settings = z.object({
|
||||
type SettingsType = z.infer<typeof settings>;
|
||||
|
||||
const Page = () => {
|
||||
const { data, refetch } = api.user.get.useQuery();
|
||||
const { data, refetch } = api.admin.one.useQuery();
|
||||
const { mutateAsync, isLoading, isError, error } =
|
||||
api.user.update.useMutation();
|
||||
api.admin.update.useMutation();
|
||||
const form = useForm<SettingsType>({
|
||||
defaultValues: {
|
||||
cleanCacheOnApplications: false,
|
||||
@@ -55,9 +55,9 @@ const Page = () => {
|
||||
});
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
cleanCacheOnApplications: data?.user.cleanupCacheApplications || false,
|
||||
cleanCacheOnCompose: data?.user.cleanupCacheOnCompose || false,
|
||||
cleanCacheOnPreviews: data?.user.cleanupCacheOnPreviews || false,
|
||||
cleanCacheOnApplications: data?.cleanupCacheApplications || false,
|
||||
cleanCacheOnCompose: data?.cleanupCacheOnCompose || false,
|
||||
cleanCacheOnPreviews: data?.cleanupCacheOnPreviews || false,
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
@@ -181,7 +181,7 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -190,7 +190,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.role === "member") {
|
||||
if (user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -205,8 +205,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -25,8 +25,8 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || user.role === "member") {
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -41,8 +41,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -13,16 +13,22 @@ import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data } = api.auth.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();
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<ProfileForm />
|
||||
{(data?.user?.canAccessToAPI || data?.role === "owner") && (
|
||||
<GenerateToken />
|
||||
)}
|
||||
{(user?.canAccessToAPI || data?.rol === "admin") && <GenerateToken />}
|
||||
|
||||
{isCloud && <RemoveSelfAccount />}
|
||||
</div>
|
||||
@@ -40,7 +46,7 @@ export async function getServerSideProps(
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const locale = getLocale(req.cookies);
|
||||
const { user, session } = await validateRequest(req);
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
@@ -48,21 +54,18 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
await helpers.auth.get.prefetch();
|
||||
if (user?.role === "member") {
|
||||
// const userR = await helpers.user.one.fetch({
|
||||
// userId: user.id,
|
||||
// });
|
||||
// await helpers.user.byAuthId.prefetch({
|
||||
// authId: user.authId,
|
||||
// });
|
||||
if (user?.rol === "user") {
|
||||
await helpers.user.byAuthId.prefetch({
|
||||
authId: user.authId,
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
|
||||
@@ -25,8 +25,8 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || user.role === "member") {
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -40,8 +40,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,19 @@ import { SetupMonitoring } from "@/components/dashboard/settings/servers/setup-m
|
||||
import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
||||
import { WebServer } from "@/components/dashboard/settings/web-server";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
@@ -13,6 +25,8 @@ import { toast } from "sonner";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
const { data, refetch } = api.admin.one.useQuery();
|
||||
const { mutateAsync: update } = api.admin.update.useMutation();
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
@@ -84,7 +98,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -93,7 +107,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.role === "member") {
|
||||
if (user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -108,8 +122,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function getServerSideProps(
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const locale = await getLocale(req.cookies);
|
||||
const { user, session } = await validateRequest(req);
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -36,7 +36,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.role === "member") {
|
||||
if (user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -51,8 +51,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -40,22 +40,23 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
authId: auth.id,
|
||||
});
|
||||
|
||||
if (!userR.canAccessToSSHKeys) {
|
||||
if (!user.canAccessToSSHKeys) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations";
|
||||
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
|
||||
@@ -13,7 +12,6 @@ const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowUsers />
|
||||
<ShowInvitations />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -27,10 +25,8 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
console.log("user", user, session);
|
||||
if (!user || user.role === "member") {
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -45,8 +41,8 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
@@ -28,7 +27,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -45,20 +44,21 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
authId: auth.id,
|
||||
});
|
||||
|
||||
if (!userR.canAccessToDocker) {
|
||||
if (!user.canAccessToDocker) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ShowTraefikSystem } from "@/components/dashboard/file-system/show-traefik-system";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
@@ -28,7 +27,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -45,20 +44,21 @@ export async function getServerSideProps(
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
authId: auth.id,
|
||||
});
|
||||
|
||||
if (!userR.canAccessToTraefikFiles) {
|
||||
if (!user.canAccessToTraefikFiles) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -3,177 +3,99 @@ import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Logo } from "@/components/shared/logo";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { CardContent, CardDescription } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/components/ui/input-otp";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { IS_CLOUD, auth, isAdminPresent } from "@dokploy/server";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import base32 from "hi-base32";
|
||||
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { TOTP } from "otpauth";
|
||||
import { type ReactElement, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const LoginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
const loginSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Email is required",
|
||||
})
|
||||
.email({
|
||||
message: "Email must be a valid email",
|
||||
}),
|
||||
|
||||
password: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Password is required",
|
||||
})
|
||||
.min(8, {
|
||||
message: "Password must be at least 8 characters",
|
||||
}),
|
||||
});
|
||||
|
||||
const TwoFactorSchema = z.object({
|
||||
code: z.string().min(6),
|
||||
});
|
||||
type Login = z.infer<typeof loginSchema>;
|
||||
|
||||
const BackupCodeSchema = z.object({
|
||||
code: z.string().min(8, {
|
||||
message: "Backup code must be at least 8 characters",
|
||||
}),
|
||||
});
|
||||
|
||||
type LoginForm = z.infer<typeof LoginSchema>;
|
||||
type BackupCodeForm = z.infer<typeof BackupCodeSchema>;
|
||||
type AuthResponse = {
|
||||
is2FAEnabled: boolean;
|
||||
authId: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
IS_CLOUD: boolean;
|
||||
}
|
||||
export default function Home({ IS_CLOUD }: Props) {
|
||||
const [temp, setTemp] = useState<AuthResponse>({
|
||||
is2FAEnabled: false,
|
||||
authId: "",
|
||||
});
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.auth.login.useMutation();
|
||||
const router = useRouter();
|
||||
const [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),
|
||||
const form = useForm<Login>({
|
||||
defaultValues: {
|
||||
email: "siumauricio@hotmail.com",
|
||||
password: "Password123",
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
resolver: zodResolver(loginSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (values: LoginForm) => {
|
||||
setIsLoginLoading(true);
|
||||
try {
|
||||
const { data, error } = await authClient.signIn.email({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (values: Login) => {
|
||||
await mutateAsync({
|
||||
email: values.email.toLowerCase(),
|
||||
password: values.password,
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.is2FAEnabled) {
|
||||
setTemp(data);
|
||||
} else {
|
||||
toast.success("Successfully signed in", {
|
||||
duration: 2000,
|
||||
});
|
||||
router.push("/dashboard/projects");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Signin failed", {
|
||||
duration: 2000,
|
||||
});
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
setError(error.message || "An error occurred while logging in");
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.twoFactorRedirect as boolean) {
|
||||
setTwoFactorCode("");
|
||||
setIsTwoFactor(true);
|
||||
toast.info("Please enter your 2FA code");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Logged in successfully");
|
||||
router.push("/dashboard/projects");
|
||||
} catch (error) {
|
||||
toast.error("An error occurred while logging in");
|
||||
} finally {
|
||||
setIsLoginLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onTwoFactorSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (twoFactorCode.length !== 6) {
|
||||
toast.error("Please enter a valid 6-digit code");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTwoFactorLoading(true);
|
||||
try {
|
||||
const { data, error } = await authClient.twoFactor.verifyTotp({
|
||||
code: twoFactorCode.replace(/\s/g, ""),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
setError(error.message || "An error occurred while verifying 2FA code");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Logged in successfully");
|
||||
router.push("/dashboard/projects");
|
||||
} catch (error) {
|
||||
toast.error("An error occurred while verifying 2FA code");
|
||||
} finally {
|
||||
setIsTwoFactorLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onBackupCodeSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (backupCode.length < 8) {
|
||||
toast.error("Please enter a valid backup code");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsBackupCodeLoading(true);
|
||||
try {
|
||||
const { data, error } = await authClient.twoFactor.verifyBackupCode({
|
||||
code: backupCode.trim(),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
setError(
|
||||
error.message || "An error occurred while verifying backup code",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Logged in successfully");
|
||||
router.push("/dashboard/projects");
|
||||
} catch (error) {
|
||||
toast.error("An error occurred while verifying backup code");
|
||||
} finally {
|
||||
setIsBackupCodeLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col space-y-2 text-center">
|
||||
@@ -187,169 +109,55 @@ export default function Home({ IS_CLOUD }: Props) {
|
||||
Enter your email and password to sign in
|
||||
</p>
|
||||
</div>
|
||||
{error && (
|
||||
{isError && (
|
||||
<AlertBlock type="error" className="my-2">
|
||||
<span>{error}</span>
|
||||
<span>{error?.message}</span>
|
||||
</AlertBlock>
|
||||
)}
|
||||
<CardContent className="p-0">
|
||||
{!isTwoFactor ? (
|
||||
<Form {...loginForm}>
|
||||
<form
|
||||
onSubmit={loginForm.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="login-form"
|
||||
>
|
||||
<FormField
|
||||
control={loginForm.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="john@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={loginForm.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
className="w-full"
|
||||
type="submit"
|
||||
isLoading={isLoginLoading}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
{!temp.is2FAEnabled ? (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-4">
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" isLoading={isLoading} className="w-full">
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
<Login2FA authId={temp.authId} />
|
||||
)}
|
||||
|
||||
<div className="flex flex-row justify-between flex-wrap">
|
||||
@@ -395,7 +203,8 @@ Home.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
if (IS_CLOUD) {
|
||||
try {
|
||||
const { user } = await validateRequest(context.req);
|
||||
const { user } = await validateRequest(context.req, context.res);
|
||||
|
||||
if (user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -423,7 +232,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
};
|
||||
}
|
||||
|
||||
const { user } = await validateRequest(context.req);
|
||||
const { user } = await validateRequest(context.req, context.res);
|
||||
|
||||
if (user) {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Logo } from "@/components/shared/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -17,11 +16,10 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { IS_CLOUD, getUserByToken } from "@dokploy/server";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertCircle, AlertTriangle } from "lucide-react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -32,9 +30,6 @@ import { z } from "zod";
|
||||
|
||||
const registerSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, {
|
||||
@@ -43,6 +38,7 @@ const registerSchema = z
|
||||
.email({
|
||||
message: "Email must be a valid email",
|
||||
}),
|
||||
|
||||
password: z
|
||||
.string()
|
||||
.min(1, {
|
||||
@@ -75,17 +71,11 @@ interface Props {
|
||||
token: string;
|
||||
invitation: Awaited<ReturnType<typeof getUserByToken>>;
|
||||
isCloud: boolean;
|
||||
userAlreadyExists: boolean;
|
||||
}
|
||||
|
||||
const Invitation = ({
|
||||
token,
|
||||
invitation,
|
||||
isCloud,
|
||||
userAlreadyExists,
|
||||
}: Props) => {
|
||||
const Invitation = ({ token, invitation, isCloud }: Props) => {
|
||||
const router = useRouter();
|
||||
const { data } = api.user.getUserByToken.useQuery(
|
||||
const { data } = api.admin.getUserByToken.useQuery(
|
||||
{
|
||||
token,
|
||||
},
|
||||
@@ -100,7 +90,6 @@ const Invitation = ({
|
||||
|
||||
const form = useForm<Register>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
@@ -109,9 +98,9 @@ const Invitation = ({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.email) {
|
||||
if (data?.auth?.email) {
|
||||
form.reset({
|
||||
email: data?.email || "",
|
||||
email: data?.auth?.email || "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
@@ -119,32 +108,20 @@ const Invitation = ({
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
const onSubmit = async (values: Register) => {
|
||||
try {
|
||||
const { data, error } = await authClient.signUp.email({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
name: values.name,
|
||||
fetchOptions: {
|
||||
headers: {
|
||||
"x-dokploy-token": token,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await authClient.organization.acceptInvitation({
|
||||
invitationId: token,
|
||||
});
|
||||
|
||||
toast.success("Account created successfully");
|
||||
router.push("/dashboard/projects");
|
||||
} catch (error) {
|
||||
toast.error("An error occurred while creating your account");
|
||||
}
|
||||
await mutateAsync({
|
||||
id: data?.authId,
|
||||
password: values.password,
|
||||
token: token,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("User registered successfuly", {
|
||||
description:
|
||||
"Please check your inbox or spam folder to confirm your account.",
|
||||
duration: 100000,
|
||||
});
|
||||
router.push("/dashboard/projects");
|
||||
})
|
||||
.catch((e) => e);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -161,155 +138,114 @@ const Invitation = ({
|
||||
</Link>
|
||||
Invitation
|
||||
</CardTitle>
|
||||
{userAlreadyExists ? (
|
||||
<div className="flex flex-col gap-4 justify-center items-center">
|
||||
<AlertBlock type="success">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="font-medium">Valid Invitation!</span>
|
||||
<span className="text-sm text-green-600 dark:text-green-400">
|
||||
We detected that you already have an account with this
|
||||
email. Please sign in to accept the invitation.
|
||||
</span>
|
||||
</div>
|
||||
</AlertBlock>
|
||||
<CardDescription>
|
||||
Fill the form below to create your account
|
||||
</CardDescription>
|
||||
<div className="w-full">
|
||||
<div className="p-3" />
|
||||
|
||||
<Button asChild variant="default" className="w-full">
|
||||
<Link href="/">Sign In</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<CardDescription>
|
||||
Fill the form below to create your account
|
||||
</CardDescription>
|
||||
<div className="w-full">
|
||||
<div className="p-3" />
|
||||
|
||||
{isError && (
|
||||
<div className="mx-5 my-2 flex flex-row items-center gap-2 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardContent className="p-0">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid gap-4"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="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>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={form.formState.isSubmitting}
|
||||
className="w-full"
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm flex flex-row justify-between gap-2 w-full">
|
||||
{isCloud && (
|
||||
<>
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/send-reset-password"
|
||||
>
|
||||
Lost your password?
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
{isError && (
|
||||
<div className="mx-5 my-2 flex flex-row items-center gap-2 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
|
||||
<CardContent className="p-0">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid gap-4"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input disabled placeholder="Email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={form.formState.isSubmitting}
|
||||
className="w-full"
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm flex flex-row justify-between gap-2 w-full">
|
||||
{isCloud && (
|
||||
<>
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/send-reset-password"
|
||||
>
|
||||
Lost your password?
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// http://localhost:3000/invitation?token=CZK4BLrUdMa32RVkAdZiLsPDdvnPiAgZ
|
||||
// /f7af93acc1a99eae864972ab4c92fee089f0d83473d415ede8e821e5dbabe79c
|
||||
|
||||
export default Invitation;
|
||||
Invitation.getLayout = (page: ReactElement) => {
|
||||
return <OnboardingLayout>{page}</OnboardingLayout>;
|
||||
@@ -318,7 +254,6 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
const { query } = ctx;
|
||||
|
||||
const token = query.token;
|
||||
console.log("query", query);
|
||||
|
||||
if (typeof token !== "string") {
|
||||
return {
|
||||
@@ -332,17 +267,6 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
try {
|
||||
const invitation = await getUserByToken(token);
|
||||
|
||||
if (invitation.userAlreadyExists) {
|
||||
return {
|
||||
props: {
|
||||
isCloud: IS_CLOUD,
|
||||
token: token,
|
||||
invitation: invitation,
|
||||
userAlreadyExists: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (invitation.isExpired) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -360,7 +284,6 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -32,9 +31,6 @@ import { z } from "zod";
|
||||
|
||||
const registerSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, {
|
||||
@@ -83,10 +79,9 @@ const Register = ({ isCloud }: Props) => {
|
||||
|
||||
const form = useForm<Register>({
|
||||
defaultValues: {
|
||||
name: "Mauricio Siu",
|
||||
email: "user5@yopmail.com",
|
||||
password: "Password1234",
|
||||
confirmPassword: "Password1234",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
},
|
||||
resolver: zodResolver(registerSchema),
|
||||
});
|
||||
@@ -96,33 +91,19 @@ const Register = ({ isCloud }: Props) => {
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (values: Register) => {
|
||||
const { data, error } = await authClient.signUp.email({
|
||||
email: values.email,
|
||||
await mutateAsync({
|
||||
email: values.email.toLowerCase(),
|
||||
password: values.password,
|
||||
name: values.name,
|
||||
});
|
||||
|
||||
// const { data, error } = await authClient.admin.createUser({
|
||||
// name: values.name,
|
||||
// email: values.email,
|
||||
// password: values.password,
|
||||
// role: "superAdmin",
|
||||
// });
|
||||
|
||||
// consol/e.log(data, error);
|
||||
// await mutateAsync({
|
||||
// email: values.email.toLowerCase(),
|
||||
// password: values.password,
|
||||
// })
|
||||
// .then(() => {
|
||||
// toast.success("User registered successfuly", {
|
||||
// duration: 2000,
|
||||
// });
|
||||
// if (!isCloud) {
|
||||
// router.push("/");
|
||||
// }
|
||||
// })
|
||||
// .catch((e) => e);
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("User registered successfuly", {
|
||||
duration: 2000,
|
||||
});
|
||||
if (!isCloud) {
|
||||
router.push("/");
|
||||
}
|
||||
})
|
||||
.catch((e) => e);
|
||||
};
|
||||
return (
|
||||
<div className="">
|
||||
@@ -166,19 +147,6 @@ const Register = ({ isCloud }: Props) => {
|
||||
className="grid gap-4"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="name" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
@@ -274,7 +242,7 @@ Register.getLayout = (page: ReactElement) => {
|
||||
};
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
if (IS_CLOUD) {
|
||||
const { user } = await validateRequest(context.req);
|
||||
const { user } = await validateRequest(context.req, context.res);
|
||||
|
||||
if (user) {
|
||||
return {
|
||||
|
||||
@@ -38,7 +38,7 @@ const Home: NextPage = () => {
|
||||
export default Home;
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { req, res } = context;
|
||||
const { user, session } = await validateRequest(context.req);
|
||||
const { user, session } = await validateRequest(context.req, context.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -53,17 +53,17 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
if (user.rol === "user") {
|
||||
const result = await helpers.user.byAuthId.fetch({
|
||||
authId: user.id,
|
||||
});
|
||||
|
||||
if (!userR.canAccessToAPI) {
|
||||
if (!result.canAccessToAPI) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
9
apps/dokploy/public/templates/appwrite.svg
Normal file
9
apps/dokploy/public/templates/appwrite.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg class="max-w-full" xmlns="http://www.w3.org/2000/svg" width="112" height="98" viewBox="0 0 112 98"
|
||||
fill="none">
|
||||
<path
|
||||
d="M111.1 73.4729V97.9638H48.8706C30.7406 97.9638 14.9105 88.114 6.44112 73.4729C5.2099 71.3444 4.13229 69.1113 3.22835 66.7935C1.45387 62.2516 0.338421 57.3779 0 52.2926V45.6712C0.0734729 44.5379 0.189248 43.4135 0.340647 42.3025C0.650124 40.0227 1.11768 37.7918 1.73218 35.6232C7.54544 15.0641 26.448 0 48.8706 0C71.2932 0 90.1935 15.0641 96.0068 35.6232H69.3985C65.0302 28.9216 57.4692 24.491 48.8706 24.491C40.272 24.491 32.711 28.9216 28.3427 35.6232C27.0113 37.6604 25.9782 39.9069 25.3014 42.3025C24.7002 44.4266 24.3796 46.6664 24.3796 48.9819C24.3796 56.0019 27.3319 62.3295 32.0653 66.7935C36.4515 70.9369 42.3649 73.4729 48.8706 73.4729H111.1Z"
|
||||
fill="#FD366E" />
|
||||
<path
|
||||
d="M111.1 42.3027V66.7937H65.6759C70.4094 62.3297 73.3616 56.0021 73.3616 48.9821C73.3616 46.6666 73.041 44.4268 72.4399 42.3027H111.1Z"
|
||||
fill="#FD366E" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 986 B |
5
apps/dokploy/public/templates/convex.svg
Normal file
5
apps/dokploy/public/templates/convex.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="184" height="188" viewBox="0 0 184 188" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M108.092 130.021C126.258 128.003 143.385 118.323 152.815 102.167C148.349 142.128 104.653 167.385 68.9858 151.878C65.6992 150.453 62.8702 148.082 60.9288 145.034C52.9134 132.448 50.2786 116.433 54.0644 101.899C64.881 120.567 86.8748 132.01 108.092 130.021Z" fill="#F3B01C"/>
|
||||
<path d="M53.4012 90.1735C46.0375 107.191 45.7186 127.114 54.7463 143.51C22.9759 119.608 23.3226 68.4578 54.358 44.7949C57.2286 42.6078 60.64 41.3097 64.2178 41.1121C78.9312 40.336 93.8804 46.0225 104.364 56.6193C83.0637 56.831 62.318 70.4756 53.4012 90.1735Z" fill="#8D2676"/>
|
||||
<path d="M114.637 61.8552C103.89 46.8701 87.0686 36.6684 68.6387 36.358C104.264 20.1876 148.085 46.4045 152.856 85.1654C153.3 88.7635 152.717 92.4322 151.122 95.6775C144.466 109.195 132.124 119.679 117.702 123.559C128.269 103.96 126.965 80.0151 114.637 61.8552Z" fill="#EE342F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 948 B |
BIN
apps/dokploy/public/templates/outline.png
Normal file
BIN
apps/dokploy/public/templates/outline.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/dokploy/public/templates/registry.png
Normal file
BIN
apps/dokploy/public/templates/registry.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
apps/dokploy/public/templates/trilium.png
Normal file
BIN
apps/dokploy/public/templates/trilium.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -19,7 +19,6 @@ import { mongoRouter } from "./routers/mongo";
|
||||
import { mountRouter } from "./routers/mount";
|
||||
import { mysqlRouter } from "./routers/mysql";
|
||||
import { notificationRouter } from "./routers/notification";
|
||||
import { organizationRouter } from "./routers/organization";
|
||||
import { portRouter } from "./routers/port";
|
||||
import { postgresRouter } from "./routers/postgres";
|
||||
import { previewDeploymentRouter } from "./routers/preview-deployment";
|
||||
@@ -34,6 +33,7 @@ import { sshRouter } from "./routers/ssh-key";
|
||||
import { stripeRouter } from "./routers/stripe";
|
||||
import { swarmRouter } from "./routers/swarm";
|
||||
import { userRouter } from "./routers/user";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
*
|
||||
@@ -75,7 +75,6 @@ export const appRouter = createTRPCRouter({
|
||||
server: serverRouter,
|
||||
stripe: stripeRouter,
|
||||
swarm: swarmRouter,
|
||||
organization: organizationRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user