mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-06 12:12:42 +00:00
Merge branch 'master' into disable-changes-from-non-authors
This commit is contained in:
94
changelog.md
94
changelog.md
@@ -1,13 +1,17 @@
|
|||||||
```css
|
```css
|
||||||
h5 {
|
h5 {
|
||||||
font-size: .35cm !important;
|
font-size: .35cm !important;
|
||||||
margin-top: 0.3cm;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page ul ul {
|
.page ul ul {
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page .taskList {
|
||||||
|
display:block;
|
||||||
|
break-inside:auto;
|
||||||
|
}
|
||||||
|
|
||||||
.taskList li input {
|
.taskList li input {
|
||||||
list-style-type : none;
|
list-style-type : none;
|
||||||
margin-left : -0.52cm;
|
margin-left : -0.52cm;
|
||||||
@@ -36,15 +40,96 @@ pre {
|
|||||||
margin-top : 0.1cm;
|
margin-top : 0.1cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page ul + h5 {
|
||||||
|
margin-top: 0.25cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page p + h5 {
|
||||||
|
margin-top: 0.25cm;
|
||||||
|
}
|
||||||
|
|
||||||
.page .openSans {
|
.page .openSans {
|
||||||
font-family: 'Open Sans';
|
font-family: 'Open Sans';
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
### Saturday 10/12/2022 - v3.4.2
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Jeddai
|
||||||
|
|
||||||
|
* [x] Fix broken tags editor
|
||||||
|
|
||||||
|
* [x] Reduce server load to fix some saving issues
|
||||||
|
|
||||||
|
Fixes issues [#2322](https://github.com/naturalcrit/homebrewery/issues/2322)
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Account page help link for Google Drive errors
|
||||||
|
|
||||||
|
Fixes issues [#2520](https://github.com/naturalcrit/homebrewery/issues/2520)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Monday 05/12/2022 - v3.4.1
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix Account page incorrect last login time
|
||||||
|
|
||||||
|
Fixes issues [#2521](https://github.com/naturalcrit/homebrewery/issues/2521)
|
||||||
|
|
||||||
|
##### Gazook
|
||||||
|
|
||||||
|
* [x] Fix crashing on iOS and Safari browsers
|
||||||
|
|
||||||
|
Fixes issues [#2531](https://github.com/naturalcrit/homebrewery/issues/2531)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Monday 28/11/2022 - v3.4.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix for Chrome v108 handling of page size
|
||||||
|
|
||||||
|
Fixes issues [#2445](https://github.com/naturalcrit/homebrewery/issues/2445), [#2516](https://github.com/naturalcrit/homebrewery/issues/2516)
|
||||||
|
|
||||||
|
* [x] New account page with some user info, at {{openSans **USERNAME {{fa,fa-user}} → ACCOUNT {{fa,fa-user}}**}}
|
||||||
|
|
||||||
|
Fixes issues [#2049](https://github.com/naturalcrit/homebrewery/issues/2049), [#2043](https://github.com/naturalcrit/homebrewery/issues/2043)
|
||||||
|
|
||||||
|
* [x] Fix "Published/Private Brews" buttons on userpage
|
||||||
|
|
||||||
|
Fixes issues [#2449](https://github.com/naturalcrit/homebrewery/issues/2449)
|
||||||
|
|
||||||
|
##### Gazook
|
||||||
|
|
||||||
|
* [x] Make autosave default on for new users
|
||||||
|
|
||||||
|
* [x] Added link to our FAQ at {{openSans **NEED HELP? {{fa,fa-question-circle}} → FAQ {{fa,fa-question-circle}}**}}
|
||||||
|
|
||||||
|
* [x] Fix curly blocks freezing with long property lists
|
||||||
|
|
||||||
|
Fixes issues [#2393](https://github.com/naturalcrit/homebrewery/issues/2393)
|
||||||
|
|
||||||
|
* [x] Items can now be removed from {{openSans **RECENT BREWS** {{fas,fa-history}} }}
|
||||||
|
|
||||||
|
Fixes issues [#1918](https://github.com/naturalcrit/homebrewery/issues/1918)
|
||||||
|
|
||||||
|
* [x] Curly injector syntax `{blue}` highlighting in editor
|
||||||
|
|
||||||
|
Fixes issues [#1670](https://github.com/naturalcrit/homebrewery/issues/1670)
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
### Thursday 28/10/2022 - v3.3.1
|
### Thursday 28/10/2022 - v3.3.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -90,7 +175,6 @@ Fixes issues [#2135](https://github.com/naturalcrit/homebrewery/issues/2135)
|
|||||||
|
|
||||||
Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
|
Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
|
||||||
|
|
||||||
|
|
||||||
##### Gazook:
|
##### Gazook:
|
||||||
|
|
||||||
* [x] Several updates to bug reporting and error popups
|
* [x] Several updates to bug reporting and error popups
|
||||||
@@ -140,6 +224,10 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
|||||||
Fixes issues: [#1797](https://github.com/naturalcrit/homebrewery/issues/1797), [#2315](https://github.com/naturalcrit/homebrewery/issues/2315), [#2326](https://github.com/naturalcrit/homebrewery/issues/2326), [#2328](https://github.com/naturalcrit/homebrewery/issues/2328)
|
Fixes issues: [#1797](https://github.com/naturalcrit/homebrewery/issues/1797), [#2315](https://github.com/naturalcrit/homebrewery/issues/2315), [#2326](https://github.com/naturalcrit/homebrewery/issues/2326), [#2328](https://github.com/naturalcrit/homebrewery/issues/2328)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Wednesday 31/08/2022 - v3.2.1
|
### Wednesday 31/08/2022 - v3.2.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -166,8 +254,6 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
|||||||
Fixes issues: [#2301](https://github.com/naturalcrit/homebrewery/issues/2301), [#2303](https://github.com/naturalcrit/homebrewery/issues/2303), [#2121](https://github.com/naturalcrit/homebrewery/issues/2121)
|
Fixes issues: [#2301](https://github.com/naturalcrit/homebrewery/issues/2301), [#2303](https://github.com/naturalcrit/homebrewery/issues/2303), [#2121](https://github.com/naturalcrit/homebrewery/issues/2121)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Saturday 27/08/2022 - v3.2.0
|
### Saturday 27/08/2022 - v3.2.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
|
|||||||
@@ -139,10 +139,10 @@ const Editor = createClass({
|
|||||||
|
|
||||||
// Highlight injectors {style}
|
// Highlight injectors {style}
|
||||||
if(line.includes('{') && line.includes('}')){
|
if(line.includes('{') && line.includes('}')){
|
||||||
const regex = /(?<!{){(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/g;
|
const regex = /(?:^|[^{\n])({(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\2})/gm;
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(line)) != null) {
|
while ((match = regex.exec(line)) != null) {
|
||||||
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'injection' });
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Highlight inline spans {{content}}
|
// Highlight inline spans {{content}}
|
||||||
|
|||||||
@@ -9,11 +9,18 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
|||||||
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
const Themes = require('themes/themes.json');
|
||||||
|
const validations = require('./validations.js');
|
||||||
|
|
||||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
||||||
|
|
||||||
const homebreweryThumbnail = require('../../thumbnail.png');
|
const homebreweryThumbnail = require('../../thumbnail.png');
|
||||||
|
|
||||||
|
const callIfExists = (val, fn, ...args)=>{
|
||||||
|
if(val[fn]) {
|
||||||
|
val[fn](...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const MetadataEditor = createClass({
|
const MetadataEditor = createClass({
|
||||||
displayName : 'MetadataEditor',
|
displayName : 'MetadataEditor',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
@@ -22,6 +29,7 @@ const MetadataEditor = createClass({
|
|||||||
editId : null,
|
editId : null,
|
||||||
title : '',
|
title : '',
|
||||||
description : '',
|
description : '',
|
||||||
|
thumbnail : '',
|
||||||
tags : [],
|
tags : [],
|
||||||
published : false,
|
published : false,
|
||||||
authors : [],
|
authors : [],
|
||||||
@@ -51,11 +59,27 @@ const MetadataEditor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleFieldChange : function(name, e){
|
handleFieldChange : function(name, e){
|
||||||
this.props.onChange({
|
// load validation rules, and check input value against them
|
||||||
...this.props.metadata,
|
const inputRules = validations[name] ?? [];
|
||||||
[name] : e.target.value
|
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
||||||
});
|
|
||||||
|
// if no validation rules, save to props
|
||||||
|
if(validationErr.length === 0){
|
||||||
|
callIfExists(e.target, 'setCustomValidity', '');
|
||||||
|
this.props.onChange({
|
||||||
|
...this.props.metadata,
|
||||||
|
[name] : e.target.value
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// if validation issues, display built-in browser error popup with each error.
|
||||||
|
const errMessage = validationErr.map((err)=>{
|
||||||
|
return `- ${err}`;
|
||||||
|
}).join('\n');
|
||||||
|
callIfExists(e.target, 'setCustomValidity', errMessage);
|
||||||
|
callIfExists(e.target, 'reportValidity');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSystem : function(system, e){
|
handleSystem : function(system, e){
|
||||||
if(e.target.checked){
|
if(e.target.checked){
|
||||||
this.props.metadata.systems.push(system);
|
this.props.metadata.systems.push(system);
|
||||||
@@ -64,6 +88,7 @@ const MetadataEditor = createClass({
|
|||||||
}
|
}
|
||||||
this.props.onChange(this.props.metadata);
|
this.props.onChange(this.props.metadata);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleRenderer : function(renderer, e){
|
handleRenderer : function(renderer, e){
|
||||||
if(e.target.checked){
|
if(e.target.checked){
|
||||||
this.props.metadata.renderer = renderer;
|
this.props.metadata.renderer = renderer;
|
||||||
@@ -228,21 +253,21 @@ const MetadataEditor = createClass({
|
|||||||
<div className='field title'>
|
<div className='field title'>
|
||||||
<label>title</label>
|
<label>title</label>
|
||||||
<input type='text' className='value'
|
<input type='text' className='value'
|
||||||
value={this.props.metadata.title}
|
defaultValue={this.props.metadata.title}
|
||||||
onChange={(e)=>this.handleFieldChange('title', e)} />
|
onChange={(e)=>this.handleFieldChange('title', e)} />
|
||||||
</div>
|
</div>
|
||||||
<div className='field-group'>
|
<div className='field-group'>
|
||||||
<div className='field-column'>
|
<div className='field-column'>
|
||||||
<div className='field description'>
|
<div className='field description'>
|
||||||
<label>description</label>
|
<label>description</label>
|
||||||
<textarea value={this.props.metadata.description} className='value'
|
<textarea defaultValue={this.props.metadata.description} className='value'
|
||||||
onChange={(e)=>this.handleFieldChange('description', e)} />
|
onChange={(e)=>this.handleFieldChange('description', e)} />
|
||||||
</div>
|
</div>
|
||||||
<div className='field thumbnail'>
|
<div className='field thumbnail'>
|
||||||
<label>thumbnail</label>
|
<label>thumbnail</label>
|
||||||
<input type='text'
|
<input type='text'
|
||||||
value={this.props.metadata.thumbnail}
|
defaultValue={this.props.metadata.thumbnail}
|
||||||
placeholder='my.thumbnail.url'
|
placeholder='https://my.thumbnail.url'
|
||||||
className='value'
|
className='value'
|
||||||
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
||||||
<button className='display' onClick={this.toggleThumbnailDisplay}>
|
<button className='display' onClick={this.toggleThumbnailDisplay}>
|
||||||
|
|||||||
@@ -42,6 +42,12 @@
|
|||||||
&>.value{
|
&>.value{
|
||||||
flex : 1 1 auto;
|
flex : 1 1 auto;
|
||||||
width : 50px;
|
width : 50px;
|
||||||
|
&:invalid {
|
||||||
|
background : #ffb9b9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input[type='text'], textarea {
|
||||||
|
border : 1px solid gray;
|
||||||
}
|
}
|
||||||
&.thumbnail{
|
&.thumbnail{
|
||||||
height : 1.4em;
|
height : 1.4em;
|
||||||
|
|||||||
34
client/homebrew/editor/metadataEditor/validations.js
Normal file
34
client/homebrew/editor/metadataEditor/validations.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
module.exports = {
|
||||||
|
title : [
|
||||||
|
(value)=>{
|
||||||
|
return value?.length > 100 ? 'Max title length of 100 characters' : null;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
description : [
|
||||||
|
(value)=>{
|
||||||
|
return value?.length > 500 ? 'Max description length of 500 characters.' : null;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
thumbnail : [
|
||||||
|
(value)=>{
|
||||||
|
return value?.length > 256 ? 'Max URL length of 256 characters.' : null;
|
||||||
|
},
|
||||||
|
(value)=>{
|
||||||
|
if(value?.length == 0){return null;}
|
||||||
|
try {
|
||||||
|
Boolean(new URL(value));
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return 'Must be a valid URL';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
language : [
|
||||||
|
(value)=>{
|
||||||
|
return new RegExp(/[a-z]{2,3}(-.*)?/).test(value || '') === false ? 'Invalid language code.' : null;
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -42,7 +42,6 @@ const AccountPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderUiItems : function() {
|
renderUiItems : function() {
|
||||||
// console.log(this.props.uiItems);
|
|
||||||
return <>
|
return <>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h1>Account Information <i className='fas fa-user'></i></h1>
|
<h1>Account Information <i className='fas fa-user'></i></h1>
|
||||||
@@ -51,12 +50,16 @@ const AccountPage = createClass({
|
|||||||
</div>
|
</div>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h3>Homebrewery Information <NaturalCritIcon /></h3>
|
<h3>Homebrewery Information <NaturalCritIcon /></h3>
|
||||||
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount || '-'}</p>
|
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
|
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
|
||||||
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
|
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
|
||||||
{this.props.uiItems.googleId ? <p><strong>Brews on Google Drive: </strong> {this.props.uiItems.fileCount || '-'}</p> : '' }
|
{this.props.uiItems.googleId &&
|
||||||
|
<p>
|
||||||
|
<strong>Brews on Google Drive: </strong> {this.props.uiItems.googleCount ?? <>Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a></>}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</>;
|
</>;
|
||||||
},
|
},
|
||||||
|
|||||||
2081
package-lock.json
generated
2081
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "3.3.1",
|
"version": "3.4.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "16.11.x"
|
"node": "16.11.x"
|
||||||
},
|
},
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.19.6",
|
"@babel/core": "^7.20.5",
|
||||||
"@babel/plugin-transform-runtime": "^7.19.6",
|
"@babel/plugin-transform-runtime": "^7.19.6",
|
||||||
"@babel/preset-env": "^7.19.4",
|
"@babel/preset-env": "^7.19.4",
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.18.6",
|
||||||
@@ -64,32 +64,32 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.1.7",
|
"express-static-gzip": "2.1.7",
|
||||||
"fs-extra": "10.1.0",
|
"fs-extra": "11.1.0",
|
||||||
"googleapis": "109.0.1",
|
"googleapis": "109.0.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^3.13.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "4.2.2",
|
"marked": "4.2.4",
|
||||||
"marked-extended-tables": "^1.0.5",
|
"marked-extended-tables": "^1.0.5",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"mongoose": "^6.7.0",
|
"mongoose": "^6.8.0",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"nconf": "^0.12.0",
|
"nconf": "^0.12.0",
|
||||||
"npm": "^8.10.0",
|
"npm": "^8.10.0",
|
||||||
"react": "^16.14.0",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^17.0.2",
|
||||||
"react-frame-component": "4.1.3",
|
"react-frame-component": "4.1.3",
|
||||||
"react-router-dom": "6.4.3",
|
"react-router-dom": "6.4.5",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^6.1.0",
|
"superagent": "^6.1.0",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.26.0",
|
"eslint": "^8.29.0",
|
||||||
"eslint-plugin-react": "^7.31.10",
|
"eslint-plugin-react": "^7.31.11",
|
||||||
"jest": "^29.2.2",
|
"jest": "^29.2.2",
|
||||||
"supertest": "^6.3.1"
|
"supertest": "^6.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
113
server/app.js
113
server/app.js
@@ -77,6 +77,14 @@ const faqText = require('fs').readFileSync('faq.md', 'utf8');
|
|||||||
|
|
||||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||||
|
|
||||||
|
const defaultMetaTags = {
|
||||||
|
site_name : 'The Homebrewery - Make your Homebrew content look legit!',
|
||||||
|
title : 'The Homebrewery',
|
||||||
|
description : 'A NaturalCrit Tool for creating authentic Homebrews using Markdown.',
|
||||||
|
image : `${config.get('publicUrl')}/thumbnail.png`,
|
||||||
|
type : 'website'
|
||||||
|
};
|
||||||
|
|
||||||
//Robots.txt
|
//Robots.txt
|
||||||
app.get('/robots.txt', (req, res)=>{
|
app.get('/robots.txt', (req, res)=>{
|
||||||
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
||||||
@@ -89,13 +97,11 @@ app.get('/', (req, res, next)=>{
|
|||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
},
|
},
|
||||||
|
|
||||||
req.ogMeta = {
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
|
|
||||||
title : 'Homepage',
|
title : 'Homepage',
|
||||||
description : 'Homepage',
|
description : 'Homepage'
|
||||||
thumbnail : `${config.get('publicUrl')}/thumbnail.png`,
|
|
||||||
type : 'website'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
@@ -107,13 +113,11 @@ app.get('/legacy', (req, res, next)=>{
|
|||||||
renderer : 'legacy'
|
renderer : 'legacy'
|
||||||
},
|
},
|
||||||
|
|
||||||
req.ogMeta = {
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
|
|
||||||
title : 'Homepage (Legacy)',
|
title : 'Homepage (Legacy)',
|
||||||
description : 'Homepage',
|
description : 'Homepage'
|
||||||
thumbnail : `${config.get('publicUrl')}/thumbnail.png`,
|
|
||||||
type : 'website'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
@@ -125,13 +129,11 @@ app.get('/migrate', (req, res, next)=>{
|
|||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
},
|
},
|
||||||
|
|
||||||
req.ogMeta = {
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
|
|
||||||
title : 'v3 Migration Guide',
|
title : 'v3 Migration Guide',
|
||||||
description : 'A brief guide to converting Legacy documents to the v3 renderer.',
|
description : 'A brief guide to converting Legacy documents to the v3 renderer.'
|
||||||
thumbnail : `${config.get('publicUrl')}/thumbnail.png`,
|
|
||||||
type : 'website'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
@@ -144,13 +146,11 @@ app.get('/changelog', async (req, res, next)=>{
|
|||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
},
|
},
|
||||||
|
|
||||||
req.ogMeta = {
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
|
|
||||||
title : 'Changelog',
|
title : 'Changelog',
|
||||||
description : 'Development changelog.',
|
description : 'Development changelog.'
|
||||||
thumbnail : null,
|
|
||||||
type : 'website'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
@@ -163,12 +163,9 @@ app.get('/faq', async (req, res, next)=>{
|
|||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
},
|
},
|
||||||
|
|
||||||
req.ogMeta = {
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
|
|
||||||
title : 'FAQ',
|
title : 'FAQ',
|
||||||
description : 'Frequently Asked Questions',
|
description : 'Frequently Asked Questions'
|
||||||
thumbnail : `${config.get('publicUrl')}/thumbnail.png`,
|
|
||||||
type : 'website'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
@@ -194,12 +191,19 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
|||||||
sanitizeBrew(brew, 'share');
|
sanitizeBrew(brew, 'share');
|
||||||
const prefix = 'HB - ';
|
const prefix = 'HB - ';
|
||||||
|
|
||||||
|
const encodeRFC3986ValueChars = (str)=>{
|
||||||
|
return (
|
||||||
|
encodeURIComponent(str)
|
||||||
|
.replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
||||||
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
||||||
res.set({
|
res.set({
|
||||||
'Cache-Control' : 'no-cache',
|
'Cache-Control' : 'no-cache',
|
||||||
'Content-Type' : 'text/plain',
|
'Content-Type' : 'text/plain',
|
||||||
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
|
'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
|
||||||
});
|
});
|
||||||
res.status(200).send(brew.text);
|
res.status(200).send(brew.text);
|
||||||
});
|
});
|
||||||
@@ -208,12 +212,10 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
|||||||
app.get('/user/:username', async (req, res, next)=>{
|
app.get('/user/:username', async (req, res, next)=>{
|
||||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||||
|
|
||||||
req.ogMeta = {
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
|
|
||||||
title : `${req.params.username}'s Collection`,
|
title : `${req.params.username}'s Collection`,
|
||||||
description : 'View my collection of homebrew on the Homebrewery.',
|
description : 'View my collection of homebrew on the Homebrewery.'
|
||||||
image : null,
|
// type : could be 'profile'?
|
||||||
type : 'website' // or 'profile'?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
@@ -274,13 +276,13 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
||||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||||
|
|
||||||
req.ogMeta = {
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
|
|
||||||
title : req.brew.title || 'Untitled Brew',
|
title : req.brew.title || 'Untitled Brew',
|
||||||
description : req.brew.description || 'No description.',
|
description : req.brew.description || 'No description.',
|
||||||
image : req.brew.thumbnail || null,
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||||
type : 'article'
|
type : 'article'
|
||||||
};
|
};
|
||||||
|
|
||||||
sanitizeBrew(req.brew, 'edit');
|
sanitizeBrew(req.brew, 'edit');
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||||
@@ -292,13 +294,12 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
|||||||
sanitizeBrew(req.brew, 'share');
|
sanitizeBrew(req.brew, 'share');
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
req.brew.title = `CLONE - ${req.brew.title}`;
|
req.brew.title = `CLONE - ${req.brew.title}`;
|
||||||
req.ogMeta = {
|
|
||||||
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : 'New',
|
title : 'New',
|
||||||
description : 'Start crafting your homebrew on the Homebrewery!',
|
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||||
image : null,
|
|
||||||
type : 'website'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -306,11 +307,10 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
|||||||
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||||
const { brew } = req;
|
const { brew } = req;
|
||||||
|
|
||||||
req.ogMeta = {
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
|
|
||||||
title : req.brew.title || 'Untitled Brew',
|
title : req.brew.title || 'Untitled Brew',
|
||||||
description : req.brew.description || 'No description.',
|
description : req.brew.description || 'No description.',
|
||||||
image : req.brew.thumbnail || null,
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||||
type : 'article'
|
type : 'article'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -340,11 +340,11 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
data.title = 'Account Information Page';
|
data.title = 'Account Information Page';
|
||||||
|
|
||||||
let auth;
|
let auth;
|
||||||
let files;
|
let googleCount = [];
|
||||||
if(req.account) {
|
if(req.account) {
|
||||||
if(req.account.googleId) {
|
if(req.account.googleId) {
|
||||||
try {
|
try {
|
||||||
auth = await GoogleActions.authCheck(req.account, res);
|
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
auth = undefined;
|
auth = undefined;
|
||||||
console.log('Google auth check failed!');
|
console.log('Google auth check failed!');
|
||||||
@@ -352,9 +352,9 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
}
|
}
|
||||||
if(auth.credentials.access_token) {
|
if(auth.credentials.access_token) {
|
||||||
try {
|
try {
|
||||||
files = await GoogleActions.listGoogleBrews(auth);
|
googleCount = await GoogleActions.listGoogleBrews(auth);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
files = undefined;
|
googleCount = undefined;
|
||||||
console.log('List Google files failed!');
|
console.log('List Google files failed!');
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
@@ -362,22 +362,29 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const query = { authors: req.account.username, googleId: { $exists: false } };
|
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||||
const brews = await HomebrewModel.find(query, 'id')
|
const mongoCount = await HomebrewModel.countDocuments(query)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
|
mongoCount = 0;
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
data.uiItems = {
|
data.uiItems = {
|
||||||
username : req.account.username,
|
username : req.account.username,
|
||||||
issued : req.account.issued,
|
issued : req.account.issued,
|
||||||
mongoCount : brews.length,
|
googleId : Boolean(req.account.googleId),
|
||||||
googleId : Boolean(req.account.googleId),
|
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
|
||||||
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
|
mongoCount : mongoCount,
|
||||||
fileCount : files?.length || '-'
|
googleCount : googleCount?.length
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
req.brew = data;
|
req.brew = data;
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : `Account Page`,
|
||||||
|
description : null
|
||||||
|
};
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -5,24 +5,28 @@ const { nanoid } = require('nanoid');
|
|||||||
const token = require('./token.js');
|
const token = require('./token.js');
|
||||||
const config = require('./config.js');
|
const config = require('./config.js');
|
||||||
|
|
||||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
|
||||||
JSON.parse(config.get('service_account')) :
|
|
||||||
config.get('service_account');
|
|
||||||
let serviceAuth;
|
let serviceAuth;
|
||||||
try {
|
if(!config.get('service_account')){
|
||||||
serviceAuth = google.auth.fromJSON(keys);
|
console.log('No Google Service Account in config files - Google Drive integration will not be available.');
|
||||||
serviceAuth.scopes = [
|
} else {
|
||||||
'https://www.googleapis.com/auth/drive'
|
const keys = typeof(config.get('service_account')) == 'string' ?
|
||||||
];
|
JSON.parse(config.get('service_account')) :
|
||||||
} catch (err) {
|
config.get('service_account');
|
||||||
console.warn(err);
|
|
||||||
console.log('Please make sure that a Google Service Account is set up properly in your config files.');
|
try {
|
||||||
|
serviceAuth = google.auth.fromJSON(keys);
|
||||||
|
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err);
|
||||||
|
console.log('Please make sure the Google Service Account is set up properly in your config files.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
google.options({ auth: serviceAuth || config.get('google_api_key') });
|
google.options({ auth: serviceAuth || config.get('google_api_key') });
|
||||||
|
|
||||||
const GoogleActions = {
|
const GoogleActions = {
|
||||||
|
|
||||||
authCheck : (account, res)=>{
|
authCheck : (account, res, updateTokens=true)=>{
|
||||||
if(!account || !account.googleId){ // If not signed into Google
|
if(!account || !account.googleId){ // If not signed into Google
|
||||||
const err = new Error('Not Signed In');
|
const err = new Error('Not Signed In');
|
||||||
err.status = 401;
|
err.status = 401;
|
||||||
@@ -40,7 +44,7 @@ const GoogleActions = {
|
|||||||
refresh_token : account.googleRefreshToken
|
refresh_token : account.googleRefreshToken
|
||||||
});
|
});
|
||||||
|
|
||||||
oAuth2Client.on('tokens', (tokens)=>{
|
updateTokens && oAuth2Client.on('tokens', (tokens)=>{
|
||||||
if(tokens.refresh_token) {
|
if(tokens.refresh_token) {
|
||||||
account.googleRefreshToken = tokens.refresh_token;
|
account.googleRefreshToken = tokens.refresh_token;
|
||||||
}
|
}
|
||||||
@@ -249,7 +253,6 @@ const GoogleActions = {
|
|||||||
text : file.data,
|
text : file.data,
|
||||||
|
|
||||||
description : obj.data.description,
|
description : obj.data.description,
|
||||||
tags : obj.data.properties.tags ? obj.data.properties.tags : '',
|
|
||||||
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
||||||
authors : [],
|
authors : [],
|
||||||
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const getId = (req)=>{
|
|||||||
return { id, googleId };
|
return { id, googleId };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBrew = (accessType, fetchGoogle = true)=>{
|
const getBrew = (accessType, stubOnly = false)=>{
|
||||||
// Create middleware with the accessType passed in as part of the scope
|
// Create middleware with the accessType passed in as part of the scope
|
||||||
return async (req, res, next)=>{
|
return async (req, res, next)=>{
|
||||||
// Get relevant IDs for the brew
|
// Get relevant IDs for the brew
|
||||||
@@ -48,7 +48,7 @@ const getBrew = (accessType, fetchGoogle = true)=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If there is a google id, try to find the google brew
|
// If there is a google id, try to find the google brew
|
||||||
if(fetchGoogle && (googleId || stub?.googleId)) {
|
if(!stubOnly && (googleId || stub?.googleId)) {
|
||||||
let googleError;
|
let googleError;
|
||||||
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
@@ -62,7 +62,7 @@ const getBrew = (accessType, fetchGoogle = true)=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If after all of that we still don't have a brew, throw an exception
|
// If after all of that we still don't have a brew, throw an exception
|
||||||
if(!stub && fetchGoogle) {
|
if(!stub && !stubOnly) {
|
||||||
throw 'Brew not found in Homebrewery database or Google Drive';
|
throw 'Brew not found in Homebrewery database or Google Drive';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,10 +237,6 @@ const updateBrew = async (req, res)=>{
|
|||||||
if(req.account) {
|
if(req.account) {
|
||||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||||
}
|
}
|
||||||
// we need the tag type change in both getBrew and here to handle the case where we don't have a stub on which to perform the modification
|
|
||||||
if(typeof brew.tags === 'string') {
|
|
||||||
brew.tags = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// define a function to catch our save errors
|
// define a function to catch our save errors
|
||||||
const saveError = (err)=>{
|
const saveError = (err)=>{
|
||||||
@@ -254,14 +250,16 @@ const updateBrew = async (req, res)=>{
|
|||||||
brew = saved?.toObject();
|
brew = saved?.toObject();
|
||||||
} else {
|
} else {
|
||||||
// if the brew does have a stub id, update it using the stub id as the key.
|
// if the brew does have a stub id, update it using the stub id as the key.
|
||||||
saved = await HomebrewModel.updateOne({ _id: brew._id }, brew).catch(saveError);
|
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||||
|
saved = await brew.save()
|
||||||
|
.catch(saveError);
|
||||||
}
|
}
|
||||||
if(!saved) return;
|
if(!saved) return;
|
||||||
// Call and wait for afterSave to complete
|
// Call and wait for afterSave to complete
|
||||||
const after = await afterSave();
|
const after = await afterSave();
|
||||||
if(!after) return;
|
if(!after) return;
|
||||||
|
|
||||||
res.status(200).send(brew);
|
res.status(200).send(saved);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteGoogleBrew = async (account, id, editId, res)=>{
|
const deleteGoogleBrew = async (account, id, editId, res)=>{
|
||||||
@@ -331,8 +329,8 @@ const deleteBrew = async (req, res, next)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
router.post('/api', asyncHandler(newBrew));
|
router.post('/api', asyncHandler(newBrew));
|
||||||
router.put('/api/:id', asyncHandler(getBrew('edit', false)), asyncHandler(updateBrew));
|
router.put('/api/:id', asyncHandler(getBrew('edit', true)), asyncHandler(updateBrew));
|
||||||
router.put('/api/update/:id', asyncHandler(getBrew('edit', false)), asyncHandler(updateBrew));
|
router.put('/api/update/:id', asyncHandler(getBrew('edit', true)), asyncHandler(updateBrew));
|
||||||
router.delete('/api/:id', asyncHandler(deleteBrew));
|
router.delete('/api/:id', asyncHandler(deleteBrew));
|
||||||
router.get('/api/remove/:id', asyncHandler(deleteBrew));
|
router.get('/api/remove/:id', asyncHandler(deleteBrew));
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ body {
|
|||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
page-break-before : always;
|
page-break-before : always;
|
||||||
page-break-after : always;
|
page-break-after : always;
|
||||||
|
contain : size;
|
||||||
//*****************************
|
//*****************************
|
||||||
// * BASE
|
// * BASE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ body {
|
|||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
page-break-before : always;
|
page-break-before : always;
|
||||||
page-break-after : always;
|
page-break-after : always;
|
||||||
|
contain : size;
|
||||||
}
|
}
|
||||||
//*****************************
|
//*****************************
|
||||||
// * BASE
|
// * BASE
|
||||||
|
|||||||
Reference in New Issue
Block a user