Skip to main content

quorum_lib/commands/
auth.rs

1use crate::common::make_auth_request;
2use serde::{Deserialize, Serialize};
3use serde_json::json;
4use tauri::Emitter;
5
6#[derive(Serialize)]
7pub struct AuthSuccess {
8    pub access_token: String,
9    pub refresh_token: String,
10    pub user_id: String,
11    pub username: String,
12}
13
14#[derive(Serialize)]
15#[serde(rename_all = "camelCase")]
16pub struct SignupSuccess {
17    pub message: String,
18    pub account_has_email: bool,
19    pub backup_codes: Option<Vec<String>>,
20}
21
22#[derive(Deserialize)]
23pub struct SignupPayload {
24    pub username: String,
25    pub email: Option<String>,
26    pub password: String,
27    pub confirm_password: String,
28}
29
30#[derive(Deserialize)]
31pub struct LoginPayload {
32    pub username_or_email: String,
33    pub password: String,
34}
35
36#[tauri::command]
37pub async fn signup(
38    app: tauri::AppHandle,
39    payload: SignupPayload,
40) -> Result<SignupSuccess, String> {
41    if payload.username.is_empty() {
42        return Err("Username must be at least 1 character.".to_string());
43    }
44
45    if payload.username.len() > 18 {
46        return Err("Username must be under 32 characters.".to_string());
47    }
48
49    if let Some(ref email) = payload.email {
50        if !email.is_empty() && !email.contains('@') {
51            return Err("Please enter a valid email address.".to_string());
52        }
53    }
54
55    if payload.password.is_empty() {
56        return Err("Password cannot be empty.".to_string());
57    }
58
59    if payload.password.len() < 8 {
60        return Err("Password must be at least 8 characters.".to_string());
61    }
62
63    if payload.password.len() > 128 {
64        return Err("Password must be under 128 characters.".to_string());
65    }
66
67    if payload.password != payload.confirm_password {
68        return Err("Passwords do not match.".to_string());
69    }
70
71    let has_email = payload
72        .email
73        .as_ref()
74        .is_some_and(|email| !email.trim().is_empty());
75
76    let new_payload = if has_email {
77        json!({
78            "username": &payload.username,
79            "email": payload.email.as_deref().map(str::trim),
80              "password": &payload.password,
81        })
82    } else {
83        json!({
84            "username": &payload.username,
85            "password": &payload.password,
86        })
87    };
88    // "The unexamined life is not worth living." - Socrates. Yeah this block of code took me way more than i'd like to admit. First easter egg! cheers Jamie
89    let auth_response = make_auth_request(&app, "/auth/signup", &new_payload, 201).await?;
90
91    let backup_codes = auth_response["backup_codes"].as_array().map(|codes| {
92        codes
93            .iter()
94            .filter_map(|code| code.as_str().map(str::to_string))
95            .collect::<Vec<String>>()
96    });
97
98    Ok(SignupSuccess {
99        message: "Account created successfully.".to_string(),
100        account_has_email: has_email,
101        backup_codes,
102    })
103}
104
105#[tauri::command]
106pub async fn login(app: tauri::AppHandle, payload: LoginPayload) -> Result<AuthSuccess, String> {
107    let request_payload = json!({
108        "username_or_email": payload.username_or_email,
109        "password": payload.password,
110    });
111
112    let auth_response = make_auth_request(&app, "/auth/login", &request_payload, 200).await?;
113
114    if !auth_response["success"].as_bool().unwrap_or(false) {
115        let error_message = auth_response["message"]
116            .as_str()
117            .unwrap_or("Invalid username/email or password")
118            .to_string();
119        return Err(error_message);
120    }
121
122    let tokens = auth_response["tokens"]
123        .as_object()
124        .ok_or("Invalid tokens in response")?;
125    let access_token = tokens["access_token"]
126        .as_str()
127        .ok_or("Missing access_token")?
128        .to_string();
129    let refresh_token = tokens["refresh_token"]
130        .as_str()
131        .ok_or("Missing refresh_token")?
132        .to_string();
133    let user = auth_response["user"]
134        .as_object()
135        .ok_or("Invalid user in response")?;
136    let user_id = user["id"].as_str().ok_or("Missing user id")?.to_string();
137    let username = user["username"]
138        .as_str()
139        .ok_or("Missing username")?
140        .to_string();
141
142    Ok(AuthSuccess {
143        access_token,
144        refresh_token,
145        user_id,
146        username,
147    })
148}
149
150#[tauri::command]
151pub async fn refresh_token(
152    refresh_token: String,
153    user_id: String,
154    app_handle: tauri::AppHandle,
155) -> Result<AuthSuccess, String> {
156    let payload = json!({
157        "refresh_token": refresh_token,
158        "user_id": user_id,
159    });
160
161    let auth_response = make_auth_request(&app_handle, "/auth/refresh", &payload, 200).await?;
162
163    if !auth_response["success"].as_bool().unwrap_or(false) {
164        let error_msg = auth_response["message"]
165            .as_str()
166            .unwrap_or("Failed to refresh token")
167            .to_string();
168
169        let _ = app_handle.emit("token_expired", ());
170
171        return Err(error_msg);
172    }
173
174    let tokens = auth_response["tokens"]
175        .as_object()
176        .ok_or("Invalid tokens in response")?;
177    let access_token = tokens["access_token"]
178        .as_str()
179        .ok_or("Missing access_token")?
180        .to_string();
181    let new_refresh_token = tokens["refresh_token"]
182        .as_str()
183        .ok_or("Missing refresh_token")?
184        .to_string();
185
186    Ok(AuthSuccess {
187        access_token,
188        refresh_token: new_refresh_token,
189        user_id: String::new(),
190        username: String::new(),
191    })
192}