1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-08-07 06:25:21 +02:00

Merge branch 'main' into feat/hawk_integration

This commit is contained in:
Umang G. Patel 2022-07-17 21:52:30 +05:30 committed by GitHub
commit 3958e959f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 23592 additions and 60 deletions

View file

@ -83,6 +83,21 @@ yarn lint
yarn test
```
### Setup
You can configure application using configs in <code>/config</code> directory.
| Property | Role |
|----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <code>port</code> | to set port of application |
| <code>database</code> | to name directory with data |
| <code>rcFile</code> | to set destination of codexdocsrc config file |
| <code>uploads</code> | to set destination of directory to save uploads |
| <code>secret</code> | to set secret |
| <code>favicon</code> | to set url or favicon path (favicon need to be in /public directory), like `/myFavicon.png`, to get favicon. Server uploads file by url and saves it to temporary directory. And you can get favicon by /favicon static route of application |
You can configure application using configs in <code>/config</code> directory.
### Authentication
To manage pages you need to authorize (available on `/auth`).

View file

@ -3,5 +3,6 @@
"database": ".db",
"rcFile": "./.codexdocsrc",
"uploads": "public/uploads",
"secret": "iamasecretstring"
"secret": "iamasecretstring",
"favicon": ""
}

View file

@ -3,5 +3,6 @@
"database": ".db",
"rcFile": "./.codexdocsrc",
"uploads": "/uploads",
"secret": "iamasecretstring"
"secret": "iamasecretstring",
"favicon": ""
}

View file

@ -3,5 +3,6 @@
"database": ".testdb",
"rcFile": "./src/test/.codexdocsrc",
"uploads": "public/uploads_test",
"secret": "iamasecretstring"
"secret": "iamasecretstring",
"favicon": ""
}

23343
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,9 @@ import HttpException from './exceptions/httpException';
import * as dotenv from 'dotenv';
import config from 'config';
import HawkCatcher from '@hawk.so/nodejs';
import os from 'os';
import appConfig from 'config';
import { downloadFavicon, FaviconData } from './utils/downloadFavicon';
dotenv.config();
@ -19,6 +22,9 @@ if (process.env.HAWK_TOKEN_BACKEND) {
HawkCatcher.init(process.env.HAWK_TOKEN_BACKEND);
}
// Get url to upload favicon from config
const favicon: string = appConfig.get('favicon');
app.locals.config = localConfig;
// Set client error tracking token as app local.
if (process.env.HAWK_TOKEN_CLIENT) {
@ -30,12 +36,34 @@ app.set('views', path.join(__dirname, './', 'views'));
app.set('view engine', 'twig');
require('./utils/twig');
const downloadedFaviconFolder = os.tmpdir();
// Check if favicon is not empty
if (favicon) {
// Upload favicon by url, it's path on server is '/temp/favicon.{format}'
downloadFavicon(favicon, downloadedFaviconFolder).then((res) => {
app.locals.favicon = res;
console.log('Favicon successfully uploaded');
})
.catch( (err) => {
console.log(err);
console.log('Favicon has not uploaded');
});
} else {
console.log('Favicon is empty, using default path');
app.locals.favicon = {
destination: '/favicon.png',
type: 'image/png',
} as FaviconData;
}
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, '../../public')));
app.use('/uploads', express.static(config.get('uploads')));
app.use('/favicon', express.static(downloadedFaviconFolder));
app.use('/', routes);

View file

@ -0,0 +1,85 @@
import path from 'path';
import fs from 'fs';
import fetch from 'node-fetch';
/**
* Uploaded favicon data
*/
export interface FaviconData {
// Uploaded favicon path
destination: string;
// File type
type: string;
}
// Initiate controller for aborting request
const controller = new AbortController();
/**
* Check if string is url
*
* @param str - string to check
*/
function checkIsUrl(str: string): boolean {
const re = new RegExp('https?://');
return re.test(str);
}
/**
* Upload favicon by url, or get it by path
*
* @param destination - url or path of favicon
* @param faviconFolder - folder to save favicon
* @returns { Promise<FaviconData> } - Promise with data about favicon
*/
export async function downloadFavicon(destination: string, faviconFolder: string): Promise<FaviconData> {
// Check of destination is empty
if (!destination) {
throw Error('Favicon destination is empty');
}
// Get file name by destination
const filename = destination.substring(destination.lastIndexOf('/')+1);
// Get file format
const format = filename.split('.')[1];
// Check if string is url
if (!checkIsUrl(destination)) {
return {
destination: `/${filename}`,
type: `image/${format}`,
} as FaviconData;
}
// Create timeout to abort request
const timeoutId = setTimeout(() => {
controller.abort();
console.log('Favicon request has timed out.');
}, 5000);
// Make get request to url
const res = await fetch(destination, { signal: controller.signal });
// Get buffer data from response
const fileData = await res.buffer();
// Clear timeout, if data was got
clearTimeout(timeoutId);
// Get file path in temporary directory
const filePath = path.join(faviconFolder, `favicon.${format}`);
// Save file
await fs.writeFile(filePath, fileData, (err) => {
if (err) {
console.log(err);
}
});
return {
destination: `/favicon/favicon.${format}`,
type: `image/${format}`,
} as FaviconData;
}

View file

@ -7,7 +7,7 @@
<meta property="og:title" content="{{ page.title | striptags }}" />
<meta property="article:modified_time" content="{{ (page.body.time / 1000) | date("c") }}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="icon" type="{{ favicon.type }}" href="{{ favicon.destination }}">
</head>
<script>
window.config = {
@ -18,11 +18,11 @@
<body>
{% include "components/header.twig" with res.locals.isAuthorized %}
<div class="docs">
{% include "components/sidebar.twig" %}
<div class="docs__content">
<div class="docs__content-inner">
{% block body %}{% endblock %}
</div>

View file

@ -13,14 +13,18 @@
}
</textarea>
<header class="writing-header">
<span class="writing-header__left">
<span>
New Page at the
<div class="writing-header__inner-container">
{% set currentPageId = 0 %}
{% if page is not empty %}
{% set currentPageId = page._id %}
{% endif %}
<select name="parent">
<div class="select-wrapper">
{% if parentsChildrenOrdered is not empty %}
<label for="parent">Parent Page</label>
{% else %}
<label for="parent">New Page at the</label>
{% endif %}
<select id="parent" name="parent">
<option value="0">Root</option>
{% for _page in pagesAvailableGrouped %}
{% if _page._id != currentPageId %}
@ -34,24 +38,25 @@
{% endif %}
{% endfor %}
</select>
</span>
</div>
{% if parentsChildrenOrdered is not empty %}
<span>
Put Above
<select name="above">
<div class="select-wrapper">
<label for="above">Put Above</label>
<select id="above" name="above">
<option value="0">—</option>
{% for _page in parentsChildrenOrdered %}
<option value="{{ _page._id }}">{{ _page.title }}</option>
{% endfor %}
</select>
</span>
</div>
{% endif %}
</span>
{% if page is not empty %}
<p><input type="text" class="uri-input" name="uri-input" placeholder="URI (Optional)" value="{{ page.uri }}"></p>
{% endif %}
{% if page is not empty %}
<div class="uri-input-wrapper">
<label for="uri-input">Alias</label>
<input type="text" id="uri-input" class="uri-input" name="uri-input" placeholder="URI (Optional)" value="{{ page.uri }}">
</div>
{% endif %}
</div>
</header>
<div class="writing-editor">
<div id="editorjs"></div>

View file

@ -4,12 +4,14 @@
<title>{{ config.title }}</title>
<link rel="stylesheet" href="/dist/main.css" />
<link rel="preload" href="{{ config.landingFrameSrc }}" as="document">
<link rel="icon" type="image/png" href="/favicon.png?v=2">
<link rel="icon" type="{{ favicon.type }}" href="{{ favicon.destination }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta property="og:title" content="{{ config.title }}" />
<meta property="og:site_name" content="{{ config.title }}" />
<meta name="description" property="og:description" content="{{ config.description }}">
</head>
<script>
</script>
<body class="landing-body">
{% include "components/header.twig" %}
<div class="landing-loader" id="frame-loader">

View file

@ -1,5 +1,4 @@
.writing-header {
display: flex;
position: sticky;
top: 0;
padding: 15px 0;
@ -15,18 +14,32 @@
margin: auto;
}
&__left {
&__inner-container {
margin: auto 0;
display: flex;
color: var(--color-text-second);
gap: 10px;
& span {
margin-right: 10px;
@media(--mobile) {
flex-flow: column;
}
}
select {
max-width: 100px;
vertical-align: bottom;
& > * {
flex: 0 1 33.3%;
}
label {
display: block;
margin-bottom: 6px;
}
select {
@apply --select;
}
input {
@apply --input;
}
}
}
@ -38,16 +51,6 @@
}
}
.uri-input {
box-sizing: border-box;
width: 100%;
padding: 10px 12px;
border-radius: 3px;
border: 1px solid rgba(201, 201, 204, 0.48);
box-shadow: inset 0 1px 2px 0 rgba(35, 44, 72, 0.06);
outline: none;
}
.writing-editor {
font-size: 15px;
line-height: 1.6;

View file

@ -6,6 +6,8 @@
--color-link-hover: #F3F6F8;
--color-bg-light: #f8f7fa;
--color-page-active: #ff1767;
--color-input-primary: #F3F6F8;
--color-input-border: #477CFF;
--color-button-primary: #3389FF;
--color-button-primary-hover: #2E7AE6;
@ -77,6 +79,49 @@
}
}
--select {
position: relative;
padding: 10px 30px 10px 10px;
width: 100%;
border: solid 1px transparent;
border-radius: 8px;
background-color: var(--color-input-primary);
box-sizing: border-box;
appearance: none;
line-height: 18px;
background-image: url("../svg/arrow-down.svg");
background-repeat: no-repeat;
background-position: right 15px center;
&:focus {
border: solid 1px var(--color-input-border);
box-shadow: 0 0 0 3px rgba(18, 155, 255, 0.33);
}
&:focus-visible {
outline: none;
}
}
--input {
padding: 10px;
width: 100%;
line-height: 18px;
border: solid 1px transparent;
border-radius: 8px;
background-color: var(--color-input-primary);
box-sizing: border-box;
&:focus {
border: solid 1px var(--color-input-border);
box-shadow: 0 0 0 3px rgba(18, 155, 255, 0.33);
}
&:focus-visible {
outline: none;
}
}
--squircle {
border-radius: 8px;
@ -95,4 +140,4 @@
@custom-media --tablet all and (min-width: 980px) and (max-width: 1050px);
@custom-media --mobile all and (max-width: 980px);
@custom-media --retina all and (-webkit-min-device-pixel-ratio: 1.5);
@custom-media --can-hover all and (hover:hover)
@custom-media --can-hover all and (hover:hover);

View file

@ -0,0 +1,3 @@
<svg width="8" height="5" viewBox="0 0 8 5" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.83053 0.193862C6.70562 0.0696944 6.53666 0 6.36053 0C6.18441 0 6.01544 0.0696944 5.89053 0.193862L3.4972 2.55386L1.1372 0.193862C1.01229 0.0696944 0.843323 0 0.667199 0C0.491075 0 0.322107 0.0696944 0.197199 0.193862C0.134713 0.255837 0.0851169 0.329571 0.0512711 0.410811C0.0174253 0.49205 0 0.579187 0 0.667195C0 0.755203 0.0174253 0.84234 0.0512711 0.92358C0.0851169 1.00482 0.134713 1.07855 0.197199 1.14053L3.02387 3.9672C3.08584 4.02968 3.15957 4.07928 3.24081 4.11312C3.32205 4.14697 3.40919 4.16439 3.4972 4.16439C3.58521 4.16439 3.67234 4.14697 3.75358 4.11312C3.83482 4.07928 3.90856 4.02968 3.97053 3.9672L6.83053 1.14053C6.89302 1.07855 6.94261 1.00482 6.97646 0.92358C7.0103 0.84234 7.02773 0.755203 7.02773 0.667195C7.02773 0.579187 7.0103 0.49205 6.97646 0.410811C6.94261 0.329571 6.89302 0.255837 6.83053 0.193862Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 958 B

View file

@ -40,7 +40,7 @@ module.exports = () => {
options: {
// you can specify a publicPath here
// by default it use publicPath in webpackOptions.output
publicPath: '../',
// publicPath: '../',
},
},
{

View file

@ -1408,9 +1408,9 @@
form-data "^3.0.0"
"@types/node@*":
version "17.0.23"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da"
integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==
version "18.0.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a"
integrity sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==
"@types/node@^16.4.1":
version "16.11.26"
@ -2278,7 +2278,7 @@ clone-deep@^4.0.1:
clone-response@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
integrity sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==
dependencies:
mimic-response "^1.0.0"
@ -2368,10 +2368,10 @@ component-emitter@^1.2.0:
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
compress-brotli@^1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/compress-brotli/-/compress-brotli-1.3.6.tgz#64bd6f21f4f3e9841dbac392f4c29218caf5e9d9"
integrity sha512-au99/GqZtUtiCBliqLFbWlhnCxn+XSYjwZ77q6mKN4La4qOXDoLVPZ50iXr0WmAyMxl8yqoq3Yq4OeQNPPkyeQ==
compress-brotli@^1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/compress-brotli/-/compress-brotli-1.3.8.tgz#0c0a60c97a989145314ec381e84e26682e7b38db"
integrity sha512-lVcQsjhxhIXsuupfy9fmZUFtAIdBmXA7EGY6GBdgZ++qkM9zG4YFT8iU7FoBxzryNDMOpD1HIFHUSX4D87oqhQ==
dependencies:
"@types/json-buffer" "~3.0.0"
json-buffer "~3.0.1"
@ -3664,9 +3664,9 @@ gonzales-pe@^4.0.3:
minimist "^1.2.5"
got@^11.8.2:
version "11.8.3"
resolved "https://registry.yarnpkg.com/got/-/got-11.8.3.tgz#f496c8fdda5d729a90b4905d2b07dbd148170770"
integrity sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==
version "11.8.5"
resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046"
integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==
dependencies:
"@sindresorhus/is" "^4.0.0"
"@szmarczak/http-timer" "^4.0.5"
@ -4254,11 +4254,11 @@ jws@^3.2.2:
safe-buffer "^5.0.1"
keyv@^4.0.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.2.2.tgz#4b6f602c0228ef4d8214c03c520bef469ed6b768"
integrity sha512-uYS0vKTlBIjNCAUqrjlxmruxOEiZxZIHXyp32sdcGmP+ukFrmWUnE//RcPXJH3Vxrni1H2gsQbjHE0bH7MtMQQ==
version "4.3.2"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.3.2.tgz#e839df676a0c7ee594c8835e7c1c83742558e5c2"
integrity sha512-kn8WmodVBe12lmHpA6W8OY7SNh6wVR+Z+wZESF4iF5FCazaVXGWOtnbnvX0tMQ1bO+/TmOD9LziuYMvrIIs0xw==
dependencies:
compress-brotli "^1.3.6"
compress-brotli "^1.3.8"
json-buffer "3.0.1"
kind-of@^6.0.2:
@ -4925,7 +4925,7 @@ on-headers@~1.0.2:
once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies:
wrappy "1"
@ -6965,7 +6965,7 @@ wrap-ansi@^7.0.0:
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
write-file-atomic@^2.4.2:
version "2.4.3"