fix: merge conflicts. remove import in quickstart

This commit is contained in:
Felix Dubrownik
2023-03-03 12:49:56 +01:00
105 changed files with 12072 additions and 28650 deletions

View File

@ -44,9 +44,9 @@
"devDependencies": {
"@github/webauthn-json": "^2.1.1",
"@types/jest": "^29.4.0",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"better-docs": "^2.7.2",
"eslint": "^8.33.0",
"eslint": "^8.35.0",
"eslint-config-google": "^0.14.0",
"eslint-config-preact": "^1.3.0",
"eslint-config-prettier": "^8.6.0",
@ -62,6 +62,6 @@
"typescript": "^4.9.5"
},
"dependencies": {
"@types/js-cookie": "^3.0.2"
"@types/js-cookie": "^3.0.3"
}
}

View File

@ -119,6 +119,7 @@ class Response {
class HttpClient {
timeout: number;
api: string;
authCookieName = "hanko";
// eslint-disable-next-line require-jsdoc
constructor(api: string, timeout = 13000) {
@ -128,11 +129,10 @@ class HttpClient {
// eslint-disable-next-line require-jsdoc
_fetch(path: string, options: RequestInit, xhr = new XMLHttpRequest()) {
const api = this.api;
const url = api + path;
const self = this;
const url = this.api + path;
const timeout = this.timeout;
const cookieName = "hanko";
const bearerToken = Cookies.get(cookieName);
const bearerToken = this._getAuthCookie();
return new Promise<Response>(function (resolve, reject) {
xhr.open(options.method, url, true);
@ -153,11 +153,7 @@ class HttpClient {
if (headers.length) {
const authToken = xhr.getResponseHeader("X-Auth-Token");
if (authToken) {
const secure = !!api.match("^https://");
Cookies.set(cookieName, authToken, { secure });
}
if (authToken) self._setAuthCookie(authToken);
}
resolve(new Response(xhr));
@ -175,6 +171,35 @@ class HttpClient {
});
}
/**
* Returns the authentication token that was stored in the cookie.
*
* @return {string}
* @return {string}
*/
_getAuthCookie(): string {
return Cookies.get(this.authCookieName);
}
/**
* Stores the authentication token to the cookie.
*
* @param {string} token - The authentication token to be stored.
*/
_setAuthCookie(token: string) {
const secure = !!this.api.match("^https://");
Cookies.set(this.authCookieName, token, { secure });
}
/**
* Removes the cookie used for authentication.
*
* @param {string} token - The authorization token to be stored.
*/
removeAuthCookie() {
Cookies.remove(this.authCookieName);
}
/**
* Performs a GET request.
*

View File

@ -100,6 +100,27 @@ class UserClient extends Client {
return userResponse.json();
}
/**
* Logs out the current user and expires the existing session cookie. A valid session cookie is required to call the logout endpoint.
*
* @return {Promise<void>}
* @throws {TechnicalError}
*/
async logout(): Promise<void> {
const logoutResponse = await this.client.post("/logout");
// For cross-domain operations, the frontend SDK creates the cookie by reading the "X-Auth-Token" header, and
// "Set-Cookie" headers sent by the backend have no effect due to the browser's security policy, which means that
// the cookie must also be removed client-side in that case.
this.client.removeAuthCookie();
if (logoutResponse.status === 401) {
return; // The user is logged out already
} else if (!logoutResponse.ok) {
throw new TechnicalError();
}
}
}
export { UserClient };

View File

@ -13,15 +13,15 @@ import {
InvalidWebauthnCredentialError,
TechnicalError,
UnauthorizedError,
WebauthnRequestCancelledError,
UserVerificationError,
WebauthnRequestCancelledError,
} from "../Errors";
import {
Attestation,
User,
WebauthnFinalized,
WebauthnCredentials,
WebauthnFinalized,
} from "../Dto";
/**

View File

@ -62,7 +62,7 @@ describe("httpClient._fetch()", () => {
this.onload();
});
Cookies.get = jest.fn().mockReturnValue(jwt);
jest.spyOn(httpClient, "_getAuthCookie").mockReturnValue(jwt);
await httpClient._fetch("/test", { method: "GET" }, xhr);
@ -84,31 +84,12 @@ describe("httpClient._fetch()", () => {
});
jest.spyOn(xhr, "getResponseHeader").mockReturnValue(jwt);
Cookies.set = jest.fn();
jest.spyOn(client, "_setAuthCookie");
await client._fetch("/test", { method: "GET" }, xhr);
expect(xhr.getResponseHeader).toHaveBeenCalledWith("X-Auth-Token");
expect(Cookies.set).toHaveBeenCalledWith("hanko", jwt, { secure: false });
});
it("should set a secure cookie if x-auth-token response header is available and https is used", async () => {
httpClient = new HttpClient("https://test.api");
jest.spyOn(xhr, "send").mockImplementation(function () {
// eslint-disable-next-line no-invalid-this
this.onload();
});
jest.spyOn(xhr, "getResponseHeader").mockReturnValue(jwt);
Cookies.set = jest.fn();
await httpClient._fetch("/test", { method: "GET" }, xhr);
expect(xhr.getResponseHeader).toHaveBeenCalledWith("X-Auth-Token");
expect(Cookies.set).toHaveBeenCalledWith("hanko", jwt, { secure: true });
expect(client._setAuthCookie).toHaveBeenCalledWith(jwt);
});
it("should handle onerror", async () => {
@ -134,6 +115,49 @@ describe("httpClient._fetch()", () => {
});
});
describe("httpClient._setAuthCookie()", () => {
it("should set a new cookie", async () => {
httpClient = new HttpClient("http://test.api");
jest.spyOn(Cookies, "set");
httpClient._setAuthCookie("test-token");
expect(Cookies.set).toHaveBeenCalledWith("hanko", "test-token", {
secure: false,
});
});
it("should set a new secure cookie", async () => {
httpClient = new HttpClient("https://test.api");
jest.spyOn(Cookies, "set");
httpClient._setAuthCookie("test-token");
expect(Cookies.set).toHaveBeenCalledWith("hanko", "test-token", {
secure: true,
});
});
});
describe("httpClient._getAuthCookie()", () => {
it("should return the contents of the authorization cookie", async () => {
httpClient = new HttpClient("https://test.api");
Cookies.get = jest.fn().mockReturnValue("test-token");
const token = httpClient._getAuthCookie();
expect(Cookies.get).toHaveBeenCalledWith("hanko");
expect(token).toBe("test-token");
});
});
describe("httpClient._removeAuthCookie()", () => {
it("should return the contents of the authorization cookie", async () => {
httpClient = new HttpClient("https://test.api");
jest.spyOn(Cookies, "remove");
httpClient.removeAuthCookie();
expect(Cookies.remove).toHaveBeenCalledWith("hanko");
});
});
describe("httpClient.get()", () => {
it("should call get with correct args", async () => {
httpClient._fetch = jest.fn();

View File

@ -179,3 +179,42 @@ describe("UserClient.create()", () => {
await expect(user).rejects.toThrowError("Test error");
});
});
describe("UserClient.logout()", () => {
it.each`
status
${200}
${401}
`("should return true if logout is successful", async ({ status }) => {
const response = new Response(new XMLHttpRequest());
response.status = status;
response.ok = status >= 200 && status <= 299;
jest.spyOn(userClient.client, "post").mockResolvedValueOnce(response);
await expect(userClient.logout()).resolves.not.toThrow();
expect(userClient.client.post).toHaveBeenCalledWith("/logout");
});
it.each`
status | error
${400} | ${"Technical error"}
${404} | ${"Technical error"}
${500} | ${"Technical error"}
`(
"should throw error if API returns an error status",
async ({ status, error }) => {
const response = new Response(new XMLHttpRequest());
response.status = status;
response.ok = status >= 200 && status <= 299;
jest
.spyOn(userClient.client, "post")
.mockResolvedValueOnce(response)
await expect(userClient.logout()).rejects.toThrow(error);
expect(userClient.client.post).toHaveBeenCalledWith("/logout");
}
);
});