Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
SHORT_DOMAIN_HOST
SHORT_DOMAIN_SCHEMA
SHORT_DOMAIN_PORT
GEOLITE_LICENSE_KEY
BACKEND_PORT
REDIS_PORT
URL_API_KEY
13 changes: 13 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ mkdir ./keys && cd ./keys
```bash
# starts dev env
rush docker-up:dev
# or
sh ./scripts/url-shortner-docker-start.sh
# start UI project separately
cd ./apps/chat-frontend/ && rushx start:dev
```

Custom Docker command scripts

```bash
# will startup all containers with the required data
sh ./scripts/url-shortner-docker-start.sh
# will shutdown all of the containers for dev
sh ./scripts/shutdown-docker.sh
```

Url Shortner docs: https://shlink.io/documentation/
5 changes: 4 additions & 1 deletion apps/chat-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
"socket.io": "^4.0.0",
"redis": "~3.1.2",
"socket.io-redis": "~6.1.1",
"jsonwebtoken": "~8.5.1"
"jsonwebtoken": "~8.5.1",
"@nestjs/axios": "~0.0.2",
"axios": "~0.21.4",
"rxjs": "~7.3.0"
},
"devDependencies": {
"@nestjs/cli": "^8.0.0",
Expand Down
8 changes: 5 additions & 3 deletions apps/chat-backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { ChatGateway } from './chat/chat.gateway';
import { AlertGateway } from './alert/alert.gateway';
import { AlertController } from './alert/alert.controller';
import { JwtTokenService } from './services/jwt-token/jwt-token.service';

import { UrlShortnerService } from './services/url-shortner/url-shortner.service';
import { HttpModule } from '@nestjs/axios';
import { RedisService } from './services/redis-service/redis-service.service';
@Module({
imports: [ConfigModule.forRoot()],
imports: [ConfigModule.forRoot(), HttpModule],
controllers: [AlertController],
providers: [ChatGateway, AlertGateway, JwtTokenService]
providers: [ChatGateway, AlertGateway, JwtTokenService, UrlShortnerService, RedisService]
})
export class AppModule {}
48 changes: 44 additions & 4 deletions apps/chat-backend/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { Socket, Server } from 'socket.io';
import { Logger } from '@nestjs/common';
import { AuthFormat, MessageFormat } from 'src/shared/types';
import { JwtTokenService, TokenInput } from 'src/services/jwt-token/jwt-token.service';
import { UrlShortnerService } from 'src/services/url-shortner/url-shortner.service';
import { CreateChatRoomPayload } from './models/chatroom-payload.model';
import { CreateUserUrlPayload } from './models/user-url-payload.model';
import { RedisService } from 'src/services/redis-service/redis-service.service';
import { ChatEvents } from './models/chat-events';

@WebSocketGateway(3001, {
cors: true,
Expand All @@ -14,13 +19,48 @@ export class ChatGateway implements OnGatewayInit {
@WebSocketServer() wss: Server;
private logger: Logger = new Logger('ChatGateway');

constructor(private jwtTokenService: JwtTokenService) {}
constructor(
private jwtTokenService: JwtTokenService,
private urlShortnerService: UrlShortnerService,
private redisService: RedisService
) {}

afterInit(): void {
this.logger.log('Initialized Gateway');
}

@SubscribeMessage('login')
@SubscribeMessage(ChatEvents.createChatRoom)
async createChatRoom(client: Socket, message: CreateChatRoomPayload): Promise<void> {
// TODO need to add this to redis through a new connection, will need to remove on chatRoomDestroy or some event
try {
this.logger.log('Creating chatroom', {
roomId: message.roomId
});
await this.redisService.set(message.roomId, 'true');
client.emit(ChatEvents.createChatRoom, {
roomId: message.roomId
});
} catch (err) {
this.logger.error(err);
// TODO send error notification
}
}

@SubscribeMessage(ChatEvents.createUserUrl)
async createUserUrl(client: Socket, message: CreateUserUrlPayload): Promise<void> {
try {
this.logger.log('Creating short url for user', {
roomId: message.roomId
});
const shortenedUrl = await this.urlShortnerService.createShortUrl(message.url);
client.emit('createUserUrl', shortenedUrl);
} catch (err) {
// TODO this should emit an error notification at the very least
this.logger.error(err);
}
}

@SubscribeMessage(ChatEvents.login)
async handleLogin(client: Socket, message: AuthFormat): Promise<void> {
try {
this.logger.verbose('Recieved Login attempt from client', {
Expand All @@ -42,7 +82,7 @@ export class ChatGateway implements OnGatewayInit {
}
}

@SubscribeMessage('logout')
@SubscribeMessage(ChatEvents.logout)
async handleLogout(client: Socket, message: AuthFormat): Promise<void> {
try {
this.logger.verbose('Recieved Logout attempt from client', {
Expand All @@ -59,7 +99,7 @@ export class ChatGateway implements OnGatewayInit {
}
}

@SubscribeMessage('chatToServer')
@SubscribeMessage(ChatEvents.chatToServer)
async handleMessage(client: Socket, message: MessageFormat): Promise<void> {
try {
this.logger.verbose('Recieved Message from client', {
Expand Down
7 changes: 7 additions & 0 deletions apps/chat-backend/src/chat/models/chat-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum ChatEvents {
createChatRoom = 'createChatRoom',
createUserUrl = 'createUserUrl',
login = 'login',
logout = 'logout',
chatToServer = 'chatToServer'
}
3 changes: 3 additions & 0 deletions apps/chat-backend/src/chat/models/chatroom-payload.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface CreateChatRoomPayload {
roomId: string;
}
4 changes: 4 additions & 0 deletions apps/chat-backend/src/chat/models/user-url-payload.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface CreateUserUrlPayload {
url: string;
roomId: string;
}
2 changes: 1 addition & 1 deletion apps/chat-backend/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import { RedisIoAdapter } from './chat/RedisIoAdapter';
import { RedisIoAdapter } from './redis/redis-io-adapter';

async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { RedisClient } from 'redis';
import { ServerOptions } from 'socket.io';
import { createAdapter } from 'socket.io-redis';

const pubClient = new RedisClient({ host: 'redis', port: 6379 });
const REDIS_PORT = Number(process.env.REDIS_PORT || 6379);
const pubClient = new RedisClient({ host: 'redis', port: REDIS_PORT });
const subClient = pubClient.duplicate();
const redisAdapter = createAdapter({ pubClient, subClient });

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RedisService } from './redis-service.service';

describe('RedisServiceService', () => {
let service: RedisService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [RedisService]
}).compile();

service = module.get<RedisService>(RedisService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import { RedisClient } from 'redis';
import { promisify } from 'util';

@Injectable()
export class RedisService {
private client: RedisClient;

constructor() {
const REDIS_PORT = Number(process.env.REDIS_PORT || 6379);
this.client = new RedisClient({ host: 'redis', port: REDIS_PORT });
}

get(key: string): Promise<string> {
return promisify(this.client.get)(key);
}

set(key: string, value: string): void {
promisify(this.client.set)(key, value);
}

delete(key: string): Promise<void> {
return new Promise((resolve, reject) => {
return this.client.del(key, (err) => {
if (err) {
return reject(err);
}
return resolve();
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { HttpModule, HttpService } from '@nestjs/axios';
import { AxiosResponse } from 'axios';
import { Test, TestingModule } from '@nestjs/testing';
import { of } from 'rxjs';
import { UrlShortnerResponse, UrlShortnerService } from './url-shortner.service';

describe('UrlShortnerService', () => {
let service: UrlShortnerService;
let httpPostSpy: jest.SpyInstance;

const data = {
shortCode: 'short',
shortUrl: 'short',
longUrl: 'long',
dateCreated: new Date().toISOString()
} as UrlShortnerResponse;
const response: AxiosResponse<UrlShortnerResponse> = {
data,
headers: {},
config: { url: 'http://localhost:3000/mockUrl' },
status: 200,
statusText: 'OK'
};
beforeEach(async () => {
httpPostSpy = jest.spyOn(HttpService.prototype, 'post').mockImplementation(() => of(response));
const module: TestingModule = await Test.createTestingModule({
providers: [UrlShortnerService],
imports: [HttpModule]
}).compile();

service = module.get<UrlShortnerService>(UrlShortnerService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

it('should go through happy path and return response', async () => {
const resp = await service.createShortUrl('test/url');
expect(resp).toEqual(data);
});

it('should throw if return status is not 200', async () => {
httpPostSpy.mockImplementation(() =>
of({
...response,
status: 403
})
);
expect(service.createShortUrl('test/url')).rejects.toMatch('Failed response status');
});
it('should reject if http call returns error', async () => {
httpPostSpy.mockRejectedValue('Err');
expect(service.createShortUrl('test/url')).rejects.toMatch('Err');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';

export interface UrlShortnerResponse {
shortCode: string;
shortUrl: string;
longUrl: string;
dateCreated: string;
}

@Injectable()
export class UrlShortnerService {
private baseUrl: string;
private logger: Logger = new Logger('Url-Shortner-Service');
constructor(private httpService: HttpService) {
this.baseUrl = `${process.env.SHORT_DOMAIN_SCHEMA}://${process.env.SHORT_DOMAIN_HOST}`;
}

async createShortUrl(url: string): Promise<UrlShortnerResponse> {
try {
const resp = await firstValueFrom(
this.httpService.post<UrlShortnerResponse>(
`${this.baseUrl}/rest/v2/short-urls`,
{
longUrl: url
},
{
headers: {
'X-Api-Key': `{${process.env.URL_SHORTNER_API_KEY}}`
}
}
)
);
if (resp.status !== 200) {
throw Error('Failed response status');
}
return resp.data;
} catch (err) {
this.logger.error(err);
throw err;
}
}
}
1 change: 1 addition & 0 deletions common-services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ services:
ports:
- '80:80'
- '443:443'
restart: always
volumes:
- ./certs/:/etc/nginx/ssl
redis-base:
Expand Down
Loading