Added Authentication

This commit is contained in:
Thom Werring 2024-03-26 21:32:33 +01:00
parent d7728f1ca4
commit 64e7fd66fc
18 changed files with 370 additions and 93 deletions

View File

@ -1,2 +1,4 @@
database
coverage
tech-assignment-data
node_modules

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
/database
/database/lib
.eslintcache
# compiled output

View File

@ -5,7 +5,7 @@ WORKDIR /usr/src/app
COPY package*.json .
RUN npm ci
RUN npm ci --no-auditdc po
COPY . .

View File

@ -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
flowchart TD
receiveTracking(["Receive Tracking"])
@ -26,8 +30,12 @@ containerNumber:**string**
%% isArrivingSoon -->|yes| sendArriveSoon
```
```mermaid
flowchart
getShipments([Get all shipments])
```
## Possible improvements
- 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

24
package-lock.json generated
View File

@ -10,6 +10,7 @@
"license": "UNLICENSED",
"dependencies": {
"@nestjs/common": "^10.3.5",
"@nestjs/config": "^3.2.1",
"@nestjs/core": "^10.3.5",
"@nestjs/platform-express": "^10.3.5",
"@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": {
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.5.tgz",
@ -4054,6 +4070,14 @@
"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": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",

View File

@ -21,6 +21,7 @@
},
"dependencies": {
"@nestjs/common": "^10.3.5",
"@nestjs/config": "^3.2.1",
"@nestjs/core": "^10.3.5",
"@nestjs/platform-express": "^10.3.5",
"@nestjs/swagger": "^7.3.0",
@ -67,8 +68,12 @@
"ts"
],
"rootDir": ".",
"roots": ["<rootDir>/tests"],
"testPathIgnorePatterns": ["./database"],
"roots": [
"<rootDir>/tests"
],
"testPathIgnorePatterns": [
"./database"
],
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"

View File

@ -1,5 +1,7 @@
POST http://localhost:3000/tracking
Accept: application/json
Content-Type: application/json
Authorization: eG5nVnZGaWtCMFN6NjRRaWQ2TUJsUTJWaDpjeGg1R3FKRUl0UTlzSGRQdElGMGg0bFJp
{
"carrierName": "Evergreen",
@ -8,3 +10,9 @@ Content-Type: application/json
"arrivalDateEstimate": "2023-02-14T15:00:00.000Z",
"containerNumber": "EGLU866139144"
}
###
GET http://localhost:3000/shipments
Accept: application/json
Authorization: eG5nVnZGaWtCMFN6NjRRaWQ2TUJsUTJWaDpjeGg1R3FKRUl0UTlzSGRQdElGMGg0bFJp

View File

@ -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();
}
}

View File

@ -1,35 +1,51 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ContainerEntity } from '@/entities/container.entity';
import { LiveTrackingEntity } from '@/entities/liveTracking.entity';
import { ShipmentEntity } from '@/entities/shipment.entity';
import { TrackingModule } from './tracking/tracking.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({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 13306,
username: 'root',
password: 'myfreight',
database: 'myfreight',
entities: [ContainerEntity, LiveTrackingEntity, ShipmentEntity],
synchronize: true,
logging: false,
TypeOrmModule.forRootAsync({
imports: [ConfigModule.forRoot()],
useFactory: async (configService: ConfigService) => {
return {
type: 'mysql',
host: configService.get('DB_HOST'),
port: Number(configService.get('DB_PORT')),
username: configService.get('DB_USER'),
password: configService.get('DB_PASS'),
database: configService.get('DB_DABA'),
entities: [
ContainerEntity,
LiveTrackingEntity,
ShipmentEntity,
ApiKeyEntity,
],
synchronize: true,
logging: false,
};
},
inject: [ConfigService],
}),
TypeOrmModule.forFeature([
ContainerEntity,
LiveTrackingEntity,
ShipmentEntity,
]),
TypeOrmModule.forFeature([ApiKeyEntity]),
TrackingModule,
ShipmentsModule,
],
controllers: [AppController],
providers: [AppService],
controllers: [],
providers: [
AuthService,
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
})
export class AppModule {}

View File

@ -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!';
}
}

28
src/auth.service.ts Normal file
View File

@ -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;
}
}

View File

@ -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;
}

47
src/guards/auth.guard.ts Normal file
View File

@ -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,
};
}
}

View File

@ -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);
});
});

View File

@ -21,7 +21,9 @@ export class ShipmentsController {
},
},
})
getShipments() {
return this.shipmentsService.getAll();
async getShipments() {
return {
data: await this.shipmentsService.getAll(),
};
}
}

View File

@ -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: [] });
});
});

View File

@ -1,5 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ShipmentsService } from './shipments.service';
import { ShipmentsService } from '@/shipments/shipments.service';
describe('ShipmentsService', () => {
let service: ShipmentsService;