Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/web/components/apps/AppSetupPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DynamicComponent } from "@calcom/app-store/_components/DynamicComponent
import dynamic from "next/dynamic";

export const AppSetupMap = {
bigbluebutton: dynamic(() => import("@calcom/web/components/apps/bigbluebutton/Setup")),
alby: dynamic(() => import("@calcom/web/components/apps/alby/Setup")),
"apple-calendar": dynamic(() => import("@calcom/web/components/apps/applecalendar/Setup")),
exchange: dynamic(() => import("@calcom/web/components/apps/exchangecalendar/Setup")),
Expand Down
95 changes: 95 additions & 0 deletions apps/web/components/apps/bigbluebutton/Setup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { Toaster } from "sonner";

import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert } from "@calcom/ui/components/alert";
import { Button } from "@calcom/ui/components/button";
import { Form } from "@calcom/ui/components/form";
import { TextField } from "@calcom/ui/components/form";
import { PasswordField } from "@calcom/ui/components/form";

export default function BigBlueButtonSetup() {
const { t } = useLocale();
const router = useRouter();
const form = useForm({
defaultValues: {
serverUrl: "",
sharedSecret: "",
},
});

const [errorMessage, setErrorMessage] = useState("");

return (
<div className="bg-emphasis flex h-screen">
<div className="bg-default m-auto rounded p-5 md:w-[560px] md:p-10">
<div className="flex flex-col space-y-5 md:flex-row md:space-x-5 md:space-y-0">
<div>
{/* eslint-disable @next/next/no-img-element */}
<img
src="/api/app-store/bigbluebutton/icon.svg"
alt="BigBlueButton"
className="h-12 w-12 max-w-2xl"
/>
</div>
<div className="flex w-10/12 flex-col">
<h1 className="text-default">Connect BigBlueButton</h1>
<div className="mt-1 text-sm">
Enter your BigBlueButton server URL and shared secret to enable video conferencing.
</div>
<div className="my-2 mt-3">
<Form
form={form}
handleSubmit={async (values) => {
setErrorMessage("");
const res = await fetch("/api/integrations/bigbluebutton/add", {
method: "POST",
body: JSON.stringify(values),
headers: {
"Content-Type": "application/json",
},
});
const json = await res.json();
if (!res.ok) {
setErrorMessage(json?.message || t("something_went_wrong"));
} else {
router.push(json.url);
}
}}>
<fieldset className="space-y-2" disabled={form.formState.isSubmitting}>
<TextField
required
type="url"
{...form.register("serverUrl")}
label="Server URL"
placeholder="https://your-bbb-server.com/bigbluebutton/"
/>
<PasswordField
required
{...form.register("sharedSecret")}
label="Shared Secret"
placeholder="Your BBB shared secret"
autoComplete="password"
/>
</fieldset>

{errorMessage && <Alert className="mt-4" severity="error" title={errorMessage} />}
<div className="mt-5 justify-end space-x-2 rtl:space-x-reverse sm:mt-4 sm:flex">
<Button type="button" color="secondary" onClick={() => router.back()}>
{t("cancel")}
</Button>
<Button type="submit" loading={form.formState.isSubmitting}>
{t("save")}
</Button>
</div>
</Form>
</div>
</div>
</div>
<Toaster />
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions packages/app-store/apps.keys-schemas.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { appKeysSchema as insihts_zod_ts } from "./insihts/zod";
import { appKeysSchema as intercom_zod_ts } from "./intercom/zod";
import { appKeysSchema as jelly_zod_ts } from "./jelly/zod";
import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod";
import { appKeysSchema as bigbluebutton_zod_ts } from "./bigbluebutton/zod";
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
import { appKeysSchema as lyra_zod_ts } from "./lyra/zod";
import { appKeysSchema as make_zod_ts } from "./make/zod";
Expand Down Expand Up @@ -74,6 +75,7 @@ export const appKeysSchemas = {
intercom: intercom_zod_ts,
jelly: jelly_zod_ts,
jitsivideo: jitsivideo_zod_ts,
bigbluebutton: bigbluebutton_zod_ts,
larkcalendar: larkcalendar_zod_ts,
lyra: lyra_zod_ts,
make: make_zod_ts,
Expand Down
2 changes: 2 additions & 0 deletions packages/app-store/apps.metadata.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import insihts_config_json from "./insihts/config.json";
import intercom_config_json from "./intercom/config.json";
import jelly_config_json from "./jelly/config.json";
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
import { metadata as bigbluebutton__metadata_ts } from "./bigbluebutton/_metadata";
import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata";
import lindy_config_json from "./lindy/config.json";
import linear_config_json from "./linear/config.json";
Expand Down Expand Up @@ -164,6 +165,7 @@ export const appStoreMetadata = {
intercom: intercom_config_json,
jelly: jelly_config_json,
jitsivideo: jitsivideo__metadata_ts,
bigbluebutton: bigbluebutton__metadata_ts,
larkcalendar: larkcalendar__metadata_ts,
lindy: lindy_config_json,
linear: linear_config_json,
Expand Down
1 change: 1 addition & 0 deletions packages/app-store/apps.server.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const apiHandlers = {
intercom: import("./intercom/api"),
jelly: import("./jelly/api"),
jitsivideo: import("./jitsivideo/api"),
bigbluebutton: import("./bigbluebutton/api"),
larkcalendar: import("./larkcalendar/api"),
linear: import("./linear/api"),
lyra: import("./lyra/api"),
Expand Down
44 changes: 44 additions & 0 deletions packages/app-store/bigbluebutton/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
items:
- icon.svg
---

BigBlueButton is an open-source video conferencing system designed for education and business. Host unlimited meetings on your own server with features like screen sharing, whiteboard, breakout rooms, and recording capabilities.

## Features

- 🎥 HD video conferencing
- 📱 Works on desktop and mobile
- 🎯 Built for education and business
- 🔒 Self-hosted for privacy and control
- 📊 Screen sharing and presentation tools
- ✏️ Interactive whiteboard
- 👥 Breakout rooms for small groups
- 📹 Optional meeting recording

## Setup

1. **BigBlueButton Server**: You need access to a BigBlueButton server. You can:
- Set up your own server following the [BigBlueButton installation guide](https://docs.bigbluebutton.org/install/install-overview)
- Use a hosted BigBlueButton provider
- Use BigBlueButton's demo server for testing (not recommended for production)

2. **Server URL**: Enter your BigBlueButton server URL (e.g., `https://your-bbb-server.com`)

3. **Shared Secret**: Enter your BigBlueButton shared secret (found in `/opt/bbb/conf/salt` on your server)

## How it works

When someone books a meeting with you:

1. A unique BigBlueButton meeting room is automatically created
2. The meeting host (you) receives a moderator link
3. Meeting attendees receive participant links
4. The meeting room is automatically deleted when the booking is cancelled

## Security

- All communications are encrypted end-to-end
- Meeting rooms are created on-demand and deleted after use
- Attendee access is controlled by unique passwords
- Self-hosted deployment keeps your data private
30 changes: 30 additions & 0 deletions packages/app-store/bigbluebutton/_metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { AppMeta } from "@calcom/types/App";

import _package from "./package.json";

export const metadata = {
name: "BigBlueButton",
description: _package.description,
type: "bigbluebutton_video",
variant: "conferencing",
categories: ["conferencing"],
logo: "icon.svg",
publisher: "Cal.com",
url: "https://bigbluebutton.org/",
slug: "bigbluebutton",
title: "BigBlueButton",
isGlobal: false,
email: "help@cal.com",
appData: {
location: {
linkType: "dynamic",
type: "integrations:bigbluebutton",
label: "BigBlueButton",
},
},
dirName: "bigbluebutton",
concurrentMeetings: true,
isOAuth: false,
} as AppMeta;

export default metadata;
114 changes: 114 additions & 0 deletions packages/app-store/bigbluebutton/api/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam";
import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown";
import prisma from "@calcom/prisma";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import { BBBApi } from "../lib/bbb-api";

/**
* BigBlueButton app installation endpoint
* Validates BBB server connection before saving credentials
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" });
}

if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}

const { teamId, returnTo } = req.query;
const { serverUrl, sharedSecret } = req.body;

// Validate required fields
if (!serverUrl || !sharedSecret) {
return res.status(400).json({
message: "Server URL and Shared Secret are required"
});
}

// Validate server URL format
try {
const url = new URL(serverUrl);
if (!url.protocol.startsWith("http")) {
throw new Error("Invalid protocol");
}
} catch {
return res.status(400).json({
message: "Invalid server URL format. Please provide a valid HTTP or HTTPS URL."
});
}

await throwIfNotHaveAdminAccessToTeam({
teamId: teamId ? Number(teamId) : null,
userId: req.session.user.id,
});

const installForObject = teamId ? { teamId: Number(teamId) } : { userId: req.session.user.id };
const appType = "bigbluebutton_video";

try {
// Test BBB server connection first before attempting to install
const bbbApi = new BBBApi({ serverUrl, sharedSecret });
const isConnected = await bbbApi.testConnection();

if (!isConnected) {
return res.status(400).json({
message: "Could not connect to BigBlueButton server. Please verify the server URL and shared secret."
});
}

// Use a transaction to prevent race conditions
const installation = await prisma.$transaction(async (tx) => {
// Check for existing installation within transaction
const existing = await tx.credential.findFirst({
where: {
type: appType,
...installForObject,
},
});

if (existing) {
// Update existing installation instead of creating duplicate
return await tx.credential.update({
where: { id: existing.id },
data: {
key: {
serverUrl: serverUrl.replace(/\/+$/, ""), // Remove trailing slashes
sharedSecret,
},
},
});
}

// Create new installation if none exists
return await tx.credential.create({
data: {
type: appType,
key: {
serverUrl: serverUrl.replace(/\/+$/, ""), // Remove trailing slashes
sharedSecret,
},
...installForObject,
appId: "bigbluebutton",
},
});
});

if (!installation) {
throw new Error("Unable to create BigBlueButton credential");
}

return res.status(200).json({
url: returnTo ?? getInstalledAppPath({ variant: "conferencing", slug: "bigbluebutton" }),
message: "BigBlueButton successfully configured!"
});

} catch (error: unknown) {
// Log safe error message without sensitive credentials
console.error("BigBlueButton installation error:", error instanceof Error ? error.message : "Unknown error");
const httpError = getServerErrorFromUnknown(error);
return res.status(httpError.statusCode).json({ message: httpError.message });
}
}
1 change: 1 addition & 0 deletions packages/app-store/bigbluebutton/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as add } from "./add";
3 changes: 3 additions & 0 deletions packages/app-store/bigbluebutton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * as lib from "./lib";
export * as api from "./api";
export { metadata } from "./_metadata";
Loading