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        error @ ClientError::EmptyNotFound { .. } =>
72            crate::commands::client_error::map_client_error(error, redactor, "authentication status"),
73        ClientError::Build(_) => AppError::new(
74            ErrorCode::InternalError,
75            "the HTTP client could not be initialized",
76            StableExitCode::Internal,
77        ),
78        ClientError::Transport(error) => AppError::new(
79            ErrorCode::TransportError,
80            redactor.redact(&format!(
81                "the authentication status request could not be completed: {error}"
82            )),
83            StableExitCode::Transport,
84        ),
85        ClientError::UnexpectedRedirect { status } => AppError::new(
86            ErrorCode::UnexpectedRedirect,
87            "the Wekan server returned a redirect; redirects are not followed for authenticated requests",
88            StableExitCode::Transport,
89        )
90        .with_details(ErrorDetails {
91            http_status: Some(status.as_u16()),
92            ..ErrorDetails::default()
93        }),
94        ClientError::ResponseTooLarge {
95            limit_bytes,
96            status,
97            retry_after_seconds,
98        } => response_body_error(
99            format!("the server response exceeded the {limit_bytes}-byte safety limit"),
100            status,
101            retry_after_seconds,
102        ),
103        ClientError::ResponseBody {
104            status,
105            retry_after_seconds,
106            ..
107        } => response_body_error(
108            "the server response body could not be read".to_owned(),
109            status,
110            retry_after_seconds,
111        ),
112        ClientError::Protocol {
113            diagnostic,
114            message,
115            success_status_received,
116        } => AppError::new(
117            ErrorCode::ProtocolError,
118            redactor.redact(&format!("invalid response from Wekan: {message}")),
119            StableExitCode::Transport,
120        )
121        .with_details(protocol_diagnostic_details(success_status_received, diagnostic, redactor)),
122        ClientError::EmbeddedProtocol {
123            http_status,
124            server_error,
125            server_reason,
126            server_message,
127            server_error_type,
128            server_is_client_safe,
129        } => AppError::new(
130            ErrorCode::ProtocolError,
131            "Wekan returned an embedded authentication-status error without statusCode",
132            StableExitCode::Server,
133        )
134        .with_details(embedded_protocol_error_details(
135            http_status,
136            server_error,
137            server_reason,
138            server_message,
139            server_error_type,
140            server_is_client_safe,
141            redactor,
142        )),
143        ClientError::Server {
144            status,
145            server_error,
146            server_reason,
147            retry_after_seconds,
148        } => {
149            let authentication_rejected = status == StatusCode::UNAUTHORIZED;
150            AppError::new(
151                if authentication_rejected {
152                    ErrorCode::AuthenticationRejected
153                } else {
154                    ErrorCode::ServerError
155                },
156                if authentication_rejected {
157                    REJECTED_CREDENTIAL_MESSAGE
158                } else {
159                    "the Wekan server returned an unexpected error while checking authentication status"
160                },
161                StableExitCode::Server,
162            )
163            .with_details(server_error_details(
164                status,
165                server_error,
166                server_reason,
167                retry_after_seconds,
168                redactor,
169            ))
170        }
171        ClientError::EmbeddedServer {
172            http_status,
173            wekan_status_code,
174            server_error,
175            server_reason,
176        } => {
177            let authentication_rejected = wekan_status_code == StatusCode::UNAUTHORIZED.as_u16();
178            AppError::new(
179                if authentication_rejected {
180                    ErrorCode::AuthenticationRejected
181                } else {
182                    ErrorCode::ServerError
183                },
184                if authentication_rejected {
185                    REJECTED_CREDENTIAL_MESSAGE
186                } else {
187                    "the Wekan server returned an embedded error while checking authentication status"
188                },
189                StableExitCode::Server,
190            )
191            .with_details(embedded_server_error_details(
192                http_status,
193                wekan_status_code,
194                server_error,
195                server_reason,
196                redactor,
197            ))
198        }
199    }
200}
201
202fn response_body_error(
203    message: String,
204    status: StatusCode,
205    retry_after_seconds: Option<u64>,
206) -> AppError {
207    let details = response_error_details(status, retry_after_seconds);
208    if status == StatusCode::OK {
209        return AppError::new(ErrorCode::ProtocolError, message, StableExitCode::Transport)
210            .with_details(details);
211    }
212
213    let authentication_rejected = status == StatusCode::UNAUTHORIZED;
214    AppError::new(
215        if authentication_rejected {
216            ErrorCode::AuthenticationRejected
217        } else {
218            ErrorCode::ServerError
219        },
220        if authentication_rejected {
221            REJECTED_CREDENTIAL_MESSAGE.to_owned()
222        } else {
223            message
224        },
225        StableExitCode::Server,
226    )
227    .with_details(details)
228}
229
230#[cfg(test)]
231mod tests;