first commit
This commit is contained in:
commit
62ff6e321e
22 changed files with 4284 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
.env
|
||||||
|
*.db
|
3680
Cargo.lock
generated
Normal file
3680
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
38
Cargo.toml
Normal file
38
Cargo.toml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
[package]
|
||||||
|
name = "mailauth"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = [".", "migration"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = { version = "0.7.5", default-features = false, features = [
|
||||||
|
"tokio",
|
||||||
|
"http1",
|
||||||
|
"tracing",
|
||||||
|
"query",
|
||||||
|
"json",
|
||||||
|
"form"
|
||||||
|
] }
|
||||||
|
axum-extra = {version = "0.9.4", features=["cookie-signed"]}
|
||||||
|
|
||||||
|
reqwest = { version = "0.12.7", default-features = false, features = [
|
||||||
|
"json",
|
||||||
|
"rustls-tls",
|
||||||
|
] }
|
||||||
|
|
||||||
|
dotenvy = "0.15"
|
||||||
|
jsonwebtoken = "9.3.0"
|
||||||
|
serde = "1.0.123"
|
||||||
|
rand= "0.8.5"
|
||||||
|
|
||||||
|
tokio = { version = "1.39.3", default-features = false, features = [
|
||||||
|
"rt-multi-thread",
|
||||||
|
"macros",
|
||||||
|
"net",
|
||||||
|
] }
|
||||||
|
sea-orm = { version = "1.1.0", features = [ "sqlx-sqlite", "runtime-tokio-native-tls", "macros" ] }
|
||||||
|
migration = { path = "migration" }
|
||||||
|
minijinja = "2.4.0"
|
||||||
|
thiserror = "2.0.3"
|
18
migration/Cargo.toml
Normal file
18
migration/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "migration"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "migration"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
||||||
|
|
||||||
|
[dependencies.sea-orm-migration]
|
||||||
|
version = "1.1.0"
|
||||||
|
features = [
|
||||||
|
"sqlx-sqlite", "runtime-tokio-native-tls"
|
||||||
|
]
|
41
migration/README.md
Normal file
41
migration/README.md
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Running Migrator CLI
|
||||||
|
|
||||||
|
- Generate a new migration file
|
||||||
|
```sh
|
||||||
|
cargo run -- generate MIGRATION_NAME
|
||||||
|
```
|
||||||
|
- Apply all pending migrations
|
||||||
|
```sh
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
```sh
|
||||||
|
cargo run -- up
|
||||||
|
```
|
||||||
|
- Apply first 10 pending migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- up -n 10
|
||||||
|
```
|
||||||
|
- Rollback last applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- down
|
||||||
|
```
|
||||||
|
- Rollback last 10 applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- down -n 10
|
||||||
|
```
|
||||||
|
- Drop all tables from the database, then reapply all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- fresh
|
||||||
|
```
|
||||||
|
- Rollback all applied migrations, then reapply all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- refresh
|
||||||
|
```
|
||||||
|
- Rollback all applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- reset
|
||||||
|
```
|
||||||
|
- Check the status of all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- status
|
||||||
|
```
|
14
migration/src/lib.rs
Normal file
14
migration/src/lib.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
pub use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
mod m20241106_212353_create_table;
|
||||||
|
|
||||||
|
pub struct Migrator;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigratorTrait for Migrator {
|
||||||
|
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||||
|
vec![
|
||||||
|
Box::new(m20241106_212353_create_table::Migration),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
35
migration/src/m20241106_212353_create_table.rs
Normal file
35
migration/src/m20241106_212353_create_table.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(User::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(pk_auto(User::Id))
|
||||||
|
.col(string(User::Password))
|
||||||
|
.col(string(User::Userid))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(User::Table).to_owned())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum User {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Userid,
|
||||||
|
Password,
|
||||||
|
}
|
6
migration/src/main.rs
Normal file
6
migration/src/main.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[async_std::main]
|
||||||
|
async fn main() {
|
||||||
|
cli::run_cli(migration::Migrator).await;
|
||||||
|
}
|
6
src/appstate.rs
Normal file
6
src/appstate.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Appstate {
|
||||||
|
pub conn: DatabaseConnection,
|
||||||
|
}
|
45
src/config.rs
Normal file
45
src/config.rs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
use std::{env, sync::OnceLock};
|
||||||
|
|
||||||
|
use axum_extra::extract::cookie::Key;
|
||||||
|
use dotenvy::dotenv;
|
||||||
|
|
||||||
|
static CONFIG: OnceLock<Config> = OnceLock::new();
|
||||||
|
|
||||||
|
pub struct Config {
|
||||||
|
pub zauth_url: String,
|
||||||
|
pub callback_url: String,
|
||||||
|
pub host_url: String,
|
||||||
|
pub zauth_client_id: String,
|
||||||
|
pub zauth_client_secret: String,
|
||||||
|
pub database_uri: String,
|
||||||
|
pub cookies_key: Key,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn initialize() {
|
||||||
|
assert!(CONFIG.get().is_none());
|
||||||
|
|
||||||
|
Config::get();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get() -> &'static Config {
|
||||||
|
CONFIG.get_or_init(|| {
|
||||||
|
dotenv().ok();
|
||||||
|
Config {
|
||||||
|
zauth_url: env::var("ZAUTH_URL").expect("ZAUTH_URL not present"),
|
||||||
|
callback_url: env::var("ZAUTH_CALLBACK_PATH")
|
||||||
|
.expect("ZAUTH_CALLBACK_PATH not present"),
|
||||||
|
host_url: env::var("HOST_URL").expect("HOST_URL not present"),
|
||||||
|
zauth_client_id: env::var("ZAUTH_CLIENT_ID").expect("ZAUTH_CLIENT_ID not present"),
|
||||||
|
zauth_client_secret: env::var("ZAUTH_CLIENT_SECRET")
|
||||||
|
.expect("ZAUTH_CLIENT_SECRET not present"),
|
||||||
|
database_uri: env::var("DATABASE_URL").expect("DATABASE_URI not present"),
|
||||||
|
cookies_key: Key::from(
|
||||||
|
env::var("COOKIES_KEY")
|
||||||
|
.expect("COOKIES_KEY not present")
|
||||||
|
.as_ref(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
5
src/entities/mod.rs
Normal file
5
src/entities/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
pub mod prelude;
|
||||||
|
|
||||||
|
pub mod user;
|
3
src/entities/prelude.rs
Normal file
3
src/entities/prelude.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
pub use super::user::Entity as User;
|
17
src/entities/user.rs
Normal file
17
src/entities/user.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "user")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i32,
|
||||||
|
pub password: String,
|
||||||
|
pub userid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
34
src/error.rs
Normal file
34
src/error.rs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use sea_orm::DbErr;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ThisError {
|
||||||
|
#[error("Datase error")]
|
||||||
|
Database(#[from] DbErr),
|
||||||
|
|
||||||
|
#[error("Client request error")]
|
||||||
|
Request(#[from] reqwest::Error),
|
||||||
|
|
||||||
|
#[error("Error: {message:?}")]
|
||||||
|
Generic { code: StatusCode, message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ThisError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
eprint!("{}", self);
|
||||||
|
match self {
|
||||||
|
ThisError::Database(_) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"500 Internal Server Error".to_string(),
|
||||||
|
),
|
||||||
|
ThisError::Request(_) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"500 Internal Server Error".to_string(),
|
||||||
|
),
|
||||||
|
ThisError::Generic { code, .. } => (code, code.to_string()),
|
||||||
|
}
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
75
src/main.rs
Normal file
75
src/main.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
mod appstate;
|
||||||
|
mod config;
|
||||||
|
mod entities;
|
||||||
|
mod error;
|
||||||
|
mod models;
|
||||||
|
mod routes;
|
||||||
|
|
||||||
|
use appstate::Appstate;
|
||||||
|
use auth::{callback, login};
|
||||||
|
use axum::{
|
||||||
|
response::Html,
|
||||||
|
routing::{get, post},
|
||||||
|
Extension, Router,
|
||||||
|
};
|
||||||
|
use config::Config;
|
||||||
|
use migration::{Migrator, MigratorTrait};
|
||||||
|
use models::user::UserSession;
|
||||||
|
use routes::{auth, middelware::auth_guard, user::update_password};
|
||||||
|
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use sea_orm::Database;
|
||||||
|
|
||||||
|
async fn index(Extension(user): Extension<UserSession>) -> impl IntoResponse {
|
||||||
|
Html(format!(
|
||||||
|
r#"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Post Request Form</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1> Good day {} </h1>
|
||||||
|
<form action="{}/update_password" method="POST">
|
||||||
|
<input type="text" name="password" placeholder="Enter some text" required />
|
||||||
|
<button type="submit">Send Data</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#,
|
||||||
|
user.name,
|
||||||
|
Config::get().host_url
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
Config::initialize();
|
||||||
|
|
||||||
|
let conn = Database::connect(Config::get().database_uri.to_string())
|
||||||
|
.await
|
||||||
|
.expect("Database connection failed");
|
||||||
|
Migrator::up(&conn, None)
|
||||||
|
.await
|
||||||
|
.expect("Cound not run migrations");
|
||||||
|
|
||||||
|
let state = Appstate { conn };
|
||||||
|
|
||||||
|
// build our application with a single route
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/", get(index))
|
||||||
|
.route("/index", get(index))
|
||||||
|
.route("/update_password", post(update_password))
|
||||||
|
.route_layer(axum::middleware::from_fn(auth_guard))
|
||||||
|
.route("/login", get(login))
|
||||||
|
.route("/oauth/callback", get(callback))
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
// run our app with hyper, listening globally on port 3000
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
}
|
9
src/models/jwt.rs
Normal file
9
src/models/jwt.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct JWTPayload {
|
||||||
|
pub sub: String,
|
||||||
|
pub iat: usize,
|
||||||
|
pub exp: usize,
|
||||||
|
pub preferred_username: String
|
||||||
|
}
|
2
src/models/mod.rs
Normal file
2
src/models/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod jwt;
|
||||||
|
pub mod user;
|
4
src/models/user.rs
Normal file
4
src/models/user.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UserSession {
|
||||||
|
pub name: String
|
||||||
|
}
|
133
src/routes/auth.rs
Normal file
133
src/routes/auth.rs
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
use axum::extract::Query;
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use axum::response::Redirect;
|
||||||
|
use axum_extra::extract::cookie::{Cookie, SameSite};
|
||||||
|
use axum_extra::extract::{CookieJar, SignedCookieJar};
|
||||||
|
use rand::distributions::{Alphanumeric, DistString};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::error::ThisError;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct LoginParams {
|
||||||
|
redirect: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login(jar: CookieJar, params: Query<LoginParams>) -> (CookieJar, Redirect) {
|
||||||
|
let state = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
|
||||||
|
// insert state so we can check it in the callback
|
||||||
|
|
||||||
|
// redirect to zauth to authenticate
|
||||||
|
let zauth_url = Config::get().zauth_url.to_string();
|
||||||
|
let callback_url = Config::get().callback_url.to_string();
|
||||||
|
let zauth_client_id = Config::get().zauth_client_id.to_string();
|
||||||
|
|
||||||
|
let redirect_cookie = if let Some(redirect_uri) = params.redirect.clone() {
|
||||||
|
Cookie::build(("redirect", redirect_uri))
|
||||||
|
} else {
|
||||||
|
Cookie::build(("redirect", "/"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let state_cookie = Cookie::build(("state", state.clone()))
|
||||||
|
.secure(true)
|
||||||
|
.http_only(true)
|
||||||
|
.same_site(SameSite::Lax);
|
||||||
|
|
||||||
|
let jar = jar.add(redirect_cookie).add(state_cookie);
|
||||||
|
|
||||||
|
(jar, Redirect::to(&format!("{zauth_url}/oauth/authorize?client_id={zauth_client_id}&response_type=code&state={state}&redirect_uri={callback_url}&scope=openid")))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct Callback {
|
||||||
|
state: String,
|
||||||
|
code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct ZauthToken {
|
||||||
|
access_token: String,
|
||||||
|
id_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct ZauthUser {
|
||||||
|
id: i32,
|
||||||
|
username: String,
|
||||||
|
admin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn callback(
|
||||||
|
Query(params): Query<Callback>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
jar: CookieJar,
|
||||||
|
) -> Result<(SignedCookieJar, Redirect), ThisError> {
|
||||||
|
let zauth_state = jar
|
||||||
|
.get("state")
|
||||||
|
.ok_or(ThisError::Generic {
|
||||||
|
code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
message: "failed to get session".to_string(),
|
||||||
|
})?
|
||||||
|
.value();
|
||||||
|
|
||||||
|
// check if saved state matches returned state
|
||||||
|
if zauth_state != params.state {
|
||||||
|
return Err(ThisError::Generic {
|
||||||
|
code: StatusCode::UNAUTHORIZED,
|
||||||
|
message: "state does not match".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let form = [
|
||||||
|
("grant_type", "authorization_code"),
|
||||||
|
("code", ¶ms.code),
|
||||||
|
("redirect_uri", &Config::get().callback_url),
|
||||||
|
];
|
||||||
|
|
||||||
|
// get token from zauth with code
|
||||||
|
let token = client
|
||||||
|
.post(format!("{}/oauth/token", Config::get().zauth_url))
|
||||||
|
.basic_auth(
|
||||||
|
Config::get().zauth_client_id.to_string(),
|
||||||
|
Some(Config::get().zauth_client_secret.to_string()),
|
||||||
|
)
|
||||||
|
.form(&form)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<ZauthToken>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// get user info from zauth
|
||||||
|
let zauth_user = client
|
||||||
|
.get(format!("{}/current_user", Config::get().zauth_url))
|
||||||
|
.header("Authorization", "Bearer ".to_owned() + &token.access_token)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<ZauthUser>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// validate is user is admin
|
||||||
|
if !zauth_user.admin {
|
||||||
|
return Err(ThisError::Generic {
|
||||||
|
code: StatusCode::UNAUTHORIZED,
|
||||||
|
message: "user is not admin".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let cookie = Cookie::build(("jwt", token.id_token))
|
||||||
|
.path("/")
|
||||||
|
.secure(true)
|
||||||
|
.http_only(true)
|
||||||
|
.same_site(SameSite::Lax)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let redirect_uri = jar.get("redirect").map(|c| c.value()).unwrap_or("/");
|
||||||
|
let signed_jar = SignedCookieJar::from_headers(&headers, Config::get().cookies_key.clone());
|
||||||
|
|
||||||
|
Ok((signed_jar.add(cookie), Redirect::to(redirect_uri)))
|
||||||
|
}
|
72
src/routes/middelware.rs
Normal file
72
src/routes/middelware.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
use axum::{
|
||||||
|
extract::Request,
|
||||||
|
http::{HeaderMap, StatusCode},
|
||||||
|
middleware::Next,
|
||||||
|
response::{IntoResponse, Redirect},
|
||||||
|
};
|
||||||
|
|
||||||
|
use axum_extra::extract::SignedCookieJar;
|
||||||
|
use jsonwebtoken::{decode, decode_header, jwk::Jwk, DecodingKey, Validation};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
error::ThisError,
|
||||||
|
models::{jwt::JWTPayload, user::UserSession},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A JWK set
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct JwkSet {
|
||||||
|
pub keys: Vec<Jwk>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn auth_guard(
|
||||||
|
headers: HeaderMap,
|
||||||
|
mut req: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<impl IntoResponse, ThisError> {
|
||||||
|
let redirect = req.uri().clone();
|
||||||
|
let jar = SignedCookieJar::from_headers(&headers, Config::get().cookies_key.clone());
|
||||||
|
|
||||||
|
let token = jar
|
||||||
|
.get("jwt")
|
||||||
|
.map(|s| s.value().to_owned())
|
||||||
|
.ok_or(ThisError::Generic {
|
||||||
|
code: StatusCode::UNAUTHORIZED,
|
||||||
|
message: String::from("You are not logged in, please provide token"),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let jwks = client
|
||||||
|
.get(format!("{}/oauth/jwks", Config::get().zauth_url))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json::<JwkSet>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let header = decode_header(&token).map_err(|e| ThisError::Generic {
|
||||||
|
code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
message: format!("invalid token format: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut validation = Validation::new(header.alg);
|
||||||
|
validation.set_audience(&[Config::get().zauth_client_id.to_string()]);
|
||||||
|
|
||||||
|
for jwk in jwks.keys {
|
||||||
|
let payload = match DecodingKey::from_jwk(&jwk) {
|
||||||
|
Ok(decode_key) => decode::<JWTPayload>(&token, &decode_key, &validation),
|
||||||
|
Err(_) => todo!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if payload.is_ok() {
|
||||||
|
let user = UserSession {
|
||||||
|
name: payload.unwrap().claims.preferred_username,
|
||||||
|
};
|
||||||
|
req.extensions_mut().insert(user);
|
||||||
|
return Ok(next.run(req).await);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Redirect::to(&format!("/login?redirect={}", redirect)).into_response())
|
||||||
|
}
|
3
src/routes/mod.rs
Normal file
3
src/routes/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod middelware;
|
||||||
|
pub mod user;
|
||||||
|
pub mod auth;
|
41
src/routes/user.rs
Normal file
41
src/routes/user.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use axum::Form;
|
||||||
|
use axum::{extract::State, Extension};
|
||||||
|
use sea_orm::{ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::entities::{prelude::*, *};
|
||||||
|
use crate::error::ThisError;
|
||||||
|
use crate::{appstate::Appstate, models::user::UserSession};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct PasswordData {
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_password(
|
||||||
|
state: State<Appstate>,
|
||||||
|
Extension(session): Extension<UserSession>,
|
||||||
|
Form(form): Form<PasswordData>,
|
||||||
|
) -> Result<String, ThisError> {
|
||||||
|
let user_option = User::find()
|
||||||
|
.filter(user::Column::Userid.eq(session.name.to_owned()))
|
||||||
|
.one(&state.conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match user_option {
|
||||||
|
Some(user) => {
|
||||||
|
let mut user: user::ActiveModel = user.into();
|
||||||
|
user.password = ActiveValue::Set(form.password);
|
||||||
|
user.update(&state.conn).await?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let user = user::ActiveModel {
|
||||||
|
userid: ActiveValue::Set(session.name.to_owned()),
|
||||||
|
password: ActiveValue::Set(form.password),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
user.insert(&state.conn).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok("Password updated".to_string())
|
||||||
|
}
|
Loading…
Reference in a new issue