Skip to content

Commit

Permalink
refactor: re-org RedirectController and its dependencies (#184)
Browse files Browse the repository at this point in the history
  • Loading branch information
JasonChong96 authored and LoneRifle committed Jun 17, 2020
1 parent 4aed66d commit 4b85d63
Show file tree
Hide file tree
Showing 15 changed files with 326 additions and 147 deletions.
119 changes: 0 additions & 119 deletions src/server/api/redirect.ts

This file was deleted.

3 changes: 3 additions & 0 deletions src/server/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const DependencyIds = {
s3Bucket: Symbol.for('s3Bucket'),
s3Client: Symbol.for('s3Client'),
fileURLPrefix: Symbol.for('fileURLPrefix'),
redirectService: Symbol.for('redirectService'),
crawlerCheckService: Symbol.for('crawlerCheckService'),
redirectController: Symbol.for('redirectController'),
}

export default DependencyIds
96 changes: 96 additions & 0 deletions src/server/controllers/RedirectController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Express from 'express'
import { inject, injectable } from 'inversify'
import { logger } from '../config'
import { NotFoundError } from '../util/error'
import parseDomain from '../util/domain'
import { DependencyIds } from '../constants'
import { AnalyticsLogger } from '../services/analyticsLogger'
import { RedirectControllerInterface } from './interfaces/RedirectControllerInterface'
import { RedirectService } from '../services/RedirectService'
import { RedirectType } from '../services/types'

const ERROR_404_PATH = '404.error.ejs'
const TRANSITION_PATH = 'transition-page.ejs'

@injectable()
export class RedirectController implements RedirectControllerInterface {
private redirectService: RedirectService

private analyticsLogger: AnalyticsLogger

public constructor(
@inject(DependencyIds.redirectService) redirectService: RedirectService,
@inject(DependencyIds.analyticsLogging) analyticsLogger: AnalyticsLogger,
) {
this.redirectService = redirectService
this.analyticsLogger = analyticsLogger
}

public redirect: (
req: Express.Request,
res: Express.Response,
) => Promise<void> = async (req, res) => {
const { shortUrl }: { shortUrl: string } = req.params

// Short link must not be null
if (!shortUrl) {
res.status(404).render(ERROR_404_PATH, { shortUrl })
return
}

try {
// Find longUrl to redirect to
const {
longUrl,
visitedUrls,
redirectType,
} = await this.redirectService.redirectFor(
shortUrl,
req.session!.visits,
req.get('user-agent') || '',
)

req.session!.visits = visitedUrls

this.analyticsLogger.logRedirectAnalytics(
req,
res,
shortUrl.toLowerCase(),
longUrl,
)

if (redirectType === RedirectType.TransitionPage) {
// Extract root domain from long url.
const rootDomain: string = parseDomain(longUrl)

res.status(200).render(TRANSITION_PATH, {
escapedLongUrl: RedirectController.encodeLongUrl(longUrl),
rootDomain,
})
} else {
res.status(302).redirect(longUrl)
}
} catch (error) {
if (!(error instanceof NotFoundError)) {
logger.error(
`Redirect error: ${error} ${error instanceof NotFoundError}`,
)
}

res.status(404).render(ERROR_404_PATH, { shortUrl })
}
}

/**
* Encodes the long URL to be template safe. Currently this function
* only templates the double-quote character as it is invalid according
* to [RFC 3986](https://tools.ietf.org/html/rfc3986#appendix-C).
* @param {string} longUrl The long URL before templating.
* @return {string} The template-safe URL.
*/
private static encodeLongUrl(longUrl: string) {
return longUrl.replace(/["]/g, encodeURIComponent)
}
}

export default RedirectController
10 changes: 10 additions & 0 deletions src/server/controllers/interfaces/RedirectControllerInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Express from 'express'

export interface RedirectControllerInterface {
/**
* The redirect function.
* @param {Object} req Express request object.
* @param {Object} res Express response object.
*/
redirect(req: Express.Request, res: Express.Response): Promise<void>
}
6 changes: 4 additions & 2 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ bindInversifyDependencies()

// Routes
import api from './api'
import redirect from './api/redirect'

// Logger configuration
import { cookieSettings, logger, sessionSettings, trustProxy } from './config'
Expand All @@ -35,6 +34,7 @@ import getIp from './util/request'
import { container } from './util/inversify'
import { DependencyIds } from './constants'
import { Mailer } from './services/email'
import { RedirectControllerInterface } from './controllers/interfaces/RedirectControllerInterface'
// Define our own token for client ip
// req.headers['cf-connecting-ip'] : Cloudflare

Expand Down Expand Up @@ -125,7 +125,9 @@ initDb()
app.use(
'/:shortUrl([a-zA-Z0-9-]+)',
...redirectSpecificMiddleware,
redirect,
container.get<RedirectControllerInterface>(
DependencyIds.redirectController,
).redirect,
) // The Redirect Endpoint
app.use((req, res) => {
const shortUrl = req.path.slice(1)
Expand Down
10 changes: 8 additions & 2 deletions src/server/inversify.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import AWS from 'aws-sdk'
import { container } from './util/inversify'
import { GaLogger } from './services/analyticsLogger'
import { DependencyIds } from './constants'
import { CookieArrayReducer } from './services/transition-page'
import { CookieArrayReducerService } from './services/CookieArrayReducerService'
import { OtpRepository } from './repositories/OtpRepository'
import { MailerNode } from './services/email'
import { CryptographyBcrypt } from './services/cryptography'
Expand All @@ -15,6 +15,9 @@ import { UserRepository } from './repositories/UserRepository'
import { UrlMapper } from './mappers/UrlMapper'
import { UserMapper } from './mappers/UserMapper'
import { OtpMapper } from './mappers/OtpMapper'
import { RedirectService } from './services/RedirectService'
import { RedirectController } from './controllers/RedirectController'
import { CrawlerCheckService } from './services/CrawlerCheckService'

function bindIfUnbound<T>(
dependencyId: symbol,
Expand All @@ -31,10 +34,13 @@ export default () => {
bindIfUnbound(DependencyIds.userMapper, UserMapper)
bindIfUnbound(DependencyIds.otpMapper, OtpMapper)
bindIfUnbound(DependencyIds.analyticsLogging, GaLogger)
bindIfUnbound(DependencyIds.cookieReducer, CookieArrayReducer)
bindIfUnbound(DependencyIds.cookieReducer, CookieArrayReducerService)
bindIfUnbound(DependencyIds.otpRepository, OtpRepository)
bindIfUnbound(DependencyIds.userRepository, UserRepository)
bindIfUnbound(DependencyIds.cryptography, CryptographyBcrypt)
bindIfUnbound(DependencyIds.redirectController, RedirectController)
bindIfUnbound(DependencyIds.redirectService, RedirectService)
bindIfUnbound(DependencyIds.crawlerCheckService, CrawlerCheckService)

container.bind(DependencyIds.s3Bucket).toConstantValue(s3Bucket)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { cookieSessionMaxSizeBytes } from '../config'
* Utility functions to store and read a user's visit
* history in the browser cookie.
*/
export interface CookieReducer {
export interface CookieArrayReducerServiceInterface {
userHasVisitedShortlink: (
cookie: string[] | null,
shortUrl: string,
Expand All @@ -32,7 +32,8 @@ export interface CookieReducer {
/* eslint class-methods-use-this: ["error", { "exceptMethods":
["userHasVisitedShortlink", "writeShortlinkToCookie"] }] */
@injectable()
export class CookieArrayReducer implements CookieReducer {
export class CookieArrayReducerService
implements CookieArrayReducerServiceInterface {
userHasVisitedShortlink(cookie: string[] | null, shortUrl: string): boolean {
if (!cookie) return false
return cookie.includes(shortUrl)
Expand Down
17 changes: 17 additions & 0 deletions src/server/services/CrawlerCheckService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { UAParser } from 'ua-parser-js'
import { injectable } from 'inversify'
import { CrawlerCheckServiceInterface } from './interfaces/CrawlerCheckServiceInterface'

@injectable()
export class CrawlerCheckService implements CrawlerCheckServiceInterface {
public isCrawler: (userAgent: string) => boolean = (userAgent) => {
const parser = new UAParser(userAgent)
const result = parser.getResult()
if (result.browser.name && result.engine.name && result.os.name) {
return false
}
return true
}
}

export default CrawlerCheckService
Loading

0 comments on commit 4b85d63

Please sign in to comment.