181 lines
4.7 KiB
Markdown
181 lines
4.7 KiB
Markdown
# Backend Architecture
|
|
|
|
This document explains the backend in beginner-friendly language.
|
|
|
|
## Big picture
|
|
|
|
```text
|
|
Browser / Frontend
|
|
|
|
|
| HTTP JSON requests
|
|
v
|
|
cmd/server/main.go
|
|
|
|
|
+-- config.Load() reads environment variables
|
|
+-- db.Connect() creates PostgreSQL pool
|
|
+-- chi router maps URLs to handler functions
|
|
+-- handlers read/write DB and return JSON
|
|
|
|
|
v
|
|
PostgreSQL + local uploads or S3
|
|
```
|
|
|
|
The backend is one Go HTTP server. There is no framework magic: `main.go` wires everything manually.
|
|
|
|
## Request flow example
|
|
|
|
Example: frontend calls `GET /api/resources?limit=20`.
|
|
|
|
1. `main.go` has this route:
|
|
```go
|
|
r.Get("/resources", handlers.ListResources)
|
|
```
|
|
2. The `/api` router has middleware:
|
|
```go
|
|
r.Use(func(next http.Handler) http.Handler {
|
|
return handlers.WithPool(next, pool)
|
|
})
|
|
```
|
|
This places the PostgreSQL pool into the request context.
|
|
3. `handlers.ListResources` calls `poolFrom(r)` to get the DB pool.
|
|
4. It reads query string filters (`q`, `type`, `language`, `category`, `sort`, `tag`).
|
|
5. It builds SQL, queries PostgreSQL, scans rows into `ResourceDTO`.
|
|
6. It returns JSON using `writeJSON`.
|
|
|
|
## Why `internal/`?
|
|
|
|
In Go, a folder named `internal` can only be imported by code inside the same module tree. This prevents other modules from depending on private implementation packages.
|
|
|
|
This project uses:
|
|
|
|
```text
|
|
internal/config private config package
|
|
internal/db private DB package
|
|
internal/auth private JWT package
|
|
internal/handlers private HTTP handler package
|
|
internal/seed private seed package
|
|
```
|
|
|
|
## Main packages
|
|
|
|
### `cmd/server`
|
|
|
|
`cmd/server/main.go` is the executable entry point.
|
|
|
|
Responsibilities:
|
|
|
|
- Load environment config.
|
|
- Connect to PostgreSQL.
|
|
- Ensure wallet nonce table when enabled.
|
|
- Create upload directory and placeholder file.
|
|
- Optionally seed the first admin.
|
|
- Configure optional S3 upload client.
|
|
- Register all HTTP middleware and routes.
|
|
- Start `http.ListenAndServe`.
|
|
|
|
### `internal/config`
|
|
|
|
`config.Load()` reads env vars and fills defaults. It returns a `Config` struct.
|
|
|
|
Important pattern:
|
|
|
|
```go
|
|
addr := os.Getenv("HTTP_ADDR")
|
|
if addr == "" {
|
|
addr = ":8080"
|
|
}
|
|
```
|
|
|
|
This means env vars are optional in local development.
|
|
|
|
### `internal/db`
|
|
|
|
`db.Connect(ctx, databaseURL)` parses `DATABASE_URL`, creates a pgx connection pool, then pings the DB.
|
|
|
|
The app shares this pool for all requests.
|
|
|
|
### `internal/auth`
|
|
|
|
Two JWT token types exist:
|
|
|
|
- Admin token: issuer `ark-admin`, includes `admin_id` and email.
|
|
- Wallet user token: issuer `ark-user`, includes wallet address and role `user`.
|
|
|
|
Do not mix these. Admin middleware only accepts admin tokens.
|
|
|
|
### `internal/handlers`
|
|
|
|
Handlers are grouped by topic:
|
|
|
|
- `public.go`: public category/resource list/detail, search logs, counters.
|
|
- `admin.go`: admin login/dashboard/resource CRUD/tag replacement.
|
|
- `wallet_auth.go`: wallet nonce and signature verification.
|
|
- `upload.go`: admin file upload.
|
|
- `middleware.go`: admin auth middleware.
|
|
- `context.go`: stores DB pool in request context.
|
|
- `util.go`: shared JSON helpers.
|
|
|
|
## Database model summary
|
|
|
|
Main tables from `migrations/001_init.sql`:
|
|
|
|
- `admins`: admin email + bcrypt password hash.
|
|
- `categories`: resource categories with multilingual names.
|
|
- `resources`: library items; can be ppt/image/video/link/etc.
|
|
- `tags`: reusable tag names.
|
|
- `resource_tags`: many-to-many join table.
|
|
- `search_logs`: saved public search terms.
|
|
|
|
Wallet auth table from migration/startup DDL:
|
|
|
|
- `wallet_auth_nonces`: one temporary nonce per wallet address.
|
|
|
|
## Route groups
|
|
|
|
### Public routes
|
|
|
|
These do not require auth:
|
|
|
|
- health: `/healthz`, `/health`
|
|
- categories/resources under `/api`
|
|
- wallet auth nonce/verify/me (me requires wallet JWT)
|
|
- resource view/download/share/favorite counters
|
|
|
|
### Admin routes
|
|
|
|
`POST /api/admin/login` is public because it returns the admin token.
|
|
|
|
Everything in this group requires admin JWT:
|
|
|
|
- dashboard
|
|
- search logs
|
|
- resource list/get/create/update/delete
|
|
- upload
|
|
- admin categories list
|
|
|
|
## Upload behavior
|
|
|
|
Admin upload endpoint: `POST /api/admin/upload` with multipart field `file`.
|
|
|
|
If S3 is configured:
|
|
|
|
```text
|
|
S3_BUCKET non-empty + AWS config loads => upload to S3
|
|
```
|
|
|
|
Otherwise:
|
|
|
|
```text
|
|
save file to UPLOAD_DIR and return /uploads/<filename>
|
|
```
|
|
|
|
The max upload size is 512 MiB (`uploadMaxBytes`).
|
|
|
|
## Things to be careful with
|
|
|
|
- `migrations/001_init.sql` is not safe to run repeatedly on the same DB.
|
|
- `JWT_SECRET` default is for dev only.
|
|
- `RUN_WALLET_AUTH_SCHEMA=true` runs DDL at startup. Use false if the runtime DB user should not create tables.
|
|
- The app does not auto-load `.env`.
|
|
- Public resources are filtered to published + public; admin list sees all resources.
|