diff --git a/desci-server/src/controllers/admin/users.ts b/desci-server/src/controllers/admin/users.ts new file mode 100644 index 00000000..0dd9e26a --- /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/permissions.ts b/desci-server/src/middleware/permissions.ts index c4480708..6c6f691f 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 78d36dbd..9c0e3048 100644 --- a/desci-server/src/routes/v1/admin/index.ts +++ b/desci-server/src/routes/v1/admin/index.ts @@ -28,7 +28,7 @@ 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); diff --git a/desci-server/src/routes/v1/admin/users/index.ts b/desci-server/src/routes/v1/admin/users/index.ts index 16554ff1..f16ef845 100644 --- a/desci-server/src/routes/v1/admin/users/index.ts +++ b/desci-server/src/routes/v1/admin/users/index.ts @@ -1,8 +1,8 @@ import { NextFunction, Response, Router } from 'express'; import { Request } from 'express'; -import z from 'zod'; 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'; @@ -11,38 +11,17 @@ import { asyncHandler } from '../../../../utils/asyncHandler.js'; // const logger = parentLogger.child({ module: 'Admin/communities' }); const router = Router(); -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), - }), -}); +router.get('/search', [ensureUser, ensureAdmin], asyncHandler(searchUserProfiles)); -router.get( - '/search', +router.patch( + '/:userId/toggleRole', [ensureUser, ensureAdmin], - asyncHandler(async (req: Request, res: Response, _next: NextFunction) => { - const { - query: { page, limit, cursor }, - } = await userSearchSchema.parseAsync(req); - 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, users }).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); }), ); -router.get( - '/toggleAdmin', - [ensureUser, ensureAdmin], - asyncHandler( - async (req: Request, res: Response, _next: NextFunction) => { - const userId = req.body.userId; - await prisma.user.update({ where: { id: userId }, data: { isAdmin: req.body.isAdmin } }); - new SuccessMessageResponse().send(res); - }, - ), -); - export default router; diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 2b07ab93..fa8993cc 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