diff --git a/.sqlx/query-164c44b8f2c7c59eeaf4402be845f5788cc0e720ac0799a5457a893b2a070dda.json b/.sqlx/query-164c44b8f2c7c59eeaf4402be845f5788cc0e720ac0799a5457a893b2a070dda.json new file mode 100644 index 0000000..6c577da --- /dev/null +++ b/.sqlx/query-164c44b8f2c7c59eeaf4402be845f5788cc0e720ac0799a5457a893b2a070dda.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "select * from refresh_tokens where token_hash = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "token_hash", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "revoked_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true + ] + }, + "hash": "164c44b8f2c7c59eeaf4402be845f5788cc0e720ac0799a5457a893b2a070dda" +} diff --git a/.sqlx/query-784522f0b0c26f78d25290e4cdb8058e68866905cd194b3f485d7563acba884c.json b/.sqlx/query-784522f0b0c26f78d25290e4cdb8058e68866905cd194b3f485d7563acba884c.json new file mode 100644 index 0000000..7e016cb --- /dev/null +++ b/.sqlx/query-784522f0b0c26f78d25290e4cdb8058e68866905cd194b3f485d7563acba884c.json @@ -0,0 +1,54 @@ +{ + "db_name": "PostgreSQL", + "query": "insert into refresh_tokens (user_id, token_hash, expires_at) values ($1, $2, $3) returning *", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "token_hash", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "revoked_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Varchar", + "Timestamptz" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true + ] + }, + "hash": "784522f0b0c26f78d25290e4cdb8058e68866905cd194b3f485d7563acba884c" +} diff --git a/.sqlx/query-cba04640f6d764123bfb0a857498001bdf0ed3c9fe66e410ccd2ef66d5e2b35a.json b/.sqlx/query-cba04640f6d764123bfb0a857498001bdf0ed3c9fe66e410ccd2ef66d5e2b35a.json new file mode 100644 index 0000000..1e48c9f --- /dev/null +++ b/.sqlx/query-cba04640f6d764123bfb0a857498001bdf0ed3c9fe66e410ccd2ef66d5e2b35a.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "update refresh_tokens set revoked_at = now() where id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "cba04640f6d764123bfb0a857498001bdf0ed3c9fe66e410ccd2ef66d5e2b35a" +} diff --git a/Cargo.lock b/Cargo.lock index f85f5ec..92ba7ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "argon2" version = "0.5.3" @@ -191,7 +197,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", "windows-link", ] @@ -494,6 +503,19 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -745,6 +767,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -774,6 +802,8 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] @@ -792,6 +822,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "10.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +dependencies = [ + "base64", + "getrandom 0.2.17", + "js-sys", + "pem", + "rand", + "serde", + "serde_json", + "signature", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -801,6 +848,12 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.186" @@ -913,6 +966,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1011,6 +1074,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1089,6 +1162,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1107,6 +1190,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.6" @@ -1134,7 +1223,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", ] [[package]] @@ -1178,7 +1267,9 @@ version = "0.1.0" dependencies = [ "argon2", "axum", + "chrono", "dotenvy", + "jsonwebtoken", "serde", "serde_json", "sqlx", @@ -1188,6 +1279,7 @@ dependencies = [ "tracing-appender", "tracing-subscriber", "tracing-tree", + "uuid", ] [[package]] @@ -1228,6 +1320,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -1351,6 +1449,18 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.12" @@ -1932,6 +2042,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "url" version = "2.5.8" @@ -1956,7 +2072,9 @@ version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ + "getrandom 0.4.2", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -1984,6 +2102,24 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + [[package]] name = "wasite" version = "0.1.0" @@ -2035,6 +2171,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "whoami" version = "1.6.1" @@ -2179,6 +2349,100 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/Cargo.toml b/Cargo.toml index fd21668..19201e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,6 @@ thiserror = "2" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" argon2 = "0.5.3" +jsonwebtoken = { version = "10.3.0", features = ["rand"] } +chrono = { version = "0.4.44", features = ["serde"] } +uuid = { version = "1.23.1", features = ["serde", "v4"] } diff --git a/migrations/0002_refresh_token_table.sql b/migrations/0002_refresh_token_table.sql new file mode 100644 index 0000000..ff93449 --- /dev/null +++ b/migrations/0002_refresh_token_table.sql @@ -0,0 +1,10 @@ +create table refresh_tokens ( + id uuid primary key default uuidv4(), + user_id uuid not null references users(id) on delete cascade, + token_hash varchar(255) not null, -- SHA-256 hash of the plain token + expires_at timestamptz not null, + created_at timestamptz not null default now(), + revoked_at timestamptz, + + constraint unique_token_hash unique (token_hash) +); diff --git a/src/config.rs b/src/config.rs index 1534d10..b58968b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,6 +29,7 @@ pub struct Config { pub db_url: String, pub app_env: AppEnv, pub socket_address: String, + pub jwt_secret: String, } impl Config { @@ -38,6 +39,7 @@ impl Config { db_url: env::var("DATABASE_URL")?, socket_address: env::var("SOCKET_ADDRESS")?, app_env: AppEnv::from_env()?, + jwt_secret: env::var("JWT_SECRET")?, }) } } diff --git a/src/db/model/mod.rs b/src/db/model/mod.rs index 22d12a3..4afde74 100644 --- a/src/db/model/mod.rs +++ b/src/db/model/mod.rs @@ -1 +1,2 @@ +pub mod refresh_token; pub mod user; diff --git a/src/db/model/refresh_token.rs b/src/db/model/refresh_token.rs new file mode 100644 index 0000000..7739002 --- /dev/null +++ b/src/db/model/refresh_token.rs @@ -0,0 +1,14 @@ +use sqlx::types::{ + Uuid, + chrono::{DateTime, Utc}, +}; + +#[derive(Debug)] +pub struct RefreshToken { + pub id: Uuid, + pub user_id: Uuid, + pub token_hash: String, + pub expires_at: DateTime, + pub created_at: DateTime, + pub revoked_at: Option>, +} diff --git a/src/db/repository/mod.rs b/src/db/repository/mod.rs index a5188ff..dc277ef 100644 --- a/src/db/repository/mod.rs +++ b/src/db/repository/mod.rs @@ -1 +1,2 @@ +pub mod refresh_token_repository; pub mod user_repository; diff --git a/src/db/repository/refresh_token_repository.rs b/src/db/repository/refresh_token_repository.rs new file mode 100644 index 0000000..42f3462 --- /dev/null +++ b/src/db/repository/refresh_token_repository.rs @@ -0,0 +1,59 @@ +use sqlx::{ + Executor, Postgres, + types::{ + Uuid, + chrono::{DateTime, Utc}, + }, +}; + +use crate::{db::model::refresh_token::RefreshToken, errors::AppError}; + +pub async fn create_refresh_token<'e, E>( + executor: E, + user_id: Uuid, + token_hash: String, + expires_at: DateTime, +) -> Result +where + E: Executor<'e, Database = Postgres>, +{ + sqlx::query_as!( + RefreshToken, + "insert into refresh_tokens (user_id, token_hash, expires_at) values ($1, $2, $3) returning *", + user_id, + token_hash, + expires_at + ).fetch_one(executor) + .await + .map_err(AppError::from) +} + +pub async fn find_by_hash<'e, E>( + executor: E, + token_hash: &str, +) -> Result, AppError> +where + E: Executor<'e, Database = Postgres>, +{ + sqlx::query_as!( + RefreshToken, + "select * from refresh_tokens where token_hash = $1", + token_hash + ) + .fetch_optional(executor) + .await + .map_err(AppError::from) +} +pub async fn revoke<'e, E>(executor: E, id: Uuid) -> Result<(), AppError> +where + E: Executor<'e, Database = Postgres>, +{ + sqlx::query!( + "update refresh_tokens set revoked_at = now() where id = $1", + id + ) + .execute(executor) + .await + .map_err(AppError::from)?; + Ok(()) +} diff --git a/src/db/repository/user_repository.rs b/src/db/repository/user_repository.rs index 821b9c0..d7e6f1c 100644 --- a/src/db/repository/user_repository.rs +++ b/src/db/repository/user_repository.rs @@ -1,33 +1,51 @@ -use sqlx::{PgPool, types::Uuid}; +use sqlx::{Executor, PgPool, Postgres, types::Uuid}; use crate::{db::model::user::User, errors::AppError}; -pub async fn create_user(pool: &PgPool, email: String, password: String) -> Result { +pub async fn create_user<'e, E>( + executor: E, + email: String, + password: String, +) -> Result +where + E: Executor<'e, Database = Postgres>, +{ let user = sqlx::query_as!( User, "insert into users (email, password) values ($1, $2) returning *", email, password ) - .fetch_one(pool) + .fetch_one(executor) .await .map_err(AppError::from)?; Ok(user) } -pub async fn get_user_by_email(pool: &PgPool, email: String) -> Result, AppError> { +/* + *And these two call patterns both work: +- Pool: user_repo::create_user(&state.db, ...) → E = &'static PgPool +- Transaction: user_repo::create_user(&mut *tx, ...) → E = &mut PgConnection + */ +pub async fn get_user_by_email<'e, E>(executor: E, email: &str) -> Result, AppError> +where + E: Executor<'e, Database = Postgres>, +{ let user = sqlx::query_as!(User, "select * from users where email=$1", email) - .fetch_optional(pool) + .fetch_optional(executor) .await .map_err(AppError::from)?; Ok(user) } -pub async fn get_user_by_id(pool: &PgPool, id: Uuid) -> Result, AppError> { +pub async fn get_user_by_id<'e, E>(executor: E, id: Uuid) -> Result, AppError> +where + E: Executor<'e, Database = Postgres>, +{ let user = sqlx::query_as!(User, "select * from users where id=$1", id) - .fetch_optional(pool) + .fetch_optional(executor) .await .map_err(AppError::from)?; diff --git a/src/server.rs b/src/server.rs index d7a0e0e..59d7a0b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -4,7 +4,10 @@ use sqlx::PgPool; use crate::{config, controller, errors::AppError, logging, state::AppState}; pub async fn init(cfg: &config::Config, db: PgPool) -> Result<(), AppError> { - let state = AppState { db }; + let state = AppState { + db, + jwt_secret: cfg.jwt_secret.clone(), + }; let app = Router::new().merge(controller::router()).with_state(state); let listener = tokio::net::TcpListener::bind(&cfg.socket_address) diff --git a/src/service/auth_service.rs b/src/service/auth_service.rs index 41daf29..75cbda2 100644 --- a/src/service/auth_service.rs +++ b/src/service/auth_service.rs @@ -5,6 +5,7 @@ use crate::db::repository::user_repository; use crate::errors::AppError; use crate::state::AppState; use crate::utils::hash; +use crate::utils::jwt::generate_access_token; pub async fn login(state: &AppState, req: LoginRequest) -> Result, AppError> { todo!() @@ -13,7 +14,16 @@ pub async fn register( state: &AppState, req: RegisterRequest, ) -> Result, AppError> { + let mut tx = state.db.begin().await?; + let user = user_repository::get_user_by_email(&mut *tx, &req.email).await?; + if user.is_some() { + // user already registered + return Err(AppError::Internal); + } let h = hash::hash(&req.password)?; - let user = user_repository::create_user(&state.db, req.email, h); + let user = user_repository::create_user(&mut *tx, req.email, h).await?; + let access_token = generate_access_token(user.id, &state.jwt_secret)?; + + tx.commit().await?; todo!("generate jwt and refresh token") } diff --git a/src/state.rs b/src/state.rs index 6b42e8c..db39793 100644 --- a/src/state.rs +++ b/src/state.rs @@ -3,4 +3,5 @@ use sqlx::PgPool; #[derive(Clone)] pub struct AppState { pub db: PgPool, + pub jwt_secret: String, } diff --git a/src/utils/jwt.rs b/src/utils/jwt.rs new file mode 100644 index 0000000..6a551dc --- /dev/null +++ b/src/utils/jwt.rs @@ -0,0 +1,33 @@ +use chrono::{Duration, Utc}; +use jsonwebtoken::{EncodingKey, Header, encode}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::errors::AppError; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: Uuid, + pub iat: i64, + pub exp: i64, + pub jti: Uuid, +} + +pub fn generate_access_token(user_id: Uuid, secret: &str) -> Result { + let now = Utc::now(); + let expires_at = now + Duration::minutes(15); + + let claims = Claims { + sub: user_id, + iat: now.timestamp(), + exp: expires_at.timestamp(), + jti: Uuid::new_v4(), + }; + + encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(secret.as_bytes()), + ) + .map_err(|_| AppError::Internal) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ec5d33c..ee79ece 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1 +1,2 @@ pub mod hash; +pub mod jwt;