1pub mod login;
2pub mod logout;
3pub mod register;
4pub mod status;
5
6use clap::{Args, Subcommand};
7use time::format_description::well_known::Rfc3339;
8
9use crate::{
10 client::AuthSession,
11 command_result::{AuthSuccess, CommandSuccess},
12 config::{MissingProfileResolution, ResolvedTarget},
13 credentials::{
14 CredentialCreateOutcome, CredentialMutation, CredentialRecord, CredentialStore,
15 SecretInputProvider,
16 },
17 error::{AppError, ErrorCode},
18 exit_code::StableExitCode,
19 input::ConfirmationProvider,
20 redaction::Redactor,
21};
22
23use super::credential_ops::map_credential_load_error;
24
25#[derive(Debug, Args)]
26pub struct AuthArgs {
27 #[command(subcommand)]
28 pub command: AuthCommand,
29}
30
31#[derive(Debug, Subcommand)]
32pub enum AuthCommand {
33 Login(login::LoginArgs),
35
36 Logout(logout::LogoutArgs),
38
39 Register(register::RegisterArgs),
41
42 Status(status::StatusArgs),
44}
45
46impl AuthCommand {
47 pub(crate) const fn missing_profile_resolution(&self) -> MissingProfileResolution {
48 match self {
49 Self::Login(_) | Self::Register(_) => MissingProfileResolution::Initialize,
50 Self::Logout(args) if args.local_only => {
51 MissingProfileResolution::LocalCredentialCleanup
52 }
53 Self::Logout(_) | Self::Status(_) => MissingProfileResolution::Reject,
54 }
55 }
56}
57
58pub(crate) async fn dispatch(
59 command: AuthCommand,
60 target: &mut ResolvedTarget<'_>,
61 credential_store: &dyn CredentialStore,
62 secret_input: &dyn SecretInputProvider,
63 confirmation: &dyn ConfirmationProvider,
64) -> Result<CommandSuccess, AppError> {
65 match command {
66 AuthCommand::Login(args) => {
67 login::execute(args, target, credential_store, secret_input).await
68 }
69 AuthCommand::Register(args) => {
70 register::execute(args, target, credential_store, secret_input).await
71 }
72 AuthCommand::Logout(args) => {
73 logout::execute(
74 args,
75 target.client_factory(),
76 credential_store,
77 confirmation,
78 )
79 .await
80 }
81 AuthCommand::Status(args) => {
82 status::execute(args, target.client_factory(), credential_store).await
83 }
84 }
85}
86
87pub(super) fn persist_session(
88 target: &mut ResolvedTarget<'_>,
89 credential_mutation: &dyn CredentialMutation,
90 server_url: String,
91 session: AuthSession,
92) -> Result<AuthSuccess, AppError> {
93 let (user_id, token, token_expires) = session.into_parts();
94 let token_expires_text = token_expires.format(&Rfc3339).map_err(|_| {
95 AppError::new(
96 ErrorCode::InternalError,
97 "the validated token expiry could not be formatted",
98 StableExitCode::Internal,
99 )
100 })?;
101 let record = CredentialRecord::new(server_url.clone(), user_id.clone(), token, token_expires);
102 let token_redactor = Redactor::with_secret(record.token());
103
104 target.initialize_profile()?;
105 let profile_created = target.profile_created();
106 let profile_active = target.profile_active();
107 match credential_mutation.create(&record).map_err(|error| {
108 AppError::new(
109 ErrorCode::CredentialStoreFailed,
110 token_redactor.redact(&error.to_string()),
111 StableExitCode::Credential,
112 )
113 .with_profile_state(profile_created, profile_active)
114 })? {
115 CredentialCreateOutcome::Created => {}
116 CredentialCreateOutcome::AlreadyExists => {
117 return Err(AppError::new(
118 ErrorCode::CredentialAlreadyExists,
119 format!(
120 "profile `{}` acquired a stored credential concurrently; the existing entry was preserved, and the remote session or account may already have been created; inspect Wekan, then run `wekan --server {} --profile {} auth logout --local-only --yes` before authenticating again",
121 target.profile(),
122 server_url,
123 target.profile()
124 ),
125 StableExitCode::Configuration,
126 )
127 .with_credential_stored(true)
128 .with_profile_state(profile_created, profile_active));
129 }
130 }
131
132 Ok(AuthSuccess {
133 server: server_url,
134 profile: target.profile().to_owned(),
135 user_id,
136 token_expires: token_expires_text,
137 credential_stored: true,
138 profile_created,
139 profile_active,
140 })
141}
142
143pub(super) fn ensure_credential_absent(
144 credential_mutation: &dyn CredentialMutation,
145 target: &ResolvedTarget<'_>,
146) -> Result<(), AppError> {
147 let exists = credential_mutation
148 .exists()
149 .map_err(map_credential_load_error)?;
150 if exists {
151 Err(credential_already_exists(target))
152 } else {
153 Ok(())
154 }
155}
156
157fn credential_already_exists(target: &ResolvedTarget<'_>) -> AppError {
158 AppError::new(
159 ErrorCode::CredentialAlreadyExists,
160 format!(
161 "profile `{}` already has a stored credential; run `wekan --server {} --profile {} auth logout --local-only --yes` before authenticating again",
162 target.profile(),
163 target.client_factory().server_identity().as_str(),
164 target.profile()
165 ),
166 StableExitCode::Configuration,
167 )
168 .with_credential_stored(true)
169 .with_profile_state(target.profile_created(), target.profile_active())
170}
171
172pub(super) fn non_empty_identity(value: &str) -> Result<String, String> {
173 if value.is_empty() {
174 Err("value must not be empty".to_owned())
175 } else {
176 Ok(value.to_owned())
177 }
178}