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