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;