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