Reapply "Merge branch 'canary' into kucherenko/canary"

This reverts commit e6cb6454db.
This commit is contained in:
Mauricio Siu
2025-03-02 00:30:02 -06:00
parent e6cb6454db
commit 747c2137c9
639 changed files with 82888 additions and 17188 deletions

View File

@@ -90,7 +90,7 @@ export default function Custom404({ statusCode, error }: Props) {
}
// @ts-ignore
Error.getInitialProps = ({ res, err, ...rest }: NextPageContext) => {
Error.getInitialProps = ({ res, err }: NextPageContext) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
return { statusCode, error: err };
};

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

@@ -1,17 +1,11 @@
import { appRouter } from "@/server/api/root";
import { createTRPCContext } from "@/server/api/trpc";
import { validateBearerToken, validateRequest } from "@dokploy/server";
import { validateRequest } from "@dokploy/server";
import { createOpenApiNextHandler } from "@dokploy/trpc-openapi";
import type { NextApiRequest, NextApiResponse } from "next";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
let { session, user } = await validateBearerToken(req);
if (!session) {
const cookieResult = await validateRequest(req, res);
session = cookieResult.session;
user = cookieResult.user;
}
const { session, user } = await validateRequest(req);
if (!user || !session) {
res.status(401).json({ message: "Unauthorized" });

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

@@ -3,9 +3,7 @@ import { applications, compose, github } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { generateRandomDomain } from "@/templates/utils";
import {
type Domain,
IS_CLOUD,
createPreviewDeployment,
findPreviewDeploymentByApplicationId,
@@ -121,7 +119,7 @@ export default async function handler(
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
return true;
continue;
}
await myQueue.add(
"deployments",
@@ -156,7 +154,7 @@ export default async function handler(
if (IS_CLOUD && composeApp.serverId) {
jobData.serverId = composeApp.serverId;
await deploy(jobData);
return true;
continue;
}
await myQueue.add(
@@ -182,8 +180,9 @@ export default async function handler(
}
} else if (req.headers["x-github-event"] === "pull_request") {
const prId = githubBody?.pull_request?.id;
const action = githubBody?.action;
if (githubBody?.action === "closed") {
if (action === "closed") {
const previewDeploymentResult =
await findPreviewDeploymentsByPullRequestId(prId);
@@ -201,79 +200,86 @@ export default async function handler(
res.status(200).json({ message: "Preview Deployment Closed" });
return;
}
// opened or synchronize or reopened
const repository = githubBody?.repository?.name;
const deploymentHash = githubBody?.pull_request?.head?.sha;
const branch = githubBody?.pull_request?.base?.ref;
const owner = githubBody?.repository?.owner?.login;
if (
action === "opened" ||
action === "synchronize" ||
action === "reopened"
) {
const repository = githubBody?.repository?.name;
const deploymentHash = githubBody?.pull_request?.head?.sha;
const branch = githubBody?.pull_request?.base?.ref;
const owner = githubBody?.repository?.owner?.login;
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.repository, repository),
eq(applications.branch, branch),
eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, owner),
),
with: {
previewDeployments: true,
},
});
const prBranch = githubBody?.pull_request?.head?.ref;
const prNumber = githubBody?.pull_request?.number;
const prTitle = githubBody?.pull_request?.title;
const prURL = githubBody?.pull_request?.html_url;
for (const app of apps) {
const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) {
continue;
}
const previewDeploymentResult =
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
let previewDeploymentId =
previewDeploymentResult?.previewDeploymentId || "";
if (!previewDeploymentResult) {
const previewDeployment = await createPreviewDeployment({
applicationId: app.applicationId as string,
branch: prBranch,
pullRequestId: prId,
pullRequestNumber: prNumber,
pullRequestTitle: prTitle,
pullRequestURL: prURL,
});
previewDeploymentId = previewDeployment.previewDeploymentId;
}
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: "Preview Deployment",
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application-preview",
server: !!app.serverId,
previewDeploymentId,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.repository, repository),
eq(applications.branch, branch),
eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, owner),
),
with: {
previewDeployments: true,
},
);
});
const prBranch = githubBody?.pull_request?.head?.ref;
const prNumber = githubBody?.pull_request?.number;
const prTitle = githubBody?.pull_request?.title;
const prURL = githubBody?.pull_request?.html_url;
for (const app of apps) {
const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) {
continue;
}
const previewDeploymentResult =
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
let previewDeploymentId =
previewDeploymentResult?.previewDeploymentId || "";
if (!previewDeploymentResult) {
const previewDeployment = await createPreviewDeployment({
applicationId: app.applicationId as string,
branch: prBranch,
pullRequestId: prId,
pullRequestNumber: prNumber,
pullRequestTitle: prTitle,
pullRequestURL: prURL,
});
previewDeploymentId = previewDeployment.previewDeploymentId;
}
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: "Preview Deployment",
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application-preview",
server: !!app.serverId,
previewDeploymentId,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
continue;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
return res.status(200).json({ message: "Apps Deployed" });
}
return res.status(200).json({ message: "Apps Deployed" });
}
return res.status(400).json({ message: "No Actions matched" });

View File

@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
_req: NextApiRequest,
res: NextApiResponse,
) {
return res.status(200).json({ ok: true });

View File

@@ -1,11 +1,6 @@
import { db } from "@/server/db";
import { github } from "@/server/db/schema";
import {
createGithub,
findAdminByAuthId,
findAuthById,
findUserByAuthId,
} from "@dokploy/server";
import { createGithub } from "@dokploy/server";
import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
import { Octokit } from "octokit";
@@ -21,14 +16,13 @@ export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { code, state, installation_id, setup_action }: Query =
req.query as Query;
const { code, state, installation_id }: Query = req.query as Query;
if (!code) {
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 +33,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 +43,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 { organization, server, users_temp } from "@/server/db/schema";
import { type Server, 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),
);
}
@@ -168,13 +172,13 @@ export default async function handler(
}
await db
.update(admins)
.update(users_temp)
.set({
serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0,
})
.where(eq(admins.stripeCustomerId, suscription.customer as string));
.where(eq(users_temp.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,
);
@@ -201,13 +205,15 @@ export default async function handler(
return res.status(400).send("Webhook Error: Admin not found");
}
await db
.update(admins)
.update(users_temp)
.set({
serversQuantity: 0,
})
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
.where(
eq(users_temp.stripeCustomerId, newInvoice.customer as string),
);
await disableServers(admin.adminId);
await disableServers(admin.id);
}
break;
@@ -216,20 +222,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 +246,26 @@ export default async function handler(
return res.status(200).json({ received: true });
}
const disableServers = async (adminId: string) => {
await db
.update(server)
.set({
serverStatus: "inactive",
})
.where(eq(server.adminId, adminId));
const disableServers = async (userId: string) => {
const organizations = await db.query.organization.findMany({
where: eq(organization.ownerId, userId),
});
for (const org of organizations) {
await db
.update(server)
.set({
serverStatus: "inactive",
})
.where(eq(server.organizationId, org.id));
}
};
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 +282,27 @@ const deactivateServer = async (serverId: string) => {
.where(eq(server.serverId, serverId));
};
export const findServersByAdminIdSorted = async (adminId: string) => {
const servers = await db.query.server.findMany({
where: eq(server.adminId, adminId),
orderBy: asc(server.createdAt),
export const findServersByUserIdSorted = async (userId: string) => {
const organizations = await db.query.organization.findMany({
where: eq(organization.ownerId, userId),
});
const servers: Server[] = [];
for (const org of organizations) {
const serversByOrg = await db.query.server.findMany({
where: eq(server.organizationId, org.id),
orderBy: asc(server.createdAt),
});
servers.push(...serversByOrg);
}
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,96 +0,0 @@
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { Logo } from "@/components/shared/logo";
import { CardDescription, CardTitle } from "@/components/ui/card";
import { db } from "@/server/db";
import { auth } from "@/server/db/schema";
import { IS_CLOUD, updateAuthById } from "@dokploy/server";
import { isBefore } from "date-fns";
import { eq } from "drizzle-orm";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import type { ReactElement } from "react";
export default function Home() {
return (
<div className="flex h-screen w-full items-center justify-center ">
<div className="flex flex-col items-center gap-4 w-full">
<Link href="/" className="flex flex-row items-center gap-2">
<Logo />
<span className="font-medium text-sm">Dokploy</span>
</Link>
<CardTitle className="text-2xl font-bold">Email Confirmed</CardTitle>
<CardDescription>
Congratulations, your email is confirmed.
</CardDescription>
<div>
<Link href="/" className="w-full text-primary">
Click here to login
</Link>
</div>
</div>
</div>
);
}
Home.getLayout = (page: ReactElement) => {
return <OnboardingLayout>{page}</OnboardingLayout>;
};
export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const { token } = context.query;
if (typeof token !== "string") {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const authR = await db.query.auth.findFirst({
where: eq(auth.confirmationToken, token),
});
if (
!authR ||
authR?.confirmationToken === null ||
authR?.confirmationExpiresAt === null
) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const isExpired = isBefore(new Date(authR.confirmationExpiresAt), new Date());
if (isExpired) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
await updateAuthById(authR.id, {
confirmationToken: null,
confirmationExpiresAt: null,
});
return {
props: {
token: authR.confirmationToken,
},
};
}

View File

@@ -1,10 +1,11 @@
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";
import type { ReactElement } from "react";
import superjson from "superjson";
const Dashboard = () => {
@@ -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,
@@ -72,7 +72,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (error) {
} catch (_error) {
return {
props: {},
};

View File

@@ -1,11 +1,86 @@
import { ShowMonitoring } from "@/components/dashboard/monitoring/web-server/show";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ShowPaidMonitoring } from "@/components/dashboard/monitoring/paid/servers/show-paid-monitoring";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { Card } from "@/components/ui/card";
import { useLocalStorage } from "@/hooks/useLocalStorage";
import { api } from "@/utils/api";
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/lib/auth";
import { Loader2 } from "lucide-react";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import type { ReactElement } from "react";
const BASE_URL = "http://localhost:3001/metrics";
const DEFAULT_TOKEN = "metrics";
const Dashboard = () => {
return <ShowMonitoring />;
const [toggleMonitoring, _setToggleMonitoring] = useLocalStorage(
"monitoring-enabled",
false,
);
const { data: monitoring, isLoading } = api.user.getMetricsToken.useQuery();
return (
<div className="space-y-4 pb-10">
{/* <AlertBlock>
You are watching the <strong>Free</strong> plan.{" "}
<a
href="https://dokploy.com#pricing"
target="_blank"
className="underline"
rel="noreferrer"
>
Upgrade
</a>{" "}
to get more features.
</AlertBlock> */}
{isLoading ? (
<Card className="bg-sidebar p-2.5 rounded-xl mx-auto items-center">
<div className="rounded-xl bg-background flex shadow-md px-4 min-h-[50vh] justify-center items-center text-muted-foreground">
Loading...
<Loader2 className="h-4 w-4 animate-spin" />
</div>
</Card>
) : (
<>
{/* {monitoring?.enabledFeatures && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
<Label className="text-muted-foreground">Change Monitoring</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)} */}
{toggleMonitoring ? (
<Card className="bg-sidebar p-2.5 rounded-xl mx-auto">
<div className="rounded-xl bg-background shadow-md">
<ShowPaidMonitoring
BASE_URL={
process.env.NODE_ENV === "production"
? `http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}/metrics`
: BASE_URL
}
token={
process.env.NODE_ENV === "production"
? monitoring?.metricsConfig?.server?.token
: DEFAULT_TOKEN
}
/>
</div>
</Card>
) : (
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
<div className="rounded-xl bg-background shadow-md p-6">
<ContainerFreeMonitoring appName="dokploy" />
</div>
</Card>
)}
</>
)}
</div>
);
};
export default Dashboard;
@@ -24,7 +99,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

@@ -13,6 +13,7 @@ import {
import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Button } from "@/components/ui/button";
@@ -25,6 +26,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import {
Command,
CommandEmpty,
@@ -49,27 +51,34 @@ 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,
Check,
CheckCircle2,
ChevronsUpDown,
CircuitBoard,
FolderInput,
GlobeIcon,
Loader2,
PlusIcon,
Search,
X,
} from "lucide-react";
import { Check, ChevronsUpDown, X } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
} from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import React, { useMemo, useState, type ReactElement } from "react";
import { type ReactElement, useMemo, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
export type Services = {
appName: string;
serverId?: string | null;
name: string;
type:
| "mariadb"
@@ -90,72 +99,86 @@ type Project = Awaited<ReturnType<typeof findProjectById>>;
export const extractServices = (data: Project | undefined) => {
const applications: Services[] =
data?.applications.map((item) => ({
appName: item.appName,
name: item.name,
type: "application",
id: item.applicationId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
const mariadb: Services[] =
data?.mariadb.map((item) => ({
appName: item.appName,
name: item.name,
type: "mariadb",
id: item.mariadbId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
const postgres: Services[] =
data?.postgres.map((item) => ({
appName: item.appName,
name: item.name,
type: "postgres",
id: item.postgresId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
const mongo: Services[] =
data?.mongo.map((item) => ({
appName: item.appName,
name: item.name,
type: "mongo",
id: item.mongoId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
const redis: Services[] =
data?.redis.map((item) => ({
appName: item.appName,
name: item.name,
type: "redis",
id: item.redisId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
const mysql: Services[] =
data?.mysql.map((item) => ({
appName: item.appName,
name: item.name,
type: "mysql",
id: item.mysqlId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
const compose: Services[] =
data?.compose.map((item) => ({
appName: item.appName,
name: item.name,
type: "compose",
id: item.composeId,
createdAt: item.createdAt,
status: item.composeStatus,
description: item.description,
serverId: item.serverId,
})) || [];
applications.push(
@@ -177,17 +200,11 @@ export const extractServices = (data: Project | undefined) => {
const Project = (
props: InferGetServerSidePropsType<typeof getServerSideProps>,
) => {
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, isLoading } = api.project.one.useQuery({ projectId });
const { data: auth } = api.user.get.useQuery();
const { data, isLoading, refetch } = api.project.one.useQuery({ projectId });
const router = useRouter();
const emptyServices =
@@ -214,6 +231,70 @@ const Project = (
const [selectedTypes, setSelectedTypes] = useState<string[]>([]);
const [openCombobox, setOpenCombobox] = useState(false);
const [selectedServices, setSelectedServices] = useState<string[]>([]);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const handleSelectAll = () => {
if (selectedServices.length === filteredServices.length) {
setSelectedServices([]);
} else {
setSelectedServices(filteredServices.map((service) => service.id));
}
};
const handleServiceSelect = (serviceId: string, event: React.MouseEvent) => {
event.stopPropagation();
setSelectedServices((prev) =>
prev.includes(serviceId)
? prev.filter((id) => id !== serviceId)
: [...prev, serviceId],
);
};
const composeActions = {
start: api.compose.start.useMutation(),
stop: api.compose.stop.useMutation(),
};
const handleBulkStart = async () => {
let success = 0;
setIsBulkActionLoading(true);
for (const serviceId of selectedServices) {
try {
await composeActions.start.mutateAsync({ composeId: serviceId });
success++;
} catch (_error) {
toast.error(`Error starting service ${serviceId}`);
}
}
if (success > 0) {
toast.success(`${success} services started successfully`);
refetch();
}
setIsBulkActionLoading(false);
setSelectedServices([]);
setIsDropdownOpen(false);
};
const handleBulkStop = async () => {
let success = 0;
setIsBulkActionLoading(true);
for (const serviceId of selectedServices) {
try {
await composeActions.stop.mutateAsync({ composeId: serviceId });
success++;
} catch (_error) {
toast.error(`Error stopping service ${serviceId}`);
}
}
if (success > 0) {
toast.success(`${success} services stopped successfully`);
refetch();
}
setSelectedServices([]);
setIsDropdownOpen(false);
setIsBulkActionLoading(false);
};
const filteredServices = useMemo(() => {
if (!applications) return [];
@@ -241,16 +322,16 @@ const Project = (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<div className="rounded-xl bg-background shadow-md ">
<div className="flex justify-between gap-4 w-full items-center">
<CardHeader className="">
<div className="flex justify-between gap-4 w-full items-center flex-wrap p-6">
<CardHeader className="p-0">
<CardTitle className="text-xl flex flex-row gap-2">
<FolderInput className="size-6 text-muted-foreground self-center" />
{data?.name}
</CardTitle>
<CardDescription>{data?.description}</CardDescription>
</CardHeader>
{(auth?.rol === "admin" || user?.canCreateServices) && (
<div className="flex flex-row gap-4 flex-wrap px-4">
{(auth?.role === "owner" || auth?.canCreateServices) && (
<div className="flex flex-row gap-4 flex-wrap">
<ProjectEnvironment projectId={projectId}>
<Button variant="outline">Project Environment</Button>
</ProjectEnvironment>
@@ -299,78 +380,151 @@ const Project = (
</div>
) : (
<>
<div className="flex flex-row gap-2 items-center">
<div className="w-full relative">
<Input
placeholder="Filter services..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pr-10"
/>
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Checkbox
checked={selectedServices.length > 0}
className={cn(
"data-[state=checked]:bg-primary",
selectedServices.length > 0 &&
selectedServices.length <
filteredServices.length &&
"bg-primary/50",
)}
onCheckedChange={handleSelectAll}
/>
<span className="text-sm">
Select All{" "}
{selectedServices.length > 0 &&
`(${selectedServices.length}/${filteredServices.length})`}
</span>
</div>
<DropdownMenu
open={isDropdownOpen}
onOpenChange={setIsDropdownOpen}
>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
disabled={selectedServices.length === 0}
isLoading={isBulkActionLoading}
>
Bulk Actions
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DialogAction
title="Start Services"
description={`Are you sure you want to start ${selectedServices.length} services?`}
type="default"
onClick={handleBulkStart}
>
<Button
variant="ghost"
className="w-full justify-start"
>
<CheckCircle2 className="mr-2 h-4 w-4" />
Start
</Button>
</DialogAction>
<DialogAction
title="Stop Services"
description={`Are you sure you want to stop ${selectedServices.length} services?`}
type="destructive"
onClick={handleBulkStop}
>
<Button
variant="ghost"
className="w-full justify-start text-destructive"
>
<Ban className="mr-2 h-4 w-4" />
Stop
</Button>
</DialogAction>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Popover open={openCombobox} onOpenChange={setOpenCombobox}>
<PopoverTrigger asChild>
<Button
variant="outline"
aria-expanded={openCombobox}
className="min-w-[200px] justify-between"
>
{selectedTypes.length === 0
? "Select types..."
: `${selectedTypes.length} selected`}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search type..." />
<CommandEmpty>No type found.</CommandEmpty>
<CommandGroup>
{serviceTypes.map((type) => (
<div className="flex flex-col gap-2 sm:flex-row sm:gap-4 sm:items-center">
<div className="w-full relative">
<Input
placeholder="Filter services..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pr-10"
/>
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
</div>
<Popover
open={openCombobox}
onOpenChange={setOpenCombobox}
>
<PopoverTrigger asChild>
<Button
variant="outline"
aria-expanded={openCombobox}
className="min-w-[200px] justify-between"
>
{selectedTypes.length === 0
? "Select types..."
: `${selectedTypes.length} selected`}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search type..." />
<CommandEmpty>No type found.</CommandEmpty>
<CommandGroup>
{serviceTypes.map((type) => (
<CommandItem
key={type.value}
onSelect={() => {
setSelectedTypes((prev) =>
prev.includes(type.value)
? prev.filter((t) => t !== type.value)
: [...prev, type.value],
);
setOpenCombobox(false);
}}
>
<div className="flex flex-row">
<Check
className={cn(
"mr-2 h-4 w-4",
selectedTypes.includes(type.value)
? "opacity-100"
: "opacity-0",
)}
/>
{type.icon && (
<type.icon className="mr-2 h-4 w-4" />
)}
{type.label}
</div>
</CommandItem>
))}
<CommandItem
key={type.value}
onSelect={() => {
setSelectedTypes((prev) =>
prev.includes(type.value)
? prev.filter((t) => t !== type.value)
: [...prev, type.value],
);
setSelectedTypes([]);
setOpenCombobox(false);
}}
className="border-t"
>
<div className="flex flex-row">
<Check
className={cn(
"mr-2 h-4 w-4",
selectedTypes.includes(type.value)
? "opacity-100"
: "opacity-0",
)}
/>
{type.icon && (
<type.icon className="mr-2 h-4 w-4" />
)}
{type.label}
<div className="flex flex-row items-center">
<X className="mr-2 h-4 w-4" />
Clear filters
</div>
</CommandItem>
))}
<CommandItem
onSelect={() => {
setSelectedTypes([]);
setOpenCombobox(false);
}}
className="border-t"
>
<div className="flex flex-row items-center">
<X className="mr-2 h-4 w-4" />
Clear filters
</div>
</CommandItem>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
<div className="flex w-full gap-8">
@@ -408,6 +562,27 @@ const Project = (
<StatusTooltip status={service.status} />
</div>
<div
className={cn(
"absolute -left-3 -bottom-3 size-9 translate-y-1 rounded-full p-0 transition-all duration-200 z-10 bg-background border",
selectedServices.includes(service.id)
? "opacity-100 translate-y-0"
: "opacity-0 group-hover:translate-y-0 group-hover:opacity-100",
)}
onClick={(e) =>
handleServiceSelect(service.id, e)
}
>
<div className="h-full w-full flex items-center justify-center">
<Checkbox
checked={selectedServices.includes(
service.id,
)}
className="data-[state=checked]:bg-primary"
/>
</div>
</div>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex flex-row items-center gap-2 justify-between w-full">
@@ -482,7 +657,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: {
@@ -498,8 +673,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,
});
@@ -516,7 +691,7 @@ export async function getServerSideProps(
projectId: params?.projectId,
},
};
} catch (error) {
} catch (_error) {
return {
redirect: {
permanent: false,

View File

@@ -13,13 +13,13 @@ import { ShowGeneralApplication } from "@/components/dashboard/application/gener
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments";
import { UpdateApplication } from "@/components/dashboard/application/update-application";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -38,9 +38,10 @@ 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 { GlobeIcon, HelpCircle, ServerOff, Trash2 } from "lucide-react";
import copy from "copy-to-clipboard";
import { GlobeIcon, HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -48,7 +49,7 @@ import type {
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useState, useEffect, type ReactElement } from "react";
import { type ReactElement, useEffect, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
@@ -64,6 +65,7 @@ type TabState =
const Service = (
props: InferGetServerSidePropsType<typeof getServerSideProps>,
) => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { applicationId, activeTab } = props;
const router = useRouter();
const { projectId } = router.query;
@@ -82,17 +84,8 @@ const Service = (
},
);
const { mutateAsync, isLoading: isRemoving } =
api.application.delete.useMutation();
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: isCloud } = api.settings.isCloud.useQuery();
const { data: auth } = api.user.get.useQuery();
return (
<div className="pb-10">
@@ -140,6 +133,13 @@ const Service = (
<div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
className="cursor-pointer"
onClick={() => {
if (data?.server?.ipAddress) {
copy(data.server.ipAddress);
toast.success("IP Address Copied!");
}
}}
variant={
!data?.serverId
? "default"
@@ -176,35 +176,8 @@ const Service = (
<div className="flex flex-row gap-2 justify-end">
<UpdateApplication applicationId={applicationId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Delete Application"
description="Are you sure you want to delete this application?"
type="destructive"
onClick={async () => {
await mutateAsync({
applicationId: applicationId,
})
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Application deleted successfully");
})
.catch(() => {
toast.error("Error deleting application");
});
}}
>
<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>
{(auth?.role === "owner" || auth?.canDeleteServices) && (
<DeleteService id={applicationId} type="application" />
)}
</div>
</div>
@@ -246,12 +219,16 @@ const Service = (
<TabsList
className={cn(
"flex gap-8 justify-start max-xl:overflow-x-scroll overflow-y-hidden",
data?.serverId ? "md:grid-cols-7" : "md:grid-cols-8",
isCloud && data?.serverId
? "md:grid-cols-7"
: data?.serverId
? "md:grid-cols-6"
: "md:grid-cols-7",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
{((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="logs">Logs</TabsTrigger>
@@ -274,13 +251,54 @@ const Service = (
<ShowEnvironment applicationId={applicationId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg p-6">
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
token={
data?.server?.metricsConfig?.server?.token || ""
}
/>
) : (
<>
{/* {monitoring?.enabledFeatures &&
isCloud &&
data?.serverId && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
<Label className="text-muted-foreground">
Change Monitoring
</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)} */}
{/* {toggleMonitoring ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
token={
monitoring?.metricsConfig?.server?.token || ""
}
/>
) : ( */}
<div>
<ContainerFreeMonitoring
appName={data?.appName || ""}
/>
</div>
{/* )} */}
</>
)}
</div>
</TabsContent>
)}
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
@@ -342,7 +360,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: {
@@ -358,8 +376,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,
});
@@ -371,6 +389,8 @@ export async function getServerSideProps(
applicationId: params?.applicationId,
});
await helpers.settings.isCloud.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
@@ -378,7 +398,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (error) {
} catch (_error) {
return {
redirect: {
permanent: false,

View File

@@ -1,14 +1,15 @@
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
import { DeleteCompose } from "@/components/dashboard/compose/delete-compose";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show-domains";
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack";
import { ShowMonitoringCompose } from "@/components/dashboard/compose/monitoring/show";
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring";
import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
@@ -31,8 +32,9 @@ 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";
import { HelpCircle } from "lucide-react";
import type {
@@ -42,7 +44,8 @@ import type {
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useState, useEffect, type ReactElement } from "react";
import { type ReactElement, useEffect, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
type TabState =
@@ -56,6 +59,7 @@ type TabState =
const Service = (
props: InferGetServerSidePropsType<typeof getServerSideProps>,
) => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { composeId, activeTab } = props;
const router = useRouter();
const { projectId } = router.query;
@@ -74,15 +78,8 @@ const Service = (
},
);
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: isCloud } = api.settings.isCloud.useQuery();
return (
<div className="pb-10">
@@ -131,6 +128,13 @@ const Service = (
<div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
className="cursor-pointer"
onClick={() => {
if (data?.server?.ipAddress) {
copy(data.server.ipAddress);
toast.success("IP Address Copied!");
}
}}
variant={
!data?.serverId
? "default"
@@ -142,7 +146,7 @@ const Service = (
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
@@ -167,8 +171,8 @@ const Service = (
<div className="flex flex-row gap-2 justify-end">
<UpdateCompose composeId={composeId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteCompose composeId={composeId} />
{(auth?.role === "owner" || auth?.canDeleteServices) && (
<DeleteService id={composeId} type="compose" />
)}
</div>
</div>
@@ -211,22 +215,16 @@ const Service = (
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
data?.composeType === "docker-compose"
? ""
: "md:grid-cols-6",
data?.serverId && data?.composeType === "stack"
? "md:grid-cols-5"
: "",
isCloud && data?.serverId
? "md:grid-cols-7"
: data?.serverId
? "md:grid-cols-6"
: "md:grid-cols-7",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
{data?.composeType === "docker-compose" && (
<TabsTrigger value="environment">
Environment
</TabsTrigger>
)}
{!data?.serverId && (
<TabsTrigger value="environment">Environment</TabsTrigger>
{((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="logs">Logs</TabsTrigger>
@@ -246,17 +244,59 @@ const Service = (
<ShowEnvironment id={composeId} type="compose" />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMonitoringCompose
serverId={data?.serverId || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col border rounded-lg ">
{data?.serverId && isCloud ? (
<ComposePaidMonitoring
serverId={data?.serverId || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
appName={data?.appName || ""}
token={
data?.server?.metricsConfig?.server?.token || ""
}
appType={data?.composeType || "docker-compose"}
/>
) : (
<>
{/* {monitoring?.enabledFeatures &&
isCloud &&
data?.serverId && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2 m-4">
<Label className="text-muted-foreground">
Change Monitoring
</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)}
{toggleMonitoring ? (
<ComposePaidMonitoring
appName={data?.appName || ""}
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
token={
monitoring?.metricsConfig?.server?.token || ""
}
appType={data?.composeType || "docker-compose"}
/>
) : ( */}
{/* <div> */}
<ComposeFreeMonitoring
serverId={data?.serverId || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
{/* </div> */}
{/* )} */}
</>
)}
</div>
</TabsContent>
)}
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
@@ -316,7 +356,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 +372,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,
});
@@ -344,7 +384,7 @@ export async function getServerSideProps(
await helpers.compose.one.fetch({
composeId: params?.composeId,
});
await helpers.settings.isCloud.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
@@ -352,7 +392,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (error) {
} catch (_error) {
return {
redirect: {
permanent: false,

View File

@@ -2,20 +2,20 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ShowExternalMariadbCredentials } from "@/components/dashboard/mariadb/general/show-external-mariadb-credentials";
import { ShowGeneralMariadb } from "@/components/dashboard/mariadb/general/show-general-mariadb";
import { ShowInternalMariadbCredentials } from "@/components/dashboard/mariadb/general/show-internal-mariadb-credentials";
import { UpdateMariadb } from "@/components/dashboard/mariadb/update-mariadb";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
import { MariadbIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -34,9 +34,9 @@ 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 { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -44,8 +44,7 @@ import type {
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useState, type ReactElement } from "react";
import { toast } from "sonner";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
@@ -53,22 +52,17 @@ type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
const Mariadb = (
props: InferGetServerSidePropsType<typeof getServerSideProps>,
) => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { mariadbId, activeTab } = props;
const router = useRouter();
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: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { mutateAsync: remove, isLoading: isRemoving } =
api.mariadb.remove.useMutation();
const { data: auth } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
return (
<div className="pb-10">
<BreadcrumbSidebar
@@ -148,35 +142,10 @@ const Mariadb = (
</TooltipProvider>
)}
</div>
<div className="flex flex-row gap-2">
<div className="flex flex-row gap-2 justify-end">
<UpdateMariadb mariadbId={mariadbId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove Mariadb"
description="Are you sure you want to delete this mariadb?"
type="destructive"
onClick={async () => {
await remove({ mariadbId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Mariadb deleted successfully");
})
.catch(() => {
toast.error("Error deleting the mariadb");
});
}}
>
<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>
{(auth?.role === "owner" || auth?.canDeleteServices) && (
<DeleteService id={mariadbId} type="mariadb" />
)}
</div>
</div>
@@ -219,12 +188,16 @@ const Mariadb = (
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
isCloud && data?.serverId
? "md:grid-cols-6"
: data?.serverId
? "md:grid-cols-5"
: "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
{((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
@@ -245,13 +218,51 @@ const Mariadb = (
<ShowEnvironment id={mariadbId} type="mariadb" />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg p-6">
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
token={
data?.server?.metricsConfig?.server?.token || ""
}
/>
) : (
<>
{/* {monitoring?.enabledFeatures && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
<Label className="text-muted-foreground">
Change Monitoring
</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)}
{toggleMonitoring ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
token={
monitoring?.metricsConfig?.server?.token || ""
}
/>
) : (
<div> */}
<ContainerFreeMonitoring
appName={data?.appName || ""}
/>
{/* </div> */}
{/* )} */}
</>
)}
</div>
</TabsContent>
)}
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
@@ -295,7 +306,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: {
@@ -311,8 +322,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,
});
@@ -322,7 +333,7 @@ export async function getServerSideProps(
await helpers.mariadb.one.fetch({
mariadbId: params?.mariadbId,
});
await helpers.settings.isCloud.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
@@ -330,7 +341,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (error) {
} catch (_error) {
return {
redirect: {
permanent: false,

View File

@@ -2,21 +2,20 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ShowExternalMongoCredentials } from "@/components/dashboard/mongo/general/show-external-mongo-credentials";
import { ShowGeneralMongo } from "@/components/dashboard/mongo/general/show-general-mongo";
import { ShowInternalMongoCredentials } from "@/components/dashboard/mongo/general/show-internal-mongo-credentials";
import { UpdateMongo } from "@/components/dashboard/mongo/update-mongo";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
import { MongodbIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -35,9 +34,9 @@ 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 { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -45,8 +44,7 @@ import type {
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useState, type ReactElement } from "react";
import { toast } from "sonner";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
@@ -54,23 +52,16 @@ type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
const Mongo = (
props: InferGetServerSidePropsType<typeof getServerSideProps>,
) => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { mongoId, activeTab } = props;
const router = useRouter();
const { projectId } = router.query;
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.mongo.one.useQuery({ mongoId });
const { data: auth } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { mutateAsync: remove, isLoading: isRemoving } =
api.mongo.remove.useMutation();
const { data: auth } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
return (
<div className="pb-10">
@@ -154,33 +145,8 @@ const Mongo = (
<div className="flex flex-row gap-2 justify-end">
<UpdateMongo mongoId={mongoId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove mongo"
description="Are you sure you want to delete this mongo?"
type="destructive"
onClick={async () => {
await remove({ mongoId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Mongo deleted successfully");
})
.catch(() => {
toast.error("Error deleting the mongo");
});
}}
>
<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>
{(auth?.role === "owner" || auth?.canDeleteServices) && (
<DeleteService id={mongoId} type="mongo" />
)}
</div>
</div>
@@ -223,12 +189,16 @@ const Mongo = (
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
isCloud && data?.serverId
? "md:grid-cols-6"
: data?.serverId
? "md:grid-cols-5"
: "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
{((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
@@ -249,13 +219,51 @@ const Mongo = (
<ShowEnvironment id={mongoId} type="mongo" />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg p-6">
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
token={
data?.server?.metricsConfig?.server?.token || ""
}
/>
) : (
<>
{/* {monitoring?.enabledFeatures && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
<Label className="text-muted-foreground">
Change Monitoring
</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)}
{toggleMonitoring ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
token={
monitoring?.metricsConfig?.server?.token || ""
}
/>
) : (
<div> */}
<ContainerFreeMonitoring
appName={data?.appName || ""}
/>
{/* </div> */}
{/* )} */}
</>
)}
</div>
</TabsContent>
)}
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
@@ -299,7 +307,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: {
@@ -315,8 +323,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,
});
@@ -326,7 +334,7 @@ export async function getServerSideProps(
await helpers.mongo.one.fetch({
mongoId: params?.mongoId,
});
await helpers.settings.isCloud.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
@@ -334,7 +342,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (error) {
} catch (_error) {
return {
redirect: {
permanent: false,

View File

@@ -2,8 +2,10 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowExternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-external-mysql-credentials";
import { ShowGeneralMysql } from "@/components/dashboard/mysql/general/show-general-mysql";
import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-internal-mysql-credentials";
@@ -12,10 +14,8 @@ import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show
import { MysqlIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -34,9 +34,9 @@ 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 { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -44,8 +44,7 @@ import type {
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useState, type ReactElement } from "react";
import { toast } from "sonner";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
@@ -53,23 +52,16 @@ type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
const MySql = (
props: InferGetServerSidePropsType<typeof getServerSideProps>,
) => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { mysqlId, activeTab } = props;
const router = useRouter();
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: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { data: auth } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync: remove, isLoading: isRemoving } =
api.mysql.remove.useMutation();
return (
<div className="pb-10">
<BreadcrumbSidebar
@@ -153,33 +145,8 @@ const MySql = (
<div className="flex flex-row gap-2 justify-end">
<UpdateMysql mysqlId={mysqlId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove Mysql"
description="Are you sure you want to delete this mysql?"
type="destructive"
onClick={async () => {
await remove({ mysqlId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Mysql deleted successfully");
})
.catch(() => {
toast.error("Error deleting the mysql");
});
}}
>
<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>
{(auth?.role === "owner" || auth?.canDeleteServices) && (
<DeleteService id={mysqlId} type="mysql" />
)}
</div>
</div>
@@ -221,15 +188,19 @@ const MySql = (
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
"md:grid md:w-fit max-md:overflow-y-scroll justify-start ",
isCloud && data?.serverId
? "md:grid-cols-6"
: data?.serverId
? "md:grid-cols-5"
: "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">
Environment
</TabsTrigger>
{!data?.serverId && (
{((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">
Monitoring
</TabsTrigger>
@@ -252,13 +223,51 @@ const MySql = (
<ShowEnvironment id={mysqlId} type="mysql" />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg p-6">
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
token={
data?.server?.metricsConfig?.server?.token || ""
}
/>
) : (
<>
{/* {monitoring?.enabledFeatures && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
<Label className="text-muted-foreground">
Change Monitoring
</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)}
{toggleMonitoring ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
token={
monitoring?.metricsConfig?.server?.token || ""
}
/>
) : (
<div> */}
<ContainerFreeMonitoring
appName={data?.appName || ""}
/>
{/* </div> */}
{/* )} */}
</>
)}
</div>
</TabsContent>
)}
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
@@ -303,7 +312,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: {
@@ -319,8 +328,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,
});
@@ -330,7 +339,7 @@ export async function getServerSideProps(
await helpers.mysql.one.fetch({
mysqlId: params?.mysqlId,
});
await helpers.settings.isCloud.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
@@ -338,7 +347,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (error) {
} catch (_error) {
return {
redirect: {
permanent: false,

View File

@@ -2,8 +2,10 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
import { ShowExternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-external-postgres-credentials";
import { ShowGeneralPostgres } from "@/components/dashboard/postgres/general/show-general-postgres";
@@ -12,11 +14,8 @@ import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres"
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -35,9 +34,9 @@ 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 { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -45,8 +44,7 @@ import type {
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useState, type ReactElement } from "react";
import { toast } from "sonner";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
@@ -54,24 +52,15 @@ type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
const Postgresql = (
props: InferGetServerSidePropsType<typeof getServerSideProps>,
) => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { postgresId, activeTab } = props;
const router = useRouter();
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: auth } = api.user.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { mutateAsync: remove, isLoading: isRemoving } =
api.postgres.remove.useMutation();
const { data: isCloud } = api.settings.isCloud.useQuery();
return (
<div className="pb-10">
@@ -155,33 +144,8 @@ const Postgresql = (
<div className="flex flex-row gap-2 justify-end">
<UpdatePostgres postgresId={postgresId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove Postgres"
description="Are you sure you want to delete this postgres?"
type="destructive"
onClick={async () => {
await remove({ postgresId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Postgres deleted successfully");
})
.catch(() => {
toast.error("Error deleting the postgres");
});
}}
>
<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>
{(auth?.role === "owner" || auth?.canDeleteServices) && (
<DeleteService id={postgresId} type="postgres" />
)}
</div>
</div>
@@ -224,12 +188,16 @@ const Postgresql = (
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
isCloud && data?.serverId
? "md:grid-cols-6"
: data?.serverId
? "md:grid-cols-5"
: "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
{((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
@@ -254,13 +222,51 @@ const Postgresql = (
<ShowEnvironment id={postgresId} type="postgres" />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg p-6">
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
token={
data?.server?.metricsConfig?.server?.token || ""
}
/>
) : (
<>
{/* {monitoring?.enabledFeatures && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
<Label className="text-muted-foreground">
Change Monitoring
</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)}
{toggleMonitoring ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
token={
monitoring?.metricsConfig?.server?.token || ""
}
/>
) : (
<div> */}
<ContainerFreeMonitoring
appName={data?.appName || ""}
/>
{/* </div> */}
{/* )} */}
</>
)}
</div>
</TabsContent>
)}
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
@@ -303,7 +309,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: {
@@ -319,8 +325,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,
});
@@ -330,7 +336,7 @@ export async function getServerSideProps(
await helpers.postgres.one.fetch({
postgresId: params?.postgresId,
});
await helpers.settings.isCloud.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
@@ -338,7 +344,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (error) {
} catch (_error) {
return {
redirect: {
permanent: false,

View File

@@ -2,7 +2,9 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
import { ShowExternalRedisCredentials } from "@/components/dashboard/redis/general/show-external-redis-credentials";
import { ShowGeneralRedis } from "@/components/dashboard/redis/general/show-general-redis";
@@ -11,10 +13,8 @@ import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
import { RedisIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -33,9 +33,9 @@ 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 { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -43,8 +43,7 @@ import type {
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useState, type ReactElement } from "react";
import { toast } from "sonner";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
type TabState = "projects" | "monitoring" | "settings" | "advanced";
@@ -52,24 +51,17 @@ type TabState = "projects" | "monitoring" | "settings" | "advanced";
const Redis = (
props: InferGetServerSidePropsType<typeof getServerSideProps>,
) => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { redisId, activeTab } = props;
const router = useRouter();
const { projectId } = router.query;
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.redis.one.useQuery({ redisId });
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: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync: remove, isLoading: isRemoving } =
api.redis.remove.useMutation();
return (
<div className="pb-10">
<BreadcrumbSidebar
@@ -152,33 +144,8 @@ const Redis = (
<div className="flex flex-row gap-2 justify-end">
<UpdateRedis redisId={redisId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove Redis"
description="Are you sure you want to delete this redis?"
type="destructive"
onClick={async () => {
await remove({ redisId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Redis deleted successfully");
})
.catch(() => {
toast.error("Error deleting the redis");
});
}}
>
<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>
{(auth?.role === "owner" || auth?.canDeleteServices) && (
<DeleteService id={redisId} type="redis" />
)}
</div>
</div>
@@ -221,12 +188,16 @@ const Redis = (
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-4" : "md:grid-cols-5",
isCloud && data?.serverId
? "md:grid-cols-5"
: data?.serverId
? "md:grid-cols-4"
: "md:grid-cols-5",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
{((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="logs">Logs</TabsTrigger>
@@ -246,13 +217,51 @@ const Redis = (
<ShowEnvironment id={redisId} type="redis" />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg p-6">
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
token={
data?.server?.metricsConfig?.server?.token || ""
}
/>
) : (
<>
{/* {monitoring?.enabledFeatures && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
<Label className="text-muted-foreground">
Change Monitoring
</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)}
{toggleMonitoring ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
token={
monitoring?.metricsConfig?.server?.token || ""
}
/>
) : (
<div> */}
<ContainerFreeMonitoring
appName={data?.appName || ""}
/>
{/* </div> */}
{/* )} */}
</>
)}
</div>
</TabsContent>
)}
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
@@ -291,7 +300,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: {
@@ -307,8 +316,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,
});
@@ -317,7 +326,7 @@ export async function getServerSideProps(
await helpers.redis.one.fetch({
redisId: params?.redisId,
});
await helpers.settings.isCloud.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
@@ -325,7 +334,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (error) {
} catch (_error) {
return {
redirect: {
permanent: false,

View File

@@ -2,11 +2,10 @@ 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";
import type React from "react";
import type { ReactElement } from "react";
import superjson from "superjson";
@@ -38,7 +37,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,14 +45,14 @@ 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();
await helpers.user.get.prefetch();
if (!user) {
return {
redirect: {

View File

@@ -1,9 +1,9 @@
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";
export default function Requests() {
return <ShowRequests />;
@@ -22,7 +22,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,10 +2,11 @@ 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";
import type { ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
@@ -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,13 +46,13 @@ 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.auth.get.prefetch();
await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch();

View File

@@ -5,7 +5,7 @@ import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import type { ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
return (
@@ -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,12 +40,12 @@ 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.auth.get.prefetch();
await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {

View File

@@ -5,7 +5,7 @@ import { appRouter } from "@/server/api/root";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import type { ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
@@ -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,12 +48,12 @@ 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.auth.get.prefetch();
await helpers.user.get.prefetch();
return {
props: {

View File

@@ -5,7 +5,7 @@ import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import type { ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
@@ -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,12 +41,12 @@ 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.auth.get.prefetch();
await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {

View File

@@ -5,7 +5,7 @@ import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import type { ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
@@ -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,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.auth.get.prefetch();
await helpers.user.get.prefetch();
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,
@@ -70,7 +68,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (error) {
} catch (_error) {
return {
props: {},
};

View File

@@ -0,0 +1,219 @@
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { DialogFooter } from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { Settings } from "lucide-react";
import type { GetServerSidePropsContext } from "next";
import { type ReactElement, useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import superjson from "superjson";
import { z } from "zod";
const settings = z.object({
cleanCacheOnApplications: z.boolean(),
cleanCacheOnCompose: z.boolean(),
cleanCacheOnPreviews: z.boolean(),
});
type SettingsType = z.infer<typeof settings>;
const Page = () => {
const { data, refetch } = api.user.get.useQuery();
const { mutateAsync, isLoading, isError, error } =
api.user.update.useMutation();
const form = useForm<SettingsType>({
defaultValues: {
cleanCacheOnApplications: false,
cleanCacheOnCompose: false,
cleanCacheOnPreviews: false,
},
resolver: zodResolver(settings),
});
useEffect(() => {
form.reset({
cleanCacheOnApplications: data?.user.cleanupCacheApplications || false,
cleanCacheOnCompose: data?.user.cleanupCacheOnCompose || false,
cleanCacheOnPreviews: data?.user.cleanupCacheOnPreviews || false,
});
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
const onSubmit = async (values: SettingsType) => {
await mutateAsync({
cleanupCacheApplications: values.cleanCacheOnApplications,
cleanupCacheOnCompose: values.cleanCacheOnCompose,
cleanupCacheOnPreviews: values.cleanCacheOnPreviews,
})
.then(() => {
toast.success("Settings updated");
refetch();
})
.catch(() => {
toast.error("Something went wrong");
});
};
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">
<Settings className="size-6 text-muted-foreground self-center" />
Settings
</CardTitle>
<CardDescription>Manage your Dokploy settings</CardDescription>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
<Form {...form}>
<form
id="hook-form-add-security"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-2"
>
<FormField
control={form.control}
name="cleanCacheOnApplications"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Clean Cache on Applications</FormLabel>
<FormDescription>
Clean the cache after every application deployment
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="cleanCacheOnPreviews"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Clean Cache on Previews</FormLabel>
<FormDescription>
Clean the cache after every preview deployment
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="cleanCacheOnCompose"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Clean Cache on Compose</FormLabel>
<FormDescription>
Clean the cache after every compose deployment
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-add-security"
type="submit"
>
Update
</Button>
</DialogFooter>
</form>
</Form>
</CardContent>
</div>
</Card>
</div>
);
};
export default Page;
Page.getLayout = (page: ReactElement) => {
return <DashboardLayout metaName="Server">{page}</DashboardLayout>;
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const { user, session } = await validateRequest(ctx.req);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
if (user.role === "member") {
return {
redirect: {
permanent: true,
destination: "/dashboard/settings/profile",
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session as any,
user: user as any,
},
transformer: superjson,
});
await helpers.user.get.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -5,7 +5,7 @@ import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import type { ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
@@ -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,12 +41,12 @@ 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.auth.get.prefetch();
await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {

View File

@@ -1,6 +1,5 @@
import { GenerateToken } from "@/components/dashboard/settings/profile/generate-token";
import { ShowApiKeys } from "@/components/dashboard/settings/api/show-api-keys";
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
import { RemoveSelfAccount } from "@/components/dashboard/settings/profile/remove-self-account";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
@@ -9,28 +8,20 @@ import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import 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();
// 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?.canAccessToAPI || data?.role === "owner") && <ShowApiKeys />}
{isCloud && <RemoveSelfAccount />}
{/* {isCloud && <RemoveSelfAccount />} */}
</div>
</div>
);
@@ -46,7 +37,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,19 +45,14 @@ 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,
});
}
await helpers.user.get.prefetch();
if (!user) {
return {

View File

@@ -5,7 +5,7 @@ import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import type { ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
@@ -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,12 +40,12 @@ 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.auth.get.prefetch();
await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {

View File

@@ -1,13 +1,12 @@
import { WebDomain } from "@/components/dashboard/settings/web-domain";
import { WebServer } from "@/components/dashboard/settings/web-server";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import type { ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
@@ -16,6 +15,49 @@ const Page = () => {
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<WebDomain />
<WebServer />
{/* <Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<LayoutDashboardIcon className="size-6 text-muted-foreground self-center" />
Paid Features
</CardTitle>
<CardDescription>
Enable or disable paid features like monitoring
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-row gap-2 items-center">
<span className="text-sm font-medium text-muted-foreground">
Enable Paid Features:
</span>
<Switch
checked={data?.enablePaidFeatures}
onCheckedChange={() => {
update({
enablePaidFeatures: !data?.enablePaidFeatures,
})
.then(() => {
toast.success(
`Paid features ${
data?.enablePaidFeatures ? "disabled" : "enabled"
} successfully`,
);
refetch();
})
.catch(() => {
toast.error("Error updating paid features");
});
}}
/>
</div>
</CardContent>
{data?.enablePaidFeatures && <SetupMonitoring />}
</div>
</Card> */}
{/* */}
</div>
</div>
);
@@ -39,7 +81,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: {
@@ -48,7 +90,7 @@ export async function getServerSideProps(
},
};
}
if (user.rol === "user") {
if (user.role === "member") {
return {
redirect: {
permanent: true,
@@ -63,12 +105,12 @@ 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.auth.get.prefetch();
await helpers.user.get.prefetch();
return {
props: {

View File

@@ -6,7 +6,7 @@ import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import type { ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
@@ -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,12 +51,12 @@ 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.auth.get.prefetch();
await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {

View File

@@ -5,7 +5,7 @@ import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import type { ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
@@ -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: {
@@ -33,30 +33,29 @@ export async function getServerSideProps(
},
};
}
const { req, res, resolvedUrl } = ctx;
const { req, res } = ctx;
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
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,
@@ -70,7 +69,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (error) {
} catch (_error) {
return {
props: {},
};

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";
@@ -5,13 +6,14 @@ import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import type { ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowUsers />
<ShowInvitations />
</div>
);
};
@@ -25,8 +27,9 @@ 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,12 +44,12 @@ 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.auth.get.prefetch();
await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {

View File

@@ -1,18 +1,15 @@
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";
import superjson from "superjson";
const Dashboard = () => {
return (
<>
<SwarmMonitorCard />
</>
);
return <SwarmMonitorCard />;
};
export default Dashboard;
@@ -31,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: {
@@ -48,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,
@@ -76,7 +72,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (error) {
} catch (_error) {
return {
props: {},
};

View File

@@ -1,10 +1,11 @@
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";
import type { ReactElement } from "react";
import superjson from "superjson";
const Dashboard = () => {
@@ -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,
@@ -72,7 +72,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (error) {
} catch (_error) {
return {
props: {},
};

View File

@@ -1,9 +1,15 @@
import { Login2FA } from "@/components/auth/login-2fa";
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 { Button } from "@/components/ui/button";
import { CardContent, CardDescription } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
@@ -13,88 +19,186 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";
import { authClient } from "@/lib/auth-client";
import { IS_CLOUD, isAdminPresent } from "@dokploy/server";
import { validateRequest } from "@dokploy/server/lib/auth";
import { zodResolver } from "@hookform/resolvers/zod";
import { REGEXP_ONLY_DIGITS } from "input-otp";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useEffect, useState } from "react";
import { type ReactElement, 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;
};
type LoginForm = z.infer<typeof LoginSchema>;
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 [isGithubLoading, setIsGithubLoading] = useState(false);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const loginForm = useForm<LoginForm>({
resolver: zodResolver(LoginSchema),
defaultValues: {
email: "",
password: "",
},
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;
}
// @ts-ignore
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 { 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 { 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);
}
};
const handleGithubSignIn = async () => {
setIsGithubLoading(true);
try {
const { error } = await authClient.signIn.social({
provider: "github",
});
if (error) {
toast.error(error.message);
return;
}
} catch (error) {
toast.error("An error occurred while signing in with GitHub", {
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setIsGithubLoading(false);
}
};
const handleGoogleSignIn = async () => {
setIsGoogleLoading(true);
try {
const { error } = await authClient.signIn.social({
provider: "google",
});
if (error) {
toast.error(error.message);
return;
}
} catch (error) {
toast.error("An error occurred while signing in with Google", {
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setIsGoogleLoading(false);
}
};
return (
<>
@@ -109,31 +213,81 @@ 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">
{!isTwoFactor ? (
<>
{IS_CLOUD && (
<Button
variant="outline"
type="button"
className="w-full mb-4"
onClick={handleGithubSignIn}
isLoading={isGithubLoading}
>
<svg viewBox="0 0 438.549 438.549" className="mr-2 size-4">
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
/>
</svg>
Sign in with GitHub
</Button>
)}
{IS_CLOUD && (
<Button
variant="outline"
type="button"
className="w-full mb-4"
onClick={handleGoogleSignIn}
isLoading={isGoogleLoading}
>
<svg viewBox="0 0 24 24" className="mr-2 size-4">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign in with Google
</Button>
)}
<Form {...loginForm}>
<form
onSubmit={loginForm.handleSubmit(onSubmit)}
className="space-y-4"
id="login-form"
>
<FormField
control={form.control}
control={loginForm.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Email" {...field} />
<Input placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
control={loginForm.control}
name="password"
render={({ field }) => (
<FormItem>
@@ -141,7 +295,7 @@ export default function Home({ IS_CLOUD }: Props) {
<FormControl>
<Input
type="password"
placeholder="Password"
placeholder="Enter your password"
{...field}
/>
</FormControl>
@@ -149,15 +303,127 @@ export default function Home({ IS_CLOUD }: Props) {
</FormItem>
)}
/>
<Button type="submit" isLoading={isLoading} className="w-full">
<Button
className="w-full"
type="submit"
isLoading={isLoginLoading}
>
Login
</Button>
</form>
</Form>
</>
) : (
<>
<form
onSubmit={onTwoFactorSubmit}
className="space-y-4"
id="two-factor-form"
autoComplete="off"
>
<div className="flex flex-col gap-2">
<Label>2FA Code</Label>
<InputOTP
value={twoFactorCode}
onChange={setTwoFactorCode}
maxLength={6}
pattern={REGEXP_ONLY_DIGITS}
autoComplete="off"
>
<InputOTPGroup>
<InputOTPSlot index={0} className="border-border" />
<InputOTPSlot index={1} className="border-border" />
<InputOTPSlot index={2} className="border-border" />
<InputOTPSlot index={3} className="border-border" />
<InputOTPSlot index={4} className="border-border" />
<InputOTPSlot index={5} className="border-border" />
</InputOTPGroup>
</InputOTP>
<CardDescription>
Enter the 6-digit code from your authenticator app
</CardDescription>
<button
type="button"
onClick={() => setIsBackupCodeModalOpen(true)}
className="text-sm text-muted-foreground hover:underline self-start mt-2"
>
Lost access to your authenticator app?
</button>
</div>
<div className="flex gap-4">
<Button
variant="outline"
className="w-full"
type="button"
onClick={() => {
setIsTwoFactor(false);
setTwoFactorCode("");
}}
>
Back
</Button>
<Button
className="w-full"
type="submit"
isLoading={isTwoFactorLoading}
>
Verify
</Button>
</div>
</form>
</Form>
) : (
<Login2FA authId={temp.authId} />
<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 +469,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: {
@@ -213,7 +478,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
},
};
}
} catch (error) {}
} catch (_error) {}
return {
props: {
@@ -232,7 +497,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,12 +1,8 @@
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 {
Card,
CardContent,
CardDescription,
CardTitle,
} from "@/components/ui/card";
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
@@ -16,10 +12,10 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { IS_CLOUD, getUserByToken } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -30,6 +26,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 +37,6 @@ const registerSchema = z
.email({
message: "Email must be a valid email",
}),
password: z
.string()
.min(1, {
@@ -71,11 +69,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,
},
@@ -85,11 +89,9 @@ const Invitation = ({ token, invitation, isCloud }: Props) => {
},
);
const { mutateAsync, error, isError, isSuccess } =
api.auth.createUser.useMutation();
const form = useForm<Register>({
defaultValues: {
name: "",
email: "",
password: "",
confirmPassword: "",
@@ -98,9 +100,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 +110,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 { 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 +152,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>;
@@ -255,6 +310,15 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const token = query.token;
// if (IS_CLOUD) {
// return {
// redirect: {
// permanent: true,
// destination: "/",
// },
// };
// }
if (typeof token !== "string") {
return {
redirect: {
@@ -267,6 +331,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 +359,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
},
};
} catch (error) {
console.log("error", error);
return {
redirect: {
permanent: true,

View File

@@ -2,12 +2,7 @@ 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 {
Card,
CardContent,
CardDescription,
CardTitle,
} from "@/components/ui/card";
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
@@ -17,20 +12,23 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { authClient } from "@/lib/auth-client";
import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useEffect } from "react";
import { type ReactElement, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const registerSchema = z
.object({
name: z.string().min(1, {
message: "Name is required",
}),
email: z
.string()
.min(1, {
@@ -74,11 +72,13 @@ interface Props {
const Register = ({ isCloud }: Props) => {
const router = useRouter();
const { mutateAsync, error, isError, data } =
api.auth.createAdmin.useMutation();
const [isError, setIsError] = useState(false);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<any>(null);
const form = useForm<Register>({
defaultValues: {
name: "",
email: "",
password: "",
confirmPassword: "",
@@ -91,19 +91,25 @@ 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,
});
if (error) {
setIsError(true);
setError(error.message || "An error occurred");
} else {
toast.success("User registered successfuly", {
duration: 2000,
});
if (!isCloud) {
router.push("/");
} else {
setData(data);
}
}
};
return (
<div className="">
@@ -125,15 +131,15 @@ const Register = ({ isCloud }: Props) => {
</CardDescription>
<div className="mx-auto w-full max-w-lg bg-transparent">
{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">
<div className=" 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}
{error}
</span>
</div>
)}
{data?.type === "cloud" && (
<AlertBlock type="success" className="mx-4 my-2">
{isCloud && data && (
<AlertBlock type="success" className="my-2">
<span>
Registered successfully, please check your inbox or spam
folder to confirm your account.
@@ -147,6 +153,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 +261,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

@@ -12,17 +12,13 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { db } from "@/server/db";
import { auth } from "@/server/db/schema";
import { api } from "@/utils/api";
import { authClient } from "@/lib/auth-client";
import { IS_CLOUD } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod";
import { isBefore } from "date-fns";
import { eq } from "drizzle-orm";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useEffect } from "react";
import { type ReactElement, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -54,11 +50,12 @@ const loginSchema = z
type Login = z.infer<typeof loginSchema>;
interface Props {
token: string;
tokenResetPassword: string;
}
export default function Home({ token }: Props) {
const { mutateAsync, isLoading, isError, error } =
api.auth.resetPassword.useMutation();
export default function Home({ tokenResetPassword }: Props) {
const [token, setToken] = useState<string | null>(tokenResetPassword);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const form = useForm<Login>({
defaultValues: {
@@ -68,26 +65,32 @@ export default function Home({ token }: Props) {
resolver: zodResolver(loginSchema),
});
useEffect(() => {
const token = new URLSearchParams(window.location.search).get("token");
if (token) {
setToken(token);
}
}, [token]);
useEffect(() => {
form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (values: Login) => {
await mutateAsync({
resetPasswordToken: token,
password: values.password,
})
.then((data) => {
toast.success("Password reset successfully", {
duration: 2000,
});
router.push("/");
})
.catch(() => {
toast.error("Error resetting password", {
duration: 2000,
});
});
setIsLoading(true);
const { error } = await authClient.resetPassword({
newPassword: values.password,
token: token || "",
});
if (error) {
setError(error.message || "An error occurred");
} else {
toast.success("Password reset successfully");
router.push("/");
}
setIsLoading(false);
};
return (
<div className="flex h-screen w-full items-center justify-center ">
@@ -104,9 +107,9 @@ export default function Home({ token }: Props) {
<div className="w-full">
<CardContent className="p-0">
{isError && (
{error && (
<AlertBlock type="error" className="my-2">
{error?.message}
{error}
</AlertBlock>
)}
<Form {...form}>
@@ -194,35 +197,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
};
}
const authR = await db.query.auth.findFirst({
where: eq(auth.resetPasswordToken, token),
});
if (!authR || authR?.resetPasswordExpiresAt === null) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const isExpired = isBefore(
new Date(authR.resetPasswordExpiresAt),
new Date(),
);
if (isExpired) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
return {
props: {
token: authR.resetPasswordToken,
tokenResetPassword: token,
},
};
}

View File

@@ -1,14 +1,8 @@
import { Login2FA } from "@/components/auth/login-2fa";
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 {
Card,
CardContent,
CardDescription,
CardTitle,
} from "@/components/ui/card";
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
@@ -18,7 +12,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { authClient } from "@/lib/auth-client";
import { IS_CLOUD } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod";
import type { GetServerSidePropsContext } from "next";
@@ -48,13 +42,14 @@ type AuthResponse = {
};
export default function Home() {
const [temp, setTemp] = useState<AuthResponse>({
const [temp, _setTemp] = useState<AuthResponse>({
is2FAEnabled: false,
authId: "",
});
const { mutateAsync, isLoading, isError, error } =
api.auth.sendResetPasswordEmail.useMutation();
const router = useRouter();
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const _router = useRouter();
const form = useForm<Login>({
defaultValues: {
email: "",
@@ -67,19 +62,20 @@ export default function Home() {
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (values: Login) => {
await mutateAsync({
setIsLoading(true);
const { error } = await authClient.forgetPassword({
email: values.email,
})
.then((data) => {
toast.success("Email sent", {
duration: 2000,
});
})
.catch(() => {
toast.error("Error sending email", {
duration: 2000,
});
redirectTo: "/reset-password",
});
if (error) {
setError(error.message || "An error occurred");
setIsLoading(false);
} else {
toast.success("Email sent", {
duration: 2000,
});
}
setIsLoading(false);
};
return (
<div className="flex w-full items-center justify-center ">
@@ -95,9 +91,9 @@ export default function Home() {
<div className="mx-auto w-full max-w-lg bg-transparent ">
<CardContent className="p-0">
{isError && (
{error && (
<AlertBlock type="error" className="my-2">
{error?.message}
{error}
</AlertBlock>
)}
{!temp.is2FAEnabled ? (
@@ -131,9 +127,7 @@ export default function Home() {
</div>
</form>
</Form>
) : (
<Login2FA authId={temp.authId} />
)}
) : null}
<div className="flex flex-row justify-between flex-wrap">
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
@@ -155,7 +149,7 @@ export default function Home() {
Home.getLayout = (page: ReactElement) => {
return <OnboardingLayout>{page}</OnboardingLayout>;
};
export async function getServerSideProps(context: GetServerSidePropsContext) {
export async function getServerSideProps(_context: GetServerSidePropsContext) {
if (!IS_CLOUD) {
return {
redirect: {

View File

@@ -30,7 +30,41 @@ const Home: NextPage = () => {
return (
<div className="h-screen bg-white">
<SwaggerUI spec={spec} />
<SwaggerUI
spec={spec}
persistAuthorization={true}
plugins={[
{
statePlugins: {
auth: {
wrapActions: {
authorize: (ori: any) => (args: any) => {
const result = ori(args);
const apiKey = args?.apiKey?.value;
if (apiKey) {
localStorage.setItem("swagger_api_key", apiKey);
}
return result;
},
logout: (ori: any) => (args: any) => {
const result = ori(args);
localStorage.removeItem("swagger_api_key");
return result;
},
},
},
},
},
]}
requestInterceptor={(request: any) => {
const apiKey = localStorage.getItem("swagger_api_key");
if (apiKey) {
request.headers = request.headers || {};
request.headers["x-api-key"] = apiKey;
}
return request;
}}
/>
</div>
);
};
@@ -38,7 +72,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 +87,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,