From 2009089e2d2f7e858957f1b087585be734620daf Mon Sep 17 00:00:00 2001 From: AmitChauhan63390 Date: Sun, 26 Jan 2025 14:22:36 +0530 Subject: [PATCH] airtable Oauth --- .gitignore | 2 + package.json | 8 + server/config/config.json | 26 ++ ...125120216-add-airtable-columns-to-robot.js | 16 + ...0125182726-update-airtable-token-length.js | 22 + server/src/models/Robot.ts | 28 +- server/src/routes/auth.ts | 243 ++++++++++ server/src/server.ts | 11 + .../integration/IntegrationSettings.tsx | 422 +++++++++++------- 9 files changed, 617 insertions(+), 161 deletions(-) create mode 100644 server/config/config.json create mode 100644 server/migrations/20250125120216-add-airtable-columns-to-robot.js create mode 100644 server/migrations/20250125182726-update-airtable-token-length.js diff --git a/.gitignore b/.gitignore index 74279fc9..a2a80766 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ /build package-lock.json + +docker-compose.yml \ No newline at end of file diff --git a/package.json b/package.json index 5dae78a2..974dfc06 100644 --- a/package.json +++ b/package.json @@ -16,16 +16,19 @@ "@types/bcrypt": "^5.0.2", "@types/body-parser": "^1.19.5", "@types/csurf": "^1.11.5", + "@types/express-session": "^1.18.1", "@types/jsonwebtoken": "^9.0.7", "@types/node": "22.7.9", "@types/react": "^18.0.5", "@types/react-dom": "^18.0.1", "@types/uuid": "^8.3.4", + "airtable": "^0.12.2", "axios": "^0.26.0", "bcrypt": "^5.1.1", "body-parser": "^1.20.3", "buffer": "^6.0.3", "bullmq": "^5.12.15", + "connect-redis": "^8.0.1", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "cron-parser": "^4.9.0", @@ -33,6 +36,7 @@ "csurf": "^1.11.0", "dotenv": "^16.0.0", "express": "^4.17.2", + "express-session": "^1.18.1", "fortawesome": "^0.0.1-security", "google-auth-library": "^9.14.1", "googleapis": "^144.0.0", @@ -52,6 +56,7 @@ "moment-timezone": "^0.5.45", "node-cron": "^3.0.3", "pg": "^8.13.0", + "pkce-challenge": "^4.1.0", "playwright": "^1.48.2", "playwright-extra": "^4.3.6", "posthog-node": "^4.2.1", @@ -65,6 +70,7 @@ "react-router-dom": "^6.26.1", "react-simple-code-editor": "^0.11.2", "react-transition-group": "^4.4.2", + "redis": "^4.7.0", "sequelize": "^6.37.3", "sequelize-typescript": "^2.1.6", "sharp": "^0.33.5", @@ -106,6 +112,7 @@ "@types/prismjs": "^1.26.0", "@types/react-highlight": "^0.12.5", "@types/react-transition-group": "^4.4.4", + "@types/redis": "^4.0.11", "@types/styled-components": "^5.1.23", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", @@ -115,6 +122,7 @@ "cross-env": "^7.0.3", "js-cookie": "^3.0.5", "nodemon": "^2.0.15", + "sequelize-cli": "^6.6.2", "ts-node": "^10.4.0", "vite": "^5.4.10" } diff --git a/server/config/config.json b/server/config/config.json new file mode 100644 index 00000000..395914f9 --- /dev/null +++ b/server/config/config.json @@ -0,0 +1,26 @@ +{ + "development": { + "username": "postgres", + "password": "postgres", + "database": "maxun", + "host": "localhost", + "port": 5432, + "dialect": "postgres" + }, + "test": { + "username": "postgres", + "password": "postgres", + "database": "maxun_test", + "host": "localhost", + "port": 5432, + "dialect": "postgres" + }, + "production": { + "username": "postgres", + "password": "postgres", + "database": "maxun_prod", + "host": "localhost", + "port": 5432, + "dialect": "postgres" + } + } \ No newline at end of file diff --git a/server/migrations/20250125120216-add-airtable-columns-to-robot.js b/server/migrations/20250125120216-add-airtable-columns-to-robot.js new file mode 100644 index 00000000..ac2c852b --- /dev/null +++ b/server/migrations/20250125120216-add-airtable-columns-to-robot.js @@ -0,0 +1,16 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + + await queryInterface.addColumn('robot', 'airtable_refresh_token', { + type: Sequelize.STRING, + allowNull: true, + }); + }, + + down: async (queryInterface, Sequelize) => { + + await queryInterface.removeColumn('robot', 'airtable_refresh_token'); + }, +}; \ No newline at end of file diff --git a/server/migrations/20250125182726-update-airtable-token-length.js b/server/migrations/20250125182726-update-airtable-token-length.js new file mode 100644 index 00000000..b75d788c --- /dev/null +++ b/server/migrations/20250125182726-update-airtable-token-length.js @@ -0,0 +1,22 @@ +// migrations/XXXXXXXXXXXXXX-update-airtable-token-length.js +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.changeColumn('robot', 'airtable_access_token', { + type: Sequelize.TEXT + }); + + await queryInterface.changeColumn('robot', 'airtable_refresh_token', { + type: Sequelize.TEXT + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.changeColumn('robot', 'airtable_access_token', { + type: Sequelize.STRING(255) + }); + + await queryInterface.changeColumn('robot', 'airtable_refresh_token', { + type: Sequelize.STRING(255) + }); + } +}; \ No newline at end of file diff --git a/server/src/models/Robot.ts b/server/src/models/Robot.ts index 3b2717d6..ffd4746d 100644 --- a/server/src/models/Robot.ts +++ b/server/src/models/Robot.ts @@ -25,6 +25,10 @@ interface RobotAttributes { google_sheet_id?: string | null; google_access_token?: string | null; google_refresh_token?: string | null; + airtable_base_id?: string | null; // New field for Airtable base ID + airtable_table_name?: string | null; // New field for Airtable table name + airtable_access_token?: string | null; // New field for Airtable access token + airtable_refresh_token?: string | null; // New field for Airtable refresh token schedule?: ScheduleConfig | null; } @@ -49,10 +53,14 @@ class Robot extends Model implements R public recording_meta!: RobotMeta; public recording!: RobotWorkflow; public google_sheet_email!: string | null; - public google_sheet_name?: string | null; - public google_sheet_id?: string | null; + public google_sheet_name!: string | null; + public google_sheet_id!: string | null; public google_access_token!: string | null; public google_refresh_token!: string | null; + public airtable_base_id!: string | null; // New field for Airtable base ID + public airtable_table_name!: string | null; // New field for Airtable table name + public airtable_access_token!: string | null; // New field for Airtable access token + public airtable_refresh_token!: string | null; // New field for Airtable refresh token public schedule!: ScheduleConfig | null; } @@ -95,6 +103,22 @@ Robot.init( type: DataTypes.STRING, allowNull: true, }, + airtable_base_id: { + type: DataTypes.STRING, + allowNull: true, + }, + airtable_table_name: { + type: DataTypes.STRING, + allowNull: true, + }, + airtable_access_token: { + type: DataTypes.TEXT, + allowNull: true, + }, + airtable_refresh_token: { + type: DataTypes.TEXT, + allowNull: true, + }, schedule: { type: DataTypes.JSONB, allowNull: true, diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 837fcd8a..574e2176 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -1,4 +1,6 @@ import { Router, Request, Response } from "express"; +import Airtable from "airtable"; + import User from "../models/User"; import Robot from "../models/Robot"; import jwt from "jsonwebtoken"; @@ -7,6 +9,16 @@ import { requireSignIn } from "../middlewares/auth"; import { genAPIKey } from "../utils/api"; import { google } from "googleapis"; import { capture } from "../utils/analytics"; + + + +declare module "express-session" { + interface SessionData { + code_verifier: string; + robotId: string; + } +} + export const router = Router(); interface AuthenticatedRequest extends Request { @@ -555,3 +567,234 @@ router.post( } } ); + + + + +import crypto from 'crypto'; + +// Add these environment variables to your .env file +// AIRTABLE_CLIENT_ID=your_client_id +// AIRTABLE_CLIENT_SECRET=your_client_secret +// AIRTABLE_REDIRECT_URI=http://localhost:8080/auth/airtable/callback + +// Airtable OAuth Routes +router.get("/airtable", (req, res) => { + const { robotId } = req.query; + if (!robotId) { + return res.status(400).json({ message: "Robot ID is required" }); + } + + // Generate PKCE codes + const code_verifier = crypto.randomBytes(64).toString('base64url'); + const code_challenge = crypto.createHash('sha256') + .update(code_verifier) + .digest('base64url'); + + // Store in session + req.session.code_verifier = code_verifier; + req.session.robotId = robotId.toString(); + + const params = new URLSearchParams({ + client_id: process.env.AIRTABLE_CLIENT_ID!, + redirect_uri: process.env.AIRTABLE_REDIRECT_URI!, + + response_type: 'code', + state: robotId.toString(), + scope: 'data.records:read data.records:write schema.bases:read', + code_challenge: code_challenge, + code_challenge_method: 'S256' + }); + + res.redirect(`https://airtable.com/oauth2/v1/authorize?${params}`); +}); + +router.get("/airtable/callback", async (req, res) => { + try { + const { code, state, error } = req.query; + + if (error) { + return res.redirect( + `${process.env.PUBLIC_URL}/robots/${state}/integrate?error=${encodeURIComponent(error.toString())}` + ); + } + + if (!code || !state) { + return res.status(400).json({ message: "Missing authorization code or state" }); + } + + // Verify session data + if (!req.session?.code_verifier || req.session.robotId !== state.toString()) { + return res.status(400).json({ + message: "Session expired - please restart the OAuth flow" + }); + } + + // Exchange code for tokens + const tokenResponse = await fetch("https://airtable.com/oauth2/v1/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code: code.toString(), + client_id: process.env.AIRTABLE_CLIENT_ID!, + + redirect_uri: process.env.AIRTABLE_REDIRECT_URI!, + code_verifier: req.session.code_verifier + }), + }); + + if (!tokenResponse.ok) { + const errorData = await tokenResponse.json(); + console.error('Token exchange failed:', errorData); + return res.redirect( + `${process.env.PUBLIC_URL}/robots/${state}/integrate?error=${encodeURIComponent(errorData.error_description || 'Authentication failed')}` + ); + } + + const tokens = await tokenResponse.json(); + + // Update robot with credentials + const robot = await Robot.findOne({ + where: { "recording_meta.id": req.session.robotId } + }); + + if (!robot) { + return res.status(404).json({ message: "Robot not found" }); + } + + await robot.update({ + airtable_access_token: tokens.access_token, + airtable_refresh_token: tokens.refresh_token, + + }); + + // Clear session data + req.session.destroy((err) => { + if (err) console.error('Session cleanup error:', err); + }); + + res.redirect( + `${process.env.PUBLIC_URL}/robots/${state}/integrate?success=true` + ); + + } catch (error: any) { + console.error('Airtable callback error:', error); + res.redirect( + `${process.env.PUBLIC_URL}/robots/${req.session.robotId}/integrate?error=${encodeURIComponent(error.message)}` + ); + } +}); + +// Get Airtable bases +router.get("/airtable/bases", requireSignIn, async (req: AuthenticatedRequest, res) => { + try { + const { robotId } = req.query; + if (!robotId) { + return res.status(400).json({ message: "Robot ID is required" }); + } + + const robot = await Robot.findOne({ + where: { "recording_meta.id": robotId }, + raw: true, + }); + + if (!robot?.airtable_access_token) { + return res.status(400).json({ message: "Robot not authenticated with Airtable" }); + } + + const response = await fetch('https://api.airtable.com/v0/meta/bases', { + headers: { + 'Authorization': `Bearer ${robot.airtable_access_token}` + } + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error.message || 'Failed to fetch bases'); + } + + const data = await response.json(); + res.json(data.bases.map((base: any) => ({ + id: base.id, + name: base.name + }))); + + } catch (error: any) { + res.status(500).json({ message: error.message }); + } +}); + +// Update robot with selected base +router.post("/airtable/update", requireSignIn, async (req: AuthenticatedRequest, res) => { + const { baseId, baseName, robotId } = req.body; + + if (!baseId || !robotId) { + return res.status(400).json({ message: "Base ID and Robot ID are required" }); + } + + try { + const robot = await Robot.findOne({ + where: { "recording_meta.id": robotId } + }); + + if (!robot) { + return res.status(404).json({ message: "Robot not found" }); + } + + await robot.update({ + airtable_base_id: baseId, + airtable_table_name: baseName, + }); + + capture("maxun-oss-airtable-integration-created", { + user_id: req.user?.id, + robot_id: robotId, + created_at: new Date().toISOString(), + }); + + res.json({ message: "Airtable base updated successfully" }); + + } catch (error: any) { + res.status(500).json({ message: error.message }); + } +}); + +// Remove Airtable integration +router.post("/airtable/remove", requireSignIn, async (req: AuthenticatedRequest, res) => { + const { robotId } = req.body; + if (!robotId) { + return res.status(400).json({ message: "Robot ID is required" }); + } + + try { + const robot = await Robot.findOne({ + where: { "recording_meta.id": robotId } + }); + + if (!robot) { + return res.status(404).json({ message: "Robot not found" }); + } + + await robot.update({ + airtable_access_token: null, + airtable_refresh_token: null, + airtable_base_id: null, + + }); + + capture("maxun-oss-airtable-integration-removed", { + user_id: req.user?.id, + robot_id: robotId, + deleted_at: new Date().toISOString(), + }); + + res.json({ message: "Airtable integration removed successfully" }); + + } catch (error: any) { + res.status(500).json({ message: error.message }); + } +}); + diff --git a/server/src/server.ts b/server/src/server.ts index 8c28c2d2..94197cfb 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -18,6 +18,7 @@ import { fork } from 'child_process'; import { capture } from "./utils/analytics"; import swaggerUi from 'swagger-ui-express'; import swaggerSpec from './swagger/config'; +import session from 'express-session'; const app = express(); app.use(cors({ @@ -26,6 +27,16 @@ app.use(cors({ })); app.use(express.json()); + +app.use( + session({ + secret: 'your_secret_key', // Replace with a secure secret key + resave: false, // Do not resave the session if it hasn't changed + saveUninitialized: true, // Save new sessions + cookie: { secure: false }, // Set to true if using HTTPS + }) +); + const server = http.createServer(app); /** diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index f9c397ae..9101b9a7 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -6,7 +6,6 @@ import { CircularProgress, Alert, AlertTitle, - Chip, } from "@mui/material"; import Button from "@mui/material/Button"; import TextField from "@mui/material/TextField"; @@ -14,10 +13,9 @@ import axios from "axios"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { getStoredRecording } from "../../api/storage"; import { apiUrl } from "../../apiConfig.js"; -import Cookies from 'js-cookie'; +import Cookies from "js-cookie"; import { useTranslation } from "react-i18next"; - interface IntegrationProps { isOpen: boolean; handleStart: (data: IntegrationSettings) => void; @@ -27,6 +25,8 @@ interface IntegrationProps { export interface IntegrationSettings { spreadsheetId: string; spreadsheetName: string; + airtableBaseId: string; + airtableBaseName: string; data: string; } @@ -35,7 +35,7 @@ const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) { - return parts.pop()?.split(';').shift() || null; + return parts.pop()?.split(";").shift() || null; } return null; }; @@ -53,58 +53,114 @@ export const IntegrationSettingsModal = ({ const [settings, setSettings] = useState({ spreadsheetId: "", spreadsheetName: "", + airtableBaseId: "", + airtableBaseName: "", data: "", }); - const [spreadsheets, setSpreadsheets] = useState< - { id: string; name: string }[] - >([]); + const [spreadsheets, setSpreadsheets] = useState<{ id: string; name: string }[]>([]); + const [airtableBases, setAirtableBases] = useState<{ id: string; name: string }[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const { recordingId, notify } = useGlobalInfoStore(); const [recording, setRecording] = useState(null); + // Authenticate with Google const authenticateWithGoogle = () => { - window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}`; + const redirectUri = `${window.location.origin}/google/callback`; + window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}&redirect_uri=${encodeURIComponent(redirectUri)}`; + }; + + const authenticateWithAirtable = () => { + const redirectUri = `${window.location.origin}/airtable/callback`; + window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}&redirect_uri=${encodeURIComponent(redirectUri)}`; }; - const handleOAuthCallback = async () => { + // Handle OAuth callback + const handleGoogleCallback = async () => { try { - const response = await axios.get(`${apiUrl}/auth/google/callback`); - const { google_sheet_email, files } = response.data; + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + + if (!code) { + setError(t("integration_settings.errors.no_auth_code")); + return; + } + + const response = await axios.get( + `${apiUrl}/auth/google/callback?code=${code}&robotId=${recordingId}` + ); + + if (response.data.accessToken) { + notify("success", t("integration_settings.notifications.google_auth_success")); + await fetchSpreadsheetFiles(); + } + + // Clear URL parameters + window.history.replaceState({}, document.title, window.location.pathname); + } catch (error) { - setError("Error authenticating with Google"); + setError(t("integration_settings.errors.google_auth_error")); } }; + const handleAirtableCallback = async () => { + try { + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + + if (!code) { + setError(t("integration_settings.errors.no_auth_code")); + return; + } + + const response = await axios.get( + `${apiUrl}/auth/airtable/callback?code=${code}&robotId=${recordingId}` + ); + + if (response.data.accessToken) { + notify("success", t("integration_settings.notifications.airtable_auth_success")); + await fetchAirtableBases(); + } + + // Clear URL parameters + window.history.replaceState({}, document.title, window.location.pathname); + + } catch (error) { + setError(t("integration_settings.errors.airtable_auth_error")); + } + }; + + // Fetch Google Sheets const fetchSpreadsheetFiles = async () => { try { - const response = await axios.get( - `${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, - { - withCredentials: true, - } - ); + const response = await axios.get(`${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, { + withCredentials: true, + }); setSpreadsheets(response.data); } catch (error: any) { - console.error( - "Error fetching spreadsheet files:", - error.response?.data?.message || error.message - ); - notify( - "error", - t('integration_settings.errors.fetch_error', { - message: error.response?.data?.message || error.message - }) - ); + console.error("Error fetching spreadsheet files:", error.response?.data?.message || error.message); + notify("error", t("integration_settings.errors.fetch_error", { message: error.response?.data?.message || error.message })); } }; + // Fetch Airtable Bases + const fetchAirtableBases = async () => { + try { + const response = await axios.get(`${apiUrl}/auth/airtable/bases?robotId=${recordingId}`, { + withCredentials: true, + }); + setAirtableBases(response.data); + } catch (error: any) { + console.error("Error fetching Airtable bases:", error.response?.data?.message || error.message); + notify("error", t("integration_settings.errors.fetch_error", { message: error.response?.data?.message || error.message })); + } + }; + + // Handle Google Sheet selection const handleSpreadsheetSelect = (e: React.ChangeEvent) => { - const selectedSheet = spreadsheets.find( - (sheet) => sheet.id === e.target.value - ); + const selectedSheet = spreadsheets.find((sheet) => sheet.id === e.target.value); if (selectedSheet) { setSettings({ ...settings, @@ -114,6 +170,19 @@ export const IntegrationSettingsModal = ({ } }; + // Handle Airtable Base selection + const handleAirtableBaseSelect = (e: React.ChangeEvent) => { + const selectedBase = airtableBases.find((base) => base.id === e.target.value); + if (selectedBase) { + setSettings({ + ...settings, + airtableBaseId: selectedBase.id, + airtableBaseName: selectedBase.name, + }); + } + }; + + // Update Google Sheet ID const updateGoogleSheetId = async () => { try { const response = await axios.post( @@ -125,16 +194,33 @@ export const IntegrationSettingsModal = ({ }, { withCredentials: true } ); - notify(`success`, t('integration_settings.notifications.sheet_selected')); + notify("success", t("integration_settings.notifications.sheet_selected")); console.log("Google Sheet ID updated:", response.data); } catch (error: any) { - console.error( - "Error updating Google Sheet ID:", - error.response?.data?.message || error.message - ); + console.error("Error updating Google Sheet ID:", error.response?.data?.message || error.message); } }; + // Update Airtable Base ID + const updateAirtableBaseId = async () => { + try { + const response = await axios.post( + `${apiUrl}/auth/airtable/update`, + { + baseId: settings.airtableBaseId, + baseName: settings.airtableBaseName, + robotId: recordingId, + }, + { withCredentials: true } + ); + notify("success", t("integration_settings.notifications.base_selected")); + console.log("Airtable Base ID updated:", response.data); + } catch (error: any) { + console.error("Error updating Airtable Base ID:", error.response?.data?.message || error.message); + } + }; + + // Remove Integration const removeIntegration = async () => { try { await axios.post( @@ -145,98 +231,89 @@ export const IntegrationSettingsModal = ({ setRecording(null); setSpreadsheets([]); - setSettings({ spreadsheetId: "", spreadsheetName: "", data: "" }); + setAirtableBases([]); + setSettings({ spreadsheetId: "", spreadsheetName: "", airtableBaseId: "", airtableBaseName: "", data: "" }); } catch (error: any) { - console.error( - "Error removing Google Sheets integration:", - error.response?.data?.message || error.message - ); + console.error("Error removing integration:", error.response?.data?.message || error.message); } }; useEffect(() => { - // Check if there is a success message in cookies - const status = getCookie("robot_auth_status"); - const message = getCookie("robot_auth_message"); - - if (status === "success" && message) { - notify("success", message); - // Clear the cookies after reading - removeCookie("robot_auth_status"); - removeCookie("robot_auth_message"); - } - - // Check if we're on the callback URL - const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get("code"); - if (code) { - handleOAuthCallback(); - } - - const fetchRecordingInfo = async () => { - if (!recordingId) return; - const recording = await getStoredRecording(recordingId); - if (recording) { - setRecording(recording); + const checkAuthCallback = () => { + const path = window.location.pathname; + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + + if (code) { + if (path.includes('/google/callback')) { + handleGoogleCallback(); + } else if (path.includes('/airtable/callback')) { + handleAirtableCallback(); + } } }; - - fetchRecordingInfo(); - }, [recordingId]); + + checkAuthCallback(); + + // Cleanup function + return () => { + window.history.replaceState({}, document.title, window.location.pathname); + }; + }, []); return ( -
- - {t('integration_settings.title')} - +
+ {t("integration_settings.title")} - {recording && recording.google_sheet_id ? ( + {recording && (recording.google_sheet_id || recording.airtable_base_id) ? ( <> - - {t('integration_settings.alerts.success.title')} - {t('integration_settings.alerts.success.content', { sheetName: recording.google_sheet_name })} - - {t('integration_settings.alerts.success.here')} - . -
- {t('integration_settings.alerts.success.note')} {t('integration_settings.alerts.success.sync_limitation')} -
- ) : ( <> - {!recording?.google_sheet_email ? ( + {!recording?.google_sheet_email && !recording?.airtable_email ? ( <> -

{t('integration_settings.descriptions.sync_info')}

- + ) : ( <> - {recording.google_sheet_email && ( + {(recording.google_sheet_email || recording.airtable_email) && ( - {t('integration_settings.descriptions.authenticated_as', { - email: recording.google_sheet_email + {t("integration_settings.descriptions.authenticated_as", { + email: recording.google_sheet_email || recording.airtable_email, })} )} @@ -245,64 +322,91 @@ export const IntegrationSettingsModal = ({ ) : error ? ( {error} - ) : spreadsheets.length === 0 ? ( - <> -
- - -
- ) : ( <> - - {spreadsheets.map((sheet) => ( - - {sheet.name} - - ))} - + {recording.google_sheet_email && ( + <> + + {spreadsheets.map((sheet) => ( + + {sheet.name} + + ))} + - {settings.spreadsheetId && ( - - {t('integration_settings.fields.selected_sheet', { - name: spreadsheets.find((s) => s.id === settings.spreadsheetId)?.name, - id: settings.spreadsheetId - })} - + {settings.spreadsheetId && ( + + {t("integration_settings.fields.selected_sheet", { + name: spreadsheets.find((s) => s.id === settings.spreadsheetId)?.name, + id: settings.spreadsheetId, + })} + + )} + + + )} - + {recording.airtable_email && ( + <> + + {airtableBases.map((base) => ( + + {base.name} + + ))} + + + {settings.airtableBaseId && ( + + {t("integration_settings.fields.selected_base", { + name: airtableBases.find((b) => b.id === settings.airtableBaseId)?.name, + id: settings.airtableBaseId, + })} + + )} + + + + )} )}