Skip to content

Commit

Permalink
refactor(modules): create display, qr modules
Browse files Browse the repository at this point in the history
- Create display module, move RotatingLinksController into it
- Create qr module, move QrCodeService and assets into it
- Extract QrCodeController from `api/qr` into `modules/qr`
- Inject config, existing inversify dependencies into classes
  wherever possible
  • Loading branch information
LoneRifle committed Feb 2, 2021
1 parent a6c48ab commit 842fb60
Show file tree
Hide file tree
Showing 18 changed files with 154 additions and 113 deletions.
4 changes: 2 additions & 2 deletions src/server/api/links.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import Express from 'express'
import { container } from '../util/inversify'
import { RotatingLinksControllerInterface } from '../controllers/interfaces/RotatingLinksControllerInterface'
import { RotatingLinksController } from '../modules/display'
import { DependencyIds } from '../constants'

const router = Express.Router()

const linksController = container.get<RotatingLinksControllerInterface>(
const linksController = container.get<RotatingLinksController>(
DependencyIds.linksController,
)

Expand Down
38 changes: 4 additions & 34 deletions src/server/api/qrcode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,20 @@ import Joi from '@hapi/joi'
import { createValidator } from 'express-joi-validation'

import ImageFormat from '../../shared/util/image-format'
import { ogUrl } from '../config'
import { QrCodeServiceInterface } from '../services/interfaces/QrCodeServiceInterface'
import { QrCodeController } from '../modules/qr'
import { isValidShortUrl } from '../../shared/util/validation'
import { container } from '../util/inversify'
import { UrlRepositoryInterface } from '../repositories/interfaces/UrlRepositoryInterface'
import { DependencyIds } from '../constants'

const urlRepository = container.get<UrlRepositoryInterface>(
DependencyIds.urlRepository,
const qrCodeController = container.get<QrCodeController>(
DependencyIds.qrCodeController,
)

function isValidFormat(format: string): boolean {
const validFormats = Object.values(ImageFormat) as string[]
return validFormats.includes(format)
}

async function shortUrlExists(shortUrl: string): Promise<boolean> {
return !!(await urlRepository.findByShortUrl(shortUrl))
}

const qrCodeRequestSchema = Joi.object({
url: Joi.string()
.custom((url: string, helpers) => {
Expand All @@ -49,31 +43,7 @@ const validator = createValidator()
router.get(
'/',
validator.query(qrCodeRequestSchema),
async (req, res): Promise<void> => {
const url = req.query.url as string
const format = req.query.format as ImageFormat

if (!(await shortUrlExists(url))) {
res.status(400).send('Short link does not exist')
return
}

// Append base url to short link before creating the qr.
const goShortLink = `${ogUrl}/${url}`

const qrCodeService = container.get<QrCodeServiceInterface>(
DependencyIds.qrCodeService,
)

// Creates the QR code and sends it to the client.
qrCodeService.createGoQrCode(goShortLink, format).then((buffer) => {
// Provides callee a proposed filename for image.
res.set('Filename', goShortLink)
res.contentType(format)
res.end(buffer)
return
})
},
qrCodeController.createGoQrCode,
)

module.exports = router
3 changes: 3 additions & 0 deletions src/server/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const DependencyIds = {
urlManagementService: Symbol.for('urlManagementService'),
userController: Symbol.for('userController'),
qrCodeService: Symbol.for('qrCodeService'),
qrCodeController: Symbol.for('qrCodeController'),
directorySearchService: Symbol.for('directorySearchService'),
directoryController: Symbol.for('directoryController'),
linkStatisticsController: Symbol.for('linkStatisticsController'),
Expand All @@ -46,6 +47,8 @@ export const DependencyIds = {
urlCheckController: Symbol.for('urlCheckController'),
userMessage: Symbol.for('userMessage'),
userAnnouncement: Symbol.for('userAnnouncement'),
linksToRotate: Symbol.for('linksToRotate'),
ogUrl: Symbol.for('ogUrl'),
}

export const ERROR_404_PATH = '404.error.ejs'
16 changes: 0 additions & 16 deletions src/server/controllers/RotatingLinksController.ts

This file was deleted.

This file was deleted.

11 changes: 9 additions & 2 deletions src/server/inversify.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
accessEndpoint,
bucketEndpoint,
cloudmersiveKey,
linksToRotate,
ogUrl,
s3Bucket,
userAnnouncement,
userMessage,
Expand Down Expand Up @@ -38,14 +40,13 @@ import { StatisticsService } from './modules/statistics/services'
import { StatisticsController } from './modules/statistics'

import { GaController } from './controllers/GaController'
import { RotatingLinksController } from './controllers/RotatingLinksController'
import { RotatingLinksController } from './modules/display/RotatingLinksController'
import { SentryController } from './modules/sentry/SentryController'
import { LoginController } from './controllers/LoginController'
import { AuthService } from './services/AuthService'
import { LogoutController } from './controllers/LogoutController'
import { UrlManagementService } from './modules/user/services'
import { UserController } from './modules/user'
import { QrCodeService } from './services/QrCodeService'
import { DirectoryController } from './controllers/DirectoryController'
import { DirectorySearchService } from './services/DirectorySearchService'
import { LinkStatisticsController } from './controllers/LinkStatisticsController'
Expand All @@ -63,6 +64,9 @@ import {
} from './modules/threat/services'
import { FileCheckController, UrlCheckController } from './modules/threat'

import { QrCodeService } from './modules/qr/services'
import { QrCodeController } from './modules/qr'

function bindIfUnbound<T>(
dependencyId: symbol,
impl: { new (...args: any[]): T },
Expand All @@ -77,6 +81,8 @@ export default () => {
container
.bind(DependencyIds.userAnnouncement)
.toConstantValue(userAnnouncement)
container.bind(DependencyIds.linksToRotate).toConstantValue(linksToRotate)
container.bind(DependencyIds.ogUrl).toConstantValue(ogUrl)

bindIfUnbound(DependencyIds.urlRepository, UrlRepository)
bindIfUnbound(DependencyIds.urlMapper, UrlMapper)
Expand All @@ -102,6 +108,7 @@ export default () => {
bindIfUnbound(DependencyIds.urlManagementService, UrlManagementService)
bindIfUnbound(DependencyIds.userController, UserController)
bindIfUnbound(DependencyIds.qrCodeService, QrCodeService)
bindIfUnbound(DependencyIds.qrCodeController, QrCodeController)
bindIfUnbound(DependencyIds.directorySearchService, DirectorySearchService)
bindIfUnbound(DependencyIds.directoryController, DirectoryController)
bindIfUnbound(DependencyIds.deviceCheckService, DeviceCheckService)
Expand Down
21 changes: 21 additions & 0 deletions src/server/modules/display/RotatingLinksController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Express from 'express'
import { inject, injectable } from 'inversify'
import { DependencyIds } from '../../constants'

@injectable()
export class RotatingLinksController {
private linksToRotate?: string

constructor(@inject(DependencyIds.linksToRotate) linksToRotate?: string) {
this.linksToRotate = linksToRotate
}

getRotatingLinks: (_: Express.Request, res: Express.Response) => void = (
_,
res,
) => {
res.send(this.linksToRotate)
}
}

export default RotatingLinksController
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import httpMocks from 'node-mocks-http'
import { RotatingLinksController } from '../../../src/server/controllers/RotatingLinksController'
import { RotatingLinksController } from '../RotatingLinksController'

describe('RotatingLinksController tests', () => {
const linksToRotate = 'testlink1,testlink2,testlink3'
const controller = new RotatingLinksController(linksToRotate)
it('Should return rotating links defined in the application configurations', () => {
const controller = new RotatingLinksController()
const { req, res } = httpMocks.createMocks()
jest.spyOn(res, 'send')
controller.getRotatingLinks(req, res)

expect(res.send).toBeCalledWith('testlink1,testlink2,testlink3')
expect(res.send).toBeCalledWith(linksToRotate)
})
})
4 changes: 4 additions & 0 deletions src/server/modules/display/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
RotatingLinksController,
RotatingLinksController as default,
} from './RotatingLinksController'
58 changes: 58 additions & 0 deletions src/server/modules/qr/QrCodeController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { UrlRepositoryInterface } from '../../repositories/interfaces/UrlRepositoryInterface'

import ImageFormat from '../../../shared/util/image-format'
import { DependencyIds } from '../../constants'

import { QrCodeService } from './interfaces'

@injectable()
export class QrCodeController {
private ogUrl: string

private qrCodeService: QrCodeService

private urlRepository: UrlRepositoryInterface

constructor(
@inject(DependencyIds.ogUrl) ogUrl: string,
@inject(DependencyIds.qrCodeService) qrCodeService: QrCodeService,
@inject(DependencyIds.urlRepository) urlRepository: UrlRepositoryInterface,
) {
this.ogUrl = ogUrl
this.qrCodeService = qrCodeService
this.urlRepository = urlRepository
}

private shortUrlExists: (shortUrl: string) => Promise<boolean> = async (
shortUrl,
) => {
return !!(await this.urlRepository.findByShortUrl(shortUrl))
}

createGoQrCode: (req: Request, res: Response) => Promise<void> = async (
req,
res,
): Promise<void> => {
const url = req.query.url as string
const format = req.query.format as ImageFormat

if (!(await this.shortUrlExists(url))) {
res.status(400).send('Short link does not exist')
return
}

// Append base url to short link before creating the qr.
const goShortLink = `${this.ogUrl}/${url}`

// Creates the QR code and sends it to the client.
const buffer = await this.qrCodeService.createGoQrCode(goShortLink, format)
// Provides callee a proposed filename for image.
res.set('Filename', goShortLink)
res.contentType(format)
res.end(buffer)
}
}

export default QrCodeController
File renamed without changes
4 changes: 4 additions & 0 deletions src/server/modules/qr/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
QrCodeController,
QrCodeController as default,
} from './QrCodeController'
7 changes: 7 additions & 0 deletions src/server/modules/qr/interfaces/QrCodeService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import ImageFormat from '../../../../shared/util/image-format'

export interface QrCodeService {
createGoQrCode: (url: string, format: ImageFormat) => Promise<Buffer>
}

export default QrCodeService
1 change: 1 addition & 0 deletions src/server/modules/qr/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { QrCodeService, QrCodeService as default } from './QrCodeService'
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { select } from 'd3'
import fs from 'fs'
import QRCode from 'qrcode'
import { resolve } from 'path'
import util from 'util'
import sharp from 'sharp'
import { injectable } from 'inversify'

import ImageFormat from '../../../shared/util/image-format'
import { QrCodeServiceInterface } from '../interfaces/QrCodeServiceInterface'
import ImageFormat from '../../../../shared/util/image-format'

import * as interfaces from '../interfaces'

const { JSDOM } = jsdom

Expand All @@ -18,11 +18,8 @@ export const MARGIN_VERTICAL = 85
export const FONT_SIZE = 32
export const LINE_HEIGHT = 1.35

// Convert readFile callback function to promise function.
const readFile = util.promisify(fs.readFile)

@injectable()
export class QrCodeService implements QrCodeServiceInterface {
export class QrCodeService implements interfaces.QrCodeService {
// Build base QR code string without logo.
private makeQrCode: (url: string) => Promise<string> = (url) => {
return QRCode.toString(url, {
Expand Down Expand Up @@ -57,8 +54,8 @@ export class QrCodeService implements QrCodeServiceInterface {
const dom = new JSDOM(`<!DOCTYPE html><body></body>`)

// Read the logo as a string.
const filePath = resolve(__dirname, 'assets/qrlogo.svg')
const logoSvg = await readFile(filePath, 'utf-8')
const filePath = resolve(__dirname, '../assets/qrlogo.svg')
const logoSvg = fs.readFileSync(filePath, 'utf-8')

const body = select(dom.window.document.querySelector('body'))

Expand Down
30 changes: 30 additions & 0 deletions src/server/modules/qr/services/__tests__/QrCodeService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import jsQR from 'jsqr'
import png from 'upng-js'

import ImageFormat from '../../../../../shared/util/image-format'

import { QrCodeService } from '..'

const testUrl = 'https://github.com/opengovsg/GoGovSG'

describe('GoGovSg QR code', () => {
describe('generates accurately', () => {
const qrCodeService = new QrCodeService()

test('png string', async () => {
const buffer = (await qrCodeService.createGoQrCode(
testUrl,
ImageFormat.PNG,
)) as Buffer
const data = png.decode(buffer)
const out = {
data: new Uint8ClampedArray(png.toRGBA8(data)[0]),
height: data.height,
width: data.width,
}
const code = jsQR(out.data, out.width, out.height)
expect(code).not.toBeNull()
expect(code!.data).toEqual(testUrl)
})
})
})
1 change: 1 addition & 0 deletions src/server/modules/qr/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { QrCodeService, QrCodeService as default } from './QrCodeService'
Loading

0 comments on commit 842fb60

Please sign in to comment.