From b22a1c973cff7887a9446a72996f56d85afcf7bd Mon Sep 17 00:00:00 2001 From: thompsonjd85 Date: Wed, 19 Mar 2025 09:37:30 -0500 Subject: [PATCH] v.1.0.0. initial push of license-lib --- .env.example | 7 ++ README.md | 144 ++++++++++++++++++++++++++++++++++++ client/config.js | 7 ++ client/index.js | 6 ++ client/offline.js | 24 ++++++ client/utils/fingerprint.js | 6 ++ client/validate.js | 14 ++++ example/client-example.js | 11 +++ example/server-example.js | 13 ++++ licenses_db_setup.sql | 27 +++++++ nextjs/withLicense.js | 11 +++ react/LicenseProvider.js | 31 ++++++++ react/useLicense.js | 8 ++ server/config.js | 13 ++++ server/database.js | 19 +++++ server/index.js | 26 +++++++ server/keygen.js | 14 ++++ server/models/license.js | 21 ++++++ server/utils/crypto.js | 8 ++ server/verify.js | 23 ++++++ 20 files changed, 433 insertions(+) create mode 100644 .env.example create mode 100644 README.md create mode 100644 client/config.js create mode 100644 client/index.js create mode 100644 client/offline.js create mode 100644 client/utils/fingerprint.js create mode 100644 client/validate.js create mode 100644 example/client-example.js create mode 100644 example/server-example.js create mode 100644 licenses_db_setup.sql create mode 100644 nextjs/withLicense.js create mode 100644 react/LicenseProvider.js create mode 100644 react/useLicense.js create mode 100644 server/config.js create mode 100644 server/database.js create mode 100644 server/index.js create mode 100644 server/keygen.js create mode 100644 server/models/license.js create mode 100644 server/utils/crypto.js create mode 100644 server/verify.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8279e29 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +DB_HOST=your-db-host +DB_NAME=license_db +DB_USER_ADMIN=license_admin +DB_PASS_ADMIN=secure_admin_password +DB_USER_READER=license_reader +DB_PASS_READER=secure_read_password +JWT_SECRET=your_jwt_secret diff --git a/README.md b/README.md new file mode 100644 index 0000000..f4cb38c --- /dev/null +++ b/README.md @@ -0,0 +1,144 @@ + +# License Key Library + +A simple, secure, and flexible licensing library built for Node.js, React, Next.js, and general JavaScript applications. + +--- + +## 🚀 Quick Start + +### Installation + +```bash +npm install license-lib + + +Environment Setup +Create a .env file at the root with your database and JWT configuration: + +env +JWT_SECRET=your_jwt_secret +DB_HOST=localhost +DB_NAME=license_db +DB_USER_ADMIN=license_admin +DB_PASS_ADMIN=secure_admin_password +DB_USER_READER=license_reader +DB_PASS_READER=secure_reader_password + + +⚙️ Database Setup +Use the provided SQL script to set up your database: + +sql +Copy code +CREATE DATABASE license_db; + +USE license_db; + +CREATE TABLE licenses ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id VARCHAR(255) NOT NULL, + hashed_key VARCHAR(255) NOT NULL, + license_type ENUM('one-time', 'subscription', 'trial') DEFAULT 'one-time', + tier VARCHAR(50) DEFAULT 'basic', + valid_until DATETIME DEFAULT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE USER 'license_reader'@'%' IDENTIFIED BY 'secure_reader_password'; +GRANT SELECT ON license_db.* TO 'license_reader'@'%'; + +CREATE USER 'license_admin'@'%' IDENTIFIED BY 'secure_admin_password'; +GRANT SELECT, INSERT, UPDATE ON license_db.* TO 'license_admin'@'%'; + +FLUSH PRIVILEGES; +📦 API Endpoints +Generate License + +POST /generate-license + +Body: + +json +Copy code +{ + "userId": "user123", + "licenseType": "subscription", + "tier": "pro", + "validUntil": "2024-12-31" +} +Response: + +json +Copy code +{ + "licenseKey": "generated.jwt.token" +} +Validate License + +POST /validate-license + +Body: + +json +Copy code +{ + "userId": "user123", + "licenseKey": "generated.jwt.token" +} +Response: + +json +Copy code +{ + "valid": true +} +🎯 Client Integration +Import functions directly from the client module: + +javascript +Copy code +import { activateLicense, generateFingerprint } from './client'; + +// Validate license +const valid = await activateLicense(userId, licenseKey); + +// Device fingerprinting +const fingerprint = generateFingerprint(); +⚛️ React Integration +Use the provided React hook and provider: + +jsx +Copy code +import { LicenseProvider } from './react/LicenseProvider'; +import useLicense from './react/useLicense'; + +const App = () => ( + + + +); + +const YourApp = () => { + const { isLicensed } = useLicense(); + return
{isLicensed ? 'Licensed!' : 'Not Licensed!'}
; +}; +▲ Next.js Integration +Wrap your component easily: + +jsx +Copy code +import withLicense from './nextjs/withLicense'; + +const Page = ({ isLicensed }) => ( +
{isLicensed ? 'Licensed!' : 'Not Licensed!'}
+); + +export default withLicense(Page, { userId: 'user123', licenseKey: 'jwt.token' }); +📌 License +This project is licensed under MIT. + +vbnet +Copy code + +Your licensing library is now fully documented and ready for developers to integrate easily into their projects. \ No newline at end of file diff --git a/client/config.js b/client/config.js new file mode 100644 index 0000000..bdba282 --- /dev/null +++ b/client/config.js @@ -0,0 +1,7 @@ +// client/config.js + +export const CONFIG = { + LICENSE_API_URL: 'http://localhost:3000', // backend API URL + OFFLINE_GRACE_PERIOD_DAYS: 14, + }; + \ No newline at end of file diff --git a/client/index.js b/client/index.js new file mode 100644 index 0000000..0a6cb16 --- /dev/null +++ b/client/index.js @@ -0,0 +1,6 @@ +// client/index.js + +export { activateLicense } from './validate'; +export { cacheLicenseData, getCachedLicenseData, isLicenseCacheValid } from './offline'; +export { generateFingerprint } from './utils/fingerprint'; +export { CONFIG } from './config'; diff --git a/client/offline.js b/client/offline.js new file mode 100644 index 0000000..99924cb --- /dev/null +++ b/client/offline.js @@ -0,0 +1,24 @@ +// client/offline.js + +const CACHE_KEY = 'license_data'; + +export const cacheLicenseData = (licenseData) => { + localStorage.setItem(CACHE_KEY, JSON.stringify({ + data: licenseData, + cachedAt: Date.now(), + })); +}; + +export const getCachedLicenseData = () => { + const item = localStorage.getItem(CACHE_KEY); + return item ? JSON.parse(item) : null; +}; + +export const isLicenseCacheValid = () => { + const cache = getCachedLicenseData(); + if (!cache) return false; + + const cachedAt = cache.cachedAt; + const elapsedDays = (Date.now() - cachedAt) / (1000 * 60 * 60 * 24); + return elapsedDays <= CONFIG.OFFLINE_GRACE_PERIOD_DAYS; +}; diff --git a/client/utils/fingerprint.js b/client/utils/fingerprint.js new file mode 100644 index 0000000..b937586 --- /dev/null +++ b/client/utils/fingerprint.js @@ -0,0 +1,6 @@ +// client/utils/fingerprint.js + +export const generateFingerprint = () => { + return btoa(navigator.userAgent + navigator.language + screen.width + screen.height); + }; + \ No newline at end of file diff --git a/client/validate.js b/client/validate.js new file mode 100644 index 0000000..cf6627c --- /dev/null +++ b/client/validate.js @@ -0,0 +1,14 @@ +// client/validate.js + +import { CONFIG } from './config'; + +export const activateLicense = async (userId, licenseKey) => { + const response = await fetch(`${CONFIG.LICENSE_API_URL}/validate-license`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ userId, licenseKey }), + }); + + const { valid } = await response.json(); + return valid; +}; diff --git a/example/client-example.js b/example/client-example.js new file mode 100644 index 0000000..721aad6 --- /dev/null +++ b/example/client-example.js @@ -0,0 +1,11 @@ +// example/client-example.js +const axios = require('axios'); + +(async () => { + const { data } = await axios.post('http://localhost:3000/validate-license', { + userId: 'user123', + licenseKey: 'your_license_key_here' + }); + + console.log('License Valid:', data.valid); +})(); \ No newline at end of file diff --git a/example/server-example.js b/example/server-example.js new file mode 100644 index 0000000..f131f3b --- /dev/null +++ b/example/server-example.js @@ -0,0 +1,13 @@ +// example/server-example.js +const axios = require('axios'); + +(async () => { + const { data } = await axios.post('http://localhost:3000/generate-license', { + userId: 'user123', + licenseType: 'subscription', + tier: 'pro', + validUntil: '2024-12-31' + }); + + console.log('Generated License:', data.licenseKey); +})(); \ No newline at end of file diff --git a/licenses_db_setup.sql b/licenses_db_setup.sql new file mode 100644 index 0000000..f07d19f --- /dev/null +++ b/licenses_db_setup.sql @@ -0,0 +1,27 @@ +-- Create Database +CREATE DATABASE license_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE license_db; + +-- Create licenses table +CREATE TABLE licenses ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id VARCHAR(255) NOT NULL, + hashed_key VARCHAR(255) NOT NULL, + license_type ENUM('one-time', 'subscription', 'trial') DEFAULT 'one-time', + tier VARCHAR(50) DEFAULT 'basic', + valid_until DATETIME DEFAULT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_user_id (user_id) +); + +-- User with Read-Only Access (Client-side validation) +CREATE USER 'license_reader'@'%' IDENTIFIED BY 'secure_read_password'; +GRANT SELECT ON license_db.licenses TO 'license_reader'@'%'; + +-- User with Read-Write Access (Server-side key generation and updating) +CREATE USER 'license_admin'@'%' IDENTIFIED BY 'secure_admin_password'; +GRANT SELECT, INSERT, UPDATE ON license_db.licenses TO 'license_admin'@'%'; + +-- Apply privileges +FLUSH PRIVILEGES; diff --git a/nextjs/withLicense.js b/nextjs/withLicense.js new file mode 100644 index 0000000..7ed9499 --- /dev/null +++ b/nextjs/withLicense.js @@ -0,0 +1,11 @@ +// nextjs/withLicense.js + +import { LicenseProvider } from '../react/LicenseProvider'; + +const withLicense = (Component, { userId, licenseKey }) => (props) => ( + + + +); + +export default withLicense; diff --git a/react/LicenseProvider.js b/react/LicenseProvider.js new file mode 100644 index 0000000..4eaae72 --- /dev/null +++ b/react/LicenseProvider.js @@ -0,0 +1,31 @@ +// react/LicenseProvider.js + +import React, { createContext, useState, useEffect } from 'react'; +import { activateLicense } from '../client/validate'; +import { cacheLicenseData, getCachedLicenseData, isLicenseCacheValid } from '../client/offline'; + +export const LicenseContext = createContext(); + +export const LicenseProvider = ({ userId, licenseKey, children }) => { + const [isLicensed, setIsLicensed] = useState(false); + + useEffect(() => { + const checkLicense = async () => { + if (navigator.onLine) { + const valid = await activateLicense(userId, licenseKey); + setIsLicensed(valid); + if (valid) cacheLicenseData({ userId, licenseKey }); + } else { + setIsLicensed(isLicenseCacheValid()); + } + }; + + checkLicense(); + }, [userId, licenseKey]); + + return ( + + {children} + + ); +}; diff --git a/react/useLicense.js b/react/useLicense.js new file mode 100644 index 0000000..3d65850 --- /dev/null +++ b/react/useLicense.js @@ -0,0 +1,8 @@ +// react/useLicense.js + +import { useContext } from 'react'; +import { LicenseContext } from './LicenseProvider'; + +const useLicense = () => useContext(LicenseContext); + +export default useLicense; diff --git a/server/config.js b/server/config.js new file mode 100644 index 0000000..da23b66 --- /dev/null +++ b/server/config.js @@ -0,0 +1,13 @@ +// server/config.js +require('dotenv').config(); + +module.exports = { + JWT_SECRET: process.env.JWT_SECRET, + DB_HOST: process.env.DB_HOST, + DB_NAME: process.env.DB_NAME, + DB_USER_ADMIN: process.env.DB_USER_ADMIN, + DB_PASS_ADMIN: process.env.DB_PASS_ADMIN, + DB_USER_READER: process.env.DB_USER_READER, + DB_PASS_READER: process.env.DB_PASS_READER, + LICENSE_CHECK_INTERVAL: '7d', +}; diff --git a/server/database.js b/server/database.js new file mode 100644 index 0000000..0cd448f --- /dev/null +++ b/server/database.js @@ -0,0 +1,19 @@ +// server/database.js +const mysql = require('mysql2/promise'); +const config = require('./config'); + +const adminPool = mysql.createPool({ + host: config.DB_HOST, + user: config.DB_USER_ADMIN, + password: config.DB_PASS_ADMIN, + database: config.DB_NAME, +}); + +const readerPool = mysql.createPool({ + host: config.DB_HOST, + user: config.DB_USER_READER, + password: config.DB_PASS_READER, + database: config.DB_NAME, +}); + +module.exports = { adminPool, readerPool }; diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..abdb413 --- /dev/null +++ b/server/index.js @@ -0,0 +1,26 @@ +// server/index.js +const express = require('express'); +const { generateLicenseKey, hashLicenseKey } = require('./keygen'); +const { saveLicense } = require('./models/license'); +const { verifyLicenseKey } = require('./verify'); + +const app = express(); +app.use(express.json()); + +app.post('/generate-license', async (req, res) => { + const { userId, licenseType, tier, validUntil } = req.body; + const licenseKey = generateLicenseKey(userId, licenseType, tier); + const hashedKey = hashLicenseKey(JSON.parse(Buffer.from(licenseKey.split('.')[1], 'base64')).rawKey); + + await saveLicense(userId, hashedKey, licenseType, tier, validUntil); + + res.json({ licenseKey }); +}); + +app.post('/validate-license', async (req, res) => { + const { userId, licenseKey } = req.body; + const valid = await verifyLicenseKey(userId, licenseKey); + res.json({ valid }); +}); + +app.listen(3000, () => console.log('License server running on port 3000')); \ No newline at end of file diff --git a/server/keygen.js b/server/keygen.js new file mode 100644 index 0000000..0cf5ece --- /dev/null +++ b/server/keygen.js @@ -0,0 +1,14 @@ +// server/keygen.js +const crypto = require('crypto'); +const { encrypt } = require('./utils/crypto'); + +const generateLicenseKey = (userId) => { + const rawKey = crypto.randomBytes(32).toString('hex'); + const payload = { userId, rawKey, issued: Date.now() }; + const encryptedKey = encrypt(payload); + return encryptedKey; +}; + +const hashLicenseKey = (key) => crypto.createHash('sha256').update(key).digest('hex'); + +module.exports = { generateLicenseKey, hashLicenseKey }; \ No newline at end of file diff --git a/server/models/license.js b/server/models/license.js new file mode 100644 index 0000000..560fd39 --- /dev/null +++ b/server/models/license.js @@ -0,0 +1,21 @@ +const { adminPool, readerPool } = require('../database'); + +const saveLicense = async (userId, hashedKey, licenseType, tier, validUntil = null) => { + const sql = ` + INSERT INTO licenses (user_id, hashed_key, license_type, tier, valid_until, created_at) + VALUES (?, ?, ?, ?, ?, NOW()) + `; + const [result] = await adminPool.execute(sql, [userId, hashedKey, licenseType, tier, validUntil]); + return result.insertId; +}; + +const getLicense = async (userId) => { + const sql = ` + SELECT hashed_key, license_type, tier, valid_until + FROM licenses WHERE user_id = ? + `; + const [rows] = await readerPool.execute(sql, [userId]); + return rows[0]; +}; + +module.exports = { saveLicense, getLicense }; diff --git a/server/utils/crypto.js b/server/utils/crypto.js new file mode 100644 index 0000000..06320fa --- /dev/null +++ b/server/utils/crypto.js @@ -0,0 +1,8 @@ +// server/utils/crypto.js +const jwt = require('jsonwebtoken'); +const config = require('../config'); + +const encrypt = (data) => jwt.sign(data, config.JWT_SECRET); +const decrypt = (token) => jwt.verify(token, config.JWT_SECRET); + +module.exports = { encrypt, decrypt }; \ No newline at end of file diff --git a/server/verify.js b/server/verify.js new file mode 100644 index 0000000..46429db --- /dev/null +++ b/server/verify.js @@ -0,0 +1,23 @@ +// server/verify.js +const { decrypt } = require('./utils/crypto'); +const { hashLicenseKey } = require('./keygen'); +const { getLicense } = require('./models/license'); + +const verifyLicenseKey = async (userId, token) => { + try { + const payload = decrypt(token); + if (payload.userId !== userId) return false; + + const storedLicense = await getLicense(userId); + const hashed = hashLicenseKey(payload.rawKey); + + if (storedLicense.hashed_key !== hashed) return false; + if (storedLicense.license_type !== 'one-time' && storedLicense.valid_until && new Date(storedLicense.valid_until) < new Date()) return false; + + return true; + } catch (err) { + return false; + } +}; + +module.exports = { verifyLicenseKey }; \ No newline at end of file