Skip to content

Commit

Permalink
Merge pull request #300 from performant-software/feature/basira294_us…
Browse files Browse the repository at this point in the history
…er_permissions

BASIRA #294 - User permissions
  • Loading branch information
dleadbetter authored Jan 3, 2025
2 parents 14d6002 + a51bc2b commit b827fe2
Show file tree
Hide file tree
Showing 14 changed files with 135 additions and 54 deletions.
9 changes: 7 additions & 2 deletions app/controllers/api/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,17 @@ def prepare_params

private

def is_owned?
item = item_class.find(params[:id])
item.created_by_id == current_user.id
end

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

def validate_update_authorization
return if current_user.admin?
return if current_user.admin? || is_owned?

unauthorized = false

Expand Down
6 changes: 3 additions & 3 deletions app/serializers/artworks_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ class ArtworksSerializer < BaseSerializer
include LocateableSerializer
include NestableSerializer

index_attributes :id, :date_start, :date_end, :date_descriptor, :published, :created_at, :updated_at,
index_attributes :id, :date_start, :date_end, :date_descriptor, :published, :created_at, :updated_at, :created_by_id,
primary_title: [:id, :title, qualifications: QualificationsSerializer],
updated_by: UsersSerializer, created_by: UsersSerializer

show_attributes :id, :date_start, :date_end, :date_descriptor, :published, :height, :width, :depth,
:notes_external, :notes_internal, :repository_work_url, :accession_number,
:documents_count, :number_documents_visible, :created_at, :updated_at,
:documents_count, :number_documents_visible, :created_at, :updated_at, :created_by_id,
artwork_titles: [:id, :title, :notes, :primary, qualifications: QualificationsSerializer],
updated_by: UsersSerializer, created_by: UsersSerializer,
participations: ParticipationsSerializer, qualifications: QualificationsSerializer

nested_attributes :id, primary_title: [:id, :title, qualifications: QualificationsSerializer],
nested_attributes :id, :created_by_id, primary_title: [:id, :title, qualifications: QualificationsSerializer],
primary_attachment: [:id, :file_url, :primary, :thumbnail_url],
children: { physical_components: PhysicalComponentsSerializer }
end
7 changes: 4 additions & 3 deletions app/serializers/documents_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ class DocumentsSerializer < BaseSerializer
include AttachableSerializer
include NestableSerializer

index_attributes :id, :name
index_attributes :id, :name, :created_by_id

show_attributes :id, :name, :visual_context_id, :notes, :number_sewing_supports, :number_fastenings,
:inscriptions_on_binding, :inscription_text, :endband_present, :uncut_fore_edges, :fore_edge_text,
:bookmarks_registers, :text_columns, :ruling, :rubrication, :transcription, :transcription_expanded,
:transcription_translation, :identity, :created_at, :updated_at,
:transcription_translation, :identity, :created_at, :updated_at, :created_by_id,
qualifications: QualificationsSerializer, actions: [:id, :document_id, :notes,
qualifications: QualificationsSerializer]

Expand All @@ -23,5 +23,6 @@ class DocumentsSerializer < BaseSerializer
}
end

nested_attributes :id, :visual_context_id, :name, primary_attachment: [:id, :file_url, :primary, :thumbnail_url]
nested_attributes :id, :visual_context_id, :name, :created_by_id,
primary_attachment: [:id, :file_url, :primary, :thumbnail_url]
end
6 changes: 3 additions & 3 deletions app/serializers/physical_components_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ class PhysicalComponentsSerializer < BaseSerializer
include AttachableSerializer
include NestableSerializer

index_attributes :id, :name
index_attributes :id, :name, :created_by_id

show_attributes :id, :artwork_id, :name, :height, :width, :depth, :notes, :created_at, :updated_at
show_attributes :id, :artwork_id, :name, :height, :width, :depth, :notes, :created_at, :updated_at, :created_by_id

nested_attributes :id, :artwork_id, :name,
nested_attributes :id, :artwork_id, :name, :created_by_id,
primary_attachment: [:id, :file_url, :primary, :thumbnail_url],
children: { visual_contexts: VisualContextsSerializer }
end
6 changes: 3 additions & 3 deletions app/serializers/visual_contexts_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ class VisualContextsSerializer < BaseSerializer
include AttachableSerializer
include NestableSerializer

index_attributes :id, :name
index_attributes :id, :name, :created_by_id

show_attributes :id, :physical_component_id, :name, :height, :width, :depth, :notes, :beta, :created_at, :updated_at,
qualifications: QualificationsSerializer
:created_by_id, qualifications: QualificationsSerializer

show_attributes(:artwork_id) { |visual_context| visual_context.physical_component&.artwork_id }

nested_attributes :id, :physical_component_id, :name,
nested_attributes :id, :physical_component_id, :name, :created_by_id,
primary_attachment: [:id, :file_url, :primary, :thumbnail_url],
children: { documents: DocumentsSerializer }
end
43 changes: 26 additions & 17 deletions client/src/components/AdminArtworkMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import ArtworksService from '../services/Artworks';
import DocumentsService from '../services/Documents';
import { getPhysicalComponents, getVisualContexts } from '../utils/Artwork';
import ItemLabel from './ItemLabel';
import PermissionsService from '../services/Permissions';
import PhysicalComponentsService from '../services/PhysicalComponents';
import Session from '../services/Session';
import VisualContextsService from '../services/VisualContexts';
Expand Down Expand Up @@ -173,7 +174,7 @@ const AdminArtworkMenu = (props: Props) => {
to={`/admin${path}`}
/>
)}
{ Session.isAdmin() && onDelete && (
{ onDelete && (
<Button
icon='times'
onClick={(e) => {
Expand Down Expand Up @@ -241,23 +242,30 @@ const AdminArtworkMenu = (props: Props) => {
const renderRight = useCallback((item: ItemType) => {
// Artwork type
if (item.type === ItemTypes.artwork) {
const onDelete = () => setDeleteItem({
...item,
onDelete: () => ArtworksService.delete(item)
});

return renderActions({
addPath: {
pathname: '/admin/physical_components/new',
state: {
artwork_id: item.id
}
},
onDelete: () => setDeleteItem({
...item,
onDelete: () => ArtworksService.delete(item)
}),
onDelete: PermissionsService.canDeleteArtwork(item) ? onDelete : undefined,
path: item.path
});
}

// Physical component type
if (item.type === ItemTypes.physicalComponent) {
const onDelete = () => setDeleteItem({
...item,
onDelete: () => PhysicalComponentsService.delete(item)
});

return renderActions({
addPath: {
pathname: '/admin/visual_contexts/new',
Expand All @@ -266,16 +274,18 @@ const AdminArtworkMenu = (props: Props) => {
physical_component_id: item.id
}
},
onDelete: () => setDeleteItem({
...item,
onDelete: () => PhysicalComponentsService.delete(item)
}),
onDelete: PermissionsService.canDeletePhysicalComponent(item) ? onDelete : undefined,
path: item.path
});
}

// Visual context type
if (item.type === ItemTypes.visualContext) {
const onDelete = () => setDeleteItem({
...item,
onDelete: () => VisualContextsService.delete(item)
});

return renderActions({
addPath: {
pathname: '/admin/documents/new',
Expand All @@ -284,10 +294,7 @@ const AdminArtworkMenu = (props: Props) => {
visual_context_id: item.id
}
},
onDelete: () => setDeleteItem({
...item,
onDelete: () => VisualContextsService.delete(item)
}),
onDelete: PermissionsService.canDeleteVisualContext(item) ? onDelete : undefined,
onReorder: () => setReorderItem({
...item,
onReorder: (parentId) => VisualContextsService.save({
Expand All @@ -301,11 +308,13 @@ const AdminArtworkMenu = (props: Props) => {

// Document type
if (item.type === ItemTypes.document) {
const onDelete = () => setDeleteItem({
...item,
onDelete: () => DocumentsService.delete(item)
});

return renderActions({
onDelete: () => setDeleteItem({
...item,
onDelete: () => DocumentsService.delete(item)
}),
onDelete: PermissionsService.canDeleteDocument(item) ? onDelete : undefined,
onReorder: () => setReorderItem({
...item,
onReorder: (parentId) => DocumentsService.save({
Expand Down
28 changes: 12 additions & 16 deletions client/src/components/ArtworkMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import React, {
useState,
type Node
} from 'react';
import { useLocation } from 'react-router-dom';
import { Image, Loader } from 'semantic-ui-react';
import _ from 'underscore';
import ArtworksService from '../services/Artworks';
import DocumentsService from '../services/Documents';
import ItemLabel from './ItemLabel';
import { useLocation } from 'react-router-dom';
import './ArtworkMenu.css';

type ItemType = {
Expand Down Expand Up @@ -159,14 +159,13 @@ const ArtworkMenu = (props: Props) => {
* level: number, name, id, type: string}}
*/
const transformDocument = useCallback((parent, doc) => ({
id: doc.id,
name: doc.name,
..._.pick(doc, 'id', 'name', 'created_by_id'),
image: doc.primary_attachment && doc.primary_attachment.thumbnail_url,
type: ItemTypes.document,
level: 3,
path: `/documents/${doc.id}`,
parent: { ...parent, path: `/admin/visual_contexts/${parent.id}` },
onDelete: () => DocumentsService.delete(doc)
onDelete: () => DocumentsService.delete(doc),
}), []);

/**
Expand All @@ -179,14 +178,13 @@ const ArtworkMenu = (props: Props) => {
* level: number, children, name, id, type: string, onAdd: (function(): *)}}
*/
const transformVisualContext = useCallback((parent, vc) => ({
id: vc.id,
name: vc.name,
image: vc.primary_attachment && vc.primary_attachment.thumbnail_url,
type: ItemTypes.visualContext,
level: 2,
path: `/visual_contexts/${vc.id}`,
parent: { ...parent, path: `/physical_components/${parent.id}` },
children: _.map(vc.documents, transformDocument.bind(this, vc))
..._.pick(vc, 'id', 'name', 'created_by_id'),
image: vc.primary_attachment && vc.primary_attachment.thumbnail_url,
type: ItemTypes.visualContext,
level: 2,
path: `/visual_contexts/${vc.id}`,
parent: { ...parent, path: `/physical_components/${parent.id}` },
children: _.map(vc.documents, transformDocument.bind(this, vc))
}), [transformDocument]);

/**
Expand All @@ -199,8 +197,7 @@ const ArtworkMenu = (props: Props) => {
* level: number, children, name, id, type: string, onAdd: (function(): *)}}
*/
const transformPhysicalComponent = useCallback((parent, pc) => ({
id: pc.id,
name: pc.name,
..._.pick(pc, 'id', 'name', 'created_by_id'),
image: pc.primary_attachment && pc.primary_attachment.thumbnail_url,
type: ItemTypes.physicalComponent,
level: 1,
Expand All @@ -218,8 +215,7 @@ const ArtworkMenu = (props: Props) => {
* level: number, children, name: *, id, type: string, onAdd: (function(): *)}}
*/
const transformArtwork = useCallback((a) => ({
id: a.id,
name: a.primary_title && a.primary_title.title,
..._.pick(a, 'id', 'name', 'created_by_id'),
image: a.primary_attachment && a.primary_attachment.thumbnail_url,
type: ItemTypes.artwork,
level: 0,
Expand Down
4 changes: 2 additions & 2 deletions client/src/pages/admin/Artworks.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Container, Header } from 'semantic-ui-react';
import _ from 'underscore';
import ArtworksService from '../../services/Artworks';
import Authorization from '../../utils/Authorization';
import Session from '../../services/Session';
import PermissionsService from '../../services/Permissions';
import Thumbnail from '../../components/Thumbnail';
import User from '../../transforms/User';
import Users from '../../services/Users';
Expand All @@ -28,7 +28,7 @@ const Artworks = (props: Props) => (
name: 'edit',
onClick: (item) => props.history.push(`/admin/artworks/${item.id}`)
}, {
accept: () => Session.isAdmin(),
accept: (item) => PermissionsService.canDeleteArtwork(item),
icon: 'times',
name: 'delete'
}, {
Expand Down
55 changes: 55 additions & 0 deletions client/src/services/Permissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// @flow

import type { Artwork } from '../types/Artwork';
import Session from './Session';
import type { PhysicalComponent } from '../types/PhysicalComponent';
import type { Document } from '../types/Document';
import type { VisualContext } from '../types/VisualContext';

class Permissions {
/**
* Returns `true` if the passed artwork can be deleted by the current user.
*
* @param artwork
*
* @returns {*|boolean}
*/
canDeleteArtwork(artwork: Artwork) {
return Session.isAdmin() || Session.getUserId() === artwork.created_by_id;
}

/**
* Returns `true` if the passed document can be deleted by the current user.
*
* @param document
*
* @returns {*|boolean}
*/
canDeleteDocument(document: Document) {
return Session.isAdmin() || Session.getUserId() === document.created_by_id;
}

/**
* Returns `true` if the passed physical component can be deleted by the current user.
*
* @param physicalComponent
*
* @returns {*|boolean}
*/
canDeletePhysicalComponent(physicalComponent: PhysicalComponent) {
return Session.isAdmin() || Session.getUserId() === physicalComponent.created_by_id;
}

/**
* Returns `true` if the passed visual context can be deleted by the current user.
*
* @param visualContext
*
* @returns {*|boolean}
*/
canDeleteVisualContext(visualContext: VisualContext) {
return Session.isAdmin() || Session.getUserId() === visualContext.created_by_id;
}
}

export default new Permissions();
15 changes: 13 additions & 2 deletions client/src/services/Session.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ class Session {
* @param response
*/
create(response: any) {
const { name, uid, admin } = response.data.data;
const { name, uid, admin, id } = response.data.data;

sessionStorage.setItem('user',
JSON.stringify({
'access-token': response.headers['access-token'],
client: response.headers.client,
name,
uid,
admin
admin,
id
}));
}

Expand All @@ -39,6 +40,16 @@ class Session {
return user.name;
}

/**
* Returns the ID of the current user.
*
* @returns {*}
*/
getUserId() {
const user = this.parseUser();
return user.id;
}

/**
* Returns true if the currently logged in user is an admin.
*
Expand Down
1 change: 1 addition & 0 deletions client/src/types/Artwork.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type Artwork = Attachable & Locateable & Participateable & {
accession_number: string,
documents_count: number,
number_documents_visible: number,
created_by_id: number,
created_by: User,
updated_by: User,
created_at: string,
Expand Down
Loading

0 comments on commit b827fe2

Please sign in to comment.