initial db interactions
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 9m41s

This commit is contained in:
Dmitri 2026-04-27 23:05:27 +02:00
parent f5fc85cb00
commit e4c582dabe
Signed by: kanopo
GPG Key ID: 759ADD40E3132AC7
27 changed files with 593 additions and 98 deletions

View File

@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "select * from users where email=$1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "password",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "25b92f656255a863ed7649125d60bd1e309bd64204925cf73f245c8ce63b27b7"
}

View File

@ -0,0 +1,47 @@
{
"db_name": "PostgreSQL",
"query": "insert into users (email, password) values ($1, $2) returning *",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "password",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "a145c5eb33f466fb3112e6feaf0e1ef56735331624010eda1703f0169271577c"
}

View File

@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "select * from users where id=$1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "password",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "af5f9eb11f896d65c13da7697dc8c18a2399aa845af63214e39040958e3e5ec4"
}

View File

@ -15,7 +15,7 @@
- **DB Access:** sqlx (compile-time checked queries, hand-written repositories)
- **PostgreSQL Driver/Pool:** sqlx::PgPool
- **Logging:** tracing + tracing-subscriber + tracing-tree (structured, hierarchical)
- **Auth:** jsonwebtoken (HS256) + argon2 (password hashing)
- **Auth:** jsonwebtoken (HS256 access tokens, 15min) + opaque refresh tokens (7d, stored hashed in DB via SHA-256) + argon2 (password hashing)
- **API Docs:** utoipa (code-first OpenAPI generation)
- **Health Probes:** liveness/readiness endpoints
- **Deployment:** Docker Compose on self-hosted hardware
@ -25,7 +25,7 @@
## Decisions Made
1. **JWT signing:** HS256 (shared secret from env, can migrate to RS256 later)
2. **Token model:** Access + Refresh (short-lived access ~15min, long-lived refresh ~7d, refresh tokens stored hashed in DB)
2. **Token model:** Access = JWT (HS256, 15min, stateless verification). Refresh = opaque UUID v4 string, stored SHA-256 hashed in DB (7 days, revocable). Logout deletes refresh token from DB; access token still valid until expiry.
3. **Roles:** Project-level roles (`admin`, `developer`, `reporter`) + Org-level roles (`owner`, `admin`, `member`)
4. **OpenAPI workflow:** Code-first with utoipa (auto-generate spec from Rust handlers/models)
5. **DB access:** sqlx with hand-written repositories (compile-time checked, no ORM)
@ -44,64 +44,37 @@
```
src/
├── main.rs # Entrypoint, server, graceful shutdown
├── config.rs # Config (exists, extend)
├── logging.rs # Logging (exists)
├── errors.rs # Unified error types → Axum responses
├── state.rs # AppState (PgPool, config, etc.)
├── routes.rs # Router composition (/api/v1/...)
├── config.rs # Config (db_url, jwt_secret, socket_address, app_env)
├── database.rs # PgPool setup, migration runner
├── logging.rs # tracing config (dev: pretty console, prod: JSON + file)
├── errors.rs # AppError enum → IntoResponse (BadRequest/Unauthorized/Internal)
├── state.rs # AppState (PgPool, jwt_secret)
├── server.rs # Axum server init + graceful shutdown
├── auth/
│ ├── jwt.rs # HS256 token creation/validation
│ ├── hash.rs # argon2 password hashing
│ ├── handlers.rs # register, login, refresh
│ ├── models.rs # auth DTOs
│ └── service.rs # auth business logic
│ ├── mod.rs
│ ├── jwt.rs # HS256 JWT creation/validation (access tokens, 15min)
│ ├── hash.rs # argon2 password hashing/verification
│ └── token.rs # Refresh token generation (UUID v4) + SHA-256 hashing for DB storage
├── db/
│ ├── mod.rs
│ ├── user_repo.rs # find_by_email, create
│ └── token_repo.rs # store, find_by_hash, delete_by_hash, delete_all_for_user
├── service/
│ ├── mod.rs
│ └── auth_service.rs # login, register, refresh, logout business logic
├── controller/
│ ├── mod.rs # Router composition (/api/v1/...)
│ ├── model/
│ │ ├── mod.rs
│ │ └── auth_model.rs # LoginRequest, RegisterRequest, AuthResponse DTOs
│ └── v1/
│ ├── mod.rs # v1 router (nests /auth)
│ └── auth_controller.rs # POST /login, /register, /refresh, /logout handlers
├── middleware/
│ ├── auth.rs # JWT extraction layer
│ └── rbac.rs # Project-level role guard
├── models/
│ ├── user.rs
│ ├── org.rs
│ ├── project.rs
│ ├── issue.rs
│ ├── comment.rs
│ ├── tag.rs
│ ├── sprint.rs
│ ├── stage.rs
│ ├── time_entry.rs
│ └── role.rs # OrgRole, ProjectRole enums
├── handlers/
│ ├── health.rs
│ ├── orgs.rs
│ ├── projects.rs
│ ├── issues.rs
│ ├── comments.rs
│ ├── tags.rs
│ ├── sprints.rs
│ ├── stages.rs
│ └── time_entries.rs
├── services/
│ ├── org.rs
│ ├── project.rs
│ ├── issue.rs
│ ├── comment.rs
│ ├── tag.rs
│ ├── sprint.rs
│ ├── stage.rs
│ └── time_entry.rs
└── db/
├── mod.rs # Pool setup, migration runner
└── repos/
├── users.rs
├── orgs.rs
├── projects.rs
├── issues.rs
├── comments.rs
├── tags.rs
├── sprints.rs
├── stages.rs
├── time_entries.rs
├── memberships.rs
└── refresh_tokens.rs
│ ├── auth.rs # JWT extraction from Authorization header, inject CurrentUser
│ └── rbac.rs # Project-level role guard
├── models/ # (future: org, project, issue, etc.)
└── handlers/ # (future: orgs, projects, issues, etc.)
migrations/
├── 001_create_users.sql
├── 002_create_organizations.sql
@ -490,23 +463,36 @@ CREATE INDEX idx_comments_issue ON comments(issue_id);
5. Wire up Axum router with `routes()`, shared state, and graceful shutdown via `tokio::signal`
6. Create first migration: `users` table
### Phase 2: Auth (JWT + Register/Login/Refresh)
### Phase 2: Auth (Hybrid JWT Access + DB-stored Refresh Tokens)
**Additional dependencies:** `jsonwebtoken`, `argon2`, `validator`
**Token model:** Access tokens are signed JWTs (15min, no DB lookup). Refresh tokens are opaque strings stored hashed in DB (7 days, revocable).
**Additional dependencies:** `jsonwebtoken`, `argon2`, `sha2`, `uuid` (v4), `chrono`
**Current progress:**
- ✅ Config, AppState (PgPool), DB migrations runner, router setup, users migration
- ✅ Error types (`AppError` with `IntoResponse` — BadRequest/Unauthorized/Internal)
- ✅ Auth controller routes wired (`POST /api/v1/auth/login`, `/register`)
- ✅ Auth DTOs (`LoginRequest`, `RegisterRequest`, `AuthResponse`) in `controller/model/auth_model.rs`
- ✅ Service stubs (`auth_service.rs``login()`, `register()` with `todo!()`)
- ⚠️ Handler parameter order needs fix (State before Json)
**Tasks:**
1. Create `users` migration (if not done in Phase 1)
2. Create `refresh_tokens` migration
3. Implement `auth::hash` — argon2 password hashing/verification
4. Implement `auth::jwt` — HS256 access + refresh token creation/validation
5. Implement `db::repos::users` — create, get by email, get by id
6. Implement `db::repos::refresh_tokens` — store, find, delete
7. Implement `auth::service` — register, login, refresh logic
8. Implement `auth::handlers``POST /api/v1/auth/register`, `/login`, `/refresh`
9. Implement `auth::models` — request/response DTOs with `validator` checks
10. Create `errors.rs` — unified `AppError` enum → `IntoResponse`
11. Create `middleware::auth` — JWT extraction, inject `CurrentUser`
12. Protect routes with auth layer
1. ~~Create `users` migration~~ (done — `0001_create_users_table.sql`)
2. Fix handler parameter order — `State` before `Json` in `auth_controller.rs`
3. Create `tokens` migration — `0002_create_tokens_table.sql` (id, user_id, token_hash, expires_at, created_at)
4. Add dependencies to `Cargo.toml``argon2`, `jsonwebtoken`, `sha2`, `uuid` (v4), `chrono`
5. Create `src/db/` module — `user_repo.rs` (find_by_email, create) + `token_repo.rs` (store, find_by_hash, delete_by_hash, delete_all_for_user)
6. Create `src/auth/hash.rs` — argon2 password hashing (`hash_password`, `verify_password`)
7. Create `src/auth/jwt.rs` — HS256 JWT access token creation/validation with 15min expiry (`create_access_token`, `verify_access_token`, `Claims` struct)
8. Create `src/auth/token.rs` — generate random refresh token (UUID v4), SHA-256 hash for DB storage
9. Add `jwt_secret` to `AppState` / `Config` (loaded from `JWT_SECRET` env var)
10. Implement `auth_service.rs` — login (find user → verify password → create JWT access token + generate/store refresh token) and register (check email → hash password → create user → create tokens)
11. Add input validation — email/password not empty → `AppError::Validation`
12. Create `middleware::auth` — JWT extraction from `Authorization: Bearer <token>`, inject `CurrentUser`
13. Add `POST /api/v1/auth/refresh` — accepts refresh token → hash → look up in DB → if valid, delete old + create new token pair
14. Add `POST /api/v1/auth/logout` — accepts refresh token → hash → delete from DB
15. Protect routes with auth layer
### Phase 3: RBAC Layer

168
Cargo.lock generated
View File

@ -17,6 +17,27 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[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]]
name = "atoi"
version = "2.0.0"
@ -111,6 +132,15 @@ dependencies = [
"serde_core",
]
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -138,12 +168,33 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cc"
version = "1.2.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"num-traits",
"windows-link",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@ -159,6 +210,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
@ -313,6 +370,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "flume"
version = "0.11.1"
@ -576,6 +639,30 @@ dependencies = [
"tower-service",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.2.0"
@ -913,6 +1000,17 @@ dependencies = [
"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]]
name = "pem-rfc7468"
version = "0.7.0"
@ -1078,8 +1176,11 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
name = "rhythm-backend"
version = "0.1.0"
dependencies = [
"argon2",
"axum",
"dotenvy",
"serde",
"serde_json",
"sqlx",
"thiserror",
"tokio",
@ -1224,6 +1325,12 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
@ -1309,6 +1416,7 @@ checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [
"base64",
"bytes",
"chrono",
"crc",
"crossbeam-queue",
"either",
@ -1329,7 +1437,6 @@ dependencies = [
"sha2",
"smallvec",
"thiserror",
"time",
"tokio",
"tokio-stream",
"tracing",
@ -1386,6 +1493,7 @@ dependencies = [
"bitflags",
"byteorder",
"bytes",
"chrono",
"crc",
"digest",
"dotenvy",
@ -1413,7 +1521,6 @@ dependencies = [
"sqlx-core",
"stringprep",
"thiserror",
"time",
"tracing",
"uuid",
"whoami",
@ -1429,6 +1536,7 @@ dependencies = [
"base64",
"bitflags",
"byteorder",
"chrono",
"crc",
"dotenvy",
"etcetera",
@ -1452,7 +1560,6 @@ dependencies = [
"sqlx-core",
"stringprep",
"thiserror",
"time",
"tracing",
"uuid",
"whoami",
@ -1465,6 +1572,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
dependencies = [
"atoi",
"chrono",
"flume",
"futures-channel",
"futures-core",
@ -1478,7 +1586,6 @@ dependencies = [
"serde_urlencoded",
"sqlx-core",
"thiserror",
"time",
"tracing",
"url",
"uuid",
@ -1938,12 +2045,65 @@ dependencies = [
"wasite",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.48.0"

View File

@ -10,6 +10,9 @@ tracing-appender = "0.2.5"
tracing-subscriber = {version="0.3.23", features = ["env-filter", "json"]}
tracing-tree = "0.4.1"
tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros", "signal"] }
sqlx = { version = "0.8", features = [ "runtime-tokio", "postgres", "time", "uuid" ] }
sqlx = { version = "0.8", features = [ "runtime-tokio", "postgres", "chrono", "uuid" ] }
axum = "0.8.9"
thiserror = "2"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
argon2 = "0.5.3"

View File

@ -1,9 +1,6 @@
FROM rust:1.95.0-alpine3.22 AS builder
WORKDIR /app
ARG DB_URL
ARG APP_ENV
COPY . .
RUN cargo build --release

13
Makefile Normal file
View File

@ -0,0 +1,13 @@
.PHONY: prepare build run clean
prepare:
cargo sqlx prepare
build: prepare
cargo build
run: build
cargo run
clean:
rm -dfR target

View File

@ -0,0 +1,7 @@
create table users (
id uuid primary key default uuidv4(),
email varchar(255) unique not null,
password varchar(255) not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);

View File

@ -35,7 +35,7 @@ impl Config {
pub fn load() -> Result<Self, AppError> {
dotenv().ok();
Ok(Self {
db_url: env::var("DB_URL")?,
db_url: env::var("DATABASE_URL")?,
socket_address: env::var("SOCKET_ADDRESS")?,
app_env: AppEnv::from_env()?,
})

12
src/controller/mod.rs Normal file
View File

@ -0,0 +1,12 @@
use axum::{Router, routing::get};
use crate::state::AppState;
pub mod model;
mod v1;
pub fn router() -> Router<AppState> {
Router::new()
.route("/", get("Server is going brr 🚀"))
.nest("/api/v1", v1::router_v1())
}

View File

@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct LoginRequest {
pub email: String,
pub password: String,
}
#[derive(Deserialize)]
pub struct RegisterRequest {
pub email: String,
pub password: String,
}
#[derive(Serialize)]
pub struct AuthResponse {
pub access_token: String,
pub refresh_token: String,
}

View File

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

View File

@ -0,0 +1,28 @@
use axum::extract::State;
use axum::{Json, Router, routing::post};
use crate::{
controller::model::auth_model::{AuthResponse, LoginRequest, RegisterRequest},
errors::AppError,
service::auth_service::{login, register},
state::AppState,
};
pub fn auth_router() -> Router<AppState> {
Router::new()
.route("/login", post(login_handler))
.route("/register", post(register_handler))
}
async fn login_handler(
State(s): State<AppState>,
Json(payload): Json<LoginRequest>,
) -> Result<Json<AuthResponse>, AppError> {
login(&s, payload).await
}
async fn register_handler(
State(s): State<AppState>,
Json(payload): Json<RegisterRequest>,
) -> Result<Json<AuthResponse>, AppError> {
register(&s, payload).await
}

10
src/controller/v1/mod.rs Normal file
View File

@ -0,0 +1,10 @@
use axum::Router;
use crate::state::AppState;
pub mod auth_controller;
pub fn router_v1() -> Router<AppState> {
Router::new().nest("/auth", auth_controller::auth_router())
}

View File

@ -1,4 +1,4 @@
use sqlx::{Pool, Postgres, postgres::PgPoolOptions, migrate::MigrateError};
use sqlx::{Pool, Postgres, migrate::MigrateError, postgres::PgPoolOptions};
use crate::errors::AppError;
@ -8,7 +8,7 @@ pub async fn init(db_url: &str) -> Result<Pool<Postgres>, AppError> {
.await
.map_err(AppError::DbConnect)?;
sqlx::migrate!()
sqlx::migrate!("./migrations")
.run(&db)
.await
.map_err(|e: MigrateError| AppError::InvalidConfig(format!("Migration failed: {}", e)))?;

2
src/db/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod model;
pub mod repository;

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

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

16
src/db/model/user.rs Normal file
View File

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

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

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

View File

@ -0,0 +1,35 @@
use sqlx::{PgPool, types::Uuid};
use crate::{db::model::user::User, errors::AppError};
pub async fn create_user(pool: &PgPool, email: String, password: String) -> Result<User, AppError> {
let user = sqlx::query_as!(
User,
"insert into users (email, password) values ($1, $2) returning *",
email,
password
)
.fetch_one(pool)
.await
.map_err(AppError::from)?;
Ok(user)
}
pub async fn get_user_by_email(pool: &PgPool, email: String) -> Result<Option<User>, AppError> {
let user = sqlx::query_as!(User, "select * from users where email=$1", email)
.fetch_optional(pool)
.await
.map_err(AppError::from)?;
Ok(user)
}
pub async fn get_user_by_id(pool: &PgPool, id: Uuid) -> Result<Option<User>, AppError> {
let user = sqlx::query_as!(User, "select * from users where id=$1", id)
.fetch_optional(pool)
.await
.map_err(AppError::from)?;
Ok(user)
}

View File

@ -1,3 +1,8 @@
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use thiserror::Error;
#[derive(Debug, Error)]
@ -13,6 +18,13 @@ pub enum AppError {
#[error("Failed to bind to address")]
Bind(#[from] std::io::Error),
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Validation error: {0}")]
Validation(String),
#[error("Internal server error")]
Internal,
}
#[derive(Debug, Error)]
@ -24,3 +36,21 @@ impl From<AppError> for MainError {
Self(err)
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match &self {
AppError::InvalidCredentials => (StatusCode::UNAUTHORIZED, self.to_string()),
AppError::Validation(_) => (StatusCode::BAD_REQUEST, self.to_string()),
AppError::DbConnect(_) | AppError::Bind(_) | AppError::Internal => (
StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_string(),
),
AppError::Config(_) | AppError::InvalidConfig(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_string(),
),
};
(status, Json(serde_json::json!({ "error": message }))).into_response()
}
}

View File

@ -1,27 +1,20 @@
use axum::{Router, routing::get};
mod config;
mod controller;
mod database;
mod db;
mod errors;
mod logging;
use errors::{AppError, MainError};
mod server;
mod service;
mod state;
use errors::MainError;
#[tokio::main]
async fn main() -> Result<(), MainError> {
let cfg = config::Config::load()?;
let _logging_guard = logging::LoggerConfig::init(cfg.app_env);
let _db = database::init(&cfg.db_url).await?;
let app = Router::new().route("/", get(|| async { "ciao" }));
let listener = tokio::net::TcpListener::bind(&cfg.socket_address)
.await
.map_err(AppError::Bind)?;
tracing::info!("Server started on {}", cfg.socket_address);
axum::serve(listener, app)
.with_graceful_shutdown(logging::shutdown_signal())
.await
.map_err(AppError::Bind)?;
let db = database::init(&cfg.db_url).await?;
server::init(&cfg, db).await?;
Ok(())
}

21
src/server.rs Normal file
View File

@ -0,0 +1,21 @@
use axum::Router;
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 app = Router::new().merge(controller::router()).with_state(state);
let listener = tokio::net::TcpListener::bind(&cfg.socket_address)
.await
.map_err(AppError::Bind)?;
tracing::info!("Server started on {}", cfg.socket_address);
axum::serve(listener, app)
.with_graceful_shutdown(logging::shutdown_signal())
.await
.map_err(AppError::Bind)?;
Ok(())
}

View File

@ -0,0 +1,15 @@
use axum::Json;
use crate::controller::model::auth_model::*;
use crate::errors::AppError;
use crate::state::AppState;
pub async fn login(state: &AppState, req: LoginRequest) -> Result<Json<AuthResponse>, AppError> {
todo!()
}
pub async fn register(
state: &AppState,
req: RegisterRequest,
) -> Result<Json<AuthResponse>, AppError> {
todo!()
}

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

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

6
src/state.rs Normal file
View File

@ -0,0 +1,6 @@
use sqlx::PgPool;
#[derive(Clone)]
pub struct AppState {
pub db: PgPool,
}