Added Authentication
This commit is contained in:
parent
d7728f1ca4
commit
64e7fd66fc
|
|
@ -1,2 +1,4 @@
|
||||||
|
database
|
||||||
|
coverage
|
||||||
tech-assignment-data
|
tech-assignment-data
|
||||||
node_modules
|
node_modules
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/database
|
/database/lib
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
|
||||||
# compiled output
|
# compiled output
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ WORKDIR /usr/src/app
|
||||||
|
|
||||||
COPY package*.json .
|
COPY package*.json .
|
||||||
|
|
||||||
RUN npm ci
|
RUN npm ci --no-auditdc po
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|
|
||||||
18
README.md
18
README.md
|
|
@ -1,3 +1,7 @@
|
||||||
|
# MyFreight Assessment
|
||||||
|
|
||||||
|
## Receiving Tracking information
|
||||||
|
When receiving Tracking information, the following steps need to be completed to process the data
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
receiveTracking(["Receive Tracking"])
|
receiveTracking(["Receive Tracking"])
|
||||||
|
|
@ -26,8 +30,12 @@ containerNumber:**string**
|
||||||
%% isArrivingSoon -->|yes| sendArriveSoon
|
%% isArrivingSoon -->|yes| sendArriveSoon
|
||||||
```
|
```
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart
|
## Possible improvements
|
||||||
getShipments([Get all shipments])
|
|
||||||
|
- Sending the actual notification, there should be a more general way to setup notifiers
|
||||||
```
|
- Moving some variables to config files
|
||||||
|
- when is a shipment "arriving soon"
|
||||||
|
- database credentials (via an .env)
|
||||||
|
- mock date
|
||||||
|
-
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -10,6 +10,7 @@
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^10.3.5",
|
"@nestjs/common": "^10.3.5",
|
||||||
|
"@nestjs/config": "^3.2.1",
|
||||||
"@nestjs/core": "^10.3.5",
|
"@nestjs/core": "^10.3.5",
|
||||||
"@nestjs/platform-express": "^10.3.5",
|
"@nestjs/platform-express": "^10.3.5",
|
||||||
"@nestjs/swagger": "^7.3.0",
|
"@nestjs/swagger": "^7.3.0",
|
||||||
|
|
@ -1766,6 +1767,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/config": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-tFZyLJKanSAu51ygQ6ZBSpx95pRcwS6qSpJDW6FFgRQzkOaOUXpL8qD8yMNoYoYxuJCxph+waiBaWKgFWxn3sw==",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "16.4.5",
|
||||||
|
"dotenv-expand": "10.0.0",
|
||||||
|
"lodash": "4.17.21",
|
||||||
|
"uuid": "9.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
||||||
|
"rxjs": "^7.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/core": {
|
"node_modules/@nestjs/core": {
|
||||||
"version": "10.3.5",
|
"version": "10.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.5.tgz",
|
||||||
|
|
@ -4054,6 +4070,14 @@
|
||||||
"url": "https://dotenvx.com"
|
"url": "https://dotenvx.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv-expand": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eastasianwidth": {
|
"node_modules/eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^10.3.5",
|
"@nestjs/common": "^10.3.5",
|
||||||
|
"@nestjs/config": "^3.2.1",
|
||||||
"@nestjs/core": "^10.3.5",
|
"@nestjs/core": "^10.3.5",
|
||||||
"@nestjs/platform-express": "^10.3.5",
|
"@nestjs/platform-express": "^10.3.5",
|
||||||
"@nestjs/swagger": "^7.3.0",
|
"@nestjs/swagger": "^7.3.0",
|
||||||
|
|
@ -67,8 +68,12 @@
|
||||||
"ts"
|
"ts"
|
||||||
],
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"roots": ["<rootDir>/tests"],
|
"roots": [
|
||||||
"testPathIgnorePatterns": ["./database"],
|
"<rootDir>/tests"
|
||||||
|
],
|
||||||
|
"testPathIgnorePatterns": [
|
||||||
|
"./database"
|
||||||
|
],
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
POST http://localhost:3000/tracking
|
POST http://localhost:3000/tracking
|
||||||
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
Authorization: eG5nVnZGaWtCMFN6NjRRaWQ2TUJsUTJWaDpjeGg1R3FKRUl0UTlzSGRQdElGMGg0bFJp
|
||||||
|
|
||||||
{
|
{
|
||||||
"carrierName": "Evergreen",
|
"carrierName": "Evergreen",
|
||||||
|
|
@ -8,3 +10,9 @@ Content-Type: application/json
|
||||||
"arrivalDateEstimate": "2023-02-14T15:00:00.000Z",
|
"arrivalDateEstimate": "2023-02-14T15:00:00.000Z",
|
||||||
"containerNumber": "EGLU866139144"
|
"containerNumber": "EGLU866139144"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET http://localhost:3000/shipments
|
||||||
|
Accept: application/json
|
||||||
|
Authorization: eG5nVnZGaWtCMFN6NjRRaWQ2TUJsUTJWaDpjeGg1R3FKRUl0UTlzSGRQdElGMGg0bFJp
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import {
|
|
||||||
ClassSerializerInterceptor,
|
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
UseInterceptors,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { AppService } from './app.service';
|
|
||||||
import { ApiExtraModels } from '@nestjs/swagger';
|
|
||||||
import { TrackingDto } from '@/dtos/tracking.dto';
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
@ApiExtraModels(TrackingDto)
|
|
||||||
@UseInterceptors(ClassSerializerInterceptor)
|
|
||||||
export class AppController {
|
|
||||||
constructor(private readonly appService: AppService) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
debug(): unknown {
|
|
||||||
return this.appService.debug();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +1,51 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AppController } from './app.controller';
|
|
||||||
import { AppService } from './app.service';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { ContainerEntity } from '@/entities/container.entity';
|
import { ContainerEntity } from '@/entities/container.entity';
|
||||||
import { LiveTrackingEntity } from '@/entities/liveTracking.entity';
|
import { LiveTrackingEntity } from '@/entities/liveTracking.entity';
|
||||||
import { ShipmentEntity } from '@/entities/shipment.entity';
|
import { ShipmentEntity } from '@/entities/shipment.entity';
|
||||||
import { TrackingModule } from './tracking/tracking.module';
|
import { TrackingModule } from './tracking/tracking.module';
|
||||||
import { ShipmentsModule } from './shipments/shipments.module';
|
import { ShipmentsModule } from './shipments/shipments.module';
|
||||||
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
|
import { AuthGuard } from '@/guards/auth.guard';
|
||||||
|
import { ApiKeyEntity } from '@/entities/apiKey.entity';
|
||||||
|
import { AuthService } from '@/auth.service';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forRoot({
|
TypeOrmModule.forRootAsync({
|
||||||
type: 'mysql',
|
imports: [ConfigModule.forRoot()],
|
||||||
host: 'localhost',
|
useFactory: async (configService: ConfigService) => {
|
||||||
port: 13306,
|
return {
|
||||||
username: 'root',
|
type: 'mysql',
|
||||||
password: 'myfreight',
|
host: configService.get('DB_HOST'),
|
||||||
database: 'myfreight',
|
port: Number(configService.get('DB_PORT')),
|
||||||
entities: [ContainerEntity, LiveTrackingEntity, ShipmentEntity],
|
username: configService.get('DB_USER'),
|
||||||
synchronize: true,
|
password: configService.get('DB_PASS'),
|
||||||
logging: false,
|
database: configService.get('DB_DABA'),
|
||||||
|
entities: [
|
||||||
|
ContainerEntity,
|
||||||
|
LiveTrackingEntity,
|
||||||
|
ShipmentEntity,
|
||||||
|
ApiKeyEntity,
|
||||||
|
],
|
||||||
|
synchronize: true,
|
||||||
|
logging: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
TypeOrmModule.forFeature([
|
TypeOrmModule.forFeature([ApiKeyEntity]),
|
||||||
ContainerEntity,
|
|
||||||
LiveTrackingEntity,
|
|
||||||
ShipmentEntity,
|
|
||||||
]),
|
|
||||||
TrackingModule,
|
TrackingModule,
|
||||||
ShipmentsModule,
|
ShipmentsModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [],
|
||||||
providers: [AppService],
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
{
|
||||||
|
provide: APP_GUARD,
|
||||||
|
useClass: AuthGuard,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { ContainerEntity } from '@/entities/container.entity';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AppService {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(ContainerEntity)
|
|
||||||
private readonly containerRepository: Repository<ContainerEntity>,
|
|
||||||
) {}
|
|
||||||
async debug(): Promise<unknown> {
|
|
||||||
return this.containerRepository.find({
|
|
||||||
take: 5,
|
|
||||||
});
|
|
||||||
|
|
||||||
// return 'Hello World!';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { ApiKeyEntity } from '@/entities/apiKey.entity';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(ApiKeyEntity)
|
||||||
|
private readonly apiKeyRepository: Repository<ApiKeyEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async isAllowed(apiKey: string, apiSecret: string): Promise<boolean> {
|
||||||
|
const apiKeyEntity = await this.apiKeyRepository.findOneBy({
|
||||||
|
apiKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!apiKeyEntity || !apiKeyEntity.enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hmac = crypto.createHmac('sha256', apiSecret);
|
||||||
|
const digest = hmac.update(apiKey).digest('hex');
|
||||||
|
|
||||||
|
return digest === apiKeyEntity.digest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: 'apiKeys',
|
||||||
|
})
|
||||||
|
export class ApiKeyEntity {
|
||||||
|
@PrimaryColumn({
|
||||||
|
type: 'char',
|
||||||
|
length: 25,
|
||||||
|
})
|
||||||
|
apiKey!: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'char',
|
||||||
|
length: 64,
|
||||||
|
})
|
||||||
|
digest!: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'boolean',
|
||||||
|
})
|
||||||
|
enabled!: boolean;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { AuthService } from '@/auth.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest<Request>();
|
||||||
|
const apiCredentials = this.extractCredentialsFromRequestApiKey(request);
|
||||||
|
|
||||||
|
if (!apiCredentials) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.authService.isAllowed(
|
||||||
|
apiCredentials.key,
|
||||||
|
apiCredentials.secret,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractCredentialsFromRequestApiKey(request: Request):
|
||||||
|
| {
|
||||||
|
key: string;
|
||||||
|
secret: string;
|
||||||
|
}
|
||||||
|
| false {
|
||||||
|
const authorization = request.headers.authorization;
|
||||||
|
|
||||||
|
if (!authorization) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [key, secret] = Buffer.from(authorization, 'base64')
|
||||||
|
.toString('utf8')
|
||||||
|
.split(':', 2);
|
||||||
|
|
||||||
|
if (!key || !secret) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
secret,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { ShipmentsController } from './shipments.controller';
|
|
||||||
|
|
||||||
describe('ShipmentsController', () => {
|
|
||||||
let controller: ShipmentsController;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [ShipmentsController],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
controller = module.get<ShipmentsController>(ShipmentsController);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(controller).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -21,7 +21,9 @@ export class ShipmentsController {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
getShipments() {
|
async getShipments() {
|
||||||
return this.shipmentsService.getAll();
|
return {
|
||||||
|
data: await this.shipmentsService.getAll(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ShipmentsController } from '@/shipments/shipments.controller';
|
||||||
|
import { ShipmentsService } from '@/shipments/shipments.service';
|
||||||
|
|
||||||
|
describe('ShipmentsController', () => {
|
||||||
|
let controller: ShipmentsController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [ShipmentsController],
|
||||||
|
})
|
||||||
|
.useMocker((token) => {
|
||||||
|
if (token === ShipmentsService) {
|
||||||
|
return {
|
||||||
|
getAll: jest.fn().mockResolvedValue([]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
controller = module.get<ShipmentsController>(ShipmentsController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a method getShipments', () => {
|
||||||
|
expect(controller.getShipments).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an object with message: ok', async () => {
|
||||||
|
await expect(controller.getShipments()).resolves.toEqual({ data: [] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { ShipmentsService } from './shipments.service';
|
import { ShipmentsService } from '@/shipments/shipments.service';
|
||||||
|
|
||||||
describe('ShipmentsService', () => {
|
describe('ShipmentsService', () => {
|
||||||
let service: ShipmentsService;
|
let service: ShipmentsService;
|
||||||
Loading…
Reference in New Issue