Skip to content

Commit

Permalink
feat: users admin functionlity
Browse files Browse the repository at this point in the history
  • Loading branch information
shadrach-tayo committed Jan 15, 2025
1 parent fc8c67e commit aa8d269
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 33 deletions.
116 changes: 116 additions & 0 deletions desci-server/src/controllers/admin/users.ts
Original file line number Diff line number Diff line change
@@ -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<never, never, never, { name?: string; orcid?: string }> & {
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<SearchProfilesResBody>) => {
// 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' });
};
2 changes: 1 addition & 1 deletion desci-server/src/middleware/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion desci-server/src/routes/v1/admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
39 changes: 9 additions & 30 deletions desci-server/src/routes/v1/admin/users/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<any, any, { userId: number; isAdmin: boolean }>, 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;
4 changes: 3 additions & 1 deletion docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit aa8d269

Please sign in to comment.