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