Skip to content

Commit

Permalink
Merge pull request #211 from performant-software/feature/basira209_ad…
Browse files Browse the repository at this point in the history
…min_role

BASIRA #209 - Admin role
  • Loading branch information
dleadbetter committed Jun 28, 2023
2 parents 4bcab5e + 7b77006 commit ce181f4
Show file tree
Hide file tree
Showing 28 changed files with 246 additions and 34 deletions.
32 changes: 32 additions & 0 deletions app/controllers/api/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ class Api::BaseController < Api::ResourceController

# Actions
before_action :authenticate_user!, except: :show
before_action :validate_delete_authorization, only: :destroy
before_action :validate_update_authorization, only: :update

protected

Expand All @@ -18,4 +20,34 @@ def prepare_params

super
end

private

def validate_delete_authorization
render json: { errors: [I18n.t('errors.unauthorized')] }, status: :unauthorized unless current_user.admin?
end

def validate_update_authorization
return if current_user.admin?

unauthorized = false

item_class.nested_attributes_options.keys.each do |key|
nested_attributes = params[param_name][key]
next unless nested_attributes.present?

# Handle JSON and FormData parameters
if nested_attributes.is_a?(Array)
attrs = nested_attributes
elsif nested_attributes.is_a?(ActionController::Parameters) && nested_attributes.keys.all?(&:is_integer?)
attrs = nested_attributes.keys.map{ |index| nested_attributes[index] }
end

attrs.each do |attr|
unauthorized = true if attr['_destroy'].to_s.to_bool
end
end

render json: { errors: [{ base: I18n.t('errors.unauthorized') }] }, status: :unauthorized if unauthorized
end
end
9 changes: 9 additions & 0 deletions app/controllers/api/users_controller.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
class Api::UsersController < Api::BaseController
# Search attributes
search_attributes :name, :email

protected

def permitted_params
parameters = super
parameters << :admin if current_user.admin?
parameters
end
end
4 changes: 2 additions & 2 deletions app/serializers/users_serializer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class UsersSerializer < BaseSerializer
index_attributes :id, :name, :email
show_attributes :id, :name, :email
index_attributes :id, :name, :email, :admin
show_attributes :id, :name, :email, :admin
end
3 changes: 2 additions & 1 deletion client/src/components/AdminArtworkMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import DocumentsService from '../services/Documents';
import { getPhysicalComponents, getVisualContexts } from '../utils/Artwork';
import ItemLabel from './ItemLabel';
import PhysicalComponentsService from '../services/PhysicalComponents';
import Session from '../services/Session';
import VisualContextsService from '../services/VisualContexts';
import './AdminArtworkMenu.css';

Expand Down Expand Up @@ -172,7 +173,7 @@ const AdminArtworkMenu = (props: Props) => {
to={`/admin${path}`}
/>
)}
{ onDelete && (
{ Session.isAdmin() && onDelete && (
<Button
icon='times'
onClick={(e) => {
Expand Down
14 changes: 12 additions & 2 deletions client/src/components/Images.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow

import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { FileUpload, PhotoViewer } from '@performant-software/semantic-components';
import { withTranslation } from 'react-i18next';
import { Button, Card, Image } from 'semantic-ui-react';
Expand All @@ -10,6 +10,7 @@ import './Images.css';
import type { Translateable } from '../types/Translateable';

type Action = {
accept?: (item: any) => boolean,
color?: (item: any) => ?string,
icon: string,
name: string,
Expand All @@ -27,6 +28,15 @@ const Images = (props: Props) => {
const [currentImage, setCurrentImage] = useState(null);
const [fileUpload, setFileUpload] = useState(false);

/**
* Returns the list of available actions for the passed item.
*
* @type {function(*): *}
*/
const getActions = useCallback((item) => (
_.filter(props.actions, (action) => !action.accept || action.accept(item))
), [props.actions]);

return (
<div
className='images'
Expand Down Expand Up @@ -72,7 +82,7 @@ const Images = (props: Props) => {
extra
textAlign='center'
>
{ _.map(props.actions, (action) => (
{ _.map(getActions(item), (action) => (
<Button
basic
color={action.color && action.color(item)}
Expand Down
Empty file.
10 changes: 9 additions & 1 deletion client/src/components/UserModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import React from 'react';
import { withTranslation } from 'react-i18next';
import { Form, Message, Modal } from 'semantic-ui-react';
import Session from '../services/Session';

import type { EditContainerProps } from 'react-components/types';
import type { EditContainerProps } from '@performant-software/shared-components/types';
import type { Translateable } from '../types/Translateable';
import type { User } from '../types/User';

Expand Down Expand Up @@ -39,6 +40,13 @@ const UserModal = (props: Props) => (
onChange={props.onTextInputChange.bind(this, 'email')}
value={props.item.email || ''}
/>
{ Session.isAdmin() && (
<Form.Checkbox
checked={props.item.admin}
label={props.t('UserModal.labels.admin')}
onChange={props.onCheckboxInputChange.bind(this, 'admin')}
/>
)}
<Message
content={props.t('UserModal.labels.passwordPolicy.content')}
header={props.t('UserModal.labels.passwordPolicy.header')}
Expand Down
15 changes: 10 additions & 5 deletions client/src/components/ValueListsTable.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
// @flow

import { ListTable, useDataList } from '@performant-software/semantic-components';
import type { EditContainerProps } from '@performant-software/shared-components/types';
import React from 'react';
import { withTranslation } from 'react-i18next';
import Authorization from '../utils/Authorization';
import Session from '../services/Session';
import type { Translateable } from '../types/Translateable';
import type { ValueList as ValueListType } from '../types/ValueList';
import ValueListModal from './ValueListModal';
import ValueListsFiltersModal from './ValueListsFiltersModal';
import ValueListsService from '../services/ValueLists';
import { withTranslation } from 'react-i18next';
import { ListTable, useDataList, type EditContainerProps } from '@performant-software/semantic-components';
import type { ValueList as ValueListType } from '../types/ValueList';
import type { Translateable } from '../types/Translateable';

type Props = EditContainerProps & Translateable & {
item: ValueListType,
Expand All @@ -22,7 +25,7 @@ const ValueListsTable = (props: Props) => (
name: 'copy'
}, {
name: 'delete',
accept: (item) => item.qualifications_count === 0
accept: (item) => Session.isAdmin() && item.qualifications_count === 0
}]}
className='value-lists-table'
collectionName='value_lists'
Expand Down Expand Up @@ -64,6 +67,8 @@ const ValueListsTable = (props: Props) => (
per_page: 25
})}
onSave={(params) => ValueListsService.save(params)}
resolveErrors={(error) => Authorization.resolveDeleteError(error)}
searchable
/>
);

Expand Down
2 changes: 1 addition & 1 deletion client/src/hooks/MenuBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ const withMenuBar = (WrappedComponent: ComponentType<any>) => withTranslation()(
.logout()
.then(() => {
Session.destroy();
props.history.push('/');
props.history.push('/login');
})
)}
/>
Expand Down
4 changes: 3 additions & 1 deletion client/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@
"save": "Save"
},
"errors": {
"header": "Oops!"
"header": "Oops!",
"unauthorized": "You do not have permission to delete records."
},
"labels": {
"artwork": "Artwork",
Expand Down Expand Up @@ -519,6 +520,7 @@
},
"UserModal": {
"labels": {
"admin": "Administrator",
"email": "Email",
"name": "Name",
"password": "Password",
Expand Down
6 changes: 4 additions & 2 deletions client/src/pages/admin/AdminNotFound.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
// @flow

import React from 'react';
import { withRouter } from 'react-router-dom';
import { RouterHistory, withRouter } from 'react-router-dom';
import {
Button, Container, Message
} from 'semantic-ui-react';
import withMenuBar from '../../hooks/MenuBar';

import type { Translateable } from '../../types/Translateable';

type Props = Translateable;
type Props = Translateable & {
history: typeof RouterHistory,
};

const AdminNotFound = (props: Props) => (
<Container>
Expand Down
7 changes: 7 additions & 0 deletions client/src/pages/admin/Artwork.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import _ from 'underscore';
import ArtworkTitleModal from '../../components/ArtworkTitleModal';
import ArtworksService from '../../services/Artworks';
import AttachmentModal from '../../components/AttachmentModal';
import Authorization from '../../utils/Authorization';
import File from '../../transforms/File';
import i18n from '../../i18n/i18n';
import Images from '../../components/Images';
Expand All @@ -16,6 +17,7 @@ import Number from '../../utils/Number';
import ParticipationModal, { ParticipationTypes } from '../../components/ParticipationModal';
import Qualifiables from '../../utils/Qualifiables';
import RecordHeader from '../../components/RecordHeader';
import Session from '../../services/Session';
import SimpleEditPage from '../../components/SimpleEditPage';
import SimpleLink from '../../components/SimpleLink';
import Validations from '../../utils/Validations';
Expand Down Expand Up @@ -155,6 +157,7 @@ const Artwork = (props: Props) => {
}, {
name: 'copy'
}, {
accept: () => Session.isAdmin(),
name: 'delete'
}]}
columns={[{
Expand Down Expand Up @@ -304,6 +307,7 @@ const Artwork = (props: Props) => {
name: 'edit',
onClick: (item) => setSelectedImage(item)
}, {
accept: () => Session.isAdmin(),
color: () => 'red',
icon: 'times',
name: 'delete',
Expand Down Expand Up @@ -343,6 +347,7 @@ const Artwork = (props: Props) => {
}, {
name: 'copy'
}, {
accept: () => Session.isAdmin(),
name: 'delete'
}]}
columns={[{
Expand Down Expand Up @@ -394,6 +399,7 @@ const Artwork = (props: Props) => {
}, {
name: 'copy'
}, {
accept: () => Session.isAdmin(),
name: 'delete'
}]}
columns={[{
Expand Down Expand Up @@ -436,6 +442,7 @@ export default useEditPage(withMenuBar(Artwork), {
onLoad: (id) => ArtworksService.fetchOne(id).then(({ data }) => data.artwork),
onSave: (artwork) => ArtworksService.save(artwork).then(({ data }) => data.artwork),
required: ['date_descriptor'],
resolveValidationError: (e) => Authorization.resolveUpdateError(e),
validate: (artwork) => {
let validationErrors = {};

Expand Down
10 changes: 7 additions & 3 deletions client/src/pages/admin/Artworks.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// @flow

import React from 'react';
import { FilterTypes, ItemList, ListFilters } from '@performant-software/semantic-components';
import React from 'react';
import { withRouter } from 'react-router-dom';
import { Container, Header } from 'semantic-ui-react';
import _ from 'underscore';
import Thumbnail from '../../components/Thumbnail';
import ArtworksService from '../../services/Artworks';
import Users from '../../services/Users';
import Authorization from '../../utils/Authorization';
import Session from '../../services/Session';
import Thumbnail from '../../components/Thumbnail';
import User from '../../transforms/User';
import Users from '../../services/Users';
import withMenuBar from '../../hooks/MenuBar';
import './Artworks.css';

Expand All @@ -26,6 +28,7 @@ const Artworks = (props: Props) => (
name: 'edit',
onClick: (item) => props.history.push(`/admin/artworks/${item.id}`)
}, {
accept: () => Session.isAdmin(),
icon: 'times',
name: 'delete'
}, {
Expand Down Expand Up @@ -173,6 +176,7 @@ const Artworks = (props: Props) => (
</div>
</>
)}
resolveErrors={(error) => Authorization.resolveDeleteError(error)}
session={{
key: 'artworks',
storage: sessionStorage
Expand Down
7 changes: 4 additions & 3 deletions client/src/pages/admin/EditPage.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// @flow

import React, { type ComponentType } from 'react';
import { useEditContainer } from '@performant-software/shared-components';
import type { EditContainerProps } from '@performant-software/shared-components/types';
import React, { type ComponentType } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import _ from 'underscore';

import type { EditContainerProps } from 'react-components/types';

type Config = {
onLoad: (params: any) => Promise<any>,
onSave: (item: any) => Promise<any>,
resolveValidationError?: (error: any) => Array<string>,
validate?: (item: any) => any
};

Expand Down Expand Up @@ -62,6 +62,7 @@ const useEditPage = (WrappedComponent: ComponentType<any>, config: Config) => (
}
})}
onSave={(item) => config.onSave(item).then(afterSave)}
resolveValidationError={config.resolveValidationError}
/>
);
}
Expand Down
5 changes: 5 additions & 0 deletions client/src/pages/admin/People.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { ListTable } from '@performant-software/semantic-components';
import { withTranslation } from 'react-i18next';
import { withRouter } from 'react-router-dom';
import { Container } from 'semantic-ui-react';
import Authorization from '../../utils/Authorization';
import PeopleService from '../../services/People';
import Qualifiables from '../../utils/Qualifiables';
import Session from '../../services/Session';
import withMenuBar from '../../hooks/MenuBar';

import type { Routeable } from '../../types/Routeable';
Expand All @@ -19,6 +21,7 @@ const People = (props: Translateable & Routeable) => (
name: 'edit',
onClick: (item) => props.history.push(`/admin/people/${item.id}`)
}, {
accept: () => Session.isAdmin(),
name: 'delete'
}]}
addButton={{
Expand Down Expand Up @@ -46,6 +49,8 @@ const People = (props: Translateable & Routeable) => (
onDelete={(person) => PeopleService.delete(person)}
onLoad={(params) => PeopleService.fetchAll(params)}
onSave={(person) => PeopleService.save(person)}
resolveErrors={(error) => Authorization.resolveDeleteError(error)}
searchable
/>
</Container>
);
Expand Down
Loading

0 comments on commit ce181f4

Please sign in to comment.