harness: POST /api/logout — wipe creds + flip to NeedsLogin (#576 backend half)
This commit is contained in:
parent
bf735357b2
commit
3526991385
1 changed files with 57 additions and 0 deletions
|
|
@ -124,6 +124,7 @@ pub async fn serve(
|
|||
.route("/api/compact", post(post_compact))
|
||||
.route("/api/model", post(post_set_model))
|
||||
.route("/api/new-session", post(post_new_session))
|
||||
.route("/api/logout", post(post_logout))
|
||||
.route("/api/loose-ends", get(api_loose_ends))
|
||||
.route("/api/stats", get(api_stats))
|
||||
.route("/screen/ws", get(screen_ws))
|
||||
|
|
@ -890,6 +891,62 @@ async fn post_new_session(State(state): State<AppState>) -> Response {
|
|||
(axum::http::StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
/// Operator-driven `/logout` (closes #576). Three-step teardown of the
|
||||
/// claude session:
|
||||
///
|
||||
/// 1. SIGINT any running claude process so we don't race a turn that's
|
||||
/// mid-API-call. Same pattern as `post_cancel_turn`; idempotent
|
||||
/// (no-op when nothing is running).
|
||||
/// 2. Wipe the credentials dir (`paths::claude_dir()`, normally
|
||||
/// `/root/.claude`). Recursive remove; recreate empty so the
|
||||
/// bind-mount target keeps existing for the next `claude auth login`.
|
||||
/// 3. Flip the in-memory `LoginState` to `NeedsLogin` and emit a Note.
|
||||
/// The turn-loop's next iteration sees the flipped state and parks
|
||||
/// into `wait_for_login`, which snapshots the (now-empty) dir and
|
||||
/// resumes only when a fresh credentials file appears via the
|
||||
/// dashboard's `/login/code` flow (#542 mtime resumption).
|
||||
///
|
||||
/// Always returns 200 with a body describing what happened — the
|
||||
/// frontend's `/logout` slash command / overflow menu item just needs
|
||||
/// to show a confirmation toast and re-fetch state. Errors at any
|
||||
/// step are logged + folded into the Note so the operator sees them in
|
||||
/// the live panel rather than as an HTTP error.
|
||||
async fn post_logout(State(state): State<AppState>) -> Response {
|
||||
// Step 1: SIGINT claude (best-effort, matches `post_cancel_turn`).
|
||||
let _ = tokio::process::Command::new("pkill")
|
||||
.args(["-INT", "claude"])
|
||||
.output()
|
||||
.await;
|
||||
// Step 2: wipe + recreate credentials dir.
|
||||
let dir = crate::paths::claude_dir();
|
||||
let mut wipe_note = String::new();
|
||||
if let Err(e) = tokio::fs::remove_dir_all(&dir).await {
|
||||
// ENOENT is fine — the dir was already empty (no creds), e.g.
|
||||
// operator clicked /logout while already logged out.
|
||||
if e.kind() != std::io::ErrorKind::NotFound {
|
||||
wipe_note = format!(" (wipe warning: {e})");
|
||||
}
|
||||
}
|
||||
if let Err(e) = tokio::fs::create_dir_all(&dir).await {
|
||||
wipe_note = format!(" (recreate warning: {e})");
|
||||
}
|
||||
// Step 3: flip LoginState + emit Note. Turn loop sees the flip on
|
||||
// its next iteration and parks into wait_for_login.
|
||||
*state.login.lock().unwrap() = LoginState::NeedsLogin;
|
||||
state.bus.emit(crate::events::LiveEvent::Note {
|
||||
text: format!(
|
||||
"operator: /logout — credentials wiped at {}{wipe_note}",
|
||||
dir.display()
|
||||
),
|
||||
});
|
||||
state.bus.emit_status("needs_login_idle");
|
||||
(
|
||||
axum::http::StatusCode::OK,
|
||||
format!("ok: credentials wiped at {}{wipe_note}", dir.display()),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn post_cancel_turn(State(state): State<AppState>) -> Response {
|
||||
let out = tokio::process::Command::new("pkill")
|
||||
.args(["-INT", "claude"])
|
||||
|
|
|
|||
Loading…
Reference in a new issue