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, protocol_error_details,
8        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            message,
112            success_status_received,
113        } => AppError::new(
114            ErrorCode::ProtocolError,
115            redactor.redact(&format!("invalid response from Wekan: {message}")),
116            StableExitCode::Transport,
117        )
118        .with_details(protocol_error_details(success_status_received)),
119        ClientError::EmbeddedProtocol {
120            http_status,
121            server_error,
122            server_reason,
123            server_message,
124            server_error_type,
125            server_is_client_safe,
126        } => AppError::new(
127            ErrorCode::ProtocolError,
128            "Wekan returned an embedded authentication-status error without statusCode",
129            StableExitCode::Server,
130        )
131        .with_details(embedded_protocol_error_details(
132            http_status,
133            server_error,
134            server_reason,
135            server_message,
136            server_error_type,
137            server_is_client_safe,
138            redactor,
139        )),
140        ClientError::Server {
141            status,
142            server_error,
143            server_reason,
144            retry_after_seconds,
145        } => {
146            let authentication_rejected = status == StatusCode::UNAUTHORIZED;
147            AppError::new(
148                if authentication_rejected {
149                    ErrorCode::AuthenticationRejected
150                } else {
151                    ErrorCode::ServerError
152                },
153                if authentication_rejected {
154                    REJECTED_CREDENTIAL_MESSAGE
155                } else {
156                    "the Wekan server returned an unexpected error while checking authentication status"
157                },
158                StableExitCode::Server,
159            )
160            .with_details(server_error_details(
161                status,
162                server_error,
163                server_reason,
164                retry_after_seconds,
165                redactor,
166            ))
167        }
168        ClientError::EmbeddedServer {
169            http_status,
170            wekan_status_code,
171            server_error,
172            server_reason,
173        } => {
174            let authentication_rejected = wekan_status_code == StatusCode::UNAUTHORIZED.as_u16();
175            AppError::new(
176                if authentication_rejected {
177                    ErrorCode::AuthenticationRejected
178                } else {
179                    ErrorCode::ServerError
180                },
181                if authentication_rejected {
182                    REJECTED_CREDENTIAL_MESSAGE
183                } else {
184                    "the Wekan server returned an embedded error while checking authentication status"
185                },
186                StableExitCode::Server,
187            )
188            .with_details(embedded_server_error_details(
189                http_status,
190                wekan_status_code,
191                server_error,
192                server_reason,
193                redactor,
194            ))
195        }
196    }
197}
198
199fn response_body_error(
200    message: String,
201    status: StatusCode,
202    retry_after_seconds: Option<u64>,
203) -> AppError {
204    let details = response_error_details(status, retry_after_seconds);
205    if status == StatusCode::OK {
206        return AppError::new(ErrorCode::ProtocolError, message, StableExitCode::Transport)
207            .with_details(details);
208    }
209
210    let authentication_rejected = status == StatusCode::UNAUTHORIZED;
211    AppError::new(
212        if authentication_rejected {
213            ErrorCode::AuthenticationRejected
214        } else {
215            ErrorCode::ServerError
216        },
217        if authentication_rejected {
218            REJECTED_CREDENTIAL_MESSAGE.to_owned()
219        } else {
220            message
221        },
222        StableExitCode::Server,
223    )
224    .with_details(details)
225}
226
227#[cfg(test)]
228mod tests;