Skip to main content

nil_server_database/model/
user.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4use crate::Database;
5use crate::error::{Error, Result};
6use crate::sql_types::hashed_password::HashedPassword;
7use crate::sql_types::id::UserId;
8use crate::sql_types::player_id::db_PlayerId;
9use crate::sql_types::zoned::db_Zoned;
10use diesel::prelude::*;
11use nil_crypto::password::Password;
12use std::fmt;
13use tokio::task::spawn_blocking;
14
15#[derive(Identifiable, Queryable, Selectable, Clone)]
16#[diesel(table_name = crate::schema::user)]
17#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
18pub struct User {
19  pub id: UserId,
20  pub player_id: db_PlayerId,
21  pub password: HashedPassword,
22  pub created_at: db_Zoned,
23  pub updated_at: db_Zoned,
24}
25
26impl User {
27  #[inline]
28  pub async fn verify_password(&self, password: Password) -> Result<bool> {
29    let hashed = self.password.clone();
30    spawn_blocking(move || hashed.verify(&password))
31      .await?
32      .map_err(Into::into)
33  }
34}
35
36impl fmt::Debug for User {
37  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38    f.debug_struct("User")
39      .field("id", &self.id.to_string())
40      .field("player_id", &self.player_id)
41      .field("created_at", &self.created_at.to_string())
42      .field("updated_at", &self.updated_at.to_string())
43      .finish_non_exhaustive()
44  }
45}
46
47#[derive(Insertable, Clone)]
48#[diesel(table_name = crate::schema::user)]
49#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
50pub struct NewUser {
51  player_id: db_PlayerId,
52  password: HashedPassword,
53  created_at: db_Zoned,
54  updated_at: db_Zoned,
55}
56
57impl NewUser {
58  pub async fn new(player_id: impl Into<db_PlayerId>, password: Password) -> Result<Self> {
59    let player_id: db_PlayerId = player_id.into();
60    let id_len = player_id.trim().chars().count();
61
62    if !(1..=20).contains(&id_len) {
63      return Err(Error::InvalidUsername(player_id));
64    }
65
66    let now = db_Zoned::now();
67
68    Ok(Self {
69      player_id,
70      password: hash_password(password).await?,
71      created_at: now.clone(),
72      updated_at: now,
73    })
74  }
75
76  pub fn player_id(&self) -> db_PlayerId {
77    self.player_id.clone()
78  }
79
80  pub async fn create(self, database: &Database) -> Result<usize> {
81    database.create_user(&self).await
82  }
83}
84
85impl fmt::Debug for NewUser {
86  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87    f.debug_struct("NewUser")
88      .field("player_id", &self.player_id)
89      .field("created_at", &self.created_at.to_string())
90      .field("updated_at", &self.updated_at.to_string())
91      .finish_non_exhaustive()
92  }
93}
94
95async fn hash_password(password: Password) -> Result<HashedPassword> {
96  let pass_len = password.trim().chars().count();
97  if !(3..=50).contains(&pass_len) {
98    return Err(Error::InvalidPassword);
99  }
100
101  spawn_blocking(move || HashedPassword::new(&password))
102    .await?
103    .map_err(Into::into)
104}