@djangocfg/api 2.1.448 → 2.1.450
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/dist/auth-server.cjs +1 -2272
- package/dist/auth-server.cjs.map +1 -1
- package/dist/auth-server.d.cts +1 -35
- package/dist/auth-server.d.ts +1 -35
- package/dist/auth-server.mjs +1 -2272
- package/dist/auth-server.mjs.map +1 -1
- package/dist/auth.cjs +285 -146
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +137 -2
- package/dist/auth.d.ts +137 -2
- package/dist/auth.mjs +294 -155
- package/dist/auth.mjs.map +1 -1
- package/dist/clients.cjs +35 -0
- package/dist/clients.cjs.map +1 -1
- package/dist/clients.d.cts +147 -2
- package/dist/clients.d.ts +147 -2
- package/dist/clients.mjs +35 -0
- package/dist/clients.mjs.map +1 -1
- package/dist/hooks.cjs +11 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.mjs +11 -0
- package/dist/hooks.mjs.map +1 -1
- package/dist/index.cjs +35 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +169 -6
- package/dist/index.d.ts +169 -6
- package/dist/index.mjs +35 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -4
- package/src/_api/generated/_cfg_accounts/api.ts +8 -0
- package/src/_api/generated/_cfg_accounts/openapi.json +84 -5
- package/src/_api/generated/_cfg_accounts/schemas/OAuthTokenResponse.ts +2 -2
- package/src/_api/generated/_cfg_accounts/schemas/OTPRequestResponse.ts +2 -0
- package/src/_api/generated/_cfg_accounts/schemas/WebmailLink.ts +15 -0
- package/src/_api/generated/_cfg_accounts/schemas/WebmailLinkProviderEnum.ts +9 -0
- package/src/_api/generated/_cfg_accounts/schemas/index.ts +2 -0
- package/src/_api/generated/_cfg_centrifugo/api.ts +8 -0
- package/src/_api/generated/_cfg_totp/api.ts +8 -0
- package/src/_api/generated/helpers/auth.ts +12 -0
- package/src/_api/generated/openapi.json +84 -5
- package/src/_api/generated/types.gen.ts +131 -2
- package/src/auth/__tests__/jwt.test.ts +119 -0
- package/src/auth/__tests__/onUnauthorized.test.ts +206 -0
- package/src/auth/__tests__/refreshRotation.test.ts +119 -0
- package/src/auth/__tests__/sessionBootstrap.test.ts +76 -0
- package/src/auth/context/AuthContext.tsx +131 -14
- package/src/auth/hooks/useAuthForm.ts +5 -2
- package/src/auth/hooks/useAuthFormState.ts +4 -1
- package/src/auth/hooks/useTokenRefresh.ts +28 -62
- package/src/auth/middlewares/index.ts +7 -7
- package/src/auth/refreshHandler.ts +79 -0
- package/src/auth/types/form.ts +10 -0
- package/src/auth/types/index.ts +1 -0
- package/src/auth/utils/jwt.ts +66 -0
- package/src/auth/middlewares/tokenRefresh.ts +0 -161
package/dist/auth.cjs
CHANGED
|
@@ -223,6 +223,7 @@ var useAuthFormState = /* @__PURE__ */ __name((initialIdentifier = "") => {
|
|
|
223
223
|
const [acceptedTerms, setAcceptedTerms] = (0, import_react3.useState)(true);
|
|
224
224
|
const [step, setStep] = (0, import_react3.useState)("identifier");
|
|
225
225
|
const [error, setError] = (0, import_react3.useState)("");
|
|
226
|
+
const [webmail, setWebmail] = (0, import_react3.useState)(null);
|
|
226
227
|
const [twoFactorSessionId, setTwoFactorSessionId] = (0, import_react3.useState)(null);
|
|
227
228
|
const [shouldPrompt2FA, setShouldPrompt2FA] = (0, import_react3.useState)(false);
|
|
228
229
|
const [twoFactorCode, setTwoFactorCode] = (0, import_react3.useState)("");
|
|
@@ -262,6 +263,7 @@ var useAuthFormState = /* @__PURE__ */ __name((initialIdentifier = "") => {
|
|
|
262
263
|
rateLimitSeconds,
|
|
263
264
|
isRateLimited: rateLimitSeconds > 0,
|
|
264
265
|
rateLimitLabel: formatCountdown(rateLimitSeconds),
|
|
266
|
+
webmail,
|
|
265
267
|
// Handlers
|
|
266
268
|
setIdentifier,
|
|
267
269
|
setOtp,
|
|
@@ -274,7 +276,8 @@ var useAuthFormState = /* @__PURE__ */ __name((initialIdentifier = "") => {
|
|
|
274
276
|
setShouldPrompt2FA,
|
|
275
277
|
setTwoFactorCode,
|
|
276
278
|
setUseBackupCode,
|
|
277
|
-
startRateLimitCountdown
|
|
279
|
+
startRateLimitCountdown,
|
|
280
|
+
setWebmail
|
|
278
281
|
};
|
|
279
282
|
}, "useAuthFormState");
|
|
280
283
|
|
|
@@ -660,6 +663,17 @@ var auth = {
|
|
|
660
663
|
*/
|
|
661
664
|
setRefreshHandler(fn) {
|
|
662
665
|
_refreshHandler = fn;
|
|
666
|
+
},
|
|
667
|
+
/**
|
|
668
|
+
* Proactively run the registered refresh handler right now, reusing the
|
|
669
|
+
* SAME single-flight promise as the 401-recovery interceptor. Callers
|
|
670
|
+
* (e.g. an expiry timer / focus / reconnect) get token rotation,
|
|
671
|
+
* de-duplication and rotated-token persistence for free — they must NOT
|
|
672
|
+
* re-implement any of it. Returns the fresh access token, or null if
|
|
673
|
+
* there is no handler / no refresh token / the refresh failed.
|
|
674
|
+
*/
|
|
675
|
+
refreshNow() {
|
|
676
|
+
return tryRefresh();
|
|
663
677
|
}
|
|
664
678
|
};
|
|
665
679
|
async function tryRefresh() {
|
|
@@ -2383,6 +2397,14 @@ var API = class {
|
|
|
2383
2397
|
setRefreshHandler(fn) {
|
|
2384
2398
|
auth.setRefreshHandler(fn);
|
|
2385
2399
|
}
|
|
2400
|
+
/**
|
|
2401
|
+
* Proactively refresh now via the registered handler, sharing the single
|
|
2402
|
+
* 401-recovery flight (rotation + dedup + rotated-token persistence).
|
|
2403
|
+
* See `auth.refreshNow`.
|
|
2404
|
+
*/
|
|
2405
|
+
refreshNow() {
|
|
2406
|
+
return auth.refreshNow();
|
|
2407
|
+
}
|
|
2386
2408
|
};
|
|
2387
2409
|
|
|
2388
2410
|
// src/_api/generated/_cfg_centrifugo/api.ts
|
|
@@ -2447,6 +2469,14 @@ var API2 = class {
|
|
|
2447
2469
|
setRefreshHandler(fn) {
|
|
2448
2470
|
auth.setRefreshHandler(fn);
|
|
2449
2471
|
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Proactively refresh now via the registered handler, sharing the single
|
|
2474
|
+
* 401-recovery flight (rotation + dedup + rotated-token persistence).
|
|
2475
|
+
* See `auth.refreshNow`.
|
|
2476
|
+
*/
|
|
2477
|
+
refreshNow() {
|
|
2478
|
+
return auth.refreshNow();
|
|
2479
|
+
}
|
|
2450
2480
|
};
|
|
2451
2481
|
|
|
2452
2482
|
// src/_api/generated/_cfg_totp/api.ts
|
|
@@ -2514,6 +2544,14 @@ var API3 = class {
|
|
|
2514
2544
|
setRefreshHandler(fn) {
|
|
2515
2545
|
auth.setRefreshHandler(fn);
|
|
2516
2546
|
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Proactively refresh now via the registered handler, sharing the single
|
|
2549
|
+
* 401-recovery flight (rotation + dedup + rotated-token persistence).
|
|
2550
|
+
* See `auth.refreshNow`.
|
|
2551
|
+
*/
|
|
2552
|
+
refreshNow() {
|
|
2553
|
+
return auth.refreshNow();
|
|
2554
|
+
}
|
|
2517
2555
|
};
|
|
2518
2556
|
|
|
2519
2557
|
// src/_api/generated/index.ts
|
|
@@ -2734,7 +2772,8 @@ var useAuthForm = /* @__PURE__ */ __name((options) => {
|
|
|
2734
2772
|
setShouldPrompt2FA,
|
|
2735
2773
|
setTwoFactorCode,
|
|
2736
2774
|
setUseBackupCode,
|
|
2737
|
-
startRateLimitCountdown
|
|
2775
|
+
startRateLimitCountdown,
|
|
2776
|
+
setWebmail
|
|
2738
2777
|
} = formState;
|
|
2739
2778
|
const twoFactor = useTwoFactor({
|
|
2740
2779
|
onSuccess: /* @__PURE__ */ __name(() => {
|
|
@@ -2786,6 +2825,7 @@ var useAuthForm = /* @__PURE__ */ __name((options) => {
|
|
|
2786
2825
|
const result = await requestOTP(identifier, sourceUrl);
|
|
2787
2826
|
if (result.success) {
|
|
2788
2827
|
saveIdentifierToStorage(identifier);
|
|
2828
|
+
setWebmail(result.webmail ?? null);
|
|
2789
2829
|
setStep("otp");
|
|
2790
2830
|
onIdentifierSuccess?.(identifier);
|
|
2791
2831
|
} else {
|
|
@@ -2815,6 +2855,7 @@ var useAuthForm = /* @__PURE__ */ __name((options) => {
|
|
|
2815
2855
|
setIsLoading,
|
|
2816
2856
|
setStep,
|
|
2817
2857
|
clearError,
|
|
2858
|
+
setWebmail,
|
|
2818
2859
|
startRateLimitCountdown,
|
|
2819
2860
|
onIdentifierSuccess,
|
|
2820
2861
|
onError,
|
|
@@ -2870,6 +2911,7 @@ var useAuthForm = /* @__PURE__ */ __name((options) => {
|
|
|
2870
2911
|
const result = await requestOTP(identifier, sourceUrl);
|
|
2871
2912
|
if (result.success) {
|
|
2872
2913
|
saveIdentifierToStorage(identifier);
|
|
2914
|
+
setWebmail(result.webmail ?? null);
|
|
2873
2915
|
setOtp("");
|
|
2874
2916
|
} else {
|
|
2875
2917
|
if (result.retryAfter) {
|
|
@@ -2887,7 +2929,7 @@ var useAuthForm = /* @__PURE__ */ __name((options) => {
|
|
|
2887
2929
|
} finally {
|
|
2888
2930
|
setIsLoading(false);
|
|
2889
2931
|
}
|
|
2890
|
-
}, [identifier, requestOTP, saveIdentifierToStorage, setOtp, setError, setIsLoading, clearError, startRateLimitCountdown, onError, sourceUrl]);
|
|
2932
|
+
}, [identifier, requestOTP, saveIdentifierToStorage, setOtp, setError, setIsLoading, clearError, startRateLimitCountdown, setWebmail, onError, sourceUrl]);
|
|
2891
2933
|
const handleBackToIdentifier = (0, import_react7.useCallback)(() => {
|
|
2892
2934
|
setStep("identifier");
|
|
2893
2935
|
clearError();
|
|
@@ -3790,50 +3832,90 @@ __name(getCacheMetadata, "getCacheMetadata");
|
|
|
3790
3832
|
|
|
3791
3833
|
// src/auth/hooks/useTokenRefresh.ts
|
|
3792
3834
|
var import_react14 = require("react");
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3835
|
+
|
|
3836
|
+
// src/auth/refreshHandler.ts
|
|
3837
|
+
var _registered = false;
|
|
3838
|
+
function ensureRefreshHandler() {
|
|
3839
|
+
if (_registered) return;
|
|
3840
|
+
_registered = true;
|
|
3841
|
+
CfgAccountsApi.setRefreshHandler(async (refresh) => {
|
|
3842
|
+
const result = await CfgAccountsAuth.cfgAccountsTokenRefreshCreate({
|
|
3843
|
+
body: { refresh },
|
|
3844
|
+
throwOnError: true
|
|
3845
|
+
});
|
|
3846
|
+
const access = result.data?.access;
|
|
3847
|
+
if (!access) {
|
|
3848
|
+
authLogger.error("Refresh response missing access token");
|
|
3849
|
+
return null;
|
|
3850
|
+
}
|
|
3851
|
+
return { access, refresh: result.data?.refresh ?? refresh };
|
|
3852
|
+
});
|
|
3853
|
+
authLogger.debug("Canonical refresh handler registered");
|
|
3854
|
+
}
|
|
3855
|
+
__name(ensureRefreshHandler, "ensureRefreshHandler");
|
|
3856
|
+
function triggerRefresh() {
|
|
3857
|
+
ensureRefreshHandler();
|
|
3858
|
+
const maybe = CfgAccountsApi;
|
|
3859
|
+
if (typeof maybe.refreshNow === "function") {
|
|
3860
|
+
return maybe.refreshNow();
|
|
3861
|
+
}
|
|
3862
|
+
authLogger.warn("api.refreshNow() unavailable \u2014 run `make generate` to regenerate the client");
|
|
3863
|
+
return Promise.resolve(null);
|
|
3864
|
+
}
|
|
3865
|
+
__name(triggerRefresh, "triggerRefresh");
|
|
3866
|
+
|
|
3867
|
+
// src/auth/utils/jwt.ts
|
|
3868
|
+
function decodeSegment(segment) {
|
|
3796
3869
|
try {
|
|
3797
|
-
const
|
|
3798
|
-
|
|
3870
|
+
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
3871
|
+
if (typeof atob !== "function") return null;
|
|
3872
|
+
const json = atob(base64);
|
|
3873
|
+
return JSON.parse(json);
|
|
3799
3874
|
} catch {
|
|
3800
3875
|
return null;
|
|
3801
3876
|
}
|
|
3802
3877
|
}
|
|
3878
|
+
__name(decodeSegment, "decodeSegment");
|
|
3879
|
+
function getTokenExpiry(token) {
|
|
3880
|
+
if (!token || typeof token !== "string") return null;
|
|
3881
|
+
const parts = token.split(".");
|
|
3882
|
+
if (parts.length !== 3) return null;
|
|
3883
|
+
const payload = decodeSegment(parts[1]);
|
|
3884
|
+
if (!payload || typeof payload !== "object") return null;
|
|
3885
|
+
const exp = payload.exp;
|
|
3886
|
+
if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
|
|
3887
|
+
return exp * 1e3;
|
|
3888
|
+
}
|
|
3803
3889
|
__name(getTokenExpiry, "getTokenExpiry");
|
|
3804
|
-
function
|
|
3890
|
+
function isTokenExpired(token, skewMs = 0, now = Date.now()) {
|
|
3891
|
+
const expiry = getTokenExpiry(token);
|
|
3892
|
+
if (expiry === null) return true;
|
|
3893
|
+
return expiry - skewMs <= now;
|
|
3894
|
+
}
|
|
3895
|
+
__name(isTokenExpired, "isTokenExpired");
|
|
3896
|
+
function isTokenExpiringSoon(token, thresholdMs, now = Date.now()) {
|
|
3805
3897
|
const expiry = getTokenExpiry(token);
|
|
3806
|
-
if (
|
|
3807
|
-
|
|
3808
|
-
return timeUntilExpiry < thresholdMs;
|
|
3898
|
+
if (expiry === null) return false;
|
|
3899
|
+
return expiry - now < thresholdMs;
|
|
3809
3900
|
}
|
|
3810
3901
|
__name(isTokenExpiringSoon, "isTokenExpiringSoon");
|
|
3902
|
+
|
|
3903
|
+
// src/auth/hooks/useTokenRefresh.ts
|
|
3904
|
+
var TOKEN_REFRESH_THRESHOLD_MS = 10 * 60 * 1e3;
|
|
3905
|
+
var CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
3811
3906
|
function useTokenRefresh(options = {}) {
|
|
3812
3907
|
const { enabled = true, onRefresh, onRefreshError } = options;
|
|
3813
|
-
const isRefreshingRef = (0, import_react14.useRef)(false);
|
|
3814
3908
|
const refreshToken = (0, import_react14.useCallback)(async () => {
|
|
3815
|
-
if (
|
|
3816
|
-
authLogger.debug("Token refresh already in progress");
|
|
3817
|
-
return false;
|
|
3818
|
-
}
|
|
3819
|
-
const refreshTokenValue = CfgAccountsApi.getRefreshToken();
|
|
3820
|
-
if (!refreshTokenValue) {
|
|
3909
|
+
if (!CfgAccountsApi.getRefreshToken()) {
|
|
3821
3910
|
authLogger.warn("No refresh token available");
|
|
3822
3911
|
return false;
|
|
3823
3912
|
}
|
|
3824
|
-
isRefreshingRef.current = true;
|
|
3825
3913
|
authLogger.info("Refreshing token...");
|
|
3826
3914
|
try {
|
|
3827
|
-
const
|
|
3828
|
-
body: { refresh: refreshTokenValue },
|
|
3829
|
-
throwOnError: true
|
|
3830
|
-
});
|
|
3831
|
-
const newAccessToken = result.data.access;
|
|
3915
|
+
const newAccessToken = await triggerRefresh();
|
|
3832
3916
|
if (!newAccessToken) {
|
|
3833
|
-
throw new Error("
|
|
3917
|
+
throw new Error("Token refresh failed");
|
|
3834
3918
|
}
|
|
3835
|
-
CfgAccountsApi.setToken(newAccessToken);
|
|
3836
|
-
CfgAccountsApi.setRefreshToken(refreshTokenValue);
|
|
3837
3919
|
authLogger.info("Token refreshed successfully");
|
|
3838
3920
|
onRefresh?.(newAccessToken);
|
|
3839
3921
|
return true;
|
|
@@ -3841,8 +3923,6 @@ function useTokenRefresh(options = {}) {
|
|
|
3841
3923
|
authLogger.error("Token refresh error:", error);
|
|
3842
3924
|
onRefreshError?.(error instanceof Error ? error : new Error(String(error)));
|
|
3843
3925
|
return false;
|
|
3844
|
-
} finally {
|
|
3845
|
-
isRefreshingRef.current = false;
|
|
3846
3926
|
}
|
|
3847
3927
|
}, [onRefresh, onRefreshError]);
|
|
3848
3928
|
const checkAndRefresh = (0, import_react14.useCallback)(async () => {
|
|
@@ -4025,8 +4105,8 @@ var OAuthTokenResponseSchema = import_zod10.z.object({
|
|
|
4025
4105
|
access: import_zod10.z.string().nullable().optional(),
|
|
4026
4106
|
refresh: import_zod10.z.string().nullable().optional(),
|
|
4027
4107
|
user: import_zod10.z.object({}).passthrough().nullable().optional(),
|
|
4028
|
-
is_new_user: import_zod10.z.boolean(),
|
|
4029
|
-
is_new_connection: import_zod10.z.boolean(),
|
|
4108
|
+
is_new_user: import_zod10.z.boolean().default(false).optional(),
|
|
4109
|
+
is_new_connection: import_zod10.z.boolean().default(false).optional(),
|
|
4030
4110
|
should_prompt_2fa: import_zod10.z.boolean().optional()
|
|
4031
4111
|
});
|
|
4032
4112
|
|
|
@@ -4043,9 +4123,27 @@ var OAuthProvidersResponseSchema = import_zod11.z.object({
|
|
|
4043
4123
|
var import_mutation7 = __toESM(require("swr/mutation"), 1);
|
|
4044
4124
|
|
|
4045
4125
|
// src/_api/generated/_cfg_accounts/schemas/OTPRequestResponse.ts
|
|
4126
|
+
var import_zod14 = require("zod");
|
|
4127
|
+
|
|
4128
|
+
// src/_api/generated/_cfg_accounts/schemas/WebmailLink.ts
|
|
4129
|
+
var import_zod13 = require("zod");
|
|
4130
|
+
|
|
4131
|
+
// src/_api/generated/_cfg_accounts/schemas/WebmailLinkProviderEnum.ts
|
|
4046
4132
|
var import_zod12 = require("zod");
|
|
4047
|
-
var
|
|
4048
|
-
|
|
4133
|
+
var WebmailLinkProviderEnumSchema = import_zod12.z.enum(["gmail", "outlook", "yahoo", "icloud", "proton", "zoho", "aol", "fastmail", "gmx", "mailcom", "mail_ru", "yandex", "rambler", "qq", "netease", "sina", "aliyun", "naver", "daum", "web_de", "tonline", "seznam", "wp_pl", "o2_pl", "interia", "libero", "virgilio", "orange", "laposte", "free_fr", "sfr"]);
|
|
4134
|
+
|
|
4135
|
+
// src/_api/generated/_cfg_accounts/schemas/WebmailLink.ts
|
|
4136
|
+
var WebmailLinkSchema = import_zod13.z.object({
|
|
4137
|
+
provider: WebmailLinkProviderEnumSchema,
|
|
4138
|
+
provider_name: import_zod13.z.string(),
|
|
4139
|
+
url: import_zod13.z.string(),
|
|
4140
|
+
is_search: import_zod13.z.boolean()
|
|
4141
|
+
});
|
|
4142
|
+
|
|
4143
|
+
// src/_api/generated/_cfg_accounts/schemas/OTPRequestResponse.ts
|
|
4144
|
+
var OTPRequestResponseSchema = import_zod14.z.object({
|
|
4145
|
+
message: import_zod14.z.string(),
|
|
4146
|
+
webmail: WebmailLinkSchema.nullable().optional()
|
|
4049
4147
|
});
|
|
4050
4148
|
|
|
4051
4149
|
// src/_api/generated/_cfg_accounts/hooks/useCfgAccountsOtpRequestCreate.ts
|
|
@@ -4099,52 +4197,52 @@ __name(useCfgAccountsOtpRequestCreate, "useCfgAccountsOtpRequestCreate");
|
|
|
4099
4197
|
var import_mutation8 = __toESM(require("swr/mutation"), 1);
|
|
4100
4198
|
|
|
4101
4199
|
// src/_api/generated/_cfg_accounts/schemas/OTPVerifyResponse.ts
|
|
4102
|
-
var
|
|
4200
|
+
var import_zod17 = require("zod");
|
|
4103
4201
|
|
|
4104
4202
|
// src/_api/generated/_cfg_accounts/schemas/User.ts
|
|
4105
|
-
var
|
|
4203
|
+
var import_zod16 = require("zod");
|
|
4106
4204
|
|
|
4107
4205
|
// src/_api/generated/_cfg_accounts/schemas/CentrifugoToken.ts
|
|
4108
|
-
var
|
|
4109
|
-
var CentrifugoTokenSchema =
|
|
4110
|
-
token:
|
|
4111
|
-
centrifugo_url:
|
|
4112
|
-
expires_at:
|
|
4113
|
-
channels:
|
|
4206
|
+
var import_zod15 = require("zod");
|
|
4207
|
+
var CentrifugoTokenSchema = import_zod15.z.object({
|
|
4208
|
+
token: import_zod15.z.string(),
|
|
4209
|
+
centrifugo_url: import_zod15.z.string(),
|
|
4210
|
+
expires_at: import_zod15.z.string().datetime({ offset: true }),
|
|
4211
|
+
channels: import_zod15.z.array(import_zod15.z.string())
|
|
4114
4212
|
});
|
|
4115
4213
|
|
|
4116
4214
|
// src/_api/generated/_cfg_accounts/schemas/User.ts
|
|
4117
|
-
var UserSchema =
|
|
4118
|
-
id:
|
|
4119
|
-
email:
|
|
4120
|
-
first_name:
|
|
4121
|
-
last_name:
|
|
4122
|
-
full_name:
|
|
4123
|
-
initials:
|
|
4124
|
-
display_username:
|
|
4125
|
-
company:
|
|
4126
|
-
phone:
|
|
4127
|
-
position:
|
|
4128
|
-
language:
|
|
4129
|
-
timezone:
|
|
4130
|
-
avatar:
|
|
4131
|
-
is_staff:
|
|
4132
|
-
is_superuser:
|
|
4133
|
-
date_joined:
|
|
4134
|
-
last_login:
|
|
4135
|
-
unanswered_messages_count:
|
|
4215
|
+
var UserSchema = import_zod16.z.object({
|
|
4216
|
+
id: import_zod16.z.number().int(),
|
|
4217
|
+
email: import_zod16.z.email(),
|
|
4218
|
+
first_name: import_zod16.z.string().max(50).nullable().optional(),
|
|
4219
|
+
last_name: import_zod16.z.string().max(50).nullable().optional(),
|
|
4220
|
+
full_name: import_zod16.z.string(),
|
|
4221
|
+
initials: import_zod16.z.string(),
|
|
4222
|
+
display_username: import_zod16.z.string(),
|
|
4223
|
+
company: import_zod16.z.string().max(100).nullable().optional(),
|
|
4224
|
+
phone: import_zod16.z.string().max(20).nullable().optional(),
|
|
4225
|
+
position: import_zod16.z.string().max(100).nullable().optional(),
|
|
4226
|
+
language: import_zod16.z.string().max(10).nullable().optional(),
|
|
4227
|
+
timezone: import_zod16.z.string().max(64).nullable().optional(),
|
|
4228
|
+
avatar: import_zod16.z.string().nullable(),
|
|
4229
|
+
is_staff: import_zod16.z.boolean(),
|
|
4230
|
+
is_superuser: import_zod16.z.boolean(),
|
|
4231
|
+
date_joined: import_zod16.z.string().datetime({ offset: true }),
|
|
4232
|
+
last_login: import_zod16.z.string().datetime({ offset: true }).nullable(),
|
|
4233
|
+
unanswered_messages_count: import_zod16.z.number().int().default(0),
|
|
4136
4234
|
centrifugo: CentrifugoTokenSchema.nullable(),
|
|
4137
|
-
api_key:
|
|
4235
|
+
api_key: import_zod16.z.string().nullable()
|
|
4138
4236
|
});
|
|
4139
4237
|
|
|
4140
4238
|
// src/_api/generated/_cfg_accounts/schemas/OTPVerifyResponse.ts
|
|
4141
|
-
var OTPVerifyResponseSchema =
|
|
4142
|
-
requires_2fa:
|
|
4143
|
-
session_id:
|
|
4144
|
-
refresh:
|
|
4145
|
-
access:
|
|
4239
|
+
var OTPVerifyResponseSchema = import_zod17.z.object({
|
|
4240
|
+
requires_2fa: import_zod17.z.boolean().default(false).optional(),
|
|
4241
|
+
session_id: import_zod17.z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i).nullable().optional(),
|
|
4242
|
+
refresh: import_zod17.z.string().nullable().optional(),
|
|
4243
|
+
access: import_zod17.z.string().nullable().optional(),
|
|
4146
4244
|
user: UserSchema.nullable().optional(),
|
|
4147
|
-
should_prompt_2fa:
|
|
4245
|
+
should_prompt_2fa: import_zod17.z.boolean().optional()
|
|
4148
4246
|
});
|
|
4149
4247
|
|
|
4150
4248
|
// src/_api/generated/_cfg_accounts/hooks/useCfgAccountsOtpVerifyCreate.ts
|
|
@@ -4246,10 +4344,10 @@ __name(useCfgAccountsProfileAvatarCreate, "useCfgAccountsProfileAvatarCreate");
|
|
|
4246
4344
|
var import_mutation10 = __toESM(require("swr/mutation"), 1);
|
|
4247
4345
|
|
|
4248
4346
|
// src/_api/generated/_cfg_accounts/schemas/AccountDeleteResponse.ts
|
|
4249
|
-
var
|
|
4250
|
-
var AccountDeleteResponseSchema =
|
|
4251
|
-
success:
|
|
4252
|
-
message:
|
|
4347
|
+
var import_zod18 = require("zod");
|
|
4348
|
+
var AccountDeleteResponseSchema = import_zod18.z.object({
|
|
4349
|
+
success: import_zod18.z.boolean(),
|
|
4350
|
+
message: import_zod18.z.string()
|
|
4253
4351
|
});
|
|
4254
4352
|
|
|
4255
4353
|
// src/_api/generated/_cfg_accounts/hooks/useCfgAccountsProfilePartialPartialUpdate.ts
|
|
@@ -4361,10 +4459,10 @@ __name(useCfgAccountsProfileUpdateUpdate, "useCfgAccountsProfileUpdateUpdate");
|
|
|
4361
4459
|
var import_mutation15 = __toESM(require("swr/mutation"), 1);
|
|
4362
4460
|
|
|
4363
4461
|
// src/_api/generated/_cfg_accounts/schemas/TokenRefresh.ts
|
|
4364
|
-
var
|
|
4365
|
-
var TokenRefreshSchema =
|
|
4366
|
-
access:
|
|
4367
|
-
refresh:
|
|
4462
|
+
var import_zod19 = require("zod");
|
|
4463
|
+
var TokenRefreshSchema = import_zod19.z.object({
|
|
4464
|
+
access: import_zod19.z.string(),
|
|
4465
|
+
refresh: import_zod19.z.string()
|
|
4368
4466
|
});
|
|
4369
4467
|
|
|
4370
4468
|
// src/_api/generated/_cfg_accounts/hooks/useCfgAccountsTokenRefreshCreate.ts
|
|
@@ -4415,98 +4513,98 @@ function useCfgAccountsTokenRefreshCreate(config) {
|
|
|
4415
4513
|
__name(useCfgAccountsTokenRefreshCreate, "useCfgAccountsTokenRefreshCreate");
|
|
4416
4514
|
|
|
4417
4515
|
// src/_api/generated/_cfg_accounts/schemas/APIKeyRequest.ts
|
|
4418
|
-
var
|
|
4419
|
-
var APIKeyRequestSchema =
|
|
4420
|
-
key:
|
|
4421
|
-
reissued_at:
|
|
4422
|
-
created_at:
|
|
4516
|
+
var import_zod20 = require("zod");
|
|
4517
|
+
var APIKeyRequestSchema = import_zod20.z.object({
|
|
4518
|
+
key: import_zod20.z.string().min(1),
|
|
4519
|
+
reissued_at: import_zod20.z.string().datetime({ offset: true }).nullable(),
|
|
4520
|
+
created_at: import_zod20.z.string().datetime({ offset: true })
|
|
4423
4521
|
});
|
|
4424
4522
|
|
|
4425
4523
|
// src/_api/generated/_cfg_accounts/schemas/APIKeyTestRequest.ts
|
|
4426
|
-
var
|
|
4427
|
-
var APIKeyTestRequestSchema =
|
|
4428
|
-
key:
|
|
4524
|
+
var import_zod21 = require("zod");
|
|
4525
|
+
var APIKeyTestRequestSchema = import_zod21.z.object({
|
|
4526
|
+
key: import_zod21.z.string().min(1)
|
|
4429
4527
|
});
|
|
4430
4528
|
|
|
4431
4529
|
// src/_api/generated/_cfg_accounts/schemas/CfgUserUpdateRequest.ts
|
|
4432
|
-
var
|
|
4433
|
-
var CfgUserUpdateRequestSchema =
|
|
4434
|
-
first_name:
|
|
4435
|
-
last_name:
|
|
4436
|
-
company:
|
|
4437
|
-
phone:
|
|
4438
|
-
position:
|
|
4439
|
-
language:
|
|
4440
|
-
timezone:
|
|
4530
|
+
var import_zod22 = require("zod");
|
|
4531
|
+
var CfgUserUpdateRequestSchema = import_zod22.z.object({
|
|
4532
|
+
first_name: import_zod22.z.string().max(50).optional(),
|
|
4533
|
+
last_name: import_zod22.z.string().max(50).optional(),
|
|
4534
|
+
company: import_zod22.z.string().max(100).optional(),
|
|
4535
|
+
phone: import_zod22.z.string().max(20).optional(),
|
|
4536
|
+
position: import_zod22.z.string().max(100).optional(),
|
|
4537
|
+
language: import_zod22.z.string().max(10).optional(),
|
|
4538
|
+
timezone: import_zod22.z.string().max(64).optional()
|
|
4441
4539
|
});
|
|
4442
4540
|
|
|
4443
4541
|
// src/_api/generated/_cfg_accounts/schemas/OAuthAuthorizeRequestRequest.ts
|
|
4444
|
-
var
|
|
4445
|
-
var OAuthAuthorizeRequestRequestSchema =
|
|
4446
|
-
redirect_uri:
|
|
4447
|
-
source_url:
|
|
4542
|
+
var import_zod23 = require("zod");
|
|
4543
|
+
var OAuthAuthorizeRequestRequestSchema = import_zod23.z.object({
|
|
4544
|
+
redirect_uri: import_zod23.z.string().optional(),
|
|
4545
|
+
source_url: import_zod23.z.string().optional()
|
|
4448
4546
|
});
|
|
4449
4547
|
|
|
4450
4548
|
// src/_api/generated/_cfg_accounts/schemas/OAuthCallbackRequestRequest.ts
|
|
4451
|
-
var
|
|
4452
|
-
var OAuthCallbackRequestRequestSchema =
|
|
4453
|
-
code:
|
|
4454
|
-
state:
|
|
4455
|
-
redirect_uri:
|
|
4549
|
+
var import_zod24 = require("zod");
|
|
4550
|
+
var OAuthCallbackRequestRequestSchema = import_zod24.z.object({
|
|
4551
|
+
code: import_zod24.z.string().min(10).max(500),
|
|
4552
|
+
state: import_zod24.z.string().min(20).max(100),
|
|
4553
|
+
redirect_uri: import_zod24.z.string().optional()
|
|
4456
4554
|
});
|
|
4457
4555
|
|
|
4458
4556
|
// src/_api/generated/_cfg_accounts/schemas/OAuthDisconnectRequestRequest.ts
|
|
4459
|
-
var
|
|
4460
|
-
var OAuthDisconnectRequestRequestSchema =
|
|
4557
|
+
var import_zod25 = require("zod");
|
|
4558
|
+
var OAuthDisconnectRequestRequestSchema = import_zod25.z.object({
|
|
4461
4559
|
provider: OAuthProviderEnumSchema
|
|
4462
4560
|
});
|
|
4463
4561
|
|
|
4464
4562
|
// src/_api/generated/_cfg_accounts/schemas/OAuthError.ts
|
|
4465
|
-
var
|
|
4466
|
-
var OAuthErrorSchema =
|
|
4467
|
-
error:
|
|
4468
|
-
error_description:
|
|
4563
|
+
var import_zod26 = require("zod");
|
|
4564
|
+
var OAuthErrorSchema = import_zod26.z.object({
|
|
4565
|
+
error: import_zod26.z.string(),
|
|
4566
|
+
error_description: import_zod26.z.string().optional()
|
|
4469
4567
|
});
|
|
4470
4568
|
|
|
4471
4569
|
// src/_api/generated/_cfg_accounts/schemas/OTPErrorResponse.ts
|
|
4472
|
-
var
|
|
4473
|
-
var OTPErrorResponseSchema =
|
|
4474
|
-
error:
|
|
4475
|
-
error_code:
|
|
4476
|
-
retry_after:
|
|
4570
|
+
var import_zod27 = require("zod");
|
|
4571
|
+
var OTPErrorResponseSchema = import_zod27.z.object({
|
|
4572
|
+
error: import_zod27.z.string(),
|
|
4573
|
+
error_code: import_zod27.z.string().nullable().optional(),
|
|
4574
|
+
retry_after: import_zod27.z.number().int().nullable().optional()
|
|
4477
4575
|
});
|
|
4478
4576
|
|
|
4479
4577
|
// src/_api/generated/_cfg_accounts/schemas/OTPRequestRequest.ts
|
|
4480
|
-
var
|
|
4481
|
-
var OTPRequestRequestSchema =
|
|
4482
|
-
identifier:
|
|
4483
|
-
source_url:
|
|
4578
|
+
var import_zod28 = require("zod");
|
|
4579
|
+
var OTPRequestRequestSchema = import_zod28.z.object({
|
|
4580
|
+
identifier: import_zod28.z.string().min(1),
|
|
4581
|
+
source_url: import_zod28.z.string().optional()
|
|
4484
4582
|
});
|
|
4485
4583
|
|
|
4486
4584
|
// src/_api/generated/_cfg_accounts/schemas/OTPVerifyRequest.ts
|
|
4487
|
-
var
|
|
4488
|
-
var OTPVerifyRequestSchema =
|
|
4489
|
-
identifier:
|
|
4490
|
-
otp:
|
|
4491
|
-
source_url:
|
|
4585
|
+
var import_zod29 = require("zod");
|
|
4586
|
+
var OTPVerifyRequestSchema = import_zod29.z.object({
|
|
4587
|
+
identifier: import_zod29.z.string().min(1),
|
|
4588
|
+
otp: import_zod29.z.string().min(4).max(4),
|
|
4589
|
+
source_url: import_zod29.z.string().optional()
|
|
4492
4590
|
});
|
|
4493
4591
|
|
|
4494
4592
|
// src/_api/generated/_cfg_accounts/schemas/PatchedCfgUserUpdateRequest.ts
|
|
4495
|
-
var
|
|
4496
|
-
var PatchedCfgUserUpdateRequestSchema =
|
|
4497
|
-
first_name:
|
|
4498
|
-
last_name:
|
|
4499
|
-
company:
|
|
4500
|
-
phone:
|
|
4501
|
-
position:
|
|
4502
|
-
language:
|
|
4503
|
-
timezone:
|
|
4593
|
+
var import_zod30 = require("zod");
|
|
4594
|
+
var PatchedCfgUserUpdateRequestSchema = import_zod30.z.object({
|
|
4595
|
+
first_name: import_zod30.z.string().max(50).optional(),
|
|
4596
|
+
last_name: import_zod30.z.string().max(50).optional(),
|
|
4597
|
+
company: import_zod30.z.string().max(100).optional(),
|
|
4598
|
+
phone: import_zod30.z.string().max(20).optional(),
|
|
4599
|
+
position: import_zod30.z.string().max(100).optional(),
|
|
4600
|
+
language: import_zod30.z.string().max(10).optional(),
|
|
4601
|
+
timezone: import_zod30.z.string().max(64).optional()
|
|
4504
4602
|
});
|
|
4505
4603
|
|
|
4506
4604
|
// src/_api/generated/_cfg_accounts/schemas/TokenRefreshRequest.ts
|
|
4507
|
-
var
|
|
4508
|
-
var TokenRefreshRequestSchema =
|
|
4509
|
-
refresh:
|
|
4605
|
+
var import_zod31 = require("zod");
|
|
4606
|
+
var TokenRefreshRequestSchema = import_zod31.z.object({
|
|
4607
|
+
refresh: import_zod31.z.string().min(1)
|
|
4510
4608
|
});
|
|
4511
4609
|
|
|
4512
4610
|
// src/auth/context/AccountsContext.tsx
|
|
@@ -4660,8 +4758,15 @@ var hasValidTokens = /* @__PURE__ */ __name(() => {
|
|
|
4660
4758
|
if (typeof window === "undefined") return false;
|
|
4661
4759
|
return CfgAccountsApi.isAuthenticated();
|
|
4662
4760
|
}, "hasValidTokens");
|
|
4761
|
+
var isSessionDeadOnBootstrap = /* @__PURE__ */ __name((accessToken, refreshToken, now = Date.now()) => {
|
|
4762
|
+
const accessDead = isTokenExpired(accessToken, 0, now);
|
|
4763
|
+
if (!accessDead) return false;
|
|
4764
|
+
const refreshDead = isTokenExpired(refreshToken, 0, now);
|
|
4765
|
+
return refreshDead;
|
|
4766
|
+
}, "isSessionDeadOnBootstrap");
|
|
4663
4767
|
var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
4664
4768
|
const accounts = useAccountsContext();
|
|
4769
|
+
ensureRefreshHandler();
|
|
4665
4770
|
const redirectManager = useAuthRedirectManager({
|
|
4666
4771
|
fallbackUrl: config?.routes?.defaultCallback || defaultRoutes.defaultCallback,
|
|
4667
4772
|
clearOnUse: true
|
|
@@ -4674,19 +4779,13 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4674
4779
|
return true;
|
|
4675
4780
|
});
|
|
4676
4781
|
const [initialized, setInitialized] = (0, import_react17.useState)(false);
|
|
4782
|
+
const [authTick, setAuthTick] = (0, import_react17.useState)(0);
|
|
4783
|
+
const bumpAuthTick = (0, import_react17.useCallback)(() => setAuthTick((t) => t + 1), []);
|
|
4784
|
+
const onUnauthorizedRef = (0, import_react17.useRef)(false);
|
|
4677
4785
|
const router = useCfgRouter();
|
|
4678
4786
|
const pathname = (0, import_navigation4.usePathname)();
|
|
4679
4787
|
const queryParams = useQueryParams();
|
|
4680
4788
|
const [storedEmail, setStoredEmail, clearStoredEmail] = useLocalStorage(EMAIL_STORAGE_KEY, null);
|
|
4681
|
-
useTokenRefresh({
|
|
4682
|
-
enabled: true,
|
|
4683
|
-
onRefresh: /* @__PURE__ */ __name((newToken) => {
|
|
4684
|
-
authLogger.info("Token auto-refreshed successfully");
|
|
4685
|
-
}, "onRefresh"),
|
|
4686
|
-
onRefreshError: /* @__PURE__ */ __name((error) => {
|
|
4687
|
-
authLogger.warn("Token auto-refresh failed:", error.message);
|
|
4688
|
-
}, "onRefreshError")
|
|
4689
|
-
});
|
|
4690
4789
|
const user = accounts.profile;
|
|
4691
4790
|
const userRef = (0, import_react17.useRef)(user);
|
|
4692
4791
|
const configRef = (0, import_react17.useRef)(config);
|
|
@@ -4703,7 +4802,23 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4703
4802
|
clearProfileCache();
|
|
4704
4803
|
setInitialized(true);
|
|
4705
4804
|
setIsLoading(false);
|
|
4706
|
-
|
|
4805
|
+
bumpAuthTick();
|
|
4806
|
+
}, [bumpAuthTick]);
|
|
4807
|
+
const handleProactiveRefreshError = (0, import_react17.useCallback)((error) => {
|
|
4808
|
+
authLogger.warn("Proactive token refresh failed \u2014 session is dead, redirecting to login:", error.message);
|
|
4809
|
+
if (onUnauthorizedRef.current) return;
|
|
4810
|
+
onUnauthorizedRef.current = true;
|
|
4811
|
+
clearAuthState("proactiveRefresh:failed");
|
|
4812
|
+
const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
|
|
4813
|
+
router.hardReplace(authCallbackUrl);
|
|
4814
|
+
}, [clearAuthState, router]);
|
|
4815
|
+
useTokenRefresh({
|
|
4816
|
+
enabled: true,
|
|
4817
|
+
onRefresh: /* @__PURE__ */ __name(() => {
|
|
4818
|
+
authLogger.info("Token auto-refreshed successfully");
|
|
4819
|
+
}, "onRefresh"),
|
|
4820
|
+
onRefreshError: handleProactiveRefreshError
|
|
4821
|
+
});
|
|
4707
4822
|
const handleGlobalAuthError = (0, import_react17.useCallback)((error, context = "API Request") => {
|
|
4708
4823
|
const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
|
|
4709
4824
|
if (isAuthError) {
|
|
@@ -4716,6 +4831,20 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4716
4831
|
}
|
|
4717
4832
|
return false;
|
|
4718
4833
|
}, [clearAuthState]);
|
|
4834
|
+
(0, import_react17.useEffect)(() => {
|
|
4835
|
+
const handler = /* @__PURE__ */ __name(() => {
|
|
4836
|
+
if (onUnauthorizedRef.current) return;
|
|
4837
|
+
onUnauthorizedRef.current = true;
|
|
4838
|
+
authLogger.warn("Unrecoverable 401 (refresh failed) \u2014 clearing session and redirecting to login");
|
|
4839
|
+
clearAuthState("onUnauthorized:401");
|
|
4840
|
+
const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
|
|
4841
|
+
router.hardReplace(authCallbackUrl);
|
|
4842
|
+
}, "handler");
|
|
4843
|
+
CfgAccountsApi.onUnauthorized(handler);
|
|
4844
|
+
return () => {
|
|
4845
|
+
CfgAccountsApi.onUnauthorized(null);
|
|
4846
|
+
};
|
|
4847
|
+
}, [clearAuthState, router]);
|
|
4719
4848
|
const isAutoLoggingOutRef = (0, import_react17.useRef)(false);
|
|
4720
4849
|
const swrOnError = (0, import_react17.useCallback)((error) => {
|
|
4721
4850
|
const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
|
|
@@ -4780,6 +4909,11 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4780
4909
|
authLogger.info("Token from API:", token ? `${token.substring(0, 20)}...` : "null");
|
|
4781
4910
|
authLogger.info("Refresh token from API:", refreshToken2 ? `${refreshToken2.substring(0, 20)}...` : "null");
|
|
4782
4911
|
authLogger.info("localStorage keys:", Object.keys(localStorage).filter((k) => k.includes("token") || k.includes("auth")));
|
|
4912
|
+
if (!isInIframe && isSessionDeadOnBootstrap(token, refreshToken2)) {
|
|
4913
|
+
authLogger.warn("Bootstrap: dead session in storage (access + refresh both unusable) \u2014 clearing and showing login");
|
|
4914
|
+
clearAuthState("initializeAuth:deadSession");
|
|
4915
|
+
return;
|
|
4916
|
+
}
|
|
4783
4917
|
const hasTokens = hasValidTokens();
|
|
4784
4918
|
authLogger.info("Has tokens:", hasTokens);
|
|
4785
4919
|
if (userRef.current) {
|
|
@@ -4880,7 +5014,8 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4880
5014
|
});
|
|
4881
5015
|
return {
|
|
4882
5016
|
success: true,
|
|
4883
|
-
message: result.message || `OTP code sent to your email address
|
|
5017
|
+
message: result.message || `OTP code sent to your email address`,
|
|
5018
|
+
webmail: result.webmail ?? null
|
|
4884
5019
|
};
|
|
4885
5020
|
} catch (error) {
|
|
4886
5021
|
authLogger.error("Request OTP error:", error);
|
|
@@ -5035,7 +5170,9 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
5035
5170
|
() => ({
|
|
5036
5171
|
user,
|
|
5037
5172
|
isLoading,
|
|
5038
|
-
// Consider authenticated if we have valid tokens, even without user profile
|
|
5173
|
+
// Consider authenticated if we have valid tokens, even without user profile.
|
|
5174
|
+
// `authTick` (in deps) forces recompute after tokens are cleared/set, since
|
|
5175
|
+
// token state lives in storage, not React state.
|
|
5039
5176
|
isAuthenticated: CfgAccountsApi.isAuthenticated(),
|
|
5040
5177
|
isAdminUser,
|
|
5041
5178
|
loadCurrentProfile,
|
|
@@ -5061,6 +5198,8 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
5061
5198
|
[
|
|
5062
5199
|
user,
|
|
5063
5200
|
isLoading,
|
|
5201
|
+
authTick,
|
|
5202
|
+
// recompute isAuthenticated when tokens are cleared/set
|
|
5064
5203
|
isAdminUser,
|
|
5065
5204
|
loadCurrentProfile,
|
|
5066
5205
|
checkAuthAndRedirect,
|