├── .env.example ├── .eslintignore ├── .eslintrc ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── create-poc-for-new-component---item.md │ ├── design-new-component-or-item.md │ ├── feature_request.md │ ├── implement-new-component---item.md │ ├── new-dashboard-page.md │ └── task-template.md ├── .gitignore ├── .storybook ├── iesd.js ├── main.js ├── manager.js ├── preview-head.html └── webpack.config.js ├── .stylelintignore ├── .stylelintrc.json ├── LICENSE.md ├── README.md ├── email ├── CSS.ts ├── partials │ └── Footer.tsx └── templates │ ├── GenericEmailTemplate.tsx │ └── main │ ├── confirmationEmail.tsx │ └── resetPasswordEmail.tsx ├── index.d.ts ├── jest.config.js ├── jest.setup.js ├── jsconfig.json ├── lib ├── auth.ts ├── db.ts └── redis.ts ├── models ├── course.ts ├── courseMeta.ts ├── lesson.ts ├── lessonMeta.ts ├── message.ts ├── notification.ts ├── options.ts ├── user.ts └── userMeta.ts ├── next-env.d.ts ├── next-seo.config.js ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── api │ ├── authenticate │ │ ├── auth.ts │ │ ├── login.ts │ │ ├── logout.ts │ │ ├── reset.ts │ │ └── signup.ts │ ├── course-students │ │ └── index.ts │ ├── course │ │ ├── add.ts │ │ └── addPicture.ts │ ├── mail.ts │ ├── messages.ts │ ├── meta.ts │ ├── notifications.ts │ ├── setup.ts │ ├── user │ │ ├── personal.ts │ │ └── upload.ts │ └── validate │ │ ├── confirm.ts │ │ ├── email.ts │ │ └── user.ts ├── authenticate.tsx ├── bookmarks │ └── index.tsx ├── community │ └── index.tsx ├── confirmation.tsx ├── courses │ ├── add.tsx │ ├── categories.tsx │ ├── index.tsx │ ├── manage.tsx │ └── tags.tsx ├── dashboard │ ├── index.tsx │ ├── students │ │ └── index.tsx │ └── updates.tsx ├── index.tsx ├── lessons │ └── index.tsx ├── logged-out.tsx ├── messages │ └── index.tsx ├── reset-password.tsx ├── resources │ └── index.tsx ├── settings │ ├── account.tsx │ ├── preferences.tsx │ └── profile.tsx └── welcome.tsx ├── public ├── fonts │ ├── AvenirLTStd-Black.otf │ ├── AvenirLTStd-Book.otf │ ├── AvenirLTStd-Heavy.otf │ ├── AvenirLTStd-Light.otf │ ├── AvenirLTStd-Medium.otf │ └── Big-John.otf ├── images │ ├── authentication-screenshot.png │ ├── avatars │ │ ├── david-8-avatar.jpg │ │ └── placeholder_image.png │ ├── coursePicture │ │ ├── upload_15eab51c419a5e174bce3860f42d1ac8 │ │ ├── upload_31cfd508e85dd3586cdc328fd593a261 │ │ ├── upload_35984330de0154260d732bc550a8ecdd │ │ ├── upload_3d5a1354820f567fbf009de45b4530ed │ │ ├── upload_606e0dfc76128734e971591e573f8257 │ │ ├── upload_a13f2281b60262fe66fdb226cafbbec7 │ │ ├── upload_a447bbd89145ec5ef69a3f2618eed98f │ │ ├── upload_a5dec182d7c8841843a7ee894aec0856 │ │ ├── upload_b31a54e4e9ec9b82460eaeed7b96e5d7 │ │ ├── upload_f6c37dc523bce5af630f65380130b7bf │ │ └── upload_fa51b1b0c5d8c332ed9e4807542f2bfa │ ├── illustrations │ │ ├── access-granted.gif │ │ └── forbidden.gif │ ├── landing │ │ ├── landingcover.jpg │ │ ├── landingcover2.jpg │ │ └── riverside-sample.jpg │ ├── logo │ │ ├── spark-360x360.png │ │ ├── spark-text-carbon.svg │ │ ├── spark-text-snow.svg │ │ └── spark-text-white.svg │ └── spark.png └── scripts │ └── upload.js ├── src ├── components │ ├── animation │ │ ├── Loader │ │ │ ├── Loader.scss │ │ │ ├── Loader.stories.tsx │ │ │ └── Loader.tsx │ │ └── Redirect │ │ │ ├── Redirect.scss │ │ │ ├── Redirect.stories.tsx │ │ │ └── Redirect.tsx │ ├── authenticate │ │ ├── LogIn │ │ │ ├── LogIn.stories.tsx │ │ │ ├── LogIn.tsx │ │ │ └── Login.scss │ │ ├── LogOut │ │ │ ├── LogOut.scss │ │ │ ├── LogOut.stories.tsx │ │ │ └── LogOut.tsx │ │ ├── Password │ │ │ ├── Password.scss │ │ │ ├── Password.stories.tsx │ │ │ └── Password.tsx │ │ ├── Reset │ │ │ ├── Reset.scss │ │ │ ├── Reset.stories.tsx │ │ │ └── Reset.tsx │ │ └── SignUp │ │ │ ├── SignUp.scss │ │ │ ├── SignUp.stories.tsx │ │ │ └── SignUp.tsx │ ├── course │ │ ├── Course.scss │ │ └── Course.tsx │ ├── courses-navigation │ │ ├── Navigation.scss │ │ └── Navigation.tsx │ ├── courses │ │ ├── Courses.scss │ │ ├── Courses.stories.tsx │ │ └── Courses.tsx │ ├── dashboard │ │ ├── home │ │ │ ├── DashboardHome.scss │ │ │ ├── DashboardHome.stories.tsx │ │ │ └── DashboardHome.tsx │ │ └── panels │ │ │ └── addCourse │ │ │ ├── AddCourse.scss │ │ │ └── AddCourse.tsx │ ├── global │ │ ├── Authorized │ │ │ ├── Authorized.scss │ │ │ ├── Authorized.stories.tsx │ │ │ ├── Authorized.tsx │ │ │ ├── _note_design.md │ │ │ └── _note_intro.md │ │ ├── ContentContainer │ │ │ └── ContentContainer.tsx │ │ ├── Footer │ │ │ ├── Footer.scss │ │ │ ├── Footer.stories.tsx │ │ │ └── Footer.tsx │ │ ├── Message │ │ │ ├── Message.scss │ │ │ ├── Message.stories.tsx │ │ │ └── Message.tsx │ │ ├── Navigation │ │ │ ├── Navigation.scss │ │ │ ├── Navigation.stories.tsx │ │ │ ├── Navigation.tsx │ │ │ └── User │ │ │ │ ├── Messages │ │ │ │ └── Messages.tsx │ │ │ │ ├── Notifications │ │ │ │ └── Notifications.tsx │ │ │ │ └── User.tsx │ │ ├── Sidebar │ │ │ ├── Sidebar.scss │ │ │ ├── Sidebar.stories.tsx │ │ │ ├── Sidebar.tsx │ │ │ └── usage.md │ │ ├── Spinner │ │ │ ├── Spinner.scss │ │ │ ├── Spinner.stories.tsx │ │ │ └── Spinner.tsx │ │ └── Unauthorized │ │ │ ├── Unauthorized.scss │ │ │ ├── Unauthorized.stories.tsx │ │ │ └── Unauthorized.tsx │ ├── landing │ │ ├── CourseProgramCard.tsx │ │ ├── Landing.scss │ │ ├── Landing.stories.tsx │ │ └── Landing.tsx │ ├── layouts │ │ ├── DashboardLayout.scss │ │ └── DashboardLayout.tsx │ ├── manage-students │ │ ├── ManageStudents.scss │ │ ├── ManageStudents.stories.tsx │ │ └── ManageStudents.tsx │ ├── panel │ │ ├── Panel.scss │ │ └── Panel.tsx │ ├── user-info-input │ │ ├── UserInfoInput.scss │ │ ├── UserInfoInput.stories.tsx │ │ ├── UserInfoInput.tsx │ │ ├── phone-number │ │ │ └── PhoneNumber.tsx │ │ └── sanitize │ │ │ └── sanitize.ts │ └── utility │ │ └── Notify.ts ├── context.ts ├── pages.ts └── style │ ├── _animation.scss │ ├── _colors.scss │ ├── _global.scss │ ├── _typography.scss │ ├── _variables.scss │ ├── index.scss │ └── pages │ ├── _dashboard.scss │ ├── _index.scss │ └── _welcome.scss ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # To enable your project environment, rename the file to ".env" (remove the ".example" suffix) 2 | # and populate the incomplete values 3 | # Please ask project administrator if you have concerns about values to use 4 | 5 | #environment 6 | HOST=http://localhost:3000/ 7 | 8 | #api 9 | SECRET= 10 | 11 | #mysql 12 | DBHOST= 13 | DBNAME= 14 | DBUSER= 15 | DBPASSWORD= 16 | 17 | #redis 18 | REDISPORT= 19 | REDISIP= 20 | REDISPASS= 21 | 22 | #jwt - Secret for signing JWTs 23 | JWTKEY= 24 | 25 | #mail - SMTP configuration for sending emails (registration, verification, etc) 26 | MAILHOST= 27 | MAILPORT= 28 | MAILUSER= 29 | MAILPASS= 30 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | sass/vendors 2 | .next/ 3 | .idea/ 4 | .vs/ 5 | assets/ 6 | docs/ 7 | node_modules/ 8 | node_modules/* 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": [ 4 | "@typescript-eslint", 5 | "react", 6 | "react-hooks" 7 | ], 8 | "extends": [ 9 | "google", 10 | "plugin:react/recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "ecmaVersion": 2019, 15 | "sourceType": "module", 16 | "ecmaFeatures": { 17 | "jsx": true 18 | } 19 | }, 20 | "env": { 21 | "es6": true, 22 | "browser": true, 23 | "node": true, 24 | "jest": true 25 | }, 26 | "settings": { 27 | "react": { 28 | "version": "detect" 29 | } 30 | }, 31 | "rules": { 32 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".ts", "tsx"] }], 33 | "@typescript-eslint/explicit-function-return-type": "off", 34 | "@typescript-eslint/explicit-member-accessibility": "off", 35 | "@typescript-eslint/no-explicit-any": "off", 36 | "@typescript-eslint/no-var-requires": "off", 37 | "max-len": "off", 38 | "no-mixed-spaces-and-tabs": 2, 39 | "quotes": 0, 40 | "require-jsdoc": 0, 41 | "one-var": 0, 42 | "react/prop-types": 0, 43 | "react/react-in-jsx-scope": 0, 44 | "strict": 2, 45 | "camelcase": 0, 46 | "linebreak-style": 0 47 | }, 48 | "overrides": [] 49 | } 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/create-poc-for-new-component---item.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Create POC for new component / item 3 | about: New task for creating POC of a design for a component or item 4 | title: Proof of concept for [name of component] component 5 | labels: story, task, testing 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Components that require updating:** 11 | - New component name 12 | 13 | **Tech to be used:** 14 | - UIKit 15 | - HTML/SASS 16 | - Fontawesome Pro 17 | 18 | **What is the current behavior:** 19 | We currently don't have a User information input panel component. 20 | 21 | **Description:** 22 | Create a proof of concept using HTML/CSS. Take the design PDF and convert it to code. You will be using UIKit as the CSS library along with Fontawesome PRO icons. Any comments you need to make, make them in the discussion of the issue or the implementation issue for this component. 23 | 24 | The expected deliverable will be a component that is responsive and mobile ready. Preferably an HTML file. No comments. No inline CSS. No JavaScript unless otherwise informed. Next task will deal with converting this to a component in React. 25 | 26 | **Justification:** Vital component 27 | 28 | **Notes:** 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/design-new-component-or-item.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Design new component or item 3 | about: For design tasks for new components or items 4 | title: Design [component name] component 5 | labels: task 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Components that require updating:** 11 | - [component name]: new component 12 | 13 | **Tech to be used:** 14 | - Design tools 15 | 16 | **What is the new component or item:** 17 | Create a design PDF for developers to create a static copy with code. You will be using UIKit components along with Fontawesome PRO icons. Any comments you need to make, make them in the discussion of the issue or the implementation issue for this component. 18 | 19 | The expected deliverable will be a design documents that shows all the possible states it has. Provide notes as necessary. 20 | 21 | **Justification:** New component 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/implement-new-component---item.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Implement new component / item 3 | about: 'Implementation of [name of component] component. ' 4 | title: Implementation of [name of component] component. 5 | labels: task 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Components that require updating:** 11 | - Navigation - new component 12 | 13 | **Tech to be used:** 14 | - Next.js 15 | - React 16 | - Typescript 17 | 18 | **What is the expected deliverable:** 19 | Take the proof of concept (HTML/CSS) and implement it. This means take it from static and add React. Build it to be modular and without dependencies on other components. 20 | 21 | **Justification:** vital component 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-dashboard-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Dashboard Page 3 | about: 'New dashboard page ' 4 | title: Dashboard page - [insert title] 5 | labels: task 6 | assignees: '' 7 | 8 | --- 9 | 10 | This is a three part task: 11 | - [ ] Design the new page 12 | - [ ] Create the proof of concept 13 | - [ ] Implement the proof of concept 14 | 15 | ### **Design** 16 | **What is the new component or item:** 17 | Create a design PDF for developers to create a static copy with code. You will be using UIKit components along with Fontawesome PRO icons. Any comments you need to make, make them in the discussion of the issue or the implementation issue for this component. 18 | 19 | The expected deliverable will be a design documents that shows all the possible states it has. Provide notes as necessary. 20 | 21 | ### ** Proof of Concept** 22 | Create a proof of concept using Storybook.js. Take the design PDF and convert it to code. You will be using UIKit as the CSS library along with Fontawesome PRO icons. Do not worry about logic during this step, just build out the component. 23 | 24 | The expected deliverable will be a component that is responsive and mobile-ready. Make sure you address all stories requested (states). The next task will deal with adding any additional logic that might be needing. For example, creating API requests, iterating through data, etc. 25 | 26 | ### **Implement the Proof of Concept** 27 | Take the proof of concept and add missing logic. This means take it from static and make it dynamic if it requires it. Build it to be modular, add comments and make sure component is polished. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Task template 3 | about: Create a task 4 | title: "[Actionable task title]" 5 | labels: task 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Components that require updating:** 11 | - component 12 | 13 | **Tech to be used:** 14 | - tech 15 | 16 | **What is the current behavior:** 17 | 18 | **What is the new behavior:** 19 | 20 | **Justification:** 21 | 22 | **Notes:** 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | *.log 3 | .env 4 | .next 5 | .idea 6 | .vscode 7 | node_modules 8 | yarn-error.log -------------------------------------------------------------------------------- /.storybook/iesd.js: -------------------------------------------------------------------------------- 1 | import {create} from '@storybook/theming/create'; 2 | 3 | export default create({ 4 | base: 'dark', 5 | brandTitle: 'IESD', 6 | brandUrl: 'https://iesd.com', 7 | brandImage: 'https://iesd.com/static/logos/iesd-initials-white.svg', 8 | }); 9 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../src/components/**/*.stories.tsx'], 3 | addons: [ 4 | '@storybook/addon-actions/register', 5 | '@storybook/addon-knobs/register', 6 | '@storybook/addon-notes/register', 7 | '@storybook/addon-storysource', 8 | '@storybook/addon-a11y/register', 9 | '@storybook/addon-viewport/register', 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import {addons} from '@storybook/addons'; 2 | import iesd from './iesd'; 3 | 4 | addons.setConfig({ 5 | theme: iesd, 6 | }); 7 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | // .storybook/webpack.config.js 2 | 3 | const path = require("path"); 4 | 5 | module.exports = ({config}) => { 6 | config.module.rules.push({ 7 | test: /\.(ts|tsx)$/, 8 | loader: require.resolve('babel-loader'), 9 | options: { 10 | presets: [require.resolve('babel-preset-react-app')], 11 | }, 12 | }); 13 | 14 | config.module.rules.push({ 15 | test: /\.(png|woff|woff2|eot|otf|ttf|svg)$/, 16 | loaders: ['file-loader'], 17 | include: path.resolve(__dirname, '../'), 18 | }); 19 | 20 | config.module.rules.push({ 21 | test: /\.s?css$/, 22 | use: [ 23 | 'style-loader', 24 | 'css-loader', 25 | { 26 | loader: 'sass-loader', 27 | options: { 28 | // Prefer `dart-sass` 29 | implementation: require('sass'), 30 | }, 31 | }, 32 | { 33 | loader: 'sass-resources-loader', 34 | options: { 35 | resources: "src/style/_variables.scss", 36 | }, 37 | }, 38 | ], 39 | include: [ 40 | path.resolve(__dirname, "../"), 41 | ], 42 | }); 43 | 44 | config.resolve.extensions.push('.ts', '.tsx'); 45 | return config; 46 | }; 47 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | sass/vendors 2 | email -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "max-line-length": null, 5 | "no-descending-specificity": null, 6 | "declaration-block-trailing-semicolon": null, 7 | "at-rule-no-unknown": null 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 lloan alas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Spark Logo 6 |

7 |

8 | Spark
9 | Learning Management System 10 |

11 |
12 |

13 | Spark is an open source learning management system, designed and built for Spark, a digital skills initiative led by a southern California nonprofit, Inland Empire Software Development, Inc. We promote free and open source software. 14 |

15 |
16 | 17 | ## 👨‍💻 **Getting Started** 18 | 19 | To get started, make sure to fill out the `.env` file with the appropriate information. Make sure that all prerequisites are met or application won't operate correctly. 20 | 21 | The application has two parts to it - the application itself and storybook. 22 | 23 | ![auth screenshot](https://raw.githubusercontent.com/lloan/next-authenticate/master/public/images/authentication-screenshot.png) 24 | 25 | ## ⛑ **Getting Help** 26 | 27 | Any questions or for support, please open up an issue and label it appropriately `support`. 28 | 29 | If you have any questions, please ask on our [Slack](https://ie-sd.slack.com). We welcome everyone to our Slack, so don't be afraid to join! Have fun coding! 30 | 31 | **Join link** 32 | 33 | ## ⟲ **Prerequisites** 34 | 35 | The minimum requirements to run this include: 36 | 37 | - Redis 38 | - MySQL 39 | - Mail SMTP provider 40 | - Yarn 41 | 42 | ## 🌐 **Installing** 43 | 44 | A step by step series of examples that tell you how to get a development env running. 45 | 46 | 1. Update your `.env` file with the required information. Make sure the mysql and redis information is correct. The mail information is used for the mail system, make sure that is correct or you won't receive account creation verification email or password reset emails. You also need to create a JWT key and an API secret key. 47 | 48 | .env example input 49 | ``` 50 | JWTKEY=Ds~4lq~}.?*f~Ql$42J%aR7%SoKxaN 51 | SECRET=SklZvh3a0PwQev901A1zT23vlG 52 | ``` 53 | 54 | 2. Install required packages 55 | 56 | ``` 57 | yarn install 58 | ``` 59 | 60 | 3. Start local development environment. 61 | ``` 62 | yarn dev 63 | ``` 64 | 65 | 4. Start storybook instance 66 | ``` 67 | yarn storybook 68 | ``` 69 | 70 | ## 💡 **Features** 71 | - Pages for authentication (login, logout, reset), dashboard, welcome, reset password, confirmation, home and logged-out. HTTPOnly token (JWT) used. 72 | - Protected pages which can be turned on and off by page. 73 | - Redirection system based on user authentication state. 74 | - Email system with very simple template system. 75 | - SASS based. 76 | - Typescript based, makes development a breeze. 77 | - UIKit notification system. 78 | - Models directory and option for initial DB setup. 79 | - ESLint and Stylelint for development. 80 | - React Context handles state at the parent level. 81 | - Storybook integration for self documentation and design system generation. 82 | 83 | ## 🔨 **Built With** 84 | * Next.js 85 | * MySQL 86 | * Redis 87 | * Typescript 88 | * SASS 89 | * UIKit - A clean CSS library 90 | * Nodemailer 91 | * Storybook 92 | 93 | For development: 94 | 95 | * ESLint 96 | * Stylelint 97 | * Jest 98 | 99 | ## 🤝 **Contributing** 100 | All contributions are welcome and we invite everyone to open issues for suggestions and/or bugs. 101 | 102 | - If you want to contribute, please open up an issue. 103 | - If you start working on a feature, branch off of the `master` branch as it should be the most up-to-date. 104 | 105 | ## 📓 **Development Team** 106 | 107 | * **👤 Lloan Alas** 108 | * **👤 Jacob Goodwin** 109 | * **👤 Tony Nguyen** 110 | * **👤 Andy Mendez** 111 | * **👤 David Huang** 112 | * **👤 Katherine Orho** 113 | * **👤 Greg Rojas** 114 | * **👤 Raul Jauregui** 115 | 116 | See also the list of [contributors](https://github.com/lloan/next-authenticate/graphs/contributors) who participated in this project. 117 | 118 | ## 🗒 **License** 119 | 120 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 121 | -------------------------------------------------------------------------------- /email/CSS.ts: -------------------------------------------------------------------------------- 1 | const uikitcss = ` 2 | html, 3 | body { 4 | margin: 0 auto !important; 5 | padding: 0 !important; 6 | height: 100% !important; 7 | width: 100% !important; 8 | } 9 | * { 10 | -ms-text-size-adjust: 100%; 11 | -webkit-text-size-adjust: 100%; 12 | } 13 | div[style*="margin: 16px 0"] { 14 | margin: 0 !important; 15 | } 16 | #MessageViewBody, #MessageWebViewDiv{ 17 | width: 100% !important; 18 | } 19 | table, 20 | td { 21 | mso-table-lspace: 0pt !important; 22 | mso-table-rspace: 0pt !important; 23 | } 24 | table { 25 | border-spacing: 0 !important; 26 | border-collapse: collapse !important; 27 | table-layout: fixed !important; 28 | margin: 0 auto !important; 29 | } 30 | img { 31 | -ms-interpolation-mode:bicubic; 32 | } 33 | a { 34 | text-decoration: none; 35 | } 36 | a[x-apple-data-detectors], 37 | .unstyle-auto-detected-links a, 38 | .aBn { 39 | border-bottom: 0 !important; 40 | cursor: default !important; 41 | color: inherit !important; 42 | text-decoration: none !important; 43 | font-size: inherit !important; 44 | font-family: inherit !important; 45 | font-weight: inherit !important; 46 | line-height: inherit !important; 47 | } 48 | .a6S { 49 | display: none !important; 50 | opacity: 0.01 !important; 51 | } 52 | .im { 53 | color: inherit !important; 54 | } 55 | img.g-img + div { 56 | display: none !important; 57 | } 58 | @media only screen and (min-device-width: 320px) and (max-device-width: 374px) { 59 | u ~ div .email-container { 60 | min-width: 320px !important; 61 | } 62 | } 63 | @media only screen and (min-device-width: 375px) and (max-device-width: 413px) { 64 | u ~ div .email-container { 65 | min-width: 375px !important; 66 | } 67 | } 68 | @media only screen and (min-device-width: 414px) { 69 | u ~ div .email-container { 70 | min-width: 414px !important; 71 | } 72 | } 73 | `; 74 | 75 | export default uikitcss; 76 | -------------------------------------------------------------------------------- /email/partials/Footer.tsx: -------------------------------------------------------------------------------- 1 | function Footer() { 2 | return ( 3 | 4 | 5 | 6 | 11 | 12 | 13 |
7 |

8 | Inland Empire Software Development, Inc.
3499 Tenth St. Riverside, CA, 92501 US
(800)437-0267
9 |

10 |
14 | ); 15 | } 16 | 17 | export default Footer; 18 | -------------------------------------------------------------------------------- /email/templates/GenericEmailTemplate.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import mailCSS from "../CSS"; 3 | import Footer from "../partials/Footer"; 4 | 5 | function GenericEmailTemplate(props: {content: JSX.Element; title: string}) { 6 | const {content, title} = props; 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | Password Reset 15 | 18 | 19 | 20 |
21 |
22 | {title} 23 |
24 |
25 | ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  26 |
27 |
28 | 29 | 30 | 33 | 34 | 35 | 38 | 39 |
31 | {content} 32 |
40 |
42 |
43 | 44 | 45 | ); 46 | } 47 | 48 | 49 | export default GenericEmailTemplate; 50 | -------------------------------------------------------------------------------- /email/templates/main/confirmationEmail.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import GenericEmailTemplate from "../GenericEmailTemplate"; 3 | 4 | 5 | const ConfirmationEmail = (options: { url: string; username: string; token: string; title: string }) => { 6 | const {url, username, token, title} = options; 7 | 8 | if (!url || !username || !token) return (

An error has occurred, please contact the administrator

); 9 | 10 | return ( 11 | 13 | 14 | 15 |

{title}

16 |

Hello {username},

17 |

This email was used to sign up for our service.

18 |

Please confirm that it was you.

19 |

After you confirm, you will be able to sign in to your account.

20 |

Confirm

21 | 22 | 23 | 24 | 25 | } /> 26 | ); 27 | }; 28 | 29 | export default ConfirmationEmail; 30 | -------------------------------------------------------------------------------- /email/templates/main/resetPasswordEmail.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import GenericEmailTemplate from "../GenericEmailTemplate"; 3 | 4 | const ResetPasswordEmail = (options: { url: string; username: string; token: string; email: string; title: string }) => { 5 | const {url, username, token, email, title} = options; 6 | 7 | if (!url || !username || !email || !token) return (

An error has occurred, please contact the administrator

); 8 | 9 | return ( 10 | 12 | 13 | 14 |

{title}

15 |

Hello {username},

16 |

A password reset request was submitted for your account.

17 |

To keep your account safe, this email has been sent with a link to reset your password.

18 |

If this request was not initiated by you, please contact an administrator.

19 |

Reset Password

20 | 21 | 22 | 23 | 24 | }/> 25 | ); 26 | }; 27 | 28 | export default ResetPasswordEmail; 29 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import {Component} from "react"; 2 | 3 | export interface CreateTable { 4 | createTable: (TableName: string, Query: string) => Function; 5 | } 6 | export interface Message { 7 | status: boolean; 8 | message: string; 9 | } 10 | 11 | export interface DBUpdateUser { 12 | [userName?: string]: string; 13 | [password?: string]: string; 14 | [email?: string]: string; 15 | [role?: string]: UserRoles; 16 | } 17 | 18 | export enum UserRoles { 19 | subscriber = 'subsciber' 20 | } 21 | 22 | // Allow arrays to be indexed with strings 23 | export interface ArrayIndexedWithStrings { 24 | [key: string]: any; 25 | } 26 | 27 | export interface Redirects { 28 | [page: string]: { 29 | redirect: string | undefined; 30 | }; 31 | } 32 | 33 | export interface Notification extends UIkit.Notify { 34 | notification: Function; 35 | } 36 | 37 | export default class NextAuthenticate extends Component { } 38 | 39 | export interface MyAppContext { 40 | setContextProperty: function | undefined; 41 | user: string | undefined; 42 | sidebarIsOpen: boolean; 43 | notifications: boolean; 44 | activeDashboardPath: string | undefined; 45 | activeDashboardMenus: Map; 46 | access: boolean; 47 | redirect: string | undefined; 48 | userID: string | undefined; 49 | isPublic: boolean; 50 | } 51 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | setupFiles: ['/jest.setup.js'], 4 | testEnvironment: 'node', 5 | }; 6 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | const Enzyme = require('enzyme'); 2 | const Adapter = require('enzyme-adapter-react-16'); 3 | 4 | Enzyme.configure({adapter: new Adapter()}); 5 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "ES6", 5 | "lib": ["es6"] 6 | }, 7 | "exclude": [ 8 | "node_modules", 9 | "**/node_modules/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /lib/auth.ts: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const bcrypt = require('bcryptjs'); 3 | const auth: any = {}; // TODO: Find a suitable type 4 | 5 | auth.verifyPassword = (password: string, hash: string) => bcrypt.compareSync(password, hash); 6 | 7 | auth.tokenize = (username: string) => jwt.sign({username}, process.env.JWTKEY, { 8 | algorithm: 'HS256', 9 | expiresIn: 300, 10 | }); 11 | 12 | export default auth; 13 | -------------------------------------------------------------------------------- /lib/redis.ts: -------------------------------------------------------------------------------- 1 | const redis = require("redis"); 2 | const bluebird = require('bluebird'); 3 | 4 | bluebird.promisifyAll(redis.RedisClient.prototype); 5 | bluebird.promisifyAll(redis.Multi.prototype); 6 | 7 | const client = redis.createClient({ 8 | port: process.env.REDISPORT, 9 | host: process.env.REDISIP, 10 | password: process.env.REDISPASS, 11 | db: process.env.REDISDB, 12 | }); 13 | 14 | client.setToken = (token: string, identifier: {user: string; userID: string}) => { 15 | console.log(JSON.stringify(identifier)); 16 | return client.setAsync(String(token), JSON.stringify(identifier)); 17 | }; 18 | 19 | client.checkToken = (token: string, userToCheck: {user: string; userID: string}) => { 20 | console.log(token); 21 | return client.getAsync(String(token)).then(function(user: null | string) { 22 | const data = user !== null ? JSON.parse(user) : null; 23 | return data === null ? false : 24 | (userToCheck.user === data.user && Number(userToCheck.userID) === Number(data.userID)); 25 | }); 26 | }; 27 | 28 | export default client; 29 | -------------------------------------------------------------------------------- /models/course.ts: -------------------------------------------------------------------------------- 1 | import {CreateTable} from ".."; 2 | 3 | /** 4 | * Creates the course database table. Only one row per course. 5 | * @param {db} db 6 | */ 7 | const course = async (db: CreateTable) => { 8 | const query = ` 9 | CREATE TABLE ${process.env.DBNAME}.course ( 10 | course_id BIGINT(20) NOT NULL AUTO_INCREMENT, 11 | code VARCHAR(45) NULL, 12 | status TINYINT NOT NULL DEFAULT 0, 13 | name VARCHAR(150) NOT NULL, 14 | instructor VARCHAR(10) NOT NULL, 15 | students_enrolled VARCHAR(255) NOT NULL, 16 | PRIMARY KEY (course_id), 17 | UNIQUE INDEX name_UNIQUE (name ASC), 18 | UNIQUE INDEX course_id_UNIQUE (course_id ASC), 19 | UNIQUE INDEX code_UNIQUE (code ASC) 20 | )`; 21 | 22 | return await db.createTable("course", query); 23 | }; 24 | 25 | export default course; 26 | -------------------------------------------------------------------------------- /models/courseMeta.ts: -------------------------------------------------------------------------------- 1 | import {CreateTable} from ".."; 2 | 3 | /** 4 | * Creates table for user course meta data. 5 | * Sample meta_keys: description, lessons, resources, students, etc.. 6 | * Multiple rows per user. One per meta_key : meta_value pair. // TODO: Create CRUD methods to manage course keys/values 7 | * Visual: https://i.imgur.com/xJoPpH5.png - // TODO: Update this comment after task completed. 8 | * @param {db} db 9 | */ 10 | const courseMeta = async (db: CreateTable) => { 11 | const query = `CREATE TABLE ${process.env.DBNAME}.course_meta 12 | ( 13 | course_meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, 14 | course_id bigint(20) unsigned NOT NULL DEFAULT '0', 15 | meta_key varchar(255) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL, 16 | meta_value longtext COLLATE utf8mb4_unicode_520_ci, 17 | PRIMARY KEY (course_meta_id), 18 | KEY course_id (course_id), 19 | KEY meta_key (meta_key), 20 | UNIQUE INDEX course_meta_id_UNIQUE (course_meta_id) 21 | ) `; 22 | 23 | return await db.createTable("course_meta", query); 24 | }; 25 | 26 | export default courseMeta; 27 | 28 | -------------------------------------------------------------------------------- /models/lesson.ts: -------------------------------------------------------------------------------- 1 | import {CreateTable} from ".."; 2 | 3 | /** 4 | * Creates the lesson database. Only one row per user. 5 | * @param {db} db 6 | */ 7 | const lesson = async (db: CreateTable) => { 8 | const query = ` 9 | CREATE TABLE ${process.env.DBNAME}.lesson ( 10 | lesson_id BIGINT(20) NOT NULL AUTO_INCREMENT, 11 | code VARCHAR(45) NULL, 12 | status TINYINT NOT NULL DEFAULT 0, 13 | name VARCHAR(150) NOT NULL, 14 | PRIMARY KEY (lesson_id), 15 | UNIQUE INDEX name_UNIQUE (name ASC), 16 | UNIQUE INDEX lesson_id_UNIQUE (lesson_id ASC), 17 | UNIQUE INDEX code_UNIQUE (code ASC) 18 | )`; 19 | 20 | return await db.createTable("lesson", query); 21 | }; 22 | 23 | export default lesson; 24 | -------------------------------------------------------------------------------- /models/lessonMeta.ts: -------------------------------------------------------------------------------- 1 | import {CreateTable} from ".."; 2 | 3 | /** 4 | * Creates table for user lesson meta data. 5 | * Sample meta_keys: description, lessons, resources, students, etc.. 6 | * Multiple rows per user. One per meta_key : meta_value pair. // TODO: Create CRUD methods to manage lesson keys/values 7 | * Visual: https://i.imgur.com/xJoPpH5.png - // TODO: Update this comment after task completed. 8 | * @param {db} db 9 | */ 10 | const lessonMeta = async (db: CreateTable) => { 11 | const query = `CREATE TABLE ${process.env.DBNAME}.lesson_meta 12 | ( 13 | lesson_meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, 14 | lesson_id bigint(20) unsigned NOT NULL DEFAULT '0', 15 | meta_key varchar(255) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL, 16 | meta_value longtext COLLATE utf8mb4_unicode_520_ci, 17 | PRIMARY KEY (lesson_meta_id), 18 | KEY lesson_id (lesson_id), 19 | KEY meta_key (meta_key), 20 | UNIQUE INDEX lesson_meta_id_UNIQUE (lesson_meta_id) 21 | ) `; 22 | 23 | return await db.createTable("lesson_meta", query); 24 | }; 25 | 26 | export default lessonMeta; 27 | 28 | -------------------------------------------------------------------------------- /models/message.ts: -------------------------------------------------------------------------------- 1 | import {CreateTable} from ".."; 2 | 3 | /** 4 | * Creates the message database table. Only one row per message. 5 | * @param {db} db 6 | */ 7 | const message = async (db: CreateTable) => { 8 | const query = ` 9 | CREATE TABLE ${process.env.DBNAME}.message ( 10 | id INT NOT NULL AUTO_INCREMENT, 11 | timestamp DATETIME NOT NULL DEFAULT NOW(), 12 | sender_id INT NOT NULL, 13 | recipient_id INT NOT NULL, 14 | body VARCHAR(350) NOT NULL, 15 | is_read TINYINT NULL DEFAULT 0, 16 | expiry_date DATETIME NULL COMMENT 'This column is the cut-off date when reminders will no longer be sent to users.', 17 | recipient_group LONGTEXT NULL, 18 | is_reminder TINYINT NULL DEFAULT 0 COMMENT 'This column flags whether or not a reminder is required for the message.', 19 | next_remind_date DATETIME NULL COMMENT 'This column holds the date when the next reminder needs to be sent. The reminder will be sent on the next_remind_date for the users for whom the ‘is_read’ flag is still ZERO. A new value for this column will be calculated every time a reminder is sent.', 20 | reminder_frequency_id INT NULL COMMENT 'This column signifies the frequency of the reminder. Should it be on daily basis or weekly basis?', 21 | ephemeral TINYINT NULL DEFAULT 0 COMMENT 'If set to true, we will delete this message on specific date.', 22 | removal_date DATETIME NULL COMMENT 'Date for ephemeral message to be removed.', 23 | PRIMARY KEY (id), 24 | UNIQUE INDEX id_UNIQUE (id ASC) 25 | )`; 26 | 27 | return await db.createTable("message", query); 28 | }; 29 | 30 | export default message; 31 | -------------------------------------------------------------------------------- /models/notification.ts: -------------------------------------------------------------------------------- 1 | import {CreateTable} from ".."; 2 | 3 | /** 4 | * Creates the notification database table. Only one row per notification. 5 | * @param {db} db 6 | */ 7 | const notification = async (db: CreateTable) => { 8 | const query = ` 9 | CREATE TABLE ${process.env.DBNAME}.notification ( 10 | id INT NOT NULL AUTO_INCREMENT, 11 | user_id INT NOT NULL, 12 | title VARCHAR(150) NOT NULL, 13 | message VARCHAR(200) NOT NULL, 14 | is_read TINYINT NULL DEFAULT 0, 15 | type_id INT NOT NULL DEFAULT 0, 16 | PRIMARY KEY (id), 17 | UNIQUE INDEX id_UNIQUE (id ASC)) 18 | `; 19 | 20 | return await db.createTable("notification", query); 21 | }; 22 | 23 | export default notification; 24 | -------------------------------------------------------------------------------- /models/options.ts: -------------------------------------------------------------------------------- 1 | import {CreateTable} from ".."; 2 | 3 | /** 4 | * Creates table for site options. 5 | * Sample option_key: admin_email, home url, theme settings, logo, etc. 6 | * Multiple rows per user. One per meta_key : meta_value pair. // TODO: Create CRUD methods to manage option_keys/values 7 | * Visual: https://i.imgur.com/xJoPpH5.png - // TODO: Update this comment after task completed. 8 | * @param {db} db 9 | */ 10 | const profile = async (db: CreateTable) => { 11 | const query = `CREATE TABLE ${process.env.DBNAME}.options 12 | ( 13 | option_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, 14 | meta_key varchar(255) COLLATE utf8mb4_unicode_520_ci NOT NULL, 15 | meta_value longtext COLLATE utf8mb4_unicode_520_ci NOT NULL, 16 | autoload tinyint NOT NULL DEFAULT 0, 17 | PRIMARY KEY (option_id), 18 | KEY key (meta_key), 19 | UNIQUE INDEX option_id_UNIQUE (option_id), 20 | UNIQUE INDEX meta_key_UNIQUE (meta_key), 21 | ) `; 22 | 23 | return await db.createTable("user_meta", query); 24 | }; 25 | 26 | export default profile; 27 | 28 | -------------------------------------------------------------------------------- /models/user.ts: -------------------------------------------------------------------------------- 1 | import {CreateTable} from ".."; 2 | 3 | /** 4 | * Creates the user database table. Only one row per user. 5 | * @param {db} db 6 | */ 7 | const user = async (db: CreateTable) => { 8 | const query = ` 9 | CREATE TABLE ${process.env.DBNAME}.user ( 10 | id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique user ID to identify a single unique user', 11 | username VARCHAR(35) NOT NULL COMMENT 'Username should be unique ', 12 | password VARCHAR(255) NOT NULL, 13 | email VARCHAR(100) NOT NULL, 14 | role VARCHAR(45) NOT NULL, 15 | last_login DATETIME NOT NULL DEFAULT now(), 16 | confirmation VARCHAR(50) NOT NULL, 17 | password_reset TINYINT NOT NULL DEFAULT 0, 18 | password_token VARCHAR(255), 19 | creation_date DATETIME NOT NULL DEFAULT now(), 20 | PRIMARY KEY (id), 21 | UNIQUE INDEX id_UNIQUE (id ASC), 22 | UNIQUE INDEX username_UNIQUE (username ASC), 23 | UNIQUE INDEX email_UNIQUE (email ASC))`; 24 | 25 | return await db.createTable("user", query); 26 | }; 27 | 28 | export default user; 29 | -------------------------------------------------------------------------------- /models/userMeta.ts: -------------------------------------------------------------------------------- 1 | import {CreateTable} from ".."; 2 | 3 | /** 4 | * Creates table for user meta data. 5 | * Sample meta_keys: status, courses, quizzes, uploads, personal information, social, etc. 6 | * Multiple rows per user. One per meta_key : meta_value pair. // TODO: Create CRUD methods to manage meta_keys/values 7 | * Visual: https://i.imgur.com/xJoPpH5.png - // TODO: Update this comment after task completed. 8 | * @param {db} db 9 | */ 10 | const userMeta = async (db: CreateTable) => { 11 | const query = `CREATE TABLE ${process.env.DBNAME}.user_meta 12 | ( 13 | user_meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, 14 | user_id bigint(20) unsigned NOT NULL DEFAULT '0', 15 | meta_key varchar(255) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL, 16 | meta_value longtext COLLATE utf8mb4_unicode_520_ci, 17 | PRIMARY KEY (user_meta_id), 18 | KEY user_id (user_id), 19 | KEY meta_key (meta_key), 20 | UNIQUE INDEX user_meta_id_UNIQUE (user_meta_id) 21 | ) `; 22 | 23 | return await db.createTable("user_meta", query); 24 | }; 25 | 26 | export default userMeta; 27 | 28 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | // / 2 | // / 3 | -------------------------------------------------------------------------------- /next-seo.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | export default { 3 | title: "Authentication System", 4 | openGraph: { 5 | type: 'website', 6 | locale: 'en_IE', 7 | url: 'https://www.github.com/lloan/next-authenticate', 8 | site_name: 'Authenticate', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const {parsed: localEnv} = require("dotenv").config(); 2 | const webpack = require("webpack"); 3 | const StylelintPlugin = require('stylelint-webpack-plugin'); 4 | const withCSS = require("@zeit/next-css"); 5 | const withSASS = require("@zeit/next-sass"); 6 | const withImages = require("next-images"); 7 | 8 | module.exports = withImages(withCSS(withSASS({ 9 | webpack(config) { 10 | config.plugins.push(new webpack.EnvironmentPlugin(localEnv)); 11 | config.plugins.push(new StylelintPlugin({ 12 | fix: true, 13 | })); 14 | 15 | return config; 16 | }, 17 | }))); 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spark-lms", 3 | "version": "0.0.1", 4 | "private": true, 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/inland-empire-software-development/spark" 8 | }, 9 | "scripts": { 10 | "dev": "next dev", 11 | "setup": "node setup.js", 12 | "development": "next start -p 6001", 13 | "staging": "next start -p 6002", 14 | "production": "next start -p 6003", 15 | "build": "next build", 16 | "start": "next start", 17 | "lint": "./node_modules/.bin/eslint './**/*.{js,ts,tsx}' --quiet --fix || true", 18 | "test": "jest || true", 19 | "jest": "jest --watchAll", 20 | "reset": "rm -rf src/ sass/ pages/ .next/ node_modules/ yarn.lock models/ lib/ email/ && git reset --hard && yarn install", 21 | "storybook": "start-storybook -s ./public HOST=http://localhost:3000/ -p 6006 -c .storybook", 22 | "sbdev": "start-storybook -s ./public HOST=http://localhost:6001/ -p 7001 -c .storybook", 23 | "sbstg": "start-storybook -s ./public HOST=http://localhost:6002/ -p 7002 -c .storybook", 24 | "sbprod": "start-storybook -s ./public HOST=http://localhost:6003/ -p 7003 -c .storybook" 25 | }, 26 | "dependencies": { 27 | "@types/formidable": "^1.0.31", 28 | "@types/react-image-crop": "^8.1.2", 29 | "@types/uikit": "^2.27.7", 30 | "@zeit/next-css": "^1.0.1", 31 | "@zeit/next-sass": "^1.0.1", 32 | "app-root-path": "^3.0.0", 33 | "babel-jest": "^24.9.0", 34 | "bcryptjs": "^2.4.3", 35 | "bluebird": "^3.7.2", 36 | "browser-image-compression": "^1.0.9", 37 | "dotenv": "^8.2.0", 38 | "formidable": "^1.2.2", 39 | "fs": "^0.0.1-security", 40 | "glob": "^7.1.6", 41 | "isomorphic-unfetch": "^3.0.0", 42 | "js-cookie": "^2.2.1", 43 | "jsonwebtoken": "^8.5.1", 44 | "multer": "^1.4.2", 45 | "mysql": "^2.17.1", 46 | "next": "^9.3.1", 47 | "next-images": "^1.2.0", 48 | "next-seo": "^4.4.0", 49 | "node-fetch": "^2.6.0", 50 | "node-sass": "^4.13.0", 51 | "nodemailer": "^6.4.1", 52 | "path": "^0.12.7", 53 | "react": "16.12.0", 54 | "react-image-crop": "^8.6.2", 55 | "redis": "^2.8.0", 56 | "sass": "^1.25.0", 57 | "sql-string-escape": "^1.1.6", 58 | "typescript": "^3.7.3", 59 | "uikit": "^3.3.6" 60 | }, 61 | "devDependencies": { 62 | "@storybook/addon-a11y": "^5.3.7", 63 | "@storybook/addon-actions": "^5.3.6", 64 | "@storybook/addon-backgrounds": "^5.3.7", 65 | "@storybook/addon-centered": "^5.3.7", 66 | "@storybook/addon-knobs": "^5.3.6", 67 | "@storybook/addon-links": "^5.3.6", 68 | "@storybook/addon-notes": "^5.3.6", 69 | "@storybook/addon-storysource": "^5.3.7", 70 | "@storybook/addon-viewport": "^5.3.7", 71 | "@storybook/addons": "^5.3.6", 72 | "@storybook/cli": "^5.3.8", 73 | "@storybook/react": "^5.3.7", 74 | "@types/jest": "^24.9.0", 75 | "@types/marked": "^0.7.2", 76 | "@types/next": "^9.0.0", 77 | "@types/next-seo": "^1.10.0", 78 | "@types/node": "^12.12.17", 79 | "@types/react": "^16.9.16", 80 | "@types/react-dom": "^16.9.4", 81 | "@typescript-eslint/eslint-plugin": "^2.11.0", 82 | "@typescript-eslint/parser": "^2.11.0", 83 | "babel-eslint": "^10.0.3", 84 | "babel-loader": "^8.0.6", 85 | "babel-preset-react-app": "^9.1.0", 86 | "enzyme": "^3.11.0", 87 | "enzyme-adapter-react-16": "^1.15.2", 88 | "eslint": "^6.7.2", 89 | "eslint-config-google": "^0.14.0", 90 | "eslint-config-prettier": "^6.7.0", 91 | "eslint-loader": "^3.0.2", 92 | "eslint-plugin-css-modules": "^2.11.0", 93 | "eslint-plugin-import": "^2.18.2", 94 | "eslint-plugin-jsx-a11y": "^6.2.3", 95 | "eslint-plugin-react": "^7.16.0", 96 | "eslint-plugin-react-hooks": "^2.2.0", 97 | "jest": "^24.9.0", 98 | "marked": "^0.8.0", 99 | "postcss-loader": "^3.0.0", 100 | "react-addons-test-utils": "^15.6.2", 101 | "react-dom": "16.12.0", 102 | "react-test-renderer": "^16.12.0", 103 | "sass-loader": "^8.0.2", 104 | "sass-resources-loader": "^2.0.1", 105 | "style-loader": "^1.1.3", 106 | "stylelint-config-prettier": "^8.0.0", 107 | "stylelint-config-rational-order": "^0.1.2", 108 | "stylelint-config-standard": "^19.0.0", 109 | "stylelint-prettier": "^1.1.1", 110 | "stylelint-scss": "^3.13.0", 111 | "stylelint-webpack-plugin": "^1.1.2", 112 | "ts-jest": "^24.3.0" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../src/style/index.scss'; 2 | import App from 'next/app'; 3 | import {Context, defaultContext} from '../src/context'; 4 | import {redirects, unprotected} from '../src/pages'; 5 | import fetch from "isomorphic-unfetch"; 6 | import {DefaultSeo} from 'next-seo'; 7 | import SEO from '../next-seo.config'; 8 | import Unauthorized from "../src/components/global/Unauthorized/Unauthorized"; 9 | import Redirect from "../src/components/animation/Redirect/Redirect"; 10 | import Loader from "../src/components/animation/Loader/Loader"; 11 | import {MyAppContext} from "../"; 12 | import Spinner from '../src/components/global/Spinner/Spinner'; 13 | export default class MyApp extends App<{}, {}, MyAppContext> { 14 | state: React.ComponentState = { 15 | ...defaultContext, 16 | } 17 | 18 | componentDidMount(): void { 19 | const {pathname} = this.props.router; 20 | const redirect = redirects[pathname] ? redirects[pathname].redirect : undefined; 21 | const isPublic = unprotected.includes(pathname); 22 | 23 | fetch(`${process.env.HOST}api/authenticate/auth`, { 24 | method: 'POST', 25 | }) 26 | .then((res) => res.json()) 27 | .then((data) => { 28 | // Allows us to set new values for context that is shared throughout the application 29 | const setContextProperty = (value: any) => { 30 | this.setState(value); 31 | }; 32 | this.setState({...data, setContextProperty, redirect, isPublic, isAccessFetched: true}); 33 | }); 34 | }; 35 | 36 | redirect(redirect: string) { 37 | setTimeout(() => { 38 | if ((process as any).browser) { 39 | document.location.href = redirect; 40 | } 41 | console.log('redirecting...'); 42 | }, 2000); 43 | } 44 | 45 | render() { 46 | const {Component, pageProps} = this.props; 47 | const {access, redirect, isPublic, isAccessFetched} = this.state; 48 | 49 | if (isAccessFetched) { 50 | if (access && redirect) { 51 | // send user to proper page if they're logged in 52 | this.redirect(redirect); 53 | return ; 54 | } else if (access || isPublic) { 55 | return ( 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | } else if (!access) { 63 | return ; 64 | } else { 65 | this.redirect("/authenticate"); 66 | return ; 67 | } 68 | } else { 69 | return ; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, {Html, Head, Main, NextScript} from 'next/document'; 2 | /** 3 | * Shared components amongst all pages - resources needed placed here. 4 | */ 5 | class Doc extends Document { 6 | /** 7 | * @return {JSX.Element} 8 | */ 9 | render(): JSX.Element { 10 | return ( 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | } 27 | 28 | export default Doc; 29 | -------------------------------------------------------------------------------- /pages/api/authenticate/auth.ts: -------------------------------------------------------------------------------- 1 | import client from '../../../lib/redis'; 2 | import {NextApiResponse, NextApiRequest} from 'next'; 3 | 4 | export default async (req: NextApiRequest, res: NextApiResponse) => { 5 | const token = req.cookies['portal-token']; 6 | const user = req.cookies['portal-user']; 7 | const userID = req.cookies['portal-user-id']; 8 | const access = await client.checkToken(token, {user, userID}); 9 | 10 | const data = { 11 | user, // user that will be rendered on the front-end 12 | userID, 13 | access, // is user authenticated / valid? 14 | }; 15 | 16 | res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); 17 | res.status(200); 18 | res.send(data); 19 | }; 20 | -------------------------------------------------------------------------------- /pages/api/authenticate/login.ts: -------------------------------------------------------------------------------- 1 | import db from '../../../lib/db'; 2 | import auth from '../../../lib/auth'; 3 | import client from '../../../lib/redis'; 4 | import {Message} from '../../..'; 5 | import {NextApiResponse, NextApiRequest} from 'next'; 6 | 7 | export default async (req: NextApiRequest, res: NextApiResponse) => { 8 | // set headers 9 | res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); 10 | res.setHeader("Content-Type", "json/javascript"); 11 | 12 | // messages 13 | const invalid = { 14 | status: false, 15 | message: "invalid credentials", 16 | } as Message; 17 | const valid = { 18 | status: true, 19 | message: "access granted", 20 | } as Message; 21 | const unconfirmed = { 22 | status: false, 23 | message: "account has not been activated", 24 | } as Message; 25 | 26 | // Get credentials from JSON body 27 | const {username, password} = req.body; 28 | 29 | const user = await db.getUser(escape(username)); 30 | 31 | res.status(200); 32 | 33 | // if user is not found, return early 34 | if (!user) { 35 | res.send(invalid); 36 | res.end(); 37 | return false; 38 | } 39 | 40 | // if user found, verify password matches 41 | if (auth.verifyPassword(password, user.password)) { 42 | if (user.confirmation === "active") { 43 | // password matched - create new token 44 | const token = auth.tokenize(password); 45 | 46 | // set token in redis 47 | client.setToken(token, {user: username, userID: user.id}); 48 | 49 | // update last login with true flag 50 | const updateLoginResp = await db.updateUser(user.id, {}, true); 51 | 52 | if (!updateLoginResp.status) { 53 | res.send(invalid); 54 | res.end(); 55 | } 56 | 57 | // set HttpOnly cookies 58 | res.setHeader('Set-Cookie', [`portal-token=${token}; path=/; HttpOnly`, `portal-user=${username}; path=/; HttpOnly`, `portal-user-id=${user.id}; path=/; HttpOnly`]); 59 | res.send(valid); 60 | res.end(); 61 | return true; 62 | } else { 63 | res.send(unconfirmed); 64 | res.end(); 65 | return false; 66 | } 67 | } else { 68 | // let user know this was an invalid request 69 | res.send(invalid); 70 | res.end(); 71 | return false; 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /pages/api/authenticate/logout.ts: -------------------------------------------------------------------------------- 1 | import {Message} from '../../..'; 2 | import {NextApiRequest, NextApiResponse} from 'next'; 3 | 4 | export default async (_req: NextApiRequest, res: NextApiResponse) => { 5 | // set headers 6 | res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); 7 | res.setHeader('Set-Cookie', [`portal-token=; Path=/; HttpOnly`, `portal-user=; Path=/; HttpOnly`, `portal-user-id=; Path=/;HttpOnly`]); 8 | res.status(200); 9 | 10 | res.send({ 11 | status: true, 12 | message: "You have been logged out.", 13 | } as Message); 14 | }; 15 | -------------------------------------------------------------------------------- /pages/api/authenticate/reset.ts: -------------------------------------------------------------------------------- 1 | import db from '../../../lib/db'; 2 | import {Message} from '../../..'; 3 | import {NextApiRequest, NextApiResponse} from 'next'; 4 | 5 | export default (req: NextApiRequest, res: NextApiResponse): void => { 6 | // set headers 7 | res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); 8 | res.setHeader("Content-Type", "json/javascript"); 9 | 10 | // Get credentials from JSON body 11 | const {email, token, user, action, password} = req.body; 12 | 13 | function handlePasswordVerification() { 14 | console.log(req.body); 15 | db.verifyPasswordToken(escape(email), escape(token)) 16 | .then((data: any) => { 17 | res.send(data); 18 | }) 19 | .catch((error: Error) => { 20 | console.log(error); 21 | }); 22 | } 23 | 24 | function handlePasswordResetInitiation(message: any) { 25 | const user = message.data.username; 26 | 27 | // will send email to user, starting the process 28 | // will also update two columns for user - password_token, password_reset 29 | if (user !== undefined) { 30 | db.initiatePasswordReset(escape(user), escape(email)) 31 | .then((data: Message) => { 32 | res.send(data); 33 | console.log(data); 34 | }) 35 | .catch((error: Error) => { 36 | console.log(error); 37 | }); 38 | } else { 39 | res.send({ 40 | status: false, 41 | message: "Invalid email provided.", 42 | } as Message); 43 | } 44 | } 45 | 46 | function handlePasswordReset() { 47 | if (token !== undefined && user !== undefined) { 48 | console.log(password, escape(password)); 49 | db.passwordReset(escape(user), escape(token), escape(password)) 50 | .then((data: any) => { 51 | res.send(data); 52 | }).catch((error: Error) => { 53 | console.log(error); 54 | }); 55 | } 56 | } 57 | 58 | if (email && action) { 59 | db.getUserByEmail(escape(email)) 60 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 61 | .then(async (data: any) => { 62 | switch (action) { 63 | case "verify": 64 | handlePasswordVerification(); 65 | break; 66 | case "initiate": 67 | handlePasswordResetInitiation(data); 68 | break; 69 | case "reset": 70 | handlePasswordReset(); 71 | break; 72 | default: 73 | res.send({ 74 | status: false, 75 | message: "Request denied.", 76 | }); 77 | } 78 | }); 79 | } else { 80 | res.send({ 81 | status: false, 82 | message: "Request denied.", 83 | }); 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /pages/api/authenticate/signup.ts: -------------------------------------------------------------------------------- 1 | import db from '../../../lib/db'; 2 | import {Message} from '../../..'; 3 | import {NextApiRequest, NextApiResponse} from 'next'; 4 | 5 | export default async (req: NextApiRequest, res: NextApiResponse): Promise => { 6 | // Get credentials from JSON body 7 | const {username, email, password, role} = req.body; 8 | 9 | if (username && email && password) { 10 | db.createUser(username, password, email, role) 11 | .then((result: any) => { 12 | res.send(JSON.stringify(result)); 13 | res.end(); 14 | } ) 15 | .catch((error: Error) => { 16 | res.send(JSON.stringify(error)); 17 | res.end(); 18 | }); 19 | } else { 20 | res.send({ 21 | status: false, 22 | message: "Error with data provided.", 23 | } as Message); 24 | res.end(); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /pages/api/course-students/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import {NextApiResponse, NextApiRequest} from 'next'; 3 | import db from '../../../lib/db'; 4 | 5 | interface Student { 6 | id: number; 7 | imageUri: string; 8 | name: string; 9 | email: string; 10 | status: number; 11 | } 12 | 13 | interface Course { 14 | id: string; 15 | code: string; 16 | name: string; 17 | students: Array; 18 | } 19 | 20 | export default async (req: NextApiRequest, res: NextApiResponse) => { 21 | const user = req.cookies['portal-user']; 22 | const userID = req.cookies['portal-user-id']; 23 | 24 | if (!user || !userID) { 25 | res.status(401).send({ 26 | message: "User is not authorized", 27 | }); 28 | } else { 29 | // TODO - Verifiy user's role is teacher/admin 30 | switch (req.method) { 31 | case "GET": 32 | db.getAllCoursesByInstructor(userID) 33 | .then((result: any) => { 34 | const {courses, users} = result.data; 35 | 36 | const c = courses.map((course: any) => { 37 | return { 38 | id: course.course_id, 39 | code: course.code, 40 | name: course.name, 41 | students: course.students_enrolled.split(',').map((student: any) => Object.assign(users[student], {id: student})), 42 | } as Course; 43 | }); 44 | res.status(200).send(c); 45 | }).catch(() => { 46 | res.status(500).send({ 47 | message: "Unknown error", 48 | }); 49 | }); 50 | break; 51 | default: 52 | res.status(404).end(); 53 | } 54 | } 55 | 56 | // students for all courses you're an instructor for 57 | }; 58 | -------------------------------------------------------------------------------- /pages/api/course/add.ts: -------------------------------------------------------------------------------- 1 | import db from "../../../lib/db"; 2 | 3 | export default async (req: any) => { 4 | const data = JSON.parse(req.body); 5 | const {courseTitle, cohortID, courseStart, courseExpire, courseDescription} = data; 6 | 7 | 8 | const query = `INSERT INTO courses (courseTitle, cohortID, courseStart, courseExpire, courseDescription) 9 | values ("${courseTitle}","${cohortID}","${courseStart}", "${courseExpire}", "${courseDescription}")`; 10 | 11 | console.log(query); 12 | 13 | db.query(query, (error: any, response: any) => { 14 | if (error) throw error; 15 | console.log(response); 16 | }); 17 | }; 18 | 19 | 20 | -------------------------------------------------------------------------------- /pages/api/course/addPicture.ts: -------------------------------------------------------------------------------- 1 | 2 | // import formidable from "formidable"; 3 | 4 | export const config = { 5 | api: { 6 | bodyParser: false, 7 | }, 8 | }; 9 | 10 | 11 | // export default async (req: any, res: any) => { 12 | // const form = new formidable.IncomingForm(); 13 | // form.uploadDir = './public/images/coursePicture'; 14 | // form.maxFileSize = 2; 15 | // form.on('file', (filename, file) => { 16 | // console.log(file.path); 17 | // // form.emit('data', {name: 'file', key: filename, value: file}); 18 | // }); 19 | // // form.parse(req, function(err, fields, files) { 20 | // // // console.log(files[0]); 21 | // // res.end(); 22 | // // }); 23 | // }; 24 | 25 | 26 | -------------------------------------------------------------------------------- /pages/api/mail.ts: -------------------------------------------------------------------------------- 1 | import ReactDOMServer from 'react-dom/server'; 2 | import confirmationEmail from './../../email/templates/main/confirmationEmail'; 3 | import resetPasswordEmail from './../../email/templates/main/resetPasswordEmail'; 4 | import {Message} from '../..'; 5 | import {NextApiRequest, NextApiResponse} from "next"; 6 | 7 | const nodemailer = require('nodemailer'); 8 | const transporter = nodemailer.createTransport({ 9 | host: process.env.MAILHOST, 10 | port: process.env.MAILPORT, 11 | auth: { 12 | user: process.env.MAILUSER, 13 | pass: process.env.MAILPASS, 14 | }, 15 | tls: { 16 | rejectUnauthorized: false, 17 | }, 18 | }); 19 | 20 | const handleAction = (options: { action: string; username: string; email: string; data?: any }) => { 21 | const {action, username, email, data} = options; 22 | 23 | // handle the action requested 24 | switch (action) { 25 | case "confirm": 26 | return { 27 | subject: "Confirm your email.", 28 | html: ReactDOMServer.renderToStaticMarkup(confirmationEmail( { 29 | username, 30 | url: (process as any).env.HOST, 31 | token: data.token, 32 | title: "Comfirm your email.", 33 | })), 34 | }; 35 | case "reset": 36 | return { 37 | subject: "Reset your password.", 38 | html: ReactDOMServer.renderToStaticMarkup(resetPasswordEmail( { 39 | username, 40 | email, 41 | url: (process as any).env.HOST, 42 | token: data.token, 43 | title: "Reset your password.", 44 | })), 45 | }; 46 | } 47 | }; 48 | 49 | export default async (req: NextApiRequest, res: NextApiResponse): Promise => { 50 | // set headers 51 | res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); 52 | res.setHeader("Content-Type", "json/javascript"); 53 | 54 | const {action, username, email, data} = req.body; 55 | 56 | if (!action || !username || !email) { 57 | res.send({ 58 | status: false, 59 | message: "Invalid input", 60 | } as Message); 61 | return; 62 | } 63 | 64 | const mailOptions= Object.assign({ 65 | from: '"Administrator" <' + process.env.MAILUSER + '>', 66 | to: String(email), 67 | }, handleAction({action, username, email, data})); 68 | 69 | 70 | transporter.sendMail(mailOptions, (err: any, info: string) => { 71 | if (err) { 72 | res.send({ 73 | status: false, 74 | message: "Email failed to send.", 75 | } as Message); 76 | } 77 | 78 | console.log("Email log: ", info); 79 | res.send({ 80 | status: true, 81 | message: "Email successfully sent.", 82 | } as Message); 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /pages/api/messages.ts: -------------------------------------------------------------------------------- 1 | import db from "../../lib/db"; 2 | import {Message} from "../../index"; 3 | import {NextApiRequest, NextApiResponse} from "next"; 4 | 5 | 6 | // Route to get meta data from the database. 7 | export default async (req: NextApiRequest, res: NextApiResponse) => { 8 | const {user, userID} = req.body; // get key provided by user 9 | 10 | if (user && userID) { 11 | const messages = await db.getMessageCount(req.body); 12 | res.send(JSON.stringify(messages)); 13 | } else { 14 | res.send({ 15 | status: false, 16 | message: "data passed incorrectly..", 17 | } as Message); 18 | } 19 | 20 | res.end(); 21 | }; 22 | -------------------------------------------------------------------------------- /pages/api/meta.ts: -------------------------------------------------------------------------------- 1 | import db from "../../lib/db"; 2 | import {Message} from "../../index"; 3 | import {NextApiRequest, NextApiResponse} from "next"; 4 | 5 | 6 | // Route to get meta data from the database. 7 | export default async (req: NextApiRequest, res: NextApiResponse) => { 8 | const {key} = req.body; // get key provided by user 9 | 10 | // Check if key is a string 11 | if (key && typeof key === "string") { 12 | const meta = await db.getKey(req.body); 13 | res.send(JSON.stringify(meta.meta_value)); 14 | // check if key is an array 15 | } else if (key && Array.isArray(key)) { 16 | const meta = await db.getKeys(req.body); 17 | res.send(JSON.stringify(meta)); 18 | } else { 19 | res.send({ 20 | status: false, 21 | message: "key(s) passed incorrectly..", 22 | } as Message); 23 | } 24 | 25 | res.end(); 26 | }; 27 | -------------------------------------------------------------------------------- /pages/api/notifications.ts: -------------------------------------------------------------------------------- 1 | import db from "../../lib/db"; 2 | import {Message} from "../../index"; 3 | import {NextApiRequest, NextApiResponse} from "next"; 4 | 5 | 6 | // Route to get meta data from the database. 7 | export default async (req: NextApiRequest, res: NextApiResponse) => { 8 | const {user, userID} = req.body; // get key provided by user 9 | 10 | if (user && userID) { 11 | const notifications = await db.getNotificationCount(req.body); 12 | res.send(JSON.stringify(notifications)); 13 | } else { 14 | res.send({ 15 | status: false, 16 | message: "data passed incorrectly..", 17 | } as Message); 18 | } 19 | 20 | res.end(); 21 | }; 22 | -------------------------------------------------------------------------------- /pages/api/setup.ts: -------------------------------------------------------------------------------- 1 | import db from "../../lib/db"; 2 | import {Message} from "../../index"; 3 | import {error} from "next/dist/build/output/log"; 4 | import {NextApiRequest, NextApiResponse} from "next"; 5 | 6 | const root = require("app-root-path"); 7 | const fs = require("fs"); 8 | 9 | interface StoreModel { 10 | [property: string]: {}; 11 | } 12 | 13 | type Messages = StoreModel; 14 | 15 | // Setup database tables if they don't exist. 16 | export default (req: NextApiRequest, res: NextApiResponse) => { 17 | const {secret} = req.query; // secret key from user requesting setup 18 | 19 | // check if secret given matches one in this environment 20 | if (secret !== undefined && process.env.SECRET === secret) { 21 | // Promise based 22 | new Promise((resolve, reject) => { 23 | const models: Messages = {}; // temporary store for all models 24 | 25 | // Iterate through file directory and find models 26 | fs.readdirSync(root + "/models") // directory to read 27 | .forEach((file: string) => { // iterate through each file in that directory 28 | const fileName = file.replace(".ts", ""); // remove .js 29 | const model = require("./../../models/" + fileName); // import the model 30 | models[fileName] = model.default; // add model to temp. store for later use 31 | }); 32 | 33 | // if models found, resolve, reject if nothing found 34 | models.length === 0 ? reject(error) : resolve(models); 35 | }) 36 | .then(async (data: any) => { 37 | const messages: Messages = {}; // temporary message store 38 | 39 | // iterate through all models found 40 | for (const model in data) { 41 | // model should have a main function with same name to trigger its creation 42 | if (data.hasOwnProperty(model)) { 43 | messages[model] = await data[model](db) as Message; // save result to the message store 44 | } 45 | } 46 | 47 | return await messages; 48 | }) 49 | .then((data) => { 50 | console.log('All models processed.'); 51 | res.send(data); // send all messages as an object in response 52 | }) 53 | .catch((error) => { 54 | res.send(error); // send error found in response 55 | }); 56 | } else { 57 | // secret did not match, let user know they're not authorized to run setup 58 | res.send({ 59 | status: false, 60 | message: "not authorized...", 61 | } as Message); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /pages/api/user/upload.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable indent */ 2 | /* eslint-disable comma-dangle */ 3 | // ======================================================================= 4 | // Known Issues/Todos 5 | // - 6 | // ======================================================================= 7 | 8 | // import db from '../../../lib/db'; 9 | // import auth from '../../../lib/auth'; 10 | import {createWriteStream} from 'fs'; 11 | import {Message} from '../../..'; 12 | import {NextApiResponse, NextApiRequest} from 'next'; 13 | 14 | export const config = { 15 | api: { 16 | bodyParser: false 17 | } 18 | }; 19 | 20 | export default async (req: NextApiRequest, res: NextApiResponse) => { 21 | // // set headers 22 | // res.setHeader( 23 | // 'Cache-Control', 24 | // 'no-store, no-cache, must-revalidate, proxy-revalidate' 25 | // ); 26 | 27 | let status = false; 28 | let message = 'Error: Image failed to upload'; 29 | 30 | if (req.pipe(createWriteStream("./public/images/avatars/" + req.headers['user-identification']))) { 31 | status = true; 32 | message = 'Image uploaded'; 33 | console.log('Will be sending this image to user ID: ' + req.headers['user-identification']); 34 | } 35 | res.statusCode = 200; 36 | 37 | const statusMessage = { 38 | status: status, 39 | message: message 40 | } as Message; 41 | 42 | // // example message - whatever you want to return, use this format. 43 | // // showing return example 44 | res.send(statusMessage); 45 | res.end(); 46 | }; 47 | 48 | -------------------------------------------------------------------------------- /pages/api/validate/confirm.ts: -------------------------------------------------------------------------------- 1 | import db from '../../../lib/db'; 2 | import {Message} from '../../..'; 3 | import {NextApiResponse, NextApiRequest} from 'next'; 4 | 5 | export default async (req: NextApiRequest, res: NextApiResponse): Promise => { 6 | const {user = false, token = false} = req.body; 7 | 8 | if (user && token) { 9 | db.confirmEmail(user, token) 10 | .then((result: any) => { 11 | if (result && result === 'active') { 12 | res.send({ 13 | status: true, 14 | message: "Account already activate.", 15 | } as Message); 16 | } else if (result && result.serverStatus === 2) { 17 | res.send({ 18 | status: true, 19 | message: "Account has been activated.", 20 | } as Message); 21 | } 22 | }).catch((error: Error) => { 23 | res.send({ 24 | status: false, 25 | message: error.message, 26 | } as Message); 27 | }); 28 | } else { 29 | res.send({ 30 | status: false, 31 | message: "Invalid input provided.", 32 | } as Message); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /pages/api/validate/email.ts: -------------------------------------------------------------------------------- 1 | import db from '../../../lib/db'; 2 | import {Message} from '../../../index'; 3 | import {NextApiRequest, NextApiResponse} from "next"; 4 | 5 | export default async (req: NextApiRequest, res: NextApiResponse) => { 6 | // Get credentials from JSON body 7 | const {email} = req.body; 8 | 9 | if (email) { 10 | db.emailExists(email) 11 | .then((result: any) => { 12 | res.send(JSON.stringify(result)); 13 | }).catch((error: any) => { 14 | res.send(JSON.stringify(error)); 15 | }); 16 | } else { 17 | res.send({ 18 | status: false, 19 | message: "Error with data provided.", 20 | } as Message); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /pages/api/validate/user.ts: -------------------------------------------------------------------------------- 1 | import db from '../../../lib/db'; 2 | import {Message} from '../../..'; 3 | import {NextApiRequest, NextApiResponse} from "next"; 4 | 5 | export default async (req: NextApiRequest, res: NextApiResponse) => { 6 | // Get credentials from JSON body 7 | const {username} = req.body; 8 | 9 | if (username) { 10 | db.userExists(username) 11 | .then((result: any) => { 12 | res.send(JSON.stringify(result)); 13 | }).catch((error: any) => { 14 | res.status(500); 15 | res.send(JSON.stringify(error)); 16 | }); 17 | } else { 18 | res.send({ 19 | status: false, 20 | message: "input not provided", 21 | } as Message); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /pages/authenticate.tsx: -------------------------------------------------------------------------------- 1 | import LogIn from '../src/components/authenticate/LogIn/LogIn'; 2 | import SignUp from '../src/components/authenticate/SignUp/SignUp'; 3 | import Reset from '../src/components/authenticate/Reset/Reset'; 4 | import ContentContainer from '../src/components/global/ContentContainer/ContentContainer'; 5 | import SEO from "../next-seo.config"; 6 | import {DefaultSeo} from "next-seo"; 7 | 8 | function Authenticate() { 9 | return ( 10 |
11 | 15 |
16 | 18 | 19 |
20 | 25 |
    26 |
  • 27 | 28 |
  • 29 |
  • 30 | 31 |
  • 32 |
  • 33 | 34 |
  • 35 |
36 |
37 | 38 | }/> 39 |
40 |
41 | ); 42 | } 43 | 44 | export default Authenticate; 45 | -------------------------------------------------------------------------------- /pages/bookmarks/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout"; 3 | 4 | const Bookmarks: React.FC = () => ( 5 | 6 |

Your Bookmarks

7 |
8 | ); 9 | 10 | export default Bookmarks; 11 | -------------------------------------------------------------------------------- /pages/community/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout"; 3 | 4 | const Community: React.FC = () => ( 5 | 6 |

Your Community

7 |
8 | ); 9 | 10 | export default Community; 11 | -------------------------------------------------------------------------------- /pages/confirmation.tsx: -------------------------------------------------------------------------------- 1 | import {NextSeo} from "next-seo"; 2 | import {useEffect, useState} from 'react'; 3 | import notify from "../src/components/utility/Notify"; 4 | import Redirect from "../src/components/animation/Redirect/Redirect"; 5 | 6 | /** 7 | * Check provided input - activate account if valid. 8 | * @param {any} props 9 | * @return {void} checks validity of input provided. 10 | */ 11 | function Confirmation(props: any) { 12 | const {query} = props; 13 | const {user, token} = query; 14 | const url = 'api/validate/confirm'; 15 | 16 | const [confirmation, setConfirmation] = useState(undefined); 17 | 18 | useEffect(() => { 19 | if (user && token) { 20 | fetch(process.env.HOST + url, { 21 | method: 'POST', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | }, 25 | body: JSON.stringify({ 26 | user, 27 | token, 28 | }), 29 | }) 30 | .then((response) => response.json()) 31 | .then((response) => { 32 | if (response.status) { 33 | notify({ 34 | message: response.message, 35 | status: 'success', 36 | pos: 'top-left', 37 | timeout: 5000, 38 | }); 39 | 40 | setTimeout(() => { 41 | if (document) { 42 | document.location.href = "/authenticate"; 43 | } 44 | }, 2500); 45 | } else { 46 | notify({ 47 | message: response.message, 48 | status: 'danger', 49 | pos: 'top-left', 50 | timeout: 5000, 51 | }); 52 | } 53 | 54 | setConfirmation(response.status); 55 | }) 56 | .catch((error) => { 57 | notify({ 58 | message: error.message, 59 | status: 'danger', 60 | pos: 'top-left', 61 | timeout: 5000, 62 | }); 63 | }); 64 | } 65 | }, []); 66 | 67 | if (props.query.hasOwnProperty('user') && props.query.hasOwnProperty('token')) { 68 | return ( 69 |
70 |
71 |
72 | {confirmation && 73 | (
74 | 77 |
78 | access granted 79 |
80 |
) 81 | } 82 | {!confirmation && 83 |
84 | 87 |
88 | access not granted 89 |
90 |
91 | } 92 |
93 |
94 |
95 | ); 96 | } else { 97 | setTimeout(()=> { 98 | if (document) { 99 | document.location.href = "/authenticate"; 100 | } 101 | }, 2500); 102 | return ( 103 | 104 | ); 105 | } 106 | } 107 | 108 | Confirmation.getInitialProps = ({query}: any) => { 109 | return {query}; 110 | }; 111 | 112 | export default Confirmation; 113 | -------------------------------------------------------------------------------- /pages/courses/add.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from 'next/head'; 3 | import DashboardLayout from "../../src/components/layouts/DashboardLayout"; 4 | import AddCourseComponent from '../../src/components/dashboard/panels/addCourse/AddCourse'; 5 | const AddCourse: React.FC = () => ( 6 | 7 | 8 |