mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-27 20:23:08 +00:00
Compare commits
89 Commits
Fix_#2954
...
Add-missin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fde21868cd | ||
|
|
980a7bd57e | ||
|
|
6b0022ad00 | ||
|
|
0f33973e58 | ||
|
|
7a41a140fd | ||
|
|
57467701d0 | ||
|
|
9dbfb26e6c | ||
|
|
7a169cbd9e | ||
|
|
2dc8a8fbe9 | ||
|
|
5f14f656ef | ||
|
|
6e8a0d7314 | ||
|
|
e61144beb8 | ||
|
|
64b792c645 | ||
|
|
aee5b7a8cc | ||
|
|
0148eafce0 | ||
|
|
a3ec5b8d3b | ||
|
|
4ded48df1e | ||
|
|
bc14246fe7 | ||
|
|
fcf985a115 | ||
|
|
a060fd123c | ||
|
|
7c7e143365 | ||
|
|
efa8f3fedf | ||
|
|
972a93d292 | ||
|
|
35be1e9b94 | ||
|
|
1a91c390f8 | ||
|
|
206e4fbda8 | ||
|
|
af98cb3867 | ||
|
|
f8fc6f7aa4 | ||
|
|
eb0fa28a03 | ||
|
|
4ab1a22eb3 | ||
|
|
962a46a670 | ||
|
|
cb16b32016 | ||
|
|
56f348f7ed | ||
|
|
b7c99b2d52 | ||
|
|
889f80f537 | ||
|
|
c270a69bb9 | ||
|
|
db0df82202 | ||
|
|
1346361f80 | ||
|
|
fdaf9d4808 | ||
|
|
3cdfae4270 | ||
|
|
a9275698fa | ||
|
|
99f2972079 | ||
|
|
afc92c4545 | ||
|
|
b26526a2f1 | ||
|
|
4f57f006ce | ||
|
|
666a94cd65 | ||
|
|
f0c094e9d8 | ||
|
|
a1c228b1d1 | ||
|
|
5e5c637c79 | ||
|
|
d573129f31 | ||
|
|
57cb334c15 | ||
|
|
c29e1905bf | ||
|
|
52d00b17a4 | ||
|
|
35364c400a | ||
|
|
77f0c1bf56 | ||
|
|
2d281072fa | ||
|
|
83b8f9c3b7 | ||
|
|
3a20452214 | ||
|
|
3e4ba89ed9 | ||
|
|
2c5c3d40df | ||
|
|
213240327d | ||
|
|
eca0f59b40 | ||
|
|
51936a1b99 | ||
|
|
6136b78395 | ||
|
|
81f56ec91d | ||
|
|
4eb8abf1e7 | ||
|
|
23910cc94c | ||
|
|
ef0ee78758 | ||
|
|
1b20c00842 | ||
|
|
db9212bd12 | ||
|
|
7348ecbb3d | ||
|
|
31a22703c1 | ||
|
|
33f8f6bf38 | ||
|
|
406f5d4e14 | ||
|
|
3178c8722e | ||
|
|
7a76c67038 | ||
|
|
108d368d45 | ||
|
|
bd413cfc55 | ||
|
|
1af13b4e94 | ||
|
|
2abc2b13f0 | ||
|
|
35856ad01e | ||
|
|
766fd40b72 | ||
|
|
3e6884b506 | ||
|
|
2118142faa | ||
|
|
2b270ccdb7 | ||
|
|
08eabf8102 | ||
|
|
c1d85bc216 | ||
|
|
3a2c213cf8 | ||
|
|
99dc0deb08 |
@@ -73,6 +73,9 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Variables
|
name: Test - Variables
|
||||||
command: npm run test:variables
|
command: npm run test:variables
|
||||||
|
- run:
|
||||||
|
name: Test - Emojis
|
||||||
|
command: npm run test:emojis
|
||||||
- run:
|
- run:
|
||||||
name: Test - Routes
|
name: Test - Routes
|
||||||
command: npm run test:route
|
command: npm run test:route
|
||||||
@@ -82,6 +85,9 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Coverage
|
name: Test - Coverage
|
||||||
command: npm run test:coverage
|
command: npm run test:coverage
|
||||||
|
- run:
|
||||||
|
name: Test - Content Negotiation
|
||||||
|
command: npm run test:content-negotiation
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
build_and_test:
|
build_and_test:
|
||||||
|
|||||||
25
.github/pull_request_template.md
vendored
25
.github/pull_request_template.md
vendored
@@ -1,26 +1,29 @@
|
|||||||
<!--
|
> [!TIP]
|
||||||
Before submitting a Pull Request, please consider the following to speed up reviews:
|
> Before submitting a Pull Request, please consider the following to speed up reviews:
|
||||||
- 👷♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs.
|
> - 👷♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs.
|
||||||
- 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first.
|
> - 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first.
|
||||||
- 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests?
|
> - 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests?
|
||||||
- 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
|
> - 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
|
||||||
-->
|
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
|
_Describe what your PR accomplishes. Consider walking through the main changes to aid reviewers in following your code, especially if it covers multiple files._
|
||||||
|
|
||||||
## Related Issues or Discussions
|
## Related Issues or Discussions
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> If no issue exists yet, create it, and get agreement on the approach (or paste in a previous agreement from chat, etc.) before moving forward. (Experimental PRs are OK without prior discussion, but do not expect to get merged.)
|
||||||
|
|
||||||
- Closes #
|
- Closes #
|
||||||
|
|
||||||
## QA Instructions, Screenshots, Recordings
|
## QA Instructions, Screenshots, Recordings
|
||||||
|
|
||||||
_Please replace this line with instructions on how to test or view your changes, as well as any before/after
|
_Replace this line with instructions on how to test or view your changes, as well as any before/after
|
||||||
images for UI changes._
|
screenshots or recordings for UI changes._
|
||||||
|
|
||||||
### Reviewer Checklist
|
### Reviewer Checklist
|
||||||
|
|
||||||
_Please replace the list below with specific features you want reviewers to look at._
|
_Replace the list below with specific features you want reviewers to look at._
|
||||||
|
|
||||||
*Reviewers, refer to this list when testing features, or suggest new items *
|
*Reviewers, refer to this list when testing features, or suggest new items *
|
||||||
- [ ] Verify new features are functional
|
- [ ] Verify new features are functional
|
||||||
@@ -32,5 +35,3 @@ _Please replace the list below with specific features you want reviewers to look
|
|||||||
- [ ] Feature A handles negative numbers
|
- [ ] Feature A handles negative numbers
|
||||||
- [ ] Identify opportunities for simplification and refactoring
|
- [ ] Identify opportunities for simplification and refactoring
|
||||||
- [ ] Check for code legibility and appropriate comments
|
- [ ] Check for code legibility and appropriate comments
|
||||||
|
|
||||||
<details><summary>Copy this list</summary>
|
|
||||||
|
|||||||
@@ -115,10 +115,10 @@
|
|||||||
color : #D3D3D3;
|
color : #D3D3D3;
|
||||||
accent-color : #D3D3D3;
|
accent-color : #D3D3D3;
|
||||||
|
|
||||||
&::-webkit-slider-thumb, &::-moz-slider-thumb {
|
&::-webkit-slider-thumb, &::-moz-range-thumb {
|
||||||
width : 5px;
|
width : 5px;
|
||||||
height : 5px;
|
height : 5px;
|
||||||
cursor : pointer;
|
cursor : ew-resize;
|
||||||
outline : none;
|
outline : none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -207,19 +207,11 @@ const Snippetbar = createClass({
|
|||||||
renderEditorButtons : function(){
|
renderEditorButtons : function(){
|
||||||
if(!this.props.showEditButtons) return;
|
if(!this.props.showEditButtons) return;
|
||||||
|
|
||||||
const foldButtons = <>
|
|
||||||
<div className={`editorTool foldAll ${this.props.view !== 'meta' && this.props.foldCode ? 'active' : ''}`}
|
|
||||||
onClick={this.props.foldCode} >
|
|
||||||
<i className='fas fa-compress-alt' />
|
|
||||||
</div>
|
|
||||||
<div className={`editorTool unfoldAll ${this.props.view !== 'meta' && this.props.unfoldCode ? 'active' : ''}`}
|
|
||||||
onClick={this.props.unfoldCode} >
|
|
||||||
<i className='fas fa-expand-alt' />
|
|
||||||
</div>
|
|
||||||
</>;
|
|
||||||
|
|
||||||
return <div className='editors'>
|
|
||||||
<div className='historyTools'>
|
return (
|
||||||
|
<div className='editors'>
|
||||||
|
{this.props.view !== 'meta' && <><div className='historyTools'>
|
||||||
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
||||||
onClick={this.toggleHistoryMenu} >
|
onClick={this.toggleHistoryMenu} >
|
||||||
<i className='fas fa-clock-rotate-left' />
|
<i className='fas fa-clock-rotate-left' />
|
||||||
@@ -235,13 +227,20 @@ const Snippetbar = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='codeTools'>
|
<div className='codeTools'>
|
||||||
{foldButtons}
|
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
|
||||||
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
onClick={this.props.foldCode} >
|
||||||
|
<i className='fas fa-compress-alt' />
|
||||||
|
</div>
|
||||||
|
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
|
||||||
|
onClick={this.props.unfoldCode} >
|
||||||
|
<i className='fas fa-expand-alt' />
|
||||||
|
</div>
|
||||||
|
<div className={`editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||||
onClick={this.toggleThemeSelector} >
|
onClick={this.toggleThemeSelector} >
|
||||||
<i className='fas fa-palette' />
|
<i className='fas fa-palette' />
|
||||||
{this.state.themeSelector && this.renderThemeSelector()}
|
{this.state.themeSelector && this.renderThemeSelector()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div></>}
|
||||||
|
|
||||||
|
|
||||||
<div className='tabs'>
|
<div className='tabs'>
|
||||||
@@ -259,7 +258,8 @@ const Snippetbar = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>;
|
</div>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
justify-content : flex-end;
|
justify-content : flex-end;
|
||||||
min-width : 225px;
|
min-width : 225px;
|
||||||
|
|
||||||
&:only-child { margin-left : auto; }
|
&:only-child { margin-left : auto;min-width:unset;}
|
||||||
|
|
||||||
>div {
|
>div {
|
||||||
display : flex;
|
display : flex;
|
||||||
@@ -38,6 +38,11 @@
|
|||||||
line-height : @menuHeight;
|
line-height : @menuHeight;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
|
|
||||||
|
&.editorTool:not(.active) {
|
||||||
|
cursor:not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover,&.selected { background-color : #999999; }
|
&:hover,&.selected { background-color : #999999; }
|
||||||
&.text {
|
&.text {
|
||||||
.tooltipLeft('Brew Editor');
|
.tooltipLeft('Brew Editor');
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
require('./homebrew.less');
|
require('./homebrew.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const { StaticRouter:Router } = require('react-router-dom/server');
|
const { StaticRouter:Router } = require('react-router');
|
||||||
const { Route, Routes, useParams, useSearchParams } = require('react-router-dom');
|
const { Route, Routes, useParams, useSearchParams } = require('react-router');
|
||||||
|
|
||||||
const HomePage = require('./pages/homePage/homePage.jsx');
|
const HomePage = require('./pages/homePage/homePage.jsx');
|
||||||
const EditPage = require('./pages/editPage/editPage.jsx');
|
const EditPage = require('./pages/editPage/editPage.jsx');
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
**[Homebrewery Link](${global.config.publicUrl}/share/${shareLink})**`;
|
**[Homebrewery Link](${global.config.publicUrl}/share/${shareLink})**`;
|
||||||
|
|
||||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title)}&text=${encodeURIComponent(text)}`;
|
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
|
|||||||
1080
package-lock.json
generated
1080
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -27,6 +27,7 @@
|
|||||||
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
|
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
|
||||||
"test:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
|
"test:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
|
||||||
"test:api-unit:notifications": "jest \"server/.*.spec.js\" -t \"Notifications\" --verbose",
|
"test:api-unit:notifications": "jest \"server/.*.spec.js\" -t \"Notifications\" --verbose",
|
||||||
|
"test:content-negotiation": "jest \"server/middleware/.*.spec.js\" --verbose",
|
||||||
"test:coverage": "jest --coverage --silent --runInBand",
|
"test:coverage": "jest --coverage --silent --runInBand",
|
||||||
"test:dev": "jest --verbose --watch",
|
"test:dev": "jest --verbose --watch",
|
||||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||||
@@ -91,9 +92,10 @@
|
|||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"create-react-class": "^15.7.0",
|
"create-react-class": "^15.7.0",
|
||||||
"dedent-tabs": "^0.10.3",
|
"dedent-tabs": "^0.10.3",
|
||||||
"dompurify": "^3.2.2",
|
"dompurify": "^3.2.3",
|
||||||
"expr-eval": "^2.0.2",
|
"expr-eval": "^2.0.2",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
@@ -111,13 +113,13 @@
|
|||||||
"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.8.3",
|
"mongoose": "^8.9.1",
|
||||||
"nanoid": "5.0.9",
|
"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",
|
||||||
"react-frame-component": "^4.1.3",
|
"react-frame-component": "^4.1.3",
|
||||||
"react-router-dom": "6.28.0",
|
"react-router": "^7.0.2",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^10.1.1",
|
"superagent": "^10.1.1",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
@@ -125,15 +127,15 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/stylelint-plugin": "^3.1.1",
|
"@stylistic/stylelint-plugin": "^3.1.1",
|
||||||
"babel-plugin-transform-import-meta": "^2.2.1",
|
"babel-plugin-transform-import-meta": "^2.2.1",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.17.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.13.0",
|
"globals": "^15.14.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.11.0",
|
"stylelint": "^16.12.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"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {model as HomebrewModel } from './homebrew.model.js';
|
import { model as HomebrewModel } from './homebrew.model.js';
|
||||||
import {model as NotificationModel } from './notifications.model.js';
|
import { model as NotificationModel } from './notifications.model.js';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import Moment from 'moment';
|
import Moment from 'moment';
|
||||||
import zlib from 'zlib';
|
import zlib from 'zlib';
|
||||||
@@ -108,6 +108,9 @@ router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin',
|
|||||||
|
|
||||||
req.body = brew;
|
req.body = brew;
|
||||||
|
|
||||||
|
// Remove Account from request to prevent Admin user from being added to brew as an Author
|
||||||
|
req.account = undefined;
|
||||||
|
|
||||||
return await HomebrewAPI.updateBrew(req, res);
|
return await HomebrewAPI.updateBrew(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Set working directory to project root
|
// Set working directory to project root
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import packageJSON from './../package.json' with { type: "json" };
|
import packageJSON from './../package.json' with { type: 'json' };
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
process.chdir(`${__dirname}/..`);
|
process.chdir(`${__dirname}/..`);
|
||||||
@@ -26,7 +26,7 @@ import serveCompressedStaticAssets from './static-assets.mv.js';
|
|||||||
import sanitizeFilename from 'sanitize-filename';
|
import sanitizeFilename from 'sanitize-filename';
|
||||||
import asyncHandler from 'express-async-handler';
|
import asyncHandler from 'express-async-handler';
|
||||||
import templateFn from '../client/template.js';
|
import templateFn from '../client/template.js';
|
||||||
import {model as HomebrewModel } from './homebrew.model.js';
|
import { model as HomebrewModel } from './homebrew.model.js';
|
||||||
|
|
||||||
import { DEFAULT_BREW } from './brewDefaults.js';
|
import { DEFAULT_BREW } from './brewDefaults.js';
|
||||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||||
@@ -47,7 +47,7 @@ const sanitizeBrew = (brew, accessType)=>{
|
|||||||
return brew;
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
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(contentNegotiation);
|
app.use(contentNegotiation);
|
||||||
@@ -55,6 +55,40 @@ app.use(bodyParser.json({ limit: '25mb' }));
|
|||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(forceSSL);
|
app.use(forceSSL);
|
||||||
|
|
||||||
|
import cors from 'cors';
|
||||||
|
|
||||||
|
const nodeEnv = config.get('node_env');
|
||||||
|
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||||
|
|
||||||
|
const corsOptions = {
|
||||||
|
origin : (origin, callback)=>{
|
||||||
|
|
||||||
|
const allowedOrigins = [
|
||||||
|
'https://homebrewery.naturalcrit.com',
|
||||||
|
'https://www.naturalcrit.com',
|
||||||
|
'https://naturalcrit-stage.herokuapp.com',
|
||||||
|
'https://homebrewery-stage.herokuapp.com',
|
||||||
|
];
|
||||||
|
|
||||||
|
if(isLocalEnvironment) {
|
||||||
|
allowedOrigins.push('http://localhost:8000', 'http://localhost:8010');
|
||||||
|
}
|
||||||
|
|
||||||
|
const herokuRegex = /^https:\/\/(?:homebrewery-pr-\d+\.herokuapp\.com|naturalcrit-pr-\d+\.herokuapp\.com)$/; // Matches any Heroku app
|
||||||
|
|
||||||
|
if(!origin || allowedOrigins.includes(origin) || herokuRegex.test(origin)) {
|
||||||
|
callback(null, true);
|
||||||
|
} else {
|
||||||
|
console.log(origin, 'not allowed');
|
||||||
|
callback(new Error('Not allowed by CORS, if you think this is an error, please contact us'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods : ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
credentials : true,
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use(cors(corsOptions));
|
||||||
|
|
||||||
//Account Middleware
|
//Account Middleware
|
||||||
app.use((req, res, next)=>{
|
app.use((req, res, next)=>{
|
||||||
if(req.cookies && req.cookies.nc_session){
|
if(req.cookies && req.cookies.nc_session){
|
||||||
@@ -62,7 +96,9 @@ app.use((req, res, next)=>{
|
|||||||
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
||||||
//console.log("Just loaded up JWT from cookie:");
|
//console.log("Just loaded up JWT from cookie:");
|
||||||
//console.log(req.account);
|
//console.log(req.account);
|
||||||
} catch (e){}
|
} catch (e){
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.config = {
|
req.config = {
|
||||||
@@ -273,7 +309,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
brews.forEach(brew => brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
||||||
|
|
||||||
if(ownAccount && req?.account?.googleId){
|
if(ownAccount && req?.account?.googleId){
|
||||||
const auth = await GoogleActions.authCheck(req.account, res);
|
const auth = await GoogleActions.authCheck(req.account, res);
|
||||||
@@ -312,6 +348,34 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Change author name on brews
|
||||||
|
app.put('/api/user/rename', async (req, res)=>{
|
||||||
|
const { username, newUsername } = req.body;
|
||||||
|
const ownAccount = req.account && (req.account.username == newUsername);
|
||||||
|
|
||||||
|
if(!username || !newUsername)
|
||||||
|
return res.status(400).json({ error: 'Username and newUsername are required.' });
|
||||||
|
if(!ownAccount)
|
||||||
|
return res.status(403).json({ error: 'Must be logged in to change your username' });
|
||||||
|
try {
|
||||||
|
const brews = await HomebrewModel.getByUser(username, true, ['authors']);
|
||||||
|
const renamePromises = brews.map(async (brew)=>{
|
||||||
|
const updatedAuthors = brew.authors.map((author)=>author === username ? newUsername : author
|
||||||
|
);
|
||||||
|
return HomebrewModel.updateOne(
|
||||||
|
{ _id: brew._id },
|
||||||
|
{ $set: { authors: updatedAuthors } }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await Promise.all(renamePromises);
|
||||||
|
|
||||||
|
return res.json({ success: true, message: `Brews for ${username} renamed to ${newUsername}.` });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error renaming brews:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to rename brews.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
//Edit Page
|
//Edit Page
|
||||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
||||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||||
@@ -413,7 +477,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
let googleCount = [];
|
let googleCount = [];
|
||||||
if(req.account) {
|
if(req.account) {
|
||||||
if(req.account.googleId) {
|
if(req.account.googleId) {
|
||||||
auth = await GoogleActions.authCheck(req.account, res, false)
|
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||||
|
|
||||||
googleCount = await GoogleActions.listGoogleBrews(auth)
|
googleCount = await GoogleActions.listGoogleBrews(auth)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
@@ -448,8 +512,6 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
return next();
|
return next();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const nodeEnv = config.get('node_env');
|
|
||||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
|
||||||
// Local only
|
// Local only
|
||||||
if(isLocalEnvironment){
|
if(isLocalEnvironment){
|
||||||
// Login
|
// Login
|
||||||
@@ -477,7 +539,7 @@ app.get('/vault', asyncHandler(async(req, res, next)=>{
|
|||||||
|
|
||||||
//Send rendered page
|
//Send rendered page
|
||||||
app.use(asyncHandler(async (req, res, next)=>{
|
app.use(asyncHandler(async (req, res, next)=>{
|
||||||
if (!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
||||||
|
|
||||||
const page = await renderPage(req, res);
|
const page = await renderPage(req, res);
|
||||||
if(!page) return;
|
if(!page) return;
|
||||||
|
|||||||
@@ -106,12 +106,12 @@ const api = {
|
|||||||
stub = stub?.toObject();
|
stub = stub?.toObject();
|
||||||
googleId ??= stub?.googleId;
|
googleId ??= stub?.googleId;
|
||||||
|
|
||||||
const isOwner = stub?.authors?.length === 0 || stub?.authors?.[0] === req.account?.username;
|
const isOwner = (accessType == 'edit' && (!stub || stub?.authors?.length === 0)) || stub?.authors?.[0] === req.account?.username;
|
||||||
const isAuthor = stub?.authors?.includes(req.account?.username);
|
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||||
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||||
|
|
||||||
if(accessType === 'edit' && !(isOwner || isAuthor || isInvited)) {
|
if(accessType === 'edit' && !(isOwner || isAuthor || isInvited)) {
|
||||||
const accessError = { name: 'Access Error', status: 401, authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
|
const accessError = { name: 'Access Error', status: 401, authors: stub?.authors, brewTitle: stub?.title, shareId: stub?.shareId };
|
||||||
if(req.account)
|
if(req.account)
|
||||||
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03' };
|
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03' };
|
||||||
else
|
else
|
||||||
@@ -119,12 +119,12 @@ const api = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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's a google id, get it if requesting the full brew or if no stub found yet
|
||||||
if(!stubOnly && googleId) {
|
if(googleId && (!stubOnly || !stub)) {
|
||||||
const oAuth2Client = isOwner? GoogleActions.authCheck(req.account, res) : undefined;
|
const oAuth2Client = isOwner ? GoogleActions.authCheck(req.account, res) : undefined;
|
||||||
|
|
||||||
const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
|
const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
|
||||||
.catch((googleError)=>{
|
.catch((googleError)=>{
|
||||||
@@ -467,12 +467,11 @@ const api = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
router.use('/api', checkClientVersion);
|
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
||||||
router.post('/api', asyncHandler(api.newBrew));
|
router.put('/api/:id', checkClientVersion, 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', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||||
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
router.get('/api/remove/:id', checkClientVersion, 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));
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import packageJSON from '../../package.json' with { type: "json" };
|
import packageJSON from '../../package.json' with { type: 'json' };
|
||||||
const version = packageJSON.version;
|
|
||||||
|
|
||||||
export default (req, res, next)=>{
|
export default (req, res, next)=>{
|
||||||
const userVersion = req.get('Homebrewery-Version');
|
const userVersion = req.get('Homebrewery-Version');
|
||||||
|
const version = packageJSON.version;
|
||||||
|
|
||||||
if(userVersion != version) {
|
if(userVersion !== version) {
|
||||||
return res.status(412).send({
|
return res.status(412).send({
|
||||||
message : `Client version ${userVersion} is out of date. Please save your changes elsewhere and refresh to pick up client version ${version}.`
|
message : `Client version ${userVersion} is out of date. Please save your changes elsewhere and refresh to pick up client version ${version}.`
|
||||||
});
|
});
|
||||||
@@ -12,3 +12,4 @@ export default (req, res, next)=>{
|
|||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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));
|
||||||
if(isImageRequest && !isLocalEnvironment && !req.url?.startsWith('/staticImages')) {
|
if(isImageRequest && !(isLocalEnvironment && req.url?.startsWith('/staticImages'))) {
|
||||||
return res.status(406).send({
|
return res.status(406).send({
|
||||||
message : 'Request for image at this URL is not supported'
|
message : 'Request for image at this URL is not supported'
|
||||||
});
|
});
|
||||||
|
|||||||
41
server/middleware/content-negotiation.spec.js
Normal file
41
server/middleware/content-negotiation.spec.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import contentNegotiationMiddleware from './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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,49 +11,54 @@
|
|||||||
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
||||||
|
|
||||||
@keyframes sourceMoveAnimation {
|
@keyframes sourceMoveAnimation {
|
||||||
50% {background-color: red; color: white;}
|
50% { color : white;background-color : red;}
|
||||||
100% {background-color: unset; color: unset;}
|
100% { color : unset;background-color : unset;}
|
||||||
}
|
}
|
||||||
|
|
||||||
.codeEditor{
|
.codeEditor {
|
||||||
@media screen and (pointer : coarse) {
|
@media screen and (pointer : coarse) {
|
||||||
font-size : 16px;
|
font-size : 16px;
|
||||||
}
|
}
|
||||||
.CodeMirror-foldmarker {
|
.CodeMirror-foldmarker {
|
||||||
font-family: inherit;
|
font-family : inherit;
|
||||||
text-shadow: none;
|
font-weight : 600;
|
||||||
font-weight: 600;
|
color : grey;
|
||||||
color: grey;
|
text-shadow : none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sourceMoveFlash .CodeMirror-line{
|
.CodeMirror-foldgutter {
|
||||||
animation-name: sourceMoveAnimation;
|
cursor : pointer;
|
||||||
animation-duration: 0.4s;
|
border-left : 1px solid #EEEEEE;
|
||||||
}
|
transition : background 0.1s;
|
||||||
|
&:hover { background : #DDDDDD; }
|
||||||
|
}
|
||||||
|
|
||||||
.CodeMirror-vscrollbar {
|
.sourceMoveFlash .CodeMirror-line {
|
||||||
&::-webkit-scrollbar {
|
animation-name : sourceMoveAnimation;
|
||||||
width: 20px;
|
animation-duration : 0.4s;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
width: 20px;
|
.CodeMirror-vscrollbar {
|
||||||
background: linear-gradient(90deg, #858585 15px, #808080 15px);
|
&::-webkit-scrollbar { width : 20px; }
|
||||||
}
|
&::-webkit-scrollbar-thumb {
|
||||||
}
|
width : 20px;
|
||||||
|
background : linear-gradient(90deg, #858585 15px, #808080 15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//.cm-tab {
|
//.cm-tab {
|
||||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||||
//}
|
//}
|
||||||
|
|
||||||
//.cm-trailingspace {
|
//.cm-trailingspace {
|
||||||
// .cm-space {
|
// .cm-space {
|
||||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emojiPreview {
|
.emojiPreview {
|
||||||
font-size: 1.5em;
|
font-size : 1.5em;
|
||||||
line-height: 1.2em;
|
line-height : 1.2em;
|
||||||
}
|
}
|
||||||
@@ -43,5 +43,6 @@ html,body, #reactRoot{
|
|||||||
}
|
}
|
||||||
&:disabled{
|
&:disabled{
|
||||||
background-color : @silver !important;
|
background-color : @silver !important;
|
||||||
|
cursor:not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user