mal_cli/
auth.rs

1use reqwest::{
2    header::{HeaderMap, HeaderValue, CONTENT_TYPE},
3    Client, Url,
4};
5use serde_json::{json, Value};
6use std::io::Read;
7use std::{env, fs, path::Path};
8use std::{
9    error::Error,
10    fs::{File, OpenOptions},
11    io::Write,
12};
13use tiny_http::Server;
14
15pub async fn authenticate() -> Result<(String, String), Box<dyn Error>> {
16    let mut auth_code = String::from("");
17
18    let client_id = env::var("MAL_CLI_CLIENT_ID").expect("Client ID not set.");
19    let client_id = client_id.as_str();
20    let code_challenge = "7tPPwQCPWku8SYxrDr1VyLBHXne7RVNmB8ndAwGvZYTCrD";
21
22    let auth_url = Url::parse_with_params(
23        "https://myanimelist.net/v1/oauth2/authorize",
24        &[
25            ("response_type", "code"),
26            ("client_id", client_id),
27            ("state", "STATE"),
28            ("redirect_uri", "http://localhost:8080"),
29            ("code_challenge", code_challenge),
30            ("code_challenge_method", "plain"),
31        ],
32    )?;
33    open::that(auth_url.as_str())?;
34
35    // parse the auth token
36    let server = Server::http("127.0.0.1:8080").unwrap();
37    for rq in server.incoming_requests() {
38        let complete_url = "http://localhost:8080".to_string() + rq.url();
39        let request_url = Url::parse(complete_url.as_str())?;
40
41        auth_code = request_url
42            .query_pairs()
43            .find(|(key, _value)| key == "code")
44            .map(|(_, val)| val)
45            .unwrap()
46            .to_string();
47
48        break;
49    }
50
51    // get access token
52    let params = [
53        ("client_id", client_id),
54        ("client_secret", ""),
55        ("code", auth_code.as_str()),
56        ("code_verifier", code_challenge),
57        ("redirect_uri", "http://localhost:8080"),
58        ("grant_type", "authorization_code"),
59    ];
60
61    let client = Client::new();
62    let mut headers = HeaderMap::new();
63    headers.insert(
64        CONTENT_TYPE,
65        HeaderValue::from_static("application/x-www-form-urlencoded"),
66    );
67    let response = client
68        .post("https://myanimelist.net/v1/oauth2/token")
69        .headers(headers)
70        .form(&params)
71        .send()
72        .await?;
73
74    let resp: Value = serde_json::from_str(response.text().await?.as_str()).unwrap();
75    Ok((
76        clean_token(resp.get("access_token").unwrap().to_string()),
77        clean_token(resp.get("refresh_token").unwrap().to_string()),
78    ))
79}
80
81pub async fn get_access_token() -> Result<String, Box<dyn Error>> {
82    let home: String = env::var("HOME").unwrap();
83    let token_location: String = format!("{home}/.config/mal-cli");
84
85    let cache = read_token(token_location.clone());
86    match cache {
87        Ok(token) => Ok(clean_token(token.get("token").unwrap().to_string())),
88        _ => {
89            let (access_token, _) = authenticate().await.unwrap();
90            save_token(&access_token, token_location);
91            Ok(access_token)
92        }
93    }
94}
95
96pub async fn reauthenticate() -> Result<String, Box<dyn Error>> {
97    let home: String = env::var("HOME").unwrap();
98    let token_location: String = format!("{home}/.config/mal-cli");
99
100    let (access_token, _) = authenticate().await.unwrap();
101    save_token(&access_token, token_location);
102    Ok(access_token)
103}
104
105fn clean_token(token: String) -> String {
106    token
107        .trim_start_matches("\"")
108        .trim_end_matches("\"")
109        .to_string()
110}
111
112fn save_token(token: &str, token_location: String) {
113    let data = json!({
114        "token": clean_token(token.to_string())
115    });
116
117    let data_string = data.to_string();
118    let mut file = File::create(format!("{token_location}/token.json"))
119        .expect("Unable to create token cache.");
120
121    // save
122    file.write_all(data_string.as_bytes())
123        .expect("Unable to write token cache.");
124}
125
126fn read_token(token_location: String) -> Result<Value, serde_json::Error> {
127    let binding = token_location.clone();
128    let path = Path::new(&binding);
129    if !path.exists() {
130        // Try to create the directory
131        match fs::create_dir_all(path) {
132            Ok(_) => (),
133            Err(e) => println!("Error creating directory: {}", e),
134        }
135    }
136
137    // Check if the directory exists
138    if !path.exists() {
139        // Try to create the directory
140        match fs::create_dir_all(path) {
141            Ok(_) => println!("Directory created successfully."),
142            Err(e) => println!("Error creating directory: {}", e),
143        }
144    }
145    let mut file = OpenOptions::new()
146        .read(true)
147        .create(true)
148        .write(true)
149        .open(format!("{token_location}/token.json"))
150        .expect("Unable to create file..");
151
152    let mut contents = Vec::new();
153    file.read_to_end(&mut contents).unwrap();
154
155    let contents_str = String::from_utf8(contents).unwrap();
156    serde_json::from_str(&contents_str)
157}