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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ repos:
entry: bash -c "yarn pretty-quick --staged"

- repo: https://github.com/golangci/golangci-lint
rev: v2.7.2
rev: v2.8.0
hooks:
- id: golangci-lint
entry: bash -c 'cd backend && golangci-lint run --timeout=5m --config ../.golangcli.yml'
12 changes: 12 additions & 0 deletions backend/internal/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,18 @@ func (h *AuthHandler) GetRoom(c echo.Context) error {
return c.String(http.StatusUnauthorized, "Unauthorized request")
}

// Check if caller has access (paid or active trial)
hasAccess, err := checkUserHasAccess(h.DB, user)
if err != nil {
c.Logger().Error("Error getting user subscription: ", err)
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to check subscription status")
}

if !hasAccess {
_ = notifications.SendTelegramNotification(fmt.Sprintf("Unsubscribed user %s tried to join room %s", user.ID, room.Name), h.Config)
return c.JSON(http.StatusPaymentRequired, map[string]string{"error": "trial-ended"})
}
Comment on lines +1027 to +1037
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

HTTP 403 for trial-ended conflicts with the frontend's expected 402 status code.

deepLinkUtils.ts (Line 94) checks for response.status === 402 to handle trial expiration, but this endpoint returns http.StatusForbidden (403). The frontend's 403 handler (Line 101-102) would catch this instead and show "You don't have access to this session. It belongs to a different team." — a misleading message for expired trials.

Align on a single status code. 402 (Payment Required) is semantically appropriate for subscription/trial access gating. This would also require updating slackHandlers.go.

Proposed fix (if choosing 402)
 	if !hasAccess {
 		_ = notifications.SendTelegramNotification(fmt.Sprintf("Unsubscribed user %s tried to join room %s", user.ID, room.Name), h.Config)
-		return c.JSON(http.StatusForbidden, map[string]string{"error": "trial-ended"})
+		return c.JSON(http.StatusPaymentRequired, map[string]string{"error": "trial-ended"})
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check if caller has access (paid or active trial)
hasAccess, err := checkUserHasAccess(h.DB, user)
if err != nil {
c.Logger().Error("Error getting user subscription: ", err)
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to check subscription status")
}
if !hasAccess {
_ = notifications.SendTelegramNotification(fmt.Sprintf("Unsubscribed user %s tried to join room %s", user.ID, room.Name), h.Config)
return c.JSON(http.StatusForbidden, map[string]string{"error": "trial-ended"})
}
// Check if caller has access (paid or active trial)
hasAccess, err := checkUserHasAccess(h.DB, user)
if err != nil {
c.Logger().Error("Error getting user subscription: ", err)
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to check subscription status")
}
if !hasAccess {
_ = notifications.SendTelegramNotification(fmt.Sprintf("Unsubscribed user %s tried to join room %s", user.ID, room.Name), h.Config)
return c.JSON(http.StatusPaymentRequired, map[string]string{"error": "trial-ended"})
}
🤖 Prompt for AI Agents
In `@backend/internal/handlers/handlers.go` around lines 1027 - 1037, The endpoint
currently returns http.StatusForbidden (403) when checkUserHasAccess returns
false, which conflicts with the frontend expecting 402 for trial-ended; change
the response code in the failure branch (where checkUserHasAccess(...) returns
!hasAccess) from http.StatusForbidden to http.StatusPaymentRequired and keep the
same JSON body, and also update any corresponding logic in slackHandlers.go that
emits a 403 for trial-ended users so both backend handlers consistently return
402 for expired trials (reference symbols: checkUserHasAccess,
notifications.SendTelegramNotification).


tokens, err := generateLiveKitTokens(&h.ServerState, room.ID, user)
if err != nil {
c.Logger().Error("Failed to generate room tokens:", err)
Expand Down
11 changes: 11 additions & 0 deletions backend/internal/handlers/slackHandlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,17 @@ func (h *SlackHandler) GetSessionTokens(c echo.Context) error {
return echo.NewHTTPError(http.StatusForbidden, "You don't have access to this session")
}

// Check if user has access (paid or active trial)
hasAccess, err := checkUserHasAccess(h.DB, user)
if err != nil {
c.Logger().Error("Error getting user subscription: ", err)
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to check subscription status")
}

if !hasAccess {
return c.JSON(http.StatusPaymentRequired, map[string]string{"error": "trial-ended"})
}

Comment on lines +561 to +571
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We have a flavour of this in 3 places, at some point it would be worth extracting this as a util

// Generate LiveKit tokens for this room
tokens, err := generateLiveKitTokens(&h.ServerState, room.ID, user)
if err != nil {
Expand Down
22 changes: 22 additions & 0 deletions backend/internal/handlers/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,25 @@ func extractUserIDFromIdentity(identity string) (string, error) {
}
return "", fmt.Errorf("invalid identity format: %s", identity)
}

// 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) (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)
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
}
11 changes: 2 additions & 9 deletions backend/internal/handlers/websocketHandlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"hopp-backend/internal/models"
"hopp-backend/internal/notifications"
"net/http"
"time"

"github.com/google/uuid"
"github.com/gorilla/websocket"
Expand Down Expand Up @@ -272,20 +271,14 @@ func initiateCall(ctx echo.Context, s *common.ServerState, ws *websocket.Conn, r
return
}

callerWithSub, err := models.GetUserWithSubscription(s.DB, caller)
// Check if caller has access (paid or active trial)
hasAccess, err := checkUserHasAccess(s.DB, caller)
if err != nil {
ctx.Logger().Error("Error getting caller subscription: ", err)
sendWSErrorMessage(ws, "Failed to check subscription status")
return
}

// Check if caller has access (paid or active trial)
hasAccess := callerWithSub.IsPro
if !hasAccess && callerWithSub.IsTrial && callerWithSub.TrialEndsAt != nil {
// Check if trial is still active
hasAccess = callerWithSub.TrialEndsAt.After(time.Now())
}

if !hasAccess {
ctx.Logger().Warn("Caller does not have active subscription or trial: ", callerId)
msg := messages.NewRejectCallMessage(calleeID, "trial-ended")
Expand Down
7 changes: 7 additions & 0 deletions tauri/src/lib/deepLinkUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ export const handleJoinSessionDeepLink = async (sessionId: string): Promise<bool
} else if (response.status === 401) {
toast.error("Please log in again to join the session");
setTab("login");
} else if (response.status === 402) {
const body = await response.json().catch(() => null);
if (body?.error === "trial-ended") {
toast.error("Trial has expired, contact us if you want to extend it");
} else {
toast.error("Payment required to access this session");
}
} else if (response.status === 403) {
toast.error("You don't have access to this session. It belongs to a different team.");
} else {
Expand Down
8 changes: 6 additions & 2 deletions tauri/src/windows/main-window/tabs/Rooms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,12 @@ export const Rooms = () => {
cameraWindowOpen: false,
krispToggle: true,
});
} catch (error) {
toast.error("Error joining room");
} catch (error: any) {
if (error?.error === "trial-ended") {
toast.error("Trial has expired, contact us if you want to extend it");
} else {
toast.error("Error joining room");
}
}
},
[getRoomTokens, callTokens, setCallTokens, endCall],
Expand Down
Loading