From b75a147ffa18a6d6d62e26ae6edb6cc8ab2e2642 Mon Sep 17 00:00:00 2001 From: Phillip Carter Date: Fri, 20 Mar 2026 17:10:27 -0700 Subject: [PATCH] layout and auth --- package-lock.json | 103 ++++++++++++++++++++++++ package.json | 1 + src/app/api/auth/[...nextauth]/route.ts | 3 + src/app/email/[id]/page.tsx | 83 +++++++++---------- src/app/globals.css | 20 ++--- src/app/layout.tsx | 36 +++++++-- src/app/login/page.tsx | 24 ++++++ src/app/page.tsx | 43 ++++++---- src/auth.ts | 16 ++++ src/components/EmailBody.tsx | 26 +++++- src/proxy.ts | 5 ++ 11 files changed, 277 insertions(+), 83 deletions(-) create mode 100644 src/app/api/auth/[...nextauth]/route.ts create mode 100644 src/app/login/page.tsx create mode 100644 src/auth.ts create mode 100644 src/proxy.ts diff --git a/package-lock.json b/package-lock.json index 19a1baf..6649804 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "dompurify": "^3.3.3", "marked": "^17.0.5", "next": "16.2.0", + "next-auth": "^5.0.0-beta.30", "react": "19.2.4", "react-dom": "19.2.4" }, @@ -39,6 +40,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@auth/core": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.0.tgz", + "integrity": "sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ==", + "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": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -1229,6 +1259,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/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -4462,6 +4501,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "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/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5076,6 +5124,33 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.30", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.30.tgz", + "integrity": "sha512-+c51gquM3F6nMVmoAusRJ7RIoY0K4Ts9HCCwyy/BRoe4mp3msZpOzYMyb5LAYc1wSo74PMQkGDcaghIO7W6Xjg==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.0" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "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", @@ -5130,6 +5205,15 @@ "dev": true, "license": "MIT" }, + "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/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5419,6 +5503,25 @@ "node": "^10 || ^12 || >=14" } }, + "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": "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/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index 88c5eba..f62685b 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dompurify": "^3.3.3", "marked": "^17.0.5", "next": "16.2.0", + "next-auth": "^5.0.0-beta.30", "react": "19.2.4", "react-dom": "19.2.4" }, diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..86c9f3d --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/auth"; + +export const { GET, POST } = handlers; diff --git a/src/app/email/[id]/page.tsx b/src/app/email/[id]/page.tsx index 4a2176c..ad73cbd 100644 --- a/src/app/email/[id]/page.tsx +++ b/src/app/email/[id]/page.tsx @@ -18,7 +18,6 @@ export default async function EmailPage({ params }: Props) { if (!email) return notFound(); - // Prefer HTML body, fall back to text let body: string | null = null; let bodyType: "html" | "text" = "text"; @@ -39,50 +38,52 @@ export default async function EmailPage({ params }: Props) { } return ( -
- {/* Back */} - - ← Inbox - +
+ {/* Toolbar */} +
+ + ← Inbox + +
- {/* Subject */} -

- {email.subject || "(no subject)"} -

+
+ {/* Subject */} +

+ {email.subject || "(no subject)"} +

- {/* Headers */} -
-
- From - {formatAddressList(email.from)} -
- {email.to && email.to.length > 0 && ( -
- To - {formatAddressList(email.to)} -
- )} - {email.cc && email.cc.length > 0 && ( -
- Cc - {formatAddressList(email.cc)} -
+ {/* Header fields — grid keeps labels and values aligned */} +
+
From
+
{formatAddressList(email.from)}
+ + {email.to && email.to.length > 0 && ( + <> +
To
+
{formatAddressList(email.to)}
+ + )} + {email.cc && email.cc.length > 0 && ( + <> +
Cc
+
{formatAddressList(email.cc)}
+ + )} +
Date
+
{formatFullDate(email.receivedAt)}
+
+ + {/* Body */} + {body ? ( + + ) : ( +

No body content.

)} -
- Date - {formatFullDate(email.receivedAt)} -
- - {/* Body */} - {body ? ( - - ) : ( -

No body content.

- )}
); } diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..12148d1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,16 @@ @import "tailwindcss"; -:root { - --background: #ffffff; - --foreground: #171717; -} - @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } +/* Force light mode — email iframes are always light, dark mode support can come later */ +:root { + color-scheme: light; } body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + background: #ffffff; + color: #111827; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1001a6e..ade728b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import Link from "next/link"; +import { auth, signOut } from "@/auth"; import "./globals.css"; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }); @@ -14,38 +15,59 @@ export const metadata: Metadata = { description: "Personal email client", }; -export default function RootLayout({ +export default async function RootLayout({ children, }: { children: React.ReactNode; }) { + const session = await auth(); + return ( - + {/* Sidebar */} -