diff options
author | Galen Guyer <galen@galenguyer.com> | 2022-04-15 21:19:42 -0400 |
---|---|---|
committer | Galen Guyer <galen@galenguyer.com> | 2022-04-15 21:19:42 -0400 |
commit | 0757b4f1ed7bca13597f10e4e9b0996a8e84c7b7 (patch) | |
tree | 90e997041df2ab4d3fe7600d8740511f2b85f1f4 | |
parent | 177ca712d626599afc2bf45a9fccba173178b58e (diff) |
add login route and modified time
-rw-r--r-- | Cargo.lock | 22 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | migrations/2022-04-09-create-schema.sql | 20 | ||||
-rw-r--r-- | src/db/models.rs | 1 | ||||
-rw-r--r-- | src/db/strings.rs | 8 | ||||
-rw-r--r-- | src/main.rs | 3 | ||||
-rw-r--r-- | src/routes/v1/requests.rs | 7 | ||||
-rw-r--r-- | src/routes/v1/users.rs | 69 |
8 files changed, 127 insertions, 8 deletions
@@ -424,6 +424,8 @@ dependencies = [ "bcrypt", "chrono", "dotenvy", + "hmac 0.12.1", + "jwt", "lazy_static", "log", "mime", @@ -431,6 +433,7 @@ dependencies = [ "rand", "serde", "serde_json", + "sha2 0.10.2", "sqlx", "tokio", "totp-rs", @@ -776,6 +779,21 @@ dependencies = [ ] [[package]] +name = "jwt" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" +dependencies = [ + "base64", + "crypto-common", + "digest 0.10.3", + "hmac 0.12.1", + "serde", + "serde_json", + "sha2 0.10.2", +] + +[[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1867,9 +1885,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9df98b037d039d03400d9dd06b0f8ce05486b5f25e9a2d7d36196e142ebbc52" +checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" dependencies = [ "ansi_term", "sharded-slab", @@ -11,6 +11,8 @@ axum = "0.5.1" bcrypt = "0.12.1" chrono = { version = "0.4.19", features = ["serde"] } dotenvy = "0.15.1" +hmac = "0.12.1" +jwt = "0.16.0" lazy_static = "1.4.0" log = "0.4.16" mime = "0.3.16" @@ -19,11 +21,12 @@ powerdns = { path = "../powerdns/" } rand = "0.8.5" serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" +sha2 = "0.10.2" sqlx = { version = "0.5.11", features = ["postgres", "runtime-tokio-rustls", "chrono"] } tokio = { version = "1.17.0", features = ["full"] } totp-rs = "0.7.4" tower = "0.4.12" tower-http = { version = "0.2.5", features = ["cors", "trace"] } -tracing-subscriber = "0.3.10" +tracing-subscriber = "0.3.11" [features] diff --git a/migrations/2022-04-09-create-schema.sql b/migrations/2022-04-09-create-schema.sql index 1bba54f..663e44f 100644 --- a/migrations/2022-04-09-create-schema.sql +++ b/migrations/2022-04-09-create-schema.sql @@ -3,7 +3,27 @@ CREATE TABLE users ( password varchar(255) NOT NULL, display_name varchar(255), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (now() AT TIME ZONE 'UTC'), + modified_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (now() AT TIME ZONE 'UTC'), admin boolean NOT NULL DEFAULT false, enabled boolean NOT NULL DEFAULT true, totp_secret varchar(255) ); + +CREATE TABLE zones ( + id varchar(255) NOT NULL UNIQUE PRIMARY KEY, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (now() AT TIME ZONE 'UTC'), + modified_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (now() AT TIME ZONE 'UTC'), + owner_email varchar(255) NOT NULL, + CONSTRAINT owner_email_fk FOREIGN KEY (owner_email) REFERENCES users (email) ON DELETE CASCADE +); + +CREATE OR REPLACE FUNCTION update_modified_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.modified_at = now() AT TIME ZONE 'UTC'; + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER update_user_modtime BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE update_modified_column(); +CREATE TRIGGER update_zone_modtime BEFORE UPDATE ON zones FOR EACH ROW EXECUTE PROCEDURE update_modified_column(); diff --git a/src/db/models.rs b/src/db/models.rs index cf5feba..10d5b57 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -9,6 +9,7 @@ pub struct User { pub password: String, pub display_name: Option<String>, pub created_at: DateTime<Utc>, + pub modified_at: DateTime<Utc>, pub admin: bool, pub enabled: bool, #[serde(skip_serializing)] diff --git a/src/db/strings.rs b/src/db/strings.rs index e5f180c..131f965 100644 --- a/src/db/strings.rs +++ b/src/db/strings.rs @@ -2,13 +2,13 @@ use lazy_static::lazy_static; lazy_static! { pub(crate) static ref GET_USER: &'static str = r" - SELECT email,password,display_name,created_at,admin,enabled,totp_secret - FROM users + SELECT email,password,display_name,created_at,modified_at,admin,enabled,totp_secret + FROM users WHERE email = $1 "; pub(crate) static ref GET_ALL_USERS: &'static str = r" - SELECT email,password,display_name,created_at,admin,enabled,totp_secret - FROM users + SELECT email,password,display_name,created_at,modified_at,admin,enabled,totp_secret + FROM users "; pub(crate) static ref CREATE_USER: &'static str = r" INSERT INTO users(email,password,display_name) VALUES ($1, $2, $3) RETURNING * diff --git a/src/main.rs b/src/main.rs index 4046d1e..0c2081e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,7 +50,8 @@ async fn main() { Router::new() .route("/", post(routes::v1::users::create_user)) .route("/all", get(routes::v1::users::get_all_users)) - .route("/totp", get(routes::v1::users::needs_totp)), + .route("/totp", get(routes::v1::users::needs_totp)) + .route("/login", post(routes::v1::users::login)), ), ), ) diff --git a/src/routes/v1/requests.rs b/src/routes/v1/requests.rs index 424dcf3..4677928 100644 --- a/src/routes/v1/requests.rs +++ b/src/routes/v1/requests.rs @@ -6,3 +6,10 @@ pub struct Signup { pub display_name: Option<String>, pub password: String, } + +#[derive(Serialize, Deserialize, Debug)] +pub struct Login { + pub email: String, + pub password: String, + pub totp_code: Option<String>, +} diff --git a/src/routes/v1/users.rs b/src/routes/v1/users.rs index 1539285..3a84c6b 100644 --- a/src/routes/v1/users.rs +++ b/src/routes/v1/users.rs @@ -6,11 +6,21 @@ use axum::http::StatusCode; use axum::response::IntoResponse; use axum::Extension; use extractors::Json; +use hmac::{Hmac, Mac}; +use jwt::SignWithKey; +use lazy_static::lazy_static; use serde_json::json; +use sha2::Sha256; use sqlx::{Error, Pool, Postgres}; +use std::collections::BTreeMap; use std::collections::HashMap; +use std::env; use std::sync::Arc; +lazy_static! { + static ref JWT_SECRET: String = env::var("JWT_SECRET").unwrap(); +} + pub async fn create_user( Json(signup): Json<requests::Signup>, Extension(pool): Extension<Arc<Pool<Postgres>>>, @@ -63,3 +73,62 @@ pub async fn needs_totp( ), } } + +pub async fn login( + Json(login_req): Json<requests::Login>, + Extension(pool): Extension<Arc<Pool<Postgres>>>, +) -> impl IntoResponse { + let user = db::users::get_user(&pool, &login_req.email).await; + let user = match user { + Ok(user) => user, + Err(err) => match err { + Error::RowNotFound => { + return ( + StatusCode::UNAUTHORIZED, + Json(json!({"error": "Invalid email or password"})), + ); + } + _ => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ "error": format!("{:?}", err) })), + ) + } + }, + }; + + if !bcrypt::verify(&login_req.password, &user.password).unwrap_or(false) { + return ( + StatusCode::UNAUTHORIZED, + Json(json!({"error": "Invalid email or password"})), + ); + } + if !user.enabled { + return ( + StatusCode::UNAUTHORIZED, + Json(json!({"error": "Invalid email or password"})), + ); + } + + let key: Hmac<Sha256> = Hmac::new_from_slice((*JWT_SECRET).as_bytes()).unwrap(); + let mut claims = BTreeMap::new(); + + let iat = chrono::Utc::now().timestamp().to_string(); + let exp = (chrono::Utc::now() + chrono::Duration::hours(24)) + .timestamp() + .to_string(); + let dn = user.display_name.unwrap_or_else(|| "".to_string()); + let admin = user.admin.to_string(); + + // https://www.iana.org/assignments/jwt/jwt.xhtml + claims.insert("iss", "fdns"); + claims.insert("sub", &user.email); + claims.insert("iat", &iat); + claims.insert("exp", &exp); + claims.insert("dn", &dn); + claims.insert("email", &user.email); + claims.insert("admin", &admin); + + let token = claims.sign_with_key(&key).unwrap(); + (StatusCode::OK, Json(json!({ "token": token }))) +} |