#!/usr/bin/env python3 """ СИСТЕМА ДОСТУПА — месячные ключи для людей и агентов Формат ключа: SA-VIP-2607-K7M2X9F4 │ │ │ └ случайная часть (8 символов, без похожих букв) │ │ └ год-месяц действия (2607 = июль 2026) │ └ тариф: FREE / PRO / VIP / AGENT └ префикс Sovereign Arena Почему месяц зашит в ключ: срок действия проверяется БЕЗ обращения к базе. Даже если файл ключей потеряется, просроченный ключ не сработает. Подпись HMAC не даёт подобрать или подделать ключ. Тарифы: FREE конфиги и трек-рекорд (можно и без ключа) PRO + живое состояние сеток, история филлов VIP + расширенная аналитика, приоритет, ранний доступ AGENT машинный доступ, повышенный лимит запросов Команды: python3 access_keys.py issue --tier VIP --months 1 --count 5 --note "покупатели июля" python3 access_keys.py list python3 access_keys.py check SA-VIP-2607-K7M2X9F4 python3 access_keys.py revoke SA-VIP-2607-K7M2X9F4 python3 access_keys.py renew --tier VIP # перевыпуск на следующий месяц python3 access_keys.py expiring # у кого истекает в этом месяце """ import argparse, hashlib, hmac, json, os, secrets, sys, time from datetime import datetime, timezone HERE = os.path.dirname(os.path.abspath(__file__)) DB = os.path.join(HERE, "access_keys.json") ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # без I,O,0,1 — не путаются на слух TIERS = ["FREE", "PRO", "VIP", "AGENT"] def secret(): """секрет подписи; при первом запуске создаётся и сохраняется рядом""" p = os.path.join(HERE, ".key_secret") if os.path.exists(p): return open(p).read().strip().encode() s = secrets.token_hex(32) with open(p, "w") as f: f.write(s) os.chmod(p, 0o600) return s.encode() SECRET = secret() def load(): if os.path.exists(DB): return json.load(open(DB, encoding="utf-8")) return {"keys": {}, "created": int(time.time())} def save(d): json.dump(d, open(DB, "w", encoding="utf-8"), ensure_ascii=False, indent=1) def sign(body): return hmac.new(SECRET, body.encode(), hashlib.sha256).hexdigest()[:4].upper() def period(months_ahead=0): n = datetime.now(timezone.utc) m = n.month + months_ahead y = n.year + (m - 1) // 12 m = (m - 1) % 12 + 1 return f"{y % 100:02d}{m:02d}" def make(tier, months=1): tier = tier.upper() if tier not in TIERS: sys.exit(f"тариф должен быть из {TIERS}") per = period(months - 1) # действует ДО конца этого периода rnd = "".join(secrets.choice(ALPHABET) for _ in range(6)) body = f"SA-{tier}-{per}-{rnd}" return f"{body}{sign(body)}" def parse(key): key = (key or "").strip().upper() parts = key.split("-") if len(parts) != 4 or parts[0] != "SA": return None tier, per, tail = parts[1], parts[2], parts[3] if tier not in TIERS or len(per) != 4 or len(tail) != 10: return None body = f"SA-{tier}-{per}-{tail[:6]}" if sign(body) != tail[6:]: return None return {"key": key, "tier": tier, "period": per} def expired(per): """ключ живёт до конца указанного месяца включительно""" return per < period() def validate(key, db=None): """главная функция — её зовёт API""" p = parse(key) if not p: return {"valid": False, "reason": "неверный формат или подпись"} db = db or load() rec = db["keys"].get(p["key"]) if rec and rec.get("revoked"): return {"valid": False, "reason": "ключ отозван", **p} if expired(p["period"]): return {"valid": False, "reason": f"истёк (действовал до {p['period']})", **p} return {"valid": True, "tier": p["tier"], "period": p["period"], "note": (rec or {}).get("note", ""), "key": p["key"]} def touch(key, db=None): """отметить использование — для статистики""" db = db or load() r = db["keys"].get(key) if r: r["last_used"] = int(time.time()) r["uses"] = r.get("uses", 0) + 1 save(db) # ─────────────────────────── команды ─────────────────────────── def cmd_issue(a): db = load() out = [] for _ in range(a.count): k = make(a.tier, a.months) db["keys"][k] = {"tier": a.tier.upper(), "period": parse(k)["period"], "note": a.note, "issued": int(time.time()), "uses": 0, "revoked": False} out.append(k) save(db) per = parse(out[0])["period"] print(f"\nвыпущено {len(out)} × {a.tier.upper()}, действуют до конца {per[2:]}.{'20'+per[:2]}\n") for k in out: print(" " + k) print(f"\nвсего ключей в базе: {len(db['keys'])}") print("передавать заголовком: X-VIP-Key: <ключ>") def cmd_list(a): db = load() if not db["keys"]: print("ключей нет"); return print(f"{'ключ':<24} {'тариф':<7} {'до':<7} {'исп.':>5} {'статус':<12} заметка") print("-" * 82) for k, r in sorted(db["keys"].items(), key=lambda x: (x[1]["period"], x[0])): st = "отозван" if r.get("revoked") else ("истёк" if expired(r["period"]) else "активен") per = r["period"] print(f"{k:<24} {r['tier']:<7} {per[2:]}.{'20'+per[:2]:<4} {r.get('uses',0):>5} {st:<12} {r.get('note','')}") act = sum(1 for r in db["keys"].values() if not r.get("revoked") and not expired(r["period"])) print(f"\nактивных: {act} из {len(db['keys'])}") def cmd_check(a): r = validate(a.key) print(json.dumps(r, ensure_ascii=False, indent=1)) sys.exit(0 if r["valid"] else 1) def cmd_revoke(a): db = load() k = a.key.strip().upper() if k not in db["keys"]: sys.exit("нет такого ключа в базе") db["keys"][k]["revoked"] = True save(db) print(f"отозван: {k}") def cmd_renew(a): """перевыпустить активные ключи тарифа на следующий месяц""" db = load() olds = [k for k, r in db["keys"].items() if r["tier"] == a.tier.upper() and not r.get("revoked") and not expired(r["period"])] if not olds: print(f"нет активных ключей тарифа {a.tier.upper()}"); return print(f"перевыпуск {len(olds)} ключей {a.tier.upper()} на следующий месяц:\n") for old in olds: new = make(a.tier, 2) # +1 месяц вперёд db["keys"][new] = {"tier": a.tier.upper(), "period": parse(new)["period"], "note": db["keys"][old].get("note", "") + " (продление)", "issued": int(time.time()), "uses": 0, "revoked": False, "renewed_from": old} print(f" {old} → {new}") save(db) print("\nстарые ключи НЕ отозваны — доработают до конца своего месяца.") def cmd_expiring(a): db = load() cur = period() rows = [(k, r) for k, r in db["keys"].items() if r["period"] == cur and not r.get("revoked")] if not rows: print("в этом месяце ничего не истекает"); return print(f"истекают в конце {cur[2:]}.{'20'+cur[:2]} — {len(rows)} шт:\n") for k, r in rows: print(f" {k} {r['tier']:<6} использований {r.get('uses',0):<5} {r.get('note','')}") print(f"\nпродлить всех: python3 access_keys.py renew --tier {rows[0][1]['tier']}") if __name__ == "__main__": ap = argparse.ArgumentParser(description="ключи доступа Sovereign Arena") sp = ap.add_subparsers(dest="cmd", required=True) p = sp.add_parser("issue", help="выпустить ключи") p.add_argument("--tier", default="VIP", choices=[t.lower() for t in TIERS] + TIERS) p.add_argument("--months", type=int, default=1, help="на сколько месяцев") p.add_argument("--count", type=int, default=1) p.add_argument("--note", default="") p.set_defaults(f=cmd_issue) p = sp.add_parser("list", help="список ключей"); p.set_defaults(f=cmd_list) p = sp.add_parser("check", help="проверить ключ") p.add_argument("key"); p.set_defaults(f=cmd_check) p = sp.add_parser("revoke", help="отозвать ключ") p.add_argument("key"); p.set_defaults(f=cmd_revoke) p = sp.add_parser("renew", help="продлить тариф на следующий месяц") p.add_argument("--tier", default="VIP"); p.set_defaults(f=cmd_renew) p = sp.add_parser("expiring", help="кто истекает в этом месяце"); p.set_defaults(f=cmd_expiring) a = ap.parse_args() a.f(a)