diff --git a/.env.example b/.env.example index 0a43f2ad..48222264 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,9 @@ S3_SECRET_ACCESS_KEY= BETTER_AUTH_SECRET= BETTER_AUTH_TRUSTED_ORIGINS= +TRELLO_APP_API_KEY= +TRELLO_APP_SECRET= + GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= DISCORD_CLIENT_ID= diff --git a/README.md b/README.md index 7dea739a..35e7f197 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,8 @@ pnpm dev | `DISCORD_CLIENT_SECRET` | Discord OAuth client secret | For Discord login | `xxx` | | `GITHUB_CLIENT_ID` | GitHub OAuth client ID | For GitHub login | `xxx` | | `GITHUB_CLIENT_SECRET` | GitHub OAuth client secret | For GitHub login | `xxx` | +| `TRELLO_APP_API_KEY` | Trello app API key | For Trello import | `xxx` | +| `TRELLO_APP_API_SECRET` | Trello app API secret | For Trello import | `xxx` | | `S3_REGION` | S3 storage region | For file uploads | `WEUR` | | `S3_ENDPOINT` | S3 endpoint URL | For file uploads | `https://xxx.r2.cloudflarestorage.com` | | `S3_ACCESS_KEY_ID` | S3 access key | For file uploads | `xxx` | diff --git a/apps/docs/imports/trello.mdx b/apps/docs/imports/trello.mdx index ab402779..b93a19b1 100644 --- a/apps/docs/imports/trello.mdx +++ b/apps/docs/imports/trello.mdx @@ -3,31 +3,25 @@ title: Trello description: "Import your Trello boards to Kan." --- -## Getting your Trello API key and token +## Authorizing Kan.bn for Trello -Before you can import your Trello boards, you need to get your Trello API key and token. Follow these steps (only takes a couple of minutes): +Before you can import your Trello boards, you need to authorize Kan.bn for Trello. Follow these steps (only takes a couple of minutes): -1. **Sign in to your Trello account** - Go to [https://trello.com](https://trello.com) and log in with your Trello account. +1. **Authorize Kan.bn for Trello** + Go to [https://kan.bn/settings](https://kan.bn/settings) and click **Connect Trello**. -2. **Create a new integration** - Visit [https://trello.com/power-ups/admin](https://trello.com/power-ups/admin). Click **Create new** and fill in the following details: +2. **Log in to your Trello account** + You will be redirected to Trello to log in to your Trello account. - - **Name**: Choose a name for your integration (e.g. "Kan Import") - - **Workspace**: Select the workspace containing the boards you want to import - - **Email**: Enter your email address - - **Support email**: Enter your email address - - **Author**: Add your name +3. **Authorize Kan.bn for Trello** + Approve the permissions and you will be redirected back to Kan.bn. -3. **Get a Trello API Key** - Now click **Generate a new API key** to create an API key. - -4. **Generate a Trello Token** - On the same page (to the right of the API key), click the link to **generate a Token**. Approve the permissions and copy your token (make sure to save it somewhere secure like a password manager). +4. **Verify the connection** + You should see a **Disconnect Trello** button. ## Importing your Trello boards -Once you have your API key and token, importing your Trello boards is easy. Follow these steps: +Once you have authorized Kan.bn for Trello, importing your Trello boards is easy. Follow these steps: 1. **Create a new import in Kan** Navigate to **[https://kan.bn/boards](https://kan.dev/boards)** and click **Import**. @@ -35,8 +29,5 @@ Once you have your API key and token, importing your Trello boards is easy. Foll 2. **Select Trello as the Import Source** Choose **Trello** from the list of import options and click **Select source**. -3. **Enter Your Trello API Key and Token** - Paste your API key and token into the provided fields and click **Fetch boards** (don't worry, we don't store your key or token). - -4. **Start the Import** +3. **Start the Import** Select the board you want to import and click **Import boards**. Once the import is complete, you'll see your boards in Kan. diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index 2eca268d..ad42bbf0 100644 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -22,6 +22,8 @@ export const env = createEnv({ ) .optional(), POSTGRES_URL: z.string().url(), + TRELLO_APP_API_KEY: z.string().optional(), + TRELLO_APP_SECRET: z.string().optional(), STRIPE_SECRET_KEY: z.string().optional(), GOOGLE_CLIENT_ID: z.string().optional(), GOOGLE_CLIENT_SECRET: z.string().optional(), diff --git a/apps/web/src/pages/api/auth/[...all].ts b/apps/web/src/pages/api/auth/[...all].ts index 7c619619..f81033aa 100644 --- a/apps/web/src/pages/api/auth/[...all].ts +++ b/apps/web/src/pages/api/auth/[...all].ts @@ -5,6 +5,6 @@ import { createDrizzleClient } from "@kan/db/client"; export const config = { api: { bodyParser: false } }; -const auth = initAuth(createDrizzleClient()); +export const auth = initAuth(createDrizzleClient()); export default toNodeHandler(auth.handler); diff --git a/apps/web/src/pages/api/trello/authenticate.ts b/apps/web/src/pages/api/trello/authenticate.ts new file mode 100644 index 00000000..50cbb9b9 --- /dev/null +++ b/apps/web/src/pages/api/trello/authenticate.ts @@ -0,0 +1,51 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { createNextApiContext } from "@kan/api/trpc"; +import { integrations } from "@kan/db/schema"; +import { addYears } from "date-fns"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method !== "POST") { + return res.status(405).json({ message: "Method not allowed" }); + } + + const { user } = await createNextApiContext(req); + + if (!user) + return res.status(401).json({ message: "User not authenticated" }); + + const apiKey = process.env.TRELLO_APP_API_KEY; + + if (!apiKey) + return res.status(500).json({ message: "Trello API key not set in Environment Variables" }); + + const token = req.body.token; + + if (!token) + return res.status(400).json({ message: "No token found" }); + + try { + const { db } = await createNextApiContext(req); + + await db.insert(integrations).values({ + provider: "trello", + userId: user.id, + accessToken: token, + expiresAt: addYears(new Date(), 1), + }).onConflictDoUpdate({ + set: { + accessToken: token, + expiresAt: addYears(new Date(), 1), + }, + target: [integrations.userId, integrations.provider], + }); + + return res.status(200).json({ message: "Trello authentication successful" }); + } catch (err) { + console.error("Trello authentication error:", err); + return res.status(400).json({ message: "Trello authentication failed" }); + } +} \ No newline at end of file diff --git a/apps/web/src/pages/settings/trello/authorize.tsx b/apps/web/src/pages/settings/trello/authorize.tsx new file mode 100644 index 00000000..bada6e5c --- /dev/null +++ b/apps/web/src/pages/settings/trello/authorize.tsx @@ -0,0 +1,24 @@ +import { useEffect } from "react"; + +export default function TrelloAuthorize() { + useEffect(() => { + const hash = window.location.hash; + const token = hash.split("=")[1]; + if (token) { + console.log("Posting token to /api/trello/authenticate", token); + fetch("/api/trello/authenticate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ token }), + }).then(() => { + window.close(); + }); + } + }, []); + + return
+

Connecting to Trello...

+
; +} \ No newline at end of file diff --git a/apps/web/src/views/boards/components/ImportBoardsForm.tsx b/apps/web/src/views/boards/components/ImportBoardsForm.tsx index d8aab5e1..f537a381 100644 --- a/apps/web/src/views/boards/components/ImportBoardsForm.tsx +++ b/apps/web/src/views/boards/components/ImportBoardsForm.tsx @@ -1,37 +1,68 @@ import Link from "next/link"; import { Listbox, Transition } from "@headlessui/react"; -import { Fragment, useState } from "react"; +import { Fragment, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { FaTrello } from "react-icons/fa"; import { HiChevronUpDown, + HiMiniArrowTopRightOnSquare, HiOutlineQuestionMarkCircle, HiXMark, } from "react-icons/hi2"; import Button from "~/components/Button"; -import Input from "~/components/Input"; +import Toggle from "~/components/Toggle"; import { useModal } from "~/providers/modal"; import { usePopup } from "~/providers/popup"; import { useWorkspace } from "~/providers/workspace"; import { api } from "~/utils/api"; -interface TrelloFormValues { - apiKey: string; - token: string; -} - -const sources = [{ source: "Trello" }]; +const integrationProviders: Record< + string, + { name: string; icon: JSX.Element } +> = { + trello: { + name: "Trello", + icon: , + }, +}; const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => { + const { data: integrations, refetch: refetchIntegrations } = + api.integration.providers.useQuery(); const { control, handleSubmit } = useForm({ defaultValues: { - source: "Trello", + source: integrations?.[0]?.provider ?? "trello", }, }); + const { data: trelloUrl } = api.integration.getAuthorizationUrl.useQuery( + { provider: "trello" }, + { + enabled: !integrations?.some( + (integration) => integration.provider === "trello", + ), + }, + ); + + const hasIntegrations = integrations && integrations.length > 0; + + useEffect(() => { + const handleFocus = () => { + refetchIntegrations(); + }; + window.addEventListener("focus", handleFocus); + return () => { + window.removeEventListener("focus", handleFocus); + }; + }, [refetchIntegrations]); + const onSubmit = () => { - handleNextStep(); + if (!hasIntegrations && trelloUrl) { + window.open(trelloUrl.url, "trello_auth", "height=800,width=600"); + } else { + handleNextStep(); + } }; return ( @@ -47,9 +78,9 @@ const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => {
- + {integrationProviders[field.value]?.icon} - {field.value} + {integrationProviders[field.value]?.name} @@ -68,20 +99,41 @@ const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => { leaveTo="opacity-0" > - {sources.map(({ source }, index) => ( + {hasIntegrations ? ( + integrations.map((integration, index) => ( + +
+ { + integrationProviders[integration.provider] + ?.icon + } + + { + integrationProviders[integration.provider] + ?.name + } + +
+
+ )) + ) : (
- + {integrationProviders.trello?.icon} - {source} + {integrationProviders.trello?.name}
- ))} + )}
@@ -94,7 +146,14 @@ const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => {
- +
@@ -103,25 +162,26 @@ const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => { const ImportTrello: React.FC = () => { const utils = api.useUtils(); - const [apiKey, setApiKey] = useState(""); - const [token, setToken] = useState(""); const { closeModal } = useModal(); const { workspace } = useWorkspace(); const { showPopup } = usePopup(); + const [isSelectAllEnabled, setIsSelectAllEnabled] = useState(false); const refetchBoards = () => utils.board.all.refetch(); - const boards = api.import.trello.getBoards.useQuery( - { apiKey, token }, - { - enabled: apiKey && token ? true : false, - }, - ); + const { data: boards, isLoading: boardsLoading } = + api.import.trello.getBoards.useQuery(); - const handleSetAuthDetails = (apiKey: string, token: string) => { - setApiKey(apiKey); - setToken(token); - }; + const { + register: registerBoards, + handleSubmit: handleSubmitBoards, + setValue, + watch, + } = useForm({ + defaultValues: Object.fromEntries( + boards?.map((board) => [board.id, true]) ?? [], + ), + }); const importBoards = api.import.trello.importBoards.useMutation({ onSuccess: async () => { @@ -146,83 +206,94 @@ const ImportTrello: React.FC = () => { }, }); - const { register, handleSubmit } = useForm({ - defaultValues: { - apiKey: "", - token: "", - }, - }); - - const onSubmit = (values: TrelloFormValues) => { - handleSetAuthDetails(values.apiKey, values.token); - }; - - const { register: registerBoards, handleSubmit: handleSubmitBoards } = - useForm({ - defaultValues: Object.fromEntries( - boards.data?.map((board) => [board.id, true]) ?? [], - ), - }); + const boardWatchers = boards?.map((board) => ({ + id: board.id, + value: watch(board.id), + })); const onSubmitBoards = (values: Record) => { const boardIds = Object.keys(values).filter((key) => values[key] === true); importBoards.mutate({ boardIds, - apiKey, - token, workspacePublicId: workspace.publicId, }); }; - if (boards.data?.length) - return ( -
-
- {boards.data.map((board) => ( -
- -
- ))} + const renderContent = () => { + if (boardsLoading) { + return ( +
+
+
+
+ ); + } -
-
- -
+ if (!boards?.length) { + return ( +
+

+ No boards found +

- - ); + ); + } + + return boards.map((board) => ( +
+ +
+ )); + }; return ( -
-
- - -
+ +
{renderContent()}
-
-
diff --git a/apps/web/src/views/settings/index.tsx b/apps/web/src/views/settings/index.tsx index 678fdff4..6f548a57 100644 --- a/apps/web/src/views/settings/index.tsx +++ b/apps/web/src/views/settings/index.tsx @@ -1,4 +1,5 @@ import { env } from "next-runtime-env"; +import { useEffect } from "react"; import { HiMiniArrowTopRightOnSquare } from "react-icons/hi2"; import Button from "~/components/Button"; @@ -6,6 +7,7 @@ import Modal from "~/components/modal"; import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; import { PageHead } from "~/components/PageHead"; import { useModal } from "~/providers/modal"; +import { usePopup } from "~/providers/popup"; import { useWorkspace } from "~/providers/workspace"; import { api } from "~/utils/api"; import Avatar from "./components/Avatar"; @@ -22,9 +24,54 @@ export default function SettingsPage() { const { modalContentType, openModal } = useModal(); const { workspace } = useWorkspace(); const utils = api.useUtils(); + const { showPopup } = usePopup(); const { data } = api.user.getUser.useQuery(); + const { + data: integrations, + refetch: refetchIntegrations, + isLoading: integrationsLoading, + } = api.integration.providers.useQuery(); + + const { data: trelloUrl, refetch: refetchTrelloUrl } = + api.integration.getAuthorizationUrl.useQuery({ provider: "trello" }, { + enabled: + !integrationsLoading && + !integrations?.some((integration) => integration.provider === "trello"), + refetchOnWindowFocus: true, + }); + + useEffect(() => { + const handleFocus = () => { + refetchIntegrations(); + }; + window.addEventListener("focus", handleFocus); + return () => { + window.removeEventListener("focus", handleFocus); + }; + }, [refetchIntegrations]); + + const { mutateAsync: disconnectTrello } = api.integration.disconnect.useMutation({ + onSuccess: () => { + refetchUser(); + refetchIntegrations(); + refetchTrelloUrl(); + showPopup({ + header: "Trello disconnected", + message: "Your Trello account has been disconnected.", + icon: "success", + }); + }, + onError: () => { + showPopup({ + header: "Error disconnecting Trello", + message: "An error occurred while disconnecting your Trello account.", + icon: "error", + }); + }, + }); + const refetchUser = () => utils.user.getUser.refetch(); const handleOpenBillingPortal = async () => { @@ -115,6 +162,48 @@ export default function SettingsPage() {
)} +
+

+ Trello +

+ {!integrations?.some( + (integration) => integration.provider === "trello", + ) && trelloUrl ? ( + <> +

+ Connect your Trello account to import boards. +

+ + + ) : ( + <> +

+ You are already connected to Trello. +

+ + + )} +
+

API keys diff --git a/docker-compose.yml b/docker-compose.yml index 8e260230..e550c580 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,8 @@ services: - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} - POSTGRES_URL=${POSTGRES_URL} + - TRELLO_APP_API_KEY=${TRELLO_APP_API_KEY} + - TRELLO_APP_SECRET=${TRELLO_APP_SECRET} - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} - DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID} diff --git a/packages/api/src/openapi.ts b/packages/api/src/openapi.ts index 9a7ad7b4..afe56656 100644 --- a/packages/api/src/openapi.ts +++ b/packages/api/src/openapi.ts @@ -9,5 +9,5 @@ export const openApiDocument = generateOpenApiDocument(appRouter, { version: "1.0.0", baseUrl: `${env("NEXT_PUBLIC_BASE_URL")}/api/v1`, docsUrl: "docs.kan.bn", - tags: ["Auth", "Users", "Boards", "Lists", "Cards", "Labels", "Imports"], + tags: ["Auth", "Users", "Boards", "Lists", "Cards", "Labels", "Imports", "Integrations"], }); diff --git a/packages/api/src/root.ts b/packages/api/src/root.ts index 691a6e3f..8005995d 100644 --- a/packages/api/src/root.ts +++ b/packages/api/src/root.ts @@ -2,6 +2,7 @@ import { boardRouter } from "./routers/board"; import { cardRouter } from "./routers/card"; import { feedbackRouter } from "./routers/feedback"; import { importRouter } from "./routers/import"; +import { integrationRouter } from "./routers/integration"; import { labelRouter } from "./routers/label"; import { listRouter } from "./routers/list"; import { memberRouter } from "./routers/member"; @@ -19,6 +20,7 @@ export const appRouter = createTRPCRouter({ import: importRouter, user: userRouter, workspace: workspaceRouter, + integration: integrationRouter, }); export type AppRouter = typeof appRouter; diff --git a/packages/api/src/routers/import.ts b/packages/api/src/routers/import.ts index 205e7459..88e7125e 100644 --- a/packages/api/src/routers/import.ts +++ b/packages/api/src/routers/import.ts @@ -5,6 +5,7 @@ import * as boardRepo from "@kan/db/repository/board.repo"; import * as cardRepo from "@kan/db/repository/card.repo"; import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo"; import * as importRepo from "@kan/db/repository/import.repo"; +import * as integrationsRepo from "@kan/db/repository/integration.repo"; import * as labelRepo from "@kan/db/repository/label.repo"; import * as listRepo from "@kan/db/repository/list.repo"; import * as workspaceRepo from "@kan/db/repository/workspace.repo"; @@ -13,10 +14,9 @@ import { generateUID } from "@kan/shared/utils"; import { createTRPCRouter, protectedProcedure } from "../trpc"; import { assertUserInWorkspace } from "../utils/auth"; +import { apiKeys, urls } from "./integration"; -const TRELLO_API_URL = "https://api.trello.com/1"; - -interface TrelloBoard { +export interface TrelloBoard { id: string; name: string; labels: TrelloLabel[]; @@ -42,10 +42,6 @@ interface TrelloCard { labels: TrelloLabel[]; } -interface MemberData { - idBoards: string[]; -} - export const importRouter = createTRPCRouter({ trello: createTRPCRouter({ getBoards: protectedProcedure @@ -53,46 +49,51 @@ export const importRouter = createTRPCRouter({ openapi: { summary: "Get boards from Trello", method: "GET", - path: "/imports/trello/boards", + path: "/integrations/trello/boards", description: "Retrieves all boards from Trello", - tags: ["Imports"], + tags: ["Integrations"], protect: true, }, }) - .input( - z.object({ - apiKey: z.string().length(32), - token: z.string().length(76), - }), - ) .output(z.array(z.object({ id: z.string(), name: z.string() }))) - .query(async ({ input }) => { - const fetchMemberRes = await fetch( - `${TRELLO_API_URL}/tokens/${input.token}/member?key=${input.apiKey}`, + .query(async ({ ctx }) => { + const apiKey = apiKeys.trello; + + if (!apiKey) + throw new TRPCError({ + message: "Trello API key not found", + code: "INTERNAL_SERVER_ERROR", + }); + + const user = ctx.user; + + if (!user) + throw new TRPCError({ + message: "User not authenticated", + code: "UNAUTHORIZED", + }); + + const integration = await integrationsRepo.getProviderForUser( + ctx.db, + user.id, + "trello", ); - const member = (await fetchMemberRes.json()) as MemberData; + const token = integration?.accessToken; - const boardIds = member.idBoards; + if (!token) + throw new TRPCError({ + message: "Trello token not found", + code: "UNAUTHORIZED", + }); - const fetchBoard = async (boardId: string) => { - const response = await fetch( - `${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}`, - ); - const data = (await response.json()) as TrelloBoard; + const response = await fetch( + `${urls.trello}/members/me/boards?key=${apiKey}&token=${token}`, + ); - return data; - }; + const data = (await response.json()) as TrelloBoard[]; - const boards = []; - - for (const boardId of boardIds) { - boards.push(Promise.resolve(fetchBoard(boardId))); - } - - const boardDataArray = await Promise.all(boards); - - return boardDataArray.map((board) => ({ + return data.map((board) => ({ id: board.id, name: board.name, })); @@ -102,7 +103,7 @@ export const importRouter = createTRPCRouter({ openapi: { summary: "Import boards from Trello", method: "POST", - path: "/imports/trello/import", + path: "/imports/trello/boards", description: "Imports boards from Trello", tags: ["Imports"], protect: true, @@ -111,8 +112,6 @@ export const importRouter = createTRPCRouter({ .input( z.object({ boardIds: z.array(z.string()), - apiKey: z.string().length(32), - token: z.string().length(76), workspacePublicId: z.string().min(12), }), ) @@ -120,12 +119,32 @@ export const importRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; + const apiKey = apiKeys.trello; + + if (!apiKey) + throw new TRPCError({ + message: "Trello API key not found", + code: "INTERNAL_SERVER_ERROR", + }); + if (!userId) throw new TRPCError({ message: `User not authenticated`, code: "UNAUTHORIZED", }); + const integration = await integrationsRepo.getProviderForUser( + ctx.db, + userId, + "trello", + ); + + if (!integration) + throw new TRPCError({ + message: "Trello token not found", + code: "UNAUTHORIZED", + }); + const workspace = await workspaceRepo.getByPublicId( ctx.db, input.workspacePublicId, @@ -150,7 +169,7 @@ export const importRouter = createTRPCRouter({ for (const boardId of input.boardIds) { const response = await fetch( - `${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}&lists=open&cards=open&labels=all`, + `${urls.trello}/boards/${boardId}?key=${apiKey}&token=${integration.accessToken}&lists=open&cards=open&labels=all`, ); const data = (await response.json()) as TrelloBoard; diff --git a/packages/api/src/routers/integration.ts b/packages/api/src/routers/integration.ts new file mode 100644 index 00000000..07864c7d --- /dev/null +++ b/packages/api/src/routers/integration.ts @@ -0,0 +1,128 @@ +import { TRPCError } from "@trpc/server"; +import { env } from "next-runtime-env"; +import { z } from "zod"; + +import * as integrationsRepo from "@kan/db/repository/integration.repo"; + +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const urls = { + trello: "https://api.trello.com/1", +}; + +export const apiKeys = { + trello: process.env.TRELLO_APP_API_KEY, +}; + +export const integrationRouter = createTRPCRouter({ + providers: protectedProcedure.query(async ({ ctx }) => { + const user = ctx.user; + + if (!user) + throw new TRPCError({ + message: "User not authenticated", + code: "UNAUTHORIZED", + }); + + const integrations = await integrationsRepo.getProvidersForUser( + ctx.db, + user.id, + ); + + return integrations; + }), + disconnect: protectedProcedure + .meta({ + openapi: { + summary: "Disconnect integration", + method: "POST", + path: "/integration/disconnect", + description: "Disconnects an integration", + tags: ["Integration"], + protect: true, + }, + }) + .input(z.object({ provider: z.enum(["trello"]) })) + .output(z.object({})) + .mutation(async ({ ctx, input }) => { + const user = ctx.user; + + if (!user) + throw new TRPCError({ + message: "User not authenticated", + code: "UNAUTHORIZED", + }); + + const integration = await integrationsRepo.getProviderForUser( + ctx.db, + user.id, + input.provider, + ); + + if (!integration) + throw new TRPCError({ + message: "Integration not found", + code: "NOT_FOUND", + }); + + await integrationsRepo.deleteProviderForUser( + ctx.db, + user.id, + input.provider, + ); + + return {}; + }), + getAuthorizationUrl: protectedProcedure + .meta({ + openapi: { + summary: "Get authorization URL for an integration", + method: "GET", + path: "/integration/authorize", + description: "Retrieves the authorization URL for an integration", + tags: ["Integration"], + protect: true, + }, + }) + .input(z.object({ provider: z.enum(["trello"]) })) + .output(z.object({ url: z.string() })) + .query(async ({ ctx, input }) => { + const apiKey = apiKeys[input.provider]; + + if (!apiKey) + throw new TRPCError({ + message: `${input.provider.at(0)?.toUpperCase() + input.provider.slice(1)} API key not set in environment variables`, + code: "INTERNAL_SERVER_ERROR", + }); + + const user = ctx.user; + + if (!user) + throw new TRPCError({ + message: "User not authenticated", + code: "UNAUTHORIZED", + }); + + const integration = await integrationsRepo.getProviderForUser( + ctx.db, + user.id, + input.provider, + ); + + if (integration) + throw new TRPCError({ + message: `${input.provider.at(0)?.toUpperCase() + input.provider.slice(1)} integration already exists`, + code: "BAD_REQUEST", + }); + + if (input.provider === "trello") { + const url = `${urls[input.provider]}/authorize?key=${apiKey}&expiration=never&response_type=token&scope=read&return_url=${env("NEXT_PUBLIC_BASE_URL")}/settings/trello/authorize&callback_method=fragment`; + return { url }; + } + + throw new TRPCError({ + message: "Invalid provider", + code: "BAD_REQUEST", + }); + }), +}); diff --git a/packages/db/migrations/20250608100932_AddIntegrationsTable.sql b/packages/db/migrations/20250608100932_AddIntegrationsTable.sql new file mode 100644 index 00000000..d7ac54b3 --- /dev/null +++ b/packages/db/migrations/20250608100932_AddIntegrationsTable.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS "integration" ( + "provider" varchar(255) NOT NULL, + "userId" uuid NOT NULL, + "accessToken" varchar(255) NOT NULL, + "refreshToken" varchar(255), + "expiresAt" timestamp NOT NULL, + "createdAt" timestamp NOT NULL, + "updatedAt" timestamp, + CONSTRAINT "integration_pkey" PRIMARY KEY("userId","provider") +); +--> statement-breakpoint +ALTER TABLE "integration" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "integration" ADD CONSTRAINT "integration_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/packages/db/migrations/meta/20250608100932_snapshot.json b/packages/db/migrations/meta/20250608100932_snapshot.json new file mode 100644 index 00000000..fc4daccf --- /dev/null +++ b/packages/db/migrations/meta/20250608100932_snapshot.json @@ -0,0 +1,2164 @@ +{ + "id": "d170c8e0-bf75-4c71-abd9-0ecbeeb14003", + "prevId": "1a07a776-0481-49f9-9098-02d530b37cf5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.apiKey": { + "name": "apiKey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "refillInterval": { + "name": "refillInterval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refillAmount": { + "name": "refillAmount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lastRefillAt": { + "name": "lastRefillAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rateLimitEnabled": { + "name": "rateLimitEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rateLimitTimeWindow": { + "name": "rateLimitTimeWindow", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rateLimitMax": { + "name": "rateLimitMax", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "requestCount": { + "name": "requestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lastRequest": { + "name": "lastRequest", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.board": { + "name": "board", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "workspaceId": { + "name": "workspaceId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "board_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'private'" + } + }, + "indexes": { + "board_visibility_idx": { + "name": "board_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_slug_per_workspace": { + "name": "unique_slug_per_workspace", + "columns": [ + { + "expression": "workspaceId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"board\".\"deletedAt\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_createdBy_user_id_fk": { + "name": "board_createdBy_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "board_deletedBy_user_id_fk": { + "name": "board_deletedBy_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "board_importId_import_id_fk": { + "name": "board_importId_import_id_fk", + "tableFrom": "board", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "board_workspaceId_workspace_id_fk": { + "name": "board_workspaceId_workspace_id_fk", + "tableFrom": "board", + "tableTo": "workspace", + "columnsFrom": [ + "workspaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "board_publicId_unique": { + "name": "board_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.card_activity": { + "name": "card_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "card_activity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "fromIndex": { + "name": "fromIndex", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "toIndex": { + "name": "toIndex", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fromListId": { + "name": "fromListId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "toListId": { + "name": "toListId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "labelId": { + "name": "labelId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "workspaceMemberId": { + "name": "workspaceMemberId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "fromTitle": { + "name": "fromTitle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "toTitle": { + "name": "toTitle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "fromDescription": { + "name": "fromDescription", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "toDescription": { + "name": "toDescription", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "commentId": { + "name": "commentId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "fromComment": { + "name": "fromComment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "toComment": { + "name": "toComment", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "card_activity_cardId_card_id_fk": { + "name": "card_activity_cardId_card_id_fk", + "tableFrom": "card_activity", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_activity_fromListId_list_id_fk": { + "name": "card_activity_fromListId_list_id_fk", + "tableFrom": "card_activity", + "tableTo": "list", + "columnsFrom": [ + "fromListId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_activity_toListId_list_id_fk": { + "name": "card_activity_toListId_list_id_fk", + "tableFrom": "card_activity", + "tableTo": "list", + "columnsFrom": [ + "toListId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_activity_labelId_label_id_fk": { + "name": "card_activity_labelId_label_id_fk", + "tableFrom": "card_activity", + "tableTo": "label", + "columnsFrom": [ + "labelId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_activity_workspaceMemberId_workspace_members_id_fk": { + "name": "card_activity_workspaceMemberId_workspace_members_id_fk", + "tableFrom": "card_activity", + "tableTo": "workspace_members", + "columnsFrom": [ + "workspaceMemberId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_activity_createdBy_user_id_fk": { + "name": "card_activity_createdBy_user_id_fk", + "tableFrom": "card_activity", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_activity_commentId_card_comments_id_fk": { + "name": "card_activity_commentId_card_comments_id_fk", + "tableFrom": "card_activity", + "tableTo": "card_comments", + "columnsFrom": [ + "commentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "card_activity_publicId_unique": { + "name": "card_activity_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public._card_workspace_members": { + "name": "_card_workspace_members", + "schema": "", + "columns": { + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "workspaceMemberId": { + "name": "workspaceMemberId", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "_card_workspace_members_cardId_card_id_fk": { + "name": "_card_workspace_members_cardId_card_id_fk", + "tableFrom": "_card_workspace_members", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_card_workspace_members_workspaceMemberId_workspace_members_id_fk": { + "name": "_card_workspace_members_workspaceMemberId_workspace_members_id_fk", + "tableFrom": "_card_workspace_members", + "tableTo": "workspace_members", + "columnsFrom": [ + "workspaceMemberId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "_card_workspace_members_cardId_workspaceMemberId_pk": { + "name": "_card_workspace_members_cardId_workspaceMemberId_pk", + "columns": [ + "cardId", + "workspaceMemberId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.card": { + "name": "card", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "listId": { + "name": "listId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "card_createdBy_user_id_fk": { + "name": "card_createdBy_user_id_fk", + "tableFrom": "card", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "card_deletedBy_user_id_fk": { + "name": "card_deletedBy_user_id_fk", + "tableFrom": "card", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "card_listId_list_id_fk": { + "name": "card_listId_list_id_fk", + "tableFrom": "card", + "tableTo": "list", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_importId_import_id_fk": { + "name": "card_importId_import_id_fk", + "tableFrom": "card", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "card_publicId_unique": { + "name": "card_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public._card_labels": { + "name": "_card_labels", + "schema": "", + "columns": { + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "labelId": { + "name": "labelId", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "_card_labels_cardId_card_id_fk": { + "name": "_card_labels_cardId_card_id_fk", + "tableFrom": "_card_labels", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_card_labels_labelId_label_id_fk": { + "name": "_card_labels_labelId_label_id_fk", + "tableFrom": "_card_labels", + "tableTo": "label", + "columnsFrom": [ + "labelId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "_card_labels_cardId_labelId_pk": { + "name": "_card_labels_cardId_labelId_pk", + "columns": [ + "cardId", + "labelId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.card_comments": { + "name": "card_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "card_comments_cardId_card_id_fk": { + "name": "card_comments_cardId_card_id_fk", + "tableFrom": "card_comments", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_comments_createdBy_user_id_fk": { + "name": "card_comments_createdBy_user_id_fk", + "tableFrom": "card_comments", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_comments_deletedBy_user_id_fk": { + "name": "card_comments_deletedBy_user_id_fk", + "tableFrom": "card_comments", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "card_comments_publicId_unique": { + "name": "card_comments_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.feedback": { + "name": "feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reviewed": { + "name": "reviewed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "feedback_createdBy_user_id_fk": { + "name": "feedback_createdBy_user_id_fk", + "tableFrom": "feedback", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.import": { + "name": "import", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "import_createdBy_user_id_fk": { + "name": "import_createdBy_user_id_fk", + "tableFrom": "import", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "import_publicId_unique": { + "name": "import_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.label": { + "name": "label", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "colourCode": { + "name": "colourCode", + "type": "varchar(12)", + "primaryKey": false, + "notNull": false + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boardId": { + "name": "boardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "label_createdBy_user_id_fk": { + "name": "label_createdBy_user_id_fk", + "tableFrom": "label", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "label_boardId_board_id_fk": { + "name": "label_boardId_board_id_fk", + "tableFrom": "label", + "tableTo": "board", + "columnsFrom": [ + "boardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "label_importId_import_id_fk": { + "name": "label_importId_import_id_fk", + "tableFrom": "label", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "label_deletedBy_user_id_fk": { + "name": "label_deletedBy_user_id_fk", + "tableFrom": "label", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "label_publicId_unique": { + "name": "label_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.list": { + "name": "list", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "boardId": { + "name": "boardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "list_createdBy_user_id_fk": { + "name": "list_createdBy_user_id_fk", + "tableFrom": "list", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "list_deletedBy_user_id_fk": { + "name": "list_deletedBy_user_id_fk", + "tableFrom": "list", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "list_boardId_board_id_fk": { + "name": "list_boardId_board_id_fk", + "tableFrom": "list", + "tableTo": "board", + "columnsFrom": [ + "boardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "list_importId_import_id_fk": { + "name": "list_importId_import_id_fk", + "tableFrom": "list", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "list_publicId_unique": { + "name": "list_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v4()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.integration": { + "name": "integration", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "refreshToken": { + "name": "refreshToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_userId_user_id_fk": { + "name": "integration_userId_user_id_fk", + "tableFrom": "integration", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_pkey": { + "name": "integration_pkey", + "columns": [ + "userId", + "provider" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.workspace_slugs": { + "name": "workspace_slugs", + "schema": "", + "columns": { + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "slug_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_slugs_slug_unique": { + "name": "workspace_slugs_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_members": { + "name": "workspace_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workspaceId": { + "name": "workspaceId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_members_userId_user_id_fk": { + "name": "workspace_members_userId_user_id_fk", + "tableFrom": "workspace_members", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_members_workspaceId_workspace_id_fk": { + "name": "workspace_members_workspaceId_workspace_id_fk", + "tableFrom": "workspace_members", + "tableTo": "workspace", + "columnsFrom": [ + "workspaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_members_deletedBy_user_id_fk": { + "name": "workspace_members_deletedBy_user_id_fk", + "tableFrom": "workspace_members", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_members_publicId_unique": { + "name": "workspace_members_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "workspace_plan", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_createdBy_user_id_fk": { + "name": "workspace_createdBy_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_deletedBy_user_id_fk": { + "name": "workspace_deletedBy_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_publicId_unique": { + "name": "workspace_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + }, + "workspace_slug_unique": { + "name": "workspace_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": { + "public.board_visibility": { + "name": "board_visibility", + "schema": "public", + "values": [ + "private", + "public" + ] + }, + "public.card_activity_type": { + "name": "card_activity_type", + "schema": "public", + "values": [ + "card.created", + "card.updated.title", + "card.updated.description", + "card.updated.index", + "card.updated.list", + "card.updated.label.added", + "card.updated.label.removed", + "card.updated.member.added", + "card.updated.member.removed", + "card.updated.comment.added", + "card.updated.comment.updated", + "card.updated.comment.deleted", + "card.archived" + ] + }, + "public.source": { + "name": "source", + "schema": "public", + "values": [ + "trello" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "started", + "success", + "failed" + ] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": [ + "admin", + "member", + "guest" + ] + }, + "public.member_status": { + "name": "member_status", + "schema": "public", + "values": [ + "invited", + "active", + "removed" + ] + }, + "public.slug_type": { + "name": "slug_type", + "schema": "public", + "values": [ + "reserved", + "premium" + ] + }, + "public.workspace_plan": { + "name": "workspace_plan", + "schema": "public", + "values": [ + "free", + "pro", + "enterprise" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index dd6ab92f..40214ec2 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1749405288761, "tag": "20250608175448_AddOnDeleteActionToUserDeletion", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1749377372062, + "tag": "20250608100932_AddIntegrationsTable", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/repository/integration.repo.ts b/packages/db/src/repository/integration.repo.ts new file mode 100644 index 00000000..f1df7240 --- /dev/null +++ b/packages/db/src/repository/integration.repo.ts @@ -0,0 +1,59 @@ +import { and, eq, gte } from "drizzle-orm"; + +import type { dbClient } from "@kan/db/client"; +import { integrations } from "@kan/db/schema"; + +export const isProviderAvailableForUser = async ( + db: dbClient, + userId: string, + provider: string, +) => { + const integration = await db.query.integrations.findFirst({ + where: and( + eq(integrations.userId, userId), + eq(integrations.provider, provider), + gte(integrations.expiresAt, new Date()), + ), + }); + + return !!integration; +}; + +export const getProviderForUser = async ( + db: dbClient, + userId: string, + provider: string, +) => { + const integration = await db.query.integrations.findFirst({ + where: and( + eq(integrations.userId, userId), + eq(integrations.provider, provider), + gte(integrations.expiresAt, new Date()), + ), + }); + + return integration; +}; + +export const getProvidersForUser = async (db: dbClient, userId: string) => { + const integration = await db.query.integrations.findMany({ + where: and( + eq(integrations.userId, userId), + gte(integrations.expiresAt, new Date()), + ), + }); + + return integration; +}; + +export const deleteProviderForUser = async ( + db: dbClient, + userId: string, + provider: string, +) => { + await db + .delete(integrations) + .where( + and(eq(integrations.userId, userId), eq(integrations.provider, provider)), + ); +}; diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index b94e20c1..c5ed6f99 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -7,4 +7,5 @@ export * from "./imports"; export * from "./labels"; export * from "./lists"; export * from "./users"; +export * from "./integrations"; export * from "./workspaces"; diff --git a/packages/db/src/schema/integrations.ts b/packages/db/src/schema/integrations.ts new file mode 100644 index 00000000..2afeef1b --- /dev/null +++ b/packages/db/src/schema/integrations.ts @@ -0,0 +1,40 @@ +import { relations } from "drizzle-orm"; +import { + pgTable, + primaryKey, + timestamp, + uuid, + varchar, +} from "drizzle-orm/pg-core"; + +import { users } from "./users"; + +export const integrations = pgTable( + "integration", + { + provider: varchar("provider", { length: 255 }).notNull(), + userId: uuid("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + accessToken: varchar("accessToken", { length: 255 }).notNull(), + refreshToken: varchar("refreshToken", { length: 255 }), + expiresAt: timestamp("expiresAt").notNull(), + createdAt: timestamp("createdAt") + .$defaultFn(() => new Date()) + .notNull(), + updatedAt: timestamp("updatedAt").$onUpdateFn(() => new Date()), + }, + (table) => [ + primaryKey({ + name: "integration_pkey", + columns: [table.userId, table.provider], + }), + ], +).enableRLS(); + +export const integrationsRelations = relations(integrations, ({ one }) => ({ + user: one(users, { + fields: [integrations.userId], + references: [users.id], + }), +})); diff --git a/packages/db/src/schema/users.ts b/packages/db/src/schema/users.ts index 51ee87ec..27acbd22 100644 --- a/packages/db/src/schema/users.ts +++ b/packages/db/src/schema/users.ts @@ -13,6 +13,7 @@ import { cards } from "./cards"; import { imports } from "./imports"; import { lists } from "./lists"; import { workspaceMembers, workspaces } from "./workspaces"; +import { integrations } from "./integrations"; export const users = pgTable("user", { id: uuid("id") @@ -55,6 +56,7 @@ export const usersRelations = relations(users, ({ many }) => ({ relationName: "workspaceCreatedByUser", }), apiKeys: many(apikey), + integrations: many(integrations), })); export const usersToWorkspacesRelations = relations( diff --git a/turbo.json b/turbo.json index 8e39abbb..31b6e9f6 100644 --- a/turbo.json +++ b/turbo.json @@ -47,6 +47,8 @@ }, "globalEnv": [ "POSTGRES_URL", + "TRELLO_APP_API_KEY", + "TRELLO_APP_SECRET", "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "DISCORD_CLIENT_ID",