Skip to main content

wekan_cli/commands/auth/
register.rs

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    /// Username for the new account.
25    #[arg(
26        long,
27        value_parser = non_empty_identity,
28        required_unless_present = "email"
29    )]
30    pub username: Option<String>,
31
32    /// Email address for the new account.
33    #[arg(
34        long,
35        value_parser = non_empty_identity,
36        required_unless_present = "username"
37    )]
38    pub email: Option<String>,
39
40    /// Read the password from one line of standard input.
41    #[arg(long)]
42    pub password_stdin: bool,
43
44    /// Read the exact password through EOF; empty input creates a passwordless account.
45    #[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;