Skip to content

Commit

Permalink
feat(api): ✨ allow non-admin users to access and modify their own cal…
Browse files Browse the repository at this point in the history
…endars
  • Loading branch information
cestoliv committed Nov 2, 2022
1 parent 286fb9d commit c0bbe59
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 26 deletions.
40 changes: 30 additions & 10 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
import { isValidObjectId } from 'mongoose';
import { Types } from 'mongoose';
import { Role } from 'src/users/enum/roles.enum';
import { IUser } from 'src/users/interface/user.interface';
import { UsersService } from 'src/users/users.service';
import { isValidObjectId } from 'src/utils';

@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
constructor(
private usersService: UsersService,
private configService: ConfigService,
) {}

async validateApiKey(apiKey: string): Promise<IUser | null> {
if (apiKey === this.configService.get('ADMIN_API_KEY'))
return {
_id: new Types.ObjectId(0),
role: Role.Admin,
username: '',
password_hash: '',
};

async validateApiKey(apiKey: string): Promise<any> {
const userId = apiKey.split(':')[0];

if (!isValidObjectId(userId)) return null;
const user = await this.usersService.getUser(userId, '');
try {
const user = await this.usersService.getUser(userId, '');

if (
user &&
user.api_key_hash &&
(await bcrypt.compare(apiKey, user.api_key_hash))
) {
return user;
if (
user &&
user.api_key_hash &&
(await bcrypt.compare(apiKey, user.api_key_hash))
) {
return user;
}
} catch (e) {
return null;
}

return null;
}
}
2 changes: 2 additions & 0 deletions src/auth/roles/roles.guard.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { Role } from 'src/users/enum/roles.enum';

@Injectable()
export class RolesGuard implements CanActivate {
Expand All @@ -18,6 +19,7 @@ export class RolesGuard implements CanActivate {
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user.role) user.role = Role.User;

return roles.includes(user.role);
}
Expand Down
56 changes: 44 additions & 12 deletions src/calendars/calendars.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Param,
Post,
Put,
Request,
Res,
UseGuards,
} from '@nestjs/common';
Expand Down Expand Up @@ -34,16 +35,20 @@ export class CalendarsController {
description: 'The created calendar',
})
@UseGuards(AuthGuard('api-key'), RolesGuard)
@Roles(Role.Admin)
@Roles(Role.Admin, Role.User) // User allowed, will check after that user can only create a calendar for himself
@Post('/add')
async createCalendar(
@Res() response,
@Request() request,
@Body() createCalendarDto: CreateCalendarDto,
) {
return response
.status(HttpStatus.CREATED)
.send(
await this.calendarsService.createCalendar(createCalendarDto),
await this.calendarsService.createCalendar(
request.user,
createCalendarDto,
),
);
}

Expand All @@ -53,17 +58,19 @@ export class CalendarsController {
description: 'The updated calendar',
})
@UseGuards(AuthGuard('api-key'), RolesGuard)
@Roles(Role.Admin)
@Roles(Role.Admin, Role.User) // User allowed, will check after that user can only update his own calendar
@Put('/update/:id')
async updateCalendar(
@Res() response,
@Request() request,
@Param('id') calendarId: string,
@Body() updateCalendarDto: UpdateCalendarDto,
) {
return response
.status(HttpStatus.OK)
.send(
await this.calendarsService.updateCalendar(
request.user,
calendarId,
updateCalendarDto,
),
Expand Down Expand Up @@ -91,12 +98,21 @@ export class CalendarsController {
description: 'The calendar',
})
@UseGuards(AuthGuard('api-key'), RolesGuard)
@Roles(Role.Admin)
@Roles(Role.Admin, Role.User) // User allowed, will check after that user can only get his own calendar
@Get('/calendar/:id')
async getCalendar(@Res() response, @Param('id') calendarId: string) {
async getCalendar(
@Res() response,
@Request() request,
@Param('id') calendarId: string,
) {
return response
.status(HttpStatus.OK)
.send(await this.calendarsService.getCalendar(calendarId));
.send(
await this.calendarsService.getCalendar(
request.user,
calendarId,
),
);
}

@ApiTags('calendars')
Expand All @@ -105,12 +121,21 @@ export class CalendarsController {
description: 'The deleted calendar',
})
@UseGuards(AuthGuard('api-key'), RolesGuard)
@Roles(Role.Admin)
@Roles(Role.Admin, Role.User) // User allowed, will check after that user can only delete his own calendar
@Delete('/delete/:id')
async deleteCalendar(@Res() response, @Param('id') calendarId: string) {
async deleteCalendar(
@Res() response,
@Request() request,
@Param('id') calendarId: string,
) {
return response
.status(HttpStatus.OK)
.send(await this.calendarsService.deleteCalendar(calendarId));
.send(
await this.calendarsService.deleteCalendar(
request.user,
calendarId,
),
);
}

@ApiTags('calendars')
Expand All @@ -119,10 +144,11 @@ export class CalendarsController {
description: 'TODO',
})
@UseGuards(AuthGuard('api-key'), RolesGuard)
@Roles(Role.Admin)
@Roles(Role.Admin, Role.User) // User allowed, will check after that user can only set a state in his own calendar
@Put('/set_state/:id/:date/:state')
async setState(
@Res() response,
@Request() request,
@Param('id') calendarId: string,
@Param('date') dateString: string,
@Param('state') state: string,
Expand All @@ -131,6 +157,7 @@ export class CalendarsController {
.status(HttpStatus.OK)
.send(
await this.calendarsService.setState(
request.user,
calendarId,
dateString,
state,
Expand All @@ -144,17 +171,22 @@ export class CalendarsController {
description: 'TODO',
})
@UseGuards(AuthGuard('api-key'), RolesGuard)
@Roles(Role.Admin)
@Roles(Role.Admin, Role.User) // User allowed, will check after that user can only get a month of his own calendar
@Get('/month/:id/:month')
async getMonth(
@Res() response,
@Request() request,
@Param('id') calendarId: string,
@Param('month') monthString: string,
) {
return response
.status(HttpStatus.OK)
.send(
await this.calendarsService.getMonth(calendarId, monthString),
await this.calendarsService.getMonth(
request.user,
calendarId,
monthString,
),
);
}
}
46 changes: 42 additions & 4 deletions src/calendars/calendars.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
BadRequestException,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
Expand All @@ -12,6 +13,9 @@ import { UpdateCalendarDto } from './dto/update-calendar.dto';
import { isValidObjectId, sortMapByKeys } from 'src/utils';
import { UsersService } from 'src/users/users.service';
import { State } from './enum/state.enum';
import { Role } from 'src/users/enum/roles.enum';
import { IUser } from 'src/users/interface/user.interface';
import { asCalendarAccess, checkCalendarAccess } from './calendars.utils';

@Injectable()
export class CalendarsService {
Expand All @@ -23,23 +27,35 @@ export class CalendarsService {
defaultFields = '_id name agenda';

async createCalendar(
requester: IUser,
createCalendarDto: CreateCalendarDto,
fields = this.defaultFields,
): Promise<ICalendar> {
// using getUser to check if user exists (will throw NotFoundException if not)
const owner = await this.usersService.getUser(createCalendarDto.user);

// check that requester is the owner (except for admin)
if (
requester.role != Role.Admin &&
owner._id.toString() != requester._id.toString()
)
throw new UnauthorizedException(
'you are not allowed to create a calendar for this user',
);

// create a new calendar object with given parameters
const createCalendar: Omit<ICalendar, '_id'> = {
name: createCalendarDto.name,
user: new Types.ObjectId(owner._id),
};

const newCalendar = await new this.CalendarModel(createCalendar).save();
// return getCalendar instead of newCalendar to apply fields selection
return this.getCalendar(newCalendar._id.toString(), fields);
return this.getCalendar(requester, newCalendar._id.toString(), fields);
}

async updateCalendar(
requester: IUser,
calendarId: string,
updateCalendarDto: UpdateCalendarDto,
fields = this.defaultFields,
Expand All @@ -48,6 +64,9 @@ export class CalendarsService {
if (!isValidObjectId(calendarId))
throw new NotFoundException('Calendar not found');

// check that requester is the owner (except for admin)
await checkCalendarAccess(requester, calendarId, this.CalendarModel);

// Create a new calendar object with given parameters
const updateCalendar: Omit<ICalendar, '_id'> = {
name: updateCalendarDto.name,
Expand Down Expand Up @@ -81,39 +100,51 @@ export class CalendarsService {
}

async getCalendar(
requester: IUser,
calendarId: string,
fields = this.defaultFields,
): Promise<ICalendar> {
// Check given parameters
if (!isValidObjectId(calendarId))
throw new NotFoundException('Calendar not found');

// Get calendar
const existingCalendar = await this.CalendarModel.findById(
calendarId,
fields,
fields + ' user',
);
if (!existingCalendar)
throw new NotFoundException('Calendar not found');

// check that creator is the owner (except for admin)
asCalendarAccess(requester, existingCalendar);

existingCalendar.user = undefined; // remove user from calendar
return existingCalendar;
}

async deleteCalendar(
requester: IUser,
calendarId: string,
fields = this.defaultFields,
): Promise<ICalendar> {
// Check given parameters
if (!isValidObjectId(calendarId))
throw new NotFoundException('Calendar not found');

// check that requester is the owner (except for admin)
await checkCalendarAccess(requester, calendarId, this.CalendarModel);

// Delete calendar
const deletedCalendar = await this.CalendarModel.findByIdAndDelete(
calendarId,
{ fields: fields },
);
).select(fields);
if (!deletedCalendar) throw new NotFoundException('Calendar not found');
return deletedCalendar;
}

async setState(
requester: IUser,
calendarId: string,
dateString: string,
state: string,
Expand All @@ -123,6 +154,9 @@ export class CalendarsService {
if (!isValidObjectId(calendarId))
throw new NotFoundException('Calendar not found');

// check that requester is the owner (except for admin)
await checkCalendarAccess(requester, calendarId, this.CalendarModel);

const date = DateTime.fromFormat(dateString, 'yyyy-MM-dd');
if (!date.isValid)
throw new BadRequestException(date.invalidExplanation);
Expand All @@ -149,6 +183,7 @@ export class CalendarsService {
}

async getMonth(
requester: IUser,
calendarId: string,
monthString: string,
fields = this.defaultFields,
Expand All @@ -157,6 +192,9 @@ export class CalendarsService {
if (!isValidObjectId(calendarId))
throw new NotFoundException('Calendar not found');

// check that requester is the owner (except for admin)
await checkCalendarAccess(requester, calendarId, this.CalendarModel);

const month = DateTime.fromFormat(monthString, 'yyyy-MM');
if (!month.isValid)
throw new BadRequestException(month.invalidExplanation);
Expand Down
26 changes: 26 additions & 0 deletions src/calendars/calendars.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NotFoundException, UnauthorizedException } from '@nestjs/common';
import { Model } from 'mongoose';
import { Role } from 'src/users/enum/roles.enum';
import { IUser } from 'src/users/interface/user.interface';
import { ICalendar } from './interface/calendar.interface';

export function asCalendarAccess(user: IUser, calendar: ICalendar): void {
if (
user.role != Role.Admin &&
calendar.user.toString() != user._id.toString()
)
throw new UnauthorizedException(
'you are not allowed to access this calendar',
);
}

export async function checkCalendarAccess(
user: IUser,
calendarId: string,
CalendarModel: Model<ICalendar>,
): Promise<void> {
const existingCalendar = await CalendarModel.findById(calendarId, 'user');
if (!existingCalendar) throw new NotFoundException('Calendar not found');

asCalendarAccess(user, existingCalendar);
}

0 comments on commit c0bbe59

Please sign in to comment.