mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(licenses): implement license management and validation features
- Added a new licenses application to handle license key management. - Implemented API endpoints for validating license keys and managing paid features. - Introduced a new component for enabling paid features in the dashboard. - Updated database schema to include a licenseKey field for users. - Refactored server API to remove the admin router and integrate license validation into user operations. - Enhanced the monitoring setup process to require valid license keys. - Updated pnpm workspace configuration to include the new licenses app.
This commit is contained in:
@@ -0,0 +1,105 @@
|
|||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { SparklesIcon } from "lucide-react";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { SetupMonitoring } from "./servers/setup-monitoring";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export const EnablePaidFeatures = () => {
|
||||||
|
const { data, refetch } = api.user.get.useQuery();
|
||||||
|
const { mutateAsync: validateLicense } =
|
||||||
|
api.user.validateLicense.useMutation();
|
||||||
|
const { mutateAsync: update } = api.user.update.useMutation();
|
||||||
|
const [licenseKey, setLicenseKey] = useState("");
|
||||||
|
|
||||||
|
const handleValidateLicense = async () => {
|
||||||
|
await validateLicense({
|
||||||
|
licenseKey,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("License validated successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error validating license");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
|
||||||
|
<div className="rounded-xl bg-background shadow-md">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl flex flex-row items-center gap-3">
|
||||||
|
<div className="p-2 rounded-lg bg-primary/10">
|
||||||
|
<SparklesIcon className="size-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
Paid Features
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="mt-2">
|
||||||
|
Unlock advanced capabilities like monitoring and enhanced
|
||||||
|
performance tracking
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex flex-row items-center justify-between p-4 border rounded-lg bg-card/50 hover:bg-card/80 transition-colors">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h3 className="font-medium">Enable Premium Features</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Access advanced monitoring tools and premium capabilities
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
className="ml-4"
|
||||||
|
checked={data?.user?.enablePaidFeatures}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
update({
|
||||||
|
enablePaidFeatures: !data?.user?.enablePaidFeatures,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
`Premium features ${
|
||||||
|
data?.user?.enablePaidFeatures
|
||||||
|
? "disabled"
|
||||||
|
: "enabled"
|
||||||
|
} successfully`,
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error updating premium features");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data?.user?.enablePaidFeatures && (
|
||||||
|
<div className="flex flex-row items-center gap-4 p-4 border rounded-lg bg-card/50">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter your license key"
|
||||||
|
value={licenseKey}
|
||||||
|
onChange={(e) => setLicenseKey(e.target.value)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleValidateLicense} variant="secondary">
|
||||||
|
Validate
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
{data?.user?.enablePaidFeatures && <SetupMonitoring />}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -202,7 +202,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
|||||||
|
|
||||||
const { mutateAsync } = serverId
|
const { mutateAsync } = serverId
|
||||||
? api.server.setupMonitoring.useMutation()
|
? api.server.setupMonitoring.useMutation()
|
||||||
: api.admin.setupMonitoring.useMutation();
|
: api.user.setupMonitoring.useMutation();
|
||||||
|
|
||||||
const generateToken = () => {
|
const generateToken = () => {
|
||||||
const array = new Uint8Array(64);
|
const array = new Uint8Array(64);
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0079_burly_lenny_balinger.sql
Normal file
1
apps/dokploy/drizzle/0079_burly_lenny_balinger.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user_temp" ADD COLUMN "licenseKey" text;
|
||||||
5169
apps/dokploy/drizzle/meta/0079_snapshot.json
Normal file
5169
apps/dokploy/drizzle/meta/0079_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -554,6 +554,13 @@
|
|||||||
"when": 1742112194375,
|
"when": 1742112194375,
|
||||||
"tag": "0078_uneven_omega_sentinel",
|
"tag": "0078_uneven_omega_sentinel",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 79,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1742188594159,
|
||||||
|
"tag": "0079_burly_lenny_balinger",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
||||||
import { WebServer } from "@/components/dashboard/settings/web-server";
|
import { WebServer } from "@/components/dashboard/settings/web-server";
|
||||||
|
import { EnablePaidFeatures } from "@/components/dashboard/settings/enable-paid-features";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||||
@@ -8,56 +9,16 @@ import { createServerSideHelpers } from "@trpc/react-query/server";
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||||
<WebDomain />
|
<WebDomain />
|
||||||
<WebServer />
|
<WebServer />
|
||||||
{/* <Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
{!isCloud && <EnablePaidFeatures />}
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { authRouter } from "@/server/api/routers/auth";
|
import { authRouter } from "@/server/api/routers/auth";
|
||||||
import { createTRPCRouter } from "../api/trpc";
|
import { createTRPCRouter } from "../api/trpc";
|
||||||
import { adminRouter } from "./routers/admin";
|
|
||||||
import { aiRouter } from "./routers/ai";
|
import { aiRouter } from "./routers/ai";
|
||||||
import { applicationRouter } from "./routers/application";
|
import { applicationRouter } from "./routers/application";
|
||||||
import { backupRouter } from "./routers/backup";
|
import { backupRouter } from "./routers/backup";
|
||||||
@@ -42,7 +41,6 @@ import { userRouter } from "./routers/user";
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
admin: adminRouter,
|
|
||||||
docker: dockerRouter,
|
docker: dockerRouter,
|
||||||
auth: authRouter,
|
auth: authRouter,
|
||||||
project: projectRouter,
|
project: projectRouter,
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import { apiUpdateWebServerMonitoring } from "@/server/db/schema";
|
|
||||||
import {
|
|
||||||
IS_CLOUD,
|
|
||||||
findUserById,
|
|
||||||
setupWebMonitoring,
|
|
||||||
updateUser,
|
|
||||||
} from "@dokploy/server";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { adminProcedure, createTRPCRouter } from "../trpc";
|
|
||||||
|
|
||||||
export const adminRouter = createTRPCRouter({
|
|
||||||
setupMonitoring: adminProcedure
|
|
||||||
.input(apiUpdateWebServerMonitoring)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
try {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "Feature disabled on cloud",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const user = await findUserById(ctx.user.ownerId);
|
|
||||||
if (user.id !== ctx.user.ownerId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to setup the monitoring",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateUser(user.id, {
|
|
||||||
metricsConfig: {
|
|
||||||
server: {
|
|
||||||
type: "Dokploy",
|
|
||||||
refreshRate: input.metricsConfig.server.refreshRate,
|
|
||||||
port: input.metricsConfig.server.port,
|
|
||||||
token: input.metricsConfig.server.token,
|
|
||||||
cronJob: input.metricsConfig.server.cronJob,
|
|
||||||
urlCallback: input.metricsConfig.server.urlCallback,
|
|
||||||
retentionDays: input.metricsConfig.server.retentionDays,
|
|
||||||
thresholds: {
|
|
||||||
cpu: input.metricsConfig.server.thresholds.cpu,
|
|
||||||
memory: input.metricsConfig.server.thresholds.memory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
containers: {
|
|
||||||
refreshRate: input.metricsConfig.containers.refreshRate,
|
|
||||||
services: {
|
|
||||||
include: input.metricsConfig.containers.services.include || [],
|
|
||||||
exclude: input.metricsConfig.containers.services.exclude || [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentServer = await setupWebMonitoring(user.id);
|
|
||||||
return currentServer;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
removeUserById,
|
removeUserById,
|
||||||
updateUser,
|
updateUser,
|
||||||
createApiKey,
|
createApiKey,
|
||||||
|
setupWebMonitoring,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import {
|
import {
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
invitation,
|
invitation,
|
||||||
member,
|
member,
|
||||||
apikey,
|
apikey,
|
||||||
|
apiUpdateWebServerMonitoring,
|
||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from "bcrypt";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
@@ -27,7 +29,7 @@ import {
|
|||||||
protectedProcedure,
|
protectedProcedure,
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
} from "../trpc";
|
} from "../trpc";
|
||||||
|
import { validateLicense } from "@/server/utils/validate-license";
|
||||||
const apiCreateApiKey = z.object({
|
const apiCreateApiKey = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
prefix: z.string().optional(),
|
prefix: z.string().optional(),
|
||||||
@@ -138,6 +140,26 @@ export const userRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return await updateUser(ctx.user.id, input);
|
return await updateUser(ctx.user.id, input);
|
||||||
}),
|
}),
|
||||||
|
validateLicense: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
licenseKey: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const isValid = await validateLicense(input.licenseKey);
|
||||||
|
if (!isValid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "Invalid license key",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateUser(ctx.user.id, {
|
||||||
|
licenseKey: input.licenseKey,
|
||||||
|
});
|
||||||
|
return isValid;
|
||||||
|
}),
|
||||||
getUserByToken: publicProcedure
|
getUserByToken: publicProcedure
|
||||||
.input(apiFindOneToken)
|
.input(apiFindOneToken)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
@@ -327,4 +349,62 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return organizations.length;
|
return organizations.length;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
setupMonitoring: adminProcedure
|
||||||
|
.input(apiUpdateWebServerMonitoring)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "Feature disabled on cloud",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await findUserById(ctx.user.id);
|
||||||
|
|
||||||
|
if (!validateLicense(user?.licenseKey || "")) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "Invalid license key",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (user.id !== ctx.user.ownerId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to setup the monitoring",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateUser(user.id, {
|
||||||
|
metricsConfig: {
|
||||||
|
server: {
|
||||||
|
type: "Dokploy",
|
||||||
|
refreshRate: input.metricsConfig.server.refreshRate,
|
||||||
|
port: input.metricsConfig.server.port,
|
||||||
|
token: input.metricsConfig.server.token,
|
||||||
|
cronJob: input.metricsConfig.server.cronJob,
|
||||||
|
urlCallback: input.metricsConfig.server.urlCallback,
|
||||||
|
retentionDays: input.metricsConfig.server.retentionDays,
|
||||||
|
thresholds: {
|
||||||
|
cpu: input.metricsConfig.server.thresholds.cpu,
|
||||||
|
memory: input.metricsConfig.server.thresholds.memory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
containers: {
|
||||||
|
refreshRate: input.metricsConfig.containers.refreshRate,
|
||||||
|
services: {
|
||||||
|
include: input.metricsConfig.containers.services.include || [],
|
||||||
|
exclude: input.metricsConfig.containers.services.exclude || [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentServer = await setupWebMonitoring(user.id);
|
||||||
|
return currentServer;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
8
apps/dokploy/server/utils/validate-license.ts
Normal file
8
apps/dokploy/server/utils/validate-license.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const validateLicense = async (licenseKey: string): Promise<boolean> => {
|
||||||
|
const response = await fetch(`${process.env.SERVER_URL}/validate-license`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ licenseKey }),
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.ok;
|
||||||
|
};
|
||||||
2
apps/licenses/.env.example
Normal file
2
apps/licenses/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
LEMON_SQUEEZY_API_KEY=""
|
||||||
|
LEMON_SQUEEZY_STORE_ID=""
|
||||||
28
apps/licenses/.gitignore
vendored
Normal file
28
apps/licenses/.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# dev
|
||||||
|
.yarn/
|
||||||
|
!.yarn/releases
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
.idea/workspace.xml
|
||||||
|
.idea/usage.statistics.xml
|
||||||
|
.idea/shelf
|
||||||
|
|
||||||
|
# deps
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# env
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
8
apps/licenses/README.md
Normal file
8
apps/licenses/README.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
```
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
open http://localhost:3000
|
||||||
|
```
|
||||||
32
apps/licenses/package.json
Normal file
32
apps/licenses/package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "@dokploy/licenses",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "PORT=4000 tsx watch src/index.ts",
|
||||||
|
"build": "tsc --project tsconfig.json",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pino": "9.4.0",
|
||||||
|
"pino-pretty": "11.2.2",
|
||||||
|
"@hono/zod-validator": "0.3.0",
|
||||||
|
"zod": "^3.23.4",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"@dokploy/server": "workspace:*",
|
||||||
|
"@hono/node-server": "^1.12.1",
|
||||||
|
"hono": "^4.5.8",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"stripe": "17.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.4.2",
|
||||||
|
"@types/react": "^18.2.37",
|
||||||
|
"@types/react-dom": "^18.2.15",
|
||||||
|
"@types/node": "^20.11.17",
|
||||||
|
"tsx": "^4.7.1"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.5.0"
|
||||||
|
}
|
||||||
39
apps/licenses/src/index.ts
Normal file
39
apps/licenses/src/index.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { serve } from "@hono/node-server";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import "dotenv/config";
|
||||||
|
import { zValidator } from "@hono/zod-validator";
|
||||||
|
import { logger } from "./logger.js";
|
||||||
|
import { deployJobSchema } from "./schema.js";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
const app = new Hono();
|
||||||
|
|
||||||
|
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
||||||
|
const data = c.req.valid("json");
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
message: "Deployment Added",
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stripe webhook
|
||||||
|
app.post("/stripe/webhook", async (c) => {
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
const event = stripe.webhooks.constructEvent(
|
||||||
|
body,
|
||||||
|
c.req.header("stripe-signature"),
|
||||||
|
process.env.STRIPE_WEBHOOK_SECRET,
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json({ status: "ok" });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/health", async (c) => {
|
||||||
|
return c.json({ status: "ok" });
|
||||||
|
});
|
||||||
|
|
||||||
|
const port = Number.parseInt(process.env.PORT || "3000");
|
||||||
|
logger.info("Starting Deployments Server ✅", port);
|
||||||
|
serve({ fetch: app.fetch, port });
|
||||||
10
apps/licenses/src/logger.ts
Normal file
10
apps/licenses/src/logger.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import pino from "pino";
|
||||||
|
|
||||||
|
export const logger = pino({
|
||||||
|
transport: {
|
||||||
|
target: "pino-pretty",
|
||||||
|
options: {
|
||||||
|
colorize: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
34
apps/licenses/src/schema.ts
Normal file
34
apps/licenses/src/schema.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||||
|
z.object({
|
||||||
|
applicationId: z.string(),
|
||||||
|
titleLog: z.string(),
|
||||||
|
descriptionLog: z.string(),
|
||||||
|
server: z.boolean().optional(),
|
||||||
|
type: z.enum(["deploy", "redeploy"]),
|
||||||
|
applicationType: z.literal("application"),
|
||||||
|
serverId: z.string().min(1),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
composeId: z.string(),
|
||||||
|
titleLog: z.string(),
|
||||||
|
descriptionLog: z.string(),
|
||||||
|
server: z.boolean().optional(),
|
||||||
|
type: z.enum(["deploy", "redeploy"]),
|
||||||
|
applicationType: z.literal("compose"),
|
||||||
|
serverId: z.string().min(1),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
applicationId: z.string(),
|
||||||
|
previewDeploymentId: z.string(),
|
||||||
|
titleLog: z.string(),
|
||||||
|
descriptionLog: z.string(),
|
||||||
|
server: z.boolean().optional(),
|
||||||
|
type: z.enum(["deploy"]),
|
||||||
|
applicationType: z.literal("application-preview"),
|
||||||
|
serverId: z.string().min(1),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type DeployJob = z.infer<typeof deployJobSchema>;
|
||||||
82
apps/licenses/src/utils.ts
Normal file
82
apps/licenses/src/utils.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
deployRemoteApplication,
|
||||||
|
deployRemoteCompose,
|
||||||
|
deployRemotePreviewApplication,
|
||||||
|
rebuildRemoteApplication,
|
||||||
|
rebuildRemoteCompose,
|
||||||
|
updateApplicationStatus,
|
||||||
|
updateCompose,
|
||||||
|
updatePreviewDeployment,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import type { DeployJob } from "./schema";
|
||||||
|
|
||||||
|
export const deploy = async (job: DeployJob) => {
|
||||||
|
try {
|
||||||
|
if (job.applicationType === "application") {
|
||||||
|
await updateApplicationStatus(job.applicationId, "running");
|
||||||
|
if (job.server) {
|
||||||
|
if (job.type === "redeploy") {
|
||||||
|
await rebuildRemoteApplication({
|
||||||
|
applicationId: job.applicationId,
|
||||||
|
titleLog: job.titleLog,
|
||||||
|
descriptionLog: job.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (job.type === "deploy") {
|
||||||
|
await deployRemoteApplication({
|
||||||
|
applicationId: job.applicationId,
|
||||||
|
titleLog: job.titleLog,
|
||||||
|
descriptionLog: job.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (job.applicationType === "compose") {
|
||||||
|
await updateCompose(job.composeId, {
|
||||||
|
composeStatus: "running",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (job.server) {
|
||||||
|
if (job.type === "redeploy") {
|
||||||
|
await rebuildRemoteCompose({
|
||||||
|
composeId: job.composeId,
|
||||||
|
titleLog: job.titleLog,
|
||||||
|
descriptionLog: job.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (job.type === "deploy") {
|
||||||
|
await deployRemoteCompose({
|
||||||
|
composeId: job.composeId,
|
||||||
|
titleLog: job.titleLog,
|
||||||
|
descriptionLog: job.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (job.applicationType === "application-preview") {
|
||||||
|
await updatePreviewDeployment(job.previewDeploymentId, {
|
||||||
|
previewStatus: "running",
|
||||||
|
});
|
||||||
|
if (job.server) {
|
||||||
|
if (job.type === "deploy") {
|
||||||
|
await deployRemotePreviewApplication({
|
||||||
|
applicationId: job.applicationId,
|
||||||
|
titleLog: job.titleLog,
|
||||||
|
descriptionLog: job.descriptionLog,
|
||||||
|
previewDeploymentId: job.previewDeploymentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
if (job.applicationType === "application") {
|
||||||
|
await updateApplicationStatus(job.applicationId, "error");
|
||||||
|
} else if (job.applicationType === "compose") {
|
||||||
|
await updateCompose(job.composeId, {
|
||||||
|
composeStatus: "error",
|
||||||
|
});
|
||||||
|
} else if (job.applicationType === "application-preview") {
|
||||||
|
await updatePreviewDeployment(job.previewDeploymentId, {
|
||||||
|
previewStatus: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
18
apps/licenses/tsconfig.json
Normal file
18
apps/licenses/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "hono/jsx",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"],
|
||||||
|
"@dokploy/server/*": ["../../packages/server/src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@ export const users_temp = pgTable("user_temp", {
|
|||||||
logCleanupCron: text("logCleanupCron"),
|
logCleanupCron: text("logCleanupCron"),
|
||||||
// Metrics
|
// Metrics
|
||||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||||
|
licenseKey: text("licenseKey"),
|
||||||
metricsConfig: jsonb("metricsConfig")
|
metricsConfig: jsonb("metricsConfig")
|
||||||
.$type<{
|
.$type<{
|
||||||
server: {
|
server: {
|
||||||
|
|||||||
57
pnpm-lock.yaml
generated
57
pnpm-lock.yaml
generated
@@ -528,6 +528,58 @@ importers:
|
|||||||
specifier: ^1.6.0
|
specifier: ^1.6.0
|
||||||
version: 1.6.0(@types/node@18.19.42)(terser@5.31.3)
|
version: 1.6.0(@types/node@18.19.42)(terser@5.31.3)
|
||||||
|
|
||||||
|
apps/licenses:
|
||||||
|
dependencies:
|
||||||
|
'@dokploy/server':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/server
|
||||||
|
'@hono/node-server':
|
||||||
|
specifier: ^1.12.1
|
||||||
|
version: 1.12.1
|
||||||
|
'@hono/zod-validator':
|
||||||
|
specifier: 0.3.0
|
||||||
|
version: 0.3.0(hono@4.5.8)(zod@3.24.1)
|
||||||
|
dotenv:
|
||||||
|
specifier: ^16.3.1
|
||||||
|
version: 16.4.5
|
||||||
|
hono:
|
||||||
|
specifier: ^4.5.8
|
||||||
|
version: 4.5.8
|
||||||
|
pino:
|
||||||
|
specifier: 9.4.0
|
||||||
|
version: 9.4.0
|
||||||
|
pino-pretty:
|
||||||
|
specifier: 11.2.2
|
||||||
|
version: 11.2.2
|
||||||
|
react:
|
||||||
|
specifier: 18.2.0
|
||||||
|
version: 18.2.0
|
||||||
|
react-dom:
|
||||||
|
specifier: 18.2.0
|
||||||
|
version: 18.2.0(react@18.2.0)
|
||||||
|
stripe:
|
||||||
|
specifier: 17.2.0
|
||||||
|
version: 17.2.0
|
||||||
|
zod:
|
||||||
|
specifier: ^3.23.4
|
||||||
|
version: 3.24.1
|
||||||
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^20.11.17
|
||||||
|
version: 20.14.10
|
||||||
|
'@types/react':
|
||||||
|
specifier: 18.3.5
|
||||||
|
version: 18.3.5
|
||||||
|
'@types/react-dom':
|
||||||
|
specifier: 18.3.0
|
||||||
|
version: 18.3.0
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.7.1
|
||||||
|
version: 4.16.2
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.4.2
|
||||||
|
version: 5.7.2
|
||||||
|
|
||||||
apps/schedules:
|
apps/schedules:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@dokploy/server':
|
'@dokploy/server':
|
||||||
@@ -8354,6 +8406,11 @@ snapshots:
|
|||||||
hono: 4.5.8
|
hono: 4.5.8
|
||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
|
|
||||||
|
'@hono/zod-validator@0.3.0(hono@4.5.8)(zod@3.24.1)':
|
||||||
|
dependencies:
|
||||||
|
hono: 4.5.8
|
||||||
|
zod: 3.24.1
|
||||||
|
|
||||||
'@hookform/resolvers@3.9.0(react-hook-form@7.52.1(react@18.2.0))':
|
'@hookform/resolvers@3.9.0(react-hook-form@7.52.1(react@18.2.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
react-hook-form: 7.52.1(react@18.2.0)
|
react-hook-form: 7.52.1(react@18.2.0)
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ packages:
|
|||||||
- "apps/schedules"
|
- "apps/schedules"
|
||||||
- "apps/models"
|
- "apps/models"
|
||||||
- "packages/server"
|
- "packages/server"
|
||||||
|
- "apps/licenses"
|
||||||
|
|||||||
Reference in New Issue
Block a user