From 25817caed0869d173271faf87a295628d2ca2637 Mon Sep 17 00:00:00 2001 From: konsalex Date: Wed, 7 Oct 2026 22:08:27 +0200 Subject: [PATCH] feat: save bandwidth for clients --- backend/internal/handlers/handlers.go | 78 ++++++---- backend/internal/handlers/slackHandlers.go | 6 +- backend/internal/handlers/utils.go | 53 +++++-- .../internal/handlers/websocketHandlers.go | 8 +- backend/internal/models/user.go | 146 +++++++++++------- backend/internal/server/server.go | 19 ++- backend/test/integration/integration_test.go | 14 +- 7 files changed, 205 insertions(+), 119 deletions(-) diff --git a/backend/internal/handlers/handlers.go b/backend/internal/handlers/handlers.go index d2a423e5..7ee35d6e 100644 --- a/backend/internal/handlers/handlers.go +++ b/backend/internal/handlers/handlers.go @@ -190,12 +190,14 @@ func (h *AuthHandler) SocialLoginCallback(c echo.Context) error { } u = models.User{ - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email, - AvatarURL: user.AvatarURL, - TeamID: assignedTeamID, - IsAdmin: isAdmin, + UserProfile: models.UserProfile{ + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + AvatarURL: user.AvatarURL, + TeamID: assignedTeamID, + IsAdmin: isAdmin, + }, } if err := tx.Create(&u).Error; err != nil { return fmt.Errorf("failed to create user: %w", err) @@ -390,10 +392,12 @@ func (h *AuthHandler) ManualSignUp(c echo.Context) error { } u := &models.User{ - FirstName: req.FirstName, - LastName: req.LastName, - Email: req.Email, - Password: req.Password, + UserProfile: models.UserProfile{ + FirstName: req.FirstName, + LastName: req.LastName, + Email: req.Email, + }, + Password: req.Password, } if burner.IsBurnerEmail(u.Email) { @@ -679,7 +683,9 @@ func (h *AuthHandler) AuthenticateApp(c echo.Context) error { } func (h *AuthHandler) User(c echo.Context) error { - user, isAuthenticated := h.getAuthenticatedUserFromJWT(c) + // Full row: the /user response includes metadata, email_subscriptions and + // unsubscribe_id, which are not part of the slim UserProfile. + user, isAuthenticated := h.getAuthenticatedFullUserFromJWT(c) if !isAuthenticated { return c.String(http.StatusUnauthorized, "Unauthorized here") } @@ -729,7 +735,7 @@ func (h *AuthHandler) GenerateDebugCallToken(c echo.Context) error { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return c.String(http.StatusNotFound, "User not found") } - tokens, err := generateLiveKitTokens(&h.ServerState, "random-name-for-now", &user) + tokens, err := generateLiveKitTokens(&h.ServerState, "random-name-for-now", &user.UserProfile) if err != nil { return c.String(http.StatusInternalServerError, "Failed to generate callee tokens") } @@ -756,14 +762,19 @@ func (h *AuthHandler) UpdateName(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } - user.FirstName = req.FirstName - user.LastName = req.LastName - - if err := h.DB.Save(user).Error; err != nil { + // Targeted column update so the slim profile never overwrites the wide + // columns (hashed_password, metadata, ...) that it does not carry. + if err := h.DB.Model(&models.User{}).Where("id = ?", user.ID).Updates(map[string]interface{}{ + "first_name": req.FirstName, + "last_name": req.LastName, + }).Error; err != nil { c.Logger().Error("Failed to save to db:", err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to update user") } + user.FirstName = req.FirstName + user.LastName = req.LastName + return c.JSON(http.StatusOK, user) } @@ -985,7 +996,9 @@ func (h *AuthHandler) SendTeamInvites(c echo.Context) error { // UpdateOnboardingFormStatus updates the user's metadata to mark the onboarding form as completed func (h *AuthHandler) UpdateOnboardingFormStatus(c echo.Context) error { - user, isAuthenticated := h.getAuthenticatedUserFromJWT(c) + // Full row: this handler merges into the JSON `metadata` column, which is + // not part of the slim UserProfile. + user, isAuthenticated := h.getAuthenticatedFullUserFromJWT(c) if !isAuthenticated { return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized") } @@ -1059,7 +1072,6 @@ func (h *AuthHandler) CreateRoom(c echo.Context) error { room := models.Room{ Name: req.Name, UserID: user.ID, - Team: user.Team, TeamID: user.TeamID, } @@ -1290,7 +1302,9 @@ func (h *AuthHandler) GetLivekitServerURL(c echo.Context) error { // SubscribeToLinuxWaitingList subscribes the user to the Linux waiting list // and unsubscribes from marketing emails func (h *AuthHandler) SubscribeToLinuxWaitingList(c echo.Context) error { - user, isAuthenticated := h.getAuthenticatedUserFromJWT(c) + // Full row: this handler mutates the JSON `email_subscriptions` column, which + // is not part of the slim UserProfile. + user, isAuthenticated := h.getAuthenticatedFullUserFromJWT(c) if !isAuthenticated { return c.String(http.StatusUnauthorized, "Unauthorized request") } @@ -1352,14 +1366,18 @@ func (h *AuthHandler) ChangeTeam(c echo.Context) error { c.Logger().Infof("Changing user %s team to %d", user.ID, invitation.TeamID) teamID := uint(invitation.TeamID) - user.TeamID = &teamID - user.Team = &invitation.Team - user.IsAdmin = false - - if err := h.DB.Save(&user).Error; err != nil { + // Targeted column update so the slim profile never overwrites the wide + // columns it does not carry. + if err := h.DB.Model(&models.User{}).Where("id = ?", user.ID).Updates(map[string]interface{}{ + "team_id": teamID, + "is_admin": false, + }).Error; err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to update user team") } + user.TeamID = &teamID + user.IsAdmin = false + return c.JSON(http.StatusOK, map[string]interface{}{ "message": "Successfully changed team", "team_name": invitation.Team.Name, @@ -1379,14 +1397,10 @@ func (h *AuthHandler) RemoveTeammate(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "User is not part of any team") } - // Preload team to avoid extra query for email - if err := h.DB.Preload("Team").Where("id = ?", user.ID).First(user).Error; err != nil { + team, err := models.GetTeamByID(h.DB, strconv.Itoa(int(*user.TeamID))) + if err != nil { c.Logger().Error("Failed to load user team:", err) - return echo.NewHTTPError(http.StatusInternalServerError, "failed to load user") - } - - if user.Team == nil { - return echo.NewHTTPError(http.StatusBadRequest, "User team not found") + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to load user team") } teammateID := c.Param("userId") @@ -1414,7 +1428,7 @@ func (h *AuthHandler) RemoveTeammate(c echo.Context) error { return echo.NewHTTPError(http.StatusForbidden, "user not in your team") } - oldTeamName := user.Team.Name + oldTeamName := team.Name var newTeamName string if err := h.DB.Transaction(func(tx *gorm.DB) error { diff --git a/backend/internal/handlers/slackHandlers.go b/backend/internal/handlers/slackHandlers.go index ab650038..17f69077 100644 --- a/backend/internal/handlers/slackHandlers.go +++ b/backend/internal/handlers/slackHandlers.go @@ -667,7 +667,7 @@ func lookupSlackUserByEmail(botToken, email string) (string, error) { // It first tries to look up the user's Slack ID by email. If found, uses the native // Slack user format (which enables automatic status updates). Falls back to external // user format if the email lookup fails. -func addParticipantToSlackCall(botToken, callID string, user *models.User) error { +func addParticipantToSlackCall(botToken, callID string, user *models.UserProfile) error { api := newSlackClient(botToken) var participant slack.CallParticipant @@ -722,7 +722,7 @@ func (h *SlackHandler) createSlackCall(botToken, externalID, createdBySlackUserI // removeParticipantFromSlackCall removes a user from a Slack call. // It uses the user's email to look up their Slack ID, falling back to external_id. -func removeParticipantFromSlackCall(botToken, callID string, user *models.User) error { +func removeParticipantFromSlackCall(botToken, callID string, user *models.UserProfile) error { api := newSlackClient(botToken) var participant slack.CallParticipant @@ -793,7 +793,7 @@ func (h *SlackHandler) LeaveRoom(c echo.Context) error { } }() } else { - h.logger.Warnf("Failed to get Slack metadata for participant removal for team %s", user.Team) + h.logger.Warnf("Failed to get Slack metadata for participant removal for team %v", user.TeamID) } } diff --git a/backend/internal/handlers/utils.go b/backend/internal/handlers/utils.go index dd653524..040be6ec 100644 --- a/backend/internal/handlers/utils.go +++ b/backend/internal/handlers/utils.go @@ -75,7 +75,7 @@ func getTeamMembersRawJSON(accessToken string) ([]byte, error) { return body, nil } -func generateLiveKitTokens(s *common.ServerState, roomName string, participant *models.User) (livekitutil.LivekitTokenSet, error) { +func generateLiveKitTokens(s *common.ServerState, roomName string, participant *models.UserProfile) (livekitutil.LivekitTokenSet, error) { // Create an access token (make sure these are loaded from your config) videoID := fmt.Sprintf("room:%s:%s:video", roomName, participant.ID) audioID := fmt.Sprintf("room:%s:%s:audio", roomName, participant.ID) @@ -137,15 +137,39 @@ func generateLiveKitTokens(s *common.ServerState, roomName string, participant * }, nil } -// GetAuthenticatedUser returns the authenticated user from the session -// Returns nil and false if the user is not authenticated or not found -func getAuthenticatedUserFromJWTCommon(c echo.Context, jwtIssuer common.JWTIssuer, db *gorm.DB) (*models.User, bool) { +// getAuthenticatedUserFromJWTCommon returns the authenticated user's slim +// profile from the JWT. It fetches only the UserProfile columns, which is all +// the read-only request paths need, and keeps the wide `users` row off the wire +// on hot endpoints. Returns nil and false if the user is not authenticated or +// not found. +// +// The returned *models.UserProfile is a partial record: never pass it to +// db.Save. Handlers that mutate JSON columns or need the full row must use +// getAuthenticatedFullUserFromJWT instead. +func getAuthenticatedUserFromJWTCommon(c echo.Context, jwtIssuer common.JWTIssuer, db *gorm.DB) (*models.UserProfile, bool) { + email, err := jwtIssuer.GetUserEmail(c) + if err != nil { + return nil, false + } + + // Fetch user profile from database + user, err := models.GetUserProfileByEmail(db, email) + if err != nil { + return nil, false + } + + return user, true +} + +// getAuthenticatedFullUserFromJWTCommon returns the full authenticated user row. +// Use this only on paths that read or write fields outside UserProfile +// (metadata, email_subscriptions, hashed_password, ...). +func getAuthenticatedFullUserFromJWTCommon(c echo.Context, jwtIssuer common.JWTIssuer, db *gorm.DB) (*models.User, bool) { email, err := jwtIssuer.GetUserEmail(c) if err != nil { return nil, false } - // Fetch user from database user, err := models.GetUserByEmail(db, email) if err != nil { return nil, false @@ -154,11 +178,15 @@ func getAuthenticatedUserFromJWTCommon(c echo.Context, jwtIssuer common.JWTIssue return user, true } -func (h *AuthHandler) getAuthenticatedUserFromJWT(c echo.Context) (*models.User, bool) { +func (h *AuthHandler) getAuthenticatedUserFromJWT(c echo.Context) (*models.UserProfile, bool) { return getAuthenticatedUserFromJWTCommon(c, h.JwtIssuer, h.DB) } -func (bh *BillingHandler) getAuthenticatedUserFromJWT(c echo.Context) (*models.User, bool) { +func (h *AuthHandler) getAuthenticatedFullUserFromJWT(c echo.Context) (*models.User, bool) { + return getAuthenticatedFullUserFromJWTCommon(c, h.JwtIssuer, h.DB) +} + +func (bh *BillingHandler) getAuthenticatedUserFromJWT(c echo.Context) (*models.UserProfile, bool) { return getAuthenticatedUserFromJWTCommon(c, bh.JwtIssuer, bh.DB) } @@ -172,21 +200,16 @@ func sameTeam(a, b *uint) bool { // checkUserHasAccess checks if a user has an active subscription or trial. // Returns true if the user is a Pro subscriber or has an active trial, false otherwise. // Returns an error if the subscription check fails. -func checkUserHasAccess(db *gorm.DB, user *models.User, stripeEnabled bool) (bool, error) { +func checkUserHasAccess(db *gorm.DB, user *models.UserProfile, stripeEnabled bool) (bool, error) { // If user has no team, they have no subscription or trial access if user.TeamID == nil { return false, nil } - userWithSub, err := models.GetUserWithSubscription(db, user, stripeEnabled) + access, err := models.GetTeamAccess(db, *user.TeamID, stripeEnabled) if err != nil { return false, fmt.Errorf("failed to get user subscription: %w", err) } - hasAccess := userWithSub.IsPro - if !hasAccess && userWithSub.IsTrial && userWithSub.TrialEndsAt != nil { - hasAccess = userWithSub.TrialEndsAt.After(time.Now()) - } - - return hasAccess, nil + return access.Active(time.Now()), nil } diff --git a/backend/internal/handlers/websocketHandlers.go b/backend/internal/handlers/websocketHandlers.go index 3f349f69..8ad96acb 100644 --- a/backend/internal/handlers/websocketHandlers.go +++ b/backend/internal/handlers/websocketHandlers.go @@ -51,7 +51,7 @@ func CreateWSHandler(server *common.ServerState) echo.HandlerFunc { return err } - user, err := models.GetUserByEmail(server.DB, email) + user, err := models.GetUserProfileByEmail(server.DB, email) if err != nil { return err } @@ -351,7 +351,7 @@ func initiateCall(ctx echo.Context, s *common.ServerState, ws *websocket.Conn, r } // Check if caller has access (paid or active trial) - hasAccess, err := checkUserHasAccess(s.DB, caller, s.Config.IsStripeEnabled()) + hasAccess, err := checkUserHasAccess(s.DB, &caller.UserProfile, s.Config.IsStripeEnabled()) if err != nil { ctx.Logger().Error("Error getting caller subscription: ", err) s.Redis.Del(rdbCtx, dedupeKey) @@ -462,14 +462,14 @@ func acceptCall(ctx echo.Context, s *common.ServerState, calleeID string, messag roomName := uuid.New().String() ctx.Logger().Info("Creating room: ", roomName, " for users ", callerID, " ", calleeID) - calleeTokens, err := generateLiveKitTokens(s, roomName, callee) + calleeTokens, err := generateLiveKitTokens(s, roomName, &callee.UserProfile) if err != nil { ctx.Logger().Error(err) sendCommonErrorMessage(s, "Failed to generate callee tokens", callerID, calleeID) return } - callerTokens, err := generateLiveKitTokens(s, roomName, caller) + callerTokens, err := generateLiveKitTokens(s, roomName, &caller.UserProfile) if err != nil { ctx.Logger().Error(err) sendCommonErrorMessage(s, "Failed to generate caller tokens", callerID, calleeID) diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go index d6dcca68..65cc4dfc 100644 --- a/backend/internal/models/user.go +++ b/backend/internal/models/user.go @@ -38,19 +38,24 @@ func (es *EmailSubscriptions) Scan(value interface{}) error { return json.Unmarshal(b, &es) } +type UserProfile struct { + ID string `json:"id" gorm:"unique;not null"` // Standard field for the primary key + FirstName string `gorm:"not null" json:"first_name" validate:"required"` + LastName string `gorm:"not null" json:"last_name" validate:"required"` + Email string `gorm:"not null;unique" json:"email" validate:"required,email"` + IsAdmin bool `gorm:"default:false" json:"is_admin"` + TeamID *uint `json:"team_id" gorm:"default:null"` + AvatarURL string `json:"avatar_url"` + CreatedAt time.Time `json:"created_at"` // Automatically managed by GORM for creation time + UpdatedAt time.Time `json:"updated_at"` // Automatically managed by GORM for update time +} + type User struct { - ID string `json:"id" gorm:"unique;not null"` // Standard field for the primary key - FirstName string `gorm:"not null" json:"first_name" validate:"required"` - LastName string `gorm:"not null" json:"last_name" validate:"required"` - Email string `gorm:"not null;unique" json:"email" validate:"required,email"` - IsAdmin bool `gorm:"default:false" json:"is_admin"` - TeamID *uint `json:"team_id" gorm:"default:null"` - Team *Team `json:"team,omitempty"` - Password string `gorm:"-" json:"password" validate:"required,min=8"` - HashedPassword string `json:"-"` // Removed "not null" constraint - AvatarURL string `json:"avatar_url"` - CreatedAt time.Time `json:"created_at"` // Automatically managed by GORM for creation time - UpdatedAt time.Time `json:"updated_at"` // Automatically managed by GORM for update time + UserProfile `gorm:"embedded"` + + Team *Team `json:"team,omitempty"` + Password string `gorm:"-" json:"password" validate:"required,min=8"` + HashedPassword string `json:"-"` // Removed "not null" constraint // Can keep data like Slack workspace friends etc SocialMetadata map[string]interface{} `gorm:"serializer:json" json:"social_metadata,omitempty"` // General user metadata for onboarding, preferences, etc. @@ -121,6 +126,24 @@ func GetUserByEmail(db *gorm.DB, email string) (*User, error) { return &user, nil } +// GetUserProfileByEmail fetches only the UserProfile columns for a user. It is +// the slim variant used on hot, read-only paths (auth middleware, websocket +// handshake, teammate/presence polling) to avoid shipping the wide `users` row +// (hashed_password, social_metadata, metadata, email_subscriptions, ...) on +// every request. +func GetUserProfileByEmail(db *gorm.DB, email string) (*UserProfile, error) { + var profile UserProfile + result := db.Model(&User{}).Where("email = ?", email).First(&profile) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, errors.New("User not found") + } + return nil, result.Error + } + return &profile, nil +} + func GetUserByID(db *gorm.DB, id string) (*User, error) { var user *User result := db.Where("id = ?", id).First(&user) @@ -134,28 +157,23 @@ func GetUserByID(db *gorm.DB, id string) (*User, error) { return user, nil } -func (u *User) GetRedisChannel() string { +func (u *UserProfile) GetRedisChannel() string { return redisutil.GetUserChannel(u.ID) } type UserWithActivity struct { - User + UserProfile IsActive bool `json:"is_active"` } -func (u *User) GetTeammates(db *gorm.DB) ([]UserWithActivity, error) { - // First preload the user's team - if err := db.Preload("Team").Where("id = ?", u.ID).First(u).Error; err != nil { - return nil, err - } - - if u.Team == nil { +func (u *UserProfile) GetTeammates(db *gorm.DB) ([]UserWithActivity, error) { + if u.TeamID == nil { return []UserWithActivity{}, nil } - var teammates []User - if err := db.Select("id, first_name, last_name, email, avatar_url, team_id, is_admin, created_at, updated_at"). - Where("team_id = ? AND id != ?", u.TeamID, u.ID). + var teammates []UserProfile + if err := db.Model(&User{}). + Where("team_id = ? AND id != ?", *u.TeamID, u.ID). Find(&teammates).Error; err != nil { return nil, err } @@ -164,8 +182,8 @@ func (u *User) GetTeammates(db *gorm.DB) ([]UserWithActivity, error) { teammatesWithActivity := make([]UserWithActivity, len(teammates)) for i, teammate := range teammates { teammatesWithActivity[i] = UserWithActivity{ - User: teammate, - IsActive: false, // Will be set by the handler + UserProfile: teammate, + IsActive: false, // Will be set by the handler } } @@ -173,7 +191,7 @@ func (u *User) GetTeammates(db *gorm.DB) ([]UserWithActivity, error) { } // GetDisplayName returns the user's display name -func (u *User) GetDisplayName() string { +func (u *UserProfile) GetDisplayName() string { if u.LastName == "" { return u.FirstName } @@ -262,13 +280,22 @@ func GetAdminUserForTeam(db *gorm.DB, teamID uint) (*User, error) { return &adminUser, nil } -type UserWithSubscription struct { - User +type TeamAccess struct { IsPro bool `json:"is_pro"` IsTrial bool `json:"is_trial"` TrialEndsAt *time.Time `json:"trial_ends_at,omitempty"` } +// Active reports whether the team is Pro or still within its trial window. +func (a TeamAccess) Active(now time.Time) bool { + return a.IsPro || (a.IsTrial && a.TrialEndsAt != nil && a.TrialEndsAt.After(now)) +} + +type UserWithSubscription struct { + User + TeamAccess +} + // hardPaywallCutoff is the launch date of the card-required trial. Teams created // before this date keep the legacy free trial derived from team.CreatedAt; teams // created on or after it get no free trial and must have an active/trialing @@ -282,39 +309,37 @@ func IsTeamPostCutoff(team *Team) bool { return !team.CreatedAt.Before(hardPaywallCutoff) } -// GetUserWithSubscription returns a user with subscription information. -// When stripeEnabled is false (self-hosted deployments without Stripe), every -// user is treated as Pro and the trial is bypassed. -// 1. Check if team is manually upgraded, if so return true to `is_pro` -// 2. Fetch if any sub for their team exists and if its active -// 3. If no sub, return `IsTrial` and `TrialEndsAt` -func GetUserWithSubscription(db *gorm.DB, user *User, stripeEnabled bool) (*UserWithSubscription, error) { +// GetTeamAccess computes a team's access state (pro/trial). +// When stripeEnabled is false (self-hosted deployments without Stripe), the team +// is always treated as Pro. +// 1. Check if team is manually upgraded, if so return pro +// 2. Fetch if any sub for the team exists and is active +// 3. If no sub, return trial state derived from the team creation date +func GetTeamAccess(db *gorm.DB, teamID uint, stripeEnabled bool) (TeamAccess, error) { if !stripeEnabled { - return &UserWithSubscription{User: *user, IsPro: true}, nil + return TeamAccess{IsPro: true}, nil } - team, err := GetTeamByID(db, strconv.Itoa(int(*user.TeamID))) + team, err := GetTeamByID(db, strconv.Itoa(int(teamID))) if err != nil { - return nil, err + return TeamAccess{}, err } if team.IsManualUpgrade { - return &UserWithSubscription{User: *user, IsPro: true}, nil + return TeamAccess{IsPro: true}, nil } - sub, err := GetSubscriptionByTeamID(db, *user.TeamID) + sub, err := GetSubscriptionByTeamID(db, teamID) if err != nil { - return nil, err + return TeamAccess{}, err } if sub != nil && sub.IsActive() { - result := &UserWithSubscription{User: *user, IsPro: true} if sub.Status == StatusTrialing { - trialEndsAt := sub.CurrentPeriodEnd - result.IsTrial = true - result.TrialEndsAt = &trialEndsAt + ends := sub.CurrentPeriodEnd + return TeamAccess{IsPro: true, IsTrial: true, TrialEndsAt: &ends}, nil } - return result, nil + return TeamAccess{IsPro: true}, nil } // Teams created on/after the cutoff get no free trial. Access requires an @@ -322,15 +347,26 @@ func GetUserWithSubscription(db *gorm.DB, user *User, stripeEnabled bool) (*User // checkUserHasAccess return false so the existing 402 paths enforce the // paywall without any new middleware. if IsTeamPostCutoff(team) { - return &UserWithSubscription{User: *user, IsPro: false, IsTrial: false}, nil + return TeamAccess{}, nil } const trialDays = 14 - trialEndsAt := team.CreatedAt.AddDate(0, 0, trialDays) - return &UserWithSubscription{ - User: *user, - IsTrial: true, - TrialEndsAt: &trialEndsAt, - IsPro: false, - }, nil + ends := team.CreatedAt.AddDate(0, 0, trialDays) + return TeamAccess{IsTrial: true, TrialEndsAt: &ends}, nil +} + +// GetUserWithSubscription returns a user with subscription information. +// When stripeEnabled is false (self-hosted deployments without Stripe), every +// user is treated as Pro and the trial is bypassed. +func GetUserWithSubscription(db *gorm.DB, user *User, stripeEnabled bool) (*UserWithSubscription, error) { + if !stripeEnabled { + return &UserWithSubscription{User: *user, TeamAccess: TeamAccess{IsPro: true}}, nil + } + + access, err := GetTeamAccess(db, *user.TeamID, stripeEnabled) + if err != nil { + return nil, err + } + + return &UserWithSubscription{User: *user, TeamAccess: access}, nil } diff --git a/backend/internal/server/server.go b/backend/internal/server/server.go index b130d4af..62a2251f 100644 --- a/backend/internal/server/server.go +++ b/backend/internal/server/server.go @@ -165,14 +165,25 @@ func (s *Server) setupDatabase() { if strings.HasPrefix(dsn, "file:") { // Use SQLite driver for testing db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{TranslateError: true, Logger: logger.Default.LogMode(logger.Silent)}) + if err != nil { + s.Echo.Logger.Fatal(err) + } } else { - // Use PostgreSQL driver for production db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{TranslateError: true, Logger: logger.Default.LogMode(logger.Silent)}) - } + if err != nil { + s.Echo.Logger.Fatal(err) + } - if err != nil { - s.Echo.Logger.Fatal(err) + sqlDB, dbErr := db.DB() + if dbErr != nil { + s.Echo.Logger.Fatal(dbErr) + } + sqlDB.SetMaxOpenConns(10) + sqlDB.SetMaxIdleConns(10) + sqlDB.SetConnMaxLifetime(30 * time.Minute) + sqlDB.SetConnMaxIdleTime(10 * time.Minute) } + s.DB = db } diff --git a/backend/test/integration/integration_test.go b/backend/test/integration/integration_test.go index cd594fb2..f00d957b 100644 --- a/backend/test/integration/integration_test.go +++ b/backend/test/integration/integration_test.go @@ -74,12 +74,14 @@ func createTestUser(t *testing.T, db *gorm.DB, email, firstName, lastName, passw } user := &models.User{ - FirstName: firstName, - LastName: lastName, - Email: email, - Password: password, - TeamID: &teamID, - IsAdmin: isAdmin, + UserProfile: models.UserProfile{ + FirstName: firstName, + LastName: lastName, + Email: email, + TeamID: &teamID, + IsAdmin: isAdmin, + }, + Password: password, EmailSubscriptions: models.EmailSubscriptions{ MarketingEmails: true, },