spotify_cli/cli/commands/
auth.rs

1use super::{init_token_store, load_config};
2use crate::io::output::{ErrorKind, Response};
3use crate::oauth::flow::OAuthFlow;
4use crate::{auth_error, storage_error};
5use tracing::{debug, warn};
6
7pub async fn auth_login(force: bool) -> Response {
8    debug!(force = force, "Starting login flow");
9
10    let config = match load_config() {
11        Ok(c) => c,
12        Err(e) => return e,
13    };
14
15    let token_store = match init_token_store() {
16        Ok(s) => s,
17        Err(e) => return e,
18    };
19
20    if !force && let Ok(token) = token_store.load() {
21        debug!(expired = token.is_expired(), "Found existing token");
22        if !token.is_expired() {
23            return Response::success_with_payload(
24                200,
25                "Already logged in",
26                serde_json::json!({
27                    "expires_in": token.seconds_until_expiry()
28                }),
29            );
30        }
31
32        if let Some(refresh_token) = &token.refresh_token {
33            let flow = OAuthFlow::new(config.client_id().to_string());
34            match flow.refresh(refresh_token).await {
35                Ok(new_token) => {
36                    if let Err(e) = token_store.save(&new_token) {
37                        return storage_error!("Failed to save token", e);
38                    }
39                    return Response::success_with_payload(
40                        200,
41                        "Token refreshed",
42                        serde_json::json!({
43                            "expires_in": new_token.seconds_until_expiry()
44                        }),
45                    );
46                }
47                Err(e) => {
48                    warn!(error = %e, "Token refresh failed, opening browser login");
49                }
50            }
51        }
52    }
53
54    let flow = OAuthFlow::new(config.client_id().to_string());
55    match flow.authenticate().await {
56        Ok(token) => {
57            if let Err(e) = token_store.save(&token) {
58                return storage_error!("Failed to save token", e);
59            }
60            Response::success_with_payload(
61                200,
62                "Login successful",
63                serde_json::json!({
64                    "expires_in": token.seconds_until_expiry()
65                }),
66            )
67        }
68        Err(e) => auth_error!("Login failed", e),
69    }
70}
71
72pub async fn auth_logout() -> Response {
73    let token_store = match init_token_store() {
74        Ok(s) => s,
75        Err(e) => return e,
76    };
77
78    if !token_store.exists() {
79        return Response::success(200, "Already logged out");
80    }
81
82    match token_store.delete() {
83        Ok(_) => Response::success(200, "Logged out successfully"),
84        Err(e) => storage_error!("Failed to delete token", e),
85    }
86}
87
88pub async fn auth_refresh() -> Response {
89    let config = match load_config() {
90        Ok(c) => c,
91        Err(e) => return e,
92    };
93
94    let token_store = match init_token_store() {
95        Ok(s) => s,
96        Err(e) => return e,
97    };
98
99    let token = match token_store.load() {
100        Ok(t) => t,
101        Err(_) => {
102            return Response::err(
103                401,
104                "Not logged in. Run: spotify-cli auth login",
105                ErrorKind::Auth,
106            );
107        }
108    };
109
110    let refresh_token = match &token.refresh_token {
111        Some(t) => t,
112        None => return Response::err(401, "No refresh token available", ErrorKind::Auth),
113    };
114
115    let flow = OAuthFlow::new(config.client_id().to_string());
116    match flow.refresh(refresh_token).await {
117        Ok(new_token) => {
118            if let Err(e) = token_store.save(&new_token) {
119                return storage_error!("Failed to save token", e);
120            }
121            Response::success_with_payload(
122                200,
123                "Token refreshed",
124                serde_json::json!({
125                    "expires_in": new_token.seconds_until_expiry()
126                }),
127            )
128        }
129        Err(e) => auth_error!("Failed to refresh token", e),
130    }
131}
132
133pub async fn auth_status() -> Response {
134    let token_store = match init_token_store() {
135        Ok(s) => s,
136        Err(e) => return e,
137    };
138
139    if !token_store.exists() {
140        return Response::success_with_payload(
141            200,
142            "Not authenticated",
143            serde_json::json!({
144                "authenticated": false
145            }),
146        );
147    }
148
149    match token_store.load() {
150        Ok(token) => {
151            let expired = token.is_expired();
152            let expires_in = token.seconds_until_expiry();
153
154            Response::success_with_payload(
155                200,
156                if expired {
157                    "Token expired"
158                } else {
159                    "Authenticated"
160                },
161                serde_json::json!({
162                    "authenticated": !expired,
163                    "expired": expired,
164                    "expires_in": expires_in
165                }),
166            )
167        }
168        Err(e) => storage_error!("Failed to load token", e),
169    }
170}