Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Environment secrets
.env

# Build artefacts
/bin/

# Go
*.test
*.out

# OS
.DS_Store
Thumbs.db

# Editor
.vscode/
.idea/
*.swp
38 changes: 38 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
GONOSUMDB := bookmyvenue.com
export GONOSUMDB

.PHONY: dev build test lint tidy infra-up infra-down migrate clean

dev:
@scripts/dev.sh

build:
cd auth-service && go build -o ../bin/auth-service ./cmd/main.go
cd booking-service && go build -o ../bin/booking-service ./cmd/main.go

test:
cd shared && go test ./...
cd auth-service && go test ./...
cd booking-service && go test ./...

lint:
cd shared && go vet ./...
cd auth-service && go vet ./...
cd booking-service && go vet ./...

tidy:
cd shared && go mod tidy
cd auth-service && go mod tidy
cd booking-service && go mod tidy

infra-up:
docker compose -f booking-service/compose.yaml up -d

infra-down:
docker compose -f booking-service/compose.yaml down

migrate:
@scripts/migrate.sh

clean:
rm -rf bin/
125 changes: 125 additions & 0 deletions PROJECT_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Venues CRUD — Project Plan

## Current State

The **backend** (booking-service) already has a **complete CRUD API** for venues:

| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| GET | `/venues` | — | List all venues |
| GET | `/venues/:id` | — | Get venue by ID |
| POST | `/venues` | JWT (owner/admin) | Create venue |
| PUT | `/venues/:id` | JWT (owner/admin) | Update venue |
| DELETE | `/venues/:id` | JWT (owner/admin) | Delete venue |
| GET | `/venues/mine` | JWT (owner/admin) | List my venues |

The **frontend** uses **hardcoded mock data** (`src/lib/venues.ts`) — no API calls. There is no UI for creating, editing, or deleting venues.

---

## Phase 1 — Connect Frontend to Real API

### 1.1 Environment variable
Add `NEXT_PUBLIC_BOOKING_URL=http://localhost:8081` to the frontend.

### 1.2 API client (`src/lib/api.ts`)
Create a shared fetch wrapper that:
- Reads `NEXT_PUBLIC_BOOKING_URL`
- Injects `Authorization: Bearer <token>` from localStorage for protected calls
- Handles JSON serialization and error responses uniformly

### 1.3 Real venue API functions (`src/lib/venues.ts`)
Replace mock functions with real HTTP calls:

```ts
fetchVenues(query?) → GET /venues?search=&category=&city=
fetchVenueById(id) → GET /venues/:id
createVenue(data) → POST /venues
updateVenue(id, data) → PUT /venues/:id
deleteVenue(id) → DELETE /venues/:id
fetchMyVenues() → GET /venues/mine
```

### 1.4 Align `Venue` type
The backend model has: `id`, `owner_id`, `name`, `description`, `location`, `capacity`, `price_per_hour`, `created_at`.

The frontend has extra fields: `city`, `category`, `rating`, `reviewCount`, `amenities`, `images`, `highlights`.

**Option A** (recommended short-term): Add `city`, `category`, `images` as nullable columns to the venues table via a new migration. Store `amenities` / `highlights` as JSONB. The `rating` / `reviewCount` fields belong in a separate reviews system (future) — for now default to `0` / `0` or seed data.

**Option B** (minimal): Keep the frontend type superset and simply don't send extra fields to the API. When reading, merge DB data with defaults. Simpler but leaves a gap.

### 1.5 Update existing pages
- `venues/page.tsx` — replace `getVenues()` with `fetchVenues()`
- `venues/[id]/page.tsx` — replace `getVenueById()` with `fetchVenueById()`
- Keep search + category filter working (can be query params sent to the API)

---

## Phase 2 — Owner Venue Management

### 2.1 My Venues page (`/my-venues`)
- Protected route for `owner` / `admin` roles
- Calls `fetchMyVenues()`
- Lists the user's venues with **Edit** and **Delete** actions
- Shows a **"Add Venue"** CTA when empty

### 2.2 Create Venue page (`/venues/new`)
- Form: name, description, location, capacity, price_per_hour, city, category, images
- Calls `createVenue()` on submit
- Redirects to `/my-venues` or the new venue's detail page on success

### 2.3 Edit Venue page (`/venues/[id]/edit`)
- Pre-populated form from `fetchVenueById()`
- Calls `updateVenue()` on submit
- Only accessible by the venue owner or an admin

### 2.4 Delete Venue
- Confirmation dialog (modal or inline)
- Calls `deleteVenue()` on confirm
- Removes the venue from the list without page reload (optimistic UI)

---

## Phase 3 (Optional) — Polish

### 3.1 Owner badge on venue detail
Show "Edit" / "Delete" buttons on the venue detail page if the logged-in user owns it.

### 3.2 Venue images upload
Add an image upload endpoint to the backend (multipart → S3/local storage) and a file picker in the create/edit forms.

### 3.3 Pagination
Add `offset` / `limit` query params to `GET /venues` and pagination UI on the listing page.

---

## File Changes Summary

| File | Action |
|------|--------|
| `frontend/.env.local` | Add `NEXT_PUBLIC_BOOKING_URL` |
| `frontend/src/lib/api.ts` | **Create** — shared fetch helper |
| `frontend/src/lib/venues.ts` | **Rewrite** — replace mocks with API calls |
| `frontend/src/app/venues/page.tsx` | Update to use `fetchVenues()` |
| `frontend/src/app/venues/[id]/page.tsx` | Update to use `fetchVenueById()`, add owner actions |
| `frontend/src/app/my-venues/page.tsx` | **Create** — owner dashboard |
| `frontend/src/app/venues/new/page.tsx` | **Create** — add venue form |
| `frontend/src/app/venues/[id]/edit/page.tsx` | **Create** — edit venue form |
| `frontend/src/components/DeleteVenueDialog.tsx` | **Create** — delete confirmation |
| `booking-service/migrations/003_add_venue_profile_fields.up.sql` | **Create** — city, category, images, amenities, highlights columns |

---

## Sequence for Delivery

```
Phase 1 ──► Phase 2 ──► Phase 3 (optional)
1.1 2.1 3.1
1.2 2.2 3.2
1.3 2.3 3.3
1.4 2.4
1.5
```

Start with Phase 1 to unblock the listing + detail pages, then Phase 2 for the owner management flows.
102 changes: 102 additions & 0 deletions auth-service/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"auth-service/internal/auth"
delivery "auth-service/internal/delivery/http"
amqp "github.com/rabbitmq/amqp091-go"

"auth-service/internal/repository"
"context"
"log"
"time"

"bookmyvenue.com/shared/config"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgxpool"
)

type amqpPublisher struct {
ch *amqp.Channel
queue string
}

func (p *amqpPublisher) PublishLogin(userID string) error {
body := []byte(`{"user_id":"` + userID + `"}`)
return p.ch.Publish(
"", // exchange
p.queue,
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "application/json",
Body: body,
Timestamp: time.Now(),
},
)
}

func main() {
// ── Config ────────────────────────────────────────────────────────────────
connStr := config.MustGetEnv("DATABASE_URL",
"postgres://postgres:secret@localhost:5432/wecode_auth?sslmode=disable")
jwtSecret := config.MustGetEnv("JWT_SECRET", "secret")
broker, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
if err != nil {
log.Fatal(err)
}
defer broker.Close()

// Create a channel for publishing. We keep a single publisher instance
// and pass it to the auth service.
ch, err := broker.Channel()
if err != nil {
log.Fatalf("failed to open amqp channel: %v", err)
}
defer ch.Close()

q, err := ch.QueueDeclare(
"logins",
true,
false,
false,
false,
nil,
)

if err != nil {
log.Fatalf("failed to declare exchange: %v", err)
}

pub := &amqpPublisher{ch: ch, queue: q.Name}

port := config.MustGetEnv("PORT", ":8080")

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

dbPool, err := pgxpool.New(ctx, connStr)
if err != nil {
log.Fatalf("DB Connection failed: %v", err)
}
defer dbPool.Close()

jwtManager := auth.NewJWTManager(jwtSecret)
userRepo := repository.NewPostgresUserRepository(dbPool)
authSvc := auth.NewAuthService(userRepo, jwtManager, pub)

r := gin.Default()
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:3000"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
}))
api := r.Group("/")
delivery.NewAuthHandler(api, authSvc)

log.Printf("auth-service listening: %s", port)
if err := r.Run(port); err != nil {
log.Fatalf("Fatal: Server hit an error: %v", err)
}
}
22 changes: 22 additions & 0 deletions auth-service/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
services:
postgres:
image: postgres:latest
container_name: postgres
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: wecode_auth
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql
networks:
- wecode_network

volumes:
postgres_data:

networks:
wecode_network:
external: true
50 changes: 50 additions & 0 deletions auth-service/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module auth-service

go 1.25.2

require (
bookmyvenue.com/shared v0.0.0
github.com/gin-gonic/gin v1.12.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/jackc/pgx/v5 v5.9.2
golang.org/x/crypto v0.52.0
)

replace bookmyvenue.com/shared => ../shared

require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/cors v1.7.7 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/rabbitmq/amqp091-go v1.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
Loading