backend: working auth
This commit is contained in:
parent
8c3becf843
commit
ab22d6d830
9 changed files with 172 additions and 10 deletions
15
Cargo.lock
generated
15
Cargo.lock
generated
|
@ -573,6 +573,7 @@ dependencies = [
|
|||
"argon2",
|
||||
"dotenvy",
|
||||
"entity",
|
||||
"futures",
|
||||
"jsonwebtoken",
|
||||
"migration",
|
||||
"sea-orm",
|
||||
|
@ -1161,6 +1162,7 @@ checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
|||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
|
@ -1239,6 +1241,17 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.30"
|
||||
|
@ -1260,6 +1273,7 @@ dependencies = [
|
|||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
|
@ -1688,6 +1702,7 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
|||
name = "migration"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"async-std",
|
||||
"sea-orm-migration",
|
||||
]
|
||||
|
|
|
@ -18,3 +18,4 @@ sea-orm = { version = "0.12", features = [
|
|||
] }
|
||||
dotenvy = "*"
|
||||
jsonwebtoken = "*"
|
||||
futures = "*"
|
||||
|
|
|
@ -1,26 +1,123 @@
|
|||
use jsonwebtoken::{EncodingKey, Header, Validation};
|
||||
use core::fmt;
|
||||
use std::{pin::Pin, time::SystemTime};
|
||||
|
||||
use actix_web::{
|
||||
error::ErrorUnauthorized,
|
||||
http::{header, StatusCode},
|
||||
web, Error, FromRequest, HttpRequest, HttpResponse, ResponseError,
|
||||
};
|
||||
use futures::Future;
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
|
||||
use migration::token;
|
||||
use sea_orm::EntityTrait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct Claims {
|
||||
sub: Uuid,
|
||||
name: String,
|
||||
exp: u64,
|
||||
}
|
||||
|
||||
pub fn create_jwt(
|
||||
user: entity::user::Model,
|
||||
key: &EncodingKey,
|
||||
) -> Result<String, jsonwebtoken::errors::Error> {
|
||||
let time = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("SystemTime before UNIX EPOCH")
|
||||
.as_secs()
|
||||
+ 86400;
|
||||
let claims = Claims {
|
||||
sub: user.id,
|
||||
name: user.name,
|
||||
exp: time,
|
||||
};
|
||||
jsonwebtoken::encode(&Header::default(), &claims, key)
|
||||
}
|
||||
|
||||
pub fn verify(token: &str) {
|
||||
let validation = Validation::new(jsonwebtoken::Algorithm::HS256);
|
||||
// jsonwebtoken::decode(token, , validation)
|
||||
pub struct AuthedUser(entity::user::Model);
|
||||
|
||||
impl AuthedUser {
|
||||
fn parse_token(req: &HttpRequest) -> Result<Claims, Error> {
|
||||
let header = req
|
||||
.headers()
|
||||
.get(header::AUTHORIZATION)
|
||||
.ok_or(AuthorizationError)?;
|
||||
|
||||
if header.len() < 8 {
|
||||
return Err(ErrorUnauthorized("Unauthorized"));
|
||||
}
|
||||
|
||||
let mut parts = header
|
||||
.to_str()
|
||||
.map_err(|_| ErrorUnauthorized("Unauthorized"))?
|
||||
.splitn(2, ' ');
|
||||
|
||||
match parts.next() {
|
||||
Some("Bearer") => {}
|
||||
_ => return Err(ErrorUnauthorized("Unauthorized")),
|
||||
}
|
||||
|
||||
let token = parts.next().ok_or(ErrorUnauthorized("Unauthorized"))?;
|
||||
|
||||
let key = &req.app_data::<web::Data<AppState>>().unwrap().secret;
|
||||
|
||||
let validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
|
||||
let claims = jsonwebtoken::decode::<Claims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(key.as_bytes()),
|
||||
&validation,
|
||||
)
|
||||
.map_err(|e| ErrorUnauthorized(e))?;
|
||||
Ok(claims.claims)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for AuthedUser {
|
||||
type Error = actix_web::Error;
|
||||
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
|
||||
|
||||
fn from_request(
|
||||
req: &actix_web::HttpRequest,
|
||||
_payload: &mut actix_web::dev::Payload,
|
||||
) -> Self::Future {
|
||||
let res = AuthedUser::parse_token(req);
|
||||
let state = req.app_data::<web::Data<AppState>>().unwrap().db.clone();
|
||||
Box::pin(async move {
|
||||
let claims = res?;
|
||||
let id = claims.sub;
|
||||
|
||||
let user = entity::prelude::User::find_by_id(id)
|
||||
.one(&state)
|
||||
.await
|
||||
.map_err(|_| ErrorUnauthorized("Unauthorized"))?
|
||||
.ok_or(ErrorUnauthorized("Unauthorized"))?;
|
||||
|
||||
Ok(AuthedUser(user))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AuthorizationError;
|
||||
|
||||
impl fmt::Display for AuthorizationError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.status_code(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for AuthorizationError {
|
||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||
StatusCode::UNAUTHORIZED
|
||||
}
|
||||
|
||||
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
|
||||
HttpResponse::build(self.status_code()).finish()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ use actix_web::{
|
|||
web, HttpResponse, Responder,
|
||||
};
|
||||
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
||||
use jsonwebtoken::EncodingKey;
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
use serde::Deserialize;
|
||||
|
||||
|
@ -38,7 +39,8 @@ impl AuthController {
|
|||
.verify_password(login.password.as_bytes(), &parsed_hash)
|
||||
.map_err(ErrorBadRequest)?;
|
||||
|
||||
let jwt = create_jwt(user, jwt_secret).map_err(ErrorInternalServerError)?;
|
||||
let jwt = create_jwt(user, &EncodingKey::from_secret(jwt_secret.as_bytes()))
|
||||
.map_err(ErrorInternalServerError)?;
|
||||
Ok(HttpResponse::Ok().body(jwt))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use sea_orm::{ActiveModelTrait, ActiveValue, EntityTrait};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::AppState;
|
||||
use crate::{auth::AuthedUser, AppState};
|
||||
|
||||
pub struct UserController;
|
||||
|
||||
|
@ -36,7 +36,10 @@ impl From<entity::user::Model> for UserWithoutPassword {
|
|||
}
|
||||
|
||||
impl UserController {
|
||||
pub async fn list_users(state: web::Data<AppState>) -> actix_web::Result<impl Responder> {
|
||||
pub async fn list_users(
|
||||
state: web::Data<AppState>,
|
||||
_executor: AuthedUser,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
let db = &state.db;
|
||||
let users = entity::prelude::User::find()
|
||||
.all(db)
|
||||
|
|
|
@ -12,7 +12,7 @@ mod routes;
|
|||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
db: DatabaseConnection,
|
||||
secret: EncodingKey,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
|
@ -37,7 +37,7 @@ async fn main() -> std::io::Result<()> {
|
|||
|
||||
let state = AppState {
|
||||
db: conn,
|
||||
secret: EncodingKey::from_secret(jwt_secret.as_bytes()),
|
||||
secret: jwt_secret,
|
||||
};
|
||||
|
||||
println!("Listening for connections...");
|
||||
|
|
|
@ -10,6 +10,7 @@ path = "src/lib.rs"
|
|||
|
||||
[dependencies]
|
||||
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
||||
argon2 = "*"
|
||||
|
||||
[dependencies.sea-orm-migration]
|
||||
version = "0.12.0"
|
||||
|
|
|
@ -3,10 +3,14 @@ pub use sea_orm_migration::prelude::*;
|
|||
pub struct Migrator;
|
||||
|
||||
mod m20240705_100914_create_user;
|
||||
mod m20240708_085852_create_admin_user;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![Box::new(m20240705_100914_create_user::Migration)]
|
||||
vec![
|
||||
Box::new(m20240705_100914_create_user::Migration),
|
||||
Box::new(m20240708_085852_create_admin_user::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
39
crates/migration/src/m20240708_085852_create_admin_user.rs
Normal file
39
crates/migration/src/m20240708_085852_create_admin_user.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use argon2::password_hash::rand_core::OsRng;
|
||||
use argon2::password_hash::SaltString;
|
||||
use argon2::Argon2;
|
||||
use argon2::PasswordHasher;
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let argon2 = Argon2::default();
|
||||
let salt = SaltString::generate(OsRng);
|
||||
let hash = argon2
|
||||
.hash_password("admin".as_bytes(), &salt)
|
||||
.expect("Generating Admin Password Hash failed");
|
||||
let insert = Query::insert()
|
||||
.into_table(User::Table)
|
||||
.columns([User::Name, User::Email, User::Hash])
|
||||
.values_panic(["admin".into(), "admin".into(), hash.to_string().into()])
|
||||
.to_owned();
|
||||
|
||||
manager.exec_stmt(insert).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum User {
|
||||
Table,
|
||||
Name,
|
||||
Email,
|
||||
Hash,
|
||||
}
|
Loading…
Reference in a new issue