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;