Skip to content
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
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ gem 'jsonapi-resources'
gem 'factory_girl'
gem 'faker'
gem 'devise_token_auth'
gem 'cancan'
gem 'rolify'
gem 'pry'

group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
Expand Down
11 changes: 11 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ GEM
bcrypt (3.1.11)
builder (3.2.3)
byebug (9.0.6)
cancan (1.6.10)
coderay (1.1.1)
concurrent-ruby (1.0.5)
devise (4.2.0)
bcrypt (~> 3.0)
Expand Down Expand Up @@ -86,6 +88,10 @@ GEM
mini_portile2 (~> 2.1.0)
orm_adapter (0.5.0)
pg (0.20.0)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
puma (3.8.2)
rack (2.0.1)
rack-cors (0.4.1)
Expand Down Expand Up @@ -120,6 +126,7 @@ GEM
ffi (>= 0.5.0)
responders (2.3.0)
railties (>= 4.2.0, < 5.1)
rolify (5.1.0)
rspec-core (3.5.4)
rspec-support (~> 3.5.0)
rspec-expectations (3.5.0)
Expand All @@ -137,6 +144,7 @@ GEM
rspec-mocks (~> 3.5.0)
rspec-support (~> 3.5.0)
rspec-support (3.5.0)
slop (3.6.0)
spring (2.0.1)
activesupport (>= 4.2)
spring-watcher-listen (2.0.1)
Expand Down Expand Up @@ -164,16 +172,19 @@ PLATFORMS

DEPENDENCIES
byebug
cancan
devise_token_auth
factory_girl
faker
foreman
jsonapi-resources
listen (~> 3.0.5)
pg (~> 0.18)
pry
puma (~> 3.0)
rack-cors
rails (~> 5.0.2)
rolify
rspec-rails
spring
spring-watcher-listen (~> 2.0.0)
Expand Down
4 changes: 4 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
# protect_from_forgery with: :null_session
#
rescue_from CanCan::AccessDenied do |exception|
render json: { message: "You don't have permissions." }, status: :forbidden
end
end
2 changes: 2 additions & 0 deletions app/controllers/roles_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class RolesController < AuthorizedController
end
1 change: 1 addition & 0 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
class UsersController < AuthorizedController
load_and_authorize_resource
end
15 changes: 15 additions & 0 deletions app/models/ability.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class Ability
include CanCan::Ability

def initialize(user)
user ||= User.new # guest user (not logged in)
if user.is_admin?
can :manage, :all
else
can :manage, Post
can :manage, Category
can :manage, Comment
can :update, User, id: user.id
end
end
end
13 changes: 13 additions & 0 deletions app/models/role.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class Role < ApplicationRecord
has_and_belongs_to_many :users, :join_table => :users_roles

belongs_to :resource,
:polymorphic => true,
:optional => true

validates :resource_type,
:inclusion => { :in => Rolify.resource_types },
:allow_nil => true

scopify
end
9 changes: 9 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
class User < ActiveRecord::Base
rolify
has_and_belongs_to_many :roles, :join_table => :users_roles

# Include default devise modules.
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:confirmable
include DeviseTokenAuth::Concerns::User

scope :email_contains, -> (value) { where('email ILIKE ?', "%#{value.join}%") }

def token_validation_response
self.as_json(except: [
:tokens, :created_at, :updated_at
]).merge(roles: self.roles.map(&:name))
end
end
3 changes: 3 additions & 0 deletions app/resources/role_resource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class RoleResource < JSONAPI::Resource
attributes :name
end
13 changes: 12 additions & 1 deletion app/resources/user_resource.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
class UserResource < JSONAPI::Resource
extend ModelFilter
attributes :email, :confirmed_at, :created_at
attributes :email, :confirmed_at, :created_at, :roles

paginator :paged
model_filters :email_contains

def roles
@model.roles.pluck(:name)
end

def roles=(roles)
@model.roles.destroy_all
roles.map do |role|
@model.add_role role
end
end
end
18 changes: 18 additions & 0 deletions client/src/api/normalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,24 @@ const serializers = {
keyForAttribute: 'camelCase',
attributes: [
'email',
'roles',
],
}),
deserializer: new Deserializer({
keyForAttribute: 'camelCase',
roles: {
valueForRelationship: relationship => ({
id: relationship.id,
}),
},
}),
},

roles: {
serializer: new Serializer('roles', {
keyForAttribute: 'camelCase',
attributes: [
'name',
],
}),
deserializer: new Deserializer({
Expand Down
60 changes: 36 additions & 24 deletions client/src/components/App.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { Component } from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import { isEmpty } from 'lodash';
import { Collapse, Container, Navbar, NavbarToggler, Nav, NavItem, NavLink } from 'reactstrap';
import { Collapse, Container, Navbar, NavbarToggler, Nav, NavItem, NavLink, NavDropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';

import { getUser, logout } from '../store/auth';

Expand All @@ -15,36 +16,47 @@ export class App extends Component {
this.props.logout(this.props.user);
};

toggle = () => this.setState({ isOpen: !this.state.isOpen });
toggle = () => this.setState({
isOpen: !this.state.isOpen
});

render() {
const { user } = this.props;
const userIsAdmin = user.roles.includes('admin')

return (
<div>
<Navbar color="faded" light toggleable>
<Container>
<NavbarToggler right onClick={this.toggle} />
<Collapse isOpen={this.state.isOpen} navbar>
<Nav navbar>
<NavItem>
<NavLink href="/#/">Dashboard</NavLink>
</NavItem>
<NavItem>
<NavLink href="/#/posts">Posts</NavLink>
</NavItem>
<NavItem>
<NavLink href="/#/categories">Categories</NavLink>
</NavItem>
<NavItem>
<NavLink href="/#/users">Users</NavLink>
</NavItem>
</Nav>
<Nav navbar className="ml-auto">
<NavItem>
<NavLink href onClick={this.logout}><small>{user.email}</small> Logout</NavLink>
</NavItem>
</Nav>
</Collapse>
<Nav navbar>
<NavItem>
<NavLink href="/#/">Dashboard</NavLink>
</NavItem>
<NavItem>
<NavLink href="/#/posts">Posts</NavLink>
</NavItem>
<NavItem>
<NavLink href="/#/categories">Categories</NavLink>
</NavItem>
<NavItem>
{
userIsAdmin && <NavLink href="/#/users">Users</NavLink>
}
</NavItem>
</Nav>
<Nav navbar className="ml-auto">
<NavDropdown isOpen={this.state.isOpen} toggle={this.toggle}>
<DropdownToggle caret>
{user.email}
</DropdownToggle>
<DropdownMenu right>
<DropdownItem>
<DropdownItem>Profile</DropdownItem>
</DropdownItem>
<DropdownItem href onClick={this.logout}> Logout </DropdownItem>
</DropdownMenu>
</NavDropdown>
</Nav>
</Container>
</Navbar>
<Container className="container-main">
Expand Down
8 changes: 6 additions & 2 deletions client/src/components/Routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import { UserList, UserEdit } from './Users';
import { Login } from './Auth';

const UserIsAuthenticated = UserAuthWrapper({ authSelector: getUser });
const UserIsAdmin = UserAuthWrapper({
authSelector: getUser,
predicate: user => user.roles.includes('admin')
});

export class Routes extends PureComponent {
static propTypes = {
Expand All @@ -28,8 +32,8 @@ export class Routes extends PureComponent {
<Route path="/posts/new" component={PostEdit}/>
<Route path="/posts/:id" component={PostEdit}/>
<Route path="/categories" component={CategoryList}/>
<Route path="/users" component={UserList}/>
<Route path="/users/:id" component={UserEdit}/>
<Route path="/users" component={UserIsAdmin(UserList)}/>
<Route path="/users/:id" component={UserIsAdmin(UserEdit)}/>
</Route>
<Route path="/login" component={Login}/>
</Router>
Expand Down
15 changes: 10 additions & 5 deletions client/src/components/Users/UserEdit.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import React, { Component, PropTypes } from 'react';
import { push } from 'react-router-redux';
import { connect } from 'react-redux';
import { get, find, omit } from 'lodash';

import { ErrorAlert, Loading, EditHeader } from '../UI';
import { withResource } from '../../hocs';
import UserForm from './UserForm';
import { getMany, fetchList } from '../../store/api';

export class UserEdit extends Component {
componentWillMount() {
const { params, fetchResource } = this.props;
const { params, fetchResource, fetchRoles } = this.props;

fetchRoles();

if (params.id) {
fetchResource({ id: params.id });
}
}

render() {
const { isNew, error, loading, resource, onSubmit } = this.props;
const { isNew, error, loading, resource, onSubmit, roles } = this.props;

if (error) {
return (<ErrorAlert {...error} />);
Expand All @@ -30,15 +32,18 @@ export class UserEdit extends Component {
return (
<div>
<EditHeader {...this.props}>{ isNew ? 'New User' : resource.email }</EditHeader>
<UserForm initialValues={resource} onSubmit={onSubmit}></UserForm>
<UserForm initialValues={resource} roles={roles} onSubmit={onSubmit}></UserForm>
</div>
);
}
}

export const mapStateToProps = (state, props) => ({});
export const mapStateToProps = (state, props) => ({
roles: getMany(state, 'roles'),
});

export const mapDispatchToProps = dispatch => ({
fetchRoles: () => dispatch(fetchList('roles', { page: { limit: 999 } })),
redirectToIndex: () => dispatch(push('/users')),
});

Expand Down
20 changes: 18 additions & 2 deletions client/src/components/Users/UserForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@ import { isEmpty } from 'lodash';
import { Field, reduxForm } from 'redux-form';
import { Button, Form } from 'reactstrap';

import { InputField, required } from '../../forms';
import { InputField, MultiselectField, required } from '../../forms';

class UserForm extends Component {
render() {
const { handleSubmit, pristine, reset, submitting } = this.props;
const { roles, handleSubmit, pristine, reset, submitting } = this.props;

const rolesOptions = [{
id: '',
name: '-- select role --',
}].concat(roles.map(role => ({
id: role.name,
name: role.name,
})));

return (
<Form onSubmit={handleSubmit}>
Expand All @@ -18,6 +26,14 @@ class UserForm extends Component {
component={InputField}
/>
</div>
<div>
<Field
name="roles"
label="Role"
component={MultiselectField}
options={rolesOptions}
/>
</div>
<div>
<Button disabled={pristine || submitting} color="primary">Submit</Button>
<Button disabled={pristine || submitting} onClick={reset}>Undo Changes</Button>
Expand Down
5 changes: 5 additions & 0 deletions client/src/components/Users/UserList.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export class UserList extends Component {
rowRender: user => formatDate(user.confirmedAt),
sortable: true,
},
{
attribute: 'role',
header: 'Role',
rowRender: user => user.roles.join(),
}
];

return (
Expand Down
Loading