Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

main: make error messages more accurate #678

Merged
merged 1 commit into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/__tests__/components/auth/AuthPage.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,6 @@ describe('AuthPage ', () => {
const input = getByLabelText('Enter your password in the field above');
fireEvent.change(input, { target: { value: 'test-pw' } });
fireEvent.click(getByText('Submit'));
expect(await findByText('oops, that password is incorrect')).toBeInTheDocument();
expect(await findByText('failed to connect')).toBeInTheDocument();
});
});
4 changes: 1 addition & 3 deletions app/src/__tests__/store/authStore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ describe('AuthStore', () => {
if (desc.methodName === 'GetInfo') throw new Error('test-err');
return undefined as any;
});
await expect(store.login('test-pw')).rejects.toThrow(
'oops, that password is incorrect',
);
await expect(store.login('test-pw')).rejects.toThrow('failed to connect');
expect(store.credentials).toBe('');
});

Expand Down
102 changes: 95 additions & 7 deletions app/src/components/auth/AuthPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import { ReactComponent as LogoImage } from 'assets/images/logo.svg';
import { usePrefixedTranslation } from 'hooks';
import { useStore } from 'store';
import { Background, Button, HeaderOne, Input } from 'components/base';
import { Background, Button, ChevronDown, ChevronUp, HeaderOne } from 'components/base';

const Styled = {
Wrapper: styled.div`
Expand Down Expand Up @@ -33,22 +33,57 @@ const Styled = {
text-align: center;
`,
Form: styled.form`
max-width: 550px;
display: flex;
flex-direction: column;
align-items: center;
`,
Label: styled.label`
margin: 10px 0 80px;
Password: styled.input`
font-family: ${props => props.theme.fonts.work.light};
font-weight: 300;
font-size: ${props => props.theme.sizes.xxl};
color: ${props => props.theme.colors.offWhite};
background-color: transparent;
border-width: 0;
border-bottom: 3px solid ${props => props.theme.colors.offWhite};
padding: 5px;
text-align: center;
width: 100%;

&:active,
&:focus {
outline: none;
background-color: ${props => props.theme.colors.overlay};
border-bottom-color: ${props => props.theme.colors.white};
}

&::placeholder {
color: ${props => props.theme.colors.gray};
}
`,
Label: styled.label``,
ErrMessage: styled.div`
width: 100%;
margin: 0 0 80px;
display: inline-block;
padding: 5px 0;
background-color: ${props => props.theme.colors.pink};
color: ${props => props.theme.colors.offWhite};
text-align: center;
`,
ErrDetail: styled.div`
width: 100%;
display: inline-block;
padding: 5px 0;
color: ${props => props.theme.colors.offWhite};
text-align: center;
`,
ErrDetailToggle: styled(Button)`
width: 100%;
padding: 5px 0;
background-color: transparent;
`,
Submit: styled(Button)`
margin-top: 80px;
background-color: transparent;
`,
};
Expand All @@ -58,10 +93,17 @@ const AuthPage: React.FC = () => {
const store = useStore();
const [pass, setPass] = useState('');
const [error, setError] = useState('');
const [errorDetailLit, setErrorDetailLit] = useState('');
const [errorDetailLnd, setErrorDetailLnd] = useState('');
const [errorDetailVisible, setErrorDetailVisible] = useState(false);
const [showDetailButton, setShowDetailButton] = useState(true);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPass(e.target.value);
setError('');
setErrorDetailLit('');
setErrorDetailLnd('');
setShowDetailButton(false);
};

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
Expand All @@ -70,14 +112,32 @@ const AuthPage: React.FC = () => {
await store.authStore.login(pass);
} catch (err) {
setError(err.message);
const errors = store.authStore.errors;
setErrorDetailLit(errors.litDetail);
setErrorDetailLnd(errors.lndDetail);

// don't display the detail toggle button if there is nothing to display
setShowDetailButton(errors.litDetail.length > 0 || errors.litDetail.length > 0);
}
};

// don't display the login UI until the app is fully initialized this prevents
// a UI flicker while validating credentials stored in session storage
if (!store.initialized) return null;

const { Wrapper, Logo, Title, Subtitle, Form, Label, ErrMessage, Submit } = Styled;
const {
Wrapper,
Logo,
Title,
Subtitle,
Form,
Password,
Label,
ErrMessage,
ErrDetail,
ErrDetailToggle,
Submit,
} = Styled;
return (
<Background gradient>
<Wrapper>
Expand All @@ -86,15 +146,43 @@ const AuthPage: React.FC = () => {
<Title>{l('terminal')}</Title>
<Subtitle>{l('subtitle')}</Subtitle>
<Form onSubmit={handleSubmit}>
<Input
<Password
id="auth"
type="password"
autoFocus
value={pass}
onChange={handleChange}
/>
{error ? (
<ErrMessage>{error}</ErrMessage>
<>
<ErrMessage>{error}</ErrMessage>
{errorDetailVisible && errorDetailLit.length > 0 ? (
<ErrDetail>{errorDetailLit}</ErrDetail>
) : (
''
)}
{errorDetailVisible && errorDetailLnd.length > 0 ? (
<ErrDetail>{errorDetailLnd}</ErrDetail>
) : (
''
)}
{showDetailButton ? (
<ErrDetailToggle
ghost
borderless
compact
nonfungible-human marked this conversation as resolved.
Show resolved Hide resolved
type="button"
onClick={() => {
setErrorDetailVisible(!errorDetailVisible);
}}
>
{!errorDetailVisible ? <ChevronDown /> : <ChevronUp />}
{!errorDetailVisible ? l('showDetail') : l('hideDetail')}
</ErrDetailToggle>
) : (
''
)}
</>
) : (
<Label htmlFor="auth">{l('passLabel')}</Label>
)}
Expand Down
10 changes: 9 additions & 1 deletion app/src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"cmps.auth.AuthPage.terminal": "Terminal",
"cmps.auth.AuthPage.subtitle": "Efficiently manage Lightning node liquidity",
"cmps.auth.AuthPage.passLabel": "Enter your password in the field above",
"cmps.auth.AuthPage.showDetail": "Show Detail",
"cmps.auth.AuthPage.hideDetail": "Hide Detail",
"cmps.auth.AuthPage.submitBtn": "Submit",
"cmps.common.Tile.maximizeTip": "Maximize",
"cmps.common.PageHeader.exportTip": "Download CSV",
Expand Down Expand Up @@ -399,6 +401,12 @@
"cmps.tour.SuccessStep.close": "Close",
"stores.authStore.emptyPassErr": "oops, password is required",
"stores.authStore.invalidPassErr": "oops, that password is incorrect",
"stores.authStore.noConnectionErr": "failed to connect",
"stores.authStore.walletLockedErr": "oops, wallet is locked",
"stores.authStore.litNotConnected": "Unable to connect to LiT. Please restart litd and try again.",
"stores.authStore.litNotRunning": "LiT is not running.",
"stores.authStore.lndNotRunning": "LND is not running. Please start lnd and try again.",
"stores.authStore.suggestWalletUnlock": " Please ensure that the wallet is unlocked.",
"stores.buildSwapView.noChannelsMsg": "You cannot perform a swap without any active channels",
"stores.orderFormView.buy": "Bid",
"stores.orderFormView.sell": "Ask",
Expand All @@ -416,7 +424,7 @@
"stores.settingsStore.httpError": "url must start with 'http'",
"stores.settingsStore.keyword": "url must contain {{keyword}}",
"stores.appView.authErrorTitle": "Your session has expired",
"stores.appView.authErrorMsg": "Please enter you password to continue",
"stores.appView.authErrorMsg": "Please enter your password to continue",
"views.fundNewAccountView.amountTooLow": "must be greater than {{accountMinimum}} sats",
"views.fundNewAccountView.amountTooHigh": "must be less than wallet balance",
"views.fundNewAccountView.lowExpireBlocks": "must be greater than {{blocks}} blocks",
Expand Down
80 changes: 78 additions & 2 deletions app/src/store/stores/authStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ import { Store } from 'store';

const { l } = prefixTranslation('stores.authStore');

class SubServerStatus {
disabled: boolean;
error: string;
running: boolean;

constructor() {
this.disabled = false;
this.error = '';
this.running = false;
}
}

export default class AuthStore {
private _store: Store;

Expand All @@ -14,6 +26,8 @@ export default class AuthStore {
/** the password encoded to base64 */
credentials = '';

errors = { mainErr: '', litDetail: '', lndDetail: '' };

constructor(store: Store) {
makeAutoObservable(this, {}, { deep: false, autoBind: true });

Expand All @@ -31,6 +45,67 @@ export default class AuthStore {
Object.values(this._store.api).forEach(api => api.setCredentials(credentials));
}

/**
* Convert exception to error message
*/
async getErrMsg(error: string) {
// determine the main error message
const invalidPassMsg = ['expected 1 macaroon, got 0'];
for (const m in invalidPassMsg) {
const errPos = error.lastIndexOf(invalidPassMsg[m]);
if (error.length - invalidPassMsg[m].length == errPos) {
this.errors.mainErr = l('invalidPassErr');
break;
}
}

let walletLocked = false;
if (this.errors.mainErr.length == 0) {
const walletLockedMsg = [
'wallet locked, unlock it to enable full RPC access',
'proxy error with context auth: unknown macaroon to use',
];
for (const m in walletLockedMsg) {
const errPos = error.lastIndexOf(walletLockedMsg[m]);
if (error.length - walletLockedMsg[m].length == errPos) {
walletLocked = true;
this.errors.mainErr = l('walletLockedErr');
break;
}
}
}

if (this.errors.mainErr.length == 0) this.errors.mainErr = l('noConnectionErr');

// get the subserver status message
try {
const serverStatus = await this._store.api.lit.listSubServerStatus();
// convert the response's nested arrays to an object mapping `subServerName` -> `{ disabled, running, error }`
const status = serverStatus.subServersMap.reduce(
(acc, [serverName, serverStatus]) => ({ ...acc, [serverName]: serverStatus }),
{} as Record<string, SubServerStatus>,
);

// check status
if (status.lit?.error) {
this.errors.litDetail = status.lit.error;
} else if (!status.lit?.running) {
this.errors.litDetail = l('litNotRunning');
if (walletLocked) this.errors.litDetail += l('suggestWalletUnlock');
}

if (status.lnd?.error) {
this.errors.lndDetail = status.lnd.error;
} else if (!status.lnd?.running) {
this.errors.lndDetail = l('lndNotRunning');
}
} catch (e) {
this.errors.litDetail = l('litNotConnected');
}

return this.errors.mainErr;
}

/**
* Validate the supplied password and save for later if successful
*/
Expand All @@ -49,8 +124,9 @@ export default class AuthStore {
} catch (error) {
// clear the credentials if incorrect
this.setCredentials('');
this._store.log.error('incorrect credentials');
throw new Error(l('invalidPassErr'));
this._store.log.error('connection failure');
this.errors = { mainErr: '', litDetail: '', lndDetail: '' };
throw new Error(await this.getErrMsg(error.message));
}
}

Expand Down