feat(apiservice): add edit client/key functionality in API Management page
- PATCH /admin/api-keys/clients/{id} — update client name and is_active
- PATCH /admin/api-keys/{id} — update key name and permissions
- Edit Client modal with name field and active/inactive toggle
- Edit Key modal with name field and permissions JSON textarea (pre-filled)
- Fix JS syntax error: use data-* attributes instead of inline JSON in onclick
This commit is contained in:
@@ -45,12 +45,22 @@ class ApiClientCreateSchema(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class ApiClientUpdateSchema(BaseModel):
|
||||||
|
name: str | None = None
|
||||||
|
is_active: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
class ApiKeyCreateSchema(BaseModel):
|
class ApiKeyCreateSchema(BaseModel):
|
||||||
client_id: int
|
client_id: int
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
permissions: list[str] = []
|
permissions: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKeyUpdateSchema(BaseModel):
|
||||||
|
name: str | None = None
|
||||||
|
permissions: list[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
@router.get("/clients", response_model=List[ApiClientSchema])
|
@router.get("/clients", response_model=List[ApiClientSchema])
|
||||||
async def list_clients(
|
async def list_clients(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -60,6 +70,30 @@ async def list_clients(
|
|||||||
return db.query(ApiClient).order_by(ApiClient.id).all()
|
return db.query(ApiClient).order_by(ApiClient.id).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/clients/{client_id}", response_model=ApiClientSchema)
|
||||||
|
async def update_client(
|
||||||
|
client_id: int,
|
||||||
|
data: ApiClientUpdateSchema,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: dict = Depends(require_role(Roles.ADMIN)),
|
||||||
|
):
|
||||||
|
"""Update API client name or active status (Admin only)"""
|
||||||
|
client = db.get(ApiClient, client_id)
|
||||||
|
if not client:
|
||||||
|
raise HTTPException(status_code=404, detail="Client not found")
|
||||||
|
if data.name is not None:
|
||||||
|
existing = db.query(ApiClient).filter(ApiClient.name == data.name, ApiClient.id != client_id).first()
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(status_code=400, detail="Client name already exists")
|
||||||
|
client.name = data.name
|
||||||
|
if data.is_active is not None:
|
||||||
|
client.is_active = data.is_active
|
||||||
|
db.commit()
|
||||||
|
db.refresh(client)
|
||||||
|
logger.info(f"Admin {current_user.get('username')} updated client {client_id}")
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
@router.post("/clients", response_model=ApiClientSchema)
|
@router.post("/clients", response_model=ApiClientSchema)
|
||||||
async def create_client(
|
async def create_client(
|
||||||
data: ApiClientCreateSchema,
|
data: ApiClientCreateSchema,
|
||||||
@@ -126,6 +160,27 @@ async def regenerate_key(
|
|||||||
return {"key_id": api_key.id, "api_key": plain_key, "key_prefix": api_key.key_prefix, "permissions": api_key.permissions}
|
return {"key_id": api_key.id, "api_key": plain_key, "key_prefix": api_key.key_prefix, "permissions": api_key.permissions}
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{key_id}", response_model=ApiKeySchema)
|
||||||
|
async def update_key(
|
||||||
|
key_id: int,
|
||||||
|
data: ApiKeyUpdateSchema,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: dict = Depends(require_role(Roles.ADMIN)),
|
||||||
|
):
|
||||||
|
"""Update API key name or permissions (Admin only)"""
|
||||||
|
api_key = db.get(ApiKey, key_id)
|
||||||
|
if not api_key:
|
||||||
|
raise HTTPException(status_code=404, detail="API Key not found")
|
||||||
|
if data.name is not None:
|
||||||
|
api_key.name = data.name
|
||||||
|
if data.permissions is not None:
|
||||||
|
api_key.permissions = data.permissions
|
||||||
|
db.commit()
|
||||||
|
db.refresh(api_key)
|
||||||
|
logger.info(f"Admin {current_user.get('username')} updated API key {key_id}")
|
||||||
|
return api_key
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{key_id}/toggle")
|
@router.patch("/{key_id}/toggle")
|
||||||
async def toggle_key(
|
async def toggle_key(
|
||||||
key_id: int,
|
key_id: int,
|
||||||
|
|||||||
@@ -207,6 +207,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.loading { text-align: center; padding: 40px; color: #666; }
|
.loading { text-align: center; padding: 40px; color: #666; }
|
||||||
|
|
||||||
|
.toggle-row { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative; width: 44px; height: 24px; cursor: pointer;
|
||||||
|
}
|
||||||
|
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute; inset: 0; background: #ccc; border-radius: 24px; transition: 0.3s;
|
||||||
|
}
|
||||||
|
.toggle-slider:before {
|
||||||
|
content: ''; position: absolute; width: 18px; height: 18px;
|
||||||
|
left: 3px; top: 3px; background: white; border-radius: 50%; transition: 0.3s;
|
||||||
|
}
|
||||||
|
.toggle-switch input:checked + .toggle-slider { background: #51cf66; }
|
||||||
|
.toggle-switch input:checked + .toggle-slider:before { transform: translateX(20px); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -291,6 +306,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: Edit Client -->
|
||||||
|
<div class="modal-overlay" id="editClientModal">
|
||||||
|
<div class="modal">
|
||||||
|
<h3>Edit API Client</h3>
|
||||||
|
<input type="hidden" id="editClientId" />
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Client Name</label>
|
||||||
|
<input type="text" id="editClientName" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Status</label>
|
||||||
|
<div class="toggle-row">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="editClientActive" onchange="updateClientActiveLabel()" />
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<span id="editClientActiveLabel">Active</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn" onclick="closeModal('editClientModal')">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="saveEditClient()">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: Edit Key -->
|
||||||
|
<div class="modal-overlay" id="editKeyModal">
|
||||||
|
<div class="modal">
|
||||||
|
<h3>Edit API Key</h3>
|
||||||
|
<input type="hidden" id="editKeyId" />
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Key Name (optional)</label>
|
||||||
|
<input type="text" id="editKeyName" placeholder="e.g. production" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Permissions</label>
|
||||||
|
<textarea id="editKeyPermissions" rows="4" placeholder='["voc.data:write"]'></textarea>
|
||||||
|
<small>JSON array of permission strings</small>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn" onclick="closeModal('editKeyModal')">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="saveEditKey()">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Modal: Show Key -->
|
<!-- Modal: Show Key -->
|
||||||
<div class="modal-overlay" id="keyResultModal">
|
<div class="modal-overlay" id="keyResultModal">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
@@ -342,6 +404,7 @@
|
|||||||
<div class="client-actions">
|
<div class="client-actions">
|
||||||
<span class="${client.is_active ? 'status-active' : 'status-inactive'}">${client.is_active ? '● Active' : '● Inactive'}</span>
|
<span class="${client.is_active ? 'status-active' : 'status-inactive'}">${client.is_active ? '● Active' : '● Inactive'}</span>
|
||||||
<button class="btn btn-success btn-sm" onclick="openNewKeyModal(${client.id})">+ Add Key</button>
|
<button class="btn btn-success btn-sm" onclick="openNewKeyModal(${client.id})">+ Add Key</button>
|
||||||
|
<button class="btn btn-warning btn-sm" data-id="${client.id}" data-name="${escapeHtml(client.name)}" data-active="${client.is_active}" onclick="openEditClientModal(this)">Edit</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${client.api_keys.length === 0
|
${client.api_keys.length === 0
|
||||||
@@ -360,6 +423,7 @@
|
|||||||
<td class="${key.is_active ? 'status-active' : 'status-inactive'}">${key.is_active ? '● Active' : '● Inactive'}</td>
|
<td class="${key.is_active ? 'status-active' : 'status-inactive'}">${key.is_active ? '● Active' : '● Inactive'}</td>
|
||||||
<td>${formatDate(key.created_at)}</td>
|
<td>${formatDate(key.created_at)}</td>
|
||||||
<td>
|
<td>
|
||||||
|
<button class="btn btn-primary btn-sm" data-id="${key.id}" data-name="${escapeHtml(key.name || '')}" data-perms="${JSON.stringify(key.permissions).replace(/"/g, '"')}" onclick="openEditKeyModal(this)">Edit</button>
|
||||||
<button class="btn btn-warning btn-sm" onclick="regenerateKey(${key.id})">Regenerate</button>
|
<button class="btn btn-warning btn-sm" onclick="regenerateKey(${key.id})">Regenerate</button>
|
||||||
<button class="btn btn-sm" style="background:#dee2e6" onclick="toggleKey(${key.id})">${key.is_active ? 'Deactivate' : 'Activate'}</button>
|
<button class="btn btn-sm" style="background:#dee2e6" onclick="toggleKey(${key.id})">${key.is_active ? 'Deactivate' : 'Activate'}</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -376,6 +440,71 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateClientActiveLabel() {
|
||||||
|
const cb = document.getElementById('editClientActive');
|
||||||
|
document.getElementById('editClientActiveLabel').textContent = cb.checked ? 'Active' : 'Inactive';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditClientModal(btn) {
|
||||||
|
const clientId = btn.dataset.id;
|
||||||
|
const name = btn.dataset.name;
|
||||||
|
const isActive = btn.dataset.active === 'true';
|
||||||
|
document.getElementById('editClientId').value = clientId;
|
||||||
|
document.getElementById('editClientName').value = name;
|
||||||
|
document.getElementById('editClientActive').checked = isActive;
|
||||||
|
document.getElementById('editClientActiveLabel').textContent = isActive ? 'Active' : 'Inactive';
|
||||||
|
document.getElementById('editClientModal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEditClient() {
|
||||||
|
const clientId = document.getElementById('editClientId').value;
|
||||||
|
const name = document.getElementById('editClientName').value.trim();
|
||||||
|
const isActive = document.getElementById('editClientActive').checked;
|
||||||
|
if (!name) return showAlert('Client name is required', 'error');
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${rootPath}/admin/api-keys/clients/${clientId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name, is_active: isActive })
|
||||||
|
});
|
||||||
|
if (!res.ok) { const e = await res.json(); throw new Error(e.detail || res.statusText); }
|
||||||
|
closeModal('editClientModal');
|
||||||
|
showAlert('Client updated', 'success');
|
||||||
|
loadClients();
|
||||||
|
} catch (err) { showAlert('Failed: ' + err.message, 'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditKeyModal(btn) {
|
||||||
|
const keyId = btn.dataset.id;
|
||||||
|
const name = btn.dataset.name;
|
||||||
|
let permissions = [];
|
||||||
|
try { permissions = JSON.parse(btn.dataset.perms); } catch {}
|
||||||
|
document.getElementById('editKeyId').value = keyId;
|
||||||
|
document.getElementById('editKeyName').value = name || '';
|
||||||
|
document.getElementById('editKeyPermissions').value = JSON.stringify(permissions, null, 2);
|
||||||
|
document.getElementById('editKeyModal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEditKey() {
|
||||||
|
const keyId = document.getElementById('editKeyId').value;
|
||||||
|
const name = document.getElementById('editKeyName').value.trim() || null;
|
||||||
|
const permsRaw = document.getElementById('editKeyPermissions').value.trim();
|
||||||
|
let permissions;
|
||||||
|
try { permissions = permsRaw ? JSON.parse(permsRaw) : []; }
|
||||||
|
catch { return showAlert('Permissions must be a valid JSON array', 'error'); }
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${rootPath}/admin/api-keys/${keyId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name, permissions })
|
||||||
|
});
|
||||||
|
if (!res.ok) { const e = await res.json(); throw new Error(e.detail || res.statusText); }
|
||||||
|
closeModal('editKeyModal');
|
||||||
|
showAlert('Key updated', 'success');
|
||||||
|
loadClients();
|
||||||
|
} catch (err) { showAlert('Failed: ' + err.message, 'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
function openNewClientModal() {
|
function openNewClientModal() {
|
||||||
document.getElementById('newClientName').value = '';
|
document.getElementById('newClientName').value = '';
|
||||||
document.getElementById('newClientModal').classList.add('active');
|
document.getElementById('newClientModal').classList.add('active');
|
||||||
|
|||||||
Reference in New Issue
Block a user