├── client ├── jsconfig.json ├── public │ ├── favicon │ │ ├── favicon.ico │ │ ├── apple-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── ms-icon-70x70.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ └── apple-icon-precomposed.png │ ├── browserconfig.xml │ └── manifest.json ├── src │ ├── _env.sass │ ├── components │ │ ├── Form │ │ │ └── components │ │ │ │ ├── index.js │ │ │ │ ├── RangeInput.js │ │ │ │ ├── FormInput.js │ │ │ │ ├── FormField.js │ │ │ │ └── RichTextEditor │ │ │ │ └── styles.sass │ │ ├── Modal │ │ │ ├── index.js │ │ │ ├── store │ │ │ │ ├── actions.js │ │ │ │ ├── index.js │ │ │ │ └── reducer.js │ │ │ ├── components │ │ │ │ ├── ModalButton.js │ │ │ │ ├── ModalLink.js │ │ │ │ └── Modal.js │ │ │ └── utils.js │ │ ├── Dropdown │ │ │ ├── style.sass │ │ │ └── index.js │ │ ├── WebLink.js │ │ ├── UserName.js │ │ ├── Icon.js │ │ ├── LoadingOverlay.js │ │ ├── Pulse │ │ │ ├── index.js │ │ │ └── style.sass │ │ ├── Date.js │ │ └── Errors.js │ ├── pages │ │ ├── Dashboard │ │ │ ├── utils │ │ │ │ ├── staff.js │ │ │ │ ├── apps.js │ │ │ │ ├── feed.js │ │ │ │ └── icons.js │ │ │ ├── components │ │ │ │ ├── StudentCard │ │ │ │ │ └── style.sass │ │ │ │ ├── StudentModal │ │ │ │ │ ├── panels │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── PanelTabs.js │ │ │ │ │ │ ├── utils.js │ │ │ │ │ │ ├── ActivityPanel.js │ │ │ │ │ │ └── StudentPanel.js │ │ │ │ │ ├── components │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── ActivityFeed │ │ │ │ │ │ │ ├── entries │ │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ │ ├── Created.js │ │ │ │ │ │ │ │ ├── Elevate.js │ │ │ │ │ │ │ │ └── Deelevate.js │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ │ └── FeedEntry.js │ │ │ │ │ │ │ ├── style.sass │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── ModalBox.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── utils.js │ │ │ │ ├── Topbar │ │ │ │ │ ├── style.sass │ │ │ │ │ └── index.js │ │ │ │ ├── RoomLink.js │ │ │ │ ├── RequirePerm.js │ │ │ │ ├── Views.js │ │ │ │ ├── Toolbar │ │ │ │ │ └── style.sass │ │ │ │ └── SortSelectDropdown.js │ │ │ ├── views │ │ │ │ ├── Classroom │ │ │ │ │ └── components │ │ │ │ │ │ └── Widget │ │ │ │ │ │ └── style.sass │ │ │ │ ├── UserSettings │ │ │ │ │ ├── components │ │ │ │ │ │ ├── NoClassroomNotification.js │ │ │ │ │ │ └── ClassroomModal │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── ClassroomModalContent.js │ │ │ │ │ │ │ └── ClassroomForm.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ └── index.js │ │ │ │ ├── Team │ │ │ │ │ ├── components │ │ │ │ │ │ ├── StaffListControls.js │ │ │ │ │ │ └── StaffGroupPanel.js │ │ │ │ │ └── index.js │ │ │ │ └── Students │ │ │ │ │ └── index.js │ │ │ ├── style.sass │ │ │ └── store │ │ │ │ ├── index.js │ │ │ │ └── actionsNames.js │ │ ├── Home │ │ │ ├── index.js │ │ │ └── components │ │ │ │ └── MainHero.js │ │ ├── index.js │ │ └── NotFound.js │ ├── animations │ │ └── Fade │ │ │ ├── style.sass │ │ │ └── index.js │ ├── layouts │ │ ├── components │ │ │ ├── Footer │ │ │ │ ├── style.sass │ │ │ │ └── index.js │ │ │ └── TopNavbar.js │ │ ├── MainWithLogin.js │ │ └── Main.js │ ├── utils │ │ ├── githubApi.js │ │ ├── windowWidth.js │ │ ├── icons.js │ │ ├── detection.js │ │ ├── socket.io.js │ │ └── ready.js │ ├── store │ │ └── actions.js │ ├── index.js │ └── App.js └── package.json ├── app ├── config │ ├── apps │ │ ├── library.js │ │ ├── registry.json │ │ └── register.js │ ├── utils │ │ ├── passwordHash.js │ │ └── validateJwtPayload.js │ ├── permissions │ │ ├── sets │ │ │ ├── FeedSet.js │ │ │ ├── FeedElevateSet.js │ │ │ ├── FeedDelevateSet.js │ │ │ ├── InviteSet.js │ │ │ ├── index.js │ │ │ ├── FeedCommentSet.js │ │ │ ├── StudentSet.js │ │ │ └── RoomSet.js │ │ ├── roles │ │ │ ├── index.js │ │ │ ├── taRole.js │ │ │ └── instructorRole.js │ │ ├── Role.js │ │ ├── index.js │ │ └── PermissionSet.js │ ├── mongoose.js │ ├── jwtstrategy.js │ ├── errors │ │ ├── handleRouteError.js │ │ └── index.js │ └── options.js ├── controllers │ ├── types │ │ ├── library.js │ │ └── Controller.js │ ├── definitions │ │ ├── models │ │ │ ├── Feed.js │ │ │ ├── Token.js │ │ │ ├── Room.js │ │ │ ├── App.js │ │ │ ├── User.js │ │ │ ├── AppType.js │ │ │ ├── index.js │ │ │ └── schema │ │ │ │ ├── FeedSchema │ │ │ │ ├── methods.js │ │ │ │ └── index.js │ │ │ │ ├── MemberSchema │ │ │ │ ├── methods.js │ │ │ │ └── index.js │ │ │ │ ├── InviteSchema.js │ │ │ │ ├── FeedEntrySchema.js │ │ │ │ ├── TokenSchema.js │ │ │ │ ├── AppSchema.js │ │ │ │ ├── UserSchema.js │ │ │ │ ├── AppTypeSchema.js │ │ │ │ ├── StudentSchema │ │ │ │ └── index.js │ │ │ │ ├── index.js │ │ │ │ └── RoomSchema.js │ │ ├── AppTypeController.js │ │ ├── FeedController.js │ │ └── AppController.js │ ├── utils │ │ ├── ioEmit.js │ │ ├── searchCtrls.js │ │ └── queryModifier.js │ └── index.js ├── graphql │ ├── modules │ │ ├── typedefs │ │ │ ├── inputs │ │ │ │ ├── index.js │ │ │ │ └── Credentials.js │ │ │ ├── scalars │ │ │ │ ├── index.js │ │ │ │ └── JSONObject.js │ │ │ ├── index.js │ │ │ └── types │ │ │ │ ├── Auth.js │ │ │ │ ├── FeedEntryComment.js │ │ │ │ ├── AppTypeDocument.js │ │ │ │ ├── UserDocument.js │ │ │ │ ├── StaffDocument.js │ │ │ │ ├── FeedEntryDocument.js │ │ │ │ ├── FeedEntryCommentDocument.js │ │ │ │ ├── RoomDocument.js │ │ │ │ ├── StudentDocument.js │ │ │ │ └── index.js │ │ ├── utils │ │ │ ├── index.js │ │ │ └── README.md │ │ ├── index.js │ │ ├── auth.js │ │ ├── student.js │ │ └── room.js │ ├── middleware │ │ ├── index.js │ │ ├── setMemberContext.js │ │ └── setRoomContext.js │ ├── context │ │ ├── db.js │ │ ├── index.js │ │ └── authentication.js │ └── index.js ├── routes │ ├── middleware │ │ ├── isAuthenticated.js │ │ ├── initCrData.js │ │ ├── setDefaultError.js │ │ ├── isVerified.js │ │ ├── setAppSearch.js │ │ ├── isFeedEntryOwner.js │ │ ├── globalParamsValidation.js │ │ ├── createValidationMiddleware.js │ │ ├── createCheckPermission.js │ │ ├── validateParamsHandler.js │ │ ├── isRoomMember.js │ │ ├── createControllerHandler.js │ │ ├── setInvite.js │ │ ├── setFeed.js │ │ └── setRoom.js │ ├── validation │ │ ├── definitions │ │ │ ├── inviteValidation.js │ │ │ ├── resendValidation.js │ │ │ ├── roomValidation.js │ │ │ ├── feedEntryValidation.js │ │ │ ├── loginValidation.js │ │ │ ├── userValidation.js │ │ │ ├── appValidation.js │ │ │ ├── registerValidation.js │ │ │ ├── commentValidation.js │ │ │ ├── createStudentValidation.js │ │ │ └── studentValidation.js │ │ ├── validator.js │ │ └── index.js │ ├── utils │ │ ├── createRouter.js │ │ └── addRouterPath.js │ ├── register.js │ ├── _auth.js │ ├── validateEmail.js │ ├── index.js │ ├── apps.js │ ├── feeds.js │ ├── rooms.js │ ├── user.js │ └── students.js ├── server.js └── mail │ ├── strategies │ ├── SendGridStrategy.js │ └── Strategy.js │ └── views │ ├── invite.mjml │ └── welcome.mjml ├── public └── images │ ├── logo-color.png │ ├── logo-white.png │ └── pexels-matilda-wormwood-4099325-edit.jpg ├── seed ├── seeds │ ├── index.js │ ├── seedUser.js │ └── seedClassroom.js ├── reset.js ├── data │ └── users.js └── index.js ├── test ├── README.md ├── lib │ ├── TestModel.js │ └── useDescribe.js ├── suite │ ├── controllers │ │ ├── types │ │ │ ├── Controller │ │ │ │ ├── test.js │ │ │ │ └── _suite │ │ │ │ │ └── constructor.js │ │ │ └── ControllerSchema │ │ │ │ ├── _utils │ │ │ │ └── createMakeCtrl.js │ │ │ │ ├── test.js │ │ │ │ └── _suite │ │ │ │ ├── makeDoc.js │ │ │ │ ├── constructor.js │ │ │ │ └── deleteOne.js │ │ └── auth.test.js │ └── graphql │ │ └── middleware │ │ ├── test.js │ │ └── _suite │ │ └── authentication.requireVerifiedUser.js └── run.js ├── migrate └── index.js ├── .github └── workflows │ └── test.yml ├── updatepassword.js ├── README.md ├── registercode.js └── package.json /client/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src" 4 | } 5 | } -------------------------------------------------------------------------------- /app/config/apps/library.js: -------------------------------------------------------------------------------- 1 | const appTypeLibrary = new Map(); 2 | 3 | module.exports = appTypeLibrary; -------------------------------------------------------------------------------- /app/controllers/types/library.js: -------------------------------------------------------------------------------- 1 | const instanceLibrary = new Map(); 2 | 3 | module.exports = instanceLibrary; -------------------------------------------------------------------------------- /public/images/logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/public/images/logo-color.png -------------------------------------------------------------------------------- /public/images/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/public/images/logo-white.png -------------------------------------------------------------------------------- /client/public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/favicon.ico -------------------------------------------------------------------------------- /client/public/favicon/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/apple-icon.png -------------------------------------------------------------------------------- /seed/seeds/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | seedUser: require("./seedUser"), 3 | seedClassroom: require("./seedClassroom") 4 | } -------------------------------------------------------------------------------- /client/public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /client/public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /client/public/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /client/public/favicon/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/ms-icon-70x70.png -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/inputs/index.js: -------------------------------------------------------------------------------- 1 | const { Credentials } = require("./Credentials"); 2 | 3 | module.exports = { 4 | Credentials 5 | } -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/scalars/index.js: -------------------------------------------------------------------------------- 1 | const { JSONObject } = require("./JSONObject"); 2 | 3 | module.exports = { 4 | JSONObject 5 | } -------------------------------------------------------------------------------- /client/public/favicon/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/apple-icon-57x57.png -------------------------------------------------------------------------------- /client/public/favicon/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/apple-icon-60x60.png -------------------------------------------------------------------------------- /client/public/favicon/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/apple-icon-72x72.png -------------------------------------------------------------------------------- /client/public/favicon/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/apple-icon-76x76.png -------------------------------------------------------------------------------- /client/public/favicon/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/ms-icon-144x144.png -------------------------------------------------------------------------------- /client/public/favicon/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/ms-icon-150x150.png -------------------------------------------------------------------------------- /client/public/favicon/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/ms-icon-310x310.png -------------------------------------------------------------------------------- /app/routes/middleware/isAuthenticated.js: -------------------------------------------------------------------------------- 1 | const passport = require("passport"); 2 | 3 | module.exports = passport.authenticate('jwt', { session: false }); -------------------------------------------------------------------------------- /client/public/favicon/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/android-icon-36x36.png -------------------------------------------------------------------------------- /client/public/favicon/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/android-icon-48x48.png -------------------------------------------------------------------------------- /client/public/favicon/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/android-icon-72x72.png -------------------------------------------------------------------------------- /client/public/favicon/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/android-icon-96x96.png -------------------------------------------------------------------------------- /client/public/favicon/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/apple-icon-114x114.png -------------------------------------------------------------------------------- /client/public/favicon/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/apple-icon-120x120.png -------------------------------------------------------------------------------- /client/public/favicon/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/apple-icon-144x144.png -------------------------------------------------------------------------------- /client/public/favicon/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/apple-icon-152x152.png -------------------------------------------------------------------------------- /client/public/favicon/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/apple-icon-180x180.png -------------------------------------------------------------------------------- /client/public/favicon/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/android-icon-144x144.png -------------------------------------------------------------------------------- /client/public/favicon/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/android-icon-192x192.png -------------------------------------------------------------------------------- /client/public/favicon/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/client/public/favicon/apple-icon-precomposed.png -------------------------------------------------------------------------------- /client/src/_env.sass: -------------------------------------------------------------------------------- 1 | $primary: #067CAC 2 | $link: #067CAC 3 | 4 | $strong-color: inherit 5 | 6 | // $link: #FCAA67 7 | @import "~bulma/sass/utilities/_all"; -------------------------------------------------------------------------------- /app/graphql/modules/utils/index.js: -------------------------------------------------------------------------------- 1 | const { createControllerModule } = require( './createControllerModule' ); 2 | 3 | module.exports = { 4 | createControllerModule 5 | } -------------------------------------------------------------------------------- /public/images/pexels-matilda-wormwood-4099325-edit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac524/instructor-utilities/HEAD/public/images/pexels-matilda-wormwood-4099325-edit.jpg -------------------------------------------------------------------------------- /client/src/components/Form/components/index.js: -------------------------------------------------------------------------------- 1 | export { FormField } from "./FormField" 2 | export { FormInput } from "./FormInput" 3 | export { RangeInput } from "./RangeInput" -------------------------------------------------------------------------------- /app/routes/middleware/initCrData.js: -------------------------------------------------------------------------------- 1 | const initCrData = ( req, res, next ) => { 2 | 3 | req.crdata = new Map(); 4 | next(); 5 | 6 | } 7 | 8 | module.exports = initCrData; -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/scalars/JSONObject.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('graphql-modules'); 2 | 3 | const JSONObject = gql`scalar JSONObject`; 4 | 5 | exports.JSONObject = JSONObject; -------------------------------------------------------------------------------- /app/controllers/definitions/models/Feed.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const FeedSchema = require("./schema/FeedSchema"); 4 | 5 | module.exports = mongoose.model( "Feed", FeedSchema ); -------------------------------------------------------------------------------- /client/src/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | export { Modal } from "./components/Modal"; 2 | export { ModalButton } from "./components/ModalButton"; 3 | export { ModalLink } from "./components/ModalLink"; 4 | -------------------------------------------------------------------------------- /client/src/pages/Dashboard/utils/staff.js: -------------------------------------------------------------------------------- 1 | export const getStaffOptionsList = staff => [ { value: "", label: "Unassigned" }, ...staff.map(({ _id, user: { name } }) => ({ value: _id, label: name })) ]; 2 | -------------------------------------------------------------------------------- /app/controllers/definitions/models/Token.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const TokenSchema = require("./schema/TokenSchema"); 4 | 5 | module.exports = mongoose.model( "Token", TokenSchema ); -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Working with Unit Tests Guide 2 | 3 | ## Organization / Structure 4 | 5 | TBD 6 | 7 | ## Tips 8 | 9 | * **How to run select unit tests:** https://jaketrent.com/post/run-single-mocha-test -------------------------------------------------------------------------------- /app/controllers/definitions/models/Room.js: -------------------------------------------------------------------------------- 1 | const mongoose = require( "mongoose" ); 2 | 3 | const RoomSchema = require( "./schema/RoomSchema" ); 4 | 5 | module.exports = mongoose.model( "Classroom", RoomSchema ); -------------------------------------------------------------------------------- /client/src/components/Dropdown/style.sass: -------------------------------------------------------------------------------- 1 | .button.dropdown-item 2 | display: flex 3 | border: none 4 | border-radius: 0 5 | padding: 0.375rem 1rem 6 | justify-content: left 7 | height: auto -------------------------------------------------------------------------------- /client/src/components/WebLink.js: -------------------------------------------------------------------------------- 1 | 2 | const WebLink = ( { children, ...props } ) => { 3 | return {children} 4 | } 5 | 6 | export default WebLink; -------------------------------------------------------------------------------- /app/controllers/definitions/models/App.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | // Create Schema 4 | const AppSchema = require("./schema/AppSchema"); 5 | 6 | module.exports = mongoose.model("App", AppSchema); -------------------------------------------------------------------------------- /app/controllers/utils/ioEmit.js: -------------------------------------------------------------------------------- 1 | const { io } = require("../../config/express"); 2 | 3 | const ioEmit = ( action, message, room ) => (room ? io.to(room) : io).emit( action, message ); 4 | 5 | module.exports = ioEmit; -------------------------------------------------------------------------------- /app/routes/middleware/setDefaultError.js: -------------------------------------------------------------------------------- 1 | const setDefaultError = ( message ) => ( req, res, next ) => { 2 | 3 | req.defaultError = message; 4 | 5 | next(); 6 | 7 | } 8 | 9 | module.exports = setDefaultError; -------------------------------------------------------------------------------- /client/src/components/Modal/store/actions.js: -------------------------------------------------------------------------------- 1 | export const REGISTER_MODAL = "REGISTER_MODAL"; 2 | export const SET_ACTIVE_MODAL = "SET_ACTIVE_MODAL"; 3 | export const DEREGISTER_MODAL = "DEREGISTER_MODAL"; 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/StudentCard/style.sass: -------------------------------------------------------------------------------- 1 | .student-card 2 | > .tags, 3 | > .tags .tag 4 | margin-bottom: 0 5 | border-radius: 0 6 | 7 | .student-name 8 | cursor: pointer -------------------------------------------------------------------------------- /app/routes/middleware/isVerified.js: -------------------------------------------------------------------------------- 1 | module.exports = async ( req, res, next ) => { 2 | 3 | if( !req.user.isVerified ) return res.status(401).json({ default: "Email is unverified." }); 4 | 5 | next(); 6 | 7 | } -------------------------------------------------------------------------------- /client/src/animations/Fade/style.sass: -------------------------------------------------------------------------------- 1 | @keyframes fadeIn 2 | 0% 3 | opacity: 0 4 | 5 | 100% 6 | opacity: 1 7 | 8 | @keyframes fadeOut 9 | 0% 10 | opacity: 1 11 | 12 | 100% 13 | opacity: 0 -------------------------------------------------------------------------------- /app/controllers/definitions/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | // Create Schema 4 | const UserSchema = require("./schema/UserSchema"); 5 | 6 | module.exports = mongoose.model( "User", UserSchema ); -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/index.js: -------------------------------------------------------------------------------- 1 | const types = require('./types'); 2 | const scalars = require('./scalars'); 3 | const inputs = require('./inputs'); 4 | 5 | module.exports = { 6 | scalars, 7 | types, 8 | inputs 9 | } -------------------------------------------------------------------------------- /app/controllers/definitions/models/AppType.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | // Create Schema 4 | const AppTypeSchema = require("./schema/AppTypeSchema"); 5 | 6 | module.exports = mongoose.model( "AppType", AppTypeSchema ); -------------------------------------------------------------------------------- /app/graphql/middleware/index.js: -------------------------------------------------------------------------------- 1 | const { useAuthentication } = require('./authentication'); 2 | const { useRoomMemberPermissions } = require('./permissions'); 3 | 4 | module.exports = { 5 | useAuthentication, 6 | useRoomMemberPermissions 7 | } -------------------------------------------------------------------------------- /app/graphql/modules/index.js: -------------------------------------------------------------------------------- 1 | const authModule = require("./auth"); 2 | const roomModule = require("./room"); 3 | const studentModule = require("./student"); 4 | 5 | module.exports = [ 6 | authModule, 7 | roomModule, 8 | studentModule 9 | ]; -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/StudentModal/panels/index.js: -------------------------------------------------------------------------------- 1 | export { ActivityPanel } from "./ActivityPanel"; 2 | export { StudentPanel } from "./StudentPanel"; 3 | export { PanelTabs } from "./PanelTabs"; 4 | export { usePanels } from "./utils"; -------------------------------------------------------------------------------- /app/controllers/definitions/models/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | User: require("./User"), 3 | Room: require("./Room"), 4 | Feed: require("./Feed"), 5 | Token: require("./Token"), 6 | App: require("./App"), 7 | AppType: require("./AppType") 8 | } -------------------------------------------------------------------------------- /app/graphql/context/db.js: -------------------------------------------------------------------------------- 1 | const db = require("../../controllers"); 2 | 3 | /** 4 | * @typedef DbContext 5 | * @property {Map} db; 6 | * 7 | * @returns {DbContext} 8 | */ 9 | const loadDb = () => ({ db }); 10 | 11 | module.exports = loadDb; -------------------------------------------------------------------------------- /client/src/layouts/components/Footer/style.sass: -------------------------------------------------------------------------------- 1 | .layout-footer 2 | .title 3 | color: #FFF 4 | padding-bottom: .5em 5 | border-bottom: 1px solid #FFF 6 | 7 | a 8 | color: #DDD 9 | 10 | a:hover 11 | color: #FFF -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/types/Auth.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('graphql-modules'); 2 | 3 | const Auth = gql` 4 | type Auth { 5 | token: String 6 | success: Boolean 7 | user: UserDocument 8 | } 9 | `; 10 | 11 | exports.Auth = Auth; -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/StudentModal/components/index.js: -------------------------------------------------------------------------------- 1 | export { StudentOptions } from "./StudentOptions"; 2 | export { SettingsForm } from "./SettingsForm"; 3 | export { ModalBox } from "./ModalBox"; 4 | export { ActivtyFeed, CommentForm } from "./ActivityFeed"; -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/inputs/Credentials.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('graphql-modules'); 2 | 3 | const Credentials = gql` 4 | input Credentials { 5 | email: String 6 | password: String 7 | } 8 | `; 9 | 10 | exports.Credentials = Credentials; -------------------------------------------------------------------------------- /app/config/utils/passwordHash.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require("bcryptjs"); 2 | const util = require('util'); 3 | 4 | const genSalt = util.promisify( bcrypt.genSalt ); 5 | const hash = util.promisify( bcrypt.hash ); 6 | 7 | module.exports = async ( raw ) => hash( raw, await genSalt( 10 ) ); -------------------------------------------------------------------------------- /test/lib/TestModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const { model, Schema } = mongoose; 3 | 4 | const TestModel = model( "Test", new Schema({ 5 | name: { 6 | type: String, 7 | required: true 8 | } 9 | }) ); 10 | 11 | module.exports = TestModel; -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/StudentModal/components/ActivityFeed/entries/index.js: -------------------------------------------------------------------------------- 1 | export { default as Created } from "./Created"; 2 | export { default as Comment } from "./Comment"; 3 | export { default as Elevate } from "./Elevate"; 4 | export { default as Deelevate } from "./Deelevate"; -------------------------------------------------------------------------------- /app/routes/middleware/setAppSearch.js: -------------------------------------------------------------------------------- 1 | const setAppSearch = async ( req, res, next ) => { 2 | 3 | req.crdata.set( 'search', { 4 | room: req.params.roomId, 5 | type: req.params.appTypeId 6 | } ); 7 | 8 | next(); 9 | 10 | } 11 | 12 | module.exports = setAppSearch; -------------------------------------------------------------------------------- /client/src/layouts/MainWithLogin.js: -------------------------------------------------------------------------------- 1 | import Main from "./Main"; 2 | import { useLoginModal } from "components/Login"; 3 | 4 | const MainWithLogin = ({ children }) => { 5 | 6 | useLoginModal(); 7 | 8 | return
{children}
9 | 10 | } 11 | 12 | export default MainWithLogin; -------------------------------------------------------------------------------- /seed/seeds/seedUser.js: -------------------------------------------------------------------------------- 1 | const { User } = require("../../app/controllers/definitions/models"); 2 | 3 | module.exports = async () => { 4 | await User.deleteMany({}); 5 | 6 | const seedData = await require("../data/users")(); 7 | 8 | return await User.collection.insertMany(seedData); 9 | }; 10 | -------------------------------------------------------------------------------- /app/config/permissions/sets/FeedSet.js: -------------------------------------------------------------------------------- 1 | const PermissionSet = require("../PermissionSet"); 2 | 3 | class FeedSet extends PermissionSet { 4 | 5 | constructor() { 6 | 7 | super( "feed", [ 8 | "view" 9 | ] ); 10 | 11 | } 12 | 13 | } 14 | 15 | module.exports = FeedSet; -------------------------------------------------------------------------------- /app/controllers/definitions/models/schema/FeedSchema/methods.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pushItem: function ( by, action, data ) { 3 | 4 | this.items.push({ 5 | by, 6 | action, 7 | data 8 | }); 9 | 10 | return this; 11 | 12 | } 13 | }; -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/Topbar/style.sass: -------------------------------------------------------------------------------- 1 | .topbar 2 | display: flex 3 | height: 56px 4 | border-bottom: 1px solid #E9EBF0 5 | 6 | .item 7 | padding: 7px .75rem 8 | line-height: 42px 9 | font-size: 20px 10 | 11 | .title 12 | font-size: 24px -------------------------------------------------------------------------------- /client/src/pages/Dashboard/views/Classroom/components/Widget/style.sass: -------------------------------------------------------------------------------- 1 | .app-widget 2 | &.is-fullscreen 3 | position: fixed 4 | z-index: 60 5 | top: 0 6 | left: 0 7 | width: 100vw 8 | height: 100vh 9 | border-radius: 0 10 | overflow-y: scroll -------------------------------------------------------------------------------- /app/routes/middleware/isFeedEntryOwner.js: -------------------------------------------------------------------------------- 1 | const isFeedEntryOwner = (req,res,next) => { 2 | 3 | if( !req.crdata.get("feedItem").by.equals(req.user._id) ) 4 | 5 | throw new InvalidUserError( "You do not own this entry." ); 6 | 7 | next(); 8 | 9 | } 10 | 11 | module.exports = isFeedEntryOwner; -------------------------------------------------------------------------------- /test/lib/useDescribe.js: -------------------------------------------------------------------------------- 1 | const useDescribe = context => ( description, content ) => { 2 | return process.argv[2] === description 3 | 4 | ? describe.only(description, content.bind(context)) 5 | 6 | : describe(description, content.bind(context)); 7 | }; 8 | 9 | module.exports = useDescribe; -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/types/FeedEntryComment.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('graphql-modules'); 2 | 3 | const FeedEntryComment = gql` 4 | type FeedEntryComment { 5 | feedId: ID 6 | comment: JSONObject 7 | } 8 | `; 9 | 10 | exports.FeedEntryComment = FeedEntryComment; 11 | 12 | -------------------------------------------------------------------------------- /client/src/pages/Home/index.js: -------------------------------------------------------------------------------- 1 | 2 | import MainHero from "./components/MainHero"; 3 | import MainWithLogin from "layouts/MainWithLogin"; 4 | 5 | const Home = () => { 6 | return ( 7 | 8 | 9 | 10 | ) 11 | } 12 | 13 | export default Home; -------------------------------------------------------------------------------- /client/src/pages/Dashboard/style.sass: -------------------------------------------------------------------------------- 1 | .dashboard-panel 2 | display: flex 3 | flex-direction: column 4 | min-height: 100vh 5 | 6 | .card.is-small 7 | 8 | .card-content 9 | padding: .75em 1em 10 | 11 | .card.button 12 | height: auto 13 | padding: 0 14 | border-radius: 0 15 | border: none -------------------------------------------------------------------------------- /app/config/permissions/roles/index.js: -------------------------------------------------------------------------------- 1 | const instructorRole = require("./instructorRole"); 2 | const taRole = require("./taRole"); 3 | 4 | const roles = new Map(); 5 | 6 | const addRole = roleType => roles.set( roleType.key, roleType ); 7 | 8 | addRole( instructorRole ); 9 | addRole( taRole ); 10 | 11 | module.exports = roles; -------------------------------------------------------------------------------- /client/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /app/config/permissions/sets/FeedElevateSet.js: -------------------------------------------------------------------------------- 1 | const PermissionSet = require("../PermissionSet"); 2 | 3 | class FeedElevateSet extends PermissionSet { 4 | 5 | constructor() { 6 | 7 | super( "feed elevate", [ 8 | "create" 9 | ] ); 10 | 11 | } 12 | 13 | } 14 | 15 | module.exports = FeedElevateSet; -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/types/AppTypeDocument.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('graphql-modules'); 2 | 3 | const AppTypeDocument = gql` 4 | type AppTypeDocument { 5 | id: ID 6 | isDisabled: Boolean 7 | name: String 8 | type: String 9 | } 10 | `; 11 | 12 | exports.AppTypeDocument = AppTypeDocument; -------------------------------------------------------------------------------- /app/config/permissions/sets/FeedDelevateSet.js: -------------------------------------------------------------------------------- 1 | const PermissionSet = require("../PermissionSet"); 2 | 3 | class FeedDeelevateSet extends PermissionSet { 4 | 5 | constructor() { 6 | 7 | super( "feed deelevate", [ 8 | "create" 9 | ] ); 10 | 11 | } 12 | 13 | } 14 | 15 | module.exports = FeedDeelevateSet; -------------------------------------------------------------------------------- /client/src/components/UserName.js: -------------------------------------------------------------------------------- 1 | import { useAuthorizedUser } from "utils/auth"; 2 | 3 | const UserName = ({ user: {_id, name }, ...props }) => { 4 | 5 | const { _id: currentUserId } = useAuthorizedUser(); 6 | 7 | return { _id === currentUserId ? "You" : name }; 8 | 9 | } 10 | 11 | export default UserName; -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/types/UserDocument.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('graphql-modules'); 2 | 3 | const UserDocument = gql` 4 | type UserDocument { 5 | _id: ID 6 | date: String 7 | email: String 8 | name: String 9 | classrooms: [ID] 10 | } 11 | `; 12 | 13 | exports.UserDocument = UserDocument; -------------------------------------------------------------------------------- /app/routes/validation/definitions/inviteValidation.js: -------------------------------------------------------------------------------- 1 | const ValidationSchema = require("../ValidationSchema"); 2 | 3 | /** 4 | * @typedef {Object} InviteData 5 | * @property {string} email 6 | */ 7 | const inviteValidation = new ValidationSchema("invite", { 8 | email: { type: "email" } 9 | }); 10 | 11 | module.exports = inviteValidation; -------------------------------------------------------------------------------- /app/routes/validation/definitions/resendValidation.js: -------------------------------------------------------------------------------- 1 | const ValidationSchema = require("../ValidationSchema"); 2 | 3 | /** 4 | * @typedef {Object} ResendData 5 | * @property {string} email 6 | */ 7 | const resendValidation = new ValidationSchema("resend", { 8 | email: { type: "email" } 9 | }); 10 | 11 | module.exports = resendValidation; -------------------------------------------------------------------------------- /client/src/components/Icon.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | Icon as BulmaIcon 4 | } from "react-bulma-components"; 5 | 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | 8 | const Icon = ({ icon, ...props }) => { 9 | return 10 | } 11 | 12 | export default Icon; -------------------------------------------------------------------------------- /client/src/layouts/Main.js: -------------------------------------------------------------------------------- 1 | import TopNavbar from "./components/TopNavbar" 2 | import Footer from "./components/Footer"; 3 | 4 | const Main = ({ children }) => { 5 | return ( 6 |
7 | 8 | {children} 9 |
11 | ) 12 | } 13 | 14 | export default Main; -------------------------------------------------------------------------------- /app/controllers/utils/searchCtrls.js: -------------------------------------------------------------------------------- 1 | const instanceLibrary = require("../types/library") 2 | 3 | 4 | /** 5 | * @param {string} path 6 | * @returns {Map} 7 | */ 8 | const searchCtrls = ( path ) => new Map( [...instanceLibrary.entries()].filter(([key]) => key.substr( 0, path.length ) === path ) ); 9 | 10 | 11 | module.exports = searchCtrls; -------------------------------------------------------------------------------- /app/routes/validation/definitions/roomValidation.js: -------------------------------------------------------------------------------- 1 | const ValidationSchema = require("../ValidationSchema"); 2 | 3 | /** 4 | * @typedef {Object} RoomData 5 | * @property {string} name 6 | */ 7 | const roomValidation = new ValidationSchema("room", { 8 | name: { type: "string", empty: false } 9 | }); 10 | 11 | module.exports = roomValidation; -------------------------------------------------------------------------------- /app/routes/validation/definitions/feedEntryValidation.js: -------------------------------------------------------------------------------- 1 | const ValidationSchema = require("../ValidationSchema"); 2 | 3 | /** 4 | * @typedef {Object} CommentData 5 | * @property {string} comment 6 | */ 7 | const commentValidation = new ValidationSchema("feedEntry", { 8 | feedId: { type: "objectID" } 9 | }); 10 | 11 | module.exports = commentValidation; -------------------------------------------------------------------------------- /app/config/permissions/sets/InviteSet.js: -------------------------------------------------------------------------------- 1 | const PermissionSet = require("../PermissionSet"); 2 | 3 | class InviteSet extends PermissionSet { 4 | 5 | constructor() { 6 | 7 | super( "invite", [ 8 | "create", 9 | "view", 10 | "delete" 11 | ] ); 12 | 13 | } 14 | 15 | } 16 | 17 | module.exports = InviteSet; -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/StudentModal/components/ActivityFeed/components/FeedEntry.js: -------------------------------------------------------------------------------- 1 | 2 | const FeedEntry = ({ children, block }) => { 3 | 4 | const classes = ["feed-entry"]; 5 | 6 | if( block ) classes.push("is-block-entry"); 7 | 8 | return
{children}
; 9 | 10 | } 11 | 12 | export default FeedEntry; -------------------------------------------------------------------------------- /client/src/utils/githubApi.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const baseUrl = "https://api.github.com" 4 | 5 | export default { 6 | getUser( user ) { 7 | return axios.get( `${baseUrl}/users/${user}` ); 8 | }, 9 | getRepoContributors( owner, repo ) { 10 | return axios.get( `${baseUrl}/repos/${owner}/${repo}/contributors` ); 11 | } 12 | } -------------------------------------------------------------------------------- /app/config/permissions/sets/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | FeedCommentSet: require("./FeedCommentSet"), 3 | FeedDeelevateSet: require("./FeedDelevateSet"), 4 | FeedElevateSet: require("./FeedElevateSet"), 5 | FeedSet: require("./FeedSet"), 6 | InviteSet: require("./InviteSet"), 7 | RoomSet: require("./RoomSet"), 8 | StudentSet: require("./StudentSet") 9 | } -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/types/StaffDocument.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('graphql-modules'); 2 | 3 | const StaffDocument = gql` 4 | type StaffDocument { 5 | _id: ID 6 | date: String 7 | meta: JSONObject 8 | role: String 9 | user: UserDocument 10 | } 11 | `; 12 | 13 | exports.StaffDocument = StaffDocument; 14 | 15 | -------------------------------------------------------------------------------- /app/routes/utils/createRouter.js: -------------------------------------------------------------------------------- 1 | const { Router } = require("express"); 2 | 3 | const addRouterPath = require("./addRouterPath"); 4 | 5 | const createRouter = routes => { 6 | 7 | const router = new Router(); 8 | 9 | for( routeConfig of routes ) addRouterPath( router, ...routeConfig ); 10 | 11 | return router; 12 | 13 | } 14 | 15 | module.exports = createRouter; -------------------------------------------------------------------------------- /test/suite/controllers/types/Controller/test.js: -------------------------------------------------------------------------------- 1 | const useDescribe = require("~crsmtest/lib/useDescribe"); 2 | 3 | const classConstructor = require("./_suite/constructor"); 4 | 5 | const describe = useDescribe(this); 6 | 7 | describe("Controller", () => { 8 | 9 | const describe = useDescribe(this); 10 | 11 | describe( "constructor()", classConstructor ); 12 | 13 | }); -------------------------------------------------------------------------------- /client/src/pages/Dashboard/utils/apps.js: -------------------------------------------------------------------------------- 1 | import { useDashboardDispatch, getDashboardAction as gda } from "pages/Dashboard/store" 2 | import { SET_MANAGE_APPS } from "pages/Dashboard/store/actionsNames"; 3 | 4 | export const useManageApps = () => { 5 | 6 | const dispatch = useDashboardDispatch(); 7 | 8 | return ( state ) => dispatch( gda( SET_MANAGE_APPS, state ) ); 9 | 10 | } -------------------------------------------------------------------------------- /app/config/permissions/sets/FeedCommentSet.js: -------------------------------------------------------------------------------- 1 | const PermissionSet = require("../PermissionSet"); 2 | 3 | class FeedCommentSet extends PermissionSet { 4 | 5 | constructor() { 6 | 7 | super( "feed comment", [ 8 | "create", 9 | "update", 10 | "delete" 11 | ] ); 12 | 13 | } 14 | 15 | } 16 | 17 | module.exports = FeedCommentSet; -------------------------------------------------------------------------------- /app/config/permissions/sets/StudentSet.js: -------------------------------------------------------------------------------- 1 | const PermissionSet = require("../PermissionSet"); 2 | 3 | class StudentSet extends PermissionSet { 4 | 5 | constructor() { 6 | 7 | super( "student", [ 8 | "create", 9 | "view", 10 | "update", 11 | "delete" 12 | ] ); 13 | 14 | } 15 | 16 | } 17 | 18 | module.exports = StudentSet; -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/types/FeedEntryDocument.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('graphql-modules'); 2 | 3 | const FeedEntryDocument = gql` 4 | type FeedEntryDocument { 5 | _id: ID 6 | action: String 7 | by: String 8 | data: FeedEntryComment 9 | date: String 10 | } 11 | `; 12 | 13 | exports.FeedEntryDocument = FeedEntryDocument; 14 | 15 | -------------------------------------------------------------------------------- /app/routes/utils/addRouterPath.js: -------------------------------------------------------------------------------- 1 | const addRequest = require("./addRequest"); 2 | 3 | const addRouterPath = ( router, path, requestTypes, sharedConfig = {} ) => { 4 | 5 | const route = router.route( path ); 6 | 7 | Object.entries( requestTypes ).forEach( ([type, config]) => addRequest( route, type, { ...sharedConfig, ...config } ) ); 8 | 9 | }; 10 | 11 | module.exports = addRouterPath; -------------------------------------------------------------------------------- /client/src/components/LoadingOverlay.js: -------------------------------------------------------------------------------- 1 | import Pulse from "./Pulse"; 2 | 3 | const LoadingOverlay = () => { 4 | 5 | return ( 6 |
7 | 8 |
9 | ) 10 | 11 | } 12 | 13 | export default LoadingOverlay; -------------------------------------------------------------------------------- /app/graphql/index.js: -------------------------------------------------------------------------------- 1 | // Import external modules 2 | const { createApplication } = require('graphql-modules'); 3 | 4 | // Import local modules 5 | const modules = require("./modules"); 6 | const context = require("./context"); 7 | 8 | const schema = 9 | createApplication({ 10 | modules 11 | }) 12 | .createSchemaForApollo(); 13 | 14 | module.exports = { 15 | schema, 16 | context 17 | } -------------------------------------------------------------------------------- /app/config/mongoose.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const mongoDbUri = require("./options")( "mongodb" ); 3 | 4 | mongoose 5 | .connect( 6 | mongoDbUri, 7 | { 8 | useNewUrlParser: true, 9 | useUnifiedTopology: true, 10 | useFindAndModify: false 11 | } 12 | ) 13 | .then(() => console.log("MongoDB successfully connected")) 14 | .catch(err => console.log(err)); -------------------------------------------------------------------------------- /app/routes/middleware/globalParamsValidation.js: -------------------------------------------------------------------------------- 1 | const validateParamsHandler = require("./validateParamsHandler"); 2 | 3 | const globalParamsValidation = validateParamsHandler({ 4 | roomId: "objectID|optional", 5 | inviteId: "objectID|optional", 6 | feedId: "objectID|optional", 7 | studentId: "objectID|optional", 8 | appTypeId: "objectID|optional" 9 | }); 10 | 11 | module.exports = globalParamsValidation; -------------------------------------------------------------------------------- /app/routes/validation/definitions/loginValidation.js: -------------------------------------------------------------------------------- 1 | const ValidationSchema = require("../ValidationSchema"); 2 | 3 | /** 4 | * @typedef {Object} LoginData 5 | * @property {string} email 6 | * @property {string} password 7 | */ 8 | const loginValidation = new ValidationSchema("login", { 9 | email: { type: "email" }, 10 | password: { type: "string", empty: false } 11 | }); 12 | 13 | module.exports = loginValidation; -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/types/FeedEntryCommentDocument.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('graphql-modules'); 2 | 3 | const FeedEntryCommentDocument = gql` 4 | type FeedEntryCommentDocument { 5 | _id: ID 6 | action: String 7 | by: String 8 | data: FeedEntryComment 9 | date: String 10 | } 11 | `; 12 | 13 | exports.FeedEntryCommentDocument = FeedEntryCommentDocument; 14 | 15 | -------------------------------------------------------------------------------- /app/routes/middleware/createValidationMiddleware.js: -------------------------------------------------------------------------------- 1 | const createValidationMiddleware = ( validations, message = "Invalid submission data." ) => ( req, res, next ) => { 2 | 3 | const errors = new Map(); 4 | 5 | for( validation of validations ) validation( errors, req ); 6 | 7 | errors.size 8 | 9 | ? next( new InvalidDataError( message, Object.fromEntries([ ...errors ]) ) ) 10 | 11 | : next(); 12 | 13 | } -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/RoomLink.js: -------------------------------------------------------------------------------- 1 | import { useClassroom } from "pages/Dashboard/store"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const RoomLink = ( { to, ...props } ) => { 5 | 6 | const classroom = useClassroom(); 7 | 8 | return classroom 9 | 10 | ? 11 | 12 | : ; 13 | 14 | } 15 | 16 | export default RoomLink; -------------------------------------------------------------------------------- /app/config/jwtstrategy.js: -------------------------------------------------------------------------------- 1 | const JwtStrategy = require("passport-jwt").Strategy; 2 | const ExtractJwt = require("passport-jwt").ExtractJwt; 3 | const validateJwtPayload = require("./utils/validateJwtPayload"); 4 | const secret = require("./options")( "secret" ); 5 | 6 | const opts = {}; 7 | 8 | opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken(); 9 | opts.secretOrKey = secret; 10 | 11 | module.exports = new JwtStrategy(opts, validateJwtPayload); -------------------------------------------------------------------------------- /app/graphql/context/index.js: -------------------------------------------------------------------------------- 1 | const authorization = require('./authentication'); 2 | const db = require('./db'); 3 | 4 | const createContext = ( { req } ) => { 5 | 6 | return [ 7 | db, 8 | authorization 9 | ].reduce(( context, partial ) => ({ 10 | ...context, 11 | // Extend context with the partial 12 | ...( partial( req, context ) || {} ) 13 | }), {}); 14 | 15 | } 16 | 17 | module.exports = createContext; -------------------------------------------------------------------------------- /seed/reset.js: -------------------------------------------------------------------------------- 1 | require("../app/config/mongoose"); 2 | 3 | const db = require("../controllers/models"); 4 | 5 | const resetDb = async () => { 6 | 7 | try { 8 | 9 | const models = Object.values( db ); 10 | 11 | for( let i = 0; i < models.length; i++ ) 12 | 13 | await models[i].deleteMany({}); 14 | 15 | process.exit(0); 16 | 17 | } catch(err) { 18 | 19 | process.exit(1); 20 | 21 | } 22 | 23 | } 24 | 25 | resetDb(); -------------------------------------------------------------------------------- /app/controllers/definitions/models/schema/MemberSchema/methods.js: -------------------------------------------------------------------------------- 1 | const roles = require("../../../../../config/permissions/roles"); 2 | 3 | module.exports = { 4 | 5 | isAllowedTo: function( permission ) { 6 | 7 | if( !roles.has( this.role ) ) return false; 8 | 9 | return roles.get( this.role ).can( permission ); 10 | 11 | }, 12 | 13 | getPermissionList: function() { 14 | 15 | return roles.get( this.role ).list; 16 | 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/routes/validation/definitions/userValidation.js: -------------------------------------------------------------------------------- 1 | const ValidationSchema = require("../ValidationSchema"); 2 | 3 | /** 4 | * @typedef {Object} UserData 5 | * @property {string} name 6 | * @property {string} email 7 | * @property {string} password 8 | */ 9 | const userValidation = new ValidationSchema("user", { 10 | name: { type: "string", min: 3 }, 11 | email: "email", 12 | password: { type: "string", min: 6, max: 30 } 13 | }); 14 | 15 | module.exports = userValidation; -------------------------------------------------------------------------------- /app/config/errors/handleRouteError.js: -------------------------------------------------------------------------------- 1 | const isProd = require("../options")("isProd"); 2 | 3 | /** 4 | * @param {RouteError} err 5 | * @param {*} res 6 | */ 7 | const handleRouteError = (err, res) => { 8 | 9 | // Log for non production environments. 10 | // if( !isProd ) 11 | console.log( err.sourceErr || err ); 12 | 13 | const { statusCode, response } = err; 14 | 15 | res.status( statusCode ).json( response ); 16 | 17 | }; 18 | 19 | module.exports = handleRouteError; -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/types/RoomDocument.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('graphql-modules'); 2 | 3 | const RoomDocument = gql` 4 | type Invite { 5 | id: ID 6 | } 7 | 8 | type RoomDocument { 9 | _id: ID 10 | apps: [ AppTypeDocument ] 11 | date: String 12 | invites: [ Invite ] 13 | name: String 14 | staff: [ StaffDocument ] 15 | students: [ StudentDocument ] 16 | } 17 | `; 18 | 19 | exports.RoomDocument = RoomDocument; -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/types/StudentDocument.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('graphql-modules'); 2 | 3 | const StudentDocument = gql` 4 | type StudentDocument { 5 | _id: ID 6 | assignedTo: String 7 | date: String 8 | elevation: Int 9 | feed: String 10 | meta: JSONObject 11 | name: String 12 | priorityLevel: Int 13 | recentComments: [ FeedEntryCommentDocument ] 14 | } 15 | `; 16 | 17 | exports.StudentDocument = StudentDocument; -------------------------------------------------------------------------------- /client/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import Home from "./Home"; 2 | import Register from "./Register"; 3 | import Developers from "./Developers"; 4 | import Privacy from "./Privacy"; 5 | import Dashboard from "./Dashboard"; 6 | import NotFound from "./NotFound"; 7 | import ValidateEmail from "./ValidateEmail"; 8 | import Invite from "./Invite"; 9 | 10 | export default { 11 | Home, 12 | Register, 13 | Developers, 14 | Privacy, 15 | Dashboard, 16 | NotFound, 17 | ValidateEmail, 18 | Invite 19 | } -------------------------------------------------------------------------------- /app/config/utils/validateJwtPayload.js: -------------------------------------------------------------------------------- 1 | const { User } = require("../../controllers/definitions/models"); 2 | 3 | module.exports = (jwtPayload, done) => { 4 | 5 | User.findById(jwtPayload.id) 6 | .select("name email isVerified classrooms date") 7 | .then(user => { 8 | if (user) { 9 | return done(null, user); 10 | } 11 | return done(null, false); 12 | }) 13 | .catch(err => { 14 | console.log(err) 15 | done(null, false); 16 | }); 17 | 18 | } -------------------------------------------------------------------------------- /app/routes/validation/definitions/appValidation.js: -------------------------------------------------------------------------------- 1 | const ValidationSchema = require("../ValidationSchema"); 2 | 3 | /** 4 | * TYPE DEFINITION IMPORTS 5 | * @typedef {import('mongoose').Schema.Types.ObjectId} ObjectId 6 | */ 7 | 8 | /** 9 | * @typedef {Object} AppData 10 | * @property {type} type 11 | * @property {ObjectId} roomId 12 | */ 13 | const appValidation = new ValidationSchema("app", { 14 | type: { type: "string", empty: false }, 15 | room: { type: "objectID" } 16 | }); 17 | 18 | module.exports = appValidation; -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/StudentModal/panels/PanelTabs.js: -------------------------------------------------------------------------------- 1 | import { Tabs } from "react-bulma-components"; 2 | 3 | const { Tab } = Tabs; 4 | 5 | export const PanelTabs = ({ activePanel, setPanel, panels }) => { 6 | return ( 7 | 8 | {[...panels].map(([key, {label}]) => ( 9 | setPanel(key)} 12 | className={activePanel === key ? "is-active" : ""}> 13 | {label} 14 | 15 | ))} 16 | 17 | ); 18 | }; -------------------------------------------------------------------------------- /app/config/permissions/sets/RoomSet.js: -------------------------------------------------------------------------------- 1 | const PermissionSet = require("../PermissionSet"); 2 | 3 | class RoomSet extends PermissionSet { 4 | 5 | constructor() { 6 | 7 | super( "room", [ 8 | "view", 9 | "update", 10 | "leave", 11 | "archive" 12 | ] ); 13 | 14 | } 15 | 16 | get leave() { 17 | return this.makeKey( "leave" ); 18 | } 19 | 20 | get archive() { 21 | return this.makeKey( "archive" ); 22 | } 23 | 24 | } 25 | 26 | module.exports = RoomSet; -------------------------------------------------------------------------------- /app/routes/validation/validator.js: -------------------------------------------------------------------------------- 1 | const Validator = require("fastest-validator"); 2 | const { ObjectID } = require("mongodb"); 3 | 4 | const v = new Validator({ 5 | defaults: { 6 | objectID: { 7 | ObjectID 8 | } 9 | } 10 | }); 11 | 12 | const compile = schema => v.compile(schema); 13 | 14 | const mapErrors = errors => errors.reduce( (errors, {field, message}) => ({ ...errors, [field]: message }), {} ); 15 | 16 | const validator = { 17 | compile, 18 | mapErrors 19 | }; 20 | 21 | module.exports = validator; -------------------------------------------------------------------------------- /test/suite/controllers/types/ControllerSchema/_utils/createMakeCtrl.js: -------------------------------------------------------------------------------- 1 | const SchemaController = require("~crsm/controllers/types/SchemaController"); 2 | 3 | const TestModel = require("~crsmtest/lib/TestModel"); 4 | 5 | const createMakeCtrl = sandbox => 6 | /** 7 | * @returns {SchemaController} 8 | */ 9 | () => { 10 | 11 | const ctrl = new SchemaController( "modelkey", TestModel ); 12 | 13 | sandbox.stub( ctrl, "query" ); 14 | 15 | return ctrl; 16 | 17 | } 18 | 19 | module.exports = createMakeCtrl; -------------------------------------------------------------------------------- /app/config/apps/registry.json: -------------------------------------------------------------------------------- 1 | { 2 | "studentselect": { 3 | "name": "Select Student", 4 | "default": { 5 | "selected": [], 6 | "disabled": [] 7 | } 8 | }, 9 | "githubProfiles": { 10 | "name": "Github Profiles", 11 | "fields": { 12 | "student": [ 13 | { 14 | "label": "Github Username", 15 | "name": "githubUser", 16 | "type": "text" 17 | } 18 | ] 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/controllers/definitions/AppTypeController.js: -------------------------------------------------------------------------------- 1 | const { AppType } = require("./models"); 2 | 3 | const SchemaController = require("../types/SchemaController"); 4 | 5 | /** 6 | * TYPE DEFINITIONS FOR METHODS 7 | */ 8 | 9 | class AppTypeController extends SchemaController { 10 | 11 | constructor() { 12 | 13 | super( 'appType', AppType ); 14 | 15 | } 16 | 17 | async getEnabled() { 18 | 19 | return this.findMany( { search: { isDisabled: false } } ); 20 | 21 | } 22 | 23 | } 24 | 25 | module.exports = AppTypeController; -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/RequirePerm.js: -------------------------------------------------------------------------------- 1 | import { useContext, useState, createContext } from "react"; 2 | import { usePermissions } from "../store"; 3 | 4 | const makeKey = ( item, action ) => `${action}_${item}`.replace(" ", "_").toUpperCase(); 5 | 6 | const RequirePerm = ({ item, action, component: Component, children }) => { 7 | 8 | const permissions = usePermissions(); 9 | 10 | if( !permissions.has( makeKey( item, action ) ) ) return null; 11 | 12 | return Component ? :
{children}
; 13 | 14 | } 15 | 16 | export default RequirePerm; -------------------------------------------------------------------------------- /app/controllers/definitions/models/schema/InviteSchema.js: -------------------------------------------------------------------------------- 1 | const Schema = require("mongoose").Schema; 2 | const { ObjectId } = Schema.Types; 3 | 4 | /** 5 | * @typedef {Object} InvitesSchema 6 | * @property {string} email 7 | * @property {ObjectId} token 8 | * 9 | * @typedef {import('mongoose').Document & InvitesSchema} InviteDocument 10 | */ 11 | const InviteSchema = new Schema({ 12 | email: { 13 | type: String, 14 | required: true 15 | }, 16 | token: { 17 | type: ObjectId, 18 | ref:"Token", 19 | } 20 | }); 21 | 22 | module.exports = InviteSchema; -------------------------------------------------------------------------------- /app/routes/register.js: -------------------------------------------------------------------------------- 1 | const createRouter = require("./utils/createRouter"); 2 | 3 | const { register: registerVal } = require("./validation"); 4 | 5 | const ctrls = require("../controllers"); 6 | 7 | 8 | const registerCtlrConfig = { 9 | keyMap: { body: "registerData" } 10 | }; 11 | 12 | module.exports = createRouter([ 13 | 14 | ["/", { 15 | post: { 16 | defaultError: "complete the registration", 17 | validation: registerVal, 18 | ctrl: [ ctrls.get("register").binding.register, registerCtlrConfig ] 19 | } 20 | }] 21 | 22 | ]); -------------------------------------------------------------------------------- /client/src/components/Modal/components/ModalButton.js: -------------------------------------------------------------------------------- 1 | import { Button } from "react-bulma-components"; 2 | import { useOpenModal } from "components/Modal/utils"; 3 | /** 4 | * Open modal button component. Requires as an ancenstor. 5 | * @param {object} props 6 | */ 7 | export const ModalButton = ({ children, modalKey, ...props }) => { 8 | // Consume the login context to fetch the live state. 9 | const openModal = useOpenModal(modalKey); 10 | 11 | return ( 12 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/StudentModal/components/ActivityFeed/entries/Created.js: -------------------------------------------------------------------------------- 1 | 2 | import Date from "components/Date"; 3 | import UserName from "components/UserName"; 4 | 5 | import FeedEntry from "../components/FeedEntry"; 6 | 7 | const Created = ( { by, date } ) => { 8 | 9 | return ( 10 | 11 | 12 | added this student 13 | 14 | 15 | 16 | ); 17 | 18 | } 19 | 20 | export default Created; -------------------------------------------------------------------------------- /app/routes/middleware/createCheckPermission.js: -------------------------------------------------------------------------------- 1 | const { InvalidDataError } = require("../../config/errors"); 2 | 3 | const createCheckPermission = permission => ( req, res, next ) => { 4 | 5 | const member = req.crdata.get( "member" ); 6 | 7 | if( ! member ) 8 | 9 | return next( new InvalidDataError( "Expected a member to validate permissions, but got none." ) ); 10 | 11 | if( ! member.isAllowedTo( permission ) ) 12 | 13 | return next( new InvalidDataError( `You are not allowed to ${permission}.` ) ); 14 | 15 | next(); 16 | 17 | } 18 | 19 | module.exports = createCheckPermission; -------------------------------------------------------------------------------- /app/routes/middleware/validateParamsHandler.js: -------------------------------------------------------------------------------- 1 | const { InvalidDataError } = require("../../config/errors"); 2 | const { compile, mapErrors } = require(".././validation/validator"); 3 | 4 | const validateParamsHandler = schema => { 5 | 6 | const check = compile(schema); 7 | 8 | return ( { params }, res, next ) => { 9 | 10 | if( !params ) return; 11 | 12 | const result = check( params ); 13 | 14 | if( true === result ) return next(); 15 | 16 | next( new InvalidDataError( `Invalid url`, mapErrors(result) ) ); 17 | 18 | } 19 | 20 | } 21 | 22 | module.exports = validateParamsHandler; -------------------------------------------------------------------------------- /client/src/store/actions.js: -------------------------------------------------------------------------------- 1 | export const SET_SOCKET = "SET_SOCKET"; 2 | 3 | export const LOGIN_USER = "LOGIN_USER"; 4 | export const LOGOUT_USER = "LOGOUT_USER"; 5 | 6 | export const UPDATE_USER = "UPDATE_USER"; 7 | export const REFRESH_USER_ROOMS = "REFRESH_USER_ROOMS"; 8 | export const ADD_USER_ROOM_ID = "ADD_USER_ROOM_ID"; 9 | export const REMOVE_USER_ROOM_ID = "REMOVE_USER_ROOM_ID"; 10 | 11 | export const ADD_READY_STEP = "ADD_READY_STEP"; 12 | export const REMOVE_READY_STEP = "REMOVE_READY_STEP"; 13 | export const COMPLETE_READY_STEP = "COMPLETE_READY_STEP"; 14 | export const UNCOMPLETE_READY_STEP = "UNCOMPLETE_READY_STEP"; -------------------------------------------------------------------------------- /client/src/components/Pulse/index.js: -------------------------------------------------------------------------------- 1 | import "./style.sass"; 2 | 3 | /** 4 | * @param {*} param0 5 | * @see https://tobiasahlin.com/spinkit/ 6 | */ 7 | const Pulse = ( { className, color, size } ) => { 8 | 9 | const style = {}; 10 | 11 | if( size ) style["--pulse-size"] = size; 12 | if( color ) style["--pulse-color"] = color; 13 | 14 | return ( 15 |
16 |
17 |
18 |
19 | ) 20 | } 21 | 22 | export default Pulse; -------------------------------------------------------------------------------- /test/suite/graphql/middleware/test.js: -------------------------------------------------------------------------------- 1 | const useDescribe = require("~crsmtest/lib/useDescribe"); 2 | 3 | const setAuthTokenUser = require("./_suite/authentication.setAuthTokenUser"); 4 | const requireVerifiedUser = require("./_suite/authentication.requireVerifiedUser"); 5 | 6 | const describe = useDescribe(this); 7 | 8 | describe("/middleware", function() { 9 | 10 | const describe = useDescribe(this); 11 | 12 | describe( 'authentication', () => { 13 | 14 | describe( 'setAuthTokenUser', setAuthTokenUser ); 15 | 16 | describe( 'requireVerifiedUser', requireVerifiedUser ); 17 | 18 | } ); 19 | 20 | }); -------------------------------------------------------------------------------- /app/routes/middleware/isRoomMember.js: -------------------------------------------------------------------------------- 1 | const { InvalidUserError } = require("../../config/errors"); 2 | 3 | const isRoomMember = async ( req, res, next ) => { 4 | 5 | try { 6 | 7 | const { staff } = req.crdata.get("room"); 8 | const staffMember = staff.find( member => member.user.equals(req.user._id) ); 9 | 10 | if( !staffMember ) throw new InvalidUserError( "You are not a member of this class" ); 11 | 12 | req.crdata.set( "member", staffMember ); 13 | 14 | next(); 15 | 16 | } catch( err ) { 17 | 18 | next( err ); 19 | 20 | } 21 | 22 | } 23 | 24 | module.exports = isRoomMember; -------------------------------------------------------------------------------- /app/routes/validation/definitions/registerValidation.js: -------------------------------------------------------------------------------- 1 | const userValidation = require("./userValidation"); 2 | const ValidationSchema = require("../ValidationSchema"); 3 | 4 | /** 5 | * @typedef {Object} RegistrationData 6 | * @property {string} name 7 | * @property {string} email 8 | * @property {string} password 9 | * @property {string} roomname 10 | * @property {string} code 11 | */ 12 | const registerValidation = new ValidationSchema("registration", { 13 | ...userValidation.schema, 14 | roomname: { type: "string", min: 3 }, 15 | code: { type: "string", empty: false } 16 | }); 17 | 18 | module.exports = registerValidation; -------------------------------------------------------------------------------- /client/src/components/Modal/components/ModalLink.js: -------------------------------------------------------------------------------- 1 | import { useOpenModal } from "components/Modal/utils"; 2 | 3 | /** 4 | * Open modal link component. Requires as an ancenstor. 5 | * @param {object} props 6 | */ 7 | export const ModalLink = ({ children, onClick, modalKey, ...props }) => { 8 | // Consume the login context to fetch the live state 9 | 10 | const openModal = useOpenModal(modalKey); 11 | 12 | const onLinkClick = () => { 13 | onClick ? onClick(openModal) : openModal(); 14 | }; 15 | 16 | return ( 17 | 18 | {children || "Launch Modal"} 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /seed/data/users.js: -------------------------------------------------------------------------------- 1 | const passwordHash = require("../../app/config/utils/passwordHash"); 2 | 3 | module.exports = async () => [ 4 | { 5 | name: "Anthony Brown", 6 | email: "anthony@classroomadmin.com", 7 | password: await passwordHash("test123"), 8 | isVerified: true 9 | }, 10 | { 11 | name: "Tom Lam", 12 | email: "tom@classroomadmin.com", 13 | password: await passwordHash("test123"), 14 | isVerified: true 15 | }, 16 | { 17 | name: "Spencer Hirata", 18 | email: "spencer@classroomadmin.com", 19 | password: await passwordHash("test123"), 20 | isVerified: true 21 | } 22 | ]; -------------------------------------------------------------------------------- /app/routes/validation/definitions/commentValidation.js: -------------------------------------------------------------------------------- 1 | const ValidationSchema = require("../ValidationSchema"); 2 | const feedEntryValidation = require("./feedEntryValidation"); 3 | 4 | /** 5 | * @typedef {Object} CommentData 6 | * @property {string} comment 7 | */ 8 | const commentValidation = new ValidationSchema("comment", { 9 | ...feedEntryValidation.schema, 10 | comment: [ 11 | { type: "string", empty: false }, 12 | { 13 | type: "object", 14 | props: { 15 | blocks: "array", 16 | entityMap: "object" 17 | } 18 | }, 19 | ] 20 | }); 21 | 22 | module.exports = commentValidation; -------------------------------------------------------------------------------- /seed/seeds/seedClassroom.js: -------------------------------------------------------------------------------- 1 | const { 2 | Feed, 3 | Room, 4 | User, 5 | } = require("../../app/controllers/definitions/models"); 6 | 7 | module.exports = async () => { 8 | await Room.deleteMany({}); 9 | await Feed.deleteMany({}); 10 | 11 | const users = await User.find({}); 12 | 13 | const seedData = await require("../data/classrooms")(users); 14 | 15 | const results = await Room.collection.insertMany(seedData); 16 | 17 | const classroomId = results.insertedIds["0"]; 18 | 19 | for (let i = 0; i < users.length; i++) 20 | // Push the classroom id to the user 21 | await users[i].update({ $push: { classrooms: classroomId } }); 22 | 23 | return results; 24 | }; 25 | -------------------------------------------------------------------------------- /client/src/utils/windowWidth.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { useState, useEffect } from "react"; 4 | 5 | export const getWindowDimensions = () => { 6 | const { innerWidth: width } = window; 7 | return { 8 | width 9 | }; 10 | }; 11 | 12 | export const useWindowDimensions = () => { 13 | const [windowDimensions, setWindowDimensions] = useState( 14 | getWindowDimensions() 15 | ); 16 | 17 | useEffect(() => { 18 | const handleResize = () => { 19 | setWindowDimensions(getWindowDimensions()); 20 | }; 21 | 22 | window.addEventListener("resize", handleResize); 23 | return () => window.removeEventListener("resize", handleResize); 24 | }, []); 25 | 26 | return windowDimensions; 27 | }; 28 | -------------------------------------------------------------------------------- /client/src/utils/icons.js: -------------------------------------------------------------------------------- 1 | import { library } from '@fortawesome/fontawesome-svg-core'; 2 | 3 | import { 4 | faAngleDown, 5 | faPaperPlane, 6 | faCheck, 7 | faBan 8 | } from '@fortawesome/free-solid-svg-icons'; 9 | 10 | import { 11 | faArrowAltCircleLeft, 12 | faArrowAltCircleRight 13 | } from '@fortawesome/free-regular-svg-icons'; 14 | 15 | import { 16 | faGithub 17 | } from '@fortawesome/free-brands-svg-icons'; 18 | 19 | const loadGlobalIcons = () => library.add( 20 | faArrowAltCircleLeft, 21 | faArrowAltCircleRight, 22 | faAngleDown, 23 | faPaperPlane, 24 | faGithub, 25 | faCheck, 26 | faBan 27 | ); 28 | 29 | export default loadGlobalIcons ; -------------------------------------------------------------------------------- /app/routes/validation/definitions/createStudentValidation.js: -------------------------------------------------------------------------------- 1 | const ValidationSchema = require("../ValidationSchema"); 2 | const studentValidation = require("./studentValidation"); 3 | 4 | /** 5 | * TYPE DEFINITION IMPORTS 6 | * @typedef {import('mongoose').Schema.Types.ObjectId} ObjectId 7 | */ 8 | 9 | /** 10 | * @typedef {Object} StudentData 11 | * @property {ObjectId} belongsTo 12 | * @property {string} name 13 | * @property {number} priorityLevel 14 | * @property {ObjectId} assignedTo 15 | */ 16 | const createStudentValidation = new ValidationSchema("student", { 17 | ...studentValidation.schema, 18 | roomId: { type: "objectID" } 19 | }); 20 | 21 | module.exports = createStudentValidation; -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/Views.js: -------------------------------------------------------------------------------- 1 | import { Switch, Route } from "react-router-dom"; 2 | import Classroom from "pages/Dashboard/views/Classroom"; 3 | import Students from "pages/Dashboard/views/Students"; 4 | import Team from "pages/Dashboard/views/Team"; 5 | import UserSettings from "pages/Dashboard/views/UserSettings"; 6 | 7 | const Views = () => { 8 | 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | } 18 | 19 | export default Views; -------------------------------------------------------------------------------- /app/routes/_auth.js: -------------------------------------------------------------------------------- 1 | const createRouter = require("./utils/createRouter"); 2 | 3 | const { login: loginVal } = require("./validation") 4 | 5 | const ctrls = require("../controllers"); 6 | 7 | const loginCtlrConfig = { 8 | keyMap: { body: "credentials" } 9 | }; 10 | 11 | module.exports = createRouter([ 12 | [ 13 | "/login", 14 | { 15 | post: { 16 | defaultError: "login", 17 | validation: loginVal, 18 | ctrl: [ctrls.get("auth").binding.login, loginCtlrConfig] 19 | } 20 | } 21 | ], 22 | 23 | [ 24 | "/authenticated", 25 | { 26 | post: { 27 | defaultError: "get the authenticated user", 28 | auth: true, 29 | ctrl: ctrls.get("auth").binding.authenticated 30 | } 31 | } 32 | ] 33 | ]); -------------------------------------------------------------------------------- /app/routes/validation/definitions/studentValidation.js: -------------------------------------------------------------------------------- 1 | const ValidationSchema = require("../ValidationSchema"); 2 | 3 | /** 4 | * TYPE DEFINITION IMPORTS 5 | * @typedef {import('mongoose').Schema.Types.ObjectId} ObjectId 6 | */ 7 | 8 | /** 9 | * @typedef {Object} StudentData 10 | * @property {string} name 11 | * @property {number} priorityLevel 12 | * @property {ObjectId} assignedTo 13 | */ 14 | const studentValidation = new ValidationSchema("student", { 15 | name: { type: "string", empty: false }, 16 | priorityLevel: { type: "number", min: 1, max: 10 }, 17 | assignedTo: { type: "objectID", nullable: true }, 18 | meta: { type: "object", optional: true } 19 | }); 20 | 21 | module.exports = studentValidation; -------------------------------------------------------------------------------- /app/routes/validateEmail.js: -------------------------------------------------------------------------------- 1 | const createRouter = require("./utils/createRouter"); 2 | 3 | const { resend: resendVal } = require("./validation"); 4 | 5 | const ctrls = require("../controllers"); 6 | 7 | module.exports = createRouter([ 8 | 9 | ["/resend", { 10 | post: { 11 | auth: true, 12 | unverified: true, 13 | defaultError: "resend the email", 14 | validation: resendVal, 15 | ctrl: ctrls.get("validate.email").binding.resend 16 | } 17 | }], 18 | 19 | ["/:tokenString", { 20 | post: { 21 | defaultError: "validate the email", 22 | ctrl: ctrls.get("validate.email").binding.validate 23 | } 24 | }] 25 | 26 | ]); -------------------------------------------------------------------------------- /app/config/permissions/roles/taRole.js: -------------------------------------------------------------------------------- 1 | const Role = require("../Role"); 2 | 3 | const perms = require("../"); 4 | 5 | const taRole = new Role( "ta", "TA", [ 6 | 7 | /** ROOM **/ 8 | perms.room.view, 9 | perms.room.leave, 10 | 11 | /** STUDENT **/ 12 | perms.student.view, 13 | perms.student.update, 14 | 15 | /** FEED PERMISSIONS **/ 16 | perms.feed.view, 17 | 18 | /** FEED COMMENT PERMISSIONS **/ 19 | perms.feedComment.create, 20 | perms.feedComment.update, 21 | perms.feedComment.delete, 22 | 23 | /** FEED ELEVATE PERMISSIONS **/ 24 | perms.feedElevate.create, 25 | 26 | /** FEED DEELEVATE PERMISSIONS **/ 27 | perms.feedDeelevate.create 28 | 29 | ] ); 30 | 31 | module.exports = taRole; -------------------------------------------------------------------------------- /client/src/pages/Dashboard/views/UserSettings/components/NoClassroomNotification.js: -------------------------------------------------------------------------------- 1 | import { 2 | Message 3 | } from "react-bulma-components"; 4 | 5 | import { useAuthorizedUser } from "utils/auth"; 6 | 7 | const NoClassroomNotification = () => { 8 | 9 | const user = useAuthorizedUser(); 10 | 11 | return ( 12 | 13 | 14 | Hey there, {user.name}! Looks like you don't belong to any classrooms at the moment. This means you'll only have access to your settings until you either create a new classroom or accept an invite as a TA to a new room. 15 | 16 | 17 | ); 18 | 19 | } 20 | 21 | export default NoClassroomNotification; -------------------------------------------------------------------------------- /app/controllers/definitions/models/schema/FeedEntrySchema.js: -------------------------------------------------------------------------------- 1 | const Schema = require("mongoose").Schema; 2 | const { ObjectId, Mixed } = Schema.Types; 3 | 4 | /** 5 | * @typedef {Object} FeedEntrySchema 6 | * @property {string} action 7 | * @property {ObjectId} token 8 | * @property {(Object|null)} data 9 | * 10 | * @typedef {import('mongoose').Document & FeedEntrySchema} FeedEntryDocument 11 | */ 12 | const FeedEntrySchema = new Schema({ 13 | action: { 14 | type: String, 15 | required: true 16 | }, 17 | by: { 18 | type: ObjectId, 19 | ref: "User", 20 | required: true 21 | }, 22 | data: { 23 | type: Mixed 24 | }, 25 | date: { 26 | type: Date, 27 | default: Date.now 28 | } 29 | }); 30 | 31 | module.exports = FeedEntrySchema; -------------------------------------------------------------------------------- /client/src/pages/Dashboard/views/Team/components/StaffListControls.js: -------------------------------------------------------------------------------- 1 | import { useClassroom } from "pages/Dashboard/store"; 2 | import { InviteModalButton, useInviteModal } from "./InviteModal"; 3 | import { PendingInvitesModalButton, usePendingInvitesModal } from "./PendingInvitesModal"; 4 | 5 | const StaffListControls = () => { 6 | const room = useClassroom(); 7 | 8 | usePendingInvitesModal(); 9 | 10 | useInviteModal(); 11 | 12 | return ( 13 |
14 |
15 | 16 | { room.invites.length ? : null } 17 |
18 |
19 | ); 20 | 21 | } 22 | 23 | export default StaffListControls; -------------------------------------------------------------------------------- /client/src/components/Form/components/RangeInput.js: -------------------------------------------------------------------------------- 1 | export const RangeInput = ({ 2 | id, 3 | name, 4 | value, 5 | color, 6 | light, 7 | size, 8 | ...props 9 | }) => { 10 | if (!id) id = `slider-${name}`; 11 | 12 | const classes = ["slider has-output is-fullwidth m-0"]; 13 | 14 | if (color) classes.push(`is-${color}`); 15 | if (light) classes.push("is-light"); 16 | if (size) classes.push(`is-${size}`); 17 | 18 | return ( 19 |
20 | 28 | {value} 29 |
30 | ); 31 | }; -------------------------------------------------------------------------------- /app/routes/validation/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./definitions/userValidation').UserData} UserData 3 | */ 4 | 5 | module.exports = { 6 | app: require("./definitions/appValidation"), 7 | feedEntry: require("./definitions/feedEntryValidation"), 8 | comment: require("./definitions/commentValidation"), 9 | invite: require("./definitions/inviteValidation"), 10 | login: require("./definitions/loginValidation"), 11 | register: require("./definitions/registerValidation"), 12 | resend: require("./definitions/resendValidation"), 13 | room: require("./definitions/roomValidation"), 14 | student: require("./definitions/studentValidation"), 15 | createStudent: require("./definitions/createStudentValidation"), 16 | user: require("./definitions/userValidation") 17 | } -------------------------------------------------------------------------------- /app/graphql/modules/typedefs/types/index.js: -------------------------------------------------------------------------------- 1 | const { RoomDocument } = require("./RoomDocument"); 2 | const { AppTypeDocument } = require("./AppTypeDocument"); 3 | const { StaffDocument } = require("./StaffDocument"); 4 | const { StudentDocument } = require("./StudentDocument"); 5 | const { UserDocument } = require("./UserDocument"); 6 | const { FeedEntryDocument } = require("./FeedEntryDocument"); 7 | const { FeedEntryCommentDocument } = require("./FeedEntryCommentDocument"); 8 | const { FeedEntryComment } = require("./FeedEntryComment"); 9 | const { Auth } = require("./Auth"); 10 | 11 | module.exports = { 12 | RoomDocument, 13 | AppTypeDocument, 14 | StaffDocument, 15 | StudentDocument, 16 | UserDocument, 17 | FeedEntryDocument, 18 | FeedEntryCommentDocument, 19 | FeedEntryComment, 20 | Auth 21 | } -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/StudentModal/components/ActivityFeed/entries/Elevate.js: -------------------------------------------------------------------------------- 1 | import Icon from "components/Icon"; 2 | import { 3 | Tag 4 | } from "react-bulma-components"; 5 | 6 | import Date from "components/Date"; 7 | import UserName from "components/UserName"; 8 | 9 | import FeedEntry from "../components/FeedEntry"; 10 | 11 | const Elevate = ( { by, data, date } ) => { 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | elevated this student 20 | 21 | 22 | 23 | ); 24 | 25 | } 26 | 27 | export default Elevate; -------------------------------------------------------------------------------- /app/controllers/definitions/models/schema/TokenSchema.js: -------------------------------------------------------------------------------- 1 | const Schema = require("mongoose").Schema; 2 | const { ObjectId } = Schema.Types; 3 | 4 | /** 5 | * @typedef {Object} TokenSchema 6 | * @property {ObjectId} relation 7 | * @property {string} token 8 | * 9 | * @typedef {import('mongoose').Document & TokenSchema} TokenDocument 10 | */ 11 | 12 | const TokenSchema = new Schema({ 13 | relation: { 14 | type: ObjectId, 15 | required: true 16 | }, 17 | tokenString: { 18 | type: String, 19 | unique: true, 20 | index: true, 21 | required: true 22 | }, 23 | createdAt: { 24 | type: Date, 25 | required: true, 26 | default: Date.now, 27 | // Expire in 3 days 28 | expires: 259200 29 | } 30 | }); 31 | 32 | module.exports = TokenSchema; -------------------------------------------------------------------------------- /test/suite/controllers/auth.test.js: -------------------------------------------------------------------------------- 1 | // const chai = require("chai"); 2 | // const { authenticated, login } = require("~crsm/controllers/auth"); 3 | // const { User } = require("~crsm/controllers/definitions/models"); 4 | // const expect = chai.expect 5 | 6 | // describe("AuthController", function() { 7 | 8 | // describe("authenticated()", function() { 9 | 10 | // it("should return the User object it's passed", function(done) { 11 | 12 | // // Arrange - Configure needed data. 13 | // const user = new User ({name: "bob"}); 14 | 15 | // // Act - Peform the action to test. 16 | // const result = authenticated({ user }); 17 | 18 | // // Assert - Expect a result. 19 | // expect( result ).to.equal( user ); 20 | 21 | // return done(); 22 | 23 | // }); 24 | 25 | // }); 26 | 27 | // }); -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/StudentModal/components/ActivityFeed/entries/Deelevate.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | Tag 4 | } from "react-bulma-components"; 5 | 6 | import Icon from "components/Icon"; 7 | import Date from "components/Date"; 8 | import UserName from "components/UserName"; 9 | 10 | import FeedEntry from "../components/FeedEntry"; 11 | 12 | const Deelevate = ( { by, date } ) => { 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | de-elevated this student 21 | 22 | 23 | 24 | ); 25 | 26 | } 27 | 28 | export default Deelevate; -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/StudentModal/panels/utils.js: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from "react"; 2 | import { ActivityPanel } from "./ActivityPanel"; 3 | import { StudentPanel } from "./StudentPanel"; 4 | 5 | export const usePanels = (roomId, student) => { 6 | 7 | const panels = useMemo(() => new Map([ 8 | ["student", { 9 | label: "Edit Student", 10 | Panel: () => 11 | }], 12 | // Only add the actity panel if the student exists 13 | ...(student._id ? [["activity", { 14 | label: "Activity", 15 | Panel: () => 16 | }]] : []) 17 | ]), [roomId, student]); 18 | 19 | const [activePanel, setPanel] = useState(() => panels.keys().next().value); 20 | 21 | return { activePanel, setPanel, panels }; 22 | 23 | } -------------------------------------------------------------------------------- /app/server.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | // MongoDB 4 | require("./config/mongoose"); 5 | 6 | // Classroom app registry 7 | require("./config/apps/register")(); 8 | 9 | // Express server configuration 10 | const { 11 | addApolloServer, 12 | addDataParsing, 13 | addCompression, 14 | addAuth, 15 | addRoutes, 16 | listen 17 | } = require("./config/express"); 18 | 19 | const { schema, context } = require("./graphql"); 20 | 21 | const apiType = require("./config/options")('apiType') 22 | 23 | async function startExpressServer() { 24 | 25 | if( apiType === 'BOTH' || apiType === 'GRAPHQL' ) await addApolloServer(schema, context); 26 | 27 | addDataParsing(); 28 | 29 | addCompression(); 30 | 31 | addAuth(); 32 | 33 | addRoutes(); 34 | 35 | listen(); 36 | 37 | } 38 | 39 | startExpressServer(); -------------------------------------------------------------------------------- /client/src/components/Modal/store/index.js: -------------------------------------------------------------------------------- 1 | import { useContext, createContext, useReducer } from "react"; 2 | import reducer from "./reducer"; 3 | // Define a new context 4 | const ModalContext = createContext(false); 5 | 6 | // Deconstruct the provider for ease of use in JSX 7 | const { Provider } = ModalContext; 8 | 9 | export const useModalContext = () => { 10 | return useContext(ModalContext); 11 | }; 12 | 13 | /** 14 | * Modal provider component. 15 | * @param {object} props 16 | */ 17 | export const ModalProvider = ({ children, isActive = false }) => { 18 | // Create the reducer state. 19 | const [modalState, modalDispatch] = useReducer(reducer, { 20 | modals: {}, 21 | activeKey: false, 22 | }); 23 | 24 | //const modalState = useState( isActive ); 25 | 26 | return {children}; 27 | }; 28 | -------------------------------------------------------------------------------- /migrate/index.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | require("~crsm/config/mongoose"); 4 | 5 | /** 6 | * Simple migration for single purpose currently needed 7 | **/ 8 | const Room = require("~crsmmodels/Room"); 9 | 10 | const aggregateAllRooms = async () => { 11 | 12 | try { 13 | 14 | const rooms = await Room.find(); 15 | 16 | for( room of rooms ) await aggregateRoom( room ); 17 | 18 | } catch(err) { 19 | 20 | console.log(err); 21 | 22 | } 23 | 24 | 25 | process.exit(0); 26 | 27 | } 28 | 29 | const aggregateRoom = async room => { 30 | 31 | for( student of room.students ) { 32 | 33 | student._doc = { 34 | ...student._doc, 35 | ...await( student.getFeedAggregateData() ) 36 | } 37 | 38 | } 39 | 40 | await room.save(); 41 | 42 | } 43 | 44 | aggregateAllRooms(); -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Name of workflow 2 | name: Test workflow 3 | # Trigger workflow on all pull requests 4 | on: 5 | pull_request: 6 | branches: 7 | - staging 8 | - main 9 | # Jobs to carry out 10 | jobs: 11 | test: 12 | # Operating system to run job on 13 | runs-on: ubuntu-latest 14 | # Steps in job 15 | steps: 16 | # Get code from repo 17 | - name: Checkout code 18 | uses: actions/checkout@v1 19 | # Install NodeJS 20 | - name: Use Node.js 16.x 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 16.x 24 | # Build the app 25 | - name: 🧰 install dev deps 26 | run: npm i --only=dev 27 | - name: Run test 28 | run: npm run test -------------------------------------------------------------------------------- /app/mail/strategies/SendGridStrategy.js: -------------------------------------------------------------------------------- 1 | const sgMail = require("@sendgrid/mail"); 2 | const Strategy = require("./Strategy"); 3 | 4 | class SendGridStrategy extends Strategy { 5 | 6 | constructor( { sgApiKey, ...options } ) { 7 | 8 | super( options ); 9 | 10 | // No need to continue if base configuration failed. 11 | if( !this.isConfigured ) return; 12 | 13 | if( !sgApiKey ) { 14 | 15 | // Invalidate configuration and exit. 16 | this.isConfigured = false; 17 | return; 18 | 19 | } 20 | 21 | // Apply the key 22 | sgMail.setApiKey( sgApiKey ); 23 | 24 | } 25 | 26 | send( { from, ...options } ) { 27 | 28 | return sgMail.send({ 29 | from: this.from, 30 | ...options 31 | }); 32 | 33 | } 34 | 35 | } 36 | 37 | module.exports = SendGridStrategy; -------------------------------------------------------------------------------- /app/mail/strategies/Strategy.js: -------------------------------------------------------------------------------- 1 | class Strategy { 2 | 3 | isConfigured; 4 | 5 | from; 6 | 7 | constructor( { from } ) { 8 | 9 | this.from = from; 10 | 11 | this.checkIsConfigured([ "from" ]); 12 | 13 | } 14 | 15 | checkIsConfigured( props ) { 16 | 17 | // If validation has already failed, skip additional checks. 18 | if( false === this.isConfigured ) return false; 19 | 20 | for( let i=0; i < props.length; i++ ) { 21 | 22 | if( undefined === props[i] ) { 23 | this.isConfigured = false; 24 | return false; 25 | } 26 | 27 | } 28 | 29 | if( !this.isConfigured ) this.isConfigured = true; 30 | 31 | return true; 32 | 33 | } 34 | 35 | send() { 36 | return new Promise((resolve) => resolve(false)); 37 | } 38 | 39 | } 40 | 41 | module.exports = Strategy; -------------------------------------------------------------------------------- /client/src/pages/Dashboard/views/UserSettings/components/ClassroomModal/components/ClassroomModalContent.js: -------------------------------------------------------------------------------- 1 | import ClassroomForm from "./ClassroomForm.js"; 2 | 3 | import { Box, Heading } from "react-bulma-components"; 4 | import { useState } from "react"; 5 | import { useEffect } from "react/cjs/react.development"; 6 | import api from "utils/api.js"; 7 | import { useOpenModal } from "components/Modal/utils.js"; 8 | 9 | const ClassroomModalContent = ({ roomId }) => { 10 | 11 | const [room, setRoom] = useState(null); 12 | 13 | const closeModal = useOpenModal( false ); 14 | 15 | useEffect(async () => { 16 | setRoom( (await api.getClassroom(roomId)).data ); 17 | }, []); 18 | 19 | return ( 20 | 21 | Classroom 22 |
23 | 24 |
25 | ); 26 | }; 27 | 28 | export default ClassroomModalContent; 29 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import {StrictMode} from "react"; 2 | import ReactDOM from 'react-dom'; 3 | import { StoreProvider } from "./store"; 4 | import App from "./App"; 5 | import { BrowserRouter as Router } from "react-router-dom"; 6 | import * as serviceWorker from './utils/serviceWorker'; 7 | import { ModalProvider } from "components/Modal/store"; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | , 19 | document.getElementById('root') 20 | ); 21 | 22 | // If you want your app to work offline and load faster, you can change 23 | // unregister() to register() below. Note this comes with some pitfalls. 24 | // Learn more about service workers: https://bit.ly/CRA-PWA 25 | serviceWorker.register(); -------------------------------------------------------------------------------- /test/suite/controllers/types/Controller/_suite/constructor.js: -------------------------------------------------------------------------------- 1 | const util = require("util"); 2 | 3 | const { expect } = require("chai"); 4 | 5 | const Controller = require("~crsm/controllers/types/Controller"); 6 | 7 | module.exports = function() { 8 | 9 | it("should create a Controller object", function(done) { 10 | 11 | // Arrange and Act 12 | const controller = new Controller; 13 | 14 | // Asset 15 | expect( controller ).to.be.instanceof( Controller ); 16 | 17 | return done(); 18 | 19 | }); 20 | 21 | it("should create a `binding` property that is a proxy of the object", function(done) { 22 | 23 | // Arrange and Act 24 | const controller = new Controller; 25 | 26 | // Asset 27 | expect( util.types.isProxy( controller.binding ) ).to.equal( true ); 28 | expect( controller.binding ).to.be.instanceof( Controller ); 29 | 30 | return done(); 31 | 32 | }); 33 | 34 | } -------------------------------------------------------------------------------- /test/suite/controllers/types/ControllerSchema/test.js: -------------------------------------------------------------------------------- 1 | const useDescribe = require("~crsmtest/lib/useDescribe"); 2 | 3 | const classConstructor = require("./_suite/constructor"); 4 | const makeDoc = require("./_suite/makeDoc"); 5 | const createOne = require("./_suite/createOne"); 6 | const deleteOne = require("./_suite/deleteOne"); 7 | const findOne = require("./_suite/findOne"); 8 | const findMany = require("./_suite/findMany"); 9 | const updateOne = require("./_suite/updateOne"); 10 | 11 | 12 | const describe = useDescribe(this); 13 | 14 | describe("ControllerSchema", function() { 15 | 16 | const describe = useDescribe(this); 17 | 18 | describe( "constructor()", classConstructor ); 19 | describe( "makeDoc()", makeDoc ); 20 | describe( "createOne()", createOne ); 21 | describe( "deleteOne()", deleteOne ); 22 | describe( "findOne()", findOne ); 23 | describe( "findMany()", findMany); 24 | describe( "updateOne()", updateOne ); 25 | 26 | }); -------------------------------------------------------------------------------- /app/config/permissions/Role.js: -------------------------------------------------------------------------------- 1 | const permissions = require("./"); 2 | 3 | // console.log( 'reg permissions', permissions ); 4 | 5 | const makePermEntry = permission => [ permission, 1 ]; 6 | const permExists = permission => permissions.has(permission); 7 | 8 | class Role { 9 | 10 | /** 11 | * @param {string} key 12 | * @param {string} name 13 | * @param {array} permissions 14 | */ 15 | constructor( key, name, permissions ) { 16 | 17 | this.key = key; 18 | this.name = name; 19 | 20 | this.permissions = new Map( permissions.filter( permExists ).map( makePermEntry ) ); 21 | 22 | } 23 | 24 | get list() { 25 | 26 | return [...this.permissions].map( ([perm]) => perm ); 27 | 28 | } 29 | 30 | can( permission ) { 31 | 32 | // console.log( permission, this.permissions ); 33 | 34 | return this.permissions.has( permission ); 35 | 36 | } 37 | 38 | } 39 | 40 | module.exports = Role; -------------------------------------------------------------------------------- /app/graphql/modules/utils/README.md: -------------------------------------------------------------------------------- 1 | # Intended Usages and Examples 2 | 3 | ## createControllerModule 4 | 5 | ```js 6 | createControllerModule({ 7 | /** @see /app/controllers/definitions */ 8 | id: "controllerkeyname", 9 | // List of abilities to translate into mutations and resolvers 10 | abilities: [ 11 | // Array list of ability names (Ex: "view", "create", "update", "delete") 12 | ] 13 | // (optional) Configuration for validating room member permissions against given `abilities` 14 | memberPermission: { 15 | /** @see /app/graphql/middleware/setRoomContext */ 16 | context: "roomContextLoaderMethodName", 17 | /** @see /app/config/permissions/sets */ 18 | set: "permissionSetName" 19 | }, 20 | // (optional) 21 | middlewares: { 22 | ...customMiddlewares 23 | } 24 | // (optional) 25 | resolvers: { 26 | ...customResolvers 27 | } 28 | ...otherCreateModuleParams, 29 | }) 30 | ``` -------------------------------------------------------------------------------- /app/graphql/middleware/setMemberContext.js: -------------------------------------------------------------------------------- 1 | const { AuthenticationError } = require("apollo-server-express"); 2 | 3 | /** 4 | * @callback next 5 | * 6 | * @typedef {import('./authentication').AuthTokenUserContext & import('./setRoomContext').RoomContext} SetMemberContext 7 | * 8 | * @typedef MemberContext 9 | * @property {import('~crsmmodels/schema/MemberSchema/index').MemberDocument} member 10 | * 11 | * @param {Object} param0 12 | * @param {SetMemberContext} param0.context 13 | * @param {next} next 14 | * @returns {*} 15 | */ 16 | const setMemberContext = ({ 17 | context 18 | }, next) => { 19 | 20 | const { 21 | authUser: { _id }, 22 | room: { staff } 23 | } = context; 24 | 25 | context.member = staff.find( member => member.user.equals( _id ) ); 26 | 27 | if( !context.member ) throw AuthenticationError('User is not a member of the associated room.'); 28 | 29 | return next(); 30 | 31 | } 32 | 33 | module.exports = setMemberContext; -------------------------------------------------------------------------------- /app/config/permissions/roles/instructorRole.js: -------------------------------------------------------------------------------- 1 | const Role = require("../Role"); 2 | 3 | const perms = require("../"); 4 | 5 | const instructorRole = new Role( "instructor", "Instructor", [ 6 | 7 | /** ROOM **/ 8 | perms.room.view, 9 | perms.room.update, 10 | perms.room.archive, 11 | 12 | /** STUDENT **/ 13 | perms.student.create, 14 | perms.student.view, 15 | perms.student.update, 16 | perms.student.delete, 17 | 18 | /** INVITE **/ 19 | perms.invite.create, 20 | perms.invite.view, 21 | perms.invite.delete, 22 | 23 | /** FEED PERMISSIONS **/ 24 | perms.feed.view, 25 | 26 | /** FEED COMMENT PERMISSIONS **/ 27 | perms.feedComment.create, 28 | perms.feedComment.update, 29 | perms.feedComment.delete, 30 | 31 | /** FEED ELEVATE PERMISSIONS **/ 32 | perms.feedElevate.create, 33 | 34 | /** FEED DEELEVATE PERMISSIONS **/ 35 | perms.feedDeelevate.create 36 | 37 | ] ); 38 | 39 | module.exports = instructorRole; -------------------------------------------------------------------------------- /updatepassword.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | // MongoDB 4 | require("~crsm/config/mongoose"); 5 | 6 | const passwordHash = require("~crsm/config/utils/passwordHash"); 7 | 8 | const { User } = require("~crsmmodels"); 9 | 10 | const [ , , userId, password ] = process.argv; 11 | 12 | const updatepassword = async ( userId, password ) => { 13 | 14 | try { 15 | 16 | const user = await User.findByIdAndUpdate( userId, { 17 | password: await passwordHash( password ) 18 | }, { new: true } ); 19 | 20 | if( !user ) { 21 | 22 | console.log( `User id ${userId} not found` ); 23 | 24 | } else { 25 | 26 | console.log( "Updated user", user ); 27 | 28 | } 29 | 30 | process.exit(0); 31 | 32 | } catch(err) { 33 | 34 | console.log( err ); 35 | 36 | console.error(err); 37 | process.exit(1); 38 | 39 | } 40 | 41 | } 42 | 43 | updatepassword( userId, password ); -------------------------------------------------------------------------------- /app/controllers/definitions/models/schema/AppSchema.js: -------------------------------------------------------------------------------- 1 | const Schema = require("mongoose").Schema; 2 | const { ObjectId, Mixed } = Schema.Types; 3 | 4 | /** 5 | * @typedef {Object} AppSchema 6 | * @property {ObjectId} room 7 | * @property {ObjectId} type 8 | * @property {String} name 9 | * @property {Object} data 10 | * @property {Date} date 11 | * 12 | * @typedef {import('mongoose').Document & AppSchema} AppDocument 13 | */ 14 | const AppSchema = new Schema({ 15 | room: { 16 | type: ObjectId, 17 | ref:'Classroom', 18 | required: true 19 | }, 20 | type: { 21 | type: ObjectId, 22 | ref:'AppType', 23 | required: true 24 | }, 25 | name: { 26 | type: String, 27 | required: true 28 | }, 29 | data: { 30 | type: Mixed 31 | }, 32 | date: { 33 | type: Date, 34 | default: Date.now 35 | } 36 | }); 37 | 38 | AppSchema.index({ room: 1, type: 1 }, { unique: true }); 39 | 40 | module.exports = AppSchema; -------------------------------------------------------------------------------- /app/mail/views/invite.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | You've been invited to Classroom! 14 | 15 | 16 | [name] has invited you to join his classroom [roomName] as a TA! Please click the button below to accept the invitation and join the classroom. 17 | 18 | 19 | Accept Invitation 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/controllers/definitions/FeedController.js: -------------------------------------------------------------------------------- 1 | const SchemaController = require("../types/SchemaController"); 2 | 3 | const { Feed } = require("./models"); 4 | 5 | /** 6 | * TYPE DEFINITIONS FOR METHODS 7 | * 8 | * @typedef GetFeedItemsOptions 9 | * @property {ObjectId} feedId 10 | */ 11 | 12 | class FeedController extends SchemaController { 13 | 14 | constructor() { 15 | 16 | super( 'feed', Feed ); 17 | 18 | } 19 | 20 | /** 21 | * 22 | * @param {GetFeedItemsOptions} param0 23 | */ 24 | async getItems({ feedId }) { 25 | 26 | const feed = 27 | await this.findOne({ docId: feedId }, { 28 | populate: [ ["items.by","name"] ], 29 | select: "items" 30 | }); 31 | 32 | const items = []; 33 | 34 | for(let i=0; i < feed.items.length; i++) 35 | 36 | items.push( feed.items[i] ); 37 | 38 | return items; 39 | 40 | } 41 | 42 | } 43 | 44 | module.exports = FeedController; -------------------------------------------------------------------------------- /client/src/utils/detection.js: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from "react"; 2 | 3 | export const useOutsideClickDispatch = ( { isActive, dispatch, action } ) => { 4 | 5 | const inBoundsElementRef = useRef(); 6 | 7 | useEffect(() => { 8 | 9 | if( !isActive ) return; 10 | 11 | /** 12 | * Alert if clicked on outside of element 13 | */ 14 | const handleClickOutside = event => { 15 | if (inBoundsElementRef.current && !inBoundsElementRef.current.contains(event.target)) { 16 | dispatch( action ); 17 | } 18 | } 19 | 20 | // Bind the event listener 21 | document.addEventListener("mousedown", handleClickOutside); 22 | 23 | return () => { 24 | // Unbind the event listener on clean up 25 | document.removeEventListener("mousedown", handleClickOutside); 26 | }; 27 | 28 | }, [inBoundsElementRef, isActive, dispatch, action]); 29 | 30 | return inBoundsElementRef; 31 | 32 | } -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/StudentModal/components/ActivityFeed/style.sass: -------------------------------------------------------------------------------- 1 | $feed-border: 1px solid hsl(0,0,90%) 2 | $feed-border-light: 1px solid hsl(0,0,94%) 3 | 4 | .feed 5 | font-size: .95rem 6 | 7 | .feed-entry 8 | display: flex 9 | padding: .75rem 0 10 | 11 | .date 12 | text-align: right 13 | margin-bottom: .75rem 14 | 15 | + .feed-entry:not(.is-block-entry) 16 | border-top: $feed-border-light 17 | 18 | &.is-block-entry 19 | + .feed-entry 20 | border-top: none 21 | 22 | .fill 23 | flex-grow: 1 24 | 25 | .start 26 | width: 40px 27 | margin-right: .75rem 28 | 29 | .end 30 | margin-left: auto 31 | 32 | .box 33 | color: inherit 34 | border: $feed-border 35 | box-shadow: none 36 | width: 80% 37 | 38 | p 39 | width: 100% 40 | word-wrap: break-word 41 | .date 42 | text-align: right 43 | margin-bottom: .75rem -------------------------------------------------------------------------------- /client/src/pages/Dashboard/views/Students/index.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import { Section } from "react-bulma-components"; 4 | 5 | import { useTopbarConfig } from "pages/Dashboard/components/Topbar"; 6 | 7 | import StudentList from "./components/StudentList"; 8 | import StudentModal from "../../components/StudentModal" 9 | import StudentListControls from "./components/StudentListControls"; 10 | 11 | const Students = () => { 12 | 13 | const [ sort, setSort ] = useState("name:asc"); 14 | const [ groupBy, setGroupBy ] = useState("none"); 15 | const [ search, setSearch ] = useState(""); 16 | useTopbarConfig({ name: "Students" }); 17 | 18 | return ( 19 |
20 | 21 | 22 | 23 |
24 | ); 25 | 26 | } 27 | 28 | export default Students; -------------------------------------------------------------------------------- /client/src/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | // Section, 4 | Container, 5 | Hero, 6 | Heading, 7 | Content 8 | } from "react-bulma-components"; 9 | 10 | import { Link } from "react-router-dom"; 11 | import MainWithLogin from "layouts/MainWithLogin"; 12 | import { LoginLink } from "components/Login"; 13 | 14 | const NotFound = () => { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | Hmmm, this doesn't seem quite right 22 |

Sorry, but the thing you were looking for couldn't be found. Try Logging in or Return Home »

23 |
24 |
25 |
26 |
27 |
28 | ); 29 | } 30 | 31 | export default NotFound; -------------------------------------------------------------------------------- /app/controllers/definitions/models/schema/UserSchema.js: -------------------------------------------------------------------------------- 1 | const Schema = require("mongoose").Schema; 2 | const { ObjectId } = Schema.Types; 3 | 4 | /** 5 | * @typedef {Object} UserSchema 6 | * @property {String} name 7 | * @property {String} email 8 | * @property {Boolean} isVerified 9 | * @property {String} password 10 | * @property {ObjectId[]} classrooms 11 | * 12 | * @typedef {import('mongoose').Document & UserSchema} UserDocument 13 | */ 14 | const UserSchema = new Schema({ 15 | name: { 16 | type: String, 17 | required: true 18 | }, 19 | email: { 20 | type: String, 21 | required: true 22 | }, 23 | isVerified: { 24 | type: Boolean, 25 | default: false 26 | }, 27 | password: { 28 | type: String, 29 | required: true 30 | }, 31 | classrooms: [ 32 | { 33 | type: ObjectId, 34 | ref: "Classroom" 35 | } 36 | ], 37 | date: { 38 | type: Date, 39 | default: Date.now 40 | } 41 | }); 42 | 43 | module.exports = UserSchema; -------------------------------------------------------------------------------- /client/src/animations/Fade/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import "./style.sass"; 4 | 5 | const Fade = ({ show = true, type = "both", duration="1s", style = {}, children, ...props }) => { 6 | 7 | const [shouldRender, setRender] = useState(show); 8 | 9 | useEffect(() => { 10 | 11 | if (show) setRender(true); 12 | 13 | }, [show]); 14 | 15 | const onAnimationEnd = () => { 16 | if (!show) setRender(false); 17 | }; 18 | 19 | const enabled = type === "both" || ( type === "in" && show ) || ( type === "out" && !show ); 20 | 21 | return ( 22 | shouldRender 23 | 24 | ? ( 25 |
30 | {show && children} 31 |
32 | ) 33 | 34 | : null 35 | ); 36 | 37 | }; 38 | 39 | export default Fade; -------------------------------------------------------------------------------- /app/routes/middleware/createControllerHandler.js: -------------------------------------------------------------------------------- 1 | const getEntriesReducer = keyMap => (data, [key, value]) => ({ ...data, [ keyMap[key] || key ]: value }); 2 | 3 | const mapRequestData = (req, include, keyMap) => ({ 4 | // Add all route parameters as keys. 5 | ...Object.entries(req.params).reduce( getEntriesReducer(keyMap), {}), 6 | // Extract target keys from the request object. 7 | ...["body", "user", ...include].reduce( (data, key) => ({ ...data, [ keyMap[key] || key ]: req[key] }), {} ), 8 | // Add all data points pushed to the `crdata` Map. 9 | ...[...req.crdata].reduce( getEntriesReducer(keyMap), {} ) 10 | }); 11 | 12 | const createControllerHandler = ( controller, { include = [], keyMap = {} } = {} ) => async ( req, res, next ) => { 13 | 14 | try { 15 | 16 | res.json( (await controller( mapRequestData( req, include, { body: "data", ...keyMap } ) )) || { success: true } ); 17 | 18 | } catch( err ) { 19 | 20 | next( err ) 21 | 22 | } 23 | 24 | } 25 | 26 | module.exports = createControllerHandler; -------------------------------------------------------------------------------- /app/mail/views/welcome.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Welcome to Classroom! 14 | 15 | 16 | [name], we are so excited you've decided to take your student management to the next level! Please click the button below to verify your email and get started with your new classroom. 17 | 18 | 19 | My email is valid. Enter my classroom! 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/controllers/definitions/models/schema/AppTypeSchema.js: -------------------------------------------------------------------------------- 1 | const Schema = require("mongoose").Schema; 2 | 3 | const appTypeRegistry = require("../../../../config/apps/registry.json"); 4 | 5 | /** 6 | * @typedef {Object} AppTypeSchema 7 | * @property {String} type 8 | * @property {Boolean} isDisabled 9 | * 10 | * @typedef {import('mongoose').Document & AppTypeSchema} AppTypeDocument 11 | */ 12 | const AppTypeSchema = new Schema({ 13 | type: { 14 | type: String, 15 | required: true 16 | }, 17 | // name: { 18 | // type: String, 19 | // required: true 20 | // }, 21 | isDisabled: { 22 | type: Boolean, 23 | default: false 24 | } 25 | }, { toJSON: { virtuals: true } }); 26 | 27 | AppTypeSchema.virtual('name').get(function() { 28 | 29 | return appTypeRegistry[this.type].name; 30 | 31 | }); 32 | 33 | AppTypeSchema.virtual('fields').get(function() { 34 | 35 | return { 36 | student: [], 37 | ...(appTypeRegistry[this.type].fields || {}) 38 | }; 39 | 40 | }); 41 | 42 | module.exports = AppTypeSchema; -------------------------------------------------------------------------------- /app/controllers/definitions/models/schema/MemberSchema/index.js: -------------------------------------------------------------------------------- 1 | const Schema = require("mongoose").Schema; 2 | const { ObjectId } = Schema.Types; 3 | 4 | const methods = require("./methods"); 5 | 6 | /** 7 | * @typedef {Object} MemberSchema 8 | * @property {ObjectId} _id 9 | * @property {string} role 10 | * @property {ObjectId} user 11 | * 12 | * @property {*} isAllowedTo 13 | * @property {*} getPermissionList 14 | * 15 | * @typedef {import('mongoose').Document & MemberSchema} MemberDocument 16 | */ 17 | const MemberSchema = new Schema({ 18 | role: { 19 | type: String, 20 | required: true 21 | }, 22 | user: { 23 | type: ObjectId, 24 | ref:'User', 25 | required: true 26 | }, 27 | meta: { 28 | type: Map, 29 | default: {} 30 | }, 31 | date: { 32 | type: Date, 33 | default: Date.now 34 | } 35 | }); 36 | 37 | MemberSchema.methods.isAllowedTo = methods.isAllowedTo; 38 | MemberSchema.methods.getPermissionList = methods.getPermissionList; 39 | 40 | module.exports = MemberSchema; -------------------------------------------------------------------------------- /app/controllers/index.js: -------------------------------------------------------------------------------- 1 | const ctrls = require("./types/library"); 2 | const FeedEntryController = require("./definitions/FeedEntryController"); 3 | 4 | const createController = registry => 5 | 6 | Array.isArray(registry) 7 | 8 | ? new registry[0](...registry[1]) 9 | 10 | : new registry(); 11 | 12 | [ 13 | require("./definitions/AuthController"), 14 | require("./definitions/AppController"), 15 | require("./definitions/AppTypeController"), 16 | require("./definitions/FeedController"), 17 | [FeedEntryController,["comment"]], 18 | [FeedEntryController,["elevate"]], 19 | [FeedEntryController,["deelevate"]], 20 | require("./definitions/InviteController"), 21 | require("./definitions/RegisterController"), 22 | require("./definitions/RoomController"), 23 | require("./definitions/StudentController"), 24 | require("./definitions/TokenController"), 25 | require("./definitions/UserController"), 26 | require("./definitions/ValidateEmailController") 27 | ].forEach(createController); 28 | 29 | module.exports = ctrls; -------------------------------------------------------------------------------- /app/graphql/modules/auth.js: -------------------------------------------------------------------------------- 1 | const { createModule, gql } = require('graphql-modules'); 2 | 3 | const { useAuthentication } = require('../middleware'); 4 | 5 | const { 6 | inputs: { 7 | Credentials 8 | }, 9 | types: { 10 | UserDocument, 11 | Auth 12 | } 13 | } = require('./typedefs'); 14 | 15 | const auth = createModule({ 16 | id: 'auth', 17 | dirname: __dirname, 18 | typeDefs: [ 19 | UserDocument, 20 | Auth, 21 | Credentials, 22 | gql` 23 | type Query { 24 | authenticated: UserDocument 25 | } 26 | 27 | type Mutation { 28 | login( credentials: Credentials ): Auth 29 | } 30 | `, 31 | ], 32 | middlewares: { 33 | Query: { 34 | authenticated: [ ...useAuthentication(false) ] 35 | } 36 | }, 37 | resolvers: { 38 | Query: { 39 | authenticated: (...[,, { authUser } ]) => { 40 | return authUser; 41 | } 42 | }, 43 | 44 | Mutation: { 45 | login: (...[, { credentials }, { db }]) => { 46 | return db.get("auth").login({ credentials }); 47 | } 48 | } 49 | }, 50 | }); 51 | 52 | module.exports = auth -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/Toolbar/style.sass: -------------------------------------------------------------------------------- 1 | @import "../../../../_env"; 2 | 3 | $width: 55px 4 | 5 | .toolbar 6 | display: flex 7 | flex-direction: column 8 | position: fixed 9 | z-index: 30 10 | top: 0 11 | left: 0 12 | height: 100vh 13 | width: $width 14 | border-right: 1px solid #e9ebf0 15 | 16 | .end 17 | margin-top: auto 18 | 19 | .item 20 | font-size: 1.5em 21 | text-align: center 22 | padding: 4px 0 23 | 24 | .action, 25 | .action-plain 26 | display: block 27 | margin: 0 auto 28 | line-height: 1em 29 | font-size: 1.1rem 30 | width: $width - 16px 31 | height: $width - 16px 32 | 33 | .action 34 | color: #7a7a7a 35 | padding: 6px 8px 36 | border-radius: 3px 37 | 38 | &:hover 39 | background: #f5f5f5 40 | 41 | &.is-active .action 42 | color: rgba(255,255,255,0.95) 43 | background: $primary 44 | -------------------------------------------------------------------------------- /client/src/pages/Dashboard/components/StudentModal/components/ModalBox.js: -------------------------------------------------------------------------------- 1 | import { 2 | Modal, 3 | Columns, 4 | } from "react-bulma-components"; 5 | 6 | import Fade from "animations/Fade"; 7 | 8 | export const ModalBox = ({ show, onClose, children, fullScreen }) => { 9 | 10 | const classes = ["is-student-modal"]; 11 | const contentClasses = ["hide-overflow"]; 12 | 13 | if( fullScreen ) { 14 | classes.push("is-fullscreen"); 15 | contentClasses.push("has-filled-content"); 16 | } 17 | 18 | return ( 19 | 24 | 25 | 26 | 27 | 28 | {children} 29 | 30 | 31 | 32 | 33 | ) 34 | } -------------------------------------------------------------------------------- /app/config/options.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | isProd: process.env.NODE_ENV === "production", 3 | port: process.env.PORT || 3001, 4 | secret: process.env.JWT_SECRET || "more security please?", 5 | mongodb: process.env.MONGODB_URI || "mongodb://localhost/instructorutilities", 6 | publicUrl: process.env.PUBLIC_URL || "http://localhost:3000", 7 | apiType: process.env.API_TYPE || "BOTH", 8 | email: { 9 | strategy: process.env.EMAIL_STRATEGY, 10 | from: process.env.EMAIL_FROM, 11 | smtp: { 12 | url: process.env.SMTP_URL, 13 | host: process.env.SMTP_HOST, 14 | port: process.env.SMTP_PORT && parseInt(process.env.SMTP_PORT), 15 | pool: process.env.SMTP_POOL && process.env.SMTP_POOL === "true", 16 | secure: process.env.SMTP_SECURE && process.env.SMTP_SECURE === "true", 17 | authUser: process.env.SMTP_AUTH_USER, 18 | authPass: process.env.SMTP_AUTH_PASS, 19 | }, 20 | sgApiKey: process.env.SENDGRID_API_KEY, 21 | } 22 | }; 23 | 24 | module.exports = ( key ) => options[key]; -------------------------------------------------------------------------------- /app/controllers/definitions/models/schema/FeedSchema/index.js: -------------------------------------------------------------------------------- 1 | const Schema = require("mongoose").Schema; 2 | const { ObjectId } = Schema.Types; 3 | 4 | /** 5 | * Type Definition Imports 6 | * @typedef {import('../FeedEntrySchema').FeedEntryDocument} FeedEntryDocument 7 | */ 8 | 9 | const FeedEntrySchema = require("../FeedEntrySchema"); 10 | 11 | const methods = require("./methods"); 12 | 13 | /** 14 | * @typedef {Object} FeedSchema 15 | * @property {string} room 16 | * @property {ObjectId} for 17 | * @property {string} in 18 | * @property {FeedEntryDocument[]} items 19 | * 20 | * @typedef {import('mongoose').Document & FeedSchema} FeedDocument 21 | */ 22 | const FeedSchema = new Schema({ 23 | room: { 24 | type: ObjectId, 25 | ref:'Classroom', 26 | required: true 27 | }, 28 | for: { 29 | type: ObjectId, 30 | required: true 31 | }, 32 | in: { 33 | type: String, 34 | required: true 35 | }, 36 | items: [ FeedEntrySchema ] 37 | }); 38 | 39 | FeedSchema.methods.pushItem = methods.pushItem; 40 | 41 | module.exports = FeedSchema; -------------------------------------------------------------------------------- /client/src/pages/Dashboard/views/UserSettings/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Section, 3 | Columns, 4 | } from "react-bulma-components"; 5 | 6 | import { useTopbarConfig } from "pages/Dashboard/components/Topbar"; 7 | import UserSettingsForm from "./components/UserSettingsForm"; 8 | import UserClassrooms from "./components/UserClassrooms"; 9 | import NoClassroomNotification from "./components/NoClassroomNotification"; 10 | import { useAuthorizedUser } from "utils/auth"; 11 | 12 | const { Column } = Columns; 13 | 14 | const UserSettings = () => { 15 | 16 | useTopbarConfig({ name: "Account Settings" }); 17 | const user = useAuthorizedUser(); 18 | 19 | return ( 20 |
21 | {!user.classrooms.length && } 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | ); 32 | 33 | } 34 | 35 | export default UserSettings; -------------------------------------------------------------------------------- /client/src/components/Form/components/FormInput.js: -------------------------------------------------------------------------------- 1 | import { Form as FormCollection } from "react-bulma-components"; 2 | 3 | import { RangeInput } from "./RangeInput"; 4 | import { RichTextEditor } from "./RichTextEditor"; 5 | 6 | const { Input, Select, Textarea } = FormCollection; 7 | 8 | export const FormInput = ({ type = "text", options = [], ...props }) => { 9 | switch (type) { 10 | case "select": 11 | return ( 12 | 21 | ); 22 | case "range": 23 | return ; 24 | case "richtext": 25 | return ; 26 | case "textarea": 27 | return