Refactor to flat APIKey model with quota, admin UI, .env config, and Berlin timezone

- Remove User/Quota models; quota fields now live directly on APIKey
- Admin UI: login, API key management, settings (Ollama URL/model), proxy info display
- .env/.env.example: ADMIN_PASSWORD, PROXY_HOST/PORT, DATABASE_URL, APP_TZ
- Admin API runs on 127.0.0.1 only; proxy host/port configurable
- API keys support optional expires_at; verified against Europe/Berlin timezone
- Daily/monthly quota resets use Europe/Berlin midnight boundary
- Fix all tests to use new flat model; add expiry tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Oliver HofmannandClaude Sonnet 4.6 committed 2026-04-28 08:21:42 +02:00
1 parent cfa874a4c3
commit c8235ec274
15 files changed
+824 -471

No files matched your search

+170 -5
View File
@@ -44,12 +44,177 @@ tr:hover {
background: #f8f9fa;
}
.status-active {
color: #27ae60;
font-weight: 600;
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40px;
}
.status-inactive {
.header h1 {
margin-bottom: 0;
}
.login-form {
max-width: 360px;
margin: 80px auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.login-form input {
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.login-form button, .header button {
padding: 10px 20px;
background: #2c3e50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.login-form button:hover, .header button:hover {
background: #34495e;
}
.error {
color: #e74c3c;
font-weight: 600;
font-size: 14px;
}
.create-form {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: flex-end;
}
.create-form input {
padding: 8px 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
flex: 1 1 160px;
}
.create-form button {
padding: 8px 20px;
background: #27ae60;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.create-form button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.new-key-box {
margin-top: 16px;
padding: 12px 16px;
background: #eafaf1;
border: 1px solid #27ae60;
border-radius: 4px;
display: flex;
align-items: center;
gap: 12px;
}
.new-key-box code {
flex: 1;
font-size: 13px;
word-break: break-all;
}
.new-key-box button {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
color: #555;
}
.btn-danger {
padding: 4px 10px;
background: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.btn-danger:hover {
background: #c0392b;
}
.settings-form {
display: flex;
flex-direction: column;
gap: 14px;
max-width: 500px;
}
.settings-row {
display: flex;
align-items: center;
gap: 12px;
}
.settings-row label {
width: 160px;
flex-shrink: 0;
font-weight: 500;
color: #2c3e50;
}
.settings-row input, .settings-row select {
flex: 1;
padding: 8px 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.settings-form button {
align-self: flex-start;
padding: 8px 20px;
background: #2c3e50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.settings-form button:hover {
background: #34495e;
}
.success {
color: #27ae60;
font-size: 14px;
}
.settings-value {
flex: 1;
font-size: 14px;
color: #2c3e50;
}
.settings-value small {
margin-left: 8px;
color: #999;
font-size: 12px;
}