1use clap::Args;
2
3use crate::commands::{
4 client_error::{
5 embedded_protocol_error_details, embedded_server_error_details, protocol_error_details,
6 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 message,
128 success_status_received,
129 } => {
130 let mut details = protocol_error_details(success_status_received);
131 details.account_created = Some(success_status_received);
132 AppError::new(
133 ErrorCode::ProtocolError,
134 redactor.redact(&format!("invalid response from Wekan: {message}")),
135 StableExitCode::Transport,
136 )
137 .with_details(details)
138 }
139 ClientError::EmbeddedProtocol {
140 http_status,
141 server_error,
142 server_reason,
143 server_message,
144 server_error_type,
145 server_is_client_safe,
146 } => {
147 let mut details = embedded_protocol_error_details(
148 http_status,
149 server_error,
150 server_reason,
151 server_message,
152 server_error_type,
153 server_is_client_safe,
154 redactor,
155 );
156 details.outcome_unknown = Some(true);
157 AppError::new(
158 ErrorCode::ProtocolError,
159 "Wekan returned an embedded registration error without statusCode; the remote outcome may be unknown",
160 StableExitCode::Server,
161 )
162 .with_details(details)
163 }
164 ClientError::Server {
165 status,
166 server_error,
167 server_reason,
168 retry_after_seconds,
169 } => {
170 let (code, message, outcome_unknown) = match status.as_u16() {
171 400 => (
172 ErrorCode::RegistrationRejected,
173 "Wekan returned a registration error; the remote outcome may be unknown, so check the server before retrying",
174 Some(true),
175 ),
176 403 => (
177 ErrorCode::RegistrationDisabled,
178 "registration is disabled on the Wekan server",
179 None,
180 ),
181 _ if status.is_server_error() => (
182 ErrorCode::ServerError,
183 "the Wekan server returned an unexpected error; the remote outcome may be unknown, so check the server before retrying",
184 Some(true),
185 ),
186 _ => (
187 ErrorCode::ServerError,
188 "the Wekan server returned an unexpected error",
189 None,
190 ),
191 };
192 let mut details = server_error_details(
193 status,
194 server_error,
195 server_reason,
196 retry_after_seconds,
197 redactor,
198 );
199 details.outcome_unknown = outcome_unknown;
200 AppError::new(code, message, StableExitCode::Server).with_details(details)
201 }
202 ClientError::EmbeddedServer {
203 http_status,
204 wekan_status_code,
205 server_error,
206 server_reason,
207 } => AppError::new(
208 ErrorCode::ServerError,
209 "the Wekan server returned an unexpected embedded registration error",
210 StableExitCode::Server,
211 )
212 .with_details({
213 let mut details = embedded_server_error_details(
214 http_status,
215 wekan_status_code,
216 server_error,
217 server_reason,
218 redactor,
219 );
220 details.outcome_unknown = Some(true);
221 details
222 }),
223 }
224}
225
226fn response_body_error(
227 message: impl Into<String>,
228 status: reqwest::StatusCode,
229 retry_after_seconds: Option<u64>,
230) -> AppError {
231 let mut message = message.into();
232 let mut details = response_error_details(status, retry_after_seconds);
233 if status == reqwest::StatusCode::OK {
234 details.account_created = Some(true);
235 return AppError::new(ErrorCode::ProtocolError, message, StableExitCode::Transport)
236 .with_details(details);
237 }
238
239 let (code, exit_code, outcome_unknown) = match status.as_u16() {
240 400 => (
241 ErrorCode::RegistrationRejected,
242 StableExitCode::Server,
243 Some(true),
244 ),
245 403 => (
246 ErrorCode::RegistrationDisabled,
247 StableExitCode::Server,
248 None,
249 ),
250 _ if status.is_server_error() => {
251 (ErrorCode::ServerError, StableExitCode::Server, Some(true))
252 }
253 _ => (ErrorCode::ServerError, StableExitCode::Server, None),
254 };
255
256 if outcome_unknown.is_some() {
257 message
258 .push_str("; the remote outcome may be unknown, so check the server before retrying");
259 }
260
261 let message = if status == reqwest::StatusCode::FORBIDDEN {
262 "registration is disabled on the Wekan server".to_owned()
263 } else {
264 message
265 };
266
267 details.outcome_unknown = outcome_unknown;
268 AppError::new(code, message, exit_code).with_details(details)
269}
270
271#[cfg(test)]
272mod tests;