Skip to content

Commit

Permalink
Writing session cookies (#15)
Browse files Browse the repository at this point in the history
* wip

Co-authored-by: DamianUduevbo <[email protected]>

* post merge

* lin:ws

* remove sessions from select

* reorder

* clean up some tests

* mock

* fixes

* add cookies to tests

* add mockClear between tests

* woo

* fix

* cookies tests

* one more test

* middleware tests

* ensure seed is working

* remove todo

* clean up test

* userWithSesion -> userWithSession

---------

Co-authored-by: DamianUduevbo <[email protected]>
Co-authored-by: DamianUduevbo <[email protected]>
  • Loading branch information
3 people authored Oct 29, 2024
1 parent 67a3646 commit 9265908
Show file tree
Hide file tree
Showing 25 changed files with 650 additions and 247 deletions.
3 changes: 3 additions & 0 deletions apps/db/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"down": "docker-compose -f compose.yml down",
"clean": "rm -rf .turbo node_modules",
"format": " prisma format && prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit --emitDeclarationOnly false",
"push": "prisma db push",
"studio": "prisma studio",
"generate": "prisma generate",
Expand All @@ -26,6 +28,7 @@
"@prisma/client": "5.19.1"
},
"devDependencies": {
"@good-dog/auth": "workspace:*",
"@good-dog/eslint": "workspace:*",
"@good-dog/prettier": "workspace:*",
"@good-dog/typescript": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "name" SET NOT NULL;
16 changes: 8 additions & 8 deletions apps/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ datasource db {
}

model User {
id String @id @default(uuid())
email String @unique
name String?
password String
sessions Session[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(uuid())
email String @unique
name String
hashedPassword String @map("password")
sessions Session[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Session {
id String @id @default(uuid())
id String @id @default(uuid())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
createdAt DateTime @default(now())
Expand Down
152 changes: 91 additions & 61 deletions apps/db/prisma/seed.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,119 @@
import { PrismaClient } from "@prisma/client";

import { hashPassword } from "@good-dog/auth/password";

const prisma = new PrismaClient();
async function main() {
const alice = await prisma.user.upsert({
// Alice has a session that expires in the future
await prisma.user.upsert({
where: { email: "[email protected]" },
update: {},
update: {
sessions: {
update: {
where: { id: "23" },
data: {
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
},
},
},
},
create: {
id: "7",
email: "[email protected]",
name: "Alice",
password: "alicePasswod",
},
});
const aliceSession = await prisma.session.upsert({
where: { id: "23" },
update: {
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
},
create: {
userId: alice.id,
id: "23",
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
hashedPassword: await hashPassword("alicePassword"),
sessions: {
create: {
id: "23",
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
},
},
},
});

const bob = await prisma.user.upsert({
// Bob has a session that expired in the past and another that expires in the future
await prisma.user.upsert({
where: { email: "[email protected]" },
update: {},
update: {
sessions: {
updateMany: [
{
where: { id: "12" },
data: {
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() - 1),
),
},
},
{
where: { id: "45" },
data: {
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
},
},
],
},
},
create: {
id: "9",
email: "[email protected]",
name: "Bob Jones",
password: "bobPassword",
},
});
const bobSession1 = await prisma.session.upsert({
where: { id: "12" },
update: {
expiresAt: new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
},
create: {
userId: bob.id,
id: "12",
expiresAt: new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
},
});
const bobSession2 = await prisma.session.upsert({
where: { id: "45" },
update: {
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
},
create: {
userId: bob.id,
id: "45",
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
hashedPassword: await hashPassword("bobPassword"),
sessions: {
createMany: {
data: [
{
id: "12",
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() - 1),
),
},
{
id: "45",
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() + 10),
),
},
],
},
},
},
});

const eve = await prisma.user.upsert({
// Eve has a session that expired in the past
await prisma.user.upsert({
where: { email: "[email protected]" },
update: {},
update: {
sessions: {
update: {
where: { id: "78" },
data: {
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() - 1),
),
},
},
},
},
create: {
id: "56",
email: "[email protected]",
name: "Eve Smith",
password: "evePassword",
},
});
const eveSession = await prisma.session.upsert({
where: { id: "78" },
update: {
expiresAt: new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
},
create: {
userId: eve.id,
id: "78",
expiresAt: new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
hashedPassword: await hashPassword("evePassword"),
sessions: {
create: {
id: "78",
expiresAt: new Date(
new Date().setFullYear(new Date().getFullYear() - 1),
),
},
},
},
});
}
Expand Down
2 changes: 1 addition & 1 deletion apps/db/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"include": ["*.ts", "src", "prisma/seed.ts"],
"exclude": ["node_modules"]
}
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"dependencies": {
"@good-dog/tailwind": "workspace:*",
"@t3-oss/env-nextjs": "^0.10.1",
"next": "^14.2.12",
"next": "14.2.12",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"server-only": "^0.0.1",
Expand Down
Binary file modified bun.lockb
Binary file not shown.
6 changes: 4 additions & 2 deletions packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"version": "0.1.0",
"type": "module",
"exports": {
".": "./src/index.ts"
"./password": "./src/password.ts",
"./cookies": "./src/cookies.ts"
},
"license": "MIT",
"scripts": {
Expand All @@ -24,6 +25,7 @@
},
"prettier": "@good-dog/prettier",
"dependencies": {
"bcryptjs": "^2.4.3"
"bcryptjs": "^2.4.3",
"next": "14.2.12"
}
}
21 changes: 21 additions & 0 deletions packages/auth/src/cookies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { cookies } from "next/headers";

export const SESSION_COOKIE_NAME = "sessionId";

export const getSessionCookie = () => {
return cookies().get(SESSION_COOKIE_NAME);
};

export const setSessionCookie = (sessionId: string, expires: Date) => {
cookies().set(SESSION_COOKIE_NAME, sessionId, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
expires,
});
};

export const deleteSessionCookie = () => {
cookies().delete(SESSION_COOKIE_NAME);
};
File renamed without changes.
6 changes: 3 additions & 3 deletions packages/trpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,15 @@ To add a new procedure, follow these steps:

### Unit Testing

You can write unit tests for your tRPC procedures using the `_trpcCaller` export. Here is an example test for the `hello` procedure:
You can write unit tests for your tRPC procedures using the `$trpcCaller` export. Here is an example test for the `hello` procedure:

```ts
import { expect, test } from "bun:test";

import { _trpcCaller } from "@good-dog/trpc/server";
import { $trpcCaller } from "@good-dog/trpc/server";

test("hello world", async () => {
const result = await _trpcCaller.hello({ text: "world" });
const result = await $trpcCaller.hello({ text: "world" });
expect(result.greeting).toEqual("hello world");
});
```
1 change: 0 additions & 1 deletion packages/trpc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
"@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
49 changes: 46 additions & 3 deletions packages/trpc/src/internal/init.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from "react";
import { cookies } from "next/headers";
import { initTRPC, TRPCError } from "@trpc/server";
import SuperJSON from "superjson";
import { ZodError } from "zod";

import { getSessionCookie } from "@good-dog/auth/cookies";
import { prisma } from "@good-dog/db";

export const createTRPCContext = React.cache(() => {
Expand All @@ -23,6 +24,23 @@ const t = initTRPC.context<ReturnType<typeof createTRPCContext>>().create({
* @see https://trpc.io/docs/server/data-transformers
*/
transformer: SuperJSON,
errorFormatter: ({ shape, error }) => ({
...shape,
data: {
...shape.data,
zodError:
error.code === "BAD_REQUEST" && error.cause instanceof ZodError
? error.cause.flatten()
: null,
prismaError:
process.env.VERCEL_ENV !== "production" &&
error.code === "INTERNAL_SERVER_ERROR" &&
error.cause &&
"clientVersion" in error.cause
? error.cause
: null,
},
}),
});

// Base router and procedure helpers
Expand All @@ -33,7 +51,7 @@ export const createCallerFactory = t.createCallerFactory;
export const baseProcedureBuilder = t.procedure;
export const authenticatedProcedureBuilder = baseProcedureBuilder.use(
async ({ ctx, next }) => {
const sessionId = cookies().get("sessionId");
const sessionId = getSessionCookie();

if (!sessionId?.value) {
throw new TRPCError({ code: "UNAUTHORIZED" });
Expand Down Expand Up @@ -65,8 +83,33 @@ export const authenticatedProcedureBuilder = baseProcedureBuilder.use(
return next({
ctx: {
...ctx,
user: sessionOrNull.user,
session: sessionOrNull,
},
});
},
);

// This middleware is used to prevent authenticated users from accessing a resource
export const notAuthenticatedProcedureBuilder = baseProcedureBuilder.use(
async ({ ctx, next }) => {
const sessionId = getSessionCookie();

if (!sessionId?.value) {
return next({ ctx });
}

const sessionOrNull = await ctx.prisma.session.findUnique({
where: {
id: sessionId.value,
},
});

if (sessionOrNull && sessionOrNull.expiresAt > new Date()) {
throw new TRPCError({ code: "FORBIDDEN" });
}

return next({
ctx,
});
},
);
Loading

0 comments on commit 9265908

Please sign in to comment.