Skip to content
Open
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
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
## Changes in version 0.1.2 (in development)

### Enhancements

- When generating titles for input fields without a title set, we skip a
prefix `x-` from the input's name before converting it. (#74)

### Fixes

- Custom Service now recognizes the renamed Cuiman service options:
`loginUrl`, `tokenUrl`, `grantType`, `accessToken`, and
`accessTokenHeader`. It applies supplied Basic, access-token, and API-key
credentials to service requests, and uses a supplied OAuth2 access token
without starting a browser authorization-code flow.

- Map process inputs now zoom to the current polygon or bbox when an
existing value is loaded, so saved geometry is visible immediately.

Expand Down
38 changes: 38 additions & 0 deletions src/config/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,44 @@ describe("parseAppBootstrapConfig", () => {
warn.mockRestore();
});

it("preserves the renamed Cuiman authentication options", () => {
const encodedService = encodeBase64Url({
id: "client",
meta: { type: "custom", title: "Client" },
options: {
apiUrl: "https://example.test/ogcapi",
authType: "oauth2",
tokenUrl: "https://auth.example.test/token",
grantType: "client_credentials",
accessToken: "secret",
accessTokenHeader: "X-Service-Token",
useBearer: false,
},
});

expect(
parseAppBootstrapConfig(`?service=${encodedService}`).service,
).toEqual({
id: "client",
meta: {
type: "custom",
title: "Client",
description: undefined,
disabled: undefined,
hidden: undefined,
},
options: {
apiUrl: "https://example.test/ogcapi",
authType: "oauth2",
tokenUrl: "https://auth.example.test/token",
grantType: "client_credentials",
accessToken: "secret",
accessTokenHeader: "X-Service-Token",
useBearer: false,
},
});
});

it("ignores invalid color scheme values", () => {
expect(parseAppBootstrapConfig("?scheme=auto")).toEqual({
compact: false,
Expand Down
58 changes: 49 additions & 9 deletions src/service/providers/custom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe("CustomServiceProvider", () => {
const service = await provider.createService({
apiUrl: "https://example.com/api/",
authType: "token",
token: "secret",
accessToken: "secret",
useBearer: true,
});

Expand All @@ -87,8 +87,8 @@ describe("CustomServiceProvider", () => {
const service = await provider.createService({
apiUrl: "https://example.com/api/",
authType: "token",
token: "secret",
tokenHeader: "X-Custom-Token",
accessToken: "secret",
accessTokenHeader: "X-Custom-Token",
});

expect(loadServiceRootMetadata).toHaveBeenCalledWith(
Expand All @@ -104,7 +104,7 @@ describe("CustomServiceProvider", () => {
const service = await provider.createService({
apiUrl: "https://example.com/api/",
authType: "token",
token: "secret",
accessToken: "secret",
});

expect(loadServiceRootMetadata).toHaveBeenCalledWith(
Expand All @@ -114,19 +114,59 @@ describe("CustomServiceProvider", () => {
expect(service.defaultHeaders).toEqual({ "X-Auth-Token": "secret" });
});

it("does not pass auth headers for other auth types", async () => {
it("uses an OAuth2 access token without starting a browser login flow", async () => {
const provider = new CustomServiceProvider();

const service = await provider.createService({
apiUrl: "https://example.com/api/",
authType: "none",
token: "secret",
authType: "oauth2",
tokenUrl: "https://auth.example.test/token",
grantType: "client_credentials",
accessToken: "secret",
useBearer: false,
accessTokenHeader: "X-Service-Token",
});

expect(loadServiceRootMetadata).toHaveBeenCalledWith(
"https://example.com/api/",
{},
{ "X-Service-Token": "secret" },
);
expect(service.defaultHeaders).toEqual({});
expect(service.defaultHeaders).toEqual({ "X-Service-Token": "secret" });
});

it("passes basic auth headers to the URL service", async () => {
const provider = new CustomServiceProvider();

const service = await provider.createService({
apiUrl: "https://example.com/api/",
authType: "basic",
username: "user",
password: "secret",
});

expect(loadServiceRootMetadata).toHaveBeenCalledWith(
"https://example.com/api/",
{ Authorization: "Basic dXNlcjpzZWNyZXQ=" },
);
expect(service.defaultHeaders).toEqual({
Authorization: "Basic dXNlcjpzZWNyZXQ=",
});
});

it("passes API key auth headers to the URL service", async () => {
const provider = new CustomServiceProvider();

const service = await provider.createService({
apiUrl: "https://example.com/api/",
authType: "api-key",
apiKey: "secret",
apiKeyHeader: "X-Service-Key",
});

expect(loadServiceRootMetadata).toHaveBeenCalledWith(
"https://example.com/api/",
{ "X-Service-Key": "secret" },
);
expect(service.defaultHeaders).toEqual({ "X-Service-Key": "secret" });
});
});
52 changes: 45 additions & 7 deletions src/service/providers/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ function getAuthorizationCodeOptions(
| "clientId"
| "oauth2Scopes"
> | null {
if (options.authType !== "oauth2" && options.authType !== "login") {
if (
options.authType !== "oauth2" ||
(!options.authorizationServerUrl &&
!(options.authorizationEndpoint && options.tokenEndpoint))
) {
return null;
}
return {
Expand All @@ -91,14 +95,48 @@ function getAuthorizationCodeOptions(
function createTokenAuthHeaders(
options: ServiceOptionsInput<UrlServiceOptions>,
): Record<string, string> {
const token = options.accessToken ?? options.token;
if (options.authType !== "token" || !token) {
switch (options.authType) {
case "basic":
return createBasicAuthHeaders(options);
case "token":
case "login":
case "oauth2":
return createAccessTokenHeaders(options);
case "api-key":
return createApiKeyAuthHeaders(options);
default:
return {};
}
}

function createBasicAuthHeaders(
options: ServiceOptionsInput<UrlServiceOptions>,
): Record<string, string> {
if (!options.username || !options.password) {
return {};
}
return {
Authorization: `Basic ${btoa(`${options.username}:${options.password}`)}`,
};
}

function createAccessTokenHeaders(
options: ServiceOptionsInput<UrlServiceOptions>,
): Record<string, string> {
if (!options.accessToken) {
return {};
}
if (options.useBearer === true) {
return { Authorization: `Bearer ${token}` };
return { Authorization: `Bearer ${options.accessToken}` };
}
return { [options.accessTokenHeader ?? "X-Auth-Token"]: options.accessToken };
}

function createApiKeyAuthHeaders(
options: ServiceOptionsInput<UrlServiceOptions>,
): Record<string, string> {
if (!options.apiKey) {
return {};
}
const tokenHeader =
options.accessTokenHeader ?? options.tokenHeader ?? "X-Auth-Token";
return { [tokenHeader]: token };
return { [options.apiKeyHeader ?? "X-API-Key"]: options.apiKey };
}
12 changes: 8 additions & 4 deletions src/service/providers/url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { describe, expect, it } from "vitest";
import { URL_SERVICE_OPTIONS_SCHEMA } from "./url";

describe("URL service options schema", () => {
it("uses the new OAuth2 configuration while retaining hidden legacy fields", () => {
it("uses the Cuiman authentication option names", () => {
expect(URL_SERVICE_OPTIONS_SCHEMA).toMatchObject({
authType: {
default: "none",
enum: ["none", "token", "login", "oauth2"],
enum: ["none", "basic", "token", "login", "oauth2", "api-key"],
},
accessToken: {
format: "password",
Expand Down Expand Up @@ -53,8 +53,12 @@ describe("URL service options schema", () => {
"x-ui-visible": "authType === 'login' || authType === 'oauth2'",
"x-ui-required": "authType === 'login' || authType === 'oauth2'",
},
token: { "x-ui-hidden": true },
tokenHeader: { "x-ui-hidden": true },
loginUrl: { format: "uri", "x-ui-hidden": true },
tokenUrl: { format: "uri", "x-ui-hidden": true },
grantType: {
enum: ["password", "client_credentials"],
"x-ui-hidden": true,
},
});
});
});
Loading