init project, impl authorization
This commit is contained in:
3
backend/.gitignore
vendored
Normal file
3
backend/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
*.env
|
||||||
|
.idea/
|
||||||
1947
backend/Cargo.lock
generated
Normal file
1947
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
backend/Cargo.toml
Normal file
22
backend/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-web = "4.0.1"
|
||||||
|
serde = "1.0.137"
|
||||||
|
rmp-serde = "1.1.0"
|
||||||
|
sqlx = { version = "0.5", features = ["runtime-actix-rustls", "postgres", "migrate", "uuid", "chrono"] }
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
uuid = { version = "1.1.1", features = ["v4", "serde"] }
|
||||||
|
futures = "0.3.21"
|
||||||
|
derive_more = "0.99.17"
|
||||||
|
mime = "0.3.16"
|
||||||
|
rand = "0.8.5"
|
||||||
|
sha2 = "0.10.2"
|
||||||
|
chrono = "0.4.19"
|
||||||
|
jsonwebtoken = "8.1.0"
|
||||||
|
base64 = "0.13.0"
|
||||||
9
backend/migrations/20220601131817_create_account.sql
Normal file
9
backend/migrations/20220601131817_create_account.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
CREATE TABLE account
|
||||||
|
(
|
||||||
|
id uuid primary key,
|
||||||
|
username varchar(128) unique not null,
|
||||||
|
password_hash bytea not null,
|
||||||
|
password_salt varchar(32) not null,
|
||||||
|
email text null
|
||||||
|
);
|
||||||
9
backend/migrations/20220604192644_auth_token.sql
Normal file
9
backend/migrations/20220604192644_auth_token.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
CREATE TABLE refresh_token
|
||||||
|
(
|
||||||
|
id uuid not null,
|
||||||
|
user_id uuid not null,
|
||||||
|
time_created timestamp not null,
|
||||||
|
CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES account (id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (id, user_id)
|
||||||
|
);
|
||||||
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