mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(licenses): enhance license validation and loading state management
- Added loading state management in the EnablePaidFeatures component to improve user experience during license validation. - Updated validateLicense function to return detailed error messages for better feedback. - Modified user API to return validation results instead of a boolean, enhancing error handling. - Removed unused SQL files and updated package.json scripts for better development workflow.
This commit is contained in:
@@ -16,6 +16,7 @@ import { useState, useEffect } from "react";
|
|||||||
|
|
||||||
export const EnablePaidFeatures = () => {
|
export const EnablePaidFeatures = () => {
|
||||||
const { data, refetch } = api.user.get.useQuery();
|
const { data, refetch } = api.user.get.useQuery();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { mutateAsync: validateLicense } =
|
const { mutateAsync: validateLicense } =
|
||||||
api.user.validateLicense.useMutation();
|
api.user.validateLicense.useMutation();
|
||||||
const { mutateAsync: update } = api.user.update.useMutation();
|
const { mutateAsync: update } = api.user.update.useMutation();
|
||||||
@@ -32,6 +33,7 @@ export const EnablePaidFeatures = () => {
|
|||||||
toast.error("Please enter a license key");
|
toast.error("Please enter a license key");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setIsLoading(true);
|
||||||
await validateLicense({
|
await validateLicense({
|
||||||
licenseKey,
|
licenseKey,
|
||||||
})
|
})
|
||||||
@@ -40,7 +42,12 @@ export const EnablePaidFeatures = () => {
|
|||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast.error("Error validating license");
|
toast.error("Error validating license", {
|
||||||
|
description: e.message,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,8 +109,12 @@ export const EnablePaidFeatures = () => {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleValidateLicense} variant="secondary">
|
<Button
|
||||||
Validate
|
onClick={handleValidateLicense}
|
||||||
|
variant="secondary"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? "Validating..." : "Validate"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -148,21 +148,21 @@ export const userRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const owner = await findUserById(ctx.user.ownerId);
|
const owner = await findUserById(ctx.user.ownerId);
|
||||||
const isValid = await validateLicense(
|
const result = await validateLicense(
|
||||||
input.licenseKey,
|
input.licenseKey,
|
||||||
owner?.serverIp || "",
|
owner?.serverIp || "",
|
||||||
);
|
);
|
||||||
if (!isValid) {
|
if (!result.isValid) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Invalid license key",
|
message: result.error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateUser(ctx.user.id, {
|
await updateUser(ctx.user.id, {
|
||||||
licenseKey: input.licenseKey,
|
licenseKey: input.licenseKey,
|
||||||
});
|
});
|
||||||
return isValid;
|
return result;
|
||||||
}),
|
}),
|
||||||
getUserByToken: publicProcedure
|
getUserByToken: publicProcedure
|
||||||
.input(apiFindOneToken)
|
.input(apiFindOneToken)
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
const licensesUrl = process.env.LICENSES_URL || "http://localhost:4002";
|
const licensesUrl = process.env.LICENSES_URL || "http://localhost:4002";
|
||||||
|
|
||||||
export const validateLicense = async (
|
export const validateLicense = async (licenseKey: string, serverIp: string) => {
|
||||||
licenseKey: string,
|
|
||||||
serverIp: string,
|
|
||||||
): Promise<boolean> => {
|
|
||||||
const response = await fetch(`${licensesUrl}/api/validate`, {
|
const response = await fetch(`${licensesUrl}/api/validate`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -17,5 +14,5 @@ export const validateLicense = async (
|
|||||||
console.log("Validation errors:", data.error.issues);
|
console.log("Validation errors:", data.error.issues);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.ok;
|
return data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
CREATE TYPE "public"."billing_type" AS ENUM('monthly', 'annual');--> statement-breakpoint
|
|
||||||
CREATE TYPE "public"."license_status" AS ENUM('active', 'expired', 'cancelled', 'payment_pending');--> statement-breakpoint
|
|
||||||
CREATE TYPE "public"."license_type" AS ENUM('basic', 'premium', 'business');--> statement-breakpoint
|
|
||||||
CREATE TABLE "licenses" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"product_id" text NOT NULL,
|
|
||||||
"license_key" text NOT NULL,
|
|
||||||
"status" "license_status" DEFAULT 'active' NOT NULL,
|
|
||||||
"type" "license_type" NOT NULL,
|
|
||||||
"billing_type" "billing_type" NOT NULL,
|
|
||||||
"server_ip" text,
|
|
||||||
"activated_at" timestamp,
|
|
||||||
"last_verified_at" timestamp,
|
|
||||||
"expires_at" timestamp NOT NULL,
|
|
||||||
"stripeCustomerId" text NOT NULL,
|
|
||||||
"stripeSubscriptionId" text NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"metadata" text,
|
|
||||||
"email" text NOT NULL,
|
|
||||||
CONSTRAINT "licenses_license_key_unique" UNIQUE("license_key")
|
|
||||||
);
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "5a996744-b11f-4f1a-b4b0-91f6bf5c2bed",
|
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"tables": {
|
|
||||||
"public.licenses": {
|
|
||||||
"name": "licenses",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "gen_random_uuid()"
|
|
||||||
},
|
|
||||||
"product_id": {
|
|
||||||
"name": "product_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"license_key": {
|
|
||||||
"name": "license_key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"name": "status",
|
|
||||||
"type": "license_status",
|
|
||||||
"typeSchema": "public",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "'active'"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "license_type",
|
|
||||||
"typeSchema": "public",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"billing_type": {
|
|
||||||
"name": "billing_type",
|
|
||||||
"type": "billing_type",
|
|
||||||
"typeSchema": "public",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"server_ip": {
|
|
||||||
"name": "server_ip",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"activated_at": {
|
|
||||||
"name": "activated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"last_verified_at": {
|
|
||||||
"name": "last_verified_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"expires_at": {
|
|
||||||
"name": "expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"stripeCustomerId": {
|
|
||||||
"name": "stripeCustomerId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"stripeSubscriptionId": {
|
|
||||||
"name": "stripeSubscriptionId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "CURRENT_TIMESTAMP"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "CURRENT_TIMESTAMP"
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"name": "metadata",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"licenses_license_key_unique": {
|
|
||||||
"name": "licenses_license_key_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"license_key"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {
|
|
||||||
"public.billing_type": {
|
|
||||||
"name": "billing_type",
|
|
||||||
"schema": "public",
|
|
||||||
"values": [
|
|
||||||
"monthly",
|
|
||||||
"annual"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"public.license_status": {
|
|
||||||
"name": "license_status",
|
|
||||||
"schema": "public",
|
|
||||||
"values": [
|
|
||||||
"active",
|
|
||||||
"expired",
|
|
||||||
"cancelled",
|
|
||||||
"payment_pending"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"public.license_type": {
|
|
||||||
"name": "license_type",
|
|
||||||
"schema": "public",
|
|
||||||
"values": [
|
|
||||||
"basic",
|
|
||||||
"premium",
|
|
||||||
"business"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"schemas": {},
|
|
||||||
"sequences": {},
|
|
||||||
"roles": {},
|
|
||||||
"policies": {},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"entries": [
|
"entries": []
|
||||||
{
|
|
||||||
"idx": 0,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1742369437742,
|
|
||||||
"tag": "0000_noisy_epoch",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"generate": "drizzle-kit generate",
|
"generate": "drizzle-kit generate",
|
||||||
"drop": "drizzle-kit drop",
|
"drop": "drizzle-kit drop",
|
||||||
|
"push": "drizzle-kit push",
|
||||||
"migrate": "tsx ./migrate.ts",
|
"migrate": "tsx ./migrate.ts",
|
||||||
"truncate": "tsx ./truncate.ts",
|
"truncate": "tsx ./truncate.ts",
|
||||||
"reset:all": "tsx ./truncate.ts && tsx ./migrate.ts",
|
"reset:all": "tsx ./truncate.ts && tsx ./migrate.ts",
|
||||||
|
|||||||
@@ -58,13 +58,12 @@ router.get("/health", async (c) => {
|
|||||||
router.post("/validate", zValidator("json", validateSchema), async (c) => {
|
router.post("/validate", zValidator("json", validateSchema), async (c) => {
|
||||||
const { licenseKey, serverIp } = c.req.valid("json");
|
const { licenseKey, serverIp } = c.req.valid("json");
|
||||||
|
|
||||||
console.log("Validating license", licenseKey, serverIp);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await validateLicense(licenseKey, serverIp);
|
const result = await validateLicense(licenseKey, serverIp);
|
||||||
|
console.log("Result", result);
|
||||||
return c.json(result);
|
return c.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error validating license:", error);
|
logger.error("Error validating license:", { error });
|
||||||
return c.json({ isValid: false, error: "Error validating license" }, 500);
|
return c.json({ isValid: false, error: "Error validating license" }, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -125,12 +124,7 @@ router.post("/resend-license", zValidator("json", resendSchema), async (c) => {
|
|||||||
customerName: license.email,
|
customerName: license.email,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
// await transporter.sendMail({
|
|
||||||
// from: fromAddress,
|
|
||||||
// to: toAddresses.join(", "),
|
|
||||||
// subject,
|
|
||||||
// html: htmlContent,
|
|
||||||
// });
|
|
||||||
await transporter.sendMail({
|
await transporter.sendMail({
|
||||||
from: process.env.SMTP_FROM_ADDRESS,
|
from: process.env.SMTP_FROM_ADDRESS,
|
||||||
to: license.email,
|
to: license.email,
|
||||||
@@ -199,7 +193,7 @@ router.post("/stripe/webhook", async (c) => {
|
|||||||
billingType,
|
billingType,
|
||||||
email: session.customer_details?.email!,
|
email: session.customer_details?.email!,
|
||||||
stripeCustomerId: customerResponse.id,
|
stripeCustomerId: customerResponse.id,
|
||||||
stripeSubscriptionId: session.id,
|
stripeSubscriptionId: session.subscription as string,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("License created", license);
|
console.log("License created", license);
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ export const validateLicense = async (
|
|||||||
license.stripeSubscriptionId,
|
license.stripeSubscriptionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("Suscription", suscription);
|
||||||
|
|
||||||
if (suscription.status !== "active") {
|
if (suscription.status !== "active") {
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user