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

Auth User Middleware #11

Merged
merged 21 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
94dd9db
first draft
owens1127 Oct 6, 2024
0ee9d0a
doc
owens1127 Oct 6, 2024
78afd39
Added script to seed the database and then run the tests
jpraissman Oct 13, 2024
50f340a
Created tests for get-authenticated-user
jpraissman Oct 13, 2024
1418098
Created mock for cookies
jpraissman Oct 13, 2024
bf9f0b0
Updated README
jpraissman Oct 13, 2024
44314e3
Merge remote-tracking branch 'origin/main' into auth-user-middleware
jpraissman Oct 14, 2024
0e72982
Github Actions runs the seed command before running the tests
jpraissman Oct 14, 2024
86583d3
Removed test:seed command because that is not needed
jpraissman Oct 16, 2024
7385fe9
Removed test:seed command because that is not needed
jpraissman Oct 16, 2024
48d1643
Updated MockNextCookies to have an apply method
jpraissman Oct 16, 2024
a5a5532
Updated tests to use the new apply method on the mock cookies and to …
jpraissman Oct 16, 2024
86a308f
Delete database records after now
jpraissman Oct 16, 2024
c6c3bed
Reverted changes
jpraissman Oct 20, 2024
d8804d9
Removed console.log statement
jpraissman Oct 20, 2024
d66e879
Merged main into branch
jpraissman Oct 20, 2024
d09de21
Updated auth builder and tests to use id instead of token
jpraissman Oct 20, 2024
4fc50e8
Merged in main
jpraissman Oct 20, 2024
10a265a
Updated get method to fix return value issue
jpraissman Oct 21, 2024
e91faa1
sessionToken -> sessionId, uses Promise.all(), and added test to make…
jpraissman Oct 21, 2024
fb01079
Fixed schema and all related things
jpraissman Oct 23, 2024
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.
1 change: 1 addition & 0 deletions packages/trpc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@trpc/client": "11.0.0-rc.544",
"@trpc/react-query": "11.0.0-rc.544",
"@trpc/server": "11.0.0-rc.544",
"next": "^14.2.12",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"server-only": "^0.0.1",
Expand Down
37 changes: 35 additions & 2 deletions packages/trpc/src/internal/init.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { initTRPC } from "@trpc/server";
import { cookies } from "next/headers";
import { initTRPC, TRPCError } from "@trpc/server";
import SuperJSON from "superjson";

import { prisma } from "@good-dog/db";
Expand All @@ -26,5 +27,37 @@ const t = initTRPC.context<ReturnType<typeof createTRPCContext>>().create({

// Base router and procedure helpers
export const createTRPCRouter = t.router;
export const baseProcedureBuilder = t.procedure;
export const createCallerFactory = t.createCallerFactory;

// Procedure builders
export const baseProcedureBuilder = t.procedure;
export const authenticatedProcedureBuilder = baseProcedureBuilder.use(
async ({ ctx, next }) => {
const sessionToken = cookies().get("sessionToken");

if (!sessionToken) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}

const sessionOrNull = await ctx.prisma.session.findUnique({
where: {
id: parseInt(sessionToken.value),
},
include: {
user: true,
},
});

if (!sessionOrNull || sessionOrNull.expiresAt < new Date()) {
// Session expired or not found
throw new TRPCError({ code: "UNAUTHORIZED" });
}

return next({
ctx: {
...ctx,
user: sessionOrNull.user,
},
});
},
);
2 changes: 2 additions & 0 deletions packages/trpc/src/internal/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import {
signOutProcedure,
signUpProcedure,
} from "../procedures/auth";
import { getAuthenticatedUserProcedure } from "../procedures/user";
import { createTRPCRouter } from "./init";

export const appRouter = createTRPCRouter({
signIn: signInProcedure,
signOut: signOutProcedure,
signUp: signUpProcedure,
deleteAccount: deleteAccountIfExistsProcedure,
user: getAuthenticatedUserProcedure,
});

export type AppRouter = typeof appRouter;
6 changes: 6 additions & 0 deletions packages/trpc/src/procedures/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { authenticatedProcedureBuilder } from "../internal/init";

export const getAuthenticatedUserProcedure =
authenticatedProcedureBuilder.query(({ ctx }) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's

  1. Verify that this endpoint doesn't return the user's password
  2. Write a test specifically for that behavior

return ctx.user;
});
17 changes: 16 additions & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
..
# Tests

## Mocks

### MockNextCookies

This is a mock for the cookies function in next/headers. In your tests, just instantiate
a new MockNextCookies object, set the cookies you would like, and run the apply() method
on the newly created object.

Example usage:

<pre>const cookies = new MockNextCookies();
cookies.set("myKey", "myValue");
cookies.apply();
... rest of your test</pre>
142 changes: 142 additions & 0 deletions tests/auth/get-authenticated-user.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { afterAll, beforeAll, expect, test } from "bun:test";

import { prisma } from "@good-dog/db";
import { _trpcCaller } from "@good-dog/trpc/server";

import { MockNextCookies } from "../mocks/MockNextCookies";

// Seeds the database before running the tests
beforeAll(async () => {
const person1 = await prisma.user.upsert({
where: { email: "person1@prisma.io" },
update: {},
create: {
email: "person1@prisma.io",
name: "Person 1",
password: "person1Password",
},
});
await prisma.session.upsert({
where: { id: 500 },
update: {
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
},
create: {
userId: person1.id,
id: 500,
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
},
});

const person2 = await prisma.user.upsert({
where: { email: "person2@gmail.com" },
update: {},
create: {
email: "person2@gmail.com",
name: "Person2 Jones",
password: "person2Password",
},
});
await prisma.session.upsert({
where: { id: 501 },
update: {
expiresAt: new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
},
create: {
userId: person2.id,
id: 501,
expiresAt: new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
},
});
await prisma.session.upsert({
where: { id: 502 },
update: {
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
},
create: {
userId: person2.id,
id: 502,
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
},
});
});

test("Correct user is returned when they have a valid session.", async () => {
// Set the cookies
const cookies = new MockNextCookies();
cookies.set("sessionToken", "500");
await cookies.apply();

const user = await _trpcCaller.user();

expect(user.email).toEqual("person1@prisma.io");
});

test("Correct user is returned when they have multiple sessions and one is valid.", async () => {
// Set the cookies
const cookies = new MockNextCookies();
cookies.set("sessionToken", "502");
await cookies.apply();

const user = await _trpcCaller.user();

expect(user.email).toEqual("person2@gmail.com");
});

test("'UNAUTHORIZED' error is thrown when no session is found for the token.", async () => {
// Set the cookies
const cookies = new MockNextCookies();
cookies.set("sessionToken", "503");
await cookies.apply();

const getUser = async () => await _trpcCaller.user();

expect(getUser).toThrow("UNAUTHORIZED");
});

test("'UNAUTHORIZED' error is thrown when there is no 'sessionToken' cookie.", async () => {
const cookies = new MockNextCookies();
await cookies.apply();

const getUser = async () => await _trpcCaller.user();

expect(getUser).toThrow("UNAUTHORIZED");
});

test("'UNAUTHORIZED' error is thrown when session is expired.", async () => {
// Set the cookies
const cookies = new MockNextCookies();
cookies.set("sessionToken", "501");
await cookies.apply();

const getUser = async () => await _trpcCaller.user();

expect(getUser).toThrow("UNAUTHORIZED");
});

// Delete the records created for these tests
afterAll(async () => {
Copy link
Collaborator

@owens1127 owens1127 Oct 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can wrap this in a Promise.all() to parallelize the requests

await prisma.session.delete({
where: { id: 500 },
});
await prisma.session.delete({
where: { id: 501 },
});
await prisma.session.delete({
where: { id: 502 },
});
await prisma.user.delete({
where: { email: "person1@prisma.io" },
});
await prisma.user.delete({
where: { email: "person2@gmail.com" },
});
});
29 changes: 29 additions & 0 deletions tests/mocks/MockNextCookies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { mock } from "bun:test";

// A mock for the cookies function in the NextJS next/header module.
export class MockNextCookies {
private cookiesMap: Map<string, string>;

constructor() {
this.cookiesMap = new Map<string, string>();
}

// Applies this mock to be the cookies used by the next/header module. This method
// must be called in order for this mock to be applied.
async apply(): Promise<void> {
await mock.module("next/headers", () => ({
cookies: () => this,
}));
}

set(key: string, value: string): void {
this.cookiesMap.set(key, value);
}

get(key: string): { value: string | undefined } {
const requestCookie = {
value: this.cookiesMap.get(key),
};
return requestCookie;
}
}