Migrate admin endpoint to JWT

This commit is contained in:
eraden 2022-05-17 08:23:39 +02:00
parent d5e675b6dc
commit f0d01a9735
17 changed files with 325 additions and 247 deletions

View File

@ -13,3 +13,7 @@ members = [
"actors/fs_manager",
"db-seed"
]
[profile.release]
lto = true
opt-level = 's'

View File

@ -116,6 +116,8 @@ pub enum Error {
Validate,
#[error("Unable to validate token. Can't connect to database")]
ValidateInternal,
#[error("Token does not exists or some fields are incorrect")]
Invalid,
}
pub type Result<T> = std::result::Result<T, Error>;
@ -247,18 +249,18 @@ pub(crate) async fn create_token(
}
#[derive(Message)]
#[rtype(result = "Result<(Token, bool)>")]
#[rtype(result = "Result<Token>")]
pub struct Validate {
pub token: AccessTokenString,
}
token_async_handler!(Validate, validate, (Token, bool));
token_async_handler!(Validate, validate, Token);
pub(crate) async fn validate(
msg: Validate,
db: Addr<Database>,
config: SharedAppConfig,
) -> Result<(Token, bool)> {
) -> Result<Token> {
use jwt::VerifyWithKey;
log::info!("Validating token {:?}", msg.token);
@ -291,32 +293,32 @@ pub(crate) async fn validate(
}
if !validate_pair(&claims, "cti", token.customer_id, validate_uuid) {
return Ok((token, false));
return Err(Error::Invalid);
}
if !validate_pair(&claims, "arl", token.role, |left, right| right == left) {
return Ok((token, false));
return Err(Error::Invalid);
}
if !validate_pair(&claims, "iss", &token.issuer, |left, right| right == left) {
return Ok((token, false));
return Err(Error::Invalid);
}
if !validate_pair(&claims, "sub", token.subject, validate_num) {
return Ok((token, false));
return Err(Error::Invalid);
}
if !validate_pair(&claims, "aud", token.audience, |left, right| right == left) {
return Ok((token, false));
return Err(Error::Invalid);
}
if !validate_pair(&claims, "exp", &token.expiration_time, validate_time) {
return Ok((token, false));
return Err(Error::Invalid);
}
if !validate_pair(&claims, "nbt", &token.not_before_time, validate_time) {
return Ok((token, false));
return Err(Error::Invalid);
}
if !validate_pair(&claims, "iat", &token.issued_at_time, validate_time) {
return Ok((token, false));
return Err(Error::Invalid);
}
log::info!("JWT token valid");
Ok((token, true))
Ok(token)
}
fn build_key(secret: String) -> Result<Hmac<Sha256>> {

View File

@ -1,15 +1,9 @@
mod api_v1;
use actix::Addr;
use actix_session::Session;
use actix_web::web::{scope, Data, Json, ServiceConfig};
use actix_web::{delete, get, post, HttpResponse};
use config::SharedAppConfig;
use database_manager::{query_db, Database};
use model::{Encrypt, PassHash};
use actix_web::web::{scope, ServiceConfig};
use actix_web::{get, HttpResponse};
use crate::routes;
use crate::routes::{RequireLogin, Result};
use crate::routes::Result;
#[macro_export]
macro_rules! admin_send_db {
@ -42,86 +36,6 @@ pub enum Error {
Database(#[from] database_manager::Error),
}
#[delete("logout")]
async fn logout(session: Session) -> Result<Json<model::api::admin::LogoutResponse>> {
session.require_admin()?;
session.clear();
Ok(Json(model::api::admin::LogoutResponse {}))
}
#[post("/sign-in")]
async fn sign_in(
session: Session,
db: Data<Addr<Database>>,
Json(payload): Json<model::api::admin::SignInInput>,
) -> Result<HttpResponse> {
log::debug!("{:?}", payload);
let db = db.into_inner();
let user: model::FullAccount = query_db!(
db,
database_manager::AccountByIdentity {
email: payload.email,
login: payload.login,
},
routes::Error::Unauthorized
);
if let Err(e) = payload.password.validate(&user.pass_hash) {
log::error!("Password validation failed. {}", e);
Err(routes::Error::Unauthorized)
} else {
if let Err(e) = session.insert("admin_id", *user.id) {
log::error!("{:?}", e);
}
Ok(HttpResponse::Ok().json(model::Account::from(user)))
}
}
// login_required
#[post("/register")]
async fn register(
session: Session,
Json(input): Json<model::api::admin::RegisterInput>,
db: Data<Addr<Database>>,
config: Data<SharedAppConfig>,
) -> Result<HttpResponse> {
let mut response = model::api::admin::RegisterResponse::default();
session.require_admin()?;
if input.password != input.password_confirmation {
response
.errors
.push(model::api::admin::RegisterError::PasswordDiffer);
}
let hash = match input.password.encrypt(&config.lock().web().pass_salt()) {
Ok(s) => s,
Err(e) => {
log::error!("{e:?}");
return Err(routes::Error::Admin(Error::HashPass));
}
};
query_db!(
db,
database_manager::CreateAccount {
email: input.email,
login: input.login,
pass_hash: PassHash::from(hash),
role: input.role,
},
super::Error::Admin(Error::Register),
super::Error::Admin(Error::Register)
);
response.success = response.errors.is_empty();
Ok(if response.success {
HttpResponse::Ok().json(response)
} else {
HttpResponse::BadRequest().json(response)
})
}
#[get("")]
async fn landing() -> Result<HttpResponse> {
Ok(HttpResponse::NotImplemented()
@ -132,9 +46,6 @@ async fn landing() -> Result<HttpResponse> {
pub fn configure(config: &mut ServiceConfig) {
config.service(
scope("/admin")
.service(sign_in)
.service(logout)
.service(register)
.service(landing)
.configure(api_v1::configure),
);

View File

@ -4,7 +4,64 @@ mod products;
mod stocks;
mod uploads;
use actix_web::web::{scope, ServiceConfig};
use actix::Addr;
use actix_web::web::{scope, Data, Json, ServiceConfig};
use actix_web::{delete, post};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use database_manager::{query_db, Database};
use model::Encrypt;
use token_manager::TokenManager;
use crate::routes;
use crate::routes::{create_auth_pair, AdminError, AuthPair, RequireUser};
#[delete("/logout")]
async fn logout(
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
) -> routes::Result<Json<model::api::admin::LogoutResponse>> {
credentials.require_admin(tm.into_inner()).await?;
todo!("Destroy token")
// Ok(Json(model::api::admin::LogoutResponse {}))
}
#[post("/sign-in")]
async fn sign_in(
tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>,
Json(payload): Json<model::api::admin::SignInInput>,
) -> routes::Result<Json<model::api::SessionOutput>> {
let db = db.into_inner();
let account: model::FullAccount = query_db!(
db,
database_manager::AccountByIdentity {
login: payload.login,
email: payload.email,
},
routes::Error::Admin(AdminError::DatabaseConnection)
);
if payload.password.validate(&account.pass_hash).is_err() {
return Err(routes::Error::Unauthorized);
}
let role = account.role;
let AuthPair {
access_token,
access_token_string,
_refresh_token: _,
refresh_token_string,
} = create_auth_pair(tm, account).await?;
Ok(Json(model::api::SessionOutput {
access_token: access_token_string,
refresh_token: refresh_token_string,
exp: access_token.expiration_time,
role,
}))
}
pub fn configure(config: &mut ServiceConfig) {
config.service(
@ -13,6 +70,8 @@ pub fn configure(config: &mut ServiceConfig) {
.configure(stocks::configure)
.configure(accounts::configure)
.configure(orders::configure)
.configure(uploads::configure),
.configure(uploads::configure)
.service(sign_in)
.service(logout),
);
}

View File

@ -1,18 +1,24 @@
use actix::Addr;
use actix_session::Session;
use actix_web::web::{Data, Json, ServiceConfig};
use actix_web::{get, patch, post, HttpResponse};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use config::SharedAppConfig;
use database_manager::Database;
use model::{AccountId, AccountState, Encrypt, PasswordConfirmation};
use token_manager::TokenManager;
use crate::routes::admin::Error;
use crate::routes::RequireLogin;
use crate::routes::RequireUser;
use crate::{admin_send_db, routes, Email, Login, PassHash, Password, Role};
#[get("/accounts")]
pub async fn accounts(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> {
session.require_admin()?;
pub async fn accounts(
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>,
) -> routes::Result<HttpResponse> {
credentials.require_admin(tm.into_inner()).await?;
let accounts = admin_send_db!(db, database_manager::AllAccounts);
Ok(HttpResponse::Ok().json(accounts))
}
@ -30,12 +36,13 @@ pub struct UpdateAccountInput {
#[patch("/account")]
pub async fn update_account(
session: Session,
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>,
Json(payload): Json<UpdateAccountInput>,
config: Data<SharedAppConfig>,
) -> routes::Result<HttpResponse> {
session.require_admin()?;
credentials.require_admin(tm.into_inner()).await?;
let hash = match (payload.password, payload.password_confirmation) {
(None, None) => None,
@ -52,7 +59,7 @@ pub async fn update_account(
return Err(routes::Error::Admin(routes::admin::Error::HashPass));
}
};
Some(PassHash::from(hash))
Some(PassHash::new(hash))
}
_ => {
return Err(routes::Error::Admin(
@ -75,37 +82,28 @@ pub async fn update_account(
Ok(HttpResponse::Ok().json(account))
}
#[derive(serde::Deserialize)]
pub struct CreateAccountInput {
pub email: Email,
pub login: Login,
pub password: Password,
pub password_confirmation: PasswordConfirmation,
pub role: Role,
}
#[post("/account")]
pub async fn create_account(
session: Session,
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>,
Json(payload): Json<CreateAccountInput>,
Json(payload): Json<model::api::admin::RegisterInput>,
config: Data<SharedAppConfig>,
) -> routes::Result<HttpResponse> {
session.require_admin()?;
) -> routes::Result<Json<model::api::admin::RegisterResponse>> {
credentials.require_admin(tm.into_inner()).await?;
if payload.password != payload.password_confirmation {
return Err(routes::Error::Admin(
routes::admin::Error::DifferentPasswords,
));
return Err(routes::Error::Admin(Error::DifferentPasswords));
}
let hash = match payload.password.encrypt(&config.lock().web().pass_salt()) {
Ok(hash) => hash,
Err(e) => {
log::error!("{e:?}");
return Err(routes::Error::Admin(routes::admin::Error::HashPass));
return Err(routes::Error::Admin(Error::HashPass));
}
};
let account = admin_send_db!(
let account: model::FullAccount = admin_send_db!(
db,
database_manager::CreateAccount {
email: payload.email,
@ -114,7 +112,10 @@ pub async fn create_account(
role: payload.role,
}
);
Ok(HttpResponse::Ok().json(account))
Ok(Json(model::api::admin::RegisterResponse {
errors: vec![],
account: Some(account.into()),
}))
}
pub fn configure(config: &mut ServiceConfig) {

View File

@ -1,17 +1,22 @@
use actix::Addr;
use actix_session::Session;
use actix_web::get;
use actix_web::web::{Data, Json, ServiceConfig};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use database_manager::Database;
use model::api::AccountOrders;
use token_manager::TokenManager;
use crate::routes::admin::Error;
use crate::routes::RequireLogin;
use crate::routes::RequireUser;
use crate::{admin_send_db, routes};
#[get("/orders")]
async fn orders(session: Session, db: Data<Addr<Database>>) -> routes::Result<Json<AccountOrders>> {
session.require_admin()?;
async fn orders(
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>,
) -> routes::Result<Json<AccountOrders>> {
credentials.require_admin(tm.into_inner()).await?;
let orders: Vec<model::AccountOrder> = admin_send_db!(&db, database_manager::AllAccountOrders);
let items: Vec<model::OrderItem> = admin_send_db!(db, database_manager::AllOrderItems);

View File

@ -1,7 +1,7 @@
use actix::Addr;
use actix_session::Session;
use actix_web::web::{Data, Json, ServiceConfig};
use actix_web::{delete, get, patch, post, HttpResponse};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use config::SharedAppConfig;
use database_manager::Database;
use model::{
@ -10,18 +10,20 @@ use model::{
};
use search_manager::SearchManager;
use serde::Deserialize;
use token_manager::TokenManager;
use crate::routes::admin::Error;
use crate::routes::RequireLogin;
use crate::routes::RequireUser;
use crate::{admin_send_db, routes};
#[get("/products")]
async fn products(
session: Session,
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>,
config: Data<SharedAppConfig>,
) -> routes::Result<Json<api::Products>> {
session.require_admin()?;
credentials.require_admin(tm.into_inner()).await?;
let public_path = {
let l = config.lock();
@ -55,11 +57,12 @@ pub struct UpdateProduct {
#[patch("/product")]
async fn update_product(
session: Session,
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>,
Json(payload): Json<UpdateProduct>,
) -> routes::Result<HttpResponse> {
session.require_admin()?;
credentials.require_admin(tm.into_inner()).await?;
let product = admin_send_db!(
db,
@ -89,12 +92,13 @@ pub struct CreateProduct {
#[post("/product")]
async fn create_product(
session: Session,
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>,
search: Data<Addr<SearchManager>>,
Json(payload): Json<CreateProduct>,
) -> routes::Result<HttpResponse> {
session.require_admin()?;
credentials.require_admin(tm.into_inner()).await?;
let product: model::Product = admin_send_db!(
db.clone(),
@ -138,11 +142,12 @@ pub struct DeleteProduct {
#[delete("/product")]
async fn delete_product(
session: Session,
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>,
Json(payload): Json<DeleteProduct>,
) -> routes::Result<HttpResponse> {
let _ = session.require_admin()?;
credentials.require_admin(tm.into_inner()).await?;
let product = admin_send_db!(
db,

View File

@ -1,21 +1,26 @@
use actix::Addr;
use actix_session::Session;
use actix_web::web::{Data, Json, ServiceConfig};
use actix_web::{delete, get, patch, post, HttpResponse};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use database_manager::Database;
use model::{ProductId, Quantity, QuantityUnit, StockId};
use serde::Deserialize;
use token_manager::TokenManager;
use crate::routes::admin::Error;
use crate::routes::RequireLogin;
use crate::routes::RequireUser;
use crate::{admin_send_db, routes};
#[get("/stocks")]
async fn stocks(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> {
session.require_admin()?;
async fn stocks(
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>,
) -> routes::Result<Json<Vec<model::Stock>>> {
credentials.require_admin(tm.into_inner()).await?;
let stocks = admin_send_db!(db, database_manager::AllStocks);
Ok(HttpResponse::Created().json(stocks))
Ok(Json(stocks))
}
#[derive(Deserialize)]
@ -28,11 +33,12 @@ pub struct UpdateStock {
#[patch("/stock")]
async fn update_stock(
session: Session,
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>,
Json(payload): Json<UpdateStock>,
) -> routes::Result<HttpResponse> {
session.require_admin()?;
) -> routes::Result<Json<model::Stock>> {
credentials.require_admin(tm.into_inner()).await?;
let stock = admin_send_db!(
db,
@ -43,7 +49,7 @@ async fn update_stock(
quantity_unit: payload.quantity_unit
}
);
Ok(HttpResponse::Created().json(stock))
Ok(Json(stock))
}
#[derive(Deserialize)]
@ -55,11 +61,12 @@ pub struct CreateStock {
#[post("/stock")]
async fn create_stock(
session: Session,
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>,
Json(payload): Json<CreateStock>,
) -> routes::Result<HttpResponse> {
session.require_admin()?;
credentials.require_admin(tm.into_inner()).await?;
let stock = admin_send_db!(
db,
@ -79,11 +86,12 @@ pub struct DeleteStock {
#[delete("/stock")]
async fn delete_stock(
session: Session,
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>,
Json(payload): Json<DeleteStock>,
) -> routes::Result<HttpResponse> {
session.require_admin()?;
) -> routes::Result<Json<Option<model::Stock>>> {
credentials.require_admin(tm.into_inner()).await?;
let stock = admin_send_db!(
db,
@ -91,7 +99,7 @@ async fn delete_stock(
stock_id: payload.id
}
);
Ok(HttpResponse::Created().json(stock))
Ok(Json(stock))
}
pub fn configure(config: &mut ServiceConfig) {

View File

@ -2,9 +2,13 @@ use actix::Addr;
use actix_multipart::Multipart;
use actix_web::web::{Data, ServiceConfig};
use actix_web::{post, HttpResponse};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use database_manager::{query_db, Database};
use fs_manager::FsManager;
use futures_util::StreamExt;
use token_manager::TokenManager;
use crate::routes::RequireUser;
#[derive(Debug, thiserror::Error)]
pub enum UploadError {
@ -21,7 +25,13 @@ async fn upload_product_image(
mut payload: Multipart,
fs: Data<Addr<FsManager>>,
db: Data<Addr<Database>>,
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
) -> HttpResponse {
if credentials.require_admin(tm.into_inner()).await.is_err() {
return HttpResponse::Unauthorized().finish();
}
let mut name = None;
while let Some(Ok(mut field)) = payload.next().await {
let field_name = field.name();
@ -106,7 +116,7 @@ async fn upload_product_image(
_ => {}
}
}
HttpResponse::NotImplemented().finish()
HttpResponse::Ok().finish()
}
pub fn configure(config: &mut ServiceConfig) {

View File

@ -8,26 +8,24 @@ use actix::Addr;
use actix_session::Session;
use actix_web::body::BoxBody;
use actix_web::http::StatusCode;
use actix_web::web::ServiceConfig;
use actix_web::web::{Data, ServiceConfig};
use actix_web::{HttpRequest, HttpResponse, Responder, ResponseError};
use model::api::Failure;
use model::{AccessTokenString, RecordId, Token};
use token_manager::{query_tm, TokenManager};
pub use self::admin::Error as AdminError;
pub use self::public::{Error as PublicError, V1Error, V1ShoppingCartError};
use crate::routes;
pub trait RequireLogin {
fn require_admin(&self) -> Result<RecordId>;
fn require_admin(&self) -> Result<model::RecordId>;
}
impl RequireLogin for Session {
fn require_admin(&self) -> Result<RecordId> {
fn require_admin(&self) -> Result<model::RecordId> {
match self.get("admin_id") {
Ok(Some(id)) => Ok(id),
_ => {
log::debug!("User is not logged as admin");
log::debug!("User is not logged as an admin");
Err(Error::Unauthorized)
}
}
@ -39,8 +37,8 @@ pub enum Error {
#[from(ignore)]
Unauthorized,
CriticalFailure,
Admin(routes::admin::Error),
Public(routes::public::Error),
Admin(admin::Error),
Public(public::Error),
}
impl From<V1Error> for Error {
@ -132,18 +130,75 @@ pub fn configure(config: &mut ServiceConfig) {
#[async_trait::async_trait]
pub trait RequireUser {
async fn require_user(&self, tm: Arc<Addr<TokenManager>>) -> Result<(Token, bool)>;
async fn require_user(&self, tm: Arc<Addr<TokenManager>>) -> Result<model::Token>;
async fn require_admin(&self, tm: Arc<Addr<TokenManager>>) -> Result<model::Token>;
}
#[async_trait::async_trait]
impl RequireUser for actix_web_httpauth::extractors::bearer::BearerAuth {
async fn require_user(&self, tm: Arc<Addr<TokenManager>>) -> Result<(Token, bool)> {
async fn require_user(&self, tm: Arc<Addr<TokenManager>>) -> Result<model::Token> {
Ok(query_tm!(
tm,
token_manager::Validate {
token: AccessTokenString::new(self.token()),
token: model::AccessTokenString::new(self.token()),
},
Error::Unauthorized
))
}
async fn require_admin(&self, tm: Arc<Addr<TokenManager>>) -> Result<model::Token> {
let token: model::Token = query_tm!(
tm,
token_manager::Validate {
token: model::AccessTokenString::new(self.token()),
},
Error::Unauthorized
);
if token.role == model::Role::Admin {
Err(Error::Unauthorized)
} else {
Ok(token)
}
}
}
pub struct AuthPair {
pub access_token: model::Token,
pub access_token_string: model::AccessTokenString,
pub _refresh_token: model::Token,
pub refresh_token_string: model::RefreshTokenString,
}
pub async fn create_auth_pair(
tm: Data<Addr<TokenManager>>,
account: model::FullAccount,
) -> Result<AuthPair> {
let (access_token, refresh_token) = query_tm!(
multi,
tm,
Error::Public(PublicError::DatabaseConnection),
token_manager::CreateToken {
customer_id: account.customer_id,
role: account.role,
subject: account.id,
audience: Some(model::Audience::Web),
exp: None
},
token_manager::CreateToken {
customer_id: account.customer_id,
role: account.role,
subject: account.id,
audience: Some(model::Audience::Web),
exp: Some((chrono::Utc::now() + chrono::Duration::days(31)).naive_utc())
}
);
let (access_token, access_token_string): (model::Token, model::AccessTokenString) =
access_token?;
let (refresh_token, refresh_token_string): (model::Token, model::AccessTokenString) =
refresh_token?;
Ok(AuthPair {
access_token,
access_token_string,
_refresh_token: refresh_token,
refresh_token_string: refresh_token_string.into(),
})
}

View File

@ -8,10 +8,9 @@ use model::api;
use payment_manager::{query_pay, PaymentManager};
use token_manager::TokenManager;
use crate::routes::public::api_v1::unrestricted::{create_auth_pair, AuthPair};
use crate::routes::public::api_v1::{Error as ApiV1Error, ShoppingCartError};
use crate::routes::public::Error as PublicError;
use crate::routes::{RequireUser, Result};
use crate::routes::{create_auth_pair, AuthPair, RequireUser, Result};
use crate::{public_send_db, routes};
/// This requires [model::AccessTokenString] to be set as bearer
@ -20,7 +19,7 @@ async fn verify_token(
tm: Data<Addr<TokenManager>>,
credentials: BearerAuth,
) -> routes::Result<String> {
let _token = credentials.require_user(tm.into_inner()).await?.0;
let _token = credentials.require_user(tm.into_inner()).await?;
Ok("".into())
}
@ -34,7 +33,6 @@ async fn refresh_token(
let account_id: model::AccountId = credentials
.require_user(tm.clone().into_inner())
.await?
.0
.subject
.into();
let account: model::FullAccount = query_db!(
@ -43,6 +41,8 @@ async fn refresh_token(
routes::Error::Unauthorized
);
let role = account.role;
let AuthPair {
access_token,
access_token_string,
@ -50,10 +50,11 @@ async fn refresh_token(
refresh_token_string,
} = create_auth_pair(tm, account).await?;
Ok(Json(api::SessionOutput {
Ok(Json(model::api::SessionOutput {
access_token: access_token_string,
refresh_token: refresh_token_string,
exp: access_token.expiration_time,
role,
}))
}
@ -63,11 +64,11 @@ async fn shopping_cart(
tm: Data<Addr<TokenManager>>,
credentials: BearerAuth,
) -> Result<Json<api::ShoppingCart>> {
let (token, _) = credentials.require_user(tm.into_inner()).await?;
let token = credentials.require_user(tm.into_inner()).await?;
let cart: model::ShoppingCart = query_db!(
db,
database_manager::EnsureActiveShoppingCart {
buyer_id: token.subject.into(),
buyer_id: token.account_id(),
},
routes::Error::Public(PublicError::ApiV1(ApiV1Error::ShoppingCart(
ShoppingCartError::Ensure
@ -94,12 +95,12 @@ async fn create_cart_item(
credentials: BearerAuth,
Json(payload): Json<api::CreateItemInput>,
) -> Result<Json<api::CreateItemOutput>> {
let (token, _) = credentials.require_user(tm.into_inner()).await?;
let token = credentials.require_user(tm.into_inner()).await?;
let item: model::ShoppingCartItem = query_cart!(
cart,
cart_manager::AddItem {
buyer_id: token.subject.into(),
buyer_id: token.account_id(),
product_id: payload.product_id,
quantity: payload.quantity,
quantity_unit: payload.quantity_unit,
@ -121,12 +122,12 @@ async fn delete_cart_item(
credentials: BearerAuth,
Json(payload): Json<api::DeleteItemInput>,
) -> Result<HttpResponse> {
let (token, _) = credentials.require_user(tm.into_inner()).await?;
let token = credentials.require_user(tm.into_inner()).await?;
let sc: model::ShoppingCart = query_db!(
db,
database_manager::EnsureActiveShoppingCart {
buyer_id: token.subject.into(),
buyer_id: token.account_id(),
},
routes::Error::Public(super::Error::RemoveItem.into()),
routes::Error::Public(PublicError::DatabaseConnection)
@ -161,9 +162,7 @@ pub(crate) async fn me(
let account_id: model::AccountId = credentials
.require_user(tm.into_inner())
.await?
.0
.subject
.into();
.account_id();
let account: model::FullAccount =
public_send_db!(owned, db, database_manager::FindAccount { account_id });
Ok(Json(account.into()))
@ -177,7 +176,10 @@ pub(crate) async fn create_order(
credentials: BearerAuth,
payment: Data<Addr<PaymentManager>>,
) -> routes::Result<HttpResponse> {
let subject = credentials.require_user(tm.into_inner()).await?.0.subject;
let account_id = credentials
.require_user(tm.into_inner())
.await?
.account_id();
let api::CreateOrderInput {
email,
@ -205,7 +207,7 @@ pub(crate) async fn create_order(
language,
},
customer_ip: ip.to_string(),
buyer_id: subject.into(),
buyer_id: account_id,
charge_client
},
routes::Error::Public(PublicError::DatabaseConnection)

View File

@ -3,14 +3,14 @@ use actix_web::web::{Data, Json, Path, Query, ServiceConfig};
use actix_web::{get, post, HttpResponse};
use config::SharedAppConfig;
use database_manager::{query_db, Database};
use model::{api, Encrypt};
use model::Encrypt;
use payment_manager::{PaymentManager, PaymentNotification};
use search_manager::SearchManager;
use token_manager::{query_tm, TokenManager};
use token_manager::TokenManager;
use crate::public_send_db;
use crate::routes::public::Error as PublicError;
use crate::routes::{self, Result};
use crate::routes::{self, create_auth_pair, AuthPair};
#[get("/search")]
async fn search(
@ -57,7 +57,7 @@ async fn search(
async fn products(
db: Data<Addr<Database>>,
config: Data<SharedAppConfig>,
) -> Result<Json<api::Products>> {
) -> routes::Result<Json<model::api::Products>> {
let db = db.into_inner();
let public_path = {
let l = config.lock();
@ -86,7 +86,7 @@ async fn product(
path: Path<model::RecordId>,
db: Data<Addr<Database>>,
config: Data<SharedAppConfig>,
) -> Result<Json<api::Product>> {
) -> routes::Result<Json<model::api::Product>> {
let product_id: model::ProductId = path.into_inner().into();
let db = db.into_inner();
let public_path = {
@ -122,14 +122,15 @@ async fn product(
}
#[get("/stocks")]
async fn stocks(db: Data<Addr<Database>>) -> Result<HttpResponse> {
public_send_db!(db.into_inner(), database_manager::AllStocks)
async fn stocks(db: Data<Addr<Database>>) -> routes::Result<Json<Vec<model::Stock>>> {
let stocks = public_send_db!(owned, db.into_inner(), database_manager::AllStocks);
Ok(Json(stocks))
}
#[post("/register")]
pub async fn create_account(
db: Data<Addr<Database>>,
Json(payload): Json<api::CreateAccountInput>,
Json(payload): Json<model::api::CreateAccountInput>,
config: Data<SharedAppConfig>,
tm: Data<Addr<TokenManager>>,
) -> routes::Result<Json<model::api::SessionOutput>> {
@ -160,6 +161,8 @@ pub async fn create_account(
routes::Error::CriticalFailure
);
let role = account.role;
let AuthPair {
access_token,
access_token_string,
@ -167,61 +170,20 @@ pub async fn create_account(
refresh_token_string,
} = create_auth_pair(tm, account).await?;
Ok(Json(api::SessionOutput {
Ok(Json(model::api::SessionOutput {
access_token: access_token_string,
refresh_token: refresh_token_string,
exp: access_token.expiration_time,
role,
}))
}
pub(crate) struct AuthPair {
pub access_token: model::Token,
pub access_token_string: model::AccessTokenString,
pub _refresh_token: model::Token,
pub refresh_token_string: model::RefreshTokenString,
}
pub(crate) async fn create_auth_pair(
tm: Data<Addr<TokenManager>>,
account: model::FullAccount,
) -> routes::Result<AuthPair> {
let (access_token, refresh_token) = query_tm!(
multi,
tm,
routes::Error::Public(PublicError::DatabaseConnection),
token_manager::CreateToken {
customer_id: account.customer_id,
role: account.role,
subject: account.id,
audience: Some(model::Audience::Web),
exp: None
},
token_manager::CreateToken {
customer_id: account.customer_id,
role: account.role,
subject: account.id,
audience: Some(model::Audience::Web),
exp: Some((chrono::Utc::now() + chrono::Duration::days(31)).naive_utc())
}
);
let (access_token, access_token_string): (model::Token, model::AccessTokenString) =
access_token?;
let (refresh_token, refresh_token_string): (model::Token, model::AccessTokenString) =
refresh_token?;
Ok(AuthPair {
access_token,
access_token_string,
_refresh_token: refresh_token,
refresh_token_string: refresh_token_string.into(),
})
}
#[post("/sign-in")]
async fn sign_in(
Json(payload): Json<api::SignInInput>,
Json(payload): Json<model::api::SignInInput>,
db: Data<Addr<Database>>,
tm: Data<Addr<TokenManager>>,
) -> Result<Json<api::SessionOutput>> {
) -> routes::Result<Json<model::api::SessionOutput>> {
let db = db.into_inner();
let account: model::FullAccount = query_db!(
@ -236,6 +198,8 @@ async fn sign_in(
return Err(routes::Error::Unauthorized);
}
let role = account.role;
let AuthPair {
access_token,
access_token_string,
@ -243,10 +207,11 @@ async fn sign_in(
refresh_token_string,
} = create_auth_pair(tm, account).await?;
Ok(Json(api::SessionOutput {
Ok(Json(model::api::SessionOutput {
access_token: access_token_string,
refresh_token: refresh_token_string,
exp: access_token.expiration_time,
role,
}))
}

View File

@ -316,6 +316,7 @@ pub struct SessionOutput {
pub access_token: AccessTokenString,
pub refresh_token: RefreshTokenString,
pub exp: NaiveDateTime,
pub role: Role,
}
#[derive(Serialize, Deserialize, Debug)]
@ -391,7 +392,6 @@ pub mod admin {
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct RegisterResponse {
pub success: bool,
pub errors: Vec<RegisterError>,
pub account: Option<Account>,
}

View File

@ -664,6 +664,12 @@ impl PasswordConfirmation {
#[serde(transparent)]
pub struct PassHash(String);
impl PassHash {
pub fn new<S: Into<String>>(s: S) -> Self {
Self(s.into())
}
}
impl PartialEq<PasswordConfirmation> for Password {
fn eq(&self, other: &PasswordConfirmation) -> bool {
self.0 == other.0
@ -977,6 +983,12 @@ pub struct Token {
pub jwt_id: uuid::Uuid,
}
impl Token {
pub fn account_id(&self) -> AccountId {
AccountId(self.subject)
}
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]

View File

@ -31,6 +31,3 @@ rusty-money = { version = "0.4.1", features = ["iso"] }
thiserror = { version = "1.0.31" }
[profile.release]
lto = true
opt-level = 's'

View File

@ -3,6 +3,7 @@ use std::ops::FromResidual;
use seed::fetch::{FetchError, Request};
pub mod admin;
pub mod public;
#[derive(Debug)]
@ -12,6 +13,12 @@ pub enum NetRes<S> {
Http(FetchError),
}
impl<S> From<FetchError> for NetRes<S> {
fn from(e: FetchError) -> Self {
Self::Http(e)
}
}
impl<S> FromResidual<Result<Infallible, NetRes<S>>> for NetRes<S> {
fn from_residual(residual: Result<Infallible, NetRes<S>>) -> Self {
match residual {

35
web/src/api/admin.rs Normal file
View File

@ -0,0 +1,35 @@
use seed::fetch::{Header, Method, Request};
use crate::api::{perform, NetRes};
pub async fn sign_in(identity: String, password: model::Password) -> NetRes<model::Account> {
use model::api::admin::SignInInput;
let input = if identity.contains('@') {
SignInInput {
login: None,
email: Some(model::Email::new(identity)),
password,
}
} else {
SignInInput {
login: Some(model::Login::new(identity)),
email: None,
password,
}
};
perform(
Request::new("/api/v1/sign-in")
.method(Method::Post)
.json(&input)
.map_err(NetRes::Http)?,
)
.await
}
/// This request validates if session is still active
/// It should be run from time to time just to check if user should be
/// redirected to sign-in page
pub async fn check_session() -> NetRes<String> {
perform(Request::new("/api/v1/check").method(Method::Get)).await
}