First working API.
Simple auth by creating sessions and storing in db
This commit is contained in:
5
src/services/mod.rs
Normal file
5
src/services/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod reservation_service;
|
||||
pub mod room_service;
|
||||
pub mod session_service;
|
||||
pub mod task_service;
|
||||
pub mod user_service;
|
322
src/services/reservation_service.rs
Normal file
322
src/services/reservation_service.rs
Normal file
@ -0,0 +1,322 @@
|
||||
use crate::models::reservation::Reservation;
|
||||
use crate::schema::{reservation, room};
|
||||
use diesel::expression_methods::ExpressionMethods;
|
||||
use diesel::result::DatabaseErrorKind;
|
||||
use diesel::{BoolExpressionMethods, QueryDsl};
|
||||
use diesel_async::{AsyncPgConnection, RunQueryDsl};
|
||||
use lib::diesel_crud_trait::CrudError;
|
||||
use lib::time::DateTimeInterval;
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
|
||||
impl Reservation {
|
||||
pub async fn check_in(id: i32, conn: &mut AsyncPgConnection) -> Result<(), ReservationError> {
|
||||
Self::update_check_in(id, true, conn).await
|
||||
}
|
||||
|
||||
pub async fn check_out(id: i32, conn: &mut AsyncPgConnection) -> Result<(), ReservationError> {
|
||||
Self::update_check_in(id, false, conn).await
|
||||
}
|
||||
|
||||
pub async fn room_available(
|
||||
room_number: i32,
|
||||
DateTimeInterval { start, end }: DateTimeInterval,
|
||||
conn: &mut AsyncPgConnection,
|
||||
) -> Result<(), ReservationError> {
|
||||
let available_rooms: i64 = room::table
|
||||
.left_outer_join(reservation::table)
|
||||
.filter(room::id.eq(room_number))
|
||||
.filter(
|
||||
reservation::id
|
||||
.is_null()
|
||||
.or(reservation::end.le(start))
|
||||
.or(reservation::start.ge(end)),
|
||||
)
|
||||
.count()
|
||||
.get_result(conn)
|
||||
.await?;
|
||||
if available_rooms == 0 {
|
||||
Err(ReservationError::RoomNotAvailable)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_check_in(
|
||||
id: i32,
|
||||
to: bool,
|
||||
conn: &mut AsyncPgConnection,
|
||||
) -> Result<(), ReservationError> {
|
||||
let find_query = reservation::table.find(id);
|
||||
|
||||
let reservation: Reservation = find_query.get_result(conn).await?;
|
||||
if to && reservation.checked_in {
|
||||
return Err(ReservationError::AlreadyCheckedIn);
|
||||
}
|
||||
diesel::update(find_query)
|
||||
.set(reservation::checked_in.eq(to))
|
||||
.execute(conn)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Error, Serialize)]
|
||||
pub enum ReservationError {
|
||||
#[error("Reservation not found")]
|
||||
NotFound,
|
||||
#[error("Reservation already checked in")]
|
||||
AlreadyCheckedIn,
|
||||
#[error("Room does not exist")]
|
||||
RoomNotFound,
|
||||
#[error("User does not exist")]
|
||||
UserNotFound,
|
||||
#[error("Room is not available")]
|
||||
RoomNotAvailable,
|
||||
#[error("Database error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<CrudError> for ReservationError {
|
||||
fn from(value: CrudError) -> Self {
|
||||
match value {
|
||||
CrudError::NotFound => ReservationError::NotFound,
|
||||
CrudError::PoolError(pool_error) => ReservationError::Other(pool_error),
|
||||
CrudError::Other(diesel_error) => diesel_error.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DieselError = diesel::result::Error;
|
||||
|
||||
impl From<DieselError> for ReservationError {
|
||||
fn from(error: DieselError) -> Self {
|
||||
match &error {
|
||||
DieselError::NotFound => ReservationError::NotFound,
|
||||
DieselError::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, info) => {
|
||||
if info.constraint_name().unwrap_or_default().contains("room") {
|
||||
ReservationError::RoomNotFound
|
||||
} else if info.constraint_name().unwrap_or_default().contains("user") {
|
||||
ReservationError::UserNotFound
|
||||
} else {
|
||||
ReservationError::Other(error.to_string())
|
||||
}
|
||||
}
|
||||
_ => ReservationError::Other(error.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::models::hotel::{CreateHotel, Hotel};
|
||||
use crate::models::reservation::CreateReservation;
|
||||
use crate::models::room::Room;
|
||||
use crate::models::user::{CreateUser, User};
|
||||
use crate::schema::{hotel, room, user};
|
||||
use crate::test::setup_test_transaction;
|
||||
use chrono::{Duration, Utc};
|
||||
use diesel::dsl::insert_into;
|
||||
use diesel_async::AsyncPgConnection;
|
||||
use lib::diesel_crud_trait::{DieselCrudCreate, DieselCrudRead};
|
||||
use secrecy::SecretString;
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_check_in(#[future] setup: Setup) {
|
||||
let Setup {
|
||||
mut conn,
|
||||
reservation,
|
||||
..
|
||||
} = setup.await;
|
||||
|
||||
Reservation::check_in(reservation.id, &mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
Reservation::read(reservation.id, &mut conn)
|
||||
.await
|
||||
.unwrap()
|
||||
.checked_in
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_check_out(#[future] setup: Setup) {
|
||||
let Setup {
|
||||
mut conn,
|
||||
reservation,
|
||||
..
|
||||
} = setup.await;
|
||||
|
||||
Reservation::check_out(reservation.id, &mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
!Reservation::read(reservation.id, &mut conn)
|
||||
.await
|
||||
.unwrap()
|
||||
.checked_in
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::available(
|
||||
Duration::days(12),
|
||||
Duration::days(14),
|
||||
Ok(())
|
||||
)]
|
||||
#[case::start_and_end_in_interval(
|
||||
Duration::days(2),
|
||||
Duration::days(4),
|
||||
Err(ReservationError::RoomNotAvailable)
|
||||
)]
|
||||
#[case::start_in_interval(
|
||||
Duration::days(6),
|
||||
Duration::days(10),
|
||||
Err(ReservationError::RoomNotAvailable)
|
||||
)]
|
||||
#[case::end_in_interval(
|
||||
Duration::days(-2),
|
||||
Duration::days(2),
|
||||
Err(ReservationError::RoomNotAvailable)
|
||||
)]
|
||||
#[case::start_before_and_end_after_interval(
|
||||
Duration::days(-2),
|
||||
Duration::days(11),
|
||||
Err(ReservationError::RoomNotAvailable)
|
||||
)]
|
||||
#[tokio::test]
|
||||
async fn test_room_available(
|
||||
#[future] setup: Setup,
|
||||
#[case] start: Duration,
|
||||
#[case] end: Duration,
|
||||
#[case] expected: Result<(), ReservationError>,
|
||||
) {
|
||||
let Setup {
|
||||
mut conn,
|
||||
reservation,
|
||||
..
|
||||
} = setup.await;
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
let start = now + start;
|
||||
let end = now + end;
|
||||
|
||||
assert_eq!(
|
||||
Reservation::room_available(reservation.room_id, (start, end).into(), &mut conn).await,
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_room_available_no_reservations(#[future] setup: Setup) {
|
||||
let Setup {
|
||||
mut conn, hotel, ..
|
||||
} = setup.await;
|
||||
|
||||
let room = Room::insert(
|
||||
Room {
|
||||
id: 2,
|
||||
hotel_id: hotel.id,
|
||||
beds: 1,
|
||||
size: 1,
|
||||
},
|
||||
&mut conn,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
let start = now;
|
||||
let end = now + Duration::days(10);
|
||||
|
||||
assert_eq!(
|
||||
Reservation::room_available(room.id, (start, end).into(), &mut conn).await,
|
||||
Ok(())
|
||||
);
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
async fn setup() -> Setup {
|
||||
let mut conn = setup_test_transaction().await.unwrap();
|
||||
let hotel = insert_hotel(&mut conn).await;
|
||||
let user = insert_user(&mut conn).await;
|
||||
let room = insert_room(&mut conn, hotel.id).await;
|
||||
let reservation = insert_reservation(&mut conn, room.id, user.email).await;
|
||||
Setup {
|
||||
conn,
|
||||
hotel,
|
||||
reservation,
|
||||
}
|
||||
}
|
||||
|
||||
struct Setup {
|
||||
conn: AsyncPgConnection,
|
||||
hotel: Hotel,
|
||||
reservation: Reservation,
|
||||
}
|
||||
|
||||
async fn insert_hotel(conn: &mut AsyncPgConnection) -> Hotel {
|
||||
insert_into(hotel::table)
|
||||
.values(CreateHotel::new(
|
||||
None,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
))
|
||||
.get_result(conn)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn insert_room(conn: &mut AsyncPgConnection, hotel_id: i32) -> Room {
|
||||
insert_into(room::table)
|
||||
.values(Room {
|
||||
id: 1,
|
||||
hotel_id,
|
||||
beds: 1,
|
||||
size: 1,
|
||||
})
|
||||
.get_result(conn)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn insert_user(conn: &mut AsyncPgConnection) -> User {
|
||||
insert_into(user::table)
|
||||
.values(
|
||||
User::hash_from_credentials(CreateUser {
|
||||
email: "test_again@test.com".to_string(),
|
||||
password: SecretString::new("test".to_string()),
|
||||
role: None,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.get_result(conn)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn insert_reservation(
|
||||
conn: &mut AsyncPgConnection,
|
||||
room_id: i32,
|
||||
user: String,
|
||||
) -> Reservation {
|
||||
let now = Utc::now().naive_utc();
|
||||
insert_into(reservation::table)
|
||||
.values(CreateReservation {
|
||||
room_id,
|
||||
user,
|
||||
start: now,
|
||||
end: now + Duration::days(7),
|
||||
})
|
||||
.get_result(conn)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
206
src/services/room_service.rs
Normal file
206
src/services/room_service.rs
Normal file
@ -0,0 +1,206 @@
|
||||
use crate::error::AppError;
|
||||
use crate::models::room::Room;
|
||||
use crate::schema::{reservation, room};
|
||||
use diesel::expression_methods::ExpressionMethods;
|
||||
use diesel::{BoolExpressionMethods, QueryDsl};
|
||||
use diesel_async::{AsyncPgConnection, RunQueryDsl};
|
||||
use lib::time::DateTimeInterval;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[cfg_attr(test, bon::builder)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default, Deserialize)]
|
||||
pub struct RoomQuery {
|
||||
pub available: Option<DateTimeInterval>,
|
||||
pub hotel_id: Option<i32>,
|
||||
pub min_beds: Option<i32>,
|
||||
pub min_size: Option<i32>,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
pub async fn query_rooms(
|
||||
RoomQuery {
|
||||
available,
|
||||
hotel_id,
|
||||
min_beds,
|
||||
min_size,
|
||||
}: RoomQuery,
|
||||
conn: &mut AsyncPgConnection,
|
||||
) -> Result<Vec<Room>, AppError> {
|
||||
let mut table = room::table.left_outer_join(reservation::table).into_boxed();
|
||||
if let Some(DateTimeInterval { start, end }) = available {
|
||||
table = table.filter(
|
||||
reservation::id
|
||||
.is_null()
|
||||
.or(reservation::end.le(start))
|
||||
.or(reservation::start.ge(end)),
|
||||
)
|
||||
}
|
||||
if let Some(id) = hotel_id {
|
||||
table = table.filter(room::hotel_id.eq(id));
|
||||
}
|
||||
if let Some(beds) = min_beds {
|
||||
table = table.filter(room::beds.ge(beds));
|
||||
}
|
||||
if let Some(size) = min_size {
|
||||
table = table.filter(room::size.ge(size));
|
||||
}
|
||||
table
|
||||
.select(room::all_columns)
|
||||
.get_results(conn)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::models::hotel::{CreateHotel, Hotel};
|
||||
use crate::models::reservation::{CreateReservation, Reservation};
|
||||
use crate::models::room::Room;
|
||||
use crate::models::user::{CreateUser, User};
|
||||
use crate::test::setup_test_transaction;
|
||||
use diesel_async::AsyncPgConnection;
|
||||
use lib::diesel_crud_trait::DieselCrudCreate;
|
||||
use serde_json::json;
|
||||
|
||||
#[rstest]
|
||||
#[case::all_none(RoomQuery::default(), &[1, 2, 3, 4])]
|
||||
#[case::min_beds_are_2(
|
||||
RoomQuery::builder()
|
||||
.min_beds(2)
|
||||
.build(),
|
||||
&[3, 4]
|
||||
)]
|
||||
#[case::min_beds_are_greater_than_all(
|
||||
RoomQuery::builder()
|
||||
.hotel_id(5)
|
||||
.build(),
|
||||
&[]
|
||||
)]
|
||||
#[case::min_size_are_5(
|
||||
RoomQuery::builder()
|
||||
.min_size(5)
|
||||
.build(),
|
||||
&[2, 4]
|
||||
)]
|
||||
#[case::min_size_is_greater_than_all(
|
||||
RoomQuery::builder()
|
||||
.min_size(10)
|
||||
.build(),
|
||||
&[]
|
||||
)]
|
||||
#[case::min_size_and_min_beds(
|
||||
RoomQuery::builder()
|
||||
.min_size(5)
|
||||
.min_beds(2)
|
||||
.build(),
|
||||
&[4]
|
||||
)]
|
||||
#[case::interval_when_no_reservations_in_interval(
|
||||
RoomQuery::builder()
|
||||
.available(DateTimeInterval {
|
||||
start: chrono::Utc::now().naive_utc() + chrono::Duration::days(10),
|
||||
end: chrono::Utc::now().naive_utc() + chrono::Duration::days(20)
|
||||
})
|
||||
.build(),
|
||||
&[1, 2, 3, 4]
|
||||
)]
|
||||
#[case::interval_when_one_reservation_in_interval(
|
||||
RoomQuery::builder()
|
||||
.available(DateTimeInterval {
|
||||
start: chrono::Utc::now().naive_utc(),
|
||||
end: chrono::Utc::now().naive_utc() + chrono::Duration::days(10)
|
||||
})
|
||||
.build(),
|
||||
&[2, 3, 4]
|
||||
)]
|
||||
#[case::all_filters(
|
||||
RoomQuery::builder()
|
||||
.available(DateTimeInterval {
|
||||
start: chrono::Utc::now().naive_utc(),
|
||||
end: chrono::Utc::now().naive_utc() + chrono::Duration::days(10)
|
||||
})
|
||||
.hotel_id(1)
|
||||
.min_beds(2)
|
||||
.min_size(1)
|
||||
.build(),
|
||||
&[3]
|
||||
)]
|
||||
#[tokio::test]
|
||||
async fn test_query_rooms(
|
||||
#[future] setup: Setup,
|
||||
#[case] query: RoomQuery,
|
||||
#[case] expected_ids: &[i32],
|
||||
) {
|
||||
let Setup {
|
||||
mut conn, rooms, ..
|
||||
} = setup.await;
|
||||
let result = Room::query_rooms(query, &mut conn).await.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
rooms
|
||||
.into_iter()
|
||||
.filter(|room| expected_ids.contains(&room.id))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
async fn setup() -> Setup {
|
||||
let mut conn = setup_test_transaction().await.unwrap();
|
||||
let hotels = insert_hotels(&mut conn).await;
|
||||
let rooms = insert_rooms(&mut conn, &hotels).await;
|
||||
let _reservation = insert_reservation(&mut conn, rooms[0].id).await;
|
||||
Setup { conn, rooms }
|
||||
}
|
||||
|
||||
struct Setup {
|
||||
conn: AsyncPgConnection,
|
||||
rooms: Vec<Room>,
|
||||
}
|
||||
|
||||
async fn insert_hotels(conn: &mut AsyncPgConnection) -> Vec<Hotel> {
|
||||
let hotels = vec![
|
||||
CreateHotel::new(Some(1), "hotel1".into(), "hotel1".into()),
|
||||
CreateHotel::new(Some(2), "hotel2".into(), "hotel2".into()),
|
||||
];
|
||||
Hotel::insert_many(&hotels, conn).await.unwrap()
|
||||
}
|
||||
|
||||
async fn insert_rooms(conn: &mut AsyncPgConnection, hotels: &[Hotel]) -> Vec<Room> {
|
||||
let rooms: Vec<Room> = json!(
|
||||
[
|
||||
{ "beds": 1, "size": 1 },
|
||||
{ "beds": 1, "size": 5 },
|
||||
{ "beds": 2, "size": 1 },
|
||||
{ "beds": 2, "size": 5 }
|
||||
]
|
||||
)
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, room)| Room {
|
||||
hotel_id: hotels[index % 2].id,
|
||||
beds: room["beds"].as_i64().unwrap() as i32,
|
||||
size: room["size"].as_i64().unwrap() as i32,
|
||||
id: index as i32 + 1,
|
||||
})
|
||||
.collect();
|
||||
Room::insert_many(&rooms, conn).await.unwrap();
|
||||
rooms
|
||||
}
|
||||
async fn insert_reservation(conn: &mut AsyncPgConnection, room_id: i32) -> Reservation {
|
||||
let user = User::insert(
|
||||
User::hash_from_credentials(CreateUser::new("test@test.test", "test")).unwrap(),
|
||||
conn,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
let reservation =
|
||||
CreateReservation::new(room_id, now, now + chrono::Duration::days(5), user.email);
|
||||
Reservation::insert(reservation, conn).await.unwrap()
|
||||
}
|
||||
}
|
102
src/services/session_service.rs
Normal file
102
src/services/session_service.rs
Normal file
@ -0,0 +1,102 @@
|
||||
use crate::error::AppError;
|
||||
use crate::models::session::Session;
|
||||
use crate::GetConnection;
|
||||
use axum::async_trait;
|
||||
use derive_more::Constructor;
|
||||
use lib::diesel_crud_trait::{DieselCrudCreate, DieselCrudDelete, DieselCrudRead};
|
||||
use std::fmt::Debug;
|
||||
use tower_sessions::session::{Id, Record};
|
||||
use tower_sessions::{session_store, SessionStore};
|
||||
|
||||
#[derive(Clone, Constructor)]
|
||||
pub struct SessionService<Pool>
|
||||
where
|
||||
Pool: GetConnection,
|
||||
{
|
||||
pool: Pool,
|
||||
}
|
||||
|
||||
impl<Pool> Debug for SessionService<Pool>
|
||||
where
|
||||
Pool: GetConnection,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SessionService")
|
||||
.field("pool", &self.pool.status())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Pool> SessionService<Pool>
|
||||
where
|
||||
Pool: GetConnection,
|
||||
{
|
||||
async fn create(&self, session: Session) -> Result<Session, AppError> {
|
||||
Session::insert(session, self.pool.get().await?.as_mut())
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
async fn read(&self, session_id: String) -> Result<Session, AppError> {
|
||||
Session::read(session_id, self.pool.get().await?.as_mut())
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn delete(&self, session_id: String) -> Result<Session, AppError> {
|
||||
Session::delete(session_id, self.pool.get().await?.as_mut())
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<Pool> SessionStore for SessionService<Pool>
|
||||
where
|
||||
Pool: GetConnection + 'static,
|
||||
{
|
||||
async fn create(&self, session_record: &mut Record) -> session_store::Result<()> {
|
||||
self.save(session_record).await
|
||||
}
|
||||
|
||||
async fn save(&self, session_record: &Record) -> session_store::Result<()> {
|
||||
let Ok(model) = Session::try_from(session_record.clone()) else {
|
||||
return Err(session_store::Error::Backend(
|
||||
"Failed to parse record to session model".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
Self::create(self, model).await.map_err(|error| {
|
||||
session_store::Error::Backend(format!("Failed to save session: {}", error))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load(&self, session_id: &Id) -> session_store::Result<Option<Record>> {
|
||||
match self.read(session_id.0.to_string()).await {
|
||||
Ok(session) => match session.try_into_record() {
|
||||
Ok(record) => Ok(Some(record)),
|
||||
Err(error) => Err(session_store::Error::Decode(error.to_string())),
|
||||
},
|
||||
Err(AppError::NotFound) => Ok(None),
|
||||
Err(error) => Err(session_store::Error::Backend(format!(
|
||||
"Failed to load session: {error}",
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Session-fixation cycling attempts to delete the session with the old session ID.
|
||||
/// If the session does not exist, it is considered a success.
|
||||
async fn delete(&self, session_id: &Id) -> session_store::Result<()> {
|
||||
match Self::delete(self, session_id.0.to_string()).await {
|
||||
Ok(_) | Err(AppError::NotFound) => Ok(()),
|
||||
Err(error) => Err(session_store::Error::Backend(format!(
|
||||
"Failed to delete session: {error}",
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// TODO
|
||||
}
|
37
src/services/task_service.rs
Normal file
37
src/services/task_service.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use crate::error::AppError;
|
||||
use crate::models::task::{Task, TaskStatus};
|
||||
use crate::schema::task;
|
||||
use diesel::expression_methods::ExpressionMethods;
|
||||
use diesel::QueryDsl;
|
||||
use diesel_async::{AsyncPgConnection, RunQueryDsl};
|
||||
|
||||
impl Task {
|
||||
pub async fn set_description(
|
||||
id: i32,
|
||||
description: String,
|
||||
conn: &mut AsyncPgConnection,
|
||||
) -> Result<(), AppError> {
|
||||
diesel::update(task::table.find(id))
|
||||
.set(task::description.eq(description))
|
||||
.execute(conn)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_status(
|
||||
id: i32,
|
||||
status: TaskStatus,
|
||||
conn: &mut AsyncPgConnection,
|
||||
) -> Result<(), AppError> {
|
||||
diesel::update(task::table.find(id))
|
||||
.set(task::status.eq(status.to_string()))
|
||||
.execute(conn)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// TODO
|
||||
}
|
75
src/services/user_service.rs
Normal file
75
src/services/user_service.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use crate::auth::hash_password;
|
||||
use crate::error::{AppError, ResponseError};
|
||||
use crate::models::user::{CreateUser, User, UserCredentials};
|
||||
use crate::GetConnection;
|
||||
use axum::async_trait;
|
||||
use axum_login::{AuthnBackend, UserId};
|
||||
use base64ct::{Base64, Encoding};
|
||||
use derive_more::Constructor;
|
||||
use lib::diesel_crud_trait::{DieselCrudCreate, DieselCrudRead};
|
||||
use secrecy::ExposeSecret;
|
||||
|
||||
#[derive(Clone, Constructor)]
|
||||
pub struct UserService<Pool>
|
||||
where
|
||||
Pool: GetConnection,
|
||||
{
|
||||
pool: Pool,
|
||||
}
|
||||
|
||||
impl<Pool> UserService<Pool>
|
||||
where
|
||||
Pool: GetConnection,
|
||||
{
|
||||
pub async fn insert(&self, create: CreateUser) -> Result<User, ResponseError> {
|
||||
User::insert(
|
||||
User::hash_from_credentials(create)?,
|
||||
self.pool.get().await?.as_mut(),
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn read(&self, email: String) -> Result<User, AppError> {
|
||||
User::read(email, self.pool.get().await?.as_mut())
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<Pool> AuthnBackend for UserService<Pool>
|
||||
where
|
||||
Pool: GetConnection,
|
||||
{
|
||||
type User = User;
|
||||
type Credentials = UserCredentials;
|
||||
type Error = AppError;
|
||||
|
||||
async fn authenticate(
|
||||
&self,
|
||||
UserCredentials { email, password }: Self::Credentials,
|
||||
) -> Result<Option<Self::User>, Self::Error> {
|
||||
let user = self.read(email.clone()).await?;
|
||||
let password = password.expose_secret();
|
||||
let mut input_hash = [0; 32];
|
||||
hash_password(password, user.salt.as_bytes(), &mut input_hash);
|
||||
|
||||
let mut user_hash = [0; 32];
|
||||
Base64::decode(&user.hash, &mut user_hash)?;
|
||||
if user.email == email && user_hash == input_hash {
|
||||
Ok(Some(user))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_user(&self, email: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
|
||||
Ok(self.read(email.to_string()).await.ok())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// TODO
|
||||
}
|
Reference in New Issue
Block a user