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
23 changes: 23 additions & 0 deletions backend/api-files/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,29 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/SessionTokensResponse"
"402":
description: Trial or subscription expired
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: trial-ended
"403":
description: User does not belong to the room's team
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: Room not found
content:
text/plain:
schema:
type: string
example: Room not found

put:
summary: Update room details
Expand Down
6 changes: 3 additions & 3 deletions backend/internal/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1019,9 +1019,9 @@ func (h *AuthHandler) GetRoom(c echo.Context) error {
return c.String(http.StatusNotFound, "Room not found")
}

// Check if user can access the room
if user.Team != room.Team {
return c.String(http.StatusUnauthorized, "Unauthorized request")
// Check if user can access the room (same team)
if room.TeamID == nil || user.TeamID == nil || *room.TeamID != *user.TeamID {
return echo.NewHTTPError(http.StatusForbidden, "You don't have access to this room")
}

// Check if caller has access (paid or active trial)
Expand Down
30 changes: 30 additions & 0 deletions tauri/src/openapi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,36 @@ export interface paths {
"application/json": components["schemas"]["SessionTokensResponse"];
};
};
/** @description Trial or subscription expired */
402: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @example trial-ended */
error?: string;
};
};
};
/** @description User does not belong to the room's team */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description Room not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"text/plain": string;
};
};
};
};
/** Update room details */
Expand Down
30 changes: 30 additions & 0 deletions web-app/src/openapi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,36 @@ export interface paths {
"application/json": components["schemas"]["SessionTokensResponse"];
};
};
/** @description Trial or subscription expired */
402: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @example trial-ended */
error?: string;
};
};
};
/** @description User does not belong to the room's team */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description Room not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"text/plain": string;
};
};
};
};
/** Update room details */
Expand Down
80 changes: 67 additions & 13 deletions web-app/src/pages/Room.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
import clsx from "clsx";
import { VideoPresets, Track, LocalTrack, Participant, ParticipantEvent, LocalTrackPublication } from "livekit-client";
import { useAPI } from "@/hooks/useQueryClients";
import { useAPI, isFetchError } from "@/hooks/useQueryClients";
import { useHoppStore } from "@/store/store";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { AlertTriangle } from "lucide-react";

// HACK: Import shared components from tauri app for cursor rendering
// These files use relative imports so they work across projects
Expand Down Expand Up @@ -70,6 +72,68 @@ function getGridCols(count: number): string {
return "grid-cols-4";
}

function RoomJoinError({ error, onGoToDashboard }: { error: unknown; onGoToDashboard: () => void }) {
if (isFetchError(error)) {
const status = error.response.status;

if (status === 403) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-center max-w-md">
<div className="text-4xl mb-4">🔒</div>
<h1 className="text-2xl font-semibold mb-2">Access Denied</h1>
<p className="text-gray-600 mb-4">You don't have access to this room.</p>
<Alert className="mb-4 text-left">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Why can't I join?</AlertTitle>
<AlertDescription>
This room belongs to a different team. Ask a member of that team to invite you, so you can join.
</AlertDescription>
</Alert>
<Button onClick={onGoToDashboard}>Go to Dashboard</Button>
</div>
</div>
);
}

if (status === 404) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-center max-w-md">
<h1 className="text-2xl font-semibold mb-2">Room Not Found</h1>
<p className="text-gray-600 mb-4">This room doesn't exist or may have been deleted.</p>
<Button onClick={onGoToDashboard}>Go to Dashboard</Button>
</div>
</div>
);
}

if (status === 402) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-center max-w-md">
<h1 className="text-2xl font-semibold mb-2">Trial Expired</h1>
<p className="text-gray-600 mb-4">Your team's trial has ended. Contact us if you want to extend it.</p>
<Button onClick={onGoToDashboard}>Go to Dashboard</Button>
</div>
</div>
);
}
}

return (
<div className="flex items-center justify-center h-full">
<div className="text-center max-w-md">
<h1 className="text-2xl font-semibold mb-2">Unable to Join Room</h1>
<p className="text-gray-600 mb-4">
{error ? "You don't have access to this room or the room doesn't exist." : "Failed to get room access."}
</p>
Comment thread
konsalex marked this conversation as resolved.
<Button onClick={onGoToDashboard}>Go to Dashboard</Button>
</div>
</div>
);
}

export function Room() {
const { roomId } = useParams<{ roomId: string }>();
const navigate = useNavigate();
Expand Down Expand Up @@ -125,17 +189,7 @@ export function Room() {
}

if (error || !roomTokens || !livekitServer) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<h1 className="text-2xl font-semibold mb-2">Unable to Join Room</h1>
<p className="text-gray-600 mb-4">
{error ? "You don't have access to this room or the room doesn't exist." : "Failed to get room access."}
</p>
<Button onClick={() => navigate("/dashboard")}>Go to Dashboard</Button>
</div>
</div>
);
return <RoomJoinError error={tokensError ?? error} onGoToDashboard={() => navigate("/dashboard")} />;
}

return (
Expand Down Expand Up @@ -502,7 +556,7 @@ function ScreenShareView({ screenShareTrack, ownerName }: { screenShareTrack: Tr
{/* Screen share label */}
<div className="absolute bottom-3 left-3 bg-black/60 text-white text-xs px-2 py-1 rounded flex items-center gap-2">
<LuScreenShare className="size-3" />
<span>{ownerName}&apos;s screen</span>
<span>{ownerName}'s screen</span>
</div>
</div>
</div>
Expand Down
Loading