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 =
40 serde_json::from_str(&content).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).with_context(|| {
50 format!(
51 "Failed to create config directory at {}",
52 config_dir.display()
53 )
54 })?;
55 }
56
57 let path = Self::credentials_path()?;
58 let content =
59 serde_json::to_string_pretty(self).context("Failed to serialize credentials")?;
60
61 fs::write(&path, content)
62 .with_context(|| format!("Failed to write credentials to {}", path.display()))?;
63
64 Self::set_secure_permissions(&path)?;
66
67 Ok(())
68 }
69
70 fn config_dir() -> Result<PathBuf> {
71 let home = dirs::home_dir().context("Could not determine home directory")?;
72 Ok(home.join(CONFIG_DIR))
73 }
74
75 fn credentials_path() -> Result<PathBuf> {
76 Ok(Self::config_dir()?.join(CREDENTIALS_FILE))
77 }
78
79 pub fn credentials_location() -> String {
80 Self::credentials_path()
81 .map(|p| p.display().to_string())
82 .unwrap_or_else(|_| format!("~/{}/{}", CONFIG_DIR, CREDENTIALS_FILE))
83 }
84
85 fn set_secure_permissions(path: &PathBuf) -> Result<()> {
87 #[cfg(unix)]
88 {
89 use std::os::unix::fs::PermissionsExt;
90 let mut perms = fs::metadata(path)
91 .context("Failed to get file metadata")?
92 .permissions();
93 perms.set_mode(0o600);
94 fs::set_permissions(path, perms).context("Failed to set file permissions to 0600")?;
95 }
96
97 #[cfg(windows)]
98 {
99 use std::os::windows::fs::MetadataExt;
100
101 let metadata = fs::metadata(path).context("Failed to get file metadata")?;
104
105 let mut perms = metadata.permissions();
107 perms.set_readonly(false); fs::set_permissions(path, perms).context("Failed to set file permissions")?;
109
110 if let Err(e) = Self::set_windows_acl(path) {
113 eprintln!(
114 "Warning: Could not set Windows ACL for credentials file: {}",
115 e
116 );
117 eprintln!(" File permissions may not be fully secure on Windows.");
118 eprintln!(" Consider protecting your user account with a strong password.");
119 }
120 }
121
122 Ok(())
123 }
124
125 #[cfg(windows)]
126 fn set_windows_acl(path: &PathBuf) -> Result<()> {
127 use std::process::Command;
128
129 let output = Command::new("icacls")
133 .arg(path)
134 .arg("/inheritance:r")
135 .arg("/grant:r")
136 .arg(format!(
137 "{}:F",
138 std::env::var("USERNAME").unwrap_or_else(|_| String::from("*S-1-5-32-544"))
139 ))
140 .output()
141 .context("Failed to execute icacls command")?;
142
143 if !output.status.success() {
144 anyhow::bail!("icacls failed: {}", String::from_utf8_lossy(&output.stderr));
145 }
146
147 Ok(())
148 }
149}