Skip to content
Closed
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 @@ -15,6 +15,7 @@ export const AppSetupMap = {
paypal: dynamic(() => import("@calcom/web/components/apps/paypal/Setup")),
hitpay: dynamic(() => import("@calcom/web/components/apps/hitpay/Setup")),
btcpayserver: dynamic(() => import("@calcom/web/components/apps/btcpayserver/Setup")),
bigbluebutton: dynamic(() => import("@calcom/web/components/apps/bigbluebuttonvideo/Setup")),
};

export const AppSetupPage = (props: { slug: string }) => {
Expand Down
31 changes: 31 additions & 0 deletions apps/web/components/apps/appsWithSetupForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* 需要自定义 setup form 的 app slug 列表
* 这些 app 不是 OAuth 类型,但有自定义凭证表单,
* 需要走 setup form 流程而非自动安装流程
*/
export const APPS_WITH_SETUP_FORM: string[] = ["bigbluebutton"];

/**
* 判断指定 app 是否需要 setup form
*/
export function appRequiresSetupForm(slug: string): boolean {
return APPS_WITH_SETUP_FORM.includes(slug);
}

/**
* 为需要 setup form 的 app 返回重定向对象
* 如果 app 不需要 setup form,返回 null
*/
export function setupFormRedirectFor(slug: string): {
redirect: { permanent: boolean; destination: string };
} | null {
if (appRequiresSetupForm(slug)) {
return {
redirect: {
permanent: false,
destination: `/apps/${slug}/setup`,
},
};
}
return null;
}
115 changes: 115 additions & 0 deletions apps/web/components/apps/bigbluebuttonvideo/Setup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"use client";

import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { useState } from "react";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";

import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { Button } from "@calcom/ui/components/button";
import {
Form,
FormField,
Input,
PasswordField,
Label,
} from "@calcom/ui/components/form";
import { showToast } from "@calcom/ui/components/toast";

/** 表单字段验证模式 */
const formSchema = z.object({
serverUrl: z.string().url(),
sharedSecret: z.string().trim().min(1),
});

type FormValues = z.infer<typeof formSchema>;

/**
* BigBlueButton Setup 组件
* 收集用户的 BBB 服务器 URL 和共享密钥
*/
export default function BigBlueButtonSetup() {
const { t } = useLocale();
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);

const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
serverUrl: "",
sharedSecret: "",
},
});

const onSubmit = form.handleSubmit(async (values) => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
setIsSubmitting(true);
try {
const res = await fetch("/api/integrations/bigbluebuttonvideo/add", {
method: "POST",
body: JSON.stringify(values),
headers: { "Content-Type": "application/json" },
});

if (!res.ok) {
const json = await res.json();
throw new HttpError({ statusCode: res.status, message: json.message });
}

const json = await res.json();
router.push(json.url);
} catch (err) {
if (err instanceof HttpError) {
showToast(err.message, "error");
} else {
showToast(t("something_went_wrong"), "error");
}
} finally {
setIsSubmitting(false);
}
});

return (
<Form form={form} handleSubmit={onSubmit}>
<div className="space-y-4">
<FormField
control={form.control}
name="serverUrl"
render={({ field }) => (
<div>
<Label>{t("bbb_server_url")}</Label>
<Input
{...field}
type="url"
placeholder={t("bbb_server_url_placeholder")}
required
/>
</div>
)}
/>
<FormField
control={form.control}
name="sharedSecret"
render={({ field }) => (
<div>
<Label>{t("bbb_shared_secret")}</Label>
<PasswordField
{...field}
placeholder={t("bbb_shared_secret_placeholder")}
required
/>
</div>
)}
/>
</div>
<Button
type="submit"
loading={isSubmitting}
className="mt-4 w-full"
data-testid="bbb-submit-button">
{t("submit")}
</Button>
</Form>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import { isConferencing as isConferencingApp } from "@calcom/app-store/utils";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps";
import { CAL_URL } from "@calcom/lib/constants";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import prisma from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";

import { setupFormRedirectFor } from "@calcom/web/components/apps/appsWithSetupForm";

import { STEPS } from "../../../../modules/apps/installation/[[...step]]/constants";
import type {
OnboardingPageProps,
Expand Down Expand Up @@ -446,6 +448,11 @@ const getCredential = async (
parsedAppSlug: string
): Promise<{ credentialId: number | null; redirect?: RedirectResult }> => {
let credentialId = getCredentialId(parsedTeamIdParam, appInstalls, user.id);
// 无凭证且 app 需要 setup form 时,重定向到 setup 页面采集凭证
if (!credentialId && !appMetadata.isOAuth) {
const setupRedirect = setupFormRedirectFor(parsedAppSlug);
if (setupRedirect) return setupRedirect;
}
Comment on lines +452 to +455
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify return statements inside getCredential and compare shape expectations.
rg -n "const getCredential|return \\{ credentialId|return setupRedirect|Promise<\\{ credentialId" apps/web/lib/apps/installation/[[...step]]/getServerSideProps.ts

Repository: calcom/cal.diy

Length of output: 391


Fix getCredential return shape when redirecting to the setup form.

getCredential is typed to return Promise<{ credentialId: number | null; redirect?: RedirectResult }>, but the setupRedirect branch returns setupRedirect directly (missing credentialId, around lines 452–455), causing a TypeScript contract mismatch.

Suggested fix
   if (!credentialId && !appMetadata.isOAuth) {
     const setupRedirect = setupFormRedirectFor(parsedAppSlug);
-    if (setupRedirect) return setupRedirect;
+    if (setupRedirect) return { credentialId: null, redirect: setupRedirect };
   }
📝 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
if (!credentialId && !appMetadata.isOAuth) {
const setupRedirect = setupFormRedirectFor(parsedAppSlug);
if (setupRedirect) return setupRedirect;
}
if (!credentialId && !appMetadata.isOAuth) {
const setupRedirect = setupFormRedirectFor(parsedAppSlug);
if (setupRedirect) return { credentialId: null, redirect: setupRedirect };
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/apps/installation/`[[...step]]/getServerSideProps.ts around
lines 452 - 455, The getCredential branch that checks if (!credentialId &&
!appMetadata.isOAuth) calls setupFormRedirectFor(parsedAppSlug) and returns
setupRedirect directly, which violates the declared return type Promise<{
credentialId: number | null; redirect?: RedirectResult }>; instead, change that
branch to return an object with credentialId: null and redirect: setupRedirect
(i.e., return { credentialId: null, redirect: setupRedirect }) so the function
signature is honored while preserving the redirect behavior.

if (
!credentialId &&
!user.teams.length &&
Expand Down
6 changes: 6 additions & 0 deletions apps/web/modules/apps/components/AppCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { InstallAppButton } from "@calcom/app-store/InstallAppButton";
import { isRedirectApp } from "@calcom/app-store/_utils/redirectApps";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { doesAppSupportTeamInstall, isConferencing } from "@calcom/app-store/utils";
import { appRequiresSetupForm } from "@calcom/web/components/apps/appsWithSetupForm";
import type { UserAdminTeams } from "@calcom/features/users/repositories/UserRepository";
import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps";
import { getAppOnboardingUrl } from "@calcom/lib/apps/getAppOnboardingUrl";
Expand Down Expand Up @@ -71,6 +72,11 @@ export function AppCard({ app, credentials, searchText, userAdminTeams }: AppCar
if (app.url) window.open(app.url, "_blank", "noopener,noreferrer");
return;
}
// 需要 setup form 的 app 直接触发安装,由后端 add handler 验证凭证并跳转到 setup 页面
if (appRequiresSetupForm(app.slug)) {
mutation.mutate({ type: app.type });
return;
}
if (isConferencing(app.categories) && !app.concurrentMeetings) {
mutation.mutate({
type: app.type,
Expand Down
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 @@ -4,6 +4,7 @@
**/
import { appKeysSchema as alby_zod_ts } from "./alby/zod";
import { appKeysSchema as basecamp3_zod_ts } from "./basecamp3/zod";
import { appKeysSchema as bigbluebuttonvideo_zod_ts } from "./bigbluebuttonvideo/zod";
import { appKeysSchema as btcpayserver_zod_ts } from "./btcpayserver/zod";
import { appKeysSchema as closecom_zod_ts } from "./closecom/zod";
import { appKeysSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
Expand Down Expand Up @@ -55,6 +56,7 @@ import { appKeysSchema as zoomvideo_zod_ts } from "./zoomvideo/zod";
export const appKeysSchemas = {
alby: alby_zod_ts,
basecamp3: basecamp3_zod_ts,
bigbluebuttonvideo: bigbluebuttonvideo_zod_ts,
btcpayserver: btcpayserver_zod_ts,
closecom: closecom_zod_ts,
dailyvideo: dailyvideo_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 @@ -9,6 +9,7 @@ import attio_config_json from "./attio/config.json";
import autocheckin_config_json from "./autocheckin/config.json";
import baa_for_hipaa_config_json from "./baa-for-hipaa/config.json";
import basecamp3_config_json from "./basecamp3/config.json";
import { metadata as bigbluebuttonvideo__metadata_ts } from "./bigbluebuttonvideo/_metadata";
import bolna_config_json from "./bolna/config.json";
import btcpayserver_config_json from "./btcpayserver/config.json";
import { metadata as caldavcalendar__metadata_ts } from "./caldavcalendar/_metadata";
Expand Down Expand Up @@ -121,6 +122,7 @@ export const appStoreMetadata = {
autocheckin: autocheckin_config_json,
"baa-for-hipaa": baa_for_hipaa_config_json,
basecamp3: basecamp3_config_json,
bigbluebuttonvideo: bigbluebuttonvideo__metadata_ts,
bolna: bolna_config_json,
btcpayserver: btcpayserver_config_json,
caldavcalendar: caldavcalendar__metadata_ts,
Expand Down
2 changes: 2 additions & 0 deletions packages/app-store/apps.schemas.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
**/
import { appDataSchema as alby_zod_ts } from "./alby/zod";
import { appDataSchema as basecamp3_zod_ts } from "./basecamp3/zod";
import { appDataSchema as bigbluebuttonvideo_zod_ts } from "./bigbluebuttonvideo/zod";
import { appDataSchema as btcpayserver_zod_ts } from "./btcpayserver/zod";
import { appDataSchema as closecom_zod_ts } from "./closecom/zod";
import { appDataSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
Expand Down Expand Up @@ -55,6 +56,7 @@ import { appDataSchema as zoomvideo_zod_ts } from "./zoomvideo/zod";
export const appDataSchemas = {
alby: alby_zod_ts,
basecamp3: basecamp3_zod_ts,
bigbluebuttonvideo: bigbluebuttonvideo_zod_ts,
btcpayserver: btcpayserver_zod_ts,
closecom: closecom_zod_ts,
dailyvideo: dailyvideo_zod_ts,
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 @@ -7,6 +7,7 @@ export const apiHandlers = {
applecalendar: import("./applecalendar/api"),
attio: import("./attio/api"),
basecamp3: import("./basecamp3/api"),
bigbluebuttonvideo: import("./bigbluebuttonvideo/api"),
btcpayserver: import("./btcpayserver/api"),
caldavcalendar: import("./caldavcalendar/api"),
campfire: import("./campfire/api"),
Expand Down
1 change: 1 addition & 0 deletions packages/app-store/bigbluebuttonvideo/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BigBlueButton is an open-source virtual classroom and web conferencing platform designed for online learning. It provides real-time sharing of audio, video, slides, chat, and screen, making it ideal for online classes, meetings, and collaborative sessions.
30 changes: 30 additions & 0 deletions packages/app-store/bigbluebuttonvideo/_metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { AppMeta } from "@calcom/types/App";

export const metadata = {
name: "BigBlueButton",
description:
"BigBlueButton is an open-source virtual classroom and web conferencing platform designed for online learning.",
installed: true,
type: "bigbluebutton_video",
variant: "conferencing",
categories: ["conferencing"],
logo: "icon.svg",
publisher: "Cal.diy",
url: "https://bigbluebutton.org/",
slug: "bigbluebutton",
title: "BigBlueButton",
isGlobal: false,
email: "help@cal.com",
appData: {
location: {
linkType: "dynamic",
type: "integrations:bigbluebutton",
label: "BigBlueButton Video",
},
},
dirName: "bigbluebuttonvideo",
concurrentMeetings: true,
isOAuth: false,
} as AppMeta;

export default metadata;
Loading
Loading