Skip to content

Commit

Permalink
Merge branch 'dev' into fix/user-creation
Browse files Browse the repository at this point in the history
  • Loading branch information
Khaybee authored Feb 28, 2025
2 parents 5e34c7d + 30a1d12 commit 0458f8a
Show file tree
Hide file tree
Showing 13 changed files with 148 additions and 19 deletions.
22 changes: 22 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,11 @@ dist
# User specific ignores
todo.txt
.vscode/

# Docker compose
docker-compose.yml
data/
<<<<<<< HEAD
data/
docker-compose.yml
package-lock.json
Expand All @@ -415,3 +420,20 @@ data/
.dev.env


=======

# Docker compose
docker-compose.yml
data/
>>>>>>> d080450 (chore: created a docker-compose.yml file and updated the gitignore)

data/
docker-compose.yml
package-lock.json
.dev.env


package-lock.json
docker-compose.yml
data/
.dev.env
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@
"source.fixAll": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
"editor.defaultFormatter": "vscode.typescript-language-features"
}
}
10 changes: 5 additions & 5 deletions src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ export default class AuthenticationService {
};

const newOrganisation = await this.organisationService.create(createOrganisationPayload);
console.log('newOrganisation', newOrganisation);

const userOrganisations = await this.organisationService.getAllUserOrganisations(user.id);
console.log('userOrganisations', userOrganisations);

const isSuperAdmin = userOrganisations.map(instance => instance.user_role).includes('super-admin');
const userOranisations = await this.organisationService.getAllUserOrganisations(user.id, 1, 10);


const token = (await this.otpService.createOtp(user.id, manager)).token;

Expand Down Expand Up @@ -193,7 +193,7 @@ export default class AuthenticationService {
if (!isMatch) {
throw new CustomHttpException(SYS_MSG.INVALID_CREDENTIALS, HttpStatus.UNAUTHORIZED);
}
const userOranisations = await this.organisationService.getAllUserOrganisations(user.id);
const userOranisations = await this.organisationService.getAllUserOrganisations(user.id, 1, 10);
const access_token = this.jwtService.sign({ id: user.id, sub: user.id });
const isSuperAdmin = userOranisations.map(instance => instance.user_role).includes('super-admin');
const responsePayload = {
Expand Down Expand Up @@ -347,7 +347,7 @@ export default class AuthenticationService {
return await this.createUserGoogle(userCreationPayload);
}

const userOranisations = await this.organisationService.getAllUserOrganisations(userExists.id);
const userOranisations = await this.organisationService.getAllUserOrganisations(userExists.id, 1, 10);
const isSuperAdmin = userOranisations.map(instance => instance.user_role).includes('super-admin');
const accessToken = this.jwtService.sign({
sub: userExists.id,
Expand Down Expand Up @@ -401,7 +401,7 @@ export default class AuthenticationService {
};
await this.organisationService.create(createOrganisationPayload);

const userOranisations = await this.organisationService.getAllUserOrganisations(newUser.id);
const userOranisations = await this.organisationService.getAllUserOrganisations(newUser.id, 1, 10);
const isSuperAdmin = userOranisations.map(instance => instance.user_role).includes('super-admin');

const accessToken = this.jwtService.sign({
Expand Down
3 changes: 2 additions & 1 deletion src/modules/blogs/blogs.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { BlogService } from './blogs.service';
import { SuperAdminGuard } from '@guards/super-admin.guard';
import { AuthGuard } from '@guards/auth.guard';
import { BlogResponseDto } from './dtos/blog-response.dto';
import { CreateBlogDto } from './dtos/create-blog.dto';
import { UpdateBlogResponseDto } from './dtos/update-blog-response.dto';
Expand All @@ -30,7 +31,7 @@ export class BlogController {

@ApiBearerAuth()
@Post()
@UseGuards(SuperAdminGuard)
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Create a new blog' })
@ApiResponse({ status: 201, description: 'The blog has been successfully created.', type: BlogResponseDto })
@ApiResponse({ status: 403, description: 'Forbidden.' })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail } from 'class-validator';

export class UnsubscribeNewsletterDto {
@ApiProperty({ description: 'Email of the user to unsubscribe' })
@IsEmail()
email: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ export class NewsletterSubscription extends AbstractBaseEntity {

@DeleteDateColumn()
deletedAt: Date;

@Column({ default: false })
isUnsubscribed: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { skipAuth } from '@shared/helpers/skipAuth';
import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { NewsletterSubscriptionResponseDto } from './dto/newsletter-subscription.response.dto';
import { SuperAdminGuard } from '@guards/super-admin.guard';
import { UnsubscribeNewsletterDto } from './dto/unsubscribe-newsletter.dto';

@ApiTags('Newsletter Subscription')
@Controller('newsletter-subscription')
Expand Down Expand Up @@ -115,4 +116,14 @@ export class NewsletterSubscriptionController {
restore(@Param('id') id: string) {
return this.newsletterSubscriptionService.restore(id);
}

@Post('unsubscribe')
@skipAuth()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Unsubscribe from the newsletter' })
@ApiResponse({ status: 200, description: 'User has been unsubscribed successfully.' })
@ApiResponse({ status: 404, description: 'Email not found' })
unsubscribe(@Body() unsubscribeDto: UnsubscribeNewsletterDto) {
return this.newsletterSubscriptionService.unsubscribe(unsubscribeDto.email);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,17 @@ export class NewsletterSubscriptionService {
email: newsletterSubscription.email,
};
}

async unsubscribe(email: string) {
const subscription = await this.newsletterSubscriptionRepository.findOne({ where: { email } });

if (!subscription) {
throw new NotFoundException(`Email ${email} not found in the subscription list`);
}

subscription.isUnsubscribed = true;
await this.newsletterSubscriptionRepository.save(subscription);

return { message: `Email ${email} has been unsubscribed successfully` };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,47 @@ describe('NewsletterService', () => {
await expect(service.restore(id)).rejects.toThrow(NotFoundException);
});
});

describe('unsubscribe', () => {
it('should successfully unsubscribe a user', async () => {
const email = '[email protected]';
const mockSubscription = { id: '1', email, isUnsubscribed: false } as NewsletterSubscription;

// Mock `findOne` to return a subscribed user
jest.spyOn(repository, 'findOne').mockResolvedValue(mockSubscription);
// Mock `save` to return updated object
jest.spyOn(repository, 'save').mockImplementation(
async sub =>
({
...sub,
isUnsubscribed: true,
email: sub.email,
deletedAt: sub.deletedAt || new Date(),
id: sub.id,
created_at: sub.created_at || new Date(),
updated_at: new Date(),
}) as NewsletterSubscription
);

const result = await service.unsubscribe(email);
expect(result).toEqual({ message: `Email ${email} has been unsubscribed successfully` });

// Ensure `isUnsubscribed` was updated
expect(repository.save).toHaveBeenCalledWith(expect.objectContaining({ isUnsubscribed: true }));
});

it('should throw NotFoundException if email is not found', async () => {
const email = '[email protected]';

// Mock `findOne` to return null
jest.spyOn(repository, 'findOne').mockResolvedValue(null);

// Expecting an error
await expect(service.unsubscribe(email)).rejects.toThrow(
new NotFoundException(`Email ${email} not found in the subscription list`)
);

expect(repository.findOne).toHaveBeenCalledWith({ where: { email } });
});
});
});
13 changes: 11 additions & 2 deletions src/modules/notifications/notifications.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ApiInternalServerErrorResponse,
ApiOkResponse,
ApiOperation,
ApiQuery,
ApiResponse,
ApiTags,
ApiUnauthorizedResponse,
Expand Down Expand Up @@ -45,6 +46,8 @@ export class NotificationsController {
}

@Get('/all')
@ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number for pagination' })
@ApiQuery({ name: 'limit', required: false, type: Number, description: 'Number of notifications per page' })
@ApiResponse({
status: 200,
description: 'Notifications retrieved successfully',
Expand All @@ -53,9 +56,13 @@ export class NotificationsController {
@ApiInternalServerErrorResponse({
description: 'Failed to retrieve notifications.',
})
async getNotifications(@Req() req: { user: UserPayload }) {
async getNotifications(
@Req() req: { user: UserPayload },
@Query('page') page: number = 1,
@Query('limit') limit: number = 10
) {
const userId = req.user.id;
const notifications = await this.notificationsService.getNotificationsForUser(userId);
const notifications = await this.notificationsService.getNotificationsForUser(userId, page, limit);

return {
status: 'success',
Expand All @@ -70,6 +77,8 @@ export class NotificationsController {
message,
created_at,
})),
current_page: page,
total_pages: Math.ceil(notifications.totalNotificationCount / limit),
},
};
}
Expand Down
12 changes: 8 additions & 4 deletions src/modules/notifications/notifications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,24 @@ export class NotificationsService {
};
}

async getNotificationsForUser(userId: string) {
async getNotificationsForUser(userId: string, page: number, limit: number) {
try {
const user = await this.userRepository.findOne({ where: { id: userId } });
if (!user) {
throw new NotFoundException('User not found');
}

const notifications = await this.notificationRepository.find({
const [notifications, totalNotificationCount] = await this.notificationRepository.findAndCount({
where: { user: { id: userId } },
order: { created_at: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});

const totalUnreadNotificationCount = await this.notificationRepository.count({
where: { user: { id: userId }, is_read: false },
});

const totalNotificationCount = notifications.length;
const totalUnreadNotificationCount = notifications.filter(notification => !notification.is_read).length;
return {
totalNotificationCount,
totalUnreadNotificationCount,
Expand Down
8 changes: 6 additions & 2 deletions src/modules/organisations/organisations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,13 @@ export class OrganisationsController {
@ApiResponse({ status: 200, description: 'Organisations retrieved successfully', type: UserOrganizationResponseDto })
@ApiResponse({ status: 400, description: 'Bad request', type: UserOrganizationErrorResponseDto })
@Get('/')
async getUserOrganisations(@Req() req) {
async getUserOrganisations(
@Req() req,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('page_size', new DefaultValuePipe(10), ParseIntPipe) page_size: number
) {
const { sub } = req.user;
return this.organisationsService.getUserOrganisations(sub);
return this.organisationsService.getUserOrganisations(sub, page, page_size);
}

@UseGuards(OwnershipGuard)
Expand Down
19 changes: 15 additions & 4 deletions src/modules/organisations/organisations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,26 +155,37 @@ export class OrganisationsService {
};
}

async getUserOrganisations(userId: string) {
const organisations = await this.getAllUserOrganisations(userId);
async getUserOrganisations(userId: string, page: number, page_size: number) {
const organisations = await this.getAllUserOrganisations(userId, page, page_size);
const total_count = await this.organisationUserRole.count({ where: { userId } });

return {
status_code: HttpStatus.OK,
message: 'Organisations retrieved successfully',
data: organisations,
data: {
organisations,
total_count,
current_page: page,
page_size,
},
};
}

async getAllUserOrganisations(userId: string) {
async getAllUserOrganisations(userId: string, page: number, page_size: number) {
const user = await this.userRepository.findOne({ where: { id: userId } });

if (!user) {
return [];
}

const skip = (page - 1) * page_size;

const userOrganisations = (
await this.organisationUserRole.find({
where: { userId },
relations: ['organisation', 'organisation.owner', 'role'],
skip,
take: page_size,
})
).map(instance => ({
organisation_id: instance?.organisation?.id || '',
Expand Down

0 comments on commit 0458f8a

Please sign in to comment.