Skip to content

Commit

Permalink
Merge pull request #74 from codex-team/feat/policy
Browse files Browse the repository at this point in the history
feat(policy): implement policies — way for custom checks for route handlers
  • Loading branch information
neSpecc authored Oct 20, 2023
2 parents 075304e + 70142ba commit e6186df
Show file tree
Hide file tree
Showing 13 changed files with 917 additions and 751 deletions.
32 changes: 32 additions & 0 deletions src/presentation/http/fastify.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import type * as fastify from 'fastify';
import type * as http from 'http';
import type { pino } from 'pino';
import type Policies from './policies/index.js';
import type AuthPayload from '@domain/entities/authPayload.js';

declare module 'fastify' {
export interface FastifyInstance<
Expand Down Expand Up @@ -29,4 +31,34 @@ declare module 'fastify' {
* Type shortcut for fastify server instance
*/
type FastifyServer = FastifyInstance<http.Server, http.IncomingMessage, http.ServerResponse, pino.Logger>;

/**
* Augment FastifyRequest.routeConfig to respect "policy" property
*/
export interface FastifyContextConfig {
/**
* Policy names to apply to the route
*
* @example
*
* fastify.post('/note', {
* config: {
* policy: [
* 'authRequired',
* ],
* },
* }, async (request, reply) => {
* // ...
* })
*/
policy?: Array<keyof typeof Policies>;
}

/**
* Augment FastifyRequest to add userId property.
* This property added by Auth Middleware
*/
export interface FastifyRequest {
userId: AuthPayload['id'] | null
}
}
63 changes: 51 additions & 12 deletions src/presentation/http/http-api.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { getLogger } from '@infrastructure/logging/index.js';
import type { HttpApiConfig } from '@infrastructure/config/index.js';
import type { FastifyInstance } from 'fastify';
import type { FastifyBaseLogger } from 'fastify';
import type { FastifyInstance, FastifyBaseLogger } from 'fastify';
import fastify from 'fastify';
import type Api from '@presentation/api.interface.js';
import type { DomainServices } from '@domain/index.js';
import cors from '@fastify/cors';
import fastifyOauth2 from '@fastify/oauth2';
import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUI from '@fastify/swagger-ui';
import initMiddlewares, { type Middlewares } from '@presentation/http/middlewares/index.js';
import addAuthMiddleware from '@presentation/http/middlewares/auth.js';
import cookie from '@fastify/cookie';
import NotFoundDecorator from './decorators/notFound.js';
import NoteRouter from '@presentation/http/router/note.js';
Expand All @@ -19,6 +18,7 @@ import UserRouter from '@presentation/http/router/user.js';
import AIRouter from '@presentation/http/router/ai.js';
import EditorToolsRouter from './router/editorTools.js';
import { UserSchema } from './schema/User.js';
import Policies from './policies/index.js';
import type { RequestParams, Response } from '@presentation/api.interface.js';
import NoteSettingsRouter from './router/noteSettings.js';

Expand Down Expand Up @@ -69,22 +69,24 @@ export default class HttpApi implements Api {
await this.addOauth2();
await this.addCORS();

this.addMiddlewares(domainServices);

this.addSchema();
this.addDecorators();
this.addPoliciesCheckHook();

const middlewares = initMiddlewares(domainServices);

await this.addApiRoutes(domainServices, middlewares);
await this.addApiRoutes(domainServices);
}


/**
* Runs http server
*/
public async run(): Promise<void> {
if (this.server === undefined || this.config === undefined) {
if (this.server === undefined) {
throw new Error('Server is not initialized');
}

await this.server.listen({
host: this.config.host,
port: this.config.port,
Expand Down Expand Up @@ -158,20 +160,17 @@ export default class HttpApi implements Api {
* Registers all routers
*
* @param domainServices - instances of domain services
* @param middlewares - middlewares
*/
private async addApiRoutes(domainServices: DomainServices, middlewares: Middlewares): Promise<void> {
private async addApiRoutes(domainServices: DomainServices): Promise<void> {
await this.server?.register(NoteRouter, {
prefix: '/note',
noteService: domainServices.noteService,
noteSettingsService: domainServices.noteSettingsService,
middlewares: middlewares,
});

await this.server?.register(NoteSettingsRouter, {
prefix: '/note-settings',
noteSettingsService: domainServices.noteSettingsService,
middlewares: middlewares,
});

await this.server?.register(OauthRouter, {
Expand All @@ -189,7 +188,6 @@ export default class HttpApi implements Api {
await this.server?.register(UserRouter, {
prefix: '/user',
userService: domainServices.userService,
middlewares: middlewares,
});

await this.server?.register(AIRouter, {
Expand Down Expand Up @@ -245,5 +243,46 @@ export default class HttpApi implements Api {
private addDecorators(): void {
this.server?.decorate('notFound', NotFoundDecorator);
}

/**
* Add middlewares
*
* @param domainServices - instances of domain services
*/
private addMiddlewares(domainServices: DomainServices): void {
if (this.server === undefined) {
throw new Error('Server is not initialized');
}

addAuthMiddleware(this.server, domainServices.authService, appServerLogger);
}

/**
* Add "onRoute" hook that will add "preHandler" checking policies passed through the route config
*/
private addPoliciesCheckHook(): void {
this.server?.addHook('onRoute', (routeOptions) => {
const policies = routeOptions.config?.policy ?? [];

if (policies.length === 0) {
return;
}

/**
* Save original route preHandler(s) if exists
*/
if (routeOptions.preHandler === undefined) {
routeOptions.preHandler = [];
} else if (!Array.isArray(routeOptions.preHandler) ) {
routeOptions.preHandler = [ routeOptions.preHandler ];
}

routeOptions.preHandler.push(async (request, reply) => {
for (const policy of policies) {
await Policies[policy](request, reply);
}
});
});
}
}

37 changes: 37 additions & 0 deletions src/presentation/http/middlewares/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { FastifyInstance } from 'fastify';
import type AuthService from '@domain/service/auth.js';
import type Logger from '@infrastructure/logging/index.js';
import notEmpty from '@infrastructure/utils/notEmpty.js';

/**
* Add middleware for resolve userId from Access Token and add it to request
*
* @param server - fastify instance
* @param authService - auth domain service
* @param logger - logger instance
*/
export default function addAuthMiddleware(server: FastifyInstance, authService: AuthService, logger: typeof Logger ): void {
/**
* Default userId value — null
*/
server.decorateRequest('userId', null);

/**
* Resolve userId from Access Token on each request
*/
server.addHook('preHandler', (request, _reply, done) => {
const authorizationHeader = request.headers.authorization;

if (notEmpty(authorizationHeader)) {
const token = authorizationHeader.replace('Bearer ', '');

try {
request.userId = authService.verifyAccessToken(token)['id'];
} catch (error) {
logger.error('Invalid Access Token');
logger.error(error);
}
}
done();
});
}
53 changes: 0 additions & 53 deletions src/presentation/http/middlewares/authRequired.ts

This file was deleted.

38 changes: 0 additions & 38 deletions src/presentation/http/middlewares/index.ts

This file was deleted.

51 changes: 0 additions & 51 deletions src/presentation/http/middlewares/withUser.ts

This file was deleted.

20 changes: 20 additions & 0 deletions src/presentation/http/policies/authRequired.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { StatusCodes } from 'http-status-codes';

/**
* Policy to enforce user to be logged in
*
* @param request - Fastify request object
* @param reply - Fastify reply object
*/
export default async function authRequired(request: FastifyRequest, reply: FastifyReply): Promise<void> {
const { userId } = request;

if (userId === null) {
await reply
.code(StatusCodes.UNAUTHORIZED)
.send({
message: 'Permission denied',
});
}
}
5 changes: 5 additions & 0 deletions src/presentation/http/policies/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import authRequired from './authRequired.js';

export default {
authRequired,
};
Loading

0 comments on commit e6186df

Please sign in to comment.