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

chore(flow): Added type declarations for accounts and made eslint and flow check happy #23

Merged
merged 1 commit into from
Dec 18, 2016
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
5 changes: 4 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
],

"rules": {
"indent": ["error", 2, { "CallExpression": {"arguments": "first"}, "SwitchCase": 1 }],
"strict": 0,
"import/no-extraneous-dependencies": 0,
"no-duplicate-imports": 0,
"no-underscore-dangle": 0,
"class-methods-use-this": 0,
"jsdoc/check-param-names": 1,
Expand Down Expand Up @@ -57,7 +59,8 @@
2,
"always",
{
"annotateUndefined": "never"
"annotateUndefined": "never",
"excludeArrowFunctions": true
}
],
"flowtype/require-valid-file-annotation": 2,
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules/
coverage/
npm-debug.log
yarn.lock
.idea
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"start": "webpack -p --config --progress --watch",
"compile": "webpack -p --config --progress",
"flow:check": "flow check",
"prepublish": "npm run compile",
"pretest": "npm run lint",
"test": "npm run testonly",
Expand Down
75 changes: 53 additions & 22 deletions src/client/AccountsClient.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,65 @@
// @flow

import { isFunction, isString, has } from 'lodash';
import type { Map } from 'immutable';

import { defaultClientConfig } from '../common/defaultConfigs';
import { AccountsError } from '../common/errors';
import store from './store';
import AccountsCommon from '../common/AccountsCommon';
import { loggingIn } from './module';
import ui from './ui';
import type { AccountsOptionsType } from '../common/AccountsCommon';

export type UserObjectType = {username: ?string, email: ?string, id: ?string};
const isValidUserObject = (user: UserObjectType) => has(user, 'user') || has(user, 'email') || has(user, 'id');

export type UserCreationInputType = {
username: ?string,
password: ?string,
email: ?string
};

const isValidUserObject = user => has(user, 'user') || has(user, 'email') || has(user, 'id');
export type AccountTokenType = {
token: string,
tokenExpiration: Date,
userId: ?string
};

export interface AccountsTransportClient {
createUser(user: UserCreationInputType): AccountTokenType,
loginWithPassword(user: UserObjectType, password: string): AccountTokenType
}

class Accounts extends AccountsCommon {
constructor(options, client) {

constructor(options: AccountsOptionsType, client: AccountsTransportClient) {
super(options);

if (!client) {
throw new AccountsError({
message: 'A REST or GraphQL client is required',
});
}
this.store = store;

this.client = client;
}
getState() {
return this.getState().get('accounts');

getState(): Map<string, any> {
return store.getState().get('accounts');
}

// TODO Accept 'profile' in the options
async createUser({ password, username, email }, callback) {
this.validatePassword(password);
async createUser(user: UserCreationInputType, callback: Function): Promise<void> {
this.validatePassword(user.password);
// TODO Throw error if client user creation is disabled

if (!this.validateUsername(username, false) && !this.validateEmail(email, false)) {
if (!this.validateUsername(user.username, false) && !this.validateEmail(user.email, false)) {
throw new AccountsError({ message: 'Username or Email is required' });
}
try {
const res = await this.client.createUser({
password,
username,
email,
});
await this.client.createUser(user);

if (isFunction(callback)) {
callback();
}
Expand All @@ -47,17 +71,20 @@ class Accounts extends AccountsCommon {
}
}
}
async loginWithPassword(user, password, callback) {

async loginWithPassword(user: UserObjectType,
password: string,
callback: Function): Promise<void> {
if (!password || !user) {
throw new AccountsError({ message: 'Unrecognized options for login request [400]' });
}
if ((!isString(user) && !isValidUserObject(user)) || !isString(password)) {
throw new AccountsError({ message: 'Match failed [400]' });
}

this.store.dispatch(loggingIn(true));
store.dispatch(loggingIn(true));
try {
const res = await this.client.loginWithPassword(user, password);
await this.client.loginWithPassword(user, password);
// TODO Update redux store with user info
if (isFunction(callback)) {
callback();
Expand All @@ -68,14 +95,16 @@ class Accounts extends AccountsCommon {
throw new AccountsError({ message: err });
}
}
this.store.dispatch(loggingIn(false));
store.dispatch(loggingIn(false));
}

// loginWith(service, options, callback) {
//
// }
loggingIn() {
return this.getState().get('loggingIn');
loggingIn(): boolean {
return (this.getState().get('loggingIn'): boolean);
}

// logout(callback) {
//
// }
Expand All @@ -97,21 +126,23 @@ class Accounts extends AccountsCommon {
// onEmailVerificationLink(callback) {
//
// }

client: AccountsTransportClient;
}


const AccountsClient = {
ui,
config(options, client) {
config(options: AccountsOptionsType, client: AccountsTransportClient) {
this.instance = new Accounts({
...defaultClientConfig,
...options,
}, client);
},
createUser(...args) {
createUser(...args: Array<mixed>): void {
return this.instance.createUser(...args);
},
loginWithPassword(...args) {
loginWithPassword(...args: Array<mixed>): void {
return this.instance.loginWithPassword(...args);
},
};
Expand Down
7 changes: 6 additions & 1 deletion src/client/store.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// @flow

import type { Map } from 'immutable';
import type { Store } from 'redux';

import createStore from './createStore';
import reducer from './module';

const store = createStore({
const store: Store<Map<string, any>> = createStore({
reducers: {
accounts: reducer,
},
Expand Down
50 changes: 33 additions & 17 deletions src/common/AccountsCommon.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,70 @@
/* @flow */
// @flow

import { trim, isEmpty } from 'lodash';
import { AccountsError } from './errors';

// TODO Check that password complexity satisfies the config
// TODO Check that email domain satisifes the config

class AccountsCommon {
export type AccountsOptionsType = {};

export default class AccountsCommon {

// TODO Handle options
constructor(options: object) {
constructor(options: AccountsOptionsType) {
this.options = options;
}
validateEmail(email: string, throwError: boolean = true): boolean {

validateEmail(email: ?string, throwError: boolean = true): boolean {
const hasError = isEmpty(trim(email));
if (hasError && throwError) {
throw new AccountsError({ message: 'Email is required' });
}
return hasError;
}
validatePassword(password: string, throwError: boolean = true) {

validatePassword(password: ?string, throwError: boolean = true): boolean {
const hasError = isEmpty(trim(password));
if (hasError && throwError) {
throw new AccountsError({ message: 'Password is required' });
}
return hasError;
}
validateUsername(username: string, throwError = true) {

validateUsername(username: ?string, throwError: boolean = true): boolean {
const hasError = isEmpty(trim(username));
if (hasError && throwError) {
throw new AccountsError({ message: 'Username is required' });
}
return hasError;
}
userId() {

userId(): ?string {

}

user() {

}
config(config) {
this.config = { ...this.config, config };

config(options: AccountsOptionsType) {
this.options = { ...this.options, options };
}
onLogin(func) {
this.onLogin = func;

onLogin(cb: Function) {
this.onLogin = cb;
}
onLoginFailure(func) {
this.onLoginFailure = func;

onLoginFailure(cb: Function) {
this.onLoginFailure = cb;
}
onLogout(func) {
this.onLogout = func;

onLogout(cb: Function) {
this.onLogout = cb;
}
}

export default AccountsCommon;
options: AccountsOptionsType;
onLogin: Function;
onLoginFailure: Function;
onLogout: Function;
}
8 changes: 7 additions & 1 deletion src/common/toUsernameAndEmail.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
// Thank you http://stackoverflow.com/a/46181
/**
* Checks if a string is actualy an email.
* Thank you http://stackoverflow.com/a/46181.
*
* @param {string} email - presumably an email
* @returns {boolean} whether or not it is an email
*/
function isEmail(email) {
// eslint-disable-next-line
const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
Expand Down
2 changes: 1 addition & 1 deletion src/server/AccountsServer.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-unused-expressions */
import 'regenerator-runtime/runtime'; // For async / await syntax
import chai, { expect } from 'chai';
import sinon from 'sinon';
// import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import chaiAsPromised from 'chai-as-promised';
// import 'localstorage-polyfill';
Expand Down