Skip to content

Commit

Permalink
Merge branch 'develop' into feat/ocrvs-7978/qr-reader
Browse files Browse the repository at this point in the history
  • Loading branch information
tahmidrahman-dsi authored Jan 14, 2025
2 parents 7e0dc81 + a12eb10 commit 02c283f
Show file tree
Hide file tree
Showing 22 changed files with 516 additions and 79 deletions.
8 changes: 7 additions & 1 deletion packages/client/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,14 @@ Query component now sends errors automatically to Sentry.`
{
name: 'date-fns',
message: 'Please use submodules instead'
},
{
name: '@opencrvs/commons',
message:
'Importing directly from `@opencrvs/commons` is not allowed. Use `@opencrvs/commons/client` instead.'
}
]
],
patterns: ['@opencrvs/commons/*', '!@opencrvs/commons/client']
}
],
'formatjs/enforce-id': 'error',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,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
9 changes: 7 additions & 2 deletions packages/data-seeder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/
import { env } from './environment'
import fetch from 'node-fetch'
import { seedLocations } from './locations'
import { seedLocations, seedLocationsForV2Events } from './locations'
import { seedRoles } from './roles'
import { seedUsers } from './users'
import { parseGQLResponse, raise } from './utils'
Expand Down Expand Up @@ -85,15 +85,20 @@ async function deactivateSuperuser(token: string) {
}
})
})

parseGQLResponse(await res.json())
}

async function main() {
const token = await getToken()
console.log('Seeding roles')
const roleIdMap = await seedRoles(token)
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, roleIdMap)
await deactivateSuperuser(token)
Expand Down
39 changes: 39 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,41 @@ 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) {
console.error(
'Unable to seed locations for v2 events. Ensure events service is running.'
)
}
}
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
}
Loading

0 comments on commit 02c283f

Please sign in to comment.