summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGalen Guyer <galen@galenguyer.com>2022-04-15 21:19:42 -0400
committerGalen Guyer <galen@galenguyer.com>2022-04-15 21:19:42 -0400
commit0757b4f1ed7bca13597f10e4e9b0996a8e84c7b7 (patch)
tree90e997041df2ab4d3fe7600d8740511f2b85f1f4
parent177ca712d626599afc2bf45a9fccba173178b58e (diff)
add login route and modified time
-rw-r--r--Cargo.lock22
-rw-r--r--Cargo.toml5
-rw-r--r--migrations/2022-04-09-create-schema.sql20
-rw-r--r--src/db/models.rs1
-rw-r--r--src/db/strings.rs8
-rw-r--r--src/main.rs3
-rw-r--r--src/routes/v1/requests.rs7
-rw-r--r--src/routes/v1/users.rs69
8 files changed, 127 insertions, 8 deletions
diff --git a/Cargo.lock b/Cargo.lock
index dc09dfe..d6d00af 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/Cargo.toml b/Cargo.toml
index 78b4a0b..09b74c9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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 })))
+}