1use clap::Args;
2
3use crate::commands::{
4 client_error::{
5 embedded_protocol_error_details, embedded_server_error_details,
6 protocol_diagnostic_details, response_error_details, server_error_details,
7 },
8 credential_ops::{credential_target, lock_credential_mutation, preflight_credentials},
9};
10use crate::{
11 client::{ClientError, RegisterRequest},
12 command_result::CommandSuccess,
13 config::ResolvedTarget,
14 credentials::{CredentialStore, SecretInputProvider},
15 error::{AppError, ErrorCode, ErrorDetails},
16 exit_code::StableExitCode,
17 redaction::Redactor,
18};
19
20use super::{ensure_credential_absent, non_empty_identity, persist_session};
21
22#[derive(Debug, Args)]
23pub struct RegisterArgs {
24 #[arg(
26 long,
27 value_parser = non_empty_identity,
28 required_unless_present = "email"
29 )]
30 pub username: Option<String>,
31
32 #[arg(
34 long,
35 value_parser = non_empty_identity,
36 required_unless_present = "username"
37 )]
38 pub email: Option<String>,
39
40 #[arg(long)]
42 pub password_stdin: bool,
43
44 #[arg(long, conflicts_with = "password_stdin")]
46 pub password_raw_stdin: bool,
47}
48
49pub(crate) async fn execute(
50 args: RegisterArgs,
51 target: &mut ResolvedTarget<'_>,
52 credential_store: &dyn CredentialStore,
53 secret_input: &dyn SecretInputProvider,
54) -> Result<CommandSuccess, AppError> {
55 let client = target.client_factory().create().map_err(AppError::from)?;
56 let server_url = client.server().as_str().to_owned();
57 let credential_target = credential_target(
58 target.profile(),
59 target.credential_namespace(),
60 server_url.clone(),
61 );
62
63 preflight_credentials(credential_store, &credential_target)
64 .map_err(|error| error.with_account_created(false))?;
65 let credential_mutation = lock_credential_mutation(credential_store, &credential_target)
66 .map_err(|error| error.with_account_created(false))?;
67 ensure_credential_absent(credential_mutation.as_ref(), target)
68 .map_err(|error| error.with_account_created(false))?;
69
70 let password = if args.password_raw_stdin {
71 secret_input.read_raw_secret()?
72 } else {
73 secret_input.read_new_account_password(args.password_stdin)?
74 };
75 let request = RegisterRequest {
76 username: args.username,
77 email: args.email,
78 password,
79 };
80 let redactor = Redactor::with_secret(&request.password);
81 let session = client
82 .register(&request)
83 .await
84 .map_err(|error| map_client_error(error, &redactor))?;
85
86 let success = persist_session(target, credential_mutation.as_ref(), server_url, session)
87 .map_err(|error| error.with_account_created(true))?;
88 Ok(CommandSuccess::Registration(success))
89}
90
91fn map_client_error(error: ClientError, redactor: &Redactor<'_>) -> AppError {
92 match error {
93 error @ ClientError::EmptyNotFound { .. } =>
94 crate::commands::client_error::map_client_error(error, redactor, "registration"),
95 ClientError::Build(_) => AppError::new(
96 ErrorCode::InternalError,
97 "the HTTP client could not be initialized",
98 StableExitCode::Internal,
99 ),
100 ClientError::Transport(_) => AppError::new(
101 ErrorCode::TransportError,
102 "the registration request failed",
103 StableExitCode::Transport,
104 ),
105 ClientError::UnexpectedRedirect { status } => AppError::new(
106 ErrorCode::UnexpectedRedirect,
107 "the server redirected registration; configure the canonical Wekan URL because registration requests are never replayed",
108 StableExitCode::Transport,
109 )
110 .with_details(ErrorDetails {
111 http_status: Some(status.as_u16()),
112 ..ErrorDetails::default()
113 }),
114 ClientError::ResponseTooLarge {
115 limit_bytes,
116 status,
117 retry_after_seconds,
118 } => response_body_error(
119 format!("the server response exceeded the {limit_bytes}-byte safety limit"),
120 status,
121 retry_after_seconds,
122 ),
123 ClientError::ResponseBody {
124 status,
125 retry_after_seconds,
126 ..
127 } => response_body_error(
128 "the server response body could not be read",
129 status,
130 retry_after_seconds,
131 ),
132 ClientError::Protocol {
133 diagnostic,
134 message,
135 success_status_received,
136 } => {
137 let mut details = protocol_diagnostic_details(success_status_received, diagnostic, redactor);
138 details.account_created = Some(success_status_received);
139 AppError::new(
140 ErrorCode::ProtocolError,
141 redactor.redact(&format!("invalid response from Wekan: {message}")),
142 StableExitCode::Transport,
143 )
144 .with_details(details)
145 }
146 ClientError::EmbeddedProtocol {
147 http_status,
148 server_error,
149 server_reason,
150 server_message,
151 server_error_type,
152 server_is_client_safe,
153 } => {
154 let details = embedded_protocol_error_details(
155 http_status,
156 server_error,
157 server_reason,
158 server_message,
159 server_error_type,
160 server_is_client_safe,
161 redactor,
162 );
163
164 AppError::new(
165 ErrorCode::ProtocolError,
166 "Wekan returned an embedded registration error without statusCode",
167 StableExitCode::Server,
168 )
169 .with_details(details)
170 }
171 ClientError::Server {
172 status,
173 server_error,
174 server_reason,
175 retry_after_seconds,
176 } => {
177 let (code, message) = match status.as_u16() {
178 400 => (
179 ErrorCode::RegistrationRejected,
180 "Wekan returned a registration error",
181 ),
182 403 => (
183 ErrorCode::RegistrationDisabled,
184 "registration is disabled on the Wekan server",
185 ),
186 _ => (
187 ErrorCode::ServerError,
188 "the Wekan server returned an unexpected error",
189 ),
190 };
191 let details = server_error_details(
192 status,
193 server_error,
194 server_reason,
195 retry_after_seconds,
196 redactor,
197 );
198
199 AppError::new(code, message, StableExitCode::Server).with_details(details)
200 }
201 ClientError::EmbeddedServer {
202 http_status,
203 wekan_status_code,
204 server_error,
205 server_reason,
206 } => AppError::new(
207 ErrorCode::ServerError,
208 "the Wekan server returned an unexpected embedded registration error",
209 StableExitCode::Server,
210 )
211 .with_details(embedded_server_error_details(
212 http_status,
213 wekan_status_code,
214 server_error,
215 server_reason,
216 redactor,
217 )),
218 }
219}
220
221fn response_body_error(
222 message: impl Into<String>,
223 status: reqwest::StatusCode,
224 retry_after_seconds: Option<u64>,
225) -> AppError {
226 let message = message.into();
227 let mut details = response_error_details(status, retry_after_seconds);
228 if status == reqwest::StatusCode::OK {
229 details.account_created = Some(true);
230 return AppError::new(ErrorCode::ProtocolError, message, StableExitCode::Transport)
231 .with_details(details);
232 }
233
234 let (code, exit_code) = match status.as_u16() {
235 400 => (ErrorCode::RegistrationRejected, StableExitCode::Server),
236 403 => (ErrorCode::RegistrationDisabled, StableExitCode::Server),
237 _ => (ErrorCode::ServerError, StableExitCode::Server),
238 };
239
240 let message = if status == reqwest::StatusCode::FORBIDDEN {
241 "registration is disabled on the Wekan server".to_owned()
242 } else {
243 message
244 };
245
246 AppError::new(code, message, exit_code).with_details(details)
247}
248
249#[cfg(test)]
250mod tests;