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
45pub(crate) async fn execute(
46 args: RegisterArgs,
47 target: &mut ResolvedTarget<'_>,
48 credential_store: &dyn CredentialStore,
49 secret_input: &dyn SecretInputProvider,
50) -> Result<CommandSuccess, AppError> {
51 let client = target.client_factory().create().map_err(AppError::from)?;
52 let server_url = client.server().as_str().to_owned();
53 let credential_target = credential_target(
54 target.profile(),
55 target.credential_namespace(),
56 server_url.clone(),
57 );
58
59 preflight_credentials(credential_store, &credential_target)
60 .map_err(|error| error.with_account_created(false))?;
61 let credential_mutation = lock_credential_mutation(credential_store, &credential_target)
62 .map_err(|error| error.with_account_created(false))?;
63 ensure_credential_absent(credential_mutation.as_ref(), target)
64 .map_err(|error| error.with_account_created(false))?;
65
66 let password = secret_input.read_new_account_password(args.password_stdin)?;
67 let request = RegisterRequest {
68 username: args.username,
69 email: args.email,
70 password,
71 };
72 let redactor = Redactor::with_secret(&request.password);
73 let session = client
74 .register(&request)
75 .await
76 .map_err(|error| map_client_error(error, &redactor))?;
77
78 let success = persist_session(target, credential_mutation.as_ref(), server_url, session)
79 .map_err(|error| error.with_account_created(true))?;
80 Ok(CommandSuccess::Registration(success))
81}
82
83fn map_client_error(error: ClientError, redactor: &Redactor<'_>) -> AppError {
84 match error {
85 ClientError::Build(_) => AppError::new(
86 ErrorCode::InternalError,
87 "the HTTP client could not be initialized",
88 StableExitCode::Internal,
89 ),
90 ClientError::Transport(_) => AppError::new(
91 ErrorCode::TransportError,
92 "the registration request failed; its remote outcome may be unknown, so check the server before retrying",
93 StableExitCode::Transport,
94 )
95 .with_details(ErrorDetails {
96 outcome_unknown: Some(true),
97 ..ErrorDetails::default()
98 }),
99 ClientError::UnexpectedRedirect { status } => AppError::new(
100 ErrorCode::UnexpectedRedirect,
101 "the server redirected registration; configure the canonical Wekan URL because registration requests are never replayed",
102 StableExitCode::Transport,
103 )
104 .with_details(ErrorDetails {
105 http_status: Some(status.as_u16()),
106 ..ErrorDetails::default()
107 }),
108 ClientError::ResponseTooLarge {
109 limit_bytes,
110 status,
111 retry_after_seconds,
112 } => response_body_error(
113 format!("the server response exceeded the {limit_bytes}-byte safety limit"),
114 status,
115 retry_after_seconds,
116 ),
117 ClientError::ResponseBody {
118 status,
119 retry_after_seconds,
120 ..
121 } => response_body_error(
122 "the server response body could not be read",
123 status,
124 retry_after_seconds,
125 ),
126 ClientError::Protocol {
127 diagnostic,
128 message,
129 success_status_received,
130 } => {
131 let mut details = protocol_diagnostic_details(success_status_received, diagnostic, redactor);
132 details.account_created = Some(success_status_received);
133 AppError::new(
134 ErrorCode::ProtocolError,
135 redactor.redact(&format!("invalid response from Wekan: {message}")),
136 StableExitCode::Transport,
137 )
138 .with_details(details)
139 }
140 ClientError::EmbeddedProtocol {
141 http_status,
142 server_error,
143 server_reason,
144 server_message,
145 server_error_type,
146 server_is_client_safe,
147 } => {
148 let mut details = embedded_protocol_error_details(
149 http_status,
150 server_error,
151 server_reason,
152 server_message,
153 server_error_type,
154 server_is_client_safe,
155 redactor,
156 );
157 details.outcome_unknown = Some(true);
158 AppError::new(
159 ErrorCode::ProtocolError,
160 "Wekan returned an embedded registration error without statusCode; the remote outcome may be unknown",
161 StableExitCode::Server,
162 )
163 .with_details(details)
164 }
165 ClientError::Server {
166 status,
167 server_error,
168 server_reason,
169 retry_after_seconds,
170 } => {
171 let (code, message, outcome_unknown) = match status.as_u16() {
172 400 => (
173 ErrorCode::RegistrationRejected,
174 "Wekan returned a registration error; the remote outcome may be unknown, so check the server before retrying",
175 Some(true),
176 ),
177 403 => (
178 ErrorCode::RegistrationDisabled,
179 "registration is disabled on the Wekan server",
180 None,
181 ),
182 _ if status.is_server_error() => (
183 ErrorCode::ServerError,
184 "the Wekan server returned an unexpected error; the remote outcome may be unknown, so check the server before retrying",
185 Some(true),
186 ),
187 _ => (
188 ErrorCode::ServerError,
189 "the Wekan server returned an unexpected error",
190 None,
191 ),
192 };
193 let mut details = server_error_details(
194 status,
195 server_error,
196 server_reason,
197 retry_after_seconds,
198 redactor,
199 );
200 details.outcome_unknown = outcome_unknown;
201 AppError::new(code, message, StableExitCode::Server).with_details(details)
202 }
203 ClientError::EmbeddedServer {
204 http_status,
205 wekan_status_code,
206 server_error,
207 server_reason,
208 } => AppError::new(
209 ErrorCode::ServerError,
210 "the Wekan server returned an unexpected embedded registration error",
211 StableExitCode::Server,
212 )
213 .with_details({
214 let mut details = embedded_server_error_details(
215 http_status,
216 wekan_status_code,
217 server_error,
218 server_reason,
219 redactor,
220 );
221 details.outcome_unknown = Some(true);
222 details
223 }),
224 }
225}
226
227fn response_body_error(
228 message: impl Into<String>,
229 status: reqwest::StatusCode,
230 retry_after_seconds: Option<u64>,
231) -> AppError {
232 let mut message = message.into();
233 let mut details = response_error_details(status, retry_after_seconds);
234 if status == reqwest::StatusCode::OK {
235 details.account_created = Some(true);
236 return AppError::new(ErrorCode::ProtocolError, message, StableExitCode::Transport)
237 .with_details(details);
238 }
239
240 let (code, exit_code, outcome_unknown) = match status.as_u16() {
241 400 => (
242 ErrorCode::RegistrationRejected,
243 StableExitCode::Server,
244 Some(true),
245 ),
246 403 => (
247 ErrorCode::RegistrationDisabled,
248 StableExitCode::Server,
249 None,
250 ),
251 _ if status.is_server_error() => {
252 (ErrorCode::ServerError, StableExitCode::Server, Some(true))
253 }
254 _ => (ErrorCode::ServerError, StableExitCode::Server, None),
255 };
256
257 if outcome_unknown.is_some() {
258 message
259 .push_str("; the remote outcome may be unknown, so check the server before retrying");
260 }
261
262 let message = if status == reqwest::StatusCode::FORBIDDEN {
263 "registration is disabled on the Wekan server".to_owned()
264 } else {
265 message
266 };
267
268 details.outcome_unknown = outcome_unknown;
269 AppError::new(code, message, exit_code).with_details(details)
270}
271
272#[cfg(test)]
273mod tests;