From 700999520485a18d4a2f371d2b7f00310ca49d3a Mon Sep 17 00:00:00 2001 From: Mythie Date: Mon, 6 Jan 2025 14:44:20 +1100 Subject: [PATCH] wip --- apps/remix/app/app.css | 7 +++++ apps/remix/app/root.tsx | 4 +++ apps/remix/app/routes/home.tsx | 6 ++++ package-lock.json | 35 +++++++++++++++++++++ packages/auth/handler.ts | 47 +++++++++++++++++++++++++++++ packages/auth/index.ts | 4 +++ packages/auth/package.json | 20 ++++++++++++ packages/auth/server/error-codes.ts | 27 +++++++++++++++++ packages/auth/server/errors.ts | 42 ++++++++++++++++++++++++++ packages/auth/server/lib/session.ts | 30 ++++++++++++++++++ packages/auth/server/lib/tokens.ts | 5 +++ packages/auth/tsconfig.json | 8 +++++ 12 files changed, 235 insertions(+) create mode 100644 packages/auth/handler.ts create mode 100644 packages/auth/index.ts create mode 100644 packages/auth/package.json create mode 100644 packages/auth/server/error-codes.ts create mode 100644 packages/auth/server/errors.ts create mode 100644 packages/auth/server/lib/session.ts create mode 100644 packages/auth/server/lib/tokens.ts create mode 100644 packages/auth/tsconfig.json diff --git a/apps/remix/app/app.css b/apps/remix/app/app.css index 044c9763f..790ce4abb 100644 --- a/apps/remix/app/app.css +++ b/apps/remix/app/app.css @@ -1 +1,8 @@ @import '@documenso/ui/styles/theme.css'; + +@layer base { + :root { + --font-sans: 'Inter'; + --font-signature: 'Caveat'; + } +} diff --git a/apps/remix/app/root.tsx b/apps/remix/app/root.tsx index 4f75dcdb5..dd88143d9 100644 --- a/apps/remix/app/root.tsx +++ b/apps/remix/app/root.tsx @@ -21,6 +21,10 @@ export const links: Route.LinksFunction = () => [ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap', }, + { + rel: 'stylesheet', + href: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400..600&display=swap', + }, { rel: 'stylesheet', href: stylesheet }, ]; diff --git a/apps/remix/app/routes/home.tsx b/apps/remix/app/routes/home.tsx index bf62d13d7..1c233eef2 100644 --- a/apps/remix/app/routes/home.tsx +++ b/apps/remix/app/routes/home.tsx @@ -8,6 +8,12 @@ export function meta({}: Route.MetaArgs) { ]; } +export const loader = () => { + return { + message: 'Hello World' as const, + }; +}; + export default function Home() { return ; } diff --git a/package-lock.json b/package-lock.json index 555380b41..330130ce2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2767,6 +2767,10 @@ "resolved": "packages/assets", "link": true }, + "node_modules/@documenso/auth": { + "resolved": "packages/auth", + "link": true + }, "node_modules/@documenso/documentation": { "resolved": "apps/documentation", "link": true @@ -36162,6 +36166,37 @@ "name": "@documenso/assets", "version": "0.1.0" }, + "packages/auth": { + "name": "@documenso/auth", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@documenso/prisma": "*", + "hono": "^4.6.15", + "luxon": "^3.5.0", + "nanoid": "^4.0.2", + "ts-pattern": "^5.0.5", + "zod": "3.24.1" + } + }, + "packages/auth/node_modules/nanoid": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", + "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^14 || ^16 || >=18" + } + }, "packages/ee": { "name": "@documenso/ee", "version": "0.0.0", diff --git a/packages/auth/handler.ts b/packages/auth/handler.ts new file mode 100644 index 000000000..fbd15cb24 --- /dev/null +++ b/packages/auth/handler.ts @@ -0,0 +1,47 @@ +import { Hono } from 'hono'; +import { DateTime } from 'luxon'; + +import { prisma } from '@documenso/prisma'; + +import { AuthenticationErrorCode } from './server/error-codes'; +import { AuthenticationError } from './server/errors'; +import { getSession } from './server/lib/session'; + +export const auth = new Hono(); + +auth.get('/session', async (c) => { + const authorization = c.req.header('Authorization'); + + const userAgent = c.req.header('User-Agent'); + const ipAddress = c.req.header('X-Forwarded-For'); + + if (!authorization) { + return new AuthenticationError( + AuthenticationErrorCode.MissingToken, + 'Missing authorization header', + ).toHonoResponse(c); + } + + // Add your session validation logic here + // eslint-disable-next-line unused-imports/no-unused-vars, prefer-const + let { session, user } = await getSession(authorization); + + const diff = DateTime.fromJSDate(session.expires).diffNow('days'); + + if (diff.days <= 3) { + session = await prisma.session.update({ + where: { + id: session.id, + }, + data: { + expires: DateTime.now().plus({ days: 7 }).toJSDate(), + }, + }); + } + + return c.json({ + success: true, + session, + user, + }); +}); diff --git a/packages/auth/index.ts b/packages/auth/index.ts new file mode 100644 index 000000000..88265f682 --- /dev/null +++ b/packages/auth/index.ts @@ -0,0 +1,4 @@ +export * from './handler'; +export * from './server/errors'; +export * from './server/error-codes'; +export * from './server/middleware'; diff --git a/packages/auth/package.json b/packages/auth/package.json new file mode 100644 index 000000000..a513b046e --- /dev/null +++ b/packages/auth/package.json @@ -0,0 +1,20 @@ +{ + "name": "@documenso/auth", + "version": "0.0.0", + "main": "./index.ts", + "types": "./index.ts", + "license": "MIT", + "scripts": { + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "clean": "rimraf node_modules" + }, + "dependencies": { + "@documenso/prisma": "*", + "hono": "^4.6.15", + "luxon": "^3.5.0", + "nanoid": "^4.0.2", + "ts-pattern": "^5.0.5", + "zod": "3.24.1" + } +} \ No newline at end of file diff --git a/packages/auth/server/error-codes.ts b/packages/auth/server/error-codes.ts new file mode 100644 index 000000000..86cbcf90b --- /dev/null +++ b/packages/auth/server/error-codes.ts @@ -0,0 +1,27 @@ +import type { ContentfulStatusCode } from 'hono/utils/http-status'; + +export const AuthenticationErrorCode = { + Unauthorized: 'UNAUTHORIZED', + InvalidCredentials: 'INVALID_CREDENTIALS', + SessionNotFound: 'SESSION_NOT_FOUND', + SessionExpired: 'SESSION_EXPIRED', + InvalidToken: 'INVALID_TOKEN', + MissingToken: 'MISSING_TOKEN', +} as const; + +export type AuthenticationErrorCode = + // eslint-disable-next-line @typescript-eslint/ban-types + (typeof AuthenticationErrorCode)[keyof typeof AuthenticationErrorCode] | (string & {}); + +export const ErrorStatusMap: Record = { + [AuthenticationErrorCode.Unauthorized]: 401, + [AuthenticationErrorCode.InvalidCredentials]: 401, + [AuthenticationErrorCode.SessionNotFound]: 401, + [AuthenticationErrorCode.SessionExpired]: 401, + [AuthenticationErrorCode.InvalidToken]: 401, + [AuthenticationErrorCode.MissingToken]: 400, +}; + +export function getErrorStatus(code: AuthenticationErrorCode) { + return ErrorStatusMap[code] ?? 400; +} diff --git a/packages/auth/server/errors.ts b/packages/auth/server/errors.ts new file mode 100644 index 000000000..4b04fda70 --- /dev/null +++ b/packages/auth/server/errors.ts @@ -0,0 +1,42 @@ +import type { Context } from 'hono'; +import type { ContentfulStatusCode } from 'hono/utils/http-status'; + +import type { AuthenticationErrorCode } from './error-codes'; +import { getErrorStatus } from './error-codes'; + +interface ErrorResponse { + error: string; + message: string; + stack?: string; +} + +export class AuthenticationError extends Error { + code: AuthenticationErrorCode; + statusCode: ContentfulStatusCode; + + constructor(code: AuthenticationErrorCode, message?: string, statusCode?: ContentfulStatusCode) { + super(message); + this.code = code; + this.name = 'AuthenticationError'; + // Use provided status code or look it up from the map + this.statusCode = statusCode ?? getErrorStatus(code); + } + + toJSON(): ErrorResponse { + return { + error: this.code, + message: this.message, + ...(process.env.NODE_ENV === 'development' && { stack: this.stack }), + }; + } + + toHonoResponse(c: Context) { + return c.json( + { + success: false, + ...this.toJSON(), + }, + this.statusCode, + ); + } +} diff --git a/packages/auth/server/lib/session.ts b/packages/auth/server/lib/session.ts new file mode 100644 index 000000000..96fec797f --- /dev/null +++ b/packages/auth/server/lib/session.ts @@ -0,0 +1,30 @@ +import { prisma } from '@documenso/prisma'; + +import { AuthenticationErrorCode } from '../error-codes'; +import { AuthenticationError } from '../errors'; + +export const getSession = async (token: string) => { + const result = await prisma.session.findUnique({ + where: { + sessionToken: token, + }, + include: { + user: true, + }, + }); + + if (!result) { + throw new AuthenticationError(AuthenticationErrorCode.SessionNotFound); + } + + if (result.expires < new Date()) { + throw new AuthenticationError(AuthenticationErrorCode.SessionExpired); + } + + const { user, ...session } = result; + + return { + session, + user, + }; +}; diff --git a/packages/auth/server/lib/tokens.ts b/packages/auth/server/lib/tokens.ts new file mode 100644 index 000000000..833471485 --- /dev/null +++ b/packages/auth/server/lib/tokens.ts @@ -0,0 +1,5 @@ +import { customAlphabet } from 'nanoid'; + +const sessionTokenId = customAlphabet('abcdefhiklmnorstuvwxz', 10); + +export const createSessionToken = (length = 10) => `session_${sessionTokenId(length)}` as const; diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json new file mode 100644 index 000000000..dc21318a7 --- /dev/null +++ b/packages/auth/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@documenso/tsconfig/react-library.json", + "include": ["."], + "exclude": ["dist", "build", "node_modules"], + "compilerOptions": { + "strict": true, + } +}