mirror of
https://github.com/docmost/docmost.git
synced 2026-08-19 20:51:38 +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');
|
||||
}
|
||||
|
||||
// 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 {
|
||||
await this.oauthService.exchangeCodeForTokens(
|
||||
type,
|
||||
@@ -74,12 +82,12 @@ export class OAuthController {
|
||||
statePayload.workspaceId,
|
||||
);
|
||||
|
||||
const appUrl = this.environmentService.getAppUrl();
|
||||
return res.redirect(`${appUrl}/settings/integrations`, 302).send();
|
||||
return res.redirect(`${returnUrl}/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`, 302).send();
|
||||
return res
|
||||
.redirect(`${returnUrl}/settings/integrations?error=oauth_failed`, 302)
|
||||
.send();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||
import { DomainService } from '../../../integrations/environment/domain.service';
|
||||
import { IntegrationRegistry } from '../registry/integration-registry';
|
||||
import { IntegrationRepo } from '../repos/integration.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 { IntegrationConnection } from '@docmost/db/types/entity.types';
|
||||
import { OAuthConfig } from '../registry/integration-provider.interface';
|
||||
@@ -25,6 +27,11 @@ export type OAuthStatePayload = {
|
||||
integrationId: string;
|
||||
userId: 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;
|
||||
};
|
||||
|
||||
@@ -34,9 +41,11 @@ export class OAuthService {
|
||||
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly domainService: DomainService,
|
||||
private readonly registry: IntegrationRegistry,
|
||||
private readonly integrationRepo: IntegrationRepo,
|
||||
private readonly connectionRepo: IntegrationConnectionRepo,
|
||||
private readonly workspaceRepo: WorkspaceRepo,
|
||||
) {}
|
||||
|
||||
async getAuthorizationUrl(
|
||||
@@ -60,10 +69,16 @@ export class OAuthService {
|
||||
|
||||
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({
|
||||
integrationId,
|
||||
userId,
|
||||
workspaceId,
|
||||
returnUrl,
|
||||
exp: Date.now() + 10 * 60 * 1000,
|
||||
});
|
||||
|
||||
|
||||
@@ -18,4 +18,20 @@ export class DomainService {
|
||||
const protocol = this.environmentService.isHttps() ? 'https' : 'http';
|
||||
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