Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin privileges and User Admin API #756

Merged
merged 3 commits into from
Jan 15, 2025
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
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