Skip to content

Commit

Permalink
Major restructuring
Browse files Browse the repository at this point in the history
  • Loading branch information
magic-ike committed Jul 5, 2022
1 parent cb4139f commit 47c87ef
Show file tree
Hide file tree
Showing 37 changed files with 547 additions and 393 deletions.
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
SPOTIFY_CLIENT_ID=XXX
SPOTIFY_CLIENT_SECRET=XXX
MONGODB_CONNECTION_STRING=XXX
REDIS_CONNECTION_STRING=XXX
MONGODB_URI=XXX
REDIS_URI=XXX
11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
{
"name": "express-ts-boilerplate",
"name": "spotify-data-card",
"version": "1.0.0",
"description": "",
"main": "dist/server.js",
"main": "dist/index.js",
"scripts": {
"start": "node .",
"dev": "nodemon src/server.ts",
"build": "rimraf dist && tsc -p tsconfig.build.json && copyup 'src/views/**/*.handlebars' 'src/public/**/*' dist",
"dev": "nodemon src/index.ts",
"build": "rimraf dist && tsc -p tsconfig.build.json && copyup 'src/views/**/*.hbs' 'src/public/**/*' dist",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"test": "jest",
"test:coverage": "jest --coverage"
},
"keywords": [],
"author": "",
"author": "Ike Ofoegbu",
"license": "ISC",
"dependencies": {
"axios": "^0.27.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"date-fns": "^2.28.0",
"dotenv": "^16.0.0",
"express": "^4.18.1",
"express-handlebars": "^6.0.5",
Expand Down
39 changes: 19 additions & 20 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,34 @@ import { engine } from 'express-handlebars';
import { setupReactViews } from 'express-tsx-views';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import { setHttpCacheControlHeader } from './middleware/http-cache.middleware';
import pageRouter from './routes/index.route';
import authRouter from './routes/auth/index.route';
import apiRouter from './routes/api/index.route';
import { API_PATH, AUTH_PATH, SITE_TITLE } from './utils/constant.util';
import router from './routes/index.route';
import { HBS_HELPERS } from './config/index.config';

// express app
const app = express();

/**
* proxies
*
* grabs the info provided by a reverse proxy if the express app is running behind one
*/
app.enable('trust proxy');

// view engine: handlebars
app.engine(
'.hbs',
'hbs',
engine({
extname: '.hbs',
defaultLayout: 'main.view.hbs',
helpers: {
siteTitle: SITE_TITLE,
areEqual: (a: any, b: any) => a === b
}
helpers: HBS_HELPERS
})
);
app.set('view engine', '.hbs');
app.set('views', path.join(__dirname, 'views'));

// view engine: tsx
/**
* view engine: tsx
*
* `setupReactViews()` specifies the views directory, which all view engines will use,
* and registers tsx as the default view engine
*/
setupReactViews(app, {
viewsDirectory: path.join(__dirname, 'views')
});
Expand All @@ -46,16 +49,12 @@ app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

// custom middleware
app.use(setHttpCacheControlHeader);
// custom middleware should go here

// static files
app.use(express.static(path.join(__dirname, 'public')));

// routes
app.use('/', pageRouter);
app.use(AUTH_PATH, authRouter);
app.use(API_PATH, apiRouter);
app.use((_req, res) => res.sendStatus(404));
app.use(router);

export default app;
21 changes: 19 additions & 2 deletions src/config/index.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
import {
SITE_TITLE,
HOME_PAGE_VIEW_PATH,
CARD_PAGE_VIEW_PATH
} from '../utils/constant.util';

// server
export const PORT = process.env.PORT || 8080;
export const HBS_HELPERS = {
siteTitle: () => SITE_TITLE,
homePageView: () => HOME_PAGE_VIEW_PATH,
cardPageView: () => CARD_PAGE_VIEW_PATH,
areEqual: (a: any, b: any) => a === b
};

// spotify
export const CLIENT_ID = process.env.SPOTIFY_CLIENT_ID!;
export const CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET!;
export const MONGODB_URI = process.env.MONGODB_CONNECTION_STRING!;
export const REDIS_URI = process.env.REDIS_CONNECTION_STRING!;

// db
export const MONGODB_URI = process.env.MONGODB_URI!;
export const REDIS_URI = process.env.REDIS_URI!;
8 changes: 4 additions & 4 deletions src/controllers/api/card.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import { RequestHandler, Response } from 'express';
import { disableHttpCaching } from '../../middleware/http-cache.middleware';
import TokenMap from '../../models/token-map.model';
import User from '../../models/user.model';
import Image from '../../models/image.model';
import CardGetRequestQueryParams from '../../interfaces/card-get-request-query-params.interface';
import CardDeleteRequestQueryParams from '../../interfaces/card-delete-request-query-params.interface';
import Track from '../../interfaces/track.interface';
import Artist from '../../interfaces/artist.interface';
import DataCardProps from '../../interfaces/data-card-props.interface';
import { SHORT_URL } from '../../utils/constant.util';
import { CARD_VIEW_PATH, SHORT_URL } from '../../utils/constant.util';
import { boolFromString, boundedIntFromString } from '../../utils/string.util';

const DEFAULT_ITEM_COUNT = 5;
const MIN_ITEM_COUNT = 1;
const MAX_ITEM_COUNT = 10;
const CARD_VIEW_PATH = 'api/card.view.tsx';

// renders a data card
export const card_get: RequestHandler = async (req, res) => {
Expand Down Expand Up @@ -163,7 +163,7 @@ export const card_get: RequestHandler = async (req, res) => {
if (showNowPlaying || showRecentlyPlayed) disableHttpCaching(res);

// render data card
const imageDataMap = await User.getImageDataMapFromItems([
const imageDataMap = await Image.getImageDataMap([
nowPlaying,
...recentlyPlayed,
...topTracks,
Expand Down Expand Up @@ -249,7 +249,7 @@ export const card_delete: RequestHandler = async (req, res) => {
res.send('Data card deleted successfully.');
};

// helper functions
// helpers

const renderErrorCard = (
res: Response,
Expand Down
3 changes: 1 addition & 2 deletions src/controllers/auth/index.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const auth_login: RequestHandler = (req, res) => {
const scope =
'user-read-currently-playing user-read-recently-played user-top-read';
const state = generateRandomString(16);

res.cookie(STATE_KEY, state, { httpOnly: true });
res.redirect(
'https://accounts.spotify.com/authorize?' +
Expand Down Expand Up @@ -91,7 +90,7 @@ export const auth_callback: RequestHandler = async (req, res) => {
redirectToHomePageWithCreds(res, userId, refreshToken);
};

// helper functions
// helpers

const redirectToHomePageWithError = (res: Response, error: string) => {
res.redirect('/#' + stringify({ error }));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { RequestHandler } from 'express';
import { HbsViewProps } from '../interfaces/hbs-view-props.interface';
import { SITE_TITLE, SPOTIFY_ICON_PATH } from '../utils/constant.util';
import { getFullUrl, getUrl } from '../utils/url.util';
import { HbsViewProps } from '../../interfaces/hbs-view-props.interface';
import {
SITE_TITLE,
CARD_PAGE_VIEW_PATH,
SPOTIFY_ICON_PATH
} from '../../utils/constant.util';
import { getFullUrl, getUrl } from '../../utils/url.util';

export const card_index: RequestHandler = (req, res) => {
const siteUrl = getUrl(req);
Expand All @@ -12,5 +16,5 @@ export const card_index: RequestHandler = (req, res) => {
pageUrl,
siteImage: siteUrl + SPOTIFY_ICON_PATH
};
res.render('card/index.view.hbs', { ...props });
res.render(CARD_PAGE_VIEW_PATH + '.hbs', { ...props });
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { RequestHandler } from 'express';
import { HbsViewProps } from '../interfaces/hbs-view-props.interface';
import { SITE_TITLE, SPOTIFY_ICON_PATH } from '../utils/constant.util';
import { getFullUrl, getUrl } from '../utils/url.util';
import { HbsViewProps } from '../../interfaces/hbs-view-props.interface';
import {
SITE_TITLE,
HOME_PAGE_VIEW_PATH,
SPOTIFY_ICON_PATH
} from '../../utils/constant.util';
import { getFullUrl, getUrl } from '../../utils/url.util';

export const index: RequestHandler = (req, res) => {
const siteUrl = getUrl(req);
Expand All @@ -12,5 +16,5 @@ export const index: RequestHandler = (req, res) => {
pageUrl,
siteImage: siteUrl + SPOTIFY_ICON_PATH
};
res.render('home/index.view.hbs', { ...props });
res.render(HOME_PAGE_VIEW_PATH + '.hbs', { ...props });
};
24 changes: 24 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* according to the dotenv docs and "The Twelve-Factor App" methodology, an app's config (stored in environment variables) is
* unique to each environment and should be kept separate from code. therefore:
* 1. there should only be 1 `.env` file per environement: https://github.com/motdotla/dotenv#should-i-have-multiple-env-files
* 2. `.env` files should never be committed to version control: https://github.com/motdotla/dotenv#should-i-commit-my-env-file
* `.env.example` files are an obvious exception. further reading: https://12factor.net/config
*/
import 'dotenv/config';
import mongoose from 'mongoose';
import app from './app';
import Redis from './models/redis.model';
import { PORT, MONGODB_URI } from './config/index.config';

mongoose
.connect(MONGODB_URI)
.then(() => {
return Redis.connect();
})
.then(() => {
app.listen(PORT, () =>
console.log(`running server on http://localhost:${PORT}`)
);
})
.catch((error) => console.log(error));
22 changes: 3 additions & 19 deletions src/middleware/http-cache.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,12 @@
import { RequestHandler, Response } from 'express';
import { API_PATH, AUTH_PATH, CARD_PATH } from '../utils/constant.util';

const ONE_DAY = 60 * 60 * 24;
const THREE_HOURS = 60 * 60 * 3;
const ONE_HOUR = 60 * 60;
const HALF_HOUR = 60 * 30;

export const setHttpCacheControlHeader: RequestHandler = (req, res, next) => {
if (req.method !== 'GET' || req.path.startsWith(AUTH_PATH)) {
disableHttpCaching(res);
next();
return;
}

let maxAge = ONE_DAY;
let staleWhileReval = THREE_HOURS;
if (req.path.startsWith(API_PATH + CARD_PATH)) {
maxAge = ONE_HOUR;
staleWhileReval = HALF_HOUR;
}
const HALF_HOUR = ONE_HOUR / 2;

export const setHttpCacheControlHeader: RequestHandler = (_req, res, next) => {
res.set(
'Cache-control',
`public, max-age=${maxAge}, stale-while-revalidate=${staleWhileReval}`
`public, max-age=${ONE_HOUR}, stale-while-revalidate=${HALF_HOUR}`
);
next();
};
Expand Down
28 changes: 14 additions & 14 deletions src/models/auth.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,6 @@ const AUTH_CODE = 'authorization_code';
const REFRESH_TOKEN = 'refresh_token';

export default class Auth {
static getAccessTokenWithAuthCode(authCode: string, redirectUri: string) {
return this.#getAccessToken(AUTH_CODE, authCode, redirectUri);
}

static getAccessTokenWithRefreshToken(refreshToken: string) {
return this.#getAccessToken(
REFRESH_TOKEN,
undefined,
undefined,
refreshToken
);
}

static #getAccessToken(
grantType: typeof AUTH_CODE | typeof REFRESH_TOKEN,
authCode?: string,
Expand All @@ -28,7 +15,7 @@ export default class Auth {
): Promise<AccessTokenResponseBody> {
return new Promise(async (resolve, reject) => {
// choose payload based on grant type
let data = {};
let data;
if (grantType === AUTH_CODE) {
data = {
grant_type: grantType,
Expand Down Expand Up @@ -66,4 +53,17 @@ export default class Auth {
resolve(response.data);
});
}

static getAccessTokenWithAuthCode(authCode: string, redirectUri: string) {
return this.#getAccessToken(AUTH_CODE, authCode, redirectUri);
}

static getAccessTokenWithRefreshToken(refreshToken: string) {
return this.#getAccessToken(
REFRESH_TOKEN,
undefined,
undefined,
refreshToken
);
}
}
20 changes: 20 additions & 0 deletions src/models/image.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Redis from './redis.model';
import { Item, isTrack } from '../interfaces/item.interface';
import StringMap from '../interfaces/map.interface';
import { getBase64DataFromImageUrl } from '../utils/image.util';

export default class Image {
static async getImageDataMap(items: Item[]) {
const map: StringMap = {};
for (const item of items) {
if (!item) continue;
const imageUrl = isTrack(item) ? item.albumImageUrl : item.imageUrl;
const imageUrlArray = imageUrl.split('/');
const imageId = imageUrlArray[imageUrlArray.length - 1];
map[imageUrl] = await Redis.getImageDataFromOrSaveToCache(imageId, () => {
return getBase64DataFromImageUrl(imageUrl);
});
}
return map;
}
}
Loading

0 comments on commit 47c87ef

Please sign in to comment.