KyrosDev Claude commited on
Commit
79daae2
·
1 Parent(s): e0ffce0

Implement complete 32-bit license system with optimized UI layout

Browse files

## Backend Updates:
- Update license code generation to true 32-character format (0-9, A-Z)
- Remove KS prefix, use pure random codes for enhanced security
- Upgrade from 36^8 to 36^32 possible combinations

## Frontend Enhancements:
- Add dual display modes: compact for tables, full for details
- Implement formatFullLicenseCode() for complete code display in modals
- Optimize column widths: license code (flex: 1.8), hardware ID (flex: 1.2)
- Enhance details modal with complete hardware ID display and copy functionality
- Add .hardware-id-full styling with proper text wrapping

## Database Updates:
- Provide comprehensive 32-bit test data covering all scenarios:
* Active users (90-day, 1-year licenses)
* Trial users (7-day trial)
* Pending activations (unused licenses)
* Expired licenses (15 days overdue)
* Suspended accounts (admin disabled)
* Permanent licenses (enterprise users)
* Near-expiry warnings (2 days remaining)
- Update usage logs with realistic IP addresses and hardware fingerprints

## UI/UX Improvements:
- License codes show as "ABCD1234...5678" in tables, full format in details
- Hardware IDs show as "A1B2C3D4...X7Y8Z9W0" in tables, complete in details
- Enhanced copy-to-clipboard functionality for both license codes and hardware IDs
- Improved responsive design and text wrapping for 32-character strings

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

app/services/license_service.py CHANGED
@@ -22,12 +22,11 @@ class LicenseService:
22
  print("❌ LicenseService: Supabase client is None - all operations will fail")
23
 
24
  def generate_license_code(self) -> str:
25
- """生成唯一授權碼"""
26
- # 生成8位隨機字符串
27
- random_part = secrets.token_hex(4).upper()
28
- # 使用時間戳確保唯一性
29
- timestamp = str(int(datetime.now().timestamp()))[-6:]
30
- return f"KS{random_part}{timestamp}"
31
 
32
  async def create_license(self, license_data: LicenseCreate) -> Dict[str, Any]:
33
  """建立新授權"""
 
22
  print("❌ LicenseService: Supabase client is None - all operations will fail")
23
 
24
  def generate_license_code(self) -> str:
25
+ """生成32位唯一授權碼"""
26
+ # 生成32位隨機字符串 (數字和大寫字母)
27
+ characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
28
+ random_code = ''.join(secrets.choice(characters) for _ in range(32))
29
+ return random_code
 
30
 
31
  async def create_license(self, license_data: LicenseCreate) -> Dict[str, Any]:
32
  """建立新授權"""
frontend/css/style.css CHANGED
@@ -1818,8 +1818,12 @@ body {
1818
  min-width: 0;
1819
  }
1820
 
 
 
 
 
1821
  .info-column:nth-child(2) {
1822
- flex: 1.5;
1823
  }
1824
 
1825
  .info-column:last-child {
@@ -1878,12 +1882,28 @@ body {
1878
 
1879
  .hardware-id-compact {
1880
  font-family: 'JetBrains Mono', 'Consolas', monospace;
1881
- font-size: 0.75rem;
1882
  color: var(--text-secondary);
1883
  word-break: break-all;
1884
- line-height: 1.3;
1885
- white-space: normal;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1886
  overflow-wrap: break-word;
 
1887
  }
1888
 
1889
  .expires-date-compact {
 
1818
  min-width: 0;
1819
  }
1820
 
1821
+ .info-column:nth-child(1) {
1822
+ flex: 1.8;
1823
+ }
1824
+
1825
  .info-column:nth-child(2) {
1826
+ flex: 1.2;
1827
  }
1828
 
1829
  .info-column:last-child {
 
1882
 
1883
  .hardware-id-compact {
1884
  font-family: 'JetBrains Mono', 'Consolas', monospace;
1885
+ font-size: 0.8rem;
1886
  color: var(--text-secondary);
1887
  word-break: break-all;
1888
+ line-height: 1.2;
1889
+ }
1890
+
1891
+ .hardware-id-full {
1892
+ font-family: 'JetBrains Mono', 'Consolas', monospace;
1893
+ font-size: 0.9rem;
1894
+ color: var(--text-primary);
1895
+ background: var(--bg-tertiary);
1896
+ padding: 8px 12px;
1897
+ border-radius: var(--radius-sm);
1898
+ border: 1px solid var(--border-primary);
1899
+ word-break: break-all;
1900
+ line-height: 1.4;
1901
+ display: block;
1902
+ margin-right: 8px;
1903
+ width: 100%;
1904
+ max-width: 400px;
1905
  overflow-wrap: break-word;
1906
+ white-space: normal;
1907
  }
1908
 
1909
  .expires-date-compact {
frontend/js/pages/users.js CHANGED
@@ -698,7 +698,7 @@ class UsersPage {
698
  <div class="detail-item">
699
  <label>授權碼</label>
700
  <div class="d-flex align-center">
701
- <code class="license-code">${Utils.formatLicenseCode(license.license_code)}</code>
702
  <button class="btn btn-sm btn-secondary ml-2" onclick="Utils.copyToClipboard('${license.license_code}')">
703
  <i class="fas fa-copy"></i>
704
  </button>
@@ -707,7 +707,12 @@ class UsersPage {
707
 
708
  <div class="detail-item">
709
  <label>硬體ID</label>
710
- <div>${Utils.formatHardwareId(license.hardware_id)}</div>
 
 
 
 
 
711
  </div>
712
 
713
  <div class="detail-item">
 
698
  <div class="detail-item">
699
  <label>授權碼</label>
700
  <div class="d-flex align-center">
701
+ <code class="license-code">${Utils.formatFullLicenseCode(license.license_code)}</code>
702
  <button class="btn btn-sm btn-secondary ml-2" onclick="Utils.copyToClipboard('${license.license_code}')">
703
  <i class="fas fa-copy"></i>
704
  </button>
 
707
 
708
  <div class="detail-item">
709
  <label>硬體ID</label>
710
+ <div class="d-flex align-center">
711
+ <code class="hardware-id-full">${license.hardware_id || '未綁定硬體'}</code>
712
+ ${license.hardware_id ? `<button class="btn btn-sm btn-secondary ml-2" onclick="Utils.copyToClipboard('${license.hardware_id}')">
713
+ <i class="fas fa-copy"></i>
714
+ </button>` : ''}
715
+ </div>
716
  </div>
717
 
718
  <div class="detail-item">
frontend/js/utils.js CHANGED
@@ -50,7 +50,21 @@ class Utils {
50
  // License code formatting
51
  static formatLicenseCode(code) {
52
  if (!code) return '';
53
- // Format as XXXX-XXXX-XXXX-XXXX
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  const cleaned = code.replace(/[^A-Z0-9]/g, '');
55
  const chunks = cleaned.match(/.{1,4}/g) || [];
56
  return chunks.join('-');
@@ -60,7 +74,7 @@ class Utils {
60
  static formatHardwareId(hardwareId) {
61
  if (!hardwareId) return '未綁定硬體';
62
  if (hardwareId.length <= 20) return hardwareId;
63
- // 顯示前8位和後8位,中間用...代替 (32位硬體指紋)
64
  return `${hardwareId.substring(0, 8)}...${hardwareId.substring(hardwareId.length - 8)}`;
65
  }
66
 
 
50
  // License code formatting
51
  static formatLicenseCode(code) {
52
  if (!code) return '';
53
+ // For display in tables, show shortened version
54
+ const cleaned = code.replace(/[^A-Z0-9]/g, '');
55
+ if (cleaned.length > 16) {
56
+ // Show first 8 and last 4 characters for long codes
57
+ return `${cleaned.substring(0, 8)}...${cleaned.substring(cleaned.length - 4)}`;
58
+ }
59
+ // For shorter codes, format normally
60
+ const chunks = cleaned.match(/.{1,4}/g) || [];
61
+ return chunks.join('-');
62
+ }
63
+
64
+ // Full license code formatting for details view
65
+ static formatFullLicenseCode(code) {
66
+ if (!code) return '';
67
+ // Format complete code as XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
68
  const cleaned = code.replace(/[^A-Z0-9]/g, '');
69
  const chunks = cleaned.match(/.{1,4}/g) || [];
70
  return chunks.join('-');
 
74
  static formatHardwareId(hardwareId) {
75
  if (!hardwareId) return '未綁定硬體';
76
  if (hardwareId.length <= 20) return hardwareId;
77
+ // 顯示前8位和後8位,中間用...代替 (32位硬體ID)
78
  return `${hardwareId.substring(0, 8)}...${hardwareId.substring(hardwareId.length - 8)}`;
79
  }
80
 
supabase-schema.sql CHANGED
@@ -248,21 +248,56 @@ COMMENT ON FUNCTION generate_license_report IS '生成指定日期範圍的授
248
 
249
  -- 插入測試用授權 (啟用來測試系統)
250
  INSERT INTO licenses (license_code, user_name, user_email, expires_at, hardware_id, is_active, activated_at) VALUES
251
- ('KS12345678', '測試用戶1', 'test1@example.com', NOW() + INTERVAL '30 days', 'HW_TEST_001', true, NOW()),
252
- ('KS87654321', '測試用戶2', 'test2@example.com', NOW() + INTERVAL '60 days', NULL, true, NULL),
253
- ('KS11111111', '過期用戶', 'expired@example.com', NOW() - INTERVAL '10 days', 'HW_EXPIRED_001', true, NOW() - INTERVAL '20 days'),
254
- ('KS99999999', '停用用戶', 'inactive@example.com', NOW() + INTERVAL '15 days', 'HW_INACTIVE_001', false, NOW() - INTERVAL '5 days'),
255
- ('KS55555555', '管理員測', 'admin@admin.com.tw', NOW() + INTERVAL '365 days', 'HW_ADMIN_001', true, NOW() - INTERVAL '1 day')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  ON CONFLICT (license_code) DO NOTHING;
257
 
258
  -- 插入測試使用記錄
259
  INSERT INTO usage_logs (license_id, action, ip_address, hardware_info) VALUES
260
- ((SELECT id FROM licenses WHERE license_code = 'KS12345678'), 'activate', '192.168.1.100', 'HW_TEST_001'),
261
- ((SELECT id FROM licenses WHERE license_code = 'KS12345678'), 'validate', '192.168.1.100', 'HW_TEST_001'),
262
- ((SELECT id FROM licenses WHERE license_code = 'KS87654321'), 'validate', '192.168.1.101', NULL),
263
- ((SELECT id FROM licenses WHERE license_code = 'KS55555555'), 'activate', '10.0.0.1', 'HW_ADMIN_001'),
264
- ((SELECT id FROM licenses WHERE license_code = 'KS55555555'), 'validate', '10.0.0.1', 'HW_ADMIN_001'),
265
- ((SELECT id FROM licenses WHERE license_code = 'KS11111111'), 'validate', '192.168.1.102', 'HW_EXPIRED_001');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
  -- =====================================================
268
  -- 權限設定 (建議)
 
248
 
249
  -- 插入測試用授權 (啟用來測試系統)
250
  INSERT INTO licenses (license_code, user_name, user_email, expires_at, hardware_id, is_active, activated_at) VALUES
251
+ -- 已啟戶 (正常使用中)
252
+ ('A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6', '王小明', 'ming@company.com.tw', NOW() + INTERVAL '90 days', 'CPU123ABC456DEF789GHI012JKL345MN', true, NOW() - INTERVAL '5 days'),
253
+ ('X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2', '李美華', 'li.meihua@design.studio', NOW() + INTERVAL '365 days', 'CPU789XYZ012ABC345DEF678GHI901JK', true, NOW() - INTERVAL '30 days'),
254
+
255
+ --用用戶 (7天試用)
256
+ ('B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4', '張工程師', 'engineer.zhang@tech.com', NOW() + INTERVAL '7 days', 'CPUABC123XYZ456QWE789RTY012UIU34', true, NOW() - INTERVAL '2 days'),
257
+
258
+ -- 未啟用授權 (已建立但未使用)
259
+ ('M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0', '陳建築師', 'chen@architect.firm', NOW() + INTERVAL '30 days', NULL, true, NULL),
260
+ ('F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8', '劉設計師', 'liu.designer@studio.tw', NOW() + INTERVAL '60 days', NULL, true, NULL),
261
+
262
+ -- 過期授權 (已啟用但過期)
263
+ ('Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5', '周老闆', 'boss.zhou@construction.co', NOW() - INTERVAL '15 days', 'CPU456DEF789ABC012GHI345JKL678MN', true, NOW() - INTERVAL '45 days'),
264
+
265
+ -- 已停用授權 (管理員手動停用)
266
+ ('Q2W3E4R5T6Y7U8I9O0P1A2S3D4F5G6H7', '黃違規用戶', 'huang@suspended.user', NOW() + INTERVAL '20 days', 'CPUXYZ789ABC012DEF345GHI678JKL90', false, NOW() - INTERVAL '10 days'),
267
+
268
+ -- 永久授權 (企業用戶)
269
+ ('K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6', 'KSTools管理員', 'admin@kstools.com.tw', NOW() + INTERVAL '3650 days', 'CPUADMIN123MASTER456CONTROL789AB', true, NOW() - INTERVAL '1 day'),
270
+
271
+ -- 即將過期 (3天內過期)
272
+ ('V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3', '趙用戶', 'zhao@expires.soon', NOW() + INTERVAL '2 days', 'CPU999END888SOON777EXPIRE666ABC', true, NOW() - INTERVAL '28 days')
273
  ON CONFLICT (license_code) DO NOTHING;
274
 
275
  -- 插入測試使用記錄
276
  INSERT INTO usage_logs (license_id, action, ip_address, hardware_info) VALUES
277
+ -- 正常用戶的使用記錄
278
+ ((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'activate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN'),
279
+ ((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'validate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN'),
280
+ ((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'validate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN'),
281
+
282
+ -- 企業用戶的使用記錄
283
+ ((SELECT id FROM licenses WHERE license_code = 'X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2'), 'activate', '10.0.1.50', 'CPU789XYZ012ABC345DEF678GHI901JK'),
284
+ ((SELECT id FROM licenses WHERE license_code = 'X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2'), 'validate', '10.0.1.50', 'CPU789XYZ012ABC345DEF678GHI901JK'),
285
+
286
+ -- 試用用戶的記錄
287
+ ((SELECT id FROM licenses WHERE license_code = 'B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4'), 'activate', '203.74.123.45', 'CPUABC123XYZ456QWE789RTY012UIU34'),
288
+ ((SELECT id FROM licenses WHERE license_code = 'B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4'), 'validate', '203.74.123.45', 'CPUABC123XYZ456QWE789RTY012UIU34'),
289
+
290
+ -- 過期用戶的歷史記錄
291
+ ((SELECT id FROM licenses WHERE license_code = 'Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5'), 'activate', '172.16.0.10', 'CPU456DEF789ABC012GHI345JKL678MN'),
292
+ ((SELECT id FROM licenses WHERE license_code = 'Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5'), 'validate', '172.16.0.10', 'CPU456DEF789ABC012GHI345JKL678MN'),
293
+
294
+ -- 管理員測試記錄
295
+ ((SELECT id FROM licenses WHERE license_code = 'K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6'), 'activate', '127.0.0.1', 'CPUADMIN123MASTER456CONTROL789AB'),
296
+ ((SELECT id FROM licenses WHERE license_code = 'K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6'), 'validate', '127.0.0.1', 'CPUADMIN123MASTER456CONTROL789AB'),
297
+
298
+ -- 即將過期用戶的記錄
299
+ ((SELECT id FROM licenses WHERE license_code = 'V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3'), 'activate', '118.163.88.99', 'CPU999END888SOON777EXPIRE666ABC'),
300
+ ((SELECT id FROM licenses WHERE license_code = 'V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3'), 'validate', '118.163.88.99', 'CPU999END888SOON777EXPIRE666ABC');
301
 
302
  -- =====================================================
303
  -- 權限設定 (建議)