forked from Ruderverein-Donau-Linz/rowt
724 lines
21 KiB
Rust
724 lines
21 KiB
Rust
use std::ops::{Deref, DerefMut};
|
|
|
|
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
|
use chrono::{Datelike, Local, NaiveDate};
|
|
use log::info;
|
|
use rocket::{
|
|
async_trait,
|
|
http::{Cookie, Status},
|
|
request,
|
|
request::{FromRequest, Outcome},
|
|
time::{Duration, OffsetDateTime},
|
|
Request,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
|
|
|
use super::{log::Log, role::Role, tripdetails::TripDetails, Day};
|
|
use crate::{tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD};
|
|
|
|
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
|
|
pub struct User {
|
|
pub id: i64,
|
|
pub name: String,
|
|
pub pw: Option<String>,
|
|
pub deleted: bool,
|
|
pub last_access: Option<chrono::NaiveDateTime>,
|
|
pub user_token: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct UserWithDetails {
|
|
#[serde(flatten)]
|
|
pub user: User,
|
|
pub amount_unread_notifications: i64,
|
|
pub allowed_to_steer: bool,
|
|
pub roles: Vec<String>,
|
|
}
|
|
|
|
impl UserWithDetails {
|
|
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
|
let allowed_to_steer = user.allowed_to_steer(db).await;
|
|
|
|
Self {
|
|
roles: user.roles(db).await,
|
|
amount_unread_notifications: user.amount_unread_notifications(db).await,
|
|
allowed_to_steer,
|
|
user,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum LoginError {
|
|
InvalidAuthenticationCombo,
|
|
UserNotFound,
|
|
UserDeleted,
|
|
NotLoggedIn,
|
|
NotAnAdmin,
|
|
NotACox,
|
|
NotATech,
|
|
GuestNotAllowed,
|
|
NoPasswordSet(Box<User>),
|
|
DeserializationError,
|
|
}
|
|
|
|
impl User {
|
|
pub async fn allowed_to_steer(&self, db: &SqlitePool) -> bool {
|
|
self.has_role(db, "cox").await || self.has_role(db, "Bootsführer").await
|
|
}
|
|
|
|
pub async fn allowed_to_steer_tx(&self, db: &mut Transaction<'_, Sqlite>) -> bool {
|
|
self.has_role_tx(db, "cox").await || self.has_role_tx(db, "Bootsführer").await
|
|
}
|
|
|
|
pub async fn amount_unread_notifications(&self, db: &SqlitePool) -> i64 {
|
|
sqlx::query!(
|
|
"SELECT COUNT(*) as count FROM notification WHERE user_id = ? AND read_at IS NULL",
|
|
self.id
|
|
)
|
|
.fetch_one(db)
|
|
.await
|
|
.unwrap()
|
|
.count
|
|
}
|
|
|
|
pub async fn has_role(&self, db: &SqlitePool, role: &str) -> bool {
|
|
if sqlx::query!(
|
|
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",
|
|
self.id,
|
|
role
|
|
)
|
|
.fetch_optional(db)
|
|
.await
|
|
.unwrap()
|
|
.is_some()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
pub async fn allowed_to_update_always_show_trip(&self, db: &SqlitePool) -> bool {
|
|
AllowedToUpdateTripToAlwaysBeShownUser::new(db, self.clone())
|
|
.await
|
|
.is_some()
|
|
}
|
|
|
|
pub async fn roles(&self, db: &SqlitePool) -> Vec<String> {
|
|
sqlx::query!(
|
|
"SELECT r.name FROM role r JOIN user_role ur ON r.id = ur.role_id JOIN user u ON u.id = ur.user_id WHERE ur.user_id = ? AND u.deleted = 0;",
|
|
self.id
|
|
)
|
|
.fetch_all(db)
|
|
.await
|
|
.unwrap()
|
|
.into_iter().map(|r| r.name).collect()
|
|
}
|
|
|
|
pub async fn real_roles(&self, db: &SqlitePool) -> Vec<Role> {
|
|
sqlx::query_as!(
|
|
Role,
|
|
"SELECT r.id, r.name, r.cluster
|
|
FROM role r
|
|
JOIN user_role ur ON r.id = ur.role_id
|
|
JOIN user u ON u.id = ur.user_id
|
|
WHERE ur.user_id = ? AND u.deleted = 0;",
|
|
self.id
|
|
)
|
|
.fetch_all(db)
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
pub async fn has_role_tx(&self, db: &mut Transaction<'_, Sqlite>, role: &str) -> bool {
|
|
if sqlx::query!(
|
|
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",
|
|
self.id,
|
|
role
|
|
)
|
|
.fetch_optional(db.deref_mut())
|
|
.await
|
|
.unwrap()
|
|
.is_some()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
|
sqlx::query_as!(
|
|
Self,
|
|
"
|
|
SELECT id, name, pw, deleted, last_access, user_token
|
|
FROM user
|
|
WHERE id like ?
|
|
",
|
|
id
|
|
)
|
|
.fetch_one(db)
|
|
.await
|
|
.ok()
|
|
}
|
|
|
|
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
|
sqlx::query_as!(
|
|
Self,
|
|
"
|
|
SELECT id, name, pw, deleted, last_access, user_token
|
|
FROM user
|
|
WHERE id like ?
|
|
",
|
|
id
|
|
)
|
|
.fetch_one(db.deref_mut())
|
|
.await
|
|
.ok()
|
|
}
|
|
|
|
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
|
|
let name = name.trim().to_lowercase();
|
|
|
|
sqlx::query_as!(
|
|
Self,
|
|
"
|
|
SELECT id, name, pw, deleted, last_access, user_token
|
|
FROM user
|
|
WHERE lower(name)=?
|
|
",
|
|
name
|
|
)
|
|
.fetch_one(db)
|
|
.await
|
|
.ok()
|
|
}
|
|
|
|
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
|
Self::all_with_order(db, "last_access", false).await
|
|
}
|
|
|
|
pub async fn all_with_order(db: &SqlitePool, sort: &str, asc: bool) -> Vec<Self> {
|
|
let mut query = format!(
|
|
"
|
|
SELECT id, name, pw, deleted, last_access, user_token
|
|
FROM user
|
|
WHERE deleted = 0
|
|
ORDER BY {}
|
|
",
|
|
sort
|
|
);
|
|
if !asc {
|
|
query.push_str(" DESC");
|
|
}
|
|
|
|
sqlx::query_as::<_, User>(&query)
|
|
.fetch_all(db)
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
pub async fn all_with_role(db: &SqlitePool, role: &Role) -> Vec<Self> {
|
|
let mut tx = db.begin().await.unwrap();
|
|
let ret = Self::all_with_role_tx(&mut tx, role).await;
|
|
tx.commit().await.unwrap();
|
|
ret
|
|
}
|
|
|
|
pub async fn all_with_role_tx(db: &mut Transaction<'_, Sqlite>, role: &Role) -> Vec<Self> {
|
|
sqlx::query_as!(
|
|
Self,
|
|
"
|
|
SELECT id, name, pw, deleted, last_access, user_token
|
|
FROM user u
|
|
JOIN user_role ur ON u.id = ur.user_id
|
|
WHERE ur.role_id = ? AND deleted = 0
|
|
ORDER BY name;
|
|
",
|
|
role.id
|
|
)
|
|
.fetch_all(db.deref_mut())
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
pub async fn cox(db: &SqlitePool) -> Vec<Self> {
|
|
sqlx::query_as!(
|
|
Self,
|
|
"
|
|
SELECT id, name, pw, deleted, last_access, user_token
|
|
FROM user
|
|
WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0
|
|
ORDER BY last_access DESC
|
|
"
|
|
)
|
|
.fetch_all(db)
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
pub async fn create(db: &SqlitePool, name: &str) {
|
|
let name = name.trim();
|
|
if sqlx::query!("INSERT INTO USER(name) VALUES (?)", name)
|
|
.execute(db)
|
|
.await
|
|
.is_ok()
|
|
{
|
|
return;
|
|
}
|
|
|
|
sqlx::query!("UPDATE user SET deleted = false where name = ?", name)
|
|
.execute(db)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
pub async fn update(&self, db: &SqlitePool, data: UserEditForm) -> Result<(), String> {
|
|
let mut db = db.begin().await.map_err(|e| e.to_string())?;
|
|
|
|
sqlx::query!("UPDATE user SET name = ? where id = ?", data.name, self.id)
|
|
.execute(db.deref_mut())
|
|
.await
|
|
.unwrap(); //Okay, because we can only create a User of a valid id
|
|
|
|
// handle roles
|
|
sqlx::query!("DELETE FROM user_role WHERE user_id = ?", self.id)
|
|
.execute(db.deref_mut())
|
|
.await
|
|
.unwrap();
|
|
|
|
for role_id in data.roles.into_keys() {
|
|
let role = Role::find_by_id_tx(&mut db, role_id.parse::<i32>().unwrap())
|
|
.await
|
|
.unwrap();
|
|
self.add_role_tx(&mut db, &role).await?;
|
|
}
|
|
|
|
db.commit().await.map_err(|e| e.to_string())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn add_role(&self, db: &SqlitePool, role: &Role) -> Result<(), String> {
|
|
sqlx::query!(
|
|
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
|
|
self.id,
|
|
role.id
|
|
)
|
|
.execute(db)
|
|
.await
|
|
.map_err(|_| {
|
|
format!(
|
|
"User already has a role in the cluster '{}'",
|
|
role.cluster
|
|
.clone()
|
|
.expect("db trigger can't activate on empty string")
|
|
)
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn add_role_tx(
|
|
&self,
|
|
db: &mut Transaction<'_, Sqlite>,
|
|
role: &Role,
|
|
) -> Result<(), String> {
|
|
sqlx::query!(
|
|
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
|
|
self.id,
|
|
role.id
|
|
)
|
|
.execute(db.deref_mut())
|
|
.await
|
|
.map_err(|_| {
|
|
format!(
|
|
"User already has a role in the cluster '{}'",
|
|
role.cluster
|
|
.clone()
|
|
.expect("db trigger can't activate on empty string")
|
|
)
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn remove_role(&self, db: &SqlitePool, role: &Role) {
|
|
sqlx::query!(
|
|
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
|
|
self.id,
|
|
role.id
|
|
)
|
|
.execute(db)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> {
|
|
let name = name.trim().to_lowercase(); // just to make sure...
|
|
let Some(user) = User::find_by_name(db, &name).await else {
|
|
Log::create(db, format!("Username ({name}) not found (tried to login)")).await;
|
|
return Err(LoginError::InvalidAuthenticationCombo); // Username not found
|
|
};
|
|
|
|
if user.deleted {
|
|
Log::create(
|
|
db,
|
|
format!("User ({name}) already deleted (tried to login)."),
|
|
)
|
|
.await;
|
|
return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has
|
|
//been deleted
|
|
}
|
|
|
|
if let Some(user_pw) = user.pw.as_ref() {
|
|
let password_hash = &Self::get_hashed_pw(pw);
|
|
if password_hash == user_pw {
|
|
return Ok(user);
|
|
}
|
|
Log::create(db, format!("User {name} supplied the wrong PW")).await;
|
|
Err(LoginError::InvalidAuthenticationCombo)
|
|
} else {
|
|
info!("User {name} has no PW set");
|
|
Err(LoginError::NoPasswordSet(Box::new(user)))
|
|
}
|
|
}
|
|
|
|
pub async fn reset_pw(&self, db: &SqlitePool) {
|
|
sqlx::query!("UPDATE user SET pw = null where id = ?", self.id)
|
|
.execute(db)
|
|
.await
|
|
.unwrap(); //Okay, because we can only create a User of a valid id
|
|
}
|
|
|
|
pub async fn update_pw(&self, db: &SqlitePool, pw: &str) {
|
|
let pw = Self::get_hashed_pw(pw);
|
|
sqlx::query!("UPDATE user SET pw = ? where id = ?", pw, self.id)
|
|
.execute(db)
|
|
.await
|
|
.unwrap(); //Okay, because we can only create a User of a valid id
|
|
}
|
|
|
|
fn get_hashed_pw(pw: &str) -> String {
|
|
let salt = SaltString::from_b64("dS/X5/sPEKTj4Rzs/CuvzQ").unwrap();
|
|
let argon2 = Argon2::default();
|
|
argon2
|
|
.hash_password(pw.as_bytes(), &salt)
|
|
.unwrap()
|
|
.to_string()
|
|
}
|
|
|
|
pub async fn logged_in(&self, db: &SqlitePool) {
|
|
sqlx::query!(
|
|
"UPDATE user SET last_access = CURRENT_TIMESTAMP where id = ?",
|
|
self.id
|
|
)
|
|
.execute(db)
|
|
.await
|
|
.unwrap(); //Okay, because we can only create a User of a valid id
|
|
}
|
|
|
|
pub async fn delete(&self, db: &SqlitePool) {
|
|
sqlx::query!("UPDATE user SET deleted=1 WHERE id=?", self.id)
|
|
.execute(db)
|
|
.await
|
|
.unwrap(); //Okay, because we can only create a User of a valid id
|
|
}
|
|
|
|
pub async fn get_days(&self, db: &SqlitePool) -> Vec<Day> {
|
|
let mut days = Vec::new();
|
|
for i in 0..self.amount_days_to_show(db).await {
|
|
let date = (Local::now() + chrono::Duration::days(i)).date_naive();
|
|
|
|
if self.has_role(db, "scheckbuch").await {
|
|
days.push(Day::new_guest(db, date, false).await);
|
|
} else {
|
|
days.push(Day::new(db, date, false).await);
|
|
}
|
|
}
|
|
|
|
for date in TripDetails::pinned_days(db, self.amount_days_to_show(db).await - 1).await {
|
|
if self.has_role(db, "scheckbuch").await {
|
|
let day = Day::new_guest(db, date, true).await;
|
|
if !day.events.is_empty() {
|
|
days.push(day);
|
|
}
|
|
} else {
|
|
days.push(Day::new(db, date, true).await);
|
|
}
|
|
}
|
|
days
|
|
}
|
|
|
|
pub(crate) async fn amount_days_to_show(&self, db: &SqlitePool) -> i64 {
|
|
if self.allowed_to_steer(db).await {
|
|
let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); //Ok,
|
|
//december
|
|
//has 31
|
|
//days
|
|
let days_left_in_year = end_of_year
|
|
.signed_duration_since(Local::now().date_naive())
|
|
.num_days()
|
|
+ 1;
|
|
|
|
if days_left_in_year <= 31 {
|
|
let end_of_next_year =
|
|
NaiveDate::from_ymd_opt(Local::now().year() + 1, 12, 31).unwrap(); //Ok,
|
|
//december
|
|
//has 31
|
|
//days
|
|
end_of_next_year
|
|
.signed_duration_since(Local::now().date_naive())
|
|
.num_days()
|
|
+ 1
|
|
} else {
|
|
days_left_in_year
|
|
}
|
|
} else {
|
|
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<'r> FromRequest<'r> for User {
|
|
type Error = LoginError;
|
|
|
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
match req.cookies().get_private("loggedin_user") {
|
|
Some(user_id) => match user_id.value().parse::<i32>() {
|
|
Ok(user_id) => {
|
|
let db = req.rocket().state::<SqlitePool>().unwrap();
|
|
let Some(user) = User::find_by_id(db, user_id).await else {
|
|
return Outcome::Error((Status::Forbidden, LoginError::UserNotFound));
|
|
};
|
|
if user.deleted {
|
|
return Outcome::Error((Status::Forbidden, LoginError::UserDeleted));
|
|
}
|
|
user.logged_in(db).await;
|
|
|
|
let mut cookie = Cookie::new("loggedin_user", format!("{}", user.id));
|
|
cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(2));
|
|
req.cookies().add_private(cookie);
|
|
|
|
Outcome::Success(user)
|
|
}
|
|
Err(_) => Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)),
|
|
},
|
|
None => Outcome::Error((Status::Unauthorized, LoginError::NotLoggedIn)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Creates a struct named $name. Allows to be created from a user, if one of the specified $roles are active for the user.
|
|
macro_rules! special_user {
|
|
($name:ident, $($role:tt)*) => {
|
|
#[derive(Debug)]
|
|
pub struct $name {
|
|
pub(crate) user: User,
|
|
}
|
|
|
|
impl Deref for $name {
|
|
type Target = User;
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.user
|
|
}
|
|
}
|
|
|
|
impl $name {
|
|
pub fn into_inner(self) -> User {
|
|
self.user
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<'r> FromRequest<'r> for $name {
|
|
type Error = LoginError;
|
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
let db = req.rocket().state::<SqlitePool>().unwrap();
|
|
match User::from_request(req).await {
|
|
Outcome::Success(user) => {
|
|
if special_user!(@check_roles user, db, $($role)*) {
|
|
Outcome::Success($name { user })
|
|
} else {
|
|
Outcome::Forward(Status::Forbidden)
|
|
}
|
|
}
|
|
Outcome::Error(f) => Outcome::Error(f),
|
|
Outcome::Forward(f) => Outcome::Forward(f),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl $name {
|
|
pub async fn new(db: &SqlitePool, user: User) -> Option<Self> {
|
|
if special_user!(@check_roles user, db, $($role)*) {
|
|
Some($name { user })
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
};
|
|
(@check_roles $user:ident, $db:ident, $(+$role:expr),* $(,-$neg_role:expr)*) => {
|
|
{
|
|
let mut has_positive_role = false;
|
|
$(
|
|
if $user.has_role($db, $role).await {
|
|
has_positive_role = true;
|
|
}
|
|
)*
|
|
has_positive_role
|
|
$(
|
|
&& !$user.has_role($db, $neg_role).await
|
|
)*
|
|
}
|
|
};
|
|
}
|
|
|
|
special_user!(SteeringUser, +"cox");
|
|
special_user!(AdminUser, +"admin");
|
|
special_user!(EventUser, +"manage_events");
|
|
special_user!(ManageUserUser, +"admin");
|
|
special_user!(AllowedToUpdateTripToAlwaysBeShownUser, +"admin");
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use std::collections::HashMap;
|
|
|
|
use crate::{tera::admin::user::UserEditForm, testdb};
|
|
|
|
use super::User;
|
|
use sqlx::SqlitePool;
|
|
|
|
#[sqlx::test]
|
|
fn test_find_correct_id() {
|
|
let pool = testdb!();
|
|
let user = User::find_by_id(&pool, 1).await.unwrap();
|
|
assert_eq!(user.id, 1);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_find_wrong_id() {
|
|
let pool = testdb!();
|
|
let user = User::find_by_id(&pool, 1337).await;
|
|
assert!(user.is_none());
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_find_correct_name() {
|
|
let pool = testdb!();
|
|
let user = User::find_by_name(&pool, "admin".into()).await.unwrap();
|
|
assert_eq!(user.id, 1);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_find_wrong_name() {
|
|
let pool = testdb!();
|
|
let user = User::find_by_name(&pool, "name-does-not-exist".into()).await;
|
|
assert!(user.is_none());
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_all() {
|
|
let pool = testdb!();
|
|
let res = User::all(&pool).await;
|
|
assert!(res.len() > 3);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_cox() {
|
|
let pool = testdb!();
|
|
let res = User::cox(&pool).await;
|
|
assert_eq!(res.len(), 4);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_succ_create() {
|
|
let pool = testdb!();
|
|
|
|
User::create(&pool, "new-user-name".into()).await;
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_duplicate_name_create() {
|
|
let pool = testdb!();
|
|
|
|
User::create(&pool, "admin".into()).await;
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn test_update() {
|
|
let pool = testdb!();
|
|
|
|
let user = User::find_by_id(&pool, 1).await.unwrap();
|
|
user.update(
|
|
&pool,
|
|
UserEditForm {
|
|
id: 1,
|
|
name: "adminn".to_string(),
|
|
roles: HashMap::new(),
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let user = User::find_by_id(&pool, 1).await.unwrap();
|
|
|
|
assert_eq!(user.name, "adminn".to_string());
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn succ_login_with_test_db() {
|
|
let pool = testdb!();
|
|
User::login(&pool, "admin".into(), "admin".into())
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn wrong_pw() {
|
|
let pool = testdb!();
|
|
assert!(User::login(&pool, "admin".into(), "admi".into())
|
|
.await
|
|
.is_err());
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn wrong_username() {
|
|
let pool = testdb!();
|
|
assert!(User::login(&pool, "admi".into(), "admin".into())
|
|
.await
|
|
.is_err());
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn reset() {
|
|
let pool = testdb!();
|
|
let user = User::find_by_id(&pool, 1).await.unwrap();
|
|
|
|
user.reset_pw(&pool).await;
|
|
|
|
let user = User::find_by_id(&pool, 1).await.unwrap();
|
|
assert_eq!(user.pw, None);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
fn update_pw() {
|
|
let pool = testdb!();
|
|
let user = User::find_by_id(&pool, 1).await.unwrap();
|
|
|
|
assert!(User::login(&pool, "admin".into(), "abc".into())
|
|
.await
|
|
.is_err());
|
|
|
|
user.update_pw(&pool, "abc".into()).await;
|
|
|
|
User::login(&pool, "admin".into(), "abc".into())
|
|
.await
|
|
.unwrap();
|
|
}
|
|
}
|