mirror of
https://github.com/docmost/docmost.git
synced 2026-08-24 23:42:21 +10:00
feat(integrations): route OAuth callbacks back to originating workspace host
This commit is contained in:
@@ -65,6 +65,14 @@ export class OAuthController {
|
|||||||
throw new BadRequestException('Invalid or expired OAuth state');
|
throw new BadRequestException('Invalid or expired OAuth state');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returnUrl is derived server-side at authorize time from the workspace's
|
||||||
|
// own hostname/customDomain (canonical DB truth, not user input), then
|
||||||
|
// signed into the state JWT. Safe to use directly here — tampering would
|
||||||
|
// invalidate the signature; older tokens predating this field will be
|
||||||
|
// undefined and fall back to APP_URL.
|
||||||
|
const returnUrl =
|
||||||
|
statePayload.returnUrl || this.environmentService.getAppUrl();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.oauthService.exchangeCodeForTokens(
|
await this.oauthService.exchangeCodeForTokens(
|
||||||
type,
|
type,
|
||||||
@@ -74,12 +82,12 @@ export class OAuthController {
|
|||||||
statePayload.workspaceId,
|
statePayload.workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const appUrl = this.environmentService.getAppUrl();
|
return res.redirect(`${returnUrl}/settings/integrations`, 302).send();
|
||||||
return res.redirect(`${appUrl}/settings/integrations`, 302).send();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(`OAuth callback error for ${type}: ${(err as Error).message}`);
|
this.logger.error(`OAuth callback error for ${type}: ${(err as Error).message}`);
|
||||||
const appUrl = this.environmentService.getAppUrl();
|
return res
|
||||||
return res.redirect(`${appUrl}/settings/integrations?error=oauth_failed`, 302).send();
|
.redirect(`${returnUrl}/settings/integrations?error=oauth_failed`, 302)
|
||||||
|
.send();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||||
|
import { DomainService } from '../../../integrations/environment/domain.service';
|
||||||
import { IntegrationRegistry } from '../registry/integration-registry';
|
import { IntegrationRegistry } from '../registry/integration-registry';
|
||||||
import { IntegrationRepo } from '../repos/integration.repo';
|
import { IntegrationRepo } from '../repos/integration.repo';
|
||||||
import { IntegrationConnectionRepo } from '../repos/integration-connection.repo';
|
import { IntegrationConnectionRepo } from '../repos/integration-connection.repo';
|
||||||
|
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
||||||
import { encryptToken, decryptToken } from '../crypto/token-crypto';
|
import { encryptToken, decryptToken } from '../crypto/token-crypto';
|
||||||
import { IntegrationConnection } from '@docmost/db/types/entity.types';
|
import { IntegrationConnection } from '@docmost/db/types/entity.types';
|
||||||
import { OAuthConfig } from '../registry/integration-provider.interface';
|
import { OAuthConfig } from '../registry/integration-provider.interface';
|
||||||
@@ -25,6 +27,11 @@ export type OAuthStatePayload = {
|
|||||||
integrationId: string;
|
integrationId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
// Workspace's canonical URL at authorize time. Cloud workspaces are routed
|
||||||
|
// through a single central OAuth callback (the only redirect_uri Slack/etc.
|
||||||
|
// accept), and this lets the callback redirect the user back to their own
|
||||||
|
// workspace host (subdomain or custom domain) after token exchange.
|
||||||
|
returnUrl: string;
|
||||||
exp: number;
|
exp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,9 +41,11 @@ export class OAuthService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
|
private readonly domainService: DomainService,
|
||||||
private readonly registry: IntegrationRegistry,
|
private readonly registry: IntegrationRegistry,
|
||||||
private readonly integrationRepo: IntegrationRepo,
|
private readonly integrationRepo: IntegrationRepo,
|
||||||
private readonly connectionRepo: IntegrationConnectionRepo,
|
private readonly connectionRepo: IntegrationConnectionRepo,
|
||||||
|
private readonly workspaceRepo: WorkspaceRepo,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getAuthorizationUrl(
|
async getAuthorizationUrl(
|
||||||
@@ -60,10 +69,16 @@ export class OAuthService {
|
|||||||
|
|
||||||
const callbackUrl = this.buildCallbackUrl(integration.type);
|
const callbackUrl = this.buildCallbackUrl(integration.type);
|
||||||
|
|
||||||
|
const workspace = await this.workspaceRepo.findById(workspaceId);
|
||||||
|
const returnUrl = this.domainService.getWorkspaceUrl(
|
||||||
|
workspace ?? { hostname: null, customDomain: null },
|
||||||
|
);
|
||||||
|
|
||||||
const state = this.createSignedState({
|
const state = this.createSignedState({
|
||||||
integrationId,
|
integrationId,
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
returnUrl,
|
||||||
exp: Date.now() + 10 * 60 * 1000,
|
exp: Date.now() + 10 * 60 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,4 +18,20 @@ export class DomainService {
|
|||||||
const protocol = this.environmentService.isHttps() ? 'https' : 'http';
|
const protocol = this.environmentService.isHttps() ? 'https' : 'http';
|
||||||
return `${protocol}://${hostname}.${domain}`;
|
return `${protocol}://${hostname}.${domain}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Canonical workspace URL: prefers customDomain, falls back to {hostname}.{cloud-domain},
|
||||||
|
// falls back to APP_URL for self-hosted. Used for multi-tenant OAuth return-redirects.
|
||||||
|
getWorkspaceUrl(workspace: {
|
||||||
|
hostname?: string | null;
|
||||||
|
customDomain?: string | null;
|
||||||
|
}): string {
|
||||||
|
if (!this.environmentService.isCloud()) {
|
||||||
|
return this.environmentService.getAppUrl();
|
||||||
|
}
|
||||||
|
if (workspace.customDomain) {
|
||||||
|
const protocol = this.environmentService.isHttps() ? 'https' : 'http';
|
||||||
|
return `${protocol}://${workspace.customDomain}`;
|
||||||
|
}
|
||||||
|
return this.getUrl(workspace.hostname ?? undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user