feat: implement Trello integration with OAuth and user fields (#48)
* feat: implement Trello integration with OAuth and user fields * feat: migrate Trello integration to new integrations table * refactor: migrate Trello integration to use new integration system * refactor: migrate Trello integration to use new integrations table * fix: add loading check for Trello integration and update VSCode settings * feat: enhance import boards UI with dynamic integration providers and local storage config * feat: add select/unselect all buttons and loading state to board import form * feat: add Trello import env vars on README * docs: update Trello import guide to use OAuth flow instead of API keys * feat: add integrations table for OAuth token management * feat: connect trello from import modal * refactor: consolidate Trello integration endpoints into unified integration router * fix: trello import again * refactor: generalize integration authorization flow and improve error handling * refactor: update Trello API endpoints and tags for better organization * feat: add Integrations tag to OpenAPI specification * refactor: update Trello auth endpoint to use generic integration provider param * refactor: move Trello integration logic from trello.ts to import.ts router * refactor: consolidate Trello integration endpoints and fix API URL * chore: remove debug logs --------- Co-authored-by: Henry <henry_ball@hotmail.co.uk>
This commit is contained in:
128
packages/api/src/routers/integration.ts
Normal file
128
packages/api/src/routers/integration.ts
Normal file
@@ -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",
|
||||
});
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user