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 ClientError::Build(_) => AppError::new(
94 ErrorCode::InternalError,
95 "the HTTP client could not be initialized",
96 StableExitCode::Internal,
97 ),
98 ClientError::Transport(_) => AppError::new(
99 ErrorCode::TransportError,
100 "the registration request failed; its remote outcome may be unknown, so check the server before retrying",
101 StableExitCode::Transport,
102 )
103 .with_details(ErrorDetails {
104 outcome_unknown: Some(true),
105 ..ErrorDetails::default()
106 }),
107 ClientError::UnexpectedRedirect { status } => AppError::new(
108 ErrorCode::UnexpectedRedirect,
109 "the server redirected registration; configure the canonical Wekan URL because registration requests are never replayed",
110 StableExitCode::Transport,
111 )
112 .with_details(ErrorDetails {
113 http_status: Some(status.as_u16()),
114 ..ErrorDetails::default()
115 }),
116 ClientError::ResponseTooLarge {
117 limit_bytes,
118 status,
119 retry_after_seconds,
120 } => response_body_error(
121 format!("the server response exceeded the {limit_bytes}-byte safety limit"),
122 status,
123 retry_after_seconds,
124 ),
125 ClientError::ResponseBody {
126 status,
127 retry_after_seconds,
128 ..
129 } => response_body_error(
130 "the server response body could not be read",
131 status,
132 retry_after_seconds,
133 ),
134 ClientError::Protocol {
135 diagnostic,
136 message,
137 success_status_received,
138 } => {
139 let mut details = protocol_diagnostic_details(success_status_received, diagnostic, redactor);
140 details.account_created = Some(success_status_received);
141 AppError::new(
142 ErrorCode::ProtocolError,
143 redactor.redact(&format!("invalid response from Wekan: {message}")),
144 StableExitCode::Transport,
145 )
146 .with_details(details)
147 }
148 ClientError::EmbeddedProtocol {
149 http_status,
150 server_error,
151 server_reason,
152 server_message,
153 server_error_type,
154 server_is_client_safe,
155 } => {
156 let mut details = embedded_protocol_error_details(
157 http_status,
158 server_error,
159 server_reason,
160 server_message,
161 server_error_type,
162 server_is_client_safe,
163 redactor,
164 );
165 details.outcome_unknown = Some(true);
166 AppError::new(
167 ErrorCode::ProtocolError,
168 "Wekan returned an embedded registration error without statusCode; the remote outcome may be unknown",
169 StableExitCode::Server,
170 )
171 .with_details(details)
172 }
173 ClientError::Server {
174 status,
175 server_error,
176 server_reason,
177 retry_after_seconds,
178 } => {
179 let (code, message, outcome_unknown) = match status.as_u16() {
180 400 => (
181 ErrorCode::RegistrationRejected,
182 "Wekan returned a registration error; the remote outcome may be unknown, so check the server before retrying",
183 Some(true),
184 ),
185 403 => (
186 ErrorCode::RegistrationDisabled,
187 "registration is disabled on the Wekan server",
188 None,
189 ),
190 _ if status.is_server_error() => (
191 ErrorCode::ServerError,
192 "the Wekan server returned an unexpected error; the remote outcome may be unknown, so check the server before retrying",
193 Some(true),
194 ),
195 _ => (
196 ErrorCode::ServerError,
197 "the Wekan server returned an unexpected error",
198 None,
199 ),
200 };
201 let mut details = server_error_details(
202 status,
203 server_error,
204 server_reason,
205 retry_after_seconds,
206 redactor,
207 );
208 details.outcome_unknown = outcome_unknown;
209 AppError::new(code, message, StableExitCode::Server).with_details(details)
210 }
211 ClientError::EmbeddedServer {
212 http_status,
213 wekan_status_code,
214 server_error,
215 server_reason,
216 } => AppError::new(
217 ErrorCode::ServerError,
218 "the Wekan server returned an unexpected embedded registration error",
219 StableExitCode::Server,
220 )
221 .with_details({
222 let mut details = embedded_server_error_details(
223 http_status,
224 wekan_status_code,
225 server_error,
226 server_reason,
227 redactor,
228 );
229 details.outcome_unknown = Some(true);
230 details
231 }),
232 }
233}
234
235fn response_body_error(
236 message: impl Into<String>,
237 status: reqwest::StatusCode,
238 retry_after_seconds: Option<u64>,
239) -> AppError {
240 let mut message = message.into();
241 let mut details = response_error_details(status, retry_after_seconds);
242 if status == reqwest::StatusCode::OK {
243 details.account_created = Some(true);
244 return AppError::new(ErrorCode::ProtocolError, message, StableExitCode::Transport)
245 .with_details(details);
246 }
247
248 let (code, exit_code, outcome_unknown) = match status.as_u16() {
249 400 => (
250 ErrorCode::RegistrationRejected,
251 StableExitCode::Server,
252 Some(true),
253 ),
254 403 => (
255 ErrorCode::RegistrationDisabled,
256 StableExitCode::Server,
257 None,
258 ),
259 _ if status.is_server_error() => {
260 (ErrorCode::ServerError, StableExitCode::Server, Some(true))
261 }
262 _ => (ErrorCode::ServerError, StableExitCode::Server, None),
263 };
264
265 if outcome_unknown.is_some() {
266 message
267 .push_str("; the remote outcome may be unknown, so check the server before retrying");
268 }
269
270 let message = if status == reqwest::StatusCode::FORBIDDEN {
271 "registration is disabled on the Wekan server".to_owned()
272 } else {
273 message
274 };
275
276 details.outcome_unknown = outcome_unknown;
277 AppError::new(code, message, exit_code).with_details(details)
278}
279
280#[cfg(test)]
281mod tests;