diff --git a/apps/wallet/components/hooks/use-change-password-mutation.ts b/apps/wallet/components/hooks/use-change-password-mutation.ts index 8030b603e05cbfbee93eb1d21b2159fb2167c0bc..e1f25855bf83244946a5c0e988d94904d049aea3 100644 --- a/apps/wallet/components/hooks/use-change-password-mutation.ts +++ b/apps/wallet/components/hooks/use-change-password-mutation.ts @@ -2,6 +2,9 @@ import { useMutation } from '@tanstack/react-query'; import { transactionService } from '@transaction/index'; import { toast } from '@ui/components/hooks/use-toast'; import { logger } from '@ui/lib/logger'; +import { useUser } from '@smart-signer/lib/auth/use-user'; +import { hbauthService } from '@smart-signer/lib/hbauth-service'; +import { KeyAuthorityType } from '@smart-signer/lib/utils'; /** * Makes change master password transaction. @@ -10,13 +13,37 @@ import { logger } from '@ui/lib/logger'; * @returns */ export function useChangePasswordMutation() { + const { user } = useUser(); const changePasswordMutation = useMutation({ - mutationFn: async (params: { account: string; keys: Record }) => { + mutationFn: async (params: { + account: string; + keys: Record; + wifs: Record; + }) => { const broadcastResult = await transactionService.changeMasterPassword(params.account, params.keys, { observe: true, singleSignKeyType: 'owner' }); const response = { ...params, broadcastResult }; + + const registeredSafeStorageUser = await ( + await hbauthService.getOnlineClient() + ).getRegisteredUserByUsername(params.account); + + for (const keyType in params.keys) { + if ( + registeredSafeStorageUser?.registeredKeyTypes.includes(keyType as KeyAuthorityType) && + params.wifs[keyType] + ) { + await ( + await hbauthService.getOnlineClient() + ).invalidateExistingKey(params.account, keyType as KeyAuthorityType); + await ( + await hbauthService.getOnlineClient() + ).importKey(params.account, params.wifs[keyType], keyType as KeyAuthorityType); + } + } + toast({ title: 'Master password changed', description: 'Your master password has been changed successfully', diff --git a/apps/wallet/components/lang-toggle.tsx b/apps/wallet/components/lang-toggle.tsx index d9deeb24684b8415aeda2b2c769f467618658d2e..f54004fc0c89c7f2edcaef9a0c2afca3755907e1 100644 --- a/apps/wallet/components/lang-toggle.tsx +++ b/apps/wallet/components/lang-toggle.tsx @@ -19,7 +19,14 @@ export default function LangToggle({ logged }: { logged: Boolean }) { const { t } = useTranslation('common_wallet'); useEffect(() => { - setLang(getCookie('NEXT_LOCALE') || 'en'); + const savedLang = getCookie('NEXT_LOCALE') || 'en'; + if (!lang) { + setLang(savedLang); + if (router.locale !== savedLang) { + router.push(router.asPath, router.asPath, { locale: savedLang }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const languages = [ @@ -35,6 +42,22 @@ export default function LangToggle({ logged }: { logged: Boolean }) { { locale: 'zh', label: '🇨🇳' } ]; + const handleLanguageChange = (locale: string) => { + // Delete any existing NEXT_LOCALE cookies first + document.cookie = 'NEXT_LOCALE=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT'; + // Set new cookie with proper path and attributes + document.cookie = `NEXT_LOCALE=${locale}; path=/; SameSite=Lax`; + setLang(locale); + + // Update the URL and locale using Next.js router + router.push(router.asPath, router.asPath, { locale }).then(() => { + // Only reload if absolutely necessary + if (document.documentElement.lang !== locale) { + router.reload(); + } + }); + }; + return ( @@ -52,13 +75,7 @@ export default function LangToggle({ logged }: { logged: Boolean }) { {languages.map(({ locale, label }) => ( - { - document.cookie = `NEXT_LOCALE=${locale}; SameSite=Lax`; - router.reload(); - }} - > + handleLanguageChange(locale)}> {label}  {locale} diff --git a/apps/wallet/locales/en/common_wallet.json b/apps/wallet/locales/en/common_wallet.json index 94870528cdd12570739224a952185adf82353fc6..a8623a5c41c00a88e985a3222abeb669375d9550 100644 --- a/apps/wallet/locales/en/common_wallet.json +++ b/apps/wallet/locales/en/common_wallet.json @@ -327,12 +327,14 @@ "change_password_page": { "change_password": "Change Password", "account_name": "Account Name", - "current_password": "Current Password", + "current_password": "Current Master Password", "recover_password": "Recover Account", - "generated_password": "Generated Password", + "generated_password": "Generated Password and Keys", "new": "New", - "click_to_generate_password": "Click to generate password", - "re_enter_generate_password": "Re-enter Generated Password", + "hide": "Hide", + "show": "Show", + "click_to_generate_password": "Click to generate new master password", + "re_enter_generate_password": "Re-enter Generated Master Password", "understand_that": "I understand that Hive cannot recover lost passwords", "i_saved_password": "I have securely saved my generated password", "update_password": "Update Password", @@ -340,15 +342,23 @@ "account_name_should_be_longer": "Account name should be longer.", "backup_password_by_storing_it": "Back it up by storing in your password manager or a text file", "passwords_do_not_match": "Passwords do not match", - "the_rules": { - "one": "The first rule of Hive is: Do not lose your password.", - "second": "The second rule of Hive is: Do not lose your password.", - "third": "The third rule of Hive is: We cannot recover your password.", - "fourth": "The fourth rule: If you can remember the password, it's not secure.", - "fifth": "The fifth rule: Use only randomly-generated passwords.", - "sixth": "The sixth rule: Do not tell anyone your password.", - "seventh": "The seventh rule: Always back up your password." - } + "key_titles": { + "master": "Master Password", + "owner": "Owner Key", + "active": "Active Key", + "posting": "Posting Key", + "memo": "Memo Key" + }, + "new_keys": "New Keys", + "security_notes": { + "title": "Security Notes", + "m1": "Save these keys immediately in a secure password manager", + "m2": "The master password can derive all other keys", + "m3": "Lost keys cannot be recovered", + "m4": "Never share your private keys", + "confirm_security": "I understand and accept these security implications" + }, + "save_all": "Save All" }, "four_oh_four": { "this_page_does_not_exist": "Sorry! This page does not exist.", diff --git a/apps/wallet/pages/[param]/password.tsx b/apps/wallet/pages/[param]/password.tsx index 5901ef418b386e4756569c9be8215fcf0abb4e82..1421924bae24f883e9e3ed7b784a4bf7174c4b5b 100644 --- a/apps/wallet/pages/[param]/password.tsx +++ b/apps/wallet/pages/[param]/password.tsx @@ -18,7 +18,7 @@ import { useChangePasswordMutation } from '@/wallet/components/hooks/use-change- import { handleError } from '@ui/lib/handle-error'; import { Icons } from '@ui/components/icons'; import { hiveChainService } from '@transaction/lib/hive-chain-service'; -import Head from 'next/head'; +import { cn } from '@ui/lib/utils'; export const getServerSideProps: GetServerSideProps = async (ctx) => { const username = ctx.params?.param as string; @@ -30,33 +30,51 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { }; }; -export default function PostForm({ metadata }: { metadata: MetadataProps }) { +// New type for derived keys +interface DerivedKeys { + master: string; + owner: string; + active: string; + posting: string; + memo: string; +} + +// Add new types for key display +interface KeyDisplay { + type: 'master' | 'owner' | 'active' | 'posting' | 'memo'; + value: string; + description: string; +} + +export default function PostForm() { const { t } = useTranslation('common_wallet'); const [isKeyGenerated, setIsKeyGenerated] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [publicKeys, setPublicKeys] = useState<{ - active: string; - owner: string; - posting: string; - }>(); + const { username } = useSiteParams(); const changePasswordMutation = useChangePasswordMutation(); const [generatedPassword, setGeneratedPassword] = useState(''); + const [derivedKeys, setDerivedKeys] = useState(null); + const [securityAccepted, setSecurityAccepted] = useState(false); + const [copiedStates, setCopiedStates] = useState>({}); const accountFormSchema = useMemo( () => z.object({ name: z.string().min(2, 'Account name should be longer'), - curr_password: z.string().min(2, { message: 'Required owner key or current password' }), + curr_password: z.string().min(2, { message: 'Required master password' }), genereted_password: z.string().refine((value) => value === generatedPassword, { message: 'Passwords do not match' }), - understand: z.boolean().refine((value) => value === true, { + understand_master: z.boolean().refine((value) => value === true, { message: 'Required' }), saved_password: z.boolean().refine((value) => value === true, { message: 'Required' + }), + understand_security: z.boolean().refine((value) => value === true, { + message: 'Required' }) }), [generatedPassword] @@ -70,7 +88,8 @@ export default function PostForm({ metadata }: { metadata: MetadataProps }) { name: username, curr_password: '', genereted_password: '', - understand: false, + understand_master: false, + understand_security: false, saved_password: false } }); @@ -101,7 +120,12 @@ export default function PostForm({ metadata }: { metadata: MetadataProps }) { async function resolveChangePasswordComponents(password: string): Promise<{ account: string; keys: Record; + wifs: Record; }> { + if (!generatedPassword) { + throw new Error('Generated password is required'); + } + const hiveChain = await hiveChainService.getHiveChain(); // password !== WIF @@ -109,14 +133,10 @@ export default function PostForm({ metadata }: { metadata: MetadataProps }) { const oldActive = hiveChain.getPrivateKeyFromPassword(username, 'active', password); const oldPosting = hiveChain.getPrivateKeyFromPassword(username, 'posting', password); - // generate password - const brainKeyData = hiveChain.suggestBrainKey(); - const passwordToBeSavedByUser = 'P' + brainKeyData.wifPrivateKey; - // private keys for account authorities - const newOwner = hiveChain.getPrivateKeyFromPassword(username, 'owner', passwordToBeSavedByUser); - const newActive = hiveChain.getPrivateKeyFromPassword(username, 'active', passwordToBeSavedByUser); - const newPosting = hiveChain.getPrivateKeyFromPassword(username, 'posting', passwordToBeSavedByUser); + const newOwner = hiveChain.getPrivateKeyFromPassword(username, 'owner', generatedPassword); + const newActive = hiveChain.getPrivateKeyFromPassword(username, 'active', generatedPassword); + const newPosting = hiveChain.getPrivateKeyFromPassword(username, 'posting', generatedPassword); return { account: username, @@ -133,6 +153,11 @@ export default function PostForm({ metadata }: { metadata: MetadataProps }) { old: oldPosting.associatedPublicKey, new: newPosting.associatedPublicKey } + }, + wifs: { + owner: newOwner.wifPrivateKey, + active: newActive.wifPrivateKey, + posting: newPosting.wifPrivateKey } }; } @@ -142,176 +167,295 @@ export default function PostForm({ metadata }: { metadata: MetadataProps }) { const brainKeyData = wax.suggestBrainKey(); const passwordToBeSavedByUser = 'P' + brainKeyData.wifPrivateKey; - const newOwner = wax.getPrivateKeyFromPassword(username, 'owner', passwordToBeSavedByUser); - const newActive = wax.getPrivateKeyFromPassword(username, 'active', passwordToBeSavedByUser); - const newPosting = wax.getPrivateKeyFromPassword(username, 'posting', passwordToBeSavedByUser); - - setPublicKeys({ - active: newActive.associatedPublicKey, - owner: newOwner.associatedPublicKey, - posting: newPosting.associatedPublicKey - }); + const newKeys = { + master: passwordToBeSavedByUser, + owner: wax.getPrivateKeyFromPassword(username, 'owner', passwordToBeSavedByUser).wifPrivateKey, + active: wax.getPrivateKeyFromPassword(username, 'active', passwordToBeSavedByUser).wifPrivateKey, + posting: wax.getPrivateKeyFromPassword(username, 'posting', passwordToBeSavedByUser).wifPrivateKey, + memo: wax.getPrivateKeyFromPassword(username, 'memo', passwordToBeSavedByUser).wifPrivateKey + }; + setDerivedKeys(newKeys); setGeneratedPassword(passwordToBeSavedByUser); setIsKeyGenerated(true); } - return ( - <> - - {metadata.tabTitle} - - - - - - -
-
{t('change_password_page.change_password')}
- -

- {t('change_password_page.the_rules.one')} -
- {t('change_password_page.the_rules.second')} -
- {t('change_password_page.the_rules.third')} -
- {t('change_password_page.the_rules.fourth')} -
- {t('change_password_page.the_rules.fifth')} -
- {t('change_password_page.the_rules.sixth')} -
- {t('change_password_page.the_rules.seventh')} -

- -
- - ( - - {t('change_password_page.account_name')} - - - - - - )} - /> - ( - - - {t('change_password_page.current_password')}{' '} - - {t('change_password_page.recover_password')} - + // Modify the UI part for displaying generated credentials + const KeyDisplaySection = ({ derivedKeys }: { derivedKeys: DerivedKeys }) => { + const { t } = useTranslation('common_wallet'); + const [showKeys, setShowKeys] = useState(false); + const [copiedStates, setCopiedStates] = useState>({}); + + const keys: KeyDisplay[] = [ + { + type: 'master', + value: derivedKeys.master, + description: t('permissions.master_key.info') + }, + { + type: 'owner', + value: derivedKeys.owner, + description: t('permissions.owner_key.info') + }, + { + type: 'active', + value: derivedKeys.active, + description: t('permissions.active_key.info') + }, + { + type: 'posting', + value: derivedKeys.posting, + description: t('permissions.posting_key.info') + }, + { + type: 'memo', + value: derivedKeys.memo, + description: t('permissions.memo_key.info') + } + ]; + + const copyToClipboard = (text: string, keyType: string) => { + navigator.clipboard.writeText(text); + setCopiedStates((prev) => ({ ...prev, [keyType]: true })); + setTimeout(() => { + setCopiedStates((prev) => ({ ...prev, [keyType]: false })); + }, 2000); + }; + + const downloadKeys = () => { + const content = `HIVE ACCOUNT: ${username} +GENERATED: ${new Date().toISOString()} + +${keys.map((k) => `${k.type.toUpperCase()} KEY:\n${k.value}\n`).join('\n---\n\n')} + +IMPORTANT: +- Save these keys immediately in a secure password manager +- The master password can derive all other keys +- Lost keys cannot be recovered +- Never share your private keys`; + + const blob = new Blob([content], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `hive-keys-${username}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + return ( +
+
+

+ {t('change_password_page.security_notes.title')} +

+
    +
  • {t('change_password_page.security_notes.m1')}
  • +
  • {t('change_password_page.security_notes.m2')}
  • +
  • {t('change_password_page.security_notes.m3')}
  • +
  • {t('change_password_page.security_notes.m4')}
  • +
+
+ ( + + + { + field.onChange(checked); + setSecurityAccepted(checked as boolean); + }} + /> + +
+ + {t('change_password_page.security_notes.confirm_security')} - - - - - )} - /> -
-
- {t('change_password_page.generated_password')} - ({t('change_password_page.new')}) -
- {isKeyGenerated ? ( -
- - {generatedPassword} - -
- {t('change_password_page.backup_password_by_storing_it')} -
- ) : ( - - )} + + )} + /> +
+
+ + {securityAccepted && form.getValues().understand_security && ( + <> +
+

{t('change_password_page.new_keys')}

+
+ +
- ( - - {t('change_password_page.re_enter_generate_password')} - - - +
+ +
+ {keys.map((key) => ( +
+
+ + {t(`change_password_page.key_titles.${key.type}`)} + + +
+
+ {showKeys ? key.value : '••••••••••••••••••••••••••'} +
+
+ ))} +
+ + )} +
+ ); + }; + + return ( + + +
+
{t('change_password_page.change_password')}
+ + + + ( + + {t('change_password_page.account_name')} + + + + + + )} + /> + ( + + + {t('change_password_page.current_password')}{' '} + + {t('change_password_page.recover_password')} + + + + + + + + )} + /> +
+
{t('change_password_page.generated_password')}
+ {isKeyGenerated && derivedKeys ? ( + + ) : ( + + )} +
+ ( + + {t('change_password_page.re_enter_generate_password')} + + + + + + )} + /> + ( + + + + +
+ {t('change_password_page.understand_that')} {' '} - - )} - /> - ( - - - - -
- {t('change_password_page.understand_that')} {' '} - -
-
- )} - />{' '} - ( - - - - +
+
+ )} + /> + ( + + + + -
- {t('change_password_page.i_saved_password')}{' '} - -
-
- )} - /> - - - -
-
- +
+ {t('change_password_page.i_saved_password')}{' '} + +
+ + )} + /> + + + +
+ ); } diff --git a/apps/wallet/pages/_app.tsx b/apps/wallet/pages/_app.tsx index e95ae780fd2568cfe560ba46f78deaad08e82156..c523ab43963b8c9760b213c63da332f0c61f5388 100644 --- a/apps/wallet/pages/_app.tsx +++ b/apps/wallet/pages/_app.tsx @@ -10,8 +10,9 @@ const Providers = lazy(() => import('@/wallet/components/common/providers')); function App({ Component, pageProps }: AppProps) { useLayoutEffect(() => { - if (!getCookie('NEXT_LOCALE')) { - document.cookie = `NEXT_LOCALE=${i18n.defaultLocale}; SameSite=Lax`; + const currentLocale = getCookie('NEXT_LOCALE'); + if (!currentLocale || !i18nConfig.i18n.locales.includes(currentLocale)) { + document.cookie = `NEXT_LOCALE=${i18n.defaultLocale}; path=/; SameSite=Lax`; } }, []); diff --git a/apps/wallet/playwright/tests/e2e/witnesses.spec.ts b/apps/wallet/playwright/tests/e2e/witnesses.spec.ts index bf71c2414b27301f68887b5cee53d520790ce1f7..44a491edbe6c5289252176e3b8cb6b78d5005e06 100644 --- a/apps/wallet/playwright/tests/e2e/witnesses.spec.ts +++ b/apps/wallet/playwright/tests/e2e/witnesses.spec.ts @@ -451,6 +451,7 @@ test.describe('Witnesses page tests', () => { await expect(homePage.languageMenu.first()).toBeVisible(); await homePage.languageMenuPl.click(); await expect(witnessPage.witnessHeaderTitle).toBeVisible(); + await homePage.page.waitForTimeout(3000); await expect(await witnessPage.witnessHeaderTitle.textContent()).toBe('Głosowanie na delegatów'); await expect(await witnessPage.witnessHeaderDescription.textContent()).toBe( 'Na poniższej liście znajduje sie 100 pierwszych delegatów, aktywnych jak również nieaktywnych. Każdy delegat powyżej 100 pozycji jest filtrowany i nie wyświetlany jeśli nie wyprodukował bloku w ostatnich 30 dniach.' diff --git a/packages/smart-signer/components/auth/form.tsx b/packages/smart-signer/components/auth/form.tsx index 3df8a33d2299f39ad1fd0fb97673cad726124bea..58c9bc624a7ece869ef59377685acc2ba64d061d 100644 --- a/packages/smart-signer/components/auth/form.tsx +++ b/packages/smart-signer/components/auth/form.tsx @@ -6,6 +6,7 @@ import { KeyType, LoginType } from '@smart-signer/types/common'; import { useProcessAuth, LoginFormSchema } from './process'; import { useLocalStorage } from 'usehooks-ts'; import Methods from './methods/methods'; +import SafeStorageKeyUpdate from './methods/safestorage-key-update'; export interface SignInFormProps { preferredKeyTypes: KeyType[]; @@ -19,7 +20,8 @@ export type SignInFormRef = { cancel: () => Promise }; export enum Steps { SAFE_STORAGE_LOGIN = 1, - OTHER_LOGIN_OPTIONS + SAFE_STORAGE_KEY_UPDATE, + OTHER_LOGIN_OPTIONS, } const SignInForm = forwardRef( @@ -89,6 +91,16 @@ const SignInForm = forwardRef( submit={submit} /> )} + + {step === Steps.SAFE_STORAGE_KEY_UPDATE && ( + setUsername(username)} + /> + )}
); } diff --git a/packages/smart-signer/components/auth/methods/safestorage-key-update.tsx b/packages/smart-signer/components/auth/methods/safestorage-key-update.tsx new file mode 100644 index 0000000000000000000000000000000000000000..36a3270220cf32d697d8d459e95aab52cef7032a --- /dev/null +++ b/packages/smart-signer/components/auth/methods/safestorage-key-update.tsx @@ -0,0 +1,372 @@ +/* Sign-in with safe storage (use beekeeper wallet through hb-auth) */ +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'next-i18next'; +import { AuthUser, AuthorizationError, OnlineClient } from '@hiveio/hb-auth'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { username } from '@smart-signer/lib/auth/utils'; +import { hbauthService } from '@smart-signer/lib/hbauth-service'; +import { Icons } from '@hive/ui/components/icons'; +import { + Input, + RadioGroup, + RadioGroupItem, + Label, + Button, + Separator, + Form, + FormField, + FormItem, + FormControl, + FormMessage, + Checkbox, + TooltipProvider, + Tooltip, + TooltipTrigger, + TooltipContent +} from '@hive/ui'; +import Step from '../step'; +import { Steps } from '../form'; +import { KeyType, LoginType } from '@smart-signer/types/common'; +import { TFunction } from 'i18next'; +import { validateWifKey } from '@smart-signer/lib/validators/validate-wif-key'; +import { KeyAuthorityType } from '@smart-signer/lib/utils'; +import { toast } from '@ui/components/hooks/use-toast'; +import { logger } from '@ui/lib/logger'; + +export interface SafeStorageKeyUpdateProps { + onSetStep: (step: Steps) => void; + i18nNamespace: string; + preferredKeyTypes: KeyType[]; + username: string; + onUsernameChange: (username: string) => void; +} + +function getFormSchema(t: TFunction<'smart-signer', undefined>) { + return z + .object({ + username, + password: z.string().min(1, { + message: t('login_form.zod_error.password_required') + }), + wif: z.string().min(1, { + message: t('login_form.zod_error.invalid_wif') + }), + keyType: z.nativeEnum(KeyType, { + invalid_type_error: t('login_form.zod_error.invalid_keytype'), + required_error: t('login_form.zod_error.keytype_required') + }), + isStrict: z.boolean().default(false) + }) + .superRefine((val, ctx) => { + const result = validateWifKey(val.wif, t); + if (result) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: result, + path: ['wif'], + fatal: true + }); + return z.NEVER; + } + return true; + }); +} + +export type SafeStorageKeyUpdateRef = { cancel: () => Promise }; + +type SafeStorageKeyUpdateForm = z.infer>; + +const SafeStorageKeyUpdate = forwardRef( + ({ onSetStep, preferredKeyTypes, i18nNamespace, username, onUsernameChange }, ref) => { + useImperativeHandle(ref, () => ({ + async cancel() { + // No need to cancel anything for key update + } + })); + + const authClient = useRef(); + const { t } = useTranslation(i18nNamespace); + const [loading, setLoading] = useState(undefined); + const [error, setError] = useState(null); + const [registeredUser, setRegisteredUser] = useState(null); + const [availableKeyTypes, setAvailableKeyTypes] = useState([]); + const [updateSuccess, setUpdateSuccess] = useState(false); + + const form = useForm({ + mode: 'onChange', + resolver: zodResolver(getFormSchema(t)), + defaultValues: { + username, + password: '', + wif: '', + keyType: preferredKeyTypes[0], + isStrict: false + } + }); + + // Check if user exists in hbauth and get available key types + useEffect(() => { + (async () => { + try { + setLoading(true); + setError(null); + + // Get the hbauth client + authClient.current = await hbauthService.getOnlineClient(); + + // Check if user exists in hbauth + const user = await authClient.current.getRegisteredUserByUsername(username); + + if (user) { + setRegisteredUser(user); + + // Filter preferred key types to only those registered in hbauth + const availableTypes = preferredKeyTypes.filter((keyType) => + user.registeredKeyTypes.includes(keyType as KeyAuthorityType) + ); + + setAvailableKeyTypes(availableTypes.length > 0 ? availableTypes : preferredKeyTypes); + + // Set the first available key type as default + if (availableTypes.length > 0) { + form.setValue('keyType', availableTypes[0]); + } + } else { + setError(t('login_form.signin_safe_storage.user_not_found')); + } + } catch (error) { + setError((error as AuthorizationError).message); + } finally { + setLoading(false); + } + })(); + }, [username, preferredKeyTypes, form, t]); + + // Handle key update + async function onUpdateKey(values: SafeStorageKeyUpdateForm) { + const { username, password, wif, keyType, isStrict } = values; + try { + setLoading(true); + setError(null); + + if (!registeredUser) { + setError(t('login_form.signin_safe_storage.user_not_found')); + return; + } + + // Try to unlock the wallet with the provided password + try { + await authClient.current?.unlock(username, password); + logger.info('Wallet unlocked successfully for user: %s', username); + } catch (unlockError) { + // If unlock fails, we'll continue with the key update process + logger.warn('Failed to unlock wallet for user: %s, continuing with key update', username); + } + + // Invalidate the existing key + await authClient.current?.invalidateExistingKey(username, keyType as KeyAuthorityType); + + // Import the new key + await authClient.current?.register(username, password, wif, keyType as KeyAuthorityType, isStrict); + + setUpdateSuccess(true); + + toast({ + title: t('login_form.signin_safe_storage.key_updated_title'), + description: t('login_form.signin_safe_storage.key_updated_description'), + variant: 'success' + }); + + logger.info('Key updated successfully for user: %s, keyType: %s', username, keyType); + + // Reset form after successful update + form.reset(); + + // Go back to previous step after a short delay + setTimeout(() => { + onSetStep(Steps.SAFE_STORAGE_LOGIN); + }, 2000); + } catch (error) { + if (typeof error === 'string') { + setError(t(error)); + } else { + setError(t('login_form.zod_error.invalid_wif')); + } + setLoading(false); + } + } + + if (loading === undefined) return ; + + return ( + +
+ {registeredUser + ? t('login_form.signin_safe_storage.update_key_description', { + username, + keyType: form.getValues().keyType + }) + : t('login_form.signin_safe_storage.user_not_found')} +
+ {error &&
{error}
} + {updateSuccess && ( +
+ {t('login_form.signin_safe_storage.key_updated_success')} +
+ )} +
+ } + loading={loading} + > +
+ + ( + + + + + + + )} + /> + + {availableKeyTypes.length > 1 && ( + ( + + + + {availableKeyTypes.map((type) => { + return ( + + + + + + + ); + })} + + + + + )} + /> + )} + + ( + + + + + + + )} + /> + + ( + + + + + + + )} + /> + + ( +
+ { + field.onChange(isStrict); + }} + {...field} + checked={field.value} + value={field.value as unknown as string} + /> + + + + + + + + + {t('login_form.signin_safe_storage.strict_mode_tooltip')} + + + +
+ )} + /> + +
+ + +
+ + + + ); + } +); + +SafeStorageKeyUpdate.displayName = 'SafeStorageKeyUpdate'; + +export default SafeStorageKeyUpdate; diff --git a/packages/smart-signer/components/auth/methods/safestorage.tsx b/packages/smart-signer/components/auth/methods/safestorage.tsx index 12551e49d09f0cf213bd69e5ae831f6fc339659a..12317341fa42e7fe7837de1480e7c7822844c132 100644 --- a/packages/smart-signer/components/auth/methods/safestorage.tsx +++ b/packages/smart-signer/components/auth/methods/safestorage.tsx @@ -35,6 +35,7 @@ import { Steps } from '../form'; import { KeyType, LoginType } from '@smart-signer/types/common'; import { TFunction } from 'i18next'; import { validateWifKey } from '@smart-signer/lib/validators/validate-wif-key'; +import { useRouter } from 'next/router'; function getFormSchema(t: TFunction<'smart-signer', undefined>) { return z @@ -112,6 +113,7 @@ const SafeStorage = forwardRef( strict: true } }); + const router = useRouter(); async function onSave(values: SafeStorageForm) { const { username, password, wif, keyType, strict } = values; @@ -135,7 +137,11 @@ const SafeStorage = forwardRef( await authClient.current?.authenticate(username, password, keyType); await finalize(values); } catch (error) { - setError((error as AuthorizationError).message); + const authError = error as AuthorizationError; + if (authError.message.includes('Not authorized') || authError.message.includes('Authentication failed')) { + onSetStep(Steps.SAFE_STORAGE_KEY_UPDATE); + } + setError(authError.message); setLoading(false); } } @@ -417,6 +423,13 @@ const SafeStorage = forwardRef( )} /> )} + {authUsers?.length > 0 && ( +
+ onSetStep(Steps.SAFE_STORAGE_KEY_UPDATE)} className="max-w-max cursor-pointer text-xs text-destructive hover:opacity-80 active:opacity-60"> + {t('login_form.signin_safe_storage.key_update')} + +
+ )} {/* Show this for new user, otherwise show unlock then authorize */}
{userFound ? ( diff --git a/packages/smart-signer/locales/en/smart-signer.json b/packages/smart-signer/locales/en/smart-signer.json index d5288562ad8d16764cf826aab2921d2a1cca64f1..19fa5177c5a25ae5a95ff02b0bbb05301a29ae29 100644 --- a/packages/smart-signer/locales/en/smart-signer.json +++ b/packages/smart-signer/locales/en/smart-signer.json @@ -88,7 +88,18 @@ "button_sign_auth": "Sign auth tx", "button_signin_unlocked": "Sign in with signed tx", "strict_mode": "Direct Authority Mode", - "strict_mode_tooltip": "When enabled, the app will only allow adding keys with account's own authority. If you want to add keys with other authority, please disable this mode." + "strict_mode_tooltip": "When enabled, the app will only allow adding keys with account's own authority. If you want to add keys with other authority, please disable this mode.", + "key_update": "Update existing saved key", + "update_key_title": "Update Key in Safe Storage", + "update_key_description": "Update {{keyType}} key for user {{username}}", + "update_key_button": "Update Key", + "key_updated_title": "Key Updated", + "key_updated_description": "Your key has been updated successfully", + "key_updated_success": "Key updated successfully! Now you can sign in again.", + "user_not_found": "User not found in safe storage", + "back": "Back", + "authenticated_title": "Authentication Successful", + "authenticated_description": "You have been authenticated successfully" }, "zod_error": { "password_length": "Password length should be more than 6 characters", @@ -96,7 +107,9 @@ "keytype_required": "keyType is required", "invalid_wif": "Invalid WIF key", "no_wif": "No WIF key from user", - "no_wif_key": "No WIF key" + "no_wif_key": "No WIF key", + "password_required": "Password is required", + "wif_required": "WIF key is required" }, "signin_with_keychain": "Hive Keychain extension", "signin_with_wif": "Sign in with WIF (Legacy)", diff --git a/packages/smart-signer/package.json b/packages/smart-signer/package.json index 0b78c2a33f36fd91f3aefa307ef4fdda8e530506..622d87e07511323ca11c84bcbbb7139617fcd32a 100644 --- a/packages/smart-signer/package.json +++ b/packages/smart-signer/package.json @@ -11,7 +11,7 @@ "@hiveio/dhive": "^1.3.0", "@hiveio/wax": "1.27.6-rc7-250404091259", "@hiveio/wax-signers-keychain": "1.27.6-rc7-stable.250314151849", - "@hiveio/hb-auth": "0.0.1-stable.250402093900", + "@hiveio/hb-auth": "0.0.1-stable.250409065242", "@tanstack/react-query": "^4.36.1", "change-case": "^5.4.3", "cors": "^2.8.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d79af0e5164cbbd96a141b3315176a3907647684..3ea6d075ff57bd932d1a1ebf0636492a4f879d76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -605,8 +605,8 @@ importers: specifier: ^1.3.0 version: 1.3.2 '@hiveio/hb-auth': - specifier: 0.0.1-stable.250402093900 - version: 0.0.1-stable.250402093900 + specifier: 0.0.1-stable.250409065242 + version: 0.0.1-stable.250409065242 '@hiveio/wax': specifier: 1.27.6-rc7-250404091259 version: 1.27.6-rc7-250404091259 @@ -1802,8 +1802,8 @@ packages: '@hiveio/dhive@1.3.2': resolution: {integrity: sha512-kJjp3TbpIlODxjJX4BWwvOf+cMxT8CFH/mNQ40RRjR2LP0a4baSWae1G+U/q/NtgjsIQz6Ja40tvnw6KF12I+g==, tarball: https://registry.npmjs.org/@hiveio/dhive/-/dhive-1.3.2.tgz} - '@hiveio/hb-auth@0.0.1-stable.250402093900': - resolution: {integrity: sha1-EO9bnyAYIPojMH/77xq9f79X+gQ=, tarball: https://gitlab.syncad.com/api/v4/projects/429/packages/npm/@hiveio/hb-auth/-/@hiveio/hb-auth-0.0.1-stable.250402093900.tgz} + '@hiveio/hb-auth@0.0.1-stable.250409065242': + resolution: {integrity: sha1-+Nz3A92RilnvseHCF4gRfOxnaag=, tarball: https://gitlab.syncad.com/api/v4/projects/429/packages/npm/@hiveio/hb-auth/-/@hiveio/hb-auth-0.0.1-stable.250409065242.tgz} '@hiveio/hivescript@1.3.3': resolution: {integrity: sha512-nOeGespwSujSaEl4R09u9FXGgMyOywtWWQiJdiWsmJDknCUqXXxTfs3KICssPTjkDlbGp2gkg3WjUyrlxm28Qg==, tarball: https://registry.npmjs.org/@hiveio/hivescript/-/hivescript-1.3.3.tgz} @@ -11480,7 +11480,7 @@ snapshots: transitivePeerDependencies: - encoding - '@hiveio/hb-auth@0.0.1-stable.250402093900': + '@hiveio/hb-auth@0.0.1-stable.250409065242': dependencies: '@hiveio/wax': 1.27.6-rc7-stable.250131113706 comlink: 4.4.2