Control plane wiring (plan §5-§11): - PageService create/update/movePage now honor provenance actor 'git-sync' (stamp lastUpdatedSource='git-sync'), closing the A.4a gap. - EnvironmentService: GIT_SYNC_ENABLED / DATA_DIR / REMOTE_TEMPLATE / POLL_INTERVAL_MS / DEBOUNCE_MS / SERVICE_USER_ID (required-if-enabled) / SSH_KEY_PATH + validation. - VaultRegistryService: per-space vault path + cached VaultGit. - GitSyncOrchestrator: per-space Redis leader-lock (SET NX PX + CAS-Lua release, randomUUID instanceId) + in-process mutex; runOnce drives the vendored engine PULL (readExisting->computePullActions->applyPullActions) then PUSH (runPush) with the bound native GitSyncClient + VaultGit; @Interval poll-safety gated on GIT_SYNC_ENABLED; imports plain ScheduleModule (TelemetryModule owns forRoot). - PageChangeListener: @OnEvent PAGE_* -> per-space debounce -> runOnce, with a best-effort lastUpdatedSource==='git-sync' loop-guard. - GitSyncController: admin POST /api/git-sync/trigger + GET /status (ops/e2e). - GitSyncModule registered in app.module. Enabled-space enumeration uses settings.gitSync.enabled, falling back to all live spaces until Phase C writes the flag (master gate = GIT_SYNC_ENABLED). tsc clean; 713 tests/71 suites pass; dev server hot-reloaded the module (route live, DI graph boots). Live pull/push round-trip verified next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
93 lines
3.0 KiB
TypeScript
93 lines
3.0 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
ForbiddenException,
|
|
HttpCode,
|
|
HttpStatus,
|
|
Post,
|
|
Get,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
|
import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
|
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
|
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
|
import WorkspaceAbilityFactory from '../../core/casl/abilities/workspace-ability.factory';
|
|
import {
|
|
WorkspaceCaslAction,
|
|
WorkspaceCaslSubject,
|
|
} from '../../core/casl/interfaces/workspace-ability.type';
|
|
import { EnvironmentService } from '../environment/environment.service';
|
|
import {
|
|
GitSyncOrchestrator,
|
|
GitSyncRunStatus,
|
|
} from './services/git-sync.orchestrator';
|
|
|
|
/** Body for the manual one-shot trigger. */
|
|
class TriggerGitSyncDto {
|
|
spaceId: string;
|
|
}
|
|
|
|
/**
|
|
* Ops/testing endpoints for the git-sync control plane (plan §6). Admin-guarded
|
|
* (workspace Manage/Settings, mirroring WorkspaceController) so only workspace
|
|
* admins can force a cycle. Mounted under the global `/api` prefix:
|
|
* - POST /api/git-sync/trigger { spaceId } — run one cycle now (await result),
|
|
* - GET /api/git-sync/status — report whether sync is enabled + config.
|
|
*/
|
|
@UseGuards(JwtAuthGuard)
|
|
@Controller('git-sync')
|
|
export class GitSyncController {
|
|
constructor(
|
|
private readonly orchestrator: GitSyncOrchestrator,
|
|
private readonly environmentService: EnvironmentService,
|
|
private readonly workspaceAbility: WorkspaceAbilityFactory,
|
|
) {}
|
|
|
|
/** Throw unless the caller is a workspace admin (Manage Settings). */
|
|
private assertAdmin(user: User, workspace: Workspace): void {
|
|
const ability = this.workspaceAbility.createForUser(user, workspace);
|
|
if (
|
|
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Settings)
|
|
) {
|
|
throw new ForbiddenException();
|
|
}
|
|
}
|
|
|
|
@HttpCode(HttpStatus.OK)
|
|
@Post('trigger')
|
|
async trigger(
|
|
@Body() dto: TriggerGitSyncDto,
|
|
@AuthUser() user: User,
|
|
@AuthWorkspace() workspace: Workspace,
|
|
): Promise<GitSyncRunStatus> {
|
|
this.assertAdmin(user, workspace);
|
|
// Use the workspace from the request context (never client-supplied).
|
|
return this.orchestrator.runOnce(dto.spaceId, workspace.id);
|
|
}
|
|
|
|
@HttpCode(HttpStatus.OK)
|
|
@Get('status')
|
|
async status(
|
|
@AuthUser() user: User,
|
|
@AuthWorkspace() workspace: Workspace,
|
|
): Promise<{
|
|
enabled: boolean;
|
|
dataDir: string;
|
|
pollIntervalMs: number;
|
|
debounceMs: number;
|
|
serviceUserConfigured: boolean;
|
|
}> {
|
|
this.assertAdmin(user, workspace);
|
|
return {
|
|
enabled: this.environmentService.isGitSyncEnabled(),
|
|
dataDir: this.environmentService.getGitSyncDataDir(),
|
|
pollIntervalMs: this.environmentService.getGitSyncPollIntervalMs(),
|
|
debounceMs: this.environmentService.getGitSyncDebounceMs(),
|
|
serviceUserConfigured: Boolean(
|
|
this.environmentService.getGitSyncServiceUserId(),
|
|
),
|
|
};
|
|
}
|
|
}
|