docs: add backend onboarding guide

This commit is contained in:
TerryM
2026-05-18 07:56:27 +08:00
parent 141d92dc15
commit 00540fbb73
6 changed files with 1067 additions and 0 deletions

360
docs/API.md Normal file
View File

@@ -0,0 +1,360 @@
# API Reference
Base URL in local development: `http://localhost:8080`.
Frontend usually calls paths like `/api/resources`; uploaded files are available under `/uploads/...`.
## Response style
Most responses are JSON. Errors use plain text via `http.Error`, for example `unauthorized` or `bad json`.
## Health
### `GET /healthz`
Returns plain text:
```text
ok
```
### `GET /health`
Same as `/healthz`.
## Public categories/resources
### `GET /api/categories`
Query/header language behavior:
- `?lang=zh-CN` returns Simplified Chinese name when available.
- `?lang=en` returns English name when available.
- otherwise Traditional Chinese is preferred.
Response:
```json
[
{
"id": 1,
"slug": "project-ppt",
"name": "項目資料PPT",
"description": "ARK 項目介紹、簡報與對外展示資料",
"iconKey": "folder",
"sortOrder": 1,
"updatedAt": "2026-01-01T00:00:00Z"
}
]
```
### `GET /api/resources`
Returns paginated public resources.
Query params:
| Param | Meaning |
| --- | --- |
| `page` | page number, default 1 |
| `limit` | page size, default 20, max 100 |
| `q` | search title/description/tag name |
| `type` | resource type; `all` means no filter |
| `language` | exact resource language filter |
| `category` | category slug |
| `tag` | tag slug or tag name, case-insensitive |
| `sort` | `latest` default, `published`, `recommended`, `popular` |
| `lang` | category display language, e.g. `en`, `zh-CN` |
Response:
```json
{
"items": [
{
"id": "uuid",
"title": "ARK 項目介紹簡報(示例)",
"description": "適合線下宣講與新人培訓。",
"type": "ppt",
"language": "zh-TW",
"categoryId": 1,
"categorySlug": "project-ppt",
"categoryName": "項目資料PPT",
"coverImage": "/uploads/placeholder-cover.svg",
"fileUrl": "/uploads/placeholder-cover.svg",
"isDownloadable": true,
"isRecommended": true,
"publishedAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z",
"tags": ["官方推薦"]
}
],
"page": 1,
"limit": 20,
"total": 1
}
```
### `GET /api/resources/recommended?limit=12`
Returns recommended resources:
```json
{ "items": [] }
```
Max limit is 50.
### `GET /api/resources/latest?limit=12`
Returns latest resources:
```json
{ "items": [] }
```
### `GET /api/resources/popular?limit=12`
Returns resources ordered by download + favorite + share count:
```json
{ "items": [] }
```
### `GET /api/resources/{id}`
Returns one published public resource. `{id}` must be a UUID.
### `GET /api/resources/{id}/related`
Returns up to 8 resources from the same category:
```json
{ "items": [] }
```
## Public activity endpoints
### `POST /api/search-log`
Request:
```json
{ "query": "ark" }
```
Response:
```json
{ "ok": true }
```
### `POST /api/resources/{id}/view`
Increments view count.
Response:
```json
{ "ok": true }
```
### `POST /api/resources/{id}/download`
Increments download count.
### `POST /api/resources/{id}/share`
Increments share count.
### `POST /api/resources/{id}/favorite`
Request:
```json
{ "add": true }
```
- `add: true` increments favorite count.
- `add: false` decrements favorite count but not below zero.
## Wallet auth
Wallet auth is for normal users, not admin users.
### `POST /api/auth/wallet/nonce`
Request:
```json
{ "address": "0x0000000000000000000000000000000000000000" }
```
Response:
```json
{
"nonce": "hex-code",
"message": "ARK Database — wallet sign-in\n\nWallet: 0x...\nOne-time code: ..."
}
```
The frontend asks the wallet to sign `message`.
### `POST /api/auth/wallet/verify`
Request:
```json
{
"address": "0x0000000000000000000000000000000000000000",
"message": "same message from nonce endpoint",
"signature": "0x..."
}
```
Response:
```json
{
"token": "jwt",
"wallet": "0xChecksumAddress"
}
```
### `GET /api/auth/wallet/me`
Requires header:
```http
Authorization: Bearer <wallet-jwt>
```
Response:
```json
{ "wallet": "0xChecksumAddress", "role": "user" }
```
## Admin auth
### `POST /api/admin/login`
Request:
```json
{ "email": "admin@ark.local", "password": "admin123" }
```
Response:
```json
{ "token": "admin-jwt" }
```
Use this token for admin endpoints:
```http
Authorization: Bearer <admin-jwt>
```
## Admin endpoints
All endpoints below require admin JWT.
### `GET /api/admin/dashboard`
Response includes counts and hot resources:
```json
{
"totalResources": 10,
"published": 8,
"todayNew": 1,
"totalViews": 100,
"totalDownloads": 20,
"totalFavorites": 5,
"totalShares": 3,
"hotResources": []
}
```
### `GET /api/admin/search-logs?limit=200`
Max limit is 500.
Response:
```json
{ "items": [{ "id": 1, "query": "ark", "createdAt": "2026-01-01T00:00:00Z" }] }
```
### `GET /api/admin/resources?page=1&limit=20`
Lists all resources, including drafts/private resources.
### `GET /api/admin/resources/{id}`
Gets one resource by UUID.
### `POST /api/admin/resources`
Creates a resource.
Example request body:
```json
{
"title": "Example",
"description": "Short text",
"type": "ppt",
"language": "zh-TW",
"categoryId": 1,
"coverImage": "/uploads/example.svg",
"fileUrl": "/uploads/example.pdf",
"previewUrl": "",
"externalUrl": "",
"bodyText": "",
"badgeLabel": "新人必看",
"isPublic": true,
"isDownloadable": true,
"isRecommended": false,
"sortOrder": 0,
"status": "draft",
"tags": ["教程"]
}
```
### `PUT /api/admin/resources/{id}`
Updates a resource. Body shape is the same as create.
### `DELETE /api/admin/resources/{id}`
Deletes a resource.
Response:
```json
{ "ok": true }
```
### `POST /api/admin/upload`
Multipart upload with field name `file`.
Response for local storage:
```json
{ "url": "/uploads/generated-name.ext", "filename": "generated-name.ext", "storage": "local" }
```
Response for S3:
```json
{ "url": "https://...", "filename": "generated-name.ext", "storage": "s3" }
```
### `GET /api/admin/categories`
Same handler as public categories; returns visible categories.

180
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,180 @@
# 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.

281
docs/GO_FOR_BEGINNERS.md Normal file
View File

@@ -0,0 +1,281 @@
# Go Beginner Guide for This Backend
This is not a full Go course. It explains only the Go ideas you need to read this project.
## 1. How a Go project starts
This repo has a `go.mod` file:
```go
module github.com/arkie/ark-database
```
That module name is used in imports:
```go
import "github.com/arkie/ark-database/internal/config"
```
The executable starts at:
```text
cmd/server/main.go
```
In Go, a runnable program has:
```go
package main
func main() {
// program starts here
}
```
## 2. Packages
Every `.go` file begins with a package name:
```go
package handlers
```
Files in the same folder usually share the same package and can call each other directly.
Example: files in `internal/handlers/` all say `package handlers`, so `public.go` can call `writeJSON` from `util.go`.
## 3. Imports
Go imports are explicit:
```go
import (
"net/http"
"github.com/go-chi/chi/v5"
)
```
If an import is unused, Go compilation fails. Run `gofmt -w .` after edits.
## 4. Structs
A struct is an object/data shape.
Example from `internal/config/config.go`:
```go
type Config struct {
Addr string
DatabaseURL string
JWTSecret string
}
```
JSON field names are controlled by tags:
```go
type ResourceDTO struct {
CategoryID int `json:"categoryId"`
}
```
The Go field is `CategoryID`, but JSON output is `categoryId`.
## 5. Functions with errors
Go commonly returns `(value, error)`:
```go
pool, err := db.Connect(ctx, cfg.DatabaseURL)
if err != nil {
log.Fatal(err)
}
```
Read this as:
1. Try connecting to DB.
2. If `err` is not nil, stop.
3. Otherwise use `pool`.
## 6. Short variable declaration `:=`
This creates a new variable:
```go
cfg := config.Load()
```
This assigns to an existing variable:
```go
cfg = config.Load()
```
Most code here uses `:=` inside functions.
## 7. HTTP handlers
A Go HTTP handler usually looks like:
```go
func ListCategories(w http.ResponseWriter, r *http.Request) {
// w writes response
// r reads request
}
```
- `w` = response writer.
- `r` = request.
Route registration in `main.go`:
```go
r.Get("/categories", handlers.ListCategories)
```
Because this is inside `r.Route("/api", ...)`, the full URL is:
```text
GET /api/categories
```
## 8. Middleware
Middleware wraps a request before it reaches the final handler.
Example in `main.go`:
```go
r.Use(middleware.Logger)
```
This logs requests.
Admin auth is also middleware:
```go
r.Use(handlers.AdminAuth(cfg.JWTSecret))
```
If token is bad, it returns `401 unauthorized` before reaching the admin handler.
## 9. Context
Context carries request-scoped values and cancellation.
This project stores the DB pool in request context:
```go
r.Use(func(next http.Handler) http.Handler {
return handlers.WithPool(next, pool)
})
```
Handlers retrieve it:
```go
pool := poolFrom(r)
```
So most handlers do not receive the DB pool as a direct function parameter.
## 10. Database queries
This project uses pgx.
One row:
```go
err := pool.QueryRow(ctx, `SELECT id FROM admins WHERE email = $1`, email).Scan(&id)
```
Many rows:
```go
rows, err := pool.Query(ctx, `SELECT id, title FROM resources`)
defer rows.Close()
for rows.Next() {
rows.Scan(&id, &title)
}
```
PostgreSQL parameters use `$1`, `$2`, etc. This protects from SQL injection when used correctly.
## 11. Pointers and nil
You will see `*string` and `*time.Time`:
```go
var pubAt *time.Time
```
This allows SQL `NULL` to become Go `nil`.
Code checks before using it:
```go
if pubAt != nil {
s := pubAt.UTC().Format(time.RFC3339)
dto.PublishedAt = &s
}
```
## 12. `defer`
`defer` runs later when the current function returns.
Examples:
```go
defer pool.Close()
defer rows.Close()
defer r.Body.Close()
```
Use it for cleanup.
## 13. JSON helpers in this project
Write JSON response:
```go
writeJSON(w, map[string]any{"ok": true})
```
Read JSON request body:
```go
var req loginReq
if err := jsonDecode(r, &req); err != nil {
http.Error(w, "bad json", http.StatusBadRequest)
return
}
```
## 14. Common edit workflow
After changing `.go` files:
```bash
gofmt -w .
go test ./...
```
`go test ./...` also compiles all packages, even if there are no test files.
## 15. Recommended reading order
1. `README.md`
2. `cmd/server/main.go`
3. `internal/config/config.go`
4. `internal/handlers/public.go`
5. `internal/handlers/admin.go`
6. `internal/handlers/wallet_auth.go`
7. `migrations/001_init.sql`
When you see a function you do not understand, search for its definition:
```bash
rg "func FunctionName" .
```