重新實施機器名稱綁定功能
Browse files- 從commit 31441f0恢復完整機器名稱功能
- 資料庫新增machine_name欄位到licenses和usage_logs表
- C# plugin自動擷取並傳送Environment.MachineName
- 後端API完整支援機器名稱儲存和更新邏輯
- 前端UI顯示機器名稱於硬體ID前方(5列布局)
- 授權記錄頁面移除敏感授權碼,新增機器名稱列
- 儀表板最近活動顯示機器名稱資訊
- 開發模式包含完整機器名稱模擬資料
- 保持強制sessionStorage認證機制
- app/api/license.py +4 -2
- app/models/license.py +4 -0
- app/services/license_service.py +15 -7
- frontend/css/style.css +40 -2
- frontend/js/api.js +77 -9
- frontend/js/pages/dashboard.js +7 -1
- frontend/js/pages/logs.js +6 -15
- frontend/js/pages/users.js +11 -1
- revit-plugin/ApiClient.cs +4 -2
- supabase-schema.sql +36 -29
app/api/license.py
CHANGED
|
@@ -33,7 +33,8 @@ async def activate_license(
|
|
| 33 |
```json
|
| 34 |
{
|
| 35 |
"license_code": "KS12345678",
|
| 36 |
-
"hardware_id": "HW_ABC123456789"
|
|
|
|
| 37 |
}
|
| 38 |
```
|
| 39 |
|
|
@@ -63,7 +64,8 @@ async def validate_license(
|
|
| 63 |
```json
|
| 64 |
{
|
| 65 |
"license_code": "KS12345678",
|
| 66 |
-
"hardware_id": "HW_ABC123456789"
|
|
|
|
| 67 |
}
|
| 68 |
```
|
| 69 |
|
|
|
|
| 33 |
```json
|
| 34 |
{
|
| 35 |
"license_code": "KS12345678",
|
| 36 |
+
"hardware_id": "HW_ABC123456789",
|
| 37 |
+
"machine_name": "DESKTOP-DEV01"
|
| 38 |
}
|
| 39 |
```
|
| 40 |
|
|
|
|
| 64 |
```json
|
| 65 |
{
|
| 66 |
"license_code": "KS12345678",
|
| 67 |
+
"hardware_id": "HW_ABC123456789",
|
| 68 |
+
"machine_name": "DESKTOP-DEV01"
|
| 69 |
}
|
| 70 |
```
|
| 71 |
|
app/models/license.py
CHANGED
|
@@ -13,11 +13,13 @@ class LicenseActivation(BaseModel):
|
|
| 13 |
"""授權啟用請求"""
|
| 14 |
license_code: str
|
| 15 |
hardware_id: str
|
|
|
|
| 16 |
|
| 17 |
class LicenseValidation(BaseModel):
|
| 18 |
"""授權驗證請求"""
|
| 19 |
license_code: str
|
| 20 |
hardware_id: str
|
|
|
|
| 21 |
|
| 22 |
class LicenseCreate(BaseModel):
|
| 23 |
"""建立授權請求"""
|
|
@@ -62,6 +64,7 @@ class License(BaseModel):
|
|
| 62 |
id: UUID
|
| 63 |
license_code: str
|
| 64 |
hardware_id: Optional[str] = None
|
|
|
|
| 65 |
user_name: str
|
| 66 |
user_email: Optional[str] = None
|
| 67 |
expires_at: datetime
|
|
@@ -79,6 +82,7 @@ class UsageLog(BaseModel):
|
|
| 79 |
ip_address: Optional[str] = None
|
| 80 |
user_agent: Optional[str] = None
|
| 81 |
hardware_info: Optional[str] = None
|
|
|
|
| 82 |
error_message: Optional[str] = None
|
| 83 |
created_at: datetime
|
| 84 |
|
|
|
|
| 13 |
"""授權啟用請求"""
|
| 14 |
license_code: str
|
| 15 |
hardware_id: str
|
| 16 |
+
machine_name: Optional[str] = None
|
| 17 |
|
| 18 |
class LicenseValidation(BaseModel):
|
| 19 |
"""授權驗證請求"""
|
| 20 |
license_code: str
|
| 21 |
hardware_id: str
|
| 22 |
+
machine_name: Optional[str] = None
|
| 23 |
|
| 24 |
class LicenseCreate(BaseModel):
|
| 25 |
"""建立授權請求"""
|
|
|
|
| 64 |
id: UUID
|
| 65 |
license_code: str
|
| 66 |
hardware_id: Optional[str] = None
|
| 67 |
+
machine_name: Optional[str] = None
|
| 68 |
user_name: str
|
| 69 |
user_email: Optional[str] = None
|
| 70 |
expires_at: datetime
|
|
|
|
| 82 |
ip_address: Optional[str] = None
|
| 83 |
user_agent: Optional[str] = None
|
| 84 |
hardware_info: Optional[str] = None
|
| 85 |
+
machine_name: Optional[str] = None
|
| 86 |
error_message: Optional[str] = None
|
| 87 |
created_at: datetime
|
| 88 |
|
app/services/license_service.py
CHANGED
|
@@ -131,9 +131,10 @@ class LicenseService:
|
|
| 131 |
message="此授權已綁定其他設備"
|
| 132 |
)
|
| 133 |
|
| 134 |
-
# 更新授權 (綁定硬體ID和啟用時間)
|
| 135 |
update_data = {
|
| 136 |
"hardware_id": activation.hardware_id,
|
|
|
|
| 137 |
"activated_at": datetime.now(timezone.utc).isoformat(),
|
| 138 |
"last_used_at": datetime.now(timezone.utc).isoformat()
|
| 139 |
}
|
|
@@ -141,7 +142,7 @@ class LicenseService:
|
|
| 141 |
self.supabase.table("licenses").update(update_data).eq("id", license_id).execute()
|
| 142 |
|
| 143 |
# 記錄使用日誌
|
| 144 |
-
await self._log_usage(license_id, "activate", client_ip, activation.hardware_id)
|
| 145 |
|
| 146 |
return ActivationResponse(
|
| 147 |
success=True,
|
|
@@ -194,13 +195,18 @@ class LicenseService:
|
|
| 194 |
message="授權已過期"
|
| 195 |
)
|
| 196 |
|
| 197 |
-
# 更新最後使用時間
|
| 198 |
-
|
| 199 |
"last_used_at": datetime.now(timezone.utc).isoformat()
|
| 200 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
|
| 202 |
# 記錄使用日誌
|
| 203 |
-
await self._log_usage(license_id, "validate", client_ip, validation.hardware_id)
|
| 204 |
|
| 205 |
return ValidationResponse(
|
| 206 |
valid=True,
|
|
@@ -465,7 +471,8 @@ class LicenseService:
|
|
| 465 |
return {"success": False, "message": f"延長失敗: {str(e)}"}
|
| 466 |
|
| 467 |
async def _log_usage(self, license_id: Optional[str], action: str, ip_address: Optional[str] = None,
|
| 468 |
-
hardware_info: Optional[str] = None,
|
|
|
|
| 469 |
"""記錄使用日誌"""
|
| 470 |
try:
|
| 471 |
log_data = {
|
|
@@ -473,6 +480,7 @@ class LicenseService:
|
|
| 473 |
"action": action,
|
| 474 |
"ip_address": ip_address,
|
| 475 |
"hardware_info": hardware_info,
|
|
|
|
| 476 |
"error_message": error_message
|
| 477 |
}
|
| 478 |
|
|
|
|
| 131 |
message="此授權已綁定其他設備"
|
| 132 |
)
|
| 133 |
|
| 134 |
+
# 更新授權 (綁定硬體ID、機器名稱和啟用時間)
|
| 135 |
update_data = {
|
| 136 |
"hardware_id": activation.hardware_id,
|
| 137 |
+
"machine_name": activation.machine_name,
|
| 138 |
"activated_at": datetime.now(timezone.utc).isoformat(),
|
| 139 |
"last_used_at": datetime.now(timezone.utc).isoformat()
|
| 140 |
}
|
|
|
|
| 142 |
self.supabase.table("licenses").update(update_data).eq("id", license_id).execute()
|
| 143 |
|
| 144 |
# 記錄使用日誌
|
| 145 |
+
await self._log_usage(license_id, "activate", client_ip, activation.hardware_id, activation.machine_name)
|
| 146 |
|
| 147 |
return ActivationResponse(
|
| 148 |
success=True,
|
|
|
|
| 195 |
message="授權已過期"
|
| 196 |
)
|
| 197 |
|
| 198 |
+
# 更新最後使用時間和機器名稱
|
| 199 |
+
update_data = {
|
| 200 |
"last_used_at": datetime.now(timezone.utc).isoformat()
|
| 201 |
+
}
|
| 202 |
+
# 如果提供了機器名稱,也一併更新
|
| 203 |
+
if validation.machine_name:
|
| 204 |
+
update_data["machine_name"] = validation.machine_name
|
| 205 |
+
|
| 206 |
+
self.supabase.table("licenses").update(update_data).eq("id", license_id).execute()
|
| 207 |
|
| 208 |
# 記錄使用日誌
|
| 209 |
+
await self._log_usage(license_id, "validate", client_ip, validation.hardware_id, validation.machine_name)
|
| 210 |
|
| 211 |
return ValidationResponse(
|
| 212 |
valid=True,
|
|
|
|
| 471 |
return {"success": False, "message": f"延長失敗: {str(e)}"}
|
| 472 |
|
| 473 |
async def _log_usage(self, license_id: Optional[str], action: str, ip_address: Optional[str] = None,
|
| 474 |
+
hardware_info: Optional[str] = None, machine_name: Optional[str] = None,
|
| 475 |
+
error_message: Optional[str] = None):
|
| 476 |
"""記錄使用日誌"""
|
| 477 |
try:
|
| 478 |
log_data = {
|
|
|
|
| 480 |
"action": action,
|
| 481 |
"ip_address": ip_address,
|
| 482 |
"hardware_info": hardware_info,
|
| 483 |
+
"machine_name": machine_name,
|
| 484 |
"error_message": error_message
|
| 485 |
}
|
| 486 |
|
frontend/css/style.css
CHANGED
|
@@ -1880,11 +1880,23 @@ body {
|
|
| 1880 |
}
|
| 1881 |
|
| 1882 |
.info-column:nth-child(1) {
|
| 1883 |
-
flex: 1.
|
| 1884 |
}
|
| 1885 |
|
| 1886 |
.info-column:nth-child(2) {
|
| 1887 |
-
flex: 1.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1888 |
}
|
| 1889 |
|
| 1890 |
.info-column:last-child {
|
|
@@ -1998,6 +2010,32 @@ body {
|
|
| 1998 |
text-overflow: ellipsis;
|
| 1999 |
}
|
| 2000 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2001 |
/* XS Buttons for Horizontal Cards */
|
| 2002 |
.btn-xs {
|
| 2003 |
padding: 3px 6px;
|
|
|
|
| 1880 |
}
|
| 1881 |
|
| 1882 |
.info-column:nth-child(1) {
|
| 1883 |
+
flex: 1.5; /* 授權碼 */
|
| 1884 |
}
|
| 1885 |
|
| 1886 |
.info-column:nth-child(2) {
|
| 1887 |
+
flex: 1.5; /* 機器名稱 */
|
| 1888 |
+
}
|
| 1889 |
+
|
| 1890 |
+
.info-column:nth-child(3) {
|
| 1891 |
+
flex: 1.5; /* 硬體ID - 較寬 */
|
| 1892 |
+
}
|
| 1893 |
+
|
| 1894 |
+
.info-column:nth-child(4) {
|
| 1895 |
+
flex: 1.3; /* 到期時間 */
|
| 1896 |
+
}
|
| 1897 |
+
|
| 1898 |
+
.info-column:nth-child(5) {
|
| 1899 |
+
flex: 1.0; /* 狀態 */
|
| 1900 |
}
|
| 1901 |
|
| 1902 |
.info-column:last-child {
|
|
|
|
| 2010 |
text-overflow: ellipsis;
|
| 2011 |
}
|
| 2012 |
|
| 2013 |
+
/* Machine Name Styles */
|
| 2014 |
+
.machine-name {
|
| 2015 |
+
color: var(--text-secondary);
|
| 2016 |
+
font-size: 0.875rem;
|
| 2017 |
+
margin-top: 0.25rem;
|
| 2018 |
+
}
|
| 2019 |
+
|
| 2020 |
+
.machine-name i {
|
| 2021 |
+
margin-right: 0.5rem;
|
| 2022 |
+
color: var(--accent-blue);
|
| 2023 |
+
}
|
| 2024 |
+
|
| 2025 |
+
.machine-name-compact {
|
| 2026 |
+
display: inline-flex;
|
| 2027 |
+
align-items: center;
|
| 2028 |
+
font-size: 0.85rem;
|
| 2029 |
+
color: var(--text-secondary);
|
| 2030 |
+
gap: 4px;
|
| 2031 |
+
}
|
| 2032 |
+
|
| 2033 |
+
.machine-name-compact i {
|
| 2034 |
+
font-size: 0.75rem;
|
| 2035 |
+
color: var(--accent-blue);
|
| 2036 |
+
flex-shrink: 0;
|
| 2037 |
+
}
|
| 2038 |
+
|
| 2039 |
/* XS Buttons for Horizontal Cards */
|
| 2040 |
.btn-xs {
|
| 2041 |
padding: 3px 6px;
|
frontend/js/api.js
CHANGED
|
@@ -21,6 +21,7 @@ class ApiClient {
|
|
| 21 |
id: 'lic_001',
|
| 22 |
user_name: '張三工程師',
|
| 23 |
user_email: 'zhang.san@company.com',
|
|
|
|
| 24 |
hardware_id: '4A1F2E3D5B6C7A8E9F0A1B2C3D4E5F6G',
|
| 25 |
license_key: 'KSTOOLS-2024-ABCD-EFGH-IJKL-MNOP',
|
| 26 |
valid_days: 365,
|
|
@@ -33,6 +34,7 @@ class ApiClient {
|
|
| 33 |
id: 'lic_002',
|
| 34 |
user_name: '李四設計師',
|
| 35 |
user_email: 'li.si@company.com',
|
|
|
|
| 36 |
hardware_id: '7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V',
|
| 37 |
license_key: 'KSTOOLS-2024-WXYZ-1234-5678-9ABC',
|
| 38 |
valid_days: 180,
|
|
@@ -45,6 +47,7 @@ class ApiClient {
|
|
| 45 |
id: 'lic_003',
|
| 46 |
user_name: '王五主管',
|
| 47 |
user_email: 'wang.wu@company.com',
|
|
|
|
| 48 |
hardware_id: '3V4W5X6Y7Z8A9B0C1D2E3F4G5H6I7J8K',
|
| 49 |
license_key: 'KSTOOLS-2024-DEFG-HIJK-LMNO-PQRS',
|
| 50 |
valid_days: 90,
|
|
@@ -57,6 +60,7 @@ class ApiClient {
|
|
| 57 |
id: 'lic_004',
|
| 58 |
user_name: '趙六實習生',
|
| 59 |
user_email: 'zhao.liu@company.com',
|
|
|
|
| 60 |
hardware_id: '9K0L1M2N3O4P5Q6R7S8T9U0V1W2X3Y4Z',
|
| 61 |
license_key: 'KSTOOLS-2024-TUVW-XYZ1-2345-6789',
|
| 62 |
valid_days: 30,
|
|
@@ -69,6 +73,7 @@ class ApiClient {
|
|
| 69 |
id: 'lic_005',
|
| 70 |
user_name: '陳七顧問',
|
| 71 |
user_email: 'chen.qi@consultant.com',
|
|
|
|
| 72 |
hardware_id: '5Z6A7B8C9D0E1F2G3H4I5J6K7L8M9N0O',
|
| 73 |
license_key: 'KSTOOLS-2024-ABCD-1234-WXYZ-5678',
|
| 74 |
valid_days: 365,
|
|
@@ -83,7 +88,12 @@ class ApiClient {
|
|
| 83 |
total_licenses: 5,
|
| 84 |
active_licenses: 4,
|
| 85 |
inactive_licenses: 1,
|
|
|
|
| 86 |
expiring_soon: 1,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
total_users: 5
|
| 88 |
};
|
| 89 |
|
|
@@ -91,32 +101,90 @@ class ApiClient {
|
|
| 91 |
{
|
| 92 |
id: 'log_001',
|
| 93 |
license_id: 'lic_001',
|
| 94 |
-
action: '
|
| 95 |
user_name: '張三工程師',
|
|
|
|
| 96 |
hardware_id: '4A1F2E3D5B6C7A8E9F0A1B2C3D4E5F6G',
|
| 97 |
-
|
| 98 |
ip_address: '192.168.1.100',
|
| 99 |
-
|
| 100 |
},
|
| 101 |
{
|
| 102 |
id: 'log_002',
|
| 103 |
license_id: 'lic_002',
|
| 104 |
-
action: '
|
| 105 |
user_name: '李四設計師',
|
|
|
|
| 106 |
hardware_id: '7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V',
|
| 107 |
-
|
| 108 |
ip_address: '192.168.1.101',
|
| 109 |
-
|
| 110 |
},
|
| 111 |
{
|
| 112 |
id: 'log_003',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
license_id: 'lic_003',
|
| 114 |
-
action: '
|
| 115 |
user_name: '王五主管',
|
|
|
|
| 116 |
hardware_id: '3V4W5X6Y7Z8A9B0C1D2E3F4G5H6I7J8K',
|
| 117 |
-
|
| 118 |
ip_address: '192.168.1.102',
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
}
|
| 121 |
];
|
| 122 |
}
|
|
|
|
| 21 |
id: 'lic_001',
|
| 22 |
user_name: '張三工程師',
|
| 23 |
user_email: 'zhang.san@company.com',
|
| 24 |
+
machine_name: 'DESKTOP-DEV01',
|
| 25 |
hardware_id: '4A1F2E3D5B6C7A8E9F0A1B2C3D4E5F6G',
|
| 26 |
license_key: 'KSTOOLS-2024-ABCD-EFGH-IJKL-MNOP',
|
| 27 |
valid_days: 365,
|
|
|
|
| 34 |
id: 'lic_002',
|
| 35 |
user_name: '李四設計師',
|
| 36 |
user_email: 'li.si@company.com',
|
| 37 |
+
machine_name: 'BIM-WORKSTATION-02',
|
| 38 |
hardware_id: '7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V',
|
| 39 |
license_key: 'KSTOOLS-2024-WXYZ-1234-5678-9ABC',
|
| 40 |
valid_days: 180,
|
|
|
|
| 47 |
id: 'lic_003',
|
| 48 |
user_name: '王五主管',
|
| 49 |
user_email: 'wang.wu@company.com',
|
| 50 |
+
machine_name: 'MANAGER-LAPTOP',
|
| 51 |
hardware_id: '3V4W5X6Y7Z8A9B0C1D2E3F4G5H6I7J8K',
|
| 52 |
license_key: 'KSTOOLS-2024-DEFG-HIJK-LMNO-PQRS',
|
| 53 |
valid_days: 90,
|
|
|
|
| 60 |
id: 'lic_004',
|
| 61 |
user_name: '趙六實習生',
|
| 62 |
user_email: 'zhao.liu@company.com',
|
| 63 |
+
machine_name: 'INTERN-PC-01',
|
| 64 |
hardware_id: '9K0L1M2N3O4P5Q6R7S8T9U0V1W2X3Y4Z',
|
| 65 |
license_key: 'KSTOOLS-2024-TUVW-XYZ1-2345-6789',
|
| 66 |
valid_days: 30,
|
|
|
|
| 73 |
id: 'lic_005',
|
| 74 |
user_name: '陳七顧問',
|
| 75 |
user_email: 'chen.qi@consultant.com',
|
| 76 |
+
machine_name: 'CONSULTANT-SURFACE',
|
| 77 |
hardware_id: '5Z6A7B8C9D0E1F2G3H4I5J6K7L8M9N0O',
|
| 78 |
license_key: 'KSTOOLS-2024-ABCD-1234-WXYZ-5678',
|
| 79 |
valid_days: 365,
|
|
|
|
| 88 |
total_licenses: 5,
|
| 89 |
active_licenses: 4,
|
| 90 |
inactive_licenses: 1,
|
| 91 |
+
expired_licenses: 0,
|
| 92 |
expiring_soon: 1,
|
| 93 |
+
today_activations: 2,
|
| 94 |
+
yesterday_activations: 1,
|
| 95 |
+
this_week_activations: 8,
|
| 96 |
+
this_month_activations: 12,
|
| 97 |
total_users: 5
|
| 98 |
};
|
| 99 |
|
|
|
|
| 101 |
{
|
| 102 |
id: 'log_001',
|
| 103 |
license_id: 'lic_001',
|
| 104 |
+
action: 'activate',
|
| 105 |
user_name: '張三工程師',
|
| 106 |
+
machine_name: 'DESKTOP-DEV01',
|
| 107 |
hardware_id: '4A1F2E3D5B6C7A8E9F0A1B2C3D4E5F6G',
|
| 108 |
+
created_at: new Date(Date.now() - 5 * 60000).toISOString(), // 5分鐘前
|
| 109 |
ip_address: '192.168.1.100',
|
| 110 |
+
error_message: null
|
| 111 |
},
|
| 112 |
{
|
| 113 |
id: 'log_002',
|
| 114 |
license_id: 'lic_002',
|
| 115 |
+
action: 'validate',
|
| 116 |
user_name: '李四設計師',
|
| 117 |
+
machine_name: 'BIM-WORKSTATION-02',
|
| 118 |
hardware_id: '7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V',
|
| 119 |
+
created_at: new Date(Date.now() - 15 * 60000).toISOString(), // 15分鐘前
|
| 120 |
ip_address: '192.168.1.101',
|
| 121 |
+
error_message: null
|
| 122 |
},
|
| 123 |
{
|
| 124 |
id: 'log_003',
|
| 125 |
+
license_id: 'lic_005',
|
| 126 |
+
action: 'activate',
|
| 127 |
+
user_name: '陳七顧問',
|
| 128 |
+
machine_name: 'CONSULTANT-SURFACE',
|
| 129 |
+
hardware_id: '5Z6A7B8C9D0E1F2G3H4I5J6K7L8M9N0O',
|
| 130 |
+
created_at: new Date(Date.now() - 30 * 60000).toISOString(), // 30分鐘前
|
| 131 |
+
ip_address: '192.168.1.105',
|
| 132 |
+
error_message: null
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
id: 'log_004',
|
| 136 |
+
license_id: 'lic_004',
|
| 137 |
+
action: 'validate',
|
| 138 |
+
user_name: '趙六實習生',
|
| 139 |
+
machine_name: 'INTERN-PC-01',
|
| 140 |
+
hardware_id: '9K0L1M2N3O4P5Q6R7S8T9U0V1W2X3Y4Z',
|
| 141 |
+
created_at: new Date(Date.now() - 45 * 60000).toISOString(), // 45分鐘前
|
| 142 |
+
ip_address: '192.168.1.103',
|
| 143 |
+
error_message: '授權即將到期,請聯繫管理員續期'
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
id: 'log_005',
|
| 147 |
license_id: 'lic_003',
|
| 148 |
+
action: 'deactivate_license',
|
| 149 |
user_name: '王五主管',
|
| 150 |
+
machine_name: 'MANAGER-LAPTOP',
|
| 151 |
hardware_id: '3V4W5X6Y7Z8A9B0C1D2E3F4G5H6I7J8K',
|
| 152 |
+
created_at: new Date(Date.now() - 2 * 60 * 60000).toISOString(), // 2小時前
|
| 153 |
ip_address: '192.168.1.102',
|
| 154 |
+
error_message: null
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
id: 'log_006',
|
| 158 |
+
license_id: 'lic_001',
|
| 159 |
+
action: 'activate',
|
| 160 |
+
user_name: '張三工程師',
|
| 161 |
+
machine_name: 'DESKTOP-DEV01',
|
| 162 |
+
hardware_id: '4A1F2E3D5B6C7A8E9F0A1B2C3D4E5F6G',
|
| 163 |
+
created_at: new Date(Date.now() - 24 * 60 * 60000).toISOString(), // 1天前
|
| 164 |
+
ip_address: '192.168.1.100',
|
| 165 |
+
error_message: null
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
id: 'log_007',
|
| 169 |
+
license_id: 'lic_006',
|
| 170 |
+
action: 'create',
|
| 171 |
+
user_name: '新用戶測試',
|
| 172 |
+
machine_name: null,
|
| 173 |
+
hardware_id: null,
|
| 174 |
+
created_at: new Date(Date.now() - 3 * 24 * 60 * 60000).toISOString(), // 3天前
|
| 175 |
+
ip_address: '192.168.1.10',
|
| 176 |
+
error_message: null
|
| 177 |
+
},
|
| 178 |
+
{
|
| 179 |
+
id: 'log_008',
|
| 180 |
+
license_id: 'lic_002',
|
| 181 |
+
action: 'extend',
|
| 182 |
+
user_name: '李四設計師',
|
| 183 |
+
machine_name: 'BIM-WORKSTATION-02',
|
| 184 |
+
hardware_id: '7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V',
|
| 185 |
+
created_at: new Date(Date.now() - 5 * 24 * 60 * 60000).toISOString(), // 5天前
|
| 186 |
+
ip_address: '192.168.1.10',
|
| 187 |
+
error_message: null
|
| 188 |
}
|
| 189 |
];
|
| 190 |
}
|
frontend/js/pages/dashboard.js
CHANGED
|
@@ -340,7 +340,7 @@ class DashboardPage {
|
|
| 340 |
activityArray = activities.data;
|
| 341 |
} else if (activities && typeof activities === 'object') {
|
| 342 |
// 如果是對象,嘗試提取可能的陣列屬性
|
| 343 |
-
const possibleArrays = ['activities', 'data', 'items', 'records'];
|
| 344 |
for (const key of possibleArrays) {
|
| 345 |
if (Array.isArray(activities[key])) {
|
| 346 |
activityArray = activities[key];
|
|
@@ -364,6 +364,7 @@ class DashboardPage {
|
|
| 364 |
const statusIcon = this.getActivityStatusIcon(activity);
|
| 365 |
const actionText = this.getActionText(activity.action);
|
| 366 |
const userName = activity.user_name || activity.licenses?.user_name || '未知用戶';
|
|
|
|
| 367 |
const time = Utils.getRelativeTime(activity.created_at);
|
| 368 |
const ipAddress = activity.ip_address || '未知';
|
| 369 |
|
|
@@ -377,6 +378,11 @@ class DashboardPage {
|
|
| 377 |
displayText += ` - ${userName}`;
|
| 378 |
}
|
| 379 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
displayText += ` (IP: ${ipAddress}`;
|
| 381 |
|
| 382 |
// 如果有錯誤訊息,放在IP後面
|
|
|
|
| 340 |
activityArray = activities.data;
|
| 341 |
} else if (activities && typeof activities === 'object') {
|
| 342 |
// 如果是對象,嘗試提取可能的陣列屬性
|
| 343 |
+
const possibleArrays = ['logs', 'activities', 'data', 'items', 'records'];
|
| 344 |
for (const key of possibleArrays) {
|
| 345 |
if (Array.isArray(activities[key])) {
|
| 346 |
activityArray = activities[key];
|
|
|
|
| 364 |
const statusIcon = this.getActivityStatusIcon(activity);
|
| 365 |
const actionText = this.getActionText(activity.action);
|
| 366 |
const userName = activity.user_name || activity.licenses?.user_name || '未知用戶';
|
| 367 |
+
const machineName = activity.machine_name || activity.licenses?.machine_name || '';
|
| 368 |
const time = Utils.getRelativeTime(activity.created_at);
|
| 369 |
const ipAddress = activity.ip_address || '未知';
|
| 370 |
|
|
|
|
| 378 |
displayText += ` - ${userName}`;
|
| 379 |
}
|
| 380 |
|
| 381 |
+
// 加入機器名稱
|
| 382 |
+
if (machineName) {
|
| 383 |
+
displayText += ` (機器: ${machineName})`;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
displayText += ` (IP: ${ipAddress}`;
|
| 387 |
|
| 388 |
// 如果有錯誤訊息,放在IP後面
|
frontend/js/pages/logs.js
CHANGED
|
@@ -244,7 +244,7 @@ class LogsPage {
|
|
| 244 |
<th>時間</th>
|
| 245 |
<th>動作</th>
|
| 246 |
<th>用戶</th>
|
| 247 |
-
<th>
|
| 248 |
<th>IP地址</th>
|
| 249 |
<th>狀態</th>
|
| 250 |
<th>詳情</th>
|
|
@@ -268,7 +268,7 @@ class LogsPage {
|
|
| 268 |
|
| 269 |
const actionText = this.getActionText(log.action);
|
| 270 |
const userName = log.user_name || log.licenses?.user_name || '未知用戶';
|
| 271 |
-
const
|
| 272 |
const time = Utils.formatDateTime(log.created_at);
|
| 273 |
const ip = log.ip_address || '未知';
|
| 274 |
|
|
@@ -283,10 +283,10 @@ class LogsPage {
|
|
| 283 |
</td>
|
| 284 |
<td>${userName}</td>
|
| 285 |
<td>
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
</td>
|
| 291 |
<td>${ip}</td>
|
| 292 |
<td>${statusBadge}</td>
|
|
@@ -436,15 +436,6 @@ class LogsPage {
|
|
| 436 |
<label>IP地址</label>
|
| 437 |
<div>${log.ip_address || '未知'}</div>
|
| 438 |
</div>
|
| 439 |
-
<div class="detail-item">
|
| 440 |
-
<label>授權碼</label>
|
| 441 |
-
<div>
|
| 442 |
-
${log.license_code || log.licenses?.license_code
|
| 443 |
-
? `<code>${Utils.formatLicenseCode(log.license_code || log.licenses?.license_code)}</code>`
|
| 444 |
-
: 'N/A'
|
| 445 |
-
}
|
| 446 |
-
</div>
|
| 447 |
-
</div>
|
| 448 |
<div class="detail-item">
|
| 449 |
<label>動作類型</label>
|
| 450 |
<div>
|
|
|
|
| 244 |
<th>時間</th>
|
| 245 |
<th>動作</th>
|
| 246 |
<th>用戶</th>
|
| 247 |
+
<th>機器名稱</th>
|
| 248 |
<th>IP地址</th>
|
| 249 |
<th>狀態</th>
|
| 250 |
<th>詳情</th>
|
|
|
|
| 268 |
|
| 269 |
const actionText = this.getActionText(log.action);
|
| 270 |
const userName = log.user_name || log.licenses?.user_name || '未知用戶';
|
| 271 |
+
const machineName = log.machine_name || '未知';
|
| 272 |
const time = Utils.formatDateTime(log.created_at);
|
| 273 |
const ip = log.ip_address || '未知';
|
| 274 |
|
|
|
|
| 283 |
</td>
|
| 284 |
<td>${userName}</td>
|
| 285 |
<td>
|
| 286 |
+
<span class="machine-name-compact">
|
| 287 |
+
<i class="fas fa-desktop"></i>
|
| 288 |
+
${machineName}
|
| 289 |
+
</span>
|
| 290 |
</td>
|
| 291 |
<td>${ip}</td>
|
| 292 |
<td>${statusBadge}</td>
|
|
|
|
| 436 |
<label>IP地址</label>
|
| 437 |
<div>${log.ip_address || '未知'}</div>
|
| 438 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
<div class="detail-item">
|
| 440 |
<label>動作類型</label>
|
| 441 |
<div>
|
frontend/js/pages/users.js
CHANGED
|
@@ -319,7 +319,7 @@ class UsersPage {
|
|
| 319 |
</div>
|
| 320 |
</div>
|
| 321 |
|
| 322 |
-
<!-- Main Content -
|
| 323 |
<div class="card-body-horizontal">
|
| 324 |
<div class="info-column">
|
| 325 |
<div class="column-header">授權碼</div>
|
|
@@ -331,6 +331,16 @@ class UsersPage {
|
|
| 331 |
</div>
|
| 332 |
</div>
|
| 333 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
<div class="info-column">
|
| 335 |
<div class="column-header">硬體ID</div>
|
| 336 |
<div class="column-content">
|
|
|
|
| 319 |
</div>
|
| 320 |
</div>
|
| 321 |
|
| 322 |
+
<!-- Main Content - 5 Equal Columns -->
|
| 323 |
<div class="card-body-horizontal">
|
| 324 |
<div class="info-column">
|
| 325 |
<div class="column-header">授權碼</div>
|
|
|
|
| 331 |
</div>
|
| 332 |
</div>
|
| 333 |
|
| 334 |
+
<div class="info-column">
|
| 335 |
+
<div class="column-header">機器名稱</div>
|
| 336 |
+
<div class="column-content">
|
| 337 |
+
<span class="machine-name-compact">
|
| 338 |
+
<i class="fas fa-desktop"></i>
|
| 339 |
+
${license.machine_name || '未綁定'}
|
| 340 |
+
</span>
|
| 341 |
+
</div>
|
| 342 |
+
</div>
|
| 343 |
+
|
| 344 |
<div class="info-column">
|
| 345 |
<div class="column-header">硬體ID</div>
|
| 346 |
<div class="column-content">
|
revit-plugin/ApiClient.cs
CHANGED
|
@@ -58,7 +58,8 @@ namespace KSTools.Licensing
|
|
| 58 |
var request = new
|
| 59 |
{
|
| 60 |
license_code = licenseCode,
|
| 61 |
-
hardware_id = hardwareId
|
|
|
|
| 62 |
};
|
| 63 |
|
| 64 |
var json = JsonSerializer.Serialize(request, new JsonSerializerOptions
|
|
@@ -149,7 +150,8 @@ namespace KSTools.Licensing
|
|
| 149 |
var request = new
|
| 150 |
{
|
| 151 |
license_code = licenseCode,
|
| 152 |
-
hardware_id = hardwareId
|
|
|
|
| 153 |
};
|
| 154 |
|
| 155 |
var json = JsonSerializer.Serialize(request, new JsonSerializerOptions
|
|
|
|
| 58 |
var request = new
|
| 59 |
{
|
| 60 |
license_code = licenseCode,
|
| 61 |
+
hardware_id = hardwareId,
|
| 62 |
+
machine_name = Environment.MachineName
|
| 63 |
};
|
| 64 |
|
| 65 |
var json = JsonSerializer.Serialize(request, new JsonSerializerOptions
|
|
|
|
| 150 |
var request = new
|
| 151 |
{
|
| 152 |
license_code = licenseCode,
|
| 153 |
+
hardware_id = hardwareId,
|
| 154 |
+
machine_name = Environment.MachineName
|
| 155 |
};
|
| 156 |
|
| 157 |
var json = JsonSerializer.Serialize(request, new JsonSerializerOptions
|
supabase-schema.sql
CHANGED
|
@@ -13,6 +13,7 @@ CREATE TABLE licenses (
|
|
| 13 |
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
| 14 |
license_code VARCHAR(50) UNIQUE NOT NULL,
|
| 15 |
hardware_id VARCHAR(100),
|
|
|
|
| 16 |
user_name VARCHAR(255) NOT NULL,
|
| 17 |
user_email VARCHAR(255),
|
| 18 |
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
@@ -28,6 +29,7 @@ COMMENT ON TABLE licenses IS 'KSTools 授權管理表';
|
|
| 28 |
COMMENT ON COLUMN licenses.id IS '唯一識別碼';
|
| 29 |
COMMENT ON COLUMN licenses.license_code IS '授權碼 (格式: 32位隨機字符,支援XXXX-XXXX-XXXX-XXXX分段顯示)';
|
| 30 |
COMMENT ON COLUMN licenses.hardware_id IS '硬體指紋ID';
|
|
|
|
| 31 |
COMMENT ON COLUMN licenses.user_name IS '授權用戶名稱';
|
| 32 |
COMMENT ON COLUMN licenses.user_email IS '用戶電子郵件';
|
| 33 |
COMMENT ON COLUMN licenses.expires_at IS '授權到期時間';
|
|
@@ -45,6 +47,7 @@ CREATE TABLE usage_logs (
|
|
| 45 |
ip_address INET,
|
| 46 |
user_agent TEXT,
|
| 47 |
hardware_info TEXT,
|
|
|
|
| 48 |
error_message TEXT, -- 錯誤訊息 (如果有)
|
| 49 |
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 50 |
);
|
|
@@ -56,6 +59,7 @@ COMMENT ON COLUMN usage_logs.action IS '動作類型: activate/validate/error';
|
|
| 56 |
COMMENT ON COLUMN usage_logs.ip_address IS '客戶端IP地址';
|
| 57 |
COMMENT ON COLUMN usage_logs.user_agent IS '用戶代理字符串';
|
| 58 |
COMMENT ON COLUMN usage_logs.hardware_info IS '硬體資訊';
|
|
|
|
| 59 |
COMMENT ON COLUMN usage_logs.error_message IS '錯誤訊息 (發生錯誤時)';
|
| 60 |
|
| 61 |
-- =====================================================
|
|
@@ -65,6 +69,7 @@ COMMENT ON COLUMN usage_logs.error_message IS '錯誤訊息 (發生錯誤時)';
|
|
| 65 |
-- 授權表索引
|
| 66 |
CREATE INDEX idx_licenses_code ON licenses(license_code);
|
| 67 |
CREATE INDEX idx_licenses_hardware ON licenses(hardware_id);
|
|
|
|
| 68 |
CREATE INDEX idx_licenses_active ON licenses(is_active, expires_at);
|
| 69 |
CREATE INDEX idx_licenses_user_email ON licenses(user_email);
|
| 70 |
CREATE INDEX idx_licenses_created_at ON licenses(created_at);
|
|
@@ -137,7 +142,8 @@ SELECT
|
|
| 137 |
WHEN l.is_active = false THEN 'inactive'
|
| 138 |
WHEN l.activated_at IS NULL THEN 'not_activated'
|
| 139 |
ELSE 'active'
|
| 140 |
-
END as status
|
|
|
|
| 141 |
FROM licenses l;
|
| 142 |
|
| 143 |
COMMENT ON VIEW license_details IS '授權詳細資訊視圖 (含統計)';
|
|
@@ -156,6 +162,7 @@ SELECT
|
|
| 156 |
ul.ip_address,
|
| 157 |
ul.user_agent,
|
| 158 |
ul.hardware_info,
|
|
|
|
| 159 |
ul.error_message,
|
| 160 |
ul.created_at,
|
| 161 |
l.license_code,
|
|
@@ -261,64 +268,64 @@ COMMENT ON FUNCTION generate_license_report IS '生成指定日期範圍的授
|
|
| 261 |
-- =====================================================
|
| 262 |
|
| 263 |
-- 插入測試用授權 (啟用來測試系統) - 統一使用台灣時間00:00計算(-8hr)
|
| 264 |
-
INSERT INTO licenses (license_code, user_name, user_email, expires_at, hardware_id, is_active, activated_at) VALUES
|
| 265 |
-- 已啟用用戶 (正常使用中)
|
| 266 |
-
('A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6', '王小明', 'ming@company.com.tw', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '90 days' - INTERVAL '1 second', 'CPU123ABC456DEF789GHI012JKL345MN', true, NOW() - INTERVAL '5 days'),
|
| 267 |
-
('X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2', '李美華', 'li.meihua@design.studio', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '365 days' - INTERVAL '1 second', 'CPU789XYZ012ABC345DEF678GHI901JK', true, NOW() - INTERVAL '30 days'),
|
| 268 |
|
| 269 |
-- 試用用戶 (7天試用)
|
| 270 |
-
('B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4', '張工程師', 'engineer.zhang@tech.com', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '7 days' - INTERVAL '1 second', 'CPUABC123XYZ456QWE789RTY012UIU34', true, NOW() - INTERVAL '2 days'),
|
| 271 |
|
| 272 |
-- 更多正常用戶
|
| 273 |
-
('M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0', '陳建築師', 'chen@architect.firm', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '30 days' - INTERVAL '1 second', 'CPUARCH456BUILD789DESIGN012PROJ34', true, NOW() - INTERVAL '1 day'),
|
| 274 |
-
('F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8', '劉設計師', 'liu.designer@studio.tw', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '60 days' - INTERVAL '1 second', 'CPUDESIGN123CREATIVE456ART789STUD', true, NOW() - INTERVAL '7 days'),
|
| 275 |
|
| 276 |
-- 過期授權 (已過期15天)
|
| 277 |
-
('Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5', '周老闆', 'boss.zhou@construction.co', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' - INTERVAL '15 days' - INTERVAL '1 second', 'CPU456DEF789ABC012GHI345JKL678MN', true, NOW() - INTERVAL '45 days'),
|
| 278 |
|
| 279 |
-- 已停用授權 (管理員手動停用)
|
| 280 |
-
('Q2W3E4R5T6Y7U8I9O0P1A2S3D4F5G6H7', '黃違規用戶', 'huang@suspended.user', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '20 days' - INTERVAL '1 second', 'CPUXYZ789ABC012DEF345GHI678JKL90', false, NOW() - INTERVAL '10 days'),
|
| 281 |
|
| 282 |
-- 永久授權 (企業用戶) - 50年
|
| 283 |
-
('K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6', 'KSTools管理員', 'admin@kstools.com.tw', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '18250 days' - INTERVAL '1 second', 'CPUADMIN123MASTER456CONTROL789AB', true, NOW() - INTERVAL '1 day'),
|
| 284 |
|
| 285 |
-- 即將過期 (今天結束就過期)
|
| 286 |
-
('V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3', '趙用戶', 'zhao@expires.soon', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '1 day' - INTERVAL '1 second', 'CPU999END888SOON777EXPIRE666ABC', true, NOW() - INTERVAL '28 days')
|
| 287 |
ON CONFLICT (license_code) DO NOTHING;
|
| 288 |
|
| 289 |
-- 插入測試使用記錄
|
| 290 |
-
INSERT INTO usage_logs (license_id, action, ip_address, hardware_info) VALUES
|
| 291 |
-- 正常用戶的使用記錄
|
| 292 |
-
((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'activate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN'),
|
| 293 |
-
((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'validate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN'),
|
| 294 |
-
((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'validate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN'),
|
| 295 |
|
| 296 |
-- 企業用戶的使用記錄
|
| 297 |
-
((SELECT id FROM licenses WHERE license_code = 'X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2'), 'activate', '10.0.1.50', 'CPU789XYZ012ABC345DEF678GHI901JK'),
|
| 298 |
-
((SELECT id FROM licenses WHERE license_code = 'X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2'), 'validate', '10.0.1.50', 'CPU789XYZ012ABC345DEF678GHI901JK'),
|
| 299 |
|
| 300 |
-- 其他正常用戶記錄
|
| 301 |
-
((SELECT id FROM licenses WHERE license_code = 'M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0'), 'activate', '192.168.50.20', 'CPUARCH456BUILD789DESIGN012PROJ34'),
|
| 302 |
-
((SELECT id FROM licenses WHERE license_code = 'M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0'), 'validate', '192.168.50.20', 'CPUARCH456BUILD789DESIGN012PROJ34'),
|
| 303 |
|
| 304 |
-
((SELECT id FROM licenses WHERE license_code = 'F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8'), 'activate', '172.20.10.15', 'CPUDESIGN123CREATIVE456ART789STUD'),
|
| 305 |
-
((SELECT id FROM licenses WHERE license_code = 'F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8'), 'validate', '172.20.10.15', 'CPUDESIGN123CREATIVE456ART789STUD'),
|
| 306 |
|
| 307 |
-- 試用用戶的記錄
|
| 308 |
-
((SELECT id FROM licenses WHERE license_code = 'B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4'), 'activate', '203.74.123.45', 'CPUABC123XYZ456QWE789RTY012UIU34'),
|
| 309 |
-
((SELECT id FROM licenses WHERE license_code = 'B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4'), 'validate', '203.74.123.45', 'CPUABC123XYZ456QWE789RTY012UIU34'),
|
| 310 |
|
| 311 |
-- 過期用戶的歷史記錄
|
| 312 |
-
((SELECT id FROM licenses WHERE license_code = 'Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5'), 'activate', '172.16.0.10', 'CPU456DEF789ABC012GHI345JKL678MN'),
|
| 313 |
-
((SELECT id FROM licenses WHERE license_code = 'Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5'), 'validate', '172.16.0.10', 'CPU456DEF789ABC012GHI345JKL678MN'),
|
| 314 |
|
| 315 |
-- 管理員測試記錄
|
| 316 |
-
((SELECT id FROM licenses WHERE license_code = 'K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6'), 'activate', '127.0.0.1', 'CPUADMIN123MASTER456CONTROL789AB'),
|
| 317 |
-
((SELECT id FROM licenses WHERE license_code = 'K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6'), 'validate', '127.0.0.1', 'CPUADMIN123MASTER456CONTROL789AB'),
|
| 318 |
|
| 319 |
-- 即將過期用戶的記錄
|
| 320 |
-
((SELECT id FROM licenses WHERE license_code = 'V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3'), 'activate', '118.163.88.99', 'CPU999END888SOON777EXPIRE666ABC'),
|
| 321 |
-
((SELECT id FROM licenses WHERE license_code = 'V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3'), 'validate', '118.163.88.99', 'CPU999END888SOON777EXPIRE666ABC');
|
| 322 |
|
| 323 |
-- =====================================================
|
| 324 |
-- 權限設定 (建議)
|
|
|
|
| 13 |
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
| 14 |
license_code VARCHAR(50) UNIQUE NOT NULL,
|
| 15 |
hardware_id VARCHAR(100),
|
| 16 |
+
machine_name VARCHAR(255),
|
| 17 |
user_name VARCHAR(255) NOT NULL,
|
| 18 |
user_email VARCHAR(255),
|
| 19 |
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
|
|
| 29 |
COMMENT ON COLUMN licenses.id IS '唯一識別碼';
|
| 30 |
COMMENT ON COLUMN licenses.license_code IS '授權碼 (格式: 32位隨機字符,支援XXXX-XXXX-XXXX-XXXX分段顯示)';
|
| 31 |
COMMENT ON COLUMN licenses.hardware_id IS '硬體指紋ID';
|
| 32 |
+
COMMENT ON COLUMN licenses.machine_name IS '機器名稱';
|
| 33 |
COMMENT ON COLUMN licenses.user_name IS '授權用戶名稱';
|
| 34 |
COMMENT ON COLUMN licenses.user_email IS '用戶電子郵件';
|
| 35 |
COMMENT ON COLUMN licenses.expires_at IS '授權到期時間';
|
|
|
|
| 47 |
ip_address INET,
|
| 48 |
user_agent TEXT,
|
| 49 |
hardware_info TEXT,
|
| 50 |
+
machine_name VARCHAR(255),
|
| 51 |
error_message TEXT, -- 錯誤訊息 (如果有)
|
| 52 |
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 53 |
);
|
|
|
|
| 59 |
COMMENT ON COLUMN usage_logs.ip_address IS '客戶端IP地址';
|
| 60 |
COMMENT ON COLUMN usage_logs.user_agent IS '用戶代理字符串';
|
| 61 |
COMMENT ON COLUMN usage_logs.hardware_info IS '硬體資訊';
|
| 62 |
+
COMMENT ON COLUMN usage_logs.machine_name IS '機器名稱';
|
| 63 |
COMMENT ON COLUMN usage_logs.error_message IS '錯誤訊息 (發生錯誤時)';
|
| 64 |
|
| 65 |
-- =====================================================
|
|
|
|
| 69 |
-- 授權表索引
|
| 70 |
CREATE INDEX idx_licenses_code ON licenses(license_code);
|
| 71 |
CREATE INDEX idx_licenses_hardware ON licenses(hardware_id);
|
| 72 |
+
CREATE INDEX idx_licenses_machine_name ON licenses(machine_name);
|
| 73 |
CREATE INDEX idx_licenses_active ON licenses(is_active, expires_at);
|
| 74 |
CREATE INDEX idx_licenses_user_email ON licenses(user_email);
|
| 75 |
CREATE INDEX idx_licenses_created_at ON licenses(created_at);
|
|
|
|
| 142 |
WHEN l.is_active = false THEN 'inactive'
|
| 143 |
WHEN l.activated_at IS NULL THEN 'not_activated'
|
| 144 |
ELSE 'active'
|
| 145 |
+
END as status,
|
| 146 |
+
l.machine_name as device_name
|
| 147 |
FROM licenses l;
|
| 148 |
|
| 149 |
COMMENT ON VIEW license_details IS '授權詳細資訊視圖 (含統計)';
|
|
|
|
| 162 |
ul.ip_address,
|
| 163 |
ul.user_agent,
|
| 164 |
ul.hardware_info,
|
| 165 |
+
ul.machine_name,
|
| 166 |
ul.error_message,
|
| 167 |
ul.created_at,
|
| 168 |
l.license_code,
|
|
|
|
| 268 |
-- =====================================================
|
| 269 |
|
| 270 |
-- 插入測試用授權 (啟用來測試系統) - 統一使用台灣時間00:00計算(-8hr)
|
| 271 |
+
INSERT INTO licenses (license_code, user_name, user_email, expires_at, hardware_id, machine_name, is_active, activated_at) VALUES
|
| 272 |
-- 已啟用用戶 (正常使用中)
|
| 273 |
+
('A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6', '王小明', 'ming@company.com.tw', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '90 days' - INTERVAL '1 second', 'CPU123ABC456DEF789GHI012JKL345MN', 'DESKTOP-MING-PC', true, NOW() - INTERVAL '5 days'),
|
| 274 |
+
('X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2', '李美華', 'li.meihua@design.studio', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '365 days' - INTERVAL '1 second', 'CPU789XYZ012ABC345DEF678GHI901JK', 'DESIGN-WORKSTATION-02', true, NOW() - INTERVAL '30 days'),
|
| 275 |
|
| 276 |
-- 試用用戶 (7天試用)
|
| 277 |
+
('B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4', '張工程師', 'engineer.zhang@tech.com', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '7 days' - INTERVAL '1 second', 'CPUABC123XYZ456QWE789RTY012UIU34', 'ENGINEER-LAPTOP-01', true, NOW() - INTERVAL '2 days'),
|
| 278 |
|
| 279 |
-- 更多正常用戶
|
| 280 |
+
('M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0', '陳建築師', 'chen@architect.firm', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '30 days' - INTERVAL '1 second', 'CPUARCH456BUILD789DESIGN012PROJ34', 'ARCHITECT-STUDIO-PC', true, NOW() - INTERVAL '1 day'),
|
| 281 |
+
('F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8', '劉設計師', 'liu.designer@studio.tw', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '60 days' - INTERVAL '1 second', 'CPUDESIGN123CREATIVE456ART789STUD', 'CREATIVE-WORKSTATION', true, NOW() - INTERVAL '7 days'),
|
| 282 |
|
| 283 |
-- 過期授權 (已過期15天)
|
| 284 |
+
('Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5', '周老闆', 'boss.zhou@construction.co', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' - INTERVAL '15 days' - INTERVAL '1 second', 'CPU456DEF789ABC012GHI345JKL678MN', 'BOSS-OFFICE-PC', true, NOW() - INTERVAL '45 days'),
|
| 285 |
|
| 286 |
-- 已停用授權 (管理員手動停用)
|
| 287 |
+
('Q2W3E4R5T6Y7U8I9O0P1A2S3D4F5G6H7', '黃違規用戶', 'huang@suspended.user', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '20 days' - INTERVAL '1 second', 'CPUXYZ789ABC012DEF345GHI678JKL90', 'SUSPENDED-PC', false, NOW() - INTERVAL '10 days'),
|
| 288 |
|
| 289 |
-- 永久授權 (企業用戶) - 50年
|
| 290 |
+
('K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6', 'KSTools管理員', 'admin@kstools.com.tw', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '18250 days' - INTERVAL '1 second', 'CPUADMIN123MASTER456CONTROL789AB', 'ADMIN-MASTER-SERVER', true, NOW() - INTERVAL '1 day'),
|
| 291 |
|
| 292 |
-- 即將過期 (今天結束就過期)
|
| 293 |
+
('V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3', '趙用戶', 'zhao@expires.soon', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '1 day' - INTERVAL '1 second', 'CPU999END888SOON777EXPIRE666ABC', 'EXPIRING-PC-TODAY', true, NOW() - INTERVAL '28 days')
|
| 294 |
ON CONFLICT (license_code) DO NOTHING;
|
| 295 |
|
| 296 |
-- 插入測試使用記錄
|
| 297 |
+
INSERT INTO usage_logs (license_id, action, ip_address, hardware_info, machine_name) VALUES
|
| 298 |
-- 正常用戶的使用記錄
|
| 299 |
+
((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'activate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN', 'DESKTOP-MING-PC'),
|
| 300 |
+
((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'validate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN', 'DESKTOP-MING-PC'),
|
| 301 |
+
((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'validate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN', 'DESKTOP-MING-PC'),
|
| 302 |
|
| 303 |
-- 企業用戶的使用記錄
|
| 304 |
+
((SELECT id FROM licenses WHERE license_code = 'X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2'), 'activate', '10.0.1.50', 'CPU789XYZ012ABC345DEF678GHI901JK', 'DESIGN-WORKSTATION-02'),
|
| 305 |
+
((SELECT id FROM licenses WHERE license_code = 'X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2'), 'validate', '10.0.1.50', 'CPU789XYZ012ABC345DEF678GHI901JK', 'DESIGN-WORKSTATION-02'),
|
| 306 |
|
| 307 |
-- 其他正常用戶記錄
|
| 308 |
+
((SELECT id FROM licenses WHERE license_code = 'M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0'), 'activate', '192.168.50.20', 'CPUARCH456BUILD789DESIGN012PROJ34', 'ARCHITECT-STUDIO-PC'),
|
| 309 |
+
((SELECT id FROM licenses WHERE license_code = 'M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0'), 'validate', '192.168.50.20', 'CPUARCH456BUILD789DESIGN012PROJ34', 'ARCHITECT-STUDIO-PC'),
|
| 310 |
|
| 311 |
+
((SELECT id FROM licenses WHERE license_code = 'F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8'), 'activate', '172.20.10.15', 'CPUDESIGN123CREATIVE456ART789STUD', 'CREATIVE-WORKSTATION'),
|
| 312 |
+
((SELECT id FROM licenses WHERE license_code = 'F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8'), 'validate', '172.20.10.15', 'CPUDESIGN123CREATIVE456ART789STUD', 'CREATIVE-WORKSTATION'),
|
| 313 |
|
| 314 |
-- 試用用戶的記錄
|
| 315 |
+
((SELECT id FROM licenses WHERE license_code = 'B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4'), 'activate', '203.74.123.45', 'CPUABC123XYZ456QWE789RTY012UIU34', 'ENGINEER-LAPTOP-01'),
|
| 316 |
+
((SELECT id FROM licenses WHERE license_code = 'B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4'), 'validate', '203.74.123.45', 'CPUABC123XYZ456QWE789RTY012UIU34', 'ENGINEER-LAPTOP-01'),
|
| 317 |
|
| 318 |
-- 過期用戶的歷史記錄
|
| 319 |
+
((SELECT id FROM licenses WHERE license_code = 'Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5'), 'activate', '172.16.0.10', 'CPU456DEF789ABC012GHI345JKL678MN', 'BOSS-OFFICE-PC'),
|
| 320 |
+
((SELECT id FROM licenses WHERE license_code = 'Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5'), 'validate', '172.16.0.10', 'CPU456DEF789ABC012GHI345JKL678MN', 'BOSS-OFFICE-PC'),
|
| 321 |
|
| 322 |
-- 管理員測試記錄
|
| 323 |
+
((SELECT id FROM licenses WHERE license_code = 'K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6'), 'activate', '127.0.0.1', 'CPUADMIN123MASTER456CONTROL789AB', 'ADMIN-MASTER-SERVER'),
|
| 324 |
+
((SELECT id FROM licenses WHERE license_code = 'K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6'), 'validate', '127.0.0.1', 'CPUADMIN123MASTER456CONTROL789AB', 'ADMIN-MASTER-SERVER'),
|
| 325 |
|
| 326 |
-- 即將過期用戶的記錄
|
| 327 |
+
((SELECT id FROM licenses WHERE license_code = 'V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3'), 'activate', '118.163.88.99', 'CPU999END888SOON777EXPIRE666ABC', 'EXPIRING-PC-TODAY'),
|
| 328 |
+
((SELECT id FROM licenses WHERE license_code = 'V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3'), 'validate', '118.163.88.99', 'CPU999END888SOON777EXPIRE666ABC', 'EXPIRING-PC-TODAY');
|
| 329 |
|
| 330 |
-- =====================================================
|
| 331 |
-- 權限設定 (建議)
|