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", "actors/fs_manager",
"db-seed" "db-seed"
] ]
[profile.release]
lto = true
opt-level = 's'

View File

@ -116,6 +116,8 @@ pub enum Error {
Validate, Validate,
#[error("Unable to validate token. Can't connect to database")] #[error("Unable to validate token. Can't connect to database")]
ValidateInternal, ValidateInternal,
#[error("Token does not exists or some fields are incorrect")]
Invalid,
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
@ -247,18 +249,18 @@ pub(crate) async fn create_token(
} }
#[derive(Message)] #[derive(Message)]
#[rtype(result = "Result<(Token, bool)>")] #[rtype(result = "Result<Token>")]
pub struct Validate { pub struct Validate {
pub token: AccessTokenString, pub token: AccessTokenString,
} }
token_async_handler!(Validate, validate, (Token, bool)); token_async_handler!(Validate, validate, Token);
pub(crate) async fn validate( pub(crate) async fn validate(
msg: Validate, msg: Validate,
db: Addr<Database>, db: Addr<Database>,
config: SharedAppConfig, config: SharedAppConfig,
) -> Result<(Token, bool)> { ) -> Result<Token> {
use jwt::VerifyWithKey; use jwt::VerifyWithKey;
log::info!("Validating token {:?}", msg.token); 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { if !validate_pair(&claims, "iat", &token.issued_at_time, validate_time) {
return Ok((token, false)); return Err(Error::Invalid);
} }
log::info!("JWT token valid"); log::info!("JWT token valid");
Ok((token, true)) Ok(token)
} }
fn build_key(secret: String) -> Result<Hmac<Sha256>> { fn build_key(secret: String) -> Result<Hmac<Sha256>> {

View File

@ -1,15 +1,9 @@
mod api_v1; mod api_v1;
use actix::Addr; use actix_web::web::{scope, ServiceConfig};
use actix_session::Session; use actix_web::{get, HttpResponse};
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 crate::routes; use crate::routes::Result;
use crate::routes::{RequireLogin, Result};
#[macro_export] #[macro_export]
macro_rules! admin_send_db { macro_rules! admin_send_db {
@ -42,86 +36,6 @@ pub enum Error {
Database(#[from] database_manager::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("")] #[get("")]
async fn landing() -> Result<HttpResponse> { async fn landing() -> Result<HttpResponse> {
Ok(HttpResponse::NotImplemented() Ok(HttpResponse::NotImplemented()
@ -132,9 +46,6 @@ async fn landing() -> Result<HttpResponse> {
pub fn configure(config: &mut ServiceConfig) { pub fn configure(config: &mut ServiceConfig) {
config.service( config.service(
scope("/admin") scope("/admin")
.service(sign_in)
.service(logout)
.service(register)
.service(landing) .service(landing)
.configure(api_v1::configure), .configure(api_v1::configure),
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,26 +8,24 @@ use actix::Addr;
use actix_session::Session; use actix_session::Session;
use actix_web::body::BoxBody; use actix_web::body::BoxBody;
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::web::ServiceConfig; use actix_web::web::{Data, ServiceConfig};
use actix_web::{HttpRequest, HttpResponse, Responder, ResponseError}; use actix_web::{HttpRequest, HttpResponse, Responder, ResponseError};
use model::api::Failure; use model::api::Failure;
use model::{AccessTokenString, RecordId, Token};
use token_manager::{query_tm, TokenManager}; use token_manager::{query_tm, TokenManager};
pub use self::admin::Error as AdminError; pub use self::admin::Error as AdminError;
pub use self::public::{Error as PublicError, V1Error, V1ShoppingCartError}; pub use self::public::{Error as PublicError, V1Error, V1ShoppingCartError};
use crate::routes;
pub trait RequireLogin { pub trait RequireLogin {
fn require_admin(&self) -> Result<RecordId>; fn require_admin(&self) -> Result<model::RecordId>;
} }
impl RequireLogin for Session { impl RequireLogin for Session {
fn require_admin(&self) -> Result<RecordId> { fn require_admin(&self) -> Result<model::RecordId> {
match self.get("admin_id") { match self.get("admin_id") {
Ok(Some(id)) => Ok(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) Err(Error::Unauthorized)
} }
} }
@ -39,8 +37,8 @@ pub enum Error {
#[from(ignore)] #[from(ignore)]
Unauthorized, Unauthorized,
CriticalFailure, CriticalFailure,
Admin(routes::admin::Error), Admin(admin::Error),
Public(routes::public::Error), Public(public::Error),
} }
impl From<V1Error> for Error { impl From<V1Error> for Error {
@ -132,18 +130,75 @@ pub fn configure(config: &mut ServiceConfig) {
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait RequireUser { 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] #[async_trait::async_trait]
impl RequireUser for actix_web_httpauth::extractors::bearer::BearerAuth { 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!( Ok(query_tm!(
tm, tm,
token_manager::Validate { token_manager::Validate {
token: AccessTokenString::new(self.token()), token: model::AccessTokenString::new(self.token()),
}, },
Error::Unauthorized 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 payment_manager::{query_pay, PaymentManager};
use token_manager::TokenManager; 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::api_v1::{Error as ApiV1Error, ShoppingCartError};
use crate::routes::public::Error as PublicError; 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}; use crate::{public_send_db, routes};
/// This requires [model::AccessTokenString] to be set as bearer /// This requires [model::AccessTokenString] to be set as bearer
@ -20,7 +19,7 @@ async fn verify_token(
tm: Data<Addr<TokenManager>>, tm: Data<Addr<TokenManager>>,
credentials: BearerAuth, credentials: BearerAuth,
) -> routes::Result<String> { ) -> routes::Result<String> {
let _token = credentials.require_user(tm.into_inner()).await?.0; let _token = credentials.require_user(tm.into_inner()).await?;
Ok("".into()) Ok("".into())
} }
@ -34,7 +33,6 @@ async fn refresh_token(
let account_id: model::AccountId = credentials let account_id: model::AccountId = credentials
.require_user(tm.clone().into_inner()) .require_user(tm.clone().into_inner())
.await? .await?
.0
.subject .subject
.into(); .into();
let account: model::FullAccount = query_db!( let account: model::FullAccount = query_db!(
@ -43,6 +41,8 @@ async fn refresh_token(
routes::Error::Unauthorized routes::Error::Unauthorized
); );
let role = account.role;
let AuthPair { let AuthPair {
access_token, access_token,
access_token_string, access_token_string,
@ -50,10 +50,11 @@ async fn refresh_token(
refresh_token_string, refresh_token_string,
} = create_auth_pair(tm, account).await?; } = create_auth_pair(tm, account).await?;
Ok(Json(api::SessionOutput { Ok(Json(model::api::SessionOutput {
access_token: access_token_string, access_token: access_token_string,
refresh_token: refresh_token_string, refresh_token: refresh_token_string,
exp: access_token.expiration_time, exp: access_token.expiration_time,
role,
})) }))
} }
@ -63,11 +64,11 @@ async fn shopping_cart(
tm: Data<Addr<TokenManager>>, tm: Data<Addr<TokenManager>>,
credentials: BearerAuth, credentials: BearerAuth,
) -> Result<Json<api::ShoppingCart>> { ) -> 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!( let cart: model::ShoppingCart = query_db!(
db, db,
database_manager::EnsureActiveShoppingCart { database_manager::EnsureActiveShoppingCart {
buyer_id: token.subject.into(), buyer_id: token.account_id(),
}, },
routes::Error::Public(PublicError::ApiV1(ApiV1Error::ShoppingCart( routes::Error::Public(PublicError::ApiV1(ApiV1Error::ShoppingCart(
ShoppingCartError::Ensure ShoppingCartError::Ensure
@ -94,12 +95,12 @@ async fn create_cart_item(
credentials: BearerAuth, credentials: BearerAuth,
Json(payload): Json<api::CreateItemInput>, Json(payload): Json<api::CreateItemInput>,
) -> Result<Json<api::CreateItemOutput>> { ) -> 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!( let item: model::ShoppingCartItem = query_cart!(
cart, cart,
cart_manager::AddItem { cart_manager::AddItem {
buyer_id: token.subject.into(), buyer_id: token.account_id(),
product_id: payload.product_id, product_id: payload.product_id,
quantity: payload.quantity, quantity: payload.quantity,
quantity_unit: payload.quantity_unit, quantity_unit: payload.quantity_unit,
@ -121,12 +122,12 @@ async fn delete_cart_item(
credentials: BearerAuth, credentials: BearerAuth,
Json(payload): Json<api::DeleteItemInput>, Json(payload): Json<api::DeleteItemInput>,
) -> Result<HttpResponse> { ) -> 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!( let sc: model::ShoppingCart = query_db!(
db, db,
database_manager::EnsureActiveShoppingCart { database_manager::EnsureActiveShoppingCart {
buyer_id: token.subject.into(), buyer_id: token.account_id(),
}, },
routes::Error::Public(super::Error::RemoveItem.into()), routes::Error::Public(super::Error::RemoveItem.into()),
routes::Error::Public(PublicError::DatabaseConnection) routes::Error::Public(PublicError::DatabaseConnection)
@ -161,9 +162,7 @@ pub(crate) async fn me(
let account_id: model::AccountId = credentials let account_id: model::AccountId = credentials
.require_user(tm.into_inner()) .require_user(tm.into_inner())
.await? .await?
.0 .account_id();
.subject
.into();
let account: model::FullAccount = let account: model::FullAccount =
public_send_db!(owned, db, database_manager::FindAccount { account_id }); public_send_db!(owned, db, database_manager::FindAccount { account_id });
Ok(Json(account.into())) Ok(Json(account.into()))
@ -177,7 +176,10 @@ pub(crate) async fn create_order(
credentials: BearerAuth, credentials: BearerAuth,
payment: Data<Addr<PaymentManager>>, payment: Data<Addr<PaymentManager>>,
) -> routes::Result<HttpResponse> { ) -> 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 { let api::CreateOrderInput {
email, email,
@ -205,7 +207,7 @@ pub(crate) async fn create_order(
language, language,
}, },
customer_ip: ip.to_string(), customer_ip: ip.to_string(),
buyer_id: subject.into(), buyer_id: account_id,
charge_client charge_client
}, },
routes::Error::Public(PublicError::DatabaseConnection) 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 actix_web::{get, post, HttpResponse};
use config::SharedAppConfig; use config::SharedAppConfig;
use database_manager::{query_db, Database}; use database_manager::{query_db, Database};
use model::{api, Encrypt}; use model::Encrypt;
use payment_manager::{PaymentManager, PaymentNotification}; use payment_manager::{PaymentManager, PaymentNotification};
use search_manager::SearchManager; use search_manager::SearchManager;
use token_manager::{query_tm, TokenManager}; use token_manager::TokenManager;
use crate::public_send_db; use crate::public_send_db;
use crate::routes::public::Error as PublicError; use crate::routes::public::Error as PublicError;
use crate::routes::{self, Result}; use crate::routes::{self, create_auth_pair, AuthPair};
#[get("/search")] #[get("/search")]
async fn search( async fn search(
@ -57,7 +57,7 @@ async fn search(
async fn products( async fn products(
db: Data<Addr<Database>>, db: Data<Addr<Database>>,
config: Data<SharedAppConfig>, config: Data<SharedAppConfig>,
) -> Result<Json<api::Products>> { ) -> routes::Result<Json<model::api::Products>> {
let db = db.into_inner(); let db = db.into_inner();
let public_path = { let public_path = {
let l = config.lock(); let l = config.lock();
@ -86,7 +86,7 @@ async fn product(
path: Path<model::RecordId>, path: Path<model::RecordId>,
db: Data<Addr<Database>>, db: Data<Addr<Database>>,
config: Data<SharedAppConfig>, config: Data<SharedAppConfig>,
) -> Result<Json<api::Product>> { ) -> routes::Result<Json<model::api::Product>> {
let product_id: model::ProductId = path.into_inner().into(); let product_id: model::ProductId = path.into_inner().into();
let db = db.into_inner(); let db = db.into_inner();
let public_path = { let public_path = {
@ -122,14 +122,15 @@ async fn product(
} }
#[get("/stocks")] #[get("/stocks")]
async fn stocks(db: Data<Addr<Database>>) -> Result<HttpResponse> { async fn stocks(db: Data<Addr<Database>>) -> routes::Result<Json<Vec<model::Stock>>> {
public_send_db!(db.into_inner(), database_manager::AllStocks) let stocks = public_send_db!(owned, db.into_inner(), database_manager::AllStocks);
Ok(Json(stocks))
} }
#[post("/register")] #[post("/register")]
pub async fn create_account( pub async fn create_account(
db: Data<Addr<Database>>, db: Data<Addr<Database>>,
Json(payload): Json<api::CreateAccountInput>, Json(payload): Json<model::api::CreateAccountInput>,
config: Data<SharedAppConfig>, config: Data<SharedAppConfig>,
tm: Data<Addr<TokenManager>>, tm: Data<Addr<TokenManager>>,
) -> routes::Result<Json<model::api::SessionOutput>> { ) -> routes::Result<Json<model::api::SessionOutput>> {
@ -160,6 +161,8 @@ pub async fn create_account(
routes::Error::CriticalFailure routes::Error::CriticalFailure
); );
let role = account.role;
let AuthPair { let AuthPair {
access_token, access_token,
access_token_string, access_token_string,
@ -167,61 +170,20 @@ pub async fn create_account(
refresh_token_string, refresh_token_string,
} = create_auth_pair(tm, account).await?; } = create_auth_pair(tm, account).await?;
Ok(Json(api::SessionOutput { Ok(Json(model::api::SessionOutput {
access_token: access_token_string, access_token: access_token_string,
refresh_token: refresh_token_string, refresh_token: refresh_token_string,
exp: access_token.expiration_time, 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")] #[post("/sign-in")]
async fn sign_in( async fn sign_in(
Json(payload): Json<api::SignInInput>, Json(payload): Json<model::api::SignInInput>,
db: Data<Addr<Database>>, db: Data<Addr<Database>>,
tm: Data<Addr<TokenManager>>, tm: Data<Addr<TokenManager>>,
) -> Result<Json<api::SessionOutput>> { ) -> routes::Result<Json<model::api::SessionOutput>> {
let db = db.into_inner(); let db = db.into_inner();
let account: model::FullAccount = query_db!( let account: model::FullAccount = query_db!(
@ -236,6 +198,8 @@ async fn sign_in(
return Err(routes::Error::Unauthorized); return Err(routes::Error::Unauthorized);
} }
let role = account.role;
let AuthPair { let AuthPair {
access_token, access_token,
access_token_string, access_token_string,
@ -243,10 +207,11 @@ async fn sign_in(
refresh_token_string, refresh_token_string,
} = create_auth_pair(tm, account).await?; } = create_auth_pair(tm, account).await?;
Ok(Json(api::SessionOutput { Ok(Json(model::api::SessionOutput {
access_token: access_token_string, access_token: access_token_string,
refresh_token: refresh_token_string, refresh_token: refresh_token_string,
exp: access_token.expiration_time, exp: access_token.expiration_time,
role,
})) }))
} }

View File

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

View File

@ -664,6 +664,12 @@ impl PasswordConfirmation {
#[serde(transparent)] #[serde(transparent)]
pub struct PassHash(String); pub struct PassHash(String);
impl PassHash {
pub fn new<S: Into<String>>(s: S) -> Self {
Self(s.into())
}
}
impl PartialEq<PasswordConfirmation> for Password { impl PartialEq<PasswordConfirmation> for Password {
fn eq(&self, other: &PasswordConfirmation) -> bool { fn eq(&self, other: &PasswordConfirmation) -> bool {
self.0 == other.0 self.0 == other.0
@ -977,6 +983,12 @@ pub struct Token {
pub jwt_id: uuid::Uuid, 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 = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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" } 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}; use seed::fetch::{FetchError, Request};
pub mod admin;
pub mod public; pub mod public;
#[derive(Debug)] #[derive(Debug)]
@ -12,6 +13,12 @@ pub enum NetRes<S> {
Http(FetchError), 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> { impl<S> FromResidual<Result<Infallible, NetRes<S>>> for NetRes<S> {
fn from_residual(residual: Result<Infallible, NetRes<S>>) -> Self { fn from_residual(residual: Result<Infallible, NetRes<S>>) -> Self {
match residual { 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
}