├── 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 |
10 |
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 |
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 |
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 |
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 |
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 ;
28 | default:
29 | return ;
30 | }
31 | };
--------------------------------------------------------------------------------
/client/src/components/Form/components/FormField.js:
--------------------------------------------------------------------------------
1 | import { Form as FormCollection } from "react-bulma-components";
2 |
3 | import { FormInput } from "./FormInput"
4 |
5 | import { Error } from "components/Errors";
6 |
7 | const { Field, Control, Label } = FormCollection;
8 |
9 | export const FormField = ({
10 | label,
11 | type = "text",
12 | name,
13 | placeholder,
14 | value,
15 | onChange,
16 | options,
17 | inputColor,
18 | inputProps = {},
19 | ...props
20 | }) => {
21 | const fieldInputProps = {
22 | name,
23 | type,
24 | value,
25 | onChange: onChange || null,
26 | ...inputProps
27 | };
28 |
29 | if (options) fieldInputProps.options = options;
30 | if (placeholder) fieldInputProps.placeholder = placeholder;
31 | if (!fieldInputProps.color && inputColor)
32 | fieldInputProps.color = inputColor(name);
33 |
34 | return (
35 |
36 | {label && }
37 |
38 |
39 |
40 |
41 |
42 | );
43 | };
--------------------------------------------------------------------------------
/app/graphql/modules/student.js:
--------------------------------------------------------------------------------
1 | const { gql } = require('graphql-modules');
2 |
3 | const { createControllerModule } = require('./utils');
4 |
5 | const {
6 | scalars: {
7 | JSONObject
8 | },
9 | types: {
10 | StudentDocument,
11 | FeedEntryCommentDocument,
12 | FeedEntryComment
13 | }
14 | } = require('./typedefs');
15 |
16 | const studentModule = createControllerModule({
17 | id: 'room.student',
18 | dirname: __dirname,
19 | typeDefs: [
20 | JSONObject,
21 | StudentDocument,
22 | FeedEntryCommentDocument,
23 | FeedEntryComment,
24 | gql`
25 | type Query {
26 | roomStudent(roomStudentId: ID): StudentDocument
27 | }
28 | `
29 | ],
30 | memberPermission: {
31 | context: "fromStudentId",
32 | set: "student"
33 | },
34 | abilites: [
35 | "view"
36 | ],
37 | // resolvers: {
38 | // Query: {
39 | // roomStudent() {
40 | // return { success: true }
41 | // }
42 | // }
43 | // }
44 | });
45 |
46 | module.exports = studentModule;
--------------------------------------------------------------------------------
/client/src/components/Date.js:
--------------------------------------------------------------------------------
1 | import { DateTime } from "luxon";
2 |
3 | const formats = {
4 | toRelative: dateTime => {
5 | const formatDate = dateTime.toRelative()
6 |
7 | const [, numberString, type ] = formatDate.match(/(\d+)\s+(second|day)s?/) || [];
8 |
9 | if(!numberString || !type) return formatDate
10 |
11 | if(type === "second") return "now"
12 |
13 | const number = parseInt(numberString)
14 |
15 | if(number >= 7) return `${Math.floor(number / 7)} weeks ago`
16 |
17 | return formatDate
18 | },
19 | default: (dateTime, format) => dateTime.toFormat(format)
20 | }
21 |
22 |
23 | const Date = ( { date, format = "toRelative", className, ...props } ) => {
24 |
25 | const classes = [ "date" ];
26 | if( className ) classes.push( className );
27 |
28 | const dateTime = DateTime.fromISO(date);
29 |
30 | return (
31 |
32 | { formats[format] ? formats[format](dateTime) : formats.default(dateTime, format) }
33 |
34 | )
35 |
36 | }
37 |
38 | export default Date;
--------------------------------------------------------------------------------
/test/suite/controllers/types/ControllerSchema/_suite/makeDoc.js:
--------------------------------------------------------------------------------
1 | const { expect } = require("chai");
2 | const { Document } = require( "mongoose" );
3 |
4 | const SchemaController = require("~crsm/controllers/types/SchemaController");
5 |
6 | const TestModel = require("~crsmtest/lib/TestModel");
7 |
8 | module.exports = function() {
9 |
10 | it("should return a `Document` of the given model", () => {
11 |
12 | // Arrange
13 | const ctrl = new SchemaController( "modelkey", TestModel );
14 |
15 | // Act
16 | const doc = ctrl.makeDoc();
17 |
18 | // Assert
19 | expect( doc ).to.be.instanceof( Document );
20 | expect( doc.collection.collectionName ).to.equal( TestModel.collection.collectionName );
21 |
22 | });
23 |
24 | it("should assign given data to the created `Model`", () => {
25 |
26 | // Arrange
27 | const ctrl = new SchemaController( "modelkey", TestModel );
28 | const test = { name: "A test" };
29 |
30 | // Act
31 | const doc = ctrl.makeDoc( test );
32 |
33 | // Assert
34 | expect( doc ).to.include( test );
35 |
36 | });
37 |
38 | }
--------------------------------------------------------------------------------
/app/config/errors/index.js:
--------------------------------------------------------------------------------
1 | class CrsmError extends Error {
2 |
3 | message;
4 | data;
5 |
6 | constructor(message, data) {
7 |
8 | super();
9 | this.message = message;
10 | this.data = data;
11 |
12 | }
13 |
14 | }
15 |
16 | class InvalidDataError extends CrsmError { };
17 |
18 | class InvalidUserError extends CrsmError { };
19 |
20 | class NotFoundError extends CrsmError { };
21 |
22 | class InvalidConfig extends CrsmError { };
23 |
24 | class RouteError extends CrsmError {
25 |
26 | statusCode;
27 | sourceErr;
28 |
29 | constructor(statusCode, message, data) {
30 |
31 | super( message, data );
32 |
33 | this.statusCode = statusCode;
34 |
35 | }
36 |
37 | get response() {
38 | return {
39 | default: this.message,
40 | ...this.data
41 | }
42 | }
43 |
44 | setSourceErr( sourceErr ) {
45 |
46 | this.sourceErr = sourceErr;
47 | return this;
48 |
49 | }
50 |
51 | }
52 |
53 |
54 | module.exports = {
55 | InvalidConfig,
56 | InvalidDataError,
57 | InvalidUserError,
58 | NotFoundError,
59 | RouteError
60 | }
--------------------------------------------------------------------------------
/client/src/components/Modal/store/reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | SET_ACTIVE_MODAL,
3 | REGISTER_MODAL,
4 | DEREGISTER_MODAL,
5 | } from "./actions.js";
6 |
7 | const reducer = (state, { type, payload }) => {
8 |
9 | // payload.data
10 |
11 | const actions = {
12 | [REGISTER_MODAL]: () => ({
13 | ...state,
14 | modals: {
15 | ...state.modals,
16 | ...payload
17 | }
18 | }),
19 | [SET_ACTIVE_MODAL]: ({ activeKey, props }) => {
20 | console.log(activeKey, props);
21 | return ({
22 | ...state,
23 | activeKey,
24 | ...(activeKey ? {
25 | modals: {
26 | ...state.modals,
27 | [activeKey]: {
28 | ...state.modals[activeKey],
29 | props: props || state.modals[activeKey].props
30 | }
31 | }
32 | } : {})
33 |
34 | })
35 | },
36 | [DEREGISTER_MODAL]: () => {
37 | const { [payload]: removed, ...modals } = state.modals;
38 | return {
39 | ...state,
40 | modals,
41 | activeKey: state.activeKey === payload ? false : state.activeKey
42 | };
43 | }
44 | };
45 |
46 | return actions.hasOwnProperty(type) ? actions[type]( payload ) : state;
47 |
48 | };
49 |
50 | export default reducer;
--------------------------------------------------------------------------------
/client/src/components/Errors.js:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from "react";
2 |
3 | import { Message } from "react-bulma-components";
4 |
5 | const ErrorContext = createContext();
6 |
7 | const { Provider } = ErrorContext;
8 |
9 | export const ErrorProvider = ( props ) => {
10 | return
11 | }
12 |
13 | export const Error = ( { name, type = "help" } ) => {
14 |
15 | const { [name]: error } = useContext( ErrorContext );
16 |
17 | const types = {
18 | help: () => {error}
,
19 | message: () => {error}
20 | }
21 |
22 | return error
23 |
24 | ? ( types[type]() )
25 |
26 | : null;
27 |
28 | }
29 |
30 | export const useHasError = ( errors ) => {
31 |
32 | return ( name ) => {
33 |
34 | return Boolean( errors[name] );
35 |
36 | }
37 |
38 | }
39 |
40 | export const useInputErrorColor = ( errors ) => {
41 |
42 | const hasError = useHasError( errors );
43 |
44 | return ( name ) => {
45 |
46 | return hasError( name ) ? "danger" : null;
47 |
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/app/routes/middleware/setInvite.js:
--------------------------------------------------------------------------------
1 | const { InvalidDataError, NotFoundError } = require("../../config/errors");
2 |
3 | const ctrls = require("../../controllers");
4 |
5 | const setInvite = async ( req, res, next ) => {
6 |
7 | try {
8 |
9 | const { tokenString } = req.params;
10 | const inviteToken = await ctrls.get("token").getByTokenString({ tokenString });
11 |
12 | if( !inviteToken ) throw new NotFoundError( "This invitation is no longer available" );
13 |
14 | const inviteRoom = await ctrls.get("room").findOne({ docId: inviteToken.relation });
15 |
16 | if( !inviteRoom ) throw new InvalidDataError( "This invitation is not longer valid" );
17 |
18 | const invite = inviteRoom.invites.find( invite => invite.token.equals( inviteToken._id ) );
19 |
20 | if( !invite ) throw new InvalidDataError( "This invitation is not longer valid" );
21 |
22 | req.crdata.set( "inviteToken", inviteToken );
23 | req.crdata.set( "inviteRoom", inviteRoom );
24 | req.crdata.set( "invite", invite );
25 |
26 | next();
27 |
28 | } catch(err) {
29 |
30 | next( err );
31 |
32 | }
33 |
34 | }
35 |
36 | module.exports = setInvite;
--------------------------------------------------------------------------------
/client/src/components/Pulse/style.sass:
--------------------------------------------------------------------------------
1 | .spinner
2 | width: var(--pulse-size,40px)
3 | height: var(--pulse-size,40px)
4 | position: relative
5 | margin: 100px auto
6 |
7 | .double-bounce1
8 | width: 100%
9 | height: 100%
10 | border-radius: 50%
11 | background-color: var(--pulse-color,#333)
12 | opacity: 0.6
13 | position: absolute
14 | top: 0
15 | left: 0
16 | -webkit-animation: sk-bounce 2.0s infinite ease-in-out
17 | animation: sk-bounce 2.0s infinite ease-in-out
18 |
19 | .double-bounce2
20 | width: 100%
21 | height: 100%
22 | border-radius: 50%
23 | background-color: var(--pulse-color,#333)
24 | opacity: 0.6
25 | position: absolute
26 | top: 0
27 | left: 0
28 | -webkit-animation: sk-bounce 2.0s infinite ease-in-out
29 | animation: sk-bounce 2.0s infinite ease-in-out
30 | -webkit-animation-delay: -1.0s
31 | animation-delay: -1.0s
32 |
33 | @-webkit-keyframes sk-bounce
34 | 0%, 100%
35 | -webkit-transform: scale(0)
36 |
37 | 50%
38 | -webkit-transform: scale(1)
39 |
40 | @keyframes sk-bounce
41 | 0%, 100%
42 | transform: scale(0)
43 | -webkit-transform: scale(0)
44 |
45 | 50%
46 | transform: scale(1)
47 | -webkit-transform: scale(1)
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/components/StudentModal/index.js:
--------------------------------------------------------------------------------
1 | import { Box, Columns } from "react-bulma-components";
2 |
3 | import { ModalBox } from "./components";
4 |
5 | import { PanelTabs } from "./panels";
6 |
7 | import { useStudentModalConfig } from "./utils";
8 |
9 | import "./index.sass" ;
10 |
11 | const { Column } = Columns;
12 |
13 | const StudentModal = () => {
14 |
15 | // We pull in the student to edit from the dashboard state
16 | const {
17 | student,
18 | isViewing,
19 | panelConfig,
20 | activePanels,
21 | clearEditStudent
22 | } = useStudentModalConfig();
23 |
24 | return (
25 |
26 | {/* Dispay panel tabs */}
27 | {panelConfig.panels.size > 1 && }
28 |
29 | {/* Display each active panel */}
30 | {[...activePanels].map(([key,{Panel}]) => (
31 | 1 ? "half" : 12 }}
34 | className={`has-filled-content is-${key}-column`}>
35 |
36 |
37 | ))}
38 |
39 | )
40 |
41 | }
42 |
43 | export default StudentModal;
--------------------------------------------------------------------------------
/client/src/components/Modal/components/Modal.js:
--------------------------------------------------------------------------------
1 | import {
2 | Modal as BulmaModal,
3 | } from "react-bulma-components";
4 | import { useOpenModal } from "components/Modal/utils";
5 | import { useModalContext } from "../store";
6 | import { useMemo } from "react";
7 |
8 |
9 | export const Modal = ( { namespace = "default", onClose, contentProps = {}, ...props } ) => {
10 |
11 | const [ modal, ] = useModalContext();
12 | const { modals, activeKey } = modal;
13 |
14 | const isActive = useMemo(() => modals[activeKey] && modals[activeKey].namespace === namespace, [activeKey]);
15 |
16 | const {
17 | component: Component,
18 | props: componentProps = {}
19 | } = (
20 | isActive
21 | ? modals[activeKey]
22 | : {}
23 | );
24 |
25 | const closeModal = useOpenModal(false);
26 |
27 | const onModalClose = () => {
28 | onClose ? onClose(closeModal) : closeModal();
29 | }
30 |
31 | return (
32 |
37 |
38 | { Component && }
39 |
40 |
41 | );
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Classroom",
3 | "name": "Classroom Student Management",
4 | "icons": [
5 | {
6 | "src": "\/favicon\/android-icon-36x36.png",
7 | "sizes": "36x36",
8 | "type": "image\/png",
9 | "density": "0.75"
10 | },
11 | {
12 | "src": "\/favicon\/android-icon-48x48.png",
13 | "sizes": "48x48",
14 | "type": "image\/png",
15 | "density": "1.0"
16 | },
17 | {
18 | "src": "\/favicon\/android-icon-72x72.png",
19 | "sizes": "72x72",
20 | "type": "image\/png",
21 | "density": "1.5"
22 | },
23 | {
24 | "src": "\/favicon\/android-icon-96x96.png",
25 | "sizes": "96x96",
26 | "type": "image\/png",
27 | "density": "2.0"
28 | },
29 | {
30 | "src": "\/favicon\/android-icon-144x144.png",
31 | "sizes": "144x144",
32 | "type": "image\/png",
33 | "density": "3.0"
34 | },
35 | {
36 | "src": "\/favicon\/android-icon-192x192.png",
37 | "sizes": "192x192",
38 | "type": "image\/png",
39 | "density": "4.0"
40 | }
41 | ],
42 | "start_url": "/",
43 | "display": "standalone",
44 | "theme_color": "#067CAC",
45 | "background_color": "#ffffff"
46 | }
47 |
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/views/UserSettings/components/ClassroomModal/index.js:
--------------------------------------------------------------------------------
1 |
2 | import { useModalRegistration, useOpenModal } from "components/Modal/utils";
3 | import ClassroomModalContent from "./components/ClassroomModalContent";
4 | import { ModalLink } from "components/Modal";
5 |
6 | const modalKey = "CLASSROOM_MODAL";
7 | /**
8 | * useClassroomModal hook
9 | * @param {object} props
10 | */
11 | export const useClassroomModalRegistration = () => {
12 | useModalRegistration(modalKey, {
13 | key: modalKey,
14 | namespace: "dashboard",
15 | component: ClassroomModalContent
16 | });
17 | };
18 |
19 | export const useOpenClassroomModal = () => {
20 | return useOpenModal( modalKey );
21 | }
22 |
23 | /**
24 | * ClassroomModalButton
25 | * @param {object} props
26 | */
27 | export const ClassroomModalButton = ({ children, ...props }) => {
28 | return (
29 |
32 | );
33 | };
34 |
35 | /**
36 | * ClassroomModalLink
37 | * @param {object} props
38 | */
39 | export const ClassroomModalLink = ({ children, ...props }) => {
40 | return (
41 |
42 | {children}
43 |
44 | );
45 | };
--------------------------------------------------------------------------------
/test/suite/controllers/types/ControllerSchema/_suite/constructor.js:
--------------------------------------------------------------------------------
1 | const { expect } = require("chai");
2 |
3 | const SchemaController = require("~crsm/controllers/types/SchemaController");
4 |
5 | const TestModel = require("~crsmtest/lib/TestModel");
6 |
7 | module.exports = function() {
8 |
9 | it("should create a SchemaController object", () => {
10 |
11 | // Arrange and Act
12 | const schemaController = new SchemaController;
13 |
14 | // Assert
15 | expect( schemaController ).to.be.instanceof( SchemaController );
16 |
17 | });
18 |
19 | it("should assign the first param to the `key` property", () => {
20 |
21 | // Arrange
22 | const key = "modelkey";
23 |
24 | // Act
25 | const schemaController = new SchemaController( key );
26 |
27 | // Assert
28 | expect( schemaController.key ).to.equal( key );
29 |
30 | });
31 |
32 | it("should assign the second param to the `model` property", () => {
33 |
34 | // Act
35 | const schemaController = new SchemaController( "modelkey", TestModel );
36 |
37 | // Assert
38 | expect( schemaController.model ).to.equal( TestModel );
39 |
40 | });
41 |
42 | }
--------------------------------------------------------------------------------
/client/src/components/Modal/utils.js:
--------------------------------------------------------------------------------
1 | import {
2 | DEREGISTER_MODAL,
3 | REGISTER_MODAL,
4 | SET_ACTIVE_MODAL,
5 | SET_ACTIVE_ROOM
6 | } from "components/Modal/store/actions";
7 | import { useModalContext } from "components/Modal/store";
8 | import { useEffect, useMemo } from "react";
9 |
10 | export const useModalRegistration = ( key, modalConfig ) => {
11 |
12 | const [ modalState, modalDispatch ] = useModalContext();
13 |
14 | useEffect(() => {
15 | modalDispatch({
16 | type: REGISTER_MODAL,
17 | payload: {
18 | [ key ] : {
19 | namespace: "default",
20 | ...modalConfig
21 | }
22 | }
23 | })
24 | return () => {
25 | modalDispatch({
26 | type: DEREGISTER_MODAL,
27 | payload: key
28 | });
29 | }
30 | }, []);
31 |
32 | return modalState
33 | }
34 |
35 | export const useOpenModal = ( key ) =>{
36 |
37 | const [, modalDispatch] = useModalContext();
38 |
39 | return useMemo(()=> ( props ) =>
40 | modalDispatch({
41 | type: SET_ACTIVE_MODAL,
42 | payload: {
43 | activeKey: key,
44 | props
45 | }
46 | }),[key]);
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/components/StudentModal/utils.js:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 |
3 | import { getDashboardAction as gda, useEditStudent, useClassroom, useDashboardContext } from "pages/Dashboard/store";
4 | import { EDIT_STUDENT } from "pages/Dashboard/store/actionsNames";
5 |
6 | import {
7 | usePanels
8 | } from "./panels";
9 |
10 | import { useWindowDimensions } from "utils/windowWidth"
11 |
12 | export const useStudentModalConfig = () => {
13 |
14 | const [{ editStudent: editStudentId }, dispatch] = useDashboardContext();
15 |
16 | const isViewing = editStudentId !== false;
17 |
18 | const { _id: roomId } = useClassroom();
19 |
20 | const student = useEditStudent();
21 |
22 | const clearEditStudent = () => dispatch(gda(EDIT_STUDENT, false));
23 |
24 | const { width } = useWindowDimensions();
25 | const windowBreakPoint = 1025;
26 |
27 | const panelConfig = usePanels( roomId, student );
28 |
29 | const activePanels = useMemo(() => new Map( [...panelConfig.panels].filter(([key]) => width >= windowBreakPoint || key === panelConfig.activePanel) ));
30 |
31 | return {
32 | student,
33 | isViewing,
34 | panelConfig,
35 | activePanels,
36 | clearEditStudent
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/app/routes/index.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const basename = path.basename(__filename);
4 | const { Router } = require("express");
5 |
6 | const initCrData = require("./middleware/initCrData");
7 |
8 | const parentRouter = Router();
9 |
10 | // Filter function for filtering out unwanted files from fs.readdirSync.
11 | const filterFiles = file => (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
12 | const getPrefix = file => file[0] === "_" ? "" : "/" + file.substr(0, file.length-3).replace(/[A-Z]/g, upper => `-${upper.toLowerCase()}`);
13 |
14 | const routes = Router();
15 |
16 | /**
17 | * Read the current directory and load found modules into the controllers list. File
18 | * names not prefixed with an `_` will be used as part of the path file.
19 | */
20 | fs
21 | .readdirSync(__dirname)
22 | .filter(filterFiles)
23 | .forEach(file => {
24 |
25 | routes.use( `/api${getPrefix(file)}`, require(path.join(__dirname, file)) );
26 |
27 | });
28 |
29 | const catchAllHandler = ( req, res ) => res.sendFile( path.join( __dirname, '../../', 'client/build/index.html' ) );
30 |
31 | parentRouter.use(
32 | initCrData,
33 | routes,
34 | catchAllHandler
35 | );
36 |
37 | module.exports = parentRouter;
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/utils/feed.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | import { useDashboardDispatch, getDashboardAction as gda, useClassroom } from "pages/Dashboard/store";
4 | import { ADD_STUDENT_FEED_ITEMS, DELETE_STUDENT_FEED_ITEMS, UPDATE_STUDENT, UPDATE_STUDENT_FEED_ITEMS } from "../store/actionsNames";
5 | import { useSocket } from "utils/socket.io";
6 |
7 | const actionsMap = {
8 | push: ADD_STUDENT_FEED_ITEMS,
9 | update: UPDATE_STUDENT_FEED_ITEMS,
10 | delete: DELETE_STUDENT_FEED_ITEMS
11 | }
12 |
13 | export const useHandleFeedEventResponse = (feedId) => {
14 |
15 | const dispatch = useDashboardDispatch();
16 | const socket = useSocket();
17 | const { _id } = useClassroom();
18 |
19 | const [ handleFeedEventResponse ] = useState(() => ( { entries, studentUpdate }, action = "push" ) => {
20 |
21 | if( entries ) {
22 | dispatch( gda( actionsMap[action], entries ) );
23 | socket.emit( `${feedId}:${action}`, entries );
24 | }
25 |
26 | if( studentUpdate ) {
27 | const message = gda( UPDATE_STUDENT, studentUpdate );
28 | dispatch( message );
29 | socket.emit( `${_id}:dispatch`, message );
30 | }
31 |
32 | });
33 |
34 | return handleFeedEventResponse;
35 |
36 | }
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import "./App.sass";
3 |
4 | import LoadingOverlay from "./components/LoadingOverlay";
5 | import Routes from "./components/Routes";
6 |
7 | import { useAuthTokenStore } from "./utils/auth";
8 | import { useIsReady, useReadyStep } from "./utils/ready";
9 | import loadGlobalIcons from "./utils/icons";
10 | import { useSocketConnection } from "./utils/socket.io";
11 | import Fade from "./animations/Fade";
12 | import { useUserSocketUpdates } from "utils/user";
13 | import { Modal } from "components/Modal";
14 |
15 | loadGlobalIcons();
16 |
17 | const App = () => {
18 |
19 | useSocketConnection();
20 |
21 | const [ completeStep ] = useReadyStep("authcheck");
22 |
23 | const isReady = useIsReady();
24 |
25 | const isAuthCheckDone = useAuthTokenStore();
26 |
27 | useUserSocketUpdates();
28 |
29 | useEffect(() => {
30 |
31 | if( isAuthCheckDone ) completeStep();
32 |
33 | }, [isAuthCheckDone, completeStep]);
34 |
35 | return (
36 |
37 | {isAuthCheckDone ? : null}
38 |
39 |
40 |
41 | )
42 |
43 | }
44 |
45 | export default App;
--------------------------------------------------------------------------------
/app/routes/middleware/setFeed.js:
--------------------------------------------------------------------------------
1 | const ctrls = require("../../controllers");
2 | const { NotFoundError } = require("../../config/errors");
3 |
4 | const setFeed = async (req, res, next) => {
5 |
6 | try {
7 |
8 | let feed;
9 | let feedId;
10 |
11 | if( req.params.itemId ) {
12 |
13 | const entryId = req.params.itemId;
14 | feed = await ctrls.get("feed").findOne(
15 | { search: { ["items._id"]: entryId } },
16 | { select: "room for in items.$" }
17 | );
18 |
19 | if( !feed ) throw new NotFoundError( "Target entry not found." );
20 |
21 | req.crdata.set( "feedItem", feed.items.id(entryId) );
22 |
23 | } else {
24 |
25 | feedId = req.params.feedId || req.body.feedId
26 | feed = await ctrls.get("feed").findOne(
27 | { docId: feedId },
28 | { select: "room for in" }
29 | );
30 | }
31 |
32 | if( !feed ) throw new NotFoundError( "Target feed not found." );
33 |
34 | req.crdata.set( "feedId", feedId );
35 | req.crdata.set( "feed", feed );
36 |
37 | next();
38 |
39 | } catch( err ) {
40 |
41 | next(err);
42 |
43 | }
44 |
45 | }
46 |
47 | module.exports = setFeed;
--------------------------------------------------------------------------------
/app/routes/apps.js:
--------------------------------------------------------------------------------
1 | const { app: appVal } = require("./validation")
2 |
3 | const ctrls = require("../controllers");
4 | //const { appTypeCtrl } = require("../controllers");
5 | const setRoom = require("./middleware/setRoom");
6 | const isRoomMember = require("./middleware/isRoomMember");
7 | const setAppSearch = require("./middleware/setAppSearch");
8 |
9 | const createRouter = require("./utils/createRouter");
10 |
11 | const sharedConfig = { auth: true };
12 |
13 | module.exports = createRouter([
14 | [
15 | "/",
16 | {
17 | post: {
18 | defaultError: "create the app",
19 | validation: appVal,
20 | middleware: [setRoom.fromBody, isRoomMember],
21 | ctrl: ctrls.get("app")
22 | }
23 | },
24 | sharedConfig
25 | ],
26 |
27 | [
28 | "/types",
29 | {
30 | get: {
31 | defaultError: "get app types",
32 | ctrl: ctrls.get("appType").binding.getEnabled
33 | }
34 | },
35 | sharedConfig
36 | ],
37 |
38 | [
39 | "/:appTypeId/:roomId",
40 | {
41 | get: {
42 | defaultError: "get the app",
43 | ctrl: ctrls.get("app")
44 | },
45 | patch: {
46 | defaultError: "update the app",
47 | ctrl: ctrls.get("app")
48 | }
49 | },
50 | {
51 | ...sharedConfig,
52 | paramCheck: true,
53 | middleware: [setRoom.fromParam, isRoomMember, setAppSearch]
54 | }
55 | ]
56 | ]);
--------------------------------------------------------------------------------
/app/graphql/modules/room.js:
--------------------------------------------------------------------------------
1 | const { gql } = require('graphql-modules');
2 | const { GraphQLJSONObject } = require('graphql-type-json');
3 | const {
4 | scalars: {
5 | JSONObject
6 | },
7 | types: {
8 | RoomDocument,
9 | AppTypeDocument,
10 | StaffDocument,
11 | StudentDocument,
12 | FeedEntryCommentDocument,
13 | FeedEntryComment,
14 | UserDocument
15 | }
16 | } = require('./typedefs');
17 |
18 | const { createControllerModule } = require('./utils');
19 |
20 | const roomModule = createControllerModule({
21 | id: 'room',
22 | dirname: __dirname,
23 | typeDefs: [
24 | JSONObject,
25 | StaffDocument,
26 | RoomDocument,
27 | AppTypeDocument,
28 | StudentDocument,
29 | FeedEntryCommentDocument,
30 | FeedEntryComment,
31 | UserDocument,
32 | gql`
33 | type Query {
34 | room(roomId: ID): RoomDocument
35 | }
36 |
37 | type Mutation {
38 | room(roomId: ID, ): RoomDocument
39 | }
40 | `
41 | ],
42 | memberPermission: {
43 | context: "fromRoomId",
44 | set: "room"
45 | },
46 | abilites: [
47 | "view"
48 | ],
49 | resolvers: {
50 | JSONObject: GraphQLJSONObject
51 | }
52 | });
53 |
54 | module.exports = roomModule
--------------------------------------------------------------------------------
/seed/index.js:
--------------------------------------------------------------------------------
1 | require("../app/config/mongoose");
2 |
3 | const {
4 | seedUser,
5 | seedClassroom
6 | } = require("./seeds");
7 |
8 | const seedDb = async () => {
9 |
10 | /**
11 | * Define seeds in the order they should run.
12 | */
13 | const seeds = [
14 | [ "User", seedUser ],
15 | [ "Classroom", seedClassroom ]
16 | ];
17 |
18 | const divider = "================================";
19 |
20 | try {
21 |
22 | let totalCreated = 0;
23 |
24 | for( let i = 0; i < seeds.length; i++ ) {
25 |
26 | const [ name, seeder ] = seeds[i];
27 |
28 | console.log("\n"+divider);
29 | console.log(`[Running ${name} seed]`);
30 |
31 | const usersResult = await seeder();
32 |
33 | totalCreated += usersResult.result.n;
34 |
35 | console.log(`${usersResult.result.n} ${name} documents created`);
36 | console.log(divider+"\n");
37 |
38 | }
39 |
40 | console.log("\n"+divider);
41 | console.log("SEED COMPLETE...");
42 | console.log(`${totalCreated} documents created`);
43 | console.log(divider+"\n");
44 |
45 | process.exit(0);
46 |
47 | } catch( err ) {
48 |
49 | console.error(err);
50 | process.exit(1);
51 |
52 | }
53 |
54 | }
55 |
56 | seedDb();
--------------------------------------------------------------------------------
/app/routes/feeds.js:
--------------------------------------------------------------------------------
1 | const {
2 | feed: feedPerm
3 | } = require("../config/permissions");
4 |
5 | const ctrls = require("../controllers");
6 |
7 | const createRouter = require("./utils/createRouter");
8 |
9 | const setRoom = require("./middleware/setRoom");
10 | const isRoomMember = require("./middleware/isRoomMember");
11 | const setFeed = require("./middleware/setFeed");
12 |
13 | const makeFeedEntryRoutesConfig = require("./utils/makeFeedEntryRoutesConfig");
14 | const searchCtrls = require("../controllers/utils/searchCtrls");
15 |
16 | const sharedConfig = {
17 | paramCheck: true,
18 | auth: true,
19 | middleware: [ setFeed, setRoom.fromFeed, isRoomMember ]
20 | }
21 |
22 | module.exports = createRouter([
23 | [
24 | "/:feedId",
25 | {
26 | get: {
27 | defaultError: "get the feed",
28 | permission: feedPerm,
29 | ctrl: ctrls.get("feed")
30 | }
31 | },
32 | sharedConfig
33 | ],
34 |
35 | [
36 | "/:feedId/items",
37 | {
38 | get: {
39 | defaultError: "get the feed items",
40 | permission: feedPerm,
41 | ctrl: ctrls.get("feed").binding.getItems
42 | }
43 | },
44 | sharedConfig
45 | ],
46 |
47 | ...[...searchCtrls("feed.item").entries()].reduce(
48 | (routes, [, entryTypeCtrl]) => [
49 | ...routes,
50 | ...makeFeedEntryRoutesConfig(entryTypeCtrl)
51 | ],
52 | []
53 | )
54 | ]);
--------------------------------------------------------------------------------
/app/config/permissions/index.js:
--------------------------------------------------------------------------------
1 | const {
2 | RoomSet,
3 | StudentSet,
4 | InviteSet,
5 | FeedSet,
6 | FeedCommentSet,
7 | FeedElevateSet,
8 | FeedDeelevateSet
9 | } = require("./sets");
10 |
11 | class Permissions {
12 |
13 | constructor() {
14 |
15 | this.room = new RoomSet();
16 | this.student = new StudentSet();
17 | this.invite = new InviteSet();
18 | this.feed = new FeedSet();
19 | this.feedComment = new FeedCommentSet();
20 | this.feedElevate = new FeedElevateSet();
21 | this.feedDeelevate = new FeedDeelevateSet();
22 |
23 | /** Validate keys */
24 | for( const key in this ) if( key !== this[key].key )
25 |
26 | throw Error( `Invalid permissions configuration for ${key} set. The permissions key must match the set key.` );
27 |
28 | }
29 |
30 | /**
31 | *
32 | * @param {string} permission
33 | */
34 | has( permission ) {
35 |
36 | const [ verb, ...setParts ] = permission.toLowerCase().split('_');
37 | const setKey = setParts.map((part,i)=>i ? (part[0].toUpperCase()+part.slice(1)) : part).join('');
38 |
39 | return setKey in this && ( this[setKey][verb] === permission );
40 |
41 | }
42 |
43 | }
44 |
45 | const permissions = new Permissions();
46 |
47 | module.exports = permissions;
--------------------------------------------------------------------------------
/app/graphql/context/authentication.js:
--------------------------------------------------------------------------------
1 | const { AuthenticationError } = require("apollo-server-express");
2 | const jwt = require("jsonwebtoken");
3 | const secret = require("../../config/options")("secret");
4 |
5 | /**
6 | * @typedef AuthReqHeaders
7 | * @property {String} authorization;
8 | *
9 | *
10 | * @typedef AuthRequest
11 | * @property {AuthReqHeaders} headers;
12 | *
13 | * @typedef AuthTokenContext
14 | * @property {import('~crsm/controllers/definitions/AuthController').JwtPayload} authTokenData;
15 | *
16 | * @param {AuthRequest} param0
17 | * @returns {AuthTokenContext}
18 | */
19 | const authorization = ({
20 | headers: { authorization }
21 | }) => {
22 |
23 | // Nothing to do here
24 | if( !authorization ) return;
25 |
26 | // The token must start with "Bearer "
27 | if( authorization.substr(0, 7) !== "Bearer " )
28 |
29 | throw new AuthenticationError("Invalid Token");
30 |
31 | // Extract the token
32 | const token = authorization.slice(7);
33 |
34 | try {
35 |
36 | const authTokenData = jwt.verify(token, secret, { maxAge: 31556926 });
37 |
38 | // Decrypt the token and provide data as `user`
39 | return {
40 | authTokenData
41 | }
42 |
43 | } catch (err) {
44 |
45 | throw new AuthenticationError("Invalid Token")
46 |
47 | }
48 |
49 | }
50 |
51 | module.exports = authorization;
--------------------------------------------------------------------------------
/app/routes/rooms.js:
--------------------------------------------------------------------------------
1 | const { room: roomVal } = require("./validation");
2 | const { room: roomPerm } = require("../config/permissions");
3 |
4 | const ctrls = require("../controllers");
5 |
6 | const isRoomMember = require("./middleware/isRoomMember");
7 | const setRoom = require("./middleware/setRoom");
8 |
9 | const createRouter = require("./utils/createRouter");
10 |
11 | const sharedConfig = {
12 | auth: true,
13 | paramCheck: true,
14 | middleware: [ setRoom.fromParam, isRoomMember ],
15 | };
16 |
17 | module.exports = createRouter([
18 | ["/",{
19 | post: {
20 | defaultError: "create the room",
21 | validation: roomVal,
22 | auth: true,
23 | ctrl: ctrls.get("room")
24 | }
25 | }],
26 |
27 | ["/:roomId", {
28 | get: {
29 | defaultError: "get the room",
30 | permission: roomPerm,
31 | ctrl: ctrls.get("room")
32 | },
33 | patch: {
34 | defaultError: "update the room",
35 | validation: roomVal,
36 | permission: roomPerm,
37 | ctrl: ctrls.get("room")
38 | }
39 | }, sharedConfig],
40 |
41 | ["/:roomId/permissions", {
42 | get: {
43 | defaultError: "get your permissions for the room",
44 | ctrl: ctrls.get("room").binding.getPermissions
45 | }
46 | }, sharedConfig]
47 |
48 | ]);
--------------------------------------------------------------------------------
/test/suite/controllers/types/ControllerSchema/_suite/deleteOne.js:
--------------------------------------------------------------------------------
1 | const { expect } = require("chai");
2 | const { createSandbox } = require("sinon");
3 | const ObjectId = require("mongoose").Types.ObjectId;
4 |
5 | const SchemaController = require("~crsm/controllers/types/SchemaController");
6 |
7 | const TestModel = require("~crsmtest/lib/TestModel");
8 |
9 | module.exports = function() {
10 |
11 | const sandbox = createSandbox();
12 |
13 | before(() => sandbox.stub( TestModel, "deleteOne" ))
14 |
15 | after(() => sandbox.restore());
16 |
17 | it( "should call the model's deleteOne method with the provided `docId`", () => {
18 |
19 | // Arrange
20 | const ctrl = new SchemaController( "modelkey", TestModel );
21 | const docId = new ObjectId();
22 |
23 | // Act
24 | ctrl.deleteOne( { docId } );
25 |
26 | // Assert
27 | expect( TestModel.deleteOne.calledWithExactly( docId ) );
28 |
29 |
30 | } );
31 |
32 |
33 |
34 | it( "should call the model's deleteOne method with the provided document's `_id`", () => {
35 |
36 | // Arrange
37 | const ctrl = new SchemaController( "modelkey", TestModel );
38 | const doc = new TestModel({name: "A test"});
39 |
40 | // Act
41 | ctrl.deleteOne( { doc } );
42 |
43 | // Assert
44 | expect( TestModel.deleteOne.calledWithExactly( doc._id ) );
45 |
46 |
47 | } );
48 |
49 | }
--------------------------------------------------------------------------------
/client/src/components/Dropdown/index.js:
--------------------------------------------------------------------------------
1 | import { useReducer } from "react";
2 |
3 | import { useOutsideClickDispatch } from "utils/detection";
4 |
5 | import "./style.sass";
6 |
7 | const Dropdown = ( { id = "dropdown-menu", label, ariaLabel, labelSize, labelClassName="", children, className = "", ...props } ) => {
8 |
9 | const [ isActive, dispatch ] = useReducer( ( state, action ) => action === "open" );
10 | const dropdownRef = useOutsideClickDispatch( { isActive, dispatch, action: "close" } );
11 |
12 | const toggleBtnProps = {
13 | className: labelClassName + " button" + (labelSize ? ` is-${labelSize}` : ""),
14 | "aria-haspopup": "true",
15 | "aria-controls": id,
16 | onClick: () => dispatch("open")
17 | }
18 |
19 | if(ariaLabel) toggleBtnProps["aria-label"] = ariaLabel;
20 |
21 | return (
22 |
23 |
24 |
27 |
28 |
29 |
dispatch("close")}>
30 | {children}
31 |
32 |
33 |
34 | );
35 |
36 | }
37 |
38 | export default Dropdown;
--------------------------------------------------------------------------------
/client/src/layouts/components/TopNavbar.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | import { Navbar } from "react-bulma-components";
4 |
5 | import { LoginButton, LogoutButton } from "components/Login";
6 | import { useIsAuthenticated } from "utils/auth";
7 | import { Link } from "react-router-dom";
8 | import Icon from "components/Icon";
9 |
10 | const { Brand, Item, Burger, Menu, Container } = Navbar;
11 |
12 | const TopNavbar = () => {
13 |
14 | const [ isNavActive, setIsNavActive ] = useState( false );
15 |
16 | const isAuth = useIsAuthenticated();
17 |
18 | return(
19 |
20 |
21 | -
22 |
23 | {
24 | isAuth
25 | ? ( Back to Class )
26 | : Classroom
27 | }
28 |
29 | setIsNavActive( !isNavActive )} />
30 |
31 |
38 |
39 | )
40 |
41 | }
42 |
43 | export default TopNavbar;
--------------------------------------------------------------------------------
/client/src/components/Form/components/RichTextEditor/styles.sass:
--------------------------------------------------------------------------------
1 | .DraftEditor-root
2 | position: relative
3 |
4 | .DraftEditor-editor
5 | border-top: 1px solid #ddd
6 | cursor: text
7 | font-size: 16px
8 | margin-top: 10px
9 |
10 | .DraftEditor-editorContainer
11 | position: relative
12 |
13 | .public-DraftEditor-content
14 | margin: 0
15 |
16 | .public-DraftEditor-content[contenteditable="true"]
17 | background: #fff
18 | border: 1px solid #ddd
19 | padding: 15px
20 | min-height: 100px
21 |
22 | .public-DraftEditorPlaceholder-root
23 | padding: 15px
24 | position: absolute
25 | top: 0
26 | left: 0
27 |
28 | .DraftEditor-hidePlaceholder .public-DraftEditorPlaceholder-root
29 | display: none
30 |
31 | .DraftEditor-blockquote
32 | border-left: 5px solid #eee
33 | color: #666
34 | font-family: "Hoefler Text", "Georgia", serif
35 | font-style: italic
36 | margin: 16px 0
37 | padding: 10px 20px
38 |
39 | .public-DraftStyleDefault-pre
40 | background-color: rgba(0, 0, 0, 0.05)
41 | font-family: "Inconsolata", "Menlo", "Consolas", monospace
42 | font-size: 16px
43 | padding: 20px
44 |
45 | .DraftEditor-controls
46 | font-family: "Helvetica", sans-serif
47 | font-size: 14px
48 | margin-bottom: 5px
49 | user-select: none
50 |
51 | .DraftEditor-styleButton
52 | color: #999
53 | cursor: pointer
54 | margin-right: 16px
55 | padding: 2px 0
56 | display: inline-block
57 |
58 | .DraftEditor-activeButton
59 | color: #5890ff
--------------------------------------------------------------------------------
/app/config/permissions/PermissionSet.js:
--------------------------------------------------------------------------------
1 | const { InvalidConfig } = require("../errors");
2 |
3 | /**
4 | * @param {string} perm
5 | */
6 | const makePermMap = perm => [ perm, 1 ];
7 |
8 | class PermissionSet {
9 |
10 | /**
11 | * @param {string} name
12 | * @param {Array} permTypes
13 | */
14 | constructor( name, permTypes ) {
15 |
16 | this.name = name;
17 | this.key = name.split(' ').map((part,i)=>i ? (part[0].toUpperCase()+part.slice(1)) : part).join('');
18 | this.types = new Map( permTypes.map( makePermMap ) );
19 |
20 | }
21 |
22 | /**
23 | * @param {string} type
24 | * @throws {InvalidConfig}
25 | */
26 | validateType( type ) {
27 |
28 | if( !this.types.has(type) )
29 |
30 | throw new InvalidConfig( `${this.name} does not have a ${type} permission.` );
31 |
32 | }
33 |
34 | /**
35 | * @param {string} type
36 | */
37 | makeKey( type ) {
38 |
39 | this.validateType( type );
40 |
41 | return `${type}_${this.name}`.replace(" ", "_").toUpperCase();
42 |
43 | }
44 |
45 | get create() {
46 | return this.makeKey( "create" );
47 | }
48 |
49 | get view() {
50 | return this.makeKey( "view" );
51 | }
52 |
53 | get update() {
54 | return this.makeKey( "update" );
55 | }
56 |
57 | get delete() {
58 | return this.makeKey( "delete" );
59 | }
60 |
61 | }
62 |
63 | module.exports = PermissionSet
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/components/Topbar/index.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import "./style.sass";
3 | import { useDashboardContext, getDashboardAction as gda } from "pages/Dashboard/store";
4 | import { SET_TOPBAR } from "pages/Dashboard/store/actionsNames";
5 |
6 | import { Heading } from "react-bulma-components";
7 |
8 | export const useTopbarConfig = ( { name, tools } ) => {
9 |
10 | const [ ,dispatch ] = useDashboardContext();
11 |
12 | const [ state, setState ] = useState({
13 | name,
14 | tools
15 | });
16 |
17 | useEffect(() => {
18 | setState({ name, tools });
19 | }, [name, tools, setState]);
20 |
21 | useEffect(() => {
22 |
23 | dispatch(gda( SET_TOPBAR, state ));
24 |
25 | return () => dispatch(gda( SET_TOPBAR, undefined ))
26 |
27 | }, [state, dispatch]);
28 |
29 | return [ state, setState ];
30 |
31 | };
32 |
33 | const Topbar = () => {
34 |
35 | const [ { topbar } ] = useDashboardContext();
36 |
37 | return topbar
38 | ? (
39 |
40 |
{topbar.name}
41 | { topbar.tools ?
{topbar.tools}
: null }
42 |
43 | )
44 |
45 | : null
46 |
47 | }
48 |
49 | export default Topbar;
--------------------------------------------------------------------------------
/app/controllers/definitions/models/schema/StudentSchema/index.js:
--------------------------------------------------------------------------------
1 | const Schema = require("mongoose").Schema;
2 | const { ObjectId } = Schema.Types;
3 |
4 | const methods = require("./methods")
5 |
6 | const FeedEntrySchema = require("../FeedEntrySchema");
7 |
8 | /**
9 | * @typedef {Object} StudentSchema
10 | * @property {ObjectId} _id
11 | * @property {string} name
12 | * @property {Number} priorityLevel
13 | * @property {(ObjectId|null)} assignedTo
14 | * @property {ObjectId} feed
15 | *
16 | * @property {*} getFeedAggregateData
17 | * @property {*} getAggregateKeysByAction
18 | *
19 | * @typedef {import('mongoose').Document & StudentSchema} StudentDocument
20 | */
21 | const StudentSchema = new Schema({
22 | name: {
23 | type: String,
24 | required: true
25 | },
26 | priorityLevel: {
27 | type: Number,
28 | min: 1,
29 | max: 10,
30 | required: true
31 | },
32 | assignedTo: {
33 | type: ObjectId
34 | },
35 | feed: {
36 | type: ObjectId,
37 | ref: "Feed",
38 | required: true
39 | },
40 | date: {
41 | type: Date,
42 | default: Date.now
43 | },
44 | meta: {
45 | type: Map,
46 | default: {}
47 | },
48 | elevation: {
49 | type: Number,
50 | default: 0
51 | },
52 | recentComments: [ FeedEntrySchema ]
53 | });
54 |
55 | StudentSchema.methods.getAggregateKeysByAction = methods.getAggregateKeysByAction;
56 |
57 | StudentSchema.methods.getFeedAggregateData = methods.getFeedAggregateData;
58 |
59 | module.exports = StudentSchema;
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cms",
3 | "version": "0.1.0",
4 | "private": true,
5 | "proxy": "http://localhost:3001/",
6 | "main": "src/index.js",
7 | "ws": true,
8 | "dependencies": {
9 | "@draft-js-plugins/editor": "4.1.2",
10 | "@draft-js-plugins/static-toolbar": "^4.1.0",
11 | "@fortawesome/fontawesome-svg-core": "1.3.0",
12 | "@fortawesome/free-brands-svg-icons": "6.0.0",
13 | "@fortawesome/free-regular-svg-icons": "6.0.0",
14 | "@fortawesome/free-solid-svg-icons": "6.0.0",
15 | "@fortawesome/react-fontawesome": "0.1.17",
16 | "axios": "0.26.0",
17 | "bulma": "^0.9.2",
18 | "bulma-slider": "2.0.4",
19 | "draft-js": "^0.11.7",
20 | "jwt-decode": "^3.1.2",
21 | "luxon": "2.3.0",
22 | "node-sass": "7.0.1",
23 | "react": "^17.0.2",
24 | "react-bulma-components": "4.1.0",
25 | "react-copy-to-clipboard": "^5.0.3",
26 | "react-dom": "^17.0.2",
27 | "react-router-dom": "5.3.0",
28 | "react-scripts": "5.0.0",
29 | "socket.io-client": "4.4.1",
30 | "workbox-core": "^6.1.5",
31 | "workbox-precaching": "^6.1.5",
32 | "workbox-routing": "^6.1.5",
33 | "workbox-strategies": "^6.1.5"
34 | },
35 | "scripts": {
36 | "start": "react-scripts start",
37 | "build": "react-scripts build",
38 | "test": "react-scripts test --env=jsdom",
39 | "eject": "react-scripts eject"
40 | },
41 | "browserslist": [
42 | ">0.2%",
43 | "not dead",
44 | "not ie <= 11",
45 | "not op_mini all"
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/components/StudentModal/panels/ActivityPanel.js:
--------------------------------------------------------------------------------
1 | import { useReducer } from "react";
2 |
3 | import {
4 | Box,
5 | Button,
6 | } from "react-bulma-components";
7 |
8 | import { useOutsideClickDispatch } from "utils/detection";
9 | import { ActivtyFeed, CommentForm } from "../components";
10 |
11 | export const ActivityPanel = ({ student }) => {
12 |
13 | const [isActive, dispatchComment] = useReducer(
14 | (state, action) => action === "open"
15 | );
16 |
17 | const commentRef = useOutsideClickDispatch({
18 | isActive,
19 | dispatch: dispatchComment,
20 | action: "close"
21 | });
22 |
23 | return (
24 | <>
25 |
29 |
30 | {isActive
31 |
32 | ? ()
33 |
34 | : (
35 |
40 | )
41 | }
42 |
43 | >
44 | )
45 | }
--------------------------------------------------------------------------------
/app/controllers/utils/queryModifier.js:
--------------------------------------------------------------------------------
1 | /**
2 | * TYPE DEFINITION IMPORTS
3 | * @typedef {import('mongoose').Query} Query
4 | */
5 |
6 | /**
7 | * @param {Query} query
8 | * @param {Array} populations
9 | */
10 | const populateMod = ( query, populations ) => {
11 |
12 | if( false === populations ) return;
13 |
14 | if( typeof populations === "string" ) {
15 | query.populate( populations );
16 | return;
17 | }
18 |
19 | for( populateParams of populations )
20 |
21 | query.populate( ...(Array.isArray( populateParams ) ? populateParams : [populateParams]) );
22 |
23 | };
24 |
25 | /**
26 | * @param {Query} query
27 | * @param {Array} populations
28 | */
29 | const selectMod = ( query, select ) => {
30 |
31 | query.select( select );
32 |
33 | }
34 |
35 | const modifierMap = {
36 | populate: populateMod,
37 | select: selectMod
38 | }
39 |
40 | /**
41 | * @typedef QueryModifierOptions
42 | * @property {Array} populate - A list of populate configurations.
43 | * @property {string} select - A string of member names to select.
44 | *
45 | * @param {Query} query
46 | * @param {QueryModifierOptions} options
47 | * @returns {Query}
48 | */
49 | const queryModifier = ( query, options = {} ) => {
50 |
51 | for( [key, value] of Object.entries( options ) ) {
52 |
53 | if( !modifierMap.hasOwnProperty(key) ) continue;
54 |
55 | modifierMap[key]( query, value );
56 |
57 | };
58 |
59 | return query;
60 |
61 | }
62 |
63 | module.exports = queryModifier;
--------------------------------------------------------------------------------
/app/controllers/definitions/models/schema/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Type definitions for easy sharing.
3 | *
4 | * @typedef {import('./MemberSchema').MemberDocument} MemberDocument
5 | * @typedef {import('./MemberSchema').MemberSchema} MemberSchema
6 | *
7 | * @typedef {import('./InviteSchema').InviteDocument} InviteDocument
8 | * @typedef {import('./InviteSchema').InvitesSchema} InvitesSchema
9 | *
10 | * @typedef {import('./StudentSchema').StudentDocument} StudentDocument
11 | * @typedef {import('./StudentSchema').StudentSchema} StudentSchema
12 | *
13 | * @typedef {import('./RoomSchema').RoomDocument} RoomDocument
14 | * @typedef {import('./RoomSchema').RoomSchema} RoomSchema
15 | *
16 | * @typedef {import('./FeedEntrySchema').FeedEntryDocument} FeedEntryDocument
17 | * @typedef {import('./FeedEntrySchema').FeedEntrySchema} FeedEntrySchema
18 | *
19 | * @typedef {import('./FeedSchema').FeedDocument} FeedDocument
20 | * @typedef {import('./FeedSchema').FeedSchema} FeedSchema
21 | *
22 | * @typedef {import('./TokenSchema').TokenDocument} TokenDocument
23 | * @typedef {import('./TokenSchema').TokenSchema} TokenSchema
24 | *
25 | * @typedef {import('./UserSchema').UserDocument} UserDocument
26 | * @typedef {import('./UserSchema').UserSchema} UserSchema
27 | *
28 | * @typedef {import('./AppTypeSchema').AppTypeDocument} AppTypeDocument
29 | * @typedef {import('./AppTypeSchema').AppTypeSchema} AppTypeSchema
30 | *
31 | * @typedef {import('./AppSchema').AppDocument} AppDocument
32 | * @typedef {import('./AppSchema').AppSchema} AppSchema
33 | */
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Instructor Utilities
2 |
3 | **Live:** https://cr-sm.herokuapp.com/
4 |
5 | **Staging (for weekly builds):** https://cr-sm-staging.herokuapp.com/
6 |
7 | ## Description
8 |
9 | This application consists of tools and utilities built to assist teachers instruct boot camp classes.
10 |
11 | ## Table of Contents
12 |
13 | * [Installation](#installation)
14 | * [Usage](#usage)
15 | * [Credits](#credits)
16 | * [License](#license)
17 |
18 | ## Installation
19 |
20 | * `npm install` - Install dependencies.
21 | * `npm run envsetup` - Generate a local .env file with the needed configuration.
22 | * `npm start` - Launch local development server.
23 |
24 | ## Registration Codes
25 |
26 | * `node regisitercode MY-CODE-NAME` - Create a new registration code for signup. Code is auto generated if not provided. Provided values will be formatted into UPPERCASE format.
27 |
28 | ### Demo Seed Data
29 |
30 | * `npm run seed` - Seed the application database with demo data
31 | * `npm run seed:reset` - Clear all collections
32 |
33 | ## Usage
34 |
35 | Prominent features include:
36 |
37 | * Creating Classrooms
38 | * Inviting TAs
39 | * Adding students
40 | * Assigning students to TAs
41 | * Adding student comments
42 | * Elevating students up the staff hierarchy
43 |
44 | ## Credits
45 |
46 | Lead Engineer: Anthony Brown
47 | Students from the UW Coding Boot Camp: Billy Hao, Christopher Marti, Niv Swamy and Trenton Creamer
48 |
49 | ## License
50 |
51 | [](https://www.gnu.org/licenses/gpl-3.0)
--------------------------------------------------------------------------------
/test/run.js:
--------------------------------------------------------------------------------
1 | const fs = require ('fs');
2 | const path = require('path');
3 | const util = require('util');
4 | const mocha = require('mocha');
5 | const chai = require('chai');
6 | const chaiAsPromised = require('chai-as-promised');
7 | const sinonChai = require("sinon-chai");
8 |
9 | chai.use(sinonChai);
10 | chai.use(chaiAsPromised);
11 |
12 | const readDir = util.promisify( fs.readdir );
13 |
14 | // Starting diretor to scan.
15 | const dirname = "suite";
16 |
17 | // Recursively looks for files in folders and builds on list of all found .js files.
18 | const getDirFiles = async folder => {
19 |
20 | const files = [];
21 |
22 | for( filename of await readDir( folder )) {
23 |
24 | const filePath = path.join(folder, filename);
25 |
26 | const match = filename.match(/\.js$/);
27 |
28 | if( !match ) {
29 | // Ignore folders prefixed with "_"
30 | if( filename[0] !== "_" )
31 |
32 | files.push( ...(await getDirFiles( filePath )) );
33 |
34 | continue;
35 | }
36 |
37 | // Push files named `test.js` or that end with `.test.js`.
38 | if( "test.js" === filename || ".test.js" === filename.substr(-8) ) files.push( filePath );
39 |
40 | }
41 |
42 | return files;
43 |
44 | }
45 |
46 | getDirFiles( path.join(__dirname, dirname) )
47 | .then(files => {
48 |
49 | const suite = new mocha();
50 |
51 | // Add each found file to the suite.
52 | files.forEach(file => suite.addFile( file ));
53 |
54 | // Run the suite.
55 | suite.run( failures => {
56 | process.exit(failures);
57 | });
58 |
59 | });
--------------------------------------------------------------------------------
/app/controllers/definitions/models/schema/RoomSchema.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const Schema = mongoose.Schema;
3 | const { ObjectId } = Schema.Types;
4 |
5 | /**
6 | * Type Definition Imports
7 | * @typedef {import('./InviteSchema').InviteDocument} InviteDocument
8 | * @typedef {import('./MemberSchema').MemberDocument} MemberDocument
9 | * @typedef {import('./StudentSchema').StudentDocument} StudentDocument
10 | */
11 |
12 | const InviteSchema = require("./InviteSchema");
13 | const MemberSchema = require("./MemberSchema");
14 | const StudentSchema = require("./StudentSchema");
15 |
16 | /**
17 | * @typedef {Object} RoomSchema
18 | * @property {ObjectId} _id
19 | * @property {string} name
20 | * @property {MemberDocument[]} staff
21 | * @property {StudentDocument[]} students
22 | * @property {InviteDocument[]} invites
23 | * @property {ObjectId} registerCode
24 | * @property {ObjectId[]} apps
25 | *
26 | * @property {*} isAllowedTo
27 | * @property {*} getPermissionList
28 | *
29 | * @typedef {import('mongoose').Document & RoomSchema} RoomDocument
30 | */
31 | const RoomSchema = new Schema({
32 | name: {
33 | type: String,
34 | required: true
35 | },
36 | staff: [MemberSchema],
37 | students: [StudentSchema],
38 | invites: [InviteSchema],
39 | registerCode: {
40 | type: ObjectId,
41 | ref:"Token",
42 | },
43 | apps: [{
44 | type: ObjectId,
45 | ref:"AppType",
46 | }],
47 | date: {
48 | type: Date,
49 | default: Date.now
50 | }
51 | });
52 |
53 | module.exports = RoomSchema;
--------------------------------------------------------------------------------
/app/routes/user.js:
--------------------------------------------------------------------------------
1 | const ctrls = require("../controllers");
2 |
3 | const { user: userVal } = require("./validation");
4 | const { room: roomPerm } = require("../config/permissions");
5 |
6 | const setRoom = require("./middleware/setRoom");
7 | const isRoomMember = require("./middleware/isRoomMember");
8 |
9 | const createRouter = require("./utils/createRouter");
10 |
11 | const sharedRoomActionsConfig = {
12 | paramCheck: true,
13 | middleware: [ setRoom.fromParam, isRoomMember ]
14 | }
15 |
16 | module.exports = createRouter([
17 |
18 | ["/", {
19 | patch: {
20 | auth: true,
21 | defaultError: "update the user",
22 | validation: userVal,
23 | ctrl: ctrls.get("user")
24 | }
25 | }],
26 |
27 | ["/rooms/:roomId/leave", {
28 | delete: {
29 | auth: true,
30 | defaultError: "leave the room",
31 | permission: roomPerm.leave,
32 | ctrl: ctrls.get("user").binding.leaveRoom
33 | }
34 | }, sharedRoomActionsConfig],
35 |
36 | ["/rooms/:roomId/archive", {
37 | delete: {
38 | auth: true,
39 | defaultError: "archive the room",
40 | permission: roomPerm.archive,
41 | ctrl: ctrls.get("user").binding.archiveRoom
42 | }
43 | }, sharedRoomActionsConfig],
44 |
45 | ["/rooms/short", {
46 | get: {
47 | auth: true,
48 | defaultError: "get short room details",
49 | ctrl: ctrls.get("user").binding.getRoomsBasics
50 | }
51 | }]
52 |
53 | ]);
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/components/SortSelectDropdown.js:
--------------------------------------------------------------------------------
1 |
2 | import {
3 | Button
4 | } from "react-bulma-components";
5 |
6 | import Dropdown from "components/Dropdown";
7 | import Icon from "components/Icon";
8 |
9 | const sortTypes = [
10 | {
11 | key: "name:asc",
12 | label: "By name A to Z",
13 | icon: "sort-alpha-down"
14 | },
15 | {
16 | key: "name:desc",
17 | label: "By name Z to A",
18 | icon: "sort-alpha-up-alt"
19 | },
20 | {
21 | key: "priorityLevel:asc",
22 | label: "By priority 1 to 10",
23 | icon: "sort-numeric-down"
24 | },
25 | {
26 | key: "priorityLevel:desc",
27 | label: "By priority 10 to 1",
28 | icon: "sort-numeric-up-alt"
29 | }
30 | ];
31 |
32 | const SortSelectDropdown = ( { state: [ value, set ], ...props } ) => {
33 |
34 | return (
35 | key === value ).icon} />} {...props}>
36 | {sortTypes.map( sortType => {
37 | const classes = ["dropdown-item"];
38 |
39 | if( sortType.key === value ) classes.push("is-active");
40 |
41 | return (
42 |
46 | )
47 | })}
48 |
49 | )
50 |
51 | }
52 |
53 | export default SortSelectDropdown;
--------------------------------------------------------------------------------
/client/src/utils/socket.io.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import socketIOClient from "socket.io-client";
3 | import api from "./api";
4 | import { useStoreDispatch, getStoreAction as gsa, useStoreContext } from "../store";
5 | import { SET_SOCKET, UPDATE_USER } from "../store/actions";
6 | import { useIsAuthenticated } from "./auth";
7 |
8 | export const useSocketConnection = () => {
9 |
10 | const dispatch = useStoreDispatch();
11 |
12 | useEffect(() => {
13 |
14 | const openSocket = socketIOClient(`${window.location.origin}`, {
15 | transportOptions: {
16 | polling: {
17 | extraHeaders: {
18 | 'Authorization': localStorage.getItem("jwtToken")
19 | }
20 | }
21 | }
22 | });
23 |
24 | // openSocket.on("disconnet", message => {
25 | // console.log("lost");
26 | // });
27 |
28 | openSocket.on("connect", () => {
29 | api.setHeader( "User-Socket-Id", openSocket.id )
30 | dispatch( gsa( SET_SOCKET, openSocket ) );
31 | });
32 |
33 | // openSocket.on("test", message => {
34 | // console.log(message);
35 | // });
36 |
37 | return () => {
38 | openSocket.disconnect();
39 | api.setHeader( "User-Socket-Id", false );
40 | dispatch( gsa( SET_SOCKET, false ) );
41 | }
42 |
43 | }, [dispatch]);
44 |
45 | };
46 |
47 | export const useSocket = () => {
48 |
49 | const [ { socket } ] = useStoreContext();
50 |
51 | return socket;
52 |
53 | }
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/store/index.js:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useReducer } from "react";
2 |
3 | import actions from "./actions";
4 |
5 | const DashboardContext = createContext([
6 | {
7 | classroom: null,
8 | permissions: new Map(),
9 | editStudent: "",
10 | studentFeed: null,
11 | topbar: {},
12 | isManagingApps: false
13 | },
14 | () => undefined
15 | ]);
16 |
17 | const { Provider } = DashboardContext;
18 |
19 | const reducer = ( state, action ) => {
20 |
21 | const doAction = (state, { type, payload }) => {
22 |
23 | return actions.hasOwnProperty( type )
24 |
25 | ? actions[type]( state, payload )
26 |
27 | : state;
28 |
29 | }
30 |
31 | return Array.isArray( action )
32 |
33 | ? action.reduce( doAction, state )
34 |
35 | : doAction( state, action );
36 |
37 | }
38 |
39 | export const getDashboardAction = ( type, payload ) => {
40 |
41 | return { type, payload };
42 |
43 | }
44 |
45 | export const DashboardProvider = ( { children } ) => {
46 |
47 | const reducerState = useReducer( reducer, {
48 | classroom: null,
49 | permissions: new Map(),
50 | editStudent: false,
51 | studentFeed: null,
52 | topbar: undefined,
53 | isManagingApps: false
54 | } );
55 |
56 | return { children }
57 |
58 | }
59 |
60 | export const useDashboardContext = () => {
61 |
62 | return useContext( DashboardContext );
63 |
64 | }
65 |
66 | export * from "./getters";
67 | export * from "./loaders";
--------------------------------------------------------------------------------
/app/config/apps/register.js:
--------------------------------------------------------------------------------
1 | const appTypes = require("./registry.json");
2 | const appTypeLibrary = require("./library");
3 |
4 | const library= require("../../controllers");
5 |
6 | const registerAppTypes = async () => {
7 |
8 | const registered = await library.get("appType").findMany({ search: {} });
9 |
10 | const processed = [];
11 | const types = Object.keys( appTypes );
12 |
13 | // Update existing entries.
14 | for( let i = 0; i < registered.length; i++ ) {
15 |
16 | appTypeLibrary.set( registered[i]._id.toString(), registered[i] );
17 |
18 | processed.push( registered[i].type );
19 |
20 | const updates = [];
21 | const appRegistry = appTypes[registered[i].type] ? appTypes[registered[i].type] : false;
22 |
23 | if( !appRegistry ) {
24 |
25 | if( !registered[i].isDisabled ) updates.push(["isDisabled", true]);
26 |
27 | } else {
28 |
29 | if( registered[i].isDisabled ) updates.push(["isDisabled", false]);
30 |
31 | }
32 |
33 | if( updates.length ) await registered[i].update( Object.fromEntries(updates) );
34 |
35 | }
36 |
37 | const toRegister = types.filter( type => !processed.includes(type) );
38 |
39 | // Register new apps.
40 | for( let i = 0; i < toRegister.length; i++ ) {
41 |
42 | const appType = await library.get("appType").createOne({
43 | data: {
44 | type: toRegister[i],
45 | }
46 | });
47 |
48 | appTypeLibrary.set( appType._id.toString(), appType );
49 |
50 | }
51 |
52 | }
53 |
54 | module.exports = registerAppTypes;
--------------------------------------------------------------------------------
/client/src/layouts/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | import {
4 | Section,
5 | Container,
6 | Columns,
7 | Heading
8 | } from "react-bulma-components";
9 |
10 | import "./style.sass";
11 | import WebLink from "components/WebLink";
12 | import { useIsAuthenticated } from "utils/auth";
13 |
14 | const { Column } = Columns;
15 |
16 | const Footer = () => {
17 |
18 | const isAuth = useIsAuthenticated();
19 |
20 | return (
21 |
22 |
23 |
24 |
25 | Links
26 | { isAuth ? null : Register
}
27 | Meet the devs
28 | Privacy
29 |
30 |
31 | Built On
32 | React
33 | Bulma
34 | Node.js
35 | Mongo
36 |
37 |
38 |
39 |
40 | );
41 |
42 | }
43 |
44 | export default Footer;
--------------------------------------------------------------------------------
/test/suite/graphql/middleware/_suite/authentication.requireVerifiedUser.js:
--------------------------------------------------------------------------------
1 | const { expect } = require("chai");
2 | const { stub } = require("sinon");
3 |
4 | const { AuthenticationError } = require("apollo-server-express");
5 |
6 | const { requireVerifiedUser } = require("~crsm/graphql/middleware/authentication");
7 |
8 | module.exports = function() {
9 | it("should call the `next` method if provided a truthy value for `authUser.isVerified` in `context`", async () => {
10 |
11 | const context = {
12 | authUser: { isVerified: true }
13 | }
14 |
15 | const next = stub();
16 |
17 | requireVerifiedUser( { context }, next )
18 |
19 | expect( next ).to.have.been.called;
20 |
21 | });
22 |
23 | /** EXCEPTION TESTS **/
24 |
25 | describe('Exceptions', () => {
26 |
27 | it("should throw an `AuthenticationError` if provided a falsy value for `authUser.isVerified` in `context`", async () => {
28 |
29 | const context = {
30 | authUser: { isVerified: false }
31 | }
32 |
33 | const act = () => requireVerifiedUser( { context }, () => {} );
34 |
35 | expect( act ).to.throw( AuthenticationError, "User is not verified" );
36 |
37 | });
38 |
39 | it("should throw an `AuthenticationError` if not provided a value for `authUser.isVerified` in `context`", async () => {
40 |
41 | const context = {}
42 |
43 | const act = () => requireVerifiedUser( { context }, () => {} );
44 |
45 | expect( act ).to.throw( AuthenticationError, "User is not verified" );
46 |
47 | });
48 |
49 | });
50 | }
--------------------------------------------------------------------------------
/app/controllers/types/Controller.js:
--------------------------------------------------------------------------------
1 | const library = require("./library");
2 |
3 | /**
4 | * @see https://ponyfoo.com/articles/binding-methods-to-class-instance-objects#proxies
5 | *
6 | * @param {Controller} controller
7 | *
8 | * @returns {Controller}
9 | */
10 | const makeFnBindingMap = controller => {
11 |
12 | const cache = new WeakMap();
13 |
14 | const handler = {
15 | get (target, key) {
16 |
17 | const value = Reflect.get(target, key);
18 |
19 | if (typeof value !== 'function') return value;
20 |
21 | if (!cache.has(value)) cache.set(value, value.bind(target));
22 |
23 | return cache.get(value);
24 |
25 | }
26 | };
27 |
28 | const proxy = new Proxy(controller, handler);
29 |
30 | return proxy;
31 |
32 | }
33 |
34 | class Controller {
35 |
36 | constructor( ctrlKey ) {
37 |
38 | /** @type {this} - A proxy of the object that can return the functions with .bind() applied. */
39 | this.binding = makeFnBindingMap( this );
40 |
41 | this.ctrlKey = ctrlKey;
42 |
43 | // TODO Fix unit testing so this error can be reenabled.
44 | // if( library.has( ctrlKey ) ) throw Error( "Cannot register duplicate controller keys" );
45 |
46 | library.set( ctrlKey, this );
47 |
48 | }
49 |
50 | /**
51 | * Returns another controller instance by key from the library to allow external side effects.
52 | * @param {String} ctrlKey
53 | * @returns {import('./SchemaController')|import('./SubSchemaController')}
54 | */
55 | effect( ctrlKey ) {
56 |
57 | return library.get( ctrlKey );
58 |
59 | }
60 |
61 | }
62 |
63 | module.exports = Controller;
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/components/StudentModal/panels/StudentPanel.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | import {
4 | Box,
5 | Heading,
6 | Tag,
7 | Button,
8 | } from "react-bulma-components";
9 |
10 | import {
11 | SettingsForm,
12 | StudentOptions,
13 | } from "../components";
14 |
15 | const StudenPanelHeading = ({ isNew }) => {
16 | return (
17 |
18 | {isNew ? "New" : "Edit"} Student
19 |
20 | );
21 | }
22 |
23 | export const StudentPanel = ( { roomId, student } ) => {
24 |
25 | const isNew = !student._id;
26 |
27 | const [ isBulkCreate, setIsBulkCreate ] = useState(false);
28 |
29 | const toggleBulkCreate = () => setIsBulkCreate( !isBulkCreate );
30 |
31 | return (
32 |
33 |
34 |
35 | {student.elevation ? (
36 |
37 |
38 |
39 | ) : null}
40 | {isNew ? (
41 |
42 |
48 |
49 | ) : (
50 |
51 |
56 |
57 | )}
58 |
59 |
60 |
65 |
66 | );
67 | }
--------------------------------------------------------------------------------
/app/routes/middleware/setRoom.js:
--------------------------------------------------------------------------------
1 | const { NotFoundError } = require("../../config/errors");
2 |
3 | const ctrls = require("../../controllers");
4 |
5 | const setRoom = async (req, next) => {
6 |
7 | try {
8 |
9 | const roomId = req.crdata.get("roomId");
10 | const classroom = await ctrls.get("room").findOne( { docId: roomId }, { select: "staff" } );
11 |
12 | if( !classroom ) throw new NotFoundError("Classroom not found");
13 |
14 | req.crdata.set( "room", classroom );
15 |
16 | next();
17 |
18 | } catch(err) {
19 |
20 | next(err);
21 |
22 | }
23 |
24 | }
25 |
26 | module.exports = {
27 | async fromBody(req, res, next) {
28 |
29 | req.crdata.set( "roomId", req.body.roomId || req.body.room );
30 |
31 | await setRoom(req, next);
32 |
33 | },
34 | async fromBody(req, res, next) {
35 |
36 | req.crdata.set( "roomId", req.body.roomId || req.body.room );
37 |
38 | await setRoom(req, next);
39 |
40 | },
41 | async fromParam(req, res, next) {
42 |
43 | req.crdata.set( "roomId", req.params.roomId );
44 |
45 | await setRoom(req, next);
46 |
47 | },
48 | async fromFeed(req, res, next) {
49 |
50 | req.crdata.set( "roomId", req.crdata.get("feed").room );
51 |
52 | await setRoom(req, next);
53 |
54 | },
55 | async fromStudent(req, res, next) {
56 |
57 | try {
58 |
59 | const room = await ctrls.get("room.student").findOwner({ docId: req.params.studentId }, { select: "_id" });
60 |
61 | req.crdata.set( "roomId", room._id );
62 |
63 | await setRoom(req, next);
64 |
65 | } catch( err ) {
66 |
67 | next(err);
68 |
69 | }
70 |
71 | }
72 | }
--------------------------------------------------------------------------------
/client/src/pages/Home/components/MainHero.js:
--------------------------------------------------------------------------------
1 | import {
2 | Hero,
3 | Heading,
4 | Container,
5 | Button
6 | } from "react-bulma-components";
7 |
8 | import { Link } from "react-router-dom";
9 | import Fade from "animations/Fade";
10 | import Icon from "components/Icon";
11 |
12 | const MainHero = () => {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Classroom
22 |
23 |
24 |
25 | Student management tools for instructional staff
26 |
27 |
28 |
29 | Classroom is a management utility helping instructional staff
30 |
better support students in an online learning evironment.
31 |
32 |
33 |
34 |
38 |
39 |
40 |
41 |
42 |
43 | )
44 | }
45 |
46 | export default MainHero;
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/views/UserSettings/components/ClassroomModal/components/ClassroomForm.js:
--------------------------------------------------------------------------------
1 | import api from "utils/api";
2 | import Form from "components/Form";
3 | import Pulse from "components/Pulse";
4 | import { createValidator } from "utils/validation";
5 | import { useStoreDispatch, getStoreAction as gsa } from "store";
6 | import { ADD_USER_ROOM_ID, REFRESH_USER_ROOMS } from "store/actions";
7 |
8 | const validateClassroomData = createValidator({
9 | validators: {
10 | name: ({ name }) => Boolean(name) || "Classroom name is required"
11 | }
12 | });
13 |
14 | const ClassroomForm = ({ room, afterUpdate }) => {
15 | const dispatch = useStoreDispatch();
16 |
17 | const handleSubmit = async (data, setErrors) => {
18 | const updateList = Object.entries(data).filter(
19 | ([key, value]) => value !== room[key]
20 | );
21 |
22 | if (updateList.length) {
23 | const updates = Object.fromEntries(updateList);
24 |
25 | try {
26 | if (!room._id) {
27 | const { data } = await api.createClassroom(updates);
28 | await dispatch(gsa(ADD_USER_ROOM_ID, data._id));
29 | if (afterUpdate) afterUpdate();
30 |
31 | return;
32 | }
33 |
34 | await api.updateClassroom(room._id, updates);
35 | dispatch(gsa(REFRESH_USER_ROOMS));
36 | if (afterUpdate) afterUpdate();
37 | } catch (err) {
38 | if (err.response) setErrors(err.response.data);
39 | }
40 | }
41 | };
42 |
43 | return room ? (
44 |
58 | ) : (
59 |
60 | );
61 | };
62 |
63 | export default ClassroomForm;
--------------------------------------------------------------------------------
/app/controllers/definitions/AppController.js:
--------------------------------------------------------------------------------
1 | const appTypes = require("../../config/apps/registry.json");
2 |
3 | const { App } = require("./models");
4 |
5 | const SchemaController = require("../types/SchemaController");
6 |
7 | /**
8 | * TYPE DEFINITION IMPORTS
9 | * @typedef {import('mongoose').Schema.Types.ObjectId} ObjectId
10 | * @typedef {import('./models/schema/AppSchema').AppDocument} AppDocument
11 | *
12 | * @typedef {import("../types/SchemaController").CreateDocOptions} CreateDocOptions
13 | * @typedef {import("../types/SchemaController").CreateDocConfig} CreateDocConfig
14 | */
15 |
16 | class AppController extends SchemaController {
17 |
18 | queryDefaults = {
19 | populate: "type"
20 | }
21 |
22 | constructor() {
23 |
24 | super( 'app', App );
25 |
26 | }
27 |
28 | /**
29 | * @param {CreateDocOptions} param0
30 | * @param {CreateDocConfig} config
31 | *
32 | * @returns {AppDocument}
33 | */
34 | async createOne( { data: { type, room, ...data } }, config ) {
35 |
36 | const appType = await this.effect("appType").findOne({ docId: type });
37 |
38 | const app = await super.createOne({
39 | data: {
40 | name: appTypes[ appType.type ].name,
41 | data: appTypes[ appType.type ].default,
42 | room,
43 | type,
44 | ...data
45 | }
46 | }, config );
47 |
48 | await this.effect("room").updateOne({
49 | docId: room,
50 | data: { $push: { apps: appType._id } },
51 | });
52 |
53 | // Populate the type document.
54 | app.type = appType;
55 |
56 | // Fetch the new doc to populate the type and return.
57 | return app;
58 |
59 | }
60 |
61 | }
62 |
63 | module.exports = AppController;
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/components/StudentModal/components/ActivityFeed/index.js:
--------------------------------------------------------------------------------
1 | import Pulse from "components/Pulse";
2 | import { useStudentFeed, useStudentFeedLoader } from "pages/Dashboard/store";
3 | import { useEffect, useRef } from "react";
4 |
5 | import {
6 | Comment,
7 | Created,
8 | Elevate,
9 | Deelevate
10 | } from "./entries"
11 |
12 | import "./style.sass";
13 |
14 | const typeMap = {
15 | create: Created,
16 | comment: Comment,
17 | elevate: Elevate,
18 | deelevate: Deelevate
19 | }
20 |
21 | export const ActivtyFeed = ({ student, className = "", style = {}, ...props }) => {
22 |
23 | const { feed } = student;
24 | const feedRef = useRef();
25 |
26 | useStudentFeedLoader(feed);
27 | const entries = useStudentFeed();
28 |
29 | const feedEntryComponentMap = item => {
30 |
31 | if( !typeMap[item.action] ) return null;
32 |
33 | const Entry = typeMap[item.action];
34 |
35 | return
36 |
37 | }
38 |
39 | useEffect(()=>{
40 |
41 | if(!feedRef.current) return;
42 |
43 | feedRef.current.scrollTop = feedRef.current.scrollHeight;
44 |
45 | },[feedRef, entries]);
46 |
47 | return (
48 | entries
49 | ? (
50 |
51 |
52 | {entries.map( feedEntryComponentMap )}
53 |
54 |
55 | )
56 | :
57 | );
58 |
59 | };
60 |
61 | export { CommentForm } from "./components/CommentForm";
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/views/Team/index.js:
--------------------------------------------------------------------------------
1 |
2 | import {
3 | Section,
4 | Columns
5 | } from "react-bulma-components";
6 |
7 | import { useTopbarConfig } from "pages/Dashboard/components/Topbar";
8 | import StaffListControls from "./components/StaffListControls";
9 | import { useStaffByRole } from "pages/Dashboard/store";
10 | import { Redirect, Route, Switch, useParams } from "react-router-dom";
11 | import StaffGroupPanel from "./components/StaffGroupPanel";
12 | import StudentModal from "pages/Dashboard/components/StudentModal";
13 | import Member from "./components/Member";
14 | import RequirePerm from "pages/Dashboard/components/RequirePerm";
15 |
16 | const { Column } = Columns;
17 |
18 | const Team = () => {
19 |
20 | useTopbarConfig({ name: "Team" });
21 |
22 | const staff = useStaffByRole();
23 | const { roomId } = useParams();
24 |
25 | return (
26 |
27 |
28 |
29 |
30 | {staff.instructor && }
31 | {staff.ta && }
32 |
33 |
34 |
35 |
36 | } />
37 |
38 |
39 |
40 |
41 |
42 | );
43 |
44 | }
45 |
46 | export default Team;
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/utils/icons.js:
--------------------------------------------------------------------------------
1 | import { library } from '@fortawesome/fontawesome-svg-core';
2 |
3 | import {
4 | faHome,
5 | faUsers,
6 | faUserGraduate,
7 | faPlusCircle,
8 | faPenSquare,
9 | faMinusSquare,
10 | faEllipsisH,
11 | faDownload,
12 | faUpload,
13 | faSortAlphaDown,
14 | faSortAlphaUpAlt,
15 | faSortNumericDown,
16 | faSortNumericUpAlt,
17 | faColumns,
18 | faUserFriends,
19 | faExclamationCircle,
20 | faChevronCircleDown,
21 | faCog,
22 | faChalkboardTeacher,
23 | faSignOutAlt,
24 | faArchive,
25 | faLevelUpAlt,
26 | faLevelDownAlt,
27 | faExpandAlt,
28 | faCompressAlt,
29 | faLink
30 | // faWeight
31 | // faSortAmountDownAlt
32 | } from '@fortawesome/free-solid-svg-icons';
33 |
34 | import {
35 | faArrowAltCircleLeft,
36 | faTrashAlt,
37 | faEye,
38 | faEyeSlash,
39 | faSquare,
40 | faEdit
41 | } from '@fortawesome/free-regular-svg-icons';
42 |
43 | export default () => library.add(
44 | // Solid Icons
45 | faHome,
46 | faUsers,
47 | faUserGraduate,
48 | faPlusCircle,
49 | faPenSquare,
50 | faMinusSquare,
51 | faEllipsisH,
52 | faDownload,
53 | faUpload,
54 | faSortAlphaDown,
55 | faSortAlphaUpAlt,
56 | faSortNumericDown,
57 | faSortNumericUpAlt,
58 | faColumns,
59 | faUserFriends,
60 | faExclamationCircle,
61 | faChevronCircleDown,
62 | faCog,
63 | faChalkboardTeacher,
64 | faSignOutAlt,
65 | faArchive,
66 | faLevelUpAlt,
67 | faLevelDownAlt,
68 | faExpandAlt,
69 | faCompressAlt,
70 | faLink,
71 |
72 | // Regular Icons
73 | faArrowAltCircleLeft,
74 | faTrashAlt,
75 | faEye,
76 | faEyeSlash,
77 | faSquare,
78 | faEdit
79 |
80 | // faWeight
81 | // faSortAmountDownAlt
82 | );
--------------------------------------------------------------------------------
/app/routes/students.js:
--------------------------------------------------------------------------------
1 | const {
2 | student: studentVal,
3 | createStudent: createStudentVal
4 | } = require("./validation");
5 |
6 | const { student: studentPerm } = require("../config/permissions");
7 |
8 | const ctrls = require("../controllers");
9 |
10 | const createRouter = require("./utils/createRouter");
11 |
12 | const isRoomMember = require("./middleware/isRoomMember");
13 | const setRoom = require("./middleware/setRoom");
14 |
15 | module.exports = createRouter([
16 |
17 | ["/", {
18 | post: {
19 | auth: true,
20 | defaultError: "create the student",
21 | validation: createStudentVal,
22 | middleware: [ setRoom.fromBody, isRoomMember ],
23 | permission: studentPerm,
24 | ctrl: ctrls.get("room.student")
25 | },
26 | patch: {
27 | auth: true,
28 | defaultError: "update the students",
29 | // validation: studentVal,
30 | middleware: [ setRoom.fromBody, isRoomMember ],
31 | permission: studentPerm,
32 | ctrl: ctrls.get("room.student").binding.updateMany
33 | }
34 | }],
35 |
36 | ["/:studentId", {
37 | get: {
38 | defaultError: "get the student",
39 | permission: studentPerm,
40 | ctrl: ctrls.get("room.student")
41 | },
42 | patch: {
43 | defaultError: "update the student",
44 | validation: studentVal,
45 | permission: studentPerm,
46 | ctrl: ctrls.get("room.student")
47 | },
48 | delete: {
49 | defaultError: "delete the student",
50 | permission: studentPerm,
51 | ctrl: ctrls.get("room.student")
52 | }
53 | }, {
54 | auth: true,
55 | paramCheck: true,
56 | middleware: [ setRoom.fromStudent, isRoomMember ]
57 | }]
58 |
59 | ]);
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/views/Team/components/StaffGroupPanel.js:
--------------------------------------------------------------------------------
1 | import { useLocation } from "react-router-dom";
2 |
3 | import {
4 | Heading,
5 | Panel,
6 | Tag
7 | } from "react-bulma-components";
8 |
9 | import { useAssignedStudents } from "pages/Dashboard/store";
10 | import { useStudentGroups } from "pages/Dashboard/utils/student";
11 | import RoomLink from "pages/Dashboard/components/RoomLink";
12 |
13 | export const StaffGroupPanelBlock = ( { member: { _id, user } } ) => {
14 |
15 | const assignedStudents = useAssignedStudents( _id );
16 | const groupedStudents = useStudentGroups( assignedStudents, "priority" );
17 |
18 | const location = useLocation();
19 | const isMemberActive = memberId => location.pathname.endsWith(`/team/${memberId}`);
20 |
21 | return (
22 |
23 | {user.name}
24 |
25 | {groupedStudents.filter(group=>group.entries.length).map( group => {
26 | return {group.entries.length};
27 | } )}
28 |
29 |
30 | );
31 |
32 | }
33 |
34 | const StaffGroupPanel = ({ title, staff }) => {
35 |
36 | return (
37 |
38 |
39 | {title}
40 |
41 | {staff.map( member => (
42 |
43 | ))}
44 |
45 | );
46 |
47 | }
48 |
49 | export default StaffGroupPanel;
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/store/actionsNames.js:
--------------------------------------------------------------------------------
1 | /**
2 | * TOPBAR ACTIONS
3 | */
4 | export const SET_TOPBAR = "SET_TOPBAR";
5 |
6 | /**
7 | * CLASSROOM ACTIONS
8 | */
9 | export const SET_CLASSROOM = "SET_CLASSROOM";
10 | export const SET_PERMISSIONS = "SET_PERMISSIONS";
11 | export const SET_CR_AND_PERMS = "SET_CR_AND_PERMS";
12 |
13 | /**
14 | * STAFF ACTIONS
15 | */
16 | export const ADD_STAFF = "ADD_STAFF";
17 | export const REMOVE_STAFF = "REMOVE_STAFF";
18 |
19 | /**
20 | * INVITE ACTIONS
21 | */
22 | export const ADD_INVITE = "ADD_INVITE";
23 | export const DELETE_INVITE = "DELETE_INVITE";
24 |
25 | /**
26 | * APP ACTIONS
27 | */
28 | export const SET_MANAGE_APPS = "SET_MANAGE_APPS";
29 | export const ADD_APP = "ADD_APP";
30 |
31 | /**
32 | * STUDENT VIEW ACTIONS
33 | */
34 | export const SET_STUDENTS = "SET_STUDENTS";
35 | export const ADD_STUDENT = "ADD_STUDENT";
36 | export const ADD_STUDENTS = "ADD_STUDENTS";
37 | export const UPDATE_STUDENT = "UPDATE_STUDENT";
38 | export const UPDATE_STUDENTS = "UPDATE_STUDENTS";
39 | export const SELECT_STUDENT = "SELECT_STUDENT";
40 | export const UNSELECT_STUDENT = "UNSELECT_STUDENT";
41 | export const SELECT_STUDENTS = "SELECT_STUDENTS";
42 | export const UNSELECT_STUDENTS = "UNSELECT_STUDENTS";
43 | export const UNSELECT_ALL_STUDENTS = "UNSELECT_ALL_STUDENTS";
44 | export const REMOVE_STUDENT = "REMOVE_STUDENT";
45 |
46 | /**
47 | * STUDENT MODAL ACTIONS
48 | */
49 | export const EDIT_STUDENT = "EDIT_STUDENT";
50 | export const SET_STUDENT_FEED = "SET_STUDENT_FEED";
51 | export const ADD_STUDENT_FEED_ITEM = "ADD_STUDENT_FEED_ITEM";
52 | export const ADD_STUDENT_FEED_ITEMS = "ADD_STUDENT_FEED_ITEMS";
53 | export const UPDATE_STUDENT_FEED_ITEM = "UPDATE_STUDENT_FEED_ITEM";
54 | export const UPDATE_STUDENT_FEED_ITEMS = "UPDATE_STUDENT_FEED_ITEMS";
55 | export const DELETE_STUDENT_FEED_ITEM = "DELETE_STUDENT_FEED_ITEM";
56 | export const DELETE_STUDENT_FEED_ITEMS = "DELETE_STUDENT_FEED_ITEMS";
--------------------------------------------------------------------------------
/registercode.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 |
3 | // MongoDB
4 | require("./app/config/mongoose");
5 |
6 | const crypto = require("crypto");
7 |
8 | // Removed for now for security
9 | // TODO explore other solutions for copy to clipboard
10 | // const ncp = require("copy-paste");
11 |
12 | const ObjectId = require("mongoose").Types.ObjectId;
13 | const { Room, Token } = require("~crsmmodels");
14 |
15 | const [ , , code ] = process.argv;
16 |
17 | // const copyToClip = (toCopy, message) => new Promise(resolve => {
18 | // ncp.copy( toCopy, () => {
19 |
20 | // if( message ) {
21 | // console.log( "!\x1b[32m", message, "\x1b[0m" );
22 | // console.log(" ");
23 | // }
24 |
25 | // resolve();
26 |
27 | // } );
28 | // });
29 |
30 | const createRoomWithCode = async ( code ) => {
31 |
32 | try {
33 |
34 | const roomId = new ObjectId();
35 |
36 | const token = new Token({
37 | relation: roomId,
38 | tokenString: (code || `ROOM-${crypto.randomBytes(4).toString('hex')}`).toUpperCase()
39 | });
40 |
41 | await token.save();
42 |
43 | const classroom = new Room({
44 | _id: roomId,
45 | registerCode: token._id,
46 | name: "PENDING"
47 | });
48 |
49 | await classroom.save();
50 |
51 | const message = `Your registration code is:`;
52 | const wrapper = `${"-".repeat(token.tokenString.length + 2 + message.length)}`;
53 |
54 | console.log("\n"+wrapper);
55 | console.log( message, "\x1b[32m", token.tokenString, "\x1b[0m" );
56 | console.log(wrapper);
57 |
58 | console.log(" ");
59 |
60 | // await copyToClip( token.token, "Code copied to clipboard" );
61 |
62 | process.exit(0);
63 |
64 | } catch(err) {
65 |
66 | console.error(err);
67 | process.exit(1);
68 |
69 | }
70 |
71 | }
72 |
73 | createRoomWithCode( code );
--------------------------------------------------------------------------------
/client/src/utils/ready.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useStoreContext, getStoreAction as gsa } from "../store";
3 | import { ADD_READY_STEP, COMPLETE_READY_STEP, REMOVE_READY_STEP, UNCOMPLETE_READY_STEP } from "../store/actions"
4 |
5 | export const useIsReady = () => {
6 |
7 | const [ { ready: { complete, steps } } ] = useStoreContext();
8 | const [ isReady, setIsReady ] = useState( false );
9 |
10 | useEffect(() => {
11 |
12 | const isReady = !steps.length || steps.every( step => complete.includes( step ) );
13 |
14 | if( !isReady ) {
15 | // Apply false states immediately.
16 | setIsReady( isReady );
17 | return;
18 | }
19 |
20 | // Add a small delay to setting true states.
21 | const timeout = setTimeout( () => setIsReady( isReady ), 250 );
22 |
23 | return () => clearTimeout(timeout);
24 |
25 | }, [ steps, complete, setIsReady ] )
26 |
27 | return isReady;
28 |
29 | }
30 |
31 | export const useReadyStep = ( step ) => {
32 |
33 | const [ { ready: { complete } } ,dispatch ] = useStoreContext();
34 |
35 | const [ [ addStep, completeStep, removeStep, uncompleteStep ] ] = useState([
36 | // add
37 | () => dispatch(gsa( ADD_READY_STEP, step )),
38 | // complete
39 | () => dispatch(gsa( COMPLETE_READY_STEP, step )),
40 | // remove
41 | () => dispatch(gsa( REMOVE_READY_STEP, step )),
42 | // uncomplete
43 | () => dispatch(gsa( UNCOMPLETE_READY_STEP, step ))
44 | ]);
45 |
46 | const [ isComplete, setIsComplete ] = useState(false);
47 |
48 | useEffect(() => {
49 |
50 | addStep();
51 |
52 | return () => removeStep();
53 |
54 | }, [step, addStep, removeStep]);
55 |
56 | useEffect(() => {
57 |
58 | setIsComplete( complete.includes(step) );
59 |
60 | }, [step, complete, setIsComplete]);
61 |
62 | return [ completeStep, uncompleteStep, isComplete ];
63 |
64 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "crsm",
3 | "version": "0.7.2",
4 | "description": "Classroom student management utilities for instructors to effectively organize and keep track of student progress with a team of staff members.",
5 | "main": "app/server.js",
6 | "_moduleAliases": {
7 | "~crsm": "app",
8 | "~crsmmodels": "app/controllers/definitions/models",
9 | "~crsmtest": "test"
10 | },
11 | "scripts": {
12 | "envsetup": "node envsetup",
13 | "start": "if-env NODE_ENV=production && npm run start:prod || npm run start:dev",
14 | "start:prod": "node app/server.js",
15 | "start:dev": "concurrently \"nodemon --ignore 'client/*'\" \"npm run client\"",
16 | "client": "cd client && npm run start",
17 | "seed": "node seed",
18 | "seed:reset": "node app/seed/reset",
19 | "install": "cd client && npm install",
20 | "postinstall": "link-module-alias",
21 | "build": "cd client && npm run build",
22 | "heroku-postbuild": "npm run build",
23 | "test": "node test/run"
24 | },
25 | "author": "",
26 | "license": "ISC",
27 | "devDependencies": {
28 | "chai": "4.3.6",
29 | "chai-as-promised": "7.1.1",
30 | "concurrently": "7.0.0",
31 | "inquirer": "8.2.0",
32 | "link-module-alias": "1.2.0",
33 | "mocha": "9.2.0",
34 | "nodemon": "2.0.15",
35 | "nyc": "15.1.0",
36 | "sinon": "13.0.1",
37 | "sinon-chai": "3.7.0"
38 | },
39 | "dependencies": {
40 | "@sendgrid/mail": "7.6.1",
41 | "apollo-server-core": "3.6.3",
42 | "apollo-server-express": "3.6.3",
43 | "bcryptjs": "2.4.3",
44 | "compression": "1.7.4",
45 | "dotenv": "16.0.0",
46 | "express": "4.17.2",
47 | "fastest-validator": "1.12.0",
48 | "graphql": "15.8.0",
49 | "graphql-modules": "2.0.0",
50 | "graphql-type-json": "0.3.2",
51 | "if-env": "1.0.4",
52 | "jsonwebtoken": "8.5.1",
53 | "mjml": "4.12.0",
54 | "mongoose": "5.13.14",
55 | "nodemailer": "6.7.2",
56 | "passport": "0.5.2",
57 | "passport-jwt": "4.0.0",
58 | "socket.io": "4.4.1"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/graphql/middleware/setRoomContext.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @callback next
3 | *
4 | * @typedef RoomContext
5 | * @property {import('~crsmmodels/schema/RoomSchema').RoomDocument} room
6 | */
7 |
8 | const roomQueryConfig = { select: "staff" };
9 |
10 | /**
11 | * Ensures a room was set to context
12 | * @param {Object} param0
13 | * @param {?import('~crsmmodels/schema/RoomSchema').RoomDocument} param0.room
14 | */
15 | const validateRoomContext = ({ room }) => {
16 | if( !room ) throw Error("Unable to locate associated room")
17 | };
18 |
19 | /**
20 | * Sets `room` context from a provided `roomId` arg
21 | * @param {Object} param0
22 | * @param {Object} param0.args
23 | * @param {import('mongoose').Schema.Types.ObjectId} param0.args.roomId
24 | * @param {import('../context/db').DbContext} param0.context
25 | * @param {next} next
26 | * @returns {*}
27 | */
28 | const fromRoomId = async ({
29 | args: { roomId },
30 | context
31 | }, next) => {
32 |
33 | const { db } = context;
34 |
35 | context.room = await db.get("room").findOne({ docId: roomId }, roomQueryConfig);
36 |
37 | validateRoomContext( context );
38 |
39 | return next();
40 |
41 | }
42 |
43 | /**
44 | * @typedef SetRoomFromStudentIdArgs
45 | * @property {import('mongoose').Schema.Types.ObjectId} roomStudentId
46 | *
47 | * Sets `room` context from a provided `studentId` arg
48 | * @param {Object} param0
49 | * @param {SetRoomFromStudentIdArgs} param0.args
50 | * @param {import('../context/db').DbContext} param0.context
51 | * @param {next} next
52 | * @returns {*}
53 | */
54 | const fromStudentId = async ({
55 | args: { roomStudentId },
56 | context
57 | }, next) => {
58 |
59 | const { db } = context;
60 |
61 | context.room = await db.get("room.student").findOwner({ docId: roomStudentId }, roomQueryConfig);
62 |
63 | validateRoomContext( context );
64 |
65 | return next();
66 |
67 | }
68 |
69 | const setRoomContext = {
70 | roomQueryConfig,
71 | fromRoomId,
72 | fromStudentId
73 | }
74 |
75 | module.exports = setRoomContext;
--------------------------------------------------------------------------------