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;