quorum_lib/commands/
auth.rs1use 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 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}