diff --git a/desci-server/src/controllers/admin/users.ts b/desci-server/src/controllers/admin/users.ts new file mode 100644 index 000000000..0dd9e26a2 --- /dev/null +++ b/desci-server/src/controllers/admin/users.ts @@ -0,0 +1,116 @@ +import { User } from '@prisma/client'; +import { Request, Response } from 'express'; +import z from 'zod'; + +import { prisma } from '../../client.js'; +import { NotFoundError } from '../../core/ApiError.js'; +import { SuccessResponse } from '../../core/ApiResponse.js'; +import { emailRegex } from '../../core/helper.js'; +import { logger as parentLogger } from '../../logger.js'; +import { formatOrcidString, orcidRegex } from '../../utils.js'; + +export type SearchProfilesRequest = Request & { + user: User; // added by auth middleware +}; + +export type SearchProfilesResBody = + | { + profiles: UserProfile[]; + } + | { + error: string; + }; + +export type UserProfile = { name: string; id: number; orcid?: string; organisations?: string[] }; + +const userSearchSchema = z.object({ + query: z.object({ + page: z.coerce.number().optional().default(0), + cursor: z.coerce.number().optional().default(1), + limit: z.coerce.number().optional().default(20), + search: z.string().optional().default(''), + }), +}); + +export const searchUserProfiles = async (req: SearchProfilesRequest, res: Response) => { + // debugger; + const user = req.user; + const { name } = req.query; + let { orcid } = req.query; + const logger = parentLogger.child({ + module: 'Users::searchProfiles', + body: req.body, + userId: user.id, + name, + orcid, + queryType: orcid ? 'orcid' : 'name', + }); + + const { + query: { page, limit, cursor, search }, + } = await userSearchSchema.parseAsync(req); + + logger.trace({ page, cursor, limit, search }); + + const count = await prisma.user.count({}); + const users = await prisma.user.findMany({ cursor: { id: cursor }, skip: page * limit, take: limit }); + + new SuccessResponse({ cursor: users[users.length - 1].id, page, count, data: users }).send(res); + return; + if (orcid && orcidRegex.test(orcid) === false) + throw new NotFoundError('Invalid orcid id, orcid must follow either 123456780000 or 1234-4567-8000-0000 format.'); + // return res + // .status(400) + // .json({ error: 'Invalid orcid id, orcid must follow either 123456780000 or 1234-4567-8000-0000 format.' }); + + if (orcid) orcid = formatOrcidString(orcid); // Ensure hyphenated + + if (name?.toString().length < 2 && !orcid) throw new NotFoundError('Name query must be at least 2 characters'); + // return res.status(400).json({ error: 'Name query must be at least 2 characters' }); + + // try { + const isEmail = emailRegex.test(name); + let emailMatches = []; + if (isEmail) { + emailMatches = await prisma.user.findMany({ + where: { + email: { + mode: 'insensitive', + equals: name as string, + }, + }, + include: { userOrganizations: { include: { organization: { select: { name: true } } } } }, + }); + } + + const profiles = orcid + ? await prisma.user.findMany({ + where: { orcid: orcid }, + include: { userOrganizations: { include: { organization: { select: { name: true } } } } }, + }) + : await prisma.user.findMany({ + where: { name: { contains: name as string, mode: 'insensitive', not: null } }, + include: { userOrganizations: { include: { organization: { select: { name: true } } } } }, + }); + + // logger.info({ profiles }, 'PROFILES'); + if (profiles || emailMatches) { + const profilesReturn: UserProfile[] = [...emailMatches, ...profiles].map((profile) => ({ + name: profile.name, + id: profile.id, + organisations: profile.userOrganizations.map((org) => org.organization.name), + ...(profile.orcid && { orcid: profile.orcid }), + })); + // return res.status(200).json({ profiles: profilesReturn }); + new SuccessResponse({ profiles: profilesReturn }); + } else { + new SuccessResponse({ profiles: [] }); + } + // } + // catch (e) { + // logger.error({ e }, 'Failed to search for profiles'); + // return res.status(500).json({ error: 'Search failed' }); + // } + + // return res.status(500).json({ error: 'Something went wrong' }); +}; diff --git a/desci-server/src/middleware/ensureAdmin.ts b/desci-server/src/middleware/ensureAdmin.ts index 66fa665fe..c76eecbd2 100644 --- a/desci-server/src/middleware/ensureAdmin.ts +++ b/desci-server/src/middleware/ensureAdmin.ts @@ -1,5 +1,7 @@ import { Request, Response, NextFunction } from 'express'; +import { RequestWithUser } from './authorisation.js'; + // export const ensureUser = async (req: Request, res: Response, next: NextFunction) => { // const userId = req.session.userId; // console.log('REQ SESS', req.session, req.cookies); @@ -20,12 +22,26 @@ import { Request, Response, NextFunction } from 'express'; // }; const disableList = ['noreply+test@desci.com']; -export const ensureAdmin = async (req: Request, res: Response, next: NextFunction) => { - const user = (req as any).user; +export const ensureAdmin = async (req: RequestWithUser, res: Response, next: NextFunction) => { + const user = req.user; + + if (user.email.indexOf('@desci.com') > -1 && disableList.indexOf(user.email) < 0) { + next(); + return; + } + + res.sendStatus(401); +}; + +export const ensureUserIsAdmin = async (req: RequestWithUser, res: Response, next: NextFunction) => { + const user = req.user; if (user.email.indexOf('@desci.com') > -1 && disableList.indexOf(user.email) < 0) { next(); return; + } else if (user.isAdmin) { + next(); + return; } res.sendStatus(401); diff --git a/desci-server/src/middleware/permissions.ts b/desci-server/src/middleware/permissions.ts index c4480708e..6c6f691f4 100644 --- a/desci-server/src/middleware/permissions.ts +++ b/desci-server/src/middleware/permissions.ts @@ -22,7 +22,7 @@ export const ensureUser = async (req: ExpressRequest, res: Response, next: NextF const retrievedUser = authTokenRetrieval || apiKeyRetrieval; if (!retrievedUser) { - // logger.trace({ token, apiKey }, 'ENSURE USER'); + logger.trace({ token, apiKey }, 'ENSURE USER'); res.status(401).send({ ok: false, message: 'Unauthorized' }); } else { (req as any).user = retrievedUser; diff --git a/desci-server/src/routes/v1/admin/index.ts b/desci-server/src/routes/v1/admin/index.ts index a3bb6733d..9c0e30481 100644 --- a/desci-server/src/routes/v1/admin/index.ts +++ b/desci-server/src/routes/v1/admin/index.ts @@ -5,17 +5,18 @@ import { listAttestations } from '../../../controllers/admin/communities/index.j import { debugAllNodesHandler, debugNodeHandler } from '../../../controllers/admin/debug.js'; import { listDoiRecords, mintDoi } from '../../../controllers/admin/doi/index.js'; import { resumePublish } from '../../../controllers/admin/publish/resumePublish.js'; -import { ensureAdmin } from '../../../middleware/ensureAdmin.js'; +import { ensureAdmin, ensureUserIsAdmin } from '../../../middleware/ensureAdmin.js'; import { ensureUser } from '../../../middleware/permissions.js'; import { asyncHandler } from '../../../utils/asyncHandler.js'; import communities from './communities/index.js'; import doiRouter from './doi.js'; +import usersRouter from './users/index.js'; const router = Router(); -router.get('/analytics', [ensureUser, ensureAdmin], getAnalytics); -router.get('/analytics/csv', [ensureUser, ensureAdmin], createCsv); +router.get('/analytics', [ensureUser, ensureUserIsAdmin], getAnalytics); +router.get('/analytics/csv', [ensureUser, ensureUserIsAdmin], createCsv); router.get('/doi/list', [ensureUser, ensureAdmin], listDoiRecords); router.post('/mint/:uuid', [ensureUser, ensureAdmin], asyncHandler(mintDoi)); @@ -27,8 +28,10 @@ router.post('/resumePublish', [ensureUser, ensureAdmin], asyncHandler(resumePubl router.use('/communities', [ensureUser, ensureAdmin], communities); router.get('/attestations', [ensureUser, ensureAdmin], asyncHandler(listAttestations)); -// router.use('/users', [ensureUser, ensureAdmin], usersRouter); +router.use('/users', usersRouter); +// router.use('/nodes', [ensureUser, ensureAdmin], usersRouter); router.use('/doi', doiRouter); +// router.use('/users', usersRouter); export default router; diff --git a/desci-server/src/routes/v1/admin/users/index.ts b/desci-server/src/routes/v1/admin/users/index.ts index 9c26843fa..f16ef8451 100644 --- a/desci-server/src/routes/v1/admin/users/index.ts +++ b/desci-server/src/routes/v1/admin/users/index.ts @@ -1,6 +1,9 @@ import { NextFunction, Response, Router } from 'express'; +import { Request } from 'express'; -import { SuccessResponse } from '../../../../core/ApiResponse.js'; +import { prisma } from '../../../../client.js'; +import { searchUserProfiles } from '../../../../controllers/admin/users.js'; +import { SuccessMessageResponse, SuccessResponse } from '../../../../core/ApiResponse.js'; import { ensureAdmin } from '../../../../middleware/ensureAdmin.js'; import { ensureUser } from '../../../../middleware/permissions.js'; import { asyncHandler } from '../../../../utils/asyncHandler.js'; @@ -8,12 +11,16 @@ import { asyncHandler } from '../../../../utils/asyncHandler.js'; // const logger = parentLogger.child({ module: 'Admin/communities' }); const router = Router(); -router.get( - '/search', +router.get('/search', [ensureUser, ensureAdmin], asyncHandler(searchUserProfiles)); + +router.patch( + '/:userId/toggleRole', [ensureUser, ensureAdmin], - asyncHandler(async (_req: Request, res: Response, _next: NextFunction) => { - // - new SuccessResponse([]).send(res); + asyncHandler(async (req: Request<{ userId: number }, any>, res: Response, _next: NextFunction) => { + const userId = req.params.userId; + const user = await prisma.user.findFirst({ where: { id: parseInt(userId.toString()) }, select: { isAdmin: true } }); + await prisma.user.update({ where: { id: parseInt(userId.toString()) }, data: { isAdmin: !user.isAdmin } }); + new SuccessMessageResponse().send(res); }), ); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 2b07ab93c..fa8993cc3 100755 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -99,10 +99,12 @@ services: condition: service_healthy db_postgres: condition: service_healthy + env_file: + - .env environment: # https://github.com/graphprotocol/graph-node/blob/master/docs/environment-variables.md postgres_host: db_postgres - # postgres_port: 5433 + postgres_port: ${PG_PORT:-5432} postgres_user: walter postgres_pass: white postgres_db: postgres