0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-27 13:53:09 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Trevor Buckner
844b6ef563 3.3.2 2022-11-28 11:31:46 -05:00
9 changed files with 1084 additions and 1195 deletions

View File

@@ -1,6 +1,7 @@
```css ```css
h5 { h5 {
font-size: .35cm !important; font-size: .35cm !important;
margin-top: 0.3cm;
} }
.page ul ul { .page ul ul {
@@ -44,77 +45,6 @@ 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).
### Saturday 10/12/2022 - v3.4.2
{{taskList
##### Jeddai
* [x] Fix broken tags editor
* [x] Reduce server load to fix some saving issues
Fixes issues [#2322](https://github.com/naturalcrit/homebrewery/issues/2322)
##### Gazook
* [x] Account page help link for Google Drive errors
Fixes issues [#2520](https://github.com/naturalcrit/homebrewery/issues/2520)
}}
### Monday 05/12/2022 - v3.4.1
{{taskList
##### G-Ambatte
* [x] Fix Account page incorrect last login time
Fixes issues [#2521](https://github.com/naturalcrit/homebrewery/issues/2521)
##### Gazook
* [x] Fix crashing on iOS and Safari browsers
Fixes issues [#2531](https://github.com/naturalcrit/homebrewery/issues/2531)
}}
### Monday 28/11/2022 - v3.4.0
{{taskList
##### G-Ambatte
* [x] Fix for Chrome v108 handling of page size
Fixes issues [#2445](https://github.com/naturalcrit/homebrewery/issues/2445), [#2516](https://github.com/naturalcrit/homebrewery/issues/2516)
* [x] New account page with some user info, at {{openSans **USERNAME {{fa,fa-user}} → ACCOUNT {{fa,fa-user}}**}}
Fixes issues [#2049](https://github.com/naturalcrit/homebrewery/issues/2049), [#2043](https://github.com/naturalcrit/homebrewery/issues/2043)
* [x] Fix "Published/Private Brews" buttons on userpage
Fixes issues [#2449](https://github.com/naturalcrit/homebrewery/issues/2449)
##### Gazook
* [x] Make autosave default on for new users
* [x] Added link to our FAQ at {{openSans **NEED HELP? {{fa,fa-question-circle}} → FAQ {{fa,fa-question-circle}}**}}
* [x] Fix curly blocks freezing with long property lists
Fixes issues [#2393](https://github.com/naturalcrit/homebrewery/issues/2393)
* [x] Items can now be removed from {{openSans **RECENT BREWS** {{fas,fa-history}} }}
Fixes issues [#1918](https://github.com/naturalcrit/homebrewery/issues/1918)
* [x] Curly injector syntax `{blue}` highlighting in editor
Fixes issues [#1670](https://github.com/naturalcrit/homebrewery/issues/1670)
}}
### Thursday 28/10/2022 - v3.3.1 ### Thursday 28/10/2022 - v3.3.1
{{taskList {{taskList
@@ -159,12 +89,8 @@ Fixes issues [#2135](https://github.com/naturalcrit/homebrewery/issues/2135)
* [x] Fix brew settings being lost on first save * [x] Fix brew settings being lost on first save
Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427) Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
}}
\column
{{taskList
##### Gazook: ##### Gazook:
* [x] Several updates to bug reporting and error popups * [x] Several updates to bug reporting and error popups

View File

@@ -139,10 +139,10 @@ const Editor = createClass({
// Highlight injectors {style} // Highlight injectors {style}
if(line.includes('{') && line.includes('}')){ if(line.includes('{') && line.includes('}')){
const regex = /(?:^|[^{\n])({(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\2})/gm; const regex = /(?<!{){(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/g;
let match; let match;
while ((match = regex.exec(line)) != null) { while ((match = regex.exec(line)) != null) {
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' }); codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'injection' });
} }
} }
// Highlight inline spans {{content}} // Highlight inline spans {{content}}

View File

@@ -9,18 +9,12 @@ const Nav = require('naturalcrit/nav/nav.jsx');
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx'); const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
const Themes = require('themes/themes.json'); const Themes = require('themes/themes.json');
const validations = require('./validations.js'); const validations = require('./validations.js')
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder']; const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
const homebreweryThumbnail = require('../../thumbnail.png'); const homebreweryThumbnail = require('../../thumbnail.png');
const callIfExists = (val, fn, ...args)=>{
if(val[fn]) {
val[fn](...args);
}
};
const MetadataEditor = createClass({ const MetadataEditor = createClass({
displayName : 'MetadataEditor', displayName : 'MetadataEditor',
getDefaultProps : function() { getDefaultProps : function() {
@@ -59,25 +53,28 @@ const MetadataEditor = createClass({
}, },
handleFieldChange : function(name, e){ handleFieldChange : function(name, e){
e.persist();
// load validation rules, and check input value against them // load validation rules, and check input value against them
const inputRules = validations[name] ?? []; const inputRules = validations[name] ?? [];
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean); const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
// if no validation rules, save to props // if no validation rules, save to props
if(validationErr.length === 0){ if(validationErr.length === 0){
callIfExists(e.target, 'setCustomValidity', ''); e.target.setCustomValidity('');
this.props.onChange({ this.props.onChange({
...this.props.metadata, ...this.props.metadata,
[name] : e.target.value [name] : e.target.value
}); });
} else { } else {
// if validation issues, display built-in browser error popup with each error. // if validation issues, display built-in browser error popup with each error.
console.log(validationErr);
const errMessage = validationErr.map((err)=>{ const errMessage = validationErr.map((err)=>{
return `- ${err}`; return `- ${err}`;
}).join('\n'); }).join('\n');
callIfExists(e.target, 'setCustomValidity', errMessage); e.target.setCustomValidity(errMessage);
callIfExists(e.target, 'reportValidity'); e.target.reportValidity();
} };
}, },
handleSystem : function(system, e){ handleSystem : function(system, e){

View File

@@ -42,6 +42,7 @@ const AccountPage = createClass({
}, },
renderUiItems : function() { renderUiItems : function() {
// console.log(this.props.uiItems);
return <> return <>
<div className='dataGroup'> <div className='dataGroup'>
<h1>Account Information <i className='fas fa-user'></i></h1> <h1>Account Information <i className='fas fa-user'></i></h1>
@@ -50,16 +51,12 @@ const AccountPage = createClass({
</div> </div>
<div className='dataGroup'> <div className='dataGroup'>
<h3>Homebrewery Information <NaturalCritIcon /></h3> <h3>Homebrewery Information <NaturalCritIcon /></h3>
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount}</p> <p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount || '-'}</p>
</div> </div>
<div className='dataGroup'> <div className='dataGroup'>
<h3>Google Information <i className='fab fa-google-drive'></i></h3> <h3>Google Information <i className='fab fa-google-drive'></i></h3>
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p> <p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
{this.props.uiItems.googleId && {this.props.uiItems.googleId ? <p><strong>Brews on Google Drive: </strong> {this.props.uiItems.fileCount || '-'}</p> : '' }
<p>
<strong>Brews on Google Drive: </strong> {this.props.uiItems.googleCount ?? <>Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a></>}
</p>
}
</div> </div>
</>; </>;
}, },

2021
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "homebrewery", "name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown", "description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.4.2", "version": "3.3.2",
"engines": { "engines": {
"node": "16.11.x" "node": "16.11.x"
}, },
@@ -51,7 +51,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.20.5", "@babel/core": "^7.19.6",
"@babel/plugin-transform-runtime": "^7.19.6", "@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.19.4", "@babel/preset-env": "^7.19.4",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.18.6",
@@ -64,32 +64,32 @@
"express": "^4.18.2", "express": "^4.18.2",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.7", "express-static-gzip": "2.1.7",
"fs-extra": "11.1.0", "fs-extra": "10.1.0",
"googleapis": "109.0.1", "googleapis": "109.0.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
"less": "^3.13.1", "less": "^3.13.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "4.2.4", "marked": "4.2.3",
"marked-extended-tables": "^1.0.5", "marked-extended-tables": "^1.0.5",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.4", "moment": "^2.29.4",
"mongoose": "^6.8.0", "mongoose": "^6.7.3",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.0", "nconf": "^0.12.0",
"npm": "^8.10.0", "npm": "^8.10.0",
"react": "^17.0.2", "react": "^16.14.0",
"react-dom": "^17.0.2", "react-dom": "^16.14.0",
"react-frame-component": "4.1.3", "react-frame-component": "4.1.3",
"react-router-dom": "6.4.5", "react-router-dom": "6.4.3",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^6.1.0", "superagent": "^6.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.29.0", "eslint": "^8.28.0",
"eslint-plugin-react": "^7.31.11", "eslint-plugin-react": "^7.31.11",
"jest": "^29.2.2", "jest": "^29.2.2",
"supertest": "^6.3.3" "supertest": "^6.3.1"
} }
} }

View File

@@ -78,10 +78,10 @@ const faqText = require('fs').readFileSync('faq.md', 'utf8');
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);}; String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
const defaultMetaTags = { const defaultMetaTags = {
site_name : 'The Homebrewery - Make your Homebrew content look legit!', siteName : 'The Homebrewery - Make your Homebrew content look legit!',
title : 'The Homebrewery', title : 'The Homebrewery',
description : 'A NaturalCrit Tool for creating authentic Homebrews using Markdown.', description : 'A NaturalCrit Tool for Homebrews',
image : `${config.get('publicUrl')}/thumbnail.png`, thumbnail : `${config.get('publicUrl')}/thumbnail.png`,
type : 'website' type : 'website'
}; };
@@ -148,7 +148,8 @@ app.get('/changelog', async (req, res, next)=>{
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
title : 'Changelog', title : 'Changelog',
description : 'Development changelog.' description : 'Development changelog.',
thumbnail : null
}; };
splitTextStyleAndMetadata(req.brew); splitTextStyleAndMetadata(req.brew);
@@ -191,19 +192,12 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
sanitizeBrew(brew, 'share'); sanitizeBrew(brew, 'share');
const prefix = 'HB - '; const prefix = 'HB - ';
const encodeRFC3986ValueChars = (str)=>{
return (
encodeURIComponent(str)
.replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
);
};
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', ''); let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; }; if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
res.set({ res.set({
'Cache-Control' : 'no-cache', 'Cache-Control' : 'no-cache',
'Content-Type' : 'text/plain', 'Content-Type' : 'text/plain',
'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt` 'Content-Disposition' : `attachment; filename="${fileName}.txt"`
}); });
res.status(200).send(brew.text); res.status(200).send(brew.text);
}); });
@@ -214,7 +208,8 @@ app.get('/user/:username', async (req, res, next)=>{
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
title : `${req.params.username}'s Collection`, title : `${req.params.username}'s Collection`,
description : 'View my collection of homebrew on the Homebrewery.' description : 'View my collection of homebrew on the Homebrewery.',
image : null
// type : could be 'profile'? // type : could be 'profile'?
}; };
@@ -279,7 +274,7 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
title : req.brew.title || 'Untitled Brew', title : req.brew.title || 'Untitled Brew',
description : req.brew.description || 'No description.', description : req.brew.description || 'No description.',
image : req.brew.thumbnail || defaultMetaTags.image, image : req.brew.thumbnail || null,
type : 'article' type : 'article'
}; };
@@ -297,7 +292,8 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
title : 'New', title : 'New',
description : 'Start crafting your homebrew on the Homebrewery!' description : 'Start crafting your homebrew on the Homebrewery!',
image : null
}; };
return next(); return next();
@@ -310,7 +306,7 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
title : req.brew.title || 'Untitled Brew', title : req.brew.title || 'Untitled Brew',
description : req.brew.description || 'No description.', description : req.brew.description || 'No description.',
image : req.brew.thumbnail || defaultMetaTags.image, image : req.brew.thumbnail || null,
type : 'article' type : 'article'
}; };
@@ -340,11 +336,11 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
data.title = 'Account Information Page'; data.title = 'Account Information Page';
let auth; let auth;
let googleCount = []; let files;
if(req.account) { if(req.account) {
if(req.account.googleId) { if(req.account.googleId) {
try { try {
auth = await GoogleActions.authCheck(req.account, res, false); auth = await GoogleActions.authCheck(req.account, res);
} catch (e) { } catch (e) {
auth = undefined; auth = undefined;
console.log('Google auth check failed!'); console.log('Google auth check failed!');
@@ -352,9 +348,9 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
} }
if(auth.credentials.access_token) { if(auth.credentials.access_token) {
try { try {
googleCount = await GoogleActions.listGoogleBrews(auth); files = await GoogleActions.listGoogleBrews(auth);
} catch (e) { } catch (e) {
googleCount = undefined; files = undefined;
console.log('List Google files failed!'); console.log('List Google files failed!');
console.log(e); console.log(e);
} }
@@ -362,19 +358,18 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
} }
const query = { authors: req.account.username, googleId: { $exists: false } }; const query = { authors: req.account.username, googleId: { $exists: false } };
const mongoCount = await HomebrewModel.countDocuments(query) const brews = await HomebrewModel.find(query, 'id')
.catch((err)=>{ .catch((err)=>{
mongoCount = 0;
console.log(err); console.log(err);
}); });
data.uiItems = { data.uiItems = {
username : req.account.username, username : req.account.username,
issued : req.account.issued, issued : req.account.issued,
mongoCount : brews.length,
googleId : Boolean(req.account.googleId), googleId : Boolean(req.account.googleId),
authCheck : Boolean(req.account.googleId && auth.credentials.access_token), authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
mongoCount : mongoCount, fileCount : files?.length || '-'
googleCount : googleCount?.length
}; };
} }
@@ -382,7 +377,8 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
title : `Account Page`, title : `Account Page`,
description : null description : null,
image : null
}; };
return next(); return next();

View File

@@ -5,28 +5,24 @@ const { nanoid } = require('nanoid');
const token = require('./token.js'); const token = require('./token.js');
const config = require('./config.js'); const config = require('./config.js');
let serviceAuth; const keys = typeof(config.get('service_account')) == 'string' ?
if(!config.get('service_account')){
console.log('No Google Service Account in config files - Google Drive integration will not be available.');
} else {
const keys = typeof(config.get('service_account')) == 'string' ?
JSON.parse(config.get('service_account')) : JSON.parse(config.get('service_account')) :
config.get('service_account'); config.get('service_account');
let serviceAuth;
try { try {
serviceAuth = google.auth.fromJSON(keys); serviceAuth = google.auth.fromJSON(keys);
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive']; serviceAuth.scopes = [
} catch (err) { 'https://www.googleapis.com/auth/drive'
];
} catch (err) {
console.warn(err); console.warn(err);
console.log('Please make sure the Google Service Account is set up properly in your config files.'); console.log('Please make sure that a Google Service Account is set up properly in your config files.');
}
} }
google.options({ auth: serviceAuth || config.get('google_api_key') }); google.options({ auth: serviceAuth || config.get('google_api_key') });
const GoogleActions = { const GoogleActions = {
authCheck : (account, res, updateTokens=true)=>{ authCheck : (account, res)=>{
if(!account || !account.googleId){ // If not signed into Google if(!account || !account.googleId){ // If not signed into Google
const err = new Error('Not Signed In'); const err = new Error('Not Signed In');
err.status = 401; err.status = 401;
@@ -44,7 +40,7 @@ const GoogleActions = {
refresh_token : account.googleRefreshToken refresh_token : account.googleRefreshToken
}); });
updateTokens && oAuth2Client.on('tokens', (tokens)=>{ oAuth2Client.on('tokens', (tokens)=>{
if(tokens.refresh_token) { if(tokens.refresh_token) {
account.googleRefreshToken = tokens.refresh_token; account.googleRefreshToken = tokens.refresh_token;
} }
@@ -253,6 +249,7 @@ const GoogleActions = {
text : file.data, text : file.data,
description : obj.data.description, description : obj.data.description,
tags : obj.data.properties.tags ? obj.data.properties.tags : '',
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [], systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
authors : [], authors : [],
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false, published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,

View File

@@ -27,7 +27,7 @@ const getId = (req)=>{
return { id, googleId }; return { id, googleId };
}; };
const getBrew = (accessType, stubOnly = false)=>{ const getBrew = (accessType)=>{
// 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
@@ -45,7 +45,7 @@ const getBrew = (accessType, stubOnly = false)=>{
stub = stub?.toObject(); stub = stub?.toObject();
// 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(googleId || stub?.googleId) {
let googleError; let googleError;
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType) const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
.catch((err)=>{ .catch((err)=>{
@@ -59,14 +59,14 @@ const getBrew = (accessType, stubOnly = false)=>{
} }
// 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 'Brew not found in Homebrewery database or Google Drive'; throw 'Brew not found in Homebrewery database or Google Drive';
} }
if(typeof stub?.tags === 'string') { if(typeof stub.tags === 'string') {
stub.tags = []; stub.tags = [];
} }
req.brew = stub || {}; req.brew = stub;
next(); next();
}; };
@@ -235,22 +235,23 @@ const updateBrew = async (req, res)=>{
brew.authors = _.uniq(_.concat(brew.authors, req.account.username)); brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
} }
// define a function to catch our save errors // Fetch the brew from the database again (if it existed there to begin with), and assign the existing brew to it
const saveError = (err)=>{ brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
if(!brew.markModified) {
// If it wasn't in the database, create a new db brew
brew = new HomebrewModel(brew);
}
brew.markModified('authors');
brew.markModified('systems');
// Save the database brew
const saved = await brew.save()
.catch((err)=>{
console.error(err); console.error(err);
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database'); res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
}; });
let saved;
if(!brew._id) {
// if the brew does not have a stub id, create and save it, then write the new value back to the brew.
saved = await new HomebrewModel(brew).save().catch(saveError);
brew = saved?.toObject();
} else {
// if the brew does have a stub id, update it using the stub id as the key.
saved = await HomebrewModel.findOneAndUpdate({ _id: brew._id }, brew, {
returnOriginal : false
}).catch(saveError);
}
if(!saved) return; if(!saved) return;
// Call and wait for afterSave to complete // Call and wait for afterSave to complete
const after = await afterSave(); const after = await afterSave();
@@ -326,8 +327,8 @@ const deleteBrew = async (req, res, next)=>{
}; };
router.post('/api', asyncHandler(newBrew)); router.post('/api', asyncHandler(newBrew));
router.put('/api/:id', asyncHandler(getBrew('edit', true)), asyncHandler(updateBrew)); router.put('/api/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
router.put('/api/update/:id', asyncHandler(getBrew('edit', true)), asyncHandler(updateBrew)); router.put('/api/update/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
router.delete('/api/:id', asyncHandler(deleteBrew)); router.delete('/api/:id', asyncHandler(deleteBrew));
router.get('/api/remove/:id', asyncHandler(deleteBrew)); router.get('/api/remove/:id', asyncHandler(deleteBrew));