@hexclave/next 1.0.20 → 1.0.22
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/README.md +187 -7
- package/dist/components/elements/sidebar-layout.js +1 -1
- package/dist/components-page/oauth-callback.js +14 -19
- package/dist/components-page/oauth-callback.js.map +1 -1
- package/dist/components-page/oauth-callback.test.d.ts +1 -0
- package/dist/components-page/oauth-callback.test.js +90 -0
- package/dist/components-page/oauth-callback.test.js.map +1 -0
- package/dist/esm/components/elements/sidebar-layout.js +1 -1
- package/dist/esm/components-page/oauth-callback.js +14 -19
- package/dist/esm/components-page/oauth-callback.js.map +1 -1
- package/dist/esm/components-page/oauth-callback.test.d.ts +1 -0
- package/dist/esm/components-page/oauth-callback.test.js +89 -0
- package/dist/esm/components-page/oauth-callback.test.js.map +1 -0
- package/dist/esm/generated/quetzal-translations.d.ts +2 -2
- package/dist/esm/lib/auth.d.ts.map +1 -1
- package/dist/esm/lib/auth.js +32 -11
- package/dist/esm/lib/auth.js.map +1 -1
- package/dist/esm/lib/auth.test.js +25 -10
- package/dist/esm/lib/auth.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +54 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +4 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +28 -4
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/common.js +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js +17 -13
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js +4 -8
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts +3 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js +19 -13
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js +4 -9
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
- package/dist/generated/quetzal-translations.d.ts +2 -2
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +31 -10
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/auth.test.js +23 -8
- package/dist/lib/auth.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +1 -0
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +54 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +4 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +27 -3
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/common.js +1 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.js +16 -12
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js +4 -8
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts +3 -1
- package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/session-replay.js +19 -12
- package/dist/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js +4 -9
- package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
- package/package.json +4 -4
- package/src/components-page/oauth-callback.test.tsx +109 -0
- package/src/components-page/oauth-callback.tsx +14 -19
- package/src/lib/auth.test.ts +32 -10
- package/src/lib/auth.ts +41 -7
- package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +2 -1
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +66 -0
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +39 -3
- package/src/lib/hexclave-app/apps/implementations/event-tracker.test.ts +5 -13
- package/src/lib/hexclave-app/apps/implementations/event-tracker.ts +19 -14
- package/src/lib/hexclave-app/apps/implementations/session-replay.test.ts +4 -20
- package/src/lib/hexclave-app/apps/implementations/session-replay.ts +19 -12
- package/src/lib/hexclave-app/apps/interfaces/admin-app.ts +1 -0
package/README.md
CHANGED
|
@@ -1,10 +1,190 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Hexclave
|
|
2
|
+
<div align="center">
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
<img src=".github/assets/hexclave-header.svg" alt="Hexclave" width="320"/>
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
- @hexclave/react
|
|
7
|
-
- @hexclave/js
|
|
8
|
-
- @hexclave/cli
|
|
6
|
+
<br/>
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
**The user infrastructure platform.**
|
|
9
|
+
|
|
10
|
+
Hexclave handles everything around your users: authentication, teams,
|
|
11
|
+
payments, emails, analytics, and much more. Start in minutes on the hosted
|
|
12
|
+
cloud. Your data is always yours to export and self-host.
|
|
13
|
+
|
|
14
|
+
[Website](https://hexclave.com) · [Docs](https://docs.hexclave.com) · [Dashboard](https://app.hexclave.com) · [Discord](https://discord.hexclave.com)
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+

|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<div align="center">
|
|
25
|
+
<img src=".github/assets/comparison.png" alt="Where Hexclave fits in the infrastructure stack" width="900"/>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
## Get started
|
|
29
|
+
|
|
30
|
+
Setting up Hexclave is one prompt. Paste this into your coding agent of choice:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
Read skill.hexclave.com and help me setup hexclave in this project
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## For AI agents
|
|
37
|
+
|
|
38
|
+
If you're an AI coding agent, use your web fetch tool or curl the skill documentation for up-to-date integration instructions:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
curl -sSL "https://skill.hexclave.com?question=<your-question>&context=<your-context>"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## What's included
|
|
45
|
+
|
|
46
|
+
Hexclave ships as a catalog of apps you switch on as your product needs them.
|
|
47
|
+
Each one is built on the same user model, and new apps land regularly.
|
|
48
|
+
|
|
49
|
+
<table><tr>
|
|
50
|
+
<td width="50%" valign="middle">
|
|
51
|
+
|
|
52
|
+
### <img src=".github/assets/logos/authentication.png" alt="" width="40" align="top"/> Authentication
|
|
53
|
+
|
|
54
|
+
Authentication that just works with passkeys, OAuth, and CLI auth. Drop in one component and ship the whole flow; auth methods toggle from the dashboard with no code changes needed.
|
|
55
|
+
|
|
56
|
+
</td>
|
|
57
|
+
<td width="50%" valign="middle" align="center">
|
|
58
|
+
<img src=".github/assets/app-shots/authentication.png" width="520" alt="Authentication"/>
|
|
59
|
+
</td>
|
|
60
|
+
</tr></table>
|
|
61
|
+
|
|
62
|
+
<table><tr>
|
|
63
|
+
<td width="50%" valign="middle">
|
|
64
|
+
|
|
65
|
+
### <img src=".github/assets/logos/teams.png" alt="" width="40" align="top"/> Teams
|
|
66
|
+
|
|
67
|
+
Build for teams, not just users, with workspaces, email invites, and roles that actually gate the work. The workspace switcher remembers selection, invites auto sign up new users, and permissions hold up under audit.
|
|
68
|
+
|
|
69
|
+
</td>
|
|
70
|
+
<td width="50%" valign="middle" align="center">
|
|
71
|
+
<img src=".github/assets/app-shots/teams.png" width="520" alt="Teams"/>
|
|
72
|
+
</td>
|
|
73
|
+
</tr></table>
|
|
74
|
+
|
|
75
|
+
<table><tr>
|
|
76
|
+
<td width="50%" valign="middle">
|
|
77
|
+
|
|
78
|
+
### <img src=".github/assets/logos/rbac.png" alt="" width="40" align="top"/> RBAC
|
|
79
|
+
|
|
80
|
+
Permissions, sorted: roles that nest and one permission check that works the same on server or client. Define them in the dashboard, check them anywhere in your code.
|
|
81
|
+
|
|
82
|
+
</td>
|
|
83
|
+
<td width="50%" valign="middle" align="center">
|
|
84
|
+
<img src=".github/assets/app-shots/rbac.png" width="520" alt="RBAC"/>
|
|
85
|
+
</td>
|
|
86
|
+
</tr></table>
|
|
87
|
+
|
|
88
|
+
<table><tr>
|
|
89
|
+
<td width="50%" valign="middle">
|
|
90
|
+
|
|
91
|
+
### <img src=".github/assets/logos/api-keys.png" alt="" width="40" align="top"/> API Keys
|
|
92
|
+
|
|
93
|
+
API keys without the footguns: leaked keys get auto-revoked, work for users and teams, and show the full secret only once. We never keep the plaintext after creation.
|
|
94
|
+
|
|
95
|
+
</td>
|
|
96
|
+
<td width="50%" valign="middle" align="center">
|
|
97
|
+
<img src=".github/assets/app-shots/api-keys.png" width="520" alt="API Keys"/>
|
|
98
|
+
</td>
|
|
99
|
+
</tr></table>
|
|
100
|
+
|
|
101
|
+
<table><tr>
|
|
102
|
+
<td width="50%" valign="middle">
|
|
103
|
+
|
|
104
|
+
### <img src=".github/assets/logos/payments.png" alt="" width="40" align="top"/> Payments
|
|
105
|
+
|
|
106
|
+
Payments without the plumbing for subscriptions, one-time charges, and usage metering with credits. Bill a person or a whole team with one model, no separate codepath.
|
|
107
|
+
|
|
108
|
+
</td>
|
|
109
|
+
<td width="50%" valign="middle" align="center">
|
|
110
|
+
<img src=".github/assets/app-shots/payments.png" width="520" alt="Payments"/>
|
|
111
|
+
</td>
|
|
112
|
+
</tr></table>
|
|
113
|
+
|
|
114
|
+
<table><tr>
|
|
115
|
+
<td width="50%" valign="middle">
|
|
116
|
+
|
|
117
|
+
### <img src=".github/assets/logos/emails.png" alt="" width="40" align="top"/> Emails
|
|
118
|
+
|
|
119
|
+
Email that delivers and tells you so, handling transactional and marketing sends from one API. Edit templates with an AI editor, theme once, and track every open and click.
|
|
120
|
+
|
|
121
|
+
</td>
|
|
122
|
+
<td width="50%" valign="middle" align="center">
|
|
123
|
+
<img src=".github/assets/app-shots/emails.png" width="520" alt="Emails"/>
|
|
124
|
+
</td>
|
|
125
|
+
</tr></table>
|
|
126
|
+
|
|
127
|
+
<table><tr>
|
|
128
|
+
<td width="50%" valign="middle">
|
|
129
|
+
|
|
130
|
+
### <img src=".github/assets/logos/analytics.png" alt="" width="40" align="top"/> Analytics
|
|
131
|
+
|
|
132
|
+
Know your users with no data stack required, with live active user counts and session replays out of the box. Ask in plain English to build dashboards or write SQL to save queries, all with one flag enabled.
|
|
133
|
+
|
|
134
|
+
</td>
|
|
135
|
+
<td width="50%" valign="middle" align="center">
|
|
136
|
+
<img src=".github/assets/app-shots/analytics.png" width="520" alt="Analytics"/>
|
|
137
|
+
</td>
|
|
138
|
+
</tr></table>
|
|
139
|
+
|
|
140
|
+
<table><tr>
|
|
141
|
+
<td width="50%" valign="middle">
|
|
142
|
+
|
|
143
|
+
### <img src=".github/assets/logos/webhooks.png" alt="" width="40" align="top"/> Webhooks
|
|
144
|
+
|
|
145
|
+
React to every user event in real time with signed, tamper-proof webhooks. Retries and backoff are handled for you; verify in five lines and manage endpoints from the dashboard.
|
|
146
|
+
|
|
147
|
+
</td>
|
|
148
|
+
<td width="50%" valign="middle" align="center">
|
|
149
|
+
<img src=".github/assets/app-shots/webhooks.png" width="520" alt="Webhooks"/>
|
|
150
|
+
</td>
|
|
151
|
+
</tr></table>
|
|
152
|
+
|
|
153
|
+
<table><tr>
|
|
154
|
+
<td width="50%" valign="middle">
|
|
155
|
+
|
|
156
|
+
### <img src=".github/assets/logos/data-vault.png" alt="" width="40" align="top"/> Data Vault
|
|
157
|
+
|
|
158
|
+
A safe for the secrets your users hand you, locked with your secret so we never see the plaintext. Store and retrieve tokens in two lines each, server-only by design.
|
|
159
|
+
|
|
160
|
+
</td>
|
|
161
|
+
<td width="50%" valign="middle" align="center">
|
|
162
|
+
<img src=".github/assets/app-shots/data-vault.png" width="520" alt="Data Vault"/>
|
|
163
|
+
</td>
|
|
164
|
+
</tr></table>
|
|
165
|
+
|
|
166
|
+
<table><tr>
|
|
167
|
+
<td width="50%" valign="middle">
|
|
168
|
+
|
|
169
|
+
### <img src=".github/assets/logos/launch-checklist.png" alt="" width="40" align="top"/> Launch Checklist
|
|
170
|
+
|
|
171
|
+
Run through the must-do checks before flipping to production: domain setup, callbacks locked, secrets rotated. The progress tracker keeps your team aligned so nothing critical slips through on launch day.
|
|
172
|
+
|
|
173
|
+
</td>
|
|
174
|
+
<td width="50%" valign="middle" align="center">
|
|
175
|
+
<img src=".github/assets/app-shots/launch-checklist.png" width="520" alt="Launch Checklist"/>
|
|
176
|
+
</td>
|
|
177
|
+
</tr></table>
|
|
178
|
+
|
|
179
|
+
## Contributing
|
|
180
|
+
|
|
181
|
+
Hexclave is open source, and contributions are welcome. Read
|
|
182
|
+
[`CONTRIBUTING.md`](./CONTRIBUTING.md) to get started, and say hello in
|
|
183
|
+
[Discord](https://discord.hexclave.com) before picking up anything large.
|
|
184
|
+
Found a security issue? Email security@hexclave.com.
|
|
185
|
+
|
|
186
|
+
## ❤ Contributors
|
|
187
|
+
|
|
188
|
+
<a href="https://github.com/hexclave/hexclave/graphs/contributors">
|
|
189
|
+
<img src="https://contrib.rocks/image?repo=hexclave/hexclave&columns=9" alt="Contributors" width="100%" />
|
|
190
|
+
</a>
|
|
@@ -7,8 +7,8 @@ let react = require("react");
|
|
|
7
7
|
react = require_chunk.__toESM(react);
|
|
8
8
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
9
|
let lucide_react = require("lucide-react");
|
|
10
|
-
let ______index_js = require("../../index.js");
|
|
11
10
|
let _hexclave_shared_dist_hooks_use_hash = require("@hexclave/shared/dist/hooks/use-hash");
|
|
11
|
+
let ______index_js = require("../../index.js");
|
|
12
12
|
|
|
13
13
|
//#region src/components/elements/sidebar-layout.tsx
|
|
14
14
|
function SidebarLayout(props) {
|
|
@@ -12,7 +12,7 @@ let react_jsx_runtime = require("react/jsx-runtime");
|
|
|
12
12
|
let _hexclave_shared = require("@hexclave/shared");
|
|
13
13
|
let ___components_elements_maybe_full_page_js = require("../components/elements/maybe-full-page.js");
|
|
14
14
|
let ___components_link_js = require("../components/link.js");
|
|
15
|
-
let
|
|
15
|
+
let __error_page_js = require("./error-page.js");
|
|
16
16
|
|
|
17
17
|
//#region src/components-page/oauth-callback.tsx
|
|
18
18
|
function OAuthCallback({ fullPage }) {
|
|
@@ -20,36 +20,32 @@ function OAuthCallback({ fullPage }) {
|
|
|
20
20
|
const app = (0, ___index_js.useStackApp)();
|
|
21
21
|
const called = (0, react.useRef)(false);
|
|
22
22
|
const [showRedirectLink, setShowRedirectLink] = (0, react.useState)(false);
|
|
23
|
-
const [
|
|
23
|
+
const [errorSearchParams, setErrorSearchParams] = (0, react.useState)(null);
|
|
24
24
|
(0, react.useEffect)(() => (0, _hexclave_shared_dist_utils_promises.runAsynchronously)(async () => {
|
|
25
25
|
if (called.current) return;
|
|
26
26
|
called.current = true;
|
|
27
|
-
const redirectToError = async (url) => {
|
|
28
|
-
const urlString = url.toString();
|
|
29
|
-
if (app[___lib_hexclave_app_index_js.hexclaveAppInternalsSymbol].getRedirectMethod() === "none") {
|
|
30
|
-
setRedirectUrl(urlString);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
await app[___lib_hexclave_app_index_js.hexclaveAppInternalsSymbol].redirectToUrl(urlString, { replace: true });
|
|
34
|
-
};
|
|
35
27
|
try {
|
|
36
28
|
if (!await app.callOAuthCallback()) await app.redirectToSignIn({ noRedirectBack: true });
|
|
37
29
|
} catch (e) {
|
|
38
30
|
if (_hexclave_shared.KnownError.isKnownError(e)) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
setErrorSearchParams({
|
|
32
|
+
errorCode: e.errorCode,
|
|
33
|
+
message: e.message,
|
|
34
|
+
details: JSON.stringify(e.details ?? {})
|
|
35
|
+
});
|
|
44
36
|
return;
|
|
45
37
|
}
|
|
46
38
|
(0, _hexclave_shared_dist_utils_errors.captureError)("<OAuthCallback />", e);
|
|
47
|
-
|
|
39
|
+
setErrorSearchParams({});
|
|
48
40
|
}
|
|
49
41
|
}), [app]);
|
|
50
42
|
(0, react.useEffect)(() => {
|
|
51
43
|
setTimeout(() => setShowRedirectLink(true), 3e3);
|
|
52
44
|
}, []);
|
|
45
|
+
if (errorSearchParams != null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__error_page_js.ErrorPage, {
|
|
46
|
+
searchParams: errorSearchParams,
|
|
47
|
+
fullPage
|
|
48
|
+
});
|
|
53
49
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(___components_elements_maybe_full_page_js.MaybeFullPage, {
|
|
54
50
|
fullPage: fullPage ?? false,
|
|
55
51
|
containerClassName: "flex items-center justify-center",
|
|
@@ -58,11 +54,10 @@ function OAuthCallback({ fullPage }) {
|
|
|
58
54
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
59
55
|
className: "flex flex-col justify-center items-center gap-4",
|
|
60
56
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hexclave_ui.Spinner, { size: 20 })
|
|
61
|
-
}), showRedirectLink
|
|
57
|
+
}), showRedirectLink ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [t("If you are not redirected automatically, "), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(___components_link_js.StyledLink, {
|
|
62
58
|
className: "whitespace-nowrap",
|
|
63
|
-
href:
|
|
59
|
+
href: "#",
|
|
64
60
|
onClick: (e) => {
|
|
65
|
-
if (redirectUrl != null) return;
|
|
66
61
|
e.preventDefault();
|
|
67
62
|
(0, _hexclave_shared_dist_utils_promises.runAsynchronously)(app.redirectToHome());
|
|
68
63
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-callback.js","names":["
|
|
1
|
+
{"version":3,"file":"oauth-callback.js","names":["KnownError","ErrorPage","MaybeFullPage","Spinner","StyledLink"],"sources":["../../src/components-page/oauth-callback.tsx"],"sourcesContent":["'use client';\n\n\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n\nimport { KnownError } from \"@hexclave/shared\";\nimport { captureError } from \"@hexclave/shared/dist/utils/errors\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Spinner, cn } from \"@hexclave/ui\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useStackApp } from \"..\";\nimport { MaybeFullPage } from \"../components/elements/maybe-full-page\";\nimport { StyledLink } from \"../components/link\";\nimport { useTranslation } from \"../lib/translations\";\nimport { ErrorPage } from \"./error-page\";\n\nexport function OAuthCallback({ fullPage }: { fullPage?: boolean }) {\n const { t } = useTranslation();\n const app = useStackApp();\n const called = useRef(false);\n const [showRedirectLink, setShowRedirectLink] = useState(false);\n const [errorSearchParams, setErrorSearchParams] = useState<Record<string, string> | null>(null);\n\n useEffect(() => runAsynchronously(async () => {\n if (called.current) return;\n called.current = true;\n try {\n const hasRedirected = await app.callOAuthCallback();\n if (!hasRedirected) {\n await app.redirectToSignIn({ noRedirectBack: true });\n }\n } catch (e) {\n if (KnownError.isKnownError(e)) {\n setErrorSearchParams({\n errorCode: e.errorCode,\n message: e.message,\n details: JSON.stringify(e.details ?? {}),\n });\n return;\n }\n captureError(\"<OAuthCallback />\", e);\n setErrorSearchParams({});\n }\n }), [app]);\n\n useEffect(() => {\n setTimeout(() => setShowRedirectLink(true), 3000);\n }, []);\n\n if (errorSearchParams != null) {\n return <ErrorPage searchParams={errorSearchParams} fullPage={fullPage} />;\n }\n\n return (\n <MaybeFullPage\n fullPage={fullPage ?? false}\n containerClassName=\"flex items-center justify-center\"\n >\n <div\n className={cn(\n \"text-center justify-center items-center stack-scope flex flex-col gap-4 max-w-[380px]\",\n fullPage ? \"p-4\" : \"p-0\"\n )}\n >\n <div className=\"flex flex-col justify-center items-center gap-4\">\n <Spinner size={20} />\n </div>\n {showRedirectLink ? <p>{t('If you are not redirected automatically, ')}<StyledLink\n className=\"whitespace-nowrap\"\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n runAsynchronously(app.redirectToHome());\n }}\n >{t(\"click here\")}</StyledLink></p> : null}\n </div>\n </MaybeFullPage>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkBA,SAAgB,cAAc,EAAE,YAAoC;CAClE,MAAM,EAAE,kDAAsB;CAC9B,MAAM,oCAAmB;CACzB,MAAM,2BAAgB,MAAM;CAC5B,MAAM,CAAC,kBAAkB,2CAAgC,MAAM;CAC/D,MAAM,CAAC,mBAAmB,4CAAgE,KAAK;AAE/F,wFAAkC,YAAY;AAC5C,MAAI,OAAO,QAAS;AACpB,SAAO,UAAU;AACjB,MAAI;AAEF,OAAI,CADkB,MAAM,IAAI,mBAAmB,CAEjD,OAAM,IAAI,iBAAiB,EAAE,gBAAgB,MAAM,CAAC;WAE/C,GAAG;AACV,OAAIA,4BAAW,aAAa,EAAE,EAAE;AAC9B,yBAAqB;KACnB,WAAW,EAAE;KACb,SAAS,EAAE;KACX,SAAS,KAAK,UAAU,EAAE,WAAW,EAAE,CAAC;KACzC,CAAC;AACF;;AAEF,wDAAa,qBAAqB,EAAE;AACpC,wBAAqB,EAAE,CAAC;;GAE1B,EAAE,CAAC,IAAI,CAAC;AAEV,4BAAgB;AACd,mBAAiB,oBAAoB,KAAK,EAAE,IAAK;IAChD,EAAE,CAAC;AAEN,KAAI,qBAAqB,KACvB,QAAO,2CAACC;EAAU,cAAc;EAA6B;GAAY;AAG3E,QACE,2CAACC;EACC,UAAU,YAAY;EACtB,oBAAmB;YAEnB,4CAAC;GACC,gCACE,yFACA,WAAW,QAAQ,MACpB;cAED,2CAAC;IAAI,WAAU;cACb,2CAACC,wBAAQ,MAAM,KAAM;KACjB,EACL,mBAAmB,4CAAC,kBAAG,EAAE,4CAA4C,EAAC,2CAACC;IACtE,WAAU;IACV,MAAK;IACL,UAAU,MAAM;AACd,OAAE,gBAAgB;AAClB,iEAAkB,IAAI,gBAAgB,CAAC;;cAEzC,EAAE,aAAa;KAAc,IAAI,GAAG;IAClC;GACQ"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const require_chunk = require('../chunk-BE-pF4vm.js');
|
|
2
|
+
let react = require("react");
|
|
3
|
+
react = require_chunk.__toESM(react);
|
|
4
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
5
|
+
let _hexclave_shared = require("@hexclave/shared");
|
|
6
|
+
let react_dom_client = require("react-dom/client");
|
|
7
|
+
let vitest = require("vitest");
|
|
8
|
+
let __oauth_callback_js = require("./oauth-callback.js");
|
|
9
|
+
let ___providers_translation_provider_client_js = require("../providers/translation-provider-client.js");
|
|
10
|
+
|
|
11
|
+
//#region src/components-page/oauth-callback.test.tsx
|
|
12
|
+
// @vitest-environment jsdom
|
|
13
|
+
const appMockState = vitest.vi.hoisted(() => ({ app: null }));
|
|
14
|
+
vitest.vi.mock("..", () => ({ useStackApp: () => {
|
|
15
|
+
if (appMockState.app == null) throw new Error("Expected test app to be set before rendering.");
|
|
16
|
+
return appMockState.app;
|
|
17
|
+
} }));
|
|
18
|
+
vitest.vi.mock("@hexclave/ui", async () => {
|
|
19
|
+
await import("react");
|
|
20
|
+
return {
|
|
21
|
+
Button: (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
22
|
+
type: "button",
|
|
23
|
+
onClick: props.onClick,
|
|
24
|
+
children: props.children
|
|
25
|
+
}),
|
|
26
|
+
Spinner: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { "data-testid": "spinner" }),
|
|
27
|
+
Typography: (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: props.children }),
|
|
28
|
+
cn: (...classes) => classes.filter(Boolean).join(" ")
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
const previousActEnvironment = Reflect.get(globalThis, "IS_REACT_ACT_ENVIRONMENT");
|
|
32
|
+
function createAppTestDouble(options) {
|
|
33
|
+
return {
|
|
34
|
+
callOAuthCallback: options.callOAuthCallback,
|
|
35
|
+
redirectToSignIn: vitest.vi.fn(async () => {}),
|
|
36
|
+
redirectToHome: vitest.vi.fn(async () => {})
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
let root = null;
|
|
40
|
+
let container = null;
|
|
41
|
+
async function renderWithApp(app) {
|
|
42
|
+
appMockState.app = app;
|
|
43
|
+
container = document.createElement("div");
|
|
44
|
+
document.body.append(container);
|
|
45
|
+
root = (0, react_dom_client.createRoot)(container);
|
|
46
|
+
await (0, react.act)(async () => {
|
|
47
|
+
root?.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(___providers_translation_provider_client_js.TranslationProviderClient, {
|
|
48
|
+
quetzalKeys: /* @__PURE__ */ new Map(),
|
|
49
|
+
quetzalLocale: /* @__PURE__ */ new Map(),
|
|
50
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__oauth_callback_js.OAuthCallback, {})
|
|
51
|
+
}));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function flushEffects() {
|
|
55
|
+
await (0, react.act)(async () => {
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
(0, vitest.describe)("OAuthCallback", () => {
|
|
60
|
+
(0, vitest.beforeEach)(() => {
|
|
61
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
62
|
+
});
|
|
63
|
+
(0, vitest.afterEach)(() => {
|
|
64
|
+
(0, react.act)(() => {
|
|
65
|
+
root?.unmount();
|
|
66
|
+
});
|
|
67
|
+
container?.remove();
|
|
68
|
+
root = null;
|
|
69
|
+
container = null;
|
|
70
|
+
appMockState.app = null;
|
|
71
|
+
vitest.vi.restoreAllMocks();
|
|
72
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", previousActEnvironment);
|
|
73
|
+
});
|
|
74
|
+
(0, vitest.it)("renders backend-encoded OAuth callback errors on the callback page", async () => {
|
|
75
|
+
const errorMessage = "Your sign up was rejected by an administrator's sign-up rule.";
|
|
76
|
+
const callOAuthCallback = vitest.vi.fn(async () => {
|
|
77
|
+
throw new _hexclave_shared.KnownErrors.SignUpRejected(errorMessage);
|
|
78
|
+
});
|
|
79
|
+
const app = createAppTestDouble({ callOAuthCallback });
|
|
80
|
+
await renderWithApp(app);
|
|
81
|
+
await flushEffects();
|
|
82
|
+
(0, vitest.expect)(callOAuthCallback).toHaveBeenCalledOnce();
|
|
83
|
+
(0, vitest.expect)(container?.textContent).toContain("SIGN_UP_REJECTED");
|
|
84
|
+
(0, vitest.expect)(container?.textContent).toContain(errorMessage);
|
|
85
|
+
(0, vitest.expect)(app.redirectToSignIn).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
//#endregion
|
|
90
|
+
//# sourceMappingURL=oauth-callback.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-callback.test.js","names":["vi","TranslationProviderClient","OAuthCallback","KnownErrors"],"sources":["../../src/components-page/oauth-callback.test.tsx"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n// @vitest-environment jsdom\n\nimport { KnownErrors } from \"@hexclave/shared\";\nimport React, { act } from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport type { StackClientApp } from \"../lib/hexclave-app/apps/interfaces/client-app\";\nimport { TranslationProviderClient } from \"../providers/translation-provider-client\";\nimport { OAuthCallback } from \"./oauth-callback\";\n\nconst appMockState = vi.hoisted(() => ({ app: null as unknown }));\n\nvi.mock(\"..\", () => ({\n useStackApp: () => {\n if (appMockState.app == null) {\n throw new Error(\"Expected test app to be set before rendering.\");\n }\n return appMockState.app;\n },\n}));\n\nvi.mock(\"@hexclave/ui\", async () => {\n const React = await import(\"react\");\n return {\n Button: (props: { children: React.ReactNode, onClick?: () => void }) => (\n <button type=\"button\" onClick={props.onClick}>{props.children}</button>\n ),\n Spinner: () => <div data-testid=\"spinner\" />,\n Typography: (props: { children: React.ReactNode }) => <div>{props.children}</div>,\n cn: (...classes: (string | false | null | undefined)[]) => classes.filter(Boolean).join(\" \"),\n };\n});\n\nconst previousActEnvironment = Reflect.get(globalThis, \"IS_REACT_ACT_ENVIRONMENT\");\n\nfunction createAppTestDouble(options: {\n callOAuthCallback: () => Promise<boolean>,\n}) {\n const app = {\n callOAuthCallback: options.callOAuthCallback,\n redirectToSignIn: vi.fn(async () => {}),\n redirectToHome: vi.fn(async () => {}),\n };\n\n // This test double intentionally implements only the StackClientApp surface\n // that OAuthCallback and the rendered error card touch.\n return app as unknown as StackClientApp<true>;\n}\n\nlet root: Root | null = null;\nlet container: HTMLDivElement | null = null;\n\nasync function renderWithApp(app: StackClientApp<true>) {\n appMockState.app = app;\n container = document.createElement(\"div\");\n document.body.append(container);\n root = createRoot(container);\n await act(async () => {\n root?.render(\n <TranslationProviderClient quetzalKeys={new Map()} quetzalLocale={new Map()}>\n <OAuthCallback />\n </TranslationProviderClient>\n );\n });\n}\n\nasync function flushEffects() {\n await act(async () => {\n await new Promise((resolve) => setTimeout(resolve, 0));\n });\n}\n\ndescribe(\"OAuthCallback\", () => {\n beforeEach(() => {\n Reflect.set(globalThis, \"IS_REACT_ACT_ENVIRONMENT\", true);\n });\n\n afterEach(() => {\n act(() => {\n root?.unmount();\n });\n container?.remove();\n root = null;\n container = null;\n appMockState.app = null;\n vi.restoreAllMocks();\n Reflect.set(globalThis, \"IS_REACT_ACT_ENVIRONMENT\", previousActEnvironment);\n });\n\n it(\"renders backend-encoded OAuth callback errors on the callback page\", async () => {\n const errorMessage = \"Your sign up was rejected by an administrator's sign-up rule.\";\n const callOAuthCallback = vi.fn(async () => {\n throw new KnownErrors.SignUpRejected(errorMessage);\n });\n const app = createAppTestDouble({ callOAuthCallback });\n\n await renderWithApp(app);\n await flushEffects();\n\n expect(callOAuthCallback).toHaveBeenCalledOnce();\n expect(container?.textContent).toContain(\"SIGN_UP_REJECTED\");\n expect(container?.textContent).toContain(errorMessage);\n expect(app.redirectToSignIn).not.toHaveBeenCalled();\n });\n});\n"],"mappings":";;;;;;;;;;;;AAcA,MAAM,eAAeA,UAAG,eAAe,EAAE,KAAK,MAAiB,EAAE;AAEjEA,UAAG,KAAK,aAAa,EACnB,mBAAmB;AACjB,KAAI,aAAa,OAAO,KACtB,OAAM,IAAI,MAAM,gDAAgD;AAElE,QAAO,aAAa;GAEvB,EAAE;AAEHA,UAAG,KAAK,gBAAgB,YAAY;AACpB,OAAM,OAAO;AAC3B,QAAO;EACL,SAAS,UACP,2CAAC;GAAO,MAAK;GAAS,SAAS,MAAM;aAAU,MAAM;IAAkB;EAEzE,eAAe,2CAAC,SAAI,eAAY,YAAY;EAC5C,aAAa,UAAyC,2CAAC,mBAAK,MAAM,WAAe;EACjF,KAAK,GAAG,YAAmD,QAAQ,OAAO,QAAQ,CAAC,KAAK,IAAI;EAC7F;EACD;AAEF,MAAM,yBAAyB,QAAQ,IAAI,YAAY,2BAA2B;AAElF,SAAS,oBAAoB,SAE1B;AASD,QARY;EACV,mBAAmB,QAAQ;EAC3B,kBAAkBA,UAAG,GAAG,YAAY,GAAG;EACvC,gBAAgBA,UAAG,GAAG,YAAY,GAAG;EACtC;;AAOH,IAAI,OAAoB;AACxB,IAAI,YAAmC;AAEvC,eAAe,cAAc,KAA2B;AACtD,cAAa,MAAM;AACnB,aAAY,SAAS,cAAc,MAAM;AACzC,UAAS,KAAK,OAAO,UAAU;AAC/B,yCAAkB,UAAU;AAC5B,sBAAU,YAAY;AACpB,QAAM,OACJ,2CAACC;GAA0B,6BAAa,IAAI,KAAK;GAAE,+BAAe,IAAI,KAAK;aACzE,2CAACC,sCAAgB;IACS,CAC7B;GACD;;AAGJ,eAAe,eAAe;AAC5B,sBAAU,YAAY;AACpB,QAAM,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;GACtD;;qBAGK,uBAAuB;AAC9B,8BAAiB;AACf,UAAQ,IAAI,YAAY,4BAA4B,KAAK;GACzD;AAEF,6BAAgB;AACd,uBAAU;AACR,SAAM,SAAS;IACf;AACF,aAAW,QAAQ;AACnB,SAAO;AACP,cAAY;AACZ,eAAa,MAAM;AACnB,YAAG,iBAAiB;AACpB,UAAQ,IAAI,YAAY,4BAA4B,uBAAuB;GAC3E;AAEF,gBAAG,sEAAsE,YAAY;EACnF,MAAM,eAAe;EACrB,MAAM,oBAAoBF,UAAG,GAAG,YAAY;AAC1C,SAAM,IAAIG,6BAAY,eAAe,aAAa;IAClD;EACF,MAAM,MAAM,oBAAoB,EAAE,mBAAmB,CAAC;AAEtD,QAAM,cAAc,IAAI;AACxB,QAAM,cAAc;AAEpB,qBAAO,kBAAkB,CAAC,sBAAsB;AAChD,qBAAO,WAAW,YAAY,CAAC,UAAU,mBAAmB;AAC5D,qBAAO,WAAW,YAAY,CAAC,UAAU,aAAa;AACtD,qBAAO,IAAI,iBAAiB,CAAC,IAAI,kBAAkB;GACnD;EACF"}
|
|
@@ -4,8 +4,8 @@ import { Button, Typography, cn } from "@hexclave/ui";
|
|
|
4
4
|
import React from "react";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { XIcon } from "lucide-react";
|
|
7
|
-
import { useStackApp } from "../../index.js";
|
|
8
7
|
import { useHash } from "@hexclave/shared/dist/hooks/use-hash";
|
|
8
|
+
import { useStackApp } from "../../index.js";
|
|
9
9
|
|
|
10
10
|
//#region src/components/elements/sidebar-layout.tsx
|
|
11
11
|
function SidebarLayout(props) {
|
|
@@ -10,7 +10,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
import { KnownError } from "@hexclave/shared";
|
|
11
11
|
import { MaybeFullPage } from "../components/elements/maybe-full-page.js";
|
|
12
12
|
import { StyledLink } from "../components/link.js";
|
|
13
|
-
import {
|
|
13
|
+
import { ErrorPage } from "./error-page.js";
|
|
14
14
|
|
|
15
15
|
//#region src/components-page/oauth-callback.tsx
|
|
16
16
|
function OAuthCallback({ fullPage }) {
|
|
@@ -18,36 +18,32 @@ function OAuthCallback({ fullPage }) {
|
|
|
18
18
|
const app = useStackApp();
|
|
19
19
|
const called = useRef(false);
|
|
20
20
|
const [showRedirectLink, setShowRedirectLink] = useState(false);
|
|
21
|
-
const [
|
|
21
|
+
const [errorSearchParams, setErrorSearchParams] = useState(null);
|
|
22
22
|
useEffect(() => runAsynchronously(async () => {
|
|
23
23
|
if (called.current) return;
|
|
24
24
|
called.current = true;
|
|
25
|
-
const redirectToError = async (url) => {
|
|
26
|
-
const urlString = url.toString();
|
|
27
|
-
if (app[hexclaveAppInternalsSymbol].getRedirectMethod() === "none") {
|
|
28
|
-
setRedirectUrl(urlString);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
await app[hexclaveAppInternalsSymbol].redirectToUrl(urlString, { replace: true });
|
|
32
|
-
};
|
|
33
25
|
try {
|
|
34
26
|
if (!await app.callOAuthCallback()) await app.redirectToSignIn({ noRedirectBack: true });
|
|
35
27
|
} catch (e) {
|
|
36
28
|
if (KnownError.isKnownError(e)) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
setErrorSearchParams({
|
|
30
|
+
errorCode: e.errorCode,
|
|
31
|
+
message: e.message,
|
|
32
|
+
details: JSON.stringify(e.details ?? {})
|
|
33
|
+
});
|
|
42
34
|
return;
|
|
43
35
|
}
|
|
44
36
|
captureError("<OAuthCallback />", e);
|
|
45
|
-
|
|
37
|
+
setErrorSearchParams({});
|
|
46
38
|
}
|
|
47
39
|
}), [app]);
|
|
48
40
|
useEffect(() => {
|
|
49
41
|
setTimeout(() => setShowRedirectLink(true), 3e3);
|
|
50
42
|
}, []);
|
|
43
|
+
if (errorSearchParams != null) return /* @__PURE__ */ jsx(ErrorPage, {
|
|
44
|
+
searchParams: errorSearchParams,
|
|
45
|
+
fullPage
|
|
46
|
+
});
|
|
51
47
|
return /* @__PURE__ */ jsx(MaybeFullPage, {
|
|
52
48
|
fullPage: fullPage ?? false,
|
|
53
49
|
containerClassName: "flex items-center justify-center",
|
|
@@ -56,11 +52,10 @@ function OAuthCallback({ fullPage }) {
|
|
|
56
52
|
children: [/* @__PURE__ */ jsx("div", {
|
|
57
53
|
className: "flex flex-col justify-center items-center gap-4",
|
|
58
54
|
children: /* @__PURE__ */ jsx(Spinner, { size: 20 })
|
|
59
|
-
}), showRedirectLink
|
|
55
|
+
}), showRedirectLink ? /* @__PURE__ */ jsxs("p", { children: [t("If you are not redirected automatically, "), /* @__PURE__ */ jsx(StyledLink, {
|
|
60
56
|
className: "whitespace-nowrap",
|
|
61
|
-
href:
|
|
57
|
+
href: "#",
|
|
62
58
|
onClick: (e) => {
|
|
63
|
-
if (redirectUrl != null) return;
|
|
64
59
|
e.preventDefault();
|
|
65
60
|
runAsynchronously(app.redirectToHome());
|
|
66
61
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-callback.js","names":[],"sources":["../../../src/components-page/oauth-callback.tsx"],"sourcesContent":["'use client';\n\n\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n\nimport { KnownError } from \"@hexclave/shared\";\nimport { captureError } from \"@hexclave/shared/dist/utils/errors\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Spinner, cn } from \"@hexclave/ui\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useStackApp } from \"..\";\nimport { MaybeFullPage } from \"../components/elements/maybe-full-page\";\nimport { StyledLink } from \"../components/link\";\nimport {
|
|
1
|
+
{"version":3,"file":"oauth-callback.js","names":[],"sources":["../../../src/components-page/oauth-callback.tsx"],"sourcesContent":["'use client';\n\n\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n\nimport { KnownError } from \"@hexclave/shared\";\nimport { captureError } from \"@hexclave/shared/dist/utils/errors\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Spinner, cn } from \"@hexclave/ui\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useStackApp } from \"..\";\nimport { MaybeFullPage } from \"../components/elements/maybe-full-page\";\nimport { StyledLink } from \"../components/link\";\nimport { useTranslation } from \"../lib/translations\";\nimport { ErrorPage } from \"./error-page\";\n\nexport function OAuthCallback({ fullPage }: { fullPage?: boolean }) {\n const { t } = useTranslation();\n const app = useStackApp();\n const called = useRef(false);\n const [showRedirectLink, setShowRedirectLink] = useState(false);\n const [errorSearchParams, setErrorSearchParams] = useState<Record<string, string> | null>(null);\n\n useEffect(() => runAsynchronously(async () => {\n if (called.current) return;\n called.current = true;\n try {\n const hasRedirected = await app.callOAuthCallback();\n if (!hasRedirected) {\n await app.redirectToSignIn({ noRedirectBack: true });\n }\n } catch (e) {\n if (KnownError.isKnownError(e)) {\n setErrorSearchParams({\n errorCode: e.errorCode,\n message: e.message,\n details: JSON.stringify(e.details ?? {}),\n });\n return;\n }\n captureError(\"<OAuthCallback />\", e);\n setErrorSearchParams({});\n }\n }), [app]);\n\n useEffect(() => {\n setTimeout(() => setShowRedirectLink(true), 3000);\n }, []);\n\n if (errorSearchParams != null) {\n return <ErrorPage searchParams={errorSearchParams} fullPage={fullPage} />;\n }\n\n return (\n <MaybeFullPage\n fullPage={fullPage ?? false}\n containerClassName=\"flex items-center justify-center\"\n >\n <div\n className={cn(\n \"text-center justify-center items-center stack-scope flex flex-col gap-4 max-w-[380px]\",\n fullPage ? \"p-4\" : \"p-0\"\n )}\n >\n <div className=\"flex flex-col justify-center items-center gap-4\">\n <Spinner size={20} />\n </div>\n {showRedirectLink ? <p>{t('If you are not redirected automatically, ')}<StyledLink\n className=\"whitespace-nowrap\"\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n runAsynchronously(app.redirectToHome());\n }}\n >{t(\"click here\")}</StyledLink></p> : null}\n </div>\n </MaybeFullPage>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,SAAgB,cAAc,EAAE,YAAoC;CAClE,MAAM,EAAE,MAAM,gBAAgB;CAC9B,MAAM,MAAM,aAAa;CACzB,MAAM,SAAS,OAAO,MAAM;CAC5B,MAAM,CAAC,kBAAkB,uBAAuB,SAAS,MAAM;CAC/D,MAAM,CAAC,mBAAmB,wBAAwB,SAAwC,KAAK;AAE/F,iBAAgB,kBAAkB,YAAY;AAC5C,MAAI,OAAO,QAAS;AACpB,SAAO,UAAU;AACjB,MAAI;AAEF,OAAI,CADkB,MAAM,IAAI,mBAAmB,CAEjD,OAAM,IAAI,iBAAiB,EAAE,gBAAgB,MAAM,CAAC;WAE/C,GAAG;AACV,OAAI,WAAW,aAAa,EAAE,EAAE;AAC9B,yBAAqB;KACnB,WAAW,EAAE;KACb,SAAS,EAAE;KACX,SAAS,KAAK,UAAU,EAAE,WAAW,EAAE,CAAC;KACzC,CAAC;AACF;;AAEF,gBAAa,qBAAqB,EAAE;AACpC,wBAAqB,EAAE,CAAC;;GAE1B,EAAE,CAAC,IAAI,CAAC;AAEV,iBAAgB;AACd,mBAAiB,oBAAoB,KAAK,EAAE,IAAK;IAChD,EAAE,CAAC;AAEN,KAAI,qBAAqB,KACvB,QAAO,oBAAC;EAAU,cAAc;EAA6B;GAAY;AAG3E,QACE,oBAAC;EACC,UAAU,YAAY;EACtB,oBAAmB;YAEnB,qBAAC;GACC,WAAW,GACT,yFACA,WAAW,QAAQ,MACpB;cAED,oBAAC;IAAI,WAAU;cACb,oBAAC,WAAQ,MAAM,KAAM;KACjB,EACL,mBAAmB,qBAAC,kBAAG,EAAE,4CAA4C,EAAC,oBAAC;IACtE,WAAU;IACV,MAAK;IACL,UAAU,MAAM;AACd,OAAE,gBAAgB;AAClB,uBAAkB,IAAI,gBAAgB,CAAC;;cAEzC,EAAE,aAAa;KAAc,IAAI,GAAG;IAClC;GACQ"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React, { act } from "react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { KnownErrors } from "@hexclave/shared";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { OAuthCallback } from "./oauth-callback.js";
|
|
7
|
+
import { TranslationProviderClient } from "../providers/translation-provider-client.js";
|
|
8
|
+
|
|
9
|
+
//#region src/components-page/oauth-callback.test.tsx
|
|
10
|
+
// @vitest-environment jsdom
|
|
11
|
+
const appMockState = vi.hoisted(() => ({ app: null }));
|
|
12
|
+
vi.mock("..", () => ({ useStackApp: () => {
|
|
13
|
+
if (appMockState.app == null) throw new Error("Expected test app to be set before rendering.");
|
|
14
|
+
return appMockState.app;
|
|
15
|
+
} }));
|
|
16
|
+
vi.mock("@hexclave/ui", async () => {
|
|
17
|
+
await import("react");
|
|
18
|
+
return {
|
|
19
|
+
Button: (props) => /* @__PURE__ */ jsx("button", {
|
|
20
|
+
type: "button",
|
|
21
|
+
onClick: props.onClick,
|
|
22
|
+
children: props.children
|
|
23
|
+
}),
|
|
24
|
+
Spinner: () => /* @__PURE__ */ jsx("div", { "data-testid": "spinner" }),
|
|
25
|
+
Typography: (props) => /* @__PURE__ */ jsx("div", { children: props.children }),
|
|
26
|
+
cn: (...classes) => classes.filter(Boolean).join(" ")
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
const previousActEnvironment = Reflect.get(globalThis, "IS_REACT_ACT_ENVIRONMENT");
|
|
30
|
+
function createAppTestDouble(options) {
|
|
31
|
+
return {
|
|
32
|
+
callOAuthCallback: options.callOAuthCallback,
|
|
33
|
+
redirectToSignIn: vi.fn(async () => {}),
|
|
34
|
+
redirectToHome: vi.fn(async () => {})
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
let root = null;
|
|
38
|
+
let container = null;
|
|
39
|
+
async function renderWithApp(app) {
|
|
40
|
+
appMockState.app = app;
|
|
41
|
+
container = document.createElement("div");
|
|
42
|
+
document.body.append(container);
|
|
43
|
+
root = createRoot(container);
|
|
44
|
+
await act(async () => {
|
|
45
|
+
root?.render(/* @__PURE__ */ jsx(TranslationProviderClient, {
|
|
46
|
+
quetzalKeys: /* @__PURE__ */ new Map(),
|
|
47
|
+
quetzalLocale: /* @__PURE__ */ new Map(),
|
|
48
|
+
children: /* @__PURE__ */ jsx(OAuthCallback, {})
|
|
49
|
+
}));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function flushEffects() {
|
|
53
|
+
await act(async () => {
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
describe("OAuthCallback", () => {
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
60
|
+
});
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
act(() => {
|
|
63
|
+
root?.unmount();
|
|
64
|
+
});
|
|
65
|
+
container?.remove();
|
|
66
|
+
root = null;
|
|
67
|
+
container = null;
|
|
68
|
+
appMockState.app = null;
|
|
69
|
+
vi.restoreAllMocks();
|
|
70
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", previousActEnvironment);
|
|
71
|
+
});
|
|
72
|
+
it("renders backend-encoded OAuth callback errors on the callback page", async () => {
|
|
73
|
+
const errorMessage = "Your sign up was rejected by an administrator's sign-up rule.";
|
|
74
|
+
const callOAuthCallback = vi.fn(async () => {
|
|
75
|
+
throw new KnownErrors.SignUpRejected(errorMessage);
|
|
76
|
+
});
|
|
77
|
+
const app = createAppTestDouble({ callOAuthCallback });
|
|
78
|
+
await renderWithApp(app);
|
|
79
|
+
await flushEffects();
|
|
80
|
+
expect(callOAuthCallback).toHaveBeenCalledOnce();
|
|
81
|
+
expect(container?.textContent).toContain("SIGN_UP_REJECTED");
|
|
82
|
+
expect(container?.textContent).toContain(errorMessage);
|
|
83
|
+
expect(app.redirectToSignIn).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
export { };
|
|
89
|
+
//# sourceMappingURL=oauth-callback.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-callback.test.js","names":[],"sources":["../../../src/components-page/oauth-callback.test.tsx"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n// @vitest-environment jsdom\n\nimport { KnownErrors } from \"@hexclave/shared\";\nimport React, { act } from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport type { StackClientApp } from \"../lib/hexclave-app/apps/interfaces/client-app\";\nimport { TranslationProviderClient } from \"../providers/translation-provider-client\";\nimport { OAuthCallback } from \"./oauth-callback\";\n\nconst appMockState = vi.hoisted(() => ({ app: null as unknown }));\n\nvi.mock(\"..\", () => ({\n useStackApp: () => {\n if (appMockState.app == null) {\n throw new Error(\"Expected test app to be set before rendering.\");\n }\n return appMockState.app;\n },\n}));\n\nvi.mock(\"@hexclave/ui\", async () => {\n const React = await import(\"react\");\n return {\n Button: (props: { children: React.ReactNode, onClick?: () => void }) => (\n <button type=\"button\" onClick={props.onClick}>{props.children}</button>\n ),\n Spinner: () => <div data-testid=\"spinner\" />,\n Typography: (props: { children: React.ReactNode }) => <div>{props.children}</div>,\n cn: (...classes: (string | false | null | undefined)[]) => classes.filter(Boolean).join(\" \"),\n };\n});\n\nconst previousActEnvironment = Reflect.get(globalThis, \"IS_REACT_ACT_ENVIRONMENT\");\n\nfunction createAppTestDouble(options: {\n callOAuthCallback: () => Promise<boolean>,\n}) {\n const app = {\n callOAuthCallback: options.callOAuthCallback,\n redirectToSignIn: vi.fn(async () => {}),\n redirectToHome: vi.fn(async () => {}),\n };\n\n // This test double intentionally implements only the StackClientApp surface\n // that OAuthCallback and the rendered error card touch.\n return app as unknown as StackClientApp<true>;\n}\n\nlet root: Root | null = null;\nlet container: HTMLDivElement | null = null;\n\nasync function renderWithApp(app: StackClientApp<true>) {\n appMockState.app = app;\n container = document.createElement(\"div\");\n document.body.append(container);\n root = createRoot(container);\n await act(async () => {\n root?.render(\n <TranslationProviderClient quetzalKeys={new Map()} quetzalLocale={new Map()}>\n <OAuthCallback />\n </TranslationProviderClient>\n );\n });\n}\n\nasync function flushEffects() {\n await act(async () => {\n await new Promise((resolve) => setTimeout(resolve, 0));\n });\n}\n\ndescribe(\"OAuthCallback\", () => {\n beforeEach(() => {\n Reflect.set(globalThis, \"IS_REACT_ACT_ENVIRONMENT\", true);\n });\n\n afterEach(() => {\n act(() => {\n root?.unmount();\n });\n container?.remove();\n root = null;\n container = null;\n appMockState.app = null;\n vi.restoreAllMocks();\n Reflect.set(globalThis, \"IS_REACT_ACT_ENVIRONMENT\", previousActEnvironment);\n });\n\n it(\"renders backend-encoded OAuth callback errors on the callback page\", async () => {\n const errorMessage = \"Your sign up was rejected by an administrator's sign-up rule.\";\n const callOAuthCallback = vi.fn(async () => {\n throw new KnownErrors.SignUpRejected(errorMessage);\n });\n const app = createAppTestDouble({ callOAuthCallback });\n\n await renderWithApp(app);\n await flushEffects();\n\n expect(callOAuthCallback).toHaveBeenCalledOnce();\n expect(container?.textContent).toContain(\"SIGN_UP_REJECTED\");\n expect(container?.textContent).toContain(errorMessage);\n expect(app.redirectToSignIn).not.toHaveBeenCalled();\n });\n});\n"],"mappings":";;;;;;;;;;AAcA,MAAM,eAAe,GAAG,eAAe,EAAE,KAAK,MAAiB,EAAE;AAEjE,GAAG,KAAK,aAAa,EACnB,mBAAmB;AACjB,KAAI,aAAa,OAAO,KACtB,OAAM,IAAI,MAAM,gDAAgD;AAElE,QAAO,aAAa;GAEvB,EAAE;AAEH,GAAG,KAAK,gBAAgB,YAAY;AACpB,OAAM,OAAO;AAC3B,QAAO;EACL,SAAS,UACP,oBAAC;GAAO,MAAK;GAAS,SAAS,MAAM;aAAU,MAAM;IAAkB;EAEzE,eAAe,oBAAC,SAAI,eAAY,YAAY;EAC5C,aAAa,UAAyC,oBAAC,mBAAK,MAAM,WAAe;EACjF,KAAK,GAAG,YAAmD,QAAQ,OAAO,QAAQ,CAAC,KAAK,IAAI;EAC7F;EACD;AAEF,MAAM,yBAAyB,QAAQ,IAAI,YAAY,2BAA2B;AAElF,SAAS,oBAAoB,SAE1B;AASD,QARY;EACV,mBAAmB,QAAQ;EAC3B,kBAAkB,GAAG,GAAG,YAAY,GAAG;EACvC,gBAAgB,GAAG,GAAG,YAAY,GAAG;EACtC;;AAOH,IAAI,OAAoB;AACxB,IAAI,YAAmC;AAEvC,eAAe,cAAc,KAA2B;AACtD,cAAa,MAAM;AACnB,aAAY,SAAS,cAAc,MAAM;AACzC,UAAS,KAAK,OAAO,UAAU;AAC/B,QAAO,WAAW,UAAU;AAC5B,OAAM,IAAI,YAAY;AACpB,QAAM,OACJ,oBAAC;GAA0B,6BAAa,IAAI,KAAK;GAAE,+BAAe,IAAI,KAAK;aACzE,oBAAC,kBAAgB;IACS,CAC7B;GACD;;AAGJ,eAAe,eAAe;AAC5B,OAAM,IAAI,YAAY;AACpB,QAAM,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;GACtD;;AAGJ,SAAS,uBAAuB;AAC9B,kBAAiB;AACf,UAAQ,IAAI,YAAY,4BAA4B,KAAK;GACzD;AAEF,iBAAgB;AACd,YAAU;AACR,SAAM,SAAS;IACf;AACF,aAAW,QAAQ;AACnB,SAAO;AACP,cAAY;AACZ,eAAa,MAAM;AACnB,KAAG,iBAAiB;AACpB,UAAQ,IAAI,YAAY,4BAA4B,uBAAuB;GAC3E;AAEF,IAAG,sEAAsE,YAAY;EACnF,MAAM,eAAe;EACrB,MAAM,oBAAoB,GAAG,GAAG,YAAY;AAC1C,SAAM,IAAI,YAAY,eAAe,aAAa;IAClD;EACF,MAAM,MAAM,oBAAoB,EAAE,mBAAmB,CAAC;AAEtD,QAAM,cAAc,IAAI;AACxB,QAAM,cAAc;AAEpB,SAAO,kBAAkB,CAAC,sBAAsB;AAChD,SAAO,WAAW,YAAY,CAAC,UAAU,mBAAmB;AAC5D,SAAO,WAAW,YAAY,CAAC,UAAU,aAAa;AACtD,SAAO,IAAI,iBAAiB,CAAC,IAAI,kBAAkB;GACnD;EACF"}
|