#Feedback on my first API

5 messages · Page 1 of 1 (latest)

glass bobcat
#

Model:

use rocket::serde::{Deserialize, Serialize};
use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "account")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub name: String,
    pub password: String,
    pub role: Role,
    pub employee_menu_access: Option<AccessLevel>,
    #[sea_orm(default_expr = "Expr::current_timestamp()")]
    pub creation_date: DateTimeWithTimeZone
}

#[derive(
    EnumIter,
    DeriveActiveEnum,
    Clone,
    Debug,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Serialize,
    Deserialize
)]
#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(13))")]
pub enum Role {
    #[sea_orm(string_value = "Employee")]
    Employee,

    #[sea_orm(string_value = "Manager")]
    Manager,

    #[sea_orm(string_value = "Administrator")]
    Administrator,
}

#[derive(EnumIter, DeriveActiveEnum, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(5))")]
pub enum AccessLevel {
    #[sea_orm(string_value = "None")]
    None,

    #[sea_orm(string_value = "Read")]
    Read,

    #[sea_orm(string_value = "Write")]
    Write
}

impl ActiveModelBehavior for ActiveModel {}

#[derive(Serialize, Deserialize)]
pub struct LoginAccountDTO {
    pub name: String,
    pub password: String
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RegisterAccountDTO {
    pub name: String,
    pub password: String,
    pub role: Role,
    pub employee_menu_access: Option<AccessLevel>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct UpdateAccountDTO {
    pub name: Option<String>,
    pub password: Option<String>,
    pub role: Option<Role>,
    pub employee_menu_access: Option<AccessLevel>,
}
#

Middelware (mod.rs of api module)

use crate::model::account::Role;
use crate::portal::account::Claims;

use std::env;

use rocket::Route;
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome, Request};
use jsonwebtoken::{decode, DecodingKey, Validation};

pub mod event;
pub mod account;

pub fn routes() -> Vec<Route> {
    routes![
        account::login_account,
        account::logout_account,
        account::register_account,
        account::update_account,
        account::delete_account,
        event::request_event
    ]
}

pub struct EmployeeLevelAuthorization {
    id: i32,
    role: Role
}

pub struct ManagerLevelAuthorization {
    id: i32,
    role: Role
}

pub struct AdministratorLevelAuthorization {
    id: i32,
    role: Role
}
#
#[rocket::async_trait]
impl<'l> FromRequest<'l> for EmployeeLevelAuthorization {
    type Error = ();

    async fn from_request(request: &'l Request<'_>) -> Outcome<Self, Self::Error> {
        verify_jwt(request, Role::Employee, |id, role| EmployeeLevelAuthorization { id, role })
    }
}

#[rocket::async_trait]
impl<'l> FromRequest<'l> for ManagerLevelAuthorization {
    type Error = ();

    async fn from_request(request: &'l Request<'_>) -> Outcome<Self, Self::Error> {
        verify_jwt(request, Role::Manager, |id, role| ManagerLevelAuthorization { id, role })
    }
}

#[rocket::async_trait]
impl<'l> FromRequest<'l> for AdministratorLevelAuthorization {
    type Error = ();

    async fn from_request(request: &'l Request<'_>) -> Outcome<Self, Self::Error> {
        verify_jwt(request, Role::Administrator, |id, role| AdministratorLevelAuthorization { id, role })
    }
}

fn verify_jwt<A>(request: &Request, role: Role, constructor: fn(i32, Role) -> A) -> Outcome<A, ()> {
    match request.cookies().get("sessionToken") {
        Some(cookie) => {
            match decode_jwt(cookie.value()) {
                Some(claims) => {
                    if claims.role >= role { Outcome::Success(constructor(claims.sub, claims.role)) }
                    else { Outcome::Error((Status::Forbidden, ())) }
                },
                None => Outcome::Error((Status::Unauthorized, ()))
            }
        }
        None => Outcome::Error((Status::Unauthorized, ()))
    }
}

pub fn decode_jwt(session_token: &str) -> Option<Claims> {
    decode::<Claims>(session_token,
                     &DecodingKey::from_secret(env::var("JWT_SECRET").unwrap().as_bytes()),
                     &Validation::default()).ok().map(|payload| payload.claims)
}
#

API

use crate::model::account;
use crate::model::account::{LoginAccountDTO, RegisterAccountDTO, UpdateAccountDTO, Role};
use crate::portal::{EmployeeLevelAuthorization, ManagerLevelAuthorization};

use std::string::String;
use std::env;

use rocket::State;
use rocket::http::{Cookie, CookieJar, SameSite, Status};
use rocket::serde::json::Json;
use argon2::password_hash::SaltString;
use argon2::password_hash::rand_core::OsRng;
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use jsonwebtoken::{encode, EncodingKey, Header};
use chrono::{Duration, Utc};
use rocket::serde::{Deserialize, Serialize};
use sea_orm::ActiveValue::*;
use sea_orm::{DatabaseConnection, EntityTrait, ColumnTrait, QueryFilter};

#[derive(Serialize, Deserialize, Debug)]
pub struct Claims {
    pub sub: i32,
    pub role: Role,
    pub iat: usize,
    pub exp: usize,
}

fn create_jwt(id: i32, role: Role, cookie_jar: &CookieJar) {
    let current_date_and_time = Utc::now();
    let expiration_date = current_date_and_time + Duration::hours(16);
    let claims = Claims {
        sub: id,
        role,
        exp: expiration_date.timestamp() as usize,
        iat: current_date_and_time.timestamp() as usize,
    };

    let session_token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(env::var("JWT_SECRET").unwrap().as_bytes()),
    ).unwrap();

    cookie_jar.add(
        Cookie::build(("sessionToken", session_token))
            .http_only(true)
            .same_site(SameSite::Lax)
            .secure(true)
            .path("/")
    );
}
#

fn normalise_name(name: &str) -> String {
    name.to_lowercase().chars().filter(|character| !character.is_whitespace()).collect()
}

#[post("/account/login", data = "<login_account_dto>")]
pub async fn login_account(database: &State<DatabaseConnection>,
                           cookie_jar: &CookieJar<'_>,
                           login_account_dto: Json<LoginAccountDTO>,
) -> Status {
    let login_account_dto = login_account_dto.into_inner();

    let account = match account::Entity::find()
        .filter(account::Column::Name.eq(normalise_name(&*login_account_dto.name)))
        .one(database.inner())
        .await {
        Ok(Some(account)) => account,
        Ok(None) => return Status::Unauthorized,
        Err(_) => return Status::InternalServerError
    };

    if Argon2::default()
        .verify_password(login_account_dto.password.as_bytes(), &PasswordHash::new(&*account.password).unwrap())
        .is_err() {
        return Status::Unauthorized;
    }

    create_jwt(account.id, account.role, cookie_jar);

    Status::Ok
}

#[post("/account/logout")]
pub fn logout_account(authorization: EmployeeLevelAuthorization, cookie_jar: &CookieJar<'_>) -> Status {
    cookie_jar.remove(Cookie::from("sessionToken"));
    Status::Ok
}