Skip to content

Commit

Permalink
Auth User Middleware (#11)
Browse files Browse the repository at this point in the history
* first draft

* doc

* Added script to seed the database and then run the tests

* Created tests for get-authenticated-user

* Created mock for cookies

* Updated README

* Github Actions runs the seed command before running the tests

* Removed test:seed command because that is not needed

* Removed test:seed command because that is not needed

* Updated MockNextCookies to have an apply method

* Updated tests to use the new apply method on the mock cookies and to seed the database on its own

* Delete database records after now

* Reverted changes

* Removed console.log statement

* Updated auth builder and tests to use id instead of token

* Updated get method to fix return value issue

* sessionToken -> sessionId, uses Promise.all(), and added test to make sure password isn't returned

* Fixed schema and all related things

---------

Co-authored-by: owen <[email protected]>
  • Loading branch information
jpraissman and owens1127 authored Oct 23, 2024
1 parent 9b740b4 commit 67a3646
Show file tree
Hide file tree
Showing 12 changed files with 290 additions and 19 deletions.
27 changes: 27 additions & 0 deletions apps/db/prisma/migrations/20241023232921_added_uuid/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
Warnings:
- The primary key for the `Session` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint.
*/
-- DropForeignKey
ALTER TABLE "Session" DROP CONSTRAINT "Session_userId_fkey";

-- AlterTable
ALTER TABLE "Session" DROP CONSTRAINT "Session_pkey",
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ALTER COLUMN "userId" SET DATA TYPE TEXT,
ADD CONSTRAINT "Session_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "Session_id_seq";

-- AlterTable
ALTER TABLE "User" DROP CONSTRAINT "User_pkey",
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "User_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "User_id_seq";

-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
8 changes: 4 additions & 4 deletions apps/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ datasource db {
}

model User {
id Int @id @default(autoincrement())
id String @id @default(uuid())
email String @unique
name String?
password String
Expand All @@ -25,9 +25,9 @@ model User {
}

model Session {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId Int
id String @id @default(uuid())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
createdAt DateTime @default(now())
expiresAt DateTime
}
22 changes: 11 additions & 11 deletions apps/db/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ async function main() {
where: { email: "[email protected]" },
update: {},
create: {
id: 7,
id: "7",
email: "[email protected]",
name: "Alice",
password: "alicePasswod",
},
});
const aliceSession = await prisma.session.upsert({
where: { id: 23 },
where: { id: "23" },
update: {
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
},
create: {
userId: alice.id,
id: 23,
id: "23",
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
Expand All @@ -32,33 +32,33 @@ async function main() {
where: { email: "[email protected]" },
update: {},
create: {
id: 9,
id: "9",
email: "[email protected]",
name: "Bob Jones",
password: "bobPassword",
},
});
const bobSession1 = await prisma.session.upsert({
where: { id: 12 },
where: { id: "12" },
update: {
expiresAt: new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
},
create: {
userId: bob.id,
id: 12,
id: "12",
expiresAt: new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
},
});
const bobSession2 = await prisma.session.upsert({
where: { id: 45 },
where: { id: "45" },
update: {
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
},
create: {
userId: bob.id,
id: 45,
id: "45",
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
Expand All @@ -69,20 +69,20 @@ async function main() {
where: { email: "[email protected]" },
update: {},
create: {
id: 56,
id: "56",
email: "[email protected]",
name: "Eve Smith",
password: "evePassword",
},
});
const eveSession = await prisma.session.upsert({
where: { id: 78 },
where: { id: "78" },
update: {
expiresAt: new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
},
create: {
userId: eve.id,
id: 78,
id: "78",
expiresAt: new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
},
});
Expand Down
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
46 changes: 44 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,46 @@ 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 sessionId = cookies().get("sessionId");

if (!sessionId?.value) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}

const sessionOrNull = await ctx.prisma.session.findUnique({
where: {
id: sessionId.value,
},
include: {
user: {
select: {
id: true,
email: true,
name: true,
sessions: true,
createdAt: true,
updatedAt: 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;
2 changes: 1 addition & 1 deletion packages/trpc/src/procedures/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export const signInProcedure = baseProcedureBuilder
export const signOutProcedure = baseProcedureBuilder
.input(
z.object({
id: z.number(),
id: z.string(),
}),
)
.mutation(async ({ ctx, input }) => {
Expand Down
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 }) => {
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>
145 changes: 145 additions & 0 deletions tests/auth/get-authenticated-user.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
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: "[email protected]" },
update: {},
create: {
email: "[email protected]",
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: "[email protected]" },
update: {},
create: {
email: "[email protected]",
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("sessionId", "500");
await cookies.apply();

const user = await _trpcCaller.user();

expect(user.email).toEqual("[email protected]");
});

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

const user = await _trpcCaller.user();

expect(user.email).toEqual("[email protected]");
});

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

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

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

test("'UNAUTHORIZED' error is thrown when there is no 'sessionId' 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("sessionId", "501");
await cookies.apply();

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

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

test("Endpoint does not return the user's password.", async () => {
// Set the cookies
const cookies = new MockNextCookies();
cookies.set("sessionId", "502");
await cookies.apply();

const user = await _trpcCaller.user();

expect(user).not.toHaveProperty("password");
});

// Delete the records created for these tests
afterAll(async () => {
await Promise.all([
prisma.user.delete({
where: { email: "[email protected]" },
}),
prisma.user.delete({
where: { email: "[email protected]" },
}),
]);
});
Loading

0 comments on commit 67a3646

Please sign in to comment.