Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Type-safe API layer — replace double-casts with adapters and zod** (#206) - removed all `as unknown as T` and `as never` casts in 15 of `src/lib/api/*.ts` files. Each SDK call now goes through an adapter function or a Set-backed narrowing helper that warns on unknown enum values. `assertData` (new in `fetch.ts`) rejects empty body responses with a contextual error. `settings.ts` uses zod `.safeParse()` at the trust boundary for `getPasswordPolicy`/`getSmtpConfig`. Public `xxxApi` return types unchanged so consumer code is untouched.

### Fixed
- **Setup Guide: sanitize repo keys for Gradle/SBT property names + clearer SSR placeholder** (#362, partial) - the Gradle credentials snippet emitted property names like `my-jvm-repoUsername` for hyphenated repo keys; technically legal in `gradle.properties` but looks broken to readers expecting identifier rules. Added a `repoKeyToGradleId` helper that camelCases kebab/dot/underscore separators and strips remaining non-alphanumerics. URLs and `<id>` slots keep the raw key — only property names sanitize. Also replaced the SSR fallback `https://artifacts.example.com` with `__REPLACE_WITH_REGISTRY_URL__` so prerendered HTML doesn't ship with a real-looking domain a user could accidentally copy. Remaining `repo_type` (proxy/virtual hides publish steps) and `is_public` (anonymous mode) fixes deferred to follow-up.
- **Per-artifact Security tab now surfaces native scan_findings** (#368) - the Security tab on the repository view (`security-tab-content.tsx`) used to show only SBOM CVE history and Dependency-Track findings, never the native `scan_findings` table. A user who triggered a scan via `POST /api/v1/security/scan` for a specific artifact had no way to see the resulting findings on the artifact's own page — they had to navigate to `/security/scans` and find the right scan ID by name+timestamp. New `ArtifactScansSection` component lists recent scan_results rows for the artifact (status / type / counts / completed_at) with a "View findings" link to the per-scan page. Sourced from `securityApi.listArtifactScans(artifact.id)` which already existed but had no consumer.
- **`getInstallCommand` returns Gradle/SBT-native snippets instead of Maven XML** (#361) - the JVM case in `package-utils.ts` returned the same `<dependency>` XML for all three of `maven` / `gradle` / `sbt`. Users browsing a Gradle-named repo saw Maven XML in the package detail / copy-snippet UI — same bug class #333 fixed on the Setup Guide page. Now `gradle` returns `implementation 'GROUP:name:version'` and `sbt` returns `libraryDependencies += "GROUP" % "name" % "version"`. Maven output is unchanged.
- **Surface load failures in `getPasswordPolicy` and `getSmtpConfig` instead of silently falling back to defaults** (#347) - both getters previously caught any SDK error or schema mismatch and returned baked-in defaults, so a backend outage rendered as plausible-looking placeholder values on the admin Settings page (same failure mode as #334). Now the getters throw on SDK error / unparseable response, and the page renders explicit "Unavailable" states (Password Policy row + SMTP tab error alert) so an operator can tell something's actually wrong.
Expand Down
50 changes: 50 additions & 0 deletions src/app/(app)/setup/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,53 @@ describe("SetupPage - CI/CD platforms", () => {
expect(screen.queryByRole("dialog")).toBeNull();
});
});

describe("SetupPage - repo key sanitization for Gradle property names (#362)", () => {
beforeEach(() => mockUseQuery.mockReset());
afterEach(() => cleanup());

it("repoKeyToGradleId converts kebab-case to camelCase", async () => {
const { repoKeyToGradleId } = await import("./page");
expect(repoKeyToGradleId("my-jvm-repo")).toBe("myJvmRepo");
expect(repoKeyToGradleId("acme-libs")).toBe("acmeLibs");
});

it("repoKeyToGradleId converts dot/underscore separators to camelCase", async () => {
const { repoKeyToGradleId } = await import("./page");
expect(repoKeyToGradleId("com.example.libs")).toBe("comExampleLibs");
expect(repoKeyToGradleId("snake_case_repo")).toBe("snakeCaseRepo");
});

it("repoKeyToGradleId strips remaining non-alphanumerics", async () => {
const { repoKeyToGradleId } = await import("./page");
expect(repoKeyToGradleId("repo@with#symbols")).toBe("repowithsymbols");
});

it("repoKeyToGradleId returns 'repo' for empty or all-symbol input", async () => {
const { repoKeyToGradleId } = await import("./page");
expect(repoKeyToGradleId("")).toBe("repo");
expect(repoKeyToGradleId("@@@")).toBe("repo");
});

it("Gradle credentials block uses sanitized property names", async () => {
const user = await openRepoDialog(makeRepo({ format: "gradle", key: "my-jvm-repo" }));
await screen.findByRole("dialog");
await user.click(screen.getByRole("tab", { name: "Gradle (Groovy)" }));
const panel = screen.getByRole("tabpanel", { name: "Gradle (Groovy)" });
const text = panel.textContent ?? "";
// Sanitized to camelCase — kebab-case property names look broken to readers.
expect(text).toContain("myJvmRepoUsername");
expect(text).toContain("myJvmRepoPassword");
expect(text).not.toContain("my-jvm-repoUsername");
});

it("URL paths still contain the raw repo key (only property names sanitize)", async () => {
const user = await openRepoDialog(makeRepo({ format: "gradle", key: "my-jvm-repo" }));
await screen.findByRole("dialog");
await user.click(screen.getByRole("tab", { name: "Gradle (Groovy)" }));
const panel = screen.getByRole("tabpanel", { name: "Gradle (Groovy)" });
const text = panel.textContent ?? "";
// The URL keeps the raw kebab-case key (URL paths permit hyphens).
expect(text).toMatch(/\/maven\/my-jvm-repo\//);
});
});
42 changes: 34 additions & 8 deletions src/app/(app)/setup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,26 +71,52 @@ interface CICDPlatform {

// -- helpers --

// SSR-safe placeholders that are obviously non-functional so the prerendered
// HTML doesn't ship with a real-looking domain (`artifacts.example.com`)
// that a user might copy into a config file before the client hydrates and
// rewrites them. After hydration `typeof window !== "undefined"` flips and
// the snippets contain the live origin (#362).
const REGISTRY_URL_PLACEHOLDER = "__REPLACE_WITH_REGISTRY_URL__";
const REGISTRY_HOST_PLACEHOLDER = "__REPLACE_WITH_REGISTRY_HOST__";

const REGISTRY_URL =
typeof window !== "undefined"
? window.location.origin
: "https://artifacts.example.com";
: REGISTRY_URL_PLACEHOLDER;

const REGISTRY_HOST =
typeof window !== "undefined"
? window.location.host
: "artifacts.example.com";
: REGISTRY_HOST_PLACEHOLDER;

/**
* Sanitize a repo key into a Gradle/SBT-friendly camelCase identifier for
* property names. Repo keys like `my-jvm-repo` are legal in `gradle.properties`
* (the file format permits hyphens and dots), but they look wrong to readers
* who assume identifier rules apply. Convert kebab/dot/underscore-case to
* camelCase and strip any remaining non-alphanumerics. URLs and `<id>` slots
* keep the raw key — only property names need this. (#362)
*/
export function repoKeyToGradleId(key: string): string {
if (!key) return "repo";
const camel = key.replace(/[-._\s]+(.)/g, (_, c: string) => c.toUpperCase());
const cleaned = camel.replace(/[^a-zA-Z0-9]/g, "");
return cleaned.length > 0 ? cleaned : "repo";
}

/** Build the JVM client variants (Maven, Gradle Groovy DSL, Gradle Kotlin DSL,
* SBT). All four clients consume the same Maven-format wire repository, so we
* surface tabs for each. */
function getJvmClientVariants(repoKey: string): SetupClientVariant[] {
const repoUrl = `${REGISTRY_URL}/maven/${repoKey}/`;
// Keep `repoKey` in URLs and `<id>` slots; sanitize for Gradle property
// names so `my-jvm-repo` doesn't emit `my-jvm-repoUsername` (#362).
const gradleId = repoKeyToGradleId(repoKey);
const gradleCredentials: SetupStep = {
title: "Configure credentials",
description: "Add to ~/.gradle/gradle.properties:",
code: `${repoKey}Username=YOUR_USERNAME
${repoKey}Password=YOUR_TOKEN`,
code: `${gradleId}Username=YOUR_USERNAME
${gradleId}Password=YOUR_TOKEN`,
};
const gradlePublish: SetupStep = { title: "Publish artifacts", code: "gradle publish" };

Expand Down Expand Up @@ -140,8 +166,8 @@ ${repoKey}Password=YOUR_TOKEN`,
maven {
url '${repoUrl}'
credentials {
username = project.findProperty('${repoKey}Username')
password = project.findProperty('${repoKey}Password')
username = project.findProperty('${gradleId}Username')
password = project.findProperty('${gradleId}Password')
}
}
}
Expand All @@ -163,8 +189,8 @@ dependencies {
maven {
url = uri("${repoUrl}")
credentials {
username = project.findProperty("${repoKey}Username") as String?
password = project.findProperty("${repoKey}Password") as String?
username = project.findProperty("${gradleId}Username") as String?
password = project.findProperty("${gradleId}Password") as String?
}
}
}
Expand Down
Loading