Compare commits

...

48 Commits

Author SHA1 Message Date
Mauricio Siu
790894ab93 refactor: migrate admin API calls to user router 2025-02-20 23:02:02 -06:00
Mauricio Siu
5a1145996d feat: add backup code authentication for 2FA login 2025-02-20 01:50:01 -06:00
Mauricio Siu
a9e12c2b18 refactor: update organization context in API routers 2025-02-20 01:42:35 -06:00
Mauricio Siu
b73e4102dd feat: add organizations and members 2025-02-17 02:48:42 -06:00
Mauricio Siu
c7d47a6003 refactor: update database foreign key constraints and user management 2025-02-17 00:30:15 -06:00
Mauricio Siu
8c28223343 refactor: remove 2fa migration 2025-02-17 00:10:34 -06:00
Mauricio Siu
7abe060fcf feat: enhance two-factor authentication and auth client implementation 2025-02-17 00:07:36 -06:00
Mauricio Siu
0e8e92c715 refactor: add 2fa 2025-02-16 20:56:50 -06:00
Mauricio Siu
e1632cbdb3 refactor: update user and authentication schema with two-factor support 2025-02-16 15:32:57 -06:00
Mauricio Siu
90156da570 refactor: remove tables 2025-02-16 14:11:47 -06:00
Mauricio Siu
9856502ece refactor: remove old references 2025-02-16 13:55:27 -06:00
Mauricio Siu
a8d1471b16 refactor: update 2025-02-16 13:28:29 -06:00
Mauricio Siu
27736c7c97 refactor: update role and validation handling across multiple pages 2025-02-16 03:06:22 -06:00
Mauricio Siu
e7db0ccb70 refactor: update invitation 2025-02-16 02:57:49 -06:00
Mauricio Siu
4a1a14aeb4 refactor: update 2025-02-15 23:24:45 -06:00
Mauricio Siu
ed62b4e1a3 refactor: lint 2025-02-15 23:01:44 -06:00
Mauricio Siu
515d65d993 refactor: adjust queries 2025-02-15 23:01:36 -06:00
Mauricio Siu
78c72b6337 refactor: update 2025-02-15 20:49:10 -06:00
Mauricio Siu
e3e35ce792 refactor: update to use organization resources 2025-02-15 20:43:23 -06:00
Mauricio Siu
6d0e195a4d refactor: update 2025-02-15 20:26:05 -06:00
Mauricio Siu
53ce5e57fa refactor: update organization 2025-02-15 20:25:58 -06:00
Mauricio Siu
87b12ff6e9 refactor: update 2025-02-15 20:06:33 -06:00
Mauricio Siu
8b71f963cc refactor: update logic 2025-02-15 19:35:22 -06:00
Mauricio Siu
1c5cc5a0db refactor: update roles 2025-02-15 19:23:08 -06:00
Mauricio Siu
d233f2c764 feat: adjust roles 2025-02-15 19:12:44 -06:00
Mauricio Siu
1bbb4c9b64 refactor: update migration 2025-02-15 18:13:20 -06:00
Mauricio Siu
6ec60b6bab refactor: update validation 2025-02-15 13:14:48 -06:00
Mauricio Siu
55abac3f2f refactor: migrate endpoints 2025-02-14 02:52:37 -06:00
Mauricio Siu
b6c29ccf05 refactor: update 2025-02-14 02:40:11 -06:00
Mauricio Siu
ca217affe6 feat: update references 2025-02-14 02:18:53 -06:00
Mauricio Siu
5c24281f72 refactor: return correct information 2025-02-13 02:45:33 -06:00
Mauricio Siu
bc901bcb25 refactor: update 2025-02-13 02:36:08 -06:00
Mauricio Siu
7c0d223e17 refactor: add fields 2025-02-13 01:42:58 -06:00
Mauricio Siu
74ee024cf9 refactor: update temps 2025-02-13 01:24:25 -06:00
Mauricio Siu
140a871275 refactor: update 2025-02-13 01:21:49 -06:00
Mauricio Siu
d1f72a2e20 refactor: update migration 2025-02-13 00:57:22 -06:00
Mauricio Siu
0d525398a8 feat: migrate rest schemas 2025-02-13 00:45:29 -06:00
Mauricio Siu
7c62408070 refactor: delete 2025-02-13 00:38:39 -06:00
Mauricio Siu
23f1ce17de refactor: add migration 2025-02-13 00:38:22 -06:00
Mauricio Siu
60eee55f2d refactor: test migrastion 2025-02-12 23:41:04 -06:00
Mauricio Siu
8f562eefc1 Merge branch 'canary' into feat/better-auth 2025-02-12 20:56:23 -06:00
Mauricio Siu
6179cef1ee refactor: update name 2025-02-10 02:13:52 -06:00
Mauricio Siu
b7112b89fd refactor: add migration 2025-02-10 00:39:46 -06:00
Mauricio Siu
1db6ba94f4 refactor: remove 2025-02-09 21:36:36 -06:00
Mauricio Siu
afd3d2eea3 refactor: lint 2025-02-09 20:53:14 -06:00
Mauricio Siu
8bd72a8a34 refactor: add organizations system 2025-02-09 20:53:06 -06:00
Mauricio Siu
fafc238e70 refactor: migration 2025-02-09 18:56:17 -06:00
Mauricio Siu
c04bf3c7e0 feat: add migration 2025-02-09 18:19:21 -06:00
188 changed files with 58585 additions and 2802 deletions

View File

@@ -45,7 +45,7 @@ const baseApp: ApplicationNested = {
previewWildcard: "",
project: {
env: "",
adminId: "",
organizationId: "",
name: "",
description: "",
createdAt: "",

View File

@@ -5,7 +5,7 @@ vi.mock("node:fs", () => ({
default: fs,
}));
import type { Admin, FileConfig } from "@dokploy/server";
import type { FileConfig, User } from "@dokploy/server";
import {
createDefaultServerTraefikConfig,
loadOrCreateConfig,
@@ -13,7 +13,7 @@ import {
} from "@dokploy/server";
import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: Admin = {
const baseAdmin: User = {
enablePaidFeatures: false,
metricsConfig: {
containers: {
@@ -40,9 +40,7 @@ const baseAdmin: Admin = {
cleanupCacheApplications: false,
cleanupCacheOnCompose: false,
cleanupCacheOnPreviews: false,
createdAt: "",
authId: "",
adminId: "string",
createdAt: new Date(),
serverIp: null,
certificateType: "none",
host: null,
@@ -53,6 +51,31 @@ const baseAdmin: Admin = {
serversQuantity: 0,
stripeCustomerId: "",
stripeSubscriptionId: "",
accessedProjects: [],
accessedServices: [],
banExpires: new Date(),
banned: true,
banReason: "",
canAccessToAPI: false,
canCreateProjects: false,
canDeleteProjects: false,
canDeleteServices: false,
canAccessToDocker: false,
canAccessToSSHKeys: false,
canCreateServices: false,
canAccessToTraefikFiles: false,
canAccessToGitProviders: false,
email: "",
expirationDate: "",
id: "",
isRegistered: false,
name: "",
createdAt2: new Date().toISOString(),
emailVerified: false,
image: "",
token: "",
updatedAt: new Date(),
twoFactorEnabled: false,
};
beforeEach(() => {

View File

@@ -26,7 +26,7 @@ const baseApp: ApplicationNested = {
previewWildcard: "",
project: {
env: "",
adminId: "",
organizationId: "",
name: "",
description: "",
createdAt: "",

View File

@@ -79,7 +79,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
data,
isLoading,
error: queryError,
} = api.admin.getContainerMetrics.useQuery(
} = api.user.getContainerMetrics.useQuery(
{
url: baseUrl,
token,

View File

@@ -73,7 +73,7 @@ export const ShowPaidMonitoring = ({
data,
isLoading,
error: queryError,
} = api.admin.getServerMetrics.useQuery(
} = api.user.getServerMetrics.useQuery(
{
url: BASE_URL,
token,

View File

@@ -0,0 +1,119 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { PenBoxIcon, Plus, SquarePen } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
interface Props {
organizationId?: string;
children?: React.ReactNode;
}
export function AddOrganization({ organizationId, children }: Props) {
const utils = api.useUtils();
const { data: organization } = api.organization.one.useQuery(
{
organizationId: organizationId ?? "",
},
{
enabled: !!organizationId,
},
);
const { mutateAsync, isLoading } = organizationId
? api.organization.update.useMutation()
: api.organization.create.useMutation();
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
useEffect(() => {
if (organization) {
setName(organization.name);
}
}, [organization]);
const handleSubmit = async () => {
await mutateAsync({ name, organizationId: organizationId ?? "" })
.then(() => {
setOpen(false);
toast.success(
`Organization ${organizationId ? "updated" : "created"} successfully`,
);
utils.organization.all.invalidate();
})
.catch((error) => {
console.error(error);
toast.error(
`Failed to ${organizationId ? "update" : "create"} organization`,
);
});
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{organizationId ? (
<DropdownMenuItem
className="group cursor-pointer hover:bg-blue-500/10"
onSelect={(e) => e.preventDefault()}
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</DropdownMenuItem>
) : (
<DropdownMenuItem
className="gap-2 p-2"
onClick={() => {
setOpen(true);
}}
onSelect={(e) => e.preventDefault()}
>
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus className="size-4" />
</div>
<div className="font-medium text-muted-foreground">
Add organization
</div>
</DropdownMenuItem>
)}
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>
{organizationId ? "Update organization" : "Add organization"}
</DialogTitle>
<DialogDescription>
{organizationId
? "Update the organization name"
: "Create a new organization to manage your projects."}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button type="submit" onClick={handleSubmit} isLoading={isLoading}>
{organizationId ? "Update organization" : "Create organization"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -21,6 +21,7 @@ import {
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon, SquarePen } from "lucide-react";
@@ -97,6 +98,18 @@ export const HandleProject = ({ projectId }: Props) => {
);
});
};
// useEffect(() => {
// const getUsers = async () => {
// const users = await authClient.admin.listUsers({
// query: {
// limit: 100,
// },
// });
// console.log(users);
// };
// getUsers();
// });
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>

View File

@@ -51,15 +51,7 @@ import { ProjectEnvironment } from "./project-environment";
export const ShowProjects = () => {
const utils = api.useUtils();
const { data, isLoading } = api.project.all.useQuery();
const { data: auth } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { data: auth } = api.user.get.useQuery();
const { mutateAsync } = api.project.remove.useMutation();
const [searchQuery, setSearchQuery] = useState("");
@@ -91,7 +83,7 @@ export const ShowProjects = () => {
</CardDescription>
</CardHeader>
{(auth?.rol === "admin" || user?.canCreateProjects) && (
{(auth?.role === "owner" || auth?.user?.canCreateProjects) && (
<div className="">
<HandleProject />
</div>
@@ -293,8 +285,8 @@ export const ShowProjects = () => {
<div
onClick={(e) => e.stopPropagation()}
>
{(auth?.rol === "admin" ||
user?.canDeleteProjects) && (
{(auth?.role === "owner" ||
auth?.user?.canDeleteProjects) && (
<AlertDialog>
<AlertDialogTrigger className="w-full">
<DropdownMenuItem

View File

@@ -18,6 +18,7 @@ import {
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import { authClient } from "@/lib/auth-client";
import {
type Services,
extractServices,
@@ -35,8 +36,10 @@ export const SearchCommand = () => {
const router = useRouter();
const [open, setOpen] = React.useState(false);
const [search, setSearch] = React.useState("");
const { data } = api.project.all.useQuery();
const { data: session } = authClient.useSession();
const { data } = api.project.all.useQuery(undefined, {
enabled: !!session,
});
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
React.useEffect(() => {

View File

@@ -39,7 +39,7 @@ export const calculatePrice = (count: number, isAnnual = false) => {
};
export const ShowBilling = () => {
const { data: servers } = api.server.all.useQuery(undefined);
const { data: admin } = api.admin.one.useQuery();
const { data: admin } = api.user.get.useQuery();
const { data, isLoading } = api.stripe.getProducts.useQuery();
const { mutateAsync: createCheckoutSession } =
api.stripe.createCheckoutSession.useMutation();
@@ -70,7 +70,7 @@ export const ShowBilling = () => {
return isAnnual ? interval === "year" : interval === "month";
});
const maxServers = admin?.serversQuantity ?? 1;
const maxServers = admin?.user.serversQuantity ?? 1;
const percentage = ((servers?.length ?? 0) / maxServers) * 100;
const safePercentage = Math.min(percentage, 100);
@@ -98,17 +98,17 @@ export const ShowBilling = () => {
<TabsTrigger value="annual">Annual</TabsTrigger>
</TabsList>
</Tabs>
{admin?.stripeSubscriptionId && (
{admin?.user.stripeSubscriptionId && (
<div className="space-y-2 flex flex-col">
<h3 className="text-lg font-medium">Servers Plan</h3>
<p className="text-sm text-muted-foreground">
You have {servers?.length} server on your plan of{" "}
{admin?.serversQuantity} servers
{admin?.user.serversQuantity} servers
</p>
<div>
<Progress value={safePercentage} className="max-w-lg" />
</div>
{admin && admin.serversQuantity! <= servers?.length! && (
{admin && admin.user.serversQuantity! <= servers?.length! && (
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
<span className="text-sm text-yellow-600 dark:text-yellow-400">
@@ -279,7 +279,7 @@ export const ShowBilling = () => {
"flex flex-row items-center gap-2 mt-4",
)}
>
{admin?.stripeCustomerId && (
{admin?.user.stripeCustomerId && (
<Button
variant="secondary"
className="w-full"

View File

@@ -10,12 +10,12 @@ import type React from "react";
import { useEffect, useState } from "react";
export const ShowWelcomeDokploy = () => {
const { data } = api.auth.get.useQuery();
const { data } = api.user.get.useQuery();
const [open, setOpen] = useState(false);
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
if (!isCloud || data?.rol !== "admin") {
if (!isCloud || data?.role !== "admin") {
return null;
}
@@ -24,14 +24,14 @@ export const ShowWelcomeDokploy = () => {
!isLoading &&
isCloud &&
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
data?.rol === "admin"
data?.role === "owner"
) {
setOpen(true);
}
}, [isCloud, isLoading]);
const handleClose = (isOpen: boolean) => {
if (data?.rol === "admin") {
if (data?.role === "owner") {
setOpen(isOpen);
if (!isOpen) {
localStorage.setItem("hasSeenCloudWelcomeModal", "true"); // Establece el flag al cerrar el modal

View File

@@ -86,6 +86,7 @@ export const AddCertificate = () => {
privateKey: data.privateKey,
autoRenew: data.autoRenew,
serverId: data.serverId,
organizationId: "",
})
.then(async () => {
toast.success("Certificate Created");

View File

@@ -53,7 +53,7 @@ export const AddBitbucketProvider = () => {
const [isOpen, setIsOpen] = useState(false);
const url = useUrl();
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
const { data: auth } = api.auth.get.useQuery();
const { data: auth } = api.user.get.useQuery();
const router = useRouter();
const form = useForm<Schema>({
defaultValues: {

View File

@@ -10,13 +10,15 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { format } from "date-fns";
import { useEffect, useState } from "react";
export const AddGithubProvider = () => {
const [isOpen, setIsOpen] = useState(false);
const { data } = api.auth.get.useQuery();
const { data: activeOrganization } = authClient.useActiveOrganization();
const { data } = api.user.get.useQuery();
const [manifest, setManifest] = useState("");
const [isOrganization, setIsOrganization] = useState(false);
const [organizationName, setOrganization] = useState("");
@@ -25,7 +27,7 @@ export const AddGithubProvider = () => {
const url = document.location.origin;
const manifest = JSON.stringify(
{
redirect_url: `${origin}/api/providers/github/setup?authId=${data?.id}`,
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}`,
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
url: origin,
hook_attributes: {
@@ -93,8 +95,8 @@ export const AddGithubProvider = () => {
<form
action={
isOrganization
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${data?.id}`
: `https://github.com/settings/apps/new?state=gh_init:${data?.id}`
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${activeOrganization?.id}`
: `https://github.com/settings/apps/new?state=gh_init:${activeOrganization?.id}`
}
method="post"
>

View File

@@ -55,7 +55,7 @@ export const AddGitlabProvider = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const url = useUrl();
const { data: auth } = api.auth.get.useQuery();
const { data: auth } = api.user.get.useQuery();
const { mutateAsync, error, isError } = api.gitlab.create.useMutation();
const webhookUrl = `${url}/api/providers/gitlab/callback`;

View File

@@ -1,52 +1,134 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const PasswordSchema = z.object({
password: z.string().min(8, {
message: "Password is required",
}),
});
type PasswordForm = z.infer<typeof PasswordSchema>;
export const Disable2FA = () => {
const utils = api.useUtils();
const { mutateAsync, isLoading } = api.auth.disable2FA.useMutation();
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const form = useForm<PasswordForm>({
resolver: zodResolver(PasswordSchema),
defaultValues: {
password: "",
},
});
const handleSubmit = async (formData: PasswordForm) => {
setIsLoading(true);
try {
const result = await authClient.twoFactor.disable({
password: formData.password,
});
if (result.error) {
form.setError("password", {
message: result.error.message,
});
toast.error(result.error.message);
return;
}
toast.success("2FA disabled successfully");
utils.auth.get.invalidate();
setIsOpen(false);
} catch (error) {
form.setError("password", {
message: "Connection error. Please try again.",
});
toast.error("Connection error. Please try again.");
} finally {
setIsLoading(false);
}
};
return (
<AlertDialog>
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
<AlertDialogTrigger asChild>
<Button variant="destructive" isLoading={isLoading}>
Disable 2FA
</Button>
<Button variant="destructive">Disable 2FA</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the 2FA
This action cannot be undone. This will permanently disable
Two-Factor Authentication for your account.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync()
.then(() => {
utils.auth.get.invalidate();
toast.success("2FA Disabled");
})
.catch(() => {
toast.error("Error disabling 2FA");
});
}}
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4"
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your password"
{...field}
/>
</FormControl>
<FormDescription>
Enter your password to disable 2FA
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-4">
<Button
type="button"
variant="outline"
onClick={() => {
form.reset();
setIsOpen(false);
}}
>
Cancel
</Button>
<Button type="submit" variant="destructive" isLoading={isLoading}>
Disable 2FA
</Button>
</div>
</form>
</Form>
</AlertDialogContent>
</AlertDialog>
);

View File

@@ -17,144 +17,315 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Fingerprint } from "lucide-react";
import { useEffect } from "react";
import { Fingerprint, QrCode } from "lucide-react";
import QRCode from "qrcode";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Enable2FASchema = z.object({
const PasswordSchema = z.object({
password: z.string().min(8, {
message: "Password is required",
}),
});
const PinSchema = z.object({
pin: z.string().min(6, {
message: "Pin is required",
}),
});
type Enable2FA = z.infer<typeof Enable2FASchema>;
type PasswordForm = z.infer<typeof PasswordSchema>;
type PinForm = z.infer<typeof PinSchema>;
type TwoFactorEnableResponse = {
totpURI: string;
backupCodes: string[];
};
type TwoFactorSetupData = {
qrCodeUrl: string;
secret: string;
totpURI: string;
};
export const Enable2FA = () => {
const utils = api.useUtils();
const { data: session } = authClient.useSession();
const [data, setData] = useState<TwoFactorSetupData | null>(null);
const [backupCodes, setBackupCodes] = useState<string[]>([]);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [step, setStep] = useState<"password" | "verify">("password");
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
const { data } = api.auth.generate2FASecret.useQuery(undefined, {
refetchOnWindowFocus: false,
const handlePasswordSubmit = async (formData: PasswordForm) => {
setIsPasswordLoading(true);
try {
const { data: enableData } = await authClient.twoFactor.enable({
password: formData.password,
});
if (!enableData) {
throw new Error("No data received from server");
}
if (enableData.backupCodes) {
setBackupCodes(enableData.backupCodes);
}
if (enableData.totpURI) {
const qrCodeUrl = await QRCode.toDataURL(enableData.totpURI);
setData({
qrCodeUrl,
secret: enableData.totpURI.split("secret=")[1]?.split("&")[0] || "",
totpURI: enableData.totpURI,
});
setStep("verify");
toast.success("Scan the QR code with your authenticator app");
} else {
throw new Error("No TOTP URI received from server");
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Error setting up 2FA",
);
passwordForm.setError("password", {
message: "Error verifying password",
});
} finally {
setIsPasswordLoading(false);
}
};
const handleVerifySubmit = async (formData: PinForm) => {
try {
const result = await authClient.twoFactor.verifyTotp({
code: formData.pin,
});
if (result.error) {
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
pinForm.setError("pin", {
message: "Invalid code. Please try again.",
});
toast.error("Invalid verification code");
return;
}
throw result.error;
}
if (!result.data) {
throw new Error("No response received from server");
}
toast.success("2FA configured successfully");
utils.auth.get.invalidate();
setIsDialogOpen(false);
} catch (error) {
if (error instanceof Error) {
const errorMessage =
error.message === "Failed to fetch"
? "Connection error. Please check your internet connection."
: error.message;
pinForm.setError("pin", {
message: errorMessage,
});
toast.error(errorMessage);
} else {
pinForm.setError("pin", {
message: "Error verifying code",
});
toast.error("Error verifying 2FA code");
}
}
};
const passwordForm = useForm<PasswordForm>({
resolver: zodResolver(PasswordSchema),
defaultValues: {
password: "",
},
});
const { mutateAsync, isLoading, error, isError } =
api.auth.verify2FASetup.useMutation();
const form = useForm<Enable2FA>({
const pinForm = useForm<PinForm>({
resolver: zodResolver(PinSchema),
defaultValues: {
pin: "",
},
resolver: zodResolver(Enable2FASchema),
});
useEffect(() => {
form.reset({
pin: "",
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
if (!isDialogOpen) {
setStep("password");
setData(null);
setBackupCodes([]);
passwordForm.reset();
pinForm.reset();
}
}, [isDialogOpen, passwordForm, pinForm]);
const onSubmit = async (formData: Enable2FA) => {
await mutateAsync({
pin: formData.pin,
secret: data?.secret || "",
})
.then(async () => {
toast.success("2FA Verified");
utils.auth.get.invalidate();
})
.catch(() => {
toast.error("Error verifying the 2FA");
});
};
return (
<Dialog>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button variant="ghost">
<Fingerprint className="size-4 text-muted-foreground" />
Enable 2FA
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen max-sm:overflow-y-auto sm:max-w-xl ">
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-xl">
<DialogHeader>
<DialogTitle>2FA Setup</DialogTitle>
<DialogDescription>Add a 2FA to your account</DialogDescription>
<DialogDescription>
{step === "password"
? "Enter your password to begin 2FA setup"
: "Scan the QR code and verify with your authenticator app"}
</DialogDescription>
</DialogHeader>
{isError && (
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)}
<Form {...form}>
<form
id="hook-form-add-2FA"
onSubmit={form.handleSubmit(onSubmit)}
className="grid sm:grid-cols-2 w-full gap-4"
>
<div className="flex flex-col gap-4 justify-center items-center">
<span className="text-sm text-muted-foreground">
{data?.qrCodeUrl ? "Scan the QR code to add 2FA" : ""}
</span>
<img
src={data?.qrCodeUrl}
alt="qrCode"
className="rounded-lg w-fit"
/>
<div className="flex flex-col gap-2">
<span className="text-sm text-muted-foreground text-center">
{data?.secret ? `Secret: ${data?.secret}` : ""}
</span>
</div>
</div>
<FormField
control={form.control}
name="pin"
render={({ field }) => (
<FormItem className="flex flex-col justify-center max-sm:items-center">
<FormLabel>Pin</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription className="max-md:text-center">
Please enter the 6 digits code provided by your
authenticator app.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-add-2FA"
type="submit"
{step === "password" ? (
<Form {...passwordForm}>
<form
id="password-form"
onSubmit={passwordForm.handleSubmit(handlePasswordSubmit)}
className="space-y-4"
>
Submit 2FA
</Button>
</DialogFooter>
</Form>
<FormField
control={passwordForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your password"
{...field}
/>
</FormControl>
<FormDescription>
Enter your password to enable 2FA
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"
isLoading={isPasswordLoading}
>
Continue
</Button>
</form>
</Form>
) : (
<Form {...pinForm}>
<form
id="pin-form"
onSubmit={pinForm.handleSubmit(handleVerifySubmit)}
className="space-y-6"
>
<div className="flex flex-col gap-6 justify-center items-center">
{data?.qrCodeUrl ? (
<>
<div className="flex flex-col items-center gap-4 p-6 border rounded-lg">
<QrCode className="size-5 text-muted-foreground" />
<span className="text-sm font-medium">
Scan this QR code with your authenticator app
</span>
<img
src={data.qrCodeUrl}
alt="2FA QR Code"
className="rounded-lg w-48 h-48"
/>
<div className="flex flex-col gap-2 text-center">
<span className="text-sm text-muted-foreground">
Can't scan the QR code?
</span>
<span className="text-xs font-mono bg-muted p-2 rounded">
{data.secret}
</span>
</div>
</div>
{backupCodes && backupCodes.length > 0 && (
<div className="w-full space-y-3 border rounded-lg p-4">
<h4 className="font-medium">Backup Codes</h4>
<div className="grid grid-cols-2 gap-2">
{backupCodes.map((code, index) => (
<code
key={index}
className="bg-muted p-2 rounded text-sm font-mono"
>
{code}
</code>
))}
</div>
<p className="text-sm text-muted-foreground">
Save these backup codes in a secure place. You can use
them to access your account if you lose access to your
authenticator device.
</p>
</div>
)}
</>
) : (
<div className="flex items-center justify-center w-full h-48 bg-muted rounded-lg">
<QrCode className="size-8 text-muted-foreground animate-pulse" />
</div>
)}
</div>
<FormField
control={pinForm.control}
name="pin"
render={({ field }) => (
<FormItem className="flex flex-col justify-center items-center">
<FormLabel>Verification Code</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription>
Enter the 6-digit code from your authenticator app
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"
isLoading={isPasswordLoading}
>
Enable 2FA
</Button>
</form>
</Form>
)}
</DialogContent>
</Dialog>
);

View File

@@ -14,7 +14,7 @@ import Link from "next/link";
import { toast } from "sonner";
export const GenerateToken = () => {
const { data, refetch } = api.auth.get.useQuery();
const { data, refetch } = api.user.get.useQuery();
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
api.auth.generateToken.useMutation();
@@ -51,7 +51,7 @@ export const GenerateToken = () => {
<Label>Token</Label>
<ToggleVisibilityInput
placeholder="Token"
value={data?.token || ""}
value={data?.user?.token || ""}
disabled
/>
</div>

View File

@@ -1,4 +1,5 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -17,6 +18,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { authClient } from "@/lib/auth-client";
import { generateSHA256Hash } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -54,7 +56,10 @@ const randomImages = [
];
export const ProfileForm = () => {
const { data, refetch, isLoading } = api.auth.get.useQuery();
const utils = api.useUtils();
const { mutateAsync: disable2FA, isLoading: isDisabling } =
api.auth.disable2FA.useMutation();
const { data, refetch, isLoading } = api.user.get.useQuery();
const {
mutateAsync,
isLoading: isUpdating,
@@ -73,9 +78,9 @@ export const ProfileForm = () => {
const form = useForm<Profile>({
defaultValues: {
email: data?.email || "",
email: data?.user?.email || "",
password: "",
image: data?.image || "",
image: data?.user?.image || "",
currentPassword: "",
},
resolver: zodResolver(profileSchema),
@@ -84,14 +89,14 @@ export const ProfileForm = () => {
useEffect(() => {
if (data) {
form.reset({
email: data?.email || "",
email: data?.user?.email || "",
password: "",
image: data?.image || "",
image: data?.user?.image || "",
currentPassword: "",
});
if (data.email) {
generateSHA256Hash(data.email).then((hash) => {
if (data.user.email) {
generateSHA256Hash(data.user.email).then((hash) => {
setGravatarHash(hash);
});
}
@@ -130,7 +135,7 @@ export const ProfileForm = () => {
{t("settings.profile.description")}
</CardDescription>
</div>
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
{!data?.user.twoFactorEnabled ? <Enable2FA /> : <Disable2FA />}
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">

View File

@@ -35,7 +35,7 @@ const profileSchema = z.object({
type Profile = z.infer<typeof profileSchema>;
export const RemoveSelfAccount = () => {
const { data } = api.auth.get.useQuery();
const { data } = api.user.get.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.auth.removeSelfAccount.useMutation();
const { t } = useTranslation("settings");

View File

@@ -7,7 +7,7 @@ interface Props {
serverId?: string;
}
export const ToggleDockerCleanup = ({ serverId }: Props) => {
const { data, refetch } = api.admin.one.useQuery(undefined, {
const { data, refetch } = api.user.get.useQuery(undefined, {
enabled: !serverId,
});
@@ -20,7 +20,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
},
);
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
const enabled = data?.user.enableDockerCleanup || server?.enableDockerCleanup;
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();

View File

@@ -91,7 +91,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
enabled: !!serverId,
},
)
: api.admin.one.useQuery();
: api.user.get.useQuery();
const url = useUrl();

View File

@@ -35,6 +35,7 @@ export const CreateSSHKey = () => {
description: "Used on Dokploy Cloud",
privateKey: keys.privateKey,
publicKey: keys.publicKey,
organizationId: "",
});
await refetch();
} catch (error) {

View File

@@ -78,6 +78,7 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
const onSubmit = async (data: SSHKey) => {
await mutateAsync({
...data,
organizationId: "",
sshKeyId: sshKeyId || "",
})
.then(async () => {

View File

@@ -19,6 +19,14 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
@@ -27,62 +35,70 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const addUser = z.object({
const addInvitation = z.object({
email: z
.string()
.min(1, "Email is required")
.email({ message: "Invalid email" }),
role: z.enum(["member", "admin"]),
});
type AddUser = z.infer<typeof addUser>;
type AddInvitation = z.infer<typeof addInvitation>;
export const AddUser = () => {
export const AddInvitation = () => {
const [open, setOpen] = useState(false);
const utils = api.useUtils();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { data: activeOrganization } = authClient.useActiveOrganization();
const { mutateAsync, isError, error, isLoading } =
api.admin.createUserInvitation.useMutation();
const form = useForm<AddUser>({
const form = useForm<AddInvitation>({
defaultValues: {
email: "",
role: "member",
},
resolver: zodResolver(addUser),
resolver: zodResolver(addInvitation),
});
useEffect(() => {
form.reset();
}, [form, form.formState.isSubmitSuccessful, form.reset]);
const onSubmit = async (data: AddUser) => {
await mutateAsync({
const onSubmit = async (data: AddInvitation) => {
setIsLoading(true);
const result = await authClient.organization.inviteMember({
email: data.email.toLowerCase(),
})
.then(async () => {
toast.success("Invitation created");
await utils.user.all.invalidate();
setOpen(false);
})
.catch(() => {
toast.error("Error creating the invitation");
});
role: data.role,
organizationId: activeOrganization?.id,
});
if (result.error) {
setError(result.error.message || "");
} else {
toast.success("Invitation created");
setError(null);
setOpen(false);
}
utils.organization.allInvitations.invalidate();
setIsLoading(false);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className="" asChild>
<Button>
<PlusIcon className="h-4 w-4" /> Add User
<PlusIcon className="h-4 w-4" /> Add Invitation
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Add User</DialogTitle>
<DialogTitle>Add Invitation</DialogTitle>
<DialogDescription>Invite a new user</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
{error && <AlertBlock type="error">{error}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-add-user"
id="hook-form-add-invitation"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 "
>
@@ -104,10 +120,39 @@ export const AddUser = () => {
);
}}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Role</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="member">Member</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Select the role for the new user
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<DialogFooter className="flex w-full flex-row">
<Button
isLoading={isLoading}
form="hook-form-add-user"
form="hook-form-add-invitation"
type="submit"
>
Create

View File

@@ -52,7 +52,7 @@ interface Props {
export const AddUserPermissions = ({ userId }: Props) => {
const { data: projects } = api.project.all.useQuery();
const { data, refetch } = api.user.byUserId.useQuery(
const { data, refetch } = api.auth.one.useQuery(
{
userId,
},
@@ -62,7 +62,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
);
const { mutateAsync, isError, error, isLoading } =
api.admin.assignPermissions.useMutation();
api.user.assignPermissions.useMutation();
const form = useForm<AddPermissions>({
defaultValues: {
@@ -92,7 +92,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
const onSubmit = async (data: AddPermissions) => {
await mutateAsync({
userId,
id: userId,
canCreateServices: data.canCreateServices,
canCreateProjects: data.canCreateProjects,
canDeleteServices: data.canDeleteServices,

View File

@@ -0,0 +1,208 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { format, isPast } from "date-fns";
import { Mail, MoreHorizontal, Users } from "lucide-react";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import { AddInvitation } from "./add-invitation";
export const ShowInvitations = () => {
const { data, isLoading, refetch } =
api.organization.allInvitations.useQuery();
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<Mail className="size-6 text-muted-foreground self-center" />
Invitations
</CardTitle>
<CardDescription>
Create invitations to your organization.
</CardDescription>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<>
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<Users className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground">
Invite users to your organization
</span>
<AddInvitation />
</div>
) : (
<div className="flex flex-col gap-4 min-h-[25vh]">
<Table>
<TableCaption>See all invitations</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Email</TableHead>
<TableHead className="text-center">Role</TableHead>
<TableHead className="text-center">Status</TableHead>
<TableHead className="text-center">
Expires At
</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.map((invitation) => {
const isExpired = isPast(
new Date(invitation.expiresAt),
);
return (
<TableRow key={invitation.id}>
<TableCell className="w-[100px]">
{invitation.email}
</TableCell>
<TableCell className="text-center">
<Badge
variant={
invitation.role === "owner"
? "default"
: "secondary"
}
>
{invitation.role}
</Badge>
</TableCell>
<TableCell className="text-center">
<Badge
variant={
invitation.status === "pending"
? "secondary"
: invitation.status === "canceled"
? "destructive"
: "default"
}
>
{invitation.status}
</Badge>
</TableCell>
<TableCell className="text-center">
{format(new Date(invitation.expiresAt), "PPpp")}{" "}
{isExpired ? (
<span className="text-muted-foreground">
(Expired)
</span>
) : null}
</TableCell>
<TableCell className="text-right flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>
Actions
</DropdownMenuLabel>
{!isExpired && (
<>
{invitation.status === "pending" && (
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(e) => {
copy(
`${origin}/invitation?token=${invitation.id}`,
);
toast.success(
"Invitation Copied to clipboard",
);
}}
>
Copy Invitation
</DropdownMenuItem>
)}
{invitation.status === "pending" && (
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={async (e) => {
const result =
await authClient.organization.cancelInvitation(
{
invitationId: invitation.id,
},
);
if (result.error) {
toast.error(
result.error.message,
);
} else {
toast.success(
"Invitation deleted",
);
refetch();
}
}}
>
Cancel Invitation
</DropdownMenuItem>
)}
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
<AddInvitation />
</div>
</div>
)}
</>
)}
</CardContent>
</div>
</Card>
</div>
);
};

View File

@@ -1,3 +1,4 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@@ -23,22 +24,19 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { format } from "date-fns";
import { MoreHorizontal, Users } from "lucide-react";
import { useEffect, useState } from "react";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import { AddUserPermissions } from "./add-permissions";
import { AddUser } from "./add-user";
import { DialogAction } from "@/components/shared/dialog-action";
import { Loader2 } from "lucide-react";
export const ShowUsers = () => {
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data, isLoading, refetch } = api.user.all.useQuery();
const { mutateAsync, isLoading: isRemoving } =
api.admin.removeUser.useMutation();
const { mutateAsync, isLoading: isRemoving } = api.user.remove.useMutation();
return (
<div className="w-full">
@@ -67,7 +65,6 @@ export const ShowUsers = () => {
<span className="text-base text-muted-foreground">
Invite users to your Dokploy account
</span>
<AddUser />
</div>
) : (
<div className="flex flex-col gap-4 min-h-[25vh]">
@@ -76,43 +73,41 @@ export const ShowUsers = () => {
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Email</TableHead>
<TableHead className="text-center">Status</TableHead>
<TableHead className="text-center">Role</TableHead>
<TableHead className="text-center">2FA</TableHead>
<TableHead className="text-center">
Expiration
Created At
</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.map((user) => {
{data?.map((member) => {
return (
<TableRow key={user.userId}>
<TableRow key={member.id}>
<TableCell className="w-[100px]">
{user.auth.email}
{member.user.email}
</TableCell>
<TableCell className="text-center">
<Badge
variant={
user.isRegistered ? "default" : "secondary"
member.role === "owner"
? "default"
: "secondary"
}
>
{user.isRegistered
? "Registered"
: "Not Registered"}
{member.role}
</Badge>
</TableCell>
<TableCell className="text-center">
{user.auth.is2FAEnabled
? "2FA Enabled"
: "2FA Not Enabled"}
{member.user.twoFactorEnabled
? "Enabled"
: "Disabled"}
</TableCell>
<TableCell className="text-right">
<TableCell className="text-center">
<span className="text-sm text-muted-foreground">
{format(
new Date(user.expirationDate),
"PPpp",
)}
{format(new Date(member.createdAt), "PPpp")}
</span>
</TableCell>
@@ -131,56 +126,63 @@ export const ShowUsers = () => {
<DropdownMenuLabel>
Actions
</DropdownMenuLabel>
{!user.isRegistered && (
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(e) => {
copy(
`${origin}/invitation?token=${user.token}`,
);
toast.success(
"Invitation Copied to clipboard",
);
}}
>
Copy Invitation
</DropdownMenuItem>
)}
{user.isRegistered && (
{member.role !== "owner" && (
<AddUserPermissions
userId={user.userId}
userId={member.user.id}
/>
)}
<DialogAction
title="Delete User"
description="Are you sure you want to delete this user?"
type="destructive"
onClick={async () => {
await mutateAsync({
authId: user.authId,
})
.then(() => {
toast.success(
"User deleted successfully",
);
refetch();
})
.catch(() => {
toast.error(
"Error deleting destination",
);
});
}}
>
<DropdownMenuItem
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
onSelect={(e) => e.preventDefault()}
{member.role !== "owner" && (
<DialogAction
title="Delete User"
description="Are you sure you want to delete this user?"
type="destructive"
onClick={async () => {
if (isCloud) {
const { error } =
await authClient.organization.removeMember(
{
memberIdOrEmail: member.id,
},
);
if (!error) {
toast.success(
"User deleted successfully",
);
refetch();
} else {
toast.error(
"Error deleting user",
);
}
} else {
await mutateAsync({
userId: member.user.id,
})
.then(() => {
toast.success(
"User deleted successfully",
);
refetch();
})
.catch(() => {
toast.error(
"Error deleting destination",
);
});
}
}}
>
Delete User
</DropdownMenuItem>
</DialogAction>
<DropdownMenuItem
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
onSelect={(e) => e.preventDefault()}
>
Delete User
</DropdownMenuItem>
</DialogAction>
)}
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
@@ -189,10 +191,6 @@ export const ShowUsers = () => {
})}
</TableBody>
</Table>
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
<AddUser />
</div>
</div>
)}
</>

View File

@@ -52,7 +52,7 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
export const WebDomain = () => {
const { t } = useTranslation("settings");
const { data: user, refetch } = api.admin.one.useQuery();
const { data, refetch } = api.user.get.useQuery();
const { mutateAsync, isLoading } =
api.settings.assignDomainServer.useMutation();
@@ -65,14 +65,14 @@ export const WebDomain = () => {
resolver: zodResolver(addServerDomain),
});
useEffect(() => {
if (user) {
if (data) {
form.reset({
domain: user?.host || "",
certificateType: user?.certificateType,
letsEncryptEmail: user?.letsEncryptEmail || "",
domain: data?.user?.host || "",
certificateType: data?.user?.certificateType,
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
});
}
}, [form, form.reset, user]);
}, [form, form.reset, data]);
const onSubmit = async (data: AddServerDomain) => {
await mutateAsync({

View File

@@ -21,7 +21,7 @@ interface Props {
}
export const WebServer = ({ className }: Props) => {
const { t } = useTranslation("settings");
const { data } = api.admin.one.useQuery();
const { data } = api.user.get.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
@@ -58,7 +58,7 @@ export const WebServer = ({ className }: Props) => {
<div className="flex items-center flex-wrap justify-between gap-4">
<span className="text-sm text-muted-foreground">
Server IP: {data?.serverIp}
Server IP: {data?.user.serverIp}
</span>
<span className="text-sm text-muted-foreground">
Version: {dokployVersion}

View File

@@ -1,5 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -47,15 +46,15 @@ interface Props {
export const UpdateServerIp = ({ children, serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data } = api.admin.one.useQuery();
const { data } = api.user.get.useQuery();
const { data: ip } = api.server.publicIp.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.admin.update.useMutation();
api.user.update.useMutation();
const form = useForm<Schema>({
defaultValues: {
serverIp: data?.serverIp || "",
serverIp: data?.user.serverIp || "",
},
resolver: zodResolver(schema),
});
@@ -63,7 +62,7 @@ export const UpdateServerIp = ({ children, serverId }: Props) => {
useEffect(() => {
if (data) {
form.reset({
serverIp: data.serverIp || "",
serverIp: data.user.serverIp || "",
});
}
}, [form, form.reset, data]);

View File

@@ -1,6 +1,7 @@
"use client";
import {
Activity,
AudioWaveform,
BarChartHorizontalBigIcon,
Bell,
BlocksIcon,
@@ -8,6 +9,7 @@ import {
Boxes,
ChevronRight,
CircleHelp,
Command,
CreditCard,
Database,
Folder,
@@ -16,11 +18,13 @@ import {
GitBranch,
HeartIcon,
KeyRound,
Loader2,
type LucideIcon,
Package,
PieChart,
Server,
ShieldCheck,
Trash2,
User,
Users,
} from "lucide-react";
@@ -74,7 +78,6 @@ import { UserNav } from "./user-nav";
// The types of the queries we are going to use
type AuthQueryOutput = inferRouterOutputs<AppRouter>["auth"]["get"];
type UserQueryOutput = inferRouterOutputs<AppRouter>["user"]["byAuthId"];
type SingleNavItem = {
isSingle?: true;
@@ -83,7 +86,6 @@ type SingleNavItem = {
icon?: LucideIcon;
isEnabled?: (opts: {
auth?: AuthQueryOutput;
user?: UserQueryOutput;
isCloud: boolean;
}) => boolean;
};
@@ -101,7 +103,6 @@ type NavItem =
items: SingleNavItem[];
isEnabled?: (opts: {
auth?: AuthQueryOutput;
user?: UserQueryOutput;
isCloud: boolean;
}) => boolean;
};
@@ -114,7 +115,6 @@ type ExternalLink = {
icon: React.ComponentType<{ className?: string }>;
isEnabled?: (opts: {
auth?: AuthQueryOutput;
user?: UserQueryOutput;
isCloud: boolean;
}) => boolean;
};
@@ -145,7 +145,7 @@ const MENU: Menu = {
url: "/dashboard/monitoring",
icon: BarChartHorizontalBigIcon,
// Only enabled in non-cloud environments
isEnabled: ({ auth, user, isCloud }) => !isCloud,
isEnabled: ({ auth, isCloud }) => !isCloud,
},
{
isSingle: true,
@@ -153,9 +153,9 @@ const MENU: Menu = {
url: "/dashboard/traefik",
icon: GalleryVerticalEnd,
// Only enabled for admins and users with access to Traefik files in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
isEnabled: ({ auth, isCloud }) =>
!!(
(auth?.rol === "admin" || user?.canAccessToTraefikFiles) &&
(auth?.role === "owner" || auth?.user?.canAccessToTraefikFiles) &&
!isCloud
),
},
@@ -165,8 +165,11 @@ const MENU: Menu = {
url: "/dashboard/docker",
icon: BlocksIcon,
// Only enabled for admins and users with access to Docker in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
isEnabled: ({ auth, isCloud }) =>
!!(
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
!isCloud
),
},
{
isSingle: true,
@@ -174,8 +177,11 @@ const MENU: Menu = {
url: "/dashboard/swarm",
icon: PieChart,
// Only enabled for admins and users with access to Docker in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
isEnabled: ({ auth, isCloud }) =>
!!(
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
!isCloud
),
},
{
isSingle: true,
@@ -183,8 +189,11 @@ const MENU: Menu = {
url: "/dashboard/requests",
icon: Forward,
// Only enabled for admins and users with access to Docker in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
isEnabled: ({ auth, isCloud }) =>
!!(
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
!isCloud
),
},
// Legacy unused menu, adjusted to the new structure
@@ -251,8 +260,7 @@ const MENU: Menu = {
url: "/dashboard/settings/server",
icon: Activity,
// Only enabled for admins in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!(auth?.rol === "admin" && !isCloud),
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
},
{
isSingle: true,
@@ -266,7 +274,7 @@ const MENU: Menu = {
url: "/dashboard/settings/servers",
icon: Server,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner"),
},
{
isSingle: true,
@@ -274,7 +282,7 @@ const MENU: Menu = {
icon: Users,
url: "/dashboard/settings/users",
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
},
{
isSingle: true,
@@ -282,8 +290,8 @@ const MENU: Menu = {
icon: KeyRound,
url: "/dashboard/settings/ssh-keys",
// Only enabled for admins and users with access to SSH keys
isEnabled: ({ auth, user }) =>
!!(auth?.rol === "admin" || user?.canAccessToSSHKeys),
isEnabled: ({ auth }) =>
!!(auth?.role === "owner" || auth?.user?.canAccessToSSHKeys),
},
{
isSingle: true,
@@ -291,8 +299,8 @@ const MENU: Menu = {
url: "/dashboard/settings/git-providers",
icon: GitBranch,
// Only enabled for admins and users with access to Git providers
isEnabled: ({ auth, user }) =>
!!(auth?.rol === "admin" || user?.canAccessToGitProviders),
isEnabled: ({ auth }) =>
!!(auth?.role === "owner" || auth?.user?.canAccessToGitProviders),
},
{
isSingle: true,
@@ -300,7 +308,7 @@ const MENU: Menu = {
url: "/dashboard/settings/registry",
icon: Package,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
},
{
isSingle: true,
@@ -308,7 +316,7 @@ const MENU: Menu = {
url: "/dashboard/settings/destinations",
icon: Database,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
},
{
@@ -317,7 +325,7 @@ const MENU: Menu = {
url: "/dashboard/settings/certificates",
icon: ShieldCheck,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
},
{
isSingle: true,
@@ -325,8 +333,7 @@ const MENU: Menu = {
url: "/dashboard/settings/cluster",
icon: Boxes,
// Only enabled for admins in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!(auth?.rol === "admin" && !isCloud),
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
},
{
isSingle: true,
@@ -334,7 +341,7 @@ const MENU: Menu = {
url: "/dashboard/settings/notifications",
icon: Bell,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
},
{
isSingle: true,
@@ -342,8 +349,7 @@ const MENU: Menu = {
url: "/dashboard/settings/billing",
icon: CreditCard,
// Only enabled for admins in cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!(auth?.rol === "admin" && isCloud),
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
},
],
@@ -379,7 +385,6 @@ const MENU: Menu = {
*/
function createMenuForAuthUser(opts: {
auth?: AuthQueryOutput;
user?: UserQueryOutput;
isCloud: boolean;
}): Menu {
return {
@@ -390,7 +395,6 @@ function createMenuForAuthUser(opts: {
? true
: item.isEnabled({
auth: opts.auth,
user: opts.user,
isCloud: opts.isCloud,
}),
),
@@ -401,7 +405,6 @@ function createMenuForAuthUser(opts: {
? true
: item.isEnabled({
auth: opts.auth,
user: opts.user,
isCloud: opts.isCloud,
}),
),
@@ -412,7 +415,6 @@ function createMenuForAuthUser(opts: {
? true
: item.isEnabled({
auth: opts.auth,
user: opts.user,
isCloud: opts.isCloud,
}),
),
@@ -480,37 +482,218 @@ interface Props {
function LogoWrapper() {
return <SidebarLogo />;
}
import { ChevronsUpDown, Plus } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { authClient } from "@/lib/auth-client";
import { toast } from "sonner";
import { AddOrganization } from "../dashboard/organization/handle-organization";
import { DialogAction } from "../shared/dialog-action";
import { Button } from "../ui/button";
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
teams: [
{
name: "Acme Inc",
logo: GalleryVerticalEnd,
plan: "Enterprise",
},
{
name: "Acme Corp.",
logo: AudioWaveform,
plan: "Startup",
},
{
name: "Evil Corp.",
logo: Command,
plan: "Free",
},
],
};
function SidebarLogo() {
const { state } = useSidebar();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: user } = api.user.get.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
const { data: session } = authClient.useSession();
const {
data: organizations,
refetch,
isLoading,
} = api.organization.all.useQuery();
const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
api.organization.delete.useMutation();
const { isMobile } = useSidebar();
const { data: activeOrganization } = authClient.useActiveOrganization();
const [activeTeam, setActiveTeam] = useState<
typeof activeOrganization | null
>(null);
useEffect(() => {
if (activeOrganization) {
setActiveTeam(activeOrganization);
}
}, [activeOrganization]);
return (
<Link
href="/dashboard/projects"
className="flex items-center gap-2 p-1 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]/35 rounded-lg "
>
<div
className={cn(
"flex aspect-square items-center justify-center rounded-lg transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
<>
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[5vh] pt-4">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
{/* <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> */}
<div
className={cn(
"flex aspect-square items-center justify-center rounded-lg transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
>
<Logo
className={cn(
"transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
/>
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{activeTeam?.name}
</span>
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Organizations
</DropdownMenuLabel>
{organizations?.map((org, index) => (
<div className="flex flex-row justify-between" key={org.name}>
<DropdownMenuItem
onClick={async () => {
await authClient.organization.setActive({
organizationId: org.id,
});
window.location.reload();
}}
className="w-full gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<Logo
className={cn(
"transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
/>
</div>
{org.name}
</DropdownMenuItem>
{(org.ownerId === session?.user?.id || isCloud) && (
<div className="flex items-center gap-2">
<AddOrganization organizationId={org.id} />
<DialogAction
title="Delete Organization"
description="Are you sure you want to delete this organization?"
type="destructive"
onClick={async () => {
await deleteOrganization({
organizationId: org.id,
})
.then(() => {
refetch();
toast.success(
"Organization deleted successfully",
);
})
.catch((error) => {
toast.error(
error?.message ||
"Error deleting organization",
);
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
)}
</div>
))}
{!isCloud && user?.role === "owner" && (
<>
<DropdownMenuSeparator />
<AddOrganization />
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)}
{/* <Link
href="/dashboard/projects"
className="flex items-center gap-2 p-1 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]/35 rounded-lg "
>
<Logo
<div
className={cn(
"transition-all",
"flex aspect-square items-center justify-center rounded-lg transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
/>
</div>
>
<Logo
className={cn(
"transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
/>
</div>
<div className="text-left text-sm leading-tight group-data-[state=open]/collapsible:rotate-90">
<p className="truncate font-semibold">Dokploy</p>
<p className="truncate text-xs text-muted-foreground">
{dokployVersion}
</p>
</div>
</Link>
<div className="text-left text-sm leading-tight group-data-[state=open]/collapsible:rotate-90">
<p className="truncate font-semibold">Dokploy</p>
<p className="truncate text-xs text-muted-foreground">
{dokployVersion}
</p>
</div>
</Link> */}
</>
);
}
@@ -531,15 +714,7 @@ export default function Page({ children }: Props) {
const router = useRouter();
const pathname = usePathname();
const currentPath = router.pathname;
const { data: auth } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { data: auth } = api.user.get.useQuery();
const includesProjects = pathname?.includes("/dashboard/project");
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
@@ -548,7 +723,7 @@ export default function Page({ children }: Props) {
home: filteredHome,
settings: filteredSettings,
help,
} = createMenuForAuthUser({ auth, user, isCloud: !!isCloud });
} = createMenuForAuthUser({ auth, isCloud: !!isCloud });
const activeItem = findActiveNavItem(
[...filteredHome, ...filteredSettings],
@@ -557,7 +732,7 @@ export default function Page({ children }: Props) {
// const showProjectsButton =
// currentPath === "/dashboard/projects" &&
// (auth?.rol === "admin" || user?.canCreateProjects);
// (auth?.rol === "owner" || user?.canCreateProjects);
return (
<SidebarProvider
@@ -577,12 +752,12 @@ export default function Page({ children }: Props) {
>
<Sidebar collapsible="icon" variant="floating">
<SidebarHeader>
<SidebarMenuButton
{/* <SidebarMenuButton
className="group-data-[collapsible=icon]:!p-0"
size="lg"
>
<LogoWrapper />
</SidebarMenuButton>
> */}
<LogoWrapper />
{/* </SidebarMenuButton> */}
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
@@ -783,7 +958,7 @@ export default function Page({ children }: Props) {
</SidebarMenuButton>
</SidebarMenuItem>
))}
{!isCloud && auth?.rol === "admin" && (
{!isCloud && auth?.role === "owner" && (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<UpdateServerButton />

View File

@@ -15,6 +15,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { authClient } from "@/lib/auth-client";
import { Languages } from "@/lib/languages";
import { api } from "@/utils/api";
import useLocale from "@/utils/hooks/use-locale";
@@ -29,18 +30,11 @@ const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
export const UserNav = () => {
const router = useRouter();
const { data } = api.auth.get.useQuery();
const { data } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: data?.id || "",
},
{
enabled: !!data?.id && data?.rol === "user",
},
);
const { locale, setLocale } = useLocale();
const { mutateAsync } = api.auth.logout.useMutation();
// const { mutateAsync } = api.auth.logout.useMutation();
return (
<DropdownMenu>
@@ -50,12 +44,15 @@ export const UserNav = () => {
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={data?.image || ""} alt={data?.image || ""} />
<AvatarImage
src={data?.user?.image || ""}
alt={data?.user?.image || ""}
/>
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">Account</span>
<span className="truncate text-xs">{data?.email}</span>
<span className="truncate text-xs">{data?.user?.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
@@ -70,7 +67,7 @@ export const UserNav = () => {
<DropdownMenuLabel className="flex flex-col">
My Account
<span className="text-xs font-normal text-muted-foreground">
{data?.email}
{data?.user?.email}
</span>
</DropdownMenuLabel>
<ModeToggle />
@@ -95,7 +92,8 @@ export const UserNav = () => {
>
Monitoring
</DropdownMenuItem>
{(data?.rol === "admin" || user?.canAccessToTraefikFiles) && (
{(data?.role === "owner" ||
data?.user?.canAccessToTraefikFiles) && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
@@ -105,7 +103,7 @@ export const UserNav = () => {
Traefik
</DropdownMenuItem>
)}
{(data?.rol === "admin" || user?.canAccessToDocker) && (
{(data?.role === "owner" || data?.user?.canAccessToDocker) && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
@@ -118,7 +116,7 @@ export const UserNav = () => {
</DropdownMenuItem>
)}
{data?.rol === "admin" && (
{data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
@@ -139,7 +137,7 @@ export const UserNav = () => {
>
Profile
</DropdownMenuItem>
{data?.rol === "admin" && (
{data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
@@ -150,7 +148,7 @@ export const UserNav = () => {
</DropdownMenuItem>
)}
{data?.rol === "admin" && (
{data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
@@ -163,7 +161,7 @@ export const UserNav = () => {
</>
)}
</DropdownMenuGroup>
{isCloud && data?.rol === "admin" && (
{isCloud && data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
@@ -178,9 +176,12 @@ export const UserNav = () => {
<DropdownMenuItem
className="cursor-pointer"
onClick={async () => {
await mutateAsync().then(() => {
await authClient.signOut().then(() => {
router.push("/");
});
// await mutateAsync().then(() => {
// router.push("/");
// });
}}
>
Log out

View File

@@ -0,0 +1,136 @@
CREATE TABLE "user_temp" (
"id" text PRIMARY KEY NOT NULL,
"name" text DEFAULT '' NOT NULL,
"token" text NOT NULL,
"isRegistered" boolean DEFAULT false NOT NULL,
"expirationDate" text NOT NULL,
"createdAt" text NOT NULL,
"canCreateProjects" boolean DEFAULT false NOT NULL,
"canAccessToSSHKeys" boolean DEFAULT false NOT NULL,
"canCreateServices" boolean DEFAULT false NOT NULL,
"canDeleteProjects" boolean DEFAULT false NOT NULL,
"canDeleteServices" boolean DEFAULT false NOT NULL,
"canAccessToDocker" boolean DEFAULT false NOT NULL,
"canAccessToAPI" boolean DEFAULT false NOT NULL,
"canAccessToGitProviders" boolean DEFAULT false NOT NULL,
"canAccessToTraefikFiles" boolean DEFAULT false NOT NULL,
"accesedProjects" text[] DEFAULT ARRAY[]::text[] NOT NULL,
"accesedServices" text[] DEFAULT ARRAY[]::text[] NOT NULL,
"two_factor_enabled" boolean DEFAULT false NOT NULL,
"email" text NOT NULL,
"email_verified" boolean NOT NULL,
"image" text,
"banned" boolean,
"ban_reason" text,
"ban_expires" timestamp,
"updated_at" timestamp NOT NULL,
"serverIp" text,
"certificateType" "certificateType" DEFAULT 'none' NOT NULL,
"host" text,
"letsEncryptEmail" text,
"sshPrivateKey" text,
"enableDockerCleanup" boolean DEFAULT false NOT NULL,
"enableLogRotation" boolean DEFAULT false NOT NULL,
"enablePaidFeatures" boolean DEFAULT false NOT NULL,
"metricsConfig" jsonb DEFAULT '{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb NOT NULL,
"cleanupCacheApplications" boolean DEFAULT false NOT NULL,
"cleanupCacheOnPreviews" boolean DEFAULT false NOT NULL,
"cleanupCacheOnCompose" boolean DEFAULT false NOT NULL,
"stripeCustomerId" text,
"stripeSubscriptionId" text,
"serversQuantity" integer DEFAULT 0 NOT NULL,
CONSTRAINT "user_temp_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "session_temp" (
"id" text PRIMARY KEY NOT NULL,
"expires_at" timestamp NOT NULL,
"token" text NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
"ip_address" text,
"user_agent" text,
"user_id" text NOT NULL,
"impersonated_by" text,
"active_organization_id" text,
CONSTRAINT "session_temp_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "account" (
"id" text PRIMARY KEY NOT NULL,
"account_id" text NOT NULL,
"provider_id" text NOT NULL,
"user_id" text NOT NULL,
"access_token" text,
"refresh_token" text,
"id_token" text,
"access_token_expires_at" timestamp,
"refresh_token_expires_at" timestamp,
"scope" text,
"password" text,
"is2FAEnabled" boolean DEFAULT false NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
"resetPasswordToken" text,
"resetPasswordExpiresAt" text,
"confirmationToken" text,
"confirmationExpiresAt" text
);
--> statement-breakpoint
CREATE TABLE "invitation" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"email" text NOT NULL,
"role" text,
"status" text NOT NULL,
"expires_at" timestamp NOT NULL,
"inviter_id" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "member" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"user_id" text NOT NULL,
"role" text NOT NULL,
"created_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "organization" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"slug" text,
"logo" text,
"created_at" timestamp NOT NULL,
"metadata" text,
"owner_id" text NOT NULL,
CONSTRAINT "organization_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
CREATE TABLE "verification" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp,
"updated_at" timestamp
);
CREATE TABLE "two_factor" (
"id" text PRIMARY KEY NOT NULL,
"secret" text NOT NULL,
"backup_codes" text NOT NULL,
"user_id" text NOT NULL
);
--> statement-breakpoint
ALTER TABLE "certificate" ALTER COLUMN "adminId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "notification" ALTER COLUMN "adminId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "ssh-key" ALTER COLUMN "adminId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "git_provider" ALTER COLUMN "adminId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "session_temp" ADD CONSTRAINT "session_temp_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_temp_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "organization" ADD CONSTRAINT "organization_owner_id_user_temp_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint

View File

@@ -0,0 +1,211 @@
-- Custom SQL migration file, put your code below! --
WITH inserted_users AS (
-- Insertar usuarios desde admins
INSERT INTO user_temp (
id,
email,
token,
"email_verified",
"updated_at",
"serverIp",
image,
"certificateType",
host,
"letsEncryptEmail",
"sshPrivateKey",
"enableDockerCleanup",
"enableLogRotation",
"enablePaidFeatures",
"metricsConfig",
"cleanupCacheApplications",
"cleanupCacheOnPreviews",
"cleanupCacheOnCompose",
"stripeCustomerId",
"stripeSubscriptionId",
"serversQuantity",
"expirationDate",
"createdAt",
"isRegistered"
)
SELECT
a."adminId",
auth.email,
COALESCE(auth.token, ''),
true,
CURRENT_TIMESTAMP,
a."serverIp",
auth.image,
a."certificateType",
a.host,
a."letsEncryptEmail",
a."sshPrivateKey",
a."enableDockerCleanup",
a."enableLogRotation",
a."enablePaidFeatures",
a."metricsConfig",
a."cleanupCacheApplications",
a."cleanupCacheOnPreviews",
a."cleanupCacheOnCompose",
a."stripeCustomerId",
a."stripeSubscriptionId",
a."serversQuantity",
NOW() + INTERVAL '1 year',
NOW(),
true
FROM admin a
JOIN auth ON auth.id = a."authId"
RETURNING *
),
inserted_accounts AS (
-- Insertar cuentas para los admins
INSERT INTO account (
id,
"account_id",
"provider_id",
"user_id",
password,
"created_at",
"updated_at"
)
SELECT
gen_random_uuid(),
gen_random_uuid(),
'credential',
a."adminId",
auth.password,
NOW(),
NOW()
FROM admin a
JOIN auth ON auth.id = a."authId"
RETURNING *
),
inserted_orgs AS (
-- Crear organizaciones para cada admin
INSERT INTO organization (
id,
name,
slug,
"owner_id",
"created_at"
)
SELECT
gen_random_uuid(),
'My Organization',
-- Generamos un slug único usando una función de hash
encode(sha256((a."adminId" || CURRENT_TIMESTAMP)::bytea), 'hex'),
a."adminId",
NOW()
FROM admin a
RETURNING *
),
inserted_members AS (
-- Insertar usuarios miembros
INSERT INTO user_temp (
id,
email,
token,
"email_verified",
"updated_at",
image,
"createdAt",
"canAccessToAPI",
"canAccessToDocker",
"canAccessToGitProviders",
"canAccessToSSHKeys",
"canAccessToTraefikFiles",
"canCreateProjects",
"canCreateServices",
"canDeleteProjects",
"canDeleteServices",
"accesedProjects",
"accesedServices",
"expirationDate",
"isRegistered"
)
SELECT
u."userId",
auth.email,
COALESCE(u.token, ''),
true,
CURRENT_TIMESTAMP,
auth.image,
NOW(),
COALESCE(u."canAccessToAPI", false),
COALESCE(u."canAccessToDocker", false),
COALESCE(u."canAccessToGitProviders", false),
COALESCE(u."canAccessToSSHKeys", false),
COALESCE(u."canAccessToTraefikFiles", false),
COALESCE(u."canCreateProjects", false),
COALESCE(u."canCreateServices", false),
COALESCE(u."canDeleteProjects", false),
COALESCE(u."canDeleteServices", false),
COALESCE(u."accesedProjects", '{}'),
COALESCE(u."accesedServices", '{}'),
NOW() + INTERVAL '1 year',
COALESCE(u."isRegistered", false)
FROM "user" u
JOIN admin a ON u."adminId" = a."adminId"
JOIN auth ON auth.id = u."authId"
RETURNING *
),
inserted_member_accounts AS (
-- Insertar cuentas para los usuarios miembros
INSERT INTO account (
id,
"account_id",
"provider_id",
"user_id",
password,
"created_at",
"updated_at"
)
SELECT
gen_random_uuid(),
gen_random_uuid(),
'credential',
u."userId",
auth.password,
NOW(),
NOW()
FROM "user" u
JOIN admin a ON u."adminId" = a."adminId"
JOIN auth ON auth.id = u."authId"
RETURNING *
),
inserted_admin_members AS (
-- Insertar miembros en las organizaciones (admins como owners)
INSERT INTO member (
id,
"organization_id",
"user_id",
role,
"created_at"
)
SELECT
gen_random_uuid(),
o.id,
a."adminId",
'owner',
NOW()
FROM admin a
JOIN inserted_orgs o ON o."owner_id" = a."adminId"
RETURNING *
)
-- Insertar miembros regulares en las organizaciones
INSERT INTO member (
id,
"organization_id",
"user_id",
role,
"created_at"
)
SELECT
gen_random_uuid(),
o.id,
u."userId",
'member',
NOW()
FROM "user" u
JOIN admin a ON u."adminId" = a."adminId"
JOIN inserted_orgs o ON o."owner_id" = a."adminId";

View File

@@ -0,0 +1,32 @@
ALTER TABLE "project" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "destination" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "certificate" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "registry" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "notification" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "ssh-key" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "git_provider" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "server" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "project" DROP CONSTRAINT "project_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "destination" DROP CONSTRAINT "destination_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "certificate" DROP CONSTRAINT "certificate_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "registry" DROP CONSTRAINT "registry_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "notification" DROP CONSTRAINT "notification_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "ssh-key" DROP CONSTRAINT "ssh-key_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "git_provider" DROP CONSTRAINT "git_provider_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "server" DROP CONSTRAINT "server_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "project" ADD CONSTRAINT "project_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "destination" ADD CONSTRAINT "destination_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "certificate" ADD CONSTRAINT "certificate_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "registry" ADD CONSTRAINT "registry_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notification" ADD CONSTRAINT "notification_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ssh-key" ADD CONSTRAINT "ssh-key_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "server" ADD CONSTRAINT "server_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "user_temp" ALTER COLUMN "token" SET DEFAULT '';--> statement-breakpoint
ALTER TABLE "user_temp" ADD COLUMN "created_at" timestamp DEFAULT now();

View File

@@ -0,0 +1,16 @@
ALTER TABLE "project" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "destination" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "certificate" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "registry" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "notification" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "ssh-key" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "git_provider" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "server" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "project" ADD CONSTRAINT "project_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "destination" ADD CONSTRAINT "destination_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "certificate" ADD CONSTRAINT "certificate_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "registry" ADD CONSTRAINT "registry_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notification" ADD CONSTRAINT "notification_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ssh-key" ADD CONSTRAINT "ssh-key_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "server" ADD CONSTRAINT "server_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,142 @@
-- Custom SQL migration file
-- Actualizar projects
UPDATE "project" p
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = p."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE p."organizationId" IS NULL;
-- Actualizar servers
UPDATE "server" s
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = s."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE s."organizationId" IS NULL;
-- Actualizar ssh-keys
UPDATE "ssh-key" k
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = k."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE k."organizationId" IS NULL;
-- Actualizar destinations
UPDATE "destination" d
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = d."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE d."organizationId" IS NULL;
-- Actualizar registry
UPDATE "registry" r
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = r."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE r."organizationId" IS NULL;
-- Actualizar notifications
UPDATE "notification" n
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = n."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE n."organizationId" IS NULL;
-- Actualizar certificates
UPDATE "certificate" c
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = c."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE c."organizationId" IS NULL;
-- Actualizar git_provider
UPDATE "git_provider" g
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = g."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE g."organizationId" IS NULL;
-- Verificar que todos los recursos tengan una organización
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM "project" WHERE "organizationId" IS NULL
UNION ALL
SELECT 1 FROM "server" WHERE "organizationId" IS NULL
UNION ALL
SELECT 1 FROM "ssh-key" WHERE "organizationId" IS NULL
UNION ALL
SELECT 1 FROM "destination" WHERE "organizationId" IS NULL
UNION ALL
SELECT 1 FROM "registry" WHERE "organizationId" IS NULL
UNION ALL
SELECT 1 FROM "notification" WHERE "organizationId" IS NULL
UNION ALL
SELECT 1 FROM "certificate" WHERE "organizationId" IS NULL
UNION ALL
SELECT 1 FROM "git_provider" WHERE "organizationId" IS NULL
) THEN
RAISE EXCEPTION 'Hay recursos sin organización asignada';
END IF;
END $$;
-- Hacer organization_id NOT NULL en todas las tablas
ALTER TABLE "project" ALTER COLUMN "organizationId" SET NOT NULL;
ALTER TABLE "server" ALTER COLUMN "organizationId" SET NOT NULL;
ALTER TABLE "ssh-key" ALTER COLUMN "organizationId" SET NOT NULL;
ALTER TABLE "destination" ALTER COLUMN "organizationId" SET NOT NULL;
ALTER TABLE "registry" ALTER COLUMN "organizationId" SET NOT NULL;
ALTER TABLE "notification" ALTER COLUMN "organizationId" SET NOT NULL;
ALTER TABLE "certificate" ALTER COLUMN "organizationId" SET NOT NULL;
ALTER TABLE "git_provider" ALTER COLUMN "organizationId" SET NOT NULL;
-- Crear índices para mejorar el rendimiento de búsquedas por organización
CREATE INDEX IF NOT EXISTS "idx_project_organization" ON "project" ("organizationId");
CREATE INDEX IF NOT EXISTS "idx_server_organization" ON "server" ("organizationId");
CREATE INDEX IF NOT EXISTS "idx_sshkey_organization" ON "ssh-key" ("organizationId");
CREATE INDEX IF NOT EXISTS "idx_destination_organization" ON "destination" ("organizationId");
CREATE INDEX IF NOT EXISTS "idx_registry_organization" ON "registry" ("organizationId");
CREATE INDEX IF NOT EXISTS "idx_notification_organization" ON "notification" ("organizationId");
CREATE INDEX IF NOT EXISTS "idx_certificate_organization" ON "certificate" ("organizationId");
CREATE INDEX IF NOT EXISTS "idx_git_provider_organization" ON "git_provider" ("organizationId");

View File

@@ -0,0 +1,32 @@
ALTER TABLE "project" DROP CONSTRAINT "project_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "destination" DROP CONSTRAINT "destination_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "certificate" DROP CONSTRAINT "certificate_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "registry" DROP CONSTRAINT "registry_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "notification" DROP CONSTRAINT "notification_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "ssh-key" DROP CONSTRAINT "ssh-key_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "git_provider" DROP CONSTRAINT "git_provider_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "server" DROP CONSTRAINT "server_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "project" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "destination" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "certificate" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "registry" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "notification" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "ssh-key" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "git_provider" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "server" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "project" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "destination" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "certificate" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "registry" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "notification" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "ssh-key" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "git_provider" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "server" DROP COLUMN "userId";

View File

@@ -0,0 +1,6 @@
--> statement-breakpoint
DROP TABLE "user" CASCADE;--> statement-breakpoint
DROP TABLE "admin" CASCADE;--> statement-breakpoint
DROP TABLE "auth" CASCADE;--> statement-breakpoint
DROP TABLE "session" CASCADE;--> statement-breakpoint
DROP TYPE "public"."Roles";

View File

@@ -0,0 +1,18 @@
ALTER TABLE "account" DROP CONSTRAINT "account_user_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "invitation" DROP CONSTRAINT "invitation_organization_id_organization_id_fk";
--> statement-breakpoint
ALTER TABLE "invitation" DROP CONSTRAINT "invitation_inviter_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "member" DROP CONSTRAINT "member_organization_id_organization_id_fk";
--> statement-breakpoint
ALTER TABLE "member" DROP CONSTRAINT "member_user_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "organization" DROP CONSTRAINT "organization_owner_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_temp_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "organization" ADD CONSTRAINT "organization_owner_id_user_temp_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,3 @@
ALTER TABLE "session_temp" DROP CONSTRAINT "session_temp_user_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "session_temp" ADD CONSTRAINT "session_temp_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;

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

View File

@@ -463,6 +463,76 @@
"when": 1739087857244,
"tag": "0065_daily_zaladane",
"breakpoints": true
},
{
"idx": 66,
"version": "7",
"when": 1739426913392,
"tag": "0066_yielding_echo",
"breakpoints": true
},
{
"idx": 67,
"version": "7",
"when": 1739427057545,
"tag": "0067_migrate-data",
"breakpoints": true
},
{
"idx": 68,
"version": "7",
"when": 1739428942964,
"tag": "0068_sour_professor_monster",
"breakpoints": true
},
{
"idx": 69,
"version": "7",
"when": 1739664410814,
"tag": "0069_broad_ken_ellis",
"breakpoints": true
},
{
"idx": 70,
"version": "7",
"when": 1739671869809,
"tag": "0070_nervous_vivisector",
"breakpoints": true
},
{
"idx": 71,
"version": "7",
"when": 1739671878698,
"tag": "0071_migrate-data-projects",
"breakpoints": true
},
{
"idx": 72,
"version": "7",
"when": 1739672367223,
"tag": "0072_lazy_pixie",
"breakpoints": true
},
{
"idx": 73,
"version": "7",
"when": 1739740193879,
"tag": "0073_polite_miss_america",
"breakpoints": true
},
{
"idx": 74,
"version": "7",
"when": 1739773539709,
"tag": "0074_lowly_jack_power",
"breakpoints": true
},
{
"idx": 75,
"version": "7",
"when": 1739781534192,
"tag": "0075_heavy_metal_master",
"breakpoints": true
}
]
}

View File

@@ -0,0 +1,8 @@
import { organizationClient } from "better-auth/client/plugins";
import { twoFactorClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
// baseURL: "http://localhost:3000", // the base url of your auth server
plugins: [organizationClient(), twoFactorClient()],
});

150
apps/dokploy/migrate.ts Normal file
View File

@@ -0,0 +1,150 @@
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import { nanoid } from "nanoid";
import postgres from "postgres";
import * as schema from "./server/db/schema";
const connectionString = process.env.DATABASE_URL!;
const sql = postgres(connectionString, { max: 1 });
const db = drizzle(sql, {
schema,
});
await db
.transaction(async (db) => {
const admins = await db.query.admins.findMany({
with: {
auth: true,
users: {
with: {
auth: true,
},
},
},
});
for (const admin of admins) {
const user = await db
.insert(schema.users_temp)
.values({
id: admin.adminId,
email: admin.auth.email,
token: admin.auth.token || "",
emailVerified: true,
updatedAt: new Date(),
role: "admin",
serverIp: admin.serverIp,
image: admin.auth.image,
certificateType: admin.certificateType,
host: admin.host,
letsEncryptEmail: admin.letsEncryptEmail,
sshPrivateKey: admin.sshPrivateKey,
enableDockerCleanup: admin.enableDockerCleanup,
enableLogRotation: admin.enableLogRotation,
enablePaidFeatures: admin.enablePaidFeatures,
metricsConfig: admin.metricsConfig,
cleanupCacheApplications: admin.cleanupCacheApplications,
cleanupCacheOnPreviews: admin.cleanupCacheOnPreviews,
cleanupCacheOnCompose: admin.cleanupCacheOnCompose,
stripeCustomerId: admin.stripeCustomerId,
stripeSubscriptionId: admin.stripeSubscriptionId,
serversQuantity: admin.serversQuantity,
})
.returning()
.then((user) => user[0]);
await db.insert(schema.account).values({
providerId: "credential",
userId: user?.id || "",
password: admin.auth.password,
is2FAEnabled: admin.auth.is2FAEnabled || false,
createdAt: new Date(admin.auth.createdAt) || new Date(),
updatedAt: new Date(admin.auth.createdAt) || new Date(),
});
const organization = await db
.insert(schema.organization)
.values({
name: "My Organization",
slug: nanoid(),
ownerId: user?.id || "",
createdAt: new Date(admin.createdAt) || new Date(),
})
.returning()
.then((organization) => organization[0]);
for (const member of admin.users) {
const userTemp = await db
.insert(schema.users_temp)
.values({
id: member.userId,
email: member.auth.email,
token: member.token || "",
emailVerified: true,
updatedAt: new Date(admin.createdAt) || new Date(),
role: "user",
image: member.auth.image,
createdAt: admin.createdAt,
canAccessToAPI: member.canAccessToAPI || false,
canAccessToDocker: member.canAccessToDocker || false,
canAccessToGitProviders: member.canAccessToGitProviders || false,
canAccessToSSHKeys: member.canAccessToSSHKeys || false,
canAccessToTraefikFiles: member.canAccessToTraefikFiles || false,
canCreateProjects: member.canCreateProjects || false,
canCreateServices: member.canCreateServices || false,
canDeleteProjects: member.canDeleteProjects || false,
canDeleteServices: member.canDeleteServices || false,
accessedProjects: member.accessedProjects || [],
accessedServices: member.accessedServices || [],
})
.returning()
.then((userTemp) => userTemp[0]);
await db.insert(schema.account).values({
providerId: "credential",
userId: member?.userId || "",
password: member.auth.password,
is2FAEnabled: member.auth.is2FAEnabled || false,
createdAt: new Date(member.auth.createdAt) || new Date(),
updatedAt: new Date(member.auth.createdAt) || new Date(),
});
await db.insert(schema.member).values({
organizationId: organization?.id || "",
userId: userTemp?.id || "",
role: "admin",
createdAt: new Date(member.createdAt) || new Date(),
});
}
}
})
.then(() => {
console.log("Migration finished");
})
.catch((error) => {
console.error(error);
});
await db
.transaction(async (db) => {
const projects = await db.query.projects.findMany({
with: {
user: {
with: {
organizations: true,
},
},
},
});
for (const project of projects) {
const user = await db.update(schema.projects).set({
organizationId: project.user.organizations[0]?.id || "",
});
}
})
.then(() => {
console.log("Migration finished");
})
.catch((error) => {
console.error(error);
});

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.18.3",
"version": "v0.18.2",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -16,6 +16,7 @@
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
"migration:run": "tsx -r dotenv/config migration.ts",
"manual-migration:run": "tsx -r dotenv/config migrate.ts",
"migration:up": "drizzle-kit up --config ./server/db/drizzle.config.ts",
"migration:drop": "drizzle-kit drop --config ./server/db/drizzle.config.ts",
"db:push": "drizzle-kit push --config ./server/db/drizzle.config.ts",
@@ -35,6 +36,7 @@
"test": "vitest --config __test__/vitest.config.ts"
},
"dependencies": {
"better-auth": "1.1.16",
"bl": "6.0.11",
"rotating-file-stream": "3.2.3",
"qrcode": "^1.5.3",

View File

@@ -0,0 +1,30 @@
import { Button } from "@/components/ui/button";
import { authClient } from "@/lib/auth-client";
import { useRouter } from "next/router";
export const AcceptInvitation = () => {
const { query } = useRouter();
const invitationId = query["accept-invitation"];
// const { data: organization } = api.organization.getById.useQuery({
// id: id as string
// })
return (
<div>
<Button
onClick={async () => {
const result = await authClient.organization.acceptInvitation({
invitationId: invitationId as string,
});
console.log(result);
}}
>
Accept Invitation
</Button>
</div>
);
};
export default AcceptInvitation;

View File

@@ -0,0 +1,7 @@
import { auth } from "@dokploy/server/index";
import { toNodeHandler } from "better-auth/node";
// Disallow body parsing, we will parse it manually
export const config = { api: { bodyParser: false } };
export default toNodeHandler(auth.handler);

View File

@@ -1,10 +1,12 @@
import { db } from "@/server/db";
import { github } from "@/server/db/schema";
import {
auth,
createGithub,
findAdminByAuthId,
findAuthById,
findUserByAuthId,
findUserById,
} from "@dokploy/server";
import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
@@ -28,7 +30,7 @@ export default async function handler(
return res.status(400).json({ error: "Missing code parameter" });
}
const [action, value] = state?.split(":");
// Value could be the authId or the githubProviderId
// Value could be the organizationId or the githubProviderId
if (action === "gh_init") {
const octokit = new Octokit({});
@@ -39,17 +41,6 @@ export default async function handler(
},
);
const auth = await findAuthById(value as string);
let adminId = "";
if (auth.rol === "admin") {
const admin = await findAdminByAuthId(auth.id);
adminId = admin.adminId;
} else {
const user = await findUserByAuthId(auth.id);
adminId = user.adminId;
}
await createGithub(
{
name: data.name,
@@ -60,7 +51,7 @@ export default async function handler(
githubWebhookSecret: data.webhook_secret,
githubPrivateKey: data.pem,
},
adminId,
value as string,
);
} else if (action === "gh_setup") {
await db

View File

@@ -1,7 +1,7 @@
import { buffer } from "node:stream/consumers";
import { db } from "@/server/db";
import { admins, server } from "@/server/db/schema";
import { findAdminById } from "@dokploy/server";
import { server, users_temp } from "@/server/db/schema";
import { findAdminById, findUserById } from "@dokploy/server";
import { asc, eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
@@ -64,33 +64,35 @@ export default async function handler(
session.subscription as string,
);
await db
.update(admins)
.update(users_temp)
.set({
stripeCustomerId: session.customer as string,
stripeSubscriptionId: session.subscription as string,
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
})
.where(eq(admins.adminId, adminId))
.where(eq(users_temp.id, adminId))
.returning();
const admin = await findAdminById(adminId);
const admin = await findUserById(adminId);
if (!admin) {
return res.status(400).send("Webhook Error: Admin not found");
}
const newServersQuantity = admin.serversQuantity;
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
break;
}
case "customer.subscription.created": {
const newSubscription = event.data.object as Stripe.Subscription;
await db
.update(admins)
.update(users_temp)
.set({
stripeSubscriptionId: newSubscription.id,
stripeCustomerId: newSubscription.customer as string,
})
.where(eq(admins.stripeCustomerId, newSubscription.customer as string))
.where(
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
)
.returning();
break;
@@ -100,14 +102,16 @@ export default async function handler(
const newSubscription = event.data.object as Stripe.Subscription;
await db
.update(admins)
.update(users_temp)
.set({
stripeSubscriptionId: null,
serversQuantity: 0,
})
.where(eq(admins.stripeCustomerId, newSubscription.customer as string));
.where(
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
);
const admin = await findAdminByStripeCustomerId(
const admin = await findUserByStripeCustomerId(
newSubscription.customer as string,
);
@@ -115,13 +119,13 @@ export default async function handler(
return res.status(400).send("Webhook Error: Admin not found");
}
await disableServers(admin.adminId);
await disableServers(admin.id);
break;
}
case "customer.subscription.updated": {
const newSubscription = event.data.object as Stripe.Subscription;
const admin = await findAdminByStripeCustomerId(
const admin = await findUserByStripeCustomerId(
newSubscription.customer as string,
);
@@ -131,23 +135,23 @@ export default async function handler(
if (newSubscription.status === "active") {
await db
.update(admins)
.update(users_temp)
.set({
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
})
.where(
eq(admins.stripeCustomerId, newSubscription.customer as string),
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
);
const newServersQuantity = admin.serversQuantity;
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
} else {
await disableServers(admin.adminId);
await disableServers(admin.id);
await db
.update(admins)
.update(users_temp)
.set({ serversQuantity: 0 })
.where(
eq(admins.stripeCustomerId, newSubscription.customer as string),
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
);
}
@@ -174,7 +178,7 @@ export default async function handler(
})
.where(eq(admins.stripeCustomerId, suscription.customer as string));
const admin = await findAdminByStripeCustomerId(
const admin = await findUserByStripeCustomerId(
suscription.customer as string,
);
@@ -182,7 +186,7 @@ export default async function handler(
return res.status(400).send("Webhook Error: Admin not found");
}
const newServersQuantity = admin.serversQuantity;
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
break;
}
case "invoice.payment_failed": {
@@ -193,7 +197,7 @@ export default async function handler(
);
if (subscription.status !== "active") {
const admin = await findAdminByStripeCustomerId(
const admin = await findUserByStripeCustomerId(
newInvoice.customer as string,
);
@@ -207,7 +211,7 @@ export default async function handler(
})
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
await disableServers(admin.adminId);
await disableServers(admin.id);
}
break;
@@ -216,20 +220,20 @@ export default async function handler(
case "customer.deleted": {
const customer = event.data.object as Stripe.Customer;
const admin = await findAdminByStripeCustomerId(customer.id);
const admin = await findUserByStripeCustomerId(customer.id);
if (!admin) {
return res.status(400).send("Webhook Error: Admin not found");
}
await disableServers(admin.adminId);
await disableServers(admin.id);
await db
.update(admins)
.update(users_temp)
.set({
stripeCustomerId: null,
stripeSubscriptionId: null,
serversQuantity: 0,
})
.where(eq(admins.stripeCustomerId, customer.id));
.where(eq(users_temp.stripeCustomerId, customer.id));
break;
}
@@ -240,20 +244,20 @@ export default async function handler(
return res.status(200).json({ received: true });
}
const disableServers = async (adminId: string) => {
const disableServers = async (userId: string) => {
await db
.update(server)
.set({
serverStatus: "inactive",
})
.where(eq(server.adminId, adminId));
.where(eq(server.userId, userId));
};
const findAdminByStripeCustomerId = async (stripeCustomerId: string) => {
const admin = db.query.admins.findFirst({
where: eq(admins.stripeCustomerId, stripeCustomerId),
const findUserByStripeCustomerId = async (stripeCustomerId: string) => {
const user = db.query.users_temp.findFirst({
where: eq(users_temp.stripeCustomerId, stripeCustomerId),
});
return admin;
return user;
};
const activateServer = async (serverId: string) => {
@@ -270,19 +274,19 @@ const deactivateServer = async (serverId: string) => {
.where(eq(server.serverId, serverId));
};
export const findServersByAdminIdSorted = async (adminId: string) => {
export const findServersByUserIdSorted = async (userId: string) => {
const servers = await db.query.server.findMany({
where: eq(server.adminId, adminId),
where: eq(server.userId, userId),
orderBy: asc(server.createdAt),
});
return servers;
};
export const updateServersBasedOnQuantity = async (
adminId: string,
userId: string,
newServersQuantity: number,
) => {
const servers = await findServersByAdminIdSorted(adminId);
const servers = await findServersByUserIdSorted(userId);
if (servers.length > newServersQuantity) {
for (const [index, server] of servers.entries()) {

View File

@@ -1,7 +1,8 @@
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
@@ -27,7 +28,7 @@ export async function getServerSideProps(
},
};
}
const { user, session } = await validateRequest(ctx.req, ctx.res);
const { user, session } = await validateRequest(ctx.req);
if (!user) {
return {
redirect: {
@@ -44,21 +45,20 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});
try {
await helpers.project.all.prefetch();
const auth = await helpers.auth.get.fetch();
if (auth.rol === "user") {
const user = await helpers.user.byAuthId.fetch({
authId: auth.id,
if (user.role === "member") {
const userR = await helpers.user.one.fetch({
userId: user.id,
});
if (!user.canAccessToDocker) {
if (!userR.canAccessToDocker) {
return {
redirect: {
permanent: true,

View File

@@ -8,7 +8,7 @@ import { Switch } from "@/components/ui/switch";
import { useLocalStorage } from "@/hooks/useLocalStorage";
import { api } from "@/utils/api";
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/index";
import { validateRequest } from "@dokploy/server/lib/auth";
import { Loader2 } from "lucide-react";
import type { GetServerSidePropsContext } from "next";
import type React from "react";
@@ -25,7 +25,7 @@ const Dashboard = () => {
false,
);
const { data: monitoring, isLoading } = api.admin.getMetricsToken.useQuery();
const { data: monitoring, isLoading } = api.user.getMetricsToken.useQuery();
return (
<div className="space-y-4 pb-10">
{/* <AlertBlock>
@@ -104,7 +104,7 @@ export async function getServerSideProps(
},
};
}
const { user } = await validateRequest(ctx.req, ctx.res);
const { user } = await validateRequest(ctx.req);
if (!user) {
return {
redirect: {

View File

@@ -49,7 +49,7 @@ import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import type { findProjectById } from "@dokploy/server";
import { validateRequest } from "@dokploy/server";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import {
Ban,
@@ -70,9 +70,9 @@ import type {
} from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import { useMemo, useState, type ReactElement } from "react";
import superjson from "superjson";
import { type ReactElement, useMemo, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
export type Services = {
appName: string;
@@ -200,15 +200,8 @@ const Project = (
) => {
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
const { projectId } = props;
const { data: auth } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { data: auth } = api.user.get.useQuery();
const { data, isLoading, refetch } = api.project.one.useQuery({ projectId });
const router = useRouter();
@@ -335,7 +328,7 @@ const Project = (
</CardTitle>
<CardDescription>{data?.description}</CardDescription>
</CardHeader>
{(auth?.rol === "admin" || user?.canCreateServices) && (
{(auth?.role === "owner" || auth?.user?.canCreateServices) && (
<div className="flex flex-row gap-4 flex-wrap">
<ProjectEnvironment projectId={projectId}>
<Button variant="outline">Project Environment</Button>
@@ -658,7 +651,7 @@ export async function getServerSideProps(
const { params } = ctx;
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
const { user, session } = await validateRequest(req);
if (!user) {
return {
redirect: {
@@ -674,8 +667,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -39,7 +39,7 @@ import {
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import copy from "copy-to-clipboard";
import { GlobeIcon, HelpCircle, ServerOff, Trash2 } from "lucide-react";
@@ -86,16 +86,8 @@ const Service = (
);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: auth } = api.auth.get.useQuery();
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { data: auth } = api.user.get.useQuery();
const { data: monitoring } = api.user.getMetricsToken.useQuery();
return (
<div className="pb-10">
@@ -186,7 +178,8 @@ const Service = (
<div className="flex flex-row gap-2 justify-end">
<UpdateApplication applicationId={applicationId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
{(auth?.role === "owner" ||
auth?.user?.canDeleteServices) && (
<DeleteService id={applicationId} type="application" />
)}
</div>
@@ -370,7 +363,7 @@ export async function getServerSideProps(
const { query, params, req, res } = ctx;
const activeTab = query.tab;
const { user, session } = await validateRequest(req, res);
const { user, session } = await validateRequest(req);
if (!user) {
return {
redirect: {
@@ -386,8 +379,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -33,7 +33,7 @@ import {
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import copy from "copy-to-clipboard";
import { CircuitBoard, ServerOff } from "lucide-react";
@@ -79,17 +79,9 @@ const Service = (
},
);
const { data: auth } = api.auth.get.useQuery();
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
const { data: auth } = api.user.get.useQuery();
const { data: monitoring } = api.user.getMetricsToken.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
return (
<div className="pb-10">
@@ -181,7 +173,8 @@ const Service = (
<div className="flex flex-row gap-2 justify-end">
<UpdateCompose composeId={composeId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
{(auth?.role === "owner" ||
auth?.user?.canDeleteServices) && (
<DeleteService id={composeId} type="compose" />
)}
</div>
@@ -366,7 +359,7 @@ export async function getServerSideProps(
const { query, params, req, res } = ctx;
const activeTab = query.tab;
const { user, session } = await validateRequest(req, res);
const { user, session } = await validateRequest(req);
if (!user) {
return {
redirect: {
@@ -382,8 +375,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -35,7 +35,7 @@ import {
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
import type {
@@ -61,16 +61,9 @@ const Mariadb = (
const { projectId } = router.query;
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.mariadb.one.useQuery({ mariadbId });
const { data: auth } = api.auth.get.useQuery();
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { data: auth } = api.user.get.useQuery();
const { data: monitoring } = api.user.getMetricsToken.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
return (
@@ -154,7 +147,8 @@ const Mariadb = (
</div>
<div className="flex flex-row gap-2 justify-end">
<UpdateMariadb mariadbId={mariadbId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
{(auth?.role === "owner" ||
auth?.user?.canDeleteServices) && (
<DeleteService id={mariadbId} type="mariadb" />
)}
</div>
@@ -316,7 +310,7 @@ export async function getServerSideProps(
const { query, params, req, res } = ctx;
const activeTab = query.tab;
const { user, session } = await validateRequest(req, res);
const { user, session } = await validateRequest(req);
if (!user) {
return {
redirect: {
@@ -332,8 +326,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -35,7 +35,7 @@ import {
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
import type {
@@ -61,16 +61,8 @@ const Mongo = (
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.mongo.one.useQuery({ mongoId });
const { data: auth } = api.auth.get.useQuery();
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { data: auth } = api.user.get.useQuery();
const { data: monitoring } = api.user.getMetricsToken.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
@@ -156,7 +148,8 @@ const Mongo = (
<div className="flex flex-row gap-2 justify-end">
<UpdateMongo mongoId={mongoId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
{(auth?.role === "owner" ||
auth?.user?.canDeleteServices) && (
<DeleteService id={mongoId} type="mongo" />
)}
</div>
@@ -318,7 +311,7 @@ export async function getServerSideProps(
const { query, params, req, res } = ctx;
const activeTab = query.tab;
const { user, session } = await validateRequest(req, res);
const { user, session } = await validateRequest(req);
if (!user) {
return {
redirect: {
@@ -334,8 +327,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -35,7 +35,7 @@ import {
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
import type {
@@ -60,16 +60,8 @@ const MySql = (
const { projectId } = router.query;
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.mysql.one.useQuery({ mysqlId });
const { data: auth } = api.auth.get.useQuery();
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { data: auth } = api.user.get.useQuery();
const { data: monitoring } = api.user.getMetricsToken.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
@@ -156,7 +148,8 @@ const MySql = (
<div className="flex flex-row gap-2 justify-end">
<UpdateMysql mysqlId={mysqlId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
{(auth?.role === "owner" ||
auth?.user?.canDeleteServices) && (
<DeleteService id={mysqlId} type="mysql" />
)}
</div>
@@ -323,7 +316,7 @@ export async function getServerSideProps(
const { query, params, req, res } = ctx;
const activeTab = query.tab;
const { user, session } = await validateRequest(req, res);
const { user, session } = await validateRequest(req);
if (!user) {
return {
redirect: {
@@ -339,8 +332,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -35,7 +35,7 @@ import {
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff, Trash2 } from "lucide-react";
import type {
@@ -60,16 +60,9 @@ const Postgresql = (
const { projectId } = router.query;
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.postgres.one.useQuery({ postgresId });
const { data: auth } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
const { data: auth } = api.user.get.useQuery();
const { data: monitoring } = api.user.getMetricsToken.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
return (
@@ -154,7 +147,8 @@ const Postgresql = (
<div className="flex flex-row gap-2 justify-end">
<UpdatePostgres postgresId={postgresId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
{(auth?.role === "owner" ||
auth?.user?.canDeleteServices) && (
<DeleteService id={postgresId} type="postgres" />
)}
</div>
@@ -319,7 +313,7 @@ export async function getServerSideProps(
) {
const { query, params, req, res } = ctx;
const activeTab = query.tab;
const { user, session } = await validateRequest(req, res);
const { user, session } = await validateRequest(req);
if (!user) {
return {
redirect: {
@@ -335,8 +329,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -34,7 +34,7 @@ import {
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
@@ -60,16 +60,8 @@ const Redis = (
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.redis.one.useQuery({ redisId });
const { data: auth } = api.auth.get.useQuery();
const { data: monitoring } = api.admin.getMetricsToken.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { data: auth } = api.user.get.useQuery();
const { data: monitoring } = api.user.getMetricsToken.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
@@ -155,7 +147,8 @@ const Redis = (
<div className="flex flex-row gap-2 justify-end">
<UpdateRedis redisId={redisId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
{(auth?.role === "owner" ||
auth?.user?.canDeleteServices) && (
<DeleteService id={redisId} type="redis" />
)}
</div>
@@ -311,7 +304,7 @@ export async function getServerSideProps(
const { query, params, req, res } = ctx;
const activeTab = query.tab;
const { user, session } = await validateRequest(req, res);
const { user, session } = await validateRequest(req);
if (!user) {
return {
redirect: {
@@ -327,8 +320,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -2,7 +2,7 @@ import { ShowProjects } from "@/components/dashboard/projects/show";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import dynamic from "next/dynamic";
@@ -38,7 +38,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
const { user, session } = await validateRequest(req);
const helpers = createServerSideHelpers({
router: appRouter,
@@ -46,8 +46,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -1,6 +1,7 @@
import { ShowRequests } from "@/components/dashboard/requests/show-requests";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/lib/auth";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import * as React from "react";
@@ -22,7 +23,7 @@ export async function getServerSideProps(
},
};
}
const { user } = await validateRequest(ctx.req, ctx.res);
const { user } = await validateRequest(ctx.req);
if (!user) {
return {
redirect: {

View File

@@ -2,7 +2,8 @@ import { ShowBilling } from "@/components/dashboard/settings/billing/show-billin
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
@@ -29,8 +30,8 @@ export async function getServerSideProps(
};
}
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
if (!user || user.rol === "user") {
const { user, session } = await validateRequest(req);
if (!user || user.role === "member") {
return {
redirect: {
permanent: true,
@@ -45,8 +46,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -24,8 +24,8 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
if (!user || user.rol === "user") {
const { user, session } = await validateRequest(req);
if (!user || user.role === "member") {
return {
redirect: {
permanent: true,
@@ -40,8 +40,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -33,8 +33,8 @@ export async function getServerSideProps(
},
};
}
const { user, session } = await validateRequest(ctx.req, ctx.res);
if (!user || user.rol === "user") {
const { user, session } = await validateRequest(ctx.req);
if (!user || user.role === "member") {
return {
redirect: {
permanent: true,
@@ -48,8 +48,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -25,8 +25,8 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
if (!user || user.rol === "user") {
const { user, session } = await validateRequest(req);
if (!user || user.role === "member") {
return {
redirect: {
permanent: true,
@@ -41,8 +41,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -24,7 +24,7 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user, session } = await validateRequest(ctx.req, ctx.res);
const { user, session } = await validateRequest(ctx.req);
if (!user) {
return {
redirect: {
@@ -40,8 +40,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});
@@ -49,14 +49,12 @@ export async function getServerSideProps(
try {
await helpers.project.all.prefetch();
await helpers.settings.isCloud.prefetch();
const auth = await helpers.auth.get.fetch();
if (auth.rol === "user") {
const user = await helpers.user.byAuthId.fetch({
authId: auth.id,
if (user.role === "member") {
const userR = await helpers.user.one.fetch({
userId: user.id,
});
if (!user.canAccessToGitProviders) {
if (!userR.canAccessToGitProviders) {
return {
redirect: {
permanent: true,

View File

@@ -42,9 +42,9 @@ const settings = z.object({
type SettingsType = z.infer<typeof settings>;
const Page = () => {
const { data, refetch } = api.admin.one.useQuery();
const { data, refetch } = api.user.get.useQuery();
const { mutateAsync, isLoading, isError, error } =
api.admin.update.useMutation();
api.user.update.useMutation();
const form = useForm<SettingsType>({
defaultValues: {
cleanCacheOnApplications: false,
@@ -55,9 +55,9 @@ const Page = () => {
});
useEffect(() => {
form.reset({
cleanCacheOnApplications: data?.cleanupCacheApplications || false,
cleanCacheOnCompose: data?.cleanupCacheOnCompose || false,
cleanCacheOnPreviews: data?.cleanupCacheOnPreviews || false,
cleanCacheOnApplications: data?.user.cleanupCacheApplications || false,
cleanCacheOnCompose: data?.user.cleanupCacheOnCompose || false,
cleanCacheOnPreviews: data?.user.cleanupCacheOnPreviews || false,
});
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
@@ -181,7 +181,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const { user, session } = await validateRequest(ctx.req, ctx.res);
const { user, session } = await validateRequest(ctx.req);
if (!user) {
return {
redirect: {
@@ -190,7 +190,7 @@ export async function getServerSideProps(
},
};
}
if (user.rol === "user") {
if (user.role === "member") {
return {
redirect: {
permanent: true,
@@ -205,8 +205,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -25,8 +25,8 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
if (!user || user.rol === "user") {
const { user, session } = await validateRequest(req);
if (!user || user.role === "member") {
return {
redirect: {
permanent: true,
@@ -41,8 +41,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -13,22 +13,16 @@ import React, { type ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
const { data } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: data?.id || "",
},
{
enabled: !!data?.id && data?.rol === "user",
},
);
const { data } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
return (
<div className="w-full">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<ProfileForm />
{(user?.canAccessToAPI || data?.rol === "admin") && <GenerateToken />}
{(data?.user?.canAccessToAPI || data?.role === "owner") && (
<GenerateToken />
)}
{isCloud && <RemoveSelfAccount />}
</div>
@@ -46,7 +40,7 @@ export async function getServerSideProps(
) {
const { req, res } = ctx;
const locale = getLocale(req.cookies);
const { user, session } = await validateRequest(req, res);
const { user, session } = await validateRequest(req);
const helpers = createServerSideHelpers({
router: appRouter,
@@ -54,18 +48,21 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
await helpers.auth.get.prefetch();
if (user?.rol === "user") {
await helpers.user.byAuthId.prefetch({
authId: user.authId,
});
if (user?.role === "member") {
// const userR = await helpers.user.one.fetch({
// userId: user.id,
// });
// await helpers.user.byAuthId.prefetch({
// authId: user.authId,
// });
}
if (!user) {

View File

@@ -25,8 +25,8 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
if (!user || user.rol === "user") {
const { user, session } = await validateRequest(req);
if (!user || user.role === "member") {
return {
redirect: {
permanent: true,
@@ -40,8 +40,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -2,19 +2,7 @@ import { SetupMonitoring } from "@/components/dashboard/settings/servers/setup-m
import { WebDomain } from "@/components/dashboard/settings/web-domain";
import { WebServer } from "@/components/dashboard/settings/web-server";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
@@ -25,8 +13,6 @@ import { toast } from "sonner";
import superjson from "superjson";
const Page = () => {
const { data, refetch } = api.admin.one.useQuery();
const { mutateAsync: update } = api.admin.update.useMutation();
return (
<div className="w-full">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
@@ -98,7 +84,7 @@ export async function getServerSideProps(
},
};
}
const { user, session } = await validateRequest(ctx.req, ctx.res);
const { user, session } = await validateRequest(ctx.req);
if (!user) {
return {
redirect: {
@@ -107,7 +93,7 @@ export async function getServerSideProps(
},
};
}
if (user.rol === "user") {
if (user.role === "member") {
return {
redirect: {
permanent: true,
@@ -122,8 +108,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -27,7 +27,7 @@ export async function getServerSideProps(
) {
const { req, res } = ctx;
const locale = await getLocale(req.cookies);
const { user, session } = await validateRequest(req, res);
const { user, session } = await validateRequest(req);
if (!user) {
return {
redirect: {
@@ -36,7 +36,7 @@ export async function getServerSideProps(
},
};
}
if (user.rol === "user") {
if (user.role === "member") {
return {
redirect: {
permanent: true,
@@ -51,8 +51,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -24,7 +24,7 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user, session } = await validateRequest(ctx.req, ctx.res);
const { user, session } = await validateRequest(ctx.req);
if (!user) {
return {
redirect: {
@@ -40,23 +40,22 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});
try {
await helpers.project.all.prefetch();
const auth = await helpers.auth.get.fetch();
await helpers.settings.isCloud.prefetch();
if (auth.rol === "user") {
const user = await helpers.user.byAuthId.fetch({
authId: auth.id,
if (user.role === "member") {
const userR = await helpers.user.one.fetch({
userId: user.id,
});
if (!user.canAccessToSSHKeys) {
if (!userR.canAccessToSSHKeys) {
return {
redirect: {
permanent: true,

View File

@@ -1,3 +1,4 @@
import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations";
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
@@ -12,6 +13,7 @@ const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowUsers />
<ShowInvitations />
</div>
);
};
@@ -25,8 +27,10 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
if (!user || user.rol === "user") {
const { user, session } = await validateRequest(req);
console.log("user", user, session);
if (!user || user.role === "member") {
return {
redirect: {
permanent: true,
@@ -41,8 +45,8 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});

View File

@@ -1,7 +1,8 @@
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
@@ -27,7 +28,7 @@ export async function getServerSideProps(
},
};
}
const { user, session } = await validateRequest(ctx.req, ctx.res);
const { user, session } = await validateRequest(ctx.req);
if (!user) {
return {
redirect: {
@@ -44,21 +45,20 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});
try {
await helpers.project.all.prefetch();
const auth = await helpers.auth.get.fetch();
if (auth.rol === "user") {
const user = await helpers.user.byAuthId.fetch({
authId: auth.id,
if (user.role === "member") {
const userR = await helpers.user.one.fetch({
userId: user.id,
});
if (!user.canAccessToDocker) {
if (!userR.canAccessToDocker) {
return {
redirect: {
permanent: true,

View File

@@ -1,7 +1,8 @@
import { ShowTraefikSystem } from "@/components/dashboard/file-system/show-traefik-system";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
@@ -27,7 +28,7 @@ export async function getServerSideProps(
},
};
}
const { user, session } = await validateRequest(ctx.req, ctx.res);
const { user, session } = await validateRequest(ctx.req);
if (!user) {
return {
redirect: {
@@ -44,21 +45,20 @@ export async function getServerSideProps(
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});
try {
await helpers.project.all.prefetch();
const auth = await helpers.auth.get.fetch();
if (auth.rol === "user") {
const user = await helpers.user.byAuthId.fetch({
authId: auth.id,
if (user.role === "member") {
const userR = await helpers.user.one.fetch({
userId: user.id,
});
if (!user.canAccessToTraefikFiles) {
if (!userR.canAccessToTraefikFiles) {
return {
redirect: {
permanent: true,

View File

@@ -3,99 +3,177 @@ import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { AlertBlock } from "@/components/shared/alert-block";
import { Logo } from "@/components/shared/logo";
import { Button, buttonVariants } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
import { CardContent, CardDescription } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";
import { authClient } from "@/lib/auth-client";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server";
import { IS_CLOUD, auth, isAdminPresent } from "@dokploy/server";
import { validateRequest } from "@dokploy/server/lib/auth";
import { zodResolver } from "@hookform/resolvers/zod";
import base32 from "hi-base32";
import { REGEXP_ONLY_DIGITS } from "input-otp";
import { AlertTriangle } from "lucide-react";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { TOTP } from "otpauth";
import { type ReactElement, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const loginSchema = z.object({
email: z
.string()
.min(1, {
message: "Email is required",
})
.email({
message: "Email must be a valid email",
}),
password: z
.string()
.min(1, {
message: "Password is required",
})
.min(8, {
message: "Password must be at least 8 characters",
}),
const LoginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
type Login = z.infer<typeof loginSchema>;
const TwoFactorSchema = z.object({
code: z.string().min(6),
});
type AuthResponse = {
is2FAEnabled: boolean;
authId: string;
};
const BackupCodeSchema = z.object({
code: z.string().min(8, {
message: "Backup code must be at least 8 characters",
}),
});
type LoginForm = z.infer<typeof LoginSchema>;
type BackupCodeForm = z.infer<typeof BackupCodeSchema>;
interface Props {
IS_CLOUD: boolean;
}
export default function Home({ IS_CLOUD }: Props) {
const [temp, setTemp] = useState<AuthResponse>({
is2FAEnabled: false,
authId: "",
});
const { mutateAsync, isLoading, error, isError } =
api.auth.login.useMutation();
const router = useRouter();
const form = useForm<Login>({
const [isLoginLoading, setIsLoginLoading] = useState(false);
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
const [isBackupCodeLoading, setIsBackupCodeLoading] = useState(false);
const [isTwoFactor, setIsTwoFactor] = useState(false);
const [error, setError] = useState<string | null>(null);
const [twoFactorCode, setTwoFactorCode] = useState("");
const [isBackupCodeModalOpen, setIsBackupCodeModalOpen] = useState(false);
const [backupCode, setBackupCode] = useState("");
const loginForm = useForm<LoginForm>({
resolver: zodResolver(LoginSchema),
defaultValues: {
email: "",
password: "",
email: "siumauricio@hotmail.com",
password: "Password123",
},
resolver: zodResolver(loginSchema),
});
useEffect(() => {
form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (values: Login) => {
await mutateAsync({
email: values.email.toLowerCase(),
password: values.password,
})
.then((data) => {
if (data.is2FAEnabled) {
setTemp(data);
} else {
toast.success("Successfully signed in", {
duration: 2000,
});
router.push("/dashboard/projects");
}
})
.catch(() => {
toast.error("Signin failed", {
duration: 2000,
});
const onSubmit = async (values: LoginForm) => {
setIsLoginLoading(true);
try {
const { data, error } = await authClient.signIn.email({
email: values.email,
password: values.password,
});
if (error) {
toast.error(error.message);
setError(error.message || "An error occurred while logging in");
return;
}
if (data?.twoFactorRedirect as boolean) {
setTwoFactorCode("");
setIsTwoFactor(true);
toast.info("Please enter your 2FA code");
return;
}
toast.success("Logged in successfully");
router.push("/dashboard/projects");
} catch (error) {
toast.error("An error occurred while logging in");
} finally {
setIsLoginLoading(false);
}
};
const onTwoFactorSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (twoFactorCode.length !== 6) {
toast.error("Please enter a valid 6-digit code");
return;
}
setIsTwoFactorLoading(true);
try {
const { data, error } = await authClient.twoFactor.verifyTotp({
code: twoFactorCode.replace(/\s/g, ""),
});
if (error) {
toast.error(error.message);
setError(error.message || "An error occurred while verifying 2FA code");
return;
}
toast.success("Logged in successfully");
router.push("/dashboard/projects");
} catch (error) {
toast.error("An error occurred while verifying 2FA code");
} finally {
setIsTwoFactorLoading(false);
}
};
const onBackupCodeSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (backupCode.length < 8) {
toast.error("Please enter a valid backup code");
return;
}
setIsBackupCodeLoading(true);
try {
const { data, error } = await authClient.twoFactor.verifyBackupCode({
code: backupCode.trim(),
});
if (error) {
toast.error(error.message);
setError(
error.message || "An error occurred while verifying backup code",
);
return;
}
toast.success("Logged in successfully");
router.push("/dashboard/projects");
} catch (error) {
toast.error("An error occurred while verifying backup code");
} finally {
setIsBackupCodeLoading(false);
}
};
return (
<>
<div className="flex flex-col space-y-2 text-center">
@@ -109,55 +187,169 @@ export default function Home({ IS_CLOUD }: Props) {
Enter your email and password to sign in
</p>
</div>
{isError && (
{error && (
<AlertBlock type="error" className="my-2">
<span>{error?.message}</span>
<span>{error}</span>
</AlertBlock>
)}
<CardContent className="p-0">
{!temp.is2FAEnabled ? (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-4">
<div className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" isLoading={isLoading} className="w-full">
Login
</Button>
</div>
{!isTwoFactor ? (
<Form {...loginForm}>
<form
onSubmit={loginForm.handleSubmit(onSubmit)}
className="space-y-4"
id="login-form"
>
<FormField
control={loginForm.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={loginForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
className="w-full"
type="submit"
isLoading={isLoginLoading}
>
Login
</Button>
</form>
</Form>
) : (
<Login2FA authId={temp.authId} />
<>
<form
onSubmit={onTwoFactorSubmit}
className="space-y-4"
id="two-factor-form"
autoComplete="off"
>
<div className="flex flex-col gap-2">
<Label>2FA Code</Label>
<InputOTP
value={twoFactorCode}
onChange={setTwoFactorCode}
maxLength={6}
pattern={REGEXP_ONLY_DIGITS}
autoComplete="off"
>
<InputOTPGroup>
<InputOTPSlot index={0} className="border-border" />
<InputOTPSlot index={1} className="border-border" />
<InputOTPSlot index={2} className="border-border" />
<InputOTPSlot index={3} className="border-border" />
<InputOTPSlot index={4} className="border-border" />
<InputOTPSlot index={5} className="border-border" />
</InputOTPGroup>
</InputOTP>
<CardDescription>
Enter the 6-digit code from your authenticator app
</CardDescription>
<button
type="button"
onClick={() => setIsBackupCodeModalOpen(true)}
className="text-sm text-muted-foreground hover:underline self-start mt-2"
>
Lost access to your authenticator app?
</button>
</div>
<div className="flex gap-4">
<Button
variant="outline"
className="w-full"
type="button"
onClick={() => {
setIsTwoFactor(false);
setTwoFactorCode("");
}}
>
Back
</Button>
<Button
className="w-full"
type="submit"
isLoading={isTwoFactorLoading}
>
Verify
</Button>
</div>
</form>
<Dialog
open={isBackupCodeModalOpen}
onOpenChange={setIsBackupCodeModalOpen}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Enter Backup Code</DialogTitle>
<DialogDescription>
Enter one of your backup codes to access your account
</DialogDescription>
</DialogHeader>
<form onSubmit={onBackupCodeSubmit} className="space-y-4">
<div className="flex flex-col gap-2">
<Label>Backup Code</Label>
<Input
value={backupCode}
onChange={(e) => setBackupCode(e.target.value)}
placeholder="Enter your backup code"
className="font-mono"
/>
<CardDescription>
Enter one of the backup codes you received when setting up
2FA
</CardDescription>
</div>
<div className="flex gap-4">
<Button
variant="outline"
className="w-full"
type="button"
onClick={() => {
setIsBackupCodeModalOpen(false);
setBackupCode("");
}}
>
Cancel
</Button>
<Button
className="w-full"
type="submit"
isLoading={isBackupCodeLoading}
>
Verify
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</>
)}
<div className="flex flex-row justify-between flex-wrap">
@@ -203,8 +395,7 @@ Home.getLayout = (page: ReactElement) => {
export async function getServerSideProps(context: GetServerSidePropsContext) {
if (IS_CLOUD) {
try {
const { user } = await validateRequest(context.req, context.res);
const { user } = await validateRequest(context.req);
if (user) {
return {
redirect: {
@@ -232,7 +423,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
};
}
const { user } = await validateRequest(context.req, context.res);
const { user } = await validateRequest(context.req);
if (user) {
return {

View File

@@ -1,4 +1,5 @@
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { AlertBlock } from "@/components/shared/alert-block";
import { Logo } from "@/components/shared/logo";
import { Button } from "@/components/ui/button";
import {
@@ -16,10 +17,11 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { IS_CLOUD, getUserByToken } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { AlertCircle, AlertTriangle } from "lucide-react";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -30,6 +32,9 @@ import { z } from "zod";
const registerSchema = z
.object({
name: z.string().min(1, {
message: "Name is required",
}),
email: z
.string()
.min(1, {
@@ -38,7 +43,6 @@ const registerSchema = z
.email({
message: "Email must be a valid email",
}),
password: z
.string()
.min(1, {
@@ -71,11 +75,17 @@ interface Props {
token: string;
invitation: Awaited<ReturnType<typeof getUserByToken>>;
isCloud: boolean;
userAlreadyExists: boolean;
}
const Invitation = ({ token, invitation, isCloud }: Props) => {
const Invitation = ({
token,
invitation,
isCloud,
userAlreadyExists,
}: Props) => {
const router = useRouter();
const { data } = api.admin.getUserByToken.useQuery(
const { data } = api.user.getUserByToken.useQuery(
{
token,
},
@@ -90,6 +100,7 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
const form = useForm<Register>({
defaultValues: {
name: "",
email: "",
password: "",
confirmPassword: "",
@@ -98,9 +109,9 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
});
useEffect(() => {
if (data?.auth?.email) {
if (data?.email) {
form.reset({
email: data?.auth?.email || "",
email: data?.email || "",
password: "",
confirmPassword: "",
});
@@ -108,20 +119,32 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
const onSubmit = async (values: Register) => {
await mutateAsync({
id: data?.authId,
password: values.password,
token: token,
})
.then(() => {
toast.success("User registered successfuly", {
description:
"Please check your inbox or spam folder to confirm your account.",
duration: 100000,
});
router.push("/dashboard/projects");
})
.catch((e) => e);
try {
const { data, error } = await authClient.signUp.email({
email: values.email,
password: values.password,
name: values.name,
fetchOptions: {
headers: {
"x-dokploy-token": token,
},
},
});
if (error) {
toast.error(error.message);
return;
}
const result = await authClient.organization.acceptInvitation({
invitationId: token,
});
toast.success("Account created successfully");
router.push("/dashboard/projects");
} catch (error) {
toast.error("An error occurred while creating your account");
}
};
return (
@@ -138,114 +161,155 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
</Link>
Invitation
</CardTitle>
<CardDescription>
Fill the form below to create your account
</CardDescription>
<div className="w-full">
<div className="p-3" />
{userAlreadyExists ? (
<div className="flex flex-col gap-4 justify-center items-center">
<AlertBlock type="success">
<div className="flex flex-col gap-2">
<span className="font-medium">Valid Invitation!</span>
<span className="text-sm text-green-600 dark:text-green-400">
We detected that you already have an account with this
email. Please sign in to accept the invitation.
</span>
</div>
</AlertBlock>
{isError && (
<div className="mx-5 my-2 flex flex-row items-center gap-2 rounded-lg bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)}
<Button asChild variant="default" className="w-full">
<Link href="/">Sign In</Link>
</Button>
</div>
) : (
<>
<CardDescription>
Fill the form below to create your account
</CardDescription>
<div className="w-full">
<div className="p-3" />
<CardContent className="p-0">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid gap-4"
>
<div className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input disabled placeholder="Email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{isError && (
<div className="mx-5 my-2 flex flex-row items-center gap-2 rounded-lg bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)}
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Confirm Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
isLoading={form.formState.isSubmitting}
className="w-full"
<CardContent className="p-0">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid gap-4"
>
Register
</Button>
</div>
<div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Enter your name"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
disabled
placeholder="Email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="mt-4 text-sm flex flex-row justify-between gap-2 w-full">
{isCloud && (
<>
<Link
className="hover:underline text-muted-foreground"
href="/"
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Confirm Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
isLoading={form.formState.isSubmitting}
className="w-full"
>
Login
</Link>
<Link
className="hover:underline text-muted-foreground"
href="/send-reset-password"
>
Lost your password?
</Link>
</>
)}
</div>
</form>
</Form>
</CardContent>
</div>
Register
</Button>
</div>
<div className="mt-4 text-sm flex flex-row justify-between gap-2 w-full">
{isCloud && (
<>
<Link
className="hover:underline text-muted-foreground"
href="/"
>
Login
</Link>
<Link
className="hover:underline text-muted-foreground"
href="/send-reset-password"
>
Lost your password?
</Link>
</>
)}
</div>
</form>
</Form>
</CardContent>
</div>
</>
)}
</div>
</div>
</div>
);
};
// http://localhost:3000/invitation?token=CZK4BLrUdMa32RVkAdZiLsPDdvnPiAgZ
// /f7af93acc1a99eae864972ab4c92fee089f0d83473d415ede8e821e5dbabe79c
export default Invitation;
Invitation.getLayout = (page: ReactElement) => {
return <OnboardingLayout>{page}</OnboardingLayout>;
@@ -254,6 +318,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const { query } = ctx;
const token = query.token;
console.log("query", query);
if (typeof token !== "string") {
return {
@@ -267,6 +332,17 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
try {
const invitation = await getUserByToken(token);
if (invitation.userAlreadyExists) {
return {
props: {
isCloud: IS_CLOUD,
token: token,
invitation: invitation,
userAlreadyExists: true,
},
};
}
if (invitation.isExpired) {
return {
redirect: {
@@ -284,6 +360,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
},
};
} catch (error) {
console.log("error", error);
return {
redirect: {
permanent: true,

View File

@@ -17,6 +17,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -31,6 +32,9 @@ import { z } from "zod";
const registerSchema = z
.object({
name: z.string().min(1, {
message: "Name is required",
}),
email: z
.string()
.min(1, {
@@ -79,9 +83,10 @@ const Register = ({ isCloud }: Props) => {
const form = useForm<Register>({
defaultValues: {
email: "",
password: "",
confirmPassword: "",
name: "Mauricio Siu",
email: "user5@yopmail.com",
password: "Password1234",
confirmPassword: "Password1234",
},
resolver: zodResolver(registerSchema),
});
@@ -91,19 +96,33 @@ const Register = ({ isCloud }: Props) => {
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (values: Register) => {
await mutateAsync({
email: values.email.toLowerCase(),
const { data, error } = await authClient.signUp.email({
email: values.email,
password: values.password,
})
.then(() => {
toast.success("User registered successfuly", {
duration: 2000,
});
if (!isCloud) {
router.push("/");
}
})
.catch((e) => e);
name: values.name,
});
// const { data, error } = await authClient.admin.createUser({
// name: values.name,
// email: values.email,
// password: values.password,
// role: "superAdmin",
// });
// consol/e.log(data, error);
// await mutateAsync({
// email: values.email.toLowerCase(),
// password: values.password,
// })
// .then(() => {
// toast.success("User registered successfuly", {
// duration: 2000,
// });
// if (!isCloud) {
// router.push("/");
// }
// })
// .catch((e) => e);
};
return (
<div className="">
@@ -147,6 +166,19 @@ const Register = ({ isCloud }: Props) => {
className="grid gap-4"
>
<div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
@@ -242,7 +274,7 @@ Register.getLayout = (page: ReactElement) => {
};
export async function getServerSideProps(context: GetServerSidePropsContext) {
if (IS_CLOUD) {
const { user } = await validateRequest(context.req, context.res);
const { user } = await validateRequest(context.req);
if (user) {
return {

View File

@@ -38,7 +38,7 @@ const Home: NextPage = () => {
export default Home;
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { req, res } = context;
const { user, session } = await validateRequest(context.req, context.res);
const { user, session } = await validateRequest(context.req);
if (!user) {
return {
redirect: {
@@ -53,17 +53,17 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
session: session as any,
user: user as any,
},
transformer: superjson,
});
if (user.rol === "user") {
const result = await helpers.user.byAuthId.fetch({
authId: user.id,
if (user.role === "member") {
const userR = await helpers.user.one.fetch({
userId: user.id,
});
if (!result.canAccessToAPI) {
if (!userR.canAccessToAPI) {
return {
redirect: {
permanent: true,

View File

@@ -19,6 +19,7 @@ import { mongoRouter } from "./routers/mongo";
import { mountRouter } from "./routers/mount";
import { mysqlRouter } from "./routers/mysql";
import { notificationRouter } from "./routers/notification";
import { organizationRouter } from "./routers/organization";
import { portRouter } from "./routers/port";
import { postgresRouter } from "./routers/postgres";
import { previewDeploymentRouter } from "./routers/preview-deployment";
@@ -33,7 +34,6 @@ import { sshRouter } from "./routers/ssh-key";
import { stripeRouter } from "./routers/stripe";
import { swarmRouter } from "./routers/swarm";
import { userRouter } from "./routers/user";
/**
* This is the primary router for your server.
*
@@ -75,6 +75,7 @@ export const appRouter = createTRPCRouter({
server: serverRouter,
stripe: stripeRouter,
swarm: swarmRouter,
organization: organizationRouter,
});
// export type definition of API

View File

@@ -1,27 +1,21 @@
import { db } from "@/server/db";
import {
apiAssignPermissions,
apiCreateUserInvitation,
apiFindOneToken,
apiRemoveUser,
apiUpdateAdmin,
apiUpdateWebServerMonitoring,
users,
} from "@/server/db/schema";
import {
IS_CLOUD,
createInvitation,
findAdminById,
findUserByAuthId,
findOrganizationById,
findUserById,
getUserByToken,
removeUserByAuthId,
removeUserById,
setupWebMonitoring,
updateAdmin,
updateAdminById,
updateUser,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { z } from "zod";
import {
adminProcedure,
@@ -32,30 +26,33 @@ import {
export const adminRouter = createTRPCRouter({
one: adminProcedure.query(async ({ ctx }) => {
const { sshPrivateKey, ...rest } = await findAdminById(ctx.user.adminId);
const { sshPrivateKey, ...rest } = await findUserById(ctx.user.id);
return {
haveSSH: !!sshPrivateKey,
...rest,
};
}),
update: adminProcedure
.input(apiUpdateAdmin)
.input(
z.object({
enableDockerCleanup: z.boolean(),
}),
)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
if (ctx.user.rol === "member") {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to update this admin",
});
}
const { authId } = await findAdminById(ctx.user.adminId);
// @ts-ignore
return updateAdmin(authId, input);
const user = await findUserById(ctx.user.ownerId);
return updateUser(user.id, {});
}),
createUserInvitation: adminProcedure
.input(apiCreateUserInvitation)
.mutation(async ({ input, ctx }) => {
try {
await createInvitation(input, ctx.user.adminId);
await createInvitation(input, ctx.user.id);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -69,15 +66,16 @@ export const adminRouter = createTRPCRouter({
.input(apiRemoveUser)
.mutation(async ({ input, ctx }) => {
try {
const user = await findUserByAuthId(input.authId);
const user = await findUserById(input.id);
if (user.adminId !== ctx.user.adminId) {
if (user.id !== ctx.user.ownerId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to delete this user",
});
}
return await removeUserByAuthId(input.authId);
return await removeUserById(input.id);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -95,20 +93,22 @@ export const adminRouter = createTRPCRouter({
.input(apiAssignPermissions)
.mutation(async ({ input, ctx }) => {
try {
const user = await findUserById(input.userId);
const user = await findUserById(input.id);
if (user.adminId !== ctx.user.adminId) {
const organization = await findOrganizationById(
ctx.session?.activeOrganizationId || "",
);
if (organization?.ownerId !== ctx.user.ownerId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to assign permissions",
});
}
await db
.update(users)
.set({
...input,
})
.where(eq(users.userId, input.userId));
await updateUser(user.id, {
...input,
});
} catch (error) {
throw error;
}
@@ -124,15 +124,15 @@ export const adminRouter = createTRPCRouter({
message: "Feature disabled on cloud",
});
}
const admin = await findAdminById(ctx.user.adminId);
if (admin.adminId !== ctx.user.adminId) {
const user = await findUserById(ctx.user.ownerId);
if (user.id !== ctx.user.ownerId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to setup this server",
message: "You are not authorized to setup the monitoring",
});
}
await updateAdminById(admin.adminId, {
await updateUser(user.id, {
metricsConfig: {
server: {
type: "Dokploy",
@@ -156,18 +156,19 @@ export const adminRouter = createTRPCRouter({
},
},
});
const currentServer = await setupWebMonitoring(admin.adminId);
const currentServer = await setupWebMonitoring(user.id);
return currentServer;
} catch (error) {
throw error;
}
}),
getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
const admin = await findAdminById(ctx.user.adminId);
const user = await findUserById(ctx.user.ownerId);
return {
serverIp: admin.serverIp,
enabledFeatures: admin.enablePaidFeatures,
metricsConfig: admin?.metricsConfig,
serverIp: user.serverIp,
enabledFeatures: user.enablePaidFeatures,
metricsConfig: user?.metricsConfig,
};
}),

View File

@@ -60,8 +60,8 @@ export const applicationRouter = createTRPCRouter({
.input(apiCreateApplication)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
if (ctx.user.rol === "member") {
await checkServiceAccess(ctx.user.id, input.projectId, "create");
}
if (IS_CLOUD && !input.serverId) {
@@ -72,7 +72,7 @@ export const applicationRouter = createTRPCRouter({
}
const project = await findProjectById(input.projectId);
if (project.adminId !== ctx.user.adminId) {
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
@@ -80,8 +80,8 @@ export const applicationRouter = createTRPCRouter({
}
const newApplication = await createApplication(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newApplication.applicationId);
if (ctx.user.rol === "member") {
await addNewService(ctx.user.id, newApplication.applicationId);
}
return newApplication;
} catch (error: unknown) {
@@ -98,15 +98,13 @@ export const applicationRouter = createTRPCRouter({
one: protectedProcedure
.input(apiFindOneApplication)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(
ctx.user.authId,
input.applicationId,
"access",
);
if (ctx.user.rol === "member") {
await checkServiceAccess(ctx.user.id, input.applicationId, "access");
}
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
@@ -119,7 +117,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiReloadApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this application",
@@ -144,16 +144,14 @@ export const applicationRouter = createTRPCRouter({
delete: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(
ctx.user.authId,
input.applicationId,
"delete",
);
if (ctx.user.rol === "member") {
await checkServiceAccess(ctx.user.id, input.applicationId, "delete");
}
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this application",
@@ -194,7 +192,7 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const service = await findApplicationById(input.applicationId);
if (service.project.adminId !== ctx.user.adminId) {
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this application",
@@ -214,7 +212,7 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const service = await findApplicationById(input.applicationId);
if (service.project.adminId !== ctx.user.adminId) {
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this application",
@@ -235,7 +233,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to redeploy this application",
@@ -268,7 +268,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiSaveEnvironmentVariables)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
@@ -284,7 +286,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiSaveBuildType)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this build type",
@@ -305,7 +309,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiSaveGithubProvider)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this github provider",
@@ -327,7 +333,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiSaveGitlabProvider)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this gitlab provider",
@@ -351,7 +359,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiSaveBitbucketProvider)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this bitbucket provider",
@@ -373,7 +383,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiSaveDockerProvider)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this docker provider",
@@ -394,7 +406,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiSaveGitProvider)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this git provider",
@@ -415,7 +429,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to mark this application as running",
@@ -427,7 +443,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiUpdateApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this application",
@@ -451,7 +469,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to refresh this application",
@@ -466,7 +486,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this application",
@@ -500,7 +522,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to clean this application",
@@ -513,7 +537,9 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication)
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to read this application",
@@ -548,7 +574,7 @@ export const applicationRouter = createTRPCRouter({
const app = await findApplicationById(input.applicationId as string);
if (app.project.adminId !== ctx.user.adminId) {
if (app.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this application",
@@ -590,7 +616,9 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this application",

View File

@@ -1,37 +1,30 @@
import {
apiCreateAdmin,
apiCreateUser,
apiFindOneAuth,
apiLogin,
apiUpdateAuth,
apiVerify2FA,
apiVerifyLogin2FA,
auth,
// apiCreateAdmin,
// apiCreateUser,
// apiFindOneAuth,
// apiLogin,
// apiUpdateAuth,
// apiVerify2FA,
// apiVerifyLogin2FA,
// auth,
member,
} from "@/server/db/schema";
import { WEBSITE_URL } from "@/server/utils/stripe";
import {
type Auth,
IS_CLOUD,
createAdmin,
createUser,
findAuthByEmail,
findAuthById,
findUserById,
generate2FASecret,
getUserByToken,
lucia,
luciaToken,
removeAdminByAuthId,
removeUserByAuthId,
sendDiscordNotification,
sendEmailNotification,
updateAuthById,
validateRequest,
verify2FA,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { isBefore } from "date-fns";
import { eq } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import { nanoid } from "nanoid";
import { z } from "zod";
import { db } from "../../db";
@@ -43,81 +36,77 @@ import {
} from "../trpc";
export const authRouter = createTRPCRouter({
createAdmin: publicProcedure
.input(apiCreateAdmin)
.mutation(async ({ ctx, input }) => {
try {
if (!IS_CLOUD) {
const admin = await db.query.admins.findFirst({});
if (admin) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Admin already exists",
});
}
}
const newAdmin = await createAdmin(input);
if (IS_CLOUD) {
await sendDiscordNotificationWelcome(newAdmin);
await sendVerificationEmail(newAdmin.id);
return {
status: "success",
type: "cloud",
};
}
const session = await lucia.createSession(newAdmin.id || "", {});
ctx.res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
return {
status: "success",
type: "selfhosted",
};
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
// @ts-ignore
message: `Error: ${error?.code === "23505" ? "Email already exists" : "Error creating admin"}`,
cause: error,
});
}
}),
createUser: publicProcedure
.input(apiCreateUser)
.mutation(async ({ ctx, input }) => {
try {
const token = await getUserByToken(input.token);
if (token.isExpired) {
createAdmin: publicProcedure.mutation(async ({ ctx, input }) => {
try {
if (!IS_CLOUD) {
const admin = await db.query.admins.findFirst({});
if (admin) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid token",
message: "Admin already exists",
});
}
const newUser = await createUser(input);
if (IS_CLOUD) {
await sendVerificationEmail(token.authId);
return true;
}
const session = await lucia.createSession(newUser?.authId || "", {});
ctx.res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the user",
cause: error,
});
}
}),
const newAdmin = await createAdmin(input);
login: publicProcedure.input(apiLogin).mutation(async ({ ctx, input }) => {
if (IS_CLOUD) {
await sendDiscordNotificationWelcome(newAdmin);
await sendVerificationEmail(newAdmin.id);
return {
status: "success",
type: "cloud",
};
}
// const session = await lucia.createSession(newAdmin.id || "", {});
// ctx.res.appendHeader(
// "Set-Cookie",
// lucia.createSessionCookie(session.id).serialize(),
// );
return {
status: "success",
type: "selfhosted",
};
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
// @ts-ignore
message: `Error: ${error?.code === "23505" ? "Email already exists" : "Error creating admin"}`,
cause: error,
});
}
}),
createUser: publicProcedure.mutation(async ({ ctx, input }) => {
try {
const token = await getUserByToken(input.token);
// if (token.isExpired) {
// throw new TRPCError({
// code: "BAD_REQUEST",
// message: "Invalid token",
// });
// }
// const newUser = await createUser(input);
// if (IS_CLOUD) {
// await sendVerificationEmail(token.authId);
// return true;
// }
// const session = await lucia.createSession(newUser?.authId || "", {});
// ctx.res.appendHeader(
// "Set-Cookie",
// lucia.createSessionCookie(session.id).serialize(),
// );
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the user",
cause: error,
});
}
}),
login: publicProcedure.mutation(async ({ ctx, input }) => {
try {
const auth = await findAuthByEmail(input.email);
@@ -149,12 +138,12 @@ export const authRouter = createTRPCRouter({
};
}
const session = await lucia.createSession(auth?.id || "", {});
// const session = await lucia.createSession(auth?.id || "", {});
ctx.res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
// ctx.res.appendHeader(
// "Set-Cookie",
// lucia.createSessionCookie(session.id).serialize(),
// );
return {
is2FAEnabled: false,
authId: auth?.id,
@@ -169,47 +158,54 @@ export const authRouter = createTRPCRouter({
}),
get: protectedProcedure.query(async ({ ctx }) => {
const auth = await findAuthById(ctx.user.authId);
return auth;
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, ctx.user.id),
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
),
with: {
user: true,
},
});
return memberResult;
}),
logout: protectedProcedure.mutation(async ({ ctx }) => {
const { req, res } = ctx;
const { session } = await validateRequest(req, res);
const { session } = await validateRequest(req);
if (!session) return false;
await lucia.invalidateSession(session.id);
res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
// await lucia.invalidateSession(session.id);
// res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
return true;
}),
update: protectedProcedure
.input(apiUpdateAuth)
.mutation(async ({ ctx, input }) => {
const currentAuth = await findAuthByEmail(ctx.user.email);
update: protectedProcedure.mutation(async ({ ctx, input }) => {
const currentAuth = await findAuthByEmail(ctx.user.email);
if (input.currentPassword || input.password) {
const correctPassword = bcrypt.compareSync(
input.currentPassword || "",
currentAuth?.password || "",
);
if (!correctPassword) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Current password is incorrect",
});
}
if (input.currentPassword || input.password) {
const correctPassword = bcrypt.compareSync(
input.currentPassword || "",
currentAuth?.password || "",
);
if (!correctPassword) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Current password is incorrect",
});
}
const auth = await updateAuthById(ctx.user.authId, {
...(input.email && { email: input.email.toLowerCase() }),
...(input.password && {
password: bcrypt.hashSync(input.password, 10),
}),
...(input.image && { image: input.image }),
});
}
// const auth = await updateAuthById(ctx.user.authId, {
// ...(input.email && { email: input.email.toLowerCase() }),
// ...(input.password && {
// password: bcrypt.hashSync(input.password, 10),
// }),
// ...(input.image && { image: input.image }),
// });
return auth;
}),
return auth;
}),
removeSelfAccount: protectedProcedure
.input(
z.object({
@@ -237,84 +233,81 @@ export const authRouter = createTRPCRouter({
});
}
const { req, res } = ctx;
const { session } = await validateRequest(req, res);
const { session } = await validateRequest(req);
if (!session) return false;
await lucia.invalidateSession(session.id);
res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
// await lucia.invalidateSession(session.id);
// res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
if (ctx.user.rol === "admin") {
await removeAdminByAuthId(ctx.user.authId);
} else {
await removeUserByAuthId(ctx.user.authId);
}
// if (ctx.user.rol === "owner") {
// await removeAdminByAuthId(ctx.user.authId);
// } else {
// await removeUserByAuthId(ctx.user.authId);
// }
return true;
}),
generateToken: protectedProcedure.mutation(async ({ ctx, input }) => {
const auth = await findAuthById(ctx.user.authId);
if (auth.token) {
await luciaToken.invalidateSession(auth.token);
}
const session = await luciaToken.createSession(auth?.id || "", {
expiresIn: 60 * 60 * 24 * 30,
});
await updateAuthById(auth.id, {
token: session.id,
});
const auth = await findUserById(ctx.user.id);
console.log(auth);
// if (auth.token) {
// await luciaToken.invalidateSession(auth.token);
// }
// const session = await luciaToken.createSession(auth?.id || "", {
// expiresIn: 60 * 60 * 24 * 30,
// });
// await updateUser(auth.id, {
// token: session.id,
// });
return auth;
}),
verifyToken: protectedProcedure.mutation(async () => {
return true;
}),
one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => {
const auth = await findAuthById(input.id);
return auth;
}),
one: adminProcedure
.input(z.object({ userId: z.string().min(1) }))
.query(async ({ input }) => {
// TODO: Check if the user is admin or member
const user = await findUserById(input.userId);
return user;
}),
generate2FASecret: protectedProcedure.query(async ({ ctx }) => {
return await generate2FASecret(ctx.user.authId);
return await generate2FASecret(ctx.user.id);
}),
verify2FASetup: protectedProcedure.mutation(async ({ ctx, input }) => {
// const auth = await findAuthById(ctx.user.authId);
// await verify2FA(auth, input.secret, input.pin);
// await updateAuthById(auth.id, {
// is2FAEnabled: true,
// secret: input.secret,
// });
// return auth;
}),
verify2FASetup: protectedProcedure
.input(apiVerify2FA)
.mutation(async ({ ctx, input }) => {
const auth = await findAuthById(ctx.user.authId);
await verify2FA(auth, input.secret, input.pin);
await updateAuthById(auth.id, {
is2FAEnabled: true,
secret: input.secret,
});
return auth;
}),
verifyLogin2FA: publicProcedure.mutation(async ({ ctx, input }) => {
// const auth = await findAuthById(input.id);
verifyLogin2FA: publicProcedure
.input(apiVerifyLogin2FA)
.mutation(async ({ ctx, input }) => {
const auth = await findAuthById(input.id);
// await verify2FA(auth, auth.secret || "", input.pin);
await verify2FA(auth, auth.secret || "", input.pin);
// const session = await lucia.createSession(auth.id, {});
const session = await lucia.createSession(auth.id, {});
// ctx.res.appendHeader(
// "Set-Cookie",
// lucia.createSessionCookie(session.id).serialize(),
// );
ctx.res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
return true;
}),
return true;
}),
disable2FA: protectedProcedure.mutation(async ({ ctx }) => {
const auth = await findAuthById(ctx.user.authId);
await updateAuthById(auth.id, {
is2FAEnabled: false,
secret: null,
});
return auth;
// const auth = await findAuthById(ctx.user.authId);
// await updateAuthById(auth.id, {
// is2FAEnabled: false,
// secret: null,
// });
// return auth;
}),
sendResetPasswordEmail: publicProcedure
.input(

View File

@@ -8,7 +8,6 @@ import {
apiUpdateBitbucket,
} from "@/server/db/schema";
import {
IS_CLOUD,
createBitbucket,
findBitbucketById,
getBitbucketBranches,
@@ -23,7 +22,7 @@ export const bitbucketRouter = createTRPCRouter({
.input(apiCreateBitbucket)
.mutation(async ({ input, ctx }) => {
try {
return await createBitbucket(input, ctx.user.adminId);
return await createBitbucket(input, ctx.session.activeOrganizationId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -37,10 +36,9 @@ export const bitbucketRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
IS_CLOUD &&
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
@@ -58,12 +56,11 @@ export const bitbucketRouter = createTRPCRouter({
},
});
if (IS_CLOUD) {
// TODO: mAyBe a rEfaCtoR 🤫
result = result.filter(
(provider) => provider.gitProvider.adminId === ctx.user.adminId,
);
}
result = result.filter(
(provider) =>
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId,
);
return result;
}),
@@ -72,10 +69,9 @@ export const bitbucketRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
IS_CLOUD &&
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
@@ -90,10 +86,9 @@ export const bitbucketRouter = createTRPCRouter({
input.bitbucketId || "",
);
if (
IS_CLOUD &&
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
@@ -107,10 +102,9 @@ export const bitbucketRouter = createTRPCRouter({
try {
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
IS_CLOUD &&
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
@@ -131,10 +125,9 @@ export const bitbucketRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
IS_CLOUD &&
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
@@ -142,7 +135,7 @@ export const bitbucketRouter = createTRPCRouter({
}
return await updateBitbucket(input.bitbucketId, {
...input,
adminId: ctx.user.adminId,
organizationId: ctx.session.activeOrganizationId,
});
}),
});

View File

@@ -25,14 +25,14 @@ export const certificateRouter = createTRPCRouter({
message: "Please set a server to create a certificate",
});
}
return await createCertificate(input, ctx.user.adminId);
return await createCertificate(input, ctx.session.activeOrganizationId);
}),
one: adminProcedure
.input(apiFindCertificate)
.query(async ({ input, ctx }) => {
const certificates = await findCertificateById(input.certificateId);
if (IS_CLOUD && certificates.adminId !== ctx.user.adminId) {
if (certificates.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this certificate",
@@ -44,7 +44,7 @@ export const certificateRouter = createTRPCRouter({
.input(apiFindCertificate)
.mutation(async ({ input, ctx }) => {
const certificates = await findCertificateById(input.certificateId);
if (IS_CLOUD && certificates.adminId !== ctx.user.adminId) {
if (certificates.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to delete this certificate",
@@ -55,8 +55,7 @@ export const certificateRouter = createTRPCRouter({
}),
all: adminProcedure.query(async ({ ctx }) => {
return await db.query.certificates.findMany({
// TODO: Remove this line when the cloud version is ready
...(IS_CLOUD && { where: eq(certificates.adminId, ctx.user.adminId) }),
where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
});
}),
});

View File

@@ -39,11 +39,11 @@ import {
createComposeByTemplate,
createDomain,
createMount,
findAdminById,
findComposeById,
findDomainsByComposeId,
findProjectById,
findServerById,
findUserById,
loadServices,
randomizeComposeFile,
randomizeIsolatedDeploymentComposeFile,
@@ -60,8 +60,8 @@ export const composeRouter = createTRPCRouter({
.input(apiCreateCompose)
.mutation(async ({ ctx, input }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
if (ctx.user.rol === "member") {
await checkServiceAccess(ctx.user.id, input.projectId, "create");
}
if (IS_CLOUD && !input.serverId) {
@@ -71,7 +71,7 @@ export const composeRouter = createTRPCRouter({
});
}
const project = await findProjectById(input.projectId);
if (project.adminId !== ctx.user.adminId) {
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
@@ -79,8 +79,8 @@ export const composeRouter = createTRPCRouter({
}
const newService = await createCompose(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newService.composeId);
if (ctx.user.rol === "member") {
await addNewService(ctx.user.id, newService.composeId);
}
return newService;
@@ -92,12 +92,12 @@ export const composeRouter = createTRPCRouter({
one: protectedProcedure
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.composeId, "access");
if (ctx.user.rol === "member") {
await checkServiceAccess(ctx.user.id, input.composeId, "access");
}
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
@@ -110,7 +110,7 @@ export const composeRouter = createTRPCRouter({
.input(apiUpdateCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this compose",
@@ -121,12 +121,15 @@ export const composeRouter = createTRPCRouter({
delete: protectedProcedure
.input(apiDeleteCompose)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.composeId, "delete");
if (ctx.user.rol === "member") {
await checkServiceAccess(ctx.user.id, input.composeId, "delete");
}
const composeResult = await findComposeById(input.composeId);
if (composeResult.project.adminId !== ctx.user.adminId) {
if (
composeResult.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this compose",
@@ -157,7 +160,7 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to clean this compose",
@@ -170,7 +173,7 @@ export const composeRouter = createTRPCRouter({
.input(apiFetchServices)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to load this compose",
@@ -184,7 +187,9 @@ export const composeRouter = createTRPCRouter({
try {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (
compose.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to fetch this compose",
@@ -209,7 +214,7 @@ export const composeRouter = createTRPCRouter({
.input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to randomize this compose",
@@ -221,7 +226,7 @@ export const composeRouter = createTRPCRouter({
.input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to randomize this compose",
@@ -236,7 +241,7 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to get this compose",
@@ -254,7 +259,7 @@ export const composeRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this compose",
@@ -287,7 +292,7 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to redeploy this compose",
@@ -319,7 +324,7 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this compose",
@@ -333,7 +338,7 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this compose",
@@ -348,7 +353,7 @@ export const composeRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to get this compose",
@@ -361,7 +366,7 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to refresh this compose",
@@ -375,8 +380,8 @@ export const composeRouter = createTRPCRouter({
deployTemplate: protectedProcedure
.input(apiCreateComposeByTemplate)
.mutation(async ({ ctx, input }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
if (ctx.user.rol === "member") {
await checkServiceAccess(ctx.user.id, input.projectId, "create");
}
if (IS_CLOUD && !input.serverId) {
@@ -390,7 +395,7 @@ export const composeRouter = createTRPCRouter({
const generate = await loadTemplateModule(input.id as TemplatesKeys);
const admin = await findAdminById(ctx.user.adminId);
const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
const project = await findProjectById(input.projectId);
@@ -418,8 +423,8 @@ export const composeRouter = createTRPCRouter({
isolatedDeployment: true,
});
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, compose.composeId);
if (ctx.user.rol === "member") {
await addNewService(ctx.user.id, compose.composeId);
}
if (mounts && mounts?.length > 0) {

View File

@@ -19,7 +19,9 @@ export const deploymentRouter = createTRPCRouter({
.input(apiFindAllByApplication)
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
@@ -32,7 +34,7 @@ export const deploymentRouter = createTRPCRouter({
.input(apiFindAllByCompose)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
@@ -44,7 +46,7 @@ export const deploymentRouter = createTRPCRouter({
.input(apiFindAllByServer)
.query(async ({ input, ctx }) => {
const server = await findServerById(input.serverId);
if (server.adminId !== ctx.user.adminId) {
if (server.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this server",

Some files were not shown because too many files have changed in this diff Show More