diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..e3ce7db
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,10 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(npx next:*)",
+ "Bash(npm run:*)",
+ "Bash(node -e \"console.log\\(require\\('eslint-config-next/package.json'\\).version\\)\")",
+ "Bash(npx tsc:*)"
+ ]
+ }
+}
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..6918b05
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,93 @@
+# Design System Document: High-End Editorial Job Board
+
+## 1. Overview & Creative North Star
+**Creative North Star: "The Architectural Curator"**
+
+This design system rejects the "commodity" look of traditional job boards. It is built on the principle of **Architectural Curation**—treating job listings not as database entries, but as editorial features. By utilizing high-contrast typography, intentional asymmetry, and deep tonal layering, we create a workspace that feels authoritative, premium, and calm.
+
+The system breaks the "template" aesthetic by favoring breathing room over density and using light as a functional tool rather than just an aesthetic choice. It is designed for the high-performing professional who values efficiency through visual clarity.
+
+---
+
+## 2. Colors
+Our palette is anchored in deep obsidian tones, punctuated by a high-energy primary yellow that acts as a beacon for action.
+
+* **Primary Palette:**
+ * `primary`: #ffdd73 (High-energy yellow for core actions)
+ * `on-primary`: #624e00 (Deep contrast for legibility on yellow)
+ * `background`: #0e0e0e (Deep charcoal/black foundation)
+* **Surface Hierarchy:**
+ * `surface-container-low`: #131313
+ * `surface-container`: #1a1a1a
+ * `surface-container-high`: #20201f
+* **Status Indicators:**
+ * `Applied`: Blue (via `tertiary` logic)
+ * `Accepted`: Green
+ * `Error`: #ff7351 (`error`)
+
+### The "No-Line" Rule
+Standard 1px borders are strictly prohibited for sectioning. Structural boundaries must be achieved through **Background Color Shifts**. For example, a `surface-container-high` card sits on a `surface-container-low` background. This creates a "shadow-less" depth that feels modern and integrated.
+
+### The "Glass & Gradient" Rule
+To elevate the experience, floating headers or navigation bars should use **Glassmorphism**. Use semi-transparent surface colors with a `backdrop-blur(20px)`. Main CTAs should utilize a subtle linear gradient from `primary` to `primary-dim` to provide a tactile, "lit-from-within" feel.
+
+---
+
+## 3. Typography
+We use a dual-typeface system to balance editorial authority with functional utility.
+
+* **Display & Headlines (Manrope):** These are the "Editorial" voice. Use `display-lg` (3.5rem) for hero sections and `headline-md` (1.75rem) for section titles. Bold weights are mandatory for headers to maintain the high-contrast signature look.
+* **Body & Labels (Inter):** These are the "Functional" voice. `body-md` (0.875rem) is the workhorse for job descriptions. Inter’s high x-height ensures maximum legibility against dark backgrounds.
+* **Hierarchy as Identity:** Use `title-lg` (1.375rem) for job titles in lists. The scale jump between headlines and body text is intentional—it mimics luxury magazine layouts, guiding the eye to the most critical information first.
+
+---
+
+## 4. Elevation & Depth
+Depth in this system is a result of light physics, not artificial decoration.
+
+* **Tonal Layering:** Always stack from darkest to lightest.
+ * *Level 0:* `surface` (Main background)
+ * *Level 1:* `surface-container-low` (Content sections)
+ * *Level 2:* `surface-container-highest` (Interactive cards/modals)
+* **Ambient Shadows:** If an element must "float" (like a Toast or Floating Action Button), use a shadow with a blur radius of at least `24px` and an opacity no higher than `8%`. Use a tinted shadow (blending `on-surface` with the background) to avoid a "dirty" grey appearance.
+* **The "Ghost Border" Fallback:** If high-density data requires containment, use a **Ghost Border**. Apply `outline-variant` at 15% opacity. It should be felt, not seen.
+
+---
+
+## 5. Components
+
+### Buttons
+* **Primary:** Solid `primary` fill, `on-primary` text. Radius: `md` (0.75rem). Bold `label-md` text.
+* **Secondary:** Ghost style. `outline` border (20% opacity) with `on-surface` text.
+* **Glass Action:** Semi-transparent `surface-bright` with backdrop blur for secondary utility actions.
+
+### Chips (Status & Tags)
+* **Status Chips:** Use a muted version of the status color for the background (e.g., 20% opacity) with a high-contrast and solid text.
+* **Filter Chips:** Use `surface-variant` with a radius of `full` (9999px) for a soft, pill-shaped aesthetic.
+
+### Cards & Lists
+* **Rule:** Forbid the use of divider lines.
+* **Separation:** Use `spacing-8` (2rem) of vertical white space or shift the `surface-container` tier.
+* **Interactive Cards:** On hover, a card should transition from `surface-container-low` to `surface-container-high`.
+
+### Input Fields
+* **Style:** Minimalist. No bottom border. Use a `surface-container-highest` background with a subtle radius of `sm` (0.25rem).
+* **Active State:** The border should only appear on focus, using a 1px `primary` line.
+
+### Job Specific: "Quick-Action" Bar
+A specialized component for the job board—a sticky footer or side-rail for "Apply Now" and "Save" actions using the **Glassmorphism** rule to keep the user grounded in the content while providing immediate utility.
+
+---
+
+## 6. Do's and Don'ts
+
+### Do:
+* **Do** use asymmetrical spacing to create visual interest (e.g., more padding on the left than the right in editorial headers).
+* **Do** use `primary` yellow sparingly. It is a "laser pointer," not a bucket of paint.
+* **Do** ensure all text on dark backgrounds meets a minimum 4.5:1 contrast ratio for accessibility.
+
+### Don't:
+* **Don't** use 100% opaque, high-contrast borders. They "trap" the content and break the fluid editorial feel.
+* **Don't** use standard "Drop Shadows" (0, 2, 4, 0). They look dated and cheap.
+* **Don't** use dividers between list items. Trust the white space and background shifts to do the work.
+* **Don't** mix the font roles. Never use Manrope for body copy; it’s too "loud" for long-form reading.
\ No newline at end of file
diff --git a/frontend/.env.example b/frontend/.env.example
new file mode 100644
index 0000000..6403b41
--- /dev/null
+++ b/frontend/.env.example
@@ -0,0 +1,11 @@
+MONGODB_URI=
+MONGODB_DATABASE=default
+
+# Auth.js (NextAuth)
+NEXTAUTH_SECRET=
+NEXTAUTH_URL=http://localhost:3000
+
+# OAuth providers
+GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_SECRET=
+
diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs
index c85fb67..f5300a0 100644
--- a/frontend/eslint.config.mjs
+++ b/frontend/eslint.config.mjs
@@ -1,16 +1,6 @@
-import { dirname } from "path";
-import { fileURLToPath } from "url";
-import { FlatCompat } from "@eslint/eslintrc";
+import coreWebVitals from "eslint-config-next/core-web-vitals";
+import typescript from "eslint-config-next/typescript";
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-const compat = new FlatCompat({
- baseDirectory: __dirname,
-});
-
-const eslintConfig = [
- ...compat.extends("next/core-web-vitals", "next/typescript"),
-];
+const eslintConfig = [...coreWebVitals, ...typescript];
export default eslintConfig;
diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts
index 9edff1c..c4b7818 100644
--- a/frontend/next-env.d.ts
+++ b/frontend/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/types/routes.d.ts";
+import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/frontend/next.config.ts b/frontend/next.config.ts
index 68a6c64..5be3493 100644
--- a/frontend/next.config.ts
+++ b/frontend/next.config.ts
@@ -1,7 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- output: "standalone",
+ serverExternalPackages: ["mongodb", "pino", "pino-pretty"],
};
export default nextConfig;
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index c25d7e9..4af8192 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,6 +8,7 @@
"name": "frontend",
"version": "0.1.0",
"dependencies": {
+ "@auth/mongodb-adapter": "^3.11.1",
"@mantine/core": "^7.17.0",
"@mantine/hooks": "^7.16.1",
"@mantine/notifications": "^7.17.1",
@@ -16,12 +17,14 @@
"@tailwindcss/typography": "^0.5.16",
"@vercel/analytics": "^1.5.0",
"@vercel/speed-insights": "^1.2.0",
+ "bcryptjs": "^3.0.3",
"dompurify": "^3.2.3",
"isomorphic-dompurify": "^2.22.0",
"jsdom": "^26.0.0",
"lru-cache": "^11.2.2",
"mongodb": "^6.14.2",
"next": "^16.2.3",
+ "next-auth": "^4.24.13",
"pino": "^9.11.0",
"pino-pretty": "^13.1.1",
"react": "^19.0.0",
@@ -75,6 +78,74 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
+ "node_modules/@auth/mongodb-adapter": {
+ "version": "3.11.1",
+ "resolved": "https://registry.npmjs.org/@auth/mongodb-adapter/-/mongodb-adapter-3.11.1.tgz",
+ "integrity": "sha512-xY+VUkC3CNXct8UwQgBAQqXASqolSlIARg6oAm1378CtRN2650tQUCOEnGLNLmroVefUeP73M6t+TpGXq72vwQ==",
+ "license": "ISC",
+ "dependencies": {
+ "@auth/core": "0.41.1"
+ },
+ "peerDependencies": {
+ "mongodb": "^6"
+ }
+ },
+ "node_modules/@auth/mongodb-adapter/node_modules/@auth/core": {
+ "version": "0.41.1",
+ "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.1.tgz",
+ "integrity": "sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw==",
+ "license": "ISC",
+ "dependencies": {
+ "@panva/hkdf": "^1.2.1",
+ "jose": "^6.0.6",
+ "oauth4webapi": "^3.3.0",
+ "preact": "10.24.3",
+ "preact-render-to-string": "6.5.11"
+ },
+ "peerDependencies": {
+ "@simplewebauthn/browser": "^9.0.1",
+ "@simplewebauthn/server": "^9.0.2",
+ "nodemailer": "^7.0.7"
+ },
+ "peerDependenciesMeta": {
+ "@simplewebauthn/browser": {
+ "optional": true
+ },
+ "@simplewebauthn/server": {
+ "optional": true
+ },
+ "nodemailer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@auth/mongodb-adapter/node_modules/jose": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
+ "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/@auth/mongodb-adapter/node_modules/oauth4webapi": {
+ "version": "3.8.5",
+ "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.5.tgz",
+ "integrity": "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/@auth/mongodb-adapter/node_modules/preact-render-to-string": {
+ "version": "6.5.11",
+ "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz",
+ "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "preact": ">=10"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -840,9 +911,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -859,9 +927,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -878,9 +943,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -897,9 +959,6 @@
"cpu": [
"riscv64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -916,9 +975,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -935,9 +991,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -954,9 +1007,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -973,9 +1023,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -992,9 +1039,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1017,9 +1061,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1042,9 +1083,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1067,9 +1105,6 @@
"cpu": [
"riscv64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1092,9 +1127,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1117,9 +1149,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1142,9 +1171,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1167,9 +1193,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1441,9 +1464,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1460,9 +1480,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1479,9 +1496,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1498,9 +1512,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1600,6 +1611,15 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@panva/hkdf": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
+ "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -2472,6 +2492,15 @@
"node": ">=6.0.0"
}
},
+ "node_modules/bcryptjs": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
+ "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
+ "license": "BSD-3-Clause",
+ "bin": {
+ "bcrypt": "bin/bcrypt"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -2766,6 +2795,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4936,6 +4974,15 @@
"jiti": "bin/jiti.js"
}
},
+ "node_modules/jose": {
+ "version": "4.15.9",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
+ "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
@@ -5425,6 +5472,38 @@
}
}
},
+ "node_modules/next-auth": {
+ "version": "4.24.13",
+ "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.13.tgz",
+ "integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==",
+ "license": "ISC",
+ "dependencies": {
+ "@babel/runtime": "^7.20.13",
+ "@panva/hkdf": "^1.0.2",
+ "cookie": "^0.7.0",
+ "jose": "^4.15.5",
+ "oauth": "^0.9.15",
+ "openid-client": "^5.4.0",
+ "preact": "^10.6.3",
+ "preact-render-to-string": "^5.1.19",
+ "uuid": "^8.3.2"
+ },
+ "peerDependencies": {
+ "@auth/core": "0.34.3",
+ "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16",
+ "nodemailer": "^7.0.7",
+ "react": "^17.0.2 || ^18 || ^19",
+ "react-dom": "^17.0.2 || ^18 || ^19"
+ },
+ "peerDependenciesMeta": {
+ "@auth/core": {
+ "optional": true
+ },
+ "nodemailer": {
+ "optional": true
+ }
+ }
+ },
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -5475,6 +5554,12 @@
"integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==",
"license": "MIT"
},
+ "node_modules/oauth": {
+ "version": "0.9.15",
+ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
+ "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
+ "license": "MIT"
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5605,6 +5690,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/oidc-token-hash": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz",
+ "integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10.13.0 || >=12.0.0"
+ }
+ },
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
@@ -5623,6 +5717,48 @@
"wrappy": "1"
}
},
+ "node_modules/openid-client": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
+ "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
+ "license": "MIT",
+ "dependencies": {
+ "jose": "^4.15.9",
+ "lru-cache": "^6.0.0",
+ "object-hash": "^2.2.0",
+ "oidc-token-hash": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/openid-client/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/openid-client/node_modules/object-hash": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
+ "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/openid-client/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC"
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -6085,6 +6221,28 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
+ "node_modules/preact": {
+ "version": "10.24.3",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
+ "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
+ "node_modules/preact-render-to-string": {
+ "version": "5.2.6",
+ "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
+ "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
+ "license": "MIT",
+ "dependencies": {
+ "pretty-format": "^3.8.0"
+ },
+ "peerDependencies": {
+ "preact": ">=10"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -6111,6 +6269,12 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
+ "node_modules/pretty-format": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
+ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
+ "license": "MIT"
+ },
"node_modules/process-warning": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
@@ -7742,6 +7906,15 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index f07d174..5887bed 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -6,10 +6,11 @@
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
- "lint": "next lint",
+ "lint": "eslint .",
"format": "npx prettier . --write"
},
"dependencies": {
+ "@auth/mongodb-adapter": "^3.11.1",
"@mantine/core": "^7.17.0",
"@mantine/hooks": "^7.16.1",
"@mantine/notifications": "^7.17.1",
@@ -18,12 +19,14 @@
"@tailwindcss/typography": "^0.5.16",
"@vercel/analytics": "^1.5.0",
"@vercel/speed-insights": "^1.2.0",
+ "bcryptjs": "^3.0.3",
"dompurify": "^3.2.3",
"isomorphic-dompurify": "^2.22.0",
"jsdom": "^26.0.0",
"lru-cache": "^11.2.2",
"mongodb": "^6.14.2",
"next": "^16.2.3",
+ "next-auth": "^4.24.13",
"pino": "^9.11.0",
"pino-pretty": "^13.1.1",
"react": "^19.0.0",
diff --git a/frontend/src/app/api/auth/[...nextauth]/route.ts b/frontend/src/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000..6d4ddf6
--- /dev/null
+++ b/frontend/src/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,7 @@
+import NextAuth from "next-auth";
+import { authOptions } from "@/lib/auth";
+
+const handler = NextAuth(authOptions);
+
+export { handler as GET, handler as POST };
+
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
index c40b579..fa79548 100644
--- a/frontend/src/app/globals.css
+++ b/frontend/src/app/globals.css
@@ -39,8 +39,7 @@
}
html,
-body,
-main {
+body {
height: 100svh;
margin: 0;
padding: 0;
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index dbaf664..eec2cb2 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -22,6 +22,7 @@ import FeedbackButton from "@/components/ui/feedback-button";
import { Notifications } from "@mantine/notifications";
import FirstVisitNotification from "@/components/ui/first-visit-notification";
+import AuthSessionProvider from "@/components/auth/session-provider";
export const metadata: Metadata = {
title: {
@@ -53,22 +54,24 @@ export default function RootLayout({ children }: PropsWithChildren) {
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+