initial db interactions
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 9m41s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 9m41s
This commit is contained in:
parent
f5fc85cb00
commit
e4c582dabe
46
.sqlx/query-25b92f656255a863ed7649125d60bd1e309bd64204925cf73f245c8ce63b27b7.json
generated
Normal file
46
.sqlx/query-25b92f656255a863ed7649125d60bd1e309bd64204925cf73f245c8ce63b27b7.json
generated
Normal 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"
|
||||
}
|
||||
47
.sqlx/query-a145c5eb33f466fb3112e6feaf0e1ef56735331624010eda1703f0169271577c.json
generated
Normal file
47
.sqlx/query-a145c5eb33f466fb3112e6feaf0e1ef56735331624010eda1703f0169271577c.json
generated
Normal 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"
|
||||
}
|
||||
46
.sqlx/query-af5f9eb11f896d65c13da7697dc8c18a2399aa845af63214e39040958e3e5ec4.json
generated
Normal file
46
.sqlx/query-af5f9eb11f896d65c13da7697dc8c18a2399aa845af63214e39040958e3e5ec4.json
generated
Normal 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"
|
||||
}
|
||||
@ -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
|
||||
│ ├── auth.rs # JWT extraction from Authorization header, inject CurrentUser
|
||||
│ └── 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
|
||||
├── 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
168
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
13
Makefile
Normal 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
|
||||
7
migrations/0001_create_users_table.sql
Normal file
7
migrations/0001_create_users_table.sql
Normal 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()
|
||||
);
|
||||
@ -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
12
src/controller/mod.rs
Normal 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())
|
||||
}
|
||||
18
src/controller/model/auth_model.rs
Normal file
18
src/controller/model/auth_model.rs
Normal 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,
|
||||
}
|
||||
1
src/controller/model/mod.rs
Normal file
1
src/controller/model/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod auth_model;
|
||||
28
src/controller/v1/auth_controller.rs
Normal file
28
src/controller/v1/auth_controller.rs
Normal 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
10
src/controller/v1/mod.rs
Normal 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())
|
||||
}
|
||||
|
||||
@ -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
2
src/db/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod model;
|
||||
pub mod repository;
|
||||
1
src/db/model/mod.rs
Normal file
1
src/db/model/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod user;
|
||||
16
src/db/model/user.rs
Normal file
16
src/db/model/user.rs
Normal 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
1
src/db/repository/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod user_repository;
|
||||
35
src/db/repository/user_repository.rs
Normal file
35
src/db/repository/user_repository.rs
Normal 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)
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
23
src/main.rs
23
src/main.rs
@ -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
21
src/server.rs
Normal 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(())
|
||||
}
|
||||
15
src/service/auth_service.rs
Normal file
15
src/service/auth_service.rs
Normal 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
1
src/service/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod auth_service;
|
||||
6
src/state.rs
Normal file
6
src/state.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use sqlx::PgPool;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub db: PgPool,
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user