register
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 6m8s

This commit is contained in:
Dmitri 2026-04-17 10:59:29 +02:00
parent 1cb32e3d2c
commit f79d830cf2
Signed by: kanopo
GPG Key ID: 759ADD40E3132AC7
21 changed files with 603 additions and 13 deletions

245
Cargo.lock generated
View File

@ -26,6 +26,24 @@ dependencies = [
"libc", "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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures",
"password-hash",
]
[[package]] [[package]]
name = "atoi" name = "atoi"
version = "2.0.0" version = "2.0.0"
@ -120,6 +138,15 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -170,7 +197,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"serde",
"wasm-bindgen",
"windows-link", "windows-link",
] ]
@ -445,6 +475,19 @@ dependencies = [
"wasi", "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]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.5" version = "0.15.5"
@ -696,6 +739,12 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]] [[package]]
name = "idna" name = "idna"
version = "1.1.0" version = "1.1.0"
@ -725,6 +774,8 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.17.0", "hashbrown 0.17.0",
"serde",
"serde_core",
] ]
[[package]] [[package]]
@ -752,6 +803,12 @@ dependencies = [
"spin", "spin",
] ]
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.185" version = "0.2.185"
@ -945,6 +1002,17 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.7.0" version = "0.7.0"
@ -1017,6 +1085,16 @@ dependencies = [
"zerocopy", "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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.106" version = "1.0.106"
@ -1035,6 +1113,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -1062,7 +1146,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.17",
] ]
[[package]] [[package]]
@ -1104,7 +1188,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
name = "rhythm_backend" name = "rhythm_backend"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"argon2",
"axum", "axum",
"chrono",
"dotenvy", "dotenvy",
"serde", "serde",
"serde_json", "serde_json",
@ -1114,6 +1200,7 @@ dependencies = [
"tower-http", "tower-http",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"uuid",
] ]
[[package]] [[package]]
@ -1124,7 +1211,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"getrandom", "getrandom 0.2.17",
"libc", "libc",
"untrusted", "untrusted",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@ -1202,6 +1289,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@ -1838,6 +1931,12 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@ -1868,7 +1967,9 @@ version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
dependencies = [ dependencies = [
"getrandom 0.4.2",
"js-sys", "js-sys",
"serde_core",
"wasm-bindgen", "wasm-bindgen",
] ]
@ -1896,6 +1997,24 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[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",
]
[[package]] [[package]]
name = "wasite" name = "wasite"
version = "0.1.0" version = "0.1.0"
@ -1947,6 +2066,40 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "0.26.11" version = "0.26.11"
@ -2182,6 +2335,94 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[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-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]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.3" version = "0.6.3"

View File

@ -4,7 +4,9 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
argon2 = "0.5.3"
axum = "0.8.9" axum = "0.8.9"
chrono = { version = "0.4.42", features = ["serde"] }
dotenvy = "0.15.7" dotenvy = "0.15.7"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
@ -14,3 +16,4 @@ tokio = { version = "1.52.0", features = ["macros", "rt-multi-thread"] }
tracing = "0.1.44" tracing = "0.1.44"
tower-http = { version = "0.6.6", features = ["trace"] } tower-http = { version = "0.6.6", features = ["trace"] }
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
uuid = { version = "1.18.1", features = ["serde", "v4"] }

9
bruno/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
# Secrets
.env*
# Dependencies
node_modules
# OS files
.DS_Store
Thumbs.db

20
bruno/Untitled.yml Normal file
View File

@ -0,0 +1,20 @@
info:
name: Untitled
type: http
seq: 1
http:
method: POST
url: "{{host}}/api/auth/register"
auth: inherit
runtime:
variables:
- name: host
value: http://localhost:8080
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

10
bruno/opencollection.yml Normal file
View File

@ -0,0 +1,10 @@
opencollection: 1.0.0
info:
name: rhythm
bundled: false
extensions:
bruno:
ignore:
- node_modules
- .git

95
docs/migration.md Normal file
View File

@ -0,0 +1,95 @@
# SQLx Migration Guide
This project uses SQLx migrations and runs them on application startup.
## 1) Install SQLx CLI
### Prerequisites
- Rust toolchain installed (`rustup`)
- PostgreSQL reachable from your machine
- `DATABASE_URL` available
### Install command
```bash
cargo install sqlx-cli --no-default-features --features rustls,postgres
```
Verify installation:
```bash
sqlx --version
```
## 2) Configure `DATABASE_URL`
Set your database connection string:
```bash
export DATABASE_URL=postgres://DB_USERNAME:DB_PASSWORD@DB_HOST:DB_PORT/DB_NAME
```
Example:
```bash
export DATABASE_URL=postgres://rhythm:rhythm@localhost:5432/rhythm_db
```
## 3) Create a new migration
Create a migration file:
```bash
sqlx migrate add create_users
```
This generates a timestamped SQL file under `migrations/`, for example:
- `migrations/20260416103000_create_users.sql`
If you want reversible migrations (up/down):
```bash
sqlx migrate add -r create_users
```
## 4) Run migrations manually
Apply pending migrations:
```bash
sqlx migrate run
```
Show migration status:
```bash
sqlx migrate info
```
Revert the most recent migration (only for reversible migrations):
```bash
sqlx migrate revert
```
## 5) Startup migrations (application)
Add this after creating the database pool in `src/main.rs`:
```rust
sqlx::migrate!("./migrations").run(&pool).await?;
```
Behavior:
- Pending migrations are applied on every startup.
- Applied migrations are tracked in the `_sqlx_migrations` table.
- If a migration fails, the app fails fast and does not start listening.
## 6) Team workflow
- Create a new migration for every schema change.
- Commit migration files to git.
- Do not modify migration files that are already applied in shared environments.
- Add a new migration to evolve schema safely.
## 7) Production notes
- Running migrations on startup in production is supported.
- In multi-instance deployments, one instance may apply migrations while others wait/retry according to orchestration settings.
- Prefer backward-compatible migrations for rolling deployments.

View File

@ -0,0 +1,9 @@
-- Add migration script here
CREATE TABLE USERS (
ID UUID PRIMARY KEY DEFAULT gen_random_uuid(),
EMAIL VARCHAR(255) NOT NULL UNIQUE,
PASSWORD VARCHAR(255) NOT NULL,
CREATED_AT TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UPDATED_AT TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

View File

@ -1 +1,3 @@
pub mod database; pub mod database;
pub mod model;
pub mod user_repo;

1
src/db/model/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod user;

1
src/db/model/user/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod user_dto;

View File

@ -0,0 +1,8 @@
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct User {
pub id: uuid::Uuid,
pub email: String,
pub password: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}

42
src/db/user_repo.rs Normal file
View File

@ -0,0 +1,42 @@
use crate::db::model::user::user_dto::User;
use crate::error::AppError;
use sqlx::{Postgres, Transaction};
pub async fn email_exists(
tx: &mut Transaction<'_, Postgres>,
email: &str,
) -> Result<bool, AppError> {
let existing = sqlx::query_scalar::<_, bool>(
r#"
SELECT TRUE
FROM users
WHERE email = $1
LIMIT 1
"#,
)
.bind(email)
.fetch_optional(&mut **tx)
.await?;
Ok(existing.unwrap_or(false))
}
pub async fn create_user_in_tx(
tx: &mut Transaction<'_, Postgres>,
email: &str,
password_hash: &str,
) -> Result<User, AppError> {
let user = sqlx::query_as::<_, User>(
r#"
INSERT INTO users (email, password)
VALUES ($1, $2)
RETURNING id, email, password, created_at, updated_at
"#,
)
.bind(email)
.bind(password_hash)
.fetch_one(&mut **tx)
.await?;
Ok(user)
}

View File

@ -1,3 +1,9 @@
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::Serialize;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -8,4 +14,31 @@ pub enum AppError {
Db(#[from] sqlx::Error), Db(#[from] sqlx::Error),
#[error(transparent)] #[error(transparent)]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error(transparent)]
Migration(#[from] sqlx::migrate::MigrateError),
#[error("validation error: {0}")]
Validation(String),
#[error("conflict: {0}")]
Conflict(String),
}
#[derive(Serialize)]
struct ErrorBody {
error: String,
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let status = match self {
Self::Validation(_) => StatusCode::BAD_REQUEST,
Self::Conflict(_) => StatusCode::CONFLICT,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
let body = Json(ErrorBody {
error: self.to_string(),
});
(status, body).into_response()
}
} }

View File

@ -1,17 +1,25 @@
use axum::{Json, Router, routing::post}; use crate::http::model::register_user_req::RegisterUserReq;
use serde::Serialize; use crate::http::model::register_user_res::RegisterUserRes;
use crate::service::auth_service;
use axum::{Json, Router, extract::State, routing::post};
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::error::AppError;
#[derive(Serialize)]
struct HealthResponse {
status: &'static str,
}
pub fn router() -> Router<AppState> { pub fn router() -> Router<AppState> {
Router::new().route("/register", post(register)) Router::new()
.route("/register", post(register))
.route("/login", post(login))
} }
async fn register() -> Json<HealthResponse> { async fn register(
Json(HealthResponse { status: "ok" }) State(state): State<AppState>,
Json(body): Json<RegisterUserReq>,
) -> Result<Json<RegisterUserRes>, AppError> {
let response = auth_service::register(&state.db, body).await?;
Ok(Json(response))
}
async fn login() -> &'static str {
"login not implemented yet"
} }

View File

@ -1,3 +1,4 @@
pub mod api_router; pub mod api_router;
mod auth_router; pub mod auth_router;
mod health_router; mod health_router;
pub mod model;

2
src/http/model/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod register_user_req;
pub mod register_user_res;

View File

@ -0,0 +1,7 @@
use serde::Deserialize;
#[derive(Deserialize)]
pub struct RegisterUserReq {
pub email: String,
pub password: String,
}

View File

@ -0,0 +1,7 @@
use serde::Serialize;
#[derive(Serialize)]
pub struct RegisterUserRes {
pub id: uuid::Uuid,
pub email: String,
}

View File

@ -3,11 +3,13 @@ mod config;
mod db; mod db;
mod error; mod error;
mod http; mod http;
mod service;
use crate::db::database; use crate::db::database;
use crate::error::AppError; use crate::error::AppError;
use crate::http::api_router; use crate::http::api_router;
use axum::Router; use axum::Router;
use sqlx::migrate;
use tower_http::trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer}; use tower_http::trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer};
use tracing::{Level, info}; use tracing::{Level, info};
use tracing_subscriber::{EnvFilter, fmt}; use tracing_subscriber::{EnvFilter, fmt};
@ -27,6 +29,8 @@ async fn main() -> Result<(), AppError> {
let pool = database::create_pool(&cfg.database_url()).await?; let pool = database::create_pool(&cfg.database_url()).await?;
info!("database connection established"); info!("database connection established");
migrate!().run(&pool).await?;
let state = app_state::AppState { db: pool }; let state = app_state::AppState { db: pool };
let app = Router::new() let app = Router::new()
.nest("/api", api_router::router()) .nest("/api", api_router::router())

View File

@ -0,0 +1,86 @@
use argon2::{
Argon2,
password_hash::{PasswordHasher, SaltString, rand_core::OsRng},
};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tokio::time::sleep;
use crate::db::user_repo;
use crate::error::AppError;
use crate::http::model::{register_user_req::RegisterUserReq, register_user_res::RegisterUserRes};
pub async fn register(
pool: &sqlx::PgPool,
req: RegisterUserReq,
) -> Result<RegisterUserRes, AppError> {
let started = Instant::now();
let result = register_inner(pool, req).await;
apply_obfuscation_delay(started).await;
result
}
async fn register_inner(
pool: &sqlx::PgPool,
req: RegisterUserReq,
) -> Result<RegisterUserRes, AppError> {
let email = req.email.trim().to_lowercase();
if email.is_empty() {
return Err(AppError::Validation("email is required".to_string()));
}
if req.password.len() < 8 {
return Err(AppError::Validation(
"password must be at least 8 characters".to_string(),
));
}
let mut tx = pool.begin().await?;
if user_repo::email_exists(&mut tx, &email).await? {
return Err(AppError::Validation(
"invalid registration request".to_string(),
));
}
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(req.password.as_bytes(), &salt)
.map_err(|e| AppError::Validation(format!("invalid password: {e}")))?
.to_string();
let user = match user_repo::create_user_in_tx(&mut tx, &email, &password_hash).await {
Ok(user) => user,
Err(AppError::Db(sqlx::Error::Database(db_err)))
if db_err.code().as_deref() == Some("23505") =>
{
return Err(AppError::Validation(
"invalid registration request".to_string(),
));
}
Err(err) => return Err(err),
};
tx.commit().await?;
Ok(RegisterUserRes {
id: user.id,
email: user.email,
})
}
async fn apply_obfuscation_delay(started: Instant) {
let min_ms = 120;
let max_ms = 320;
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let jitter = (nanos as u64) % (max_ms - min_ms + 1);
let target = Duration::from_millis(min_ms + jitter);
let elapsed = started.elapsed();
if target > elapsed {
sleep(target - elapsed).await;
}
}

1
src/service/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod auth_service;