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",
|
"argon2",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"entity",
|
"entity",
|
||||||
|
"futures",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"migration",
|
"migration",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
|
@ -1161,6 +1162,7 @@ checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
@ -1239,6 +1241,17 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"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]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.30"
|
version = "0.3.30"
|
||||||
|
@ -1260,6 +1273,7 @@ dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
|
"futures-macro",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -1688,6 +1702,7 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
name = "migration"
|
name = "migration"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"argon2",
|
||||||
"async-std",
|
"async-std",
|
||||||
"sea-orm-migration",
|
"sea-orm-migration",
|
||||||
]
|
]
|
||||||
|
|
|
@ -18,3 +18,4 @@ sea-orm = { version = "0.12", features = [
|
||||||
] }
|
] }
|
||||||
dotenvy = "*"
|
dotenvy = "*"
|
||||||
jsonwebtoken = "*"
|
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 migration::token;
|
||||||
|
use sea_orm::EntityTrait;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
struct Claims {
|
struct Claims {
|
||||||
sub: Uuid,
|
sub: Uuid,
|
||||||
name: String,
|
name: String,
|
||||||
|
exp: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_jwt(
|
pub fn create_jwt(
|
||||||
user: entity::user::Model,
|
user: entity::user::Model,
|
||||||
key: &EncodingKey,
|
key: &EncodingKey,
|
||||||
) -> Result<String, jsonwebtoken::errors::Error> {
|
) -> 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 {
|
let claims = Claims {
|
||||||
sub: user.id,
|
sub: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
|
exp: time,
|
||||||
};
|
};
|
||||||
jsonwebtoken::encode(&Header::default(), &claims, key)
|
jsonwebtoken::encode(&Header::default(), &claims, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify(token: &str) {
|
pub struct AuthedUser(entity::user::Model);
|
||||||
let validation = Validation::new(jsonwebtoken::Algorithm::HS256);
|
|
||||||
// jsonwebtoken::decode(token, , validation)
|
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,
|
web, HttpResponse, Responder,
|
||||||
};
|
};
|
||||||
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
||||||
|
use jsonwebtoken::EncodingKey;
|
||||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
@ -38,7 +39,8 @@ impl AuthController {
|
||||||
.verify_password(login.password.as_bytes(), &parsed_hash)
|
.verify_password(login.password.as_bytes(), &parsed_hash)
|
||||||
.map_err(ErrorBadRequest)?;
|
.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))
|
Ok(HttpResponse::Ok().body(jwt))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use sea_orm::{ActiveModelTrait, ActiveValue, EntityTrait};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::{auth::AuthedUser, AppState};
|
||||||
|
|
||||||
pub struct UserController;
|
pub struct UserController;
|
||||||
|
|
||||||
|
@ -36,7 +36,10 @@ impl From<entity::user::Model> for UserWithoutPassword {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserController {
|
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 db = &state.db;
|
||||||
let users = entity::prelude::User::find()
|
let users = entity::prelude::User::find()
|
||||||
.all(db)
|
.all(db)
|
||||||
|
|
|
@ -12,7 +12,7 @@ mod routes;
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
db: DatabaseConnection,
|
db: DatabaseConnection,
|
||||||
secret: EncodingKey,
|
secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
|
@ -37,7 +37,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
db: conn,
|
db: conn,
|
||||||
secret: EncodingKey::from_secret(jwt_secret.as_bytes()),
|
secret: jwt_secret,
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("Listening for connections...");
|
println!("Listening for connections...");
|
||||||
|
|
|
@ -10,6 +10,7 @@ path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
||||||
|
argon2 = "*"
|
||||||
|
|
||||||
[dependencies.sea-orm-migration]
|
[dependencies.sea-orm-migration]
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
|
|
|
@ -3,10 +3,14 @@ pub use sea_orm_migration::prelude::*;
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
mod m20240705_100914_create_user;
|
mod m20240705_100914_create_user;
|
||||||
|
mod m20240708_085852_create_admin_user;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MigratorTrait for Migrator {
|
impl MigratorTrait for Migrator {
|
||||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
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