0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-24 20:42:43 +00:00

Merge branch 'master' into horizontalSpace

This commit is contained in:
David Bolack
2024-12-17 21:35:26 -06:00
10 changed files with 258 additions and 967 deletions

View File

@@ -82,6 +82,9 @@ jobs:
- run:
name: Test - Coverage
command: npm run test:coverage
- run:
name: Test - Content Negotiation
command: npm run test:content-negotiation
workflows:
build_and_test:

View File

@@ -1,8 +1,8 @@
require('./homebrew.less');
const React = require('react');
const createClass = require('create-react-class');
const { StaticRouter:Router } = require('react-router-dom/server');
const { Route, Routes, useParams, useSearchParams } = require('react-router-dom');
const { StaticRouter:Router } = require('react-router');
const { Route, Routes, useParams, useSearchParams } = require('react-router');
const HomePage = require('./pages/homePage/homePage.jsx');
const EditPage = require('./pages/editPage/editPage.jsx');

View File

@@ -381,7 +381,7 @@ const EditPage = createClass({
**[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(){

1062
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@
"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:notifications": "jest \"server/.*.spec.js\" -t \"Notifications\" --verbose",
"test:content-negotiation": "jest \"server/middleware/.*.spec.js\" --verbose",
"test:coverage": "jest --coverage --silent --runInBand",
"test:dev": "jest --verbose --watch",
"test:basic": "jest tests/markdown/basic.test.js --verbose",
@@ -91,6 +92,7 @@
"classnames": "^2.5.1",
"codemirror": "^5.65.6",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3",
"dompurify": "^3.2.3",
@@ -111,13 +113,13 @@
"marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1",
"mongoose": "^8.8.4",
"mongoose": "^8.9.1",
"nanoid": "5.0.9",
"nconf": "^0.12.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-frame-component": "^4.1.3",
"react-router-dom": "6.28.0",
"react-router": "^7.0.2",
"sanitize-filename": "1.6.3",
"superagent": "^10.1.1",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
@@ -125,7 +127,7 @@
"devDependencies": {
"@stylistic/stylelint-plugin": "^3.1.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-react": "^7.37.2",
"globals": "^15.13.0",
@@ -133,7 +135,7 @@
"jest-expect-message": "^1.1.3",
"jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0",
"stylelint": "^16.11.0",
"stylelint": "^16.12.0",
"stylelint-config-recess-order": "^5.1.1",
"stylelint-config-recommended": "^14.0.1",
"supertest": "^7.0.0"

View File

@@ -2,7 +2,7 @@
// Set working directory to project root
import { dirname } from 'path';
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));
process.chdir(`${__dirname}/..`);
@@ -26,7 +26,7 @@ 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';
import { model as HomebrewModel } from './homebrew.model.js';
import { DEFAULT_BREW } from './brewDefaults.js';
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
@@ -47,7 +47,7 @@ const sanitizeBrew = (brew, accessType)=>{
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(contentNegotiation);
@@ -55,6 +55,40 @@ app.use(bodyParser.json({ limit: '25mb' }));
app.use(cookieParser());
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
app.use((req, res, next)=>{
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'));
//console.log("Just loaded up JWT from cookie:");
//console.log(req.account);
} catch (e){}
} catch (e){
console.log(e);
}
}
req.config = {
@@ -273,7 +309,7 @@ app.get('/user/:username', async (req, res, next)=>{
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){
const auth = await GoogleActions.authCheck(req.account, res);
@@ -312,6 +348,33 @@ app.get('/user/:username', async (req, res, 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
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
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)=>{
const data = {};
data.title = 'Account Information Page';
if(!req.account) {
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
const error = new Error('No valid account');
@@ -413,7 +476,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
let googleCount = [];
if(req.account) {
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)
.catch((err)=>{
@@ -448,8 +511,6 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
return next();
}));
const nodeEnv = config.get('node_env');
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
// Local only
if(isLocalEnvironment){
// Login
@@ -477,8 +538,8 @@ app.get('/vault', asyncHandler(async(req, res, next)=>{
//Send rendered page
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);
if(!page) return;
res.send(page);

View File

@@ -467,12 +467,11 @@ const api = {
}
};
router.use('/api', checkClientVersion);
router.post('/api', asyncHandler(api.newBrew));
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.delete('/api/:id', asyncHandler(api.deleteBrew));
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
export default api;

View File

@@ -1,10 +1,10 @@
import packageJSON from '../../package.json' with { type: "json" };
const version = packageJSON.version;
import packageJSON from '../../package.json' with { type: 'json' };
export default (req, res, next)=>{
const userVersion = req.get('Homebrewery-Version');
const version = packageJSON.version;
if(userVersion != version) {
if(userVersion !== version) {
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}.`
});
@@ -12,3 +12,4 @@ export default (req, res, next)=>{
next();
};

View File

@@ -6,7 +6,7 @@ export default (req, res, next)=>{
const isImageRequest = req.get('Accept')?.split(',')
?.filter((h)=>!h.includes('q='))
?.every((h)=>/image\/.*/.test(h));
if(isImageRequest && !isLocalEnvironment && !req.url?.startsWith('/staticImages')) {
if(isImageRequest && !(isLocalEnvironment && req.url?.startsWith('/staticImages'))) {
return res.status(406).send({
message : 'Request for image at this URL is not supported'
});

View 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();
});
});