mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-08-08 06:55:26 +02:00
Ft: users db, multi user auth, files and pages authored by username
This commit is contained in:
parent
c383ee48c2
commit
c2c891740a
26 changed files with 29263 additions and 4255 deletions
|
@ -1,5 +1,5 @@
|
|||
# codex-docs password.
|
||||
PASSWORD=
|
||||
# codex-docs admin password.
|
||||
ADMIN_PASSWORD=
|
||||
|
||||
# Backend errors tracking Integraton Token
|
||||
HAWK_TOKEN_BACKEND=
|
||||
|
|
23310
package-lock.json
generated
Normal file
23310
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -34,9 +34,10 @@ class Transport {
|
|||
* @returns {Promise<FileData>}
|
||||
*/
|
||||
public static async save(multerData: Dict, map: Dict): Promise<FileData> {
|
||||
const { originalname: name, path, filename, size, mimetype, url } = multerData;
|
||||
const { username, originalname: name, path, filename, size, mimetype, url } = multerData;
|
||||
|
||||
const file = new File({
|
||||
username,
|
||||
name,
|
||||
filename,
|
||||
path,
|
||||
|
@ -60,10 +61,11 @@ class Transport {
|
|||
* Fetches file by passed URL
|
||||
*
|
||||
* @param {string} url - URL of the file
|
||||
* @param {string} username – username
|
||||
* @param {object} map - object that represents how should fields of File object should be mapped to response
|
||||
* @returns {Promise<FileData>}
|
||||
*/
|
||||
public static async fetch(url: string, map: Dict): Promise<FileData> {
|
||||
public static async fetch(url: string, username: string, map: Dict): Promise<FileData> {
|
||||
const fetchedFile = await fetch(url);
|
||||
const buffer = await fetchedFile.buffer();
|
||||
const filename = await random16();
|
||||
|
@ -85,6 +87,7 @@ class Transport {
|
|||
const mimeType = type ? type.mime : fetchedMimeType;
|
||||
|
||||
const file = new File({
|
||||
username,
|
||||
name: url,
|
||||
filename: `${filename}.${ext}`,
|
||||
path: `${config.get('uploads')}/${filename}.${ext}`,
|
||||
|
|
|
@ -6,6 +6,7 @@ const filesDb = database['files'];
|
|||
* @typedef {object} FileData
|
||||
*
|
||||
* @property {string} _id - file id
|
||||
* @property {string} username - username
|
||||
* @property {string} name - original file name
|
||||
* @property {string} filename - name of uploaded file
|
||||
* @property {string} path - path to uploaded file
|
||||
|
@ -15,6 +16,7 @@ const filesDb = database['files'];
|
|||
*/
|
||||
export interface FileData {
|
||||
_id?: string;
|
||||
username?: string;
|
||||
name?: string;
|
||||
filename?: string;
|
||||
path?: string;
|
||||
|
@ -29,6 +31,7 @@ export interface FileData {
|
|||
* @class File model
|
||||
*
|
||||
* @property {string} _id - file id
|
||||
* @property {string} username - username
|
||||
* @property {string} name - original file name
|
||||
* @property {string} filename - name of uploaded file
|
||||
* @property {string} path - path to uploaded file
|
||||
|
@ -37,6 +40,7 @@ export interface FileData {
|
|||
*/
|
||||
class File {
|
||||
public _id?: string;
|
||||
public username?: string;
|
||||
public name?: string;
|
||||
public filename?: string;
|
||||
public path?: string;
|
||||
|
@ -102,8 +106,9 @@ class File {
|
|||
* @param {FileData} fileData - info about file
|
||||
*/
|
||||
public set data(fileData: FileData) {
|
||||
const { name, filename, path, mimetype, size, url } = fileData;
|
||||
const { username, name, filename, path, mimetype, size, url } = fileData;
|
||||
|
||||
this.username = username || this.username;
|
||||
this.name = name || this.name;
|
||||
this.filename = filename || this.filename;
|
||||
this.path = path ? this.processPath(path) : this.path;
|
||||
|
@ -120,6 +125,7 @@ class File {
|
|||
public get data(): FileData {
|
||||
return {
|
||||
_id: this._id,
|
||||
username: this.username,
|
||||
name: this.name,
|
||||
filename: this.filename,
|
||||
path: this.path,
|
||||
|
|
|
@ -6,6 +6,7 @@ const pagesDb = database['pages'];
|
|||
/**
|
||||
* @typedef {object} PageData
|
||||
* @property {string} _id - page id
|
||||
* @property {string} username - username
|
||||
* @property {string} title - page title
|
||||
* @property {string} uri - page uri
|
||||
* @property {*} body - page body
|
||||
|
@ -13,6 +14,7 @@ const pagesDb = database['pages'];
|
|||
*/
|
||||
export interface PageData {
|
||||
_id?: string;
|
||||
username?: string;
|
||||
title?: string;
|
||||
uri?: string;
|
||||
body?: any;
|
||||
|
@ -24,6 +26,7 @@ export interface PageData {
|
|||
* @class Page model
|
||||
*
|
||||
* @property {string} _id - page id
|
||||
* @property {string} username - username
|
||||
* @property {string} title - page title
|
||||
* @property {string} uri - page uri
|
||||
* @property {*} body - page body
|
||||
|
@ -31,6 +34,7 @@ export interface PageData {
|
|||
*/
|
||||
class Page {
|
||||
public _id?: string;
|
||||
public username?: string;
|
||||
public body?: any;
|
||||
public title?: string;
|
||||
public uri?: string;
|
||||
|
@ -95,8 +99,9 @@ class Page {
|
|||
* @param {PageData} pageData - page's data
|
||||
*/
|
||||
public set data(pageData: PageData) {
|
||||
const { body, parent, uri } = pageData;
|
||||
const { username, body, parent, uri } = pageData;
|
||||
|
||||
this.username = username || this.username;
|
||||
this.body = body || this.body;
|
||||
this.title = this.extractTitleFromBody();
|
||||
this.uri = uri || '';
|
||||
|
@ -111,6 +116,7 @@ class Page {
|
|||
public get data(): PageData {
|
||||
return {
|
||||
_id: this._id,
|
||||
username: this.username,
|
||||
title: this.title,
|
||||
uri: this.uri,
|
||||
body: this.body,
|
||||
|
|
172
src/backend/models/user.ts
Normal file
172
src/backend/models/user.ts
Normal file
|
@ -0,0 +1,172 @@
|
|||
import database from '../utils/database/index.js';
|
||||
import config from 'config';
|
||||
import { binaryMD5 } from '../utils/crypto.js';
|
||||
|
||||
const usersDb = database['users'];
|
||||
|
||||
/**
|
||||
* @typedef {object} UserData
|
||||
*
|
||||
* @property {string} _id - user id
|
||||
* @property {string} username - username
|
||||
* @property {string} role - user access role
|
||||
* @property {string} password - user plain text password
|
||||
* @property {string} passwordHash - user password hash
|
||||
*/
|
||||
export interface UserData {
|
||||
_id?: string;
|
||||
username?: string;
|
||||
role?: string;
|
||||
password?: string,
|
||||
passwordHash?: string;
|
||||
[key: string]: string | number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class User
|
||||
* @class User model
|
||||
*
|
||||
* @property {string} _id - user id
|
||||
* @property {string} username - username
|
||||
* @property {string} role - user access role
|
||||
* @property {string} passwordHash - user password hash
|
||||
*/
|
||||
class User {
|
||||
public _id?: string;
|
||||
public username?: string;
|
||||
public role?: string;
|
||||
public passwordHash?: string;
|
||||
|
||||
/**
|
||||
* @class
|
||||
*
|
||||
* @param {UserData} data - info about user
|
||||
*/
|
||||
constructor(data: UserData = {}) {
|
||||
if (data === null) {
|
||||
data = {};
|
||||
}
|
||||
|
||||
if (data._id) {
|
||||
this._id = data._id;
|
||||
}
|
||||
|
||||
if (data.password) {
|
||||
data.passwordHash = binaryMD5(data.password + config.get('secret'));
|
||||
delete data.password;
|
||||
}
|
||||
|
||||
this.data = data;
|
||||
}
|
||||
/**
|
||||
* Find and return model of file with given id
|
||||
*
|
||||
* @param {string} _id - user id
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
public static async get(_id: string): Promise<User> {
|
||||
const data: UserData = await usersDb.findOne({ _id });
|
||||
|
||||
return new User(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and return model of user with given id
|
||||
*
|
||||
* @param {string} username - username
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
public static async getByUsername(username: string): Promise<User> {
|
||||
const data = await usersDb.findOne({ username });
|
||||
|
||||
return new User(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all users which match passed query object
|
||||
*
|
||||
* @param {object} query - input query
|
||||
* @returns {Promise<User[]>}
|
||||
*/
|
||||
public static async getAll(query: Record<string, unknown> = {}): Promise<User[]> {
|
||||
const records = await usersDb.find(query);
|
||||
|
||||
return Promise.all(records.map(data => new User(data)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set UserData object fields to internal model fields
|
||||
*
|
||||
* @param {UserData} userData - info about user
|
||||
*/
|
||||
public set data(userData: UserData) {
|
||||
const { username, role, passwordHash } = userData;
|
||||
|
||||
this.username = username || this.username;
|
||||
this.role = role || this.role;
|
||||
this.passwordHash = passwordHash || this.passwordHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return UserData object
|
||||
*
|
||||
* @returns {UserData}
|
||||
*/
|
||||
public get data(): UserData {
|
||||
return {
|
||||
_id: this._id,
|
||||
username: this.username,
|
||||
role: this.role,
|
||||
passwordHash: this.passwordHash,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or update user data in the database
|
||||
*
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
public async save(): Promise<User> {
|
||||
if (!this._id) {
|
||||
if (this.username) {
|
||||
const user = await User.getByUsername(this.username);
|
||||
|
||||
if (typeof user._id !== 'undefined') {
|
||||
throw new Error('Username is occupied by another user');
|
||||
}
|
||||
}
|
||||
|
||||
const insertedRow = await usersDb.insert(this.data) as { _id: string };
|
||||
|
||||
this._id = insertedRow._id;
|
||||
} else {
|
||||
await usersDb.update({ _id: this._id }, this.data);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user data from the database
|
||||
*
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
public async destroy(): Promise<User> {
|
||||
await usersDb.remove({ _id: this._id });
|
||||
|
||||
delete this._id;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return readable user data
|
||||
*
|
||||
* @returns {UserData}
|
||||
*/
|
||||
public toJSON(): UserData {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
export default User;
|
21
src/backend/routes/admin.ts
Normal file
21
src/backend/routes/admin.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import express, { NextFunction, Request, Response } from 'express';
|
||||
import { allowAdmin } from './middlewares/locals.js';
|
||||
import verifyToken from './middlewares/token.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Admin settings
|
||||
*/
|
||||
router.get('/admin/settings', verifyToken, allowAdmin, async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.render('pages/settings', {
|
||||
users: [],
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(404);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -2,6 +2,7 @@ import express, { Request, Response } from 'express';
|
|||
import multerFunc from 'multer';
|
||||
import Pages from '../../controllers/pages.js';
|
||||
import PagesOrder from '../../controllers/pagesOrder.js';
|
||||
import verifyToken from '../middlewares/token.js';
|
||||
|
||||
const router = express.Router();
|
||||
const multer = multerFunc();
|
||||
|
@ -54,10 +55,11 @@ router.get('/pages', async (req: Request, res: Response) => {
|
|||
*
|
||||
* Create new page in the database
|
||||
*/
|
||||
router.put('/page', multer.none(), async (req: Request, res: Response) => {
|
||||
router.put('/page', multer.none(), verifyToken, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { title, body, parent } = req.body;
|
||||
const page = await Pages.insert({
|
||||
username: res.locals.tokenData.username,
|
||||
title,
|
||||
body,
|
||||
parent,
|
||||
|
@ -87,7 +89,7 @@ router.put('/page', multer.none(), async (req: Request, res: Response) => {
|
|||
*
|
||||
* Update page data in the database
|
||||
*/
|
||||
router.post('/page/:id', multer.none(), async (req: Request, res: Response) => {
|
||||
router.post('/page/:id', multer.none(), verifyToken, async (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
|
||||
try {
|
||||
|
@ -122,6 +124,7 @@ router.post('/page/:id', multer.none(), async (req: Request, res: Response) => {
|
|||
}
|
||||
|
||||
page = await Pages.update(id, {
|
||||
username: res.locals.tokenData.username,
|
||||
title,
|
||||
body,
|
||||
parent,
|
||||
|
@ -144,7 +147,7 @@ router.post('/page/:id', multer.none(), async (req: Request, res: Response) => {
|
|||
*
|
||||
* Remove page from the database
|
||||
*/
|
||||
router.delete('/page/:id', async (req: Request, res: Response) => {
|
||||
router.delete('/page/:id', verifyToken, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const pageId = req.params.id;
|
||||
const page = await Pages.get(pageId);
|
||||
|
|
|
@ -5,6 +5,7 @@ import mkdirp from 'mkdirp';
|
|||
import config from 'config';
|
||||
import Transport from '../../controllers/transport.js';
|
||||
import { random16 } from '../../utils/crypto.js';
|
||||
import verifyToken from '../middlewares/token.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
@ -59,7 +60,7 @@ const fileUploader = multer({
|
|||
/**
|
||||
* Accepts images to upload
|
||||
*/
|
||||
router.post('/transport/image', imageUploader, async (req: Request, res: Response) => {
|
||||
router.post('/transport/image', verifyToken, imageUploader, async (req: Request, res: Response) => {
|
||||
const response = {
|
||||
success: 0,
|
||||
message: '',
|
||||
|
@ -79,6 +80,7 @@ router.post('/transport/image', imageUploader, async (req: Request, res: Respons
|
|||
|
||||
const fileData = {
|
||||
...req.files.image[0],
|
||||
username: res.locals.tokenData.username,
|
||||
url: '/uploads/' + req.files.image[0].filename,
|
||||
};
|
||||
|
||||
|
@ -100,7 +102,7 @@ router.post('/transport/image', imageUploader, async (req: Request, res: Respons
|
|||
/**
|
||||
* Accepts files to upload
|
||||
*/
|
||||
router.post('/transport/file', fileUploader, async (req: Request, res: Response) => {
|
||||
router.post('/transport/file', verifyToken, fileUploader, async (req: Request, res: Response) => {
|
||||
const response = { success: 0 };
|
||||
|
||||
if (req.files === undefined) {
|
||||
|
@ -117,7 +119,13 @@ router.post('/transport/file', fileUploader, async (req: Request, res: Response)
|
|||
try {
|
||||
Object.assign(
|
||||
response,
|
||||
await Transport.save(req.files.file[0], req.body.map ? JSON.parse(req.body.map) : undefined)
|
||||
await Transport.save(
|
||||
{
|
||||
...req.files.file[0],
|
||||
username: res.locals.tokenData.username,
|
||||
},
|
||||
req.body.map ? JSON.parse(req.body.map) : undefined
|
||||
)
|
||||
);
|
||||
|
||||
response.success = 1;
|
||||
|
@ -130,7 +138,7 @@ router.post('/transport/file', fileUploader, async (req: Request, res: Response)
|
|||
/**
|
||||
* Accept file url to fetch
|
||||
*/
|
||||
router.post('/transport/fetch', multer().none(), async (req: Request, res: Response) => {
|
||||
router.post('/transport/fetch', verifyToken, multer().none(), async (req: Request, res: Response) => {
|
||||
const response = { success: 0 };
|
||||
|
||||
if (!req.body.url) {
|
||||
|
@ -140,7 +148,7 @@ router.post('/transport/fetch', multer().none(), async (req: Request, res: Respo
|
|||
}
|
||||
|
||||
try {
|
||||
Object.assign(response, await Transport.fetch(req.body.url, req.body.map ? JSON.parse(req.body.map) : undefined));
|
||||
Object.assign(response, await Transport.fetch(req.body.url, res.locals.tokenData.username, req.body.map ? JSON.parse(req.body.map) : undefined));
|
||||
|
||||
response.success = 1;
|
||||
res.status(200).json(response);
|
||||
|
|
|
@ -2,6 +2,8 @@ import express, { Request, Response } from 'express';
|
|||
import jwt from 'jsonwebtoken';
|
||||
import config from 'config';
|
||||
import csrf from 'csurf';
|
||||
import User from '../models/user.js';
|
||||
import { binaryMD5 } from '../utils/crypto.js';
|
||||
|
||||
const router = express.Router();
|
||||
const csrfProtection = csrf({ cookie: true });
|
||||
|
@ -14,6 +16,7 @@ router.get('/auth', csrfProtection, function (req: Request, res: Response) {
|
|||
res.render('auth', {
|
||||
title: 'Login page',
|
||||
csrfToken: req.csrfToken(),
|
||||
redirectUrl: req.query.redirect || '/',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -21,22 +24,65 @@ router.get('/auth', csrfProtection, function (req: Request, res: Response) {
|
|||
* Process given password
|
||||
*/
|
||||
router.post('/auth', parseForm, csrfProtection, async (req: Request, res: Response) => {
|
||||
const { ADMIN_PASSWORD } = process.env;
|
||||
const { _redirect: redirectUrl } = req.body;
|
||||
|
||||
try {
|
||||
if (!process.env.PASSWORD) {
|
||||
if (!ADMIN_PASSWORD) {
|
||||
res.render('auth', {
|
||||
title: 'Login page',
|
||||
header: 'Password not set',
|
||||
header: 'Admin password not set',
|
||||
csrfToken: req.csrfToken(),
|
||||
redirectUrl,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.body.password !== process.env.PASSWORD) {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username) {
|
||||
res.render('auth', {
|
||||
title: 'Login page',
|
||||
header: 'Username required',
|
||||
csrfToken: req.csrfToken(),
|
||||
redirectUrl,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (username === 'admin' && password !== ADMIN_PASSWORD) {
|
||||
res.render('auth', {
|
||||
title: 'Login page',
|
||||
header: 'Wrong password',
|
||||
csrfToken: req.csrfToken(),
|
||||
redirectUrl,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordHash = binaryMD5(password + config.get('secret'));
|
||||
const user = await User.getByUsername(username);
|
||||
|
||||
if (username !== 'admin' && typeof user.username === 'undefined') {
|
||||
res.render('auth', {
|
||||
title: 'Login page',
|
||||
header: 'Wrong username',
|
||||
csrfToken: req.csrfToken(),
|
||||
redirectUrl,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (username !== 'admin' && user.passwordHash !== passwordHash) {
|
||||
res.render('auth', {
|
||||
title: 'Login page',
|
||||
header: 'Wrong password',
|
||||
csrfToken: req.csrfToken(),
|
||||
redirectUrl,
|
||||
});
|
||||
|
||||
return;
|
||||
|
@ -45,20 +91,23 @@ router.post('/auth', parseForm, csrfProtection, async (req: Request, res: Respon
|
|||
const token = jwt.sign({
|
||||
iss: 'Codex Team',
|
||||
sub: 'auth',
|
||||
username,
|
||||
role: user.role || 'admin',
|
||||
iat: Date.now(),
|
||||
}, process.env.PASSWORD + config.get('secret'));
|
||||
}, config.get('secret'));
|
||||
|
||||
res.cookie('authToken', token, {
|
||||
httpOnly: true,
|
||||
expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year
|
||||
});
|
||||
|
||||
res.redirect('/');
|
||||
res.redirect(redirectUrl || '/');
|
||||
} catch (err) {
|
||||
res.render('auth', {
|
||||
title: 'Login page',
|
||||
header: 'Password not set',
|
||||
header: 'Unexpected error',
|
||||
csrfToken: req.csrfToken(),
|
||||
redirectUrl,
|
||||
});
|
||||
|
||||
return;
|
||||
|
|
|
@ -2,6 +2,7 @@ import express from 'express';
|
|||
import home from './home.js';
|
||||
import pages from './pages.js';
|
||||
import auth from './auth.js';
|
||||
import admin from './admin.js';
|
||||
import aliases from './aliases.js';
|
||||
import api from './api/index.js';
|
||||
import pagesMiddleware from './middlewares/pages.js';
|
||||
|
@ -10,6 +11,7 @@ const router = express.Router();
|
|||
|
||||
router.use('/', pagesMiddleware, home);
|
||||
router.use('/', pagesMiddleware, pages);
|
||||
router.use('/', pagesMiddleware, admin);
|
||||
router.use('/', pagesMiddleware, auth);
|
||||
router.use('/api', api);
|
||||
router.use('/', aliases);
|
||||
|
|
|
@ -7,10 +7,25 @@ import { NextFunction, Request, Response } from 'express';
|
|||
* @param res - response object
|
||||
* @param next - next function
|
||||
*/
|
||||
export default function allowEdit(req: Request, res: Response, next: NextFunction): void {
|
||||
export function allowEdit(req: Request, res: Response, next: NextFunction): void {
|
||||
if (res.locals.isAuthorized) {
|
||||
next();
|
||||
} else {
|
||||
res.redirect('/auth');
|
||||
res.redirect('/auth?redirect=' + req.path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware for checking locals.isAuthorized property and checking that username is equal admin, which allows to edit/create users
|
||||
*
|
||||
* @param req - request object
|
||||
* @param res - response object
|
||||
* @param next - next function
|
||||
*/
|
||||
export function allowAdmin(req: Request, res: Response, next: NextFunction): void {
|
||||
if (res.locals.isAuthorized && res.locals.tokenData.username === 'admin') {
|
||||
next();
|
||||
} else {
|
||||
res.redirect('/auth?redirect=' + req.path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,7 +71,8 @@ export default asyncMiddleware(async (req: Request, res: Response, next: NextFun
|
|||
const pages = await Pages.getAll();
|
||||
const pagesOrder = await PagesOrder.getAll();
|
||||
|
||||
res.locals.menu = createMenuTree(parentIdOfRootPages, pages, pagesOrder, 2);
|
||||
res.locals.menu = createMenuTree(parentIdOfRootPages, pages, pagesOrder, 7);
|
||||
console.log(JSON.stringify(res.locals.menu));
|
||||
} catch (error) {
|
||||
console.log('Can not load menu:', error);
|
||||
}
|
||||
|
|
|
@ -14,16 +14,17 @@ export default async function verifyToken(req: Request, res: Response, next: Nex
|
|||
const token = req.cookies.authToken;
|
||||
|
||||
try {
|
||||
if (!process.env.PASSWORD) {
|
||||
if (!process.env.ADMIN_PASSWORD) {
|
||||
res.locals.isAuthorized = false;
|
||||
next();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const decodedToken = jwt.verify(token, process.env.PASSWORD + config.get('secret'));
|
||||
const decodedToken = jwt.verify(token, config.get('secret'));
|
||||
|
||||
res.locals.isAuthorized = !!decodedToken;
|
||||
res.locals.tokenData = decodedToken;
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import express, { NextFunction, Request, Response } from 'express';
|
|||
import Pages from '../controllers/pages.js';
|
||||
import PagesOrder from '../controllers/pagesOrder.js';
|
||||
import verifyToken from './middlewares/token.js';
|
||||
import allowEdit from './middlewares/locals.js';
|
||||
import { allowEdit } from './middlewares/locals.js';
|
||||
import PagesFlatArray from '../models/pagesFlatArray.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
@ -12,10 +12,7 @@ const router = express.Router();
|
|||
*/
|
||||
router.get('/page/new', verifyToken, allowEdit, async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const pagesAvailableGrouped = await Pages.groupByParent();
|
||||
|
||||
res.render('pages/form', {
|
||||
pagesAvailableGrouped,
|
||||
page: null,
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
@ -3,6 +3,7 @@ import { AliasData } from '../../models/alias.js';
|
|||
import { FileData } from '../../models/file.js';
|
||||
import { PageData } from '../../models/page.js';
|
||||
import { PageOrderData } from '../../models/pageOrder.js';
|
||||
import { UserData } from '../../models/user.js';
|
||||
import initDb from './initDb.js';
|
||||
|
||||
/**
|
||||
|
@ -172,4 +173,5 @@ export default {
|
|||
aliases: new Database<AliasData>(initDb('aliases')),
|
||||
pagesOrder: new Database<PageOrderData>(initDb('pagesOrder')),
|
||||
files: new Database<FileData>(initDb('files')),
|
||||
users: new Database<UserData>(initDb('users')),
|
||||
};
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
┬┴┬┴┤ ͜ʖ ͡°) ├┬┴┬┴
|
||||
</h1>
|
||||
<p>
|
||||
Enter a password to access pages editing
|
||||
Please login to access pages editing
|
||||
</p>
|
||||
<p>
|
||||
{{ header }}
|
||||
</p>
|
||||
<input type="hidden" name="_csrf" value={{ csrfToken }}>
|
||||
<input type="hidden" name="_redirect" value={{ redirectUrl }}>
|
||||
<input type="username" name="username" placeholder="Username">
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
|
|
|
@ -10,6 +10,11 @@
|
|||
<li class="docs-header__menu-add docs-header__menu-add--mobile">
|
||||
{% include 'components/button.twig' with {icon: 'plus', size: 'small', url: '/page/new'} %}
|
||||
</li>
|
||||
{% if tokenData.username == "admin" %}
|
||||
<li class="docs-header__menu-settings">
|
||||
{% include 'components/button.twig' with {icon: 'settings', size: 'small', url: '/admin/settings'} %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% for option in config.menu %}
|
||||
<li>
|
||||
|
|
|
@ -34,6 +34,81 @@
|
|||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% if child.children is not empty %}
|
||||
<ul class="docs-sidebar__section-list">
|
||||
{% for child in child.children %}
|
||||
<li>
|
||||
<a
|
||||
class="docs-sidebar__section-list-item-wrapper"
|
||||
href="{{ child.uri ? '/' ~ child.uri : '/page/' ~ child._id }}">
|
||||
<div class="docs-sidebar__section-list-item {{page is defined and page._id == child._id ? 'docs-sidebar__section-list-item--active' : ''}}">
|
||||
<span> • {{ child.title | striptags }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% if child.children is not empty %}
|
||||
<ul class="docs-sidebar__section-list">
|
||||
{% for child in child.children %}
|
||||
<li>
|
||||
<a
|
||||
class="docs-sidebar__section-list-item-wrapper"
|
||||
href="{{ child.uri ? '/' ~ child.uri : '/page/' ~ child._id }}">
|
||||
<div class="docs-sidebar__section-list-item {{page is defined and page._id == child._id ? 'docs-sidebar__section-list-item--active' : ''}}">
|
||||
<span>  • {{ child.title | striptags }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% if child.children is not empty %}
|
||||
<ul class="docs-sidebar__section-list">
|
||||
{% for child in child.children %}
|
||||
<li>
|
||||
<a
|
||||
class="docs-sidebar__section-list-item-wrapper"
|
||||
href="{{ child.uri ? '/' ~ child.uri : '/page/' ~ child._id }}">
|
||||
<div class="docs-sidebar__section-list-item {{page is defined and page._id == child._id ? 'docs-sidebar__section-list-item--active' : ''}}">
|
||||
<span>  • {{ child.title | striptags }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% if child.children is not empty %}
|
||||
<ul class="docs-sidebar__section-list">
|
||||
{% for child in child.children %}
|
||||
<li>
|
||||
<a
|
||||
class="docs-sidebar__section-list-item-wrapper"
|
||||
href="{{ child.uri ? '/' ~ child.uri : '/page/' ~ child._id }}">
|
||||
<div class="docs-sidebar__section-list-item {{page is defined and page._id == child._id ? 'docs-sidebar__section-list-item--active' : ''}}">
|
||||
<span>  • {{ child.title | striptags }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% if child.children is not empty %}
|
||||
<ul class="docs-sidebar__section-list">
|
||||
{% for child in child.children %}
|
||||
<li>
|
||||
<a
|
||||
class="docs-sidebar__section-list-item-wrapper"
|
||||
href="{{ child.uri ? '/' ~ child.uri : '/page/' ~ child._id }}">
|
||||
<div class="docs-sidebar__section-list-item {{page is defined and page._id == child._id ? 'docs-sidebar__section-list-item--active' : ''}}">
|
||||
<span>  • {{ child.title | striptags }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
{% endif %}
|
||||
<select id="parent" name="parent">
|
||||
<option value="0">Root</option>
|
||||
{% for _page in pagesAvailableGrouped %}
|
||||
{% for _page in menu %}
|
||||
{% if _page._id != currentPageId %}
|
||||
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
|
||||
{% if _page._parent != "0" %}
|
||||
|
@ -35,6 +35,126 @@
|
|||
{% endif %}
|
||||
{{ _page.title }}
|
||||
</option>
|
||||
{% if _page.children.length != "0" %}
|
||||
{% for _page in _page.children %}
|
||||
{% if _page._id != currentPageId %}
|
||||
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
|
||||
{% if _page._parent != "0" %}
|
||||
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
{{ _page.title }}
|
||||
</option>
|
||||
{% if _page.children.length != "0" %}
|
||||
{% for _page in _page.children %}
|
||||
{% if _page._id != currentPageId %}
|
||||
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
|
||||
{% if _page._parent != "0" %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
{{ _page.title }}
|
||||
</option>
|
||||
{% if _page.children.length != "0" %}
|
||||
{% for _page in _page.children %}
|
||||
{% if _page._id != currentPageId %}
|
||||
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
|
||||
{% if _page._parent != "0" %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
{{ _page.title }}
|
||||
</option>
|
||||
{% if _page.children.length != "0" %}
|
||||
{% for _page in _page.children %}
|
||||
{% if _page._id != currentPageId %}
|
||||
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
|
||||
{% if _page._parent != "0" %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
{{ _page.title }}
|
||||
</option>
|
||||
{% if _page.children.length != "0" %}
|
||||
{% for _page in _page.children %}
|
||||
{% if _page._id != currentPageId %}
|
||||
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
|
||||
{% if _page._parent != "0" %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
{{ _page.title }}
|
||||
</option>
|
||||
{% if _page.children.length != "0" %}
|
||||
{% for _page in _page.children %}
|
||||
{% if _page._id != currentPageId %}
|
||||
<option value="{{ _page._id }}" disabled {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
|
||||
{% if _page._parent != "0" %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
{{ _page.title }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
|
|
@ -20,6 +20,10 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
<time class="page__header-time">
|
||||
{% if page.username %}
|
||||
Author <span style="color: #000;">@{{ page.username }}</span>
|
||||
|
|
||||
{% endif %}
|
||||
Last edit {{ (page.body.time / 1000) | date("M d Y") }}
|
||||
</time>
|
||||
{% if isAuthorized == true %}
|
||||
|
|
9
src/backend/views/pages/settings.twig
Normal file
9
src/backend/views/pages/settings.twig
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% extends 'layout.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<style>
|
||||
.docs-header__menu-settings {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
|
@ -121,7 +121,7 @@ export default class Sidebar {
|
|||
|
||||
const itemsCount = sectionList.children.length;
|
||||
|
||||
sectionList.style.maxHeight = `${itemsCount * ITEM_HEIGHT}px`;
|
||||
// sectionList.style.maxHeight = `${itemsCount * ITEM_HEIGHT}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,6 +13,15 @@
|
|||
margin: 40px 0 20px;
|
||||
}
|
||||
|
||||
input[type="username"] {
|
||||
padding: 9px;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
border-radius: 3px;
|
||||
-webkit-appearance: none;
|
||||
border: 1px solid var(--color-line-gray);
|
||||
}
|
||||
|
||||
input[type="password"] {
|
||||
padding: 9px;
|
||||
font-size: 15px;
|
||||
|
|
32
src/frontend/svg/settings.svg
Normal file
32
src/frontend/svg/settings.svg
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 478.7 478.7" style="enable-background:new 0 0 478.7 478.7;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M383,205.7l-22.5-3.8c-2.4-7.6-5.3-14.8-9.1-21.8l13.3-18.6c5.6-7.9,4.8-18.7-2.1-25.5l-19.9-19.9
|
||||
c-3.7-3.7-8.7-5.9-14-5.9c-4.1,0-8,1.3-11.4,3.7l-18.6,13.3c-7.2-3.8-14.8-6.9-22.6-9.3l-3.7-22.2c-1.6-9.6-9.8-16.5-19.5-16.5
|
||||
h-28.2c-9.7,0-17.9,6.9-19.5,16.5l-3.8,22.8c-7.5,2.4-14.8,5.4-21.8,9.1l-18.4-13.3c-3.4-2.4-7.3-3.7-11.5-3.7
|
||||
c-5.3,0-10.3,2.1-14,5.9l-20,19.9c-6.9,6.9-7.8,17.6-2.1,25.5l13.3,18.7c-3.7,7-6.6,14.3-8.9,21.9l-22.2,3.7
|
||||
c-9.6,1.6-16.5,9.8-16.5,19.5v28.2c0,9.7,6.9,17.9,16.5,19.5l22.8,3.8c2.4,7.5,5.4,14.8,9.1,21.8l-13.2,18.3
|
||||
c-5.6,7.9-4.8,18.7,2.1,25.5l19.9,19.9c3.7,3.7,8.7,5.9,14,5.9c4.1,0,8-1.3,11.4-3.7l18.7-13.3c6.8,3.6,13.8,6.4,21.1,8.7
|
||||
l3.7,22.5c1.6,9.6,9.8,16.5,19.5,16.5h28.3c9.7,0,17.9-6.9,19.5-16.5l3.8-22.5c7.6-2.4,14.8-5.3,21.8-9.1l18.6,13.3
|
||||
c3.4,2.4,7.3,3.7,11.5,3.7l0,0c5.3,0,10.2-2.1,14-5.9l19.9-19.9c6.9-6.9,7.8-17.6,2.1-25.5l-13.2-18.4c3.7-7,6.8-14.3,9.1-21.8
|
||||
l22.5-3.7c9.6-1.6,16.5-9.8,16.5-19.5v-28.2C399.5,215.6,392.6,207.4,383,205.7z M381.5,253.4c0,0.9-0.6,1.6-1.5,1.7l-28.1,4.7
|
||||
c-3.6,0.6-6.3,3.2-7.2,6.6c-2.6,9.8-6.4,19.3-11.6,28c-1.8,3.1-1.6,6.9,0.4,9.8l16.5,23.2c0.5,0.6,0.4,1.6-0.2,2.3l-19.9,19.9
|
||||
c-0.5,0.5-0.9,0.5-1.3,0.5s-0.7-0.1-1-0.4l-23.2-16.5c-2.8-2.1-6.8-2.2-9.8-0.4c-8.8,5.2-18.2,9.1-28,11.6
|
||||
c-3.5,0.9-6.1,3.7-6.6,7.2l-4.8,28.1c-0.1,0.9-0.9,1.5-1.7,1.5h-28.3c-0.9,0-1.6-0.6-1.7-1.5l-4.7-28.1c-0.6-3.6-3.2-6.3-6.6-7.2
|
||||
c-9.6-2.5-18.7-6.3-27.4-11.2c-1.4-0.8-3-1.2-4.6-1.2c-1.8,0-3.7,0.5-5.2,1.6l-23.4,16.9c-0.4,0.2-0.6,0.4-1,0.4
|
||||
c-0.3,0-0.8-0.1-1.3-0.5l-19.9-19.9c-0.6-0.6-0.6-1.6-0.2-2.3l16.5-23c2.1-2.9,2.2-6.9,0.4-9.9c-5.2-8.7-9.2-18.1-11.8-28
|
||||
c-0.9-3.4-3.7-6-7.2-6.6l-28.3-4.8c-0.9-0.1-1.5-0.9-1.5-1.7v-28.2c0-0.9,0.6-1.6,1.5-1.7l27.9-4.7c3.6-0.6,6.4-3.2,7.3-6.7
|
||||
c2.5-9.8,6.3-19.3,11.4-28.1c1.8-3.1,1.6-6.9-0.5-9.8l-16.6-23.4c-0.5-0.6-0.4-1.6,0.2-2.3l19.9-19.9c0.5-0.5,0.9-0.5,1.3-0.5
|
||||
c0.4,0,0.7,0.1,1,0.4l23,16.5c2.9,2.1,6.9,2.2,9.9,0.4c8.7-5.2,18.1-9.2,28-11.8c3.4-0.9,6-3.7,6.6-7.2l4.8-28.3
|
||||
c0.1-0.9,0.9-1.5,1.7-1.5H253c0.9,0,1.6,0.6,1.7,1.5l4.7,27.9c0.6,3.6,3.2,6.4,6.7,7.3c10.1,2.6,19.7,6.5,28.7,11.8
|
||||
c3.1,1.8,6.9,1.6,9.8-0.4l23-16.5c0.4-0.2,0.6-0.4,1-0.4c0.3,0,0.8,0.1,1.3,0.5l19.9,19.9c0.6,0.6,0.6,1.6,0.2,2.3l-16.5,23.2
|
||||
c-2.1,2.8-2.2,6.8-0.4,9.8c5.2,8.8,9.1,18.2,11.6,28c0.9,3.5,3.7,6.1,7.2,6.6l28.1,4.8c0.9,0.1,1.5,0.9,1.5,1.7L381.5,253.4
|
||||
L381.5,253.4z"/>
|
||||
<path d="M239.4,170.3c-38.1,0-69,31-69,69s31,69,69,69s69-31,69-69S277.5,170.3,239.4,170.3z M239.4,290.3c-28.2,0-51-22.9-51-51
|
||||
s22.9-51,51-51s51,22.9,51,51S267.6,290.3,239.4,290.3z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
Loading…
Add table
Add a link
Reference in a new issue