Skip to content

Commit 9b8ee19

Browse files
committed
feat(i18n): add authentication error handling and localization
1 parent ca1f1ab commit 9b8ee19

4 files changed

Lines changed: 124 additions & 17 deletions

File tree

packages/ui/src/amplify/errors.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
type AuthErrorLike = {
2+
code?: string
3+
name?: string
4+
message?: string
5+
}
6+
7+
function getAuthErrorName(error: unknown) {
8+
const authError = (error ?? {}) as AuthErrorLike
9+
return authError.name || authError.code || ''
10+
}
11+
12+
export function getAuthErrorMessageKey(error: unknown) {
13+
switch (getAuthErrorName(error)) {
14+
case 'UserNotFoundException':
15+
return 'AUTH_ERROR_USER_NOT_FOUND'
16+
case 'NotAuthorizedException':
17+
return 'AUTH_ERROR_INVALID_CREDENTIALS'
18+
case 'UserNotConfirmedException':
19+
return 'AUTH_ERROR_USER_NOT_CONFIRMED'
20+
case 'UsernameExistsException':
21+
return 'AUTH_ERROR_USERNAME_EXISTS'
22+
case 'InvalidPasswordException':
23+
return 'PW_POLICY_TIP'
24+
case 'CodeMismatchException':
25+
return 'AUTH_ERROR_CODE_MISMATCH'
26+
case 'ExpiredCodeException':
27+
return 'AUTH_ERROR_CODE_EXPIRED'
28+
case 'LimitExceededException':
29+
case 'TooManyRequestsException':
30+
return 'AUTH_ERROR_TOO_MANY_REQUESTS'
31+
case 'TooManyFailedAttemptsException':
32+
return 'AUTH_ERROR_TOO_MANY_ATTEMPTS'
33+
case 'CodeDeliveryFailureException':
34+
return 'AUTH_ERROR_CODE_DELIVERY_FAILED'
35+
case 'UserAlreadyAuthenticatedException':
36+
return 'AUTH_ERROR_ALREADY_AUTHENTICATED'
37+
case 'InvalidParameterException':
38+
return 'AUTH_ERROR_INVALID_PARAMETER'
39+
default:
40+
return 'AUTH_ERROR_GENERIC'
41+
}
42+
}

packages/ui/src/amplify/lang.ts

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,21 @@ export default {
3737
'Enter your email': 'Enter your email',
3838
'Invalid Password': 'Invalid Password',
3939
'Passwords do not match.': 'Passwords do not match.',
40-
'Password must be at least 8 characters.': 'Password must be at least 8 characters.',
4140
'We have sent a numeric verification code to your email address at': 'We have sent a numeric verification code to your email address at',
4241
'COUNTDOWN_SUFFIX': 's',
4342
'Unsupported sign-in step:': 'Unsupported sign-in step:',
43+
'AUTH_ERROR_GENERIC': 'Authentication failed. Please try again.',
44+
'AUTH_ERROR_INVALID_CREDENTIALS': 'Incorrect email or password.',
45+
'AUTH_ERROR_USER_NOT_CONFIRMED': 'Your account is not confirmed yet. Please verify it with the code we sent.',
46+
'AUTH_ERROR_USERNAME_EXISTS': 'This username is already taken.',
47+
'AUTH_ERROR_USER_NOT_FOUND': 'We could not find an account for that email address.',
48+
'AUTH_ERROR_CODE_MISMATCH': 'The verification code is incorrect.',
49+
'AUTH_ERROR_CODE_EXPIRED': 'The verification code has expired. Please request a new one.',
50+
'AUTH_ERROR_TOO_MANY_REQUESTS': 'Too many requests. Please wait a moment and try again.',
51+
'AUTH_ERROR_TOO_MANY_ATTEMPTS': 'Too many failed attempts. Please wait a moment and try again.',
52+
'AUTH_ERROR_CODE_DELIVERY_FAILED': 'We could not send the verification code. Please try again later.',
53+
'AUTH_ERROR_ALREADY_AUTHENTICATED': 'You are already signed in.',
54+
'AUTH_ERROR_INVALID_PARAMETER': 'Some information is invalid. Please check your input and try again.',
4455
},
4556
'zh-CN': {
4657
'login': '登录',
@@ -81,10 +92,21 @@ export default {
8192
'Enter your email': '请输入您的邮箱',
8293
'Invalid Password': '密码无效',
8394
'Passwords do not match.': '两次输入的密码不一致。',
84-
'Password must be at least 8 characters.': '密码至少需要 8 个字符。',
8595
'We have sent a numeric verification code to your email address at': '我们已向此邮箱发送数字验证码:',
8696
'COUNTDOWN_SUFFIX': '秒',
87-
'Unsupported sign-in step:': '不支持的登录步骤:'
97+
'Unsupported sign-in step:': '不支持的登录步骤:',
98+
'AUTH_ERROR_GENERIC': '认证失败,请重试。',
99+
'AUTH_ERROR_INVALID_CREDENTIALS': '邮箱或密码错误。',
100+
'AUTH_ERROR_USER_NOT_CONFIRMED': '账户尚未完成验证,请使用我们发送的验证码完成验证。',
101+
'AUTH_ERROR_USERNAME_EXISTS': '该用户名已被占用。',
102+
'AUTH_ERROR_USER_NOT_FOUND': '未找到与该邮箱地址对应的账户。',
103+
'AUTH_ERROR_CODE_MISMATCH': '验证码不正确。',
104+
'AUTH_ERROR_CODE_EXPIRED': '验证码已过期,请重新获取。',
105+
'AUTH_ERROR_TOO_MANY_REQUESTS': '请求过于频繁,请稍后再试。',
106+
'AUTH_ERROR_TOO_MANY_ATTEMPTS': '失败次数过多,请稍后再试。',
107+
'AUTH_ERROR_CODE_DELIVERY_FAILED': '验证码发送失败,请稍后再试。',
108+
'AUTH_ERROR_ALREADY_AUTHENTICATED': '您已登录。',
109+
'AUTH_ERROR_INVALID_PARAMETER': '输入信息无效,请检查后重试。'
88110
},
89111
'zh-Hant': {
90112
'login': '登入',
@@ -125,10 +147,21 @@ export default {
125147
'Enter your email': '請輸入您的電子郵件',
126148
'Invalid Password': '密碼無效',
127149
'Passwords do not match.': '兩次輸入的密碼不一致。',
128-
'Password must be at least 8 characters.': '密碼至少需要 8 個字元。',
129150
'We have sent a numeric verification code to your email address at': '我們已向此電子郵件地址發送數字驗證碼:',
130151
'COUNTDOWN_SUFFIX': '秒',
131-
'Unsupported sign-in step:': '不支援的登入步驟:'
152+
'Unsupported sign-in step:': '不支援的登入步驟:',
153+
'AUTH_ERROR_GENERIC': '驗證失敗,請再試一次。',
154+
'AUTH_ERROR_INVALID_CREDENTIALS': '電子郵件或密碼錯誤。',
155+
'AUTH_ERROR_USER_NOT_CONFIRMED': '帳戶尚未完成驗證,請使用我們發送的驗證碼完成驗證。',
156+
'AUTH_ERROR_USERNAME_EXISTS': '此用戶名已被使用。',
157+
'AUTH_ERROR_USER_NOT_FOUND': '找不到與該電子郵件地址對應的帳戶。',
158+
'AUTH_ERROR_CODE_MISMATCH': '驗證碼不正確。',
159+
'AUTH_ERROR_CODE_EXPIRED': '驗證碼已過期,請重新取得。',
160+
'AUTH_ERROR_TOO_MANY_REQUESTS': '請求過於頻繁,請稍後再試。',
161+
'AUTH_ERROR_TOO_MANY_ATTEMPTS': '失敗次數過多,請稍後再試。',
162+
'AUTH_ERROR_CODE_DELIVERY_FAILED': '無法發送驗證碼,請稍後再試。',
163+
'AUTH_ERROR_ALREADY_AUTHENTICATED': '您已登入。',
164+
'AUTH_ERROR_INVALID_PARAMETER': '輸入資訊無效,請檢查後再試。'
132165
},
133166
'ja': {
134167
'login': 'ログイン',
@@ -169,9 +202,20 @@ export default {
169202
'Enter your email': 'メールアドレスを入力してください',
170203
'Invalid Password': '無効なパスワード',
171204
'Passwords do not match.': 'パスワードが一致しません。',
172-
'Password must be at least 8 characters.': 'パスワードは 8 文字以上である必要があります。',
173205
'We have sent a numeric verification code to your email address at': '次のメールアドレスに数字の確認コードを送信しました:',
174206
'COUNTDOWN_SUFFIX': '秒',
175-
'Unsupported sign-in step:': '未対応のサインイン手順:'
207+
'Unsupported sign-in step:': '未対応のサインイン手順:',
208+
'AUTH_ERROR_GENERIC': '認証に失敗しました。もう一度お試しください。',
209+
'AUTH_ERROR_INVALID_CREDENTIALS': 'メールアドレスまたはパスワードが正しくありません。',
210+
'AUTH_ERROR_USER_NOT_CONFIRMED': 'アカウントの確認がまだ完了していません。送信されたコードで確認してください。',
211+
'AUTH_ERROR_USERNAME_EXISTS': 'このユーザー名は既に使用されています。',
212+
'AUTH_ERROR_USER_NOT_FOUND': 'そのメールアドレスに対応するアカウントが見つかりません。',
213+
'AUTH_ERROR_CODE_MISMATCH': '確認コードが正しくありません。',
214+
'AUTH_ERROR_CODE_EXPIRED': '確認コードの有効期限が切れています。新しいコードをリクエストしてください。',
215+
'AUTH_ERROR_TOO_MANY_REQUESTS': 'リクエストが多すぎます。しばらく待ってからやり直してください。',
216+
'AUTH_ERROR_TOO_MANY_ATTEMPTS': '失敗回数が多すぎます。しばらく待ってからやり直してください。',
217+
'AUTH_ERROR_CODE_DELIVERY_FAILED': '確認コードを送信できませんでした。しばらくしてからもう一度お試しください。',
218+
'AUTH_ERROR_ALREADY_AUTHENTICATED': 'すでにサインインしています。',
219+
'AUTH_ERROR_INVALID_PARAMETER': '入力内容が正しくありません。確認してからやり直してください。'
176220
}
177221
}

packages/ui/src/amplify/ui.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { AuthFormRootContext, t, useAuthFormState } from './core'
99
import * as Auth from 'aws-amplify/auth'
1010
import { Skeleton } from '@/components/ui/skeleton'
1111
import * as React from 'react'
12+
import { getAuthErrorMessageKey } from './errors'
1213

1314
function ErrorTip({ error, removeError }: {
1415
error: string | { variant?: 'warning' | 'destructive', title?: string, message: string | any },
@@ -108,6 +109,10 @@ function validatePasswordPolicy(password: string) {
108109
}
109110
}
110111

112+
function getAuthErrorMessage(error: unknown) {
113+
return t(getAuthErrorMessageKey(error))
114+
}
115+
111116
function useCountDown() {
112117
const [countDownNum, setCountDownNum] = useState<number>(0)
113118
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
@@ -238,7 +243,7 @@ export function LoginForm() {
238243
throw new Error(`${t('Unsupported sign-in step:')} ${nextStep}`)
239244
}
240245
} catch (e) {
241-
setErrors({ password: { message: (e as Error).message, title: t('Bad Response.') } })
246+
setErrors({ password: { message: getAuthErrorMessage(e), title: t('Bad Response.') } })
242247
console.error(e)
243248
} finally {
244249
setLoading(false)
@@ -349,7 +354,7 @@ export function SignupForm() {
349354
}
350355
} catch (e: any) {
351356
console.error(e)
352-
const error = { title: t('Bad Response.'), message: (e as Error).message }
357+
const error = { title: t('Bad Response.'), message: getAuthErrorMessage(e) }
353358
let k = 'confirm_password'
354359
if (e.name === 'UsernameExistsException') {
355360
k = 'username'
@@ -434,21 +439,25 @@ export function ResetPasswordForm() {
434439
setIsSentCode(true)
435440
} catch (error) {
436441
console.error('Error sending reset code:', error)
437-
setErrors({ email: { message: (error as Error).message, title: t('Bad Response.') } })
442+
setErrors({ email: { message: getAuthErrorMessage(error), title: t('Bad Response.') } })
438443
} finally {
439444
setLoading(false)
440445
}
441446
} else {
442447
// confirm reset password
443-
if ((data.password as string)?.length < 8) {
448+
try {
449+
validatePasswordPolicy(data.password as string)
450+
} catch (error) {
444451
setErrors({
445452
password: {
446-
message: t('Password must be at least 8 characters.'),
453+
message: (error as Error).message,
447454
title: t('Invalid Password')
448455
}
449456
})
450457
return
451-
} else if (data.password !== data.confirm_password) {
458+
}
459+
460+
if (data.password !== data.confirm_password) {
452461
setErrors({
453462
confirm_password: {
454463
message: t('Passwords do not match.'),
@@ -469,7 +478,7 @@ export function ResetPasswordForm() {
469478
setCurrentTab('login')
470479
} catch (error) {
471480
console.error('Error confirming reset password:', error)
472-
setErrors({ 'confirm_password': { message: (error as Error).message, title: t('Bad Response.') } })
481+
setErrors({ 'confirm_password': { message: getAuthErrorMessage(error), title: t('Bad Response.') } })
473482
} finally {
474483
setLoading(false)
475484
}
@@ -490,7 +499,7 @@ export function ResetPasswordForm() {
490499
console.debug('[Auth] reset pw code re-sent: ', ret)
491500
} catch (error) {
492501
console.error('Error resending reset code:', error)
493-
setErrors({ email: { message: (error as Error).message, title: t('Bad Response.') } })
502+
setErrors({ email: { message: getAuthErrorMessage(error), title: t('Bad Response.') } })
494503
} finally {}
495504
}} className={'text-sm opacity-70 hover:opacity-90 underline absolute top-3 right-0 select-none'}>
496505
{t('Resend code')}
@@ -600,7 +609,7 @@ export function ConfirmWithCodeForm(
600609
console.debug('confirmSignIn: ', ret)
601610
}
602611
} catch (e) {
603-
setErrors({ code: { message: (e as Error).message, title: t('Bad Response.') } })
612+
setErrors({ code: { message: getAuthErrorMessage(e), title: t('Bad Response.') } })
604613
console.error(e)
605614
} finally {
606615
setLoading(false)
@@ -643,7 +652,7 @@ export function ConfirmWithCodeForm(
643652
// await Auth.resendSignInCode(props.user)
644653
}
645654
} catch (e) {
646-
setErrors({ code: { message: (e as Error).message, title: t('Bad Response.') } })
655+
setErrors({ code: { message: getAuthErrorMessage(e), title: t('Bad Response.') } })
647656
setCountDownNum(0)
648657
console.error(e)
649658
} finally {}

packages/ui/src/i18n.test.mts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import test from 'node:test'
22
import assert from 'node:assert/strict'
33

44
import { setLocale, setNSDicts, setTranslate, translate } from './i18n'
5+
import { getAuthErrorMessageKey } from './amplify/errors'
56

67
test('translate uses the selected locale when the namespace dict contains it', () => {
78
setTranslate((locale, dicts, key, ...args) => dicts[locale]?.[key] ?? args[0] ?? key)
@@ -34,3 +35,14 @@ test('translate falls back to English when the current locale dict has no corres
3435

3536
assert.equal(translate('fallback', 'greeting'), 'Hello')
3637
})
38+
39+
test('getAuthErrorMessageKey maps common Cognito errors to localized keys', () => {
40+
assert.equal(getAuthErrorMessageKey({ name: 'NotAuthorizedException' }), 'AUTH_ERROR_INVALID_CREDENTIALS')
41+
assert.equal(getAuthErrorMessageKey({ name: 'CodeMismatchException' }), 'AUTH_ERROR_CODE_MISMATCH')
42+
assert.equal(getAuthErrorMessageKey({ name: 'InvalidPasswordException' }), 'PW_POLICY_TIP')
43+
})
44+
45+
test('getAuthErrorMessageKey falls back to a generic localized key for unknown errors', () => {
46+
assert.equal(getAuthErrorMessageKey({ name: 'SomethingUnexpected' }), 'AUTH_ERROR_GENERIC')
47+
assert.equal(getAuthErrorMessageKey(new Error('plain error')), 'AUTH_ERROR_GENERIC')
48+
})

0 commit comments

Comments
 (0)