Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d17915907c | |||
| 4e8c66f3ca | |||
| ea28095434 | |||
| 3b9c72c2c4 | |||
| 62dfc7776b | |||
| 68df697cf3 | |||
| 174d1c667b | |||
| 9fe6e15012 | |||
| 002e6575ba | |||
| f9c679b392 | |||
| ce0739b3ba | |||
| 3609087980 | |||
| 7b2b533c16 | |||
| 55894c6ff2 | |||
| f55c74983f |
@@ -78,6 +78,8 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Log in to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -89,6 +91,7 @@ jobs:
|
||||
- name: Build and push API image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
provenance: false
|
||||
context: .
|
||||
file: Dockerfile
|
||||
target: runner
|
||||
@@ -102,6 +105,7 @@ jobs:
|
||||
- name: Build and push Migrate image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
provenance: false
|
||||
context: .
|
||||
file: Dockerfile
|
||||
target: migrate
|
||||
@@ -115,6 +119,7 @@ jobs:
|
||||
- name: Build and push Seed image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
provenance: false
|
||||
context: .
|
||||
file: Dockerfile
|
||||
target: seed
|
||||
@@ -128,6 +133,7 @@ jobs:
|
||||
- name: Build and push Reset image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
provenance: false
|
||||
context: .
|
||||
file: Dockerfile
|
||||
target: reset
|
||||
|
||||
@@ -21,6 +21,14 @@ GroomBook API is a Hono-based REST service (TypeScript/Node.js) powering the pet
|
||||
|
||||
## Test Cases
|
||||
|
||||
### 4.0 Health Check
|
||||
|
||||
| # | Scenario | Steps | Expected |
|
||||
|---|----------|-------|----------|
|
||||
| TC-API-0.1 | Unauthenticated health check | GET /api/health | 200 OK, `{"status":"ok"}` |
|
||||
|
||||
> **Note (GRO-1544):** Health endpoint registered on `api` basePath before auth middleware at `/api/health`. The old path `/health` was incorrect (routed to web pod via HTTPRoute `/*` rule).
|
||||
|
||||
### 4.1 Authentication
|
||||
|
||||
| # | Scenario | Steps | Expected |
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
CREATE TYPE "pet_size_category" AS ENUM ('small', 'medium', 'large', 'xlarge');
|
||||
CREATE TYPE "coat_type" AS ENUM ('smooth', 'double', 'wire', 'curly', 'long', 'hairless');
|
||||
|
||||
-- ─── Alter pets columns to use new enums ─────────────────────────────────────
|
||||
-- ─── Add columns to pets if missing, then cast to enums ──────────────────────
|
||||
|
||||
ALTER TABLE "pets" ADD COLUMN IF NOT EXISTS "coat_type" text;
|
||||
ALTER TABLE "pets" ADD COLUMN IF NOT EXISTS "pet_size_category" text;
|
||||
ALTER TABLE "pets" ALTER COLUMN "coat_type" TYPE "coat_type" USING "coat_type"::text::"coat_type";
|
||||
ALTER TABLE "pets" ALTER COLUMN "pet_size_category" TYPE "pet_size_category" USING "pet_size_category"::text::"pet_size_category";
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- no-op: journal entry exists but no schema change was needed
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Migration: 0033_add_services_default_buffer_minutes.sql
|
||||
-- Adds missing default_buffer_minutes column to services table.
|
||||
-- 0031_buffer_rules was applied to the DB but its journal entry was missing,
|
||||
-- so this ensures idempotent column addition for fresh DB restores.
|
||||
|
||||
ALTER TABLE "services" ADD COLUMN IF NOT EXISTS "default_buffer_minutes" integer DEFAULT 0 NOT NULL;
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"id": "0033_add_services_default_buffer_minutes",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"authProviderConfig": {
|
||||
"name": "auth_provider_config",
|
||||
"columns": {
|
||||
"id": { "name": "id", "type": "uuid", "primaryKey": true, "default": "gen_random_uuid()", "isNullable": false },
|
||||
"providerId": { "name": "provider_id", "type": "text", "isNullable": false },
|
||||
"displayName": { "name": "display_name", "type": "text", "isNullable": false },
|
||||
"issuerUrl": { "name": "issuer_url", "type": "text", "isNullable": false },
|
||||
"internalBaseUrl": { "name": "internal_base_url", "type": "text", "isNullable": true },
|
||||
"clientId": { "name": "client_id", "type": "text", "isNullable": false },
|
||||
"clientSecret": { "name": "client_secret", "type": "text", "isNullable": false },
|
||||
"scopes": { "name": "scopes", "type": "text", "isNullable": false, "default": "'openid profile email'" },
|
||||
"enabled": { "name": "enabled", "type": "boolean", "isNullable": false, "default": "true" },
|
||||
"createdAt": { "name": "created_at", "type": "timestamp", "isNullable": false, "default": "now()" },
|
||||
"updatedAt": { "name": "updated_at", "type": "timestamp", "isNullable": false, "default": "now()" }
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {}
|
||||
},
|
||||
"businessSettings": {
|
||||
"name": "business_settings",
|
||||
"columns": {
|
||||
"id": { "name": "id", "type": "uuid", "primaryKey": true, "default": "gen_random_uuid()", "isNullable": false },
|
||||
"businessName": { "name": "business_name", "type": "text", "isNullable": false, "default": "'GroomBook'" },
|
||||
"logoBase64": { "name": "logo_base64", "type": "text", "isNullable": true },
|
||||
"logoMimeType": { "name": "logo_mime_type", "type": "text", "isNullable": true },
|
||||
"logoKey": { "name": "logo_key", "type": "text", "isNullable": true },
|
||||
"primaryColor": { "name": "primary_color", "type": "text", "isNullable": false, "default": "'#4f8a6f'" },
|
||||
"accentColor": { "name": "accent_color", "type": "text", "isNullable": false, "default": "'#8b7355'" },
|
||||
"createdAt": { "name": "created_at", "type": "timestamp", "isNullable": false, "default": "now()" },
|
||||
"updatedAt": { "name": "updated_at", "type": "timestamp", "isNullable": false, "default": "now()" }
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {}
|
||||
},
|
||||
"clients": {
|
||||
"name": "clients",
|
||||
"columns": {
|
||||
"id": { "name": "id", "type": "uuid", "primaryKey": true, "default": "gen_random_uuid()", "isNullable": false },
|
||||
"name": { "name": "name", "type": "text", "isNullable": false },
|
||||
"email": { "name": "email", "type": "text", "isNullable": true },
|
||||
"phone": { "name": "phone", "type": "text", "isNullable": true },
|
||||
"address": { "name": "address", "type": "text", "isNullable": true },
|
||||
"notes": { "name": "notes", "type": "text", "isNullable": true },
|
||||
"emailOptOut": { "name": "email_opt_out", "type": "boolean", "isNullable": false, "default": "false" },
|
||||
"smsOptIn": { "name": "sms_opt_in", "type": "boolean", "isNullable": false, "default": "false" },
|
||||
"smsConsentDate": { "name": "sms_consent_date", "type": "timestamp", "isNullable": true },
|
||||
"smsOptOutDate": { "name": "sms_opt_out_date", "type": "timestamp", "isNullable": true },
|
||||
"smsConsentText": { "name": "sms_consent_text", "type": "text", "isNullable": true },
|
||||
"stripeCustomerId": { "name": "stripe_customer_id", "type": "text", "isNullable": true },
|
||||
"status": { "name": "status", "type": "client_status", "isNullable": false, "default": "'active'" },
|
||||
"disabledAt": { "name": "disabled_at", "type": "timestamp", "isNullable": true },
|
||||
"createdAt": { "name": "created_at", "type": "timestamp", "isNullable": false, "default": "now()" },
|
||||
"updatedAt": { "name": "updated_at", "type": "timestamp", "isNullable": false, "default": "now()" }
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": { "idx_clients_stripe_customer_id": { "columns": ["stripe_customer_id"] } }
|
||||
},
|
||||
"invoices": {
|
||||
"name": "invoices",
|
||||
"columns": {
|
||||
"id": { "name": "id", "type": "uuid", "primaryKey": true, "default": "gen_random_uuid()", "isNullable": false },
|
||||
"appointmentId": { "name": "appointment_id", "type": "uuid", "isNullable": true },
|
||||
"clientId": { "name": "client_id", "type": "uuid", "isNullable": false },
|
||||
"subtotalCents": { "name": "subtotal_cents", "type": "integer", "isNullable": false },
|
||||
"taxCents": { "name": "tax_cents", "type": "integer", "isNullable": false, "default": "0" },
|
||||
"tipCents": { "name": "tip_cents", "type": "integer", "isNullable": false, "default": "0" },
|
||||
"totalCents": { "name": "total_cents", "type": "integer", "isNullable": false },
|
||||
"status": { "name": "status", "type": "invoice_status", "isNullable": false, "default": "'draft'" },
|
||||
"paymentMethod": { "name": "payment_method", "type": "payment_method", "isNullable": true },
|
||||
"paidAt": { "name": "paid_at", "type": "timestamp", "isNullable": true },
|
||||
"stripePaymentIntentId": { "name": "stripe_payment_intent_id", "type": "text", "isNullable": true },
|
||||
"stripeRefundId": { "name": "stripe_refund_id", "type": "text", "isNullable": true },
|
||||
"paymentFailureReason": { "name": "payment_failure_reason", "type": "text", "isNullable": true },
|
||||
"notes": { "name": "notes", "type": "text", "isNullable": true },
|
||||
"createdAt": { "name": "created_at", "type": "timestamp", "isNullable": false, "default": "now()" },
|
||||
"updatedAt": { "name": "updated_at", "type": "timestamp", "isNullable": false, "default": "now()" }
|
||||
},
|
||||
"indexes": { "idx_invoices_client_id": { "columns": ["client_id"] }, "idx_invoices_status": { "columns": ["status"] }, "idx_invoices_created_at": { "columns": ["created_at"] } },
|
||||
"foreignKeys": { "invoices_appointment_id_fkey": { "columns": ["appointmentId"], "reference": { "table": "appointments", "columns": ["id"] } }, "invoices_client_id_fkey": { "columns": ["clientId"], "reference": { "table": "clients", "columns": ["id"] } } },
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": { "idx_invoices_stripe_payment_intent_id": { "columns": ["stripe_payment_intent_id"] } }
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"appointment_status": { "name": "appointment_status", "values": ["scheduled", "confirmed", "in_progress", "completed", "cancelled", "no_show"] },
|
||||
"client_status": { "name": "client_status", "values": ["active", "disabled"] },
|
||||
"impersonation_session_status": { "name": "impersonation_session_status", "values": ["active", "ended", "expired"] },
|
||||
"invoice_status": { "name": "invoice_status", "values": ["draft", "pending", "paid", "void"] },
|
||||
"payment_method": { "name": "payment_method", "values": ["cash", "card", "check", "other"] },
|
||||
"staff_role": { "name": "staff_role", "values": ["groomer", "receptionist", "manager"] },
|
||||
"waitlist_status": { "name": "waitlist_status", "values": ["active", "notified", "expired", "cancelled"] }
|
||||
},
|
||||
"nativeEnums": {}
|
||||
}
|
||||
@@ -218,6 +218,27 @@
|
||||
"when": 1775828067192,
|
||||
"tag": "0030_messaging",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 31,
|
||||
"version": "7",
|
||||
"when": 1775860800000,
|
||||
"tag": "0031_buffer_rules",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 32,
|
||||
"version": "7",
|
||||
"when": 1775894400000,
|
||||
"tag": "0032_staff_read_at",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 33,
|
||||
"version": "7",
|
||||
"when": 1779500000000,
|
||||
"tag": "0033_add_services_default_buffer_minutes",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+2
-2
@@ -58,8 +58,8 @@ app.use(
|
||||
})
|
||||
);
|
||||
|
||||
// Health check (no auth required)
|
||||
app.get("/health", (c) => c.json({ status: "ok" }));
|
||||
// Health check — no auth required, registered on app at full path before auth middleware
|
||||
app.get("/api/health", (c) => c.json({ status: "ok" }));
|
||||
|
||||
// Public booking routes — no auth required, must be registered before auth middleware
|
||||
app.route("/api/book", bookRouter);
|
||||
|
||||
@@ -23,7 +23,7 @@ if (process.env.AUTH_DISABLED === "true") {
|
||||
}
|
||||
|
||||
export const authMiddleware: MiddlewareHandler = async (c, next) => {
|
||||
if (c.req.path.startsWith("/api/auth/")) {
|
||||
if (c.req.path.startsWith("/api/auth/") || c.req.path === "/api/health") {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user