Skip to content

Commit

Permalink
Merge branch 'dev' into search-page-alerts
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] committed May 9, 2024
2 parents d1660ff + 6f383f2 commit 807bf0d
Show file tree
Hide file tree
Showing 20 changed files with 485 additions and 203 deletions.
2 changes: 1 addition & 1 deletion packages/api/lib/context.ts
Expand Up @@ -22,8 +22,8 @@ export type CreateContextOptions = {

export const createContextInner = (opts: CreateContextOptions) => {
return {
session: opts.session,
generateId,
session: opts.session,
skipCache: false,
req: opts.req,
res: opts.res,
Expand Down
35 changes: 26 additions & 9 deletions packages/api/lib/middleware/permissions.ts
Expand Up @@ -11,16 +11,19 @@ const reject = () => {
export const checkPermissions = (meta: Meta | undefined, ctx: Context) => {
try {
/** No permissions submitted, throw error */
if (typeof meta?.hasPerm === 'undefined')
if (typeof meta?.hasPerm === 'undefined') {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Invalid procedure configuration, missing permission requirements.',
})
}

/** Check for session object, error if missing */
invariant(ctx.session?.user)
/** Check if user is `root`. If so, allow. */
if (ctx.session.user.permissions.includes('root')) return true
if (ctx.session.user.permissions.includes('root')) {
return true
}

/** Check multiple permissions */
if (Array.isArray(meta.hasPerm)) {
Expand All @@ -44,38 +47,48 @@ export const checkPermissions = (meta: Meta | undefined, ctx: Context) => {

export const checkRole = (allowedRoles: string[], userRoles: string[]) => {
for (const userRole of userRoles) {
if (allowedRoles.includes(userRole)) return true
if (allowedRoles.includes(userRole)) {
return true
}
}
return false
}

export const isAdmin = t.middleware(({ ctx, meta, next }) => {
if (!ctx.session || !ctx.session.user) return reject()
if (ctx.session === null) {
return reject()
}
if (
!(checkRole(['dataAdmin', 'sysadmin', 'root'], ctx.session?.user?.roles) && checkPermissions(meta, ctx))
)
) {
return reject()
}

return next({
ctx: {
...ctx,
session: { ...ctx.session, user: ctx.session.user },
actorId: ctx.session.user.id,
skipCache: true,
},
})
})
export const isStaff = t.middleware(({ ctx, meta, next }) => {
if (!ctx.session || !ctx.session.user) return reject()
if (ctx.session === null) {
return reject()
}
if (
!(
checkRole(['dataManager', 'dataAdmin', 'sysadmin', 'system', 'root'], ctx.session?.user?.roles) &&
checkPermissions(meta, ctx)
)
)
) {
return reject()
}

return next({
ctx: {
...ctx,
session: { ...ctx.session, user: ctx.session.user },
actorId: ctx.session.user.id,
skipCache: true,
Expand All @@ -84,16 +97,20 @@ export const isStaff = t.middleware(({ ctx, meta, next }) => {
})

export const hasPermissions = t.middleware(({ ctx, meta, next }) => {
if (!ctx.session || !ctx.session.user) return reject()
if (ctx.session === null) {
return reject()
}

if (checkPermissions(meta, ctx))
if (checkPermissions(meta, ctx)) {
return next({
ctx: {
...ctx,
session: { ...ctx.session, user: ctx.session.user },
actorId: ctx.session.user.id,
skipCache: true,
},
})
}

throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Insufficient permissions' })
})
2 changes: 1 addition & 1 deletion packages/api/lib/trpc.ts
Expand Up @@ -55,7 +55,7 @@ export const importHandler = async <
name: string,
importer: () => Promise<T>
) => {
const nameInCache = name as keyof typeof HANDLER_CACHE
const nameInCache = name satisfies keyof typeof HANDLER_CACHE

if (!HANDLER_CACHE[nameInCache]) {
const importedModule = await importer()
Expand Down
77 changes: 42 additions & 35 deletions packages/api/router/location/query.forGoogleMaps.handler.ts
Expand Up @@ -2,6 +2,7 @@ import { TRPCError } from '@trpc/server'
import { getBounds, getCenterOfBounds } from 'geolib'

import { prisma } from '@weareinreach/db'
import { handleError } from '~api/lib'
import { globalWhere } from '~api/selects/global'
import { type TRPCHandlerParams } from '~api/types/handler'

Expand All @@ -21,45 +22,51 @@ const getCenter = (coords: { latitude: number; longitude: number }[]): google.ma
return { lat: center.latitude, lng: center.longitude }
}

const forGoogleMaps = async ({ input }: TRPCHandlerParams<TForGoogleMapsSchema>) => {
const result = await prisma.orgLocation.findMany({
where: {
...globalWhere.isPublic(),
id: { in: Array.isArray(input.locationIds) ? input.locationIds : [input.locationIds] },
AND: [{ latitude: { not: 0 } }, { longitude: { not: 0 } }],
},
select: {
id: true,
name: true,
latitude: true,
longitude: true,
},
})
if (!result.length) {
throw new TRPCError({ code: 'NOT_FOUND' })
}
const coordsForBounds: { latitude: number; longitude: number }[] = []
const forGoogleMaps = async ({ input, ctx }: TRPCHandlerParams<TForGoogleMapsSchema>) => {
try {
const { locationIds, isEditMode } = input
const canSeeAll = isEditMode && !!ctx.session?.user?.permissions
const result = await prisma.orgLocation.findMany({
where: {
...(!canSeeAll && globalWhere.isPublic()),
id: { in: locationIds },
AND: [{ latitude: { not: 0 } }, { longitude: { not: 0 } }],
},
select: {
id: true,
name: true,
latitude: true,
longitude: true,
},
})
if (!result.length) {
throw new TRPCError({ code: 'NOT_FOUND' })
}
const coordsForBounds: { latitude: number; longitude: number }[] = []

for (const { latitude, longitude } of result) {
if (latitude && longitude) {
coordsForBounds.push({ latitude, longitude })
for (const { latitude, longitude } of result) {
if (latitude && longitude) {
coordsForBounds.push({ latitude, longitude })
}
}
}
const bounds = result.length > 1 ? getBoundary(coordsForBounds) : null
const singleLat = result.at(0)?.latitude
const singleLon = result.at(0)?.longitude
const bounds = result.length > 1 ? getBoundary(coordsForBounds) : null
const singleLat = result.at(0)?.latitude
const singleLon = result.at(0)?.longitude

const center =
result.length === 1 && singleLat && singleLon
? ({ lat: singleLat, lng: singleLon } satisfies google.maps.LatLngLiteral)
: getCenter(coordsForBounds)
const zoom = result.length === 1 ? 17 : null
const center =
result.length === 1 && singleLat && singleLon
? ({ lat: singleLat, lng: singleLon } satisfies google.maps.LatLngLiteral)
: getCenter(coordsForBounds)
const zoom = result.length === 1 ? 17 : null

return {
locations: result,
bounds,
center,
zoom,
return {
locations: result,
bounds,
center,
zoom,
}
} catch (e) {
return handleError(e)
}
}
export default forGoogleMaps
1 change: 1 addition & 0 deletions packages/api/router/location/query.forGoogleMaps.schema.ts
Expand Up @@ -4,5 +4,6 @@ import { prefixedId } from '~api/schemas/idPrefix'

export const ZForGoogleMapsSchema = z.object({
locationIds: prefixedId('orgLocation').array(),
isEditMode: z.boolean().optional().default(false),
})
export type TForGoogleMapsSchema = z.infer<typeof ZForGoogleMapsSchema>
93 changes: 52 additions & 41 deletions packages/api/router/location/query.forLocationCard.handler.ts
@@ -1,56 +1,67 @@
import { prisma } from '@weareinreach/db'
import { handleError } from '~api/lib/errorHandler'
import { globalWhere } from '~api/selects/global'
import { type TRPCHandlerParams } from '~api/types/handler'

import { type TForLocationCardSchema } from './query.forLocationCard.schema'

const forLocationCard = async ({ input }: TRPCHandlerParams<TForLocationCardSchema>) => {
const result = await prisma.orgLocation.findUniqueOrThrow({
where: {
id: input,
...globalWhere.isPublic(),
},
select: {
id: true,
name: true,
street1: true,
street2: true,
city: true,
postCode: true,
latitude: true,
longitude: true,
notVisitable: true,
country: { select: { cca2: true } },
govDist: { select: { abbrev: true, tsKey: true, tsNs: true } },
phones: {
where: { phone: globalWhere.isPublic() },
select: { phone: { select: { primary: true, number: true, country: { select: { cca2: true } } } } },
const forLocationCard = async ({ input, ctx }: TRPCHandlerParams<TForLocationCardSchema>) => {
try {
const { id, isEditMode } = input

const canSeeAll = isEditMode && !!ctx.session?.user?.permissions

const result = await prisma.orgLocation.findUniqueOrThrow({
where: {
id,
...(!canSeeAll && globalWhere.isPublic()),
},
attributes: { select: { attribute: { select: { tsNs: true, tsKey: true, icon: true } } } },
services: {
select: {
service: {
select: {
services: { select: { tag: { select: { primaryCategory: { select: { tsKey: true } } } } } },
select: {
id: true,
name: true,
street1: true,
street2: true,
city: true,
postCode: true,
latitude: true,
longitude: true,
notVisitable: true,
country: { select: { cca2: true } },
govDist: { select: { abbrev: true, tsKey: true, tsNs: true } },
phones: {
...(!canSeeAll && { where: { phone: globalWhere.isPublic() } }),
select: { phone: { select: { primary: true, number: true, country: { select: { cca2: true } } } } },
},
attributes: { select: { attribute: { select: { tsNs: true, tsKey: true, icon: true } } } },
services: {
select: {
service: {
select: {
services: { select: { tag: { select: { primaryCategory: { select: { tsKey: true } } } } } },
},
},
},
},
},
},
})
})

const transformed = {
...result,
country: result.country.cca2,
phones: result.phones.map(({ phone }) => ({ ...phone, country: phone.country.cca2 })),
attributes: result.attributes.map(({ attribute }) => attribute),
services: [
...new Set(
result.services.flatMap(({ service }) => service.services.map(({ tag }) => tag.primaryCategory.tsKey))
),
],
}
const transformed = {
...result,
country: result.country.cca2,
phones: result.phones.map(({ phone }) => ({ ...phone, country: phone.country.cca2 })),
attributes: result.attributes.map(({ attribute }) => attribute),
services: [
...new Set(
result.services.flatMap(({ service }) =>
service.services.map(({ tag }) => tag.primaryCategory.tsKey)
)
),
],
}

return transformed
return transformed
} catch (err) {
return handleError(err)
}
}
export default forLocationCard
7 changes: 5 additions & 2 deletions packages/api/router/location/query.forLocationCard.schema.ts
@@ -1,6 +1,9 @@
import { type z } from 'zod'
import { z } from 'zod'

import { prefixedId } from '~api/schemas/idPrefix'

export const ZForLocationCardSchema = prefixedId('orgLocation')
export const ZForLocationCardSchema = z.object({
id: prefixedId('orgLocation'),
isEditMode: z.boolean().optional().default(false),
})
export type TForLocationCardSchema = z.infer<typeof ZForLocationCardSchema>
6 changes: 6 additions & 0 deletions packages/api/router/orgPhone/index.ts
Expand Up @@ -68,4 +68,10 @@ export const orgPhoneRouter = defineRouter({
)
return handler(opts)
}),
upsert: permissionedProcedure('updatePhone')
.input(schema.ZUpsertSchema)
.mutation(async (opts) => {
const handler = await importHandler(namespaced('upsert'), () => import('./mutation.upsert.handler'))
return handler(opts)
}),
})

0 comments on commit 807bf0d

Please sign in to comment.