feat(tokens): allow permanent deletion of revoked tokens
Add POST /admin/tokens/{id}/delete endpoint that permanently removes
a token from the database (only if already revoked). Add delete button
in backoffice UI for revoked tokens.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -96,6 +96,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
.route("/folders", get(index_jobs::list_folders))
|
||||
.route("/admin/tokens", get(tokens::list_tokens).post(tokens::create_token))
|
||||
.route("/admin/tokens/:id", delete(tokens::revoke_token))
|
||||
.route("/admin/tokens/:id/delete", axum::routing::post(tokens::delete_token))
|
||||
.merge(settings::settings_routes())
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
|
||||
@@ -31,6 +31,7 @@ use utoipa::OpenApi;
|
||||
crate::tokens::list_tokens,
|
||||
crate::tokens::create_token,
|
||||
crate::tokens::revoke_token,
|
||||
crate::tokens::delete_token,
|
||||
crate::settings::get_settings,
|
||||
crate::settings::get_setting,
|
||||
crate::settings::update_setting,
|
||||
|
||||
@@ -170,3 +170,35 @@ pub async fn revoke_token(
|
||||
|
||||
Ok(Json(serde_json::json!({"revoked": true, "id": id})))
|
||||
}
|
||||
|
||||
/// Permanently delete a revoked API token
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/admin/tokens/{id}/delete",
|
||||
tag = "tokens",
|
||||
params(
|
||||
("id" = String, Path, description = "Token UUID"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Token permanently deleted"),
|
||||
(status = 404, description = "Token not found or not revoked"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden - Admin scope required"),
|
||||
),
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn delete_token(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let result = sqlx::query("DELETE FROM api_tokens WHERE id = $1 AND revoked_at IS NOT NULL")
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(ApiError::not_found("token not found or not revoked"));
|
||||
}
|
||||
|
||||
Ok(Json(serde_json::json!({"deleted": true, "id": id})))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { listTokens, createToken, revokeToken, TokenDto } from "../../lib/api";
|
||||
import { listTokens, createToken, revokeToken, deleteToken, TokenDto } from "../../lib/api";
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Button, Badge, FormField, FormInput, FormSelect, FormRow } from "../components/ui";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -31,6 +31,13 @@ export default async function TokensPage({
|
||||
revalidatePath("/tokens");
|
||||
}
|
||||
|
||||
async function deleteTokenAction(formData: FormData) {
|
||||
"use server";
|
||||
const id = formData.get("id") as string;
|
||||
await deleteToken(id);
|
||||
revalidatePath("/tokens");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
@@ -109,7 +116,7 @@ export default async function TokensPage({
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{!token.revoked_at && (
|
||||
{!token.revoked_at ? (
|
||||
<form action={revokeTokenAction}>
|
||||
<input type="hidden" name="id" value={token.id} />
|
||||
<Button type="submit" variant="destructive" size="sm">
|
||||
@@ -119,6 +126,16 @@ export default async function TokensPage({
|
||||
Revoke
|
||||
</Button>
|
||||
</form>
|
||||
) : (
|
||||
<form action={deleteTokenAction}>
|
||||
<input type="hidden" name="id" value={token.id} />
|
||||
<Button type="submit" variant="destructive" size="sm">
|
||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
Delete
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -254,6 +254,10 @@ export async function revokeToken(id: string) {
|
||||
return apiFetch<void>(`/admin/tokens/${id}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
export async function deleteToken(id: string) {
|
||||
return apiFetch<void>(`/admin/tokens/${id}/delete`, { method: "POST" });
|
||||
}
|
||||
|
||||
export async function fetchBooks(
|
||||
libraryId?: string,
|
||||
series?: string,
|
||||
|
||||
Reference in New Issue
Block a user