Skip to content

Commit

Permalink
feat(api): refactored to share more code between frontend and backend
Browse files Browse the repository at this point in the history
  • Loading branch information
xmlking committed Apr 5, 2019
1 parent 67fb833 commit ddb088d
Show file tree
Hide file tree
Showing 38 changed files with 175 additions and 118 deletions.
14 changes: 4 additions & 10 deletions apps/api/src/app/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';

import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { User } from './user.entity';
import { JwtStrategy } from './passport/jwt.strategy';
import { AuthGuard } from './guards/auth.guard';
import { AllowGuard } from './guards/allow.guard';
import { RoleGuard } from './guards/role.guard';
import { ComposeGuard } from './guards/compose.guard';
import { WsAuthGuard } from './guards/ws-auth.guard';
import { WsJwtStrategy } from './passport/ws-jwt.strategy';
import { UserModule } from '../user';

@Module({
imports: [TypeOrmModule.forFeature([User])],
imports: [UserModule],
providers: [
AuthService,
JwtStrategy,
WsJwtStrategy,
AllowGuard,
Expand All @@ -40,7 +35,6 @@ import { WsJwtStrategy } from './passport/ws-jwt.strategy';
useClass: ComposeGuard,
},
],
controllers: [AuthController],
exports: [AuthService, AuthGuard, WsAuthGuard],
exports: [AuthGuard, WsAuthGuard],
})
export class AuthModule {}
7 changes: 4 additions & 3 deletions apps/api/src/app/auth/guards/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import * as passport from 'passport';
import { HttpAuthException } from '../auth.exception';
import { User, JwtToken } from '@ngx-starter-kit/models';

export const defaultOptions = {
session: false,
Expand All @@ -16,7 +17,7 @@ export const defaultOptions = {
};

const createPassportContext = (request, response) => (type, options) =>
new Promise((resolve, reject) =>
new Promise<{ user: User; info: JwtToken }>((resolve, reject) =>
passport.authenticate(type, options, (err, user, info) => {
try {
return resolve(options.callback(err, user, info));
Expand All @@ -36,8 +37,8 @@ export class AuthGuard implements CanActivate {
const [request, response] = [httpContext.getRequest(), httpContext.getResponse()];
const passportFn = createPassportContext(request, response);
const userAndInfo = await passportFn('jwt', defaultOptions);
request[defaultOptions.property] = (userAndInfo as any).user;
request[defaultOptions.infoProperty] = (userAndInfo as any).info;
request[defaultOptions.property] = userAndInfo.user;
request[defaultOptions.infoProperty] = userAndInfo.info;
return true;
}
}
5 changes: 3 additions & 2 deletions apps/api/src/app/auth/guards/role.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class RoleGuard implements CanActivate {
throw new UnauthorizedException('RoleGuard should have been executed after AuthGuard');
}

// FIXME: useless
if (endpointRoles.includes(RolesEnum.SELF)) {
if (token.preferred_username === this.resolveUsername(request)) {
return true;
Expand All @@ -38,15 +39,15 @@ export class RoleGuard implements CanActivate {
if (this.isRoleOverlay(token.realm_access.roles, [RolesEnum.ADMIN])) {
return true;
} else {
throw new ForbiddenException(`SumoApp admin users only`);
throw new ForbiddenException(`NgxApp admin users only`);
}
}

if (endpointRoles.includes(RolesEnum.USER)) {
if (this.isRoleOverlay(token.realm_access.roles, [RolesEnum.USER])) {
return true;
} else {
throw new ForbiddenException(`SumoApp users only`);
throw new ForbiddenException(`NgxApp users only`);
}
}

Expand Down
7 changes: 4 additions & 3 deletions apps/api/src/app/auth/guards/ws-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import * as passport from 'passport';
import { WsAuthException } from '../auth.exception';
import { User, JwtToken } from '@ngx-starter-kit/models';

export const defaultWsOptions = {
session: false,
Expand All @@ -16,7 +17,7 @@ export const defaultWsOptions = {
};

const createPassportContext = (request, response) => (type, options) =>
new Promise((resolve, reject) =>
new Promise<{ user: User; info: JwtToken }>((resolve, reject) =>
passport.authenticate(type, options, (err, user, info) => {
try {
return resolve(options.callback(err, user, info));
Expand All @@ -37,8 +38,8 @@ export class WsAuthGuard implements CanActivate {
const passportFn = createPassportContext(request, {});
try {
const userAndInfo = await passportFn('ws-jwt', defaultWsOptions);
request[defaultWsOptions.property] = (userAndInfo as any).user;
request[defaultWsOptions.infoProperty] = (userAndInfo as any).info;
request[defaultWsOptions.property] = userAndInfo.user;
request[defaultWsOptions.infoProperty] = userAndInfo.info;
return true;
} catch (err) {
request.disconnect();
Expand Down
8 changes: 3 additions & 5 deletions apps/api/src/app/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
export * from './auth.module';
export * from './auth.service';
export * from './user.entity';
export { AuthModule } from './auth.module';
export * from './decorators';
export * from './guards/ws-auth.guard';
export * from './guards/auth.guard';
export { WsAuthGuard } from './guards/ws-auth.guard';
export { AuthGuard } from './guards/auth.guard';
8 changes: 4 additions & 4 deletions apps/api/src/app/auth/passport/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { passportJwtSecret, SigningKeyNotFoundError } from '@xmlking/jwks-rsa';

import { AuthService } from '../auth.service';
import { JwtToken } from '../interfaces/jwt-token.interface';
import { environment as env } from '@env-api/environment';
import { JwtToken } from '@ngx-starter-kit/models';
import { UserService } from '../../user';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
constructor(private readonly userService: UserService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
// secretOrKey: env.auth.publicKey,
Expand All @@ -36,7 +36,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {

// tslint:disable-next-line:ban-types
async validate(token: any, done: Function) {
const user = await this.authService.getLoggedUserOrCreate(token).catch(console.error);
const user = await this.userService.getLoggedUserOrCreate(token).catch(console.error);
if (!user) {
return done(new UnauthorizedException('user not found and cannot create new user in database'), false);
}
Expand Down
8 changes: 4 additions & 4 deletions apps/api/src/app/auth/passport/ws-jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { passportJwtSecret, SigningKeyNotFoundError } from '@xmlking/jwks-rsa';

import { AuthService } from '../auth.service';
import { JwtToken } from '../interfaces/jwt-token.interface';
import { WsException } from '@nestjs/websockets';
import { environment as env } from '@env-api/environment';
import { JwtToken } from '@ngx-starter-kit/models';
import { UserService } from '../../user';

const extractJwtFromWsQuery = req => {
let token = null;
Expand All @@ -20,7 +20,7 @@ const extractJwtFromWsQuery = req => {

@Injectable()
export class WsJwtStrategy extends PassportStrategy(Strategy, 'ws-jwt') {
constructor(private readonly authService: AuthService) {
constructor(private readonly userService: UserService) {
super({
jwtFromRequest: extractJwtFromWsQuery, // ExtractJwt.fromUrlQueryParameter('token'),
// secretOrKey: env.auth.publicKey,
Expand All @@ -47,7 +47,7 @@ export class WsJwtStrategy extends PassportStrategy(Strategy, 'ws-jwt') {

// tslint:disable-next-line:ban-types
async validate(token: any, done: Function) {
const user = await this.authService.getLoggedUserOrCreate(token).catch(console.error);
const user = await this.userService.getLoggedUserOrCreate(token).catch(console.error);
if (!user) {
return done(new WsException('user not found and cannot create new user in database'), false);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app/chat/interfaces/message.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { User } from './user';
import { User } from '@ngx-starter-kit/models';

export class Message {
message: string;
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/app/core/context/request-context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpException, HttpStatus } from '@nestjs/common';
import * as cls from 'cls-hooked';
import uuid from 'uuid';
import { User } from '../../auth';
import { User } from '@ngx-starter-kit/models';

export class RequestContext {
readonly id: number;
Expand Down Expand Up @@ -38,7 +38,7 @@ export class RequestContext {

if (requestContext) {
// tslint:disable-next-line
const user: any = requestContext.request['user'];
const user: User = requestContext.request['user'];
if (user) {
return user;
}
Expand Down
18 changes: 9 additions & 9 deletions apps/api/src/app/core/core.module.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { EmailModule } from '../email';
import { ConfigModule } from '../config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor, TransformInterceptor } from './interceptors';
import { RequestContextMiddleware } from './context';
import { ConfigService } from '../config';
import { ConnectionOptions } from 'typeorm';
import { environment as env } from '@env-api/environment';
import { isClass } from '@ngx-starter-kit/utils';

function requireAllClasses(rc) {
return rc
.keys()
.filter(filePath => !filePath.includes('base'))
.flatMap(key => Object.values(rc(key)))
.filter(isClass);
}

const requireContext = (require as any).context('../..', true, /\.entity.ts/);
// const requireContext = (require as any).context('../..', true, /^\.\/.*\/.*\/(?!(base|audit-base)).*\.entity.ts$/);
const entities = requireAllClasses(requireContext);

// const migrationsRequireContext = (require as any).context('../../../migrations/', true, /\.ts/);
// const migrations = requireAllClasses(migrationsRequireContext);

@Module({
imports: [
ConfigModule,
EmailModule.forRoot(env.email),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (config: ConfigService) =>
({
...env.database,
entities,
} as ConnectionOptions),
useFactory: (config: ConfigService): TypeOrmModuleOptions => ({
...env.database,
entities,
// migrations,
}),
inject: [ConfigService],
}),
],
Expand Down
9 changes: 6 additions & 3 deletions apps/api/src/app/core/crud/crud.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Get, Post, Put, Delete, Body, Param, HttpStatus } from '@nestjs/common';
import { Get, Post, Put, Delete, Body, Param, HttpStatus, HttpCode } from '@nestjs/common';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
import { Base } from '../entities/base.entity';
import { Base } from '../entities/base';
import { DeepPartial } from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { ICrudService } from './icrud.service';
Expand All @@ -13,7 +13,7 @@ export abstract class CrudController<T extends Base> {
protected constructor(private readonly crudService: ICrudService<T>) {}

@ApiOperation({ title: 'find all' })
@ApiResponse({ status: HttpStatus.OK, description: 'Found records', /* type: IPagination<T> */ })
@ApiResponse({ status: HttpStatus.OK, description: 'Found records' /* type: IPagination<T> */ })
@Get()
async findAll(filter?: PaginationParams<T>): Promise<IPagination<T>> {
return this.crudService.findAll(filter);
Expand All @@ -33,6 +33,7 @@ export abstract class CrudController<T extends Base> {
status: HttpStatus.BAD_REQUEST,
description: 'Invalid input, The response body may contain clues as to what went wrong',
})
@HttpCode(HttpStatus.CREATED)
@Post()
async create(@Body() entity: DeepPartial<T>, ...options: any[]): Promise<T> {
return this.crudService.create(entity);
Expand All @@ -45,6 +46,7 @@ export abstract class CrudController<T extends Base> {
status: HttpStatus.BAD_REQUEST,
description: 'Invalid input, The response body may contain clues as to what went wrong',
})
@HttpCode(HttpStatus.ACCEPTED)
@Put(':id')
async update(@Param('id') id: string, @Body() entity: QueryDeepPartialEntity<T>, ...options: any[]): Promise<any> {
return this.crudService.update(id, entity); // FIXME: https://github.com/typeorm/typeorm/issues/1544
Expand All @@ -53,6 +55,7 @@ export abstract class CrudController<T extends Base> {
@ApiOperation({ title: 'Delete record' })
@ApiResponse({ status: HttpStatus.NO_CONTENT, description: 'The record has been successfully deleted' })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Record not found' })
@HttpCode(HttpStatus.ACCEPTED)
@Delete(':id')
async delete(@Param('id') id: string, ...options: any[]): Promise<any> {
return this.crudService.delete(id);
Expand Down
12 changes: 10 additions & 2 deletions apps/api/src/app/core/crud/crud.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { DeepPartial, DeleteResult, FindConditions, FindManyOptions, FindOneOptions, Repository, UpdateResult } from 'typeorm';
import {
DeepPartial,
DeleteResult,
FindConditions,
FindManyOptions,
FindOneOptions,
Repository,
UpdateResult,
} from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { mergeMap } from 'rxjs/operators';
import { of, throwError } from 'rxjs';
import { Base } from '../entities/base.entity';
import { Base } from '../entities/base';
import { ICrudService } from './icrud.service';
import { IPagination } from './pagination';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import {
BeforeInsert,
BeforeUpdate,
Column,
CreateDateColumn,
ManyToOne,
PrimaryGeneratedColumn, RelationId,
PrimaryGeneratedColumn,
UpdateDateColumn,
VersionColumn,
} from 'typeorm';
import { Exclude } from 'class-transformer';
import { ApiModelProperty } from '@nestjs/swagger';
// FIXME: we need to import User like this to avoid Circular denpendence problem
import { User } from '../../auth/user.entity';
import { RequestContext } from '../context';
import { User } from '../../user';

// TODO: Implement Soft Delete

export abstract class AuditBase {
@ApiModelProperty()
@PrimaryGeneratedColumn()
id: number;
@ApiModelProperty({ type: String })
@PrimaryGeneratedColumn('uuid')
id?: string;

@ApiModelProperty({ type: 'string', format: 'date-time', example: '2018-11-21T06:20:32.232Z' })
@CreateDateColumn({ type: 'timestamptz' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { PrimaryGeneratedColumn } from 'typeorm';
import { ApiModelPropertyOptional } from '@nestjs/swagger';

export abstract class Base {
@ApiModelPropertyOptional()
@PrimaryGeneratedColumn()
id?: number;
@ApiModelPropertyOptional({ type: String })
@PrimaryGeneratedColumn('uuid')
id?: string;
}
4 changes: 1 addition & 3 deletions apps/api/src/app/external/external.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ import { HttpModule, Module } from '@nestjs/common';
import { SharedModule } from '../shared';
import { WeatherController } from './weather/weather.controller';
import { WeatherService } from './weather/weather.service';
import { WeatherHealthIndicator } from './weather/weather.health';

@Module({
imports: [SharedModule, HttpModule.register({ timeout: 5000 })],
providers: [WeatherService, WeatherHealthIndicator],
exports: [WeatherHealthIndicator],
providers: [WeatherService],
controllers: [WeatherController],
})
export class ExternalModule {}
1 change: 0 additions & 1 deletion apps/api/src/app/external/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './external.module';
export { WeatherHealthIndicator } from './weather/weather.health';
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { IsAscii, IsEmail, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
import { ApiModelProperty } from '@nestjs/swagger';
import { Validate } from 'class-validator';
import { IsEmailUnique } from '../validator/is-email-unique.validator';

export class CreateUserDto {
@ApiModelProperty({ type: String })
Expand All @@ -15,6 +17,7 @@ export class CreateUserDto {
@ApiModelProperty({ type: String, minLength: 10, maxLength: 100 })
@IsEmail()
@IsNotEmpty()
@Validate(IsEmailUnique)
readonly email: string;

@ApiModelProperty({ type: String, minLength: 8, maxLength: 20 })
Expand Down
Loading

0 comments on commit ddb088d

Please sign in to comment.