Skip to content

Commit

Permalink
Feat/roles (#16)
Browse files Browse the repository at this point in the history
* full rework of roles: string[] => {[privilege]?: boolean}

* roles {} are flattened to [] for transport and persistence
  • Loading branch information
jlarsson authored Sep 19, 2023
1 parent 1ff80d3 commit 965d293
Show file tree
Hide file tree
Showing 37 changed files with 481 additions and 207 deletions.
1 change: 0 additions & 1 deletion openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,3 @@ components:
type: array
items:
type: string

61 changes: 39 additions & 22 deletions src/adverts/advert-meta/advert-meta.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,74 @@
import { normalizeRoles } from '../../login'
import type { HaffaUser } from '../../login/types'
import { AdvertClaimType, AdvertType } from '../types'
import type { Advert, AdvertMeta } from '../types'

export const getAdvertMeta = (advert: Advert, user: HaffaUser): AdvertMeta => {
const { type, quantity } = advert
const mine = advert.createdBy === user.id
const admin = user.roles.includes('admin')

const claimCount = advert.claims.reduce((s, { quantity }) => s + quantity, 0)
const claimCount = advert.claims.reduce((s, c) => s + c.quantity, 0)
const myCollectedCount = advert.claims
.filter(
({ by, type }) => by === user.id && type === AdvertClaimType.collected
)
.map(({ quantity }) => quantity)
.filter(c => c.by === user.id && c.type === AdvertClaimType.collected)
.map(c => c.quantity)
.reduce((s, v) => s + v, 0)

const myReservationCount = advert.claims
.filter(
({ by, type }) => by === user.id && type === AdvertClaimType.reserved
)
.map(({ quantity }) => quantity)
.filter(c => c.by === user.id && c.type === AdvertClaimType.reserved)
.map(c => c.quantity)
.reduce((s, v) => s + v, 0)

const isNotArchived = !advert.archivedAt
const isArchived = !isNotArchived

const {
canEditOwnAdverts,
canArchiveOwnAdverts,
canRemoveOwnAdverts,
canReserveAdverts,
canCollectAdverts,
canManageOwnAdvertsHistory,
canManageAllAdverts,
} = normalizeRoles(user.roles)

if (type === AdvertType.recycle) {
return {
reservableQuantity: quantity - claimCount,
collectableQuantity: myReservationCount + quantity - claimCount,
isMine: mine,
canEdit: mine || admin,
canArchive: isNotArchived && (mine || admin),
canUnarchive: isArchived && (mine || admin),
canRemove: admin,
canEdit: canEditOwnAdverts && (mine || canManageAllAdverts),
canArchive:
isNotArchived && canArchiveOwnAdverts && (mine || canManageAllAdverts),
canUnarchive:
isArchived && canArchiveOwnAdverts && (mine || canManageAllAdverts),
canRemove: canRemoveOwnAdverts && (mine || canManageAllAdverts),
canBook: false, // type === AdvertType.borrow,
canReserve: isNotArchived && quantity > claimCount,
canCancelReservation: myReservationCount > 0,
canReserve: isNotArchived && quantity > claimCount && canReserveAdverts,
canCancelReservation: myReservationCount > 0 && canReserveAdverts,
canCollect:
isNotArchived && (myReservationCount > 0 || quantity > claimCount),
canCancelClaim: mine || admin,
isNotArchived &&
(myReservationCount > 0 || quantity > claimCount) &&
canCollectAdverts,
canCancelClaim:
canManageOwnAdvertsHistory && (mine || canManageAllAdverts),
reservedyMe: myReservationCount,
collectedByMe: myCollectedCount,
claims: mine || admin ? advert.claims : [],
claims:
canManageOwnAdvertsHistory && (mine || canManageAllAdverts)
? advert.claims
: [],
}
}
return {
reservableQuantity: 0,
collectableQuantity: 0,
isMine: mine,
canEdit: mine,
canArchive: !advert.archivedAt && (mine || admin),
canUnarchive: !!advert.archivedAt && (mine || admin),
canRemove: admin,
canArchive:
isNotArchived && canArchiveOwnAdverts && (mine || canManageAllAdverts),
canUnarchive:
isArchived && canArchiveOwnAdverts && (mine || canManageAllAdverts),
canRemove: canRemoveOwnAdverts && (mine || canManageAllAdverts),
canBook: false, // type === AdvertType.borrow,
canReserve: false,
canCancelReservation: false,
Expand Down
23 changes: 15 additions & 8 deletions src/adverts/advert-meta/tests/can-archive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@ import type { HaffaUser } from '../../../login/types'
import { createEmptyAdvert } from '../../mappers'

describe('getAdvertMeta::canArchive', () => {
const testUser: HaffaUser = { id: 'test@user', roles: [] }
it('owner can archive', () => {
const createUserWithRights = (id: string): HaffaUser => ({
id,
roles: { canArchiveOwnAdverts: true },
})
it('owner with rights can archive', () => {
expect(
getAdvertMeta(createEmptyAdvert({ createdBy: 'test@user' }), testUser)
.canArchive
getAdvertMeta(
createEmptyAdvert({ createdBy: 'test@user' }),
createUserWithRights('test@user')
).canArchive
).toBe(true)
})
it('owner can archive unless already archived', () =>
it('owner with rights can archive unless already archived', () =>
expect(
getAdvertMeta(
createEmptyAdvert({ createdBy: 'test@user', archivedAt: 'now' }),
testUser
createUserWithRights('test@user')
).canArchive
).toBe(false))

Expand All @@ -39,8 +44,10 @@ describe('getAdvertMeta::canArchive', () => {

it('others may not archive', () => {
expect(
getAdvertMeta(createEmptyAdvert({ createdBy: 'another@user' }), testUser)
.canArchive
getAdvertMeta(
createEmptyAdvert({ createdBy: 'test@user' }),
createUserWithRights('another@user')
).canArchive
).toBe(false)
})
})
7 changes: 4 additions & 3 deletions src/adverts/advert-meta/tests/can-book.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { getAdvertMeta } from '..'
import { makeAdmin } from '../../../login'
import { HaffaUser } from '../../../login/types'
import { createEmptyAdvert } from '../../mappers'
import { AdvertType } from '../../types'

describe('getAdvertMeta::canBook', () => {
const testUser: HaffaUser = { id: 'test@user', roles: [] }

const unbookables = [
createEmptyAdvert({ quantity: 1, type: AdvertType.recycle }),
createEmptyAdvert({ quantity: 1, type: AdvertType.borrow }),
Expand All @@ -22,7 +21,9 @@ describe('getAdvertMeta::canBook', () => {
]
it('bookings are never supported', () => {
unbookables.forEach(advert =>
expect(getAdvertMeta(advert, testUser).canBook).toBe(false)
expect(
getAdvertMeta(advert, makeAdmin({ id: 'some@admin' })).canBook
).toBe(false)
)
})
})
17 changes: 13 additions & 4 deletions src/adverts/advert-meta/tests/can-cancel-reservation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import type { AdvertClaim } from '../../types'
import { AdvertClaimType, AdvertType } from '../../types'

describe('getAdvertMeta::canCancelReservation', () => {
const testUser: HaffaUser = { id: 'test@user', roles: [] }
const createUserWithRights = (id: string): HaffaUser => ({
id,
roles: { canReserveAdverts: true },
})

const createReservation = (defaults?: Partial<AdvertClaim>): AdvertClaim => ({
by: testUser.id,
by: 'test@user',
at: new Date().toISOString(),
quantity: 1,
type: AdvertClaimType.reserved,
Expand Down Expand Up @@ -54,12 +57,18 @@ describe('getAdvertMeta::canCancelReservation', () => {

it('allows', () => {
cancellableAdverts.forEach(advert =>
expect(getAdvertMeta(advert, testUser).canCancelReservation).toBe(true)
expect(
getAdvertMeta(advert, createUserWithRights('test@user'))
.canCancelReservation
).toBe(true)
)
})
it('denies', () => {
uncancellableAdverts.forEach(advert =>
expect(getAdvertMeta(advert, testUser).canCancelReservation).toBe(false)
expect(
getAdvertMeta(advert, createUserWithRights('test@user'))
.canCancelReservation
).toBe(false)
)
})
})
26 changes: 20 additions & 6 deletions src/adverts/advert-meta/tests/can-collect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import type { AdvertClaim } from '../../types'
import { AdvertClaimType, AdvertType } from '../../types'

describe('getAdvertMeta::canCollect', () => {
const testUser: HaffaUser = { id: 'test@user', roles: [] }
const createUserWithRights = (id: string): HaffaUser => ({
id,
roles: { canCollectAdverts: true },
})

const createClaim = (defaults?: Partial<AdvertClaim>): AdvertClaim => ({
by: testUser.id,
by: 'test@user',
at: new Date().toISOString(),
quantity: 1,
type: AdvertClaimType.reserved,
Expand Down Expand Up @@ -54,14 +57,25 @@ describe('getAdvertMeta::canCollect', () => {
createEmptyAdvert({ quantity: 1, claims: [createCollect()] }),
]

it('allows', () => {
it('allows user with rights', () => {
collectableAdverts.forEach(advert =>
expect(
getAdvertMeta(advert, createUserWithRights('test@user')).canCollect
).toBe(true)
)
})

it('denies user without rights', () => {
collectableAdverts.forEach(advert =>
expect(getAdvertMeta(advert, testUser).canCollect).toBe(true)
expect(getAdvertMeta(advert, { id: 'test@user' }).canCollect).toBe(false)
)
})
it('denies', () => {

it('denies fully collected', () => {
uncollctableAdverts.forEach(advert =>
expect(getAdvertMeta(advert, testUser).canCollect).toBe(false)
expect(
getAdvertMeta(advert, createUserWithRights('test@user')).canCollect
).toBe(false)
)
})
})
29 changes: 23 additions & 6 deletions src/adverts/advert-meta/tests/can-edit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,34 @@ import type { HaffaUser } from '../../../login/types'
import { createEmptyAdvert } from '../../mappers'

describe('getAdvertMeta::canEdit', () => {
const testUser: HaffaUser = { id: 'test@user', roles: [] }
it('owner can edit', () => {
const createUserWithRights = (id: string): HaffaUser => ({
id,
roles: { canEditOwnAdverts: true },
})

it('owner with rights can edit', () => {
expect(
getAdvertMeta(createEmptyAdvert({ createdBy: 'test@user' }), testUser)
.canEdit
getAdvertMeta(
createEmptyAdvert({ createdBy: 'test@user' }),
createUserWithRights('test@user')
).canEdit
).toBe(true)
})

it('owner without rights can not edit', () => {
expect(
getAdvertMeta(createEmptyAdvert({ createdBy: 'test@user' }), {
id: 'test@user',
}).canEdit
).toBe(false)
})

it('others may not edit', () => {
expect(
getAdvertMeta(createEmptyAdvert({ createdBy: 'another@user' }), testUser)
.canEdit
getAdvertMeta(
createEmptyAdvert({ createdBy: 'test@user' }),
createUserWithRights('another@user')
).canEdit
).toBe(false)
})
})
24 changes: 15 additions & 9 deletions src/adverts/advert-meta/tests/can-remove.spec.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
import { getAdvertMeta } from '..'
import { makeAdmin } from '../../../login'
import type { HaffaUser } from '../../../login/types'
import { createEmptyAdvert } from '../../mappers'

describe('getAdvertMeta::canRemove', () => {
const testUser: HaffaUser = { id: 'test@user', roles: [] }
it('admin can remove', () => {
const createUserWithRights = (id: string): HaffaUser => ({
id,
roles: { canRemoveOwnAdverts: true },
})

it('with rights can remove own', () => {
expect(
getAdvertMeta(
createEmptyAdvert({ createdBy: 'test@user' }),
makeAdmin(testUser)
createUserWithRights('test@user')
).canRemove
).toBe(true)
})
it('owner may not remove', () => {
it('without rights can not remove', () => {
expect(
getAdvertMeta(createEmptyAdvert({ createdBy: 'another@user' }), testUser)
.canRemove
getAdvertMeta(createEmptyAdvert({ createdBy: 'test@user' }), {
id: 'test@user',
}).canRemove
).toBe(false)
})

it('others may not remove', () => {
expect(
getAdvertMeta(createEmptyAdvert({ createdBy: 'another@user' }), testUser)
.canRemove
getAdvertMeta(
createEmptyAdvert({ createdBy: 'test@user' }),
createUserWithRights('another@user')
).canRemove
).toBe(false)
})
})
26 changes: 21 additions & 5 deletions src/adverts/advert-meta/tests/can-reserve.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { getAdvertMeta } from '..'
import { HaffaUser } from '../../../login/types'
import type { HaffaUser } from '../../../login/types'
import { createEmptyAdvert } from '../../mappers'
import { AdvertClaim, AdvertClaimType, AdvertType } from '../../types'
import type { AdvertClaim } from '../../types'
import { AdvertClaimType, AdvertType } from '../../types'

describe('getAdvertMeta::canReserve', () => {
const testUser: HaffaUser = { id: 'test@user', roles: [] }
const testUser: HaffaUser = { id: 'test@user' }
const createUserWithRights = (id: string): HaffaUser => ({
id,
roles: { canReserveAdverts: true },
})

const createReservation = (defaults?: Partial<AdvertClaim>): AdvertClaim => ({
by: 'test@user',
Expand Down Expand Up @@ -43,12 +48,23 @@ describe('getAdvertMeta::canReserve', () => {

it('allows reservations if advert.quantity exceeds total reservations', () => {
reservableAdverts.forEach(advert =>
expect(getAdvertMeta(advert, testUser).canReserve).toBe(true)
expect(
getAdvertMeta(advert, createUserWithRights('test@user')).canReserve
).toBe(true)
)
})

it('denies reservations if rights are missing', () => {
reservableAdverts.forEach(advert =>
expect(getAdvertMeta(advert, { id: 'test@user' }).canReserve).toBe(false)
)
})

it('denies reservations if total reservations amounts ot advert.quantity', () => {
unreservableAdverts.forEach(advert =>
expect(getAdvertMeta(advert, testUser).canReserve).toBe(false)
expect(
getAdvertMeta(advert, createUserWithRights('test@user')).canReserve
).toBe(false)
)
})
})
10 changes: 9 additions & 1 deletion src/adverts/advert-mutations/archiving/archive-advert.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ describe('archiveAdvert', () => {

return end2endTest(
{ services: { notifications } },
async ({ mappedGqlRequest, adverts, user }) => {
async ({ mappedGqlRequest, adverts, user, loginPolicies }) => {
// give us rights to archive
await loginPolicies.updateLoginPolicies([
{
emailPattern: user.id,
roles: ['canArchiveOwnAdverts'],
},
])

// eslint-disable-next-line no-param-reassign
adverts['advert-123'] = {
...createEmptyAdvert(),
Expand Down
Loading

0 comments on commit 965d293

Please sign in to comment.