Skip to content

Commit 8b05cf4

Browse files
authored
Merge pull request #78 from js-accounts/typesafe-config
Added typings to configuration objects on all three packages
2 parents e2c9713 + cbd1c3f commit 8b05cf4

File tree

13 files changed

+178
-51
lines changed

13 files changed

+178
-51
lines changed

.eslintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
],
8484
"flowtype/type-id-match": [
8585
2,
86-
"^([A-Z][a-z0-9]+)+Type$"
86+
"^([A-Z][a-z0-9]+)+"
8787
],
8888
"flowtype/union-intersection-spacing": [
8989
2,

packages/client/src/AccountsClient.js

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import config from './config';
1717
import createStore from './createStore';
1818
import reducer, { loggingIn, setUser, clearUser, setTokens, clearTokens as clearStoreTokens } from './module';
1919
import type { TransportInterface } from './TransportInterface';
20+
import type { TokenStorage, AccountsClientConfiguration } from './config';
2021

2122
const isValidUserObject = (user: PasswordLoginUserIdentityType) => has(user, 'user') || has(user, 'email') || has(user, 'id');
2223

@@ -26,21 +27,15 @@ const REFRESH_TOKEN = 'accounts:refreshToken';
2627
const getTokenKey = (type: string, options: Object) =>
2728
(isString(options.tokenStoragePrefix) && options.tokenStoragePrefix.length > 0 ? `${options.tokenStoragePrefix}:${type}` : type);
2829

29-
export interface TokenStorage {
30-
getItem(key: string): Promise<string>,
31-
removeItem(key: string): Promise<string>,
32-
setItem(key: string, value: string): Promise<string>
33-
}
34-
3530
export class AccountsClient {
36-
options: Object;
31+
options: AccountsClientConfiguration;
3732
transport: TransportInterface;
38-
store: Store<Map<string, any>, Object>;
33+
store: Store<Object, Object>;
3934
storage: TokenStorage;
4035

41-
constructor(options: Object, transport: TransportInterface) {
36+
constructor(options: AccountsClientConfiguration, transport: TransportInterface) {
4237
this.options = options;
43-
this.storage = options.tokenStorage;
38+
this.storage = options.tokenStorage || config.tokenStorage;
4439
if (!transport) {
4540
throw new AccountsError('A REST or GraphQL transport is required');
4641
}
@@ -51,9 +46,10 @@ export class AccountsClient {
5146
options.reduxLogger,
5247
] : [];
5348

49+
const reduxStoreKey = options.reduxStoreKey || config.reduxStoreKey;
5450
this.store = options.store || createStore({
5551
reducers: {
56-
[options.reduxStoreKey]: reducer,
52+
[reduxStoreKey]: reducer,
5753
},
5854
middleware,
5955
});
@@ -124,6 +120,7 @@ export class AccountsClient {
124120
const { accessToken, refreshToken } = await this.tokens();
125121
if (accessToken && refreshToken) {
126122
try {
123+
this.store.dispatch(loggingIn(true));
127124
const decodedRefreshToken = jwtDecode(refreshToken);
128125
const currentTime = Date.now() / 1000;
129126
// Refresh token is expired, user must sign back in
@@ -134,12 +131,14 @@ export class AccountsClient {
134131
// Request a new token pair
135132
const refreshedSession : LoginReturnType =
136133
await this.transport.refreshTokens(accessToken, refreshToken);
134+
this.store.dispatch(loggingIn(false));
137135

138136
await this.storeTokens(refreshedSession);
139137
this.store.dispatch(setTokens(refreshedSession.tokens));
140138
this.store.dispatch(setUser(refreshedSession.user));
141139
}
142140
} catch (err) {
141+
this.store.dispatch(loggingIn(false));
143142
this.clearTokens();
144143
this.clearUser();
145144
throw new AccountsError('falsy token provided');
@@ -187,7 +186,7 @@ export class AccountsClient {
187186

188187
async loginWithPassword(user: PasswordLoginUserType,
189188
password: ?string,
190-
callback?: Function): Promise<void> {
189+
callback?: Function): Promise<LoginReturnType> {
191190
if (!password || !user) {
192191
throw new AccountsError('Unrecognized options for login request', user, 400);
193192
}
@@ -202,7 +201,11 @@ export class AccountsClient {
202201
await this.storeTokens(res);
203202
this.store.dispatch(setTokens(res.tokens));
204203
this.store.dispatch(setUser(res.user));
205-
this.options.onSignedInHook();
204+
205+
if (this.options.onSignedInHook && isFunction(this.options.onSignedInHook)) {
206+
this.options.onSignedInHook();
207+
}
208+
206209
if (callback && isFunction(callback)) {
207210
callback(null, res);
208211
}
@@ -238,7 +241,10 @@ export class AccountsClient {
238241
if (callback && isFunction(callback)) {
239242
callback();
240243
}
241-
this.options.onSignedOutHook();
244+
245+
if (this.options.onSignedOutHook) {
246+
this.options.onSignedOutHook();
247+
}
242248
} catch (err) {
243249
if (callback && isFunction(callback)) {
244250
callback(err);
@@ -263,15 +269,17 @@ export class AccountsClient {
263269
}
264270
}
265271

266-
async requestPasswordReset(email?: string): Promise<void> {
272+
async requestPasswordReset(email: string): Promise<void> {
273+
if (!validators.validateEmail(email)) throw new AccountsError('Valid email must be provided');
267274
try {
268275
await this.transport.sendResetPasswordEmail(email);
269276
} catch (err) {
270277
throw new AccountsError(err.message);
271278
}
272279
}
273280

274-
async requestVerificationEmail(email?: string): Promise<void> {
281+
async requestVerificationEmail(email: string): Promise<void> {
282+
if (!validators.validateEmail(email)) throw new AccountsError('Valid email must be provided');
275283
try {
276284
await this.transport.sendVerificationEmail(email);
277285
} catch (err) {
@@ -283,7 +291,7 @@ export class AccountsClient {
283291
const Accounts = {
284292
instance: AccountsClient,
285293
ui: {},
286-
config(options: Object, transport: TransportInterface) {
294+
config(options: AccountsClientConfiguration, transport: TransportInterface) {
287295
this.instance = new AccountsClient({
288296
...config,
289297
...options,
@@ -292,7 +300,7 @@ const Accounts = {
292300
user(): UserObjectType | null {
293301
return this.instance.user();
294302
},
295-
options(): Object {
303+
options(): AccountsClientConfiguration {
296304
return this.instance.options;
297305
},
298306
createUser(user: CreateUserType, callback: ?Function): Promise<void> {

packages/client/src/AccountsClient.spec.js

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ describe('Accounts', () => {
494494
const error = 'something bad';
495495
Accounts.config({}, { sendResetPasswordEmail: () => Promise.reject({ message: error }) });
496496
try {
497-
await Accounts.requestPasswordReset();
497+
await Accounts.requestPasswordReset('[email protected]');
498498
throw new Error();
499499
} catch (err) {
500500
expect(err.message).toEqual(error);
@@ -504,9 +504,21 @@ describe('Accounts', () => {
504504
it('should call transport.sendResetPasswordEmail', async () => {
505505
const mock = jest.fn(() => Promise.resolve());
506506
Accounts.config({}, { sendResetPasswordEmail: mock });
507-
await Accounts.requestPasswordReset('email');
507+
await Accounts.requestPasswordReset('email@g.co');
508508
expect(mock.mock.calls.length).toEqual(1);
509-
expect(mock.mock.calls[0][0]).toEqual('email');
509+
expect(mock.mock.calls[0][0]).toEqual('[email protected]');
510+
});
511+
512+
it('should throw if an invalid email is provided', async () => {
513+
const mock = jest.fn();
514+
Accounts.config({}, { sendResetPasswordEmail: mock });
515+
try {
516+
await Accounts.requestPasswordReset('email');
517+
throw new Error();
518+
} catch (err) {
519+
expect(err.message).toEqual('Valid email must be provided');
520+
expect(mock.mock.calls.length).toEqual(0);
521+
}
510522
});
511523
});
512524

@@ -515,7 +527,7 @@ describe('Accounts', () => {
515527
const error = 'something bad';
516528
Accounts.config({}, { sendVerificationEmail: () => Promise.reject({ message: error }) });
517529
try {
518-
await Accounts.requestVerificationEmail();
530+
await Accounts.requestVerificationEmail('[email protected]');
519531
throw new Error();
520532
} catch (err) {
521533
expect(err.message).toEqual(error);
@@ -525,9 +537,21 @@ describe('Accounts', () => {
525537
it('should call transport.sendVerificationEmail', async () => {
526538
const mock = jest.fn(() => Promise.resolve());
527539
Accounts.config({}, { sendVerificationEmail: mock });
528-
await Accounts.requestVerificationEmail('email');
540+
await Accounts.requestVerificationEmail('email@g.co');
529541
expect(mock.mock.calls.length).toEqual(1);
530-
expect(mock.mock.calls[0][0]).toEqual('email');
542+
expect(mock.mock.calls[0][0]).toEqual('[email protected]');
543+
});
544+
545+
it('should throw if an invalid email is provided', async () => {
546+
const mock = jest.fn();
547+
Accounts.config({}, { sendVerificationEmail: mock });
548+
try {
549+
await Accounts.requestVerificationEmail('email');
550+
throw new Error();
551+
} catch (err) {
552+
expect(err.message).toEqual('Valid email must be provided');
553+
expect(mock.mock.calls.length).toEqual(0);
554+
}
531555
});
532556
});
533557
});

packages/client/src/TransportInterface.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,9 @@ export interface TransportInterface {
1010
createUser(user: CreateUserType): Promise<string>,
1111
loginWithPassword(user: PasswordLoginUserType, password: string): Promise<LoginReturnType>,
1212
logout(accessToken: string): Promise<void>,
13-
refreshTokens(accessToken: string, refreshToken: string) : Promise<LoginReturnType>
13+
refreshTokens(accessToken: string, refreshToken: string) : Promise<LoginReturnType>,
14+
verifyEmail(token: string): Promise<void>,
15+
resetPassword(token: string, newPassword: string): Promise<void>,
16+
sendResetPasswordEmail(email: string): Promise<void>,
17+
sendVerificationEmail(email: string): Promise<void>
1418
}

packages/client/src/config.js

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,45 @@
1+
// @flow
2+
13
import { config as sharedConfig } from '@accounts/common';
4+
import type { AccountsCommonConfiguration } from '@accounts/common';
5+
import type { Store } from 'redux';
26
import AccountsClient from './AccountsClient';
37
import redirect from './redirect';
48

9+
export interface TokenStorage {
10+
getItem(key: string): Promise<string>,
11+
removeItem(key: string): Promise<string>,
12+
setItem(key: string, value: string): Promise<string>
13+
}
14+
15+
export type AccountsClientConfiguration = AccountsCommonConfiguration & {
16+
store?: ?Store<Object, Object>,
17+
reduxLogger?: ?Object,
18+
reduxStoreKey?: string,
19+
tokenStorage?: ?TokenStorage,
20+
server?: string,
21+
tokenStoragePrefix?: string,
22+
title?: string,
23+
requestPermissions?: Array<any>,
24+
requestOfflineToken?: Object,
25+
forceApprovalPrompt?: Object,
26+
requireEmailVerification?: boolean,
27+
loginPath?: string,
28+
signUpPath?: ?string,
29+
resetPasswordPath?: ?string,
30+
profilePath?: string,
31+
changePasswordPath?: ?string,
32+
homePath?: string,
33+
signOutPath?: string,
34+
onEnrollAccountHook?: Function,
35+
onResetPasswordHook?: Function,
36+
onVerifyEmailHook?: Function,
37+
onSignedInHook?: Function,
38+
onSignedOutHook?: Function,
39+
loginOnSignUp?: boolean,
40+
history?: Object
41+
};
42+
543
export default {
644
...sharedConfig,
745
store: null,
@@ -26,10 +64,10 @@ export default {
2664
// onSubmitHook: () => {},
2765
// onPreSignUpHook: () => new Promise(resolve => resolve()),
2866
// onPostSignUpHook: () => {},
29-
onEnrollAccountHook: () => redirect(AccountsClient.options().loginPath),
30-
onResetPasswordHook: () => redirect(AccountsClient.options().loginPath),
31-
onVerifyEmailHook: () => redirect(AccountsClient.options().profilePath),
32-
onSignedInHook: () => redirect(AccountsClient.options().homePath),
33-
onSignedOutHook: () => redirect(AccountsClient.options().signOutPath),
67+
onEnrollAccountHook: () => redirect(AccountsClient.options().loginPath || '/'),
68+
onResetPasswordHook: () => redirect(AccountsClient.options().loginPath || '/'),
69+
onVerifyEmailHook: () => redirect(AccountsClient.options().profilePath || '/'),
70+
onSignedInHook: () => redirect(AccountsClient.options().homePath || '/'),
71+
onSignedOutHook: () => redirect(AccountsClient.options().signOutPath || '/'),
3472
loginOnSignUp: true,
3573
};

packages/client/src/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// @flow
2+
import type { TokenStorage } from './config';
3+
import type { TransportInterface } from './TransportInterface';
24

35
import Accounts, { AccountsClient } from './AccountsClient';
4-
import type { TokenStorage } from './AccountsClient';
5-
import type { TransportInterface } from './TransportInterface';
66
import config from './config';
77
import reducer from './module';
88

packages/client/src/redirect.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
// eslint-disable-next-line import/no-named-as-default
33
import AccountsClient from './AccountsClient';
44

5+
// $FlowFixMe
56
export default (path: string) => AccountsClient.options().history.push(path);

packages/common/src/config.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
1+
// @flow
12
import { EMAIL_ONLY } from './passwordSignupFields';
2-
// eslint-disable-next-line import/no-named-as-default
3+
import type { PasswordSignupFields } from './passwordSignupFields';
34

4-
export default {
5+
export type AccountsCommonConfiguration = {
6+
siteUrl?: string,
7+
sendVerificationEmail?: boolean,
8+
sendEnrollmentEmail?: boolean,
9+
sendWelcomeEmail?: boolean,
10+
forbidClientAccountCreation?: boolean,
11+
restrictCreationByEmailDomain?: ?string,
12+
passwordResetTokenExpirationInDays?: number,
13+
passwordEnrollTokenExpirationInDays?: number,
14+
passwordSignupFields?: PasswordSignupFields,
15+
minimumPasswordLength?: number,
16+
path?: string
17+
};
18+
19+
export default ({
520
siteUrl: 'http://localhost:3000',
621
sendVerificationEmail: false,
722
sendEnrollmentEmail: false,
823
sendWelcomeEmail: false,
924
forbidClientAccountCreation: false,
1025
restrictCreationByEmailDomain: null,
11-
loginExpirationInDays: 90,
1226
passwordResetTokenExpirationInDays: 3,
1327
passwordEnrollTokenExpirationInDays: 30,
1428
passwordSignupFields: EMAIL_ONLY,
1529
minimumPasswordLength: 7,
1630
path: '/accounts',
17-
};
31+
}: AccountsCommonConfiguration);

packages/common/src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as validators from './validators';
55
import { AccountsError } from './errors';
66
import toUsernameAndEmail from './toUsernameAndEmail';
77
import config from './config';
8+
import type { AccountsCommonConfiguration } from './config';
89

910
import type {
1011
UserObjectType,
@@ -24,6 +25,7 @@ export type {
2425
LoginReturnType,
2526
TokensType,
2627
SessionType,
28+
AccountsCommonConfiguration,
2729
};
2830

2931
export {

packages/common/src/passwordSignupFields.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,9 @@ export const EMAIL_ONLY = 'EMAIL_ONLY';
22
export const USERNAME_AND_EMAIL = 'USERNAME_AND_EMAIL';
33
export const USERNAME_AND_OPTIONAL_EMAIL = 'USERNAME_AND_OPTIONAL_EMAIL';
44
export const USERNAME_ONLY = 'USERNAME_ONLY';
5+
6+
export type PasswordSignupFields =
7+
EMAIL_ONLY |
8+
USERNAME_AND_EMAIL |
9+
USERNAME_AND_OPTIONAL_EMAIL |
10+
USERNAME_ONLY;

0 commit comments

Comments
 (0)