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
59 loop {
60 let options = vec![
62 "Generate secure random credentials",
63 "Enter my own username and password",
64 ];
65
66 let selection = Select::new()
67 .with_prompt("How would you like to set credentials?")
68 .items(&options)
69 .default(0)
70 .interact()
71 .map_err(|_| handle_interrupt())?;
72
73 match selection {
74 0 => {
75 let password = generate_random_password();
77
78 println!();
79 println!("{}", style("Generated credentials:").green());
80 println!(" Username: {}", style("admin").cyan());
81 println!(" Password: {}", style(&password).cyan());
82 println!();
83 println!(
84 "{}",
85 style("Save these credentials securely - the password won't be shown again.")
86 .yellow()
87 );
88 println!();
89
90 let use_these = Confirm::new()
91 .with_prompt("Use these credentials?")
92 .default(true)
93 .interact()
94 .map_err(|_| handle_interrupt())?;
95
96 if use_these {
97 return Ok(("admin".to_string(), password));
98 }
99 println!();
101 }
102 1 => {
103 println!();
105
106 let username: String = Input::new()
107 .with_prompt("Username")
108 .validate_with(|input: &String| validate_username(input))
109 .interact_text()
110 .map_err(|_| handle_interrupt())?;
111
112 let password = Password::new()
113 .with_prompt("Password")
114 .with_confirmation("Confirm password", "Passwords do not match")
115 .interact()
116 .map_err(|_| handle_interrupt())?;
117
118 if password.is_empty() {
119 println!("{}", style("Password cannot be empty").red());
120 println!();
121 continue;
122 }
123
124 return Ok((username, password));
125 }
126 _ => unreachable!(),
127 }
128 }
129}
130
131pub async fn create_container_user(
136 client: &DockerClient,
137 username: &str,
138 password: &str,
139) -> Result<()> {
140 if user_exists(client, CONTAINER_NAME, username).await? {
142 println!(" User '{username}' exists, updating password...");
144 } else {
145 println!(" Creating user '{username}' in container...");
147 create_user(client, CONTAINER_NAME, username).await?;
148 }
149
150 set_user_password(client, CONTAINER_NAME, username, password).await?;
152 println!(" Password set for '{username}'");
153
154 Ok(())
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_validate_username_valid() {
163 assert!(validate_username("admin").is_ok());
164 assert!(validate_username("user_123").is_ok());
165 assert!(validate_username("ABC").is_ok());
166 assert!(validate_username("a_b_c_d_e_f_g_h_i_j_k_l_m_n_").is_ok()); }
168
169 #[test]
170 fn test_validate_username_empty() {
171 assert!(validate_username("").is_err());
172 }
173
174 #[test]
175 fn test_validate_username_too_short() {
176 assert!(validate_username("ab").is_err());
177 }
178
179 #[test]
180 fn test_validate_username_too_long() {
181 let long = "a".repeat(33);
182 assert!(validate_username(&long).is_err());
183 }
184
185 #[test]
186 fn test_validate_username_invalid_chars() {
187 assert!(validate_username("user@name").is_err());
188 assert!(validate_username("user-name").is_err());
189 assert!(validate_username("user name").is_err());
190 }
191
192 #[test]
193 fn test_generate_random_password_length() {
194 let password = generate_random_password();
195 assert_eq!(password.len(), 24);
196 }
197
198 #[test]
199 fn test_generate_random_password_alphanumeric() {
200 let password = generate_random_password();
201 assert!(password.chars().all(|c| c.is_alphanumeric()));
202 }
203
204 #[test]
205 fn test_generate_random_password_uniqueness() {
206 let p1 = generate_random_password();
208 let p2 = generate_random_password();
209 let p3 = generate_random_password();
210 assert_ne!(p1, p2);
211 assert_ne!(p2, p3);
212 assert_ne!(p1, p3);
213 }
214}