This commit is contained in:
Philipinho
2026-02-22 22:32:40 +00:00
parent aeb30ad096
commit 81c6fb0d56
6 changed files with 154 additions and 11 deletions
@@ -18,6 +18,8 @@ const providerIcons: Record<string, string> = {
gitlab: "https://gitlab.com/assets/favicon-72a2cad5025aa931d6ea56c3201d1f18e68a8571da3c2571592f63571e0c5571.png",
jira: "https://wac-cdn.atlassian.com/assets/img/favicons/atlassian/favicon.png",
linear: "https://linear.app/favicon.ico",
google_docs: "https://ssl.gstatic.com/docs/documents/images/kix-favicon7.ico",
figma: "https://static.figma.com/app/icon/1/favicon.png",
};
function IntegrationLinkView(props: any) {
@@ -4,4 +4,6 @@ export enum IntegrationType {
GITLAB = 'gitlab',
JIRA = 'jira',
LINEAR = 'linear',
GOOGLE_DOCS = 'google_docs',
FIGMA = 'figma',
}
@@ -75,11 +75,11 @@ export class OAuthController {
);
const appUrl = this.environmentService.getAppUrl();
return res.redirect(`${appUrl}/settings/integrations`);
return res.redirect(`${appUrl}/settings/integrations`, 302).send();
} catch (err) {
this.logger.error(`OAuth callback error for ${type}: ${(err as Error).message}`);
const appUrl = this.environmentService.getAppUrl();
return res.redirect(`${appUrl}/settings/integrations?error=oauth_failed`);
return res.redirect(`${appUrl}/settings/integrations?error=oauth_failed`, 302).send();
}
}
@@ -3,7 +3,10 @@ import { IntegrationRegistry } from '../registry/integration-registry';
import { IntegrationConnectionRepo } from '../repos/integration-connection.repo';
import { IntegrationRepo } from '../repos/integration.repo';
import { OAuthService } from '../oauth/oauth.service';
import { UnfurlResult, IntegrationProvider } from '../registry/integration-provider.interface';
import {
UnfurlResult,
IntegrationProvider,
} from '../registry/integration-provider.interface';
import { RedisService } from '@nestjs-labs/nestjs-ioredis';
import type { Redis } from 'ioredis';
import * as crypto from 'crypto';
@@ -38,6 +41,7 @@ export class UnfurlService {
}
const resolved = await this.resolveProvider(url, workspaceId);
if (!resolved) {
return null;
}
@@ -58,7 +62,8 @@ export class UnfurlService {
}
try {
const accessToken = await this.oauthService.getValidAccessToken(connection);
const accessToken =
await this.oauthService.getValidAccessToken(connection);
const unfurlResult = await provider.unfurl({
url,
@@ -123,7 +128,11 @@ export class UnfurlService {
}
private buildCacheKey(workspaceId: string, url: string): string {
const hash = crypto.createHash('sha256').update(url).digest('hex').slice(0, 16);
const hash = crypto
.createHash('sha256')
.update(url)
.digest('hex')
.slice(0, 16);
return `${UNFURL_CACHE_PREFIX}${workspaceId}:${hash}`;
}
}
@@ -4,28 +4,158 @@ export type IntegrationLinkPattern = {
};
export const integrationLinkPatterns: IntegrationLinkPattern[] = [
// GitHub (cloud + GHE): /:owner/:repo/pull/:num or /issues/:num
// GitHub PR commit (must be before generic PR pattern)
{
provider: "github",
regex:
/^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/(pull|issues)\/(\d+)/,
/^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/pull\/(\d+)\/commits\/([a-f0-9]+)/,
},
// GitLab (cloud + self-hosted): /-/issues/:num or /-/merge_requests/:num
// GitHub PR (with optional /checks, /commits, /files sub-pages)
{
provider: "github",
regex:
/^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/pull\/(\d+)/,
},
// GitHub issue
{
provider: "github",
regex:
/^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/issues\/(\d+)/,
},
// GitHub commit
{
provider: "github",
regex:
/^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/commits?\/([a-f0-9]+)/,
},
// GitHub file/blob
{
provider: "github",
regex:
/^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.+?)(?:#L(\d+)(?:-L(\d+))?)?$/,
},
// GitHub pulls list
{
provider: "github",
regex:
/^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/pulls(?:\/.*)?(?:\?.*)?$/,
},
// GitHub releases list
{
provider: "github",
regex:
/^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/releases(?:\/.*)?(?:\?.*)?$/,
},
// GitHub issues list
{
provider: "github",
regex:
/^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/issues(?:\/(?:created_by|assigned)\/[\w.\/-]+)?\/?(?:\?.*)?$/,
},
// GitHub repo
{
provider: "github",
regex:
/^https?:\/\/[^\/]+\/([a-zA-Z0-9\-_.]+)\/([a-zA-Z0-9\-_.]+)\/?$/,
},
// GitLab commit in MR diff (must be before generic MR pattern)
{
provider: "gitlab",
regex:
/^https?:\/\/[^\/]+\/(.+)\/-\/(issues|merge_requests)\/(\d+)/,
/^https?:\/\/[^\/]+\/(.+)\/-\/merge_requests\/(\d+)\/diffs\?.*commit_id=([a-f0-9]+)/,
},
// GitLab merge request
{
provider: "gitlab",
regex:
/^https?:\/\/[^\/]+\/(.+)\/-\/merge_requests\/(\d+)/,
},
// GitLab issue
{
provider: "gitlab",
regex:
/^https?:\/\/[^\/]+\/(.+)\/-\/issues\/(\d+)/,
},
// GitLab commit
{
provider: "gitlab",
regex:
/^https?:\/\/[^\/]+\/(.+)\/-\/commits?\/([a-f0-9]+)/,
},
// GitLab issues list
{
provider: "gitlab",
regex:
/^https?:\/\/[^\/]+\/(.+)\/-\/issues\/?(?:\?.*)?$/,
},
// GitLab merge requests list
{
provider: "gitlab",
regex:
/^https?:\/\/[^\/]+\/(.+)\/-\/merge_requests\/?(?:\?.*)?$/,
},
// GitLab project
{
provider: "gitlab",
regex:
/^https?:\/\/[^\/]+\/([a-zA-Z0-9\-_.]+)\/([a-zA-Z0-9\-_]+)\/?$/,
},
// Google Docs
{
provider: "google_docs",
regex: /^https?:\/\/docs\.google\.com\/document\/d\/([\w-]+)/,
},
// Google Sheets
{
provider: "google_docs",
regex: /^https?:\/\/docs\.google\.com\/spreadsheets\/d\/([\w-]+)/,
},
// Google Slides
{
provider: "google_docs",
regex: /^https?:\/\/docs\.google\.com\/presentation\/d\/([\w-]+)/,
},
// Google Forms
{
provider: "google_docs",
regex: /^https?:\/\/docs\.google\.com\/forms\/d\/([\w-]+)/,
},
// Google Drive file
{
provider: "google_docs",
regex: /^https?:\/\/drive\.google\.com\/file\/d\/([\w-]+)/,
},
// Figma file (design, file, proto, board)
{
provider: "figma",
regex:
/^https?:\/\/([\w.-]+\.)?figma\.com\/(file|proto|board|design)\/([0-9a-zA-Z]{22,128})/,
},
// Jira (cloud + server): /browse/KEY-123
{
provider: "jira",
regex: /^https?:\/\/[^\/]+\/browse\/([A-Z][A-Z0-9]+-\d+)/,
},
// Linear (cloud only): /team/issue/KEY-123
// Linear issue: /team/issue/KEY-123(/:title-slug)?
{
provider: "linear",
regex: /^https?:\/\/linear\.app\/([^\/]+)\/issue\/([A-Z]+-\d+)/,
},
// Linear project: /team/project/:slug(/:tab)?
{
provider: "linear",
regex: /^https?:\/\/linear\.app\/([^\/]+)\/project\/([^\/]+)/,
},
// Linear initiative: /team/initiative/:slug(/:tab)?
{
provider: "linear",
regex: /^https?:\/\/linear\.app\/([^\/]+)\/initiative\/([^\/]+)/,
},
// Linear view: /team/view/:id(/:tab)?
{
provider: "linear",
regex: /^https?:\/\/linear\.app\/([^\/]+)\/view\/([^\/]+)/,
},
];
export function matchIntegrationLink(