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:
@@ -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 $$;
|
||||
2164
packages/db/migrations/meta/20250608100932_snapshot.json
Normal file
2164
packages/db/migrations/meta/20250608100932_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,13 @@
|
||||
"when": 1749405288761,
|
||||
"tag": "20250608175448_AddOnDeleteActionToUserDeletion",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "7",
|
||||
"when": 1749377372062,
|
||||
"tag": "20250608100932_AddIntegrationsTable",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
59
packages/db/src/repository/integration.repo.ts
Normal file
59
packages/db/src/repository/integration.repo.ts
Normal file
@@ -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)),
|
||||
);
|
||||
};
|
||||
@@ -7,4 +7,5 @@ export * from "./imports";
|
||||
export * from "./labels";
|
||||
export * from "./lists";
|
||||
export * from "./users";
|
||||
export * from "./integrations";
|
||||
export * from "./workspaces";
|
||||
|
||||
40
packages/db/src/schema/integrations.ts
Normal file
40
packages/db/src/schema/integrations.ts
Normal file
@@ -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],
|
||||
}),
|
||||
}));
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user