Skip to content

Commit

Permalink
Merge pull request #756 from desci-labs/tay/admin-privileges
Browse files Browse the repository at this point in the history
Admin privileges and User Admin API
  • Loading branch information
shadrach-tayo authored Jan 15, 2025
2 parents 177c07b + aa8d269 commit 6ededac
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 14 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' });
};
20 changes: 18 additions & 2 deletions desci-server/src/middleware/ensureAdmin.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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);
Expand Down
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
11 changes: 7 additions & 4 deletions desci-server/src/routes/v1/admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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;
19 changes: 13 additions & 6 deletions desci-server/src/routes/v1/admin/users/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
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';

// 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);
}),
);

Expand Down
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 6ededac

Please sign in to comment.