0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-03-26 03:58:11 +00:00

Merge branch 'master' into Intersection-Observer

This commit is contained in:
Trevor Buckner
2024-12-09 16:16:11 -05:00
committed by GitHub
64 changed files with 844 additions and 689 deletions

10
babel.config.json Normal file
View File

@@ -0,0 +1,10 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-transform-runtime",
"babel-plugin-transform-import-meta"
]
}

View File

@@ -85,6 +85,52 @@ pre {
## 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).
### Wednesday 11/27/2024 - v3.16.1
{{taskList
##### 5e-Cleric
* [x] Allow linking to specific HTML IDs via `#ID` at the end of the URL, e.g.: `homebrewery.naturalcrit.com/share/share/a6RCXwaDS58i#p4` to link to Page 4 directly
Fixes issues [#2820](https://github.com/naturalcrit/homebrewery/issues/2820), [#3505](https://github.com/naturalcrit/homebrewery/issues/3505)
* [x] Fix generation of link to certain Google Drive brews
Fixes issue [#3776](https://github.com/naturalcrit/homebrewery/issues/3776)
##### abquintic
* [x] Fix blank pages appearing when pasting text
Fixes issue [#3718](https://github.com/naturalcrit/homebrewery/issues/3718)
##### Gazook89
* [x] Add new brew viewing options to the view toolbar
- {{fac,single-spread}} {{openSans **SINGLE PAGE**}}
- {{fac,facing-spread}} {{openSans **TWO PAGE**}}
- {{fac,flow-spread}} {{openSans **GRID**}}
Fixes issue [#1379](https://github.com/naturalcrit/homebrewery/issues/1379)
* [x] Updates to tag input boxes
##### G-Ambatte
* [x] Admin tools to fix certain corrupted documents
Fixes issue [#3801](https://github.com/naturalcrit/homebrewery/issues/3801)
* [x] Fix print window being affected by document zoom
Fixes issue [#3744](https://github.com/naturalcrit/homebrewery/issues/3744)
##### calculuschild, 5e-Cleric, G-Ambatte, Gazook89, abquintic
* [x] Multiple code refactors, cleanups, and security fixes
}}
### Saturday 10/12/2024 - v3.16.0 ### Saturday 10/12/2024 - v3.16.0
{{taskList {{taskList

View File

@@ -1,3 +1,5 @@
require('./brewLookup.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const cx = require('classnames'); const cx = require('classnames');
@@ -12,22 +14,43 @@ const BrewLookup = createClass({
}, },
getInitialState() { getInitialState() {
return { return {
query : '', query : '',
foundBrew : null, foundBrew : null,
searching : false, searching : false,
error : null error : null,
scriptCount : 0
}; };
}, },
handleChange(e){ handleChange(e){
this.setState({ query: e.target.value }); this.setState({ query: e.target.value });
}, },
lookup(){ lookup(){
this.setState({ searching: true, error: null }); this.setState({ searching: true, error: null, scriptCount: 0 });
request.get(`/admin/lookup/${this.state.query}`) request.get(`/admin/lookup/${this.state.query}`)
.then((res)=>this.setState({ foundBrew: res.body })) .then((res)=>{
const foundBrew = res.body;
const scriptCheck = foundBrew?.text.match(/(<\/?s)cript/g);
this.setState({
foundBrew : foundBrew,
scriptCount : scriptCheck?.length || 0,
});
})
.catch((err)=>this.setState({ error: err })) .catch((err)=>this.setState({ error: err }))
.finally(()=>this.setState({ searching: false })); .finally(()=>{
this.setState({
searching : false
});
});
},
async cleanScript(){
if(!this.state.foundBrew?.shareId) return;
await request.put(`/admin/clean/script/${this.state.foundBrew.shareId}`)
.catch((err)=>{ this.setState({ error: err }); return; });
this.lookup();
}, },
renderFoundBrew(){ renderFoundBrew(){
@@ -46,12 +69,23 @@ const BrewLookup = createClass({
<dt>Share Link</dt> <dt>Share Link</dt>
<dd><a href={`/share/${brew.shareId}`} target='_blank' rel='noopener noreferrer'>/share/{brew.shareId}</a></dd> <dd><a href={`/share/${brew.shareId}`} target='_blank' rel='noopener noreferrer'>/share/{brew.shareId}</a></dd>
<dt>Created Time</dt>
<dd>{brew.createdAt ? Moment(brew.createdAt).toLocaleString() : 'No creation date'}</dd>
<dt>Last Updated</dt> <dt>Last Updated</dt>
<dd>{Moment(brew.updatedAt).fromNow()}</dd> <dd>{Moment(brew.updatedAt).fromNow()}</dd>
<dt>Num of Views</dt> <dt>Num of Views</dt>
<dd>{brew.views}</dd> <dd>{brew.views}</dd>
<dt>SCRIPT tags detected</dt>
<dd>{this.state.scriptCount}</dd>
</dl> </dl>
{this.state.scriptCount > 0 &&
<div className='cleanButton'>
<button onClick={this.cleanScript}>CLEAN BREW</button>
</div>
}
</div>; </div>;
}, },

View File

@@ -0,0 +1,6 @@
.brewLookup {
.cleanButton {
display : inline-block;
width : 100%;
}
}

View File

@@ -5,7 +5,7 @@ const { useState, useRef, useCallback, useMemo, useEffect } = React;
const _ = require('lodash'); const _ = require('lodash');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js'); const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
const Markdown = require('naturalcrit/markdown.js'); import Markdown from 'naturalcrit/markdown.js';
const ErrorBar = require('./errorBar/errorBar.jsx'); const ErrorBar = require('./errorBar/errorBar.jsx');
const ToolBar = require('./toolBar/toolBar.jsx'); const ToolBar = require('./toolBar/toolBar.jsx');
@@ -201,7 +201,8 @@ const BrewRenderer = (props)=>{
renderedPages.length = 0; renderedPages.length = 0;
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first // Render currently-edited page first so cross-page effects (variables, links) can propagate out first
renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1); if(rawPages.length > props.currentEditorCursorPageNum -1)
renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1);
_.forEach(rawPages, (page, index)=>{ _.forEach(rawPages, (page, index)=>{
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){ if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){

View File

@@ -1,6 +1,6 @@
require('./notificationPopup.less'); require('./notificationPopup.less');
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
const request = require('../../utils/request-middleware.js'); import request from '../../utils/request-middleware.js';
import Dialog from '../../../components/dialog.jsx'; import Dialog from '../../../components/dialog.jsx';

View File

@@ -13,6 +13,7 @@
height : auto; height : auto;
padding : 2px 0; padding : 2px 0;
font-family : 'Open Sans', sans-serif; font-family : 'Open Sans', sans-serif;
font-size : 13px;
color : #CCCCCC; color : #CCCCCC;
background-color : #555555; background-color : #555555;
& > *:not(.toggleButton) { & > *:not(.toggleButton) {
@@ -154,13 +155,6 @@
width : auto; width : auto;
min-width : 46px; min-width : 46px;
height : 100%; height : 100%;
padding : 0 0px;
font-weight : unset;
color : inherit;
background-color : unset;
&:not(button:has(i, svg)) { padding : 0 8px; }
&:hover { background-color : #444444; } &:hover { background-color : #444444; }
&:focus { border : 1px solid #D3D3D3;outline : none;} &:focus { border : 1px solid #D3D3D3;outline : none;}
&:disabled { &:disabled {

View File

@@ -4,7 +4,7 @@ const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
const Markdown = require('../../../shared/naturalcrit/markdown.js'); import Markdown from '../../../shared/naturalcrit/markdown.js';
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx'); const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
const SnippetBar = require('./snippetbar/snippetbar.jsx'); const SnippetBar = require('./snippetbar/snippetbar.jsx');

View File

@@ -3,10 +3,10 @@ require('./metadataEditor.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const request = require('../../utils/request-middleware.js'); import request from '../../utils/request-middleware.js';
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const Combobox = require('client/components/combobox.jsx'); const Combobox = require('client/components/combobox.jsx');
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx'); const TagInput = require('../tagInput/tagInput.jsx');
const Themes = require('themes/themes.json'); const Themes = require('themes/themes.json');
@@ -341,10 +341,11 @@ const MetadataEditor = createClass({
{this.renderThumbnail()} {this.renderThumbnail()}
</div> </div>
<StringArrayEditor label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]} <TagInput label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]}
placeholder='add tag' unique={true} placeholder='add tag' unique={true}
values={this.props.metadata.tags} values={this.props.metadata.tags}
onChange={(e)=>this.handleFieldChange('tags', e)}/> onChange={(e)=>this.handleFieldChange('tags', e)}
/>
<div className='field systems'> <div className='field systems'>
<label>systems</label> <label>systems</label>
@@ -363,12 +364,13 @@ const MetadataEditor = createClass({
{this.renderAuthors()} {this.renderAuthors()}
<StringArrayEditor label='invited authors' valuePatterns={[/.+/]} <TagInput label='invited authors' valuePatterns={[/.+/]}
validators={[(v)=>!this.props.metadata.authors?.includes(v)]} validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
placeholder='invite author' unique={true} placeholder='invite author' unique={true}
values={this.props.metadata.invitedAuthors} values={this.props.metadata.invitedAuthors}
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']} notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/> onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
/>
<h2>Privacy</h2> <h2>Privacy</h2>

View File

@@ -79,6 +79,7 @@
text-overflow : ellipsis; text-overflow : ellipsis;
} }
button { button {
.colorButton();
padding : 0px 5px; padding : 0px 5px;
color : white; color : white;
background-color : black; background-color : black;
@@ -138,16 +139,16 @@
margin-bottom : 15px; margin-bottom : 15px;
button { width : 100%; } button { width : 100%; }
button.publish { button.publish {
.button(@blueLight); .colorButton(@blueLight);
} }
button.unpublish { button.unpublish {
.button(@silver); .colorButton(@silver);
} }
} }
.delete.field .value { .delete.field .value {
button { button {
.button(@red); .colorButton(@red);
} }
} }
.authors.field .value { .authors.field .value {
@@ -271,7 +272,7 @@
&:last-child { border-radius : 0 0.5em 0.5em 0; } &:last-child { border-radius : 0 0.5em 0.5em 0; }
} }
.badge { .tag {
padding : 0.3em; padding : 0.3em;
margin : 2px; margin : 2px;
font-size : 0.9em; font-size : 0.9em;

View File

@@ -1,149 +0,0 @@
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const StringArrayEditor = createClass({
displayName : 'StringArrayEditor',
getDefaultProps : function() {
return {
label : '',
values : [],
valuePatterns : null,
validators : [],
placeholder : '',
notes : [],
unique : false,
cannotEdit : [],
onChange : ()=>{}
};
},
getInitialState : function() {
return {
valueContext : !!this.props.values ? this.props.values.map((value)=>({
value,
editing : false
})) : [],
temporaryValue : '',
updateValue : ''
};
},
componentDidUpdate : function(prevProps) {
if(!_.eq(this.props.values, prevProps.values)) {
this.setState({
valueContext : this.props.values ? this.props.values.map((newValue)=>({
value : newValue,
editing : this.state.valueContext.find(({ value })=>value === newValue)?.editing || false
})) : []
});
}
},
handleChange : function(value) {
this.props.onChange({
target : {
value
}
});
},
addValue : function(value){
this.handleChange(_.uniq([...this.props.values, value]));
this.setState({
temporaryValue : ''
});
},
removeValue : function(index){
this.handleChange(this.props.values.filter((_, i)=>i !== index));
},
updateValue : function(value, index){
const valueContext = this.state.valueContext;
valueContext[index].value = value;
valueContext[index].editing = false;
this.handleChange(valueContext.map((context)=>context.value));
this.setState({ valueContext, updateValue: '' });
},
editValue : function(index){
if(!!this.props.cannotEdit && this.props.cannotEdit.includes(this.props.values[index])) {
return;
}
const valueContext = this.state.valueContext.map((context, i)=>{
context.editing = index === i;
return context;
});
this.setState({ valueContext, updateValue: this.props.values[index] });
},
valueIsValid : function(value, index) {
const values = _.clone(this.props.values);
if(index !== undefined) {
values.splice(index, 1);
}
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
const uniqueIfSet = !this.props.unique || !values.includes(value);
const passesValidators = !this.props.validators || this.props.validators.every((validator)=>validator(value));
return matchesPatterns && uniqueIfSet && passesValidators;
},
handleValueInputKeyDown : function(event, index) {
if(event.key === 'Enter') {
if(this.valueIsValid(event.target.value, index)) {
if(index !== undefined) {
this.updateValue(event.target.value, index);
} else {
this.addValue(event.target.value);
}
}
} else if(event.key === 'Escape') {
this.closeEditInput(index);
}
},
closeEditInput : function(index) {
const valueContext = this.state.valueContext;
valueContext[index].editing = false;
this.setState({ valueContext, updateValue: '' });
},
render : function() {
const valueElements = Object.values(this.state.valueContext).map((context, i)=>context.editing
? <React.Fragment key={i}>
<div className='input-group'>
<input type='text' className={`value ${this.valueIsValid(this.state.updateValue, i) ? '' : 'invalid'}`} autoFocus placeholder={this.props.placeholder}
value={this.state.updateValue}
onKeyDown={(e)=>this.handleValueInputKeyDown(e, i)}
onChange={(e)=>this.setState({ updateValue: e.target.value })}/>
{<div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.closeEditInput(i); }}><i className='fa fa-undo fa-fw'/></div>}
{this.valueIsValid(this.state.updateValue, i) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.updateValue(this.state.updateValue, i); }}><i className='fa fa-check fa-fw'/></div> : null}
</div>
</React.Fragment>
: <div className='badge' key={i} onClick={()=>this.editValue(i)}>{context.value}
{!!this.props.cannotEdit && this.props.cannotEdit.includes(context.value) ? null : <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.removeValue(i); }}><i className='fa fa-times fa-fw'/></div>}
</div>
);
return <div className='field'>
<label>{this.props.label}</label>
<div style={{ flex: '1 0' }} className='value'>
<div className='list'>
{valueElements}
<div className='input-group'>
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
value={this.state.temporaryValue}
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
</div>
</div>
{this.props.notes ? this.props.notes.map((n, index)=><p key={index}><small>{n}</small></p>) : null}
</div>
</div>;
}
});
module.exports = StringArrayEditor;

View File

@@ -0,0 +1,105 @@
require('./tagInput.less');
const React = require('react');
const { useState, useEffect } = React;
const _ = require('lodash');
const TagInput = ({ unique = true, values = [], ...props }) => {
const [tempInputText, setTempInputText] = useState('');
const [tagList, setTagList] = useState(values.map((value) => ({ value, editing: false })));
useEffect(()=>{
handleChange(tagList.map((context)=>context.value))
}, [tagList])
const handleChange = (value)=>{
props.onChange({
target : { value }
})
};
const handleInputKeyDown = ({ evt, value, index, options = {} }) => {
if (_.includes(['Enter', ','], evt.key)) {
evt.preventDefault();
submitTag(evt.target.value, value, index);
if (options.clear) {
setTempInputText('');
}
}
};
const submitTag = (newValue, originalValue, index) => {
setTagList((prevContext) => {
// remove existing tag
if(newValue === null){
return [...prevContext].filter((context, i)=>i !== index);
}
// add new tag
if(originalValue === null){
return [...prevContext, { value: newValue, editing: false }]
}
// update existing tag
return prevContext.map((context, i) => {
if (i === index) {
return { ...context, value: newValue, editing: false };
}
return context;
});
});
};
const editTag = (index) => {
setTagList((prevContext) => {
return prevContext.map((context, i) => {
if (i === index) {
return { ...context, editing: true };
}
return { ...context, editing: false };
});
});
};
const renderReadTag = (context, index) => {
return (
<li key={index}
data-value={context.value}
className='tag'
onClick={() => editTag(index)}>
{context.value}
<button onClick={(evt)=>{evt.stopPropagation(); submitTag(null, context.value, index)}}><i className='fa fa-times fa-fw'/></button>
</li>
);
};
const renderWriteTag = (context, index) => {
return (
<input type='text'
key={index}
defaultValue={context.value}
onKeyDown={(evt) => handleInputKeyDown({evt, value: context.value, index: index})}
autoFocus
/>
);
};
return (
<div className='field'>
<label>{props.label}</label>
<div className='value'>
<ul className='list'>
{tagList.map((context, index) => { return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
</ul>
<input
type='text'
className='value'
placeholder={props.placeholder}
value={tempInputText}
onChange={(e) => setTempInputText(e.target.value)}
onKeyDown={(evt) => handleInputKeyDown({ evt, value: null, options: { clear: true } })}
/>
</div>
</div>
);
};
module.exports = TagInput;

View File

@@ -2,7 +2,7 @@ require('./brewItem.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const moment = require('moment'); const moment = require('moment');
const request = require('../../../../utils/request-middleware.js'); import request from '../../../../utils/request-middleware.js';
const googleDriveIcon = require('../../../../googleDrive.svg'); const googleDriveIcon = require('../../../../googleDrive.svg');
const homebreweryIcon = require('../../../../thumbnail.png'); const homebreweryIcon = require('../../../../thumbnail.png');

View File

@@ -4,7 +4,7 @@ const React = require('react');
const _ = require('lodash'); const _ = require('lodash');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const request = require('../../utils/request-middleware.js'); import request from '../../utils/request-middleware.js';
const { Meta } = require('vitreum/headtags'); const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
@@ -16,6 +16,7 @@ const PrintNavItem = require('../../navbar/print.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx'); const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const Account = require('../../navbar/account.navitem.jsx'); const Account = require('../../navbar/account.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx'); const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx'); const Editor = require('../../editor/editor.jsx');
@@ -23,7 +24,7 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const LockNotification = require('./lockNotification/lockNotification.jsx'); const LockNotification = require('./lockNotification/lockNotification.jsx');
const Markdown = require('naturalcrit/markdown.js'); import Markdown from 'naturalcrit/markdown.js';
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js'); const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js'); const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
@@ -417,6 +418,7 @@ const EditPage = createClass({
</Nav.item> </Nav.item>
</Nav.dropdown> </Nav.dropdown>
<PrintNavItem /> <PrintNavItem />
<VaultNavItem />
<RecentNavItem brew={this.state.brew} storageKey='edit' /> <RecentNavItem brew={this.state.brew} storageKey='edit' />
<Account /> <Account />
</Nav.section> </Nav.section>

View File

@@ -1,7 +1,7 @@
require('./errorPage.less'); require('./errorPage.less');
const React = require('react'); const React = require('react');
const UIPage = require('../basePages/uiPage/uiPage.jsx'); const UIPage = require('../basePages/uiPage/uiPage.jsx');
const Markdown = require('../../../../shared/naturalcrit/markdown.js'); import Markdown from '../../../../shared/naturalcrit/markdown.js';
const ErrorIndex = require('./errors/errorIndex.js'); const ErrorIndex = require('./errors/errorIndex.js');
const ErrorPage = ({ brew })=>{ const ErrorPage = ({ brew })=>{

View File

@@ -2,7 +2,7 @@ require('./homePage.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const cx = require('classnames'); const cx = require('classnames');
const request = require('../../utils/request-middleware.js'); import request from '../../utils/request-middleware.js';
const { Meta } = require('vitreum/headtags'); const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');

View File

@@ -2,9 +2,9 @@
require('./newPage.less'); require('./newPage.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const request = require('../../utils/request-middleware.js'); import request from '../../utils/request-middleware.js';
const Markdown = require('naturalcrit/markdown.js'); import Markdown from 'naturalcrit/markdown.js';
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const PrintNavItem = require('../../navbar/print.navitem.jsx'); const PrintNavItem = require('../../navbar/print.navitem.jsx');

View File

@@ -8,6 +8,7 @@ const Navbar = require('../../navbar/navbar.jsx');
const MetadataNav = require('../../navbar/metadata.navitem.jsx'); const MetadataNav = require('../../navbar/metadata.navitem.jsx');
const PrintNavItem = require('../../navbar/print.navitem.jsx'); const PrintNavItem = require('../../navbar/print.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
const Account = require('../../navbar/account.navitem.jsx'); const Account = require('../../navbar/account.navitem.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
@@ -110,6 +111,7 @@ const SharePage = createClass({
</Nav.item> </Nav.item>
</Nav.dropdown> </Nav.dropdown>
</>} </>}
<VaultNavItem/>
<RecentNavItem brew={this.props.brew} storageKey='view' /> <RecentNavItem brew={this.props.brew} storageKey='view' />
<Account /> <Account />
</Nav.section> </Nav.section>

View File

@@ -15,7 +15,7 @@ const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx'); const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
const ErrorIndex = require('../errorPage/errors/errorIndex.js'); const ErrorIndex = require('../errorPage/errors/errorIndex.js');
const request = require('../../utils/request-middleware.js'); import request from '../../utils/request-middleware.js';
const VaultPage = (props)=>{ const VaultPage = (props)=>{
const [pageState, setPageState] = useState(parseInt(props.query.page) || 1); const [pageState, setPageState] = useState(parseInt(props.query.page) || 1);

View File

@@ -92,49 +92,11 @@
&:invalid { background : rgb(255, 188, 181); } &:invalid { background : rgb(255, 188, 181); }
&[type='checkbox'] {
position : relative;
display : inline-block;
width : 50px;
height : 30px;
font-family : 'WalterTurncoat';
font-size : 20px;
font-weight : 800;
color : white;
letter-spacing : 2px;
appearance : none;
background : red;
isolation : isolate;
border-radius : 5px;
&::before,&::after {
position : absolute;
inset : 0;
z-index : 5;
padding-top : 2px;
text-align : center;
}
&::before {
display : block;
content : 'No';
}
&::after {
display : none;
content : 'Yes';
}
&:checked {
background : green;
&::before { display : none; }
&::after { display : block; }
}
}
} }
#searchButton { #searchButton {
.colorButton(@green);
position : absolute; position : absolute;
right : 20px; right : 20px;
bottom : 0; bottom : 0;
@@ -152,7 +114,6 @@
flex-direction : column; flex-direction : column;
height : 100%; height : 100%;
overflow-y : auto; overflow-y : auto;
font-family : 'BookInsanityRemake';
font-size : 0.34cm; font-size : 0.34cm;
h3 { h3 {
@@ -356,6 +317,7 @@
} }
button { button {
.colorButton(@green);
width : max-content; width : max-content;
&.previousPage { grid-area : previousPage; } &.previousPage { grid-area : previousPage; }

View File

@@ -1,4 +1,4 @@
const request = require('superagent'); import request from 'superagent';
const addHeader = (request)=>request.set('Homebrewery-Version', global.version); const addHeader = (request)=>request.set('Homebrewery-Version', global.version);
@@ -9,4 +9,4 @@ const requestMiddleware = {
delete : (path)=>addHeader(request.delete(path)), delete : (path)=>addHeader(request.delete(path)),
}; };
module.exports = requestMiddleware; export default requestMiddleware;

View File

@@ -8,6 +8,8 @@ const template = async function(name, title='', props = {}){
}); });
const ogMetaTags = ogTags.join('\n'); const ogMetaTags = ogTags.join('\n');
const ssrModule = await import(`../build/${name}/ssr.cjs`);
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html> <html>
<head> <head>
@@ -21,7 +23,7 @@ const template = async function(name, title='', props = {}){
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title> <title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
</head> </head>
<body> <body>
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main> <main id="reactRoot">${ssrModule.default(props)}</main>
<script src=${`/${name}/bundle.js`}></script> <script src=${`/${name}/bundle.js`}></script>
<script>start_app(${JSON.stringify(props)})</script> <script>start_app(${JSON.stringify(props)})</script>
</body> </body>
@@ -29,4 +31,4 @@ const template = async function(name, title='', props = {}){
`; `;
}; };
module.exports = template; export default template;

19
faq.md
View File

@@ -69,7 +69,6 @@ pre {
You can check the site status here: [Everyone or Just Me](https://downforeveryoneorjustme.com/homebrewery.naturalcrit.com) You can check the site status here: [Everyone or Just Me](https://downforeveryoneorjustme.com/homebrewery.naturalcrit.com)
### Why am I getting an error when trying to save, and my account is linked to Google? ### Why am I getting an error when trying to save, and my account is linked to Google?
A sign-in with Google only lasts a year until the authentication expires. You must go [here](https://www.naturalcrit.com/login), click the *Log-out* button, and then sign back in using your Google account. A sign-in with Google only lasts a year until the authentication expires. You must go [here](https://www.naturalcrit.com/login), click the *Log-out* button, and then sign back in using your Google account.
@@ -82,12 +81,17 @@ If you have linked your account with a Google account, you would change your pas
### Is there a way to restore a previous version of my brew? ### Is there a way to restore a previous version of my brew?
Currently, there is no way to do this through the site yourself. This would take too much of a toll on the amount of storage the homebrewery requires. However, we do have daily backups of our database that we keep for 8 days, and you can contact the moderators on [the subreddit](https://www.reddit.com/r/homebrewery) with your Homebrewery username, the name of the lost brew, and the last known time it was working properly. We can manually look through our backups and restore it if it exists. In your brew, there is an icon, :fas_clock_rotate_left:, that button opens up a menu with versions of your brew, stored in order from newer to older, up to a week old. Because of the amount of duplicates this function creates, this information is stored in **your browser**, so if you were to uninstall it or clear your cookies and site data, or change computers, the info will not be there.
Also, we do have daily backups of our database that we keep for 8 days, and you can contact the moderators on [the subreddit](https://www.reddit.com/r/homebrewery) with your Homebrewery username, the name of the lost brew, and the last known time it was working properly. We can manually look through our backups and restore it if it exists.
### I worked on a brew for X hours, and suddenly all the text disappeared! ### I worked on a brew for X hours, and suddenly all the text disappeared!
This usually happens if you accidentally drag-select all of your text and then start typing which overwrites the selection. Do not panic, and do not refresh the page or reload your brew quite yet as it is probably auto-saved in this state already. Simply press CTRL+Z as many times as needed to undo your last few changes and you will be back to where you were, then make sure to save your brew in the "good" state. This usually happens if you accidentally drag-select all of your text and then start typing which overwrites the selection. Do not panic, and do not refresh the page or reload your brew quite yet as it is probably auto-saved in this state already. Simply press CTRL+Z as many times as needed to undo your last few changes and you will be back to where you were, then make sure to save your brew in the "good" state.
You can also load a history version old enough to have all the text, using the :fas_clock_rotate_left: history versions button.
\column \column
### Why is only Chrome supported? ### Why is only Chrome supported?
@@ -112,10 +116,7 @@ Once you have an image you would like to use, it is recommended to host it somew
\page \page
### A particular font does not work for my language, what do I do? ### A particular font does not work for my language, what do I do?
The fonts used were originally created for use with the English language, though revisions since then have added more support for other languages. They are still not complete sets and may be missing a glyph/character you need. Unfortunately, the volunteer group as it stands at the time of this writing does not have a font guru, so it would be difficult to add more glyphs (especially complicated glyphs). Let us know which glyph is missing on the subreddit, but you may need to search [Google Fonts](https://fonts.google.com) for an alternative font if you need something fast. The fonts used were originally created for use with the English language, though revisions since then have added more support for other languages. They are still not complete sets and may be missing a glyph/character you need. Unfortunately, the volunteer group as it stands at the time of this writing does not have a font guru, so it would be difficult to add more glyphs (especially complicated glyphs). Let us know which glyph is missing on the subreddit, but you may need to search [Google Fonts](https://fonts.google.com) for an alternative font if you need something fast.
### Whenever I click on the "Get PDF" button, instead of getting a download, it opens Print Preview in another tab.
Yes, this is by design. In the print preview, select "Save as PDF" as the Destination, and then click "Save". There will be a normal download dialog where you can save your brew as a PDF.
### I have white borders on the bottom/sides of the print preview. ### I have white borders on the bottom/sides of the print preview.
@@ -126,4 +127,8 @@ The Homebrewery defaults to creating US Letter page sizes. If you are printing
### Typing `#### Adhesion` in the text editor doesn't show the header at all in the completed page? ### Typing `#### Adhesion` in the text editor doesn't show the header at all in the completed page?
Your ad-blocking software is mistakenly assuming your text to be an ad. Whitelist homebrewery.naturalcrit.com in your ad-blocking software. Your ad-blocking software is mistakenly assuming your text to be an ad. We recommend whitelisting homebrewery.naturalcrit.com in your ad-blocking software, as we have no ads.
### My username appears as _hidden_ when checking my brews in the Vault, why is that?
Your username is most likely your e-mail adress, and our code is picking that up and protecting your identity. This will remain as is, but you can ask for a name change by contacting the moderators on [the subreddit](https://www.reddit.com/r/homebrewery) with your Homebrewery username, and your desired new name. You will also be asked to provide details about some of your unpublished brews, to verify your identity. No information will be leaked or shared.

396
package-lock.json generated
View File

@@ -1,19 +1,19 @@
{ {
"name": "homebrewery", "name": "homebrewery",
"version": "3.16.0", "version": "3.16.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "homebrewery", "name": "homebrewery",
"version": "3.16.0", "version": "3.16.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/core": "^7.26.0", "@babel/core": "^7.26.0",
"@babel/plugin-transform-runtime": "^7.25.9", "@babel/plugin-transform-runtime": "^7.25.9",
"@babel/preset-env": "^7.26.0", "@babel/preset-env": "^7.26.0",
"@babel/preset-react": "^7.25.9", "@babel/preset-react": "^7.26.3",
"@googleapis/drive": "^8.14.0", "@googleapis/drive": "^8.14.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"classnames": "^2.5.1", "classnames": "^2.5.1",
@@ -21,11 +21,11 @@
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3", "dedent-tabs": "^0.10.3",
"dompurify": "^3.1.7", "dompurify": "^3.2.2",
"expr-eval": "^2.0.2", "expr-eval": "^2.0.2",
"express": "^4.21.1", "express": "^4.21.2",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.8", "express-static-gzip": "2.2.0",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
@@ -33,14 +33,14 @@
"less": "^3.13.1", "less": "^3.13.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "11.2.0", "marked": "11.2.0",
"marked-emoji": "^1.4.2", "marked-emoji": "^1.4.3",
"marked-extended-tables": "^1.0.10", "marked-extended-tables": "^1.0.10",
"marked-gfm-heading-id": "^3.2.0", "marked-gfm-heading-id": "^3.2.0",
"marked-smartypants-lite": "^1.0.2", "marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.7.3", "mongoose": "^8.8.4",
"nanoid": "3.3.4", "nanoid": "5.0.9",
"nconf": "^0.12.1", "nconf": "^0.12.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@@ -52,15 +52,16 @@
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^3.1.1", "@stylistic/stylelint-plugin": "^3.1.1",
"eslint": "^9.14.0", "babel-plugin-transform-import-meta": "^2.2.1",
"eslint": "^9.16.0",
"eslint-plugin-jest": "^28.9.0", "eslint-plugin-jest": "^28.9.0",
"eslint-plugin-react": "^7.37.2", "eslint-plugin-react": "^7.37.2",
"globals": "^15.12.0", "globals": "^15.13.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^16.10.0", "stylelint": "^16.11.0",
"stylelint-config-recess-order": "^5.1.1", "stylelint-config-recess-order": "^5.1.1",
"stylelint-config-recommended": "^14.0.1", "stylelint-config-recommended": "^14.0.1",
"supertest": "^7.0.0" "supertest": "^7.0.0"
@@ -559,6 +560,7 @@
"version": "7.26.0", "version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz",
"integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.25.9" "@babel/helper-plugin-utils": "^7.25.9"
}, },
@@ -1659,9 +1661,10 @@
} }
}, },
"node_modules/@babel/preset-react": { "node_modules/@babel/preset-react": {
"version": "7.25.9", "version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.26.3.tgz",
"integrity": "sha512-D3to0uSPiWE7rBrdIICCd0tJSIGpLaaGptna2+w7Pft5xMqLpA1sz99DK5TZ1TjGbdQ/VI1eCSZ06dv3lT4JOw==", "integrity": "sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9",
"@babel/helper-validator-option": "^7.25.9", "@babel/helper-validator-option": "^7.25.9",
@@ -1747,9 +1750,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@csstools/css-parser-algorithms": { "node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.1", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz",
"integrity": "sha512-lSquqZCHxDfuTg/Sk2hiS0mcSFCEBuj49JfzPHJogDBT0mGCyY5A1AQzBWngitrp7i1/HAZpIgzF/VjhOEIJIg==", "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -1761,17 +1764,18 @@
"url": "https://opencollective.com/csstools" "url": "https://opencollective.com/csstools"
} }
], ],
"license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
"peerDependencies": { "peerDependencies": {
"@csstools/css-tokenizer": "^3.0.1" "@csstools/css-tokenizer": "^3.0.3"
} }
}, },
"node_modules/@csstools/css-tokenizer": { "node_modules/@csstools/css-tokenizer": {
"version": "3.0.1", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz",
"integrity": "sha512-UBqaiu7kU0lfvaP982/o3khfXccVlHPWp0/vwwiIgDF0GmqqqxoiXC/6FCjlS9u92f7CoEz6nXKQnrn1kIAkOw==", "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -1783,6 +1787,7 @@
"url": "https://opencollective.com/csstools" "url": "https://opencollective.com/csstools"
} }
], ],
"license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1810,28 +1815,6 @@
"@csstools/css-tokenizer": "^3.0.1" "@csstools/css-tokenizer": "^3.0.1"
} }
}, },
"node_modules/@csstools/selector-specificity": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-4.0.0.tgz",
"integrity": "sha512-189nelqtPd8++phaHNwYovKZI0FOzH1vQEE3QhHHkNIGrg5fSs9CbYP3RvfEH5geztnIA9Jwq91wyOIwAW5JIQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"engines": {
"node": ">=18"
},
"peerDependencies": {
"postcss-selector-parser": "^6.1.0"
}
},
"node_modules/@dual-bundle/import-meta-resolve": { "node_modules/@dual-bundle/import-meta-resolve": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
@@ -1869,9 +1852,9 @@
} }
}, },
"node_modules/@eslint/config-array": { "node_modules/@eslint/config-array": {
"version": "0.18.0", "version": "0.19.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz",
"integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint/object-schema": "^2.1.4", "@eslint/object-schema": "^2.1.4",
@@ -1883,20 +1866,19 @@
} }
}, },
"node_modules/@eslint/core": { "node_modules/@eslint/core": {
"version": "0.7.0", "version": "0.9.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz",
"integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@eslint/eslintrc": { "node_modules/@eslint/eslintrc": {
"version": "3.1.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
"integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"ajv": "^6.12.4", "ajv": "^6.12.4",
"debug": "^4.3.2", "debug": "^4.3.2",
@@ -1920,7 +1902,6 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -1929,10 +1910,11 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.14.0", "version": "9.16.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz",
"integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
@@ -1947,9 +1929,9 @@
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.2.0", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz",
"integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"levn": "^0.4.1" "levn": "^0.4.1"
@@ -3082,6 +3064,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"optional": true
},
"node_modules/@types/webidl-conversions": { "node_modules/@types/webidl-conversions": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
@@ -3283,7 +3271,6 @@
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true, "dev": true,
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
} }
@@ -3337,7 +3324,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -3881,6 +3867,27 @@
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
} }
}, },
"node_modules/babel-plugin-transform-import-meta": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-import-meta/-/babel-plugin-transform-import-meta-2.2.1.tgz",
"integrity": "sha512-AxNh27Pcg8Kt112RGa3Vod2QS2YXKKJ6+nSvRtv7qQTJAdx0MZa4UHZ4lnxHUWA2MNbLuZQv5FVab4P1CoLOWw==",
"dev": true,
"license": "BSD",
"dependencies": {
"@babel/template": "^7.4.4",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@babel/core": "^7.10.0"
}
},
"node_modules/babel-plugin-transform-import-meta/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/babel-preset-current-node-syntax": { "node_modules/babel-preset-current-node-syntax": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz",
@@ -4327,9 +4334,9 @@
} }
}, },
"node_modules/bson": { "node_modules/bson": {
"version": "6.8.0", "version": "6.9.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", "resolved": "https://registry.npmjs.org/bson/-/bson-6.9.0.tgz",
"integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", "integrity": "sha512-X9hJeyeM0//Fus+0pc5dSUMhhrrmWwQUtdavaQeF3Ta6m69matZkGWV/MrBcnwUeLC8W9kwwc2hfkZgUuCX3Ig==",
"engines": { "engines": {
"node": ">=16.20.1" "node": ">=16.20.1"
} }
@@ -4977,11 +4984,10 @@
} }
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
"shebang-command": "^2.0.0", "shebang-command": "^2.0.0",
@@ -5023,12 +5029,13 @@
} }
}, },
"node_modules/css-tree": { "node_modules/css-tree": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.0.0.tgz", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.0.1.tgz",
"integrity": "sha512-o88DVQ6GzsABn1+6+zo2ct801dBO5OASVyxbbvA2W20ue2puSh/VOuqUj90eUeMSX/xqGqBmOKiRQN7tJOuBXw==", "integrity": "sha512-8Fxxv+tGhORlshCdCwnNJytvlvq46sOLSYEx2ZIGurahWvMucSRnyjPA3AmrMq4VPRYbHVpWj5VkiVasrM2H4Q==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"mdn-data": "2.10.0", "mdn-data": "2.12.1",
"source-map-js": "^1.0.1" "source-map-js": "^1.0.1"
}, },
"engines": { "engines": {
@@ -5455,9 +5462,13 @@
} }
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "3.1.7", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.2.tgz",
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==" "integrity": "sha512-YMM+erhdZ2nkZ4fTNRTSI94mb7VG7uVF5vj5Zde7tImgnhZE3R6YW/IACGIHb2ux+QkEXMhe591N+5jWOmL4Zw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
}, },
"node_modules/duplexer2": { "node_modules/duplexer2": {
"version": "0.1.4", "version": "0.1.4",
@@ -5489,9 +5500,9 @@
"integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==" "integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw=="
}, },
"node_modules/elliptic": { "node_modules/elliptic": {
"version": "6.5.7", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz",
"integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==",
"dependencies": { "dependencies": {
"bn.js": "^4.11.9", "bn.js": "^4.11.9",
"brorand": "^1.1.0", "brorand": "^1.1.0",
@@ -5761,26 +5772,27 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.14.0", "version": "9.16.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz",
"integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.18.0", "@eslint/config-array": "^0.19.0",
"@eslint/core": "^0.7.0", "@eslint/core": "^0.9.0",
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.2.0",
"@eslint/js": "9.14.0", "@eslint/js": "9.16.0",
"@eslint/plugin-kit": "^0.2.0", "@eslint/plugin-kit": "^0.2.3",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.0", "@humanwhocodes/retry": "^0.4.1",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"ajv": "^6.12.4", "ajv": "^6.12.4",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.2", "cross-spawn": "^7.0.5",
"debug": "^4.3.2", "debug": "^4.3.2",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.2.0", "eslint-scope": "^8.2.0",
@@ -5799,8 +5811,7 @@
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"optionator": "^0.9.3", "optionator": "^0.9.3"
"text-table": "^0.2.0"
}, },
"bin": { "bin": {
"eslint": "bin/eslint.js" "eslint": "bin/eslint.js"
@@ -6243,9 +6254,10 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.21.1", "version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
@@ -6266,7 +6278,7 @@
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.10", "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.13.0", "qs": "6.13.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
@@ -6281,6 +6293,10 @@
}, },
"engines": { "engines": {
"node": ">= 0.10.0" "node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-async-handler": { "node_modules/express-async-handler": {
@@ -6290,10 +6306,11 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/express-static-gzip": { "node_modules/express-static-gzip": {
"version": "2.1.8", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.1.8.tgz", "resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.2.0.tgz",
"integrity": "sha512-g8tiJuI9Y9Ffy59ehVXvqb0hhP83JwZiLxzanobPaMbkB5qBWA8nuVgd+rcd5qzH3GkgogTALlc0BaADYwnMbQ==", "integrity": "sha512-4ZQ0pHX0CAauxmzry2/8XFLM6aZA4NBvg9QezSlsEO1zLnl7vMFa48/WIcjzdfOiEUS4S1npPPKP2NHHYAp6qg==",
"dependencies": { "dependencies": {
"parseurl": "^1.3.3",
"serve-static": "^1.16.2" "serve-static": "^1.16.2"
} }
}, },
@@ -6948,10 +6965,11 @@
} }
}, },
"node_modules/globals": { "node_modules/globals": {
"version": "15.12.0", "version": "15.13.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-15.13.0.tgz",
"integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==", "integrity": "sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -10063,8 +10081,7 @@
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true, "dev": true
"license": "MIT"
}, },
"node_modules/json-stable-stringify": { "node_modules/json-stable-stringify": {
"version": "0.0.1", "version": "0.0.1",
@@ -10225,9 +10242,9 @@
} }
}, },
"node_modules/known-css-properties": { "node_modules/known-css-properties": {
"version": "0.34.0", "version": "0.35.0",
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.34.0.tgz", "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz",
"integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==", "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -10487,11 +10504,11 @@
} }
}, },
"node_modules/marked-emoji": { "node_modules/marked-emoji": {
"version": "1.4.2", "version": "1.4.3",
"resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-1.4.2.tgz", "resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-1.4.3.tgz",
"integrity": "sha512-2sP+bp2z76dwbILzQ7ijy2PyjjAJR3iAZCzaNGThD2UijFUBeidkn6MoCdX/j47tPIcWt9nwnjqRQPd01ZrfdA==", "integrity": "sha512-HDZx1VOmzu7XT2QNKWfrHGbNRMTWKj9XD78yrcH1madD30HpGLMODPOmKr/e7CA7NKKXkpXXNdndQn++ysXmHg==",
"peerDependencies": { "peerDependencies": {
"marked": ">=4 <15" "marked": ">=4 <16"
} }
}, },
"node_modules/marked-extended-tables": { "node_modules/marked-extended-tables": {
@@ -10559,10 +10576,11 @@
} }
}, },
"node_modules/mdn-data": { "node_modules/mdn-data": {
"version": "2.10.0", "version": "2.12.1",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.10.0.tgz", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.1.tgz",
"integrity": "sha512-qq7C3EtK3yJXMwz1zAab65pjl+UhohqMOctTgcqjLOWABqmwj+me02LSsCuEUxnst9X1lCBpoE0WArGKgdGDzw==", "integrity": "sha512-rsfnCbOHjqrhWxwt5/wtSLzpoKTzW7OXdT5lLOIH1OTYhWu9rRJveGq0sKvDZODABH7RX+uoR+DYcpFnq4Tf6Q==",
"dev": true "dev": true,
"license": "CC0-1.0"
}, },
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
@@ -10865,13 +10883,14 @@
} }
}, },
"node_modules/mongoose": { "node_modules/mongoose": {
"version": "8.7.3", "version": "8.8.4",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.3.tgz", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.4.tgz",
"integrity": "sha512-Xl6+dzU5ZpEcDoJ8/AyrIdAwTY099QwpolvV73PIytpK13XqwllLq/9XeVzzLEQgmyvwBVGVgjmMrKbuezxrIA==", "integrity": "sha512-yJbn695qCsqDO+xyPII29x2R7flzXhxCDv09mMZPSGllf0sm4jKw3E9s9uvQ9hjO6bL2xjU8KKowYqcY9eSTMQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"bson": "^6.7.0", "bson": "^6.7.0",
"kareem": "2.6.3", "kareem": "2.6.3",
"mongodb": "6.9.0", "mongodb": "~6.10.0",
"mpath": "0.9.0", "mpath": "0.9.0",
"mquery": "5.0.0", "mquery": "5.0.0",
"ms": "2.1.3", "ms": "2.1.3",
@@ -10943,9 +10962,9 @@
} }
}, },
"node_modules/mongoose/node_modules/mongodb": { "node_modules/mongoose/node_modules/mongodb": {
"version": "6.9.0", "version": "6.10.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.9.0.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.10.0.tgz",
"integrity": "sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==", "integrity": "sha512-gP9vduuYWb9ZkDM546M+MP2qKVk5ZG2wPF63OvSRuUbqCR+11ZCAE1mOfllhlAG0wcoJY5yDL/rV3OmYEwXIzg==",
"dependencies": { "dependencies": {
"@mongodb-js/saslprep": "^1.1.5", "@mongodb-js/saslprep": "^1.1.5",
"bson": "^6.7.0", "bson": "^6.7.0",
@@ -11021,15 +11040,21 @@
"optional": true "optional": true
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.4", "version": "5.0.9",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", "integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"nanoid": "bin/nanoid.cjs" "nanoid": "bin/nanoid.js"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^18 || >=20"
} }
}, },
"node_modules/nanomatch": { "node_modules/nanomatch": {
@@ -11723,9 +11748,10 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.10", "version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
}, },
"node_modules/path-type": { "node_modules/path-type": {
"version": "4.0.0", "version": "4.0.0",
@@ -11754,9 +11780,10 @@
} }
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@@ -11879,9 +11906,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.47", "version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -11897,9 +11924,10 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"picocolors": "^1.1.0", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
"engines": { "engines": {
@@ -13646,9 +13674,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/stylelint": { "node_modules/stylelint": {
"version": "16.10.0", "version": "16.11.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.10.0.tgz", "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.11.0.tgz",
"integrity": "sha512-z/8X2rZ52dt2c0stVwI9QL2AFJhLhbPkyfpDFcizs200V/g7v+UYY6SNcB9hKOLcDDX/yGLDsY/pX08sLkz9xQ==", "integrity": "sha512-zrl4IrKmjJQ+h9FoMp69UMCq5SxeHk0URhxUBj4d3ISzo/DplOFBJZc7t7Dr6otB+1bfbbKNLOmCDpzKSlW+Nw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -13660,17 +13688,18 @@
"url": "https://github.com/sponsors/stylelint" "url": "https://github.com/sponsors/stylelint"
} }
], ],
"license": "MIT",
"dependencies": { "dependencies": {
"@csstools/css-parser-algorithms": "^3.0.1", "@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.1", "@csstools/css-tokenizer": "^3.0.3",
"@csstools/media-query-list-parser": "^3.0.1", "@csstools/media-query-list-parser": "^4.0.2",
"@csstools/selector-specificity": "^4.0.0", "@csstools/selector-specificity": "^5.0.0",
"@dual-bundle/import-meta-resolve": "^4.1.0", "@dual-bundle/import-meta-resolve": "^4.1.0",
"balanced-match": "^2.0.0", "balanced-match": "^2.0.0",
"colord": "^2.9.3", "colord": "^2.9.3",
"cosmiconfig": "^9.0.0", "cosmiconfig": "^9.0.0",
"css-functions-list": "^3.2.3", "css-functions-list": "^3.2.3",
"css-tree": "^3.0.0", "css-tree": "^3.0.1",
"debug": "^4.3.7", "debug": "^4.3.7",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"fastest-levenshtein": "^1.0.16", "fastest-levenshtein": "^1.0.16",
@@ -13682,16 +13711,16 @@
"ignore": "^6.0.2", "ignore": "^6.0.2",
"imurmurhash": "^0.1.4", "imurmurhash": "^0.1.4",
"is-plain-object": "^5.0.0", "is-plain-object": "^5.0.0",
"known-css-properties": "^0.34.0", "known-css-properties": "^0.35.0",
"mathml-tag-names": "^2.1.3", "mathml-tag-names": "^2.1.3",
"meow": "^13.2.0", "meow": "^13.2.0",
"micromatch": "^4.0.8", "micromatch": "^4.0.8",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"picocolors": "^1.0.1", "picocolors": "^1.1.1",
"postcss": "^8.4.47", "postcss": "^8.4.49",
"postcss-resolve-nested-selector": "^0.1.6", "postcss-resolve-nested-selector": "^0.1.6",
"postcss-safe-parser": "^7.0.1", "postcss-safe-parser": "^7.0.1",
"postcss-selector-parser": "^6.1.2", "postcss-selector-parser": "^7.0.0",
"postcss-value-parser": "^4.2.0", "postcss-value-parser": "^4.2.0",
"resolve-from": "^5.0.0", "resolve-from": "^5.0.0",
"string-width": "^4.2.3", "string-width": "^4.2.3",
@@ -13756,6 +13785,53 @@
"stylelint": "^14.0.0 || ^15.0.0 || ^16.0.1" "stylelint": "^14.0.0 || ^15.0.0 || ^16.0.1"
} }
}, },
"node_modules/stylelint/node_modules/@csstools/media-query-list-parser": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz",
"integrity": "sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3"
}
},
"node_modules/stylelint/node_modules/@csstools/selector-specificity": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz",
"integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"postcss-selector-parser": "^7.0.0"
}
},
"node_modules/stylelint/node_modules/balanced-match": { "node_modules/stylelint/node_modules/balanced-match": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz",
@@ -13797,6 +13873,20 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/stylelint/node_modules/postcss-selector-parser": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz",
"integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/stylelint/node_modules/resolve-from": { "node_modules/stylelint/node_modules/resolve-from": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@@ -14060,13 +14150,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true,
"license": "MIT"
},
"node_modules/through": { "node_modules/through": {
"version": "2.3.8", "version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -14678,7 +14761,6 @@
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }

View File

@@ -1,7 +1,8 @@
{ {
"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.16.0", "version": "3.16.1",
"type": "module",
"engines": { "engines": {
"npm": "^10.2.x", "npm": "^10.2.x",
"node": "^20.18.x" "node": "^20.18.x"
@@ -56,6 +57,9 @@
"shared", "shared",
"server" "server"
], ],
"transformIgnorePatterns": [
"node_modules/(?!nanoid/).*"
],
"coveragePathIgnorePatterns": [ "coveragePathIgnorePatterns": [
"build/*" "build/*"
], ],
@@ -77,20 +81,11 @@
"jest-expect-message" "jest-expect-message"
] ]
}, },
"babel": {
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-transform-runtime"
]
},
"dependencies": { "dependencies": {
"@babel/core": "^7.26.0", "@babel/core": "^7.26.0",
"@babel/plugin-transform-runtime": "^7.25.9", "@babel/plugin-transform-runtime": "^7.25.9",
"@babel/preset-env": "^7.26.0", "@babel/preset-env": "^7.26.0",
"@babel/preset-react": "^7.25.9", "@babel/preset-react": "^7.26.3",
"@googleapis/drive": "^8.14.0", "@googleapis/drive": "^8.14.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"classnames": "^2.5.1", "classnames": "^2.5.1",
@@ -98,11 +93,11 @@
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3", "dedent-tabs": "^0.10.3",
"dompurify": "^3.1.7", "dompurify": "^3.2.2",
"expr-eval": "^2.0.2", "expr-eval": "^2.0.2",
"express": "^4.21.1", "express": "^4.21.2",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.8", "express-static-gzip": "2.2.0",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
@@ -110,14 +105,14 @@
"less": "^3.13.1", "less": "^3.13.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "11.2.0", "marked": "11.2.0",
"marked-emoji": "^1.4.2", "marked-emoji": "^1.4.3",
"marked-extended-tables": "^1.0.10", "marked-extended-tables": "^1.0.10",
"marked-gfm-heading-id": "^3.2.0", "marked-gfm-heading-id": "^3.2.0",
"marked-smartypants-lite": "^1.0.2", "marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.7.3", "mongoose": "^8.8.4",
"nanoid": "3.3.4", "nanoid": "5.0.9",
"nconf": "^0.12.1", "nconf": "^0.12.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@@ -129,15 +124,16 @@
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^3.1.1", "@stylistic/stylelint-plugin": "^3.1.1",
"eslint": "^9.14.0", "babel-plugin-transform-import-meta": "^2.2.1",
"eslint": "^9.16.0",
"eslint-plugin-jest": "^28.9.0", "eslint-plugin-jest": "^28.9.0",
"eslint-plugin-react": "^7.37.2", "eslint-plugin-react": "^7.37.2",
"globals": "^15.12.0", "globals": "^15.13.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^16.10.0", "stylelint": "^16.11.0",
"stylelint-config-recess-order": "^5.1.1", "stylelint-config-recess-order": "^5.1.1",
"stylelint-config-recommended": "^14.0.1", "stylelint-config-recommended": "^14.0.1",
"supertest": "^7.0.0" "supertest": "^7.0.0"

View File

@@ -1,13 +1,14 @@
const fs = require('fs-extra');
const Proj = require('./project.json');
const { pack } = require('vitreum'); import fs from 'fs-extra';
import Proj from './project.json' with { type: 'json' };
import vitreum from 'vitreum';
const { pack } = vitreum;
import lessTransform from 'vitreum/transforms/less.js';
import assetTransform from 'vitreum/transforms/asset.js';
const isDev = !!process.argv.find((arg)=>arg=='--dev'); const isDev = !!process.argv.find((arg)=>arg=='--dev');
const lessTransform = require('vitreum/transforms/less.js');
const assetTransform = require('vitreum/transforms/asset.js');
//const Meta = require('vitreum/headtags');
const transforms = { const transforms = {
'.less' : lessTransform, '.less' : lessTransform,
'*' : assetTransform('./build') '*' : assetTransform('./build')
@@ -17,7 +18,7 @@ const build = async ({ bundle, render, ssr })=>{
const css = await lessTransform.generate({ paths: './shared' }); const css = await lessTransform.generate({ paths: './shared' });
await fs.outputFile('./build/admin/bundle.css', css); await fs.outputFile('./build/admin/bundle.css', css);
await fs.outputFile('./build/admin/bundle.js', bundle); await fs.outputFile('./build/admin/bundle.js', bundle);
await fs.outputFile('./build/admin/ssr.js', ssr); await fs.outputFile('./build/admin/ssr.cjs', ssr);
}; };
fs.emptyDirSync('./build/admin'); fs.emptyDirSync('./build/admin');

View File

@@ -1,14 +1,15 @@
const fs = require('fs-extra'); import fs from 'fs-extra';
const zlib = require('zlib'); import zlib from 'zlib';
const Proj = require('./project.json'); import Proj from './project.json' with { type: 'json' };
import vitreum from 'vitreum';
const { pack, watchFile, livereload } = vitreum;
const { pack, watchFile, livereload } = require('vitreum'); import lessTransform from 'vitreum/transforms/less.js';
const isDev = !!process.argv.find((arg)=>arg=='--dev'); import assetTransform from 'vitreum/transforms/asset.js';
import babel from '@babel/core';
import less from 'less';
const lessTransform = require('vitreum/transforms/less.js'); const isDev = !!process.argv.find((arg) => arg === '--dev');
const assetTransform = require('vitreum/transforms/asset.js');
const babel = require('@babel/core');
const less = require('less');
const babelify = async (code)=>(await babel.transformAsync(code, { presets: [['@babel/preset-env', { 'exclude': ['proposal-dynamic-import'] }], '@babel/preset-react'], plugins: ['@babel/plugin-transform-runtime'] })).code; const babelify = async (code)=>(await babel.transformAsync(code, { presets: [['@babel/preset-env', { 'exclude': ['proposal-dynamic-import'] }], '@babel/preset-react'], plugins: ['@babel/plugin-transform-runtime'] })).code;
@@ -24,7 +25,7 @@ const build = async ({ bundle, render, ssr })=>{
//css = `@layer bundle {\n${css}\n}`; //css = `@layer bundle {\n${css}\n}`;
await fs.outputFile('./build/homebrew/bundle.css', css); await fs.outputFile('./build/homebrew/bundle.css', css);
await fs.outputFile('./build/homebrew/bundle.js', bundle); await fs.outputFile('./build/homebrew/bundle.js', bundle);
await fs.outputFile('./build/homebrew/ssr.js', ssr); await fs.outputFile('./build/homebrew/ssr.cjs', ssr);
await fs.copy('./client/homebrew/favicon.ico', './build/assets/favicon.ico'); await fs.copy('./client/homebrew/favicon.ico', './build/assets/favicon.ico');
@@ -51,7 +52,7 @@ fs.emptyDirSync('./build');
const themes = { Legacy: {}, V3: {} }; const themes = { Legacy: {}, V3: {} };
let themeFiles = fs.readdirSync('./themes/Legacy'); let themeFiles = fs.readdirSync('./themes/Legacy');
for (dir of themeFiles) { for (let dir of themeFiles) {
const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`).toString()); const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`).toString());
themeData.path = dir; themeData.path = dir;
themes.Legacy[dir] = (themeData); themes.Legacy[dir] = (themeData);
@@ -68,7 +69,7 @@ fs.emptyDirSync('./build');
} }
themeFiles = fs.readdirSync('./themes/V3'); themeFiles = fs.readdirSync('./themes/V3');
for (dir of themeFiles) { for (let dir of themeFiles) {
const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`).toString()); const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`).toString());
themeData.path = dir; themeData.path = dir;
themes.V3[dir] = (themeData); themes.V3[dir] = (themeData);
@@ -104,14 +105,14 @@ fs.emptyDirSync('./build');
const editorThemesBuildDir = './build/homebrew/cm-themes'; const editorThemesBuildDir = './build/homebrew/cm-themes';
await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir); await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir);
await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir); await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir);
editorThemeFiles = fs.readdirSync(editorThemesBuildDir); const editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
const editorThemeFile = './themes/codeMirror/editorThemes.json'; const editorThemeFile = './themes/codeMirror/editorThemes.json';
if(fs.existsSync(editorThemeFile)) fs.rmSync(editorThemeFile); if(fs.existsSync(editorThemeFile)) fs.rmSync(editorThemeFile);
const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' }); const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
stream.write('[\n"default"'); stream.write('[\n"default"');
for (themeFile of editorThemeFiles) { for (let themeFile of editorThemeFiles) {
stream.write(`,\n"${themeFile.slice(0, -4)}"`); stream.write(`,\n"${themeFile.slice(0, -4)}"`);
} }
stream.write('\n]\n'); stream.write('\n]\n');

View File

@@ -1,12 +1,12 @@
const DB = require('./server/db.js'); import DB from './server/db.js';
const server = require('./server/app.js'); import server from './server/app.js';
const config = require('./server/config.js'); import config from './server/config.js';
DB.connect(config).then(()=>{ DB.connect(config).then(()=>{
// Ensure that we have successfully connected to the database // Ensure that we have successfully connected to the database
// before launching server // before launching server
const PORT = process.env.PORT || config.get('web_port') || 8000; const PORT = process.env.PORT || config.get('web_port') || 8000;
server.app.listen(PORT, ()=>{ server.listen(PORT, ()=>{
const reset = '\x1b[0m'; // Reset to default style const reset = '\x1b[0m'; // Reset to default style
const bright = '\x1b[1m'; // Bright (bold) style const bright = '\x1b[1m'; // Bright (bold) style
const cyan = '\x1b[36m'; // Cyan color const cyan = '\x1b[36m'; // Cyan color

View File

@@ -1,9 +1,15 @@
const HomebrewModel = require('./homebrew.model.js').model; import {model as HomebrewModel } from './homebrew.model.js';
const NotificationModel = require('./notifications.model.js').model; import {model as NotificationModel } from './notifications.model.js';
const router = require('express').Router(); import express from 'express';
const Moment = require('moment'); import Moment from 'moment';
const templateFn = require('../client/template.js'); import zlib from 'zlib';
const zlib = require('zlib'); import templateFn from '../client/template.js';
import HomebrewAPI from './homebrew.api.js';
import asyncHandler from 'express-async-handler';
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
const router = express.Router();
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin'; process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3'; process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
@@ -66,23 +72,8 @@ router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
}); });
/* Searches for matching edit or share id, also attempts to partial match */ /* Searches for matching edit or share id, also attempts to partial match */
router.get('/admin/lookup/:id', mw.adminOnly, async (req, res, next)=>{ router.get('/admin/lookup/:id', mw.adminOnly, asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res, next)=>{
HomebrewModel.findOne({ return res.json(req.brew);
$or : [
{ editId: { $regex: req.params.id, $options: 'i' } },
{ shareId: { $regex: req.params.id, $options: 'i' } },
]
}).exec()
.then((brew)=>{
if(!brew) // No document found
return res.status(404).json({ error: 'Document not found' });
else
return res.json(brew);
})
.catch((err)=>{
console.error(err);
return res.status(500).json({ error: 'Internal Server Error' });
});
}); });
/* Find 50 brews that aren't compressed yet */ /* Find 50 brews that aren't compressed yet */
@@ -100,6 +91,25 @@ router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
}); });
}); });
/* Cleans `<script` and `</script>` from the "text" field of a brew */
router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res)=>{
console.log(`[ADMIN] Cleaning script tags from ShareID ${req.params.id}`);
function cleanText(text){return text.replaceAll(/(<\/?s)cript/gi, '');};
const brew = req.brew;
const properties = ['text', 'description', 'title'];
properties.forEach((property)=>{
brew[property] = cleanText(brew[property]);
});
splitTextStyleAndMetadata(brew);
req.body = brew;
return await HomebrewAPI.updateBrew(req, res);
});
/* Compresses the "text" field of a brew to binary */ /* Compresses the "text" field of a brew to binary */
router.put('/admin/compress/:id', (req, res)=>{ router.put('/admin/compress/:id', (req, res)=>{
@@ -144,7 +154,7 @@ router.get('/admin/notification/all', async (req, res, next)=>{
try { try {
const notifications = await NotificationModel.getAll(); const notifications = await NotificationModel.getAll();
return res.json(notifications); return res.json(notifications);
} catch (error) { } catch (error) {
console.log('Error getting all notifications: ', error.message); console.log('Error getting all notifications: ', error.message);
return res.status(500).json({ message: error.message }); return res.status(500).json({ message: error.message });
@@ -182,4 +192,4 @@ router.get('/admin', mw.adminOnly, (req, res)=>{
}); });
}); });
module.exports = router; export default router;

View File

@@ -1,9 +1,10 @@
const supertest = require('supertest'); import supertest from 'supertest';
import HBApp from './app.js';
import {model as NotificationModel } from './notifications.model.js';
const app = supertest.agent(require('app.js').app)
.set('X-Forwarded-Proto', 'https');
const NotificationModel = require('./notifications.model.js').model; // Mimic https responses to avoid being redirected all the time
const app = supertest.agent(HBApp).set('X-Forwarded-Proto', 'https');
describe('Tests for admin api', ()=>{ describe('Tests for admin api', ()=>{
afterEach(()=>{ afterEach(()=>{

View File

@@ -1,25 +1,41 @@
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
// Set working directory to project root // Set working directory to project root
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import packageJSON from './../package.json' with { type: "json" };
const __dirname = dirname(fileURLToPath(import.meta.url));
process.chdir(`${__dirname}/..`); process.chdir(`${__dirname}/..`);
const version = packageJSON.version;
import _ from 'lodash';
import jwt from 'jwt-simple';
import express from 'express';
import yaml from 'js-yaml';
import config from './config.js';
import fs from 'fs-extra';
const _ = require('lodash');
const jwt = require('jwt-simple');
const express = require('express');
const yaml = require('js-yaml');
const app = express(); const app = express();
const config = require('./config.js');
const fs = require('fs-extra');
const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = require('./homebrew.api.js'); import api from './homebrew.api.js';
const GoogleActions = require('./googleActions.js'); const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = api;
const serveCompressedStaticAssets = require('./static-assets.mv.js'); import adminApi from './admin.api.js';
const sanitizeFilename = require('sanitize-filename'); import vaultApi from './vault.api.js';
const asyncHandler = require('express-async-handler'); import GoogleActions from './googleActions.js';
const templateFn = require('./../client/template.js'); import serveCompressedStaticAssets from './static-assets.mv.js';
import sanitizeFilename from 'sanitize-filename';
import asyncHandler from 'express-async-handler';
import templateFn from '../client/template.js';
import {model as HomebrewModel } from './homebrew.model.js';
const { DEFAULT_BREW } = require('./brewDefaults.js'); import { DEFAULT_BREW } from './brewDefaults.js';
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
const { splitTextStyleAndMetadata } = require('../shared/helpers.js'); //==== Middleware Imports ====//
import contentNegotiation from './middleware/content-negotiation.js';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import forceSSL from './forcessl.mw.js';
const sanitizeBrew = (brew, accessType)=>{ const sanitizeBrew = (brew, accessType)=>{
@@ -34,10 +50,10 @@ const sanitizeBrew = (brew, accessType)=>{
app.set('trust proxy', 1 /* number of proxies between user and server */) app.set('trust proxy', 1 /* number of proxies between user and server */)
app.use('/', serveCompressedStaticAssets(`build`)); app.use('/', serveCompressedStaticAssets(`build`));
app.use(require('./middleware/content-negotiation.js')); app.use(contentNegotiation);
app.use(require('body-parser').json({ limit: '25mb' })); app.use(bodyParser.json({ limit: '25mb' }));
app.use(require('cookie-parser')()); app.use(cookieParser());
app.use(require('./forcessl.mw.js')); app.use(forceSSL);
//Account Middleware //Account Middleware
app.use((req, res, next)=>{ app.use((req, res, next)=>{
@@ -57,15 +73,14 @@ app.use((req, res, next)=>{
}); });
app.use(homebrewApi); app.use(homebrewApi);
app.use(require('./admin.api.js')); app.use(adminApi);
app.use(require('./vault.api.js')); app.use(vaultApi);
const HomebrewModel = require('./homebrew.model.js').model; const welcomeText = fs.readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8'); const welcomeTextLegacy = fs.readFileSync('client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8');
const welcomeTextLegacy = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8'); const migrateText = fs.readFileSync('client/homebrew/pages/homePage/migrate.md', 'utf8');
const migrateText = require('fs').readFileSync('client/homebrew/pages/homePage/migrate.md', 'utf8'); const changelogText = fs.readFileSync('changelog.md', 'utf8');
const changelogText = require('fs').readFileSync('changelog.md', 'utf8'); const faqText = fs.readFileSync('faq.md', 'utf8');
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);};
@@ -479,7 +494,7 @@ const renderPage = async (req, res)=>{
deployment : config.get('heroku_app_name') ?? '' deployment : config.get('heroku_app_name') ?? ''
}; };
const props = { const props = {
version : require('./../package.json').version, version : version,
url : req.customUrl || req.originalUrl, url : req.customUrl || req.originalUrl,
brew : req.brew, brew : req.brew,
brews : req.brews, brews : req.brews,
@@ -556,6 +571,4 @@ app.use((req, res)=>{
}); });
//^=====--------------------------------------=====^// //^=====--------------------------------------=====^//
module.exports = { export default app;
app : app
};

View File

@@ -1,4 +1,4 @@
const _ = require('lodash'); import _ from 'lodash';
// Default properties for newly-created brews // Default properties for newly-created brews
const DEFAULT_BREW = { const DEFAULT_BREW = {
@@ -32,7 +32,7 @@ const DEFAULT_BREW_LOAD = _.defaults(
}, },
DEFAULT_BREW); DEFAULT_BREW);
module.exports = { export {
DEFAULT_BREW, DEFAULT_BREW,
DEFAULT_BREW_LOAD DEFAULT_BREW_LOAD
}; };

View File

@@ -1,5 +1,7 @@
module.exports = require('nconf') import nconf from 'nconf';
.argv()
.env({ lowerCase: true }) export default nconf
.file('environment', { file: `config/${process.env.NODE_ENV}.json` }) .argv()
.file('defaults', { file: 'config/default.json' }); .env({ lowerCase: true })
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
.file('defaults', { file: 'config/default.json' });

View File

@@ -5,7 +5,7 @@
// reused by both the main application and all tests which require database // reused by both the main application and all tests which require database
// connection. // connection.
const Mongoose = require('mongoose'); import Mongoose from 'mongoose';
const getMongoDBURL = (config)=>{ const getMongoDBURL = (config)=>{
return config.get('mongodb_uri') || return config.get('mongodb_uri') ||
@@ -31,7 +31,7 @@ const connect = async (config)=>{
.catch((error)=>handleConnectionError(error)); .catch((error)=>handleConnectionError(error));
}; };
module.exports = { export default {
connect : connect, connect,
disconnect : disconnect disconnect
}; };

View File

@@ -1,4 +1,4 @@
module.exports = (req, res, next)=>{ export default (req, res, next)=>{
if(process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'docker') return next(); if(process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'docker') return next();
if(req.header('x-forwarded-proto') !== 'https') { if(req.header('x-forwarded-proto') !== 'https') {
return res.redirect(302, `https://${req.get('Host')}${req.url}`); return res.redirect(302, `https://${req.get('Host')}${req.url}`);

View File

@@ -1,8 +1,9 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
const googleDrive = require('@googleapis/drive'); import googleDrive from '@googleapis/drive';
const { nanoid } = require('nanoid'); import { nanoid } from 'nanoid';
const token = require('./token.js'); import token from './token.js';
const config = require('./config.js'); import config from './config.js';
let serviceAuth; let serviceAuth;
if(!config.get('service_account')){ if(!config.get('service_account')){
@@ -59,7 +60,7 @@ const GoogleActions = {
account.googleRefreshToken = tokens.refresh_token; account.googleRefreshToken = tokens.refresh_token;
} }
account.googleAccessToken = tokens.access_token; account.googleAccessToken = tokens.access_token;
const JWTToken = token.generateAccessToken(account); const JWTToken = token(account);
//Save updated token to cookie //Save updated token to cookie
//res.cookie('nc_session', JWTToken, { maxAge: 1000*60*60*24*365, path: '/', sameSite: 'lax' }); //res.cookie('nc_session', JWTToken, { maxAge: 1000*60*60*24*365, path: '/', sameSite: 'lax' });
@@ -72,7 +73,7 @@ const GoogleActions = {
getGoogleFolder : async (auth)=>{ getGoogleFolder : async (auth)=>{
const drive = googleDrive.drive({ version: 'v3', auth }); const drive = googleDrive.drive({ version: 'v3', auth });
fileMetadata = { const fileMetadata = {
'name' : 'Homebrewery', 'name' : 'Homebrewery',
'mimeType' : 'application/vnd.google-apps.folder' 'mimeType' : 'application/vnd.google-apps.folder'
}; };
@@ -240,8 +241,8 @@ const GoogleActions = {
return obj.data.id; return obj.data.id;
}, },
getGoogleBrew : async (id, accessId, accessType)=>{ getGoogleBrew : async (auth = defaultAuth, id, accessId, accessType)=>{
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth }); const drive = googleDrive.drive({ version: 'v3', auth: auth });
const obj = await drive.files.get({ const obj = await drive.files.get({
fileId : id, fileId : id,
@@ -344,4 +345,4 @@ const GoogleActions = {
} }
}; };
module.exports = GoogleActions; export default GoogleActions;

View File

@@ -1,18 +1,20 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
const _ = require('lodash'); import _ from 'lodash';
const HomebrewModel = require('./homebrew.model.js').model; import {model as HomebrewModel} from './homebrew.model.js';
const router = require('express').Router(); import express from 'express';
const zlib = require('zlib'); import zlib from 'zlib';
const GoogleActions = require('./googleActions.js'); import GoogleActions from './googleActions.js';
const Markdown = require('../shared/naturalcrit/markdown.js'); import Markdown from '../shared/naturalcrit/markdown.js';
const yaml = require('js-yaml'); import yaml from 'js-yaml';
const asyncHandler = require('express-async-handler'); import asyncHandler from 'express-async-handler';
const { nanoid } = require('nanoid'); import { nanoid } from 'nanoid';
const { splitTextStyleAndMetadata } = require('../shared/helpers.js'); import { splitTextStyleAndMetadata } from '../shared/helpers.js';
import checkClientVersion from './middleware/check-client-version.js';
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js'); const router = express.Router();
const Themes = require('../themes/themes.json'); import { DEFAULT_BREW, DEFAULT_BREW_LOAD } from './brewDefaults.js';
import Themes from '../themes/themes.json' with { type: 'json' };
const isStaticTheme = (renderer, themeName)=>{ const isStaticTheme = (renderer, themeName)=>{
return Themes[renderer]?.[themeName] !== undefined; return Themes[renderer]?.[themeName] !== undefined;
@@ -85,66 +87,68 @@ const api = {
// 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
const { id, googleId } = api.getId(req); let { id, googleId } = api.getId(req);
const accessMap = {
edit : { editId: id },
share : { shareId: id },
admin : { $or : [{ editId: id }, { shareId: id }] }
};
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine. // Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id }) let stub = await HomebrewModel.get(accessMap[accessType])
.catch((err)=>{ .catch((err)=>{
if(googleId) { if(googleId)
console.warn(`Unable to find document stub for ${accessType}Id ${id}`); console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
} else { else
console.warn(err); console.warn(err);
}
}); });
stub = stub?.toObject(); stub = stub?.toObject();
googleId ??= stub?.googleId;
const isOwner = stub?.authors?.length === 0 || stub?.authors?.[0] === req.account?.username;
const isAuthor = stub?.authors?.includes(req.account?.username);
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
if(accessType === 'edit' && !(isOwner || isAuthor || isInvited)) {
const accessError = { name: 'Access Error', status: 401, authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
if(req.account)
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03' };
else
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04' };
}
if(stub?.lock?.locked && accessType != 'edit') { if(stub?.lock?.locked && accessType != 'edit') {
throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title }; throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title };
} }
// 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(!stubOnly && (googleId || stub?.googleId)) { if(!stubOnly && googleId) {
let googleError; const oAuth2Client = isOwner? GoogleActions.authCheck(req.account, res) : undefined;
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
.catch((err)=>{ const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
googleError = err; .catch((googleError)=>{
const reason = googleError.errors?.[0].reason;
if(reason == 'notFound')
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
else
throw { ...googleError, HBErrorCode: '01' };
}); });
// Throw any error caught while attempting to retrieve Google brew.
if(googleError) {
const reason = googleError.errors?.[0].reason;
if(reason == 'notFound') {
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
} else {
throw { ...googleError, HBErrorCode: '01' };
}
}
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew // Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew; stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
} }
const authorsExist = stub?.authors?.length > 0;
const isAuthor = stub?.authors?.includes(req.account?.username);
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
const accessError = { name: 'Access Error', status: 401 };
if(req.account){
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
}
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
}
// 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 && !stubOnly) { if(!stub)
throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id }; throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id };
}
// Clean up brew: fill in missing fields with defaults / fix old invalid values // Clean up brew: fill in missing fields with defaults / fix old invalid values
if(stub) { stub.tags = stub.tags || undefined; // Clear empty strings
stub.tags = stub.tags || undefined; // Clear empty strings stub.renderer = stub.renderer || undefined; // Clear empty strings
stub.renderer = stub.renderer || undefined; // Clear empty strings stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
}
req.brew = stub ?? {}; req.brew = stub;
next(); next();
}; };
}, },
@@ -295,9 +299,8 @@ const api = {
req.params.id = currentTheme.theme; req.params.id = currentTheme.theme;
req.params.renderer = currentTheme.renderer; req.params.renderer = currentTheme.renderer;
} } else {
//=== Static Themes ===// //=== Static Themes ===//
else {
const localSnippets = `${req.params.renderer}_${req.params.id}`; // Just log the name for loading on client const localSnippets = `${req.params.renderer}_${req.params.id}`; // Just log the name for loading on client
const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`; const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`;
completeSnippets.push(localSnippets); completeSnippets.push(localSnippets);
@@ -464,7 +467,7 @@ const api = {
} }
}; };
router.use('/api', require('./middleware/check-client-version.js')); router.use('/api', checkClientVersion);
router.post('/api', asyncHandler(api.newBrew)); router.post('/api', asyncHandler(api.newBrew));
router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew)); router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew)); router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
@@ -472,4 +475,4 @@ router.delete('/api/:id', asyncHandler(api.deleteBrew));
router.get('/api/remove/:id', asyncHandler(api.deleteBrew)); router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle)); router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
module.exports = api; export default api;

View File

@@ -1,5 +1,7 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
describe('Tests for api', ()=>{ describe('Tests for api', ()=>{
let api; let api;
let google; let google;
@@ -36,8 +38,9 @@ describe('Tests for api', ()=>{
} }
}); });
google = require('./googleActions.js'); google = require('./googleActions.js').default;
model = require('./homebrew.model.js').model; model = require('./homebrew.model.js').model;
api = require('./homebrew.api').default;
jest.mock('./googleActions.js'); jest.mock('./googleActions.js');
google.authCheck = jest.fn(()=>'client'); google.authCheck = jest.fn(()=>'client');
@@ -54,8 +57,6 @@ describe('Tests for api', ()=>{
setHeader : jest.fn(()=>{}) setHeader : jest.fn(()=>{})
}; };
api = require('./homebrew.api');
hbBrew = { hbBrew = {
text : `brew text`, text : `brew text`,
style : 'hello yes i am css', style : 'hello yes i am css',
@@ -297,7 +298,7 @@ describe('Tests for api', ()=>{
expect(next).toHaveBeenCalled(); expect(next).toHaveBeenCalled();
expect(api.getId).toHaveBeenCalledWith(req); expect(api.getId).toHaveBeenCalledWith(req);
expect(model.get).toHaveBeenCalledWith({ shareId: '1' }); expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share'); expect(google.getGoogleBrew).toHaveBeenCalledWith(undefined, '2', '1', 'share');
}); });
it('access is denied to a locked brew', async()=>{ it('access is denied to a locked brew', async()=>{
@@ -969,4 +970,57 @@ brew`);
expect(res.send).toHaveBeenCalledWith(''); expect(res.send).toHaveBeenCalledWith('');
}); });
}); });
describe('Split Text, Style, and Metadata', ()=>{
it('basic splitting', async ()=>{
const testBrew = {
text : '```metadata\n' +
'title: title\n' +
'description: description\n' +
'tags: [ \'tag a\' , \'tag b\' ]\n' +
'systems: [ test system ]\n' +
'renderer: legacy\n' +
'theme: 5ePHB\n' +
'lang: en\n' +
'\n' +
'```\n' +
'\n' +
'```css\n' +
'style\n' +
'style\n' +
'style\n' +
'```\n' +
'\n' +
'text\n'
};
splitTextStyleAndMetadata(testBrew);
// Metadata
expect(testBrew.title).toEqual('title');
expect(testBrew.description).toEqual('description');
expect(testBrew.tags).toEqual(['tag a', 'tag b']);
expect(testBrew.systems).toEqual(['test system']);
expect(testBrew.renderer).toEqual('legacy');
expect(testBrew.theme).toEqual('5ePHB');
expect(testBrew.lang).toEqual('en');
// Style
expect(testBrew.style).toEqual('style\nstyle\nstyle');
// Text
expect(testBrew.text).toEqual('text\n');
});
it('convert tags string to array', async ()=>{
const testBrew = {
text : '```metadata\n' +
'tags: tag a\n' +
'```\n\n'
};
splitTextStyleAndMetadata(testBrew);
// Metadata
expect(testBrew.tags).toEqual(['tag a']);
});
});
}); });

View File

@@ -1,7 +1,8 @@
const mongoose = require('mongoose'); import mongoose from 'mongoose';
const { nanoid } = require('nanoid'); import { nanoid } from 'nanoid';
const _ = require('lodash'); import _ from 'lodash';
const zlib = require('zlib'); import zlib from 'zlib';
const HomebrewSchema = mongoose.Schema({ const HomebrewSchema = mongoose.Schema({
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } }, shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
@@ -44,7 +45,7 @@ HomebrewSchema.statics.get = async function(query, fields=null){
const brew = await Homebrew.findOne(query, fields).orFail() const brew = await Homebrew.findOne(query, fields).orFail()
.catch((error)=>{throw 'Can not find brew';}); .catch((error)=>{throw 'Can not find brew';});
if(!_.isNil(brew.textBin)) { // Uncompress zipped text field if(!_.isNil(brew.textBin)) { // Uncompress zipped text field
unzipped = zlib.inflateRawSync(brew.textBin); const unzipped = zlib.inflateRawSync(brew.textBin);
brew.text = unzipped.toString(); brew.text = unzipped.toString();
} }
return brew; return brew;
@@ -62,7 +63,7 @@ HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, f
const Homebrew = mongoose.model('Homebrew', HomebrewSchema); const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
module.exports = { export {
schema : HomebrewSchema, HomebrewSchema as schema,
model : Homebrew, Homebrew as model
}; };

View File

@@ -1,6 +1,8 @@
module.exports = (req, res, next)=>{ import packageJSON from '../../package.json' with { type: "json" };
const version = packageJSON.version;
export default (req, res, next)=>{
const userVersion = req.get('Homebrewery-Version'); const userVersion = req.get('Homebrewery-Version');
const version = require('../../package.json').version;
if(userVersion != version) { if(userVersion != version) {
return res.status(412).send({ return res.status(412).send({

View File

@@ -1,8 +1,8 @@
const config = require('../config.js'); import config from '../config.js';
const nodeEnv = config.get('node_env'); const nodeEnv = config.get('node_env');
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv); const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
module.exports = (req, res, next)=>{ export default (req, res, next)=>{
const isImageRequest = req.get('Accept')?.split(',') const isImageRequest = req.get('Accept')?.split(',')
?.filter((h)=>!h.includes('q=')) ?.filter((h)=>!h.includes('q='))
?.every((h)=>/image\/.*/.test(h)); ?.every((h)=>/image\/.*/.test(h));

View File

@@ -1,41 +0,0 @@
const contentNegotiationMiddleware = require('./content-negotiation.js');
describe('content-negotiation-middleware', ()=>{
let request;
let response;
let next;
beforeEach(()=>{
request = {
get : function(key) {
return this[key];
}
};
response = {
status : jest.fn(()=>response),
send : jest.fn(()=>{})
};
next = jest.fn();
});
it('should return 406 on image request', ()=>{
contentNegotiationMiddleware({
Accept : 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
...request
}, response);
expect(response.status).toHaveBeenLastCalledWith(406);
expect(response.send).toHaveBeenCalledWith({
message : 'Request for image at this URL is not supported'
});
});
it('should call next on non-image request', ()=>{
contentNegotiationMiddleware({
Accept : 'text,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
...request
}, response, next);
expect(next).toHaveBeenCalled();
});
});

View File

@@ -1,5 +1,5 @@
const mongoose = require('mongoose'); import mongoose from 'mongoose';
const _ = require('lodash'); import _ from 'lodash';
const NotificationSchema = new mongoose.Schema({ const NotificationSchema = new mongoose.Schema({
dismissKey : { type: String, unique: true, required: true }, dismissKey : { type: String, unique: true, required: true },
@@ -56,7 +56,7 @@ NotificationSchema.statics.getAll = async function() {
const Notification = mongoose.model('Notification', NotificationSchema); const Notification = mongoose.model('Notification', NotificationSchema);
module.exports = { export {
schema : NotificationSchema, NotificationSchema as schema,
model : Notification, Notification as model
}; };

View File

@@ -1,4 +1,4 @@
const expressStaticGzip = require('express-static-gzip'); import expressStaticGzip from 'express-static-gzip';
// Serve brotli-compressed static files if available // Serve brotli-compressed static files if available
const customCacheControlHandler=(response, path)=>{ const customCacheControlHandler=(response, path)=>{
@@ -28,4 +28,4 @@ const init=(pathToAssets)=>{
} }); } });
}; };
module.exports = init; export default init;

View File

@@ -1,7 +1,5 @@
const jwt = require('jwt-simple'); import jwt from 'jwt-simple';
import config from './config.js';
// Load configuration values
const config = require('./config.js');
// Generate an Access Token for the given User ID // Generate an Access Token for the given User ID
const generateAccessToken = (account)=>{ const generateAccessToken = (account)=>{
@@ -24,6 +22,4 @@ const generateAccessToken = (account)=>{
return token; return token;
}; };
module.exports = { export default generateAccessToken;
generateAccessToken : generateAccessToken
};

View File

@@ -1,6 +1,6 @@
const express = require('express'); import express from 'express';
const asyncHandler = require('express-async-handler'); import asyncHandler from 'express-async-handler';
const HomebrewModel = require('./homebrew.model.js').model; import {model as HomebrewModel } from './homebrew.model.js';
const router = express.Router(); const router = express.Router();
@@ -106,4 +106,4 @@ const findTotal = async (req, res)=>{
router.get('/api/vault/total', asyncHandler(findTotal)); router.get('/api/vault/total', asyncHandler(findTotal));
router.get('/api/vault', asyncHandler(findBrews)); router.get('/api/vault', asyncHandler(findBrews));
module.exports = router; export default router;

View File

@@ -1,6 +1,6 @@
const _ = require('lodash'); import _ from 'lodash';
const yaml = require('js-yaml'); import yaml from 'js-yaml';
const request = require('../client/homebrew/utils/request-middleware.js'); import request from '../client/homebrew/utils/request-middleware.js';
const splitTextStyleAndMetadata = (brew)=>{ const splitTextStyleAndMetadata = (brew)=>{
brew.text = brew.text.replaceAll('\r\n', '\n'); brew.text = brew.text.replaceAll('\r\n', '\n');
@@ -21,6 +21,9 @@ const splitTextStyleAndMetadata = (brew)=>{
brew.snippets = brew.text.slice(11, index - 1); brew.snippets = brew.text.slice(11, index - 1);
brew.text = brew.text.slice(index + 5); brew.text = brew.text.slice(index + 5);
} }
// Handle old brews that still have empty strings in the tags metadata
if(typeof brew.tags === 'string') brew.tags = brew.tags ? [brew.tags] : [];
}; };
const printCurrentBrew = ()=>{ const printCurrentBrew = ()=>{
@@ -51,7 +54,7 @@ const fetchThemeBundle = async (obj, renderer, theme)=>{
})); }));
}; };
module.exports = { export {
splitTextStyleAndMetadata, splitTextStyleAndMetadata,
printCurrentBrew, printCurrentBrew,
fetchThemeBundle, fetchThemeBundle,

View File

@@ -1,7 +1,7 @@
const diceFont = require('../../../themes/fonts/iconFonts/diceFont.js'); import diceFont from '../../../themes/fonts/iconFonts/diceFont.js';
const elderberryInn = require('../../../themes/fonts/iconFonts/elderberryInn.js'); import elderberryInn from '../../../themes/fonts/iconFonts/elderberryInn.js';
const fontAwesome = require('../../../themes/fonts/iconFonts/fontAwesome.js'); import fontAwesome from '../../../themes/fonts/iconFonts/fontAwesome.js';
const gameIcons = require('../../../themes/fonts/iconFonts/gameIcons.js'); import gameIcons from '../../../themes/fonts/iconFonts/gameIcons.js';
const emojis = { const emojis = {
...diceFont, ...diceFont,

View File

@@ -1,19 +1,19 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
const _ = require('lodash'); import _ from 'lodash';
const Marked = require('marked'); import { Parser as MathParser } from 'expr-eval';
const MarkedExtendedTables = require('marked-extended-tables'); import { marked as Marked } from 'marked';
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite'); import MarkedExtendedTables from 'marked-extended-tables';
const { gfmHeadingId: MarkedGFMHeadingId, resetHeadings: MarkedGFMResetHeadingIDs } = require('marked-gfm-heading-id'); import { markedSmartypantsLite as MarkedSmartypantsLite } from 'marked-smartypants-lite';
const { markedEmoji: MarkedEmojis } = require('marked-emoji'); import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id';
import { markedEmoji as MarkedEmojis } from 'marked-emoji';
//Icon fonts included so they can appear in emoji autosuggest dropdown //Icon fonts included so they can appear in emoji autosuggest dropdown
const diceFont = require('../../themes/fonts/iconFonts/diceFont.js'); import diceFont from '../../themes/fonts/iconFonts/diceFont.js';
const elderberryInn = require('../../themes/fonts/iconFonts/elderberryInn.js'); import elderberryInn from '../../themes/fonts/iconFonts/elderberryInn.js';
const fontAwesome = require('../../themes/fonts/iconFonts/fontAwesome.js'); import gameIcons from '../../themes/fonts/iconFonts/gameIcons.js';
const gameIcons = require('../../themes/fonts/iconFonts/gameIcons.js'); import fontAwesome from '../../themes/fonts/iconFonts/fontAwesome.js';
const MathParser = require('expr-eval').Parser; const renderer = new Marked.Renderer();
const renderer = new Marked.Renderer();
const tokenizer = new Marked.Tokenizer(); const tokenizer = new Marked.Tokenizer();
//Limit math features to simple items //Limit math features to simple items
@@ -854,7 +854,7 @@ const globalVarsList = {};
let varsQueue = []; let varsQueue = [];
let globalPageNumber = 0; let globalPageNumber = 0;
module.exports = { const Markdown = {
marked : Marked, marked : Marked,
render : (rawBrewText, pageNumber=0)=>{ render : (rawBrewText, pageNumber=0)=>{
globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order
@@ -865,6 +865,7 @@ module.exports = {
} }
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`); rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`);
const opts = Marked.defaults; const opts = Marked.defaults;
rawBrewText = opts.hooks.preprocess(rawBrewText); rawBrewText = opts.hooks.preprocess(rawBrewText);
@@ -935,3 +936,6 @@ module.exports = {
return errors; return errors;
}, },
}; };
export default Markdown;

View File

@@ -21,10 +21,7 @@ html,body, #reactRoot{
*{ *{
box-sizing : border-box; box-sizing : border-box;
} }
button{ .colorButton(@backgroundColor : @green){
.button();
}
.button(@backgroundColor : @green){
.animate(background-color); .animate(background-color);
display : inline-block; display : inline-block;
padding : 0.6em 1.2em; padding : 0.6em 1.2em;

View File

@@ -1,4 +1,4 @@
:where(html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video){ :where(html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,button,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video){
border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0 border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0
} }
@@ -25,3 +25,9 @@
:where(table){ :where(table){
border-collapse:collapse;border-spacing:0 border-collapse:collapse;border-spacing:0
} }
:where(button) {
background-color: unset;
text-transform: unset;
color: unset;
}

View File

@@ -1,6 +1,6 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
const Markdown = require('naturalcrit/markdown.js'); import Markdown from 'naturalcrit/markdown.js';
test('Processes the markdown within an HTML block if its just a class wrapper', function() { test('Processes the markdown within an HTML block if its just a class wrapper', function() {
const source = '<div>*Bold text*</div>'; const source = '<div>*Bold text*</div>';

View File

@@ -1,6 +1,6 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
const Markdown = require('naturalcrit/markdown.js'); import Markdown from 'naturalcrit/markdown.js';
describe('Inline Definition Lists', ()=>{ describe('Inline Definition Lists', ()=>{
test('No Term 1 Definition', function() { test('No Term 1 Definition', function() {

View File

@@ -1,4 +1,4 @@
const Markdown = require('naturalcrit/markdown.js'); import Markdown from 'naturalcrit/markdown.js';
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
// Marked.js adds line returns after closing tags on some default tokens. // Marked.js adds line returns after closing tags on some default tokens.

View File

@@ -1,6 +1,6 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
const Markdown = require('naturalcrit/markdown.js'); import Markdown from 'naturalcrit/markdown.js';
describe('Hard Breaks', ()=>{ describe('Hard Breaks', ()=>{
test('Single Break', function() { test('Single Break', function() {

View File

@@ -1,7 +1,7 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
const Markdown = require('naturalcrit/markdown.js'); import Markdown from 'naturalcrit/markdown.js';
// Marked.js adds line returns after closing tags on some default tokens. // Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake. // This removes those line returns for comparison sake.

View File

@@ -1,7 +1,7 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
const Markdown = require('naturalcrit/markdown.js'); import Markdown from 'naturalcrit/markdown.js';
// Marked.js adds line returns after closing tags on some default tokens. // Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake. // This removes those line returns for comparison sake.

View File

@@ -1,8 +1,8 @@
const supertest = require('supertest'); import supertest from 'supertest';
import HBApp from 'app.js';
// Mimic https responses to avoid being redirected all the time // Mimic https responses to avoid being redirected all the time
const app = supertest.agent(require('app.js').app) const app = supertest.agent(HBApp).set('X-Forwarded-Proto', 'https');
.set('X-Forwarded-Proto', 'https');
describe('Tests for static pages', ()=>{ describe('Tests for static pages', ()=>{
it('Home page works', ()=>{ it('Home page works', ()=>{

View File

@@ -1,4 +1,4 @@
const Markdown = require('../../../../shared/naturalcrit/markdown.js'); import Markdown from '../../../../shared/naturalcrit/markdown.js';
module.exports = { module.exports = {
createFooterFunc : function(headerSize=1){ createFooterFunc : function(headerSize=1){

View File

@@ -93,4 +93,4 @@ const diceFont = {
'df_solid_small_dot_d6_6' : 'df solid-small-dot-d6-6' 'df_solid_small_dot_d6_6' : 'df solid-small-dot-d6-6'
}; };
module.exports = diceFont; export default diceFont;

View File

@@ -206,4 +206,4 @@ const elderberryInn = {
'ei_wish' : 'ei wish' 'ei_wish' : 'ei wish'
}; };
module.exports = elderberryInn; export default elderberryInn;

View File

@@ -2051,4 +2051,4 @@ const fontAwesome = {
'fab_zhihu' : 'fab fa-zhihu' 'fab_zhihu' : 'fab fa-zhihu'
}; };
module.exports = fontAwesome; export default fontAwesome;

View File

@@ -506,4 +506,4 @@ const gameIcons = {
'gi_acid' : 'gi acid' 'gi_acid' : 'gi acid'
}; };
module.exports = gameIcons; export default gameIcons;