Fix license creation and API connectivity issues
Browse files- Fix API endpoint paths to include /api prefix for unified server
- Ensure field name consistency across frontend, backend, and database
- Add comprehensive debugging and logging to track API requests/responses
- Improve Supabase connection error handling and user feedback
- Add debugging to license creation and data loading processes
- Fix new license button functionality with proper error handling
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- app/models/database.py +18 -4
- app/services/license_service.py +23 -5
- frontend/js/api.js +23 -14
- frontend/js/components.js +19 -10
- frontend/js/pages/users.js +8 -0
app/models/database.py
CHANGED
|
@@ -14,18 +14,32 @@ class Database:
|
|
| 14 |
self.key: str = os.environ.get("SUPABASE_SERVICE_KEY", "")
|
| 15 |
self.client: Optional[Client] = None
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
else:
|
| 20 |
-
print("⚠️ Supabase 環境變數未設定")
|
|
|
|
| 21 |
|
| 22 |
def get_client(self) -> Optional[Client]:
|
| 23 |
"""取得 Supabase 客戶端"""
|
|
|
|
|
|
|
| 24 |
return self.client
|
| 25 |
|
| 26 |
def is_connected(self) -> bool:
|
| 27 |
"""檢查是否連接成功"""
|
| 28 |
-
|
|
|
|
|
|
|
| 29 |
|
| 30 |
# 全域資料庫實例
|
| 31 |
db = Database()
|
|
|
|
| 14 |
self.key: str = os.environ.get("SUPABASE_SERVICE_KEY", "")
|
| 15 |
self.client: Optional[Client] = None
|
| 16 |
|
| 17 |
+
print(f"🔗 Initializing Supabase connection...")
|
| 18 |
+
print(f" URL: {self.url[:50]}{'...' if len(self.url) > 50 else ''}")
|
| 19 |
+
print(f" Key: {'*' * 20 if self.key else 'NOT SET'}")
|
| 20 |
+
|
| 21 |
+
if self.url and self.key and not self.url.startswith('your-') and not self.key.startswith('your-'):
|
| 22 |
+
try:
|
| 23 |
+
self.client = create_client(self.url, self.key)
|
| 24 |
+
print("✅ Supabase connection initialized successfully")
|
| 25 |
+
except Exception as e:
|
| 26 |
+
print(f"❌ Supabase connection failed: {e}")
|
| 27 |
+
self.client = None
|
| 28 |
else:
|
| 29 |
+
print("⚠️ Supabase 環境變數未設定或使用預設值")
|
| 30 |
+
print(" 請在 Hugging Face Spaces Settings 或 .env 檔案中設定正確的 SUPABASE_URL 和 SUPABASE_SERVICE_KEY")
|
| 31 |
|
| 32 |
def get_client(self) -> Optional[Client]:
|
| 33 |
"""取得 Supabase 客戶端"""
|
| 34 |
+
if not self.client:
|
| 35 |
+
print("❌ Supabase client 未初始化")
|
| 36 |
return self.client
|
| 37 |
|
| 38 |
def is_connected(self) -> bool:
|
| 39 |
"""檢查是否連接成功"""
|
| 40 |
+
connected = self.client is not None
|
| 41 |
+
print(f"🔍 Supabase connection status: {'Connected' if connected else 'Not connected'}")
|
| 42 |
+
return connected
|
| 43 |
|
| 44 |
# 全域資料庫實例
|
| 45 |
db = Database()
|
app/services/license_service.py
CHANGED
|
@@ -18,6 +18,8 @@ class LicenseService:
|
|
| 18 |
|
| 19 |
def __init__(self):
|
| 20 |
self.supabase = db.get_client()
|
|
|
|
|
|
|
| 21 |
|
| 22 |
def generate_license_code(self) -> str:
|
| 23 |
"""生成唯一授權碼"""
|
|
@@ -30,11 +32,14 @@ class LicenseService:
|
|
| 30 |
async def create_license(self, license_data: LicenseCreate) -> Dict[str, Any]:
|
| 31 |
"""建立新授權"""
|
| 32 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
license_code = self.generate_license_code()
|
| 34 |
expires_at = datetime.now(timezone.utc) + timedelta(days=license_data.expires_days)
|
| 35 |
|
| 36 |
-
|
| 37 |
-
result = self.supabase.table("licenses").insert({
|
| 38 |
"license_code": license_code,
|
| 39 |
"user_name": license_data.user_name,
|
| 40 |
"user_email": license_data.user_email,
|
|
@@ -42,7 +47,14 @@ class LicenseService:
|
|
| 42 |
"expires_at": expires_at.isoformat(),
|
| 43 |
"is_active": True,
|
| 44 |
"activated_at": datetime.now(timezone.utc).isoformat() if license_data.hardware_id else None
|
| 45 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
if result.data:
|
| 48 |
return {
|
|
@@ -56,7 +68,7 @@ class LicenseService:
|
|
| 56 |
return {"success": False, "message": "授權建立失敗"}
|
| 57 |
|
| 58 |
except Exception as e:
|
| 59 |
-
print(f"建立授權錯誤: {e}")
|
| 60 |
return {"success": False, "message": f"系統錯誤: {str(e)}"}
|
| 61 |
|
| 62 |
async def activate_license(self, activation: LicenseActivation, client_ip: str = None) -> ActivationResponse:
|
|
@@ -260,10 +272,16 @@ class LicenseService:
|
|
| 260 |
async def get_all_licenses(self) -> List[Dict[str, Any]]:
|
| 261 |
"""取得所有授權列表"""
|
| 262 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
result = self.supabase.table("licenses").select("*").order("created_at", desc=True).execute()
|
|
|
|
| 264 |
return result.data or []
|
| 265 |
except Exception as e:
|
| 266 |
-
print(f"取得授權列表錯誤: {e}")
|
| 267 |
return []
|
| 268 |
|
| 269 |
async def get_usage_logs(self, limit: int = 100) -> List[Dict[str, Any]]:
|
|
|
|
| 18 |
|
| 19 |
def __init__(self):
|
| 20 |
self.supabase = db.get_client()
|
| 21 |
+
if not self.supabase:
|
| 22 |
+
print("❌ LicenseService: Supabase client is None - all operations will fail")
|
| 23 |
|
| 24 |
def generate_license_code(self) -> str:
|
| 25 |
"""生成唯一授權碼"""
|
|
|
|
| 32 |
async def create_license(self, license_data: LicenseCreate) -> Dict[str, Any]:
|
| 33 |
"""建立新授權"""
|
| 34 |
try:
|
| 35 |
+
if not self.supabase:
|
| 36 |
+
print("❌ create_license: Supabase client not available")
|
| 37 |
+
return {"success": False, "message": "資料庫連接失敗"}
|
| 38 |
+
|
| 39 |
license_code = self.generate_license_code()
|
| 40 |
expires_at = datetime.now(timezone.utc) + timedelta(days=license_data.expires_days)
|
| 41 |
|
| 42 |
+
license_record = {
|
|
|
|
| 43 |
"license_code": license_code,
|
| 44 |
"user_name": license_data.user_name,
|
| 45 |
"user_email": license_data.user_email,
|
|
|
|
| 47 |
"expires_at": expires_at.isoformat(),
|
| 48 |
"is_active": True,
|
| 49 |
"activated_at": datetime.now(timezone.utc).isoformat() if license_data.hardware_id else None
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
print(f"🔄 Creating license with data: {license_record}")
|
| 53 |
+
|
| 54 |
+
# 插入授權記錄
|
| 55 |
+
result = self.supabase.table("licenses").insert(license_record).execute()
|
| 56 |
+
|
| 57 |
+
print(f"✅ License creation result: {result}")
|
| 58 |
|
| 59 |
if result.data:
|
| 60 |
return {
|
|
|
|
| 68 |
return {"success": False, "message": "授權建立失敗"}
|
| 69 |
|
| 70 |
except Exception as e:
|
| 71 |
+
print(f"❌ 建立授權錯誤: {e}")
|
| 72 |
return {"success": False, "message": f"系統錯誤: {str(e)}"}
|
| 73 |
|
| 74 |
async def activate_license(self, activation: LicenseActivation, client_ip: str = None) -> ActivationResponse:
|
|
|
|
| 272 |
async def get_all_licenses(self) -> List[Dict[str, Any]]:
|
| 273 |
"""取得所有授權列表"""
|
| 274 |
try:
|
| 275 |
+
if not self.supabase:
|
| 276 |
+
print("❌ get_all_licenses: Supabase client not available")
|
| 277 |
+
return []
|
| 278 |
+
|
| 279 |
+
print("🔄 Fetching all licenses from Supabase...")
|
| 280 |
result = self.supabase.table("licenses").select("*").order("created_at", desc=True).execute()
|
| 281 |
+
print(f"✅ Retrieved {len(result.data or [])} licenses from database")
|
| 282 |
return result.data or []
|
| 283 |
except Exception as e:
|
| 284 |
+
print(f"❌ 取得授權列表錯誤: {e}")
|
| 285 |
return []
|
| 286 |
|
| 287 |
async def get_usage_logs(self, limit: int = 100) -> List[Dict[str, Any]]:
|
frontend/js/api.js
CHANGED
|
@@ -36,22 +36,32 @@ class ApiClient {
|
|
| 36 |
config.body = JSON.stringify(config.body);
|
| 37 |
}
|
| 38 |
|
|
|
|
|
|
|
| 39 |
try {
|
| 40 |
const response = await fetch(url, config);
|
| 41 |
|
|
|
|
|
|
|
| 42 |
if (!response.ok) {
|
|
|
|
|
|
|
| 43 |
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
| 44 |
}
|
| 45 |
|
| 46 |
const contentType = response.headers.get('content-type');
|
| 47 |
if (contentType?.includes('application/json')) {
|
| 48 |
-
|
|
|
|
|
|
|
| 49 |
}
|
| 50 |
|
| 51 |
-
|
|
|
|
|
|
|
| 52 |
|
| 53 |
} catch (error) {
|
| 54 |
-
console.error(`API Error [${config.method} ${endpoint}]:`, error);
|
| 55 |
throw error;
|
| 56 |
}
|
| 57 |
}
|
|
@@ -59,7 +69,7 @@ class ApiClient {
|
|
| 59 |
// Health check
|
| 60 |
async checkHealth() {
|
| 61 |
try {
|
| 62 |
-
await this.request('/health');
|
| 63 |
return { success: true };
|
| 64 |
} catch (error) {
|
| 65 |
return { success: false, error: error.message };
|
|
@@ -68,42 +78,41 @@ class ApiClient {
|
|
| 68 |
|
| 69 |
// License endpoints
|
| 70 |
async getLicenses() {
|
| 71 |
-
return this.request('/licenses');
|
| 72 |
}
|
| 73 |
|
| 74 |
async createLicense(licenseData) {
|
| 75 |
-
return this.request('/
|
| 76 |
method: 'POST',
|
| 77 |
body: licenseData
|
| 78 |
});
|
| 79 |
}
|
| 80 |
|
| 81 |
-
async
|
| 82 |
-
return this.request(`/
|
| 83 |
-
method: '
|
| 84 |
-
body: licenseData
|
| 85 |
});
|
| 86 |
}
|
| 87 |
|
| 88 |
async deleteLicense(licenseId) {
|
| 89 |
-
return this.request(`/
|
| 90 |
method: 'DELETE'
|
| 91 |
});
|
| 92 |
}
|
| 93 |
|
| 94 |
// Stats endpoints
|
| 95 |
async getLicenseStats() {
|
| 96 |
-
return this.request('/licenses/stats');
|
| 97 |
}
|
| 98 |
|
| 99 |
// Logs endpoints
|
| 100 |
async getLicenseLogs(limit = 50) {
|
| 101 |
-
return this.request(`/licenses/logs?limit=${limit}`);
|
| 102 |
}
|
| 103 |
|
| 104 |
// System info
|
| 105 |
async getSystemInfo() {
|
| 106 |
-
return this.request('/system/info');
|
| 107 |
}
|
| 108 |
}
|
| 109 |
|
|
|
|
| 36 |
config.body = JSON.stringify(config.body);
|
| 37 |
}
|
| 38 |
|
| 39 |
+
console.log(`🌐 API Request [${config.method}] ${url}`, config.body ? JSON.parse(config.body) : '(no body)');
|
| 40 |
+
|
| 41 |
try {
|
| 42 |
const response = await fetch(url, config);
|
| 43 |
|
| 44 |
+
console.log(`📡 API Response [${config.method}] ${url}: ${response.status} ${response.statusText}`);
|
| 45 |
+
|
| 46 |
if (!response.ok) {
|
| 47 |
+
const errorText = await response.text();
|
| 48 |
+
console.error(`❌ API Error Response:`, errorText);
|
| 49 |
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
| 50 |
}
|
| 51 |
|
| 52 |
const contentType = response.headers.get('content-type');
|
| 53 |
if (contentType?.includes('application/json')) {
|
| 54 |
+
const result = await response.json();
|
| 55 |
+
console.log(`✅ API Success Response:`, result);
|
| 56 |
+
return result;
|
| 57 |
}
|
| 58 |
|
| 59 |
+
const textResult = await response.text();
|
| 60 |
+
console.log(`✅ API Text Response:`, textResult);
|
| 61 |
+
return textResult;
|
| 62 |
|
| 63 |
} catch (error) {
|
| 64 |
+
console.error(`❌ API Error [${config.method} ${endpoint}]:`, error);
|
| 65 |
throw error;
|
| 66 |
}
|
| 67 |
}
|
|
|
|
| 69 |
// Health check
|
| 70 |
async checkHealth() {
|
| 71 |
try {
|
| 72 |
+
await this.request('/api/health');
|
| 73 |
return { success: true };
|
| 74 |
} catch (error) {
|
| 75 |
return { success: false, error: error.message };
|
|
|
|
| 78 |
|
| 79 |
// License endpoints
|
| 80 |
async getLicenses() {
|
| 81 |
+
return this.request('/api/licenses');
|
| 82 |
}
|
| 83 |
|
| 84 |
async createLicense(licenseData) {
|
| 85 |
+
return this.request('/api/license/create', {
|
| 86 |
method: 'POST',
|
| 87 |
body: licenseData
|
| 88 |
});
|
| 89 |
}
|
| 90 |
|
| 91 |
+
async toggleLicense(licenseId) {
|
| 92 |
+
return this.request(`/api/license/${licenseId}/toggle`, {
|
| 93 |
+
method: 'PATCH'
|
|
|
|
| 94 |
});
|
| 95 |
}
|
| 96 |
|
| 97 |
async deleteLicense(licenseId) {
|
| 98 |
+
return this.request(`/api/license/${licenseId}`, {
|
| 99 |
method: 'DELETE'
|
| 100 |
});
|
| 101 |
}
|
| 102 |
|
| 103 |
// Stats endpoints
|
| 104 |
async getLicenseStats() {
|
| 105 |
+
return this.request('/api/licenses/stats');
|
| 106 |
}
|
| 107 |
|
| 108 |
// Logs endpoints
|
| 109 |
async getLicenseLogs(limit = 50) {
|
| 110 |
+
return this.request(`/api/licenses/logs?limit=${limit}`);
|
| 111 |
}
|
| 112 |
|
| 113 |
// System info
|
| 114 |
async getSystemInfo() {
|
| 115 |
+
return this.request('/api/system/info');
|
| 116 |
}
|
| 117 |
}
|
| 118 |
|
frontend/js/components.js
CHANGED
|
@@ -107,11 +107,13 @@ class Components {
|
|
| 107 |
const formData = new FormData(form);
|
| 108 |
const licenseData = {
|
| 109 |
user_name: formData.get('userName'),
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
};
|
| 114 |
|
|
|
|
|
|
|
| 115 |
const submitBtn = form.querySelector('button[type="submit"]');
|
| 116 |
const originalContent = submitBtn.innerHTML;
|
| 117 |
|
|
@@ -119,17 +121,24 @@ class Components {
|
|
| 119 |
submitBtn.disabled = true;
|
| 120 |
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 建立中...';
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
this.closeModal(modal);
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
}
|
| 131 |
|
| 132 |
} catch (error) {
|
|
|
|
| 133 |
Utils.handleError(error, '建立授權時');
|
| 134 |
submitBtn.disabled = false;
|
| 135 |
submitBtn.innerHTML = originalContent;
|
|
|
|
| 107 |
const formData = new FormData(form);
|
| 108 |
const licenseData = {
|
| 109 |
user_name: formData.get('userName'),
|
| 110 |
+
user_email: formData.get('email'),
|
| 111 |
+
expires_days: parseInt(formData.get('validDays')),
|
| 112 |
+
hardware_id: null // 可選,管理界面可以留空
|
| 113 |
};
|
| 114 |
|
| 115 |
+
console.log('📝 License Creation Data:', licenseData);
|
| 116 |
+
|
| 117 |
const submitBtn = form.querySelector('button[type="submit"]');
|
| 118 |
const originalContent = submitBtn.innerHTML;
|
| 119 |
|
|
|
|
| 121 |
submitBtn.disabled = true;
|
| 122 |
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 建立中...';
|
| 123 |
|
| 124 |
+
console.log('🚀 Sending create license request...');
|
| 125 |
+
const response = await api.createLicense(licenseData);
|
| 126 |
+
console.log('✅ Create license response:', response);
|
|
|
|
| 127 |
|
| 128 |
+
if (response && response.success) {
|
| 129 |
+
Utils.showSuccess('授權建立成功');
|
| 130 |
+
this.closeModal(modal);
|
| 131 |
+
|
| 132 |
+
// Refresh current page if it has a refresh method
|
| 133 |
+
if (window.currentPage && window.currentPage.refresh) {
|
| 134 |
+
window.currentPage.refresh();
|
| 135 |
+
}
|
| 136 |
+
} else {
|
| 137 |
+
throw new Error(response?.message || '建立授權失敗');
|
| 138 |
}
|
| 139 |
|
| 140 |
} catch (error) {
|
| 141 |
+
console.error('❌ Create license error:', error);
|
| 142 |
Utils.handleError(error, '建立授權時');
|
| 143 |
submitBtn.disabled = false;
|
| 144 |
submitBtn.innerHTML = originalContent;
|
frontend/js/pages/users.js
CHANGED
|
@@ -156,26 +156,33 @@ class UsersPage {
|
|
| 156 |
|
| 157 |
async loadLicenses() {
|
| 158 |
try {
|
|
|
|
| 159 |
const response = await api.getLicenses();
|
|
|
|
| 160 |
|
| 161 |
// 確保 licenses 是陣列
|
| 162 |
if (Array.isArray(response)) {
|
| 163 |
this.licenses = response;
|
|
|
|
| 164 |
} else if (response && Array.isArray(response.data)) {
|
| 165 |
this.licenses = response.data;
|
|
|
|
| 166 |
} else if (response && typeof response === 'object') {
|
| 167 |
// 如果是對象,嘗試提取可能的陣列屬性
|
| 168 |
const possibleArrays = ['licenses', 'data', 'items', 'records'];
|
| 169 |
for (const key of possibleArrays) {
|
| 170 |
if (Array.isArray(response[key])) {
|
| 171 |
this.licenses = response[key];
|
|
|
|
| 172 |
break;
|
| 173 |
}
|
| 174 |
}
|
| 175 |
if (!this.licenses) {
|
|
|
|
| 176 |
this.licenses = [];
|
| 177 |
}
|
| 178 |
} else {
|
|
|
|
| 179 |
this.licenses = [];
|
| 180 |
}
|
| 181 |
|
|
@@ -184,6 +191,7 @@ class UsersPage {
|
|
| 184 |
Utils.showSuccess('授權列表載入完成');
|
| 185 |
}
|
| 186 |
} catch (error) {
|
|
|
|
| 187 |
Utils.handleError(error, '載入授權列表時');
|
| 188 |
this.licenses = [];
|
| 189 |
this.renderError();
|
|
|
|
| 156 |
|
| 157 |
async loadLicenses() {
|
| 158 |
try {
|
| 159 |
+
console.log('🔄 Loading licenses...');
|
| 160 |
const response = await api.getLicenses();
|
| 161 |
+
console.log('📄 Licenses response:', response);
|
| 162 |
|
| 163 |
// 確保 licenses 是陣列
|
| 164 |
if (Array.isArray(response)) {
|
| 165 |
this.licenses = response;
|
| 166 |
+
console.log(`✅ Loaded ${response.length} licenses (direct array)`);
|
| 167 |
} else if (response && Array.isArray(response.data)) {
|
| 168 |
this.licenses = response.data;
|
| 169 |
+
console.log(`✅ Loaded ${response.data.length} licenses (from .data property)`);
|
| 170 |
} else if (response && typeof response === 'object') {
|
| 171 |
// 如果是對象,嘗試提取可能的陣列屬性
|
| 172 |
const possibleArrays = ['licenses', 'data', 'items', 'records'];
|
| 173 |
for (const key of possibleArrays) {
|
| 174 |
if (Array.isArray(response[key])) {
|
| 175 |
this.licenses = response[key];
|
| 176 |
+
console.log(`✅ Loaded ${response[key].length} licenses (from .${key} property)`);
|
| 177 |
break;
|
| 178 |
}
|
| 179 |
}
|
| 180 |
if (!this.licenses) {
|
| 181 |
+
console.warn('⚠️ No valid license data found in response object, using empty array');
|
| 182 |
this.licenses = [];
|
| 183 |
}
|
| 184 |
} else {
|
| 185 |
+
console.warn('⚠️ Response is not an array or object with license data, using empty array');
|
| 186 |
this.licenses = [];
|
| 187 |
}
|
| 188 |
|
|
|
|
| 191 |
Utils.showSuccess('授權列表載入完成');
|
| 192 |
}
|
| 193 |
} catch (error) {
|
| 194 |
+
console.error('❌ Error loading licenses:', error);
|
| 195 |
Utils.handleError(error, '載入授權列表時');
|
| 196 |
this.licenses = [];
|
| 197 |
this.renderError();
|