diff --git a/.gitignore b/.gitignore index b1ff706a..6aa6839e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ yarn-debug.log* yarn-error.log* firebase-debug.log .firebase/*.cache +logo.ai +keycaplendar.xd diff --git a/firebase.json b/firebase.json index 76cb94c5..338518ec 100644 --- a/firebase.json +++ b/firebase.json @@ -17,12 +17,8 @@ ], "rewrites": [ { - "source": "/api/isEditor", - "function": "isEditor" - }, - { - "source": "/api/isAdmin", - "function": "isAdmin" + "source": "/api/getClaims", + "function": "getClaims" }, { "source": "/api/listUsers", diff --git a/functions/index.js b/functions/index.js index deb3e4f2..a233c501 100644 --- a/functions/index.js +++ b/functions/index.js @@ -1,31 +1,35 @@ - -const admin = require('firebase-admin'); -const functions = require('firebase-functions'); +const admin = require("firebase-admin"); +const functions = require("firebase-functions"); const app = admin.initializeApp(); -exports.isEditor = functions.https.onCall((data, context) => { - if (context.auth && (context.auth.token.editor === true)) { - return true; - } - return false; -}); - -exports.isAdmin = functions.https.onCall((data, context) => { - if (context.auth && (context.auth.token.admin === true)) { - return true; +exports.getClaims = functions.https.onCall((data, context) => { + if (context.auth) { + return { + nickname: context.auth.token.nickname ? context.auth.token.nickname : "", + designer: context.auth.token.designer ? context.auth.token.designer : false, + editor: context.auth.token.editor ? context.auth.token.editor : false, + admin: context.auth.token.admin ? context.auth.token.admin : false, + }; } - return false; + return { + nickname: "", + designer: false, + editor: false, + admin: false, + }; }); exports.listUsers = functions.https.onCall(async (data, context) => { if (!context.auth || context.auth.token.admin !== true) { return { - error: "Current user is not an admin. Access is not permitted." - } + error: "Current user is not an admin. Access is not permitted.", + }; } // List batch of users, 1000 at a time. - const users = await admin.auth().listUsers(1000) + const users = await admin + .auth() + .listUsers(1000) .then((result) => { const newArray = result.users.map((user) => { if (user.customClaims) { @@ -33,70 +37,93 @@ exports.listUsers = functions.https.onCall(async (data, context) => { displayName: user.displayName, email: user.email, photoURL: user.photoURL, - editor: (user.customClaims.editor ? true : false), - admin: (user.customClaims.admin ? true : false ) + nickname: user.customClaims.nickname ? user.customClaims.nickname : "", + designer: user.customClaims.designer ? true : false, + editor: user.customClaims.editor ? true : false, + admin: user.customClaims.admin ? true : false, }; } else { return { displayName: user.displayName, email: user.email, photoURL: user.photoURL, + nickname: "", + designer: false, editor: false, - admin: false + admin: false, }; } - }) + }); return newArray; }) .catch((error) => { - return { error: 'Error listing users: ' + error }; + return { error: "Error listing users: " + error }; }); return users; }); - exports.deleteUser = functions.https.onCall(async (data, context) => { if (!context.auth || context.auth.token.admin !== true) { return { - error: "Current user is not an admin. Access is not permitted." - } + error: "Current user is not an admin. Access is not permitted.", + }; } - if (data.email === 'ben.j.durrant@gmail.com') { + if (data.email === "ben.j.durrant@gmail.com") { return { - error: "This user cannot be deleted" - } + error: "This user cannot be deleted", + }; } const currentUser = await admin.auth().getUser(context.auth.uid); const user = await admin.auth().getUserByEmail(data.email); - admin.auth().deleteUser(user.uid) + admin + .auth() + .deleteUser(user.uid) .then(() => { - console.log(currentUser.displayName + ' successfully deleted account of ' + user.displayName + '.'); + console.log(currentUser.displayName + " successfully deleted account of " + user.displayName + "."); return null; }) .catch((error) => { - return { error: 'Error deleting user: ' + error}; + return { error: "Error deleting user: " + error }; }); - return 'Success'; + return "Success"; }); exports.setRoles = functions.https.onCall(async (data, context) => { if (!context.auth || context.auth.token.admin !== true) { return { - error: "User not admin." - } + error: "User not admin.", + }; } const currentUser = await admin.auth().getUser(context.auth.uid); const user = await admin.auth().getUserByEmail(data.email); const claims = { + designer: data.designer, + nickname: data.nickname, editor: data.editor, - admin: data.admin - } - await admin.auth().setCustomUserClaims(user.uid, claims).then(() => { - console.log(currentUser.displayName + ' successfully edited account of ' + user.displayName + '. Editor: ' + data.editor + ', admin: ' + data.admin); - return null - }).catch((error) => { - return { error: 'Error setting roles: ' + error}; - }) + admin: data.admin, + }; + await admin + .auth() + .setCustomUserClaims(user.uid, claims) + .then(() => { + console.log( + currentUser.displayName + + " successfully edited account of " + + user.displayName + + ". Designer: " + + data.designer + + ", editor: " + + data.editor + + ", admin: " + + data.admin + + ", nickname: " + + data.nickname + ); + return null; + }) + .catch((error) => { + return { error: "Error setting roles: " + error }; + }); const newUser = await admin.auth().getUserByEmail(data.email); return newUser.customClaims; }); diff --git a/keycaplendar.xd b/keycaplendar.xd deleted file mode 100644 index d65cb723..00000000 Binary files a/keycaplendar.xd and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 3a06c39b..7433ff1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6151,9 +6151,9 @@ } }, "dom-align": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.10.4.tgz", - "integrity": "sha512-wytDzaru67AmqFOY4B9GUb/hrwWagezoYYK97D/vpK+ezg+cnuZO0Q2gltUPa7KfNmIqfRIYVCF8UhRDEHAmgQ==" + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.0.tgz", + "integrity": "sha512-YkoezQuhp3SLFGdOlr5xkqZ640iXrnHAwVYcDg8ZKRUtO7mSzSC2BA5V0VuyAwPSJA4CLIc6EDDJh4bEsD2+zA==" }, "dom-converter": { "version": "0.2.0", @@ -13511,9 +13511,9 @@ } }, "rc-animate": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-2.10.2.tgz", - "integrity": "sha512-cE/A7piAzoWFSgUD69NmmMraqCeqVBa51UErod8NS3LUEqWfppSVagHfa0qHAlwPVPiIBg3emRONyny3eiH0Dg==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-2.11.1.tgz", + "integrity": "sha512-1NyuCGFJG/0Y+9RKh5y/i/AalUCA51opyyS/jO2seELpgymZm2u9QV3xwODwEuzkmeQ1BDPxMLmYLcTJedPlkQ==", "requires": { "babel-runtime": "6.x", "classnames": "^2.2.6", @@ -13549,13 +13549,13 @@ } }, "rc-util": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.19.0.tgz", - "integrity": "sha512-mptALlLwpeczS3nrv83DbwJNeupolbuvlIEjcvimSiWI8NUBjpF0HgG3kWp1RymiuiRCNm9yhaXqDz0a99dpgQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.21.1.tgz", + "integrity": "sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==", "requires": { "add-dom-event-listener": "^1.1.0", - "babel-runtime": "6.x", "prop-types": "^15.5.10", + "react-is": "^16.12.0", "react-lifecycles-compat": "^3.0.4", "shallowequal": "^1.1.0" } diff --git a/src/App.js b/src/App.js index 13d6f34e..3e5f709c 100644 --- a/src/App.js +++ b/src/App.js @@ -31,7 +31,7 @@ class App extends React.Component { loading: false, content: true, search: "", - user: { email: null, name: null, avatar: null, isEditor: false, isAdmin: false }, + user: { email: null, name: null, avatar: null, isEditor: false, isAdmin: false, nickname: "", isDesigner: false }, whitelist: { vendors: [], profiles: [], edited: false }, cookies: true, applyTheme: "manual", @@ -689,40 +689,27 @@ class App extends React.Component { document.querySelector("html").classList = this.state.theme; this.unregisterAuthObserver = firebase.auth().onAuthStateChanged((user) => { if (user) { - const isEditorFn = firebase.functions().httpsCallable("isEditor"); - isEditorFn() + const getClaimsFn = firebase.functions().httpsCallable("getClaims"); + getClaimsFn() .then((result) => { - const isEditor = result.data; - const isAdminFn = firebase.functions().httpsCallable("isAdmin"); - isAdminFn() - .then((result) => { - this.setUser({ - email: user.email, - name: user.displayName, - avatar: user.photoURL, - isEditor: isEditor, - isAdmin: result.data, - }); - }) - .catch((error) => { - console.log("Error verifying admin access: " + error); - queue.notify({ title: "Error verifying admin access: " + error }); - this.setUser({ - email: user.email, - name: user.displayName, - avatar: user.photoURL, - isEditor: isEditor, - isAdmin: false, - }); - }); + this.setUser({ + email: user.email, + name: user.displayName, + avatar: user.photoURL, + nickname: result.data.nickname, + isDesigner: result.data.designer, + isEditor: result.data.editor, + isAdmin: result.data.admin, + }); }) .catch((error) => { - console.log("Error verifying editor access: " + error); - queue.notify({ title: "Error verifying editor access: " + error }); + queue.notify({ title: "Error verifying custom claims: " + error }); this.setUser({ email: user.email, name: user.displayName, avatar: user.photoURL, + nickname: "", + isDesigner: false, isEditor: false, isAdmin: false, }); @@ -877,7 +864,12 @@ class App extends React.Component {
- +
diff --git a/src/components/Content.js b/src/components/Content.js index 9d7f11db..63025c99 100644 --- a/src/components/Content.js +++ b/src/components/Content.js @@ -192,7 +192,6 @@ export class DesktopContent extends React.Component { sort={this.props.sort} page={this.props.page} view={this.props.view} - editor={this.props.editor} details={this.openDetailsDrawer} closeDetails={this.closeDetailsDrawer} detailSet={this.state.detailSet} @@ -204,12 +203,13 @@ export class DesktopContent extends React.Component { ); const editorElements = - this.props.editor && this.props.page !== "statistics" ? ( + (this.props.user.isEditor || this.props.user.isDesigner) && this.props.page !== "statistics" ? (
- - + {this.props.user.isEditor ? ( +
+ + +
+ ) : ( + "" + )}
) : ( "" @@ -251,7 +258,7 @@ export class DesktopContent extends React.Component { this.props.view === "compact" ? (
); const editorElements = - this.props.editor && this.props.page !== "statistics" ? ( + (this.props.user.isEditor || this.props.user.isDesigner) && this.props.page !== "statistics" ? (
- - + {this.props.user.isEditor ? ( +
+ + +
+ ) : ( + "" + )}
) : ( "" @@ -588,7 +602,7 @@ export class TabletContent extends React.Component { {editorElements} ); const editorElements = - this.props.editor && this.props.page !== "statistics" ? ( + (this.props.user.isEditor || this.props.user.isDesigner) && this.props.page !== "statistics" ? (
- - + {this.props.user.isEditor ? ( +
+ + +
+ ) : ( + "" + )}
) : ( "" @@ -853,7 +874,7 @@ export class MobileContent extends React.Component { close={this.closeNavDrawer} openSettings={this.openSettingsDialog} /> - {this.props.editor && this.props.page !== "statistics" ? ( + {(this.props.user.isEditor || this.props.user.isDesigner) && this.props.page !== "statistics" ? ( ); const search = - this.props.bottomNav && this.props.editor ? ( + this.props.bottomNav && (this.props.user.isEditor || this.props.user.isDesigner) ? ( @@ -942,7 +963,7 @@ export class MobileContent extends React.Component { {editorElements} ); } - const editorButtons = this.props.editor ? ( -
-
- ) : ( - "" - ); + const editorButtons = + this.props.user.isEditor || + (this.props.user.isDesigner && set.designer && set.designer.indexOf(this.props.user.nickname) > -1) ? ( +
+
+ ) : ( + "" + ); return ( @@ -488,29 +500,41 @@ export class TabletDrawerDetails extends React.Component { ); } } - const editorButtons = this.props.editor ? ( -
-
- ) : ( -
- ); + const editorButtons = + this.props.user.isEditor || + (this.props.user.isDesigner && set.designer && set.designer.indexOf(this.props.user.nickname) > -1) ? ( +
+
+ ) : ( +
+ ); return ( diff --git a/src/components/DrawerDetails.scss b/src/components/DrawerDetails.scss index 14c23e2f..b6cc2b1a 100644 --- a/src/components/DrawerDetails.scss +++ b/src/components/DrawerDetails.scss @@ -90,11 +90,10 @@ justify-content: stretch; .edit { flex-grow: 1; - margin-right: 8px; } .delete { flex-grow: 0; - margin-left: 8px; + margin-left: 16px; } } .search-chips { diff --git a/src/components/DrawerEntry.js b/src/components/DrawerEntry.js index 357d5e35..0b6de594 100644 --- a/src/components/DrawerEntry.js +++ b/src/components/DrawerEntry.js @@ -267,6 +267,16 @@ export class DrawerCreate extends React.Component { } }; + componentDidUpdate(prevProps) { + if (this.props.open !== prevProps.open) { + if (this.props.user.isEditor === false && this.props.user.isDesigner) { + this.setState({ + designer: [this.props.user.nickname], + }); + } + } + } + render() { const formFilled = this.state.profile !== "" && @@ -443,6 +453,7 @@ export class DrawerCreate extends React.Component { onChange={this.handleChange} onFocus={this.handleFocus} onBlur={this.handleBlur} + disabled={this.props.user.isEditor === false && this.props.user.isDesigner} /> { + this.setState({ + focused: e.target.name, + }); + }; + handleBlur = () => { + this.setState({ + focused: "", + }); + }; + handleTextChange = (e) => { + const newUser = this.state.user; + newUser[e.target.name] = e.target.value; + this.setState({ + user: newUser, + edited: true, + }); + }; + selectValue = (prop, value) => { + const newUser = this.state.user; + newUser[prop] = value; + this.setState({ + user: newUser, + edited: true, + }); + }; setRoles = () => { this.setState({ loading: true }); const setRolesFn = firebase.functions().httpsCallable("setRoles"); setRolesFn({ email: this.state.user.email, + nickname: this.state.user.nickname, + designer: this.state.user.designer, editor: this.state.user.editor, admin: this.state.user.admin, }).then((result) => { @@ -57,26 +90,8 @@ export class UserRow extends React.Component { }; render() { const user = this.state.user; - const editorCheckbox = ( - - ); - const adminCheckbox = ( - - ); const saveButton = this.state.loading ? ( - ) : user.email === this.props.currentUser.email || user.email === "ben.j.durrant@gmail.com" ? ( - "" ) : (
{ @@ -121,15 +136,48 @@ export class UserRow extends React.Component { ); return ( - -
-
- {user.displayName} -
- + {user.displayName} {user.email} - {editorCheckbox} - {adminCheckbox} + + + + + + + + + + + + + + + {saveButton} {deleteButton} diff --git a/src/components/Users.js b/src/components/Users.js index b1ca6dbd..c3cc4e71 100644 --- a/src/components/Users.js +++ b/src/components/Users.js @@ -136,35 +136,40 @@ export class Users extends React.Component {
- - - - - User - Email - Editor - Admin - Save - Delete - {refreshButton} - - - - {this.state.users.map((user, index) => { - return ( - - ); - })} - - - +
+ + + + + User + Email + Nickname + Designer + Editor + Admin + Save + Delete + {refreshButton} + + + + {this.state.users.map((user, index) => { + return ( + + ); + })} + + + +
Delete User diff --git a/src/components/Users.scss b/src/components/Users.scss index 87c8a273..a765c6a8 100644 --- a/src/components/Users.scss +++ b/src/components/Users.scss @@ -1,6 +1,7 @@ @import "@material/animation/variables"; @import "@material/elevation/mixins"; @import "@material/ripple/mixins"; +@import "@material/textfield/mixins"; @import "@material/typography/mixins"; .users-container { @@ -16,16 +17,19 @@ z-index: -1; } .rmwc-data-table { + overflow: visible; border: none; border-radius: 4px; @include mdc-elevation(1); &__content { width: 100%; + background-color: transparent; } &__head .rmwc-data-table__row { height: 56px; .rmwc-data-table__head-cell { @include mdc-typography(subtitle2); + font-family: inherit; vertical-align: middle; } } @@ -33,6 +37,7 @@ height: 52px; .rmwc-data-table__cell { @include mdc-typography(body2); + font-family: inherit; &::before { transition: opacity 100ms; } @@ -44,20 +49,20 @@ cursor: default; } } - } - } - .user-cell { - padding: 0 16px; - .user { - display: flex; - align-items: center; - .avatar { + .nickname { + height: 32px; + width: 200px; + .mdc-text-field__input { + padding: 0 12px; + @include mdc-typography(body2); + font-family: inherit; + } + } + .autocomplete.mdc-menu .mdc-list-item { + padding: 0 12px; height: 40px; - width: 40px; - border-radius: 20px; - margin-right: 12px; - background-size: cover; - background-position: center; + @include mdc-typography(body2); + font-family: inherit; } } } diff --git a/src/theme.scss b/src/theme.scss index 826326a5..6f108dbe 100644 --- a/src/theme.scss +++ b/src/theme.scss @@ -535,10 +535,8 @@ $overlay-values: ( } .rmwc-data-table { + background-color: if($dark, overlay-elevation(map-get($variables, surface), 1), map-get($variables, surface)); color: map-get($variables, text-primary); - &__content { - background-color: if($dark, overlay-elevation(map-get($variables, surface), 1), map-get($variables, surface)); - } &__cell { background-color: transparent; border-color: var(--divider-color);