Skip to main content

wekan_cli/commands/auth/
status.rs

1use clap::Args;
2use reqwest::StatusCode;
3
4use crate::commands::{
5    authenticated::AuthenticatedContext,
6    client_error::{
7        embedded_protocol_error_details, embedded_server_error_details,
8        protocol_diagnostic_details, response_error_details, server_error_details,
9    },
10};
11use crate::{
12    client::{ClientError, WekanClientFactory},
13    command_result::{AuthStatusEmail, AuthStatusSuccess, AuthStatusUser, CommandSuccess},
14    credentials::CredentialStore,
15    error::{AppError, ErrorCode, ErrorDetails},
16    exit_code::StableExitCode,
17    redaction::Redactor,
18};
19
20const REJECTED_CREDENTIAL_MESSAGE: &str = "the stored credential was rejected by Wekan; remove it with `wekan auth logout --local-only`, then log in again to create a new session";
21
22#[derive(Debug, Args)]
23pub struct StatusArgs {}
24
25pub(crate) async fn execute(
26    _args: StatusArgs,
27    client_factory: &WekanClientFactory,
28    credential_store: &dyn CredentialStore,
29) -> Result<CommandSuccess, AppError> {
30    let context = AuthenticatedContext::load(client_factory, credential_store)?;
31    let redactor = Redactor::with_secret(context.record().token());
32    let current_user = context
33        .client()
34        .current_user(context.record().token())
35        .await
36        .map_err(|error| map_client_error(error, &redactor))?;
37
38    if current_user.user_id() != context.record().user_id() {
39        return Err(AppError::new(
40            ErrorCode::CredentialStoreFailed,
41            "the stored credential user id did not match the authenticated Wekan user",
42            StableExitCode::Credential,
43        ));
44    }
45
46    let (user_id, username, full_name, is_admin, emails) = current_user.into_auth_parts();
47    Ok(CommandSuccess::AuthStatus(AuthStatusSuccess {
48        server: context.server().to_owned(),
49        profile: context.profile().to_owned(),
50        authenticated: true,
51        token_expires: context.token_expires().to_owned(),
52        credential_stored: true,
53        user: AuthStatusUser {
54            user_id,
55            username,
56            full_name,
57            is_admin,
58            emails: emails
59                .into_iter()
60                .map(|email| {
61                    let (address, verified) = email.into_parts();
62                    AuthStatusEmail { address, verified }
63                })
64                .collect(),
65        },
66    }))
67}
68
69fn map_client_error(error: ClientError, redactor: &Redactor) -> AppError {
70    match error {
71        ClientError::Build(_) => AppError::new(
72            ErrorCode::InternalError,
73            "the HTTP client could not be initialized",
74            StableExitCode::Internal,
75        ),
76        ClientError::Transport(error) => AppError::new(
77            ErrorCode::TransportError,
78            redactor.redact(&format!(
79                "the authentication status request could not be completed: {error}"
80            )),
81            StableExitCode::Transport,
82        ),
83        ClientError::UnexpectedRedirect { status } => AppError::new(
84            ErrorCode::UnexpectedRedirect,
85            "the Wekan server returned a redirect; redirects are not followed for authenticated requests",
86            StableExitCode::Transport,
87        )
88        .with_details(ErrorDetails {
89            http_status: Some(status.as_u16()),
90            ..ErrorDetails::default()
91        }),
92        ClientError::ResponseTooLarge {
93            limit_bytes,
94            status,
95            retry_after_seconds,
96        } => response_body_error(
97            format!("the server response exceeded the {limit_bytes}-byte safety limit"),
98            status,
99            retry_after_seconds,
100        ),
101        ClientError::ResponseBody {
102            status,
103            retry_after_seconds,
104            ..
105        } => response_body_error(
106            "the server response body could not be read".to_owned(),
107            status,
108            retry_after_seconds,
109        ),
110        ClientError::Protocol {
111            diagnostic,
112            message,
113            success_status_received,
114        } => AppError::new(
115            ErrorCode::ProtocolError,
116            redactor.redact(&format!("invalid response from Wekan: {message}")),
117            StableExitCode::Transport,
118        )
119        .with_details(protocol_diagnostic_details(success_status_received, diagnostic, redactor)),
120        ClientError::EmbeddedProtocol {
121            http_status,
122            server_error,
123            server_reason,
124            server_message,
125            server_error_type,
126            server_is_client_safe,
127        } => AppError::new(
128            ErrorCode::ProtocolError,
129            "Wekan returned an embedded authentication-status error without statusCode",
130            StableExitCode::Server,
131        )
132        .with_details(embedded_protocol_error_details(
133            http_status,
134            server_error,
135            server_reason,
136            server_message,
137            server_error_type,
138            server_is_client_safe,
139            redactor,
140        )),
141        ClientError::Server {
142            status,
143            server_error,
144            server_reason,
145            retry_after_seconds,
146        } => {
147            let authentication_rejected = status == StatusCode::UNAUTHORIZED;
148            AppError::new(
149                if authentication_rejected {
150                    ErrorCode::AuthenticationRejected
151                } else {
152                    ErrorCode::ServerError
153                },
154                if authentication_rejected {
155                    REJECTED_CREDENTIAL_MESSAGE
156                } else {
157                    "the Wekan server returned an unexpected error while checking authentication status"
158                },
159                StableExitCode::Server,
160            )
161            .with_details(server_error_details(
162                status,
163                server_error,
164                server_reason,
165                retry_after_seconds,
166                redactor,
167            ))
168        }
169        ClientError::EmbeddedServer {
170            http_status,
171            wekan_status_code,
172            server_error,
173            server_reason,
174        } => {
175            let authentication_rejected = wekan_status_code == StatusCode::UNAUTHORIZED.as_u16();
176            AppError::new(
177                if authentication_rejected {
178                    ErrorCode::AuthenticationRejected
179                } else {
180                    ErrorCode::ServerError
181                },
182                if authentication_rejected {
183                    REJECTED_CREDENTIAL_MESSAGE
184                } else {
185                    "the Wekan server returned an embedded error while checking authentication status"
186                },
187                StableExitCode::Server,
188            )
189            .with_details(embedded_server_error_details(
190                http_status,
191                wekan_status_code,
192                server_error,
193                server_reason,
194                redactor,
195            ))
196        }
197    }
198}
199
200fn response_body_error(
201    message: String,
202    status: StatusCode,
203    retry_after_seconds: Option<u64>,
204) -> AppError {
205    let details = response_error_details(status, retry_after_seconds);
206    if status == StatusCode::OK {
207        return AppError::new(ErrorCode::ProtocolError, message, StableExitCode::Transport)
208            .with_details(details);
209    }
210
211    let authentication_rejected = status == StatusCode::UNAUTHORIZED;
212    AppError::new(
213        if authentication_rejected {
214            ErrorCode::AuthenticationRejected
215        } else {
216            ErrorCode::ServerError
217        },
218        if authentication_rejected {
219            REJECTED_CREDENTIAL_MESSAGE.to_owned()
220        } else {
221            message
222        },
223        StableExitCode::Server,
224    )
225    .with_details(details)
226}
227
228#[cfg(test)]
229mod tests;