Files
fusion/packages/dashboard/src/gitlab-auth.ts
gsxdsm f9733341b5 FN-7453: add GitLab enablement controls
Add a settings-controlled GitLab enablement path while preserving saved GitLab configuration.

- Add project and global GitLab enable settings with collapsible Settings UI for URL and token controls.
- Gate GitLab auth, import loading, and import actions when GitLab integration is disabled.
- Preserve global GitLab values separately during global settings saves and document the new behavior.
- Cover enablement resolution, settings save behavior, disabled import UI, and GitLab route/auth gating with tests.

Files changed:
 .changeset/fn-7453-gitlab-enable-disclosure.md     |  7 ++
 docs/dashboard-guide.md                            |  7 ++
 docs/gitlab-parity-inventory.md                    |  1 +
 docs/settings-reference.md                         |  4 ++
 docs/task-management.md                            |  6 +-
 packages/core/src/__tests__/gitlab-config.test.ts  | 17 ++++-
 .../core/src/__tests__/settings-parity.test.ts     | 18 +++--
 packages/core/src/gitlab-config.ts                 | 15 ++++-
 packages/core/src/index.ts                         |  2 +-
 packages/core/src/settings-schema.ts               |  2 +
 packages/core/src/types.ts                         |  5 ++
 .../app/__tests__/settings-save-split.test.ts      | 20 +++---
 .../dashboard/app/components/GitHubImportModal.tsx | 43 +++++++++---
 .../dashboard/app/components/SettingsModal.css     | 77 +++++++++++++++++++++
 .../dashboard/app/components/SettingsModal.tsx     | 34 ++++++++-
 .../__tests__/GitHubImportModal.test.tsx           | 19 ++++++
 .../__tests__/SettingsModal.general.test.tsx       | 78 ++++++++++++++++++++++
 .../SettingsModal.scheduling-merge.test.tsx        |  7 +-
 .../app/components/settings/save-split.ts          |  5 +-
 .../settings/sections/GeneralSection.tsx           | 36 ++++++----
 .../settings/sections/GlobalGeneralSection.tsx     | 71 ++++++++++++--------
 .../components/settings/sections/MergeSection.tsx  | 44 +++++++-----
 .../dashboard/src/__tests__/gitlab-auth.test.ts    | 20 ++++++
 .../dashboard/src/__tests__/routes-gitlab.test.ts  | 11 +++
 packages/dashboard/src/gitlab-auth.ts              | 11 ++-
 25 files changed, 458 insertions(+), 102 deletions(-)

Fusion-Task-Id: FN-7453

Fusion-Task-Lineage: 81fbd39d-675f-49d6-8d13-b7fcb4d25658

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-02 19:01:09 -07:00

129 lines
4.7 KiB
TypeScript

import { resolveGitlabConfig, resolveGitlabEnabled, type GlobalSettings, type ProjectSettings } from "@fusion/core";
import type { GitlabAuthTokenType } from "@fusion/core";
export const GITLAB_AUTH_HEADER_NAME = "PRIVATE-TOKEN" as const;
export const GITLAB_AUTH_TOKEN_TYPES = ["personal", "project", "group"] as const satisfies readonly GitlabAuthTokenType[];
export interface GitlabAuthSettingsSource {
gitlabEnabled?: boolean;
gitlabInstanceUrl?: string;
gitlabApiBaseUrl?: string;
gitlabAuthToken?: string;
gitlabAuthTokenType?: GitlabAuthTokenType | string;
}
export interface ResolvedGitlabAuth {
apiBaseUrl: string;
webBaseUrl: string;
token: string;
tokenType: GitlabAuthTokenType;
headerName: typeof GITLAB_AUTH_HEADER_NAME;
}
export type GitlabAuthResolution =
| { ok: true; auth: ResolvedGitlabAuth }
| {
ok: false;
reason: "disabled" | "token_missing" | "invalid_token_type" | "invalid_config";
message: string;
};
export interface ResolveGitlabAuthDeps {
projectSettings?: GitlabAuthSettingsSource | Pick<ProjectSettings, "gitlabEnabled" | "gitlabInstanceUrl" | "gitlabApiBaseUrl" | "gitlabAuthToken" | "gitlabAuthTokenType"> | null;
globalSettings?: GitlabAuthSettingsSource | Partial<GlobalSettings> | Record<string, unknown> | null;
env?: NodeJS.ProcessEnv;
}
function pickString(source: Record<string, unknown> | undefined | null, key: string): string | undefined {
const value = source?.[key];
return typeof value === "string" ? value : undefined;
}
function readConfiguredString(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function normalizeTokenType(value: unknown): GitlabAuthTokenType | undefined | "invalid" {
if (value === undefined || value === null || value === "") return undefined;
if (typeof value !== "string") return "invalid";
const trimmed = value.trim();
if (trimmed === "") return undefined;
return (GITLAB_AUTH_TOKEN_TYPES as readonly string[]).includes(trimmed) ? trimmed as GitlabAuthTokenType : "invalid";
}
function firstConfiguredTokenType(...values: unknown[]): GitlabAuthTokenType | undefined | "invalid" {
for (const value of values) {
const normalized = normalizeTokenType(value);
if (normalized !== undefined) return normalized;
}
return undefined;
}
/**
* FNXC:GitLabAuthentication 2026-07-02-00:00:
* FN-7423 resolves personal, project, and group GitLab access tokens for future HTTP API integrations without invoking `glab` or any GitLab CLI. GitLab REST auth uses the PRIVATE-TOKEN header; read-only features require read_api or api, while future write/comment/close features require api and token resource membership.
*/
export function resolveGitlabAuth(deps: ResolveGitlabAuthDeps = {}): GitlabAuthResolution {
if (!resolveGitlabEnabled({ project: deps.projectSettings ?? undefined, global: deps.globalSettings as Partial<GlobalSettings> | undefined })) {
return { ok: false, reason: "disabled", message: "GitLab integration is disabled in Settings." };
}
let config: ReturnType<typeof resolveGitlabConfig>;
try {
config = resolveGitlabConfig({
project: deps.projectSettings ?? undefined,
global: deps.globalSettings as Partial<GlobalSettings> | undefined,
});
} catch (error) {
return {
ok: false,
reason: "invalid_config",
message: error instanceof Error ? error.message : "Invalid GitLab configuration.",
};
}
const project = deps.projectSettings as Record<string, unknown> | null | undefined;
const global = deps.globalSettings as Record<string, unknown> | null | undefined;
const env = deps.env ?? process.env;
const token = readConfiguredString(project?.gitlabAuthToken)
?? readConfiguredString(global?.gitlabAuthToken)
?? readConfiguredString(pickString(global, "projectGitlabAuthToken"))
?? readConfiguredString(env.GITLAB_TOKEN)
?? "";
const tokenType = firstConfiguredTokenType(
project?.gitlabAuthTokenType,
global?.gitlabAuthTokenType,
pickString(global, "projectGitlabAuthTokenType"),
);
if (tokenType === "invalid") {
return {
ok: false,
reason: "invalid_token_type",
message: "Invalid gitlabAuthTokenType. Expected \"personal\", \"project\", or \"group\".",
};
}
if (!token) {
return {
ok: false,
reason: "token_missing",
message: "GitLab auth requires gitlabAuthToken or GITLAB_TOKEN.",
};
}
return {
ok: true,
auth: {
apiBaseUrl: config.apiBaseUrl,
webBaseUrl: config.instanceUrl,
token,
tokenType: tokenType ?? "personal",
headerName: GITLAB_AUTH_HEADER_NAME,
},
};
}