diff --git a/app/components/Launcher.vue b/app/components/Launcher.vue
index d6747d3c..4429df5d 100644
--- a/app/components/Launcher.vue
+++ b/app/components/Launcher.vue
@@ -1,6 +1,5 @@
diff --git a/app/components/Recaptcha.vue b/app/components/Recaptcha.vue
deleted file mode 100644
index bb2cd946..00000000
--- a/app/components/Recaptcha.vue
+++ /dev/null
@@ -1,89 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/stores/api.ts b/app/stores/api.ts
index ea2693a7..57392eb8 100644
--- a/app/stores/api.ts
+++ b/app/stores/api.ts
@@ -1,4 +1,4 @@
-import type { RequestHandlers } from "@ogw_shared/utils/types";
+import type { ParamsOf, RequestHandlers, ResponseOf } from "@ogw_shared/utils/types";
import { api_fetch } from "@ogw_internal/utils/api_fetch";
import { consola } from "consola";
@@ -8,7 +8,7 @@ interface ApiSchema {
[key: string]: unknown;
}
-type ApiCallbacks = RequestHandlers & {
+type ApiCallbacks = RequestHandlers & {
skip_feedback_error?: boolean;
};
@@ -26,21 +26,21 @@ export const useAPIStore = defineStore("api", () => {
request_counter.value -= 1;
}
- // `TResult` is asserted, not verified: the response is only checked against `schema` at runtime
- async function request(
+ // The response type comes from the schema's generated `response` type (see the Cloud API typed schemas), asserted at the API boundary below, not verified
+ async function request(
{
schema,
- params = {},
+ params,
headers = {},
- }: { schema: ApiSchema; params?: Record; headers?: Record },
- callbacks: ApiCallbacks = {},
- ): Promise {
+ }: { schema: Schema; params?: ParamsOf; headers?: Record },
+ callbacks: ApiCallbacks> = {},
+ ): Promise> {
consola.info("[API] Request:", schema.$id);
const start = Date.now();
const result = await api_fetch(
{ $id: schema.$id, base_url: base_url.value, start_request, stop_request },
- { schema, params, headers },
+ { schema, params: params ?? {}, headers },
{
...callbacks,
response_function: async (response: unknown) => {
@@ -52,13 +52,14 @@ export const useAPIStore = defineStore("api", () => {
"s",
);
if (callbacks.response_function) {
- await callbacks.response_function(response);
+ // oxlint-disable-next-line no-unsafe-type-assertion -- trusted API boundary; see comment above.
+ await callbacks.response_function(response as ResponseOf);
}
},
},
);
// oxlint-disable-next-line no-unsafe-type-assertion -- trusted API boundary; see comment above.
- return result as TResult;
+ return result as ResponseOf;
}
return {
base_url,
diff --git a/app/stores/cloud.ts b/app/stores/cloud.ts
index f720bf94..0a57f26e 100644
--- a/app/stores/cloud.ts
+++ b/app/stores/cloud.ts
@@ -1,9 +1,8 @@
import { clearCloudUrlParam, getCloudUrlParam } from "@ogw_front/utils/cloud";
-import type { RunCloudResponse } from "@geode/cloud-api/types";
import { Status } from "@ogw_front/utils/status";
import { api_fetch } from "@ogw_internal/utils/api_fetch";
import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_typed_schemas.js";
-import cloud_api_schemas from "@geode/cloud-api/cloud_api_schemas.json";
+import cloud_api_schemas from "@geode/cloud-api/cloud_api_typed_schemas.js";
import { setAppBaseUrl } from "@ogw_shared/scripts";
import { useAPIStore } from "@ogw_front/stores/api";
import { useFeedbackStore } from "@ogw_front/stores/feedback";
@@ -17,10 +16,13 @@ export const useCloudStore = defineStore("cloud", {
actions: {
// Reuses the running service given by `?cloud_url=` when present, otherwise launches a new one.
// The param is single use: it is cleared whatever the outcome, so a retry launches a new service.
- async start(email: string) {
+ async start(authToken?: string) {
const existing_host = getCloudUrlParam();
if (existing_host === undefined) {
- await this.launch(email);
+ if (authToken === undefined) {
+ throw new Error("Launching a cloud service requires an authenticated user");
+ }
+ await this.launch(authToken);
return;
}
try {
@@ -29,29 +31,23 @@ export const useCloudStore = defineStore("cloud", {
clearCloudUrlParam();
}
},
- async launch(email: string) {
+ // `authToken` is the user's Firebase ID token, checked by the Cloud API
+ async launch(authToken: string) {
this.status = Status.CONNECTING;
const { PROJECT, BRANCH } = useRuntimeConfig().public;
- const params = { email, project: PROJECT, branch: BRANCH };
+ const params = { project: PROJECT, branch: BRANCH };
+ const headers = { Authorization: `Bearer ${authToken}` };
const feedbackStore = useFeedbackStore();
const APIStore = useAPIStore();
- const result = await APIStore.request(
- { schema: run_cloud_schema, params },
+ const result = await APIStore.request(
+ { schema: run_cloud_schema, params, headers },
{
request_error_function: () => {
feedbackStore.$patch({ server_error: true });
this.status = Status.NOT_CONNECTED;
},
- response_function: async (response: unknown) => {
- if (
- typeof response !== "object" ||
- response === null ||
- !("url" in response) ||
- typeof response.url !== "string"
- ) {
- return;
- }
- await this.on_connected(response.url);
+ response_function: async ({ url }) => {
+ await this.on_connected(url);
},
response_error_function: () => {
feedbackStore.$patch({ server_error: true });
diff --git a/app/stores/infra.ts b/app/stores/infra.ts
index 84d6dc8e..10106abf 100644
--- a/app/stores/infra.ts
+++ b/app/stores/infra.ts
@@ -44,7 +44,7 @@ export const useInfraStore = defineStore("infra", {
(microservice: Microservice) => microservice.$id !== microserviceId,
);
},
- async create_backend(email?: string) {
+ async create_backend(authToken?: string) {
if (this.status === Status.CREATED) {
return undefined;
}
@@ -56,7 +56,7 @@ export const useInfraStore = defineStore("infra", {
if (this.app_mode === appMode.CLOUD) {
const cloudStore = useCloudStore();
try {
- await cloudStore.start(email ?? "");
+ await cloudStore.start(authToken);
} catch (error) {
// Back to the "Load the app" button so the user is not stuck on the loading screen.
this.status = Status.NOT_CREATED;
diff --git a/tests/unit/stores/cloud.nuxt.test.ts b/tests/unit/stores/cloud.nuxt.test.ts
index 26832268..1ae87399 100644
--- a/tests/unit/stores/cloud.nuxt.test.ts
+++ b/tests/unit/stores/cloud.nuxt.test.ts
@@ -28,7 +28,7 @@ const mockedFetch = vi.mocked($fetch);
const PROJECT = "project";
const BRANCH = "branch";
const CLOUD_API_URL = "https://api.example.com";
-const EMAIL = "noreply@example.com";
+const TOKEN = "token";
const RESPONSE_OK_STATUS = 200;
const RESPONSE_ERROR_STATUS = 500;
const CLOUD_RUN_HOST = "vease-abc123.europe-west1.run.app";
@@ -114,13 +114,14 @@ describe("cloud store", () => {
return data;
});
- await cloudStore.launch(EMAIL);
+ await cloudStore.launch(TOKEN);
expect(mockedFetch).toHaveBeenCalledWith(
"cloud_api/cloud/run",
expect.objectContaining({
baseURL: CLOUD_API_URL,
- body: { email: EMAIL, project: PROJECT, branch: BRANCH },
+ body: { project: PROJECT, branch: BRANCH },
+ headers: { Authorization: `Bearer ${TOKEN}` },
}),
);
expect(cloudStore.status).toBe(Status.CONNECTED);
@@ -152,7 +153,7 @@ describe("cloud store", () => {
throw error;
});
- await expect(cloudStore.launch(EMAIL)).rejects.toThrow("500 Internal Server Error");
+ await expect(cloudStore.launch(TOKEN)).rejects.toThrow("500 Internal Server Error");
expect(cloudStore.status).toBe(Status.NOT_CONNECTED);
expect(feedbackStore.server_error).toBe(true);
@@ -249,16 +250,24 @@ describe("cloud store", () => {
test("without cloud_url launches a new service", async () => {
setupConfig();
setCloudUrlParam();
- await useCloudStore().start(EMAIL);
+ await useCloudStore().start(TOKEN);
expect(mockedFetch).toHaveBeenCalledWith("cloud_api/cloud/run", expect.anything());
expect(useInfraStore().domain_name).toBe("test.com");
});
+ test("without cloud_url nor auth token getter rejects", async () => {
+ setupConfig();
+ setCloudUrlParam();
+ await expect(useCloudStore().start()).rejects.toThrow("requires an authenticated user");
+
+ expect(mockedFetch).not.toHaveBeenCalledWith("cloud_api/cloud/run", expect.anything());
+ });
+
test("with cloud_url connects to the existing service", async () => {
setupConfig();
setCloudUrlParam(CLOUD_RUN_HOST);
- await useCloudStore().start(EMAIL);
+ await useCloudStore().start(TOKEN);
expect(mockedFetch).not.toHaveBeenCalledWith("cloud_api/cloud/run", expect.anything());
expect(mockedFetch).toHaveBeenCalledWith("opengeodeweb_back/ping", expect.anything());
@@ -272,11 +281,11 @@ describe("cloud store", () => {
const cloudStore = useCloudStore();
mockedFetch.mockRejectedValueOnce(new Error("unreachable"));
- await expect(cloudStore.start(EMAIL)).rejects.toThrow("unreachable");
+ await expect(cloudStore.start(TOKEN)).rejects.toThrow("unreachable");
expect(globalThis.location.search).toBe("");
mockedFetch.mockClear();
- await cloudStore.start(EMAIL);
+ await cloudStore.start(TOKEN);
expect(mockedFetch).toHaveBeenCalledWith("cloud_api/cloud/run", expect.anything());
expect(mockedFetch).not.toHaveBeenCalledWith("opengeodeweb_back/ping", expect.anything());
expect(useInfraStore().domain_name).toBe("test.com");
diff --git a/tests/unit/stores/infra.nuxt.test.ts b/tests/unit/stores/infra.nuxt.test.ts
index 885c2f12..e45a17e5 100644
--- a/tests/unit/stores/infra.nuxt.test.ts
+++ b/tests/unit/stores/infra.nuxt.test.ts
@@ -264,7 +264,7 @@ describe("infra store", () => {
return data;
});
- await infraStore.create_backend("noreply@example.com");
+ await infraStore.create_backend("token");
expect(infraStore.status).toBe(Status.CREATED);
expect(infraStore.domain_name).toBe(url);
@@ -278,7 +278,7 @@ describe("infra store", () => {
infraStore.app_mode = appMode.CLOUD;
vi.mocked($fetch).mockRejectedValue(new Error("unreachable"));
- await expect(infraStore.create_backend("noreply@example.com")).rejects.toThrow("unreachable");
+ await expect(infraStore.create_backend("token")).rejects.toThrow("unreachable");
expect(infraStore.status).toBe(Status.NOT_CREATED);
});
});