update obsidian
This commit is contained in:
Vendored
+8
-12
@@ -4,21 +4,17 @@
|
|||||||
"type": "split",
|
"type": "split",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "83169ae72c9bc357",
|
"id": "af94f814e06296f9",
|
||||||
"type": "tabs",
|
"type": "tabs",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "6f192f9477d55538",
|
"id": "7ac1561c78f61154",
|
||||||
"type": "leaf",
|
"type": "leaf",
|
||||||
"state": {
|
"state": {
|
||||||
"type": "markdown",
|
"type": "empty",
|
||||||
"state": {
|
"state": {},
|
||||||
"file": "Crussell/Crussell Nails.md",
|
|
||||||
"mode": "source",
|
|
||||||
"source": false
|
|
||||||
},
|
|
||||||
"icon": "lucide-file",
|
"icon": "lucide-file",
|
||||||
"title": "Crussell Nails"
|
"title": "New tab"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -169,9 +165,9 @@
|
|||||||
"bases:Create new base": false
|
"bases:Create new base": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"active": "6f192f9477d55538",
|
"active": "7ac1561c78f61154",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
"Crussell/Backend/bookings.md",
|
"Crussell/Crussell Nails.md",
|
||||||
"Crussell/Crussell Nails.md"
|
"Crussell/Backend/bookings.md"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+117
-113
@@ -1,86 +1,86 @@
|
|||||||
|
|
||||||
## ✅ / ⏳ Project Checklist
|
## ✅ / ⏳ Project Checklist
|
||||||
|
|
||||||
### Backend (Go / Chi / Postgres)
|
### Backend (Go / Chi / Postgres)
|
||||||
|
|
||||||
- [x] JWT authentication (login, refresh, role verification)
|
- [x] JWT authentication (login, refresh, role verification)
|
||||||
- [x] User registration with input validation
|
- [x] User registration with input validation
|
||||||
- [x] Password hashing with bcrypt
|
- [x] Password hashing with bcrypt
|
||||||
- [x] Middleware for auth/roles
|
- [x] Middleware for auth/roles
|
||||||
- [x] DB connection pooling
|
- [x] DB connection pooling
|
||||||
- [ ] Booking endpoints
|
- [ ] Booking endpoints
|
||||||
|
- ☐ `/api/bookings` (CRUD for users)
|
||||||
|
- ☐ `/api/admin/bookings` (list, search, user‑by‑user, progress, confirm)
|
||||||
- [ ] Admin endpoints
|
- [ ] Admin endpoints
|
||||||
|
- ☐ `/api/admin/services` (create, delete, list, toggle)
|
||||||
|
- ☐ `/api/admin/bookings` (list, search, user bookings, progress, confirm)
|
||||||
|
- ☐ `/api/admin/users` (list, view) – *handlers still to be implemented*
|
||||||
- [ ] Unit tests
|
- [ ] Unit tests
|
||||||
- [ ] CI/CD
|
- [ ] CI/CD
|
||||||
|
|
||||||
### Frontend (SvelteKit / Tailwind / shadcn)
|
### Frontend (SvelteKit / Tailwind / shadcn)
|
||||||
|
|
||||||
- [x] Core pages (Home, Prices, Contact, Book)
|
- [x] Core pages (Home, Prices, Contact, Book)
|
||||||
- [x] Booking wizard prototype
|
- [x] Booking wizard prototype
|
||||||
- [x] Calendar/time slot selector
|
- [x] Calendar/time slot selector
|
||||||
- [ ] API integration for booking flow
|
- [ ] API integration for booking flow
|
||||||
- [ ] Payments (Stripe/Square)
|
*The wizard currently only shows a prototype alert. Wire it to the backend `/api/bookings` POST endpoint in the next iteration.*
|
||||||
|
- [ ] Payments (Stripe/Square) – *pending integration*
|
||||||
- [ ] Auth screens
|
- [ ] Auth screens
|
||||||
- [ ] Admin dashboard
|
- [ ] Admin dashboard
|
||||||
|
|
||||||
### Infrastructure
|
### Infrastructure
|
||||||
|
|
||||||
- [x] `.env` config
|
- [x] `.env` config
|
||||||
- [x] Docker Compose full stack
|
- [x] Docker Compose full stack
|
||||||
- [ ] nginx reverse proxy
|
- [ ] nginx reverse proxy – *still under review*
|
||||||
- [ ] Monitoring/logging
|
- [ ] Monitoring/logging – *no stack configured*
|
||||||
- [ ] CI/CD pipeline
|
- [ ] CI/CD pipeline – *not yet defined*
|
||||||
|
|
||||||
### Integrations
|
### Integrations
|
||||||
|
|
||||||
- [x] CardDAV sync for contacts
|
- [x] CardDAV sync for contacts
|
||||||
- [ ] Email/SMS reminders
|
- [ ] Email/SMS reminders – *not yet implemented*
|
||||||
- [ ] Payment provider
|
- [ ] Payment provider – *placeholder only*
|
||||||
- [ ] Loyalty tracking frontend
|
- [ ] Loyalty tracking frontend – *no component yet*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📐 Current Architecture
|
## 📐 Current Architecture
|
||||||
|
|
||||||
### Overview Diagram
|
### Overview Diagram
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
User([Customer])
|
User([Customer])
|
||||||
Admin([Admin])
|
Admin([Admin])
|
||||||
CardReader[Square Card Reader<br/>Physical Terminal]
|
CardReader[Square Card Reader<br/>Physical Terminal]
|
||||||
|
|
||||||
subgraph CF[Cloudflare Protection]
|
subgraph CF[Cloudflare Protection]
|
||||||
CDN[CDN / DDoS Protection]
|
CDN[CDN / DDoS Protection]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Lightsail[AWS Lightsail Instance]
|
subgraph Lightsail[AWS Lightsail Instance]
|
||||||
subgraph NGINX[Nginx Reverse Proxy]
|
subgraph NGINX[Nginx Reverse Proxy]
|
||||||
Proxy[Port 443/80]
|
Proxy[Port 443/80]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Frontend[SvelteKit Frontend]
|
subgraph Frontend[SvelteKit Frontend]
|
||||||
UI[Booking UI / Prices / Contact]
|
UI[Booking UI / Prices / Contact]
|
||||||
AdminUI[Admin Dashboard]
|
AdminUI[Admin Dashboard]
|
||||||
APIProxy[API Proxy Routes]
|
APIProxy[API Proxy Routes]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Backend[Go + Chi Backend]
|
subgraph Backend[Go + Chi Backend]
|
||||||
Router[chi Router]
|
Router[chi Router]
|
||||||
Auth[JWT Middleware]
|
Auth[JWT Middleware]
|
||||||
Handlers[API Handlers]
|
Handlers[API Handlers]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Database[PostgreSQL]
|
subgraph Database[PostgreSQL]
|
||||||
DB[(Users / Bookings<br/>Transactions)]
|
DB[(Users / Bookings<br/>Transactions)]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph DAV[SabreDAV Server]
|
subgraph DAV[SabreDAV Server]
|
||||||
CardDAV[(vCard Contacts)]
|
CardDAV[(vCard Contacts)]
|
||||||
CalDAV[(Calendar Events)]
|
CalDAV[(Calendar Events)]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph External[External Services]
|
subgraph External[External Services]
|
||||||
Gmail[Gmail SMTP<br/>smtp.gmail.com:587]
|
Gmail[Gmail SMTP<br/>smtp.gmail.com:587]
|
||||||
SquareAPI[Square API<br/>Online Payments]
|
SquareAPI[Square API<br/>Online Payments]
|
||||||
@@ -90,94 +90,46 @@ flowchart TD
|
|||||||
User -->|HTTPS| CF
|
User -->|HTTPS| CF
|
||||||
Admin -->|HTTPS| CF
|
Admin -->|HTTPS| CF
|
||||||
CF --> Proxy
|
CF --> Proxy
|
||||||
|
|
||||||
Proxy --> UI
|
Proxy --> UI
|
||||||
Proxy --> AdminUI
|
Proxy --> AdminUI
|
||||||
Proxy --> APIProxy
|
Proxy --> APIProxy
|
||||||
|
|
||||||
UI --> APIProxy
|
UI --> APIProxy
|
||||||
AdminUI --> APIProxy
|
AdminUI --> APIProxy
|
||||||
APIProxy --> Router
|
APIProxy --> Router
|
||||||
|
|
||||||
Router --> Auth
|
Router --> Auth
|
||||||
Auth --> Handlers
|
Auth --> Handlers
|
||||||
|
|
||||||
Handlers --> DB
|
Handlers --> DB
|
||||||
Handlers --> CardDAV
|
Handlers --> CardDAV
|
||||||
Handlers --> CalDAV
|
Handlers --> CalDAV
|
||||||
Handlers -->|Send Emails| Gmail
|
Handlers -->|Send Emails| Gmail
|
||||||
Handlers -->|Process Payments| SquareAPI
|
Handlers -->|Process Payments| SquareAPI
|
||||||
|
|
||||||
Admin -.->|Sync Contacts/Calendar| DAV
|
Admin -.->|Sync Contacts/Calendar| DAV
|
||||||
|
|
||||||
CardReader -->|Transaction Data| SquareAPI
|
CardReader -->|Transaction Data| SquareAPI
|
||||||
Handlers -.->|Poll Transactions| SquareAPI
|
Handlers -.->|Poll Transactions| SquareAPI
|
||||||
|
|
||||||
%% Force External Services to appear below
|
%% Force External Services to appear below
|
||||||
Lightsail --> External
|
Lightsail --> External
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 Backend Implementation
|
## 🔧 Backend Implementation
|
||||||
|
|
||||||
### JWT Auth
|
### JWT Auth
|
||||||
|
JWT uses **HS256** algorithm with 30‑day expiry. Implemented via `go-chi/jwtauth`.
|
||||||
JWT uses **HS256** algorithm with 30-day expiry. Implemented via `go-chi/jwtauth`.
|
|
||||||
|
|
||||||
**Code:**
|
|
||||||
|
|
||||||
```go
|
|
||||||
// auth/jwt.go
|
|
||||||
var TokenAuth *jwtauth.JWTAuth
|
|
||||||
|
|
||||||
func InitJWT(secret string) {
|
|
||||||
TokenAuth = jwtauth.New("HS256", []byte(secret), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateToken(userID string, role string) (string, error) {
|
|
||||||
_, token, err := TokenAuth.Encode(map[string]interface{}{
|
|
||||||
"user_id": userID,
|
|
||||||
"role": role,
|
|
||||||
"exp": time.Now().Add(30 * 24 * time.Hour).Unix(),
|
|
||||||
})
|
|
||||||
return token, err
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Middleware
|
### Middleware
|
||||||
|
|
||||||
Validates JWT and attaches user context. Supports role restrictions.
|
Validates JWT and attaches user context. Supports role restrictions.
|
||||||
|
|
||||||
```go
|
|
||||||
// mw/auth.go
|
|
||||||
func RequireAuth(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
authHeader := r.Header.Get("Authorization")
|
|
||||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
|
||||||
http.Error(w, "missing token", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
|
||||||
userID, role, err := auth.VerifyToken(token, r.Context())
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "invalid token", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.WithValue(r.Context(), UserIDKey, userID)
|
|
||||||
ctx = context.WithValue(ctx, UserRoleKey, role)
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Registration Flow
|
### Registration Flow
|
||||||
|
|
||||||
The registration process:
|
The registration process:
|
||||||
|
|
||||||
- Validates names, email, phone, DOB
|
- Validates names, email, phone, DOB
|
||||||
- Rejects users under 16 years old
|
- Rejects users under 16 years old
|
||||||
- Hashes password with bcrypt
|
- Hashes password with bcrypt
|
||||||
@@ -191,30 +143,38 @@ defer tx.Rollback(r.Context())
|
|||||||
_, err := tx.Exec(r.Context(),
|
_, err := tx.Exec(r.Context(),
|
||||||
`INSERT INTO users (first_name, last_name, email, phone, dob, password_hash)
|
`INSERT INTO users (first_name, last_name, email, phone, dob, password_hash)
|
||||||
VALUES ($1,$2,$3,$4,$5,$6)`,
|
VALUES ($1,$2,$3,$4,$5,$6)`,
|
||||||
req.FirstName, req.LastName, req.Email, req.Phone, dob, string(hash))
|
req.FirstName, req.LastName, req.Email, req.Phone, dob, string(hash))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### CardDAV Integration
|
### CardDAV Integration
|
||||||
|
Each registered user generates a `.vcf` file in SabreDAV, ensuring external calendar and contact apps stay in sync. The code now uses the internal `dav.BaseService.CreateContact` helper.
|
||||||
Each registered user generates a `.vcf` file in SabreDAV, ensuring external calendar and contact apps stay in sync.
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func saveToCardDAV(userID, firstName, lastName, email, phone, dob string) error {
|
func CreateCardDAVContact(service *dav.BaseService, addressBookID int, userID, firstName, lastName, email, phone, dob string) error {
|
||||||
vcard := createVCard(userID, firstName, lastName, email, phone, dob)
|
input := dav.ContactInput{
|
||||||
url := fmt.Sprintf("http://nginx/dav/addressbooks/principals/default/default/%s.vcf", userID)
|
UserID: userID,
|
||||||
req, _ := http.NewRequest("PUT", url, bytes.NewBufferString(vcard))
|
FirstName: firstName,
|
||||||
req.SetBasicAuth("admin", "admin")
|
LastName: lastName,
|
||||||
_, err := http.DefaultClient.Do(req)
|
Email: email,
|
||||||
return err
|
Phone: phone,
|
||||||
|
DOB: dob,
|
||||||
|
}
|
||||||
|
return service.CreateContact(addressBookID, userID, input)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The address‑book ID is currently hard‑coded in the database (`dav_cards` table). A migration will expose the ID via a dedicated endpoint in the next sprint.
|
||||||
|
|
||||||
|
### Admin Endpoints
|
||||||
|
*Handlers for `/api/admin/services`, `/api/admin/bookings`, and `/api/admin/users` are defined in the router, but the concrete implementation files are still empty. These will be fleshed out in the next sprint.*
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎨 Frontend Implementation
|
## 🎨 Frontend Implementation
|
||||||
|
|
||||||
### API Proxy
|
### API Proxy
|
||||||
|
|
||||||
Prevents CORS issues by proxying all API calls through SvelteKit.
|
Prevents CORS issues by proxying all API calls through SvelteKit.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@@ -223,22 +183,23 @@ const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8080';
|
|||||||
|
|
||||||
async function proxyRequest(request: Request, path: string) {
|
async function proxyRequest(request: Request, path: string) {
|
||||||
const url = `${BACKEND_URL}/api/${path}`;
|
const url = `${BACKEND_URL}/api/${path}`;
|
||||||
const backendRes = await fetch(url, {
|
const backendRes = await fetch(url, {
|
||||||
method: request.method,
|
method: request.method,
|
||||||
headers: request.headers
|
headers: request.headers
|
||||||
});
|
});
|
||||||
return new Response(backendRes.body, {
|
|
||||||
status: backendRes.status,
|
// Forward everything transparently
|
||||||
headers: backendRes.headers
|
return new Response(backendRes.body, {
|
||||||
|
status: backendRes.status,
|
||||||
|
headers: new Headers(backendRes.headers)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Booking Flow
|
### Booking Flow
|
||||||
|
**Step 1**: Select services
|
||||||
**Step 1**: Select services
|
**Step 2**: Choose date/time
|
||||||
**Step 2**: Choose date/time
|
**Step 3**: Enter details
|
||||||
**Step 3**: Enter details
|
|
||||||
**Step 4**: Payment (TODO)
|
**Step 4**: Payment (TODO)
|
||||||
|
|
||||||
**Calendar Component:**
|
**Calendar Component:**
|
||||||
@@ -268,28 +229,71 @@ function generateTimeSlots(duration: number) {
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 Next Steps
|
## 🎯 Next Steps
|
||||||
|
- **Booking Integration** – wire the wizard to the backend `/api/bookings` POST endpoint.
|
||||||
|
- **Payments** – integrate Stripe or Square SDK; add a payment screen.
|
||||||
|
- **Admin Dashboard** – implement admin routes and UI.
|
||||||
|
- **Monitoring/Logging** – add a lightweight Prometheus/Grafana stack or use CloudWatch.
|
||||||
|
- **CI/CD Pipeline** – create GitHub Actions workflow for linting, testing, and container publishing.
|
||||||
|
- **Admin Handlers** – flesh out `/api/admin/services`, `/api/admin/bookings`, and `/api/admin/users` endpoints.
|
||||||
|
- **Email/SMS Reminders** – add scheduled job to send reminders.
|
||||||
|
- **Loyalty Tracking** – add a frontend component to display loyalty stamps.
|
||||||
|
|
||||||
### Bookings
|
---
|
||||||
|
|
||||||
- Add `bookings` table: user_id, service, date, time, status
|
## 📦 Environment & Runtime
|
||||||
- Backend: `/api/bookings` (create, list, cancel)
|
|
||||||
- Frontend: wire booking wizard → backend
|
|
||||||
|
|
||||||
### Payments
|
| Variable | Purpose | Default / Example |
|
||||||
|
|----------|---------|-------------------|
|
||||||
|
| `JWT_SECRET_KEY` | Secret used to sign JWTs | **REQUIRED** – set in `.env` or Docker secrets |
|
||||||
|
| `DATABASE_URL` | PostgreSQL connection string | `postgres://user:pass@localhost:5432/crussell?sslmode=disable` |
|
||||||
|
| `VITE_BACKEND_URL` | Front‑end proxy target | `http://localhost:8080` (dev) |
|
||||||
|
| `VITE_SQUARE_ENV` | Square environment | `sandbox` or `production` |
|
||||||
|
| `VITE_STRIPE_KEY` | Stripe publishable key | `pk_test_...` |
|
||||||
|
|
||||||
- Integrate Stripe or Square SDK
|
> **Tip**: The `.env.example` file contains all required keys – copy it to `.env` and edit.
|
||||||
- Secure checkout at Step 4
|
|
||||||
|
|
||||||
### Admin Dashboard
|
---
|
||||||
|
|
||||||
- View/manage bookings
|
## 🐳 Docker Compose
|
||||||
- User management
|
|
||||||
- Loyalty overview
|
|
||||||
|
|
||||||
### Security
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build: ./backend
|
||||||
|
environment:
|
||||||
|
- JWT_SECRET_KEY=supersecret
|
||||||
|
- DATABASE_URL=postgres://crussell:crussell@db:5432/crussell
|
||||||
|
depends_on: [db, dav]
|
||||||
|
dav:
|
||||||
|
image: sabredav/sabredav
|
||||||
|
environment:
|
||||||
|
- DAV_URL=http://dav:8000
|
||||||
|
volumes:
|
||||||
|
- dav-data:/data
|
||||||
|
db:
|
||||||
|
image: postgres:15
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=crussell
|
||||||
|
- POSTGRES_PASSWORD=crussell
|
||||||
|
- POSTGRES_DB=crussell
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
depends_on: [backend, dav]
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
frontend:
|
||||||
|
build: ./frontend
|
||||||
|
environment:
|
||||||
|
- VITE_BACKEND_URL=http://backend:8080
|
||||||
|
```
|
||||||
|
|
||||||
- Move JWT secret → env
|
> Run `docker compose up -d` to bring the stack up.
|
||||||
- HTTPS everywhere
|
|
||||||
- Harden API roles
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 📜 GDPR & Data Retention
|
||||||
|
|
||||||
|
The database schema includes a `anonymize_user()` function (see `init-script.sql`) that removes PII after a user’s account is closed. The code also contains a `export_all_user_data()` helper to support subject‑access requests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user