Skip to content
Merged
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
8 changes: 8 additions & 0 deletions backend/.env.sample
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Port
PORT=3003

# Db address
DB_ADDRESS=data/sqlite3.db

Expand All @@ -22,3 +25,8 @@ FRONTEND_URL=https://c2r5p11.hive.fi:5173

# 2FA
TWO_FA_URL_PREFIX=otpauth://totp/aaa?secret=

# Rate Limiter
RATE_LIMITER_DURATION_IN_SECONDS=60
RATE_LIMITER_REQUEST_LIMIT=1000
RATE_LIMITER_CLEANUP_INTERVAL_IN_SECONDS=300
64 changes: 45 additions & 19 deletions backend/cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package main

import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"

swaggerfiles "github.com/swaggo/files"
Expand All @@ -12,10 +16,9 @@ import (
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
_ "github.com/paularynty/transcendence/auth-service-go/docs"
"github.com/paularynty/transcendence/auth-service-go/internal/config"
"github.com/paularynty/transcendence/auth-service-go/internal/db"
"github.com/paularynty/transcendence/auth-service-go/internal/dto"
"github.com/paularynty/transcendence/auth-service-go/internal/routers"
"github.com/paularynty/transcendence/auth-service-go/internal/service"
"github.com/paularynty/transcendence/auth-service-go/internal/util"

"log/slog"
Expand Down Expand Up @@ -56,7 +59,7 @@ func SetupRouter(dep *dependency.Dependency) *gin.Engine {
MaxAge: 12 * time.Hour,
}))

rateLimiter := middleware.NewRateLimiter(60*time.Second, 1000)
rateLimiter := middleware.NewRateLimiter(time.Duration(dep.Cfg.RateLimiterDurationInSec)*time.Second, dep.Cfg.RateLimiterRequestLimit, time.Duration(dep.Cfg.RateLimiterCleanupIntervalInSec)*time.Second)
r.Use(rateLimiter.RateLimit())

r.Use(middleware.PanicHandler())
Expand All @@ -66,15 +69,6 @@ func SetupRouter(dep *dependency.Dependency) *gin.Engine {
return r
}

func initDependency() *dependency.Dependency {
logger := util.GetLogger(slog.LevelInfo)
cfg := config.LoadConfigFromEnv()
myDB := db.GetDB(cfg.DbAddress, logger)
redis := db.GetRedis(cfg.RedisURL, cfg, logger)

return dependency.NewDependency(cfg, myDB, redis, logger)
}

// @title Auth Service API
// @version 1.0
// @description Auth service
Expand All @@ -83,17 +77,28 @@ func main() {
// config
_ = godotenv.Load()

// logger
logger := util.GetLogger(slog.LevelDebug)

// init dependency
dep := initDependency()
defer db.CloseDB(dep.DB, dep.Logger)
defer db.CloseRedis(dep.Redis, dep.Logger)
dep, err := dependency.InitDependency(logger)
if err != nil {
util.LogFatalErr(logger, err, "failed to create dependency")
}
defer dependency.CloseDependency(dep)

// validator
dto.InitValidator()

// create services
userService, err := service.NewUserService(dep)
if err != nil {
util.LogFatalErr(logger, err, "failed to create user service")
}

// router
r := SetupRouter(dep)
routers.UsersRouter(r.Group("/api/users"), dep)
routers.UsersRouter(r.Group("/api/users"), userService)

// Health check
r.GET("/api/ping", func(c *gin.Context) {
Expand All @@ -105,8 +110,29 @@ func main() {
// Swagger
r.GET("/api/docs/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))

if err := r.Run(":3003"); err != nil {
dep.Logger.Error("failed to start server", "err", err)
os.Exit(1)
// http server
srv := &http.Server{
Addr: fmt.Sprintf(":%d", dep.Cfg.Port),
Handler: r,
}

// Start server
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
util.LogFatalErr(logger, err, "failed to start server")
}
}()

// Graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit // consume the signal, blocking here
logger.Info("shutting down server...")

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
util.LogFatalErr(logger, err, "server forced to shutdown")
}
logger.Info("server exiting")
}
17 changes: 17 additions & 0 deletions backend/internal/auth_error/auth_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package authError

type AuthError struct {
Status int
Message string
}

func (e *AuthError) Error() string {
return e.Message
}

func NewAuthError(status int, message string) *AuthError {
return &AuthError{
Status: status,
Message: message,
}
}
90 changes: 57 additions & 33 deletions backend/internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
package config

import (
"fmt"
"os"
"strconv"
)

type Config struct {
GinMode string
DbAddress string
JwtSecret string
UserTokenExpiry int
OauthStateTokenExpiry int
GoogleClientId string
GoogleClientSecret string
GoogleRedirectUri string
FrontendUrl string
TwoFaUrlPrefix string
TwoFaTokenExpiry int
RedisURL string
IsRedisEnabled bool
UserTokenAbsoluteExpiry int
GinMode string
DbAddress string
JwtSecret string
UserTokenExpiry int
OauthStateTokenExpiry int
GoogleClientId string
GoogleClientSecret string
GoogleRedirectUri string
FrontendUrl string
TwoFaUrlPrefix string
TwoFaTokenExpiry int
RedisURL string
IsRedisEnabled bool
UserTokenAbsoluteExpiry int
Port int
RateLimiterDurationInSec int
RateLimiterRequestLimit int
RateLimiterCleanupIntervalInSec int
}

func getEnvStrOrDefault(key string, defaultValue string) string {
Expand All @@ -32,14 +37,14 @@ func getEnvStrOrDefault(key string, defaultValue string) string {
return value
}

func getEnvStrOrPanic(key string) string {
func getEnvStrOrError(key string) (string, error) {
value := os.Getenv(key)

if value == "" {
panic("environment variable " + key + " is required but not set")
return "", fmt.Errorf("environment variable %s is required but not set", key)
}

return value
return value, nil
}

func getEnvIntOrDefault(key string, defaultValue int) int {
Expand All @@ -53,21 +58,40 @@ func getEnvIntOrDefault(key string, defaultValue int) int {
return intValue
}

func LoadConfigFromEnv() *Config {
return &Config{
GinMode: getEnvStrOrDefault("GIN_MODE", "debug"),
DbAddress: getEnvStrOrDefault("DB_ADDRESS", "data/auth_service_db.sqlite"),
JwtSecret: getEnvStrOrPanic("JWT_SECRET"),
UserTokenExpiry: getEnvIntOrDefault("USER_TOKEN_EXPIRY", 3600),
OauthStateTokenExpiry: getEnvIntOrDefault("OAUTH_STATE_TOKEN_EXPIRY", 600),
GoogleClientId: getEnvStrOrPanic("GOOGLE_CLIENT_ID"),
GoogleClientSecret: getEnvStrOrPanic("GOOGLE_CLIENT_SECRET"),
GoogleRedirectUri: getEnvStrOrDefault("GOOGLE_REDIRECT_URI", "test-google-redirect-uri"),
FrontendUrl: getEnvStrOrDefault("FRONTEND_URL", "http://localhost:5173"),
TwoFaUrlPrefix: getEnvStrOrDefault("TWO_FA_URL_PREFIX", "otpauth://totp/Transcendence?secret="),
TwoFaTokenExpiry: getEnvIntOrDefault("TWO_FA_TOKEN_EXPIRY", 600),
RedisURL: getEnvStrOrDefault("REDIS_URL", ""),
IsRedisEnabled: getEnvStrOrDefault("REDIS_URL", "") != "",
UserTokenAbsoluteExpiry: getEnvIntOrDefault("USER_TOKEN_ABSOLUTE_EXPIRY", 2592000),
func LoadConfigFromEnv() (*Config, error) {
jwtSecret, err := getEnvStrOrError("JWT_SECRET")
if err != nil {
return nil, err
}

GoogleClientId, err := getEnvStrOrError("GOOGLE_CLIENT_ID")
if err != nil {
return nil, err
}

GoogleClientSecret, err := getEnvStrOrError("GOOGLE_CLIENT_SECRET")
if err != nil {
return nil, err
}

return &Config{
GinMode: getEnvStrOrDefault("GIN_MODE", "debug"),
DbAddress: getEnvStrOrDefault("DB_ADDRESS", "data/auth_service_db.sqlite"),
JwtSecret: jwtSecret,
UserTokenExpiry: getEnvIntOrDefault("USER_TOKEN_EXPIRY", 3600),
OauthStateTokenExpiry: getEnvIntOrDefault("OAUTH_STATE_TOKEN_EXPIRY", 600),
GoogleClientId: GoogleClientId,
GoogleClientSecret: GoogleClientSecret,
GoogleRedirectUri: getEnvStrOrDefault("GOOGLE_REDIRECT_URI", "test-google-redirect-uri"),
FrontendUrl: getEnvStrOrDefault("FRONTEND_URL", "http://localhost:5173"),
TwoFaUrlPrefix: getEnvStrOrDefault("TWO_FA_URL_PREFIX", "otpauth://totp/Transcendence?secret="),
TwoFaTokenExpiry: getEnvIntOrDefault("TWO_FA_TOKEN_EXPIRY", 600),
RedisURL: getEnvStrOrDefault("REDIS_URL", ""),
IsRedisEnabled: getEnvStrOrDefault("REDIS_URL", "") != "",
UserTokenAbsoluteExpiry: getEnvIntOrDefault("USER_TOKEN_ABSOLUTE_EXPIRY", 2592000),
Port: getEnvIntOrDefault("PORT", 3003),
RateLimiterDurationInSec: getEnvIntOrDefault("RATE_LIMITER_DURATION_IN_SECONDS", 60),
RateLimiterRequestLimit: getEnvIntOrDefault("RATE_LIMITER_REQUEST_LIMIT", 1000),
RateLimiterCleanupIntervalInSec: getEnvIntOrDefault("RATE_LIMITER_CLEANUP_INTERVAL_IN_SECONDS", 300),
}, nil
}
61 changes: 25 additions & 36 deletions backend/internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,18 @@ import (
"testing"
)

func assertPanics(t *testing.T, fn func(), name string) {
func assertError(t *testing.T, err error, name string) {
t.Helper()
defer func() {
if r := recover(); r == nil {
t.Fatalf("expected panic for %s", name)
}
}()
fn()
if err == nil {
t.Fatalf("expected error for %s", name)
}
}

func assertNotPanics(t *testing.T, fn func(), name string) {
func assertNoError(t *testing.T, err error, name string) {
t.Helper()
defer func() {
if r := recover(); r != nil {
t.Fatalf("unexpected panic for %s: %v", name, r)
}
}()
fn()
if err != nil {
t.Fatalf("unexpected error for %s: %v", name, err)
}
}

func TestGetEnvStrOrDefault(t *testing.T) {
Expand All @@ -36,18 +30,17 @@ func TestGetEnvStrOrDefault(t *testing.T) {
}
}

func TestGetEnvStrOrPanic(t *testing.T) {
func TestGetEnvStrOrError(t *testing.T) {
t.Setenv("TEST_PANIC", "")
assertPanics(t, func() {
_ = getEnvStrOrPanic("TEST_PANIC")
}, "empty env")
_, err := getEnvStrOrError("TEST_PANIC")
assertError(t, err, "empty env")

t.Setenv("TEST_PANIC", "value")
assertNotPanics(t, func() {
if got := getEnvStrOrPanic("TEST_PANIC"); got != "value" {
t.Fatalf("expected env value, got %q", got)
}
}, "set env")
got, err := getEnvStrOrError("TEST_PANIC")
assertNoError(t, err, "set env")
if got != "value" {
t.Fatalf("expected env value, got %q", got)
}
}

func TestGetEnvIntOrDefault(t *testing.T) {
Expand All @@ -67,29 +60,25 @@ func TestGetEnvIntOrDefault(t *testing.T) {
}
}

func TestLoadConfigFromEnv_PanicsOnMissingRequired(t *testing.T) {
func TestLoadConfigFromEnv_ErrsOnMissingRequired(t *testing.T) {
t.Setenv("JWT_SECRET", "jwt")
t.Setenv("GOOGLE_CLIENT_ID", "client")
t.Setenv("GOOGLE_CLIENT_SECRET", "secret")

assertNotPanics(t, func() {
_ = LoadConfigFromEnv()
}, "all required set")
_, err := LoadConfigFromEnv()
assertNoError(t, err, "all required set")

t.Setenv("JWT_SECRET", "")
assertPanics(t, func() {
_ = LoadConfigFromEnv()
}, "JWT_SECRET unset")
_, err = LoadConfigFromEnv()
assertError(t, err, "JWT_SECRET unset")

t.Setenv("JWT_SECRET", "jwt")
t.Setenv("GOOGLE_CLIENT_ID", "")
assertPanics(t, func() {
_ = LoadConfigFromEnv()
}, "GOOGLE_CLIENT_ID unset")
_, err = LoadConfigFromEnv()
assertError(t, err, "GOOGLE_CLIENT_ID unset")

t.Setenv("GOOGLE_CLIENT_ID", "client")
t.Setenv("GOOGLE_CLIENT_SECRET", "")
assertPanics(t, func() {
_ = LoadConfigFromEnv()
}, "GOOGLE_CLIENT_SECRET unset")
_, err = LoadConfigFromEnv()
assertError(t, err, "GOOGLE_CLIENT_SECRET unset")
}
Loading
Loading