Skip to content

Commit f46912c

Browse files
authored
fix(auth): preserve output preferences on logout (#49)
`qn auth logout` deleted the entire config file, wiping the user's `[output]` preferences (format, wide) along with the API key. Logout now clears only `[api].key` and rewrites the file, keeping output settings intact. Extracts the atomic-write path from `save_api_key` into a shared `write_config` helper, reused by the new `clear_api_key`. Adds 3 config tests (preserves output section, writes defaults when no file exists, keeps 0600 perms).
1 parent 2adfdcb commit f46912c

3 files changed

Lines changed: 54 additions & 17 deletions

File tree

src/commands/agent/context.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ Non-interactive paths:
2626
- Pass `--api-key <KEY>` on every invocation, or
2727
- Write the key once: `qn auth login --api-key <KEY>` (saves the config file).
2828

29+
`qn auth logout` removes the saved API key but preserves your `[output]`
30+
preferences in the config file.
31+
2932
Config file location:
3033

3134
- Linux/macOS: `$XDG_CONFIG_HOME/qn/config.toml`, else `~/.config/qn/config.toml`.

src/commands/auth.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ fn logout(global: GlobalArgs) -> Result<(), CliError> {
153153
.to_string(),
154154
)
155155
})?;
156-
config::delete_config(&path)?;
156+
config::clear_api_key(&path)?;
157157
if !global.quiet {
158158
let _ = writeln!(std::io::stderr(), "✓ Removed saved API key");
159159
}

src/config.rs

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,23 @@ pub fn load_from(path: &Path) -> Result<Option<ConfigFile>, CliError> {
130130
pub fn save_api_key(path: &Path, api_key: &str) -> Result<(), CliError> {
131131
let mut cfg = load_from(path)?.unwrap_or_default();
132132
cfg.api.key = Some(api_key.to_string());
133-
let text = toml::to_string_pretty(&cfg).map_err(|e| CliError::ConfigWrite {
133+
write_config(path, &cfg)
134+
}
135+
136+
/// Removes the saved API key while preserving the rest of the config (output
137+
/// preferences). Used by `qn auth logout` so logging out doesn't reset
138+
/// `[output]` settings the user deliberately chose.
139+
pub fn clear_api_key(path: &Path) -> Result<(), CliError> {
140+
let mut cfg = load_from(path)?.unwrap_or_default();
141+
cfg.api.key = None;
142+
write_config(path, &cfg)
143+
}
144+
145+
/// Atomically writes `cfg` to `path`: serialize, write to a 0600 tempfile in the
146+
/// same directory, fsync, then rename into place. Shared by [`save_api_key`] and
147+
/// [`clear_api_key`].
148+
fn write_config(path: &Path, cfg: &ConfigFile) -> Result<(), CliError> {
149+
let text = toml::to_string_pretty(cfg).map_err(|e| CliError::ConfigWrite {
134150
path: path.to_path_buf(),
135151
source: std::io::Error::other(e),
136152
})?;
@@ -196,15 +212,6 @@ pub fn save_api_key(path: &Path, api_key: &str) -> Result<(), CliError> {
196212
Ok(())
197213
}
198214

199-
/// Deletes the saved config file. No error if it didn't exist.
200-
pub fn delete_config(path: &Path) -> Result<(), CliError> {
201-
match fs::remove_file(path) {
202-
Ok(()) => Ok(()),
203-
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
204-
Err(e) => Err(e.into()),
205-
}
206-
}
207-
208215
/// Resolves an API key per the documented precedence: flag > config file.
209216
///
210217
/// `allow_prompt` and `prompt` exist only so `qn auth login` can opt into the
@@ -432,14 +439,41 @@ mod tests {
432439
}
433440

434441
#[test]
435-
fn delete_is_idempotent() {
442+
fn clear_api_key_preserves_output_section() {
443+
let dir = tempdir().unwrap();
444+
let path = dir.path().join("config.toml");
445+
std::fs::write(
446+
&path,
447+
"[api]\nkey = \"k\"\n\n[output]\nformat = \"json\"\nwide = true\n",
448+
)
449+
.unwrap();
450+
clear_api_key(&path).unwrap();
451+
let cfg = load_from(&path).unwrap().unwrap();
452+
assert_eq!(cfg.api.key, None);
453+
assert_eq!(cfg.output.format, Some(Format::Json));
454+
assert!(cfg.output.wide);
455+
}
456+
457+
#[test]
458+
fn clear_api_key_with_no_existing_file_writes_defaults() {
459+
let dir = tempdir().unwrap();
460+
let path = dir.path().join("config.toml");
461+
clear_api_key(&path).unwrap();
462+
assert!(path.exists());
463+
let cfg = load_from(&path).unwrap().unwrap();
464+
assert_eq!(cfg.api.key, None);
465+
}
466+
467+
#[cfg(unix)]
468+
#[test]
469+
fn clear_api_key_writes_mode_0600() {
470+
use std::os::unix::fs::PermissionsExt;
436471
let dir = tempdir().unwrap();
437472
let path = dir.path().join("config.toml");
438-
delete_config(&path).unwrap(); // no file yet
439-
save_api_key(&path, "k").unwrap();
440-
delete_config(&path).unwrap();
441-
delete_config(&path).unwrap(); // already gone
442-
assert!(!path.exists());
473+
save_api_key(&path, "secret").unwrap();
474+
clear_api_key(&path).unwrap();
475+
let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
476+
assert_eq!(mode, 0o600, "expected 0600, got {mode:o}");
443477
}
444478

445479
#[cfg(unix)]

0 commit comments

Comments
 (0)