Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DataQueryApi } from './dataQuery'
import { InfoApi } from './info'
import { MessageApi } from './message'
import { User2FAAuthApi } from './user2FAAuth'
import { UserGroupApi } from './userGroup'

export const Api: ExpressInitializer = {
init: (express: Express): void => {
Expand All @@ -17,5 +18,6 @@ export const Api: ExpressInitializer = {
InfoApi.init(express)
MessageApi.init(express)
User2FAAuthApi.init(express)
UserGroupApi.init(express)
},
}
2 changes: 2 additions & 0 deletions src/api/endpoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { dataQuery } from './dataQuery'
import { info } from './info'
import { message } from './message'
import { user2FAAuth } from './user2FA'
import { userGroup } from './userGroup'

export const ApiEndpoint = {
auth,
Expand All @@ -12,4 +13,5 @@ export const ApiEndpoint = {
info,
message,
user2FAAuth,
userGroup,
}
13 changes: 13 additions & 0 deletions src/api/endpoint/userGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getApiPathSurvey } from './common'

const moduleName = 'user-groups'

export const userGroup = {
userGroups: (surveyId: string): string => getApiPathSurvey(surveyId, moduleName),
userGroupsCount: (surveyId: string): string => getApiPathSurvey(surveyId, moduleName, 'count'),
userGroup: (surveyId: string, groupUuid: string): string => getApiPathSurvey(surveyId, moduleName, groupUuid),
userGroupMembers: (surveyId: string, groupUuid: string): string =>
getApiPathSurvey(surveyId, moduleName, groupUuid, 'members'),
userGroupMember: (surveyId: string, groupUuid: string, userUuid: string): string =>
getApiPathSurvey(surveyId, moduleName, groupUuid, 'members', userUuid),
}
16 changes: 16 additions & 0 deletions src/api/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextFunction, Request, Response } from 'express'
import {
Authorizer,
AuthGroupName,
DownloadAuthTokenPayload,
RecordService,
ServiceRegistry,
Expand Down Expand Up @@ -112,6 +113,18 @@ const requireUserPermission =
}
}

// Only the survey's `surveyAdmin` group (or system admins) may manage user-groups.
// Deliberately checks the auth-group's name rather than its stored `permissions`
// column: AuthGroups.getPermissions() (arena-core) derives permissions from a
// static, non-exported map keyed by group name, not from that DB column.
const canManageUserGroups = (user: User, surveyInfo?: any) => {
if (!user) return false
if (Users.isSystemAdmin(user)) return true
if (!surveyInfo) return false
const authGroup = Users.getAuthGroupBySurveyUuid(surveyInfo.uuid)(user)
return authGroup?.name === AuthGroupName.surveyAdmin
}

const requireLoggedInUser = async (req: Request, _res: Response, next: NextFunction) => {
try {
const user = Requests.getUser(req)
Expand Down Expand Up @@ -196,5 +209,8 @@ export const ApiAuthMiddleware = {
requireCanViewAccessRequestsPermission: requirePermission(Authorizer.canViewUsersAccessRequests),
requireCanEditAccessRequestsPermission: requirePermission(Authorizer.canEditUsersAccessRequests),

// User groups
requireUserGroupManagePermission: requireSurveyPermission(canManageUserGroups),

requireDownloadToken,
}
37 changes: 37 additions & 0 deletions src/api/userGroup/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ServiceRegistry, ServiceType, SurveyService } from '@openforis/arena-core'

import { ServerError, ServerErrorCode } from '../../server'
import { ServerServiceType } from '../../server/arenaServer/serverServiceType'
import { UserGroup } from '../../repository/userGroup'
import { UserGroupService } from '../../service'

export const getUserGroupService = (): UserGroupService =>
ServiceRegistry.getInstance().getService(ServerServiceType.userGroup) as UserGroupService

export const getSurveyUuid = async (surveyId: number): Promise<string> => {
const surveyService = ServiceRegistry.getInstance().getService(ServiceType.survey) as SurveyService
const survey = await surveyService.get({ surveyId })
return survey.uuid
}

/**
* Loads the user group by uuid and ensures it belongs to the survey identified by surveyId.
* This prevents a survey admin from acting on a user group belonging to a different survey
* just by guessing/knowing its uuid.
*/
export const getUserGroupBelongingToSurvey = async (params: {
surveyId: number
groupUuid: string
}): Promise<{ surveyUuid: string; userGroup: UserGroup }> => {
const { surveyId, groupUuid } = params

const surveyUuid = await getSurveyUuid(surveyId)

const service = getUserGroupService()
const userGroup = await service.getByUuid({ uuid: groupUuid })

if (userGroup?.surveyUuid !== surveyUuid) {
throw new ServerError('appErrors.userGroup.notFound', { groupUuid }, ServerErrorCode.NOT_FOUND)
}
return { surveyUuid, userGroup }
}
54 changes: 54 additions & 0 deletions src/api/userGroup/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Express } from 'express'

import { ExpressInitializer } from '../../server'
import { Requests } from '../../utils'

import { ApiEndpoint } from '../endpoint'
import { ApiAuthMiddleware } from '../middleware'
import { getSurveyUuid, getUserGroupBelongingToSurvey, getUserGroupService } from './common'

const { requireUserGroupManagePermission } = ApiAuthMiddleware

export const UserGroupCreate: ExpressInitializer = {
init: (express: Express): void => {
express.post(
ApiEndpoint.userGroup.userGroups(':surveyId'),
requireUserGroupManagePermission,
async (req, res, next) => {
try {
const { surveyId } = Requests.getParams(req)
const item = req.body

const surveyUuid = await getSurveyUuid(surveyId)

const service = getUserGroupService()
const userGroupInserted = await service.insert({ surveyUuid, item })

res.json(userGroupInserted)
} catch (error) {
next(error)
}
}
)

express.post(
ApiEndpoint.userGroup.userGroupMembers(':surveyId', ':groupUuid'),
requireUserGroupManagePermission,
async (req, res, next) => {
try {
const { surveyId, groupUuid } = Requests.getParams(req)
const { userUuid } = req.body

await getUserGroupBelongingToSurvey({ surveyId, groupUuid })

const service = getUserGroupService()
await service.addMember({ groupUuid, userUuid })

res.json({ groupUuid, userUuid })
} catch (error) {
next(error)
}
}
)
},
}
52 changes: 52 additions & 0 deletions src/api/userGroup/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Express } from 'express'

import { ExpressInitializer } from '../../server'
import { Requests } from '../../utils'

import { ApiEndpoint } from '../endpoint'
import { ApiAuthMiddleware } from '../middleware'
import { getUserGroupBelongingToSurvey, getUserGroupService } from './common'

const { requireUserGroupManagePermission } = ApiAuthMiddleware

export const UserGroupDelete: ExpressInitializer = {
init: (express: Express): void => {
express.delete(
ApiEndpoint.userGroup.userGroup(':surveyId', ':groupUuid'),
requireUserGroupManagePermission,
async (req, res, next) => {
try {
const { surveyId, groupUuid } = Requests.getParams(req)

await getUserGroupBelongingToSurvey({ surveyId, groupUuid })

const service = getUserGroupService()
const userGroupDeleted = await service.deleteItem({ uuid: groupUuid })

res.json(userGroupDeleted)
} catch (error) {
next(error)
}
}
)

express.delete(
ApiEndpoint.userGroup.userGroupMember(':surveyId', ':groupUuid', ':userUuid'),
requireUserGroupManagePermission,
async (req, res, next) => {
try {
const { surveyId, groupUuid, userUuid } = Requests.getParams(req)

await getUserGroupBelongingToSurvey({ surveyId, groupUuid })

const service = getUserGroupService()
await service.removeMember({ groupUuid, userUuid })

res.json({ groupUuid, userUuid })
} catch (error) {
next(error)
}
}
)
},
}
16 changes: 16 additions & 0 deletions src/api/userGroup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Express } from 'express'

import { ExpressInitializer } from '../../server'
import { UserGroupCreate } from './create'
import { UserGroupRead } from './read'
import { UserGroupUpdate } from './update'
import { UserGroupDelete } from './delete'

export const UserGroupApi: ExpressInitializer = {
init: (express: Express): void => {
UserGroupCreate.init(express)
UserGroupRead.init(express)
UserGroupUpdate.init(express)
UserGroupDelete.init(express)
},
}
81 changes: 81 additions & 0 deletions src/api/userGroup/read.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Express } from 'express'

import { ExpressInitializer } from '../../server'
import { Requests } from '../../utils'

import { ApiEndpoint } from '../endpoint'
import { ApiAuthMiddleware } from '../middleware'
import { getSurveyUuid, getUserGroupBelongingToSurvey, getUserGroupService } from './common'

const { requireSurveyViewPermission } = ApiAuthMiddleware

export const UserGroupRead: ExpressInitializer = {
init: (express: Express): void => {
express.get(
ApiEndpoint.userGroup.userGroupsCount(':surveyId'),
requireSurveyViewPermission,
async (req, res, next) => {
try {
const { surveyId } = Requests.getParams(req)
const surveyUuid = await getSurveyUuid(surveyId)

const service = getUserGroupService()
const count = await service.count({ surveyUuid })

res.json({ count })
} catch (error) {
next(error)
}
}
)

express.get(ApiEndpoint.userGroup.userGroups(':surveyId'), requireSurveyViewPermission, async (req, res, next) => {
try {
const { surveyId } = Requests.getParams(req)
const surveyUuid = await getSurveyUuid(surveyId)

const service = getUserGroupService()
const list = await service.getAll({ surveyUuid })

res.json({ list })
} catch (error) {
next(error)
}
})

express.get(
ApiEndpoint.userGroup.userGroup(':surveyId', ':groupUuid'),
requireSurveyViewPermission,
async (req, res, next) => {
try {
const { surveyId, groupUuid } = Requests.getParams(req)

const { userGroup } = await getUserGroupBelongingToSurvey({ surveyId, groupUuid })

res.json(userGroup)
} catch (error) {
next(error)
}
}
)

express.get(
ApiEndpoint.userGroup.userGroupMembers(':surveyId', ':groupUuid'),
requireSurveyViewPermission,
async (req, res, next) => {
try {
const { surveyId, groupUuid } = Requests.getParams(req)

await getUserGroupBelongingToSurvey({ surveyId, groupUuid })

const service = getUserGroupService()
const list = await service.getMembers({ groupUuid })

res.json({ list })
} catch (error) {
next(error)
}
}
)
},
}
34 changes: 34 additions & 0 deletions src/api/userGroup/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Express } from 'express'

import { ExpressInitializer } from '../../server'
import { Requests } from '../../utils'

import { ApiEndpoint } from '../endpoint'
import { ApiAuthMiddleware } from '../middleware'
import { getUserGroupBelongingToSurvey, getUserGroupService } from './common'

const { requireUserGroupManagePermission } = ApiAuthMiddleware

export const UserGroupUpdate: ExpressInitializer = {
init: (express: Express): void => {
express.put(
ApiEndpoint.userGroup.userGroup(':surveyId', ':groupUuid'),
requireUserGroupManagePermission,
async (req, res, next) => {
try {
const { surveyId, groupUuid } = Requests.getParams(req)
const { props } = req.body

await getUserGroupBelongingToSurvey({ surveyId, groupUuid })

const service = getUserGroupService()
const userGroupUpdated = await service.update({ uuid: groupUuid, props })

res.json(userGroupUpdated)
} catch (error) {
next(error)
}
}
)
},
}
Loading
Loading