1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5
6const CONFIG_DIR: &str = ".nsg";
7const CREDENTIALS_FILE: &str = "credentials.json";
8
9#[derive(Debug, Serialize, Deserialize, Clone)]
10pub struct Credentials {
11 pub username: String,
12 pub password: String,
13 pub app_key: String,
14}
15
16impl Credentials {
17 pub fn new(username: String, password: String, app_key: String) -> Self {
18 Self {
19 username,
20 password,
21 app_key,
22 }
23 }
24
25 pub fn load() -> Result<Self> {
26 let path = Self::credentials_path()?;
27
28 if !path.exists() {
29 anyhow::bail!(
30 "No credentials found. Please run 'nsg login' first.\n\
31 Expected credentials at: {}",
32 path.display()
33 );
34 }
35
36 let content = fs::read_to_string(&path)
37 .with_context(|| format!("Failed to read credentials from {}", path.display()))?;
38
39 let creds: Credentials = serde_json::from_str(&content)
40 .context("Failed to parse credentials file")?;
41
42 Ok(creds)
43 }
44
45 pub fn save(&self) -> Result<()> {
46 let config_dir = Self::config_dir()?;
47
48 if !config_dir.exists() {
49 fs::create_dir_all(&config_dir)
50 .with_context(|| format!("Failed to create config directory at {}", config_dir.display()))?;
51 }
52
53 let path = Self::credentials_path()?;
54 let content = serde_json::to_string_pretty(self)
55 .context("Failed to serialize credentials")?;
56
57 fs::write(&path, content)
58 .with_context(|| format!("Failed to write credentials to {}", path.display()))?;
59
60 Self::set_secure_permissions(&path)?;
62
63 Ok(())
64 }
65
66 fn config_dir() -> Result<PathBuf> {
67 let home = dirs::home_dir()
68 .context("Could not determine home directory")?;
69 Ok(home.join(CONFIG_DIR))
70 }
71
72 fn credentials_path() -> Result<PathBuf> {
73 Ok(Self::config_dir()?.join(CREDENTIALS_FILE))
74 }
75
76 pub fn credentials_location() -> String {
77 Self::credentials_path()
78 .map(|p| p.display().to_string())
79 .unwrap_or_else(|_| format!("~/{}/{}", CONFIG_DIR, CREDENTIALS_FILE))
80 }
81
82 fn set_secure_permissions(path: &PathBuf) -> Result<()> {
84 #[cfg(unix)]
85 {
86 use std::os::unix::fs::PermissionsExt;
87 let mut perms = fs::metadata(path)
88 .context("Failed to get file metadata")?
89 .permissions();
90 perms.set_mode(0o600);
91 fs::set_permissions(path, perms)
92 .context("Failed to set file permissions to 0600")?;
93 }
94
95 #[cfg(windows)]
96 {
97 use std::os::windows::fs::MetadataExt;
98
99 let metadata = fs::metadata(path)
102 .context("Failed to get file metadata")?;
103
104 let mut perms = metadata.permissions();
106 perms.set_readonly(false); fs::set_permissions(path, perms)
108 .context("Failed to set file permissions")?;
109
110 if let Err(e) = Self::set_windows_acl(path) {
113 eprintln!("Warning: Could not set Windows ACL for credentials file: {}", e);
114 eprintln!(" File permissions may not be fully secure on Windows.");
115 eprintln!(" Consider protecting your user account with a strong password.");
116 }
117 }
118
119 Ok(())
120 }
121
122 #[cfg(windows)]
123 fn set_windows_acl(path: &PathBuf) -> Result<()> {
124 use std::process::Command;
125
126 let output = Command::new("icacls")
130 .arg(path)
131 .arg("/inheritance:r")
132 .arg("/grant:r")
133 .arg(format!("{}:F", std::env::var("USERNAME").unwrap_or_else(|_| String::from("*S-1-5-32-544"))))
134 .output()
135 .context("Failed to execute icacls command")?;
136
137 if !output.status.success() {
138 anyhow::bail!(
139 "icacls failed: {}",
140 String::from_utf8_lossy(&output.stderr)
141 );
142 }
143
144 Ok(())
145 }
146}