Skip to content

Commit 3d9bf15

Browse files
authored
feat(server): refactoring the development environment [VIZ-2314] (#1866)
1 parent 0e09b41 commit 3d9bf15

File tree

19 files changed

+332
-35
lines changed

19 files changed

+332
-35
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.DS_Store
22
node_modules
3-
/mongo
4-
/data
53
/.env
64
/.env.*
75
/.idea
6+
*.log
7+
mongo/

docker-compose.dev.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
services:
2+
3+
reearth-visualizer-dev:
4+
build:
5+
context: ./server
6+
dockerfile: Dockerfile.dev
7+
env_file:
8+
- ./server/.env.docker
9+
ports:
10+
- 8080:8080
11+
depends_on:
12+
- reearth-mongo
13+
- reearth-gcs
14+
volumes:
15+
- ./server:/reearth-visualizer # Mount source code for hot reload
16+
- /reearth-visualizer/tmp # Exclude tmp directory
17+
networks:
18+
- reearth
19+
20+
reearth-mongo:
21+
image: mongo:7
22+
ports:
23+
- 27017:27017
24+
volumes:
25+
- ${PWD}/tmp/mongo:/data/db
26+
networks:
27+
- reearth
28+
29+
reearth-gcs:
30+
image: fsouza/fake-gcs-server:1.52.1
31+
ports:
32+
- 4443:4443
33+
volumes:
34+
- ${PWD}/tmp/gcs:/storage
35+
command: -scheme http
36+
networks:
37+
- reearth
38+
39+
networks:
40+
reearth:
41+
name: reearth

server/.env.docker

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Docker-specific environment variables
2+
# This file is used when running the application in Docker
3+
4+
# Database connection (Docker service name)
5+
REEARTH_DB=mongodb://reearth-mongo
6+
7+
# Server host (bind to all interfaces in Docker)
8+
REEARTH_SERVERHOST=0.0.0.0
9+
10+
# GCS configuration for Docker
11+
STORAGE_EMULATOR_HOST=http://reearth-gcs:4443
12+
REEARTH_GCS_BUCKETNAME=test-bucket
13+
REEARTH_GCS_ISFAKE=true
14+
GOOGLE_CLOUD_PROJECT=reearth-local
15+
16+
# Asset base URL
17+
REEARTH_ASSETBASEURL=http://localhost:8080
18+
19+
# Auth0 configuration
20+
REEARTH_AUTH0_DOMAIN=
21+
REEARTH_AUTH0_AUDIENCE=
22+
REEARTH_AUTH0_CLIENTID=
23+
REEARTH_AUTH0_CLIENTSECRET=
24+
25+
# Account Auth
26+
REEARTH_ACCOUNTSAPI_ENABLED=true
27+
REEARTH_ACCOUNTSAPI_HOST=http://reearth-accounts-dev:8090
28+
29+
# Migration
30+
RUN_MIGRATION=fasle
31+
32+
# Mock auth for development
33+
REEARTH_MOCKAUTH=false
34+
35+
# Internal API
36+
REEARTH_VISUALIZER_INTERNALAPI_ACTIVE=false
37+
38+
# Dashboard API
39+
REEARTH_DASHBOARD_API=https://api.dev.reearth.io:8080
40+
41+
# Policy checker
42+
REEARTH_VISUALIZER_POLICY_CHECKER_TYPE=permissive
43+
REEARTH_VISUALIZER_POLICY_CHECKER_ENDPOINT=https://cloud.visualizer.dev.reearth.io/api/internal/policy-check
44+
REEARTH_CLOUD_INTERNAL_API_KEY=
45+
REEARTH_VISUALIZER_POLICY_CHECKER_TIMEOUT=30

server/.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ REEARTH_VISUALIZER_INTERNALAPI_TOKEN=token
4242
REEARTH_GCP_PROJECT=
4343

4444
# Trace API
45-
REEARTH_TRACER=gcp
46-
REEARTH_TRACER_SAMPLE=1.0
45+
# REEARTH_TRACER=gcp
46+
# REEARTH_TRACER_SAMPLE=1.0
4747

4848
# Auth client
4949
#REEARTH_AUTH_ISS=https://hoge.com

server/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ __debug_bin
2626
/mongo
2727
/.env*
2828
!/.env.example
29+
!/.env.docker
2930
/coverage.txt
3031
/web
3132
tmp

server/Dockerfile.dev

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
FROM golang:1.24.4-alpine
2+
3+
RUN apk add --update --no-cache git ca-certificates build-base curl
4+
5+
# Install air for hot reload
6+
RUN go install github.com/air-verse/[email protected]
7+
8+
WORKDIR /reearth-visualizer
9+
10+
# Copy go mod files
11+
COPY go.mod go.sum ./
12+
RUN go mod download
13+
14+
# Copy source code
15+
COPY . .
16+
17+
# Expose port
18+
EXPOSE 8080
19+
20+
# Run air for hot reload
21+
CMD ["air", "-c", ".air.toml"]

server/Makefile

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ REEARTH_DB := mongodb://localhost
88
SCHEMATYPER := github.com/idubinskiy/schematyper
99
MANIFEST_DIR := pkg/plugin/manifest
1010
DOCKER_COMPOSE := docker compose -f ../docker-compose.yml
11+
DOCKER_COMPOSE_DEV := docker compose -f ../docker-compose.dev.yml
1112

1213
# =======================
1314
# Default
@@ -67,6 +68,12 @@ dev: dev-install
6768
run-app:
6869
go run ./cmd/reearth
6970

71+
run:
72+
${DOCKER_COMPOSE_DEV} up reearth-visualizer-dev
73+
74+
down:
75+
${DOCKER_COMPOSE_DEV} down
76+
7077
clean:
7178
go clean -modcache
7279
go clean -cache
@@ -87,6 +94,22 @@ run-reset:
8794
make run-db
8895
make mockuser
8996

97+
reset:
98+
@echo "==== Stopping database and GCS services ===="
99+
make stop-db
100+
@echo ""
101+
@echo "==== Removing data directory (requires sudo password) ===="
102+
sudo rm -rf tmp
103+
@echo ""
104+
@echo "==== Starting database and GCS services ===="
105+
make run-db
106+
@echo ""
107+
@echo "==== Waiting for services to be ready (3 seconds) ===="
108+
sleep 3
109+
@echo ""
110+
make init
111+
@echo "✓ Reset complete!"
112+
90113
up-gcs:
91114
${DOCKER_COMPOSE} up -d gcs
92115

@@ -148,10 +171,39 @@ dev-install:
148171
# Others
149172
# =======================
150173

174+
init:
175+
@echo "==== Initializing GCS bucket ===="
176+
make init-gcs
177+
@echo ""
178+
@echo "==== Waiting for GCS initialization (1 second) ===="
179+
sleep 1
180+
@echo ""
181+
@echo "==== Creating mock user ===="
182+
make mockuser
183+
@echo ""
184+
151185
mockuser:
152186
curl -H 'Content-Type: application/json' \
153187
-d '{"email": "[email protected]", "username": "Mock User"}' \
154-
http://localhost:8080/api/signup
188+
http://localhost:8080/api/signup -vvv
189+
190+
init-gcs:
191+
curl -H 'Content-Type: application/json' \
192+
-d '{"name": "test-bucket"}' \
193+
http://localhost:4443/storage/v1/b -vvv
194+
195+
destroy:
196+
@echo "⚠️ WARNING: This will remove ALL Docker resources!"
197+
@echo "This includes containers, images, volumes, and networks."
198+
@echo ""
199+
@read -p "Are you sure you want to continue? [y/N] " -n 1 -r; \
200+
echo; \
201+
if [[ $$REPLY =~ ^[Yy]$$ ]]; then \
202+
docker system prune -a --volumes -f; \
203+
echo "✓ All Docker resources have been removed"; \
204+
else \
205+
echo "✗ Operation cancelled"; \
206+
fi
155207

156208
.PHONY: default help lint test test-debug e2e build dev-install dev run-app run-db run-reset run-clean-start clean \
157209
gql mockuser schematyper deep-copy up-gcs down-gcs generate

server/internal/adapter/accounts/auth_transport.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package accounts
33
import (
44
"net/http"
55

6+
"github.com/reearth/reearth/server/internal/adapter"
67
"github.com/reearth/reearth/server/internal/adapter/internal"
8+
"github.com/reearth/reearthx/log"
79
)
810

911
type DynamicAuthTransport struct{}
@@ -15,7 +17,9 @@ func (t DynamicAuthTransport) RoundTrip(req *http.Request) (*http.Response, erro
1517
// - JWT tokens set directly in context (new auth middleware)
1618
// - JWT tokens extracted from AuthInfo (legacy auth middleware)
1719
// TODO: Remove authInfo handling once the migration is complete
18-
if jwtToken := internal.GetContextJWT(req.Context()); jwtToken != "" {
20+
if jwtToken := adapter.GetContextJWT(req.Context()); jwtToken != "" {
21+
token = jwtToken
22+
} else if jwtToken := internal.GetContextJWT(req.Context()); jwtToken != "" {
1923
token = jwtToken
2024
} else if authInfo := internal.GetContextAuthInfo(req.Context()); authInfo != nil && authInfo.Token != "" {
2125
token = authInfo.Token
@@ -25,5 +29,10 @@ func (t DynamicAuthTransport) RoundTrip(req *http.Request) (*http.Response, erro
2529
req.Header.Set("Authorization", "Bearer "+token)
2630
}
2731

28-
return http.DefaultTransport.RoundTrip(req)
32+
resp, err := http.DefaultTransport.RoundTrip(req)
33+
if err != nil {
34+
log.Errorfc(req.Context(), "[Accounts API] Request failed: %v", err)
35+
return nil, err
36+
}
37+
return resp, nil
2938
}

server/internal/adapter/context.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const (
2323
contextLang ContextKey = "lang"
2424
contextInternal ContextKey = "Internal"
2525
contextUserID ContextKey = "reearth_user"
26+
contextJWT ContextKey = "jwtToken"
2627
)
2728

2829
var defaultLang = language.English
@@ -65,6 +66,17 @@ func AttachInternal(ctx context.Context, isInternal bool) context.Context {
6566
return context.WithValue(ctx, contextInternal, isInternal)
6667
}
6768

69+
func SetContextJWT(ctx context.Context, token string) context.Context {
70+
return context.WithValue(ctx, contextJWT, token)
71+
}
72+
73+
func GetContextJWT(ctx context.Context) string {
74+
if token, ok := ctx.Value(contextJWT).(string); ok {
75+
return token
76+
}
77+
return ""
78+
}
79+
6880
func User(ctx context.Context) *user.User {
6981
if v := ctx.Value(contextUser); v != nil {
7082
if u, ok := v.(*user.User); ok {

server/internal/adapter/middleware/accounts_middleware.go

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package middleware
22

33
import (
4+
"errors"
5+
"fmt"
6+
"net/http"
7+
"strings"
8+
49
echo "github.com/labstack/echo/v4"
510
"github.com/reearth/reearth/server/internal/adapter/internal"
611
"github.com/reearth/reearth/server/internal/infrastructure/accounts"
12+
"github.com/reearth/reearth/server/internal/infrastructure/accounts/gqlerror"
13+
"github.com/reearth/reearthx/log"
714
)
815

916
type AccountsMiddlewares []echo.MiddlewareFunc
@@ -14,7 +21,8 @@ type NewAccountsMiddlewaresParam struct {
1421

1522
func NewAccountsMiddlewares(param *NewAccountsMiddlewaresParam) AccountsMiddlewares {
1623
return []echo.MiddlewareFunc{
17-
jwtContextMiddleware(),
24+
// jwtContextMiddleware(),
25+
newAccountsMiddleware(param.AccountsClient),
1826
}
1927
}
2028

@@ -23,19 +31,72 @@ type NewAccountsMiddlewaresMockParam struct {
2331
AccountsClient *accounts.Client
2432
}
2533

26-
func jwtContextMiddleware() echo.MiddlewareFunc {
34+
// func jwtContextMiddleware() echo.MiddlewareFunc {
35+
// return func(next echo.HandlerFunc) echo.HandlerFunc {
36+
// return func(c echo.Context) error {
37+
// authHeader := c.Request().Header.Get("Authorization")
38+
// if authHeader != "" {
39+
// // Remove the "Bearer " prefix from the Authorization header to extract the token
40+
// const bearerPrefix = "Bearer "
41+
// if len(authHeader) > len(bearerPrefix) && authHeader[:len(bearerPrefix)] == bearerPrefix {
42+
// token := authHeader[len(bearerPrefix):]
43+
// ctx := internal.SetContextJWT(c.Request().Context(), token)
44+
// c.SetRequest(c.Request().WithContext(ctx))
45+
// }
46+
// }
47+
// }
48+
// }
49+
// }
50+
51+
var accountsMiddlewareSkipPaths = []string{
52+
"/api/ping",
53+
"/api/published/",
54+
"/plugins/",
55+
"/assets/",
56+
"/favicon.ico",
57+
}
58+
59+
func shouldSkipAccountsMiddleware(method, path string) bool {
60+
// Only skip authentication for GET requests
61+
if method != http.MethodGet {
62+
return false
63+
}
64+
65+
for _, skipPath := range accountsMiddlewareSkipPaths {
66+
if skipPath == path || strings.HasPrefix(path, skipPath) {
67+
return true
68+
}
69+
}
70+
return false
71+
}
72+
73+
func newAccountsMiddleware(accountsClient *accounts.Client) echo.MiddlewareFunc {
2774
return func(next echo.HandlerFunc) echo.HandlerFunc {
2875
return func(c echo.Context) error {
29-
authHeader := c.Request().Header.Get("Authorization")
30-
if authHeader != "" {
31-
// Remove the "Bearer " prefix from the Authorization header to extract the token
32-
const bearerPrefix = "Bearer "
33-
if len(authHeader) > len(bearerPrefix) && authHeader[:len(bearerPrefix)] == bearerPrefix {
34-
token := authHeader[len(bearerPrefix):]
35-
ctx := internal.SetContextJWT(c.Request().Context(), token)
36-
c.SetRequest(c.Request().WithContext(ctx))
76+
77+
if shouldSkipAccountsMiddleware(c.Request().Method, c.Request().URL.Path) {
78+
return next(c)
79+
}
80+
81+
ctx := c.Request().Context()
82+
83+
// TODO: Optimize performance by including necessary user information (userID, email, etc.)
84+
// directly in the JWT token instead of executing a GQL query on every request.
85+
// This will eliminate the overhead of making an API call to fetch user data for each request.
86+
u, err := accountsClient.UserRepo.FindMe(ctx)
87+
if err != nil {
88+
if errors.Is(err, gqlerror.ErrUnauthorized) {
89+
return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized: user not found")
3790
}
91+
log.Errorc(ctx, fmt.Errorf("[Accounts Middleware] failed to fetch user: %w", err))
92+
return echo.NewHTTPError(http.StatusInternalServerError, "server error: failed to fetch user")
3893
}
94+
if u == nil {
95+
return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized: user not found")
96+
}
97+
98+
ctx = internal.SetContextUser(ctx, u)
99+
c.SetRequest(c.Request().WithContext(ctx))
39100
return next(c)
40101
}
41102
}

0 commit comments

Comments
 (0)