Complete Better Auth migration, harden security, update all docs

Console:
- Rewrite auth from JWT decoding to Better Auth useSession()
- Add auth-client.ts with createAuthClient
- Remove elydora_token localStorage, dead auth API functions
- Fix login/register pages to use signIn.email/signUp.email

Server:
- Upgrade better-auth ^1.2.0 → ^1.5.5
- CORS: enforce allowlist on all environments
- Sync auth service and middleware with backend

SDKs:
- Deprecate login/register in Node, Python, Go SDKs
- Update READMEs to show direct token initialization

Config:
- Sanitize .env with dev-only placeholder values
- Update helm chart and install scripts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-25 00:48:57 -04:00
1 parent e8fa5e557e
commit 27a8f0667c
39 files changed
+1464 -643

No files matched your search

+363
View File
@@ -8,6 +8,7 @@
"name": "elydora-console",
"version": "0.1.0",
"dependencies": {
"better-auth": "^1.5.5",
"i18next": "^25.8.13",
"i18next-browser-languagedetector": "^8.2.1",
"next": "^16.1.6",
@@ -48,6 +49,134 @@
"node": ">=6.9.0"
}
},
"node_modules/@better-auth/core": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.5.6.tgz",
"integrity": "sha512-Ez9DZdIMFyxHremmoLz1emFPGNQomDC1jqqBPnZ6Ci+6TiGN3R9w/Y03cJn6I8r1ycKgOzeVMZtJ/erOZ27Gsw==",
"license": "MIT",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.39.0",
"@standard-schema/spec": "^1.1.0",
"zod": "^4.3.6"
},
"peerDependencies": {
"@better-auth/utils": "0.3.1",
"@better-fetch/fetch": "1.1.21",
"@cloudflare/workers-types": ">=4",
"@opentelemetry/api": "^1.9.0",
"better-call": "1.3.2",
"jose": "^6.1.0",
"kysely": "^0.28.5",
"nanostores": "^1.0.1"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"optional": true
}
}
},
"node_modules/@better-auth/drizzle-adapter": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.5.6.tgz",
"integrity": "sha512-VfFFmaoFw3ug12SiSuIwzrMoHyIVmkMGWm9gZ4sXdYYVX4HboCL4m3fjzOhppcmK5OGatRuU+N1UX6wxCITcXw==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0",
"drizzle-orm": ">=0.41.0"
},
"peerDependenciesMeta": {
"drizzle-orm": {
"optional": true
}
}
},
"node_modules/@better-auth/kysely-adapter": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.5.6.tgz",
"integrity": "sha512-Fnf+h8WVKtw6lEOmVmiVVzDf3shJtM60AYf9XTnbdCeUd6MxN/KnaJZpkgtYnRs7a+nwtkVB+fg4lGETebGFXQ==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0",
"kysely": "^0.27.0 || ^0.28.0"
},
"peerDependenciesMeta": {
"kysely": {
"optional": true
}
}
},
"node_modules/@better-auth/memory-adapter": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.5.6.tgz",
"integrity": "sha512-rS7ZsrIl5uvloUgNN0u9LOZJMMXnsZXVdUZ3MrTBKWM2KpoJjzPr9yN3Szyma5+0V7SltnzSGHPkYj2bEzzmlA==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0"
}
},
"node_modules/@better-auth/mongo-adapter": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.5.6.tgz",
"integrity": "sha512-6+M3MS2mor8fTUV3EI1FBLP0cs6QfbN+Ovx9+XxR/GdfKIBoNFzmPEPRbdGt+ft6PvrITsUm+T70+kkHgVSP6w==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0",
"mongodb": "^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"mongodb": {
"optional": true
}
}
},
"node_modules/@better-auth/prisma-adapter": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.5.6.tgz",
"integrity": "sha512-UxY9vQJs1Tt+O+T2YQnseDMlWmUSQvFZSBb5YiFRg7zcm+TEzujh4iX2/csA0YiZptLheovIuVWTP9nriewEBA==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0",
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
"prisma": "^5.0.0 || ^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"@prisma/client": {
"optional": true
},
"prisma": {
"optional": true
}
}
},
"node_modules/@better-auth/telemetry": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.5.6.tgz",
"integrity": "sha512-yXC7NSxnIFlxDkGdpD7KA+J9nqIQAPCJKe77GoaC5bWoe/DALo1MYorZfTgOafS7wrslNtsPT4feV/LJi1ubqQ==",
"license": "MIT",
"dependencies": {
"@better-auth/utils": "0.3.1",
"@better-fetch/fetch": "1.1.21"
},
"peerDependencies": {
"@better-auth/core": "1.5.6"
}
},
"node_modules/@better-auth/utils": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.1.tgz",
"integrity": "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==",
"license": "MIT"
},
"node_modules/@better-fetch/fetch": {
"version": "1.1.21",
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz",
"integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="
},
"node_modules/@emnapi/runtime": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
@@ -708,6 +837,55 @@
"node": ">= 10"
}
},
"node_modules/@noble/ciphers": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz",
"integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@opentelemetry/semantic-conventions": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz",
"integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1030,6 +1208,131 @@
"node": ">=6.0.0"
}
},
"node_modules/better-auth": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.5.6.tgz",
"integrity": "sha512-QSpJTqaT1XVfWRQe/fm3PgeuwOIlz1nWX/Dx7nsHStJ382bLzmDbQk2u7IT0IJ6wS5SRxfqEE1Ev9TXontgyAQ==",
"license": "MIT",
"dependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/drizzle-adapter": "1.5.6",
"@better-auth/kysely-adapter": "1.5.6",
"@better-auth/memory-adapter": "1.5.6",
"@better-auth/mongo-adapter": "1.5.6",
"@better-auth/prisma-adapter": "1.5.6",
"@better-auth/telemetry": "1.5.6",
"@better-auth/utils": "0.3.1",
"@better-fetch/fetch": "1.1.21",
"@noble/ciphers": "^2.1.1",
"@noble/hashes": "^2.0.1",
"better-call": "1.3.2",
"defu": "^6.1.4",
"jose": "^6.1.3",
"kysely": "^0.28.12",
"nanostores": "^1.1.1",
"zod": "^4.3.6"
},
"peerDependencies": {
"@lynx-js/react": "*",
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
"@sveltejs/kit": "^2.0.0",
"@tanstack/react-start": "^1.0.0",
"@tanstack/solid-start": "^1.0.0",
"better-sqlite3": "^12.0.0",
"drizzle-kit": ">=0.31.4",
"drizzle-orm": ">=0.41.0",
"mongodb": "^6.0.0 || ^7.0.0",
"mysql2": "^3.0.0",
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
"pg": "^8.0.0",
"prisma": "^5.0.0 || ^6.0.0 || ^7.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"solid-js": "^1.0.0",
"svelte": "^4.0.0 || ^5.0.0",
"vitest": "^2.0.0 || ^3.0.0 || ^4.0.0",
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"@lynx-js/react": {
"optional": true
},
"@prisma/client": {
"optional": true
},
"@sveltejs/kit": {
"optional": true
},
"@tanstack/react-start": {
"optional": true
},
"@tanstack/solid-start": {
"optional": true
},
"better-sqlite3": {
"optional": true
},
"drizzle-kit": {
"optional": true
},
"drizzle-orm": {
"optional": true
},
"mongodb": {
"optional": true
},
"mysql2": {
"optional": true
},
"next": {
"optional": true
},
"pg": {
"optional": true
},
"prisma": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
},
"solid-js": {
"optional": true
},
"svelte": {
"optional": true
},
"vitest": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/better-call": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.2.tgz",
"integrity": "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw==",
"license": "MIT",
"dependencies": {
"@better-auth/utils": "^0.3.1",
"@better-fetch/fetch": "^1.1.21",
"rou3": "^0.7.12",
"set-cookie-parser": "^3.0.1"
},
"peerDependencies": {
"zod": "^4.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001774",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz",
@@ -1063,6 +1366,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -1162,6 +1471,24 @@
"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/kysely": {
"version": "0.28.14",
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.14.tgz",
"integrity": "sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/lightningcss": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
@@ -1451,6 +1778,21 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/nanostores": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.2.0.tgz",
"integrity": "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"engines": {
"node": "^20.0.0 || >=22.0.0"
}
},
"node_modules/next": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
@@ -1615,6 +1957,12 @@
}
}
},
"node_modules/rou3": {
"version": "0.7.12",
"resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz",
"integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==",
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -1634,6 +1982,12 @@
"node": ">=10"
}
},
"node_modules/set-cookie-parser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz",
"integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==",
"license": "MIT"
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -1789,6 +2143,15 @@
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
+1
View File
@@ -9,6 +9,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"better-auth": "^1.5.5",
"i18next": "^25.8.13",
"i18next-browser-languagedetector": "^8.2.1",
"next": "^16.1.6",
+5 -5
View File
@@ -3,14 +3,12 @@
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { api } from '@/lib/api';
import { useAuth } from '@/lib/auth';
import { signIn } from '@/lib/auth-client';
import { useTranslation } from 'react-i18next';
export default function LoginPage() {
const { t } = useTranslation();
const router = useRouter();
const { login } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
@@ -21,8 +19,10 @@ export default function LoginPage() {
setError('');
setIsSubmitting(true);
try {
const result = await api.auth.login(email, password);
login(result.token, { display_name: result.user.display_name, email: result.user.email });
const result = await signIn.email({ email, password });
if (result.error) {
throw new Error(result.error.message ?? t('login.loginFailed'));
}
router.push('/');
} catch (err) {
setError(err instanceof Error ? err.message : t('login.loginFailed'));
+5 -5
View File
@@ -3,14 +3,12 @@
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { api } from '@/lib/api';
import { useAuth } from '@/lib/auth';
import { signUp } from '@/lib/auth-client';
import { useTranslation } from 'react-i18next';
export default function RegisterPage() {
const { t } = useTranslation();
const router = useRouter();
const { login } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [displayName, setDisplayName] = useState('');
@@ -23,8 +21,10 @@ export default function RegisterPage() {
setError('');
setIsSubmitting(true);
try {
const result = await api.auth.register(email, password, displayName, orgName);
login(result.token, { display_name: result.user.display_name, email: result.user.email });
const result = await signUp.email({ email, password, name: displayName });
if (result.error) {
throw new Error(result.error.message ?? t('register.registrationFailed'));
}
router.push('/');
} catch (err) {
setError(err instanceof Error ? err.message : t('register.registrationFailed'));
@@ -76,7 +76,7 @@ const INTEGRATIONS: Integration[] = [
export default function AgentRegistrationForm({ onSuccess, onCancel }: AgentRegistrationFormProps) {
const { t } = useTranslation();
const { user, token: authToken } = useAuth();
const { user } = useAuth();
const [displayName, setDisplayName] = useState('');
const [responsibleEntity, setResponsibleEntity] = useState('');
const [submitting, setSubmitting] = useState(false);
@@ -186,9 +186,9 @@ export default function AgentRegistrationForm({ onSuccess, onCancel }: AgentRegi
const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:8787';
// CLI install commands for supported integrations
const buildCliCommands = (integration: Integration) => ({
node: `npx @elydora/sdk install --agent ${integration.agentFlag} --org_id "${creds.orgId}" --agent_id "${creds.agentId}" --private_key "${creds.privateKey}" --kid "${creds.kid}" --token "${apiToken ?? authToken ?? ''}" --base_url "${apiBaseUrl}"`,
python: `pip install elydora && elydora install --agent ${integration.agentFlag} --org_id "${creds.orgId}" --agent_id "${creds.agentId}" --private_key "${creds.privateKey}" --kid "${creds.kid}" --token "${apiToken ?? authToken ?? ''}" --base_url "${apiBaseUrl}"`,
go: `go install github.com/Elydora-Infrastructure/Elydora-Go-SDK/cmd/elydora@latest && elydora install --agent ${integration.agentFlag} --org_id "${creds.orgId}" --agent_id "${creds.agentId}" --private_key "${creds.privateKey}" --kid "${creds.kid}" --token "${apiToken ?? authToken ?? ''}" --base-url "${apiBaseUrl}"`,
node: `npx @elydora/sdk install --agent ${integration.agentFlag} --org_id "${creds.orgId}" --agent_id "${creds.agentId}" --private_key "${creds.privateKey}" --kid "${creds.kid}" --token "${apiToken ?? ''}" --base_url "${apiBaseUrl}"`,
python: `pip install elydora && elydora install --agent ${integration.agentFlag} --org_id "${creds.orgId}" --agent_id "${creds.agentId}" --private_key "${creds.privateKey}" --kid "${creds.kid}" --token "${apiToken ?? ''}" --base_url "${apiBaseUrl}"`,
go: `go install github.com/Elydora-Infrastructure/Elydora-Go-SDK/cmd/elydora@latest && elydora install --agent ${integration.agentFlag} --org_id "${creds.orgId}" --agent_id "${creds.agentId}" --private_key "${creds.privateKey}" --kid "${creds.kid}" --token "${apiToken ?? ''}" --base-url "${apiBaseUrl}"`,
});
// SDK step-by-step tutorial (used for unsupported / SDK integrations)
@@ -401,7 +401,7 @@ export const client = new ElydoraClient({
kid: '${creds.kid}',
baseUrl: '${apiBaseUrl}',
});
client.setToken('${apiToken ?? authToken ?? ''}');
client.setToken('${apiToken ?? ''}');
client.prevChainHash = readChainState();
export async function recordOperation({ operationType, subject, action, payload, verify = false }) {
@@ -792,7 +792,7 @@ export const client = new ElydoraClient({
kid: '${creds.kid}',
baseUrl: '${apiBaseUrl}',
});
client.setToken('${apiToken ?? authToken ?? ''}');
client.setToken('${apiToken ?? ''}');
client.prevChainHash = readChainState();
export async function recordOperation({ operationType, subject, action, payload, verify = false }) {
+4 -41
View File
@@ -18,10 +18,6 @@ import type {
CreateExportRequest,
CreateExportResponse,
GetExportResponse,
AuthLoginResponse,
AuthRegisterResponse,
AuthMeResponse,
AuthRefreshResponse,
IssueTokenResponse,
JWKSResponse,
ErrorResponse,
@@ -42,34 +38,26 @@ class ApiError extends Error {
}
}
function getAuthToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('elydora_token');
}
async function request<T>(
path: string,
options: RequestInit = {},
): Promise<T> {
const token = getAuthToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(options.headers as Record<string, string> ?? {}),
};
const response = await fetch(`${API_BASE_URL}${path}`, {
...options,
headers,
credentials: 'include',
});
if (!response.ok) {
// On 401, clear stored tokens and redirect to login
// On 401, redirect to login (session expired or not authenticated)
if (response.status === 401 && typeof window !== 'undefined') {
localStorage.removeItem('elydora_token');
localStorage.removeItem('elydora_display_name');
localStorage.removeItem('elydora_email');
window.location.href = '/login';
return undefined as T;
}
let errorBody: ErrorResponse | null = null;
@@ -213,11 +201,10 @@ export const api = {
},
async download(exportId: string): Promise<Blob> {
const token = getAuthToken();
const response = await fetch(
`${API_BASE_URL}/v1/exports/${encodeURIComponent(exportId)}/download`,
{
headers: token ? { Authorization: `Bearer ${token}` } : {},
credentials: 'include',
},
);
if (!response.ok) {
@@ -234,30 +221,6 @@ export const api = {
},
auth: {
login(email: string, password: string): Promise<AuthLoginResponse> {
return request<AuthLoginResponse>('/v1/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
},
register(email: string, password: string, displayName: string, orgName: string): Promise<AuthRegisterResponse> {
return request<AuthRegisterResponse>('/v1/auth/register', {
method: 'POST',
body: JSON.stringify({ email, password, display_name: displayName, org_name: orgName }),
});
},
me(): Promise<AuthMeResponse> {
return request<AuthMeResponse>('/v1/auth/me');
},
refresh(): Promise<AuthRefreshResponse> {
return request<AuthRefreshResponse>('/v1/auth/refresh', {
method: 'POST',
});
},
issueToken(ttlSeconds: number | null): Promise<IssueTokenResponse> {
return request<IssueTokenResponse>('/v1/auth/token', {
method: 'POST',
+11
View File
@@ -0,0 +1,11 @@
import { createAuthClient } from 'better-auth/react';
import { organizationClient } from 'better-auth/client/plugins';
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8787',
plugins: [
organizationClient(),
],
});
export const { signIn, signUp, signOut, useSession } = authClient;
+51 -69
View File
@@ -1,101 +1,83 @@
'use client';
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { createContext, useContext, useMemo, useCallback } from 'react';
import type { ReactNode } from 'react';
import { useSession, signOut } from '@/lib/auth-client';
import type { RbacRole } from '@elydora/shared';
interface AuthUser {
id: string;
sub: string;
org_id: string;
role: RbacRole;
display_name?: string;
email?: string;
onboarding_completed: boolean;
}
interface AuthContextValue {
user: AuthUser | null;
token: string | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (token: string, user?: { display_name?: string; email?: string }) => void;
logout: () => void;
isAdminRole: boolean;
canManageMembers: boolean;
canManageAgents: boolean;
canViewAudit: boolean;
}
const AuthContext = createContext<AuthContextValue | null>(null);
function decodeJWTPayload(token: string): AuthUser | null {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
// base64url decode
let base64 = parts[1]!.replace(/-/g, '+').replace(/_/g, '/');
while (base64.length % 4 !== 0) base64 += '=';
const json = atob(base64);
const payload = JSON.parse(json);
// Check expiration
const nowSeconds = Math.floor(Date.now() / 1000);
if (typeof payload.exp === 'number' && payload.exp < nowSeconds) return null;
if (!payload.sub || !payload.org_id || !payload.role) return null;
return { sub: payload.sub, org_id: payload.org_id, role: payload.role };
} catch {
return null;
}
}
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const { data: session, isPending } = useSession();
useEffect(() => {
const stored = localStorage.getItem('elydora_token');
if (stored) {
const decoded = decodeJWTPayload(stored);
if (decoded) {
setToken(stored);
// Try to fetch full user profile from localStorage
const displayName = localStorage.getItem('elydora_display_name');
const email = localStorage.getItem('elydora_email');
if (displayName) decoded.display_name = displayName;
if (email) decoded.email = email;
setUser({ ...decoded });
} else {
// Token expired or invalid, clear it
localStorage.removeItem('elydora_token');
localStorage.removeItem('elydora_display_name');
localStorage.removeItem('elydora_email');
}
}
setIsLoading(false);
}, []);
const user: AuthUser | null = useMemo(() => {
if (!session?.user) return null;
const login = useCallback((newToken: string, userInfo?: { display_name?: string; email?: string }) => {
localStorage.setItem('elydora_token', newToken);
const decoded = decodeJWTPayload(newToken);
if (decoded) {
if (userInfo?.display_name) {
decoded.display_name = userInfo.display_name;
localStorage.setItem('elydora_display_name', userInfo.display_name);
}
if (userInfo?.email) {
decoded.email = userInfo.email;
localStorage.setItem('elydora_email', userInfo.email);
}
setUser(decoded);
}
setToken(newToken);
}, []);
const sessionUser = session.user as Record<string, unknown>;
const sessionData = session.session as Record<string, unknown>;
const logout = useCallback(() => {
localStorage.removeItem('elydora_token');
localStorage.removeItem('elydora_display_name');
localStorage.removeItem('elydora_email');
setUser(null);
setToken(null);
const orgId =
(sessionData.activeOrganizationId as string) ??
(sessionUser.org_id as string) ??
'';
const role =
(sessionUser.role as RbacRole) ?? 'readonly_investigator';
return {
id: session.user.id,
sub: session.user.id,
org_id: orgId,
role,
display_name: session.user.name || undefined,
email: session.user.email || undefined,
onboarding_completed: !!(sessionUser.onboarding_completed),
};
}, [session]);
const isAdminRole = user?.role === 'org_owner' || user?.role === 'security_admin';
const canManageMembers = isAdminRole;
const canManageAgents = isAdminRole || user?.role === 'integration_engineer';
const canViewAudit = isAdminRole || user?.role === 'compliance_auditor';
const logout = useCallback(async () => {
await signOut();
window.location.href = '/login';
}, []);
return (
<AuthContext.Provider value={{ user, token, isAuthenticated: !!user, isLoading, login, logout }}>
<AuthContext.Provider
value={{
user,
isAuthenticated: !!user,
isLoading: isPending,
logout,
isAdminRole: !!isAdminRole,
canManageMembers: !!canManageMembers,
canManageAgents: !!canManageAgents,
canViewAudit: !!canViewAudit,
}}
>
{children}
</AuthContext.Provider>
);