Delete license_server from source
This commit is contained in:
@@ -1,342 +0,0 @@
|
||||
"""
|
||||
TechDesk License Server
|
||||
=======================
|
||||
A simple, password-protected internal Flask app for generating and tracking
|
||||
TechDesk license keys.
|
||||
|
||||
Setup
|
||||
-----
|
||||
1. Run generate_keys.py once to produce private_key.pem and public_key.pem.
|
||||
2. Set environment variables (or edit the constants below):
|
||||
LICENSE_SERVER_PASSWORD — admin password (default: change-me)
|
||||
SECRET_KEY — Flask secret key for sessions
|
||||
3. pip install -r requirements.txt
|
||||
4. python app.py
|
||||
|
||||
This app is intended for localhost / internal network use only.
|
||||
Do NOT expose it to the public internet.
|
||||
|
||||
License key format
|
||||
------------------
|
||||
TDESK-<payload_b64>.<signature_b64>
|
||||
|
||||
Where:
|
||||
payload_b64 = base64url( JSON payload bytes )
|
||||
signature_b64 = base64url( RSA-SHA256 signature of payload bytes )
|
||||
|
||||
JSON payload fields:
|
||||
customer — company / customer name
|
||||
email — admin contact email
|
||||
tier — "community" | "business" | "enterprise"
|
||||
issued_at — ISO date string (YYYY-MM-DD)
|
||||
expires_at — ISO date string (YYYY-MM-DD)
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import base64
|
||||
import sqlite3
|
||||
from datetime import date, timedelta
|
||||
from functools import wraps
|
||||
from flask import Flask, request, render_template_string, redirect, url_for, session, flash
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.environ.get('SECRET_KEY', 'license-server-secret-change-me')
|
||||
|
||||
ADMIN_PASSWORD = os.environ.get('LICENSE_SERVER_PASSWORD', 'change-me')
|
||||
PRIVATE_KEY_FILE = os.path.join(os.path.dirname(__file__), 'private_key.pem')
|
||||
DB_FILE = os.path.join(os.path.dirname(__file__), 'licenses.db')
|
||||
|
||||
TIERS = ['community', 'business', 'enterprise']
|
||||
|
||||
|
||||
# ── Database ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def get_db():
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def init_db():
|
||||
with get_db() as conn:
|
||||
conn.execute('''
|
||||
CREATE TABLE IF NOT EXISTS licenses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
customer TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
tier TEXT NOT NULL,
|
||||
issued_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
key_preview TEXT NOT NULL,
|
||||
full_key TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
|
||||
# ── RSA helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _load_private_key():
|
||||
if not os.path.exists(PRIVATE_KEY_FILE):
|
||||
raise FileNotFoundError(
|
||||
f'private_key.pem not found at {PRIVATE_KEY_FILE}. '
|
||||
'Run generate_keys.py first.'
|
||||
)
|
||||
with open(PRIVATE_KEY_FILE, 'rb') as f:
|
||||
return serialization.load_pem_private_key(f.read(), password=None)
|
||||
|
||||
|
||||
def generate_license_key(customer, email, tier, expires_at):
|
||||
payload = {
|
||||
'customer' : customer,
|
||||
'email' : email,
|
||||
'tier' : tier,
|
||||
'issued_at' : date.today().isoformat(),
|
||||
'expires_at': expires_at,
|
||||
}
|
||||
payload_bytes = json.dumps(payload, separators=(',', ':')).encode('utf-8')
|
||||
private_key = _load_private_key()
|
||||
signature = private_key.sign(payload_bytes, padding.PKCS1v15(), hashes.SHA256())
|
||||
|
||||
payload_b64 = base64.urlsafe_b64encode(payload_bytes).decode()
|
||||
sig_b64 = base64.urlsafe_b64encode(signature).decode()
|
||||
return f'TDESK-{payload_b64}.{sig_b64}'
|
||||
|
||||
|
||||
# ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def login_required(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
if not session.get('authenticated'):
|
||||
return redirect(url_for('login'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
|
||||
|
||||
# ── Templates ─────────────────────────────────────────────────────────────────
|
||||
|
||||
_BASE = '''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>TechDesk License Server</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; background: #f5f5f5; margin: 0; padding: 0; }
|
||||
.header { background: #1e3a5f; color: #fff; padding: 16px 32px; }
|
||||
.header h1 { margin: 0; font-size: 1.4rem; }
|
||||
.container { max-width: 1000px; margin: 32px auto; padding: 0 16px; }
|
||||
.card { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,.1); padding: 24px; margin-bottom: 24px; }
|
||||
label { display: block; margin-bottom: 4px; font-weight: 600; font-size: .9rem; color: #555; }
|
||||
input, select { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px;
|
||||
font-size: .95rem; box-sizing: border-box; margin-bottom: 14px; }
|
||||
.btn { background: #1e3a5f; color: #fff; border: none; padding: 10px 20px;
|
||||
border-radius: 6px; cursor: pointer; font-size: .95rem; }
|
||||
.btn:hover { background: #163055; }
|
||||
.btn-danger { background: #dc2626; }
|
||||
.alert { padding: 12px 16px; border-radius: 6px; margin-bottom: 16px; }
|
||||
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
|
||||
.alert-error { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; }
|
||||
.key-box { background: #f0f4f8; border: 1px solid #cbd5e0; border-radius: 6px;
|
||||
padding: 12px; font-family: monospace; word-break: break-all;
|
||||
font-size: .85rem; margin-top: 8px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: .9rem; }
|
||||
th { background: #f1f5f9; padding: 10px 12px; text-align: left; color: #475569; }
|
||||
td { padding: 10px 12px; border-bottom: 1px solid #f1f5f9; }
|
||||
.badge { display: inline-block; padding: 2px 10px; border-radius: 99px; font-size: .8rem; font-weight: 600; }
|
||||
.badge-community { background: #e2e8f0; color: #475569; }
|
||||
.badge-business { background: #dbeafe; color: #1d4ed8; }
|
||||
.badge-enterprise { background: #ede9fe; color: #6d28d9; }
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header"><h1>🔑 TechDesk License Server</h1></div>
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for cat, msg in messages %}
|
||||
<div class="alert alert-{{ cat }}">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
_LOGIN = '''
|
||||
{% extends base %}
|
||||
{% block content %}
|
||||
<div class="card" style="max-width:400px;margin:0 auto;">
|
||||
<h2 style="margin-top:0;">Login</h2>
|
||||
<form method="POST">
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" autofocus required>
|
||||
<button type="submit" class="btn">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
'''
|
||||
|
||||
_INDEX = '''
|
||||
{% extends base %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h2 style="margin-top:0;">Generate New License Key</h2>
|
||||
<form method="POST" action="/generate">
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<label>Customer / Company Name</label>
|
||||
<input type="text" name="customer" required placeholder="Acme Corp">
|
||||
</div>
|
||||
<div>
|
||||
<label>Contact Email</label>
|
||||
<input type="email" name="email" required placeholder="admin@acme.com">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<label>Tier</label>
|
||||
<select name="tier">
|
||||
<option value="business">Business</option>
|
||||
<option value="enterprise">Enterprise</option>
|
||||
<option value="community">Community (free)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Duration</label>
|
||||
<select name="duration">
|
||||
<option value="365">1 year</option>
|
||||
<option value="730">2 years</option>
|
||||
<option value="180">6 months</option>
|
||||
<option value="36500">Lifetime (100 years)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn">Generate Key</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if new_key %}
|
||||
<div class="card" style="border: 2px solid #3b82f6;">
|
||||
<h3 style="margin-top:0;color:#1d4ed8;">✅ New License Key Generated</h3>
|
||||
<p style="margin:0 0 8px;color:#555;">Copy this key and send it to the customer.
|
||||
It will not be shown again in full.</p>
|
||||
<div class="key-box">{{ new_key }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin-top:0;">Issued Licenses ({{ licenses|length }})</h2>
|
||||
{% if licenses %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th><th>Customer</th><th>Email</th><th>Tier</th>
|
||||
<th>Issued</th><th>Expires</th><th>Key Preview</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for lic in licenses %}
|
||||
<tr>
|
||||
<td>{{ lic.id }}</td>
|
||||
<td>{{ lic.customer }}</td>
|
||||
<td>{{ lic.email }}</td>
|
||||
<td><span class="badge badge-{{ lic.tier }}">{{ lic.tier }}</span></td>
|
||||
<td>{{ lic.issued_at }}</td>
|
||||
<td>{{ lic.expires_at }}</td>
|
||||
<td style="font-family:monospace;font-size:.8rem;">{{ lic.key_preview }}…</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p style="color:#888;">No licenses issued yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<p style="text-align:right;">
|
||||
<a href="/logout" style="color:#dc2626;font-size:.9rem;">Logout</a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
'''
|
||||
|
||||
|
||||
# ── Routes ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
if request.form.get('password') == ADMIN_PASSWORD:
|
||||
session['authenticated'] = True
|
||||
return redirect(url_for('index'))
|
||||
flash('Incorrect password.', 'error')
|
||||
return render_template_string(_LOGIN, base=_BASE)
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect(url_for('login'))
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
with get_db() as conn:
|
||||
licenses = conn.execute(
|
||||
'SELECT * FROM licenses ORDER BY id DESC'
|
||||
).fetchall()
|
||||
return render_template_string(_INDEX, base=_BASE, licenses=licenses, new_key=None)
|
||||
|
||||
|
||||
@app.route('/generate', methods=['POST'])
|
||||
@login_required
|
||||
def generate():
|
||||
customer = request.form.get('customer', '').strip()
|
||||
email = request.form.get('email', '').strip()
|
||||
tier = request.form.get('tier', 'business')
|
||||
duration = int(request.form.get('duration', 365))
|
||||
|
||||
if not customer or not email:
|
||||
flash('Customer name and email are required.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if tier not in TIERS:
|
||||
flash('Invalid tier.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
expires_at = (date.today() + timedelta(days=duration)).isoformat()
|
||||
|
||||
try:
|
||||
key = generate_license_key(customer, email, tier, expires_at)
|
||||
except FileNotFoundError as exc:
|
||||
flash(str(exc), 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
key_preview = key[:32]
|
||||
|
||||
with get_db() as conn:
|
||||
conn.execute(
|
||||
'INSERT INTO licenses (customer, email, tier, issued_at, expires_at, key_preview, full_key) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
(customer, email, tier, date.today().isoformat(), expires_at, key_preview, key),
|
||||
)
|
||||
|
||||
with get_db() as conn:
|
||||
licenses = conn.execute(
|
||||
'SELECT * FROM licenses ORDER BY id DESC'
|
||||
).fetchall()
|
||||
|
||||
flash(f'License key generated for {customer}.', 'success')
|
||||
return render_template_string(_INDEX, base=_BASE, licenses=licenses, new_key=key)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
||||
print('TechDesk License Server running at http://127.0.0.1:5001')
|
||||
print(f'Admin password: {ADMIN_PASSWORD}')
|
||||
app.run(host='127.0.0.1', port=5001, debug=False)
|
||||
@@ -1,73 +0,0 @@
|
||||
"""
|
||||
generate_keys.py — One-time RSA-2048 key pair generator for TechDesk licensing.
|
||||
|
||||
Run this ONCE on your server to produce the key pair:
|
||||
|
||||
python generate_keys.py
|
||||
|
||||
Output:
|
||||
private_key.pem — Keep secret. Never ship to customers.
|
||||
public_key.pem — Paste the contents into license_service.py in TechDesk.
|
||||
|
||||
Security notes:
|
||||
- private_key.pem must NEVER be committed to git or shipped in any customer build.
|
||||
- public_key.pem is safe to embed in TechDesk — it can only verify signatures,
|
||||
not forge them.
|
||||
- Store private_key.pem on a secure, offline or access-restricted machine.
|
||||
"""
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import os
|
||||
|
||||
KEY_SIZE = 2048
|
||||
PRIVATE_KEY_FILE = 'private_key.pem'
|
||||
PUBLIC_KEY_FILE = 'public_key.pem'
|
||||
|
||||
|
||||
def generate():
|
||||
if os.path.exists(PRIVATE_KEY_FILE) or os.path.exists(PUBLIC_KEY_FILE):
|
||||
print('Key files already exist. Delete them manually before regenerating.')
|
||||
print(f' {PRIVATE_KEY_FILE}')
|
||||
print(f' {PUBLIC_KEY_FILE}')
|
||||
return
|
||||
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=KEY_SIZE,
|
||||
)
|
||||
|
||||
# Write private key (unencrypted PEM — restrict file permissions after writing)
|
||||
private_pem = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
with open(PRIVATE_KEY_FILE, 'wb') as f:
|
||||
f.write(private_pem)
|
||||
|
||||
# Write public key (PEM)
|
||||
public_pem = private_key.public_key().public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
with open(PUBLIC_KEY_FILE, 'wb') as f:
|
||||
f.write(public_pem)
|
||||
|
||||
# Restrict private key permissions on Unix-like systems
|
||||
try:
|
||||
os.chmod(PRIVATE_KEY_FILE, 0o600)
|
||||
except AttributeError:
|
||||
pass # Windows — manage permissions manually
|
||||
|
||||
print('Key pair generated successfully.')
|
||||
print(f' Private key : {PRIVATE_KEY_FILE} (KEEP SECRET)')
|
||||
print(f' Public key : {PUBLIC_KEY_FILE} (safe to embed in TechDesk)')
|
||||
print()
|
||||
print('Next steps:')
|
||||
print(' 1. Copy the contents of public_key.pem into license_service.py (PUBLIC_KEY_PEM constant).')
|
||||
print(' 2. Never commit or share private_key.pem.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
generate()
|
||||
@@ -1,2 +0,0 @@
|
||||
flask==3.0.3
|
||||
cryptography==42.0.8
|
||||
Reference in New Issue
Block a user