mirror of
https://github.com/teamhanko/hanko.git
synced 2025-10-27 14:17:56 +08:00
fix: merge conflicts. remove import in quickstart
This commit is contained in:
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -13,15 +13,15 @@ import {
|
||||
InvalidWebauthnCredentialError,
|
||||
TechnicalError,
|
||||
UnauthorizedError,
|
||||
WebauthnRequestCancelledError,
|
||||
UserVerificationError,
|
||||
WebauthnRequestCancelledError,
|
||||
} from "../Errors";
|
||||
|
||||
import {
|
||||
Attestation,
|
||||
User,
|
||||
WebauthnFinalized,
|
||||
WebauthnCredentials,
|
||||
WebauthnFinalized,
|
||||
} from "../Dto";
|
||||
|
||||
/**
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user