REST backend for a finance dashboard: users and roles, financial records (CRUD + filters), aggregated dashboard data, and JWT-based access control.
- Python 3.12+
- FastAPI
- SQLAlchemy 2.x
- SQLite (file
finance.dbby default; configurable viaDATABASE_URL)
cd zoryn
python -m venv .venv
.\.venv\Scripts\pip install -r requirements.txt
.\.venv\Scripts\uvicorn app.main:app --reload --host 0.0.0.0 --port 8000- Interactive docs: http://127.0.0.1:8000/docs
- Health: http://127.0.0.1:8000/health
On first startup, if no users exist, an admin user is created from environment variables (see below).
Optional .env (or real environment variables):
| Variable | Default | Description |
|---|---|---|
SECRET_KEY |
(dev placeholder) | JWT signing secret; change in any shared or production use |
ACCESS_TOKEN_EXPIRE_MINUTES |
1440 |
Bearer token lifetime |
DATABASE_URL |
sqlite:///./finance.db |
SQLAlchemy URL |
SEED_ADMIN_EMAIL |
admin@example.com |
First-run admin email |
SEED_ADMIN_PASSWORD |
ChangeMe123! |
First-run admin password |
| Role | Capabilities |
|---|---|
| viewer | Read dashboard summaries and trends only (/api/v1/dashboard/*). Cannot list or mutate records. |
| analyst | Everything viewer can do, plus read financial records (GET /api/v1/records). Cannot create, update, or delete records. |
| admin | Full access: user management, record CRUD (create/update/soft delete), and optional include_deleted=true on record listing. |
Inactive users cannot authenticate or call protected routes.
POST /api/v1/auth/loginwith JSON body:{ "email", "password" }- Use returned
access_tokenas header:Authorization: Bearer <token> GET /api/v1/auth/mereturns the current user.
POST /auth/login— obtain JWTGET /auth/me— current profile
POST /users— create user (email,password,role)GET /users— list usersGET /users/{id}— get userPATCH /users/{id}— updaterole,is_active, optionalpassword(admin cannot deactivate self)
POST /records— create (admin)GET /records— list with pagination and filters:skip,limit,type(income|expense),category(substring, case-insensitive),date_from,date_to. Admins may passinclude_deleted=true.GET /records/{id}— detail (analyst, admin)PATCH /records/{id}— update (admin)DELETE /records/{id}— soft delete (admin)
GET /dashboard/summary— totals, net balance, count, category breakdown; optionaldate_from,date_toGET /dashboard/recent— recent non-deleted records (limit1–100)GET /dashboard/trends/monthly—months(1–36), SQLitestrftimebucketsGET /dashboard/trends/weekly—weeks(1–52), weeks start Monday
Aggregations exclude soft-deleted records.
- Single-tenant: all records belong to one logical organization; summaries are global, not per-user.
- Audit:
created_by_idstores the admin who created the record (nullable for legacy rows). - Soft delete:
is_deletedhides records from default queries and dashboard math; admins can still list them withinclude_deleted=true.
- Request bodies are validated with Pydantic; failures return 422 with
detail(field errors) andmessage. - Auth failures: 401; forbidden role: 403; not found: 404; duplicate email: 409; bad business state: 400 with a clear
detailstring.
.\.venv\Scripts\python -m pytest tests/ -vTests use an isolated SQLite file under tests/pytest_finance.db (removed when conftest runs).
- SQLite and JWT in settings are chosen for a small, reviewable assignment; swap
DATABASE_URLand secrets for a real deployment. - Monthly/weekly trend SQL uses SQLite date functions; another database would need dialect-specific grouping.
- Password hashing uses
bcryptdirectly (no passlib) for compatibility with currentbcryptreleases.