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