opencode_cloud/wizard/
auth.rs1use anyhow::{Result, anyhow};
7use console::{Term, style};
8use dialoguer::{Confirm, Input, Password, Select};
9use opencode_cloud_core::docker::{
10 CONTAINER_NAME, DockerClient, create_user, set_user_password, user_exists,
11};
12use rand::Rng;
13use rand::distr::Alphanumeric;
14
15fn handle_interrupt() -> anyhow::Error {
17 let _ = Term::stdout().show_cursor();
18 anyhow!("Setup cancelled")
19}
20
21fn validate_username(input: &str) -> Result<(), String> {
23 if input.is_empty() {
24 return Err("Username cannot be empty".to_string());
25 }
26 if input.len() < 3 {
27 return Err("Username must be at least 3 characters".to_string());
28 }
29 if input.len() > 32 {
30 return Err("Username must be at most 32 characters".to_string());
31 }
32 if !input.chars().all(|c| c.is_alphanumeric() || c == '_') {
33 return Err("Username must contain only letters, numbers, and underscores".to_string());
34 }
35 Ok(())
36}
37
38fn generate_random_password() -> String {
40 rand::rng()
41 .sample_iter(Alphanumeric)
42 .take(24)
43 .map(char::from)
44 .collect()
45}
46
47pub fn prompt_auth(step: usize, total: usize) -> Result<(String, String)> {
52 println!(
53 "{} {}",
54 style(format!("[{step}/{total}]")).dim(),
55 style("Authentication").bold()
56 );
57 println!();
58 println!(
59 "{}",
60 style("These are Linux system credentials created inside the container. The web").dim()
61 );
62 println!(
63 "{}",
64 style("interface uses PAM to authenticate against them, and they also work for SSH").dim()
65 );
66 println!(
67 "{}",
68 style("or any PAM-enabled service. Passwords are SHA-512 hashed via chpasswd and").dim()
69 );
70 println!(
71 "{}",
72 style("stored in /etc/shadow. Your config file only stores usernames, never passwords.")
73 .dim()
74 );
75 println!();
76
77 loop {
78 let options = vec![
80 "Generate secure random credentials",
81 "Enter my own username and password",
82 ];
83
84 let selection = Select::new()
85 .with_prompt("How would you like to set credentials?")
86 .items(&options)
87 .default(0)
88 .interact()
89 .map_err(|_| handle_interrupt())?;
90
91 match selection {
92 0 => {
93 let password = generate_random_password();
95
96 println!();
97 println!("{}", style("Generated credentials:").green());
98 println!(" Username: {}", style("admin").cyan());
99 println!(" Password: {}", style(&password).cyan());
100 println!();
101 println!(
102 "{}",
103 style("Save these credentials securely - the password won't be shown again.")
104 .yellow()
105 );
106 println!();
107
108 let use_these = Confirm::new()
109 .with_prompt("Use these credentials?")
110 .default(true)
111 .interact()
112 .map_err(|_| handle_interrupt())?;
113
114 if use_these {
115 return Ok(("admin".to_string(), password));
116 }
117 println!();
119 }
120 1 => {
121 println!();
123
124 let username: String = Input::new()
125 .with_prompt("Username")
126 .validate_with(|input: &String| validate_username(input))
127 .interact_text()
128 .map_err(|_| handle_interrupt())?;
129
130 let password = Password::new()
131 .with_prompt("Password")
132 .with_confirmation("Confirm password", "Passwords do not match")
133 .interact()
134 .map_err(|_| handle_interrupt())?;
135
136 if password.is_empty() {
137 println!("{}", style("Password cannot be empty").red());
138 println!();
139 continue;
140 }
141
142 return Ok((username, password));
143 }
144 _ => unreachable!(),
145 }
146 }
147}
148
149pub async fn create_container_user(
154 client: &DockerClient,
155 username: &str,
156 password: &str,
157) -> Result<()> {
158 if user_exists(client, CONTAINER_NAME, username).await? {
160 println!(" User '{username}' exists, updating password...");
162 } else {
163 println!(" Creating user '{username}' in container...");
165 create_user(client, CONTAINER_NAME, username).await?;
166 }
167
168 set_user_password(client, CONTAINER_NAME, username, password).await?;
170 println!(" Password set for '{username}'");
171
172 Ok(())
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn test_validate_username_valid() {
181 assert!(validate_username("admin").is_ok());
182 assert!(validate_username("user_123").is_ok());
183 assert!(validate_username("ABC").is_ok());
184 assert!(validate_username("a_b_c_d_e_f_g_h_i_j_k_l_m_n_").is_ok()); }
186
187 #[test]
188 fn test_validate_username_empty() {
189 assert!(validate_username("").is_err());
190 }
191
192 #[test]
193 fn test_validate_username_too_short() {
194 assert!(validate_username("ab").is_err());
195 }
196
197 #[test]
198 fn test_validate_username_too_long() {
199 let long = "a".repeat(33);
200 assert!(validate_username(&long).is_err());
201 }
202
203 #[test]
204 fn test_validate_username_invalid_chars() {
205 assert!(validate_username("user@name").is_err());
206 assert!(validate_username("user-name").is_err());
207 assert!(validate_username("user name").is_err());
208 }
209
210 #[test]
211 fn test_generate_random_password_length() {
212 let password = generate_random_password();
213 assert_eq!(password.len(), 24);
214 }
215
216 #[test]
217 fn test_generate_random_password_alphanumeric() {
218 let password = generate_random_password();
219 assert!(password.chars().all(|c| c.is_alphanumeric()));
220 }
221
222 #[test]
223 fn test_generate_random_password_uniqueness() {
224 let p1 = generate_random_password();
226 let p2 = generate_random_password();
227 let p3 = generate_random_password();
228 assert_ne!(p1, p2);
229 assert_ne!(p2, p3);
230 assert_ne!(p1, p3);
231 }
232}