mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2025-12-24 16:22:44 +00:00
Merge branch 'master' of https://github.com/naturalcrit/homebrewery into re-author-brews-on-batch
This commit is contained in:
25
.github/pull_request_template.md
vendored
25
.github/pull_request_template.md
vendored
@@ -1,26 +1,29 @@
|
||||
<!--
|
||||
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.
|
||||
- 🚩 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?
|
||||
- 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
|
||||
-->
|
||||
> [!TIP]
|
||||
> 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.
|
||||
> - 🚩 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?
|
||||
> - 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
|
||||
|
||||
## 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
|
||||
|
||||
> [!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 #
|
||||
|
||||
## QA Instructions, Screenshots, Recordings
|
||||
|
||||
_Please replace this line with instructions on how to test or view your changes, as well as any before/after
|
||||
images for UI changes._
|
||||
_Replace this line with instructions on how to test or view your changes, as well as any before/after
|
||||
screenshots or recordings for UI changes._
|
||||
|
||||
### 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 *
|
||||
- [ ] 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
|
||||
- [ ] Identify opportunities for simplification and refactoring
|
||||
- [ ] Check for code legibility and appropriate comments
|
||||
|
||||
<details><summary>Copy this list</summary>
|
||||
|
||||
@@ -115,10 +115,10 @@
|
||||
color : #D3D3D3;
|
||||
accent-color : #D3D3D3;
|
||||
|
||||
&::-webkit-slider-thumb, &::-moz-slider-thumb {
|
||||
&::-webkit-slider-thumb, &::-moz-range-thumb {
|
||||
width : 5px;
|
||||
height : 5px;
|
||||
cursor : pointer;
|
||||
cursor : ew-resize;
|
||||
outline : none;
|
||||
}
|
||||
|
||||
|
||||
@@ -207,19 +207,11 @@ const Snippetbar = createClass({
|
||||
renderEditorButtons : function(){
|
||||
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' : ''}`}
|
||||
onClick={this.toggleHistoryMenu} >
|
||||
<i className='fas fa-clock-rotate-left' />
|
||||
@@ -235,14 +227,21 @@ const Snippetbar = createClass({
|
||||
</div>
|
||||
</div>
|
||||
<div className='codeTools'>
|
||||
{foldButtons}
|
||||
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||
<div className={`editorTool foldAll ${this.props.foldCode ? '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} >
|
||||
<i className='fas fa-palette' />
|
||||
{this.state.themeSelector && this.renderThemeSelector()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div></>}
|
||||
|
||||
|
||||
<div className='tabs'>
|
||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||
@@ -259,7 +258,8 @@ const Snippetbar = createClass({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>;
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
render : function(){
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
justify-content : flex-end;
|
||||
min-width : 225px;
|
||||
|
||||
&:only-child { margin-left : auto; }
|
||||
&:only-child { margin-left : auto;min-width:unset;}
|
||||
|
||||
>div {
|
||||
display : flex;
|
||||
@@ -38,6 +38,11 @@
|
||||
line-height : @menuHeight;
|
||||
text-align : center;
|
||||
cursor : pointer;
|
||||
|
||||
&.editorTool:not(.active) {
|
||||
cursor:not-allowed;
|
||||
}
|
||||
|
||||
&:hover,&.selected { background-color : #999999; }
|
||||
&.text {
|
||||
.tooltipLeft('Brew Editor');
|
||||
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -22,7 +22,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent-tabs": "^0.10.3",
|
||||
"dompurify": "^3.2.2",
|
||||
"dompurify": "^3.2.3",
|
||||
"expr-eval": "^2.0.2",
|
||||
"express": "^4.21.2",
|
||||
"express-async-handler": "^1.2.0",
|
||||
@@ -40,7 +40,7 @@
|
||||
"marked-smartypants-lite": "^1.0.2",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.30.1",
|
||||
"mongoose": "^8.8.3",
|
||||
"mongoose": "^8.8.4",
|
||||
"nanoid": "5.0.9",
|
||||
"nconf": "^0.12.1",
|
||||
"react": "^18.3.1",
|
||||
@@ -5476,10 +5476,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.2.tgz",
|
||||
"integrity": "sha512-YMM+erhdZ2nkZ4fTNRTSI94mb7VG7uVF5vj5Zde7tImgnhZE3R6YW/IACGIHb2ux+QkEXMhe591N+5jWOmL4Zw==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz",
|
||||
"integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
@@ -10897,9 +10896,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "8.8.3",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.3.tgz",
|
||||
"integrity": "sha512-/I4n/DcXqXyIiLRfAmUIiTjj3vXfeISke8dt4U4Y8Wfm074Wa6sXnQrXN49NFOFf2mM1kUdOXryoBvkuCnr+Qw==",
|
||||
"version": "8.8.4",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.4.tgz",
|
||||
"integrity": "sha512-yJbn695qCsqDO+xyPII29x2R7flzXhxCDv09mMZPSGllf0sm4jKw3E9s9uvQ9hjO6bL2xjU8KKowYqcY9eSTMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bson": "^6.7.0",
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent-tabs": "^0.10.3",
|
||||
"dompurify": "^3.2.2",
|
||||
"dompurify": "^3.2.3",
|
||||
"expr-eval": "^2.0.2",
|
||||
"express": "^4.21.2",
|
||||
"express-async-handler": "^1.2.0",
|
||||
@@ -112,7 +112,7 @@
|
||||
"marked-smartypants-lite": "^1.0.2",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.30.1",
|
||||
"mongoose": "^8.8.3",
|
||||
"mongoose": "^8.8.4",
|
||||
"nanoid": "5.0.9",
|
||||
"nconf": "^0.12.1",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -241,8 +241,8 @@ const GoogleActions = {
|
||||
return obj.data.id;
|
||||
},
|
||||
|
||||
getGoogleBrew : async (id, accessId, accessType)=>{
|
||||
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
||||
getGoogleBrew : async (auth = defaultAuth, id, accessId, accessType)=>{
|
||||
const drive = googleDrive.drive({ version: 'v3', auth: auth });
|
||||
|
||||
const obj = await drive.files.get({
|
||||
fileId : id,
|
||||
|
||||
@@ -87,76 +87,68 @@ const api = {
|
||||
// Create middleware with the accessType passed in as part of the scope
|
||||
return async (req, res, next)=>{
|
||||
// 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 },
|
||||
] }
|
||||
admin : { $or : [{ editId: id }, { shareId: id }] }
|
||||
};
|
||||
|
||||
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
||||
let stub = await HomebrewModel.get(accessMap[accessType])
|
||||
.catch((err)=>{
|
||||
if(googleId) {
|
||||
if(googleId)
|
||||
console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
|
||||
} else {
|
||||
else
|
||||
console.warn(err);
|
||||
}
|
||||
});
|
||||
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') {
|
||||
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(!stubOnly && (googleId || stub?.googleId)) {
|
||||
let googleError;
|
||||
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
||||
.catch((err)=>{
|
||||
googleError = err;
|
||||
if(!stubOnly && googleId) {
|
||||
const oAuth2Client = isOwner? GoogleActions.authCheck(req.account, res) : undefined;
|
||||
|
||||
const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
|
||||
.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
|
||||
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(!stub && !stubOnly) {
|
||||
if(!stub)
|
||||
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
|
||||
if(stub) {
|
||||
stub.tags = stub.tags || undefined; // Clear empty strings
|
||||
stub.renderer = stub.renderer || undefined; // Clear empty strings
|
||||
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
|
||||
}
|
||||
stub.tags = stub.tags || undefined; // Clear empty strings
|
||||
stub.renderer = stub.renderer || undefined; // Clear empty strings
|
||||
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
|
||||
|
||||
req.brew = stub ?? {};
|
||||
req.brew = stub;
|
||||
next();
|
||||
};
|
||||
},
|
||||
|
||||
@@ -298,7 +298,7 @@ describe('Tests for api', ()=>{
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(api.getId).toHaveBeenCalledWith(req);
|
||||
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()=>{
|
||||
|
||||
@@ -11,49 +11,54 @@
|
||||
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
||||
|
||||
@keyframes sourceMoveAnimation {
|
||||
50% {background-color: red; color: white;}
|
||||
100% {background-color: unset; color: unset;}
|
||||
50% { color : white;background-color : red;}
|
||||
100% { color : unset;background-color : unset;}
|
||||
}
|
||||
|
||||
.codeEditor{
|
||||
@media screen and (pointer : coarse) {
|
||||
font-size : 16px;
|
||||
}
|
||||
.CodeMirror-foldmarker {
|
||||
font-family: inherit;
|
||||
text-shadow: none;
|
||||
font-weight: 600;
|
||||
color: grey;
|
||||
}
|
||||
.codeEditor {
|
||||
@media screen and (pointer : coarse) {
|
||||
font-size : 16px;
|
||||
}
|
||||
.CodeMirror-foldmarker {
|
||||
font-family : inherit;
|
||||
font-weight : 600;
|
||||
color : grey;
|
||||
text-shadow : none;
|
||||
}
|
||||
|
||||
.sourceMoveFlash .CodeMirror-line{
|
||||
animation-name: sourceMoveAnimation;
|
||||
animation-duration: 0.4s;
|
||||
}
|
||||
.CodeMirror-foldgutter {
|
||||
cursor : pointer;
|
||||
border-left : 1px solid #EEEEEE;
|
||||
transition : background 0.1s;
|
||||
&:hover { background : #DDDDDD; }
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 20px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
width: 20px;
|
||||
background: linear-gradient(90deg, #858585 15px, #808080 15px);
|
||||
}
|
||||
}
|
||||
.sourceMoveFlash .CodeMirror-line {
|
||||
animation-name : sourceMoveAnimation;
|
||||
animation-duration : 0.4s;
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar {
|
||||
&::-webkit-scrollbar { width : 20px; }
|
||||
&::-webkit-scrollbar-thumb {
|
||||
width : 20px;
|
||||
background : linear-gradient(90deg, #858585 15px, #808080 15px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//.cm-tab {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||
//}
|
||||
//.cm-tab {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||
//}
|
||||
|
||||
//.cm-trailingspace {
|
||||
// .cm-space {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||
// }
|
||||
//}
|
||||
//.cm-trailingspace {
|
||||
// .cm-space {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
.emojiPreview {
|
||||
font-size: 1.5em;
|
||||
line-height: 1.2em;
|
||||
font-size : 1.5em;
|
||||
line-height : 1.2em;
|
||||
}
|
||||
@@ -43,5 +43,6 @@ html,body, #reactRoot{
|
||||
}
|
||||
&:disabled{
|
||||
background-color : @silver !important;
|
||||
cursor:not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user