Home Reference Source

application/components/admin/user.js

import React, {Component} from 'react';
import Immutable from 'immutable';
import {browserHistory} from 'react-router';

import generatePassword from 'password-generator';

import {List, ListItem} from 'material-ui/List';
import Subheader from 'material-ui/Subheader';
import Avatar from 'material-ui/Avatar';
import FontIcon from 'material-ui/FontIcon';
import Divider from 'material-ui/Divider';
import Checkbox from 'material-ui/Checkbox';
import LinearProgress from 'material-ui/LinearProgress';

import PersonIcon from 'material-ui/svg-icons/social/person-outline';
import ReloadIcon from 'material-ui/svg-icons/action/autorenew';

import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import RaisedButton from 'material-ui/RaisedButton';
import IconButton from 'material-ui/IconButton';
import TextField from 'material-ui/TextField';

import {Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn} from 'material-ui/Table';
import {Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarTitle} from 'material-ui/Toolbar';

import {connect} from 'react-redux';
import ExpandingCard from './../expanding-card';
import ReduxPaginator from './../common/redux-paginator';
import DeleteButton from './../common/delete-button';

import CurrentUser from './../../current-user';

import {
    usersMaybeFetch,
    usersFetchOne,
    usersFetch,
    usersRemove,
    userSetSelected,
    userUpdate,
    userCreate
} from './../../redux/actions/user-actions';

import {
    aclsMaybeFetch
} from './../../redux/actions/acl-actions';

import {
    snackbarShowMessage
} from './../../redux/actions/snackbar-actions';
import {
    alertShowMessage
} from './../../redux/actions/alert-actions';

import {Row, Col} from './../flexbox';
import request from './../../util/request';
import {ENTER_KEY} from './../../util/key-codes';

class UserCard extends Component {
    constructor(props) {
        super(props);

        this.state = {
            expanded: false,
            hasMore: false,
            hasLoaded: false,
            modalOpen: false,
            selected: []
        };

        this.loadScrollPage.bind(this);
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.openCard == 'user' && nextProps.openCard != this.props.openCard) {
            this.handleToggle(true);
        }
    }

    handleToggle(expanded) {
        if (this.props.onToggle) {
            this.props.onToggle('user', expanded);
        }

        if (expanded && !this.state.hasLoaded) {
            const {dispatch} = this.props;
            dispatch(usersMaybeFetch())
                .then(() => dispatch(aclsMaybeFetch()))
                .then(() => this.setState({hasLoaded: true}));
        }
    }

    loadScrollPage(page = 0) {
        const {dispatch} = this.props;
        dispatch(usersMaybeFetch({page: page}));
    }

    handleUserClick(user, e) {
        const {dispatch} = this.props;

        dispatch(usersFetchOne(user.get('uuid')))
            .then(() => dispatch(userSetSelected(user.get('uuid'))))
            .then(() => {
                this.setState({
                    'modalOpen': true
                });
            });

    }

    handleNewUser(e) {
        const {dispatch} = this.props;
        dispatch(userSetSelected(false));
        this.setState({'modalOpen': true});
    }

    handleModalClose(e) {
        const {dispatch} = this.props;
        dispatch(userSetSelected(false));
        this.setState({'modalOpen': false, 'selected': [], 'selectedItems': []});
        // fixes selection state in 0.15.6.1
        this.tableBody.setState({selectedRows: []});
    }

    handleRowSelection(selection) {
        let selectedItems = [];

        if (selection == 'all') {
            selectedItems = this.props.users.items.map((item) => item.get('uuid'));
        } else {
            const items = this.props.users.items.toJS();
            selectedItems = selection.map((i) =>items[i].uuid);
        }

        this.setState({
            'selectedItems': selectedItems,
            'selected': selection
        });
    }

    handlePagination(page) {
        if (this.props.users.pagination.current == page) {
            return;
        }
        const keyword = (this.props.location && this.props.location.query.keyword)
            ? decodeURIComponent(this.props.location.query.keyword)
            : null;
        const q = request.setQuery({
            'page': page
        });
        const this_url = request.getPath() + '?' + q;
        browserHistory.push(this_url);

        let params = {'page': page};
        if (keyword) {
            params['keyword'] = keyword;
            params['includesnw'] = 1;
        }

        const {dispatch} = this.props;
        dispatch(usersFetch(params));
        this.setState({'selected': [], 'selectedItems': []});
        // fixes selection state in 0.15.6.1
        this.tableBody.setState({selectedRows: []});
    }

    handleKeyDown(e) {
        const val = e.target.value;
        if (e.keyCode !== ENTER_KEY) {
            return;
        }

        const {dispatch} = this.props;

        const q = request.setQuery({
            'keyword': encodeURIComponent(val),
            'page': 1
        });
        const this_url = request.getPath() + '?' + q;
        browserHistory.push(this_url);

        dispatch(usersFetch({page: 1, keyword: val, includesnw: 1}));
    }

    handleSearchReset() {
        const {dispatch} = this.props;
        const q = request.setQuery({
            'keyword': '',
            'page': 1
        });
        const this_url = request.getPath() + '?' + q;
        browserHistory.push(this_url);
        this.quickSearch.value = '';

        dispatch(usersFetch({page: 1}));
    }

    handleDelete(next) {
        const uuids = this.state.selectedItems.join(',');
        const {dispatch} = this.props;

        dispatch(usersRemove(uuids))
            .then(() => dispatch(usersFetch()))
            .then(() => { this.setState({'selected': [], 'selectedItems': []}) })
            .then(() => { this.tableBody.setState({selectedRows: []}) })
            .then(() => next());

        return true;
    }

    render() {
        let items = this.props.users.items.map((item, i) => {
            let snw = false;
            if (parseInt(item.get('is_snworks'))) {
                snw = true;
            }

            const groups = item.get('roles').map((role, i) => {
                return <span key={i}>{role.get('name')}{(i < item.get('roles').size-1) ? ', ' : ''}</span>
            })

            return (
                <TableRow
                    key={i}
                    selected={this.state.selected == 'all' || this.state.selected.indexOf(i) != -1 ? true : false}
                    selectable={(!CurrentUser.isSnworks() && parseInt(item.get('is_snworks'))) ? false : true}
                    >
                    <TableRowColumn>
                        <Row middle={['xs']}>
                            <Col xs={1} start={['xs']}>
                                {snw ? <img style={{'maxWidth': '16px'}} src="/assets/img/cube-flat.png" /> : ''}
                                {parseInt(item.get('is_service')) ? <FontIcon tooltip='Service User' style={{'fontSize': '1em'}} className='fa fa-cog' /> : ''}
                            </Col>
                            <Col xs={11}>
                                {
                                    (!CurrentUser.isSnworks() && parseInt(item.get('is_snworks')))
                                    ? (
                                        <span>
                                            {item.get('name')}
                                            <br />
                                            {item.get('email')}
                                        </span>
                                    )
                                    : (
                                        <a
                                            onClick={this.handleUserClick.bind(this, item)}
                                            >
                                            {item.get('name')}
                                            <br />
                                            {item.get('email')}
                                        </a>

                                    )
                                }
                            </Col>
                        </Row>
                    </TableRowColumn>
                    <TableRowColumn>{groups}</TableRowColumn>
                </TableRow>
            );
        });

        return (
            <ExpandingCard
                onToggle={this.handleToggle.bind(this)}
                title="Users"
                subtitle="Manage user accounts"
                expanded={this.props.openCard === 'user'}
                >

                <Row middle='xs'>
                    <Col xs={3} autoBox={false}>
                        <div className="box toolbar-search-wrap">
                            <FontIcon className='mui-icons'>search</FontIcon>
                            <input
                                ref={quickSearch => this.quickSearch = quickSearch}
                                className="search"
                                type="text"
                                placeholder="Search Users"
                                onKeyDown={this.handleKeyDown.bind(this)}
                                defaultValue={this.props.location.query.keyword ? decodeURIComponent(this.props.location.query.keyword) : ''}
                                />
                            <FontIcon className='mui-icons clear-search' onClick={this.handleSearchReset.bind(this)}>close</FontIcon>
                        </div>
                    </Col>
                    <Col xs={4}>
                        <DeleteButton
                            disabled={this.state.selectedItems && this.state.selectedItems.length ? false : true}
                            onDelete={this.handleDelete.bind(this)}
                            />
                        <RaisedButton
                            primary={true}
                            label="Add User"
                            onClick={this.handleNewUser.bind(this)}
                            />
                    </Col>
                    <Col xs={5} end='xs'>
                        <ReduxPaginator
                            pagination={this.props.users.pagination}
                            onPaginate={this.handlePagination.bind(this)}
                            />
                    </Col>
                </Row>

                <Table
                    fixedHeader={true}
                    selectable={true}
                    multiSelectable={true}
                    onRowSelection={this.handleRowSelection.bind(this)}
                    >
                    <TableHeader>
                        <TableRow>
                            <TableHeaderColumn>
                                <Row>
                                    <Col xs={11} offsetXs={1}>
                                        Name
                                    </Col>
                                </Row>
                            </TableHeaderColumn>
                            <TableHeaderColumn>Groups</TableHeaderColumn>
                        </TableRow>
                    </TableHeader>
                    <TableBody
                        ref={tableBody => this.tableBody = tableBody}
                        deselectOnClickaway={false}
                        >
                        {items}
                    </TableBody>
                </Table>

                <UserModal onClose={this.handleModalClose.bind(this)} open={this.state.modalOpen} />
            </ExpandingCard>
        );
    }
}

let mapStateToProps = (state) => {
    return {
        users: state.users,
        acls: state.acls
    };
}

export default connect(mapStateToProps)(UserCard);

class UnwrappedUserModal extends Component {
    constructor(props) {
        super(props);

        this.state = {
            loaded: false,
            changedPassword: false,
            changedRoles: false,
            passwordStrength: 0,
            didUpdatePassword: false,
            email_updated_password: false,
            create_as_author: false,
            user: {
                'name': false,
                'email': false,
                'phone': false,
                'roles': [],
                'is_snworks': 0
            }
        };

    }

    // componentWillUpdate(nextProps, nextState) {
    componentWillReceiveProps(nextProps) {
        // this.setState({'user': {
        //     'name': false,
        //     'email': false,
        //     'phone': false,
        //     'roles': [],
        //     'is_snworks': 0
        // }});

        if (!this.state.loaded || nextProps.users.selected != this.props.users.selected) {
            const user = nextProps.users.items.find((item) => {
                return item.get('uuid') == nextProps.users.selected;
            });

            if (user) {
                console.log('SET USER');
                this.setState({'user': user.toJS()});
            }

            this.setState({
                'loaded': true,
                'changedPassword': false,
                'passwordStrength': 0,
                'changedRoles': false,
                didUpdatePassword: false,
                email_updated_password: false,
                create_as_author: false
            })
        }
    }

    handleClose(e) {
        this.setState({'user': {
            'name': false,
            'email': false,
            'phone': false,
            'roles': [],
            'is_snworks': 0,
            'is_service': 0
        }});
        this.props.onClose(e);
    }

    handleSave(e) {
        let user = this.state.user;

        const {dispatch} = this.props;

        let data = {
            'name':     user.name,
            'email':    user.email,
            'phone':    user.phone,
            'is_service': user.is_service
        };

        if (CurrentUser.isSnworks()) {
            data.is_snworks = user.is_snworks;
        }


        if (this.state.changedRoles) {
            data.roles = [];

            user.roles.map((item, i) => {
                data.roles.push(item.name);
            });
        }

        if (this.state.didUpdatePassword) {
            data['password'] = this.state.changedPassword;
        }

        if (this.state.didUpdatePassword && this.state.email_updated_password) {
            data['email_updated_password'] = 1;
        }

        if (!user.uuid && this.state.create_as_author) {
            data['create_as_author'] = 1;
        }

        if (user.uuid) {
            dispatch(userUpdate(user.uuid, data))
                .then(() => dispatch(usersFetch()))
                .then(() => dispatch(snackbarShowMessage('User updated')))
                .then(() => dispatch(userSetSelected(false)))
                .then(() => {
                    this.handleClose(e);
                })
                .catch((e) => {
                    e.response.json().then((json) => {
                        if (json.message.indexOf('email is already registered') !== -1) {
                            dispatch(alertShowMessage({
                                'title': 'Oops',
                                'message': 'A user with that email address already exists'
                            }));
                        } else {
                            dispatch(alertShowMessage({
                                'title': 'Oops',
                                'message': json.message
                            }));
                        }
                    });
                });
        } else {
            dispatch(userCreate(data))
                .then((resp) => {
                    const u = resp.payload.user;
                    if (u.get('is_service') && u.get('service_key')) {
                        dispatch(alertShowMessage({
                            'title': 'New Service User',
                            'message': (
                                <div>
                                    <p>
                                        Please copy this service user's access keys and store it in a safe place. <strong>This is the only time it will be shown.</strong>
                                    </p>
                                    <TextField
                                        floatingLabelText='Public Key'
                                        readOnly={true}
                                        value={u.get('public_key')}
                                        fullWidth={true}
                                        />
                                    <TextField
                                        floatingLabelText='Private Key'
                                        readOnly={true}
                                        value={u.get('service_key')}
                                        fullWidth={true}
                                        />
                                </div>
                            )
                        }));
                    }
                })
                .then(() => dispatch(usersFetch()))
                .then(() => dispatch(snackbarShowMessage('User created')))
                .then(() => dispatch(userSetSelected(false)))
                .then(() => {
                    this.handleClose(e);
                })
                .catch((e) => {
                    e.response.json().then((json) => {
                        if (json.message.indexOf('email is already registered') !== -1) {
                            dispatch(alertShowMessage({
                                'title': 'Oops',
                                'message': 'A user with that email address already exists'
                            }));
                        } else {
                            dispatch(alertShowMessage({
                                'title': 'Oops',
                                'message': json.message
                            }));
                        }
                    });
                });
        }
    }

    handleFieldUpdate(e, val) {
        const prop = e.target.name;
        let user = this.state.user;

        user[prop] = val;

        this.setState({'user': user});
    }

    handleSnworksUpdate(e, checked) {
        let user = this.state.user;
        user.is_snworks = checked ? 1 : 0;

        this.setState({'user': user});
    }

    handleServiceUpdate(e, checked) {
        let user = this.state.user;
        user.is_service = checked ? 1 : 0;

        this.setState({'user': user});
    }

    handleEmailPassUpdate(e, checked) {
        this.setState({'email_updated_password': checked ? true : false});
    }

    handleCreateAsAuthor(e, checked) {
        this.setState({'create_as_author': checked ? true : false});
    }

    handleRoleUpdate(e, checked) {
        let user = this.state.user;
        let role = e.target.name;

        if (checked) {
            user.roles.push({'name': role});
        } else {
            let roles = user.roles.map((r, i) => {
                if (r.name != role) {
                    return r;
                }

                return false;
            });

            user.roles = roles;
        }

        this.setState({'user': user, 'changedRoles': true});
    }

    handlePasswordChange(e) {
        this.setState({
            'changedPassword': e.target.value,
            'didUpdatePassword': true,
            'passwordStrength': zxcvbn(e.target.value).score + 1
        });
    }

    handleGeneratePassword() {
        const p = generatePassword(12, false, /[\w\d\?\-]/);
        this.setState({
            'changedPassword': p,
            'didUpdatePassword': true,
            'passwordStrength': zxcvbn(p).score + 1,
            'email_updated_password': true
        });
    }


    getStrengthColor() {
        let strengthColor = 'red';
        switch (this.state.passwordStrength) {
            case 0:
            case 1:
                strengthColor = 'red';
                break;
            case 2:
                strengthColor = 'orange';
                break;
            case 3:
                strengthColor = 'yellow';
                break;
            case 4:
            case 5:
                strengthColor = 'green';
                break;

        }

        return strengthColor;
    }

    render() {
        const actions = [
            <FlatButton
                label="Cancel"
                primary={false}
                onClick={this.handleClose.bind(this)}
                />,
            <FlatButton
                label="Save"
                primary={true}
                keyboardFocused={true}
                onClick={this.handleSave.bind(this)}
                />
        ];

        return (
            <Dialog
                title="Edit User"
                actions={actions}
                modal={false}
                open={this.props.open}
                onRequestClose={this.handleClose.bind(this)}
                autoScrollBodyContent={true}
                >
                <p className='small'>
                    <strong>Do you need to a lot of users?</strong> Contact <a href="mailto:support@getsnworks.com">support@getsnworks.com</a> and we can help you out!
                </p>
                <TextField
                    floatingLabelText="Name"
                    value={this.state.user.name ? this.state.user.name : ''}
                    name='name'
                    fullWidth={true}
                    onChange={this.handleFieldUpdate.bind(this)}
                    />

                <TextField
                    floatingLabelText="Email"
                    value={this.state.user.email ? this.state.user.email : ''}
                    disabled={this.state.user.is_service ? true : false}
                    name='email'
                    fullWidth={true}
                    onChange={this.handleFieldUpdate.bind(this)}
                    />

                <TextField
                    floatingLabelText="Phone"
                    value={this.state.user.phone ? this.state.user.phone : ''}
                    disabled={this.state.user.is_service ? true : false}
                    name='phone'
                    fullWidth={true}
                    onChange={this.handleFieldUpdate.bind(this)}
                    className='clear-bottom'
                    />

                {
                    CurrentUser.isSnworks()
                    ? (<Checkbox
                        label="SNworks User"
                        labelPosition="right"
                        name='snw'
                        disabled={this.state.user.is_service ? true : false}
                        checked={parseInt(this.state.user.is_snworks) ? true : false}
                        onCheck={this.handleSnworksUpdate.bind(this)}
                        />)
                    : ''
                }
                {
                    !this.state.user.uuid
                    ? (<Checkbox
                        label="Also create author"
                        labelPosition="right"
                        name='create_as_author'
                        disabled={this.state.user.is_service ? true : false}
                        checked={this.state.create_as_author}
                        onCheck={this.handleCreateAsAuthor.bind(this)}
                        />)
                    : ''
                }
                <Checkbox
                    label="Service User"
                    labelPosition="right"
                    name='is_service'
                    checked={parseInt(this.state.user.is_service) ? true : false}
                    onCheck={this.handleServiceUpdate.bind(this)}
                    />

                <h3>Roles</h3>

                {this.props.acls.items.map((item, i) => {
                    let checked = false;
                    this.state.user.roles.map((r, i) => {
                        if (r.name == item.get('name')) {
                            checked = true;
                        }
                    });
                    return <Checkbox
                        label={item.get('name')}
                        labelPosition="right"
                        name={item.get('name')}
                        key={i}
                        defaultChecked={checked}
                        onCheck={this.handleRoleUpdate.bind(this)}
                        />;
                })}


                <Row bottom={['xs']}>
                    <Col xs={8}>
                        <h3>Password</h3>
                        <TextField
                            floatingLabelText="Enter a new password"
                            value={this.state.changedPassword ? this.state.changedPassword : ''}
                            fullWidth={true}
                            disabled={parseInt(this.state.user.is_service) ? true : false}
                            onChange={this.handlePasswordChange.bind(this)}
                            />
                        <LinearProgress
                            mode="determinate"
                            value={this.state.passwordStrength}
                            color={this.getStrengthColor()}
                            max={5}
                            style={{backgroundColor: 'white'}}
                            />
                    </Col>
                    <Col xs={4}>
                        <RaisedButton
                            onClick={this.handleGeneratePassword.bind(this)}
                            label='Generate a password'
                            disabled={parseInt(this.state.user.is_service) ? true : false}
                            secondary={true}
                            />
                    </Col>
                </Row>
                <Row top={['xs']}>
                    <Col xs={12}>
                        <Checkbox
                            label='Send user new password notification'
                            labelPosition="right"
                            checked={this.state.email_updated_password}
                            onCheck={this.handleEmailPassUpdate.bind(this)}
                            disabled={!this.state.didUpdatePassword}
                        />
                    </Col>
                </Row>
            </Dialog>
        );
    }
}

const UserModal = connect(mapStateToProps)(UnwrappedUserModal);