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
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;