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,
+ }
+}