@henosia/app-next 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/henosia-app-next/SKILL.md +228 -0
- package/.agents/skills/henosia-app-next/assets/template/app/(auth)/sign-in/loading.tsx +10 -0
- package/.agents/skills/henosia-app-next/assets/template/app/(auth)/sign-in/page.tsx +15 -0
- package/.agents/skills/henosia-app-next/assets/template/app/api/auth/[...all]/route.ts +9 -0
- package/.agents/skills/henosia-app-next/assets/template/app/api/henosia-platform/[...all]/route.ts +2 -0
- package/.agents/skills/henosia-app-next/assets/template/app/layout.tsx +35 -0
- package/.agents/skills/henosia-app-next/assets/template/components/app-switcher.tsx +148 -0
- package/.agents/skills/henosia-app-next/assets/template/components/nav-user.tsx +143 -0
- package/.agents/skills/henosia-app-next/assets/template/components/query-provider.tsx +23 -0
- package/.agents/skills/henosia-app-next/assets/template/components/sign-in.tsx +85 -0
- package/.agents/skills/henosia-app-next/assets/template/lib/auth-client.ts +3 -0
- package/.agents/skills/henosia-app-next/assets/template/lib/supabase/client.ts +2 -0
- package/.agents/skills/henosia-app-next/assets/template/lib/supabase/server.ts +2 -0
- package/.agents/skills/henosia-app-next/assets/template/middleware.ts +29 -0
- package/.agents/skills/henosia-app-next/assets/template/next.config.mjs +65 -0
- package/.agents/skills/henosia-app-next/assets/template/package.json +15 -0
- package/.agents/skills/henosia-app-next/assets/template/tsconfig.json +26 -0
- package/.agents/skills/henosia-auth-guards/SKILL.md +121 -0
- package/dist/auth/server.mjs +2 -1
- package/package.json +2 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: henosia-app-next
|
|
3
|
+
description: |
|
|
4
|
+
Integrate the `@henosia/app-next` package into a Next.js app: install,
|
|
5
|
+
configure middleware, mount the better-auth and Henosia Platform catch-all
|
|
6
|
+
routes, wire up the sign-in page, and (optionally) add the app switcher and
|
|
7
|
+
Supabase helpers. Load this skill when adding `@henosia/app-next` to a
|
|
8
|
+
project, scaffolding a new Henosia-Auth-backed Next.js app, or troubleshooting
|
|
9
|
+
the integration scaffolding (middleware, sign-in route, providers,
|
|
10
|
+
re-exports). For per-page / per-route authorization once integration is in
|
|
11
|
+
place, use the `henosia-auth-guards` skill instead.
|
|
12
|
+
enabled: false # flip to true to enable during Henosia Auth integration in projects with legacy auth
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Integrating `@henosia/app-next`
|
|
16
|
+
|
|
17
|
+
`@henosia/app-next` is the Henosia integration for Next.js apps. It provides:
|
|
18
|
+
|
|
19
|
+
- Henosia Auth middleware (pre-check + token refresh + optional Supabase session exchange).
|
|
20
|
+
- better-auth and Henosia Platform catch-all Route Handlers.
|
|
21
|
+
- A React `authClient` (better-auth) and a `useAppSwitcher` hook.
|
|
22
|
+
- Optional Supabase server/browser client factories.
|
|
23
|
+
- Server-side auth guards — see the **`henosia-auth-guards` skill** for those
|
|
24
|
+
(this skill covers the one-time integration; that skill covers the per-route
|
|
25
|
+
authorization work an agent does on every protected page/route/server action).
|
|
26
|
+
|
|
27
|
+
## Authoritative reference
|
|
28
|
+
|
|
29
|
+
For complete and up-to-date package usage details (peer dependencies, subpath
|
|
30
|
+
exports, Supabase exchange behavior, app-switcher hook options), read:
|
|
31
|
+
|
|
32
|
+
- `node_modules/@henosia/app-next/README.md`
|
|
33
|
+
|
|
34
|
+
The README is the source of truth. This skill summarizes the integration
|
|
35
|
+
workflow and explains how to use the bundled reference template.
|
|
36
|
+
|
|
37
|
+
## Environment variables — do not edit
|
|
38
|
+
|
|
39
|
+
`@henosia/app-next` requires several env vars (`BETTER_AUTH_URL`,
|
|
40
|
+
`BETTER_AUTH_SECRET`, `HENOSIA_AUTH_*`, optional `NEXT_PUBLIC_SUPABASE_*`). In
|
|
41
|
+
the consuming app, **these are provided automatically by the Henosia builder
|
|
42
|
+
environment** which is where this skill is invoked from.
|
|
43
|
+
|
|
44
|
+
- ❌ Do **not** add Henosia env vars to `.env.local`, deployment
|
|
45
|
+
config, or CI scripts.
|
|
46
|
+
- ❌ Do **not** read or echo their values back to the user.
|
|
47
|
+
- ✅ Treat the env as a managed contract. If a value is missing at runtime, the
|
|
48
|
+
package surfaces a `[Henosia Auth] Missing required env var: …` error — that
|
|
49
|
+
is a Henosia environment issue, not a code change.
|
|
50
|
+
- See the README "Required environment variables" section for the full list and
|
|
51
|
+
what each one is for, but you should not need to set any of them yourself.
|
|
52
|
+
|
|
53
|
+
## Reference template (use this as the starting point)
|
|
54
|
+
|
|
55
|
+
The package ships a working reference template at:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
node_modules/@henosia/app-next/.agents/skills/henosia-app-next/assets/template/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use it as the source for all integration files. **Always copy or merge from the
|
|
62
|
+
template — do not re-derive these files from scratch.**
|
|
63
|
+
|
|
64
|
+
### File-to-target-path map
|
|
65
|
+
|
|
66
|
+
| Template file | Target in consuming app | Required? |
|
|
67
|
+
|----------------------------------------------|----------------------------------------------|--------------------------------------------|
|
|
68
|
+
| `middleware.ts` | `middleware.ts` (app root) | Required |
|
|
69
|
+
| `app/api/auth/[...all]/route.ts` | Same path | Required |
|
|
70
|
+
| `app/api/henosia-platform/[...all]/route.ts` | Same path | Required |
|
|
71
|
+
| `lib/auth-client.ts` | `lib/auth-client.ts` (or app's equivalent) | Required |
|
|
72
|
+
| `app/(auth)/sign-in/page.tsx` | A route that resolves to `/sign-in` | Required |
|
|
73
|
+
| `app/(auth)/sign-in/loading.tsx` | Same folder as the sign-in page | Recommended |
|
|
74
|
+
| `components/sign-in.tsx` | `components/sign-in.tsx` | Required (used by the sign-in page) |
|
|
75
|
+
| `app/layout.tsx` | Merge into existing root layout | Required (partial merge) |
|
|
76
|
+
| `components/query-provider.tsx` | `components/query-provider.tsx` | Required if using the app switcher |
|
|
77
|
+
| `components/app-switcher.tsx` | `components/app-switcher.tsx` | Optional (only if surfacing the switcher) |
|
|
78
|
+
| `components/nav-user.tsx` | `components/nav-user.tsx` | Optional (only if surfacing user identity) |
|
|
79
|
+
| `lib/supabase/server.ts` | `lib/supabase/server.ts` | Optional (only if using Supabase) |
|
|
80
|
+
| `lib/supabase/client.ts` | `lib/supabase/client.ts` | Optional (only if using Supabase) |
|
|
81
|
+
| `package.json` | Additive merge into existing `package.json` | Required (additive) |
|
|
82
|
+
| `tsconfig.json` | Additive merge into existing `tsconfig.json` | Required (additive) |
|
|
83
|
+
| `next.config.mjs` | Additive merge into existing `next.config.*` | Required (additive) |
|
|
84
|
+
|
|
85
|
+
The sign-in page lives under the `(auth)` Next.js route group in the template.
|
|
86
|
+
The route group itself is an organization choice; what matters is that the
|
|
87
|
+
URL path resolves to `/sign-in` (the value of `HENOSIA_AUTH_SIGN_IN_PATH_NAME`).
|
|
88
|
+
|
|
89
|
+
### Template conventions — follow exactly
|
|
90
|
+
|
|
91
|
+
When copying a template file into the consuming app, apply these rules:
|
|
92
|
+
|
|
93
|
+
1. **Strip `//@ts-nocheck (remove when using the template)`** from the first
|
|
94
|
+
line of every copied file. It exists only so the template files type-check
|
|
95
|
+
in isolation here.
|
|
96
|
+
2. **Strip `// Template use guidance: …`** lines. They are instructions for the
|
|
97
|
+
integrating agent (you), not for the consuming app.
|
|
98
|
+
3. **Preserve verbatim** every comment from `// To Henosia assistant regarding
|
|
99
|
+
this file:` to end-of-file. These currently appear in `middleware.ts` and
|
|
100
|
+
`app/api/auth/[...all]/route.ts`. They protect the Henosia Auth flows from
|
|
101
|
+
well-intentioned future edits and **must remain in the consuming app's
|
|
102
|
+
files**, byte-for-byte.
|
|
103
|
+
4. **`package.json` is an additive merge.** The template uses
|
|
104
|
+
`"__add_these__:"` and `"__others_omitted_for_brevity__"` sentinel keys to
|
|
105
|
+
mark merge boundaries. Add the listed deps to the consuming app's existing
|
|
106
|
+
`dependencies` (preserve everything already there). Use the template's
|
|
107
|
+
pinned versions for `@henosia/app-next`, `@tanstack/react-query`, and
|
|
108
|
+
`better-auth` unless the consuming app already has a newer compatible
|
|
109
|
+
version. Drop the sentinel keys when merging.
|
|
110
|
+
5. **`tsconfig.json` is an additive merge.** Ensure the consuming app has the
|
|
111
|
+
`@/*` path alias under `compilerOptions.paths` and the `next` plugin under
|
|
112
|
+
`compilerOptions.plugins`; the template's other settings are typical Next.js
|
|
113
|
+
defaults — only fold them in if missing.
|
|
114
|
+
6. **`app/layout.tsx` is a partial merge.** Preserve the host app's existing
|
|
115
|
+
layout structure (fonts, metadata, theming, providers, slots, etc.). Fold
|
|
116
|
+
in only:
|
|
117
|
+
- The `usePathname()` import + `path !== HENOSIA_AUTH_SIGN_IN_PATH_NAME`
|
|
118
|
+
branching, so the sign-in page renders without the host app's chrome.
|
|
119
|
+
- The `<QueryProvider>` wrap (required for the app switcher and any other
|
|
120
|
+
`@tanstack/react-query` consumer).
|
|
121
|
+
The template comment `// Preserve the existing layout elements as it stands
|
|
122
|
+
today when using this template` is the directive — follow it.
|
|
123
|
+
7. **`components/sign-in.tsx`, `app-switcher.tsx`, `nav-user.tsx` are
|
|
124
|
+
reference UIs.** Adapt the visuals to the host app's design system, but
|
|
125
|
+
**preserve the Henosia-specific behavior**:
|
|
126
|
+
- `sign-in.tsx`: keep `signIn.social({ provider: "henosia", callbackURL: "/" })`,
|
|
127
|
+
the `data-henosia-auth-sign-in` attribute on the trigger, and the
|
|
128
|
+
`loading` UX around the call.
|
|
129
|
+
- `app-switcher.tsx`: keep the `useAppSwitcher({ enabled: open })` hook
|
|
130
|
+
usage, the `groupedApps` rendering, and the `error` / `isLoading` paths.
|
|
131
|
+
- `nav-user.tsx`: keep `authClient.useSession()`, and on sign-out keep the
|
|
132
|
+
`router.push(HENOSIA_AUTH_SIGN_IN_PATH_NAME)` + `router.refresh()`
|
|
133
|
+
sequence inside `authClient.signOut`'s `onSuccess`.
|
|
134
|
+
8. **The thin re-export files** (`lib/auth-client.ts`, `lib/supabase/server.ts`,
|
|
135
|
+
`lib/supabase/client.ts`) exist so the rest of the consuming app imports
|
|
136
|
+
from `@/lib/...` and the `@henosia/app-next` subpaths are referenced from
|
|
137
|
+
exactly one place. Keep them as one-line re-exports.
|
|
138
|
+
|
|
139
|
+
## Integration checklist
|
|
140
|
+
|
|
141
|
+
Work through these in order. Most steps map directly to a template file or a
|
|
142
|
+
README section.
|
|
143
|
+
|
|
144
|
+
1. **Install the package and peer dependencies.** See README "Installation".
|
|
145
|
+
Use `pnpm`/`npm`/`yarn` per the host app's convention. Install
|
|
146
|
+
`@supabase/ssr` and `@supabase/supabase-js` only if the app uses Supabase.
|
|
147
|
+
2. **Do not configure env vars.** They are provided by the Henosia builder
|
|
148
|
+
environment (see "Environment variables — do not edit" above).
|
|
149
|
+
3. **Add `middleware.ts`** at the app root. Copy from the template, applying
|
|
150
|
+
the template-conventions rules. Keep the `// To Henosia assistant…` block.
|
|
151
|
+
4. **Mount `app/api/auth/[...all]/route.ts`** by re-exporting `GET, POST` from
|
|
152
|
+
`@henosia/app-next/api/auth`. Copy from the template; keep the `// To
|
|
153
|
+
Henosia assistant…` block.
|
|
154
|
+
5. **Mount `app/api/henosia-platform/[...all]/route.ts`** by re-exporting `GET`
|
|
155
|
+
from `@henosia/app-next/api/platform`. Copy from the template.
|
|
156
|
+
6. **Add the React auth client re-export** at `lib/auth-client.ts`.
|
|
157
|
+
7. **Create the sign-in route at `/sign-in`.** Copy `app/(auth)/sign-in/page.tsx`,
|
|
158
|
+
`app/(auth)/sign-in/loading.tsx`, and `components/sign-in.tsx`. The
|
|
159
|
+
route group is optional; the resolved URL must be `/sign-in`.
|
|
160
|
+
8. **Merge `app/layout.tsx`** per the partial-merge rule. Wrap the tree with
|
|
161
|
+
`<QueryProvider>` and skip the host app's chrome on
|
|
162
|
+
`HENOSIA_AUTH_SIGN_IN_PATH_NAME`.
|
|
163
|
+
9. **Add `<QueryProvider>`** (`components/query-provider.tsx`) if you don't
|
|
164
|
+
already have a `QueryClientProvider` higher in the tree. Required for
|
|
165
|
+
`useAppSwitcher` and any other `@tanstack/react-query` consumer.
|
|
166
|
+
10. **(Optional) Add the app switcher.** Copy `components/app-switcher.tsx`
|
|
167
|
+
where you want it in your shell (e.g. inside a `<Sidebar>`). It depends on
|
|
168
|
+
shadcn/ui primitives — install the ones it imports if missing.
|
|
169
|
+
11. **(Optional) Add the user nav / sign-out menu.** Copy
|
|
170
|
+
`components/nav-user.tsx`. Adapt visuals; keep the Henosia behavior.
|
|
171
|
+
12. **(Optional) Add Supabase helpers.** When `NEXT_PUBLIC_SUPABASE_URL` is
|
|
172
|
+
set in the Henosia builder env, install the Supabase peer deps and copy
|
|
173
|
+
`lib/supabase/server.ts` and `lib/supabase/client.ts` from the template.
|
|
174
|
+
The middleware automatically exchanges the Henosia Auth token for a
|
|
175
|
+
Supabase session — no extra wiring required. See README "5. (Optional)
|
|
176
|
+
Supabase helpers".
|
|
177
|
+
13. **Protect every non-public page, layout, server action, and route
|
|
178
|
+
handler.** Use the **`henosia-auth-guards` skill**. The middleware is a
|
|
179
|
+
pre-check only — it does **not** satisfy the per-route authorization
|
|
180
|
+
requirement.
|
|
181
|
+
14. Carefully remove the old auth flow components and routes that Henosia Auth replaces
|
|
182
|
+
|
|
183
|
+
## Subpath imports cheat sheet
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// Server-side helpers (advanced; prefer the auth guards)
|
|
187
|
+
import { auth, verifyHenosiaAuthToken, isUnauthorizedException, isPageRequest, HENOSIA_AUTH_SIGN_IN_PATH_NAME } from '@henosia/app-next'
|
|
188
|
+
|
|
189
|
+
// Shared constants & types (no runtime deps; safe to import from server or client)
|
|
190
|
+
import { HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE } from '@henosia/app-next/shared'
|
|
191
|
+
import type { HenosiaOrganizationContext } from '@henosia/app-next/shared'
|
|
192
|
+
|
|
193
|
+
// Middleware
|
|
194
|
+
import { createHenosiaAuthMiddleware } from '@henosia/app-next/auth/middleware'
|
|
195
|
+
|
|
196
|
+
// Server-side auth guards (see henosia-auth-guards skill)
|
|
197
|
+
import { requireHenosiaAuth, getHenosiaAuth, routeWithHenosiaAuth } from '@henosia/app-next/auth/server-guards'
|
|
198
|
+
|
|
199
|
+
// React auth client
|
|
200
|
+
import { authClient } from '@henosia/app-next/auth/client'
|
|
201
|
+
|
|
202
|
+
// Catch-all route re-exports
|
|
203
|
+
export { GET, POST } from '@henosia/app-next/api/auth'
|
|
204
|
+
export { GET } from '@henosia/app-next/api/platform'
|
|
205
|
+
|
|
206
|
+
// App-switcher hook
|
|
207
|
+
import { useAppSwitcher } from '@henosia/app-next/platform/app-switcher'
|
|
208
|
+
|
|
209
|
+
// Supabase helpers (peer deps required)
|
|
210
|
+
import { createClient } from '@henosia/app-next/supabase/server'
|
|
211
|
+
import { createClient } from '@henosia/app-next/supabase/client'
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Lower-level helpers — usually not needed
|
|
215
|
+
|
|
216
|
+
`auth`, `verifyHenosiaAuthToken`, `isUnauthorizedException`, `isPageRequest` are
|
|
217
|
+
exported from the package root for advanced/edge cases. For protecting pages
|
|
218
|
+
and routes, **always prefer the guards** documented in the
|
|
219
|
+
`henosia-auth-guards` skill. They encode the canonical redirect / 401 / 500
|
|
220
|
+
contract, apply the correct security headers, and integrate with React's
|
|
221
|
+
`cache()` for per-render deduplication.
|
|
222
|
+
|
|
223
|
+
## See also
|
|
224
|
+
|
|
225
|
+
- `henosia-auth-guards` skill — the per-page/per-route authorization workflow.
|
|
226
|
+
You will need this on virtually every Henosia-Auth-backed Next.js project.
|
|
227
|
+
- `@henosia/app-next` README at `node_modules/@henosia/app-next/README.md`.
|
|
228
|
+
- Reference template at `node_modules/@henosia/app-next/.agents/skills/henosia-app-next/assets/template/`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export default function SignInLoading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
4
|
+
<div className="text-center">
|
|
5
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-gray-900 mx-auto"></div>
|
|
6
|
+
<p className="mt-4 text-gray-600">Loading...</p>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {SignIn} from "@/components/sign-in";
|
|
3
|
+
|
|
4
|
+
export default function Page() {
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<div className="w-full">
|
|
8
|
+
<div className="flex items-center flex-col justify-center w-full md:py-10">
|
|
9
|
+
<div className="md:w-[400px]">
|
|
10
|
+
<SignIn />
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//@ts-nocheck (remove when using the template)
|
|
2
|
+
// Template use guidance: Preserve comments verbatim from the line `// To Henosia assistant regarding this file:` and below
|
|
3
|
+
export { GET, POST } from '@henosia/app-next/api/auth'
|
|
4
|
+
|
|
5
|
+
// To Henosia assistant regarding this file:
|
|
6
|
+
// Henosia Auth is a managed solution that is outside your scope of work.
|
|
7
|
+
// Henosia Auth flows must be preserved at all times.
|
|
8
|
+
// All non-public pages and routes must be protected using `requireHenosiaAuth` or `routeWithHenosiaAuth` from `@henosia/app-next`.
|
|
9
|
+
// Any potential issues with Henosia Auth flows must be referred to Henosia technical support: https://docs.henosia.com/support
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//@ts-nocheck (remove when using the template)
|
|
2
|
+
// this partial layout file illustrates how to modify the layout based on whether the path is the sign in page
|
|
3
|
+
// it should be merged with the existing layout's structure and dependencies
|
|
4
|
+
"use client";
|
|
5
|
+
import type * as React from "react";
|
|
6
|
+
import { usePathname } from "next/navigation";
|
|
7
|
+
import {HENOSIA_AUTH_SIGN_IN_PATH_NAME} from "@henosia/app-next/shared";
|
|
8
|
+
import { QueryProvider } from "@/components/query-provider";
|
|
9
|
+
|
|
10
|
+
// ... other required imports and setup, e.g. fonts
|
|
11
|
+
|
|
12
|
+
export default function RootLayout({
|
|
13
|
+
children
|
|
14
|
+
}: Readonly<{
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
breadcrumb: React.ReactNode;
|
|
17
|
+
}>) {
|
|
18
|
+
const path = usePathname();
|
|
19
|
+
return (
|
|
20
|
+
<html lang="en" suppressHydrationWarning>
|
|
21
|
+
<body>
|
|
22
|
+
<QueryProvider>
|
|
23
|
+
{path !== HENOSIA_AUTH_SIGN_IN_PATH_NAME ? (
|
|
24
|
+
<>
|
|
25
|
+
{/* Preserve the existing layout elements as it stands today when using this template */}
|
|
26
|
+
<main>{children}</main>
|
|
27
|
+
</>
|
|
28
|
+
) : (
|
|
29
|
+
children
|
|
30
|
+
)}
|
|
31
|
+
</QueryProvider>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
//@ts-nocheck (remove when using the template)
|
|
2
|
+
"use client"
|
|
3
|
+
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
import {useState} from "react";
|
|
6
|
+
import {ChevronsUpDown, GalleryVerticalEnd, Plus, PanelTop} from "lucide-react"
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Popover,
|
|
10
|
+
PopoverContent,
|
|
11
|
+
PopoverTrigger,
|
|
12
|
+
} from "@/components/ui/popover"
|
|
13
|
+
import {
|
|
14
|
+
Command,
|
|
15
|
+
CommandEmpty,
|
|
16
|
+
CommandGroup,
|
|
17
|
+
CommandInput,
|
|
18
|
+
CommandItem,
|
|
19
|
+
CommandList,
|
|
20
|
+
CommandSeparator,
|
|
21
|
+
} from "@/components/ui/command"
|
|
22
|
+
import {
|
|
23
|
+
SidebarMenu,
|
|
24
|
+
SidebarMenuButton,
|
|
25
|
+
SidebarMenuItem,
|
|
26
|
+
useSidebar,
|
|
27
|
+
} from "@/components/ui/sidebar"
|
|
28
|
+
import {Skeleton} from "@/components/ui/skeleton";
|
|
29
|
+
import {AppSwitcherApp, useAppSwitcher} from "@henosia/app-next/platform/app-switcher";
|
|
30
|
+
|
|
31
|
+
export function AppSwitcher() {
|
|
32
|
+
const { isMobile } = useSidebar();
|
|
33
|
+
const [open, setOpen] = useState(false);
|
|
34
|
+
|
|
35
|
+
const {organization, groupedApps, error, isLoading} = useAppSwitcher({
|
|
36
|
+
enabled: open
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const handleSelectApp = (app: AppSwitcherApp) => {
|
|
40
|
+
window.open(app.url);
|
|
41
|
+
setOpen(false);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<SidebarMenu>
|
|
46
|
+
<SidebarMenuItem>
|
|
47
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
48
|
+
<PopoverTrigger asChild>
|
|
49
|
+
<SidebarMenuButton
|
|
50
|
+
size="lg"
|
|
51
|
+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
52
|
+
>
|
|
53
|
+
<div className="flex aspect-square size-8 items-center justify-center rounded-lg text-sidebar-primary-foreground">
|
|
54
|
+
{organization ? (
|
|
55
|
+
<>
|
|
56
|
+
{organization.logoUrl ? (
|
|
57
|
+
<>
|
|
58
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
59
|
+
<img alt={`Logo for ${organization.name}`} src={organization.logoUrl} className={"size-6"} />
|
|
60
|
+
</>
|
|
61
|
+
) : (
|
|
62
|
+
<GalleryVerticalEnd className={"size-4 text-primary"} />
|
|
63
|
+
)}
|
|
64
|
+
</>
|
|
65
|
+
) : (
|
|
66
|
+
<Skeleton className={"size-6 opacity-40 rounded-lg"} />
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
70
|
+
<span className="truncate font-semibold">
|
|
71
|
+
{organization?.name ?? <Skeleton className={"w-16 h-[1em] opacity-40"} />}
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
<ChevronsUpDown className="ml-auto" />
|
|
75
|
+
</SidebarMenuButton>
|
|
76
|
+
</PopoverTrigger>
|
|
77
|
+
<PopoverContent
|
|
78
|
+
className="w-[22rem] rounded-lg p-0"
|
|
79
|
+
align="start"
|
|
80
|
+
side={isMobile ? "bottom" : "right"}
|
|
81
|
+
sideOffset={4}
|
|
82
|
+
>
|
|
83
|
+
<Command>
|
|
84
|
+
<CommandInput placeholder="Search apps…" />
|
|
85
|
+
<CommandList className="max-h-[80vh]">
|
|
86
|
+
{isLoading ? (
|
|
87
|
+
<div className="flex flex-col gap-1 p-2" role="status" aria-label="Loading apps">
|
|
88
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
89
|
+
<React.Fragment key={i}>
|
|
90
|
+
{i === 0 && (<div className={"p-2"}><Skeleton className="h-2 w-24" /></div>)}
|
|
91
|
+
<div key={i} className="flex items-center gap-2 p-2">
|
|
92
|
+
<Skeleton className="size-6 rounded-sm" />
|
|
93
|
+
<Skeleton className="h-3 flex-1" />
|
|
94
|
+
</div>
|
|
95
|
+
</React.Fragment>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
) : error ? (
|
|
99
|
+
<div className="p-4 text-sm text-destructive">
|
|
100
|
+
{error.message ?? 'Failed to load apps.'}
|
|
101
|
+
</div>
|
|
102
|
+
) : (
|
|
103
|
+
<>
|
|
104
|
+
<CommandEmpty>No apps found.</CommandEmpty>
|
|
105
|
+
{groupedApps.map(({ group, apps }) => (
|
|
106
|
+
<CommandGroup key={group} heading={group}>
|
|
107
|
+
{apps.map((app) => (
|
|
108
|
+
<CommandItem
|
|
109
|
+
key={app.id}
|
|
110
|
+
value={`${group} ${app.name}`}
|
|
111
|
+
onSelect={() => handleSelectApp(app)}
|
|
112
|
+
className="gap-2 p-2"
|
|
113
|
+
title={`${app.name} – ${app.url}`}
|
|
114
|
+
>
|
|
115
|
+
<div className="flex size-6 items-center justify-center rounded-sm border bg-background">
|
|
116
|
+
<PanelTop className="size-4 shrink-0" />
|
|
117
|
+
</div>
|
|
118
|
+
<span className="truncate">{app.name}</span>
|
|
119
|
+
</CommandItem>
|
|
120
|
+
))}
|
|
121
|
+
</CommandGroup>
|
|
122
|
+
))}
|
|
123
|
+
</>
|
|
124
|
+
)}
|
|
125
|
+
<CommandSeparator />
|
|
126
|
+
<CommandGroup>
|
|
127
|
+
<CommandItem
|
|
128
|
+
disabled={isLoading}
|
|
129
|
+
onSelect={() => {
|
|
130
|
+
window.open('https://app.henosia.com/app');
|
|
131
|
+
setOpen(false)
|
|
132
|
+
}}
|
|
133
|
+
className="gap-2 p-2"
|
|
134
|
+
>
|
|
135
|
+
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
|
|
136
|
+
<Plus className="size-4" />
|
|
137
|
+
</div>
|
|
138
|
+
<div className="font-medium text-muted-foreground">Build new app</div>
|
|
139
|
+
</CommandItem>
|
|
140
|
+
</CommandGroup>
|
|
141
|
+
</CommandList>
|
|
142
|
+
</Command>
|
|
143
|
+
</PopoverContent>
|
|
144
|
+
</Popover>
|
|
145
|
+
</SidebarMenuItem>
|
|
146
|
+
</SidebarMenu>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
//@ts-nocheck (remove when using the template)
|
|
2
|
+
"use client"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
BadgeCheck,
|
|
6
|
+
ChevronsUpDown,
|
|
7
|
+
LogOut,
|
|
8
|
+
} from "lucide-react"
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
Avatar,
|
|
12
|
+
AvatarFallback,
|
|
13
|
+
AvatarImage,
|
|
14
|
+
} from "@/components/ui/avatar"
|
|
15
|
+
import {
|
|
16
|
+
DropdownMenu,
|
|
17
|
+
DropdownMenuContent,
|
|
18
|
+
DropdownMenuGroup,
|
|
19
|
+
DropdownMenuItem,
|
|
20
|
+
DropdownMenuLabel,
|
|
21
|
+
DropdownMenuSeparator,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from "@/components/ui/dropdown-menu"
|
|
24
|
+
import {
|
|
25
|
+
SidebarMenu,
|
|
26
|
+
SidebarMenuButton,
|
|
27
|
+
SidebarMenuItem,
|
|
28
|
+
useSidebar,
|
|
29
|
+
} from "@/components/ui/sidebar"
|
|
30
|
+
import { Skeleton } from "@/components/ui/skeleton"
|
|
31
|
+
import Link from "next/link"
|
|
32
|
+
import { useRouter } from "next/navigation"
|
|
33
|
+
import { authClient } from "@/lib/auth-client";
|
|
34
|
+
import { HENOSIA_AUTH_SIGN_IN_PATH_NAME } from "@henosia/app-next/shared"
|
|
35
|
+
|
|
36
|
+
function getInitials(name?: string | null): string {
|
|
37
|
+
if (!name) return ""
|
|
38
|
+
const parts = name.trim().split(/\s+/).filter(Boolean)
|
|
39
|
+
if (parts.length === 0) return ""
|
|
40
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
|
|
41
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function NavUser() {
|
|
45
|
+
const { isMobile } = useSidebar()
|
|
46
|
+
const router = useRouter()
|
|
47
|
+
const { data: session, isPending } = authClient.useSession()
|
|
48
|
+
|
|
49
|
+
if (isPending) {
|
|
50
|
+
return (
|
|
51
|
+
<SidebarMenu>
|
|
52
|
+
<SidebarMenuItem>
|
|
53
|
+
<SidebarMenuButton size="lg" disabled>
|
|
54
|
+
<Skeleton className="h-8 w-8 rounded-lg" />
|
|
55
|
+
<div className="grid flex-1 gap-1 text-left text-sm leading-tight">
|
|
56
|
+
<Skeleton className="h-3 w-24" />
|
|
57
|
+
<Skeleton className="h-3 w-32" />
|
|
58
|
+
</div>
|
|
59
|
+
</SidebarMenuButton>
|
|
60
|
+
</SidebarMenuItem>
|
|
61
|
+
</SidebarMenu>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sessionUser = session?.user
|
|
66
|
+
if (!sessionUser) {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const name = sessionUser.name ?? ""
|
|
71
|
+
const email = sessionUser.email ?? ""
|
|
72
|
+
const avatar = sessionUser.image ?? undefined
|
|
73
|
+
const initials = getInitials(name || email)
|
|
74
|
+
|
|
75
|
+
const handleSignOut = async () => {
|
|
76
|
+
await authClient.signOut({
|
|
77
|
+
fetchOptions: {
|
|
78
|
+
onSuccess: () => {
|
|
79
|
+
router.push(HENOSIA_AUTH_SIGN_IN_PATH_NAME)
|
|
80
|
+
router.refresh()
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<SidebarMenu>
|
|
88
|
+
<SidebarMenuItem>
|
|
89
|
+
<DropdownMenu>
|
|
90
|
+
<DropdownMenuTrigger asChild>
|
|
91
|
+
<SidebarMenuButton
|
|
92
|
+
size="lg"
|
|
93
|
+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
94
|
+
>
|
|
95
|
+
<Avatar className="h-8 w-8 rounded-lg">
|
|
96
|
+
{avatar ? <AvatarImage src={avatar} alt={name} /> : null}
|
|
97
|
+
<AvatarFallback className="rounded-lg">{initials}</AvatarFallback>
|
|
98
|
+
</Avatar>
|
|
99
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
100
|
+
<span className="truncate font-semibold">{name}</span>
|
|
101
|
+
<span className="truncate text-xs">{email}</span>
|
|
102
|
+
</div>
|
|
103
|
+
<ChevronsUpDown className="ml-auto size-4" />
|
|
104
|
+
</SidebarMenuButton>
|
|
105
|
+
</DropdownMenuTrigger>
|
|
106
|
+
<DropdownMenuContent
|
|
107
|
+
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
|
108
|
+
side={isMobile ? "bottom" : "right"}
|
|
109
|
+
align="end"
|
|
110
|
+
sideOffset={4}
|
|
111
|
+
>
|
|
112
|
+
<DropdownMenuLabel className="p-0 font-normal">
|
|
113
|
+
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
|
114
|
+
<Avatar className="h-8 w-8 rounded-lg">
|
|
115
|
+
{avatar ? <AvatarImage src={avatar} alt={name} /> : null}
|
|
116
|
+
<AvatarFallback className="rounded-lg">{initials}</AvatarFallback>
|
|
117
|
+
</Avatar>
|
|
118
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
119
|
+
<span className="truncate font-semibold">{name}</span>
|
|
120
|
+
<span className="truncate text-xs">{email}</span>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</DropdownMenuLabel>
|
|
124
|
+
<DropdownMenuSeparator />
|
|
125
|
+
<DropdownMenuGroup>
|
|
126
|
+
<DropdownMenuItem asChild={true}>
|
|
127
|
+
<Link target={'_blank'} href={"https://app.henosia.com/settings/profile"}>
|
|
128
|
+
<BadgeCheck />
|
|
129
|
+
Profile
|
|
130
|
+
</Link>
|
|
131
|
+
</DropdownMenuItem>
|
|
132
|
+
</DropdownMenuGroup>
|
|
133
|
+
<DropdownMenuSeparator />
|
|
134
|
+
<DropdownMenuItem onSelect={handleSignOut}>
|
|
135
|
+
<LogOut />
|
|
136
|
+
Log out
|
|
137
|
+
</DropdownMenuItem>
|
|
138
|
+
</DropdownMenuContent>
|
|
139
|
+
</DropdownMenu>
|
|
140
|
+
</SidebarMenuItem>
|
|
141
|
+
</SidebarMenu>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//@ts-nocheck (remove when using the template)
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
6
|
+
|
|
7
|
+
export function QueryProvider({ children }: { children: React.ReactNode }) {
|
|
8
|
+
const [queryClient] = React.useState(
|
|
9
|
+
() =>
|
|
10
|
+
new QueryClient({
|
|
11
|
+
defaultOptions: {
|
|
12
|
+
queries: {
|
|
13
|
+
staleTime: 60 * 1000,
|
|
14
|
+
refetchOnWindowFocus: false,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//@ts-nocheck (remove when using the template)
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import {Button} from "@/components/ui/button";
|
|
6
|
+
import {Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";
|
|
7
|
+
import {authClient} from "@/lib/auth-client";
|
|
8
|
+
import {cn} from "@/lib/utils";
|
|
9
|
+
import {useToast} from "@/hooks/use-toast";
|
|
10
|
+
import {useState} from "react";
|
|
11
|
+
|
|
12
|
+
export function SignIn() {
|
|
13
|
+
|
|
14
|
+
const {signIn} = authClient;
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
|
|
17
|
+
const {toast} = useToast();
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Card className="max-w-md rounded-none">
|
|
21
|
+
<CardHeader>
|
|
22
|
+
<CardTitle className="text-lg md:text-xl">Sign In</CardTitle>
|
|
23
|
+
<CardDescription className="text-xs md:text-sm">
|
|
24
|
+
Sign in for this app is provided by Henosia Auth.
|
|
25
|
+
</CardDescription>
|
|
26
|
+
</CardHeader>
|
|
27
|
+
<CardContent>
|
|
28
|
+
<div className="grid gap-4">
|
|
29
|
+
|
|
30
|
+
<div
|
|
31
|
+
className={cn(
|
|
32
|
+
"w-full gap-2 flex items-center",
|
|
33
|
+
"justify-between flex-col",
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
<Button
|
|
37
|
+
variant="outline"
|
|
38
|
+
className={cn("w-full gap-2 flex relative data-[loading=true]:cursor-progress")}
|
|
39
|
+
data-henosia-auth-sign-in
|
|
40
|
+
data-loading={loading}
|
|
41
|
+
onClick={async () => {
|
|
42
|
+
if (loading) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
setLoading(true);
|
|
47
|
+
const {error} = await signIn.social({
|
|
48
|
+
provider: "henosia",
|
|
49
|
+
callbackURL: "/",
|
|
50
|
+
});
|
|
51
|
+
if (error) {
|
|
52
|
+
console.error('[Henosia Auth] Sign-in error', error);
|
|
53
|
+
toast({ variant: "destructive", title: "Sign-in failed", description: error.message });
|
|
54
|
+
}
|
|
55
|
+
} finally {
|
|
56
|
+
setLoading(false)
|
|
57
|
+
}
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<span className={"relative"}>
|
|
61
|
+
Sign in with Henosia
|
|
62
|
+
</span>
|
|
63
|
+
</Button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</CardContent>
|
|
67
|
+
<CardFooter>
|
|
68
|
+
<div className="flex justify-center w-full border-t pt-4">
|
|
69
|
+
<p className="text-center text-xs text-neutral-500">
|
|
70
|
+
Learn more about {" "}
|
|
71
|
+
<Link
|
|
72
|
+
href="https://docs.henosia.com"
|
|
73
|
+
className="underline"
|
|
74
|
+
target="_blank"
|
|
75
|
+
>
|
|
76
|
+
<span className="dark:text-white/70 cursor-pointer">
|
|
77
|
+
Henosia Auth.
|
|
78
|
+
</span>
|
|
79
|
+
</Link>
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
</CardFooter>
|
|
83
|
+
</Card>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//@ts-nocheck (remove when using the template)
|
|
2
|
+
// Template use guidance: Preserve comments verbatim from the line `// To Henosia assistant regarding this file:` and below
|
|
3
|
+
import { type NextRequest } from 'next/server'
|
|
4
|
+
import { createHenosiaAuthMiddleware } from '@henosia/app-next/auth/middleware'
|
|
5
|
+
|
|
6
|
+
const henosiaAuthMiddleware = createHenosiaAuthMiddleware()
|
|
7
|
+
|
|
8
|
+
export async function middleware(request: NextRequest) {
|
|
9
|
+
// App-specific pre-checks here, if any.
|
|
10
|
+
return await henosiaAuthMiddleware(request)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const config = {
|
|
14
|
+
matcher: [
|
|
15
|
+
/*
|
|
16
|
+
* Match all request paths except for the ones starting with:
|
|
17
|
+
* - _next/static (static files)
|
|
18
|
+
* - _next/image (image optimization files)
|
|
19
|
+
* - favicon.ico (favicon file)
|
|
20
|
+
*/
|
|
21
|
+
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
|
22
|
+
],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// To Henosia assistant regarding this file:
|
|
26
|
+
// Henosia Auth is a managed solution that is outside your scope of work.
|
|
27
|
+
// Henosia Auth flows must be preserved at all times.
|
|
28
|
+
// Important: This middleware performs an auth *pre-check*. Authorization MUST still be performed at the relevant pages and route handlers using `requireHenosiaAuth` or `routeWithHenosiaAuth` from `@henosia/app-next`.
|
|
29
|
+
// Any potential issues with Henosia Auth flows must be referred to Henosia technical support: https://docs.henosia.com/support
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Makes a webpack OutputFileSystem emit assets atomically.
|
|
5
|
+
* Fixes `Uncaught SyntaxError: Invalid or unexpected token` in the browser when the
|
|
6
|
+
* served layout.js is visibly cut off mid-string due to concurrent non-atomic writes.
|
|
7
|
+
*/
|
|
8
|
+
function patchWebpackOutputFileSystem(ofs) {
|
|
9
|
+
if (!ofs || ofs.__atomicEmitPatched) return;
|
|
10
|
+
if (typeof ofs.writeFile !== 'function' || typeof ofs.rename !== 'function') {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const origWriteFile = ofs.writeFile.bind(ofs);
|
|
15
|
+
const origRename = ofs.rename.bind(ofs);
|
|
16
|
+
const origUnlink = typeof ofs.unlink === 'function' ? ofs.unlink.bind(ofs) : null;
|
|
17
|
+
|
|
18
|
+
ofs.writeFile = function patchedWriteFile(filePath, content, optionsOrCb, maybeCb) {
|
|
19
|
+
const hasOptions = typeof optionsOrCb !== 'function';
|
|
20
|
+
const options = hasOptions ? optionsOrCb : undefined;
|
|
21
|
+
const cb = hasOptions ? maybeCb : optionsOrCb;
|
|
22
|
+
const tmp = `${filePath}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
|
|
23
|
+
|
|
24
|
+
const done = (err) => {
|
|
25
|
+
if (err) {
|
|
26
|
+
if (origUnlink) origUnlink(tmp, () => cb(err));
|
|
27
|
+
else cb(err);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
origRename(tmp, filePath, cb);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (hasOptions && options !== undefined) {
|
|
34
|
+
origWriteFile(tmp, content, options, done);
|
|
35
|
+
} else {
|
|
36
|
+
origWriteFile(tmp, content, done);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
Object.defineProperty(ofs, '__atomicEmitPatched', { value: true, enumerable: false });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class AtomicEmitPlugin {
|
|
44
|
+
apply(compiler) {
|
|
45
|
+
// `outputFileSystem` may be assigned/replaced after `apply` runs (e.g. by
|
|
46
|
+
// Next.js or webpack-dev-middleware), so patch lazily right before each
|
|
47
|
+
// build's emit phase. This is idempotent thanks to `__atomicEmitPatched`.
|
|
48
|
+
const patch = () => patchWebpackOutputFileSystem(compiler.outputFileSystem);
|
|
49
|
+
compiler.hooks.beforeRun.tap('AtomicEmitPlugin', patch);
|
|
50
|
+
compiler.hooks.watchRun.tap('AtomicEmitPlugin', patch);
|
|
51
|
+
compiler.hooks.beforeCompile.tap('AtomicEmitPlugin', patch);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @type {import('next').NextConfig} */
|
|
56
|
+
const nextConfig = {
|
|
57
|
+
webpack: (config, { dev }) => {
|
|
58
|
+
if (dev) {
|
|
59
|
+
config.plugins.push(new AtomicEmitPlugin());
|
|
60
|
+
}
|
|
61
|
+
return config;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default nextConfig;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextjs-shadcn-henosia-auth",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"__add_these__:": "",
|
|
7
|
+
"@henosia/app-next": "^1.0.3",
|
|
8
|
+
"@tanstack/react-query": "^5.100.11",
|
|
9
|
+
"better-auth": "1.6.11",
|
|
10
|
+
"__others_omitted_for_brevity__": ""
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"__others_omitted_for_brevity__": ""
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"module": "esnext",
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"jsx": "preserve",
|
|
14
|
+
"incremental": true,
|
|
15
|
+
"plugins": [
|
|
16
|
+
{
|
|
17
|
+
"name": "next"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["./*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
25
|
+
"exclude": ["node_modules"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: henosia-auth-guards
|
|
3
|
+
description: >-
|
|
4
|
+
Enforce @henosia/app-next's server authorization guards on Next.js App Router
|
|
5
|
+
code before any per-user data is read or returned. Use this skill WHENEVER you
|
|
6
|
+
create, edit, or review a server-side handler: any app/**/page.tsx or
|
|
7
|
+
layout.tsx, any file with a "use server" directive or Server Action, and any
|
|
8
|
+
Route Handler (route.ts/route.js) exporting GET/POST/PUT/PATCH/DELETE — or any
|
|
9
|
+
file importing from @henosia/app-next. Trigger even when the user never mentions
|
|
10
|
+
auth, and EVEN IF Henosia auth middleware is present, since the middleware is a
|
|
11
|
+
pre-check only and does NOT satisfy a handler's own authorization requirement.
|
|
12
|
+
Do NOT use for middleware (guards are invalid there), Client Components ("use
|
|
13
|
+
client"), or purely presentational components with no server data access.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Henosia Auth Guards
|
|
17
|
+
|
|
18
|
+
Authorization guards for Next.js apps using `@henosia/app-next`. **Every
|
|
19
|
+
protected page, layout, Server Action, and Route Handler MUST call one of these
|
|
20
|
+
guards.** The Henosia Auth middleware (`createHenosiaAuthMiddleware`) is a fast
|
|
21
|
+
*pre-check* only — it does not satisfy the authorization requirement of the
|
|
22
|
+
underlying request handler.
|
|
23
|
+
|
|
24
|
+
## Import
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import {
|
|
28
|
+
requireHenosiaAuth,
|
|
29
|
+
getHenosiaAuth,
|
|
30
|
+
routeWithHenosiaAuth,
|
|
31
|
+
type HenosiaAuthContext,
|
|
32
|
+
} from '@henosia/app-next/auth/server-guards'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Pick the right guard
|
|
36
|
+
|
|
37
|
+
| Context | Use | Behavior on unauthorized |
|
|
38
|
+
|-------------------------------------------------------------------------|------------------------------|-------------------------------------------------------|
|
|
39
|
+
| Server Component / Page / Server Action / protected layout | `requireHenosiaAuth()` | `redirect()` to `/sign-in` (`HENOSIA_AUTH_SIGN_IN_PATH_NAME`) |
|
|
40
|
+
| Route Handler (`app/api/**/route.ts`) | `routeWithHenosiaAuth(handler)` | `401 Unauthorized` JSON (RFC 7235) — never a redirect |
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
### Page / Server Component / Server Action
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// app/dashboard/page.tsx
|
|
48
|
+
import { requireHenosiaAuth } from '@henosia/app-next/auth/server-guards'
|
|
49
|
+
|
|
50
|
+
export default async function Page() {
|
|
51
|
+
const claims = await requireHenosiaAuth()
|
|
52
|
+
return <Dashboard org={claims['https://henosia.com/organization']} />
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// Server Action
|
|
58
|
+
'use server'
|
|
59
|
+
import { requireHenosiaAuth } from '@henosia/app-next/auth/server-guards'
|
|
60
|
+
|
|
61
|
+
export async function createWidget(formData: FormData) {
|
|
62
|
+
await requireHenosiaAuth() // throws-redirects to sign-in if unauthorized
|
|
63
|
+
// ...mutate
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Route Handler
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// app/api/me/route.ts
|
|
71
|
+
import { routeWithHenosiaAuth } from '@henosia/app-next/auth/server-guards'
|
|
72
|
+
|
|
73
|
+
export const GET = routeWithHenosiaAuth((_request, { payload }) => ({
|
|
74
|
+
organization: payload['https://henosia.com/organization'],
|
|
75
|
+
preview: payload['https://henosia.com/preview'],
|
|
76
|
+
}))
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## `routeWithHenosiaAuth` handler return values
|
|
80
|
+
|
|
81
|
+
The handler may return any of:
|
|
82
|
+
- A `Response` — returned verbatim (security headers applied automatically).
|
|
83
|
+
- A JSON-serializable value — auto-wrapped with `NextResponse.json` (with security headers).
|
|
84
|
+
- `undefined` / `void` — translated to a `204 No Content` response.
|
|
85
|
+
|
|
86
|
+
Every response (success, 401, 500) gets these headers applied if not already set:
|
|
87
|
+
- `Cache-Control: private, no-store`
|
|
88
|
+
- `Vary: Cookie`
|
|
89
|
+
|
|
90
|
+
Do not override these unless you have a specific reason; they keep auth-protected,
|
|
91
|
+
per-user data out of shared and browser caches.
|
|
92
|
+
|
|
93
|
+
## Security rules — must follow
|
|
94
|
+
|
|
95
|
+
1. **`accessToken` is a bearer credential.** Treat it like a password.
|
|
96
|
+
- Do **not** log it, echo it back in a response body, embed it in error
|
|
97
|
+
messages, persist it, or send it to untrusted services.
|
|
98
|
+
- Forward over TLS to trusted downstream services only.
|
|
99
|
+
2. **Never call guards from middleware.** The middleware path uses
|
|
100
|
+
`createHenosiaAuthMiddleware`, which is the lightweight pre-check.
|
|
101
|
+
`requireHenosiaAuth` / `getHenosiaAuth` rely on `next/headers` and
|
|
102
|
+
`next/navigation` and are valid only inside Server Components, layouts,
|
|
103
|
+
Server Actions, and Route Handlers.
|
|
104
|
+
3. **Every protected handler must call a guard.** Don't rely on the middleware.
|
|
105
|
+
Even paths matched by the middleware can be reached without it (e.g. when
|
|
106
|
+
`matcher` excludes them, when running in tests, or when a future change
|
|
107
|
+
relaxes the matcher).
|
|
108
|
+
|
|
109
|
+
## Common pitfalls
|
|
110
|
+
|
|
111
|
+
- ❌ `const claims = requireHenosiaAuth()` — missing `await`. The guard returns a Promise.
|
|
112
|
+
- ❌ Calling `requireHenosiaAuth()` from a Route Handler. It will redirect, which
|
|
113
|
+
is the wrong UX for an API. Use `routeWithHenosiaAuth(handler)` instead.
|
|
114
|
+
- ❌ Using `getHenosiaAuth()` to "protect" a page. It returns `null` on
|
|
115
|
+
unauthorized — the page will render without auth. Use `requireHenosiaAuth()`
|
|
116
|
+
for actual protection; reserve `getHenosiaAuth()` for layouts that render
|
|
117
|
+
different UI for signed-in vs signed-out users.
|
|
118
|
+
- ❌ Manually calling `verifyHenosiaAuthToken` and reinventing the
|
|
119
|
+
redirect/401/500 flow. Use the guards; they encode the canonical contract.
|
|
120
|
+
- ❌ Setting `Cache-Control: public` or otherwise loosening cache headers on a
|
|
121
|
+
guard-wrapped response. Per-user JSON must remain `private, no-store`.
|
package/dist/auth/server.mjs
CHANGED
|
@@ -59,7 +59,8 @@ const henosiaAuthConfig = {
|
|
|
59
59
|
* Gets whether the specified request is for a page (a top level page or iframe page)
|
|
60
60
|
*/
|
|
61
61
|
function isPageRequest(request) {
|
|
62
|
-
const
|
|
62
|
+
const { headers } = request;
|
|
63
|
+
const dest = headers.get("X-Henosia-Fetch-Dest") ?? headers.get("Sec-Fetch-Dest");
|
|
63
64
|
if (dest === "document" || dest === "iframe" || dest === "fencedframe") return true;
|
|
64
65
|
const isRsc = request.headers.get("RSC") === "1";
|
|
65
66
|
const isPrefetch = request.headers.get("Next-Router-Prefetch") === "1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henosia/app-next",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Henosia integration for Next.js apps: Provides Henosia Auth and Henosia Platform features like app switcher",
|
|
5
5
|
"author": "Henosia",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"type": "module",
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
|
+
".agents/skills",
|
|
18
19
|
"LICENSE"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|