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:
ff
2026-05-26 21:22:34 -04:00
parent 594df39741
commit 070752380d
11 changed files with 472 additions and 81 deletions

View File

@@ -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

View File

@@ -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;

View 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;