Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade to Remix Auth v4 #57

Merged
merged 2 commits into from
Nov 27, 2024
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
Binary file modified bun.lockb
Binary file not shown.
6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,14 @@
"./package.json": "./package.json"
},
"peerDependencies": {
"@remix-run/server-runtime": "^1.0.0 || ^2.0.0",
"remix-auth": "^3.6.0"
"remix-auth": "^4.0.0"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.15.3",
"@biomejs/biome": "^1.7.2",
"@remix-run/node": "^2.9.2",
"@types/bun": "^1.1.1",
"consola": "^3.2.3",
"remix-auth": "^3.7.0",
"remix-auth": "^4.0.0",
"typedoc": "^0.25.13",
"typedoc-plugin-mdn-links": "^3.1.23",
"typescript": "^5.4.5"
Expand Down
70 changes: 11 additions & 59 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
import { beforeEach, expect, mock, test } from "bun:test";
import { createCookieSessionStorage } from "@remix-run/node";
import { AuthenticateOptions, AuthorizationError } from "remix-auth";
import { FormStrategy, FormStrategyVerifyParams } from ".";

let verify = mock();

let sessionStorage = createCookieSessionStorage({
cookie: { secrets: ["s3cr3t"] },
});

let options = {
name: "form",
sessionKey: "user",
sessionErrorKey: "error",
sessionStrategyKey: "strategy",
} satisfies AuthenticateOptions;

beforeEach(() => {
verify.mockReset();
});
Expand All @@ -33,7 +20,7 @@ test("should pass to the verify callback a FormData object", async () => {

let strategy = new FormStrategy(verify);

await strategy.authenticate(request, sessionStorage, options);
await strategy.authenticate(request);

expect(verify).toBeCalledWith({ form: body, request });
});
Expand All @@ -50,29 +37,11 @@ test("should return what the verify callback returned", async () => {

let strategy = new FormStrategy<string>(verify);

let user = await strategy.authenticate(request, sessionStorage, options);
let user = await strategy.authenticate(request);

expect(user).toBe("test@example.com");
});

test("should pass the context to the verify callback", async () => {
let body = new FormData();
body.set("email", "test@example.com");

let request = new Request("http://.../test", { body, method: "POST" });

let context = { test: "it works!" };

let strategy = new FormStrategy(verify);

await strategy.authenticate(request, sessionStorage, {
...options,
context,
});

expect(verify).toBeCalledWith({ form: body, context, request });
});

test("should pass error as cause on failure", async () => {
verify.mockImplementationOnce(() => {
throw new TypeError("Invalid email address");
Expand All @@ -85,15 +54,10 @@ test("should pass error as cause on failure", async () => {

let strategy = new FormStrategy(verify);

let result = await strategy
.authenticate(request, sessionStorage, {
...options,
throwOnError: true,
})
.catch((error) => error);
let result = await strategy.authenticate(request).catch((error) => error);

expect(result).toEqual(new AuthorizationError("Invalid email address"));
expect((result as AuthorizationError).cause).toEqual(
expect(result).toEqual(new Error("Invalid email address"));
expect((result as Error).cause).toEqual(
new TypeError("Invalid email address"),
);
});
Expand All @@ -113,17 +77,10 @@ test("should pass generate error from string on failure", async () => {

let strategy = new FormStrategy(verify);

let result = await strategy
.authenticate(request, sessionStorage, {
...options,
throwOnError: true,
})
.catch((error) => error);
let result = await strategy.authenticate(request).catch((error) => error);

expect(result).toEqual(new AuthorizationError("Invalid email address"));
expect((result as AuthorizationError).cause).toEqual(
new Error("Invalid email address"),
);
expect(result).toEqual(new Error("Invalid email address"));
expect((result as Error).cause).toEqual(new Error("Invalid email address"));
});

test("should create Unknown error if thrown value is not Error or string", async () => {
Expand All @@ -138,15 +95,10 @@ test("should create Unknown error if thrown value is not Error or string", async

let strategy = new FormStrategy(verify);

let result = await strategy
.authenticate(request, sessionStorage, {
...options,
throwOnError: true,
})
.catch((error) => error);
let result = await strategy.authenticate(request).catch((error) => error);

expect(result).toEqual(new AuthorizationError("Unknown error"));
expect((result as AuthorizationError).cause).toEqual(
expect(result).toEqual(new Error("Unknown error"));
expect((result as Error).cause).toEqual(
new Error(JSON.stringify({ message: "Invalid email address" }, null, 2)),
);
});
83 changes: 22 additions & 61 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,15 @@
import type { AppLoadContext, SessionStorage } from "@remix-run/server-runtime";
import { AuthenticateOptions, Strategy } from "remix-auth";

export interface FormStrategyVerifyParams {
/**
* A FormData object with the content of the form used to trigger the
* authentication.
*
* Here you can read any input value using the FormData API.
*/
form: FormData;
/**
* An object of arbitrary for route loaders and actions provided by the
* server's `getLoadContext()` function.
*/
context?: AppLoadContext;
/**
* The request that triggered the authentication.
*/
request: Request;
}
import { Strategy } from "remix-auth/strategy";

export class FormStrategy<User> extends Strategy<
User,
FormStrategyVerifyParams
FormStrategy.VerifyOptions
> {
name = "form";

async authenticate(
request: Request,
sessionStorage: SessionStorage,
options: AuthenticateOptions,
): Promise<User> {
try {
if (request.bodyUsed) throw new BodyUsedError();
let form = await request.clone().formData();
let user = await this.verify({ form, context: options.context, request });
return this.success(user, request, sessionStorage, options);
} catch (error) {
if (error instanceof Error) {
return await this.failure(
error.message,
request,
sessionStorage,
options,
error,
);
}

if (typeof error === "string") {
return await this.failure(
error,
request,
sessionStorage,
options,
new Error(error),
);
}

return await this.failure(
"Unknown error",
request,
sessionStorage,
options,
new Error(JSON.stringify(error, null, 2)),
);
}
async authenticate(request: Request): Promise<User> {
if (request.bodyUsed) throw new BodyUsedError();
let form = await request.clone().formData();
return this.verify({ form, request });
}
}

Expand Down Expand Up @@ -98,3 +43,19 @@ export class BodyUsedError extends Error {
);
}
}

export namespace FormStrategy {
export interface VerifyOptions {
/**
* A FormData object with the content of the form used to trigger the
* authentication.
*
* Here you can read any input value using the FormData API.
*/
form: FormData;
/**
* The request that triggered the authentication.
*/
request: Request;
}
}