Skip to content
Draft
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
23 changes: 16 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
## if set to development, morgan middleware will log every request
## if set to development, will briefly log every request
NODE_ENV=production

### optional properties. default shown. enable by removing # in front of them.
## if using docker-compose.yml, make sure the port there is also the same as that in here.
# PORT=3000
#
## duration for which static files' cached copies are valid in the browser(eg: 1m, 3600, '2 days')
# CACHE_PERIOD=1h
# CACHE_PERIOD=1y
#
## user agent and accept header that quora will see
# AXIOS_USER_AGENT='axios/0.26.1'
# AXIOS_ACCEPT='application/json, text/plain, */*'
# USER_AGENT='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'
# ACCEPT='text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
# ACCEPT_ENCODING='gzip, deflate, br, zstd'
#
## for caching api responses using redis
# REDIS_URL=localhost:6379 # if left unset, there'll be no caching
# REDIS_TTL=3600
# REDIS_URL= #localhost:6379
# REDIS_TTL=3600 # in seconds
#
## cooldown when rate-limited from upstream in milliseconds
# RATE_LIMIT_COOLDOWN=7200000 # 2 hours

### for specific use-cases.
#
## add any value here (e.g.: 1, true, 'por favor') if you're using any service where http is the preferred method(e.g.: tor, i2p). else leave it blank
NO_UPGRADE=
# NO_UPGRADE=
36 changes: 8 additions & 28 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
////////////////////////////////////////////////////////
// IMPORTS
////////////////////////////////////////////////////////
import express from 'express';
import morgan from 'morgan';
import helmet from 'helmet';
Expand All @@ -10,28 +7,24 @@ import viewRouter from './routes/viewRoutes.js';
import apiRouter from './routes/apiRoutes.js';
import globalErrorHandler from './controllers/errorController.js';
import AppError from './utils/AppError.js';
import { formatReq } from './middlewares/middlewares.js';
import env from './utils/env.js';

////////////////////////////////////////////////////////
// CREATING AND CONFIGURING APP
////////////////////////////////////////////////////////
// 0. CREATING APP
const app = express();

// 1. IMPORTANT MIDDLWARES
app.use(compression()); // compressing responses
app.use(compression());
app.use(
helmet({
contentSecurityPolicy: {
directives: {
'block-all-mixed-content': null, // deprecated.
'upgrade-insecure-requests': process.env.NO_UPGRADE ? null : [],
'upgrade-insecure-requests': env.NO_UPGRADE ? null : [],
},
},
crossOriginEmbedderPolicy: false,
})
); // using sane headers on response

// 2. SETTING VIEW ENGINE AND PATH TO STATIC ASSETS
app.set('view engine', 'pug');
const pathToViews = fileURLToPath(
new URL('./views/pug/pages', import.meta.url)
Expand All @@ -42,33 +35,20 @@ const pathToPublicDirectory = fileURLToPath(
);
app.use(
express.static(pathToPublicDirectory, {
maxAge: process.env.CACHE_PERIOD || '1h',
maxAge: env.CACHE_PERIOD,
})
);
if (env.NODE_ENV === 'development') app.use(morgan('dev'));

// 3. MISC MIDDLEWARES
if (process.env.NODE_ENV === 'development') app.use(morgan('dev')); // for logging during development
// middleware to add baseUrl to req object
app.use((req, res, next) => {
req.urlObj = new URL(req.originalUrl, `${req.protocol}://${req.get('host')}`);
next();
});
app.use(formatReq);

// 4. MIDDLWARES FOR ROUTES
app.use('/', viewRouter);
app.use('/api/v1/', apiRouter);

////////////////////////////////////////////////////////
// HANDLING ERRORS
////////////////////////////////////////////////////////
// for all other routes, throwing error
app.all('*', (req, res, next) => {
app.all('*', (req, _res, next) => {
next(new AppError(`this route(${req.originalUrl}) doesn't exist`, 404));
});

app.use(globalErrorHandler);

////////////////////////////////////////////////////////
// EXPORTS
////////////////////////////////////////////////////////
export default app;
67 changes: 11 additions & 56 deletions controllers/apiController.js
Original file line number Diff line number Diff line change
@@ -1,87 +1,42 @@
/* eslint-disable no-unused-vars */
////////////////////////////////////////////////////////
// IMPORTS
////////////////////////////////////////////////////////
import getAxiosInstance from '../utils/getAxiosInstance.js';
import axiosInstance from '../utils/axiosInstance.js';
import catchAsyncErrors from '../utils/catchAsyncErrors.js';
import getAnswers from '../fetchers/getAnswers.js';
import getTopic from '../fetchers/getTopic.js';
import getProfile from '../fetchers/getProfile.js';
import getOrSetCache from '../utils/getOrSetCache.js';
import { answersKey, profileKey, topicKey } from '../utils/cacheKeys.js';

////////////////////////////////////////////////////////
// EXPORTS
////////////////////////////////////////////////////////
export const about = (req, res, next) => {
/** @type {import('express').RequestHandler} */
export const about = (_req, res, _next) => {
res.status(200).json({
status: 'success',
message: `make a request.
available endpoints are: '/slug', '/unanswered/slug', '/topic/slug', '/profile/slug'`,
message: `make a request. available endpoints are: '/slug', '/unanswered/slug', '/topic/slug', '/profile/slug'`,
});
};

export const answers = catchAsyncErrors(async (req, res, next) => {
const {
urlObj,
params: { slug },
query: { lang },
} = req;

const data = await getOrSetCache(answersKey(urlObj), getAnswers, slug, lang);
res.status(200).json({ status: 'success', data });
});

export const topic = catchAsyncErrors(async (req, res, next) => {
const {
urlObj,
params: { slug },
query: { lang },
} = req;

const data = await getOrSetCache(topicKey(urlObj), getTopic, slug, lang);
res.status(200).json({ status: 'success', data });
});

export const profile = catchAsyncErrors(async (req, res, next) => {
const {
urlObj,
params: { name },
query: { lang },
} = req;

const data = await getOrSetCache(profileKey(urlObj), getProfile, name, lang);
res.status(200).json({ status: 'success', data });
});

export const unimplemented = (req, res, next) => {
/** @type {import('express').RequestHandler} */
export const unimplemented = (_req, res, _next) => {
res.status(501).json({
status: 'fail',
message: "This route isn't yet implemented. Check back sometime later!",
});
};

export const gone = (req, res, next) => {
/** @type {import('express').RequestHandler} */
export const gone = (_req, res, _next) => {
res.status(501).json({
status: 'fail',
message: "This route doesn't exist anymore.",
});
};


export const image = catchAsyncErrors(async (req, res, next) => {
/** @type {import('express').RequestHandler} */
export const image = catchAsyncErrors(async (req, res, _next) => {
const { domain, path } = req.params;
if (!domain.endsWith('quoracdn.net')) {
return res.status(403).json({
status: 'fail',
message: 'Invalid domain',
});
}
// changing defaults for this particular endpoint
const axiosInstance = getAxiosInstance();
axiosInstance.defaults.baseURL = `https://${domain}/`;

const imageRes = await axiosInstance.get(path, { responseType: 'stream' });
const imageRes = await axiosInstance.get(path, { baseURL: `https://${domain}/`, responseType: 'stream' });

res.set('Content-Type', imageRes.headers['content-type']);
res.set('Cache-Control', 'public, max-age=315360000');
Expand Down
69 changes: 69 additions & 0 deletions controllers/controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import catchAsyncErrors from '../utils/catchAsyncErrors.js';
import { acceptedLanguages } from '../utils/constants.js';
import getAnswers from '../fetchers/getAnswers.js';
import getTopic from '../fetchers/getTopic.js';
import getProfile from '../fetchers/getProfile.js';

export const answers = catchAsyncErrors(async (req, res, next) => {
const { params: { slug }, query: { lang } } = req;

/** @type{Awaited<ReturnType<typeof getAnswers>>} */
let data = res.locals.data;
if (!data) data = await getAnswers(slug, lang);

const title = data.question.text[0].spans.map(span => span.text).join('');

res.locals.data = data;
res.locals.title = title;
res.locals.description = `Answers to ${title}`;

next();
});

export const topic = catchAsyncErrors(async (req, res, next) => {
const { params: { slug }, query: { lang } } = req;

/** @type{Awaited<ReturnType<typeof getTopic>>} */
let data = res.locals.data;
if (!data) data = await getTopic(slug, lang);

res.locals.data = data;
res.locals.title = data.name;
res.locals.description = `Information about ${data.name} topic.`;

next();
});

export const profile = catchAsyncErrors(async (req, res, next) => {
const { params: { name }, query: { lang } } = req;

/** @type{Awaited<ReturnType<typeof getProfile>>} */
let data = res.locals.data;
if (!data) data = await getProfile(name, lang);

res.locals.data = data;
res.locals.title = data.basic.name;
res.locals.description = `${data.basic.name}'s profile.`;

next();
});

const regex = /^https:\/\/(.{2,})\.quora\.com(\/.*)$/; // local helper constant
export const redirect = (req, res, _next) => {
const url = req.originalUrl.replace('/redirect/', ''); // removing `/redirect/` part.
const match = regex.exec(url);

if (!match) return res.redirect('/');

const [_, subdomain, rest] = match; // eg: subdomain: 'es', rest: '/topic/linux?share=1'
let link;

if (acceptedLanguages.includes(subdomain))
// adding lang param
link = `${rest}${rest.includes('?') ? '&' : '?'}lang=${subdomain}`;
else if (subdomain === 'www')
link = rest; // doing nothing
else link = `/space/${subdomain}${rest}`; // gotta be a space url.

return res.redirect(link);
};
39 changes: 12 additions & 27 deletions controllers/errorController.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,19 @@
/* eslint-disable no-unused-vars */
/* eslint-disable no-param-reassign */

////////////////////////////////////////////////////////
// IMPORTS
////////////////////////////////////////////////////////
import env from '../utils/env.js';
import log from '../utils/log.js';

////////////////////////////////////////////////////////
// FUNCTIONS
////////////////////////////////////////////////////////
/**
* @description function to send error responses to the client
* @param {{}} err error object
* @param {{}} req request object provided by express
* @param {{}} res response object provided by express
* @param {boolean} devMode if set to true, will send full stack trace to the client
* @param {Parameters<ErrorRequestHandler>['0']} err
* @param {Parameters<ErrorRequestHandler>['1']} req
* @param {Parameters<ErrorRequestHandler>['2']} res
* @param {boolean} devMode
*/
const sendErrorResponse = (err, req, res, devMode = false) => {
// 1. FOR API
if (req.originalUrl.startsWith('/api/'))
res.status(err.statusCode).json({
status: err.status,
message: err.message,
// only if devMode is true, will this stack trace get sent. using es6 spreading and short circuiting
// only if devMode is true, will this stack trace get sent
...(devMode && { stack: err.stack }),
});
// 2. FOR WEBPAGES
Expand All @@ -43,29 +34,23 @@ const sendErrorResponse = (err, req, res, devMode = false) => {
};

/**
* @description function to handle all errors occuring in the app
* @param {{}} err object containing full error
* @param {{}} req request object in express
* @param {{}} res response object in express
* @param {function} next function to call next middleware in express
* @import { type ErrorRequestHandler } from "express";
* @description function to handle all errors occurring in the app
* @type {ErrorRequestHandler}
*/

const globalErrorHandler = (err, req, res, next) => {
const globalErrorHandler = (err, req, res, _next) => {
// since not all errors will be an instance of AppError class(as not errors will be manually thrown by us), we have to set sensible defaults before dealing with those errors
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
log(err, 'error');

if (process.env.NODE_ENV === 'development')
if (env.NODE_ENV === 'development') {
sendErrorResponse(err, req, res, true);
else {
} else {
// if error is not operational, sending a generic error message and not revealing full details in production mode
if (err.name !== 'OperationalError') err.message = 'something went wrong!';
sendErrorResponse(err, req, res);
}
};

////////////////////////////////////////////////////////
// EXPORTS
////////////////////////////////////////////////////////
export default globalErrorHandler;
Loading