@autobusal/providers 1.2.11 → 1.3.0
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/CHANGELOG.md +18 -0
- package/Providers.tsx +8 -4
- package/Recaptcha/Recaptcha.tsx +29 -6
- package/Setup/Setup.tsx +2 -0
- package/Setup/useUserBootstrap.ts +52 -0
- package/package.json +1 -1
- package/stores/user.ts +30 -4
- package/types/settings.ts +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to `@autobusal/providers` are documented here. This project
|
|
|
4
4
|
adheres to [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.3.0] - 2026-07-25
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Captcha is now a runtime, per-label setting.** `Recaptcha.tsx` reads the
|
|
12
|
+
label's captcha config (provider + public site key) from `/api/settings/get`
|
|
13
|
+
via `useGetSettings()` and mounts `GoogleReCaptchaProvider` ONLY when the
|
|
14
|
+
provider is `recaptcha` (build-time `VITE_RECAPTCHA_KEY` remains as a
|
|
15
|
+
fallback). Cloudflare Turnstile needs no app-level provider. Because the
|
|
16
|
+
component now needs the query client, it moved INSIDE `<Queries>` in
|
|
17
|
+
`Providers.tsx`. `SettingsData`/`LabelSettings` types gained `captcha`.
|
|
18
|
+
|
|
19
|
+
## [1.2.12] - 2026-07-24
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- The full user profile - name, email, and via the nested admin/operator/agent/subagent/employee/driver/visitor sub-object: phone, address, passport, date of birth, company/NIPT - was persisted verbatim to localStorage, readable at rest by any script on the origin. Only a display-only subset (id, name, name_display, type, status) is now persisted; the full object still lives in-memory during an active session exactly as before. A new bootstrap fetch (wired into Setup) re-hydrates the full profile once per app load using the same reduced signal, guarded so it never fires for a genuine guest.
|
|
24
|
+
|
|
7
25
|
## [1.2.11] - 2026-07-19
|
|
8
26
|
|
|
9
27
|
### Fixed
|
package/Providers.tsx
CHANGED
|
@@ -20,9 +20,13 @@ const Providers = ({ type, loading, children }: Props): JSX.Element => (
|
|
|
20
20
|
// the initial settings fetch). The QueryClient in <Queries> is created at
|
|
21
21
|
// module scope, so remounting this subtree while suspended keeps the same
|
|
22
22
|
// client and cache - no refetch loop.
|
|
23
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
24
|
+
// Recaptcha moved INSIDE Queries: it now reads the label's runtime captcha
|
|
25
|
+
// settings via useGetSettings(), which needs the QueryClientProvider (and
|
|
26
|
+
// suspends into the outer boundary on a cold cache, like Styles/Setup).
|
|
23
27
|
<Suspense fallback={ loading }>
|
|
24
|
-
<
|
|
25
|
-
<
|
|
28
|
+
<Queries>
|
|
29
|
+
<Recaptcha>
|
|
26
30
|
<Styles>
|
|
27
31
|
<Notifications>
|
|
28
32
|
<Setup type={ type }>
|
|
@@ -32,8 +36,8 @@ const Providers = ({ type, loading, children }: Props): JSX.Element => (
|
|
|
32
36
|
</Setup>
|
|
33
37
|
</Notifications>
|
|
34
38
|
</Styles>
|
|
35
|
-
</
|
|
36
|
-
</
|
|
39
|
+
</Recaptcha>
|
|
40
|
+
</Queries>
|
|
37
41
|
</Suspense>
|
|
38
42
|
);
|
|
39
43
|
|
package/Recaptcha/Recaptcha.tsx
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
|
|
2
|
+
import { useGetSettings } from '../services';
|
|
2
3
|
|
|
3
4
|
interface Props {
|
|
4
5
|
children: JSX.Element
|
|
5
6
|
}
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
9
|
+
// The captcha provider + site key are now the label's runtime settings
|
|
10
|
+
// (/api/settings/get, managed at /admin/label?type=captcha) instead of the
|
|
11
|
+
// build-time VITE_RECAPTCHA_KEY - which stays only as a fallback for labels
|
|
12
|
+
// without a captcha config yet. The Google provider script is mounted ONLY
|
|
13
|
+
// when reCAPTCHA is the active provider; Cloudflare Turnstile needs no
|
|
14
|
+
// app-wide provider (its script is loaded on demand by useRecaptcha in
|
|
15
|
+
// @autobusal/hooks). NB: this component now calls useGetSettings(), so it
|
|
16
|
+
// must sit INSIDE <Queries> (see Providers.tsx).
|
|
17
|
+
const Recaptcha = ({ children }: Props): JSX.Element => {
|
|
18
|
+
const { data } = useGetSettings();
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
const provider = data?.captcha?.provider
|
|
21
|
+
?? (import.meta.env.VITE_RECAPTCHA_KEY ? 'recaptcha' : 'none');
|
|
22
|
+
|
|
23
|
+
const siteKey = data?.captcha?.site_key ?? import.meta.env.VITE_RECAPTCHA_KEY;
|
|
24
|
+
|
|
25
|
+
if (provider !== 'recaptcha' || !siteKey) {
|
|
26
|
+
return children;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<GoogleReCaptchaProvider reCaptchaKey={ siteKey }>
|
|
31
|
+
{ children }
|
|
32
|
+
</GoogleReCaptchaProvider>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default Recaptcha;
|
package/Setup/Setup.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Preload from './Preload';
|
|
2
2
|
import languages from './languages';
|
|
3
3
|
import analytics from './analytics';
|
|
4
|
+
import useUserBootstrap from './useUserBootstrap';
|
|
4
5
|
import { useGetSettings, useGetMenu } from '../services';
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
@@ -12,6 +13,7 @@ const Setup = ({ type, children }: Props): JSX.Element => {
|
|
|
12
13
|
const { data } = useGetSettings();
|
|
13
14
|
|
|
14
15
|
useGetMenu();
|
|
16
|
+
useUserBootstrap();
|
|
15
17
|
|
|
16
18
|
languages(type, data.preferences.languages.default);
|
|
17
19
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import apiClient, { isBffMode } from '../Queries/apiClient';
|
|
4
|
+
import { useUserStore } from '../stores/user';
|
|
5
|
+
import { UserData } from '../types/users';
|
|
6
|
+
|
|
7
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-24
|
|
8
|
+
// The user store now only persists a display-only subset to localStorage
|
|
9
|
+
// (see stores/user.ts) - the full profile (nested role data: phone, address,
|
|
10
|
+
// passport, company, etc.) is no longer restored on a cold page load. This
|
|
11
|
+
// hook re-fetches it once per app mount, so any component reading
|
|
12
|
+
// `data.visitor.phone` etc. gets it back a moment after load instead of
|
|
13
|
+
// instantly - the only user-visible change from this fix.
|
|
14
|
+
//
|
|
15
|
+
// This must NOT fire for a genuine guest: apiClient's response interceptor
|
|
16
|
+
// treats any 401 as "session is gone" and redirects to /account/logout
|
|
17
|
+
// (bearer mode immediately, bff mode after confirming via /session) - firing
|
|
18
|
+
// this unconditionally would send every anonymous visitor there. It only
|
|
19
|
+
// runs when there's an actual signal of a prior login: the bearer token
|
|
20
|
+
// (bearer mode) or the redacted user stub this same store persists (bff
|
|
21
|
+
// mode, where the token itself is an httpOnly cookie invisible to JS).
|
|
22
|
+
const hasLoginSignal = (): boolean => (
|
|
23
|
+
(!isBffMode && localStorage.getItem('token') !== null)
|
|
24
|
+
|| localStorage.getItem('user') !== null
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const useUserBootstrap = (): void => {
|
|
28
|
+
const user = useUserStore();
|
|
29
|
+
|
|
30
|
+
const enabled = hasLoginSignal();
|
|
31
|
+
|
|
32
|
+
const { data } = useQuery<UserData>({
|
|
33
|
+
queryKey: ['account-bootstrap'],
|
|
34
|
+
queryFn: async () => (
|
|
35
|
+
await apiClient
|
|
36
|
+
.get('/api/account/refresh')
|
|
37
|
+
.then(response => response.data)
|
|
38
|
+
),
|
|
39
|
+
enabled,
|
|
40
|
+
retry: false,
|
|
41
|
+
staleTime: Infinity
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (data) {
|
|
46
|
+
user.save(data);
|
|
47
|
+
}
|
|
48
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
49
|
+
}, [data]);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default useUserBootstrap;
|
package/package.json
CHANGED
package/stores/user.ts
CHANGED
|
@@ -7,19 +7,45 @@ export interface UserStore {
|
|
|
7
7
|
remove: () => void
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-24
|
|
11
|
+
// Bug: the full UserData object - name, email, and (via the nested
|
|
12
|
+
// admin/operator/agent/subagent/employee/driver/visitor sub-object) phone,
|
|
13
|
+
// address, passport, date of birth, company/NIPT - was persisted verbatim to
|
|
14
|
+
// localStorage, readable at rest by any script on the origin (any XSS,
|
|
15
|
+
// a malicious browser extension, disk/backup access on a shared machine).
|
|
16
|
+
// Fix: only a display-only subset - enough to paint the UI immediately on
|
|
17
|
+
// load - is ever written to disk. The full object still lives in the
|
|
18
|
+
// in-memory store during an active session exactly as before (nothing
|
|
19
|
+
// changes for any component reading `data.visitor.phone` etc. right after
|
|
20
|
+
// login/activate/refresh); it's just never written to disk. A bootstrap
|
|
21
|
+
// fetch (see Setup/useUserBootstrap.ts) re-hydrates the full in-memory
|
|
22
|
+
// object shortly after app load using this same reduced signal.
|
|
23
|
+
type PersistedUser = Pick<UserData, 'id' | 'name' | 'name_display' | 'type' | 'status'>;
|
|
24
|
+
|
|
25
|
+
const STORAGE_KEY = 'user';
|
|
26
|
+
|
|
27
|
+
const redact = (user: UserData): PersistedUser => ({
|
|
28
|
+
id: user.id,
|
|
29
|
+
name: user.name,
|
|
30
|
+
name_display: user.name_display,
|
|
31
|
+
type: user.type,
|
|
32
|
+
status: user.status
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
36
|
+
const initial: PersistedUser | undefined = stored ? JSON.parse(stored) : undefined;
|
|
11
37
|
|
|
12
38
|
export const useUserStore = create<UserStore>((set) => ({
|
|
13
|
-
data: initial,
|
|
39
|
+
data: initial as UserData | undefined,
|
|
14
40
|
save: (user) => set(() => {
|
|
15
|
-
localStorage.setItem(
|
|
41
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(redact(user)));
|
|
16
42
|
|
|
17
43
|
return {
|
|
18
44
|
data: user
|
|
19
45
|
};
|
|
20
46
|
}),
|
|
21
47
|
remove: () => set(() => {
|
|
22
|
-
localStorage.removeItem(
|
|
48
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
23
49
|
|
|
24
50
|
return {
|
|
25
51
|
data: undefined
|
package/types/settings.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
export interface SettingsData {
|
|
2
2
|
url: string
|
|
3
3
|
|
|
4
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
5
|
+
// Runtime captcha config (provider + PUBLIC site key) from
|
|
6
|
+
// /api/settings/get - replaces the build-time VITE_RECAPTCHA_KEY.
|
|
7
|
+
captcha?: {
|
|
8
|
+
provider: 'none' | 'recaptcha' | 'turnstile'
|
|
9
|
+
site_key: string | null
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
preferences: {
|
|
5
13
|
company: string
|
|
6
14
|
|
|
@@ -122,6 +130,21 @@ export interface LabelSettings {
|
|
|
122
130
|
client_secret: string
|
|
123
131
|
}
|
|
124
132
|
}
|
|
133
|
+
|
|
134
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
135
|
+
// Admin-managed captcha configuration; a single provider field makes
|
|
136
|
+
// reCAPTCHA and Turnstile mutually exclusive.
|
|
137
|
+
captcha?: {
|
|
138
|
+
provider: 'none' | 'recaptcha' | 'turnstile'
|
|
139
|
+
recaptcha: {
|
|
140
|
+
site_key?: string
|
|
141
|
+
secret_key?: string
|
|
142
|
+
}
|
|
143
|
+
turnstile: {
|
|
144
|
+
site_key?: string
|
|
145
|
+
secret_key?: string
|
|
146
|
+
}
|
|
147
|
+
}
|
|
125
148
|
}
|
|
126
149
|
|
|
127
150
|
// Edited: Ferjolt Ozuni - Date: 2026-07-18
|