From ed3c604c445d86de8cab7d80edeacc0e91b65426 Mon Sep 17 00:00:00 2001 From: Lyons800 <94101691+Lyons800@users.noreply.github.com> Date: Sun, 26 Jul 2026 07:20:34 +0100 Subject: [PATCH] feat(web): Google sign-in and company onboarding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merges the Better Auth identity stack and connects it to the workspace that already existed. The auth schema was already migrated into Neon; this wires it to the product. Two changes were needed beyond the merge. The auth instance was constructed at module scope, so next build — which imports every route module while collecting page data — required production secrets to compile. CI has none, so this would have gone red on merge and would have forced real credentials into the build environment for nothing. getAuth() now builds on first use and memoises; the pg pool still lives on globalThis so it survives across invocations. Verified by building with all four auth variables unset. Organisation scoping now has exactly one decision point. resolveOrganizationId reads the session and returns the active organisation, falling back to the demo organisation only for visitors. The direction matters: an unauthenticated visitor can reach only the demo, and a signed-in user is never silently handed it. The seed agents are inserted into the demo organisation and nowhere else — pre-populating a real company with approvers it never added would be inventing authority. sign-in and onboarding move into the (marketing) group so they render without the workspace chrome. Their test imports carry the .js extensions node16 resolution requires; pnpm check typechecks a config that turbo run typecheck does not. Google and Better Auth organization roles remain application identity only. They never grant payment authority: that still requires a distinct human resolved through AgentBook plus a role issued by the company. --- .env.example | 10 +- HACKATHON_PROVENANCE.md | 13 +- README.md | 42 ++ apps/web/package.json | 4 + apps/web/scripts/migrate-auth.mjs | 54 +++ apps/web/src/app/(app)/dashboard/page.tsx | 11 +- .../onboarding/company-onboarding-form.tsx | 150 +++++++ .../src/app/(marketing)/onboarding/page.tsx | 42 ++ .../sign-in/google-sign-in-button.tsx | 45 ++ apps/web/src/app/(marketing)/sign-in/page.tsx | 43 ++ apps/web/src/app/api/auth/[...all]/route.ts | 23 + apps/web/src/app/api/people/route.ts | 34 +- apps/web/src/components/sidebar.tsx | 101 ++++- apps/web/src/lib/auth-client.ts | 8 + apps/web/src/lib/auth-environment.test.ts | 41 ++ apps/web/src/lib/auth-environment.ts | 70 +++ apps/web/src/lib/auth.server.ts | 82 ++++ apps/web/src/lib/company-slug.test.ts | 21 + apps/web/src/lib/company-slug.ts | 25 ++ apps/web/src/lib/workspace.server.ts | 50 +++ apps/web/tsconfig.json | 1 + docs/architecture/AUTHORITY-MODEL.md | 21 +- docs/architecture/SYSTEM-ARCHITECTURE.md | 18 +- docs/architecture/TECHNOLOGY-BASELINE.md | 37 +- .../0010-company-identity-and-membership.md | 106 +++++ pnpm-lock.yaml | 407 ++++++++++++++++++ 26 files changed, 1393 insertions(+), 66 deletions(-) create mode 100644 apps/web/scripts/migrate-auth.mjs create mode 100644 apps/web/src/app/(marketing)/onboarding/company-onboarding-form.tsx create mode 100644 apps/web/src/app/(marketing)/onboarding/page.tsx create mode 100644 apps/web/src/app/(marketing)/sign-in/google-sign-in-button.tsx create mode 100644 apps/web/src/app/(marketing)/sign-in/page.tsx create mode 100644 apps/web/src/app/api/auth/[...all]/route.ts create mode 100644 apps/web/src/lib/auth-client.ts create mode 100644 apps/web/src/lib/auth-environment.test.ts create mode 100644 apps/web/src/lib/auth-environment.ts create mode 100644 apps/web/src/lib/auth.server.ts create mode 100644 apps/web/src/lib/company-slug.test.ts create mode 100644 apps/web/src/lib/company-slug.ts create mode 100644 docs/architecture/decisions/0010-company-identity-and-membership.md diff --git a/.env.example b/.env.example index ed85a5b..da23369 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,13 @@ SETTLEMENT_WORKER_PORT=4500 # Persistence DATABASE_URL=postgresql://remit:remit@localhost:5432/remit +DATABASE_URL_UNPOOLED=postgresql://remit:remit@localhost:5432/remit + +# Application identity (Better Auth + Google) +BETTER_AUTH_URL=http://localhost:3000 +BETTER_AUTH_SECRET=replace-with-at-least-32-random-characters +GOOGLE_CLIENT_ID=replace-with-google-oauth-client-id +GOOGLE_CLIENT_SECRET=replace-with-google-oauth-client-secret # Public, non-secret network configuration WORLD_CHAIN_ID=eip155:480 @@ -20,8 +27,7 @@ WORLD_AGENTBOOK_ADDRESS=0xA23aB2712eA7BBa896930544C7d6636a96b944dA HEDERA_NETWORK=hedera:testnet HEDERA_MIRROR_BASE_URL=https://testnet.mirrornode.hedera.com -# Secrets are intentionally unnamed until the owning integration PR defines -# its exact process boundary. Never place real values in this file. +# Never place real credentials in this file. # Demo and gate scripts (scripts/demo.ts, scripts/verify-humans.ts). # Required only for live runs; `pnpm demo -- --offline` works without them. diff --git a/HACKATHON_PROVENANCE.md b/HACKATHON_PROVENANCE.md index 1fdfcb6..aad75f6 100644 --- a/HACKATHON_PROVENANCE.md +++ b/HACKATHON_PROVENANCE.md @@ -28,11 +28,14 @@ The Hedera x402 slice consumes these exact public packages. pnpm installs their published artifacts under the repository lockfile; no third-party package source is copied, vendored, patched, or modified. -| Package | Exact version | Published source | Upstream source | License | Modifications | -| ------------------- | ------------: | ------------------------------------------------------------------------------ | -------------------------------------------------------------------- | ---------- | --------------------------------------- | -| `@x402/core` | `2.19.0` | [npm registry manifest](https://registry.npmjs.org/@x402%2Fcore/2.19.0) | [x402 Foundation `x402`](https://github.com/x402-foundation/x402) | Apache-2.0 | None; installed artifact is unmodified. | -| `@x402/hedera` | `2.19.0` | [npm registry manifest](https://registry.npmjs.org/@x402%2Fhedera/2.19.0) | [x402 Foundation `x402`](https://github.com/x402-foundation/x402) | Apache-2.0 | None; installed artifact is unmodified. | -| `@hiero-ledger/sdk` | `2.85.0` | [npm registry manifest](https://registry.npmjs.org/@hiero-ledger%2Fsdk/2.85.0) | [Hiero JavaScript SDK](https://github.com/hiero-ledger/hiero-sdk-js) | Apache-2.0 | None; installed artifact is unmodified. | +| Package | Exact version | Published source | Upstream source | License | Modifications | +| ------------------- | ------------: | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ---------- | --------------------------------------- | +| `@x402/core` | `2.19.0` | [npm registry manifest](https://registry.npmjs.org/@x402%2Fcore/2.19.0) | [x402 Foundation `x402`](https://github.com/x402-foundation/x402) | Apache-2.0 | None; installed artifact is unmodified. | +| `@x402/hedera` | `2.19.0` | [npm registry manifest](https://registry.npmjs.org/@x402%2Fhedera/2.19.0) | [x402 Foundation `x402`](https://github.com/x402-foundation/x402) | Apache-2.0 | None; installed artifact is unmodified. | +| `@hiero-ledger/sdk` | `2.85.0` | [npm registry manifest](https://registry.npmjs.org/@hiero-ledger%2Fsdk/2.85.0) | [Hiero JavaScript SDK](https://github.com/hiero-ledger/hiero-sdk-js) | Apache-2.0 | None; installed artifact is unmodified. | +| `better-auth` | `1.6.23` | [npm registry manifest](https://registry.npmjs.org/better-auth/1.6.23) | [Better Auth](https://github.com/better-auth/better-auth) | MIT | None; installed artifact is unmodified. | +| `pg` | `8.22.0` | [npm registry manifest](https://registry.npmjs.org/pg/8.22.0) | [node-postgres](https://github.com/brianc/node-postgres) | MIT | None; installed artifact is unmodified. | +| `@types/pg` | `8.20.0` | [npm registry manifest](https://registry.npmjs.org/@types%2Fpg/8.20.0) | [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | None; development types are unmodified. | Remit's adapter, trust-policy checks, canonical bindings, recovery seam, and tests are original event-window code outside those packages. diff --git a/README.md b/README.md index 55becf4..8bdc34f 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,48 @@ pnpm dev | Payment agent | 4400 | | Settlement worker | 4500 | +### Company sign-in + +The web app uses Google through Better Auth for application identity and +organization membership. Copy only the variable names from `.env.example` into +`apps/web/.env.local` and provide: + +- `BETTER_AUTH_URL` (`http://localhost:3000` locally); +- a random `BETTER_AUTH_SECRET` of at least 32 characters; +- `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`; +- pooled `DATABASE_URL`; and +- unpooled `DATABASE_URL_UNPOOLED` for migrations. + +The Google OAuth client must allow +`http://localhost:3000/api/auth/callback/google` locally and the corresponding +production origin callback. Apply the isolated, idempotent auth migration before +enabling sign-in: + +```bash +cd apps/web +pnpm auth:migrate +``` + +The migration owns only the `invoiceguard_auth` schema. Google and Better Auth +organization roles never grant payment authority; that remains a separate +control-API decision. + +| Process | Local port | +| ----------------- | ---------: | +| Web | 3000 | +| Control API | 4100 | +| Extraction worker | 4150 | +| Verifier | 4200 | +| x402 facilitator | 4300 | +| Payment agent | 4400 | +| Settlement worker | 4500 | + +Run the complete local quality gate before every push: + +```bash +pnpm check +``` + ## Provenance Work began in this repository during ETHGlobal Lisbon 2026. See diff --git a/apps/web/package.json b/apps/web/package.json index 433e419..079885b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "scripts": { + "auth:migrate": "node --env-file-if-exists=.env.local scripts/migrate-auth.mjs", "build": "next build", "clean": "rm -rf .next", "dev": "next dev --port 3000", @@ -14,10 +15,12 @@ "@remit/persistence": "workspace:*", "@remit/world-adapter": "workspace:*", "@tailwindcss/postcss": "^4.3.3", + "better-auth": "1.6.23", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.26.0", "next": "16.2.11", + "pg": "8.22.0", "postgres": "catalog:", "react": "catalog:", "react-dom": "catalog:", @@ -26,6 +29,7 @@ }, "devDependencies": { "@types/node": "catalog:", + "@types/pg": "8.20.0", "@types/react": "catalog:", "@types/react-dom": "catalog:", "typescript": "catalog:" diff --git a/apps/web/scripts/migrate-auth.mjs b/apps/web/scripts/migrate-auth.mjs new file mode 100644 index 0000000..df0a198 --- /dev/null +++ b/apps/web/scripts/migrate-auth.mjs @@ -0,0 +1,54 @@ +import { getMigrations } from 'better-auth/db/migration'; +import { organization } from 'better-auth/plugins'; +import process from 'node:process'; +import pg from 'pg'; + +const schema = 'invoiceguard_auth'; +const connectionString = + process.env.DATABASE_URL_UNPOOLED ?? process.env.DATABASE_URL; + +if (connectionString === undefined || connectionString.length === 0) { + throw new Error('DATABASE_URL_UNPOOLED or DATABASE_URL is required'); +} + +const administrativePool = new pg.Pool({ + connectionString, + connectionTimeoutMillis: 10_000, + max: 1, +}); + +await administrativePool.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`); +await administrativePool.end(); + +const migrationPool = new pg.Pool({ + connectionString, + connectionTimeoutMillis: 10_000, + max: 1, + options: `-c search_path=${schema},public`, +}); + +try { + const migration = await getMigrations({ + database: migrationPool, + plugins: [ + organization({ + creatorRole: 'owner', + requireEmailVerificationOnInvitation: true, + }), + ], + }); + + if (process.argv.includes('--check')) { + if (migration.toBeCreated.length > 0 || migration.toBeAdded.length > 0) { + process.exitCode = 1; + process.stderr.write('Better Auth schema is not current\n'); + } else { + process.stdout.write('Better Auth schema is current\n'); + } + } else { + await migration.runMigrations(); + process.stdout.write('Better Auth schema is current\n'); + } +} finally { + await migrationPool.end(); +} diff --git a/apps/web/src/app/(app)/dashboard/page.tsx b/apps/web/src/app/(app)/dashboard/page.tsx index 093f14b..0d2f364 100644 --- a/apps/web/src/app/(app)/dashboard/page.tsx +++ b/apps/web/src/app/(app)/dashboard/page.tsx @@ -68,9 +68,14 @@ export default async function DashboardPage() { A product scenario beside independently verified public evidence.

- - - +
+ + + + + + +
diff --git a/apps/web/src/app/(marketing)/onboarding/company-onboarding-form.tsx b/apps/web/src/app/(marketing)/onboarding/company-onboarding-form.tsx new file mode 100644 index 0000000..0bfac09 --- /dev/null +++ b/apps/web/src/app/(marketing)/onboarding/company-onboarding-form.tsx @@ -0,0 +1,150 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { type FormEvent, useEffect, useState } from 'react'; + +import { Button } from '../../../components/ui/button'; +import { authClient } from '../../../lib/auth-client'; +import { createCompanySlug } from '../../../lib/company-slug'; + +type CompanyOnboardingFormProperties = Readonly<{ + email: string; + userName: string; +}>; + +export function CompanyOnboardingForm({ + email, + userName, +}: CompanyOnboardingFormProperties) { + const router = useRouter(); + const [companyName, setCompanyName] = useState(''); + const [error, setError] = useState(); + const [isChecking, setIsChecking] = useState(true); + const [isPending, setIsPending] = useState(false); + + useEffect(() => { + let isCurrent = true; + + async function selectExistingCompany() { + const organizations = await authClient.organization.list(); + + if (!isCurrent) { + return; + } + + const firstOrganization = organizations.data?.[0]; + + if (firstOrganization !== undefined) { + await authClient.organization.setActive({ + organizationId: firstOrganization.id, + }); + router.replace('/invoices'); + router.refresh(); + return; + } + + if (organizations.error !== null) { + setError( + organizations.error.message ?? + 'Your company workspaces could not be loaded.', + ); + } + + setIsChecking(false); + } + + void selectExistingCompany(); + + return () => { + isCurrent = false; + }; + }, [router]); + + async function createCompany(event: FormEvent) { + event.preventDefault(); + const name = companyName.trim(); + + if (name.length < 2) { + setError('Enter the legal or trading name of your company.'); + return; + } + + setError(undefined); + setIsPending(true); + + const created = await authClient.organization.create({ + name, + slug: createCompanySlug(name, crypto.randomUUID()), + }); + + if (created.error !== null || created.data === null) { + setError( + created.error?.message ?? + 'Your company workspace could not be created.', + ); + setIsPending(false); + return; + } + + const activated = await authClient.organization.setActive({ + organizationId: created.data.id, + }); + + if (activated.error !== null) { + setError( + activated.error.message ?? + 'The company was created but could not be selected.', + ); + setIsPending(false); + return; + } + + router.replace('/invoices'); + router.refresh(); + } + + if (isChecking) { + return ( +

+ Checking your company workspace… +

+ ); + } + + return ( +
+
+

{userName}

+

{email}

+
+
+ + setCompanyName(event.target.value)} + placeholder="Padel Peru, Lda" + required + value={companyName} + /> +

+ You will be the workspace owner. Financial approval roles are assigned + separately. +

+
+ {error === undefined ? null : ( +

+ {error} +

+ )} + +
+ ); +} diff --git a/apps/web/src/app/(marketing)/onboarding/page.tsx b/apps/web/src/app/(marketing)/onboarding/page.tsx new file mode 100644 index 0000000..37fc3fa --- /dev/null +++ b/apps/web/src/app/(marketing)/onboarding/page.tsx @@ -0,0 +1,42 @@ +import { headers } from 'next/headers'; +import { redirect } from 'next/navigation'; + +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from '../../../components/ui/card'; +import { getAuth } from '../../../lib/auth.server'; +import { CompanyOnboardingForm } from './company-onboarding-form'; + +export const dynamic = 'force-dynamic'; + +export default async function OnboardingPage() { + const session = await getAuth().api.getSession({ headers: await headers() }); + + if (session === null) { + redirect('/sign-in'); + } + + return ( +
+ + +

One last step

+ Create your company workspace +

+ This keeps invoices, suppliers, policies, and audit evidence scoped + to the correct company. +

+
+ + + +
+
+ ); +} diff --git a/apps/web/src/app/(marketing)/sign-in/google-sign-in-button.tsx b/apps/web/src/app/(marketing)/sign-in/google-sign-in-button.tsx new file mode 100644 index 0000000..dd4e28a --- /dev/null +++ b/apps/web/src/app/(marketing)/sign-in/google-sign-in-button.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { useState } from 'react'; + +import { Button } from '../../../components/ui/button'; +import { authClient } from '../../../lib/auth-client'; + +export function GoogleSignInButton() { + const [error, setError] = useState(); + const [isPending, setIsPending] = useState(false); + + async function signIn() { + setError(undefined); + setIsPending(true); + + const result = await authClient.signIn.social({ + callbackURL: '/onboarding', + newUserCallbackURL: '/onboarding', + provider: 'google', + }); + + if (result.error !== null) { + setError(result.error.message ?? 'Google sign-in could not be started.'); + setIsPending(false); + } + } + + return ( +
+ + {error === undefined ? null : ( +

+ {error} +

+ )} +
+ ); +} diff --git a/apps/web/src/app/(marketing)/sign-in/page.tsx b/apps/web/src/app/(marketing)/sign-in/page.tsx new file mode 100644 index 0000000..63b92a9 --- /dev/null +++ b/apps/web/src/app/(marketing)/sign-in/page.tsx @@ -0,0 +1,43 @@ +import { headers } from 'next/headers'; +import { redirect } from 'next/navigation'; + +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from '../../../components/ui/card'; +import { getAuth } from '../../../lib/auth.server'; +import { GoogleSignInButton } from './google-sign-in-button'; + +export const dynamic = 'force-dynamic'; + +export default async function SignInPage() { + const session = await getAuth().api.getSession({ headers: await headers() }); + + if (session !== null) { + redirect('/onboarding'); + } + + return ( +
+ + +

Company workspace

+ Sign in to InvoiceGuard +

+ Use your work Google account. Payment approval remains a separate, + explicit authority check. +

+
+ + +

+ Signing in identifies you to this application. It does not grant + permission to approve or settle supplier payments. +

+
+
+
+ ); +} diff --git a/apps/web/src/app/api/auth/[...all]/route.ts b/apps/web/src/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..a837e18 --- /dev/null +++ b/apps/web/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,23 @@ +import { toNextJsHandler } from 'better-auth/next-js'; + +import { getAuth } from '../../../../lib/auth.server'; + +/** + * Better Auth's HTTP surface. + * + * The handler is built per request rather than at module scope. `next build` + * imports this file while collecting page data, and constructing the auth + * instance there would make the build itself require production secrets — + * failing CI, which has none, for no benefit. `getAuth()` memoises, so the + * cost after the first request is a property read. + */ + +export const dynamic = 'force-dynamic'; + +const handle = (method: 'GET' | 'POST') => + async function route(request: Request): Promise { + return toNextJsHandler(getAuth())[method](request); + }; + +export const GET = handle('GET'); +export const POST = handle('POST'); diff --git a/apps/web/src/app/api/people/route.ts b/apps/web/src/app/api/people/route.ts index 735f0f1..121c96c 100644 --- a/apps/web/src/app/api/people/route.ts +++ b/apps/web/src/app/api/people/route.ts @@ -7,7 +7,13 @@ import { type WorkspacePerson, } from '@remit/persistence'; -import { DEMO_ORGANIZATION_ID, db } from '../../../lib/workspace.server'; +import { headers } from 'next/headers'; + +import { + DEMO_ORGANIZATION_ID, + db, + resolveOrganizationId, +} from '../../../lib/workspace.server'; /** * The workspace roster. @@ -46,25 +52,35 @@ const SEED = [ }, ]; -async function roster(): Promise { +async function roster( + organizationId: string, +): Promise { const sql = db(); - const existing = await listWorkspacePeople(sql, DEMO_ORGANIZATION_ID); + const existing = await listWorkspacePeople(sql, organizationId); if (existing.length > 0) return existing; + // Only the public demo organisation is ever pre-populated. A real workspace + // starts empty; inserting these agents into someone's company would be + // inventing approvers they never added. + if (organizationId !== DEMO_ORGANIZATION_ID) return existing; + for (const person of SEED) { try { - await addWorkspacePerson(sql, DEMO_ORGANIZATION_ID, person); + await addWorkspacePerson(sql, organizationId, person); } catch { /* a concurrent request seeded first — harmless */ } } - return listWorkspacePeople(sql, DEMO_ORGANIZATION_ID); + return listWorkspacePeople(sql, organizationId); } export async function GET(): Promise { try { + const { organizationId, isDemo } = await resolveOrganizationId( + await headers(), + ); return Response.json( - { people: await roster() }, + { people: await roster(organizationId), isDemo }, { headers: { 'cache-control': 'no-store' } }, ); } catch (error) { @@ -126,7 +142,8 @@ export async function POST(request: Request): Promise { typeof role === 'string' && ROLES.has(role) ? (role as PersonRole) : null; try { - const person = await addWorkspacePerson(db(), DEMO_ORGANIZATION_ID, { + const { organizationId } = await resolveOrganizationId(await headers()); + const person = await addWorkspacePerson(db(), organizationId, { personId: `p-${resolvedAddress.slice(2, 10).toLowerCase()}`, displayName, agentAddress: resolvedAddress, @@ -165,9 +182,10 @@ export async function PATCH(request: Request): Promise { typeof role === 'string' && ROLES.has(role) ? (role as PersonRole) : null; try { + const { organizationId } = await resolveOrganizationId(await headers()); const person = await setWorkspacePersonRole( db(), - DEMO_ORGANIZATION_ID, + organizationId, personId, nextRole, ); diff --git a/apps/web/src/components/sidebar.tsx b/apps/web/src/components/sidebar.tsx index a2bc749..1a52f5e 100644 --- a/apps/web/src/components/sidebar.tsx +++ b/apps/web/src/components/sidebar.tsx @@ -4,6 +4,8 @@ import { ArrowLeftRight, Building2, LayoutGrid, + LogIn, + LogOut, ReceiptText, ScrollText, Users, @@ -11,9 +13,10 @@ import { SlidersHorizontal, } from 'lucide-react'; import Link from 'next/link'; -import { usePathname } from 'next/navigation'; +import { usePathname, useRouter } from 'next/navigation'; import { useState } from 'react'; +import { authClient } from '../lib/auth-client'; import { cn } from '../lib/utils'; /* midday's sidebar recipe: fixed rail, 70px -> 240px on hover, 200ms @@ -35,6 +38,81 @@ const items = [ { href: '/policies', icon: SlidersHorizontal, label: 'Policies' }, ] as const; +function initials(value: string): string { + return value + .split(/\s+/) + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]?.toUpperCase()) + .join(''); +} + +function SidebarAccount({ isExpanded }: Readonly<{ isExpanded: boolean }>) { + const router = useRouter(); + const session = authClient.useSession(); + const activeOrganization = authClient.useActiveOrganization(); + + if (session.data === null || session.data === undefined) { + return ( + + + + + Company sign in + + + ); + } + + const organizationName = activeOrganization.data?.name ?? 'Company workspace'; + + async function signOut() { + await authClient.signOut(); + router.push('/'); + router.refresh(); + } + + return ( +
+ + + {initials(organizationName) || 'IG'} + + + + + {organizationName} + + + {session.data.user.email} + + + +
+ ); +} + export function Sidebar() { const pathname = usePathname(); const [isExpanded, setIsExpanded] = useState(false); @@ -102,26 +180,7 @@ export function Sidebar() { })} -
- - - PP - - - - - Padel Peru, Lda - - - synthetic scenario - - -
+