@djangocfg/api 2.1.566 → 2.1.569
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.cjs +98 -69
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +73 -3
- package/dist/auth.d.ts +73 -3
- package/dist/auth.mjs +98 -69
- package/dist/auth.mjs.map +1 -1
- package/package.json +2 -2
- package/src/_api/generated/_cfg_accounts/schemas/OAuthAuthorizeRequestRequest.ts +2 -2
- package/src/_api/generated/_cfg_accounts/schemas/OAuthAuthorizeResponse.ts +1 -1
- package/src/_api/generated/_cfg_accounts/schemas/OAuthCallbackRequestRequest.ts +1 -1
- package/src/_api/generated/_cfg_accounts/schemas/OAuthConnection.ts +2 -2
- package/src/_api/generated/_cfg_accounts/schemas/OTPRequestRequest.ts +1 -1
- package/src/_api/generated/_cfg_accounts/schemas/OTPVerifyRequest.ts +1 -1
- package/src/_api/generated/_cfg_accounts/schemas/User.ts +2 -2
- package/src/_api/generated/_cfg_accounts/schemas/WebmailLink.ts +1 -1
- package/src/_api/generated/_cfg_totp/schemas/TotpVerifyUser.ts +2 -2
- package/src/auth/__tests__/formatAuthError.test.ts +70 -6
- package/src/auth/__tests__/logAuthFailure.test.ts +89 -0
- package/src/auth/context/AuthContext.tsx +10 -17
- package/src/auth/hooks/twoFactor.shared.ts +5 -20
- package/src/auth/hooks/useGithubAuth.ts +3 -2
- package/src/auth/hooks/useTwoFactorSetup.ts +6 -5
- package/src/auth/hooks/useTwoFactorStatus.ts +6 -5
- package/src/auth/hooks/useTwoFactorVerify.ts +6 -5
- package/src/auth/utils/errors.ts +141 -7
- package/src/auth/utils/index.ts +8 -1
package/src/auth/utils/errors.ts
CHANGED
|
@@ -5,29 +5,163 @@ const prop = (value: unknown, key: string): unknown =>
|
|
|
5
5
|
: undefined;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* The transport's own fallback message, e.g. `HTTP 404: ` or `HTTP 500: Boom`.
|
|
9
|
+
*
|
|
10
|
+
* `APIError` builds this when the caller supplies no message of its own
|
|
11
|
+
* (`_api/generated/helpers/errors.ts`), so it is never server prose — it is the
|
|
12
|
+
* status line restated. Under HTTP/2 `statusText` is empty, which is how a bare
|
|
13
|
+
* `HTTP 404:` reached a sign-in form and told the user nothing at all.
|
|
14
|
+
*/
|
|
15
|
+
const TRANSPORT_FALLBACK = /^HTTP \d{3}:\s*/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Translation keys for the copy this module can choose.
|
|
19
|
+
*
|
|
20
|
+
* Every one already exists, translated, in all 17 locales of `@djangocfg/i18n`
|
|
21
|
+
* — `api.errors.*` for transport conditions and `ui.errors.*` for the ones a
|
|
22
|
+
* person can act on. Nothing is added here: a second key saying "Server is
|
|
23
|
+
* unavailable" in a namespace of its own would be a second owner of the same
|
|
24
|
+
* sentence, and the one that loses is whichever the caller did not use.
|
|
25
|
+
*
|
|
26
|
+
* A 404 maps to `serverUnavailable`, not to `ui.errors.notFound` ("Not found"):
|
|
27
|
+
* on a sign-in POST the missing thing is the ENDPOINT, which tells the visitor
|
|
28
|
+
* nothing about a page and everything about the service being down.
|
|
29
|
+
*/
|
|
30
|
+
export const AUTH_ERROR_KEYS = {
|
|
31
|
+
invalidCredentials: 'ui.errors.unauthorized',
|
|
32
|
+
rateLimited: 'api.errors.rateLimited',
|
|
33
|
+
serverUnavailable: 'api.errors.serverUnavailable',
|
|
34
|
+
networkError: 'api.errors.networkError',
|
|
35
|
+
generic: 'ui.errors.generic',
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
export type AuthErrorKey = (typeof AUTH_ERROR_KEYS)[keyof typeof AUTH_ERROR_KEYS];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* English copy for each key, so a caller without a translator still renders a
|
|
42
|
+
* sentence. The catalogue is authoritative; this is the offline twin.
|
|
43
|
+
*/
|
|
44
|
+
const FALLBACK_COPY: Record<AuthErrorKey, string> = {
|
|
45
|
+
[AUTH_ERROR_KEYS.invalidCredentials]: 'Please sign in to continue',
|
|
46
|
+
[AUTH_ERROR_KEYS.rateLimited]: 'Too many requests. Please wait.',
|
|
47
|
+
[AUTH_ERROR_KEYS.serverUnavailable]: 'Server is unavailable',
|
|
48
|
+
[AUTH_ERROR_KEYS.networkError]: 'Network error',
|
|
49
|
+
[AUTH_ERROR_KEYS.generic]: 'Something went wrong',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Map a status code to the key whose copy explains it. */
|
|
53
|
+
const keyForStatus = (status: number | undefined): AuthErrorKey => {
|
|
54
|
+
if (status === undefined) return AUTH_ERROR_KEYS.generic;
|
|
55
|
+
if (status === 401 || status === 403) return AUTH_ERROR_KEYS.invalidCredentials;
|
|
56
|
+
// A 404 here is a missing endpoint, not a missing page — see AUTH_ERROR_KEYS.
|
|
57
|
+
if (status === 404) return AUTH_ERROR_KEYS.serverUnavailable;
|
|
58
|
+
if (status === 429) return AUTH_ERROR_KEYS.rateLimited;
|
|
59
|
+
if (status >= 500) return AUTH_ERROR_KEYS.serverUnavailable;
|
|
60
|
+
return AUTH_ERROR_KEYS.generic;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** What the auth layer decided to show, and why. */
|
|
64
|
+
export interface AuthErrorCopy {
|
|
65
|
+
/** Ready-to-render text: server prose, or this module's English fallback. */
|
|
66
|
+
message: string;
|
|
67
|
+
/**
|
|
68
|
+
* Set only when `message` came from `FALLBACK_COPY`. A caller holding a
|
|
69
|
+
* translator renders `t(key)` instead; server prose carries no key because
|
|
70
|
+
* the backend already chose the words and the locale.
|
|
71
|
+
*/
|
|
72
|
+
key?: AuthErrorKey;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Decide what a failed auth call should say.
|
|
77
|
+
*
|
|
78
|
+
* Order: text the SERVER wrote, then copy chosen from the status code, then a
|
|
79
|
+
* generic sentence. Server prose wins because the backend knows the specific
|
|
80
|
+
* reason ("This code has expired") that a status code cannot express.
|
|
9
81
|
*
|
|
10
82
|
* Each candidate is checked to be a string before it is returned. Under the
|
|
11
83
|
* previous `any` the check was only that the property was truthy, so a DRF
|
|
12
84
|
* field-error object (`{detail: {email: [...]}}`) reached the UI as
|
|
13
85
|
* "[object Object]" — an error message that tells the user nothing.
|
|
86
|
+
*
|
|
87
|
+
* `HTTP <code>:` strings are rejected on the way through. They are the
|
|
88
|
+
* transport restating its own status line, not prose anyone wrote to be read,
|
|
89
|
+
* and treating them as a message is what put a bare `HTTP 404:` under a
|
|
90
|
+
* sign-in field. The code itself stays useful — `logAuthFailure` puts it in
|
|
91
|
+
* the console, where diagnosis belongs.
|
|
14
92
|
*/
|
|
15
|
-
export const
|
|
16
|
-
if (typeof error === 'string') {
|
|
17
|
-
return error;
|
|
93
|
+
export const resolveAuthError = (error: unknown, fallback?: string): AuthErrorCopy => {
|
|
94
|
+
if (typeof error === 'string' && error && !TRANSPORT_FALLBACK.test(error)) {
|
|
95
|
+
return { message: error };
|
|
18
96
|
}
|
|
19
97
|
|
|
20
98
|
const candidates = [
|
|
21
99
|
prop(error, 'message'),
|
|
22
100
|
prop(error, 'detail'),
|
|
101
|
+
prop(prop(error, 'response'), 'error'),
|
|
102
|
+
prop(prop(error, 'response'), 'detail'),
|
|
103
|
+
prop(prop(error, 'response'), 'message'),
|
|
23
104
|
prop(prop(prop(error, 'response'), 'data'), 'detail'),
|
|
24
105
|
];
|
|
25
106
|
|
|
26
107
|
for (const candidate of candidates) {
|
|
27
|
-
if (typeof candidate === 'string' && candidate) {
|
|
28
|
-
return candidate;
|
|
108
|
+
if (typeof candidate === 'string' && candidate && !TRANSPORT_FALLBACK.test(candidate)) {
|
|
109
|
+
return { message: candidate };
|
|
29
110
|
}
|
|
30
111
|
}
|
|
31
112
|
|
|
32
|
-
|
|
113
|
+
// Both spellings are read: `statusCode` is what `APIError` declares, while a
|
|
114
|
+
// Response-shaped rejection reaching here from outside the interceptor
|
|
115
|
+
// carries `status`.
|
|
116
|
+
const rawStatus = prop(error, 'statusCode') ?? prop(error, 'status');
|
|
117
|
+
const status = typeof rawStatus === 'number' ? rawStatus : undefined;
|
|
118
|
+
|
|
119
|
+
// A caller's own fallback only applies where the status says nothing; a 404
|
|
120
|
+
// must not be reported as "Failed to send the code".
|
|
121
|
+
if (status === undefined && fallback) {
|
|
122
|
+
return { message: fallback };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const key = keyForStatus(status);
|
|
126
|
+
return { message: FALLBACK_COPY[key], key };
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* `resolveAuthError` for callers that only want the string.
|
|
131
|
+
*
|
|
132
|
+
* Kept because most call sites store a plain `error` string in form state and
|
|
133
|
+
* have no translator to hand.
|
|
134
|
+
*/
|
|
135
|
+
export const formatAuthError = (error: unknown, fallback?: string): string =>
|
|
136
|
+
resolveAuthError(error, fallback).message;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Log a failed auth call at the level its cause deserves.
|
|
140
|
+
*
|
|
141
|
+
* A 4xx on an auth endpoint is the USER getting it wrong — a mistyped OTP, an
|
|
142
|
+
* expired code, a bad password. It is the endpoint working. Logged through
|
|
143
|
+
* `console.error` it became a production alert: `@djangocfg/devtools` captures
|
|
144
|
+
* `console.error` as an `ERROR` event, so every typo raised "❌ Error" in
|
|
145
|
+
* Telegram, complete with consola's `%c` CSS in the message body. Real faults
|
|
146
|
+
* drown in that.
|
|
147
|
+
*
|
|
148
|
+
* 5xx, a network failure and a broken response contract stay at `error`:
|
|
149
|
+
* nobody can fix those by retyping, and they are what the alert is for.
|
|
150
|
+
*
|
|
151
|
+
* Both `statusCode` (what `APIError` declares) and `status` are read, because
|
|
152
|
+
* a Response-shaped rejection can reach here from code that does not go
|
|
153
|
+
* through the interceptor.
|
|
154
|
+
*/
|
|
155
|
+
export const logAuthFailure = (
|
|
156
|
+
log: { warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void },
|
|
157
|
+
message: string,
|
|
158
|
+
error: unknown,
|
|
159
|
+
): void => {
|
|
160
|
+
const code = prop(error, 'statusCode') ?? prop(error, 'status');
|
|
161
|
+
const isClientError = typeof code === 'number' && code >= 400 && code < 500;
|
|
162
|
+
if (isClientError) {
|
|
163
|
+
log.warn(message, error);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
log.error(message, error);
|
|
33
167
|
};
|
package/src/auth/utils/index.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export { validateEmail } from './validation';
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
formatAuthError,
|
|
4
|
+
logAuthFailure,
|
|
5
|
+
resolveAuthError,
|
|
6
|
+
AUTH_ERROR_KEYS,
|
|
7
|
+
type AuthErrorCopy,
|
|
8
|
+
type AuthErrorKey,
|
|
9
|
+
} from './errors';
|
|
3
10
|
export {
|
|
4
11
|
resolveGuardIsLoading,
|
|
5
12
|
shouldRedirectToAuth,
|