feat: add license activation system with feature gates
- License key generator tool at ~/Desktop/LicenseGenerator - Backend validator route (GET /api/license/status, POST /api/license/activate) - Activation page in sidebar with status, key input, and free/premium feature list - useLicense hook for frontend license state - Feature gates: premium eligibility buttons (DDMA, DeltaIns, Tufts, United, CCA) disabled without license - Feature gates: premium claim buttons (CCA, Delta MA, United, Tufts) and all PreAuth buttons disabled without license - Free features always active: MassHealth eligibility/claim, Documents, Payments, Backups, Reports - README: license key generator usage instructions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
NODE_ENV="development"
|
||||
HOST=0.0.0.0
|
||||
PORT=5000
|
||||
CLOUDFLARE_HOST=communitydentistsoflowell.mydentalofficemanagement.com
|
||||
FRONTEND_URLS=http://localhost:3000,http://communitydentistsoflowell.mydentalofficemanagement.com,https://communitydentistsoflowell.mydentalofficemanagement.com
|
||||
SELENIUM_AGENT_BASE_URL=http://localhost:5002
|
||||
JWT_SECRET = 'dentalsecret'
|
||||
@@ -8,4 +9,5 @@ DB_HOST=localhost
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=mypassword
|
||||
DB_NAME=dentalapp
|
||||
DATABASE_URL=postgresql://postgres:mypassword@localhost:5432/dentalapp
|
||||
DATABASE_URL=postgresql://postgres:mypassword@localhost:5432/dentalapp
|
||||
LICENSE_SECRET=3aa4ab937e46c6863b9e3c2b591a595b31ea3af1060bf5e7961ad722a8b54f92
|
||||
@@ -39,6 +39,7 @@ import insuranceContactsRoutes from "./insurance-contacts";
|
||||
import commissionsRoutes from "./commissions";
|
||||
import shoppingVendorsRoutes from "./shopping-vendors";
|
||||
import feeScheduleRoutes from "./feeSchedule";
|
||||
import licenseRoutes from "./license";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -82,5 +83,6 @@ router.use("/insurance-contacts", insuranceContactsRoutes);
|
||||
router.use("/commissions", commissionsRoutes);
|
||||
router.use("/shopping-vendors", shoppingVendorsRoutes);
|
||||
router.use("/fee-schedule", feeScheduleRoutes);
|
||||
router.use("/license", licenseRoutes);
|
||||
|
||||
export default router;
|
||||
|
||||
97
apps/Backend/src/routes/license.ts
Normal file
97
apps/Backend/src/routes/license.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Router } from "express";
|
||||
import crypto from "crypto";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const router = Router();
|
||||
|
||||
const LICENSE_FILE = path.join(process.cwd(), "license.json");
|
||||
const SECRET = process.env.LICENSE_SECRET || "";
|
||||
|
||||
function validateKey(key: string): { valid: boolean; expiry?: string; error?: string } {
|
||||
// Format: DENTAL-{24-char-signature}-{YYYY-MM-DD}
|
||||
const parts = key.trim().split("-");
|
||||
if (parts.length !== 6 || parts[0] !== "DENTAL") {
|
||||
return { valid: false, error: "Invalid license key format" };
|
||||
}
|
||||
|
||||
const signature = parts[1];
|
||||
const expiryStr = `${parts[2]}-${parts[3]}-${parts[4]}`;
|
||||
|
||||
// Verify signature
|
||||
const payload = `DENTAL:${expiryStr}`;
|
||||
const expectedSig = crypto
|
||||
.createHmac("sha256", SECRET)
|
||||
.update(payload)
|
||||
.digest("hex")
|
||||
.substring(0, 24)
|
||||
.toUpperCase();
|
||||
|
||||
if (signature !== expectedSig) {
|
||||
return { valid: false, error: "Invalid license key" };
|
||||
}
|
||||
|
||||
// Check expiry
|
||||
const expiry = new Date(expiryStr);
|
||||
if (isNaN(expiry.getTime())) {
|
||||
return { valid: false, error: "Invalid expiry date in key" };
|
||||
}
|
||||
|
||||
if (new Date() > expiry) {
|
||||
return { valid: false, expiry: expiryStr, error: "License key has expired" };
|
||||
}
|
||||
|
||||
return { valid: true, expiry: expiryStr };
|
||||
}
|
||||
|
||||
function loadLicense(): { key: string; expiry: string } | null {
|
||||
try {
|
||||
if (!fs.existsSync(LICENSE_FILE)) return null;
|
||||
return JSON.parse(fs.readFileSync(LICENSE_FILE, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveLicense(key: string, expiry: string) {
|
||||
fs.writeFileSync(LICENSE_FILE, JSON.stringify({ key, expiry }, null, 2));
|
||||
}
|
||||
|
||||
// GET /api/license/status
|
||||
router.get("/status", (req, res) => {
|
||||
const stored = loadLicense();
|
||||
if (!stored) {
|
||||
return res.json({ activated: false, expired: false, expiry: null, daysLeft: null });
|
||||
}
|
||||
|
||||
const result = validateKey(stored.key);
|
||||
if (!result.valid && result.expiry) {
|
||||
return res.json({ activated: true, expired: true, expiry: stored.expiry, daysLeft: 0 });
|
||||
}
|
||||
if (!result.valid) {
|
||||
return res.json({ activated: false, expired: false, expiry: null, daysLeft: null });
|
||||
}
|
||||
|
||||
const expiry = new Date(stored.expiry);
|
||||
const daysLeft = Math.ceil((expiry.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
return res.json({ activated: true, expired: false, expiry: stored.expiry, daysLeft });
|
||||
});
|
||||
|
||||
// POST /api/license/activate
|
||||
router.post("/activate", (req, res) => {
|
||||
const { key } = req.body;
|
||||
if (!key) {
|
||||
return res.status(400).json({ error: "License key is required" });
|
||||
}
|
||||
|
||||
const result = validateKey(key);
|
||||
if (!result.valid) {
|
||||
return res.status(400).json({ error: result.error });
|
||||
}
|
||||
|
||||
saveLicense(key.trim(), result.expiry!);
|
||||
return res.json({ success: true, expiry: result.expiry });
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user