Skip to content

Commit afd87e9

Browse files
committed
feat: implement CORS middleware and update service configurations
1 parent 7989d59 commit afd87e9

16 files changed

Lines changed: 171 additions & 54 deletions

File tree

Makefile

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ SHELL := /bin/bash
1818

1919
COMPOSE := docker compose -f deploy/docker/docker-compose.dev.yml
2020

21-
# ── module lists ──────────────────────────────────────────────────────────────
21+
# module lists
2222
SERVICES := packages/go-common \
2323
services/api-gateway \
2424
services/auth-service \
@@ -31,7 +31,7 @@ SERVICES := packages/go-common \
3131
gateway auth tenant booking config ui \
3232
migrate migrate-down
3333

34-
# ── docker-compose targets ────────────────────────────────────────────────────
34+
# docker-compose targets
3535

3636
## up: start all services (build images if needed)
3737
up:
@@ -58,7 +58,7 @@ logs:
5858
build:
5959
$(COMPOSE) build
6060

61-
# ── migration targets ────────────────────────────────────────────────────────
61+
# migration targets ─
6262

6363
## migrate: run all pending DB migrations
6464
migrate:
@@ -68,7 +68,7 @@ migrate:
6868
migrate-down:
6969
$(COMPOSE) run --rm migrate down 1
7070

71-
# ── Go targets ────────────────────────────────────────────────────────────────
71+
# Go targets
7272

7373
## test: run tests in every module
7474
test:
@@ -105,7 +105,7 @@ tidy:
105105
done
106106
go work sync
107107

108-
# ── local run targets (infra must be up first) ────────────────────────────────
108+
# local run targets (infra must be up first)
109109

110110
## gateway: run api-gateway locally
111111
gateway:
@@ -139,7 +139,7 @@ ui-install:
139139
ui-build:
140140
cd apps/management-ui && npm run build
141141

142-
# ── help ──────────────────────────────────────────────────────────────────────
142+
# help
143143

144144
help:
145145
@grep -E '^##' Makefile | sed 's/## //'

apps/management-ui/Dockerfile

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,39 @@
44
# Version 2.0 (the "LICENSE"); you may not use this file except
55
# in compliance with the LICENSE.
66

7-
# ── Stage 1: dependencies ────────────────────────────────────────────────────
7+
# Stage 1: install dependencies ─
88
FROM node:20-alpine AS deps
99
WORKDIR /app
1010
COPY package.json package-lock.json* ./
1111
RUN npm ci --prefer-offline
1212

13-
# ── Stage 2: build ────────────────────────────────────────────────────────────
13+
# Stage 2: build
1414
FROM node:20-alpine AS builder
1515
WORKDIR /app
1616
COPY --from=deps /app/node_modules ./node_modules
1717
COPY . .
18+
1819
ENV NEXT_TELEMETRY_DISABLED=1
20+
21+
# Build-time public env vars (baked into JS bundle).
22+
# Override with ARG/ENV when running inside Docker Compose.
23+
ARG NEXT_PUBLIC_API_URL=http://localhost:8081
24+
ARG NEXT_PUBLIC_GATEWAY_URL=http://localhost:8081
25+
ARG NEXT_PUBLIC_AUTH_URL=http://localhost:8082
26+
ARG NEXT_PUBLIC_TENANT_URL=http://localhost:8083
27+
ARG NEXT_PUBLIC_BOOKING_URL=http://localhost:8084
28+
ARG NEXT_PUBLIC_CONFIG_URL=http://localhost:8085
29+
30+
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
31+
ENV NEXT_PUBLIC_GATEWAY_URL=$NEXT_PUBLIC_GATEWAY_URL
32+
ENV NEXT_PUBLIC_AUTH_URL=$NEXT_PUBLIC_AUTH_URL
33+
ENV NEXT_PUBLIC_TENANT_URL=$NEXT_PUBLIC_TENANT_URL
34+
ENV NEXT_PUBLIC_BOOKING_URL=$NEXT_PUBLIC_BOOKING_URL
35+
ENV NEXT_PUBLIC_CONFIG_URL=$NEXT_PUBLIC_CONFIG_URL
36+
1937
RUN npm run build
2038

21-
# ── Stage 3: runner ───────────────────────────────────────────────────────────
39+
# Stage 3: minimal runtime
2240
FROM node:20-alpine AS runner
2341
WORKDIR /app
2442
ENV NODE_ENV=production

apps/management-ui/lib/api/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const configApi = {
4242
config: Record<string, unknown>,
4343
tenantId: string,
4444
): Promise<ModuleConfig> {
45-
return api.put(`/v1/config/${module}`, { config }, { tenantId });
45+
return api.put(`/v1/config/${module}`, config, { tenantId });
4646
},
4747

4848
getHistory(module: string, tenantId: string): Promise<ConfigHistoryEntry[]> {

apps/management-ui/public/.gitkeep

Whitespace-only changes.

build/Dockerfile.service

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,28 @@
55
# in compliance with the LICENSE.
66

77
# Stage 1: build
8-
FROM golang:1.23-alpine AS builder
8+
FROM golang:1.25-alpine AS builder
99

1010
# Receive the service name via build-arg, e.g. "tenant-service"
1111
ARG SERVICE
1212
RUN test -n "$SERVICE" || (echo "SERVICE build-arg is required" && exit 1)
1313

1414
WORKDIR /workspace
1515

16-
# Copy workspace descriptor first so the module graph is resolvable.
17-
COPY go.work go.work.sum ./
18-
1916
# Copy shared packages (go-common is the only shared module for now).
2017
COPY packages/ packages/
2118

2219
# Copy only the target service — avoids cache invalidation from sibling changes.
2320
COPY services/${SERVICE}/ services/${SERVICE}/
2421

2522
# Build with module cache mount for fast rebuilds.
23+
# GOWORK=off: each service module has its own replace directive for go-common,
24+
# so the workspace file is not needed and only the target service + packages/
25+
# need to be in the build context.
2626
RUN --mount=type=cache,target=/root/go/pkg/mod \
2727
--mount=type=cache,target=/root/.cache/go-build \
2828
cd services/${SERVICE} && \
29-
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /bin/server ./cmd/server
29+
GOWORK=off CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /bin/server ./cmd/server
3030

3131
# Stage 2: runtime
3232
FROM alpine:3.20

deploy/docker/docker-compose.dev.yml

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# specific language governing permissions and limitations
1515
# under the LICENSE.
1616
#
17-
# ── ServiceForge development compose ────────────────────────────────────────
17+
# ServiceForge development compose ─
1818
#
1919
# Startup order (via depends_on + condition):
2020
# postgres (healthy)
@@ -31,8 +31,6 @@
3131
# docker compose -f deploy/docker/docker-compose.dev.yml up --build
3232
# docker compose -f deploy/docker/docker-compose.dev.yml run --rm migrate down 1
3333

34-
version: "3.9"
35-
3634
x-service-defaults: &service-defaults
3735
build:
3836
context: ../..
@@ -44,7 +42,7 @@ x-service-defaults: &service-defaults
4442
DATABASE_URL: postgres://serviceforge:serviceforge@postgres:5432/serviceforge?sslmode=disable
4543
KAFKA_BROKERS: kafka:9092
4644

47-
# ── Infrastructure ────────────────────────────────────────────────────────────
45+
# Infrastructure
4846

4947
services:
5048
postgres:
@@ -95,34 +93,36 @@ services:
9593
start_period: 5s
9694

9795
zookeeper:
98-
image: bitnami/zookeeper:3.9
96+
image: confluentinc/cp-zookeeper:7.5.0
9997
container_name: serviceforge-zookeeper
10098
environment:
101-
- ALLOW_ANONYMOUS_LOGIN=yes
99+
ZOOKEEPER_CLIENT_PORT: 2181
100+
ZOOKEEPER_TICK_TIME: 2000
102101
ports:
103102
- "2181:2181"
104103
healthcheck:
105-
test: ["CMD-SHELL", "echo ruok | nc -w 2 localhost 2181 | grep imok"]
104+
test: ["CMD-SHELL", "echo srvr | nc localhost 2181 2>/dev/null | grep -q 'Zookeeper version' || exit 1"]
106105
interval: 10s
107106
timeout: 5s
108107
retries: 10
109108
start_period: 15s
110109

111110
kafka:
112-
image: bitnami/kafka:3.7
111+
image: confluentinc/cp-kafka:7.5.0
113112
container_name: serviceforge-kafka
114113
depends_on:
115114
zookeeper:
116115
condition: service_healthy
117116
ports:
118117
- "9092:9092"
119118
environment:
120-
- KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
121-
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092
122-
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092
123-
- ALLOW_PLAINTEXT_LISTENER=yes
119+
KAFKA_BROKER_ID: 1
120+
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
121+
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
122+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
123+
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
124124
healthcheck:
125-
test: ["CMD-SHELL", "kafka-topics.sh --bootstrap-server localhost:9092 --list"]
125+
test: ["CMD-SHELL", "kafka-topics --bootstrap-server localhost:9092 --list"]
126126
interval: 10s
127127
timeout: 10s
128128
retries: 10
@@ -140,7 +140,7 @@ services:
140140
KAFKA_CLUSTERS_0_NAME: local
141141
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
142142

143-
# ── Microservices ────────────────────────────────────────────────────────────
143+
# Microservices ─
144144

145145
tenant-service:
146146
<<: *service-defaults
@@ -299,22 +299,24 @@ services:
299299
management-ui:
300300
container_name: serviceforge-management-ui
301301
build:
302-
context: ../..
303-
dockerfile: apps/management-ui/Dockerfile
302+
context: ../../apps/management-ui
303+
dockerfile: Dockerfile
304+
args:
305+
# NEXT_PUBLIC_* vars are baked into the bundle at build time.
306+
# The gateway is exposed on the host at port 8081 so the browser can reach it.
307+
NEXT_PUBLIC_API_URL: http://localhost:8081
308+
NEXT_PUBLIC_GATEWAY_URL: http://localhost:8081
309+
NEXT_PUBLIC_AUTH_URL: http://localhost:8082
310+
NEXT_PUBLIC_TENANT_URL: http://localhost:8083
311+
NEXT_PUBLIC_BOOKING_URL: http://localhost:8084
312+
NEXT_PUBLIC_CONFIG_URL: http://localhost:8085
304313
depends_on:
305314
api-gateway:
306315
condition: service_healthy
307316
ports:
308317
- "3000:3000"
309-
environment:
310-
NEXT_PUBLIC_API_URL: http://localhost:8081
311-
NEXT_PUBLIC_GATEWAY_URL: http://localhost:8081
312-
NEXT_PUBLIC_AUTH_URL: http://localhost:8082
313-
NEXT_PUBLIC_TENANT_URL: http://localhost:8083
314-
NEXT_PUBLIC_BOOKING_URL: http://localhost:8084
315-
NEXT_PUBLIC_CONFIG_URL: http://localhost:8085
316318

317-
# ── Volumes ──────────────────────────────────────────────────────────────────
319+
# Volumes ─
318320

319321
volumes:
320322
pgdata:
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright (c) 2026, SoftlaneIT (https://softlaneit.com/) All Rights Reserved.
3+
*
4+
* SoftlaneIT licenses this file to you under the Apache License,
5+
* Version 2.0 (the "LICENSE"); you may not use this file except
6+
* in compliance with the LICENSE.
7+
*/
8+
9+
// Package middleware provides reusable HTTP middleware for all ServiceForge services.
10+
package middleware
11+
12+
import "net/http"
13+
14+
// CORS returns a middleware that adds cross-origin response headers so the
15+
// management-UI (served on a different port) can call every service directly.
16+
//
17+
// allowedOrigins is a set of exact Origin values that are permitted.
18+
// Pass "*" as the only element to allow any origin (dev-only).
19+
func CORS(allowedOrigins ...string) func(http.Handler) http.Handler {
20+
allowed := make(map[string]bool, len(allowedOrigins))
21+
for _, o := range allowedOrigins {
22+
allowed[o] = true
23+
}
24+
wildcard := allowed["*"]
25+
26+
return func(next http.Handler) http.Handler {
27+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28+
origin := r.Header.Get("Origin")
29+
30+
if wildcard || allowed[origin] {
31+
w.Header().Set("Access-Control-Allow-Origin", origin)
32+
w.Header().Set("Access-Control-Allow-Credentials", "true")
33+
w.Header().Set("Access-Control-Allow-Methods",
34+
"GET, POST, PUT, PATCH, DELETE, OPTIONS")
35+
w.Header().Set("Access-Control-Allow-Headers",
36+
"Content-Type, Authorization, X-Tenant-ID, X-Changed-By")
37+
w.Header().Set("Access-Control-Max-Age", "3600")
38+
}
39+
40+
// Handle preflight.
41+
if r.Method == http.MethodOptions {
42+
w.WriteHeader(http.StatusNoContent)
43+
return
44+
}
45+
46+
next.ServeHTTP(w, r)
47+
})
48+
}
49+
}

services/api-gateway/cmd/server/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import (
5757

5858
"github.com/SoftLaneIT/serviceforge/packages/go-common/config"
5959
"github.com/SoftLaneIT/serviceforge/packages/go-common/logger"
60+
commidware "github.com/SoftLaneIT/serviceforge/packages/go-common/middleware"
6061
"github.com/SoftLaneIT/serviceforge/packages/go-common/tenant"
6162
gw "github.com/SoftLaneIT/serviceforge/services/api-gateway/internal/middleware"
6263
"github.com/SoftLaneIT/serviceforge/services/api-gateway/internal/proxy"
@@ -125,7 +126,12 @@ func main() {
125126
mux.Handle("/v1/config", withAuth(configProxy))
126127

127128
// Outer middleware applied to the whole mux.
128-
httpHandler := tenant.Middleware(logger.HTTPMiddleware(log)(mux))
129+
// CORS wraps everything so that preflight OPTIONS requests are handled before
130+
// auth/rate-limit middleware runs.
131+
corsOrigins := config.GetEnv("CORS_ORIGINS", "http://localhost:3000")
132+
httpHandler := commidware.CORS(corsOrigins)(
133+
tenant.Middleware(logger.HTTPMiddleware(log)(mux)),
134+
)
129135

130136
// HTTP server
131137
port := config.GetEnv("PORT", "8081")

services/auth-service/cmd/server/main.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242

4343
"github.com/SoftLaneIT/serviceforge/packages/go-common/config"
4444
"github.com/SoftLaneIT/serviceforge/packages/go-common/logger"
45+
commidware "github.com/SoftLaneIT/serviceforge/packages/go-common/middleware"
4546
"github.com/SoftLaneIT/serviceforge/packages/go-common/tenant"
4647
"github.com/SoftLaneIT/serviceforge/services/auth-service/internal/cache"
4748
"github.com/SoftLaneIT/serviceforge/services/auth-service/internal/handler"
@@ -75,7 +76,10 @@ func main() {
7576
// Middleware chain (outermost first):
7677
// tenant.Middleware → injects tenant_id from X-Tenant-ID header
7778
// logger.HTTPMiddleware → structured request logging
78-
httpHandler := tenant.Middleware(logger.HTTPMiddleware(log)(mux))
79+
corsOrigins := config.GetEnv("CORS_ORIGINS", "http://localhost:3000")
80+
httpHandler := commidware.CORS(corsOrigins)(
81+
tenant.Middleware(logger.HTTPMiddleware(log)(mux)),
82+
)
7983

8084
port := config.GetEnv("PORT", "8082")
8185
srv := &http.Server{

services/booking-service/cmd/server/main.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343

4444
"github.com/SoftLaneIT/serviceforge/packages/go-common/config"
4545
"github.com/SoftLaneIT/serviceforge/packages/go-common/logger"
46+
commidware "github.com/SoftLaneIT/serviceforge/packages/go-common/middleware"
4647
"github.com/SoftLaneIT/serviceforge/packages/go-common/tenant"
4748
"github.com/SoftLaneIT/serviceforge/services/booking-service/internal/events"
4849
"github.com/SoftLaneIT/serviceforge/services/booking-service/internal/handler"
@@ -79,7 +80,10 @@ func main() {
7980
mux := http.NewServeMux()
8081
h.RegisterRoutes(mux)
8182

82-
httpHandler := tenant.Middleware(logger.HTTPMiddleware(log)(mux))
83+
corsOrigins := config.GetEnv("CORS_ORIGINS", "http://localhost:3000")
84+
httpHandler := commidware.CORS(corsOrigins)(
85+
tenant.Middleware(logger.HTTPMiddleware(log)(mux)),
86+
)
8387

8488
port := config.GetEnv("PORT", "8084")
8589
srv := &http.Server{

0 commit comments

Comments
 (0)