mirror of
https://github.com/lukevella/rallly.git
synced 2025-08-06 01:48:32 +02:00
✨ Add licensing api (#1723)
This commit is contained in:
parent
982fc39ac7
commit
679d6fb034
14 changed files with 607 additions and 40 deletions
43
apps/web/src/features/licensing/actions/validate-license.ts
Normal file
43
apps/web/src/features/licensing/actions/validate-license.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
"use server";
|
||||
|
||||
import { rateLimit } from "@/features/rate-limit";
|
||||
import { prisma } from "@rallly/database";
|
||||
import { licensingClient } from "../client";
|
||||
|
||||
export async function validateLicenseKey(key: string) {
|
||||
const { success } = await rateLimit("validate_license_key", 10, "1 m");
|
||||
|
||||
if (!success) {
|
||||
throw new Error("Rate limit exceeded");
|
||||
}
|
||||
|
||||
const { data, error } = await licensingClient.validateLicenseKey({
|
||||
key,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`License validation failed: ${error}`);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
|
||||
await prisma.instanceLicense.create({
|
||||
data: {
|
||||
licenseKey: data.key,
|
||||
licenseeName: data.licenseeName,
|
||||
licenseeEmail: data.licenseeEmail,
|
||||
issuedAt: data.issuedAt,
|
||||
expiresAt: data.expiresAt,
|
||||
seats: data.seats,
|
||||
type: data.type,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
};
|
||||
}
|
7
apps/web/src/features/licensing/client.ts
Normal file
7
apps/web/src/features/licensing/client.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { env } from "@/env";
|
||||
import { LicensingClient } from "./lib/licensing-client";
|
||||
|
||||
export const licensingClient = new LicensingClient({
|
||||
apiUrl: env.LICENSE_API_URL,
|
||||
authToken: env.LICENSE_API_AUTH_TOKEN,
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Calculate a checksum for a string
|
||||
* @param str The string to calculate the checksum for
|
||||
* @returns The checksum
|
||||
*/
|
||||
export function calculateChecksum(str: string): string {
|
||||
// Simple checksum: sum char codes, mod 100000, base36
|
||||
let sum = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
sum += str.charCodeAt(i);
|
||||
}
|
||||
return (sum % 100000).toString(36).toUpperCase().padStart(5, "0");
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { calculateChecksum } from "./calculate-checksum";
|
||||
|
||||
export function checkLicenseKey(key: string): boolean {
|
||||
const parts = key.split("-");
|
||||
if (parts.length !== 6) return false;
|
||||
const checksum = parts[5];
|
||||
const licenseBody = parts.slice(0, 5).join("-");
|
||||
return calculateChecksum(licenseBody) === checksum;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { customAlphabet } from "nanoid";
|
||||
import { calculateChecksum } from "./calculate-checksum";
|
||||
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
const generate = customAlphabet(alphabet, 4);
|
||||
|
||||
/**
|
||||
* Generate user friendly licenses
|
||||
* eg. RLYV4-ABCD-1234-ABCD-1234-XXXX
|
||||
*/
|
||||
export const generateLicenseKey = ({ version }: { version?: number }) => {
|
||||
let license = `RLYV${version ?? "X"}-`;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
license += generate();
|
||||
if (i < 3) {
|
||||
license += "-";
|
||||
}
|
||||
}
|
||||
const checksum = calculateChecksum(license);
|
||||
return `${license}-${checksum}`;
|
||||
};
|
1
apps/web/src/features/licensing/index.ts
Normal file
1
apps/web/src/features/licensing/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { LicensingClient } from "./lib/licensing-client";
|
59
apps/web/src/features/licensing/lib/licensing-client.ts
Normal file
59
apps/web/src/features/licensing/lib/licensing-client.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import {
|
||||
type CreateLicenseInput,
|
||||
type ValidateLicenseInputKeySchema,
|
||||
createLicenseResponseSchema,
|
||||
validateLicenseKeyResponseSchema,
|
||||
} from "../schema";
|
||||
|
||||
export class LicensingClient {
|
||||
apiUrl: string;
|
||||
authToken?: string;
|
||||
|
||||
constructor({
|
||||
apiUrl = "https://licensing.rallly.co",
|
||||
authToken,
|
||||
}: {
|
||||
apiUrl?: string;
|
||||
authToken?: string;
|
||||
}) {
|
||||
this.apiUrl = apiUrl;
|
||||
this.authToken = authToken;
|
||||
}
|
||||
async createLicense(input: CreateLicenseInput) {
|
||||
if (!this.authToken) {
|
||||
throw new Error("Licensing API auth token is not configured.");
|
||||
}
|
||||
|
||||
const res = await fetch(`${this.apiUrl}/api/v1/licenses`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.authToken}`,
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to create license.");
|
||||
}
|
||||
return createLicenseResponseSchema.parse(await res.json());
|
||||
}
|
||||
async validateLicenseKey(input: ValidateLicenseInputKeySchema) {
|
||||
const res = await fetch(
|
||||
`${this.apiUrl}/api/v1/licenses/actions/validate-key`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to validate license key.");
|
||||
}
|
||||
|
||||
return validateLicenseKeyResponseSchema.parse(await res.json());
|
||||
}
|
||||
}
|
88
apps/web/src/features/licensing/schema.ts
Normal file
88
apps/web/src/features/licensing/schema.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
import { z } from "zod";
|
||||
|
||||
// =========================
|
||||
// Enums & Basic Types
|
||||
// =========================
|
||||
|
||||
export const licenseTypeSchema = z.enum(["PLUS", "ORGANIZATION", "ENTERPRISE"]);
|
||||
export type LicenseType = z.infer<typeof licenseTypeSchema>;
|
||||
|
||||
export const licenseStatusSchema = z.enum(["ACTIVE", "REVOKED"]);
|
||||
export type LicenseStatus = z.infer<typeof licenseStatusSchema>;
|
||||
|
||||
// =========================
|
||||
// Generic API Response
|
||||
// =========================
|
||||
|
||||
export const apiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
|
||||
z.object({
|
||||
data: dataSchema.optional(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
export type ApiResponse<T> = {
|
||||
data?: T;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
// =========================
|
||||
// Create License
|
||||
// =========================
|
||||
|
||||
export const createLicenseInputSchema = z.object({
|
||||
type: licenseTypeSchema,
|
||||
seats: z.number().optional(),
|
||||
expiresAt: z.date().optional(),
|
||||
licenseeEmail: z.string().optional(),
|
||||
licenseeName: z.string().optional(),
|
||||
version: z.number().optional(),
|
||||
stripeCustomerId: z.string().optional(),
|
||||
});
|
||||
export type CreateLicenseInput = z.infer<typeof createLicenseInputSchema>;
|
||||
|
||||
export const createLicenseResponseSchema = apiResponseSchema(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
}),
|
||||
);
|
||||
export type CreateLicenseResponse = z.infer<typeof createLicenseResponseSchema>;
|
||||
|
||||
// =========================
|
||||
// Validate License Key
|
||||
// =========================
|
||||
|
||||
export const validateLicenseKeyInputSchema = z.object({
|
||||
key: z.string(),
|
||||
fingerprint: z.string().optional(),
|
||||
});
|
||||
export type ValidateLicenseInputKeySchema = z.infer<
|
||||
typeof validateLicenseKeyInputSchema
|
||||
>;
|
||||
|
||||
export const validateLicenseKeyResponseSchema = apiResponseSchema(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
valid: z.boolean(),
|
||||
status: licenseStatusSchema,
|
||||
issuedAt: z.date(),
|
||||
expiresAt: z.date().nullable(),
|
||||
licenseeEmail: z.string().nullable(),
|
||||
licenseeName: z.string().nullable(),
|
||||
seats: z.number().nullable(),
|
||||
type: licenseTypeSchema,
|
||||
version: z.number().nullable(),
|
||||
}),
|
||||
);
|
||||
|
||||
export type ValidateLicenseKeyResponse = z.infer<
|
||||
typeof validateLicenseKeyResponseSchema
|
||||
>;
|
||||
|
||||
export const licenseCheckoutMetadataSchema = z.object({
|
||||
licenseType: licenseTypeSchema,
|
||||
seats: z.number(),
|
||||
});
|
||||
|
||||
export type LicenseCheckoutMetada = z.infer<
|
||||
typeof licenseCheckoutMetadataSchema
|
||||
>;
|
Loading…
Add table
Add a link
Reference in a new issue