Skip to content
Draft
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
13 changes: 12 additions & 1 deletion netlify/functions-src/functions/data/mentors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { UpsertResult } from './types';
import { upsertEntityByCondition } from './utils';
import { getCollection } from '../utils/db';
import { ObjectId, type Filter, type MatchKeysAndValues } from 'mongodb';
import type { User } from '../common/interfaces/user.interface';
import { Role, type User } from '../common/interfaces/user.interface';

export const upsertApplication = async (application: MatchKeysAndValues<Application>): UpsertResult<Application> => {
const filter: Filter<Application> = {
Expand Down Expand Up @@ -84,4 +84,15 @@ export const getApplications = async (status?: string): Promise<Application[]> =
];

return collection.aggregate<Application>(pipeline).toArray();
}

export const getInactiveMentors = async (): Promise<User[]> => {
const collection = getCollection<User>('users');
// Matches mentors with available: false OR missing available field (never activated)
return collection
.find(
{ roles: Role.MENTOR, available: { $ne: true } },
{ projection: { _id: 1, name: 1, email: 1, avatar: 1, createdAt: 1 } }
)
.toArray();
}
2 changes: 2 additions & 0 deletions netlify/functions-src/functions/mentors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { handler as getMentorsHanler } from './modules/mentors/get';
import { handler as getApplicationsHandler } from './modules/mentors/applications/get';
import { handler as upsertApplicationHandler } from './modules/mentors/applications/post';
import { handler as updateApplicationHandler } from './modules/mentors/applications/put';
import { handler as getPendingActivationHandler } from './modules/mentors/pendingActivation';
import { Role } from './common/interfaces/user.interface';

export const handler: ApiHandler = withDB(
Expand All @@ -14,6 +15,7 @@ export const handler: ApiHandler = withDB(
['/applications', 'GET', withAuth(getApplicationsHandler, { includeFullUser: true, role: Role.ADMIN })],
['/applications/:applicationId', 'PUT', withAuth(updateApplicationHandler, { includeFullUser: true, role: Role.ADMIN })],
['/applications', 'POST', withAuth(upsertApplicationHandler, { includeFullUser: true, })],
['/pending-activation', 'GET', withAuth(getPendingActivationHandler, { role: Role.ADMIN })],
// TODO: find out if needed
// ['/:userId/applications', 'GET', withAuth(getUserApplicationsHandler, { returnUser: true })],
])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { handler } from './pendingActivation';
import { getCollection } from '../../utils/db';
import { HandlerContext, HandlerEvent } from '@netlify/functions';
import { AuthContext, AuthUser } from '../../types';

jest.mock('../../utils/db');

describe('pendingActivation handler', () => {
const collectionMock = {
find: jest.fn(),
};

const baseEvent: HandlerEvent = {
body: null,
headers: {},
multiValueHeaders: {},
httpMethod: 'GET',
isBase64Encoded: false,
path: '',
queryStringParameters: null,
multiValueQueryStringParameters: null,
rawQuery: '',
rawUrl: '',
};

const baseContext: HandlerContext = {
callbackWaitsForEmptyEventLoop: true,
functionName: 'mentors',
functionVersion: '1',
invokedFunctionArn: '',
memoryLimitInMB: '128',
awsRequestId: '',
logGroupName: '',
logStreamName: '',
getRemainingTimeInMillis: () => 0,
done: () => {},
fail: () => {},
succeed: () => {},
};

const adminContext: AuthContext<AuthUser> = {
...baseContext,
user: { auth0Id: 'admin-auth0-id' },
};

beforeEach(() => {
jest.clearAllMocks();
const mocked = getCollection as unknown as jest.Mock;
mocked.mockReturnValue(collectionMock);
});

it('should return mentors with Mentor role who are not active', async () => {
const mockMentors = [
{ _id: 'id1', name: 'Alice', email: 'alice@example.com', createdAt: '2024-01-01T00:00:00.000Z', roles: ['Mentor'], available: false },
{ _id: 'id2', name: 'Bob', email: 'bob@example.com', createdAt: '2024-02-01T00:00:00.000Z', roles: ['Mentor'] },
];

collectionMock.find.mockReturnValue({ toArray: jest.fn().mockResolvedValue(mockMentors) });

const response = await handler(baseEvent, adminContext);
const body = JSON.parse(response.body || '{}');

expect(response.statusCode).toBe(200);
expect(body.success).toBe(true);
expect(body.data).toHaveLength(2);
});

it('should return an empty list when all mentors are active', async () => {
collectionMock.find.mockReturnValue({ toArray: jest.fn().mockResolvedValue([]) });

const response = await handler(baseEvent, adminContext);
const body = JSON.parse(response.body || '{}');

expect(response.statusCode).toBe(200);
expect(body.success).toBe(true);
expect(body.data).toHaveLength(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getInactiveMentors } from '../../data/mentors';
import type { ApiHandler } from '../../types';
import { success } from '../../utils/response';

export const handler: ApiHandler = async () => {
const mentors = await getInactiveMentors();
return success({ data: mentors });
};
41 changes: 41 additions & 0 deletions src/Me/Routes/Admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import {
freezeMentor,
getAllMentorshipRequests,
getInactiveMentors,
getUserRecords,
sendMentorNotActive,
sendStaledRequestEmail,
Expand Down Expand Up @@ -138,6 +139,7 @@ const Admin = () => {
const [mentorshipRequests, setMentorshipRequests] = useState<
MentorshipRequest[]
>([]);
const [inactiveMentors, setInactiveMentors] = useState<User[]>([]);
const api = useApi()

const filteredMentorshipRequests = useMemo(() => {
Expand Down Expand Up @@ -166,6 +168,14 @@ const Admin = () => {
});
}, [showDaysAgo, api]);

useEffect(() => {
getInactiveMentors(api).then((response) => {
if (response?.success) {
setInactiveMentors(response.data);
}
});
}, [api]);

useEffect(() => {
setName(user?.name || '');
}, [user]);
Expand Down Expand Up @@ -269,6 +279,37 @@ const Admin = () => {
mentorships={filteredMentorshipRequests}
/>
)}
<Card className='wide'>
<h3>Pending Activation ({inactiveMentors.length})</h3>
{inactiveMentors.length ? (
<table>
<thead>
<tr>
<td>Name</td>
<td>Email</td>
<td>Joined</td>
<td>Profile</td>
</tr>
</thead>
<tbody>
{inactiveMentors.map(({ _id, name: mentorName, email, createdAt }) => (
<tr key={_id}>
<td>{mentorName}</td>
<td>{email}</td>
<td>{formatTimeAgo(new Date(createdAt))}</td>
<td>
<a target="_blank" rel="noreferrer" href={`/u/${_id}`} aria-label={`View profile for ${mentorName}`}>
🔗
</a>
</td>
</tr>
))}
</tbody>
</table>
) : (
<>No pending activation mentors</>
)}
</Card>
<Card className='wide'>
<Filters>
<FormField>
Expand Down
6 changes: 5 additions & 1 deletion src/api/admin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ApiService, { paths } from '.';
import { MentorshipRequest, UserRecord } from '../types/models';
import { MentorshipRequest, User, UserRecord } from '../types/models';

export function getAllMentorshipRequests(apiService: ApiService, numMonthAgo: number = 1) {
const monthAgo = new Date();
Expand Down Expand Up @@ -34,4 +34,8 @@ export async function freezeMentor(apiService: any, mentorId: string) {

export function getUserRecords(apiService: ApiService, userId: string) {
return apiService.makeApiCall<UserRecord[]>(`${paths.USERS}/${userId}/records`);
}

export function getInactiveMentors(apiService: ApiService) {
return apiService.makeApiCall<User[]>(`${paths.MENTORS}/pending-activation`);
}
2 changes: 1 addition & 1 deletion tsconfig.tsbuildinfo

Large diffs are not rendered by default.