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 +5 -6
- frontend/css/style.css +24 -4
- frontend/js/pages/users.js +7 -2
- frontend/js/utils.js +16 -2
- supabase-schema.sql +46 -11
|
@@ -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 |
-
# 生成
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 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 |
"""建立新授權"""
|
|
@@ -1818,8 +1818,12 @@ body {
|
|
| 1818 |
min-width: 0;
|
| 1819 |
}
|
| 1820 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1821 |
.info-column:nth-child(2) {
|
| 1822 |
-
flex: 1.
|
| 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.
|
| 1882 |
color: var(--text-secondary);
|
| 1883 |
word-break: break-all;
|
| 1884 |
-
line-height: 1.
|
| 1885 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
@@ -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.
|
| 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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
|
@@ -50,7 +50,21 @@ class Utils {
|
|
| 50 |
// License code formatting
|
| 51 |
static formatLicenseCode(code) {
|
| 52 |
if (!code) return '';
|
| 53 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|
|
@@ -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 |
-
|
| 252 |
-
('
|
| 253 |
-
('
|
| 254 |
-
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
ON CONFLICT (license_code) DO NOTHING;
|
| 257 |
|
| 258 |
-- 插入測試使用記錄
|
| 259 |
INSERT INTO usage_logs (license_id, action, ip_address, hardware_info) VALUES
|
| 260 |
-
|
| 261 |
-
((SELECT id FROM licenses WHERE license_code = '
|
| 262 |
-
((SELECT id FROM licenses WHERE license_code = '
|
| 263 |
-
((SELECT id FROM licenses WHERE license_code = '
|
| 264 |
-
|
| 265 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-- 權限設定 (建議)
|