Skip to content
Open
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
51 changes: 50 additions & 1 deletion apps/backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,24 @@ import {
ConflictException,
Injectable,
InternalServerErrorException,
Logger,
} from '@nestjs/common';
import {
CognitoIdentityProviderClient,
AdminCreateUserCommand,
AdminAddUserToGroupCommand,
AdminRemoveUserFromGroupCommand,
} from '@aws-sdk/client-cognito-identity-provider';

import CognitoAuthConfig from './aws-exports';
import { SignUpDto } from './dtos/sign-up.dto';
import { createHmac } from 'crypto';
import { Role } from '../users/types';
import { validateEnv } from '../utils/validation.utils';

@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
private readonly providerClient: CognitoIdentityProviderClient;
private readonly clientSecret: string;

Expand Down Expand Up @@ -44,7 +49,8 @@ export class AuthService {
firstName,
lastName,
email,
}: Omit<SignUpDto, 'password' | 'phone'>): Promise<string> {
role,
}: Omit<SignUpDto, 'password' | 'phone'> & { role: Role }): Promise<string> {
const createUserCommand = new AdminCreateUserCommand({
UserPoolId: CognitoAuthConfig.userPoolId,
Username: email,
Expand All @@ -61,6 +67,10 @@ export class AuthService {
const sub = response.User?.Attributes?.find(
(attr) => attr.Name === 'sub',
)?.Value;

// Add user to the appropriate Cognito group based on their role
await this.addUserToGroup(email, role);

return sub ?? '';
} catch (error) {
if (error instanceof Error && error.name == 'UsernameExistsException') {
Expand All @@ -70,4 +80,43 @@ export class AuthService {
}
}
}

async addUserToGroup(username: string, groupName: string): Promise<void> {
Comment thread
dburkhart07 marked this conversation as resolved.
const command = new AdminAddUserToGroupCommand({
UserPoolId: CognitoAuthConfig.userPoolId,
Username: username,
GroupName: groupName,
});

try {
await this.providerClient.send(command);
} catch (error) {
this.logger.error(
`Failed to add user ${username} to group ${groupName}`,
error,
);
throw new InternalServerErrorException(
`Failed to add user to group ${groupName}`,
Comment thread
jxuistrying marked this conversation as resolved.
);
}
}

async removeUserFromGroup(
username: string,
groupName: string,
): Promise<void> {
const command = new AdminRemoveUserFromGroupCommand({
UserPoolId: CognitoAuthConfig.userPoolId,
Username: username,
GroupName: groupName,
});

try {
await this.providerClient.send(command);
} catch (error) {
throw new InternalServerErrorException(
`Failed to remove user from group ${groupName}`,
);
}
}
}
5 changes: 5 additions & 0 deletions apps/backend/src/foodRequests/request.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { FoodRequest } from './request.entity';
import { RequestsService } from './request.service';
import { Pantry } from '../pantries/pantries.entity';
Expand Down Expand Up @@ -66,6 +67,10 @@ describe('RequestsService', () => {
provide: EmailsService,
useValue: mockEmailsService,
},
{
provide: DataSource,
useValue: testDataSource,
},
],
}).compile();

Expand Down
6 changes: 5 additions & 1 deletion apps/backend/src/pantries/pantries.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PantriesService } from './pantries.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { In } from 'typeorm';
import { DataSource, In } from 'typeorm';
import { Pantry } from './pantries.entity';
import {
BadRequestException,
Expand Down Expand Up @@ -145,6 +145,10 @@ describe('PantriesService', () => {
provide: getRepositoryToken(FoodManufacturer),
useValue: testDataSource.getRepository(FoodManufacturer),
},
{
provide: DataSource,
useValue: testDataSource,
},
],
}).compile();

Expand Down
35 changes: 34 additions & 1 deletion apps/backend/src/users/users.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { userSchemaDto } from './dtos/userSchema.dto';
import { Test, TestingModule } from '@nestjs/testing';
import { mock } from 'jest-mock-extended';
import { UpdateUserInfoDto } from './dtos/update-user-info.dto';
import { BadRequestException } from '@nestjs/common';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { AuthenticatedRequest } from '../auth/authenticated-request';

const mockUserService = mock<UsersService>();
Expand All @@ -31,6 +31,7 @@ describe('UsersController', () => {
mockUserService.create.mockReset();
mockUserService.getUserDashboardStats.mockReset();
mockUserService.getRecentPendingApplications.mockReset();
mockUserService.promoteVolunteerToAdmin.mockReset();

const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
Expand Down Expand Up @@ -211,4 +212,36 @@ describe('UsersController', () => {
expect(result).toEqual([]);
});
});

describe('PATCH /:id/promote-volunteer', () => {
it('should promote volunteer to admin successfully', async () => {
mockUserService.promoteVolunteerToAdmin.mockResolvedValueOnce(undefined);

await controller.promoteToAdmin(1);

expect(mockUserService.promoteVolunteerToAdmin).toHaveBeenCalledWith(1);
});

it('should throw NotFoundException from service when user not found', async () => {
Comment thread
jxuistrying marked this conversation as resolved.
mockUserService.promoteVolunteerToAdmin.mockRejectedValueOnce(
new NotFoundException('User 999 not found'),
);

await expect(controller.promoteToAdmin(999)).rejects.toThrow(
new NotFoundException('User 999 not found'),
);
});

it('should throw BadRequestException from service when user is not a volunteer', async () => {
mockUserService.promoteVolunteerToAdmin.mockRejectedValueOnce(
new BadRequestException(
'User 1 is not a volunteer. Current role: admin',
),
);

await expect(controller.promoteToAdmin(1)).rejects.toThrow(
BadRequestException,
);
});
});
});
6 changes: 6 additions & 0 deletions apps/backend/src/users/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ export class UsersController {
return this.usersService.update(id, dto);
}

@Patch('/:id/promote-volunteer')
@Roles(Role.ADMIN)
async promoteToAdmin(@Param('id', ParseIntPipe) id: number): Promise<void> {
await this.usersService.promoteVolunteerToAdmin(id);
}

// Keeping these two as functionality seems useful
@Post('/')
async createUser(@Body() createUserDto: userSchemaDto): Promise<User> {
Expand Down
Loading
Loading