Skip to content

Commit

Permalink
Merge branch 'develop' into configurable-roles
Browse files Browse the repository at this point in the history
  • Loading branch information
Nil20 committed Jan 10, 2025
2 parents a87ba79 + 0c24f14 commit f68d20d
Show file tree
Hide file tree
Showing 14 changed files with 440 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ describe('when user starts a new declaration', () => {
*/
store.dispatch(storeDeclaration(draft))

router.navigate(
await router.navigate(
formatUrl(DRAFT_BIRTH_PARENT_FORM, {
declarationId: draft.id.toString()
})
Expand Down
10 changes: 7 additions & 3 deletions packages/data-seeder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
*/
import { env } from './environment'
import fetch from 'node-fetch'
import { seedLocations } from './locations'

import { seedLocations, seedLocationsForV2Events } from './locations'
import { seedUsers } from './users'
import { parseGQLResponse, raise } from './utils'
import { print } from 'graphql'
Expand Down Expand Up @@ -89,14 +88,19 @@ async function deactivateSuperuser(token: string) {
}
})
})

parseGQLResponse(await res.json())
}

async function main() {
const token = await getToken()

console.log('Seeding locations')
console.log('Seeding locations for v1 system')
await seedLocations(token)

console.log('Seeding locations for v2 system (events)')
await seedLocationsForV2Events(token)

console.log('Seeding users')
await seedUsers(token)
await deactivateSuperuser(token)
Expand Down
37 changes: 37 additions & 0 deletions packages/data-seeder/src/locations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ export async function seedLocations(token: string) {
)
)
).flat()

const savedLocationsSet = new Set(savedLocations)
const locations = (await getLocations()).filter((location) => {
return !savedLocationsSet.has(location.id)
Expand Down Expand Up @@ -252,3 +253,39 @@ export async function seedLocations(token: string) {
}
})
}

function updateLocationPartOf(partOf: string) {
const locationPrefix = 'Location/'

const parent = partOf.replace(locationPrefix, '')

if (parent === '0') {
return null
}

return parent
}

export async function seedLocationsForV2Events(token: string) {
const locations = await getLocations()

const simplifiedLocations = locations.map((location) => ({
id: location.id,
name: location.name,
partOf: updateLocationPartOf(location.partOf)
}))

// NOTE: TRPC expects certain format, which may seem unconventional.
const res = await fetch(`${env.GATEWAY_HOST}/events/locations.set`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ json: simplifiedLocations })
})

if (!res.ok) {
raise(await res.json())
}
}
2 changes: 2 additions & 0 deletions packages/events/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@trpc/server": "^11.0.0-rc.532",
"app-module-path": "^2.2.0",
"envalid": "^8.0.0",
"jsonwebtoken": "^9.0.0",
"mongodb": "6.9.0",
"superjson": "1.9.0-0",
"tsconfig-paths": "^3.13.0",
Expand All @@ -30,6 +31,7 @@
"@testcontainers/elasticsearch": "^10.15.0",
"@typescript-eslint/eslint-plugin": "^4.5.0",
"@typescript-eslint/parser": "^4.5.0",
"@types/jsonwebtoken": "^9.0.0",
"cross-env": "^7.0.0",
"eslint": "^7.11.0",
"eslint-config-prettier": "^9.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Prevents sending empty payload 1`] = `
[TRPCError: [
{
"code": "too_small",
"minimum": 1,
"type": "array",
"inclusive": true,
"exact": false,
"message": "Array must contain at least 1 element(s)",
"path": []
}
]]
`;

exports[`prevents unauthorized access from registrar 1`] = `[TRPCError: UNAUTHORIZED]`;
45 changes: 45 additions & 0 deletions packages/events/src/router/locations.get.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import { createTestClient } from '@events/tests/utils'
import { payloadGenerator } from '@events/tests/generators'
import { userScopes } from '@opencrvs/commons'

const nationalSystemAdminClient = createTestClient([
userScopes.nationalSystemAdmin
])
const generator = payloadGenerator()

test('Returns empty list when no locations are set', async () => {
const locations = await nationalSystemAdminClient.locations.get()

expect(locations).toEqual([])
})

test('Returns single location in right format', async () => {
const setLocationPayload = [
{ id: '123-456-789', partOf: null, name: 'Location foobar' }
]

await nationalSystemAdminClient.locations.set(setLocationPayload)

const locations = await nationalSystemAdminClient.locations.get()

expect(locations).toHaveLength(1)
expect(locations).toMatchObject(setLocationPayload)
})

test('Returns multiple locations', async () => {
await nationalSystemAdminClient.locations.set(generator.locations.set(5))

const locations = await nationalSystemAdminClient.locations.get()

expect(locations).toHaveLength(5)
})
95 changes: 95 additions & 0 deletions packages/events/src/router/locations.set.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import { createTestClient } from '@events/tests/utils'
import { payloadGenerator } from '@events/tests/generators'
import { userScopes } from '@opencrvs/commons'

const nationalSystemAdminClient = createTestClient([
userScopes.nationalSystemAdmin
])

const registrarClient = createTestClient()

const generator = payloadGenerator()

test('prevents unauthorized access from registrar', async () => {
await expect(
registrarClient.locations.set([])
).rejects.toThrowErrorMatchingSnapshot()
})

test('Allows national system admin to set locations', async () => {
await expect(
nationalSystemAdminClient.locations.set(generator.locations.set(1))
).resolves.toEqual(undefined)
})

test('Prevents sending empty payload', async () => {
await expect(
nationalSystemAdminClient.locations.set([])
).rejects.toThrowErrorMatchingSnapshot()
})

test('Creates single location', async () => {
const locationPayload = [
{ id: '123-456-789', partOf: null, name: 'Location foobar' }
]

await nationalSystemAdminClient.locations.set(locationPayload)

const locations = await nationalSystemAdminClient.locations.get()

expect(locations).toHaveLength(1)
expect(locations).toMatchObject(locationPayload)
})

test('Creates multiple locations', async () => {
const parentId = 'parent-id'

const locationPayload = generator.locations.set([
{ id: 'parentId' },
{ partOf: parentId },
{ partOf: parentId },
{}
])

await nationalSystemAdminClient.locations.set(locationPayload)

const locations = await nationalSystemAdminClient.locations.get()

expect(locations).toEqual(locationPayload)
})

test('Removes existing locations not in payload', async () => {
const initialPayload = generator.locations.set(5)

await nationalSystemAdminClient.locations.set(initialPayload)

const initialLocations = await nationalSystemAdminClient.locations.get()
expect(initialLocations).toHaveLength(initialPayload.length)

const [removedLocation, ...remainingLocationsPayload] = initialPayload

await nationalSystemAdminClient.locations.set(remainingLocationsPayload)

const remainingLocationsAfterDeletion =
await nationalSystemAdminClient.locations.get()

expect(remainingLocationsAfterDeletion).toHaveLength(
remainingLocationsPayload.length
)

expect(
remainingLocationsAfterDeletion.some(
(location) => location.id === removedLocation.id
)
).toBe(false)
})
64 changes: 64 additions & 0 deletions packages/events/src/router/middleware/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/

import { inScope, Scope, userScopes } from '@opencrvs/commons'
import { TRPCError, AnyTRPCMiddlewareFunction } from '@trpc/server'

import { z } from 'zod'

const ContextSchema = z.object({
user: z.object({
id: z.string(),
primaryOfficeId: z.string()
}),
token: z.string()
})

export type Context = z.infer<typeof ContextSchema>

/**
* TRPC Middleware options with correct context.
* Actual middleware type definition is only for internal use within TRPC.
*/
type MiddlewareOptions = Omit<
Parameters<AnyTRPCMiddlewareFunction>[0],
'ctx'
> & { ctx: Context }

/**
* Depending on how the API is called, there might or might not be Bearer keyword in the header.
* To allow for usage with both direct HTTP calls and TRPC, ensure it's present to be able to use shared scope auth functions.
*/
const setBearerForToken = (token: string) => {
const bearer = 'Bearer'

return token.startsWith(bearer) ? token : `${bearer} ${token}`
}
/**
* @param scopes scopes that are allowed to access the resource
* @returns TRPC compatible middleware function
*/
const createScopeAuthMiddleware =
(scopes: Scope[]) => (opts: MiddlewareOptions) => {
if (inScope({ Authorization: setBearerForToken(opts.ctx.token) }, scopes)) {
return opts.next()
}

throw new TRPCError({ code: 'UNAUTHORIZED' })
}

const isNationalSystemAdminUser = createScopeAuthMiddleware([
userScopes.nationalSystemAdmin
])

export const middleware = {
isNationalSystemAdminUser
}
29 changes: 16 additions & 13 deletions packages/events/src/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ import {
patchEvent
} from '@events/service/events'
import { presignFilesInEvent } from '@events/service/files'
import {
getLocations,
Location,
setLocations
} from '@events/service/locations/locations'
import { Context, middleware } from './middleware/middleware'
import { getIndexedEvents } from '@events/service/indexing/indexing'
import { EventConfig, getUUID } from '@opencrvs/commons'
import {
Expand All @@ -35,14 +41,6 @@ import {
ValidateActionInput
} from '@opencrvs/commons/events'

const ContextSchema = z.object({
user: z.object({
id: z.string(),
primaryOfficeId: z.string()
}),
token: z.string()
})

const validateEventType = ({
eventTypes,
eventInputType
Expand All @@ -60,8 +58,6 @@ const validateEventType = ({
}
}

type Context = z.infer<typeof ContextSchema>

export const t = initTRPC.context<Context>().create({
transformer: superjson
})
Expand Down Expand Up @@ -171,8 +167,15 @@ export const appRouter = router({
})
}),
events: router({
get: publicProcedure.output(z.array(EventIndex)).query(async () => {
return getIndexedEvents()
})
get: publicProcedure.output(z.array(EventIndex)).query(getIndexedEvents)
}),
locations: router({
set: publicProcedure
.use(middleware.isNationalSystemAdminUser)
.input(z.array(Location).min(1))
.mutation(async (options) => {
await setLocations(options.input)
}),
get: publicProcedure.output(z.array(Location)).query(getLocations)
})
})
Loading

0 comments on commit f68d20d

Please sign in to comment.