mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-01 17:22:42 +00:00
Merge branch 'master' into horizontalSpace
This commit is contained in:
@@ -82,6 +82,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:
|
||||||
|
|||||||
@@ -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(){
|
||||||
|
|||||||
1062
package-lock.json
generated
1062
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
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,6 +92,7 @@
|
|||||||
"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.3",
|
"dompurify": "^3.2.3",
|
||||||
@@ -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.4",
|
"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,7 +127,7 @@
|
|||||||
"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.13.0",
|
||||||
@@ -133,7 +135,7 @@
|
|||||||
"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"
|
||||||
|
|||||||
@@ -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,33 @@ 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;
|
||||||
|
console.log(req.account);
|
||||||
|
|
||||||
|
if(!username || !newUsername) {
|
||||||
|
return res.status(400).json({ error: 'Username and newUsername are required.' });
|
||||||
|
}
|
||||||
|
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;
|
||||||
@@ -399,7 +462,7 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
|
|||||||
app.get('/account', asyncHandler(async (req, res, next)=>{
|
app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||||
const data = {};
|
const data = {};
|
||||||
data.title = 'Account Information Page';
|
data.title = 'Account Information Page';
|
||||||
|
|
||||||
if(!req.account) {
|
if(!req.account) {
|
||||||
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
|
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
|
||||||
const error = new Error('No valid account');
|
const error = new Error('No valid account');
|
||||||
@@ -413,7 +476,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 +511,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,8 +538,8 @@ 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;
|
||||||
res.send(page);
|
res.send(page);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user