Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3933fe5
chore/backend: add redis packages, and env
danielxfeng Jan 27, 2026
b0e6107
feat/backend: add config, and init redis
danielxfeng Jan 27, 2026
e9f787b
fix/backend: panic when redis cannot be inited
danielxfeng Jan 27, 2026
fce4dcb
feat/backend: integrate Redis for user token management and heartbeat
danielxfeng Jan 27, 2026
0b67834
fix/backend: fix the tests for redis
danielxfeng Jan 27, 2026
3964c1b
test/backend: add some tests for redis integration
danielxfeng Jan 27, 2026
d881dfd
docs: update README to include Redis configuration and features
danielxfeng Jan 27, 2026
4ec7750
chore/backend: format
danielxfeng Jan 27, 2026
41d1866
feat/backend: add tests for user logout and token revocation in Redis
danielxfeng Jan 27, 2026
03cbe89
test/backend: add more tests for duplicated email
danielxfeng Jan 27, 2026
fafb3e8
chore/backend: add redis packages, and env
danielxfeng Jan 27, 2026
5a43a94
feat/backend: add config, and init redis
danielxfeng Jan 27, 2026
3038d7d
fix/backend: panic when redis cannot be inited
danielxfeng Jan 27, 2026
a758af7
feat/backend: integrate Redis for user token management and heartbeat
danielxfeng Jan 27, 2026
4698ba0
fix/backend: fix the tests for redis
danielxfeng Jan 27, 2026
2d5b52b
test/backend: add some tests for redis integration
danielxfeng Jan 27, 2026
d860200
docs: update README to include Redis configuration and features
danielxfeng Jan 27, 2026
f3c58a4
chore/backend: format
danielxfeng Jan 27, 2026
8dedd6e
feat/backend: add tests for user logout and token revocation in Redis
danielxfeng Jan 27, 2026
cc24fe9
test/backend: add more tests for duplicated email
danielxfeng Jan 27, 2026
c04d7a4
Merge branch 'feat-backend-redis' of github.com:danielxfeng/auth-user…
danielxfeng Jan 27, 2026
bbc04fe
test/backend: update Google OAuth callback tests for same-email linki…
danielxfeng Jan 27, 2026
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
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@ Currently supported features include:
- Friend listing
- Friend requests
- Online status tracking
- Redis-backed session tokens (Optional) (revocation + sliding expiration)
- Redis-backed heartbeats for online status (Optional)

## Libraries

### Backend

- `gin`: web framework
- `gorm`: ORM
- `go-redis`: Redis
- `go-playground/validator v10`: data validation
- `godotenv`: environment variables
- `slog-gin`: logging
Expand Down Expand Up @@ -77,6 +80,21 @@ make dev

Then navigate to `http://localhost:3003/api/docs/index.html` for swagger.

Redis is optional. To enable it locally:

```bash
# example: run redis with docker
docker run --rm -p 6379:6379 redis:latest

# enable redis mode for the backend
export REDIS_URL=redis://localhost:6379/0
```

Token extension (sliding expiration) in Redis mode:

- `USER_TOKEN_EXPIRY` controls the Redis TTL and is extended on token validation.
- `USER_TOKEN_ABSOLUTE_EXPIRY` caps the maximum lifetime via the JWT `exp` claim.

### Frontend

```bash
Expand All @@ -93,6 +111,6 @@ Due to the constraints of the Hive project, `SQLite` was required for the projec

As a result:

- `SQLite` is used to store authentication tokens and heartbeat data. In production, these would be better handled by `Redis`.
- Stale tokens and heartbeat data are not automatically cleaned up, and token auto-renewal is not implemented.
- The project still uses `SQLite` for core data due to Hive constraints.
- Redis-backed tokens and heartbeats are implemented, but the sliding expiration and cleanup strategy is simple.
- On the frontend side, friend auto-completion is implemented in a basic manner.
7 changes: 6 additions & 1 deletion backend/.env.sample
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# Db address
DB_ADDRESS=data/sqlite3.db

# Redis address, leave empty to disable Redis
REDIS_URL=rediss://example-redis.upstash.io

# JWT
JWT_SECRET=not-dev-secret
USER_TOKEN_EXPIRY=3600
USER_TOKEN_EXPIRY=3600 # 1 hour
TWO_FA_TOKEN_EXPIRY=600
OAUTH_STATE_TOKEN_EXPIRY=300
# Sliding expiration is enabled under Redis mode, while this limits the longest possible expiry time.
USER_TOKEN_ABSOLUTE_EXPIRY=2592000 # 30 days

# Google OAuth
GOOGLE_CLIENT_ID=100000000-e3dsadsadsa321321.apps.googleusercontent.com
Expand Down
3 changes: 3 additions & 0 deletions backend/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ func main() {
db.ConnectDB(config.Cfg.DbAddress)
defer db.CloseDB()

db.ConnectRedis(config.Cfg.RedisURL)
defer db.CloseRedis()

// router
r := SetupRouter(util.Logger)
routers.UsersRouter(r.Group("/api/users"))
Expand Down
8 changes: 8 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ require (
gorm.io/gorm v1.31.1
)

require (
github.com/alicebob/miniredis/v2 v2.36.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
)

require (
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
Expand Down Expand Up @@ -63,6 +70,7 @@ require (
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.58.0 // indirect
github.com/redis/go-redis/v9 v9.17.3
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
Expand Down
10 changes: 10 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI=
github.com/alicebob/miniredis/v2 v2.36.1/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
Expand All @@ -12,11 +14,15 @@ github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPII
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
Expand Down Expand Up @@ -125,6 +131,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/samber/slog-gin v1.19.0 h1:zwlPQhwvi3o1lufsfVSoyCaFHMWfUCsJD0mcb+1P6WQ=
Expand All @@ -151,6 +159,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
Expand Down
50 changes: 28 additions & 22 deletions backend/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ import (
)

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
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
}

var Cfg *Config
Expand Down Expand Up @@ -44,16 +47,19 @@ func getEnvIntOrDefault(key string, defaultValue int) int {

func LoadConfig() {
Cfg = &Config{
GinMode: getEnvStrOrDefault("GIN_MODE", "debug"),
DbAddress: getEnvStrOrDefault("DB_ADDRESS", "data/auth_service_db.sqlite"),
JwtSecret: getEnvStrOrDefault("JWT_SECRET", "test-secret"),
UserTokenExpiry: getEnvIntOrDefault("USER_TOKEN_EXPIRY", 3600),
OauthStateTokenExpiry: getEnvIntOrDefault("OAUTH_STATE_TOKEN_EXPIRY", 600),
GoogleClientId: getEnvStrOrDefault("GOOGLE_CLIENT_ID", "test-google-client-id"),
GoogleClientSecret: getEnvStrOrDefault("GOOGLE_CLIENT_SECRET", "test-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),
GinMode: getEnvStrOrDefault("GIN_MODE", "debug"),
DbAddress: getEnvStrOrDefault("DB_ADDRESS", "data/auth_service_db.sqlite"),
JwtSecret: getEnvStrOrDefault("JWT_SECRET", "test-secret"),
UserTokenExpiry: getEnvIntOrDefault("USER_TOKEN_EXPIRY", 3600),
OauthStateTokenExpiry: getEnvIntOrDefault("OAUTH_STATE_TOKEN_EXPIRY", 600),
GoogleClientId: getEnvStrOrDefault("GOOGLE_CLIENT_ID", "test-google-client-id"),
GoogleClientSecret: getEnvStrOrDefault("GOOGLE_CLIENT_SECRET", "test-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),
}
}
50 changes: 50 additions & 0 deletions backend/internal/db/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package db

import (
"context"

"github.com/paularynty/transcendence/auth-service-go/internal/config"
"github.com/paularynty/transcendence/auth-service-go/internal/util"
"github.com/redis/go-redis/v9"
)

var Redis *redis.Client

func ConnectRedis(redisURL string) {
if !config.Cfg.IsRedisEnabled {
util.Logger.Info("redis is disabled by config")
return
}

opt, err := redis.ParseURL(redisURL)

if err != nil {
panic("failed to parse redis url, err: " + err.Error())
}

client := redis.NewClient(opt)

ctx := context.Background()

_, err = client.Ping(ctx).Result()
if err != nil {
panic("failed to connect to redis: " + err.Error())
}

Redis = client

util.Logger.Info("connected to redis")
}

func CloseRedis() {
if Redis == nil {
return
}

err := Redis.Close()
if err != nil {
util.Logger.Error("failed to close redis connection", "error", err)
} else {
util.Logger.Info("redis connection closed")
}
}
2 changes: 1 addition & 1 deletion backend/internal/routers/users_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

func UsersRouter(r *gin.RouterGroup) {
h := &handler.UserHandler{Service: service.NewUserService(db.DB)}
h := &handler.UserHandler{Service: service.NewUserService(db.DB, db.Redis)}

// Public endpoints
r.POST("/", middleware.ValidateBody[dto.CreateUserRequest](), h.CreateUserHandler)
Expand Down
8 changes: 4 additions & 4 deletions backend/internal/routers/users_router_failure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func TestUsersRouter_LoginUser_Failures(t *testing.T) {

// 3. Invalid Credentials
// Create user
svc := service.NewUserService(model.DB)
svc := service.NewUserService(model.DB, nil)
_, _ = svc.CreateUser(context.Background(), &dto.CreateUserRequest{
User: dto.User{UserName: dto.UserName{Username: "loginfail"}, Email: "fail@e.com"},
Password: dto.Password{Password: "correct"},
Expand All @@ -168,7 +168,7 @@ func TestUsersRouter_UpdateUser_Failures(t *testing.T) {
router, cleanup := setupUsersRouterTestFailure(t)
defer cleanup()

svc := service.NewUserService(model.DB)
svc := service.NewUserService(model.DB, nil)
u, _ := svc.CreateUser(context.Background(), &dto.CreateUserRequest{
User: dto.User{UserName: dto.UserName{Username: "u1"}, Email: "u1@e.com"},
Password: dto.Password{Password: "pass"},
Expand Down Expand Up @@ -216,7 +216,7 @@ func TestUsersRouter_Friends_Failures(t *testing.T) {
router, cleanup := setupUsersRouterTestFailure(t)
defer cleanup()

svc := service.NewUserService(model.DB)
svc := service.NewUserService(model.DB, nil)
u1, _ := svc.CreateUser(context.Background(), &dto.CreateUserRequest{
User: dto.User{UserName: dto.UserName{Username: "f1"}, Email: "f1@e.com"},
Password: dto.Password{Password: "pass"},
Expand Down Expand Up @@ -274,7 +274,7 @@ func TestUsersRouter_2FA_Failures(t *testing.T) {
router, cleanup := setupUsersRouterTestFailure(t)
defer cleanup()

svc := service.NewUserService(model.DB)
svc := service.NewUserService(model.DB, nil)
u, _ := svc.CreateUser(context.Background(), &dto.CreateUserRequest{
User: dto.User{UserName: dto.UserName{Username: "2fafail"}, Email: "2fafail@e.com"},
Password: dto.Password{Password: "pass"},
Expand Down
Loading
Loading