1use crate::permissions::Permission;
2use crate::validation::{Validate, ValidationError};
3use serde::{Deserialize, Serialize};
4use uptrakit_shared_types::SecretString;
5use uuid::Uuid;
6
7#[derive(Serialize, Deserialize)]
8#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
9#[cfg_attr(feature = "openapi", schema(example = json!({
10 "email": "admin@example.com",
11 "first_name": "Admin",
12 "last_name": "User",
13 "password": "SecurePass123"
14})))]
15pub struct RegisterRequest {
16 #[cfg_attr(feature = "openapi", schema(example = "admin@example.com"))]
17 pub email: String,
18 #[cfg_attr(feature = "openapi", schema(example = "Admin"))]
19 pub first_name: String,
20 #[cfg_attr(feature = "openapi", schema(example = "User"))]
21 pub last_name: String,
22 #[cfg_attr(feature = "openapi", schema(example = "SecurePass123", min_length = 8))]
23 pub password: SecretString,
24 pub registration_token: Option<SecretString>,
26}
27
28#[derive(Serialize, Deserialize)]
29#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
30#[cfg_attr(feature = "openapi", schema(example = json!({
31 "email": "admin@example.com",
32 "password": "SecurePass123"
33})))]
34pub struct LoginRequest {
35 #[cfg_attr(feature = "openapi", schema(example = "admin@example.com"))]
36 pub email: String,
37 #[cfg_attr(feature = "openapi", schema(example = "SecurePass123"))]
38 pub password: SecretString,
39}
40
41#[derive(Serialize, Deserialize)]
42#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
43pub struct LogoutRequest {
44 #[serde(default)]
47 pub refresh_token: Option<SecretString>,
48}
49
50#[derive(Serialize, Deserialize)]
51#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
52pub struct RefreshRequest {
53 #[serde(default)]
56 pub refresh_token: Option<SecretString>,
57}
58
59#[derive(Serialize, Deserialize, Clone)]
60#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
61pub struct AuthResponse {
62 pub access_token: SecretString,
63 pub refresh_token: SecretString,
64 pub expires_in: i64,
65 pub token_type: String,
66 pub user: UserResponse,
67}
68
69#[derive(Serialize, Deserialize)]
70#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
71pub struct RefreshResponse {
72 pub access_token: SecretString,
73 pub refresh_token: SecretString,
75 pub expires_in: i64,
76 pub token_type: String,
77}
78
79#[derive(Serialize, Deserialize, Clone)]
80#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
81pub struct UserResponse {
82 pub id: Uuid,
83 pub email: String,
84 pub first_name: String,
85 pub last_name: String,
86 pub permissions: Vec<Permission>,
87 pub has_pending_email_change: bool,
88}
89
90impl Validate for RegisterRequest {
91 fn validate(&self) -> Result<(), ValidationError> {
92 if self.email.len() > 254 {
93 return Err(ValidationError {
94 field: "email",
95 message: "email must not exceed 254 characters".to_string(),
96 });
97 }
98 if !self.email.contains('@') {
99 return Err(ValidationError {
100 field: "email",
101 message: "email must contain '@'".to_string(),
102 });
103 }
104 if self.first_name.is_empty() {
105 return Err(ValidationError {
106 field: "first_name",
107 message: "first_name must not be empty".to_string(),
108 });
109 }
110 let password_len = self.password.expose_secret().len();
111 if password_len < 8 {
112 return Err(ValidationError {
113 field: "password",
114 message: "password must be at least 8 characters".to_string(),
115 });
116 }
117 if password_len > 1024 {
118 return Err(ValidationError {
119 field: "password",
120 message: "password must not exceed 1024 characters".to_string(),
121 });
122 }
123 Ok(())
124 }
125}
126
127impl Validate for LoginRequest {
128 fn validate(&self) -> Result<(), ValidationError> {
129 if self.email.len() > 254 {
130 return Err(ValidationError {
131 field: "email",
132 message: "email must not exceed 254 characters".to_string(),
133 });
134 }
135 if !self.email.contains('@') {
136 return Err(ValidationError {
137 field: "email",
138 message: "email must contain '@'".to_string(),
139 });
140 }
141 if self.password.expose_secret().is_empty() {
142 return Err(ValidationError {
143 field: "password",
144 message: "password must not be empty".to_string(),
145 });
146 }
147 Ok(())
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 fn valid_register() -> RegisterRequest {
158 RegisterRequest {
159 email: "user@example.com".to_string(),
160 first_name: "Alice".to_string(),
161 last_name: "Smith".to_string(),
162 password: SecretString::new("password123"),
163 registration_token: None,
164 }
165 }
166
167 #[test]
168 fn register_valid() {
169 assert!(valid_register().validate().is_ok());
170 }
171
172 #[test]
173 fn register_email_too_long() {
174 let mut req = valid_register();
175 req.email = format!("{}@x.com", "a".repeat(250));
176 let err = req.validate().unwrap_err();
177 assert_eq!(err.field, "email");
178 }
179
180 #[test]
181 fn register_email_no_at_sign() {
182 let mut req = valid_register();
183 req.email = "notanemail".to_string();
184 let err = req.validate().unwrap_err();
185 assert_eq!(err.field, "email");
186 }
187
188 #[test]
189 fn register_first_name_empty() {
190 let mut req = valid_register();
191 req.first_name = String::new();
192 let err = req.validate().unwrap_err();
193 assert_eq!(err.field, "first_name");
194 }
195
196 #[test]
197 fn register_password_too_short() {
198 let mut req = valid_register();
199 req.password = SecretString::new("short");
200 let err = req.validate().unwrap_err();
201 assert_eq!(err.field, "password");
202 }
203
204 #[test]
205 fn register_password_too_long() {
206 let mut req = valid_register();
207 req.password = SecretString::new("a".repeat(1025));
208 let err = req.validate().unwrap_err();
209 assert_eq!(err.field, "password");
210 }
211
212 fn valid_login() -> LoginRequest {
215 LoginRequest {
216 email: "user@example.com".to_string(),
217 password: SecretString::new("password123"),
218 }
219 }
220
221 #[test]
222 fn login_valid() {
223 assert!(valid_login().validate().is_ok());
224 }
225
226 #[test]
227 fn login_email_too_long() {
228 let mut req = valid_login();
229 req.email = format!("{}@x.com", "a".repeat(250));
230 let err = req.validate().unwrap_err();
231 assert_eq!(err.field, "email");
232 }
233
234 #[test]
235 fn login_email_no_at_sign() {
236 let mut req = valid_login();
237 req.email = "notanemail".to_string();
238 let err = req.validate().unwrap_err();
239 assert_eq!(err.field, "email");
240 }
241
242 #[test]
243 fn login_password_empty() {
244 let mut req = valid_login();
245 req.password = SecretString::new(String::new());
246 let err = req.validate().unwrap_err();
247 assert_eq!(err.field, "password");
248 }
249}