1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-08-03 20:45:24 +02:00

Typescript rewrite (#147)

* Updated highlight.js

* Update .codexdocsrc.sample

remove undefined page for a fresh new install

* backend rewritten in TS

* test -> TS, .added dockerignore, bug fixed

* Removed compiled js files, eslint codex/ts added

* fixed jsdocs warning, leaving editor confirmation

* use path.resolve for DB paths

* db drives updated + fixed User model

* redundant cleared + style fixed

* explicit type fixing

* fixing testing code

* added body block type

* compiled JS files -> dist, fixed compiling errors

* fixed compiling error, re-organized ts source code

* updated Dockerfile

* fixed link to parent page

* up nodejs version

* fix package name

* fix deps

Co-authored-by: nvc8996 <nvc.8996@gmail.com>
Co-authored-by: Taly <vitalik7tv@yandex.ru>
This commit is contained in:
Nikita Melnikov 2022-03-05 22:57:23 +04:00 committed by GitHub
parent 059cfb96f9
commit 34514761f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
99 changed files with 3817 additions and 2249 deletions

View file

@ -0,0 +1,50 @@
import express, { Request, Response } from 'express';
import Aliases from '../controllers/aliases';
import Pages from '../controllers/pages';
import Alias from '../models/alias';
import verifyToken from './middlewares/token';
const router = express.Router();
/**
* GET /*
*
* Return document with given alias
*/
router.get('*', verifyToken, async (req: Request, res: Response) => {
try {
let url = req.originalUrl.slice(1); // Cuts first '/' character
const queryParamsIndex = url.indexOf('?');
if (queryParamsIndex !== -1) {
url = url.slice(0, queryParamsIndex); // Cuts off query params
}
const alias = await Aliases.get(url);
if (alias.id === undefined) {
throw new Error('Alias not found');
}
switch (alias.type) {
case Alias.types.PAGE: {
const page = await Pages.get(alias.id);
const pageParent = await page.getParent();
res.render('pages/page', {
page,
pageParent,
config: req.app.locals.config,
});
}
}
} catch (err) {
res.status(400).json({
success: false,
error: err,
});
}
});
export default router;

View file

@ -0,0 +1,12 @@
import express from 'express';
import pagesAPI from './pages';
import transportAPI from './transport';
import linksAPI from './links';
const router = express.Router();
router.use('/', pagesAPI);
router.use('/', transportAPI);
router.use('/', linksAPI);
export default router;

View file

@ -0,0 +1,62 @@
import express, { Request, Response } from 'express';
import ogs from 'open-graph-scraper';
const router = express.Router();
interface ResponseData {
success: number;
meta?: {
title: string | undefined;
description: string | undefined;
siteName: string | undefined;
image: { url: string | undefined }
}
}
/**
* Accept file url to fetch
*/
router.get('/fetchUrl', async (req: Request, res: Response) => {
const response: ResponseData = {
success: 0,
};
if (!req.query.url) {
res.status(400).json(response);
return;
}
if (typeof req.query.url !== 'string') {
return;
}
try {
const linkData = (await ogs({ url: req.query.url })).result;
if (!linkData.success) {
return;
}
response.success = 1;
response.meta = {
title: linkData.ogTitle,
description: linkData.ogDescription,
siteName: linkData.ogSiteName,
image: {
url: undefined,
},
};
if (linkData.ogImage !== undefined) {
response.meta.image = { url: linkData.ogImage.toString() };
}
res.status(200).json(response);
} catch (e) {
console.log(e);
res.status(500).json(response);
}
});
export default router;

View file

@ -0,0 +1,221 @@
import express, { Request, Response } from 'express';
import multerFunc from 'multer';
import Pages from '../../controllers/pages';
import PagesOrder from '../../controllers/pagesOrder';
const router = express.Router();
const multer = multerFunc();
/**
* GET /page/:id
*
* Return PageData of page with given id
*/
router.get('/page/:id', async (req: Request, res: Response) => {
try {
const page = await Pages.get(req.params.id);
res.json({
success: true,
result: page.data,
});
} catch (err) {
res.status(400).json({
success: false,
error: (err as Error).message,
});
}
});
/**
* GET /pages
*
* Return PageData for all pages
*/
router.get('/pages', async (req: Request, res: Response) => {
try {
const pages = await Pages.getAll();
res.json({
success: true,
result: pages,
});
} catch (err) {
res.status(400).json({
success: false,
error: (err as Error).message,
});
}
});
/**
* PUT /page
*
* Create new page in the database
*/
router.put('/page', multer.none(), async (req: Request, res: Response) => {
try {
const { title, body, parent } = req.body;
const page = await Pages.insert({
title,
body,
parent,
});
if (page._id === undefined) {
throw new Error('Page not found');
}
/** push to the orders array */
await PagesOrder.push(parent, page._id);
res.json({
success: true,
result: page,
});
} catch (err) {
res.status(400).json({
success: false,
error: (err as Error).message,
});
}
});
/**
* POST /page/:id
*
* Update page data in the database
*/
router.post('/page/:id', multer.none(), async (req: Request, res: Response) => {
const { id } = req.params;
try {
const { title, body, parent, putAbovePageId, uri } = req.body;
const pages = await Pages.getAll();
let page = await Pages.get(id);
if (page._id === undefined) {
throw new Error('Page not found');
}
if (!page._parent) {
throw new Error('Parent not found');
}
if (page._parent !== parent) {
await PagesOrder.move(page._parent, parent, id);
} else {
if (putAbovePageId && putAbovePageId !== '0') {
const unordered = pages.filter(_page => _page._parent === page._parent).map(_page => _page._id);
const unOrdered: string[] = [];
unordered.forEach(item => {
if (typeof item === 'string') {
unOrdered.push(item);
}
});
await PagesOrder.update(unOrdered, page._id, page._parent, putAbovePageId);
}
}
page = await Pages.update(id, {
title,
body,
parent,
uri,
});
res.json({
success: true,
result: page,
});
} catch (err) {
res.status(400).json({
success: false,
error: (err as Error).message,
});
}
});
/**
* DELETE /page/:id
*
* Remove page from the database
*/
router.delete('/page/:id', async (req: Request, res: Response) => {
try {
const pageId = req.params.id;
const page = await Pages.get(pageId);
if (page._id === undefined) {
throw new Error('Page not found');
}
if (!page._parent) {
throw new Error('Parent not found');
}
const parentPageOrder = await PagesOrder.get(page._parent);
const pageBeforeId = parentPageOrder.getPageBefore(page._id);
const pageAfterId = parentPageOrder.getPageAfter(page._id);
let pageToRedirect;
if (pageBeforeId) {
pageToRedirect = await Pages.get(pageBeforeId);
} else if (pageAfterId) {
pageToRedirect = await Pages.get(pageAfterId);
} else {
pageToRedirect = page._parent !== '0' ? await Pages.get(page._parent) : null;
}
/**
* remove current page and go deeper to remove children with orders
*
* @param {string} startFrom - start point to delete
* @returns {Promise<void>}
*/
const deleteRecursively = async (startFrom: string): Promise<void> => {
let order: string[] = [];
try {
const children = await PagesOrder.get(startFrom);
order = children.order;
} catch (e) {
order = [];
}
order.forEach(async id => {
await deleteRecursively(id);
});
await Pages.remove(startFrom);
try {
await PagesOrder.remove(startFrom);
} catch (e) {
order = [];
}
};
await deleteRecursively(req.params.id);
// remove also from parent's order
parentPageOrder.remove(req.params.id);
await parentPageOrder.save();
res.json({
success: true,
result: pageToRedirect,
});
} catch (err) {
res.status(400).json({
success: false,
error: (err as Error).message,
});
}
});
export default router;

View file

@ -0,0 +1,146 @@
import { Request, Response, Router } from 'express';
import multer, { StorageEngine } from 'multer';
import mime from 'mime';
import mkdirp from 'mkdirp';
import config from 'config';
import Transport from '../../controllers/transport';
import { random16 } from '../../utils/crypto';
const router = Router();
/**
* Multer storage for uploaded files and images
*
* @type {StorageEngine}
*/
const storage: StorageEngine = multer.diskStorage({
destination: (req, file, cb) => {
const dir: string = config.get('uploads') || 'public/uploads';
mkdirp(dir);
cb(null, dir);
},
filename: async (req, file, cb) => {
const filename = await random16();
cb(null, `${filename}.${mime.getExtension(file.mimetype)}`);
},
});
/**
* Multer middleware for image uploading
*/
const imageUploader = multer({
storage: storage,
fileFilter: (req, file, cb) => {
if (!/image/.test(file.mimetype) && !/video\/mp4/.test(file.mimetype)) {
cb(null, false);
return;
}
cb(null, true);
},
}).fields([ {
name: 'image',
maxCount: 1,
} ]);
/**
* Multer middleware for file uploading
*/
const fileUploader = multer({
storage: storage,
}).fields([ {
name: 'file',
maxCount: 1,
} ]);
/**
* Accepts images to upload
*/
router.post('/transport/image', imageUploader, async (req: Request, res: Response) => {
const response = {
success: 0,
message: '',
};
if (req.files === undefined) {
response.message = 'No files found';
res.status(400).json(response);
return;
}
if (!('image' in req.files)) {
res.status(400).json(response);
return;
}
try {
Object.assign(
response,
await Transport.save(req.files.image[0], req.body.map ? JSON.parse(req.body.map) : undefined)
);
response.success = 1;
res.status(200).json(response);
} catch (e) {
res.status(500).json(response);
}
});
/**
* Accepts files to upload
*/
router.post('/transport/file', fileUploader, async (req: Request, res: Response) => {
const response = { success: 0 };
if (req.files === undefined) {
res.status(400).json(response);
return;
}
if (!('file' in req.files)) {
res.status(400).json(response);
return;
}
try {
Object.assign(
response,
await Transport.save(req.files.file[0], req.body.map ? JSON.parse(req.body.map) : undefined)
);
response.success = 1;
res.status(200).json(response);
} catch (e) {
res.status(500).json(response);
}
});
/**
* Accept file url to fetch
*/
router.post('/transport/fetch', multer().none(), async (req: Request, res: Response) => {
const response = { success: 0 };
if (!req.body.url) {
res.status(400).json(response);
return;
}
try {
Object.assign(response, await Transport.fetch(req.body.url, req.body.map ? JSON.parse(req.body.map) : undefined));
response.success = 1;
res.status(200).json(response);
} catch (e) {
console.log(e);
res.status(500).json(response);
}
});
export default router;

View file

@ -0,0 +1,78 @@
import express, { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import config from 'config';
import bcrypt from 'bcrypt';
import csrf from 'csurf';
import * as dotenv from 'dotenv';
import Users from '../controllers/users';
dotenv.config();
const router = express.Router();
const csrfProtection = csrf({ cookie: true });
const parseForm = express.urlencoded({ extended: false });
/**
* Authorization page
*/
router.get('/auth', csrfProtection, function (req: Request, res: Response) {
res.render('auth', {
title: 'Login page',
csrfToken: req.csrfToken(),
});
});
/**
* Process given password
*/
router.post('/auth', parseForm, csrfProtection, async (req: Request, res: Response) => {
try {
const userDoc = await Users.get();
const passHash = userDoc.passHash;
if (!passHash) {
res.render('auth', {
title: 'Login page',
header: 'Password not set',
csrfToken: req.csrfToken(),
});
return;
}
bcrypt.compare(req.body.password, passHash, async (err, result) => {
if (err || result === false) {
res.render('auth', {
title: 'Login page',
header: 'Wrong password',
csrfToken: req.csrfToken(),
});
return;
}
const token = jwt.sign({
iss: 'Codex Team',
sub: 'auth',
iat: Date.now(),
}, passHash + config.get('secret'));
res.cookie('authToken', token, {
httpOnly: true,
expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year
});
res.redirect('/');
});
} catch (err) {
res.render('auth', {
title: 'Login page',
header: 'Password not set',
csrfToken: req.csrfToken(),
});
return;
}
});
export default router;

View file

@ -0,0 +1,16 @@
import express, { Request, Response } from 'express';
import verifyToken from './middlewares/token';
const router = express.Router();
/* GET home page. */
router.get('/', verifyToken, async (req: Request, res: Response) => {
const config = req.app.locals.config;
if (config.startPage) {
return res.redirect(config.startPage);
}
res.render('pages/index', { isAuthorized: res.locals.isAuthorized });
});
export default router;

View file

@ -0,0 +1,17 @@
import express from 'express';
import home from './home';
import pages from './pages';
import auth from './auth';
import aliases from './aliases';
import api from './api';
import pagesMiddleware from './middlewares/pages';
const router = express.Router();
router.use('/', pagesMiddleware, home);
router.use('/', pagesMiddleware, pages);
router.use('/', pagesMiddleware, auth);
router.use('/api', api);
router.use('/', aliases);
export default router;

View file

@ -0,0 +1,16 @@
import { NextFunction, Request, Response } from 'express';
/**
* Middleware for checking locals.isAuthorized property, which allows to edit/create pages
*
* @param req - request object
* @param res - response object
* @param next - next function
*/
export default function allowEdit(req: Request, res: Response, next: NextFunction): void {
if (res.locals.isAuthorized) {
next();
} else {
res.redirect('/auth');
}
}

View file

@ -0,0 +1,79 @@
import { NextFunction, Request, Response } from 'express';
import Pages from '../../controllers/pages';
import PagesOrder from '../../controllers/pagesOrder';
import Page from '../../models/page';
import asyncMiddleware from '../../utils/asyncMiddleware';
import PageOrder from '../../models/pageOrder';
/**
* Process one-level pages list to parent-children list
*
* @param {string} parentPageId - parent page id
* @param {Page[]} pages - list of all available pages
* @param {PagesOrder[]} pagesOrder - list of pages order
* @param {number} level - max level recursion
* @param {number} currentLevel - current level of element
*
* @returns {Page[]}
*/
function createMenuTree(parentPageId: string, pages: Page[], pagesOrder: PageOrder[], level = 1, currentLevel = 1): Page[] {
const childrenOrder = pagesOrder.find(order => order.data.page === parentPageId);
/**
* branch is a page children in tree
* if we got some children order on parents tree, then we push found pages in order sequence
* otherwise just find all pages includes parent tree
*/
let ordered: any[] = [];
if (childrenOrder) {
ordered = childrenOrder.order.map((pageId: string) => {
return pages.find(page => page._id === pageId);
});
}
const unordered = pages.filter(page => page._parent === parentPageId);
const branch = Array.from(new Set([...ordered, ...unordered]));
/**
* stop recursion when we got the passed max level
*/
if (currentLevel === level + 1) {
return [];
}
/**
* Each parents children can have subbranches
*/
return branch.filter(page => page && page._id).map(page => {
return Object.assign({
children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1),
}, page.data);
});
}
/**
* Middleware for all /page/... routes
*
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
export default asyncMiddleware(async (req: Request, res: Response, next: NextFunction) => {
/**
* Pages without parent
*
* @type {string}
*/
const parentIdOfRootPages = '0';
try {
const pages = await Pages.getAll();
const pagesOrder = await PagesOrder.getAll();
res.locals.menu = createMenuTree(parentIdOfRootPages, pages, pagesOrder, 2);
} catch (error) {
console.log('Can not load menu:', error);
}
next();
});

View file

@ -0,0 +1,38 @@
import * as dotenv from 'dotenv';
import config from 'config';
import { NextFunction, Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import Users from '../../controllers/users';
dotenv.config();
/**
* Middleware for checking jwt token
*
* @param req - request object
* @param res - response object
* @param next - next function
*/
export default async function verifyToken(req: Request, res: Response, next: NextFunction): Promise<void> {
const token = req.cookies.authToken;
try {
const userDoc = await Users.get();
if (!userDoc.passHash) {
res.locals.isAuthorized = false;
next();
return;
}
const decodedToken = jwt.verify(token, userDoc.passHash + config.get('secret'));
res.locals.isAuthorized = !!decodedToken;
next();
} catch (err) {
res.locals.isAuthorized = false;
next();
}
}

View file

@ -0,0 +1,75 @@
import express, { NextFunction, Request, Response } from 'express';
import Pages from '../controllers/pages';
import PagesOrder from '../controllers/pagesOrder';
import verifyToken from './middlewares/token';
import allowEdit from './middlewares/locals';
const router = express.Router();
/**
* Create new page form
*/
router.get('/page/new', verifyToken, allowEdit, async (req: Request, res: Response, next: NextFunction) => {
try {
const pagesAvailable = await Pages.getAll();
res.render('pages/form', {
pagesAvailable,
page: null,
});
} catch (error) {
res.status(404);
next(error);
}
});
/**
* Edit page form
*/
router.get('/page/edit/:id', verifyToken, allowEdit, async (req: Request, res: Response, next: NextFunction) => {
const pageId = req.params.id;
try {
const page = await Pages.get(pageId);
const pagesAvailable = await Pages.getAllExceptChildren(pageId);
if (!page._parent) {
throw new Error('Parent not found');
}
const parentsChildrenOrdered = await PagesOrder.getOrderedChildren(pagesAvailable, pageId, page._parent, true);
res.render('pages/form', {
page,
parentsChildrenOrdered,
pagesAvailable,
});
} catch (error) {
res.status(404);
next(error);
}
});
/**
* View page
*/
router.get('/page/:id', verifyToken, async (req: Request, res: Response, next: NextFunction) => {
const pageId = req.params.id;
try {
const page = await Pages.get(pageId);
const pageParent = await page.parent;
res.render('pages/page', {
page,
pageParent,
config: req.app.locals.config,
});
} catch (error) {
res.status(404);
next(error);
}
});
export default router;