mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-08-07 22:45:23 +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.
|
# codex-docs admin password.
|
||||||
PASSWORD=
|
ADMIN_PASSWORD=
|
||||||
|
|
||||||
# Backend errors tracking Integraton Token
|
# Backend errors tracking Integraton Token
|
||||||
HAWK_TOKEN_BACKEND=
|
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>}
|
* @returns {Promise<FileData>}
|
||||||
*/
|
*/
|
||||||
public static async save(multerData: Dict, map: Dict): 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({
|
const file = new File({
|
||||||
|
username,
|
||||||
name,
|
name,
|
||||||
filename,
|
filename,
|
||||||
path,
|
path,
|
||||||
|
@ -60,10 +61,11 @@ class Transport {
|
||||||
* Fetches file by passed URL
|
* Fetches file by passed URL
|
||||||
*
|
*
|
||||||
* @param {string} url - URL of the file
|
* @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
|
* @param {object} map - object that represents how should fields of File object should be mapped to response
|
||||||
* @returns {Promise<FileData>}
|
* @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 fetchedFile = await fetch(url);
|
||||||
const buffer = await fetchedFile.buffer();
|
const buffer = await fetchedFile.buffer();
|
||||||
const filename = await random16();
|
const filename = await random16();
|
||||||
|
@ -85,6 +87,7 @@ class Transport {
|
||||||
const mimeType = type ? type.mime : fetchedMimeType;
|
const mimeType = type ? type.mime : fetchedMimeType;
|
||||||
|
|
||||||
const file = new File({
|
const file = new File({
|
||||||
|
username,
|
||||||
name: url,
|
name: url,
|
||||||
filename: `${filename}.${ext}`,
|
filename: `${filename}.${ext}`,
|
||||||
path: `${config.get('uploads')}/${filename}.${ext}`,
|
path: `${config.get('uploads')}/${filename}.${ext}`,
|
||||||
|
|
|
@ -6,6 +6,7 @@ const filesDb = database['files'];
|
||||||
* @typedef {object} FileData
|
* @typedef {object} FileData
|
||||||
*
|
*
|
||||||
* @property {string} _id - file id
|
* @property {string} _id - file id
|
||||||
|
* @property {string} username - username
|
||||||
* @property {string} name - original file name
|
* @property {string} name - original file name
|
||||||
* @property {string} filename - name of uploaded file
|
* @property {string} filename - name of uploaded file
|
||||||
* @property {string} path - path to uploaded file
|
* @property {string} path - path to uploaded file
|
||||||
|
@ -15,6 +16,7 @@ const filesDb = database['files'];
|
||||||
*/
|
*/
|
||||||
export interface FileData {
|
export interface FileData {
|
||||||
_id?: string;
|
_id?: string;
|
||||||
|
username?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
|
@ -29,6 +31,7 @@ export interface FileData {
|
||||||
* @class File model
|
* @class File model
|
||||||
*
|
*
|
||||||
* @property {string} _id - file id
|
* @property {string} _id - file id
|
||||||
|
* @property {string} username - username
|
||||||
* @property {string} name - original file name
|
* @property {string} name - original file name
|
||||||
* @property {string} filename - name of uploaded file
|
* @property {string} filename - name of uploaded file
|
||||||
* @property {string} path - path to uploaded file
|
* @property {string} path - path to uploaded file
|
||||||
|
@ -37,6 +40,7 @@ export interface FileData {
|
||||||
*/
|
*/
|
||||||
class File {
|
class File {
|
||||||
public _id?: string;
|
public _id?: string;
|
||||||
|
public username?: string;
|
||||||
public name?: string;
|
public name?: string;
|
||||||
public filename?: string;
|
public filename?: string;
|
||||||
public path?: string;
|
public path?: string;
|
||||||
|
@ -102,8 +106,9 @@ class File {
|
||||||
* @param {FileData} fileData - info about file
|
* @param {FileData} fileData - info about file
|
||||||
*/
|
*/
|
||||||
public set data(fileData: FileData) {
|
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.name = name || this.name;
|
||||||
this.filename = filename || this.filename;
|
this.filename = filename || this.filename;
|
||||||
this.path = path ? this.processPath(path) : this.path;
|
this.path = path ? this.processPath(path) : this.path;
|
||||||
|
@ -120,6 +125,7 @@ class File {
|
||||||
public get data(): FileData {
|
public get data(): FileData {
|
||||||
return {
|
return {
|
||||||
_id: this._id,
|
_id: this._id,
|
||||||
|
username: this.username,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
filename: this.filename,
|
filename: this.filename,
|
||||||
path: this.path,
|
path: this.path,
|
||||||
|
|
|
@ -6,6 +6,7 @@ const pagesDb = database['pages'];
|
||||||
/**
|
/**
|
||||||
* @typedef {object} PageData
|
* @typedef {object} PageData
|
||||||
* @property {string} _id - page id
|
* @property {string} _id - page id
|
||||||
|
* @property {string} username - username
|
||||||
* @property {string} title - page title
|
* @property {string} title - page title
|
||||||
* @property {string} uri - page uri
|
* @property {string} uri - page uri
|
||||||
* @property {*} body - page body
|
* @property {*} body - page body
|
||||||
|
@ -13,6 +14,7 @@ const pagesDb = database['pages'];
|
||||||
*/
|
*/
|
||||||
export interface PageData {
|
export interface PageData {
|
||||||
_id?: string;
|
_id?: string;
|
||||||
|
username?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
uri?: string;
|
uri?: string;
|
||||||
body?: any;
|
body?: any;
|
||||||
|
@ -24,6 +26,7 @@ export interface PageData {
|
||||||
* @class Page model
|
* @class Page model
|
||||||
*
|
*
|
||||||
* @property {string} _id - page id
|
* @property {string} _id - page id
|
||||||
|
* @property {string} username - username
|
||||||
* @property {string} title - page title
|
* @property {string} title - page title
|
||||||
* @property {string} uri - page uri
|
* @property {string} uri - page uri
|
||||||
* @property {*} body - page body
|
* @property {*} body - page body
|
||||||
|
@ -31,6 +34,7 @@ export interface PageData {
|
||||||
*/
|
*/
|
||||||
class Page {
|
class Page {
|
||||||
public _id?: string;
|
public _id?: string;
|
||||||
|
public username?: string;
|
||||||
public body?: any;
|
public body?: any;
|
||||||
public title?: string;
|
public title?: string;
|
||||||
public uri?: string;
|
public uri?: string;
|
||||||
|
@ -95,8 +99,9 @@ class Page {
|
||||||
* @param {PageData} pageData - page's data
|
* @param {PageData} pageData - page's data
|
||||||
*/
|
*/
|
||||||
public set data(pageData: PageData) {
|
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.body = body || this.body;
|
||||||
this.title = this.extractTitleFromBody();
|
this.title = this.extractTitleFromBody();
|
||||||
this.uri = uri || '';
|
this.uri = uri || '';
|
||||||
|
@ -111,6 +116,7 @@ class Page {
|
||||||
public get data(): PageData {
|
public get data(): PageData {
|
||||||
return {
|
return {
|
||||||
_id: this._id,
|
_id: this._id,
|
||||||
|
username: this.username,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
uri: this.uri,
|
uri: this.uri,
|
||||||
body: this.body,
|
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 multerFunc from 'multer';
|
||||||
import Pages from '../../controllers/pages.js';
|
import Pages from '../../controllers/pages.js';
|
||||||
import PagesOrder from '../../controllers/pagesOrder.js';
|
import PagesOrder from '../../controllers/pagesOrder.js';
|
||||||
|
import verifyToken from '../middlewares/token.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const multer = multerFunc();
|
const multer = multerFunc();
|
||||||
|
@ -54,10 +55,11 @@ router.get('/pages', async (req: Request, res: Response) => {
|
||||||
*
|
*
|
||||||
* Create new page in the database
|
* 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 {
|
try {
|
||||||
const { title, body, parent } = req.body;
|
const { title, body, parent } = req.body;
|
||||||
const page = await Pages.insert({
|
const page = await Pages.insert({
|
||||||
|
username: res.locals.tokenData.username,
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
parent,
|
parent,
|
||||||
|
@ -87,7 +89,7 @@ router.put('/page', multer.none(), async (req: Request, res: Response) => {
|
||||||
*
|
*
|
||||||
* Update page data in the database
|
* 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;
|
const { id } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -122,6 +124,7 @@ router.post('/page/:id', multer.none(), async (req: Request, res: Response) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
page = await Pages.update(id, {
|
page = await Pages.update(id, {
|
||||||
|
username: res.locals.tokenData.username,
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
parent,
|
parent,
|
||||||
|
@ -144,7 +147,7 @@ router.post('/page/:id', multer.none(), async (req: Request, res: Response) => {
|
||||||
*
|
*
|
||||||
* Remove page from the database
|
* 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 {
|
try {
|
||||||
const pageId = req.params.id;
|
const pageId = req.params.id;
|
||||||
const page = await Pages.get(pageId);
|
const page = await Pages.get(pageId);
|
||||||
|
|
|
@ -5,6 +5,7 @@ import mkdirp from 'mkdirp';
|
||||||
import config from 'config';
|
import config from 'config';
|
||||||
import Transport from '../../controllers/transport.js';
|
import Transport from '../../controllers/transport.js';
|
||||||
import { random16 } from '../../utils/crypto.js';
|
import { random16 } from '../../utils/crypto.js';
|
||||||
|
import verifyToken from '../middlewares/token.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
@ -59,7 +60,7 @@ const fileUploader = multer({
|
||||||
/**
|
/**
|
||||||
* Accepts images to upload
|
* 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 = {
|
const response = {
|
||||||
success: 0,
|
success: 0,
|
||||||
message: '',
|
message: '',
|
||||||
|
@ -79,6 +80,7 @@ router.post('/transport/image', imageUploader, async (req: Request, res: Respons
|
||||||
|
|
||||||
const fileData = {
|
const fileData = {
|
||||||
...req.files.image[0],
|
...req.files.image[0],
|
||||||
|
username: res.locals.tokenData.username,
|
||||||
url: '/uploads/' + req.files.image[0].filename,
|
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
|
* 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 };
|
const response = { success: 0 };
|
||||||
|
|
||||||
if (req.files === undefined) {
|
if (req.files === undefined) {
|
||||||
|
@ -117,7 +119,13 @@ router.post('/transport/file', fileUploader, async (req: Request, res: Response)
|
||||||
try {
|
try {
|
||||||
Object.assign(
|
Object.assign(
|
||||||
response,
|
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;
|
response.success = 1;
|
||||||
|
@ -130,7 +138,7 @@ router.post('/transport/file', fileUploader, async (req: Request, res: Response)
|
||||||
/**
|
/**
|
||||||
* Accept file url to fetch
|
* 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 };
|
const response = { success: 0 };
|
||||||
|
|
||||||
if (!req.body.url) {
|
if (!req.body.url) {
|
||||||
|
@ -140,7 +148,7 @@ router.post('/transport/fetch', multer().none(), async (req: Request, res: Respo
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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;
|
response.success = 1;
|
||||||
res.status(200).json(response);
|
res.status(200).json(response);
|
||||||
|
|
|
@ -2,6 +2,8 @@ import express, { Request, Response } from 'express';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import config from 'config';
|
import config from 'config';
|
||||||
import csrf from 'csurf';
|
import csrf from 'csurf';
|
||||||
|
import User from '../models/user.js';
|
||||||
|
import { binaryMD5 } from '../utils/crypto.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const csrfProtection = csrf({ cookie: true });
|
const csrfProtection = csrf({ cookie: true });
|
||||||
|
@ -14,6 +16,7 @@ router.get('/auth', csrfProtection, function (req: Request, res: Response) {
|
||||||
res.render('auth', {
|
res.render('auth', {
|
||||||
title: 'Login page',
|
title: 'Login page',
|
||||||
csrfToken: req.csrfToken(),
|
csrfToken: req.csrfToken(),
|
||||||
|
redirectUrl: req.query.redirect || '/',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,22 +24,65 @@ router.get('/auth', csrfProtection, function (req: Request, res: Response) {
|
||||||
* Process given password
|
* Process given password
|
||||||
*/
|
*/
|
||||||
router.post('/auth', parseForm, csrfProtection, async (req: Request, res: Response) => {
|
router.post('/auth', parseForm, csrfProtection, async (req: Request, res: Response) => {
|
||||||
|
const { ADMIN_PASSWORD } = process.env;
|
||||||
|
const { _redirect: redirectUrl } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!process.env.PASSWORD) {
|
if (!ADMIN_PASSWORD) {
|
||||||
res.render('auth', {
|
res.render('auth', {
|
||||||
title: 'Login page',
|
title: 'Login page',
|
||||||
header: 'Password not set',
|
header: 'Admin password not set',
|
||||||
csrfToken: req.csrfToken(),
|
csrfToken: req.csrfToken(),
|
||||||
|
redirectUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
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', {
|
res.render('auth', {
|
||||||
title: 'Login page',
|
title: 'Login page',
|
||||||
header: 'Wrong password',
|
header: 'Wrong password',
|
||||||
csrfToken: req.csrfToken(),
|
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;
|
return;
|
||||||
|
@ -45,20 +91,23 @@ router.post('/auth', parseForm, csrfProtection, async (req: Request, res: Respon
|
||||||
const token = jwt.sign({
|
const token = jwt.sign({
|
||||||
iss: 'Codex Team',
|
iss: 'Codex Team',
|
||||||
sub: 'auth',
|
sub: 'auth',
|
||||||
|
username,
|
||||||
|
role: user.role || 'admin',
|
||||||
iat: Date.now(),
|
iat: Date.now(),
|
||||||
}, process.env.PASSWORD + config.get('secret'));
|
}, config.get('secret'));
|
||||||
|
|
||||||
res.cookie('authToken', token, {
|
res.cookie('authToken', token, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year
|
expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year
|
||||||
});
|
});
|
||||||
|
|
||||||
res.redirect('/');
|
res.redirect(redirectUrl || '/');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.render('auth', {
|
res.render('auth', {
|
||||||
title: 'Login page',
|
title: 'Login page',
|
||||||
header: 'Password not set',
|
header: 'Unexpected error',
|
||||||
csrfToken: req.csrfToken(),
|
csrfToken: req.csrfToken(),
|
||||||
|
redirectUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import express from 'express';
|
||||||
import home from './home.js';
|
import home from './home.js';
|
||||||
import pages from './pages.js';
|
import pages from './pages.js';
|
||||||
import auth from './auth.js';
|
import auth from './auth.js';
|
||||||
|
import admin from './admin.js';
|
||||||
import aliases from './aliases.js';
|
import aliases from './aliases.js';
|
||||||
import api from './api/index.js';
|
import api from './api/index.js';
|
||||||
import pagesMiddleware from './middlewares/pages.js';
|
import pagesMiddleware from './middlewares/pages.js';
|
||||||
|
@ -10,6 +11,7 @@ const router = express.Router();
|
||||||
|
|
||||||
router.use('/', pagesMiddleware, home);
|
router.use('/', pagesMiddleware, home);
|
||||||
router.use('/', pagesMiddleware, pages);
|
router.use('/', pagesMiddleware, pages);
|
||||||
|
router.use('/', pagesMiddleware, admin);
|
||||||
router.use('/', pagesMiddleware, auth);
|
router.use('/', pagesMiddleware, auth);
|
||||||
router.use('/api', api);
|
router.use('/api', api);
|
||||||
router.use('/', aliases);
|
router.use('/', aliases);
|
||||||
|
|
|
@ -7,10 +7,25 @@ import { NextFunction, Request, Response } from 'express';
|
||||||
* @param res - response object
|
* @param res - response object
|
||||||
* @param next - next function
|
* @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) {
|
if (res.locals.isAuthorized) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} 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 pages = await Pages.getAll();
|
||||||
const pagesOrder = await PagesOrder.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) {
|
} catch (error) {
|
||||||
console.log('Can not load menu:', 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;
|
const token = req.cookies.authToken;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!process.env.PASSWORD) {
|
if (!process.env.ADMIN_PASSWORD) {
|
||||||
res.locals.isAuthorized = false;
|
res.locals.isAuthorized = false;
|
||||||
next();
|
next();
|
||||||
|
|
||||||
return;
|
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.isAuthorized = !!decodedToken;
|
||||||
|
res.locals.tokenData = decodedToken;
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import express, { NextFunction, Request, Response } from 'express';
|
||||||
import Pages from '../controllers/pages.js';
|
import Pages from '../controllers/pages.js';
|
||||||
import PagesOrder from '../controllers/pagesOrder.js';
|
import PagesOrder from '../controllers/pagesOrder.js';
|
||||||
import verifyToken from './middlewares/token.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';
|
import PagesFlatArray from '../models/pagesFlatArray.js';
|
||||||
|
|
||||||
const router = express.Router();
|
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) => {
|
router.get('/page/new', verifyToken, allowEdit, async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const pagesAvailableGrouped = await Pages.groupByParent();
|
|
||||||
|
|
||||||
res.render('pages/form', {
|
res.render('pages/form', {
|
||||||
pagesAvailableGrouped,
|
|
||||||
page: null,
|
page: null,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { AliasData } from '../../models/alias.js';
|
||||||
import { FileData } from '../../models/file.js';
|
import { FileData } from '../../models/file.js';
|
||||||
import { PageData } from '../../models/page.js';
|
import { PageData } from '../../models/page.js';
|
||||||
import { PageOrderData } from '../../models/pageOrder.js';
|
import { PageOrderData } from '../../models/pageOrder.js';
|
||||||
|
import { UserData } from '../../models/user.js';
|
||||||
import initDb from './initDb.js';
|
import initDb from './initDb.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -172,4 +173,5 @@ export default {
|
||||||
aliases: new Database<AliasData>(initDb('aliases')),
|
aliases: new Database<AliasData>(initDb('aliases')),
|
||||||
pagesOrder: new Database<PageOrderData>(initDb('pagesOrder')),
|
pagesOrder: new Database<PageOrderData>(initDb('pagesOrder')),
|
||||||
files: new Database<FileData>(initDb('files')),
|
files: new Database<FileData>(initDb('files')),
|
||||||
|
users: new Database<UserData>(initDb('users')),
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,12 +6,14 @@
|
||||||
┬┴┬┴┤ ͜ʖ ͡°) ├┬┴┬┴
|
┬┴┬┴┤ ͜ʖ ͡°) ├┬┴┬┴
|
||||||
</h1>
|
</h1>
|
||||||
<p>
|
<p>
|
||||||
Enter a password to access pages editing
|
Please login to access pages editing
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{ header }}
|
{{ header }}
|
||||||
</p>
|
</p>
|
||||||
<input type="hidden" name="_csrf" value={{ csrfToken }}>
|
<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="password" name="password" placeholder="Password">
|
||||||
<input type="submit" value="Login">
|
<input type="submit" value="Login">
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -10,6 +10,11 @@
|
||||||
<li class="docs-header__menu-add docs-header__menu-add--mobile">
|
<li class="docs-header__menu-add docs-header__menu-add--mobile">
|
||||||
{% include 'components/button.twig' with {icon: 'plus', size: 'small', url: '/page/new'} %}
|
{% include 'components/button.twig' with {icon: 'plus', size: 'small', url: '/page/new'} %}
|
||||||
</li>
|
</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 %}
|
{% endif %}
|
||||||
{% for option in config.menu %}
|
{% for option in config.menu %}
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -34,6 +34,81 @@
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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 %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<select id="parent" name="parent">
|
<select id="parent" name="parent">
|
||||||
<option value="0">Root</option>
|
<option value="0">Root</option>
|
||||||
{% for _page in pagesAvailableGrouped %}
|
{% for _page in menu %}
|
||||||
{% if _page._id != currentPageId %}
|
{% if _page._id != currentPageId %}
|
||||||
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
|
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
|
||||||
{% if _page._parent != "0" %}
|
{% if _page._parent != "0" %}
|
||||||
|
@ -35,6 +35,126 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ _page.title }}
|
{{ _page.title }}
|
||||||
</option>
|
</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 %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
|
|
@ -20,6 +20,10 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<time class="page__header-time">
|
<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") }}
|
Last edit {{ (page.body.time / 1000) | date("M d Y") }}
|
||||||
</time>
|
</time>
|
||||||
{% if isAuthorized == true %}
|
{% 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;
|
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;
|
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"] {
|
input[type="password"] {
|
||||||
padding: 9px;
|
padding: 9px;
|
||||||
font-size: 15px;
|
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