Update to use MySQL Database
This commit is contained in:
13
CLAUDE.md
13
CLAUDE.md
@@ -23,6 +23,7 @@ For a single Go package test: `go test ./internal/volunteer/...`
|
||||
## Architecture
|
||||
|
||||
### Go Backend
|
||||
|
||||
Domain-based packaging under `internal/` — each domain owns its models, store (DB queries), and HTTP handler in a single package:
|
||||
|
||||
| Package | Responsibility |
|
||||
@@ -43,23 +44,31 @@ Domain-based packaging under `internal/` — each domain owns its models, store
|
||||
All API routes are prefixed `/api/v1`. The Go binary serves the compiled React app from `STATIC_DIR` (default `./web/dist`) for all non-API routes.
|
||||
|
||||
### React Frontend
|
||||
|
||||
Standard CRA layout in `web/src/`:
|
||||
|
||||
- `api.ts` — typed fetch wrapper; reads JWT from `localStorage`, prefixes `BASE = '/api/v1'`
|
||||
- `auth.tsx` — `AuthProvider` context; decodes the JWT payload to expose `role` and `volunteerID`
|
||||
- `pages/` — one file per route (`Dashboard`, `Schedules`, `TimeOff`, `Volunteers`, `Login`)
|
||||
- `App.tsx` — `BrowserRouter` with a `ProtectedLayout` that redirects to `/login` when no token is present; admin-only nav links gated on `role === 'admin'`
|
||||
|
||||
### Data Storage
|
||||
SQLite via `modernc.org/sqlite` (pure Go, no CGO). Schema is defined inline in `internal/db/schema.go` and applied via `CREATE TABLE IF NOT EXISTS` on every startup. No migration framework — additive changes are safe; destructive changes require manual handling.
|
||||
|
||||
MySQL 8.0 via `github.com/go-sql-driver/mysql`. Schema is defined inline in `internal/db/schema.go` and applied via `CREATE TABLE IF NOT EXISTS` on every startup. No migration framework — additive changes are safe; destructive changes require manual handling.
|
||||
|
||||
### Auth
|
||||
|
||||
HS256 JWT signed with `JWT_SECRET`. Token payload: `{ volunteer_id, role, exp }`. Role is either `volunteer` or `admin`. The `RequireAdmin` middleware enforces admin-only routes server-side.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `DATABASE_DSN` | `walkies.db` | SQLite file path |
|
||||
| `DB_HOST` | `localhost` | MySQL host |
|
||||
| `DB_PORT` | `3306` | MySQL port |
|
||||
| `DB_USER` | `root` | MySQL username |
|
||||
| `DB_PASSWORD` | `` | MySQL password |
|
||||
| `DB_NAME` | `walkies` | MySQL database name |
|
||||
| `JWT_SECRET` | `change-me-in-production` | HMAC signing key |
|
||||
| `STATIC_DIR` | `./web/dist` | Path to compiled React app |
|
||||
| `PORT` | `8080` | HTTP listen port |
|
||||
|
||||
@@ -11,7 +11,14 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
dsn := getenv("DATABASE_DSN", "walkies.db")
|
||||
// Build MySQL DSN from environment variables
|
||||
dbHost := getenv("DB_HOST", "localhost")
|
||||
dbPort := getenv("DB_PORT", "3306")
|
||||
dbUser := getenv("DB_USER", "root")
|
||||
dbPassword := getenv("DB_PASSWORD", "")
|
||||
dbName := getenv("DB_NAME", "walkies")
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", dbUser, dbPassword, dbHost, dbPort, dbName)
|
||||
|
||||
jwtSecret := getenv("JWT_SECRET", "change-me-in-production")
|
||||
staticDir := getenv("STATIC_DIR", "./web/dist")
|
||||
port := getenv("PORT", "8080")
|
||||
|
||||
@@ -1,15 +1,37 @@
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: walkies-mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: walkies
|
||||
MYSQL_USER: walkies
|
||||
MYSQL_PASSWORD: walkies-password
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- walkies-mysql-data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
||||
walkies:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- walkies-data:/app/data
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_DSN: /app/data/walkies.db
|
||||
DB_HOST: mysql
|
||||
DB_PORT: "3306"
|
||||
DB_USER: walkies
|
||||
DB_PASSWORD: walkies-password
|
||||
DB_NAME: walkies
|
||||
JWT_SECRET: change-me-in-production
|
||||
PORT: "8080"
|
||||
STATIC_DIR: /app/web/dist
|
||||
|
||||
volumes:
|
||||
walkies-data:
|
||||
walkies-mysql-data:
|
||||
|
||||
18
go.mod
18
go.mod
@@ -3,18 +3,8 @@ module git.unsupervised.ca/walkies
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.46.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
golang.org/x/crypto v0.48.0
|
||||
)
|
||||
|
||||
@@ -4,20 +4,17 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func Open(dsn string) (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite", dsn)
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open db: %w", err)
|
||||
}
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("ping db: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;`); err != nil {
|
||||
return nil, fmt.Errorf("pragma: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,54 +2,67 @@ package db
|
||||
|
||||
const schema = `
|
||||
CREATE TABLE IF NOT EXISTS volunteers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'volunteer', -- 'admin' | 'volunteer'
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'volunteer' COMMENT 'admin or volunteer',
|
||||
active TINYINT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schedules (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
volunteer_id INTEGER NOT NULL REFERENCES volunteers(id),
|
||||
title TEXT NOT NULL,
|
||||
starts_at TEXT NOT NULL,
|
||||
ends_at TEXT NOT NULL,
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
volunteer_id INT NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
starts_at DATETIME NOT NULL,
|
||||
ends_at DATETIME NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (volunteer_id) REFERENCES volunteers(id) ON DELETE CASCADE,
|
||||
INDEX idx_volunteer_id (volunteer_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS time_off_requests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
volunteer_id INTEGER NOT NULL REFERENCES volunteers(id),
|
||||
starts_at TEXT NOT NULL,
|
||||
ends_at TEXT NOT NULL,
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
volunteer_id INT NOT NULL,
|
||||
starts_at DATETIME NOT NULL,
|
||||
ends_at DATETIME NOT NULL,
|
||||
reason TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending', -- 'pending' | 'approved' | 'rejected'
|
||||
reviewed_by INTEGER REFERENCES volunteers(id),
|
||||
reviewed_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'pending' COMMENT 'pending, approved, rejected',
|
||||
reviewed_by INT,
|
||||
reviewed_at DATETIME,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (volunteer_id) REFERENCES volunteers(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (reviewed_by) REFERENCES volunteers(id) ON DELETE SET NULL,
|
||||
INDEX idx_volunteer_id (volunteer_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS checkins (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
volunteer_id INTEGER NOT NULL REFERENCES volunteers(id),
|
||||
schedule_id INTEGER REFERENCES schedules(id),
|
||||
checked_in_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
checked_out_at TEXT,
|
||||
notes TEXT
|
||||
);
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
volunteer_id INT NOT NULL,
|
||||
schedule_id INT,
|
||||
checked_in_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
checked_out_at TIMESTAMP NULL,
|
||||
notes TEXT,
|
||||
FOREIGN KEY (volunteer_id) REFERENCES volunteers(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (schedule_id) REFERENCES schedules(id) ON DELETE SET NULL,
|
||||
INDEX idx_volunteer_id (volunteer_id),
|
||||
INDEX idx_schedule_id (schedule_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
volunteer_id INTEGER NOT NULL REFERENCES volunteers(id),
|
||||
message TEXT NOT NULL,
|
||||
read INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
volunteer_id INT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
read TINYINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (volunteer_id) REFERENCES volunteers(id) ON DELETE CASCADE,
|
||||
INDEX idx_volunteer_id (volunteer_id),
|
||||
INDEX idx_read (read)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user