Add login+sign_up routes and auth guard

This commit is contained in:
D. Scott Boggs 2023-06-26 10:59:56 -04:00
parent 62ba1420b9
commit 14bd4b48ca
3 changed files with 82 additions and 4 deletions

73
server/src/api/auth.rs Normal file
View file

@ -0,0 +1,73 @@
use log::warn;
use rocket::{
http::{Cookie, CookieJar, Status},
outcome::IntoOutcome,
request::{self, FromRequest},
serde::json::Json,
Request, State,
};
use sea_orm::{prelude::*, DatabaseConnection};
use serde::Deserialize;
use crate::{
api::error::ApiResult,
entities::{prelude::*, *},
error::Error,
};
#[derive(Clone, Deserialize)]
pub(super) struct LoginData {
name: String,
password: String,
}
#[put("/", data = "<user_data>", format = "application/json")]
pub(super) async fn login(
db: &State<DatabaseConnection>,
user_data: Json<LoginData>,
cookies: &CookieJar<'_>,
) -> ApiResult<Status> {
let users = User::find()
.filter(user::Column::Name.eq(&user_data.name))
.all(db as &DatabaseConnection)
.await
.map_err(Error::from)?;
if users.len() > 1 {
warn!(count = users.len(), name = &user_data.name; "multiple entries found in database for user");
}
let Some(user) = users.get(0) else {
return Ok(Status::Unauthorized);
};
cookies.add_private(Cookie::new("user_id", user.id.to_string()));
Ok(Status::Ok)
}
#[post("/", data = "<user_data>", format = "application/json")]
pub(super) async fn sign_up(
db: &State<DatabaseConnection>,
user_data: Json<LoginData>,
cookies: &CookieJar<'_>,
) -> ApiResult<()> {
let user_data = user::ActiveModel::new(&user_data.name, &user_data.password)?
.insert(db as &DatabaseConnection)
.await
.map_err(Error::from)?;
cookies.add_private(Cookie::new("user_id", user_data.id.to_string()));
Ok(())
}
/// Authentication guard
struct Auth(i32);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Auth {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
request
.cookies()
.get_private("user_id")
.and_then(|val| val.value().parse().ok())
.map(|id| Auth(id))
.into_outcome((Status::Unauthorized, ()))
}
}

View file

@ -1,3 +1,4 @@
mod auth;
mod error;
mod groups;
#[cfg(feature = "unsafe_import")]
@ -111,6 +112,7 @@ pub(crate) fn start_server(db: DatabaseConnection) -> Rocket<Build> {
"/api/v1/groups",
routes![all_groups, group, insert_group, update_group, delete_group],
)
.mount("/api/v1/auth", routes![auth::login, auth::sign_up])
.mount("/", FileServer::from("/src/public"));
#[cfg(feature = "unsafe_import")]

View file

@ -3,19 +3,22 @@
use std::default::default;
use bcrypt::*;
// TODO Add option for argon2 https://docs.rs/argon2/latest/argon2/
use either::Either::{self, Left, Right};
use rocket::response::status::Unauthorized;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::{
api::ErrorResponder,
error::{self, Error},
};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: i32,
pub name: String,
pub password_hash: String,
@ -27,10 +30,10 @@ pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
impl ActiveModel {
pub fn new(name: String, password: String) -> error::Result<Self> {
pub fn new(name: impl AsRef<str>, password: impl AsRef<str>) -> error::Result<Self> {
use sea_orm::ActiveValue::Set;
let name = Set(name);
let password_hash = Set(hash(password, DEFAULT_COST + 2)?);
let name = Set(name.as_ref().to_string());
let password_hash = Set(hash(password.as_ref(), DEFAULT_COST + 2)?);
Ok(Self {
name,
password_hash,