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

fix: create application request and role guard introduction #338

Merged
merged 10 commits into from
Oct 10, 2024
6 changes: 3 additions & 3 deletions apps/api/OsmoX-API.postman_collection.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"info": {
"_postman_id": "b97d878b-aa80-4b2f-a9f0-a0640b2bc6e2",
"_postman_id": "36ff2947-7450-4393-b3cd-39e82ee073e6",
"name": "OsmoX-API",
"description": "List of all APIs used in OsmoX",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
Expand Down Expand Up @@ -781,7 +781,7 @@
"body": {
"mode": "graphql",
"graphql": {
"query": "mutation CreateApplication {\r\n application(createApplicationInput: {\r\n name: \"<newApplicationName>\",\r\n userId: 2,\r\n }) {\r\n applicationId\r\n name\r\n userId\r\n createdOn\r\n updatedOn\r\n status\r\n }\r\n}",
"query": "mutation CreateApplication {\r\n application(createApplicationInput: {\r\n name: \"<newApplicationName>\",\r\n }) {\r\n applicationId\r\n name\r\n userId\r\n createdOn\r\n updatedOn\r\n status\r\n }\r\n}",
"variables": ""
}
},
Expand Down Expand Up @@ -883,7 +883,7 @@
"body": {
"mode": "graphql",
"graphql": {
"query": "mutation CreateProvider {\n provider(createProviderInput: {\n applicationId: 2,\n channelType: 2,\n configuration: {},\n isEnabled: 1,\n name: \"<New Provider Name>\",\n userId: 1,\n }) {\n applicationId\n channelType\n configuration\n isEnabled\n name\n userId\n createdOn\n updatedOn\n status\n }\n}",
"query": "mutation CreateProvider {\n provider(createProviderInput: {\n applicationId: 2,\n channelType: 2,\n configuration: {},\n isEnabled: 1,\n name: \"<New Provider Name>\",\n userId: 1,\n }) {\n providerId\n applicationId\n channelType\n configuration\n isEnabled\n name\n userId\n createdOn\n updatedOn\n status\n }\n}",
"variables": ""
}
},
Expand Down
32 changes: 16 additions & 16 deletions apps/api/OsmoX.postman_collection.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"info": {
"_postman_id": "e9aeb9a0-761f-4b10-86a7-dd4bcaebca94",
"_postman_id": "336711a4-7ddf-4f32-b136-4aa09b0e284b",
"name": "OsmoX",
"description": "OsmoX API helps creating new notifications, fetching existing notifications as well as perform authorization related tasks.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
Expand Down Expand Up @@ -3276,11 +3276,11 @@
" pm.expect(jsonData).to.have.property(\"data\");\r",
" pm.expect(jsonData).to.have.property(\"errors\").to.be.an(\"array\");\r",
"});\r",
"pm.test(\"message: Access Denied. Not an ADMIN.\", function () {\r",
"pm.test(\"message: Forbidden resource\", function () {\r",
" var jsonData = pm.response.json();\r",
" pm.expect(jsonData).to.have.property(\"data\");\r",
" pm.expect(jsonData).to.have.property(\"errors\").to.be.an(\"array\");\r",
" pm.expect(jsonData.errors[0]).to.have.property(\"message\", \"Access Denied. Not an ADMIN.\");\r",
" pm.expect(jsonData.errors[0]).to.have.property(\"message\", \"Forbidden resource\");\r",
"});"
],
"type": "text/javascript",
Expand Down Expand Up @@ -3430,7 +3430,7 @@
"body": {
"mode": "graphql",
"graphql": {
"query": "mutation CreateApplication {\r\n application(createApplicationInput: {\r\n name: \"sampleTestXApp\",\r\n userId: 2,\r\n }) {\r\n applicationId\r\n name\r\n userId\r\n createdOn\r\n updatedOn\r\n status\r\n }\r\n}",
"query": "mutation CreateApplication {\r\n application(createApplicationInput: {\r\n name: \"sampleTestXApp\",\r\n }) {\r\n applicationId\r\n name\r\n userId\r\n createdOn\r\n updatedOn\r\n status\r\n }\r\n}",
"variables": ""
}
},
Expand Down Expand Up @@ -3464,11 +3464,11 @@
" pm.expect(jsonData).to.have.property(\"data\");\r",
" pm.expect(jsonData).to.have.property(\"errors\").to.be.an(\"array\");\r",
"});\r",
"pm.test(\"message: Access Denied. Not an ADMIN.\", function () {\r",
"pm.test(\"message: Forbidden resource\", function () {\r",
" var jsonData = pm.response.json();\r",
" pm.expect(jsonData).to.have.property(\"data\");\r",
" pm.expect(jsonData).to.have.property(\"errors\").to.be.an(\"array\");\r",
" pm.expect(jsonData.errors[0]).to.have.property(\"message\", \"Access Denied. Not an ADMIN.\");\r",
" pm.expect(jsonData.errors[0]).to.have.property(\"message\", \"Forbidden resource\");\r",
"});"
],
"type": "text/javascript",
Expand All @@ -3493,7 +3493,7 @@
"body": {
"mode": "graphql",
"graphql": {
"query": "mutation CreateApplication {\r\n application(createApplicationInput: {\r\n name: \"sampleTestXApp\",\r\n userId: 2,\r\n }) {\r\n applicationId\r\n name\r\n userId\r\n createdOn\r\n updatedOn\r\n status\r\n }\r\n}",
"query": "mutation CreateApplication {\r\n application(createApplicationInput: {\r\n name: \"sampleTestXApp\",\r\n }) {\r\n applicationId\r\n name\r\n userId\r\n createdOn\r\n updatedOn\r\n status\r\n }\r\n}",
"variables": ""
}
},
Expand Down Expand Up @@ -3551,7 +3551,7 @@
"body": {
"mode": "graphql",
"graphql": {
"query": "mutation CreateApplication {\r\n application(createApplicationInput: {\r\n unknownValue: \"Some unknown parameter\"\r\n name: \"sampleFoundationXApp\",\r\n userId: 2,\r\n }) {\r\n applicationId\r\n name\r\n userId\r\n createdOn\r\n updatedOn\r\n status\r\n }\r\n}",
"query": "mutation CreateApplication {\r\n application(createApplicationInput: {\r\n unknownValue: \"Some unknown parameter\"\r\n name: \"sampleFoundationXApp\",\r\n }) {\r\n applicationId\r\n name\r\n userId\r\n createdOn\r\n updatedOn\r\n status\r\n }\r\n}",
"variables": ""
}
},
Expand Down Expand Up @@ -3656,11 +3656,11 @@
" pm.expect(jsonData).to.have.property(\"data\");\r",
" pm.expect(jsonData).to.have.property(\"errors\").to.be.an(\"array\");\r",
"});\r",
"pm.test(\"message: Access Denied. Not an ADMIN.\", function () {\r",
"pm.test(\"message: Forbidden resource\", function () {\r",
" var jsonData = pm.response.json();\r",
" pm.expect(jsonData).to.have.property(\"data\");\r",
" pm.expect(jsonData).to.have.property(\"errors\").to.be.an(\"array\");\r",
" pm.expect(jsonData.errors[0]).to.have.property(\"message\", \"Access Denied. Not an ADMIN.\");\r",
" pm.expect(jsonData.errors[0]).to.have.property(\"message\", \"Forbidden resource\");\r",
"});"
],
"type": "text/javascript",
Expand Down Expand Up @@ -3810,7 +3810,7 @@
"body": {
"mode": "graphql",
"graphql": {
"query": "mutation CreateProvider {\n provider(createProviderInput: {\n applicationId: 2,\n channelType: 2,\n configuration: {},\n isEnabled: 1,\n name: \"Mailgun PineStem\",\n userId: 1,\n }) {\n applicationId\n channelType\n configuration\n isEnabled\n name\n userId\n createdOn\n updatedOn\n status\n }\n}",
"query": "mutation CreateProvider {\n provider(createProviderInput: {\n applicationId: 2,\n channelType: 2,\n configuration: {},\n isEnabled: 1,\n name: \"Mailgun PineStem\",\n userId: 1,\n }) {\n providerId\n applicationId\n channelType\n configuration\n isEnabled\n name\n userId\n createdOn\n updatedOn\n status\n }\n}",
"variables": ""
}
},
Expand Down Expand Up @@ -3873,7 +3873,7 @@
"body": {
"mode": "graphql",
"graphql": {
"query": "mutation CreateProvider {\n provider(createProviderInput: {\n # Assuming applicationId 500 is invalid\n applicationId: 500,\n channelType: 2,\n configuration: {},\n isEnabled: 1,\n name: \"Mailgun PineStem\",\n userId: 1,\n }) {\n applicationId\n channelType\n configuration\n isEnabled\n name\n userId\n createdOn\n updatedOn\n status\n }\n}",
"query": "mutation CreateProvider {\n provider(createProviderInput: {\n # Assuming applicationId 500 is invalid\n applicationId: 500,\n channelType: 2,\n configuration: {},\n isEnabled: 1,\n name: \"Mailgun PineStem\",\n userId: 1,\n }) {\n providerId\n applicationId\n channelType\n configuration\n isEnabled\n name\n userId\n createdOn\n updatedOn\n status\n }\n}",
"variables": ""
}
},
Expand Down Expand Up @@ -3907,11 +3907,11 @@
" pm.expect(jsonData).to.have.property(\"data\");\r",
" pm.expect(jsonData).to.have.property(\"errors\").to.be.an(\"array\");\r",
"});\r",
"pm.test(\"message: Access Denied. Not an ADMIN.\", function () {\r",
"pm.test(\"message: Forbidden resource\", function () {\r",
" var jsonData = pm.response.json();\r",
" pm.expect(jsonData).to.have.property(\"data\");\r",
" pm.expect(jsonData).to.have.property(\"errors\").to.be.an(\"array\");\r",
" pm.expect(jsonData.errors[0]).to.have.property(\"message\", \"Access Denied. Not an ADMIN.\");\r",
" pm.expect(jsonData.errors[0]).to.have.property(\"message\", \"Forbidden resource\");\r",
"});"
],
"type": "text/javascript",
Expand All @@ -3936,7 +3936,7 @@
"body": {
"mode": "graphql",
"graphql": {
"query": "mutation CreateProvider {\n provider(createProviderInput: {\n applicationId: 5,\n channelType: 2,\n configuration: {},\n isEnabled: 1,\n name: \"Mailgun PineStem\",\n userId: 1,\n }) {\n applicationId\n channelType\n configuration\n isEnabled\n name\n userId\n createdOn\n updatedOn\n status\n }\n}",
"query": "mutation CreateProvider {\n provider(createProviderInput: {\n applicationId: 5,\n channelType: 2,\n configuration: {},\n isEnabled: 1,\n name: \"Mailgun PineStem\",\n userId: 1,\n }) {\n providerId\n applicationId\n channelType\n configuration\n isEnabled\n name\n userId\n createdOn\n updatedOn\n status\n }\n}",
"variables": ""
}
},
Expand Down Expand Up @@ -3994,7 +3994,7 @@
"body": {
"mode": "graphql",
"graphql": {
"query": "mutation CreateProvider {\n provider(createProviderInput: {\n applicationId: 5,\n unknownValue: \"Some unknown parameter\",\n channelType: 2,\n configuration: {},\n isEnabled: 1,\n name: \"Mailgun PineStem\",\n userId: 1,\n }) {\n applicationId\n channelType\n configuration\n isEnabled\n name\n userId\n createdOn\n updatedOn\n status\n }\n}",
"query": "mutation CreateProvider {\n provider(createProviderInput: {\n applicationId: 5,\n unknownValue: \"Some unknown parameter\",\n channelType: 2,\n configuration: {},\n isEnabled: 1,\n name: \"Mailgun PineStem\",\n userId: 1,\n }) {\n providerId\n applicationId\n channelType\n configuration\n isEnabled\n name\n userId\n createdOn\n updatedOn\n status\n }\n}",
"variables": ""
}
},
Expand Down
3 changes: 1 addition & 2 deletions apps/api/docs/api-documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,6 @@ Allows the user with `Admin` role to create a new application.
mutation CreateApplication {
application(createApplicationInput: {
name: "newSampleApp",
userId: 2,
}) {
applicationId
name
Expand Down Expand Up @@ -348,7 +347,7 @@ curl --location 'http://localhost:3000/graphql' \

Allows the user to fetch all applications based on the passed query parameters. Requires passing bearer token for authorization.

Note: The API will return a successful response when the `server-api-key` passed uses `application_id` that is associated with an `Admin`.
Note: The API will return a successful response when the Bearer `authorization-token` passed is associated with an `Admin`.

The different options that can be used while fetching notifications are as follows:

Expand Down
8 changes: 8 additions & 0 deletions apps/api/src/common/decorators/roles.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SetMetadata } from '@nestjs/common';
import { UserRoles } from '../constants/database';

// Type definition for allowed values (only values from UserRoles)
type UserRoleValues = (typeof UserRoles)[keyof typeof UserRoles];

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export const Roles = (...roles: UserRoleValues[]) => SetMetadata('roles', roles);
49 changes: 49 additions & 0 deletions apps/api/src/common/guards/role.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { GqlExecutionContext } from '@nestjs/graphql';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { UserRoles } from 'src/common/constants/database';

@Injectable()
export class RolesGuard implements CanActivate {
constructor(
private reflector: Reflector,
private jwtService: JwtService,
private configService: ConfigService,
) {}

canActivate(context: ExecutionContext): boolean {
// Fetch required roles from the metadata
const requiredRoles = this.reflector.getAllAndOverride<(keyof typeof UserRoles)[]>('roles', [
context.getHandler(),
context.getClass(),
]);

if (!requiredRoles || requiredRoles.length === 0) {
return true; // Allow access if no roles are specified
}

const ctx = GqlExecutionContext.create(context);
const req = ctx.getContext().req;
const authorizationHeader = req.headers.authorization;
xixas marked this conversation as resolved.
Show resolved Hide resolved

if (!authorizationHeader || !authorizationHeader.startsWith('Bearer ')) {
return false; // No or invalid authorization token
}
xixas marked this conversation as resolved.
Show resolved Hide resolved

const token = authorizationHeader.split(' ')[1];
const secret = this.configService.getOrThrow('JWT_SECRET');

try {
// Decode the JWT token to get the user information
const decodedToken = this.jwtService.verify(token, { secret });
const userRoleId = decodedToken.role;

// Check if the user's role matches any of the required roles
return requiredRoles.includes(userRoleId);
} catch (error) {
return false; // Invalid token or other error
}
xixas marked this conversation as resolved.
Show resolved Hide resolved
}
}
19 changes: 8 additions & 11 deletions apps/api/src/modules/applications/applications.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import { Application } from './entities/application.entity';
import { QueryOptionsDto } from 'src/common/graphql/dtos/query-options.dto';
import { ApplicationResponse } from './dto/application-response.dto';
import { GqlAuthGuard } from 'src/common/guards/api-key/gql-auth.guard';
import { Roles } from 'src/common/decorators/roles.decorator';
import { RolesGuard } from 'src/common/guards/role.guard';
import { UserRoles } from 'src/common/constants/database';

@Resolver(() => Application)
@UseGuards(GqlAuthGuard)
@Roles(UserRoles.ADMIN)
@UseGuards(GqlAuthGuard, RolesGuard)
export class ApplicationsResolver {
constructor(private readonly applicationsService: ApplicationsService) {}

Expand All @@ -17,22 +21,15 @@ export class ApplicationsResolver {
@Context() context,
@Args('createApplicationInput') createApplicationInput: CreateApplicationInput,
): Promise<Application> {
const request: Request = context.req;
const authorizationHeader = request.headers['authorization'];
return await this.applicationsService.createApplication(
createApplicationInput,
authorizationHeader,
);
const requestUserId: number = context.req.userId;
return await this.applicationsService.createApplication(createApplicationInput, requestUserId);
}

@Query(() => ApplicationResponse, { name: 'applications' })
async findAll(
@Context() context,
@Args('options', { type: () => QueryOptionsDto, nullable: true, defaultValue: {} })
options: QueryOptionsDto,
): Promise<ApplicationResponse> {
const request: Request = context.req;
const authorizationHeader = request.headers['authorization'];
return this.applicationsService.getAllApplications(options, authorizationHeader);
return this.applicationsService.getAllApplications(options);
}
}
Loading
Loading