akanjs 3.0.0-alpha.85 → 3.0.0-alpha.87
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/client/cookie.ts +41 -8
- package/common/authToken.ts +44 -0
- package/common/index.ts +8 -0
- package/fetch/client/fetchClient.ts +9 -2
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/server/resolver/signal.resolver.ts +2 -2
- package/server/routing/appWsData.ts +11 -2
- package/types/client/cookie.d.ts +7 -2
- package/types/common/authToken.d.ts +19 -0
- package/types/common/index.d.ts +1 -0
- package/types/ui/recipe/buttonRecipe.d.ts +2 -2
- package/ui/Load/Units.tsx +17 -14
- package/webkit/bootCsr.tsx +2 -2
package/client/cookie.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { getEnv } from "akanjs/base";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
authTokenKey,
|
|
4
|
+
decodeJwtPayload,
|
|
5
|
+
isOwnAuthToken,
|
|
6
|
+
Logger,
|
|
7
|
+
legacyAuthTokenKey,
|
|
8
|
+
readAuthToken,
|
|
9
|
+
} from "akanjs/common";
|
|
3
10
|
import type { Account } from "akanjs/fetch";
|
|
4
11
|
import { parseCookieHeader, cookies as serverCookies, headers as serverHeaders } from "akanjs/fetch";
|
|
5
12
|
import { loadCapacitorCore } from "./capacitor";
|
|
@@ -57,15 +64,29 @@ export const headers = (): Map<string, string> => {
|
|
|
57
64
|
export const getHeader = (key: string): string | undefined => {
|
|
58
65
|
return headers().get(key);
|
|
59
66
|
};
|
|
67
|
+
|
|
68
|
+
export { authTokenKey };
|
|
69
|
+
|
|
70
|
+
/** The auth token this app holds in the cookie jar, under the app-scoped key or a legacy global one. */
|
|
71
|
+
export const getAuthToken = (): string | undefined => readAuthToken(getCookie);
|
|
72
|
+
|
|
73
|
+
/** The auth token this app holds in client storage — localStorage under SSR, Capacitor Preferences on CSR. */
|
|
74
|
+
export const getStoredAuthToken = async (): Promise<string | undefined> => {
|
|
75
|
+
const scoped = await storage.getItem(authTokenKey());
|
|
76
|
+
if (scoped) return scoped;
|
|
77
|
+
const legacy = await storage.getItem(legacyAuthTokenKey);
|
|
78
|
+
return legacy && isOwnAuthToken(legacy) ? legacy : undefined;
|
|
79
|
+
};
|
|
80
|
+
|
|
60
81
|
/**
|
|
61
82
|
* Decodes the current JWT into account data when it belongs to this app/environment.
|
|
62
83
|
*
|
|
63
|
-
* The two credentials read here are exactly the two the server honours — the
|
|
84
|
+
* The two credentials read here are exactly the two the server honours — the app-scoped auth cookie and
|
|
64
85
|
* `Authorization: Bearer` (`AccountMiddleware`). It used to fall back to a bare `jwt` *header*, which nothing
|
|
65
86
|
* sends and no guard accepts, so a request carrying one read as signed in here and anonymous at the endpoint.
|
|
66
87
|
*/
|
|
67
88
|
export const getAccount = <AddData = unknown>(): Account<AddData> => {
|
|
68
|
-
const jwt =
|
|
89
|
+
const jwt = getAuthToken() ?? getHeader("authorization")?.replace(/^Bearer\s+/i, "");
|
|
69
90
|
const defaultAccount = { appName: getEnv().appName, environment: getEnv().environment } as Account<AddData>;
|
|
70
91
|
if (!jwt) return defaultAccount;
|
|
71
92
|
const account = decodeJwtPayload<Account<AddData>>(jwt);
|
|
@@ -81,15 +102,25 @@ interface SetAuthOption {
|
|
|
81
102
|
/** Sets the active auth token on fetch, cookie storage, and client storage. */
|
|
82
103
|
export const setAuth = ({ jwt }: SetAuthOption) => {
|
|
83
104
|
fetch.setJwt(jwt);
|
|
84
|
-
setCookie(
|
|
85
|
-
void storage.setItem(
|
|
105
|
+
setCookie(authTokenKey(), jwt);
|
|
106
|
+
void storage.setItem(authTokenKey(), jwt);
|
|
107
|
+
|
|
108
|
+
removeCookie(legacyAuthTokenKey);
|
|
109
|
+
void storage.removeItem(legacyAuthTokenKey);
|
|
86
110
|
};
|
|
87
111
|
|
|
88
112
|
interface InitAuthOption {
|
|
89
113
|
jwt?: string;
|
|
90
114
|
}
|
|
91
115
|
export const initAuth = ({ jwt }: InitAuthOption = {}) => {
|
|
92
|
-
const
|
|
116
|
+
const stored = getAuthToken();
|
|
117
|
+
const token = jwt ?? stored;
|
|
118
|
+
if (token && !isOwnAuthToken(token)) {
|
|
119
|
+
|
|
120
|
+
Logger.warn("JWT ignored: it was minted for another app or environment");
|
|
121
|
+
if (token === stored) resetAuth();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
93
124
|
if (token) setAuth({ jwt: token });
|
|
94
125
|
|
|
95
126
|
Logger.verbose(`JWT ${token ? "restored from cookie" : "not found in cookie"}`);
|
|
@@ -97,6 +128,8 @@ export const initAuth = ({ jwt }: InitAuthOption = {}) => {
|
|
|
97
128
|
|
|
98
129
|
export const resetAuth = () => {
|
|
99
130
|
fetch.setJwt(null);
|
|
100
|
-
removeCookie(
|
|
101
|
-
|
|
131
|
+
removeCookie(authTokenKey());
|
|
132
|
+
removeCookie(legacyAuthTokenKey);
|
|
133
|
+
void storage.removeItem(authTokenKey());
|
|
134
|
+
void storage.removeItem(legacyAuthTokenKey);
|
|
102
135
|
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getEnv } from "akanjs/base";
|
|
2
|
+
import { decodeJwtPayload } from "./jwtDecode";
|
|
3
|
+
|
|
4
|
+
/** The pre-scoping key. Read as a fallback, never written; drop it once deployments have rotated past it. */
|
|
5
|
+
export const legacyAuthTokenKey = "jwt";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Cookies carry no port (RFC 6265), so every app served from one host shares a jar — which on localhost is
|
|
9
|
+
* every app in the workspace at once. Under a global `jwt` key each signin overwrites the neighbour's
|
|
10
|
+
* session, and the JWT's own `appName` check only turns the stolen cookie into a failing credential.
|
|
11
|
+
*/
|
|
12
|
+
export const authTokenKey = (): string => `${legacyAuthTokenKey}:${getEnv().appName}`;
|
|
13
|
+
|
|
14
|
+
/** Matches both spellings so a signout can sweep a jar written before and after the key was scoped. */
|
|
15
|
+
export const isAuthTokenKey = (key: string): boolean =>
|
|
16
|
+
key === legacyAuthTokenKey || key.startsWith(`${legacyAuthTokenKey}:`);
|
|
17
|
+
|
|
18
|
+
const authCookiePattern = new RegExp(`(?:^|;\\s*)${legacyAuthTokenKey}(?::[^=\\s;]+)?=`);
|
|
19
|
+
|
|
20
|
+
/** Whether a raw `cookie` header carries an auth token under either spelling. */
|
|
21
|
+
export const cookieHeaderHasAuthToken = (cookieHeader: string | null | undefined): boolean =>
|
|
22
|
+
!!cookieHeader && authCookiePattern.test(cookieHeader);
|
|
23
|
+
|
|
24
|
+
/** Whether a token was minted for this app and environment — the same pair the server's account check makes. */
|
|
25
|
+
export const isOwnAuthToken = (jwt: string): boolean => {
|
|
26
|
+
const { appName, environment } = getEnv();
|
|
27
|
+
try {
|
|
28
|
+
const account = decodeJwtPayload<{ appName?: string; environment?: string }>(jwt);
|
|
29
|
+
return account.appName === appName && account.environment === environment;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reads the app-scoped token, falling back to a leftover global one only when its payload names this app —
|
|
37
|
+
* a neighbour's token would decode fine here and fail every guard, so adopting it is worse than none.
|
|
38
|
+
*/
|
|
39
|
+
export const readAuthToken = (read: (key: string) => string | null | undefined): string | undefined => {
|
|
40
|
+
const scoped = read(authTokenKey());
|
|
41
|
+
if (scoped) return scoped;
|
|
42
|
+
const legacy = read(legacyAuthTokenKey);
|
|
43
|
+
return legacy && isOwnAuthToken(legacy) ? legacy : undefined;
|
|
44
|
+
};
|
package/common/index.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export { applyMixins } from "./applyMixins";
|
|
2
|
+
export {
|
|
3
|
+
authTokenKey,
|
|
4
|
+
cookieHeaderHasAuthToken,
|
|
5
|
+
isAuthTokenKey,
|
|
6
|
+
isOwnAuthToken,
|
|
7
|
+
legacyAuthTokenKey,
|
|
8
|
+
readAuthToken,
|
|
9
|
+
} from "./authToken";
|
|
2
10
|
export { capitalize } from "./capitalize";
|
|
3
11
|
export { clamp } from "./clamp";
|
|
4
12
|
export {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { getEnv, PrimitiveRegistry, type PromiseOrObject } from "akanjs/base";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
capitalize,
|
|
4
|
+
type FetchPolicy,
|
|
5
|
+
fileUploadContract,
|
|
6
|
+
Logger,
|
|
7
|
+
readAuthToken,
|
|
8
|
+
resolveFileUploadCapability,
|
|
9
|
+
} from "akanjs/common";
|
|
3
10
|
import { type BaseInsight, type BaseObject, ConstantRegistry, deserialize, serialize } from "akanjs/constant";
|
|
4
11
|
import type {
|
|
5
12
|
DatabaseSignal,
|
|
@@ -272,7 +279,7 @@ export class FetchClient {
|
|
|
272
279
|
if (getEnv().side === "server") {
|
|
273
280
|
const authorization = requestHeaders().get("authorization");
|
|
274
281
|
if (authorization) return { Authorization: authorization };
|
|
275
|
-
const token = requestCookies().get(
|
|
282
|
+
const token = readAuthToken((key) => requestCookies().get(key)?.value);
|
|
276
283
|
if (token) return { Authorization: `Bearer ${token}` };
|
|
277
284
|
}
|
|
278
285
|
} catch {
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type PromiseOrObject,
|
|
13
13
|
SLICE_META,
|
|
14
14
|
} from "akanjs/base";
|
|
15
|
-
import { capitalize, Logger } from "akanjs/common";
|
|
15
|
+
import { capitalize, cookieHeaderHasAuthToken, Logger } from "akanjs/common";
|
|
16
16
|
import { deserialize, resolvePageLimit, resolvePageSkip, serialize } from "akanjs/constant";
|
|
17
17
|
import { documentQueryHelper } from "akanjs/document";
|
|
18
18
|
import {
|
|
@@ -581,7 +581,7 @@ export class SignalResolver {
|
|
|
581
581
|
}
|
|
582
582
|
|
|
583
583
|
static #hasAuthCredential(req: Request) {
|
|
584
|
-
return Boolean(req.headers.get("authorization") || req.headers.get("cookie")
|
|
584
|
+
return Boolean(req.headers.get("authorization") || cookieHeaderHasAuthToken(req.headers.get("cookie")));
|
|
585
585
|
}
|
|
586
586
|
|
|
587
587
|
/**
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
clientAddressFromHeaders,
|
|
3
|
+
clientPortFromHeaders,
|
|
4
|
+
forwardedHeaders,
|
|
5
|
+
isAuthTokenKey,
|
|
6
|
+
TrustedProxy,
|
|
7
|
+
} from "akanjs/common";
|
|
2
8
|
|
|
3
9
|
const CREDENTIAL_HEADERS = ["authorization", "cookie", "user-agent"] as const;
|
|
4
10
|
|
|
@@ -30,7 +36,10 @@ export class AppWsData {
|
|
|
30
36
|
if (jwt) data.headers.set("authorization", `Bearer ${jwt}`);
|
|
31
37
|
else {
|
|
32
38
|
data.headers.delete("authorization");
|
|
33
|
-
|
|
39
|
+
|
|
40
|
+
for (const name of [...data.cookies].map(([name]) => name)) {
|
|
41
|
+
if (isAuthTokenKey(name)) data.cookies.delete(name);
|
|
42
|
+
}
|
|
34
43
|
const cookie = [...data.cookies].map(([name, value]) => `${name}=${value}`).join("; ");
|
|
35
44
|
if (cookie) data.headers.set("cookie", cookie);
|
|
36
45
|
else data.headers.delete("cookie");
|
package/types/client/cookie.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { authTokenKey } from "akanjs/common";
|
|
1
2
|
import type { Account } from "akanjs/fetch";
|
|
2
3
|
interface CookieOptions {
|
|
3
4
|
path?: string;
|
|
@@ -20,10 +21,15 @@ export declare const removeCookie: (key: string, options?: {
|
|
|
20
21
|
}) => void;
|
|
21
22
|
export declare const headers: () => Map<string, string>;
|
|
22
23
|
export declare const getHeader: (key: string) => string | undefined;
|
|
24
|
+
export { authTokenKey };
|
|
25
|
+
/** The auth token this app holds in the cookie jar, under the app-scoped key or a legacy global one. */
|
|
26
|
+
export declare const getAuthToken: () => string | undefined;
|
|
27
|
+
/** The auth token this app holds in client storage — localStorage under SSR, Capacitor Preferences on CSR. */
|
|
28
|
+
export declare const getStoredAuthToken: () => Promise<string | undefined>;
|
|
23
29
|
/**
|
|
24
30
|
* Decodes the current JWT into account data when it belongs to this app/environment.
|
|
25
31
|
*
|
|
26
|
-
* The two credentials read here are exactly the two the server honours — the
|
|
32
|
+
* The two credentials read here are exactly the two the server honours — the app-scoped auth cookie and
|
|
27
33
|
* `Authorization: Bearer` (`AccountMiddleware`). It used to fall back to a bare `jwt` *header*, which nothing
|
|
28
34
|
* sends and no guard accepts, so a request carrying one read as signed in here and anonymous at the endpoint.
|
|
29
35
|
*/
|
|
@@ -41,4 +47,3 @@ interface InitAuthOption {
|
|
|
41
47
|
}
|
|
42
48
|
export declare const initAuth: ({ jwt }?: InitAuthOption) => void;
|
|
43
49
|
export declare const resetAuth: () => void;
|
|
44
|
-
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The pre-scoping key. Read as a fallback, never written; drop it once deployments have rotated past it. */
|
|
2
|
+
export declare const legacyAuthTokenKey = "jwt";
|
|
3
|
+
/**
|
|
4
|
+
* Cookies carry no port (RFC 6265), so every app served from one host shares a jar — which on localhost is
|
|
5
|
+
* every app in the workspace at once. Under a global `jwt` key each signin overwrites the neighbour's
|
|
6
|
+
* session, and the JWT's own `appName` check only turns the stolen cookie into a failing credential.
|
|
7
|
+
*/
|
|
8
|
+
export declare const authTokenKey: () => string;
|
|
9
|
+
/** Matches both spellings so a signout can sweep a jar written before and after the key was scoped. */
|
|
10
|
+
export declare const isAuthTokenKey: (key: string) => boolean;
|
|
11
|
+
/** Whether a raw `cookie` header carries an auth token under either spelling. */
|
|
12
|
+
export declare const cookieHeaderHasAuthToken: (cookieHeader: string | null | undefined) => boolean;
|
|
13
|
+
/** Whether a token was minted for this app and environment — the same pair the server's account check makes. */
|
|
14
|
+
export declare const isOwnAuthToken: (jwt: string) => boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Reads the app-scoped token, falling back to a leftover global one only when its payload names this app —
|
|
17
|
+
* a neighbour's token would decode fine here and fail every guard, so adopting it is worse than none.
|
|
18
|
+
*/
|
|
19
|
+
export declare const readAuthToken: (read: (key: string) => string | null | undefined) => string | undefined;
|
package/types/common/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { applyMixins } from "./applyMixins.d.ts";
|
|
2
|
+
export { authTokenKey, cookieHeaderHasAuthToken, isAuthTokenKey, isOwnAuthToken, legacyAuthTokenKey, readAuthToken, } from "./authToken.d.ts";
|
|
2
3
|
export { capitalize } from "./capitalize.d.ts";
|
|
3
4
|
export { clamp } from "./clamp.d.ts";
|
|
4
5
|
export { clientAddressFromHeaders, clientPortFromHeaders, forwardedHeaders, normalizeIpAddress, } from "./clientAddress.d.ts";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export declare const buttonRecipe: (variants?: Omit<({
|
|
3
3
|
size?: "lg" | "md" | "sm" | "xs" | "icon" | undefined;
|
|
4
4
|
outline?: boolean | undefined;
|
|
5
|
-
variant?: "
|
|
5
|
+
variant?: "link" | "default" | "info" | "success" | "warning" | "primary" | "secondary" | "accent" | "destructive" | "neutral" | "outline" | "ghost" | undefined;
|
|
6
6
|
shape?: "default" | "circle" | "square" | undefined;
|
|
7
7
|
} & {
|
|
8
8
|
class?: import("tailwind-variants").ClassValue;
|
|
@@ -10,7 +10,7 @@ export declare const buttonRecipe: (variants?: Omit<({
|
|
|
10
10
|
}) | ({
|
|
11
11
|
size?: "lg" | "md" | "sm" | "xs" | "icon" | undefined;
|
|
12
12
|
outline?: boolean | undefined;
|
|
13
|
-
variant?: "
|
|
13
|
+
variant?: "link" | "default" | "info" | "success" | "warning" | "primary" | "secondary" | "accent" | "destructive" | "neutral" | "outline" | "ghost" | undefined;
|
|
14
14
|
shape?: "default" | "circle" | "square" | undefined;
|
|
15
15
|
} & {
|
|
16
16
|
class?: never;
|
package/ui/Load/Units.tsx
CHANGED
|
@@ -65,7 +65,7 @@ function Render<RefName extends string, Light extends { id: string }>({
|
|
|
65
65
|
pagination,
|
|
66
66
|
staleTime,
|
|
67
67
|
}: RenderProps<RefName, Light>) {
|
|
68
|
-
const
|
|
68
|
+
const loadedQueryArgs = useRef<object[] | null>(null);
|
|
69
69
|
const storeUse = st.use as { [key: string]: () => unknown };
|
|
70
70
|
const storeDo = st.do as unknown as { [key: string]: (...args: any[]) => Promise<void> };
|
|
71
71
|
const storeGet = st.get as unknown as <T>() => { [key: string]: T };
|
|
@@ -114,23 +114,26 @@ function Render<RefName extends string, Light extends { id: string }>({
|
|
|
114
114
|
const initModelObjInsight = (init as DynamicRecord)[names.modelObjInsight] as BaseInsight | null;
|
|
115
115
|
const initLimitOfModel = (init as DynamicRecord)[names.limitOfModel] as number;
|
|
116
116
|
const initPageOfModel = (init as DynamicRecord)[names.pageOfModel] as number;
|
|
117
|
+
const initSignature = JSON.stringify(initQueryArgs);
|
|
117
118
|
|
|
118
119
|
const useCache =
|
|
119
120
|
!modelListLoading &&
|
|
120
121
|
isQueryEqual(storeGet<object[]>()[namesOfSlice.queryArgsOfModel], initQueryArgs) &&
|
|
121
122
|
storeGet<Date>()[namesOfSlice.modelInitAt].getTime() >= initModelInitAt.getTime();
|
|
122
|
-
if (useCache)
|
|
123
|
+
if (useCache) loadedQueryArgs.current = initQueryArgs;
|
|
124
|
+
|
|
125
|
+
const loaded = !!loadedQueryArgs.current && isQueryEqual(loadedQueryArgs.current, initQueryArgs);
|
|
123
126
|
|
|
124
127
|
const modelInitList = useMemo<DataList<Light>>(() => {
|
|
125
|
-
if (loaded
|
|
128
|
+
if (loaded) return modelList;
|
|
126
129
|
const initModelObjList = (init as DynamicRecord)[names.modelObjList] as Light[];
|
|
127
130
|
return new DataList<Light>(
|
|
128
131
|
withSharedInstances(() => initModelObjList.map((model) => new cnst.light().set(model) as unknown as Light)),
|
|
129
132
|
);
|
|
130
|
-
}, []);
|
|
133
|
+
}, [initSignature]);
|
|
131
134
|
|
|
132
135
|
useEffect(() => {
|
|
133
|
-
if (loaded
|
|
136
|
+
if (loaded) return;
|
|
134
137
|
const modelObjInsight = (init as DynamicRecord)[names.modelObjInsight] as BaseInsight | null;
|
|
135
138
|
|
|
136
139
|
const insight = new cnst.insight().set(
|
|
@@ -153,8 +156,8 @@ function Render<RefName extends string, Light extends { id: string }>({
|
|
|
153
156
|
[namesOfSlice.queryArgsOfModel]: initQueryArgsOfModel,
|
|
154
157
|
[namesOfSlice.sortOfModel]: initSortOfModel,
|
|
155
158
|
});
|
|
156
|
-
|
|
157
|
-
}, []);
|
|
159
|
+
loadedQueryArgs.current = initQueryArgs;
|
|
160
|
+
}, [initSignature]);
|
|
158
161
|
|
|
159
162
|
useEffect(() => {
|
|
160
163
|
const modelStaleAt = storeGet<Date>()[namesOfSlice.modelStaleAt];
|
|
@@ -162,15 +165,15 @@ function Render<RefName extends string, Light extends { id: string }>({
|
|
|
162
165
|
if (storeGet<Date>()[namesOfSlice.modelInitAt].getTime() >= staleThreshold) return;
|
|
163
166
|
if (storeGet<boolean>()[namesOfSlice.modelListLoading]) return;
|
|
164
167
|
void storeDo[namesOfSlice.refreshModel]({ invalidate: true });
|
|
165
|
-
}, []);
|
|
168
|
+
}, [initSignature]);
|
|
166
169
|
|
|
167
170
|
const modelInsight = storeUse[namesOfSlice.modelInsight]() as BaseInsight;
|
|
168
171
|
const limitOfModel = storeUse[namesOfSlice.limitOfModel]() as number;
|
|
169
172
|
const pageOfModel = storeUse[namesOfSlice.pageOfModel]() as number;
|
|
170
|
-
const insight = loaded
|
|
171
|
-
const limit = loaded
|
|
172
|
-
const page = loaded
|
|
173
|
-
const total = insight?.count ?? (loaded
|
|
173
|
+
const insight = loaded ? modelInsight : initModelObjInsight;
|
|
174
|
+
const limit = loaded ? limitOfModel : initLimitOfModel;
|
|
175
|
+
const page = loaded ? pageOfModel : initPageOfModel;
|
|
176
|
+
const total = insight?.count ?? (loaded ? modelList : modelInitList).length;
|
|
174
177
|
const moreProps = {
|
|
175
178
|
total,
|
|
176
179
|
currentPage: page,
|
|
@@ -197,7 +200,7 @@ function Render<RefName extends string, Light extends { id: string }>({
|
|
|
197
200
|
onSelect: (page) => moreProps.onPageSelect(page, { scrollToTop: false }),
|
|
198
201
|
});
|
|
199
202
|
|
|
200
|
-
const modelDataList = !loaded
|
|
203
|
+
const modelDataList = !loaded ? modelInitList.filter(filter).sort(sort) : modelList.filter(filter).sort(sort);
|
|
201
204
|
const scopePath = useScreenScope({
|
|
202
205
|
id: sliceName,
|
|
203
206
|
kind: refName,
|
|
@@ -207,7 +210,7 @@ function Render<RefName extends string, Light extends { id: string }>({
|
|
|
207
210
|
return { id: item.id, ...(label ? { label } : {}) };
|
|
208
211
|
}),
|
|
209
212
|
});
|
|
210
|
-
const showLoading = loaded
|
|
213
|
+
const showLoading = loaded && modelListLoading;
|
|
211
214
|
if (renderList)
|
|
212
215
|
return (
|
|
213
216
|
<>
|
package/webkit/bootCsr.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
csrContext,
|
|
4
4
|
Device,
|
|
5
5
|
getExplicitPageConfigKeys,
|
|
6
|
+
getStoredAuthToken,
|
|
6
7
|
initAuth,
|
|
7
8
|
type LayoutModule,
|
|
8
9
|
type PageConfig,
|
|
@@ -13,7 +14,6 @@ import {
|
|
|
13
14
|
type RouteRender,
|
|
14
15
|
readCssSafeAreaInsets,
|
|
15
16
|
resolvePageState,
|
|
16
|
-
storage,
|
|
17
17
|
validatePageConfig,
|
|
18
18
|
} from "akanjs/client";
|
|
19
19
|
import {
|
|
@@ -86,7 +86,7 @@ export const bootCsr = async (context: Record<string, CsrRouteModuleEntry>) => {
|
|
|
86
86
|
const pathname = mobileBasePath && window.location.pathname === "/" ? `/${mobileBasePath}` : window.location.pathname;
|
|
87
87
|
if (pathname === "/404") return;
|
|
88
88
|
|
|
89
|
-
const [device, jwt] = await Promise.all([Device.load({ supportLanguages: i18n.locales }),
|
|
89
|
+
const [device, jwt] = await Promise.all([Device.load({ supportLanguages: i18n.locales }), getStoredAuthToken()]);
|
|
90
90
|
if (!window.__AKAN_MOBILE_TARGET__ && !pathname.startsWith(`/${device.lang}`))
|
|
91
91
|
window.location.replace(`/${device.lang}${pathname}${window.location.search}${window.location.hash}`);
|
|
92
92
|
|