1use argon2::{
2 Argon2,
3 password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
4};
5use chrono::{DateTime, Utc};
6use rand::rngs::OsRng;
7use sea_orm::entity::prelude::*;
8use sea_orm::{
9 ActiveModelTrait, ColumnTrait, DatabaseConnection, DbErr, EntityTrait, QueryFilter, Set,
10};
11
12use crate::models::user_module_role::Role;
13use crate::models::{
14 module::Entity as ModuleEntity,
15 user::{self, ActiveModel as UserActiveModel, Entity as UserEntity},
16 user_module_role::{Column as RoleColumn, Entity as RoleEntity},
17};
18use std::str::FromStr;
19
20#[derive(Clone, Debug, PartialEq, DeriveEntityModel, serde::Serialize)]
22#[sea_orm(table_name = "users")]
23pub struct Model {
24 #[sea_orm(primary_key)]
26 pub id: i64,
27 pub username: String,
29 pub email: String,
31 pub password_hash: String,
33 pub admin: bool,
35 pub created_at: DateTime<Utc>,
37 pub updated_at: DateTime<Utc>,
39 pub profile_picture_path: Option<String>,
41}
42
43#[derive(Copy, Clone, Debug, EnumIter)]
45pub enum Relation {}
46
47impl RelationTrait for Relation {
48 fn def(&self) -> RelationDef {
49 panic!("No RelationDef implemented")
50 }
51}
52
53impl ActiveModelBehavior for ActiveModel {}
55
56#[derive(Debug, Clone)]
58pub struct UserModuleRole {
59 pub module_id: i64,
60 pub module_code: String,
61 pub module_year: i32,
62 pub module_description: Option<String>,
63 pub module_credits: i32,
64 pub module_created_at: String,
65 pub module_updated_at: String,
66 pub role: String,
67}
68
69impl Model {
70 pub async fn create(
79 db: &DatabaseConnection,
80 username: &str,
81 email: &str,
82 password: &str,
83 admin: bool,
84 ) -> Result<Model, DbErr> {
85 let hash = Self::hash_password(password);
86 let active = UserActiveModel {
87 username: Set(username.to_owned()),
88 email: Set(email.to_owned()),
89 password_hash: Set(hash),
90 admin: Set(admin),
91 ..Default::default()
92 };
93 active.insert(db).await
94 }
95
96 pub async fn create_fake_user_with_no_hashed_password_do_not_use(
107 db: &DatabaseConnection,
108 username: &str,
109 email: &str,
110 password: &str,
111 admin: bool,
112 ) -> Result<Model, DbErr> {
113 let hash = password;
114 let active = UserActiveModel {
115 username: Set(username.to_owned()),
116 email: Set(email.to_owned()),
117 password_hash: Set(hash.to_string()),
118 admin: Set(admin),
119 ..Default::default()
120 };
121 active.insert(db).await
122 }
123
124 pub async fn get_by_username(
133 db: &DatabaseConnection,
134 username: &str,
135 ) -> Result<Option<Model>, DbErr> {
136 UserEntity::find()
137 .filter(user::Column::Username.eq(username))
138 .one(db)
139 .await
140 }
141
142 pub async fn verify_credentials(
152 db: &DatabaseConnection,
153 username: &str,
154 password: &str,
155 ) -> Result<Option<Model>, DbErr> {
156 let username = username.trim();
157
158 if let Some(user) = Self::get_by_username(db, username).await? {
159 let parsed = PasswordHash::new(&user.password_hash)
160 .map_err(|e| DbErr::Custom(format!("Invalid hash: {}", e)))?;
161
162 if Argon2::default()
163 .verify_password(password.as_bytes(), &parsed)
164 .is_ok()
165 {
166 return Ok(Some(user));
167 }
168 }
169
170 Ok(None)
171 }
172
173 pub async fn get_module_roles(
182 db: &DatabaseConnection,
183 user_id: i64,
184 ) -> Result<Vec<UserModuleRole>, DbErr> {
185 let roles = RoleEntity::find()
186 .filter(RoleColumn::UserId.eq(user_id))
187 .find_also_related(ModuleEntity)
188 .all(db)
189 .await?;
190
191 Ok(roles
192 .into_iter()
193 .filter_map(|(role, maybe_module)| {
194 maybe_module.map(|module| UserModuleRole {
195 module_id: module.id,
196 module_code: module.code,
197 module_year: module.year,
198 module_description: module.description,
199 module_credits: module.credits,
200 module_created_at: module.created_at.to_string(),
201 module_updated_at: module.updated_at.to_string(),
202 role: role.role.to_string(),
203 })
204 })
205 .collect())
206 }
207
208 pub async fn is_in_role(
219 db: &DatabaseConnection,
220 user_id: i64,
221 module_id: i64,
222 role: &str,
223 ) -> Result<bool, DbErr> {
224 let parsed_role = Role::from_str(role)
225 .map_err(|_| DbErr::Custom(format!("Invalid role string: '{}'", role)))?;
226
227 let exists = RoleEntity::find()
228 .filter(RoleColumn::UserId.eq(user_id))
229 .filter(RoleColumn::ModuleId.eq(module_id))
230 .filter(RoleColumn::Role.eq(parsed_role))
231 .one(db)
232 .await?;
233
234 Ok(exists.is_some())
235 }
236
237 pub fn hash_password(password: &str) -> String {
245 let salt = SaltString::generate(&mut OsRng);
246 Argon2::default()
247 .hash_password(password.as_bytes(), &salt)
248 .expect("Failed to hash password")
249 .to_string()
250 }
251
252 pub fn verify_password(&self, password: &str) -> bool {
254 let parsed = match PasswordHash::new(&self.password_hash) {
255 Ok(parsed) => parsed,
256 Err(_) => return false,
257 };
258
259 Argon2::default()
260 .verify_password(password.as_bytes(), &parsed)
261 .is_ok()
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::models::user_module_role::Role as UserRole;
269 use crate::test_utils::setup_test_db;
270
271 #[tokio::test]
272 async fn test_create_and_get_user() {
273 let db = setup_test_db().await;
274 let username = "u12345678";
275 let email = "[email protected]";
276 let password = "secret123";
277
278 let _user = Model::create(&db, username, email, password, false)
279 .await
280 .expect("Failed to create user");
281
282 let found = Model::get_by_username(&db, username)
283 .await
284 .expect("Failed to query user");
285
286 assert!(found.is_some());
287 let found = found.unwrap();
288 assert_eq!(found.email, email);
289 assert_eq!(found.username, username);
290 assert_eq!(found.admin, false);
291 }
292
293 #[tokio::test]
294 async fn test_verify_credentials_success_and_failure() {
295 let db = setup_test_db().await;
296 let username = "u87654321";
297 let email = "[email protected]";
298 let password = "correct_pw";
299
300 Model::create(&db, username, email, password, false)
301 .await
302 .expect("Failed to create user");
303
304 let ok = Model::verify_credentials(&db, username, password)
305 .await
306 .expect("Failed to verify credentials");
307 assert!(ok.is_some());
308
309 let bad = Model::verify_credentials(&db, username, "wrong_pw")
310 .await
311 .expect("Failed to verify wrong credentials");
312 assert!(bad.is_none());
313 }
314
315 #[tokio::test]
316 async fn test_is_in_role_and_get_module_roles() {
317 use crate::models::{
318 module::ActiveModel as ModuleActiveModel,
319 user_module_role::ActiveModel as RoleActiveModel,
320 };
321
322 let db = setup_test_db().await;
323
324 let user = Model::create(&db, "u00001111", "[email protected]", "pw", false)
326 .await
327 .expect("create user");
328
329 let module = ModuleActiveModel {
331 code: Set("COS999".into()),
332 year: Set(2025),
333 description: Set(Some("Test Module".into())),
334 credits: Set(15),
335 ..Default::default()
336 }
337 .insert(&db)
338 .await
339 .expect("create module");
340
341 RoleActiveModel {
343 user_id: Set(user.id),
344 module_id: Set(module.id),
345 role: Set(UserRole::Lecturer),
346 ..Default::default()
347 }
348 .insert(&db)
349 .await
350 .expect("assign role");
351
352 let is_lecturer = Model::is_in_role(&db, user.id, module.id, "lecturer")
354 .await
355 .expect("check is_in_role");
356 assert!(is_lecturer);
357
358 let is_tutor = Model::is_in_role(&db, user.id, module.id, "tutor")
359 .await
360 .expect("check is_in_role");
361 assert!(!is_tutor);
362
363 let roles = Model::get_module_roles(&db, user.id)
365 .await
366 .expect("get_module_roles");
367
368 assert_eq!(roles.len(), 1);
369 let r = &roles[0];
370 assert_eq!(r.module_id, module.id);
371 assert_eq!(r.module_code, "COS999");
372 assert_eq!(r.role, "lecturer");
373 }
374}