init project, impl authorization
This commit is contained in:
41
backend/src/controller/auth_controller.rs
Normal file
41
backend/src/controller/auth_controller.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use crate::dto::account::{CreateAccount, Login, Tokens};
|
||||
use crate::error::AppError;
|
||||
use crate::utils::extractor::msgpack::MsgPack;
|
||||
use crate::AccountService;
|
||||
use actix_web::{post, web, Scope};
|
||||
|
||||
pub mod dto {}
|
||||
|
||||
#[post("/register")]
|
||||
pub async fn create_account(
|
||||
account: MsgPack<CreateAccount>,
|
||||
account_service: web::Data<AccountService>,
|
||||
) -> MsgPack<Tokens> {
|
||||
MsgPack(account_service.create_account(account.0).await)
|
||||
}
|
||||
|
||||
#[post("/login")]
|
||||
pub async fn login(
|
||||
login: MsgPack<Login>,
|
||||
account_service: web::Data<AccountService>,
|
||||
) -> Result<MsgPack<Tokens>, AppError> {
|
||||
account_service
|
||||
.login(login.0)
|
||||
.await
|
||||
.map(|data| MsgPack(data))
|
||||
}
|
||||
|
||||
#[post("/refresh")]
|
||||
pub async fn refresh(
|
||||
token: String,
|
||||
account_service: web::Data<AccountService>,
|
||||
) -> Result<MsgPack<Tokens>, AppError> {
|
||||
account_service
|
||||
.refresh_token(token)
|
||||
.await
|
||||
.map(|data| MsgPack(data))
|
||||
}
|
||||
|
||||
pub fn auth_controller_builder() -> Scope {
|
||||
web::scope("/auth").service(create_account)
|
||||
}
|
||||
1
backend/src/controller/mod.rs
Normal file
1
backend/src/controller/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod auth_controller;
|
||||
34
backend/src/data/account/account.rs
Normal file
34
backend/src/data/account/account.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use crate::service::account::entity::Account as AccountEntity;
|
||||
use crate::utils::uuid::{sqlx_uuid_to_uuid, uuid_to_sqlx_uuid};
|
||||
|
||||
pub struct Account {
|
||||
pub id: sqlx::types::Uuid,
|
||||
pub username: String,
|
||||
pub password_hash: Vec<u8>,
|
||||
pub password_salt: String,
|
||||
pub email: Option<String>,
|
||||
}
|
||||
|
||||
impl From<AccountEntity> for Account {
|
||||
fn from(entity: AccountEntity) -> Self {
|
||||
Account {
|
||||
id: uuid_to_sqlx_uuid(entity.id),
|
||||
username: entity.username,
|
||||
password_hash: entity.password_hash,
|
||||
password_salt: entity.password_salt,
|
||||
email: entity.email,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<AccountEntity> for Account {
|
||||
fn into(self) -> AccountEntity {
|
||||
AccountEntity::new(
|
||||
sqlx_uuid_to_uuid(self.id),
|
||||
self.username,
|
||||
self.password_hash,
|
||||
self.password_salt,
|
||||
self.email,
|
||||
)
|
||||
}
|
||||
}
|
||||
103
backend/src/data/account/account_repository.rs
Normal file
103
backend/src/data/account/account_repository.rs
Normal file
@ -0,0 +1,103 @@
|
||||
use crate::data::account::token::RefreshToken;
|
||||
use crate::data::account::Account;
|
||||
use crate::error::AppError;
|
||||
use crate::service::account::entity::Account as AccountEntity;
|
||||
use crate::utils::uuid::uuid_to_sqlx_uuid;
|
||||
use sqlx::types::Uuid;
|
||||
use sqlx::PgPool;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AccountRepository {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl AccountRepository {
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
AccountRepository { pool }
|
||||
}
|
||||
|
||||
pub async fn create_account(&self, create_account: AccountEntity) -> AccountEntity {
|
||||
let account: Account = create_account.into();
|
||||
sqlx::query_as!(
|
||||
Account,
|
||||
"INSERT INTO account(id, username, password_hash, password_salt, email) VALUES ($1, $2, $3, $4, $5) RETURNING *",
|
||||
account.id,
|
||||
account.username,
|
||||
account.password_hash,
|
||||
account.password_salt,
|
||||
account.email,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.unwrap()
|
||||
.into()
|
||||
}
|
||||
|
||||
pub async fn create_refresh_token(&self, for_account: uuid::Uuid) -> RefreshToken {
|
||||
let id = Uuid::from_bytes(for_account.into_bytes());
|
||||
|
||||
sqlx::query_as!(
|
||||
RefreshToken,
|
||||
"INSERT INTO refresh_token(id, user_id, time_created) VALUES ($1, $2, $3) RETURNING *",
|
||||
Uuid::from_bytes(uuid::Uuid::new_v4().into_bytes()),
|
||||
id,
|
||||
chrono::Utc::now().naive_utc(),
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn get_account(&self, id: uuid::Uuid) -> Result<Option<AccountEntity>, AppError> {
|
||||
Ok(sqlx::query_as!(
|
||||
Account,
|
||||
"SELECT * FROM account WHERE id = $1",
|
||||
uuid_to_sqlx_uuid(id),
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?
|
||||
.map(|account| account.into()))
|
||||
}
|
||||
|
||||
pub async fn get_account_by_name(
|
||||
&self,
|
||||
name: String,
|
||||
) -> Result<Option<AccountEntity>, AppError> {
|
||||
Ok(
|
||||
sqlx::query_as!(Account, "SELECT * FROM account WHERE username = $1", name,)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?
|
||||
.map(|account| account.into()),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn get_refresh_token(
|
||||
&self,
|
||||
token_id: uuid::Uuid,
|
||||
user_id: uuid::Uuid,
|
||||
) -> Result<Option<RefreshToken>, AppError> {
|
||||
Ok(sqlx::query_as!(
|
||||
RefreshToken,
|
||||
"SELECT * FROM refresh_token WHERE id = $1 AND user_id = $2",
|
||||
uuid_to_sqlx_uuid(token_id),
|
||||
uuid_to_sqlx_uuid(user_id),
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn delete_refresh_token(
|
||||
&self,
|
||||
token_id: uuid::Uuid,
|
||||
user_id: uuid::Uuid,
|
||||
) -> Result<(), AppError> {
|
||||
sqlx::query!(
|
||||
"DELETE FROM refresh_token WHERE id = $1 AND user_id = $2",
|
||||
uuid_to_sqlx_uuid(token_id),
|
||||
uuid_to_sqlx_uuid(user_id),
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
6
backend/src/data/account/mod.rs
Normal file
6
backend/src/data/account/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
mod account;
|
||||
mod account_repository;
|
||||
mod token;
|
||||
|
||||
pub use account::Account;
|
||||
pub use account_repository::AccountRepository;
|
||||
7
backend/src/data/account/token.rs
Normal file
7
backend/src/data/account/token.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use chrono::NaiveDateTime;
|
||||
|
||||
pub struct RefreshToken {
|
||||
pub id: sqlx::types::Uuid,
|
||||
pub user_id: sqlx::types::Uuid,
|
||||
pub time_created: NaiveDateTime,
|
||||
}
|
||||
1
backend/src/data/mod.rs
Normal file
1
backend/src/data/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod account;
|
||||
57
backend/src/dto/account/mod.rs
Normal file
57
backend/src/dto/account/mod.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use crate::service::account::entity::Account as AccountEntity;
|
||||
use derive_more::Constructor;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateAccount {
|
||||
pub name: String,
|
||||
pub email: Option<String>,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Login {
|
||||
pub name: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Constructor, Serialize)]
|
||||
pub struct Account {
|
||||
pub email: Option<String>,
|
||||
pub name: String,
|
||||
pub id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Constructor, Serialize)]
|
||||
pub struct Tokens {
|
||||
pub refresh: String,
|
||||
pub access: String,
|
||||
}
|
||||
|
||||
#[derive(Constructor, Serialize)]
|
||||
pub struct AccountCreated {
|
||||
pub account: Account,
|
||||
pub tokens: Tokens,
|
||||
}
|
||||
|
||||
#[derive(Constructor, Serialize, Deserialize)]
|
||||
pub struct AccessTokenClaims {
|
||||
pub exp: i64,
|
||||
pub sub: String,
|
||||
pub iat: i64,
|
||||
}
|
||||
|
||||
#[derive(Constructor, Serialize, Deserialize)]
|
||||
pub struct RefreshTokenClaims {
|
||||
pub exp: i64,
|
||||
pub sub: String,
|
||||
pub iat: i64,
|
||||
pub refresh_token_id: Uuid,
|
||||
}
|
||||
|
||||
impl From<AccountEntity> for Account {
|
||||
fn from(entity: AccountEntity) -> Self {
|
||||
Account::new(entity.email, entity.username, entity.id)
|
||||
}
|
||||
}
|
||||
10
backend/src/dto/mod.rs
Normal file
10
backend/src/dto/mod.rs
Normal file
@ -0,0 +1,10 @@
|
||||
pub mod account;
|
||||
|
||||
use derive_more::Constructor;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Constructor)]
|
||||
pub struct ErrorDto {
|
||||
pub code: String,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
57
backend/src/error/mod.rs
Normal file
57
backend/src/error/mod.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use crate::dto::ErrorDto;
|
||||
use actix_web::body::BoxBody;
|
||||
use actix_web::http::header::TryIntoHeaderValue;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpResponse, ResponseError};
|
||||
use derive_more::{Constructor, Display};
|
||||
use sqlx::Error;
|
||||
|
||||
#[derive(Constructor, Display, Debug)]
|
||||
#[display(
|
||||
fmt = "AppError:status: {}, code: {}, message: {:?}",
|
||||
status,
|
||||
code,
|
||||
message
|
||||
)]
|
||||
pub struct AppError {
|
||||
code: String,
|
||||
status: u16,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
pub fn from_code(code: String) -> Self {
|
||||
AppError::from_code_and_status(code, 400)
|
||||
}
|
||||
|
||||
pub fn from_code_and_status(code: String, status: u16) -> Self {
|
||||
AppError::new(code, status, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for AppError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::from_u16(self.status).unwrap()
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse<BoxBody> {
|
||||
let mut response = HttpResponse::new(self.status_code());
|
||||
response.headers_mut().insert(
|
||||
actix_web::http::header::CONTENT_TYPE,
|
||||
mime::APPLICATION_MSGPACK.try_into_value().unwrap(),
|
||||
);
|
||||
response.set_body(BoxBody::new(
|
||||
rmp_serde::to_vec(&ErrorDto::new(
|
||||
self.code.clone(),
|
||||
self.message.as_ref().map(|data| data.clone()),
|
||||
))
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for AppError {
|
||||
fn from(_: Error) -> Self {
|
||||
AppError::from_code_and_status("INTERNAL_ERROR".to_string(), 500)
|
||||
}
|
||||
}
|
||||
46
backend/src/main.rs
Normal file
46
backend/src/main.rs
Normal file
@ -0,0 +1,46 @@
|
||||
mod controller;
|
||||
mod data;
|
||||
mod dto;
|
||||
mod error;
|
||||
mod service;
|
||||
mod utils;
|
||||
|
||||
use crate::controller::auth_controller::auth_controller_builder;
|
||||
use crate::data::account::AccountRepository;
|
||||
use crate::service::account::AccountService;
|
||||
use crate::utils::env::extract_env_var;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
|
||||
const DB_URL_KEY: &str = "DATABASE_URL";
|
||||
const JWT_SECRET_KEY: &str = "JWT_SECRET";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JwtSecretKey(Vec<u8>);
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
dotenv::dotenv().unwrap();
|
||||
|
||||
let db_url = extract_env_var(DB_URL_KEY);
|
||||
let jwt_secret = extract_env_var(JWT_SECRET_KEY);
|
||||
|
||||
let pool = PgPoolOptions::new().connect(db_url.as_str()).await.unwrap();
|
||||
sqlx::migrate!().run(&pool).await.unwrap();
|
||||
let key = JwtSecretKey(base64::decode(jwt_secret).unwrap());
|
||||
|
||||
let account_repository = AccountRepository::new(pool.clone());
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.service(auth_controller_builder())
|
||||
.app_data(web::Data::new(AccountService::new(
|
||||
account_repository.clone(),
|
||||
key.clone(),
|
||||
)))
|
||||
.app_data(web::Data::new(key.clone()))
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
139
backend/src/service/account/account_service.rs
Normal file
139
backend/src/service/account/account_service.rs
Normal file
@ -0,0 +1,139 @@
|
||||
use crate::data::account::AccountRepository;
|
||||
use crate::dto::account::{AccessTokenClaims, CreateAccount, Login, RefreshTokenClaims, Tokens};
|
||||
use crate::error::AppError;
|
||||
use crate::service::account::entity::Account;
|
||||
use crate::utils::uuid::sqlx_uuid_to_uuid;
|
||||
use crate::JwtSecretKey;
|
||||
use chrono::{Duration, Utc};
|
||||
use derive_more::Constructor;
|
||||
use jsonwebtoken::errors::ErrorKind;
|
||||
use jsonwebtoken::{decode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||
use rand::Rng;
|
||||
use sha2::{Digest, Sha512};
|
||||
use std::ops::Add;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Constructor, Clone)]
|
||||
pub struct AccountService {
|
||||
account_repository: AccountRepository,
|
||||
secret: JwtSecretKey,
|
||||
}
|
||||
|
||||
impl AccountService {
|
||||
pub async fn create_account(&self, create_account_dto: CreateAccount) -> Tokens {
|
||||
let salt: String = rand::thread_rng()
|
||||
.sample_iter::<char, _>(rand::distributions::Standard)
|
||||
.take(32)
|
||||
.collect();
|
||||
let mut hasher = Sha512::new();
|
||||
hasher.update(create_account_dto.password.add(salt.as_str()).as_bytes());
|
||||
let hash = hasher.finalize().to_vec();
|
||||
let account = self
|
||||
.account_repository
|
||||
.create_account(Account::new(
|
||||
Uuid::new_v4(),
|
||||
create_account_dto.name,
|
||||
hash,
|
||||
salt,
|
||||
create_account_dto.email,
|
||||
))
|
||||
.await;
|
||||
self.create_tokens(account.id).await
|
||||
}
|
||||
|
||||
pub async fn create_tokens(&self, for_account: Uuid) -> Tokens {
|
||||
let header = Header::new(Algorithm::HS512);
|
||||
let iat = Utc::now();
|
||||
let access_token_claims = AccessTokenClaims::new(
|
||||
iat.add(Duration::hours(1)).timestamp(),
|
||||
for_account.to_string(),
|
||||
iat.timestamp(),
|
||||
);
|
||||
let encoding_key = EncodingKey::from_secret(self.secret.0.as_slice());
|
||||
let access_token =
|
||||
jsonwebtoken::encode(&header, &access_token_claims, &encoding_key).unwrap();
|
||||
let refresh_token_data = self
|
||||
.account_repository
|
||||
.create_refresh_token(for_account)
|
||||
.await;
|
||||
let refresh_token_claims = RefreshTokenClaims::new(
|
||||
iat.add(Duration::days(30)).timestamp(),
|
||||
refresh_token_data.user_id.to_string(),
|
||||
iat.timestamp(),
|
||||
sqlx_uuid_to_uuid(refresh_token_data.id),
|
||||
);
|
||||
let refresh_token =
|
||||
jsonwebtoken::encode(&header, &refresh_token_claims, &encoding_key).unwrap();
|
||||
Tokens::new(access_token, refresh_token)
|
||||
}
|
||||
|
||||
pub async fn login(&self, login_dto: Login) -> Result<Tokens, AppError> {
|
||||
let account = if let Some(account) = self
|
||||
.account_repository
|
||||
.get_account_by_name(login_dto.name)
|
||||
.await?
|
||||
{
|
||||
account
|
||||
} else {
|
||||
return Err(AppError::from_code("USER_NOT_FOUND".to_string()));
|
||||
};
|
||||
let mut hasher = Sha512::new();
|
||||
hasher.update(
|
||||
login_dto
|
||||
.password
|
||||
.add(account.password_salt.as_str())
|
||||
.as_bytes(),
|
||||
);
|
||||
let hash = hasher.finalize().to_vec();
|
||||
if hash == account.password_hash {
|
||||
Ok(self.create_tokens(account.id).await)
|
||||
} else {
|
||||
Err(AppError::from_code("INCORRECT_PASSWORD".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn refresh_token(&self, token: String) -> Result<Tokens, AppError> {
|
||||
let token = match decode::<RefreshTokenClaims>(
|
||||
token.as_str(),
|
||||
&DecodingKey::from_secret(self.secret.0.as_slice()),
|
||||
&Validation::new(Algorithm::HS512),
|
||||
) {
|
||||
Err(err) => {
|
||||
return Err(match *err.kind() {
|
||||
ErrorKind::InvalidToken => {
|
||||
AppError::from_code_and_status("NOT_TOKEN".to_string(), 400)
|
||||
}
|
||||
ErrorKind::ExpiredSignature => {
|
||||
AppError::from_code_and_status("EXPIRED".to_string(), 403)
|
||||
}
|
||||
_ => AppError::from_code_and_status("INVALID_TOKEN".to_string(), 401),
|
||||
});
|
||||
}
|
||||
Ok(claims) => claims,
|
||||
};
|
||||
let user_uuid = Uuid::parse_str(&token.claims.sub).unwrap();
|
||||
let token_uuid = token.claims.refresh_token_id;
|
||||
let old_token = if let Some(token) = self
|
||||
.account_repository
|
||||
.get_refresh_token(token_uuid, user_uuid)
|
||||
.await?
|
||||
{
|
||||
token
|
||||
} else {
|
||||
return Err(AppError::from_code_and_status(
|
||||
"INVALID_TOKEN".to_string(),
|
||||
401,
|
||||
));
|
||||
};
|
||||
if old_token.time_created.add(Duration::days(30)) > Utc::now().naive_utc() {
|
||||
return Err(AppError::from_code_and_status(
|
||||
"TOKEN_EXPIRED".to_string(),
|
||||
400,
|
||||
));
|
||||
};
|
||||
self.account_repository
|
||||
.delete_refresh_token(token_uuid, user_uuid)
|
||||
.await?;
|
||||
Ok(self.create_tokens(user_uuid).await)
|
||||
}
|
||||
}
|
||||
12
backend/src/service/account/entity/account.rs
Normal file
12
backend/src/service/account/entity/account.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use derive_more::Constructor;
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Constructor, Serialize)]
|
||||
pub struct Account {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
pub password_hash: Vec<u8>,
|
||||
pub password_salt: String,
|
||||
pub email: Option<String>,
|
||||
}
|
||||
3
backend/src/service/account/entity/mod.rs
Normal file
3
backend/src/service/account/entity/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod account;
|
||||
|
||||
pub use account::Account;
|
||||
4
backend/src/service/account/mod.rs
Normal file
4
backend/src/service/account/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod account_service;
|
||||
pub mod entity;
|
||||
|
||||
pub use account_service::AccountService;
|
||||
1
backend/src/service/mod.rs
Normal file
1
backend/src/service/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod account;
|
||||
8
backend/src/utils/env.rs
Normal file
8
backend/src/utils/env.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use std::env;
|
||||
|
||||
pub fn extract_env_var(key: &str) -> String {
|
||||
env::var(key).unwrap_or_else(|_| {
|
||||
println!("Bruh, we need {} to be set", key);
|
||||
std::process::exit(1)
|
||||
})
|
||||
}
|
||||
1
backend/src/utils/extractor/mod.rs
Normal file
1
backend/src/utils/extractor/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod msgpack;
|
||||
149
backend/src/utils/extractor/msgpack.rs
Normal file
149
backend/src/utils/extractor/msgpack.rs
Normal file
@ -0,0 +1,149 @@
|
||||
use actix_web::body::EitherBody;
|
||||
use actix_web::dev::{Decompress, Payload};
|
||||
use actix_web::error::PayloadError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Bytes, BytesMut};
|
||||
use actix_web::{Error, FromRequest, HttpRequest, HttpResponse, Responder, ResponseError};
|
||||
use derive_more::Display;
|
||||
use futures::{ready, Stream};
|
||||
use rmp_serde::{decode::Error as RmpDecodeError, encode::Error as RmpEncodeError};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::task::Poll::{Pending, Ready};
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
pub struct MsgPack<T>(pub T);
|
||||
|
||||
impl<T: DeserializeOwned> FromRequest for MsgPack<T> {
|
||||
type Error = Error;
|
||||
type Future = MsgPackExtractFuture<T>;
|
||||
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
MsgPackExtractFuture::new(req.clone(), payload)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum MsgPackError {
|
||||
#[display(fmt = "Can not deserialize")]
|
||||
Deserialize(RmpDecodeError),
|
||||
#[display(fmt = "Reading payload error {}", _0)]
|
||||
ReadPayload(PayloadError),
|
||||
#[display(fmt = "Can not serialize msgpack")]
|
||||
Serialize(RmpEncodeError),
|
||||
}
|
||||
|
||||
impl ResponseError for MsgPackError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
MsgPackError::Deserialize(_) => StatusCode::UNPROCESSABLE_ENTITY,
|
||||
MsgPackError::ReadPayload(err) => err.status_code(),
|
||||
MsgPackError::Serialize(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MsgPackExtractFuture<T> {
|
||||
_req: HttpRequest,
|
||||
fut: MsgPackBody<T>,
|
||||
_res: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: DeserializeOwned> MsgPackExtractFuture<T> {
|
||||
fn new(req: HttpRequest, payload: &mut Payload) -> Self {
|
||||
MsgPackExtractFuture {
|
||||
_req: req.clone(),
|
||||
fut: MsgPackBody::new(req.clone(), payload),
|
||||
_res: PhantomData {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Unpin for MsgPackExtractFuture<T> {}
|
||||
|
||||
impl<T: DeserializeOwned> Future for MsgPackExtractFuture<T> {
|
||||
type Output = Result<MsgPack<T>, Error>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.get_mut();
|
||||
|
||||
let res = ready!(Pin::new(&mut this.fut).poll(cx));
|
||||
match res {
|
||||
Ok(data) => Ready(Ok(MsgPack(data))),
|
||||
Err(err) => Ready(Err(err.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MsgPackBody<T> {
|
||||
buf: BytesMut,
|
||||
payload: Decompress<Payload>,
|
||||
_res: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Unpin for MsgPackBody<T> {}
|
||||
|
||||
impl<T: DeserializeOwned> MsgPackBody<T> {
|
||||
fn new(req: HttpRequest, payload: &mut Payload) -> Self {
|
||||
MsgPackBody {
|
||||
payload: Decompress::from_headers(payload.take(), req.headers()),
|
||||
buf: BytesMut::new(),
|
||||
_res: PhantomData {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DeserializeOwned> Future for MsgPackBody<T> {
|
||||
type Output = Result<T, MsgPackError>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.get_mut();
|
||||
|
||||
let next = ready!(Pin::new(&mut this.payload).poll_next(cx));
|
||||
match next {
|
||||
None => {
|
||||
let res = match rmp_serde::from_slice(&this.buf) {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
return Ready(Err(MsgPackError::Deserialize(err)));
|
||||
}
|
||||
};
|
||||
Ready(Ok(res))
|
||||
}
|
||||
Some(res) => {
|
||||
let data: Bytes = match res {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
return Ready(Err(MsgPackError::ReadPayload(err)));
|
||||
}
|
||||
};
|
||||
this.buf.extend_from_slice(&data);
|
||||
Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> Responder for MsgPack<T> {
|
||||
type Body = EitherBody<Bytes>;
|
||||
|
||||
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||
match rmp_serde::to_vec(&self.0) {
|
||||
Ok(data) => {
|
||||
match HttpResponse::Ok()
|
||||
.content_type(mime::APPLICATION_MSGPACK)
|
||||
.message_body(Bytes::from(data))
|
||||
{
|
||||
Ok(response) => response.map_into_left_body(),
|
||||
Err(err) => HttpResponse::from_error(err).map_into_right_body(),
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
HttpResponse::from_error(MsgPackError::Serialize(err)).map_into_right_body()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
backend/src/utils/mod.rs
Normal file
3
backend/src/utils/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod env;
|
||||
pub mod extractor;
|
||||
pub mod uuid;
|
||||
7
backend/src/utils/uuid.rs
Normal file
7
backend/src/utils/uuid.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub fn sqlx_uuid_to_uuid(from: sqlx::types::Uuid) -> uuid::Uuid {
|
||||
uuid::Uuid::from_bytes(*from.as_bytes())
|
||||
}
|
||||
|
||||
pub fn uuid_to_sqlx_uuid(from: uuid::Uuid) -> sqlx::types::Uuid {
|
||||
sqlx::types::Uuid::from_bytes(from.into_bytes())
|
||||
}
|
||||
Reference in New Issue
Block a user