├── .dockerignore ├── .env ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── build.yml │ ├── dockerimage.yml │ ├── lint.yml │ └── notify.yml ├── .gitignore ├── .nvmrc ├── .pa11yci ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── Procfile ├── README.md ├── app.js ├── app.json ├── app ├── client │ ├── assets │ │ ├── images │ │ │ ├── Banner.jpg │ │ │ ├── favicon.png │ │ │ ├── logo-color.svg │ │ │ └── logo.svg │ │ ├── schools.csv │ │ └── schools.json │ ├── index.html │ ├── src │ │ ├── app.js │ │ ├── constants.js │ │ ├── interceptors │ │ │ └── AuthInterceptor.js │ │ ├── modules │ │ │ ├── Session.js │ │ │ └── Utils.js │ │ ├── routes.js │ │ └── services │ │ │ ├── AuthService.js │ │ │ ├── SettingsService.js │ │ │ └── UserService.js │ ├── stylesheets │ │ ├── _admin.scss │ │ ├── _animation.scss │ │ ├── _application.scss │ │ ├── _base.scss │ │ ├── _branding.scss │ │ ├── _custom.scss │ │ ├── _dashboard.scss │ │ ├── _login.scss │ │ ├── _page.scss │ │ ├── _sidebar.scss │ │ ├── _team.scss │ │ ├── _verify.scss │ │ ├── normalize.css │ │ └── site.scss │ └── views │ │ ├── 404.html │ │ ├── admin │ │ ├── admin.html │ │ ├── adminCtrl.js │ │ ├── settings │ │ │ ├── adminSettingsCtrl.js │ │ │ └── settings.html │ │ ├── stats │ │ │ ├── adminStatsCtrl.js │ │ │ └── stats.html │ │ ├── user │ │ │ ├── adminUserCtrl.js │ │ │ └── user.html │ │ └── users │ │ │ ├── adminUsersCtrl.js │ │ │ └── users.html │ │ ├── application │ │ ├── application.html │ │ └── applicationCtrl.js │ │ ├── base.html │ │ ├── confirmation │ │ ├── confirmation.html │ │ └── confirmationCtrl.js │ │ ├── dashboard │ │ ├── dashboard.html │ │ └── dashboardCtrl.js │ │ ├── login │ │ ├── login.html │ │ └── loginCtrl.js │ │ ├── reset │ │ ├── reset.html │ │ └── resetCtrl.js │ │ ├── sidebar │ │ ├── sidebar.html │ │ └── sidebarCtrl.js │ │ ├── team │ │ ├── team.html │ │ └── teamCtrl.js │ │ └── verify │ │ ├── verify.html │ │ └── verifyCtrl.js └── server │ ├── controllers │ ├── SettingsController.js │ └── UserController.js │ ├── emails │ ├── email-basic │ │ ├── html.pug │ │ ├── style.css │ │ └── text.pug │ ├── email-link-action │ │ ├── html.pug │ │ ├── style.css │ │ └── text.pug │ └── email-verify │ │ ├── html.pug │ │ ├── style.css │ │ └── text.pug │ ├── models │ ├── Settings.js │ └── User.js │ ├── routes.js │ ├── routes │ ├── api.js │ └── auth.js │ └── services │ ├── email.js │ └── stats.js ├── config ├── admin.js └── settings.js ├── docs └── images │ └── screenshots │ ├── admin-users.png │ ├── application.png │ ├── dashboard.png │ ├── login.png │ ├── settings.png │ └── stats.png ├── gulpfile.js ├── package-lock.json ├── package.json ├── scripts ├── acceptUsers.js ├── createHellaUsers.js ├── getVerifyToken.js └── testChangePassword.js └── test.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | app/client/build/ 3 | app/client/plugins/ 4 | *.log 5 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # 'dev' or 'production' 2 | NODE_ENV= 'dev' 3 | 4 | # Connection string URI for your MongoDB database 5 | DATABASE='' 6 | 7 | # Port that this app runs on 8 | PORT='3000' 9 | 10 | # Long random string used to verify JWT tokens for user authentication 11 | JWT_SECRET='shhhh super secret code here bro' 12 | 13 | # Root URL for this app 14 | ROOT_URL='http://localhost:3000' 15 | 16 | # Credentials for the admin user created at app initialization 17 | ADMIN_EMAIL='admin@example.com' 18 | ADMIN_PASS='party' 19 | 20 | # Used to send verification, registration, and confirmation emails 21 | EMAIL_CONTACT='Hackathon Team ' 22 | EMAIL_HOST='smtp.gmail.com' 23 | EMAIL_USER='foo@bar.com' 24 | EMAIL_PASS='password' 25 | EMAIL_PORT='465' 26 | EMAIL_HEADER_IMAGE='https://s3.amazonaws.com/hackmit-assets/Banner_600.jpg' 27 | 28 | # Information linked at the bottom of emails 29 | EMAIL_ADDRESS='team@example.com' 30 | HACKATHON_NAME='Hackathon' 31 | TWITTER_HANDLE='hackathon' 32 | FACEBOOK_HANDLE='hackathon' 33 | 34 | # Limits the number of users that can join a team 35 | TEAM_MAX_SIZE=4 36 | 37 | # Used to send error messages to your Slack team when the API catches an error 38 | SLACK_HOOK='https://hooks.slack.com/services/yourapikey' 39 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /app/client/build/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 8, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "jsx": true 7 | } 8 | }, 9 | "rules": { 10 | "semi": "error" 11 | } 12 | } -------------------------------------------------------------------------------- /.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 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Desktop (please complete the following information):** 28 | 29 | - OS: [e.g. iOS] 30 | - Browser [e.g. chrome, safari] 31 | - Version [e.g. 22] 32 | 33 | **Smartphone (please complete the following information):** 34 | 35 | - Device: [e.g. iPhone6] 36 | - OS: [e.g. iOS8.1] 37 | - Browser [e.g. stock browser, safari] 38 | - Version [e.g. 22] 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /.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/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## How Has This Been Tested 11 | 12 | 13 | 14 | 15 | ## Screenshots (if appropriate) 16 | 17 | ## Types of changes 18 | 19 | - [ ] Bug fix (non-breaking change which fixes an issue) 20 | - [ ] New feature (non-breaking change which adds functionality) 21 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 22 | 23 | ## Checklist 24 | 25 | 26 | - [ ] My code follows the code style of this project. 27 | - [ ] My change requires a change to the documentation. 28 | - [ ] I have updated the documentation accordingly. 29 | - [ ] I have read the **CONTRIBUTING** document. 30 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout Code 11 | uses: actions/checkout@v1 12 | 13 | - name: Start MongoDB As Docker 14 | uses: wbari/start-mongoDB@v0.2 15 | with: 16 | mongoDBVersion: 4.2 17 | 18 | - name: Read .nvmrc 19 | run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)" 20 | id: nvm 21 | 22 | - name: Cache 23 | uses: actions/cache@v1 24 | with: 25 | path: ~/.npm 26 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 27 | restore-keys: | 28 | ${{ runner.os }}-node- 29 | 30 | - name: Setup Node 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: "${{ steps.nvm.outputs.NVMRC }}" 34 | 35 | - name: Install Dependencies 36 | if: steps.cache.outputs.cache-hit != 'true' 37 | run: npm install 38 | 39 | - name: Run Test Cases 40 | run: npm run test -------------------------------------------------------------------------------- /.github/workflows/dockerimage.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Docker Hub 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - name: Publish to Registry 14 | uses: elgohr/Publish-Docker-Github-Action@master 15 | with: 16 | name: ${{ secrets.DOCKER_USER }}/quill:latest 17 | username: ${{ secrets.DOCKER_USER }} 18 | password: ${{ secrets.DOCKER_PASS }} 19 | dockerfile: Dockerfile 20 | workdir: . 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout Code 11 | uses: actions/checkout@v1 12 | 13 | - name: ESLint 14 | uses: gimenete/eslint-action@master 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/notify.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: Notification Workflow 3 | jobs: 4 | slackNotification: 5 | name: Slack Notification 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | - name: Slack Notification 10 | uses: rtCamp/action-slack-notify@master 11 | env: 12 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 13 | 14 | - name: Discord notification 15 | env: 16 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 17 | uses: Ilshidur/action-discord@master 18 | with: 19 | args: 'The project has been deployed.' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project 2 | db/ 3 | node_modules/ 4 | semantic/ 5 | .env 6 | 7 | # Output 8 | app/client/build/ 9 | 10 | # macOS 11 | .DS_Store 12 | .python-version 13 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.17.0 -------------------------------------------------------------------------------- /.pa11yci: -------------------------------------------------------------------------------- 1 | { 2 | "standard": "WCAG2AAA", 3 | "level": "error", 4 | "defaults": { 5 | "timeout": 30000, 6 | "wait": 2000, 7 | "ignore": [] 8 | }, 9 | "urls": [ 10 | { 11 | "url": "http://localhost:3000/login" 12 | }, 13 | { 14 | "url": "http://localhost:3000", 15 | "actions": [ 16 | "wait for element #email to be added", 17 | "wait for element #password to be added", 18 | "wait for element .pa11y-click-login to be added", 19 | "set field #email to admin@example.com", 20 | "set field #password to party", 21 | "click element .pa11y-click-login", 22 | "wait for url to not be http://localhost:3000/login" 23 | ] 24 | }, 25 | { 26 | "url": "http://localhost:3000/application" 27 | }, 28 | { 29 | "url": "http://localhost:3000/team" 30 | }, 31 | { 32 | "url": "http://localhost:3000/admin" 33 | }, 34 | { 35 | "url": "http://localhost:3000/admin/users" 36 | }, 37 | { 38 | "url": "http://localhost:3000/admin/settings" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | All kinds of contributions to Quill are greatly appreciated. For someone 5 | unfamiliar with the code base, the most efficient way to contribute is usually 6 | to submit a [feature request](#feature-requests) or [bug report](#bug-reports). 7 | 8 | If you want to dive into the source code, you can submit a [patch](#patches) as 9 | well. Working on [existing issues][issues] is super helpful! 10 | 11 | Feature Requests 12 | ---------------- 13 | 14 | Do you have an idea for an awesome new feature for Quill? Please [submit a 15 | feature request][issue]. It's great to hear about new ideas. 16 | 17 | If you are inclined to do so, you're welcome to [fork][fork] Quill, work on 18 | implementing the feature yourself, and submit a patch. In this case, it's 19 | *highly recommended* that you first [open an issue][issue] describing your 20 | enhancement to get early feedback on the new feature that you are implementing. 21 | This will help avoid wasted efforts and ensure that your work is incorporated 22 | into the code base. 23 | 24 | Bug Reports 25 | ----------- 26 | 27 | Did something go wrong with Quill? Sorry about that! Bug reports are greatly 28 | appreciated! 29 | 30 | When you [submit a bug report][issue], please include relevant information such 31 | as Quill version, operating system, configuration, error messages, and steps to 32 | reproduce the bug. The more details you can include, the easier it is to find 33 | and fix the bug. 34 | 35 | Patches 36 | ------- 37 | 38 | Want to hack on Quill? Awesome! 39 | 40 | If there are [open issues][issues], you're more than welcome to work on those - 41 | this is probably the best way to contribute to Quill. If you have your own 42 | ideas, that's great too! In that case, before working on substantial changes to 43 | the code base, it is *highly recommended* that you first [open an issue][issue] 44 | describing what you intend to work on. 45 | 46 | **Patches are generally submitted as pull requests.** Patches are also 47 | [accepted over email][email]. 48 | 49 | Any changes to the code base should follow the style and coding conventions 50 | used in the rest of the project. The version history should be clean, and 51 | commit messages should be descriptive and [properly formatted][commit-messages]. 52 | 53 | --- 54 | 55 | If you have any questions about anything, feel free to [ask][email]! 56 | 57 | *Thanks to Anish Athalye for allowing Quill to shamelessly steal this contributing guide from [Gavel][gavel]!* 58 | 59 | [issue]: https://github.com/techx/quill/issues/new 60 | [issues]: https://github.com/techx/quill/issues 61 | [fork]: https://github.com/techx/quill/fork 62 | [email]: mailto:quill@hackmit.org 63 | [commit-messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 64 | [gavel]: https://github.com/anishathalye/gavel 65 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.17.0-buster-slim 2 | 3 | WORKDIR /usr/app/src 4 | EXPOSE 3000 5 | 6 | # Load Source 7 | COPY . . 8 | 9 | # Install node_modules 10 | RUN npm install 11 | RUN ./node_modules/.bin/gulp build 12 | 13 | CMD node app.js 14 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node node_modules/gulp/bin/gulp build && node app.js 2 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // Load the dotfiles. 2 | require('dotenv').load({silent: true}); 3 | 4 | var express = require('express'); 5 | 6 | // Middleware! 7 | var bodyParser = require('body-parser'); 8 | var methodOverride = require('method-override'); 9 | var morgan = require('morgan'); 10 | 11 | var mongoose = require('mongoose'); 12 | var port = process.env.PORT || 3000; 13 | var database = process.env.DATABASE || process.env.MONGODB_URI || "mongodb://localhost:27017"; 14 | 15 | var settingsConfig = require('./config/settings'); 16 | var adminConfig = require('./config/admin'); 17 | 18 | var app = express(); 19 | 20 | // Connect to mongodb 21 | mongoose.connect(database); 22 | 23 | app.use(morgan('dev')); 24 | 25 | app.use(bodyParser.urlencoded({ 26 | extended: true 27 | })); 28 | app.use(bodyParser.json()); 29 | 30 | app.use(methodOverride()); 31 | 32 | app.use(express.static(__dirname + '/app/client')); 33 | 34 | // Routers ===================================================================== 35 | 36 | var apiRouter = express.Router(); 37 | require('./app/server/routes/api')(apiRouter); 38 | app.use('/api', apiRouter); 39 | 40 | var authRouter = express.Router(); 41 | require('./app/server/routes/auth')(authRouter); 42 | app.use('/auth', authRouter); 43 | 44 | require('./app/server/routes')(app); 45 | 46 | // listen (start app with node server.js) ====================================== 47 | module.exports = app.listen(port); 48 | console.log("App listening on port " + port); -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Quill", 3 | "description": "Registration, for hackathons!", 4 | "env": { 5 | "NODE_ENV": { 6 | "description": "dev or production", 7 | "value": "dev" 8 | }, 9 | "JWT_SECRET": { 10 | "description": "Long random string used to verify JWT tokens for user authentication", 11 | "generator": "secret" 12 | }, 13 | "ROOT_URL": { 14 | "description": "Root URL for your registration system", 15 | "value": "http://localhost:3000" 16 | }, 17 | "ADMIN_EMAIL": { 18 | "description": "Credentials for the admin user created at app initialization", 19 | "value": "admin@example.com" 20 | }, 21 | "ADMIN_PASS": "party", 22 | "EMAIL_ADDRESS": { 23 | "description": "The email address that is included in the 'email us' link at the bottom of emails.", 24 | "value": "team@example.com" 25 | }, 26 | "HACKATHON_NAME": "Hackathon", 27 | "TWITTER_HANDLE": { 28 | "description": "Everything after https://twitter.com/", 29 | "value": "hackathon" 30 | }, 31 | "FACEBOOK_HANDLE": { 32 | "description": "Everything after https://facebook.com/", 33 | "value": "hackathon" 34 | }, 35 | "EMAIL_CONTACT": { 36 | "description": "Used to send verification, registration, and confirmation emails", 37 | "value": "Hackathon Team " 38 | }, 39 | "EMAIL_HOST": "smtp.gmail.com", 40 | "EMAIL_USER": "foo@bar.com", 41 | "EMAIL_PASS": "password", 42 | "EMAIL_PORT": "465", 43 | "EMAIL_HEADER_IMAGE": "https://s3.amazonaws.com/hackmit-assets/Banner_600.jpg", 44 | "TEAM_MAX_SIZE": { 45 | "description": "Limits the number of users that can join a team", 46 | "value": "4" 47 | }, 48 | "SLACK_HOOK": { 49 | "description": "Used to send error messages to your Slack team when the API catches an error", 50 | "value": "https://hooks.slack.com/services/your-api-key" 51 | } 52 | }, 53 | "addons": [ 54 | "mongolab" 55 | ], 56 | "keywords": ["quill", "node", "express", "mongo"] 57 | } 58 | -------------------------------------------------------------------------------- /app/client/assets/images/Banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techx/quill/3227d1f60409eb02d682e14dd63197cd3073f63a/app/client/assets/images/Banner.jpg -------------------------------------------------------------------------------- /app/client/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techx/quill/3227d1f60409eb02d682e14dd63197cd3073f63a/app/client/assets/images/favicon.png -------------------------------------------------------------------------------- /app/client/assets/images/logo-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/client/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | HackMIT 2015 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 40 | 41 | 42 | 56 | 57 | 58 | 59 |
60 | 61 | 62 | -------------------------------------------------------------------------------- /app/client/src/app.js: -------------------------------------------------------------------------------- 1 | const $ = require('jquery'); 2 | 3 | var angular = require('angular'); 4 | var uiRouter = require('angular-ui-router'); 5 | 6 | var app = angular.module('reg', [ 7 | 'ui.router', 8 | ]); 9 | 10 | const constants = require('./constants.js'); 11 | 12 | var AuthService = require('./services/AuthService.js'); 13 | var AuthInterceptor = require('./interceptors/AuthInterceptor.js'); 14 | var Session = require('./modules/Session.js'); 15 | 16 | var routes = require('./routes.js'); 17 | 18 | app 19 | .config([ 20 | '$httpProvider', 21 | function($httpProvider){ 22 | 23 | // Add auth token to Authorization header 24 | $httpProvider.interceptors.push('AuthInterceptor'); 25 | 26 | }]) 27 | .run([ 28 | 'AuthService', 29 | 'Session', 30 | function(AuthService, Session){ 31 | 32 | // Startup, login if there's a token. 33 | var token = Session.getToken(); 34 | if (token){ 35 | AuthService.loginWithToken(token); 36 | } 37 | 38 | }]); 39 | -------------------------------------------------------------------------------- /app/client/src/constants.js: -------------------------------------------------------------------------------- 1 | const angular = require('angular'); 2 | 3 | angular.module('reg') 4 | .constant('EVENT_INFO', { 5 | NAME: 'HackMIT 2015', 6 | }) 7 | .constant('DASHBOARD', { 8 | UNVERIFIED: 'You should have received an email asking you verify your email. Click the link in the email and you can start your application!', 9 | INCOMPLETE_TITLE: 'You still need to complete your application!', 10 | INCOMPLETE: 'If you do not complete your application before the [APP_DEADLINE], you will not be considered for the admissions lottery!', 11 | SUBMITTED_TITLE: 'Your application has been submitted!', 12 | SUBMITTED: 'Feel free to edit it at any time. However, once registration is closed, you will not be able to edit it any further.\nAdmissions will be determined by a random lottery. Please make sure your information is accurate before registration is closed!', 13 | CLOSED_AND_INCOMPLETE_TITLE: 'Unfortunately, registration has closed, and the lottery process has begun.', 14 | CLOSED_AND_INCOMPLETE: 'Because you have not completed your profile in time, you will not be eligible for the lottery process.', 15 | ADMITTED_AND_CAN_CONFIRM_TITLE: 'You must confirm by [CONFIRM_DEADLINE].', 16 | ADMITTED_AND_CANNOT_CONFIRM_TITLE: 'Your confirmation deadline of [CONFIRM_DEADLINE] has passed.', 17 | ADMITTED_AND_CANNOT_CONFIRM: 'Although you were accepted, you did not complete your confirmation in time.\nUnfortunately, this means that you will not be able to attend the event, as we must begin to accept other applicants on the waitlist.\nWe hope to see you again next year!', 18 | CONFIRMED_NOT_PAST_TITLE: 'You can edit your confirmation information until [CONFIRM_DEADLINE]', 19 | DECLINED: 'We\'re sorry to hear that you won\'t be able to make it to HackMIT 2015! :(\nMaybe next year! We hope you see you again soon.', 20 | }) 21 | .constant('TEAM',{ 22 | NO_TEAM_REG_CLOSED: 'Unfortunately, it\'s too late to enter the lottery with a team.\nHowever, you can still form teams on your own before or during the event!', 23 | }); 24 | -------------------------------------------------------------------------------- /app/client/src/interceptors/AuthInterceptor.js: -------------------------------------------------------------------------------- 1 | angular.module('reg') 2 | .factory('AuthInterceptor', [ 3 | 'Session', 4 | function(Session){ 5 | return { 6 | request: function(config){ 7 | var token = Session.getToken(); 8 | if (token){ 9 | config.headers['x-access-token'] = token; 10 | } 11 | return config; 12 | } 13 | }; 14 | }]); 15 | -------------------------------------------------------------------------------- /app/client/src/modules/Session.js: -------------------------------------------------------------------------------- 1 | angular.module('reg') 2 | .service('Session', [ 3 | '$rootScope', 4 | '$window', 5 | function($rootScope, $window){ 6 | 7 | this.create = function(token, user){ 8 | $window.localStorage.jwt = token; 9 | $window.localStorage.userId = user._id; 10 | $window.localStorage.currentUser = JSON.stringify(user); 11 | $rootScope.currentUser = user; 12 | }; 13 | 14 | this.destroy = function(onComplete){ 15 | delete $window.localStorage.jwt; 16 | delete $window.localStorage.userId; 17 | delete $window.localStorage.currentUser; 18 | $rootScope.currentUser = null; 19 | if (onComplete){ 20 | onComplete(); 21 | } 22 | }; 23 | 24 | this.getToken = function(){ 25 | return $window.localStorage.jwt; 26 | }; 27 | 28 | this.getUserId = function(){ 29 | return $window.localStorage.userId; 30 | }; 31 | 32 | this.getUser = function(){ 33 | return JSON.parse($window.localStorage.currentUser); 34 | }; 35 | 36 | this.setUser = function(user){ 37 | $window.localStorage.currentUser = JSON.stringify(user); 38 | $rootScope.currentUser = user; 39 | }; 40 | 41 | }]); -------------------------------------------------------------------------------- /app/client/src/modules/Utils.js: -------------------------------------------------------------------------------- 1 | const angular = require('angular'); 2 | const moment = require('moment'); 3 | 4 | angular.module('reg') 5 | .factory('Utils', [ 6 | function(){ 7 | return { 8 | isRegOpen: function(settings){ 9 | return Date.now() > settings.timeOpen && Date.now() < settings.timeClose; 10 | }, 11 | isAfter: function(time){ 12 | return Date.now() > time; 13 | }, 14 | formatTime: function(time){ 15 | 16 | if (!time){ 17 | return "Invalid Date"; 18 | } 19 | 20 | date = new Date(time); 21 | // Hack for timezone 22 | return moment(date).format('dddd, MMMM Do YYYY, h:mm a') + 23 | " " + date.toTimeString().split(' ')[2]; 24 | 25 | } 26 | }; 27 | }]); 28 | -------------------------------------------------------------------------------- /app/client/src/routes.js: -------------------------------------------------------------------------------- 1 | const angular = require('angular'); 2 | const SettingsService = require('./services/SettingsService.js'); 3 | const UserService = require('./services/UserService.js'); 4 | 5 | const AdminCtrl = require('../views/admin/adminCtrl.js'); 6 | const AdminSettingsCtrl = require('../views/admin/settings/adminSettingsCtrl.js'); 7 | const AdminStatsCtrl = require('../views/admin/stats/adminStatsCtrl.js'); 8 | const AdminUserCtrl = require('../views/admin/user/adminUserCtrl.js'); 9 | const AdminUsersCtrl = require('../views/admin/users/adminUsersCtrl.js'); 10 | const ApplicationCtrl = require('../views/application/applicationCtrl.js'); 11 | const ConfirmationCtrl = require('../views/confirmation/confirmationCtrl.js'); 12 | const DashboardCtrl = require('../views/dashboard/dashboardCtrl.js'); 13 | const LoginCtrl = require('../views/login/loginCtrl.js'); 14 | const ResetCtrl = require('../views/reset/resetCtrl.js'); 15 | const SidebarCtrl = require('../views/sidebar/sidebarCtrl.js'); 16 | const TeamCtrl = require('../views/team/teamCtrl.js'); 17 | const VerifyCtrl = require('../views/verify/verifyCtrl.js'); 18 | 19 | angular.module('reg') 20 | .config([ 21 | '$stateProvider', 22 | '$urlRouterProvider', 23 | '$locationProvider', 24 | function( 25 | $stateProvider, 26 | $urlRouterProvider, 27 | $locationProvider) { 28 | 29 | // For any unmatched url, redirect to /state1 30 | $urlRouterProvider.otherwise("/404"); 31 | 32 | // Set up de states 33 | $stateProvider 34 | .state('login', { 35 | url: "/login", 36 | templateUrl: "views/login/login.html", 37 | controller: 'LoginCtrl', 38 | data: { 39 | requireLogin: false 40 | }, 41 | resolve: { 42 | 'settings': function(SettingsService){ 43 | return SettingsService.getPublicSettings(); 44 | } 45 | } 46 | }) 47 | .state('app', { 48 | views: { 49 | '': { 50 | templateUrl: "views/base.html" 51 | }, 52 | 'sidebar@app': { 53 | templateUrl: "views/sidebar/sidebar.html", 54 | controller: 'SidebarCtrl', 55 | resolve: { 56 | settings: function(SettingsService) { 57 | return SettingsService.getPublicSettings(); 58 | } 59 | } 60 | } 61 | }, 62 | data: { 63 | requireLogin: true 64 | } 65 | }) 66 | .state('app.dashboard', { 67 | url: "/", 68 | templateUrl: "views/dashboard/dashboard.html", 69 | controller: 'DashboardCtrl', 70 | resolve: { 71 | currentUser: function(UserService){ 72 | return UserService.getCurrentUser(); 73 | }, 74 | settings: function(SettingsService){ 75 | return SettingsService.getPublicSettings(); 76 | } 77 | }, 78 | }) 79 | .state('app.application', { 80 | url: "/application", 81 | templateUrl: "views/application/application.html", 82 | controller: 'ApplicationCtrl', 83 | data: { 84 | requireVerified: true 85 | }, 86 | resolve: { 87 | currentUser: function(UserService){ 88 | return UserService.getCurrentUser(); 89 | }, 90 | settings: function(SettingsService){ 91 | return SettingsService.getPublicSettings(); 92 | } 93 | } 94 | }) 95 | .state('app.confirmation', { 96 | url: "/confirmation", 97 | templateUrl: "views/confirmation/confirmation.html", 98 | controller: 'ConfirmationCtrl', 99 | data: { 100 | requireAdmitted: true 101 | }, 102 | resolve: { 103 | currentUser: function(UserService){ 104 | return UserService.getCurrentUser(); 105 | } 106 | } 107 | }) 108 | .state('app.team', { 109 | url: "/team", 110 | templateUrl: "views/team/team.html", 111 | controller: 'TeamCtrl', 112 | data: { 113 | requireVerified: true 114 | }, 115 | resolve: { 116 | currentUser: function(UserService){ 117 | return UserService.getCurrentUser(); 118 | }, 119 | settings: function(SettingsService){ 120 | return SettingsService.getPublicSettings(); 121 | } 122 | } 123 | }) 124 | .state('app.admin', { 125 | views: { 126 | '': { 127 | templateUrl: "views/admin/admin.html", 128 | controller: 'AdminCtrl' 129 | } 130 | }, 131 | data: { 132 | requireAdmin: true 133 | } 134 | }) 135 | .state('app.admin.stats', { 136 | url: "/admin", 137 | templateUrl: "views/admin/stats/stats.html", 138 | controller: 'AdminStatsCtrl' 139 | }) 140 | .state('app.admin.users', { 141 | url: "/admin/users?" + 142 | '&page' + 143 | '&size' + 144 | '&query', 145 | templateUrl: "views/admin/users/users.html", 146 | controller: 'AdminUsersCtrl' 147 | }) 148 | .state('app.admin.user', { 149 | url: "/admin/users/:id", 150 | templateUrl: "views/admin/user/user.html", 151 | controller: 'AdminUserCtrl', 152 | resolve: { 153 | 'user': function($stateParams, UserService){ 154 | return UserService.get($stateParams.id); 155 | } 156 | } 157 | }) 158 | .state('app.admin.settings', { 159 | url: "/admin/settings", 160 | templateUrl: "views/admin/settings/settings.html", 161 | controller: 'AdminSettingsCtrl', 162 | }) 163 | .state('reset', { 164 | url: "/reset/:token", 165 | templateUrl: "views/reset/reset.html", 166 | controller: 'ResetCtrl', 167 | data: { 168 | requireLogin: false 169 | } 170 | }) 171 | .state('verify', { 172 | url: "/verify/:token", 173 | templateUrl: "views/verify/verify.html", 174 | controller: 'VerifyCtrl', 175 | data: { 176 | requireLogin: false 177 | } 178 | }) 179 | .state('404', { 180 | url: "/404", 181 | templateUrl: "views/404.html", 182 | data: { 183 | requireLogin: false 184 | } 185 | }); 186 | 187 | $locationProvider.html5Mode({ 188 | enabled: true, 189 | }); 190 | 191 | }]) 192 | .run($transitions => { 193 | $transitions.onStart({}, transition => { 194 | const Session = transition.injector().get("Session"); 195 | 196 | var requireLogin = transition.to().data.requireLogin; 197 | var requireAdmin = transition.to().data.requireAdmin; 198 | var requireVerified = transition.to().data.requireVerified; 199 | var requireAdmitted = transition.to().data.requireAdmitted; 200 | 201 | if (requireLogin && !Session.getToken()) { 202 | return transition.router.stateService.target("login"); 203 | } 204 | 205 | if (requireAdmin && !Session.getUser().admin) { 206 | return transition.router.stateService.target("app.dashboard"); 207 | } 208 | 209 | if (requireVerified && !Session.getUser().verified) { 210 | return transition.router.stateService.target("app.dashboard"); 211 | } 212 | 213 | if (requireAdmitted && !Session.getUser().status.admitted) { 214 | return transition.router.stateService.target("app.dashboard"); 215 | } 216 | }); 217 | 218 | $transitions.onSuccess({}, transition => { 219 | document.body.scrollTop = document.documentElement.scrollTop = 0; 220 | }); 221 | }); 222 | -------------------------------------------------------------------------------- /app/client/src/services/AuthService.js: -------------------------------------------------------------------------------- 1 | var angular = require('angular'); 2 | 3 | angular.module('reg') 4 | .factory('AuthService', [ 5 | '$http', 6 | '$rootScope', 7 | '$state', 8 | '$window', 9 | 'Session', 10 | function($http, $rootScope, $state, $window, Session) { 11 | var authService = {}; 12 | 13 | function loginSuccess(data, cb){ 14 | // Winner winner you get a token 15 | Session.create(data.token, data.user); 16 | 17 | if (cb){ 18 | cb(data.user); 19 | } 20 | } 21 | 22 | function loginFailure(data, cb){ 23 | $state.go('login'); 24 | if (cb) { 25 | cb(data); 26 | } 27 | } 28 | 29 | authService.loginWithPassword = function(email, password, onSuccess, onFailure) { 30 | return $http 31 | .post('/auth/login', { 32 | email: email, 33 | password: password 34 | }) 35 | .then(response => { 36 | loginSuccess(response.data, onSuccess); 37 | }, response => { 38 | loginFailure(response.data, onFailure); 39 | }); 40 | }; 41 | 42 | authService.loginWithToken = function(token, onSuccess, onFailure){ 43 | return $http 44 | .post('/auth/login', { 45 | token: token 46 | }) 47 | .then(response => { 48 | loginSuccess(response.data, onSuccess); 49 | }, response => { 50 | if (response.status === 400) { 51 | Session.destroy(loginFailure); 52 | } 53 | }); 54 | }; 55 | 56 | authService.logout = function(callback) { 57 | // Clear the session 58 | Session.destroy(callback); 59 | $state.go('login'); 60 | }; 61 | 62 | authService.register = function(email, password, onSuccess, onFailure) { 63 | return $http 64 | .post('/auth/register', { 65 | email: email, 66 | password: password 67 | }) 68 | .then(response => { 69 | loginSuccess(response.data, onSuccess); 70 | }, response => { 71 | loginFailure(response.data, onFailure); 72 | }); 73 | }; 74 | 75 | authService.verify = function(token, onSuccess, onFailure) { 76 | return $http 77 | .get('/auth/verify/' + token) 78 | .then(response => { 79 | Session.setUser(response.data); 80 | if (onSuccess) { 81 | onSuccess(response.data); 82 | } 83 | }, response => { 84 | if (onFailure) { 85 | onFailure(response.data); 86 | } 87 | }); 88 | }; 89 | 90 | authService.resendVerificationEmail = function(onSuccess, onFailure){ 91 | return $http 92 | .post('/auth/verify/resend', { 93 | id: Session.getUserId() 94 | }); 95 | }; 96 | 97 | authService.sendResetEmail = function(email){ 98 | return $http 99 | .post('/auth/reset', { 100 | email: email 101 | }); 102 | }; 103 | 104 | authService.resetPassword = function(token, pass, onSuccess, onFailure){ 105 | return $http 106 | .post('/auth/reset/password', { 107 | token: token, 108 | password: pass 109 | }) 110 | .then(onSuccess, onFailure); 111 | }; 112 | 113 | return authService; 114 | } 115 | ]); 116 | -------------------------------------------------------------------------------- /app/client/src/services/SettingsService.js: -------------------------------------------------------------------------------- 1 | angular.module('reg') 2 | .factory('SettingsService', [ 3 | '$http', 4 | function($http){ 5 | 6 | var base = '/api/settings/'; 7 | 8 | return { 9 | getPublicSettings: function(){ 10 | return $http.get(base); 11 | }, 12 | updateRegistrationTimes: function(open, close){ 13 | return $http.put(base + 'times', { 14 | timeOpen: open, 15 | timeClose: close, 16 | }); 17 | }, 18 | updateConfirmationTime: function(time){ 19 | return $http.put(base + 'confirm-by', { 20 | time: time 21 | }); 22 | }, 23 | getWhitelistedEmails: function(){ 24 | return $http.get(base + 'whitelist'); 25 | }, 26 | updateWhitelistedEmails: function(emails){ 27 | return $http.put(base + 'whitelist', { 28 | emails: emails 29 | }); 30 | }, 31 | updateWaitlistText: function(text){ 32 | return $http.put(base + 'waitlist', { 33 | text: text 34 | }); 35 | }, 36 | updateAcceptanceText: function(text){ 37 | return $http.put(base + 'acceptance', { 38 | text: text 39 | }); 40 | }, 41 | updateConfirmationText: function(text){ 42 | return $http.put(base + 'confirmation', { 43 | text: text 44 | }); 45 | }, 46 | updateAllowMinors: function(allowMinors){ 47 | return $http.put(base + 'minors', { 48 | allowMinors: allowMinors 49 | }); 50 | }, 51 | }; 52 | 53 | } 54 | ]); 55 | -------------------------------------------------------------------------------- /app/client/src/services/UserService.js: -------------------------------------------------------------------------------- 1 | angular.module('reg') 2 | .factory('UserService', [ 3 | '$http', 4 | 'Session', 5 | function($http, Session){ 6 | 7 | var users = '/api/users'; 8 | var base = users + '/'; 9 | 10 | return { 11 | 12 | // ---------------------- 13 | // Basic Actions 14 | // ---------------------- 15 | getCurrentUser: function(){ 16 | return $http.get(base + Session.getUserId()); 17 | }, 18 | 19 | get: function(id){ 20 | return $http.get(base + id); 21 | }, 22 | 23 | getAll: function(){ 24 | return $http.get(base); 25 | }, 26 | 27 | getPage: function(page, size, text){ 28 | return $http.get(users + '?' + $.param( 29 | { 30 | text: text, 31 | page: page ? page : 0, 32 | size: size ? size : 50 33 | }) 34 | ); 35 | }, 36 | 37 | updateProfile: function(id, profile){ 38 | return $http.put(base + id + '/profile', { 39 | profile: profile 40 | }); 41 | }, 42 | 43 | updateConfirmation: function(id, confirmation){ 44 | return $http.put(base + id + '/confirm', { 45 | confirmation: confirmation 46 | }); 47 | }, 48 | 49 | declineAdmission: function(id){ 50 | return $http.post(base + id + '/decline'); 51 | }, 52 | 53 | // ------------------------ 54 | // Team 55 | // ------------------------ 56 | joinOrCreateTeam: function(code){ 57 | return $http.put(base + Session.getUserId() + '/team', { 58 | code: code 59 | }); 60 | }, 61 | 62 | leaveTeam: function(){ 63 | return $http.delete(base + Session.getUserId() + '/team'); 64 | }, 65 | 66 | getMyTeammates: function(){ 67 | return $http.get(base + Session.getUserId() + '/team'); 68 | }, 69 | 70 | // ------------------------- 71 | // Admin Only 72 | // ------------------------- 73 | 74 | getCSV: function(){ 75 | $http.get(base + 'exportcsv').then(function (data, status, headers) { 76 | var linkElement = document.createElement('a'); 77 | try { 78 | linkElement.setAttribute('href', data.data.path); 79 | linkElement.setAttribute("download", data.data.filename); 80 | var clickEvent = new MouseEvent("click", { 81 | "view": window, 82 | "bubbles": true, 83 | "cancelable": false 84 | }); 85 | linkElement.dispatchEvent(clickEvent); 86 | } catch (ex) { 87 | console.log(ex); 88 | } 89 | 90 | }, function (data) { 91 | console.log(data); 92 | }); 93 | }, 94 | 95 | getStats: function(){ 96 | return $http.get(base + 'stats'); 97 | }, 98 | 99 | admitUser: function(id){ 100 | return $http.post(base + id + '/admit'); 101 | }, 102 | 103 | checkIn: function(id){ 104 | return $http.post(base + id + '/checkin'); 105 | }, 106 | 107 | checkOut: function(id){ 108 | return $http.post(base + id + '/checkout'); 109 | }, 110 | 111 | makeAdmin: function(id){ 112 | return $http.post(base + id + '/makeadmin'); 113 | }, 114 | 115 | removeAdmin: function(id){ 116 | return $http.post(base + id + '/removeadmin'); 117 | }, 118 | }; 119 | } 120 | ]); 121 | -------------------------------------------------------------------------------- /app/client/stylesheets/_admin.scss: -------------------------------------------------------------------------------- 1 | #admin { 2 | .label { 3 | margin-bottom: 6px; 4 | } 5 | 6 | .page.button { 7 | margin-bottom: 6px; 8 | } 9 | 10 | #table-container { 11 | overflow-x: auto; 12 | } 13 | 14 | table.users { 15 | 16 | tr { 17 | cursor: pointer; 18 | 19 | &.admin { 20 | background: lighten($brand-secondary, 50%) !important; 21 | &:hover { 22 | background: lighten($brand-secondary, 45%) !important; 23 | } 24 | } 25 | } 26 | 27 | } 28 | } -------------------------------------------------------------------------------- /app/client/stylesheets/_animation.scss: -------------------------------------------------------------------------------- 1 | #root-view { 2 | } 3 | 4 | #sidebar, #login .segment{ 5 | 6 | } 7 | 8 | #root-view.ng-enter { 9 | } 10 | 11 | #root-view.ng-enter-active { 12 | } 13 | 14 | #root-view.ng-leave { 15 | } 16 | 17 | #root-view.ng-leave-active { 18 | } -------------------------------------------------------------------------------- /app/client/stylesheets/_application.scss: -------------------------------------------------------------------------------- 1 | #application { 2 | #school .title { 3 | text-align: left; 4 | text-transform: none; 5 | letter-spacing: 0; 6 | } 7 | } -------------------------------------------------------------------------------- /app/client/stylesheets/_base.scss: -------------------------------------------------------------------------------- 1 | #base { 2 | background: $sidebar-color; 3 | width: 100%; 4 | height: 100%; 5 | 6 | > .content { 7 | width: 100%; 8 | min-height: 100%; 9 | padding-left: $sidebar-width; 10 | background: white; 11 | box-shadow: 0 0 8px rgba(0,0,0,.4); 12 | 13 | transition: padding-left $sidebar-transition-time; 14 | 15 | > .container { 16 | padding: 36px; 17 | } 18 | 19 | } 20 | } 21 | 22 | @media only screen and (max-width: 768px) { 23 | #base > .content { 24 | padding-left: 0; 25 | > .container { 26 | padding: 0; 27 | } 28 | } 29 | } 30 | 31 | .danger-button { 32 | background-color: #dd6b55; 33 | } 34 | 35 | .danger-button:hover { 36 | background-color: #d9593f !important; 37 | } 38 | -------------------------------------------------------------------------------- /app/client/stylesheets/_branding.scss: -------------------------------------------------------------------------------- 1 | /** BRANDING COLORS 2 | * primary (default pink): sidebar color, admitted dashboard status 3 | * secondary (default purple): header text, submitted/confirmed/checked-in dashboard status 4 | * tertiary (default blue): waitlist/declined dashboard status 5 | */ 6 | 7 | $primary: #e76482; 8 | $secondary: #473899; 9 | $tertiary: #4c82cc; 10 | 11 | /** ASSET URLS 12 | * login-bg: image for login splash page 13 | */ 14 | 15 | $login-bg: url('https://s3.amazonaws.com/hackmit-assets/tintedphoto_1920_1080_low.jpg'); 16 | -------------------------------------------------------------------------------- /app/client/stylesheets/_custom.scss: -------------------------------------------------------------------------------- 1 | // Branding overrides 2 | // 3 | // Copy variables from `_branding.scss` to this file to override default values. 4 | -------------------------------------------------------------------------------- /app/client/stylesheets/_dashboard.scss: -------------------------------------------------------------------------------- 1 | #dashboard { 2 | .status { 3 | text-align: center; 4 | p { 5 | text-align: left; 6 | } 7 | .button { 8 | margin-top: 12px; 9 | } 10 | .header { 11 | font-size: 1.2em; 12 | } 13 | .state { 14 | text-align: center; 15 | padding: 12px 24px; 16 | margin-bottom: 1em; 17 | color: white; 18 | font-size: 1.3em; 19 | text-transform: uppercase; 20 | 21 | &.unverified { 22 | background: $status-unverified-color; 23 | } 24 | 25 | &.incomplete { 26 | background: $status-incomplete-color; 27 | color: black; 28 | } 29 | 30 | &.submitted { 31 | background: $status-submitted-color; 32 | } 33 | 34 | &.waitlist { 35 | background: $status-waitlist-color; 36 | } 37 | 38 | &.admitted { 39 | background: $status-admitted-color; 40 | } 41 | 42 | &.confirmed { 43 | background: $status-confirmed-color; 44 | } 45 | 46 | &.checked.in { 47 | background: $status-checkedin-color; 48 | } 49 | 50 | &.declined { 51 | background: $status-declined-color; 52 | } 53 | } 54 | } 55 | 56 | .segment { 57 | padding: 36px 36px 12px 36px; 58 | } 59 | 60 | .description { 61 | margin-bottom: 24px; 62 | .markdown { 63 | text-align: left; 64 | margin-bottom: 24px; 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /app/client/stylesheets/_login.scss: -------------------------------------------------------------------------------- 1 | #login { 2 | height: 100%; 3 | width: 100%; 4 | background-color: $login-bg-color; 5 | background: $login-bg; 6 | background-size: cover; 7 | 8 | > .container { 9 | position: relative; 10 | top: 50%; 11 | 12 | -webkit-transform: translateY(-50%); 13 | -ms-transform: translateY(-50%); 14 | transform: translateY(-50%); 15 | 16 | > .content { 17 | max-width: 320px; 18 | margin: 0 auto; 19 | 20 | .logo { 21 | max-width: 100px; 22 | margin: .5rem auto 1rem auto; 23 | img { 24 | width: 100px; 25 | height: 100px; 26 | } 27 | } 28 | 29 | .button { 30 | color: white; 31 | &.login { 32 | background: $login-color; 33 | } 34 | &.register { 35 | background: $register-color 36 | } 37 | } 38 | 39 | .forgot { 40 | text-align: center; 41 | } 42 | 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/client/stylesheets/_page.scss: -------------------------------------------------------------------------------- 1 | .page { 2 | padding-top: 2em; 3 | padding-bottom: 4em; 4 | .title { 5 | font-family: $font-header; 6 | text-transform: uppercase; 7 | font-size: 2.5em; 8 | text-align: center; 9 | color: $page-header-color; 10 | letter-spacing: 8px; 11 | font-weight: 700; 12 | &.divided:after { 13 | content: " "; 14 | width: 200px; 15 | display: block; 16 | margin: 1em auto 2em; 17 | border-bottom: 4px solid $page-header-color; 18 | } 19 | 20 | &.small { 21 | font-size: 1em; 22 | letter-spacing: 4px; 23 | &:after { 24 | border-width: 2px; 25 | } 26 | } 27 | } 28 | 29 | .form { 30 | .title { 31 | font-size: 1em; 32 | letter-spacing: 4px; 33 | &:after { 34 | border-width: 2px; 35 | } 36 | } 37 | } 38 | 39 | .subheader { 40 | font-size: 1.2em; 41 | text-align: center; 42 | } 43 | 44 | } 45 | 46 | @media only screen and (max-width: 768px) { 47 | .page { 48 | .title { 49 | font-size: 1.25em; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/client/stylesheets/_sidebar.scss: -------------------------------------------------------------------------------- 1 | #sidebar { 2 | height: 100%; 3 | position: fixed; 4 | background: $sidebar-color; 5 | color: white; 6 | width: $sidebar-width; 7 | z-index: 999; 8 | overflow: auto; 9 | 10 | @include transition-duration($sidebar-transition-time); 11 | 12 | .item { 13 | width: 100%; 14 | padding: 16px; 15 | transition-duration: .1s; 16 | transition: border-left, .5s, linear; 17 | border-bottom: 1px solid lighten($sidebar-color, 3%); 18 | opacity: .7; 19 | border-left: 0px solid transparent; 20 | 21 | &.active { 22 | background: lighten($sidebar-color, 4%); 23 | opacity: 1; 24 | border-left: 8px solid lighten($sidebar-color, 10%); 25 | } 26 | &:hover { 27 | cursor: pointer; 28 | background: darken($sidebar-color, 2%); 29 | opacity: 1; 30 | } 31 | &:active { 32 | background: darken($sidebar-color, 5%); 33 | opacity: 1; 34 | } 35 | 36 | &.logo { 37 | opacity: 1; 38 | padding: 48px; 39 | } 40 | 41 | } 42 | 43 | .note { 44 | font-size: .8em; 45 | text-align: center; 46 | padding: 16px; 47 | left: 0; 48 | right: 0; 49 | bottom: 16px; 50 | } 51 | 52 | // Tab is the sibling of the sidebar 53 | +.tab { 54 | position: fixed; 55 | width: $sidebar-tab-size; 56 | height: $sidebar-tab-size; 57 | background: $sidebar-color; 58 | top: 12px; 59 | 60 | z-index: 9999; 61 | 62 | // Is not visible unless mobile. 63 | visibility: 'hidden'; 64 | opacity: 0; 65 | 66 | @include transform(translate3d($sidebar-width, 0,0)); 67 | @include transition-duration($sidebar-transition-time); 68 | 69 | cursor: pointer; 70 | 71 | .close { 72 | color: white; 73 | font-size: 2em; 74 | } 75 | } 76 | 77 | } 78 | 79 | @media only screen and (max-width: 768px) { 80 | #sidebar { 81 | @include transform(translate3d(-$sidebar-width, 0, 0)); 82 | 83 | // When the sidebar is open 84 | &.open { 85 | // Transform the sidebar to original position 86 | @include transform(translate3d(0,0,0)); 87 | +.tab { 88 | // Transform the tab to the side of the sidebar 89 | @include transform(translate3d($sidebar-width, 0, 0)); 90 | 91 | // Hack to get the X lined up 92 | line-height: $sidebar-tab-size; 93 | text-align: center; 94 | padding: 0; 95 | } 96 | } 97 | // When the tab is closed. 98 | +.tab { 99 | visibility: 'visible'; 100 | opacity: 1; 101 | @include transform(translate3d(0,0,0)); 102 | padding: 12px; 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /app/client/stylesheets/_team.scss: -------------------------------------------------------------------------------- 1 | #team { 2 | text-align: center; 3 | .title { 4 | padding-top: 24px; 5 | } 6 | p { 7 | text-align: center; 8 | margin: 12px; 9 | } 10 | .subheader { 11 | word-wrap: break-word; 12 | } 13 | } -------------------------------------------------------------------------------- /app/client/stylesheets/_verify.scss: -------------------------------------------------------------------------------- 1 | .verify.page { 2 | padding-top: 4em; 3 | .segment { 4 | padding: 36px; 5 | padding-top: 70px; 6 | max-width: 300px; 7 | margin: 0 auto; 8 | text-align: center; 9 | .icon { 10 | font-size: 5em; 11 | margin-bottom: 36px; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /app/client/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.1 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox. 29 | * Correct `block` display not defined for `main` in IE 11. 30 | */ 31 | 32 | article, 33 | aside, 34 | details, 35 | figcaption, 36 | figure, 37 | footer, 38 | header, 39 | hgroup, 40 | main, 41 | nav, 42 | section, 43 | summary { 44 | display: block; 45 | } 46 | 47 | /** 48 | * 1. Correct `inline-block` display not defined in IE 8/9. 49 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 50 | */ 51 | 52 | audio, 53 | canvas, 54 | progress, 55 | video { 56 | display: inline-block; /* 1 */ 57 | vertical-align: baseline; /* 2 */ 58 | } 59 | 60 | /** 61 | * Prevent modern browsers from displaying `audio` without controls. 62 | * Remove excess height in iOS 5 devices. 63 | */ 64 | 65 | audio:not([controls]) { 66 | display: none; 67 | height: 0; 68 | } 69 | 70 | /** 71 | * Address `[hidden]` styling not present in IE 8/9/10. 72 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 73 | */ 74 | 75 | [hidden], 76 | template { 77 | display: none; 78 | } 79 | 80 | /* Links 81 | ========================================================================== */ 82 | 83 | /** 84 | * Remove the gray background color from active links in IE 10. 85 | */ 86 | 87 | a { 88 | background: transparent; 89 | } 90 | 91 | /** 92 | * Improve readability when focused and also mouse hovered in all browsers. 93 | */ 94 | 95 | a:active, 96 | a:hover { 97 | outline: 0; 98 | } 99 | 100 | /* Text-level semantics 101 | ========================================================================== */ 102 | 103 | /** 104 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 105 | */ 106 | 107 | abbr[title] { 108 | border-bottom: 1px dotted; 109 | } 110 | 111 | /** 112 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 113 | */ 114 | 115 | b, 116 | strong { 117 | font-weight: bold; 118 | } 119 | 120 | /** 121 | * Address styling not present in Safari and Chrome. 122 | */ 123 | 124 | dfn { 125 | font-style: italic; 126 | } 127 | 128 | /** 129 | * Address variable `h1` font-size and margin within `section` and `article` 130 | * contexts in Firefox 4+, Safari, and Chrome. 131 | */ 132 | 133 | h1 { 134 | font-size: 2em; 135 | margin: 0.67em 0; 136 | } 137 | 138 | /** 139 | * Address styling not present in IE 8/9. 140 | */ 141 | 142 | mark { 143 | background: #ff0; 144 | color: #000; 145 | } 146 | 147 | /** 148 | * Address inconsistent and variable font size in all browsers. 149 | */ 150 | 151 | small { 152 | font-size: 80%; 153 | } 154 | 155 | /** 156 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 157 | */ 158 | 159 | sub, 160 | sup { 161 | font-size: 75%; 162 | line-height: 0; 163 | position: relative; 164 | vertical-align: baseline; 165 | } 166 | 167 | sup { 168 | top: -0.5em; 169 | } 170 | 171 | sub { 172 | bottom: -0.25em; 173 | } 174 | 175 | /* Embedded content 176 | ========================================================================== */ 177 | 178 | /** 179 | * Remove border when inside `a` element in IE 8/9/10. 180 | */ 181 | 182 | img { 183 | border: 0; 184 | } 185 | 186 | /** 187 | * Correct overflow not hidden in IE 9/10/11. 188 | */ 189 | 190 | svg:not(:root) { 191 | overflow: hidden; 192 | } 193 | 194 | /* Grouping content 195 | ========================================================================== */ 196 | 197 | /** 198 | * Address margin not present in IE 8/9 and Safari. 199 | */ 200 | 201 | figure { 202 | margin: 1em 40px; 203 | } 204 | 205 | /** 206 | * Address differences between Firefox and other browsers. 207 | */ 208 | 209 | hr { 210 | -moz-box-sizing: content-box; 211 | box-sizing: content-box; 212 | height: 0; 213 | } 214 | 215 | /** 216 | * Contain overflow in all browsers. 217 | */ 218 | 219 | pre { 220 | overflow: auto; 221 | } 222 | 223 | /** 224 | * Address odd `em`-unit font size rendering in all browsers. 225 | */ 226 | 227 | code, 228 | kbd, 229 | pre, 230 | samp { 231 | font-family: monospace, monospace; 232 | font-size: 1em; 233 | } 234 | 235 | /* Forms 236 | ========================================================================== */ 237 | 238 | /** 239 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 240 | * styling of `select`, unless a `border` property is set. 241 | */ 242 | 243 | /** 244 | * 1. Correct color not being inherited. 245 | * Known issue: affects color of disabled elements. 246 | * 2. Correct font properties not being inherited. 247 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 248 | */ 249 | 250 | button, 251 | input, 252 | optgroup, 253 | select, 254 | textarea { 255 | color: inherit; /* 1 */ 256 | font: inherit; /* 2 */ 257 | margin: 0; /* 3 */ 258 | } 259 | 260 | /** 261 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 262 | */ 263 | 264 | button { 265 | overflow: visible; 266 | } 267 | 268 | /** 269 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 270 | * All other form control elements do not inherit `text-transform` values. 271 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 272 | * Correct `select` style inheritance in Firefox. 273 | */ 274 | 275 | button, 276 | select { 277 | text-transform: none; 278 | } 279 | 280 | /** 281 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 282 | * and `video` controls. 283 | * 2. Correct inability to style clickable `input` types in iOS. 284 | * 3. Improve usability and consistency of cursor style between image-type 285 | * `input` and others. 286 | */ 287 | 288 | button, 289 | html input[type="button"], /* 1 */ 290 | input[type="reset"], 291 | input[type="submit"] { 292 | -webkit-appearance: button; /* 2 */ 293 | cursor: pointer; /* 3 */ 294 | } 295 | 296 | /** 297 | * Re-set default cursor for disabled elements. 298 | */ 299 | 300 | button[disabled], 301 | html input[disabled] { 302 | cursor: default; 303 | } 304 | 305 | /** 306 | * Remove inner padding and border in Firefox 4+. 307 | */ 308 | 309 | button::-moz-focus-inner, 310 | input::-moz-focus-inner { 311 | border: 0; 312 | padding: 0; 313 | } 314 | 315 | /** 316 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 317 | * the UA stylesheet. 318 | */ 319 | 320 | input { 321 | line-height: normal; 322 | } 323 | 324 | /** 325 | * It's recommended that you don't attempt to style these elements. 326 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 327 | * 328 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 329 | * 2. Remove excess padding in IE 8/9/10. 330 | */ 331 | 332 | input[type="checkbox"], 333 | input[type="radio"] { 334 | box-sizing: border-box; /* 1 */ 335 | padding: 0; /* 2 */ 336 | } 337 | 338 | /** 339 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 340 | * `font-size` values of the `input`, it causes the cursor style of the 341 | * decrement button to change from `default` to `text`. 342 | */ 343 | 344 | input[type="number"]::-webkit-inner-spin-button, 345 | input[type="number"]::-webkit-outer-spin-button { 346 | height: auto; 347 | } 348 | 349 | /** 350 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 351 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 352 | * (include `-moz` to future-proof). 353 | */ 354 | 355 | input[type="search"] { 356 | -webkit-appearance: textfield; /* 1 */ 357 | -moz-box-sizing: content-box; 358 | -webkit-box-sizing: content-box; /* 2 */ 359 | box-sizing: content-box; 360 | } 361 | 362 | /** 363 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 364 | * Safari (but not Chrome) clips the cancel button when the search input has 365 | * padding (and `textfield` appearance). 366 | */ 367 | 368 | input[type="search"]::-webkit-search-cancel-button, 369 | input[type="search"]::-webkit-search-decoration { 370 | -webkit-appearance: none; 371 | } 372 | 373 | /** 374 | * Define consistent border, margin, and padding. 375 | */ 376 | 377 | fieldset { 378 | border: 1px solid #c0c0c0; 379 | margin: 0 2px; 380 | padding: 0.35em 0.625em 0.75em; 381 | } 382 | 383 | /** 384 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 385 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 386 | */ 387 | 388 | legend { 389 | border: 0; /* 1 */ 390 | padding: 0; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove default vertical scrollbar in IE 8/9/10/11. 395 | */ 396 | 397 | textarea { 398 | overflow: auto; 399 | } 400 | 401 | /** 402 | * Don't inherit the `font-weight` (applied by a rule above). 403 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 404 | */ 405 | 406 | optgroup { 407 | font-weight: bold; 408 | } 409 | 410 | /* Tables 411 | ========================================================================== */ 412 | 413 | /** 414 | * Remove most spacing between table cells. 415 | */ 416 | 417 | table { 418 | border-collapse: collapse; 419 | border-spacing: 0; 420 | } 421 | 422 | td, 423 | th { 424 | padding: 0; 425 | } 426 | -------------------------------------------------------------------------------- /app/client/stylesheets/site.scss: -------------------------------------------------------------------------------- 1 | @import 'branding'; 2 | @import 'custom'; 3 | $font-body: 'Lato', 'Helvetica Neue', 'Helvetica', sans-serif; 4 | $font-header: 'Montserrat', 'Helvetica Neue', 'Helvetica', sans-serif; 5 | 6 | // Login screen variables 7 | $login-bg-color: $secondary; 8 | $login-color: $primary; 9 | $register-color: $tertiary; 10 | 11 | $status-unverified-color: rgb(82, 82, 82); 12 | $status-incomplete-color: #F5F5F5; 13 | $status-submitted-color: lighten($secondary, 20%); 14 | $status-admitted-color: $primary; 15 | $status-confirmed-color: $secondary; 16 | $status-checkedin-color: $secondary; 17 | $status-waitlist-color: $tertiary; 18 | $status-declined-color: $tertiary; 19 | $status-rejected-color: red; // Not used 20 | 21 | // Sidebar variables 22 | $sidebar-color: $primary; 23 | $sidebar-width: 265px; 24 | $sidebar-transition-time: .5s; 25 | $sidebar-tab-size: 55px; 26 | 27 | // Page variables 28 | $page-header-color: $secondary; 29 | 30 | $brand-primary: $primary; 31 | $brand-secondary: $secondary; 32 | $brand-tertiary: $tertiary; 33 | 34 | @mixin transform($transforms) { 35 | -webkit-transform: $transforms; 36 | transform: $transforms; 37 | } 38 | 39 | @mixin transition-duration($duration) { 40 | -webkit-transition-duration: $duration; 41 | transition-duration: $duration; 42 | } 43 | 44 | @import 'login'; 45 | @import 'base'; 46 | @import 'sidebar'; 47 | @import 'page'; 48 | @import 'dashboard'; 49 | @import 'application'; 50 | @import 'verify'; 51 | @import 'team'; 52 | @import 'admin'; 53 | 54 | html, 55 | body { 56 | height: 100%; 57 | width: 100%; 58 | font-size: 1.02em; 59 | } 60 | 61 | body { 62 | margin: 0; 63 | font-family: 'Lato', 'Helvetica Neue', 'Helvetica', sans-serif; 64 | font-weight: 300; 65 | 66 | > .container { 67 | width: 100%; 68 | height: 100%; 69 | } 70 | } 71 | 72 | /** 73 | * SweetAlert Overrides 74 | */ 75 | .sweet-alert { 76 | font-family: $font-body; 77 | } 78 | 79 | /** 80 | * Semantic-UI Overrides for squareishness 81 | */ 82 | .ui.segment { 83 | border-radius: 0; 84 | box-shadow: none; 85 | } 86 | input { 87 | border-radius: 0 !important; 88 | } 89 | .ui.button { 90 | border-radius: 0; 91 | &.primary { 92 | background: $primary; 93 | &:hover { 94 | background: lighten($primary, 3%); 95 | } 96 | &:active { 97 | background: darken($primary, 3%); 98 | } 99 | } 100 | &.secondary { 101 | background: $secondary; 102 | &:hover { 103 | background: lighten($secondary, 3%); 104 | } 105 | &:active { 106 | background: darken($secondary, 3%); 107 | } 108 | &:focus { 109 | background: $secondary; 110 | } 111 | } 112 | } 113 | 114 | .ui.form select { 115 | height: 2.5em; 116 | } 117 | 118 | fieldset { 119 | padding: 0; 120 | border: none; 121 | } 122 | 123 | .ui.search >.results { 124 | width: 100%; 125 | } 126 | // @import 'animation'; 127 | -------------------------------------------------------------------------------- /app/client/views/404.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

Oops. Looks like this doesn't exist.

7 |
8 | -------------------------------------------------------------------------------- /app/client/views/admin/admin.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | GOD MODE 4 |
5 | 6 | 28 | 29 | 30 |
31 | -------------------------------------------------------------------------------- /app/client/views/admin/adminCtrl.js: -------------------------------------------------------------------------------- 1 | angular.module('reg') 2 | .controller('AdminCtrl', [ 3 | '$scope', 4 | 'UserService', 5 | function($scope, UserService){ 6 | $scope.loading = true; 7 | }]); -------------------------------------------------------------------------------- /app/client/views/admin/settings/adminSettingsCtrl.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const showdown = require('showdown'); 3 | const swal = require('sweetalert'); 4 | 5 | angular.module('reg') 6 | .controller('AdminSettingsCtrl', [ 7 | '$scope', 8 | '$sce', 9 | 'SettingsService', 10 | function($scope, $sce, SettingsService){ 11 | 12 | $scope.settings = {}; 13 | SettingsService 14 | .getPublicSettings() 15 | .then(response => { 16 | updateSettings(response.data); 17 | }); 18 | 19 | function updateSettings(settings){ 20 | $scope.loading = false; 21 | // Format the dates in settings. 22 | settings.timeOpen = new Date(settings.timeOpen); 23 | settings.timeClose = new Date(settings.timeClose); 24 | settings.timeConfirm = new Date(settings.timeConfirm); 25 | 26 | $scope.settings = settings; 27 | } 28 | 29 | // Additional Options -------------------------------------- 30 | 31 | $scope.updateAllowMinors = function () { 32 | SettingsService 33 | .updateAllowMinors($scope.settings.allowMinors) 34 | .then(response => { 35 | $scope.settings.allowMinors = response.data.allowMinors; 36 | const successText = $scope.settings.allowMinors ? 37 | "Minors are now allowed to register." : 38 | "Minors are no longer allowed to register."; 39 | swal("Looks good!", successText, "success"); 40 | }); 41 | }; 42 | 43 | // Whitelist -------------------------------------- 44 | 45 | SettingsService 46 | .getWhitelistedEmails() 47 | .then(response => { 48 | $scope.whitelist = response.data.join(", "); 49 | }); 50 | 51 | $scope.updateWhitelist = function(){ 52 | SettingsService 53 | .updateWhitelistedEmails($scope.whitelist.replace(/ /g, '').split(',')) 54 | .then(response => { 55 | swal('Whitelist updated.'); 56 | $scope.whitelist = response.data.whitelistedEmails.join(", "); 57 | }); 58 | }; 59 | 60 | // Registration Times ----------------------------- 61 | 62 | $scope.formatDate = function(date){ 63 | if (!date){ 64 | return "Invalid Date"; 65 | } 66 | 67 | // Hack for timezone 68 | return moment(date).format('dddd, MMMM Do YYYY, h:mm a') + 69 | " " + date.toTimeString().split(' ')[2]; 70 | }; 71 | 72 | // Take a date and remove the seconds. 73 | function cleanDate(date){ 74 | return new Date( 75 | date.getFullYear(), 76 | date.getMonth(), 77 | date.getDate(), 78 | date.getHours(), 79 | date.getMinutes() 80 | ); 81 | } 82 | 83 | $scope.updateRegistrationTimes = function(){ 84 | // Clean the dates and turn them to ms. 85 | var open = cleanDate($scope.settings.timeOpen).getTime(); 86 | var close = cleanDate($scope.settings.timeClose).getTime(); 87 | 88 | if (open < 0 || close < 0 || open === undefined || close === undefined){ 89 | return swal('Oops...', 'You need to enter valid times.', 'error'); 90 | } 91 | if (open >= close){ 92 | swal('Oops...', 'Registration cannot open after it closes.', 'error'); 93 | return; 94 | } 95 | 96 | SettingsService 97 | .updateRegistrationTimes(open, close) 98 | .then(response => { 99 | updateSettings(response.data); 100 | swal("Looks good!", "Registration Times Updated", "success"); 101 | }); 102 | }; 103 | 104 | // Confirmation Time ----------------------------- 105 | 106 | $scope.updateConfirmationTime = function(){ 107 | var confirmBy = cleanDate($scope.settings.timeConfirm).getTime(); 108 | 109 | SettingsService 110 | .updateConfirmationTime(confirmBy) 111 | .then(response => { 112 | updateSettings(response.data); 113 | swal("Sounds good!", "Confirmation Date Updated", "success"); 114 | }); 115 | }; 116 | 117 | // Acceptance / Confirmation Text ---------------- 118 | 119 | var converter = new showdown.Converter(); 120 | 121 | $scope.markdownPreview = function(text){ 122 | return $sce.trustAsHtml(converter.makeHtml(text)); 123 | }; 124 | 125 | $scope.updateWaitlistText = function(){ 126 | var text = $scope.settings.waitlistText; 127 | SettingsService 128 | .updateWaitlistText(text) 129 | .then(response => { 130 | swal("Looks good!", "Waitlist Text Updated", "success"); 131 | updateSettings(response.data); 132 | }); 133 | }; 134 | 135 | $scope.updateAcceptanceText = function(){ 136 | var text = $scope.settings.acceptanceText; 137 | SettingsService 138 | .updateAcceptanceText(text) 139 | .then(response => { 140 | swal("Looks good!", "Acceptance Text Updated", "success"); 141 | updateSettings(response.data); 142 | }); 143 | }; 144 | 145 | $scope.updateConfirmationText = function(){ 146 | var text = $scope.settings.confirmationText; 147 | SettingsService 148 | .updateConfirmationText(text) 149 | .then(response => { 150 | swal("Looks good!", "Confirmation Text Updated", "success"); 151 | updateSettings(response.data); 152 | }); 153 | }; 154 | 155 | }]); 156 | -------------------------------------------------------------------------------- /app/client/views/admin/settings/settings.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
8 |
9 | 10 |
11 | Open/Close Registration 12 |
13 | 14 |

15 | Users will be able to register new accounts within the time period specified. 16 |

17 | 18 |
19 | 22 | 23 |
24 | 25 |
26 | 29 | 30 |
31 | 32 |
33 |
36 | Update 37 |
38 |
39 |
40 |
41 | 42 |
43 |
44 | 45 | 46 |
49 |
50 | 51 |
52 | Confirmation Date 53 |
54 | 55 |

56 | Any users that are accepted will have to confirm by the date selected. 57 |

58 | 59 |
60 | 63 | 64 |
65 | 66 |
67 |
70 | Update 71 |
72 |
73 |
74 |
75 | 76 |
77 |
78 | 79 | 80 |
83 | 84 |
85 | Additional Options 86 |
87 | 88 |
89 |
90 |
91 |
92 |
93 | 99 | 100 |
101 |
102 |
103 |
104 |
105 |
106 | 107 | 108 |
111 | 112 |
113 | Whitelisted Emails 114 |
115 | 116 |
117 |
120 | {{email}} 121 |
122 |
123 |
124 |
125 | 127 |
128 |
129 |
132 | Update 133 |
134 |
135 |
136 |
137 | 138 | 139 |
142 |
143 | Waitlist Text 144 |
145 |
146 |
147 |
148 |
149 | 154 |
155 |
156 |
159 | Update 160 |
161 |
162 |
163 |
164 |
165 |
168 |
169 |
170 |
171 | 172 |
173 | 174 | 175 |
178 |
179 | Acceptance Text 180 |
181 |
182 |
183 |
184 |
185 | 190 |
191 |
192 |
195 | Update 196 |
197 |
198 |
199 |
200 |
201 |
204 |
205 |
206 |
207 | 208 |
209 | 210 |
213 |
214 | Confirmation Text 215 |
216 |
217 |
218 |
219 |
220 | 225 |
226 |
227 |
230 | Update 231 |
232 |
233 |
234 |
235 |
236 |
239 |
240 |
241 |
242 |
243 | -------------------------------------------------------------------------------- /app/client/views/admin/stats/adminStatsCtrl.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | angular.module('reg') 4 | .controller('AdminStatsCtrl',[ 5 | '$scope', 6 | 'UserService', 7 | function($scope, UserService){ 8 | 9 | UserService 10 | .getStats() 11 | .then(stats => { 12 | $scope.stats = stats.data; 13 | $scope.loading = false; 14 | }); 15 | 16 | $scope.fromNow = function(date){ 17 | return moment(date).fromNow(); 18 | }; 19 | 20 | }]); 21 | -------------------------------------------------------------------------------- /app/client/views/admin/stats/stats.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
6 |
7 | Stats 8 |
9 |
10 | Last updated: {{fromNow(stats.lastUpdated)}} 11 |
12 |
14 | 15 |
16 | 17 | Total Users 18 | : 19 | {{stats.total}} 20 |
21 |
22 |
24 | 25 |
26 | 27 | Verified Users 28 | : 29 | {{stats.verified}} ({{stats.verified / stats.total * 100 | number: 1}}%) 30 |
31 |
32 |
34 | 35 |
36 | 37 | Submitted Users 38 | : 39 | {{stats.submitted}} ({{stats.submitted / stats.total * 100 | number: 1}}%) 40 |
41 |
42 |
43 |
45 | 46 |
47 | 48 | Admitted 49 | : 50 | {{stats.admitted}} 51 |
52 |
53 |
55 | 56 |
57 | 58 | Confirmed (Total) 59 | : 60 | {{stats.confirmed}} 61 |
62 |
63 |
65 | 66 |
67 | 68 | Confirmed (MIT) 69 | : 70 | {{stats.confirmedMit}} 71 |
72 |
73 |
74 | 75 |
76 | 77 | Declined 78 | : 79 | {{stats.declined}} 80 |
81 |
82 | 83 |
84 | 85 |
86 | 87 | Checked In 88 | : 89 | {{stats.checkedIn}} 90 |
91 |
92 | 93 |
94 | 95 |
96 | 97 |
98 | 99 | Unisex shirt sizes: 100 | : 101 | XS ({{stats.shirtSizes['XS']}}) 102 | S ({{stats.shirtSizes['S']}}) 103 | M ({{stats.shirtSizes['M']}}) 104 | L ({{stats.shirtSizes['L']}}) 105 | XL ({{stats.shirtSizes['XL']}}) 106 | XXL ({{stats.shirtSizes['XXL']}}) 107 |
108 |
109 | 110 |
111 | 112 |
113 | 114 | Women's shirt sizes: 115 | : 116 | XS ({{stats.shirtSizes['WXS']}}) 117 | S ({{stats.shirtSizes['WS']}}) 118 | M ({{stats.shirtSizes['WM']}}) 119 | L ({{stats.shirtSizes['WL']}}) 120 | XL ({{stats.shirtSizes['WXL']}}) 121 | XXL ({{stats.shirtSizes['WXXL']}}) 122 |
123 |
124 | 125 |
126 | 127 |
128 | 129 |
130 | 131 | Total Reimbursements: 132 | {{stats.reimbursementTotal}} 133 |
134 |
135 | 136 |
137 | 138 |
139 | 140 | Still Need Reimbursement: 141 | {{stats.reimbursementMissing}} 142 |
143 |
144 | 145 |
146 | 147 |
148 | 149 |
150 | 151 | Need/Want Hardware: 152 | {{stats.wantsHardware}} 153 |
154 |
155 | 156 |
157 | 158 |
159 | 160 |
161 | 162 | Dietary Restrictions: 163 | 164 | 166 | {{restriction.name}} ({{restriction.count}}) 167 | 168 |
169 |
170 | 171 |
172 |
173 |
174 | 175 |
178 |
179 | Demographics 180 |
181 |
182 |
183 | 184 |
185 | Total users: {{stats.total}} 186 |
187 |
188 |
189 | 190 |
191 | Female: {{stats.demo.gender['F']}} ({{stats.demo.gender['F'] / stats.total * 100 | number:1}}%) 192 |
193 |
194 |
195 | 196 |
197 | Male: {{stats.demo.gender['M']}} ({{stats.demo.gender['M'] / stats.total * 100 | number:1}}%) 198 |
199 |
200 |
201 | 202 |
203 | Other: {{stats.demo.gender['O']}} ({{stats.demo.gender['O'] / stats.total * 100 | number:1}}%) 204 |
205 |
206 |
207 | 208 |
209 | Did not respond: {{stats.demo.gender['N']}} ({{stats.demo.gender['N'] / stats.total * 100 | number:1 }}%) 210 |
211 |
212 |
213 |
214 |
215 |
216 | Fresh: {{stats.demo.year['2019']}} ({{stats.demo.year['2019'] / stats.total * 100 | number: 1}}%) 217 |
218 |
219 | Soph: {{stats.demo.year['2018']}} ({{stats.demo.year['2018'] / stats.total * 100 | number: 1}}%) 220 |
221 |
222 | Junior: {{stats.demo.year['2017']}} ({{stats.demo.year['2017'] / stats.total * 100 | number: 1}}%) 223 |
224 |
225 | Senior: {{stats.demo.year['2016']}} ({{stats.demo.year['2016'] / stats.total * 100 | number: 1}}%) 226 |
227 |
228 |
229 |
230 |
233 |
234 | {{school.email}} : {{school.count}} ({{school.count / stats.total * 100 | number: 1}}%) 235 |

236 |

237 | Submitted: {{school.stats.submitted}} 238 |
239 |
240 | Admitted: {{school.stats.admitted}} 241 |
242 |
243 | Confirmed: {{school.stats.confirmed}} 244 |
245 |
246 | Declined: {{school.stats.declined}} 247 |
248 |

249 |
250 |
251 |
252 |
253 |
254 | 255 | 272 | 273 |
274 | -------------------------------------------------------------------------------- /app/client/views/admin/user/adminUserCtrl.js: -------------------------------------------------------------------------------- 1 | const swal = require('sweetalert'); 2 | 3 | angular.module('reg') 4 | .controller('AdminUserCtrl',[ 5 | '$scope', 6 | '$http', 7 | 'user', 8 | 'UserService', 9 | function($scope, $http, User, UserService){ 10 | $scope.selectedUser = User.data; 11 | 12 | // Populate the school dropdown 13 | populateSchools(); 14 | 15 | /** 16 | * TODO: JANK WARNING 17 | */ 18 | function populateSchools(){ 19 | 20 | $http 21 | .get('/assets/schools.json') 22 | .then(function(res){ 23 | var schools = res.data; 24 | var email = $scope.selectedUser.email.split('@')[1]; 25 | 26 | if (schools[email]){ 27 | $scope.selectedUser.profile.school = schools[email].school; 28 | $scope.autoFilledSchool = true; 29 | } 30 | 31 | }); 32 | } 33 | 34 | 35 | $scope.updateProfile = function(){ 36 | UserService 37 | .updateProfile($scope.selectedUser._id, $scope.selectedUser.profile) 38 | .then(response => { 39 | $selectedUser = response.data; 40 | swal("Updated!", "Profile updated.", "success"); 41 | }, response => { 42 | swal("Oops, you forgot something."); 43 | }); 44 | }; 45 | }]); 46 | -------------------------------------------------------------------------------- /app/client/views/admin/user/user.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | {{selectedUser.profile.name ? selectedUser.profile.name : selectedUser.email}} 5 | 7 | (Admin) 8 | 9 |
10 | 11 |
12 | 13 |
14 | 15 |
16 |

17 | Status: 18 | 20 | {{selectedUser.status.name.toUpperCase()}} 21 | 22 |

23 |

24 | Admitted by: {{selectedUser.status.admittedBy ? selectedUser.status.admittedBy : 'N/A'}} 25 |

26 |
27 | 28 |
29 | 30 |
31 | 32 |
33 | 34 |
35 | Basic Info 36 |
37 | 38 |
39 | 40 | Created on: 41 | 42 | {{formatTime(selectedUser.timestamp)}} 43 |
44 | 45 |
46 | 47 | Last Updated: 48 | 49 | {{formatTime(selectedUser.lastUpdated)}} 50 |
51 | 52 | 53 |
54 | 55 | 56 |
57 | 58 |
59 | 60 | 61 |
62 | 63 |
64 | 65 |
66 | 67 |
68 | 69 |
70 | 71 |
72 | 73 |
74 | Profile 75 |
76 | 77 |
78 | 79 | 83 |
84 | 85 |
86 | 87 | 90 |
91 | 92 |
93 | 94 | 98 |
99 | 100 |
101 | 102 | 105 |
106 | 107 |
108 | 109 | 118 |
119 | 120 |
121 | 122 | 131 |
132 | 133 |
134 | 135 | 138 |
139 | 140 |
141 | 144 | 146 |
147 | 148 |
150 |

151 | Because of limitations imposed by MIT, we are not legally allowed to host 152 | non-MIT minors (those under 18) for HackMIT 2015. Checking the box below affirms that you are or will be 18 years or older by September 19th, 2015. 153 |

154 |

155 | 156 | We will be checking ID. If you are a non-MIT minor, you will be turned away at the door. 157 | 158 |

159 |
160 | 161 | 162 |
163 |
164 | 165 |
166 |
169 | Update Profile 170 | 171 |
172 |
173 | 174 |
175 |
176 | 177 |
178 | 179 |
180 | 181 |
182 | 183 |
184 | Confirmation 185 |
186 | 187 |
188 | 189 | 190 |
191 | 192 |
193 | 194 | 196 |
197 | 198 |
199 | 200 | 201 |
202 | 203 |
204 | 205 | 206 |
207 | 208 |
209 | 210 | 211 |
212 | 213 |
214 | 215 | 216 |
217 | 218 |
219 | 220 | 221 |
222 | 223 |
224 | 225 |
226 | 229 |
230 | 231 | 232 |
233 |
234 | 235 |
236 | 239 | 240 |
241 | 242 |
243 | 244 |
245 | Hosting 246 |
247 | 248 |
249 | Needs Hosting Friday 250 | 253 | 254 | 257 | 258 |
259 | 260 |
261 | Needs Hosting Saturday 262 | 265 | 266 | 269 | 270 |
271 | 272 |
273 | Gender Neutral 274 | 277 | 278 | 281 | 282 |
283 | 284 |
285 | Cat Friendly 286 | 289 | 290 | 293 | 294 |
295 | 296 |
297 | Smoking Friendly 298 | 301 | 302 | 305 | 306 |
307 | 308 |
309 | 310 | 311 |
312 | 313 |
314 | 315 |
316 | Travel 317 |
318 | 319 |
320 | Needs Reimbursement 321 | 324 | 325 | 328 | 329 |
330 | 331 |
332 | 333 | 336 |
337 | 338 |
339 | 340 | 344 |
345 | 346 |
347 | 348 | 352 |
353 | 354 |
355 |
356 | 357 | 361 |
362 |
363 | 364 | 368 |
369 |
370 | 371 | 375 |
376 |
377 | 378 | 382 |
383 |
384 | 385 |
386 | 387 | 388 |
389 | 390 |
391 | 392 | 393 |
394 | 395 |
396 | 397 |
398 |
399 | -------------------------------------------------------------------------------- /app/client/views/admin/users/adminUsersCtrl.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const swal = require('sweetalert'); 3 | 4 | angular.module('reg') 5 | .controller('AdminUsersCtrl',[ 6 | '$scope', 7 | '$state', 8 | '$stateParams', 9 | 'UserService', 10 | function($scope, $state, $stateParams, UserService){ 11 | 12 | $scope.pages = []; 13 | $scope.users = []; 14 | 15 | // Semantic-UI moves modal content into a dimmer at the top level. 16 | // While this is usually nice, it means that with our routing will generate 17 | // multiple modals if you change state. Kill the top level dimmer node on initial load 18 | // to prevent this. 19 | $('.ui.dimmer').remove(); 20 | // Populate the size of the modal for when it appears, with an arbitrary user. 21 | $scope.selectedUser = {}; 22 | $scope.selectedUser.sections = generateSections({status: '', confirmation: { 23 | dietaryRestrictions: [] 24 | }, profile: ''}); 25 | 26 | function updatePage(data){ 27 | $scope.users = data.users; 28 | $scope.currentPage = data.page; 29 | $scope.pageSize = data.size; 30 | 31 | var p = []; 32 | for (var i = 0; i < data.totalPages; i++){ 33 | p.push(i); 34 | } 35 | $scope.pages = p; 36 | } 37 | 38 | UserService 39 | .getPage($stateParams.page, $stateParams.size, $stateParams.query) 40 | .then(response => { 41 | updatePage(response.data); 42 | }); 43 | 44 | $scope.$watch('queryText', function(queryText){ 45 | UserService 46 | .getPage($stateParams.page, $stateParams.size, queryText) 47 | .then(response => { 48 | updatePage(response.data); 49 | }); 50 | }); 51 | 52 | $scope.goToPage = function(page){ 53 | $state.go('app.admin.users', { 54 | page: page, 55 | size: $stateParams.size || 50 56 | }); 57 | }; 58 | 59 | $scope.goUser = function($event, user){ 60 | $event.stopPropagation(); 61 | 62 | $state.go('app.admin.user', { 63 | id: user._id 64 | }); 65 | }; 66 | 67 | $scope.getCSV = function(){ 68 | UserService.getCSV(); 69 | }; 70 | 71 | $scope.toggleCheckIn = function($event, user, index) { 72 | $event.stopPropagation(); 73 | 74 | if (!user.status.checkedIn){ 75 | swal({ 76 | title: "Whoa, wait a minute!", 77 | text: "You are about to check in " + user.profile.name + "!", 78 | icon: "warning", 79 | buttons: { 80 | cancel: { 81 | text: "Cancel", 82 | value: null, 83 | visible: true 84 | }, 85 | checkIn: { 86 | className: "danger-button", 87 | closeModal: false, 88 | text: "Yes, check them in", 89 | value: true, 90 | visible: true 91 | } 92 | } 93 | }) 94 | .then(value => { 95 | if (!value) { 96 | return; 97 | } 98 | 99 | UserService 100 | .checkIn(user._id) 101 | .then(response => { 102 | $scope.users[index] = response.data; 103 | swal("Accepted", response.data.profile.name + " has been checked in.", "success"); 104 | }); 105 | }); 106 | } else { 107 | UserService 108 | .checkOut(user._id) 109 | .then(response => { 110 | $scope.users[index] = response.data; 111 | swal("Accepted", response.data.profile.name + ' has been checked out.', "success"); 112 | }); 113 | } 114 | }; 115 | 116 | $scope.acceptUser = function($event, user, index) { 117 | $event.stopPropagation(); 118 | 119 | console.log(user); 120 | 121 | swal({ 122 | buttons: { 123 | cancel: { 124 | text: "Cancel", 125 | value: null, 126 | visible: true 127 | }, 128 | accept: { 129 | className: "danger-button", 130 | closeModal: false, 131 | text: "Yes, accept them", 132 | value: true, 133 | visible: true 134 | } 135 | }, 136 | dangerMode: true, 137 | icon: "warning", 138 | text: "You are about to accept " + user.profile.name + "!", 139 | title: "Whoa, wait a minute!" 140 | }).then(value => { 141 | if (!value) { 142 | return; 143 | } 144 | 145 | swal({ 146 | buttons: { 147 | cancel: { 148 | text: "Cancel", 149 | value: null, 150 | visible: true 151 | }, 152 | yes: { 153 | className: "danger-button", 154 | closeModal: false, 155 | text: "Yes, accept this user", 156 | value: true, 157 | visible: true 158 | } 159 | }, 160 | dangerMode: true, 161 | title: "Are you sure?", 162 | text: "Your account will be logged as having accepted this user. " + 163 | "Remember, this power is a privilege.", 164 | icon: "warning" 165 | }).then(value => { 166 | if (!value) { 167 | return; 168 | } 169 | 170 | UserService 171 | .admitUser(user._id) 172 | .then(response => { 173 | $scope.users[index] = response.data; 174 | swal("Accepted", response.data.profile.name + ' has been admitted.', "success"); 175 | }); 176 | }); 177 | }); 178 | }; 179 | 180 | $scope.toggleAdmin = function($event, user, index) { 181 | $event.stopPropagation(); 182 | 183 | if (!user.admin){ 184 | swal({ 185 | title: "Whoa, wait a minute!", 186 | text: "You are about make " + user.profile.name + " an admin!", 187 | icon: "warning", 188 | buttons: { 189 | cancel: { 190 | text: "Cancel", 191 | value: null, 192 | visible: true 193 | }, 194 | confirm: { 195 | text: "Yes, make them an admin", 196 | className: "danger-button", 197 | closeModal: false, 198 | value: true, 199 | visible: true 200 | } 201 | } 202 | }).then(value => { 203 | if (!value) { 204 | return; 205 | } 206 | 207 | UserService 208 | .makeAdmin(user._id) 209 | .then(response => { 210 | $scope.users[index] = response.data; 211 | swal("Made", response.data.profile.name + ' an admin.', "success"); 212 | }); 213 | } 214 | ); 215 | } else { 216 | UserService 217 | .removeAdmin(user._id) 218 | .then(response => { 219 | $scope.users[index] = response.data; 220 | swal("Removed", response.data.profile.name + ' as admin', "success"); 221 | }); 222 | } 223 | }; 224 | 225 | function formatTime(time){ 226 | if (time) { 227 | return moment(time).format('MMMM Do YYYY, h:mm:ss a'); 228 | } 229 | } 230 | 231 | $scope.rowClass = function(user) { 232 | if (user.admin){ 233 | return 'admin'; 234 | } 235 | if (user.status.confirmed) { 236 | return 'positive'; 237 | } 238 | if (user.status.admitted && !user.status.confirmed) { 239 | return 'warning'; 240 | } 241 | }; 242 | 243 | function selectUser(user){ 244 | $scope.selectedUser = user; 245 | $scope.selectedUser.sections = generateSections(user); 246 | $('.long.user.modal') 247 | .modal('show'); 248 | } 249 | 250 | function generateSections(user){ 251 | return [ 252 | { 253 | name: 'Basic Info', 254 | fields: [ 255 | { 256 | name: 'Created On', 257 | value: formatTime(user.timestamp) 258 | },{ 259 | name: 'Last Updated', 260 | value: formatTime(user.lastUpdated) 261 | },{ 262 | name: 'Confirm By', 263 | value: formatTime(user.status.confirmBy) || 'N/A' 264 | },{ 265 | name: 'Checked In', 266 | value: formatTime(user.status.checkInTime) || 'N/A' 267 | },{ 268 | name: 'Email', 269 | value: user.email 270 | },{ 271 | name: 'Team', 272 | value: user.teamCode || 'None' 273 | } 274 | ] 275 | },{ 276 | name: 'Profile', 277 | fields: [ 278 | { 279 | name: 'Name', 280 | value: user.profile.name 281 | },{ 282 | name: 'Gender', 283 | value: user.profile.gender 284 | },{ 285 | name: 'School', 286 | value: user.profile.school 287 | },{ 288 | name: 'Graduation Year', 289 | value: user.profile.graduationYear 290 | },{ 291 | name: 'Description', 292 | value: user.profile.description 293 | },{ 294 | name: 'Essay', 295 | value: user.profile.essay 296 | } 297 | ] 298 | },{ 299 | name: 'Confirmation', 300 | fields: [ 301 | { 302 | name: 'Phone Number', 303 | value: user.confirmation.phoneNumber 304 | },{ 305 | name: 'Dietary Restrictions', 306 | value: user.confirmation.dietaryRestrictions.join(', ') 307 | },{ 308 | name: 'Shirt Size', 309 | value: user.confirmation.shirtSize 310 | },{ 311 | name: 'Major', 312 | value: user.confirmation.major 313 | },{ 314 | name: 'Github', 315 | value: user.confirmation.github 316 | },{ 317 | name: 'Website', 318 | value: user.confirmation.website 319 | },{ 320 | name: 'Needs Hardware', 321 | value: user.confirmation.wantsHardware, 322 | type: 'boolean' 323 | },{ 324 | name: 'Hardware Requested', 325 | value: user.confirmation.hardware 326 | } 327 | ] 328 | },{ 329 | name: 'Hosting', 330 | fields: [ 331 | { 332 | name: 'Needs Hosting Friday', 333 | value: user.confirmation.hostNeededFri, 334 | type: 'boolean' 335 | },{ 336 | name: 'Needs Hosting Saturday', 337 | value: user.confirmation.hostNeededSat, 338 | type: 'boolean' 339 | },{ 340 | name: 'Gender Neutral', 341 | value: user.confirmation.genderNeutral, 342 | type: 'boolean' 343 | },{ 344 | name: 'Cat Friendly', 345 | value: user.confirmation.catFriendly, 346 | type: 'boolean' 347 | },{ 348 | name: 'Smoking Friendly', 349 | value: user.confirmation.smokingFriendly, 350 | type: 'boolean' 351 | },{ 352 | name: 'Hosting Notes', 353 | value: user.confirmation.hostNotes 354 | } 355 | ] 356 | },{ 357 | name: 'Travel', 358 | fields: [ 359 | { 360 | name: 'Needs Reimbursement', 361 | value: user.confirmation.needsReimbursement, 362 | type: 'boolean' 363 | },{ 364 | name: 'Received Reimbursement', 365 | value: user.confirmation.needsReimbursement && user.status.reimbursementGiven 366 | },{ 367 | name: 'Address', 368 | value: user.confirmation.address ? [ 369 | user.confirmation.address.line1, 370 | user.confirmation.address.line2, 371 | user.confirmation.address.city, 372 | ',', 373 | user.confirmation.address.state, 374 | user.confirmation.address.zip, 375 | ',', 376 | user.confirmation.address.country, 377 | ].join(' ') : '' 378 | },{ 379 | name: 'Additional Notes', 380 | value: user.confirmation.notes 381 | } 382 | ] 383 | } 384 | ]; 385 | } 386 | 387 | $scope.selectUser = selectUser; 388 | 389 | }]); 390 | -------------------------------------------------------------------------------- /app/client/views/admin/users/users.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 | Search 7 |
8 |
9 |
10 |
11 | 15 | 16 |
17 |
18 | 19 |
20 | 21 |
22 | 23 | 30 | 31 |
32 | 33 | 34 | 35 |
36 |
37 |
38 | Users 39 |
40 | 93 | 94 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 114 | 121 | 122 | 123 | 127 | 179 | 180 | 181 | 200 | 201 | 214 | 215 | 223 | 224 | 262 | 263 | 264 |
NameE-mail addressSchoolGradV/S/A/CHost$
115 | 116 | {{user.profile.name}} 117 |   118 | 119 | 120 | {{user.email}}{{user.profile.school}} 125 | {{user.profile.graduationYear}} 126 | 129 | 130 | 131 | 134 | 135 | 138 | 139 | 140 | 141 | 144 | 145 | 148 | 149 | 150 | 151 | 154 | 155 | 158 | 159 | 160 | 161 | 164 | 165 | 168 | 169 | 172 | 173 | 176 | 177 | 178 | 183 | 184 | 185 | 188 | 189 | 192 | 193 | 196 | 197 | 198 | 199 | 203 | 204 | 207 | 208 | 211 | 212 | 213 | 217 | 222 | 226 | 227 | 232 | 233 | 246 | 247 | 260 | 261 |
265 | 266 |
267 |
268 | 269 |
270 | -------------------------------------------------------------------------------- /app/client/views/application/application.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Application 4 |
5 | 6 |
7 |
8 |
10 | 11 |
Basic Information
12 | 13 |
14 | 15 |
16 | 17 | 21 |
22 | 23 |
24 | 25 | 28 |
29 | 30 |
31 | 32 | 36 |
37 | 38 |
39 | 40 | 46 |
47 | 48 |
49 | 50 | 59 |
60 | 61 |
62 | 63 | 72 |
73 | 74 |
75 | 76 | 79 |
80 | 81 |
82 | 85 | 87 |
88 | 89 |
91 |

92 | Because of limitations imposed by MIT, we are not legally allowed to host 93 | non-MIT minors (those under 18) for HackMIT 2015. Checking the box below affirms that you are or will be 18 years or older by September 19th, 2015. 94 |

95 |

96 | 97 | We will be checking ID. If you are a non-MIT minor, you will be turned away at the door. 98 | 99 |

100 |
101 | 102 | 103 |
104 |
105 | 106 |
107 | 108 |
109 | 114 |
115 | 116 |
117 |
118 |
119 |
120 |
121 | -------------------------------------------------------------------------------- /app/client/views/application/applicationCtrl.js: -------------------------------------------------------------------------------- 1 | const angular = require("angular"); 2 | const swal = require("sweetalert"); 3 | 4 | angular.module('reg') 5 | .controller('ApplicationCtrl', [ 6 | '$scope', 7 | '$rootScope', 8 | '$state', 9 | '$http', 10 | 'currentUser', 11 | 'settings', 12 | 'Session', 13 | 'UserService', 14 | function($scope, $rootScope, $state, $http, currentUser, settings, Session, UserService) { 15 | 16 | // Set up the user 17 | $scope.user = currentUser.data; 18 | 19 | // Is the student from MIT? 20 | $scope.isMitStudent = $scope.user.email.split('@')[1] == 'mit.edu'; 21 | 22 | // If so, default them to adult: true 23 | if ($scope.isMitStudent){ 24 | $scope.user.profile.adult = true; 25 | } 26 | 27 | // Populate the school dropdown 28 | populateSchools(); 29 | _setupForm(); 30 | 31 | $scope.regIsClosed = Date.now() > settings.data.timeClose; 32 | 33 | /** 34 | * TODO: JANK WARNING 35 | */ 36 | function populateSchools(){ 37 | $http 38 | .get('/assets/schools.json') 39 | .then(function(res){ 40 | var schools = res.data; 41 | var email = $scope.user.email.split('@')[1]; 42 | 43 | if (schools[email]){ 44 | $scope.user.profile.school = schools[email].school; 45 | $scope.autoFilledSchool = true; 46 | } 47 | }); 48 | 49 | $http 50 | .get('/assets/schools.csv') 51 | .then(function(res){ 52 | $scope.schools = res.data.split('\n'); 53 | $scope.schools.push('Other'); 54 | 55 | var content = []; 56 | 57 | for(i = 0; i < $scope.schools.length; i++) { 58 | $scope.schools[i] = $scope.schools[i].trim(); 59 | content.push({title: $scope.schools[i]}); 60 | } 61 | 62 | $('#school.ui.search') 63 | .search({ 64 | source: content, 65 | cache: true, 66 | onSelect: function(result, response) { 67 | $scope.user.profile.school = result.title.trim(); 68 | } 69 | }); 70 | }); 71 | } 72 | 73 | function _updateUser(e){ 74 | UserService 75 | .updateProfile(Session.getUserId(), $scope.user.profile) 76 | .then(response => { 77 | swal("Awesome!", "Your application has been saved.", "success").then(value => { 78 | $state.go("app.dashboard"); 79 | }); 80 | }, response => { 81 | swal("Uh oh!", "Something went wrong.", "error"); 82 | }); 83 | } 84 | 85 | function isMinor() { 86 | return !$scope.user.profile.adult; 87 | } 88 | 89 | function minorsAreAllowed() { 90 | return settings.data.allowMinors; 91 | } 92 | 93 | function minorsValidation() { 94 | // Are minors allowed to register? 95 | if (isMinor() && !minorsAreAllowed()) { 96 | return false; 97 | } 98 | return true; 99 | } 100 | 101 | function _setupForm(){ 102 | // Custom minors validation rule 103 | $.fn.form.settings.rules.allowMinors = function (value) { 104 | return minorsValidation(); 105 | }; 106 | 107 | // Semantic-UI form validation 108 | $('.ui.form').form({ 109 | inline: true, 110 | fields: { 111 | name: { 112 | identifier: 'name', 113 | rules: [ 114 | { 115 | type: 'empty', 116 | prompt: 'Please enter your name.' 117 | } 118 | ] 119 | }, 120 | school: { 121 | identifier: 'school', 122 | rules: [ 123 | { 124 | type: 'empty', 125 | prompt: 'Please enter your school name.' 126 | } 127 | ] 128 | }, 129 | year: { 130 | identifier: 'year', 131 | rules: [ 132 | { 133 | type: 'empty', 134 | prompt: 'Please select your graduation year.' 135 | } 136 | ] 137 | }, 138 | gender: { 139 | identifier: 'gender', 140 | rules: [ 141 | { 142 | type: 'empty', 143 | prompt: 'Please select a gender.' 144 | } 145 | ] 146 | }, 147 | adult: { 148 | identifier: 'adult', 149 | rules: [ 150 | { 151 | type: 'allowMinors', 152 | prompt: 'You must be an adult, or an MIT student.' 153 | } 154 | ] 155 | } 156 | } 157 | }); 158 | } 159 | 160 | $scope.submitForm = function(){ 161 | if ($('.ui.form').form('is valid')){ 162 | _updateUser(); 163 | } else { 164 | swal("Uh oh!", "Please Fill The Required Fields", "error"); 165 | } 166 | }; 167 | }]); 168 | -------------------------------------------------------------------------------- /app/client/views/base.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 |
8 |
9 |
10 | 11 |
12 | -------------------------------------------------------------------------------- /app/client/views/confirmation/confirmationCtrl.js: -------------------------------------------------------------------------------- 1 | const swal = require('sweetalert'); 2 | 3 | angular.module('reg') 4 | .controller('ConfirmationCtrl', [ 5 | '$scope', 6 | '$rootScope', 7 | '$state', 8 | 'currentUser', 9 | 'Utils', 10 | 'UserService', 11 | function($scope, $rootScope, $state, currentUser, Utils, UserService){ 12 | 13 | // Set up the user 14 | var user = currentUser.data; 15 | $scope.user = user; 16 | 17 | $scope.pastConfirmation = Date.now() > user.status.confirmBy; 18 | 19 | $scope.formatTime = Utils.formatTime; 20 | 21 | _setupForm(); 22 | 23 | $scope.fileName = user._id + "_" + user.profile.name.split(" ").join("_"); 24 | 25 | // ------------------------------- 26 | // All this just for dietary restriction checkboxes fml 27 | 28 | var dietaryRestrictions = { 29 | 'Vegetarian': false, 30 | 'Vegan': false, 31 | 'Halal': false, 32 | 'Kosher': false, 33 | 'Nut Allergy': false 34 | }; 35 | 36 | if (user.confirmation.dietaryRestrictions){ 37 | user.confirmation.dietaryRestrictions.forEach(function(restriction){ 38 | if (restriction in dietaryRestrictions){ 39 | dietaryRestrictions[restriction] = true; 40 | } 41 | }); 42 | } 43 | 44 | $scope.dietaryRestrictions = dietaryRestrictions; 45 | 46 | // ------------------------------- 47 | 48 | function _updateUser(e){ 49 | var confirmation = $scope.user.confirmation; 50 | // Get the dietary restrictions as an array 51 | var drs = []; 52 | Object.keys($scope.dietaryRestrictions).forEach(function(key){ 53 | if ($scope.dietaryRestrictions[key]){ 54 | drs.push(key); 55 | } 56 | }); 57 | confirmation.dietaryRestrictions = drs; 58 | 59 | UserService 60 | .updateConfirmation(user._id, confirmation) 61 | .then(response => { 62 | swal("Woo!", "You're confirmed!", "success").then(value => { 63 | $state.go("app.dashboard"); 64 | }); 65 | }, response => { 66 | swal("Uh oh!", "Something went wrong.", "error"); 67 | }); 68 | } 69 | 70 | function _setupForm(){ 71 | // Semantic-UI form validation 72 | $('.ui.form').form({ 73 | fields: { 74 | shirt: { 75 | identifier: 'shirt', 76 | rules: [ 77 | { 78 | type: 'empty', 79 | prompt: 'Please give us a shirt size!' 80 | } 81 | ] 82 | }, 83 | phone: { 84 | identifier: 'phone', 85 | rules: [ 86 | { 87 | type: 'empty', 88 | prompt: 'Please enter a phone number.' 89 | } 90 | ] 91 | }, 92 | signatureLiability: { 93 | identifier: 'signatureLiabilityWaiver', 94 | rules: [ 95 | { 96 | type: 'empty', 97 | prompt: 'Please type your digital signature.' 98 | } 99 | ] 100 | }, 101 | signaturePhotoRelease: { 102 | identifier: 'signaturePhotoRelease', 103 | rules: [ 104 | { 105 | type: 'empty', 106 | prompt: 'Please type your digital signature.' 107 | } 108 | ] 109 | }, 110 | signatureCodeOfConduct: { 111 | identifier: 'signatureCodeOfConduct', 112 | rules: [ 113 | { 114 | type: 'empty', 115 | prompt: 'Please type your digital signature.' 116 | } 117 | ] 118 | }, 119 | } 120 | }); 121 | } 122 | 123 | $scope.submitForm = function(){ 124 | if ($('.ui.form').form('is valid')){ 125 | _updateUser(); 126 | } 127 | }; 128 | 129 | }]); 130 | -------------------------------------------------------------------------------- /app/client/views/dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Dashboard 4 |
5 | 6 |
7 |
8 |
9 |
10 | Your status: 11 |
12 |
13 | 14 |
18 | {{user.status.name}} 19 |
20 | 21 |
24 | Expired Admission 25 |
26 | 27 | 28 |
31 | On waitlist 32 |
33 | 34 |
35 | 36 |
37 |

38 | 39 | Welcome back, {{user.profile.name}}! 40 | 41 |

42 |

43 |

44 |
45 | 46 |
49 |

50 | 51 | Your email address ({{user.email}}) is not yet verified. 52 | 53 |

54 |

55 | {{ DASHBOARD.UNVERIFIED }} 56 |

57 |
58 | Resend verification email 59 |
60 |
61 | 62 |
65 | 66 | Registration Deadline: 67 | 68 | {{ timeClose }} 69 |
70 | 71 |
74 | 75 | Confirmation Deadline: 76 | 77 | {{ timeConfirm }} 78 |
79 | 80 |
83 |

84 | 85 | {{ DASHBOARD.INCOMPLETE_TITLE }} 86 | 87 |

88 |

89 | {{ DASHBOARD.INCOMPLETE }} 90 |

91 | 92 |
94 | Complete your application 95 |
96 | 97 |
98 | 99 |
102 |

103 | 104 | {{ DASHBOARD.SUBMITTED_TITLE }} 105 | 106 |

107 |

108 | {{ DASHBOARD.SUBMITTED }} 109 |

110 |
112 | Edit your application 113 |
114 | 115 |
119 | Create or join a team 120 |
121 | 122 |
126 | View your Team 127 |
128 | 129 |
130 | 131 |
133 |

134 | 135 | {{ DASHBOARD.CLOSED_AND_INCOMPLETE_TITLE }} 136 | 137 |

138 |

139 | {{ DASHBOARD.CLOSED_AND_INCOMPLETE }} 140 |

141 |
142 | 143 |
146 | 147 |
150 |
151 | 152 |
154 | View your application 155 |
156 | 157 |
161 | View your Team 162 |
163 |
164 | 165 |
168 | 169 |
170 |

171 | {{ DASHBOARD.ADMITTED_AND_CAN_CONFIRM_TITLE }} 172 |

173 |
174 | 175 |
178 |
179 | 180 |
181 |

182 | {{ DASHBOARD.ADMITTED_AND_CAN_CONFIRM }} 183 |

184 |
185 | 186 |
188 | Confirm your spot 189 |
190 | 191 |
193 | Sorry, I can't make it 194 |
195 | 196 |
197 | 198 |
201 | 202 |
203 |

204 | {{ DASHBOARD.ADMITTED_AND_CANNOT_CONFIRM_TITLE }} 205 |

206 | 207 |

208 | {{ DASHBOARD.ADMITTED_AND_CANNOT_CONFIRM }} 209 |

210 |
211 | 212 |
213 | 214 |
217 | 218 |
221 |

222 | {{ DASHBOARD.CONFIRMED_NOT_PAST_TITLE }} 223 |

224 |
225 | 226 |
229 |
230 | 231 |
233 | {{pastConfirmation ? 'View' : 'Edit'}} your confirmation information 234 |
235 | 236 |
238 | Sorry, I can't make it 239 |
240 | 241 |
242 | 243 |
246 | 247 |

248 | {{ DASHBOARD.DECLINED }} 249 |

250 |
251 | 252 |
253 | 254 |
255 |
256 |
257 | -------------------------------------------------------------------------------- /app/client/views/dashboard/dashboardCtrl.js: -------------------------------------------------------------------------------- 1 | const angular = require('angular'); 2 | const showdown = require('showdown'); 3 | const swal = require('sweetalert'); 4 | 5 | angular.module('reg') 6 | .controller('DashboardCtrl', [ 7 | '$rootScope', 8 | '$scope', 9 | '$sce', 10 | 'currentUser', 11 | 'settings', 12 | 'Utils', 13 | 'AuthService', 14 | 'UserService', 15 | 'EVENT_INFO', 16 | 'DASHBOARD', 17 | function($rootScope, $scope, $sce, currentUser, settings, Utils, AuthService, UserService, EVENT_INFO, DASHBOARD){ 18 | var Settings = settings.data; 19 | var user = currentUser.data; 20 | $scope.user = user; 21 | $scope.timeClose = Utils.formatTime(Settings.timeClose); 22 | $scope.timeConfirm = Utils.formatTime(Settings.timeConfirm); 23 | 24 | $scope.DASHBOARD = DASHBOARD; 25 | 26 | for (var msg in $scope.DASHBOARD) { 27 | if ($scope.DASHBOARD[msg].includes('[APP_DEADLINE]')) { 28 | $scope.DASHBOARD[msg] = $scope.DASHBOARD[msg].replace('[APP_DEADLINE]', Utils.formatTime(Settings.timeClose)); 29 | } 30 | if ($scope.DASHBOARD[msg].includes('[CONFIRM_DEADLINE]')) { 31 | $scope.DASHBOARD[msg] = $scope.DASHBOARD[msg].replace('[CONFIRM_DEADLINE]', Utils.formatTime(user.status.confirmBy)); 32 | } 33 | } 34 | 35 | // Is registration open? 36 | var regIsOpen = $scope.regIsOpen = Utils.isRegOpen(Settings); 37 | 38 | // Is it past the user's confirmation time? 39 | var pastConfirmation = $scope.pastConfirmation = Utils.isAfter(user.status.confirmBy); 40 | 41 | $scope.dashState = function(status){ 42 | var user = $scope.user; 43 | switch (status) { 44 | case 'unverified': 45 | return !user.verified; 46 | case 'openAndIncomplete': 47 | return regIsOpen && user.verified && !user.status.completedProfile; 48 | case 'openAndSubmitted': 49 | return regIsOpen && user.status.completedProfile && !user.status.admitted; 50 | case 'closedAndIncomplete': 51 | return !regIsOpen && !user.status.completedProfile && !user.status.admitted; 52 | case 'closedAndSubmitted': // Waitlisted State 53 | return !regIsOpen && user.status.completedProfile && !user.status.admitted; 54 | case 'admittedAndCanConfirm': 55 | return !pastConfirmation && 56 | user.status.admitted && 57 | !user.status.confirmed && 58 | !user.status.declined; 59 | case 'admittedAndCannotConfirm': 60 | return pastConfirmation && 61 | user.status.admitted && 62 | !user.status.confirmed && 63 | !user.status.declined; 64 | case 'confirmed': 65 | return user.status.admitted && user.status.confirmed && !user.status.declined; 66 | case 'declined': 67 | return user.status.declined; 68 | } 69 | return false; 70 | }; 71 | 72 | $scope.showWaitlist = !regIsOpen && user.status.completedProfile && !user.status.admitted; 73 | 74 | $scope.resendEmail = function(){ 75 | AuthService 76 | .resendVerificationEmail() 77 | .then(response => { 78 | swal("Your email has been sent."); 79 | }); 80 | }; 81 | 82 | 83 | // ----------------------------------------------------- 84 | // Text! 85 | // ----------------------------------------------------- 86 | var converter = new showdown.Converter(); 87 | $scope.acceptanceText = $sce.trustAsHtml(converter.makeHtml(Settings.acceptanceText)); 88 | $scope.confirmationText = $sce.trustAsHtml(converter.makeHtml(Settings.confirmationText)); 89 | $scope.waitlistText = $sce.trustAsHtml(converter.makeHtml(Settings.waitlistText)); 90 | 91 | $scope.declineAdmission = function(){ 92 | 93 | swal({ 94 | title: "Whoa!", 95 | text: "Are you sure you would like to decline your admission? \n\n You can't go back!", 96 | icon: "warning", 97 | buttons: { 98 | cancel: { 99 | text: "Cancel", 100 | value: null, 101 | visible: true 102 | }, 103 | confirm: { 104 | text: "Yes, I can't make it", 105 | value: true, 106 | visible: true, 107 | className: "danger-button" 108 | } 109 | } 110 | }).then(value => { 111 | if (!value) { 112 | return; 113 | } 114 | 115 | UserService 116 | .declineAdmission(user._id) 117 | .then(response => { 118 | $rootScope.currentUser = response.data; 119 | $scope.user = response.data; 120 | }); 121 | }); 122 | }; 123 | }]); 124 | -------------------------------------------------------------------------------- /app/client/views/login/login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 9 | 10 |
11 | 12 |
14 | {{error}} 15 |
16 | 17 |
18 | 43 | 44 |
45 |
46 |
47 | 53 |
54 |
55 |
56 | 57 |
58 |
59 |
60 |
61 | 62 | 66 |
67 |
68 | 73 |
74 |
75 |
76 |
77 | 78 |
79 | 80 | 85 | 86 | 91 | 92 | 93 |
94 |
95 | 96 |
97 |
98 | -------------------------------------------------------------------------------- /app/client/views/login/loginCtrl.js: -------------------------------------------------------------------------------- 1 | angular.module('reg') 2 | .controller('LoginCtrl', [ 3 | '$scope', 4 | '$http', 5 | '$state', 6 | 'settings', 7 | 'Utils', 8 | 'AuthService', 9 | function($scope, $http, $state, settings, Utils, AuthService){ 10 | 11 | // Is registration open? 12 | var Settings = settings.data; 13 | $scope.regIsOpen = Utils.isRegOpen(Settings); 14 | 15 | // Start state for login 16 | $scope.loginState = 'login'; 17 | 18 | function onSuccess() { 19 | $state.go('app.dashboard'); 20 | } 21 | 22 | function onError(data){ 23 | $scope.error = data.message; 24 | } 25 | 26 | function resetError(){ 27 | $scope.error = null; 28 | } 29 | 30 | $scope.login = function(){ 31 | resetError(); 32 | AuthService.loginWithPassword( 33 | $scope.email, $scope.password, onSuccess, onError); 34 | }; 35 | 36 | $scope.register = function(){ 37 | resetError(); 38 | AuthService.register( 39 | $scope.email, $scope.password, onSuccess, onError); 40 | }; 41 | 42 | $scope.setLoginState = function(state) { 43 | $scope.loginState = state; 44 | }; 45 | 46 | $scope.sendResetEmail = function() { 47 | var email = $scope.email; 48 | AuthService.sendResetEmail(email); 49 | swal("Don't sweat!", "An email should be sent to you shortly.", "success"); 50 | }; 51 | 52 | } 53 | ]); 54 | -------------------------------------------------------------------------------- /app/client/views/reset/reset.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 9 | 10 |
11 | 12 |
14 | {{error}} 15 |
16 | 17 | 43 | 44 |
45 |
46 | 47 |
48 |
-------------------------------------------------------------------------------- /app/client/views/reset/resetCtrl.js: -------------------------------------------------------------------------------- 1 | const swal = require('sweetalert'); 2 | 3 | angular.module('reg') 4 | .controller('ResetCtrl', [ 5 | '$scope', 6 | '$stateParams', 7 | '$state', 8 | 'AuthService', 9 | function($scope, $stateParams, $state, AuthService){ 10 | var token = $stateParams.token; 11 | 12 | $scope.loading = true; 13 | 14 | $scope.changePassword = function(){ 15 | var password = $scope.password; 16 | var confirm = $scope.confirm; 17 | 18 | if (password !== confirm){ 19 | $scope.error = "Passwords don't match!"; 20 | $scope.confirm = ""; 21 | return; 22 | } 23 | 24 | AuthService.resetPassword( 25 | token, 26 | $scope.password, 27 | message => { 28 | swal("Neato!", "Your password has been changed!", "success").then(value => { 29 | $state.go("login"); 30 | }); 31 | }, 32 | data => { 33 | $scope.error = data.message; 34 | $scope.loading = false; 35 | }); 36 | }; 37 | }]); 38 | -------------------------------------------------------------------------------- /app/client/views/sidebar/sidebar.html: -------------------------------------------------------------------------------- 1 | 60 | 61 |
63 | 64 | 65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /app/client/views/sidebar/sidebarCtrl.js: -------------------------------------------------------------------------------- 1 | const angular = require('angular'); 2 | const Utils = require('../../src/modules/Utils.js'); 3 | 4 | angular.module('reg') 5 | .service('settings', function() {}) 6 | .controller('SidebarCtrl', [ 7 | '$rootScope', 8 | '$scope', 9 | 'settings', 10 | 'Utils', 11 | 'AuthService', 12 | 'Session', 13 | 'EVENT_INFO', 14 | function($rootScope, $scope, settings, Utils, AuthService, Session, EVENT_INFO){ 15 | 16 | var settings = settings.data; 17 | var user = $rootScope.currentUser; 18 | 19 | $scope.EVENT_INFO = EVENT_INFO; 20 | 21 | $scope.pastConfirmation = Utils.isAfter(user.status.confirmBy); 22 | 23 | $scope.logout = function(){ 24 | AuthService.logout(); 25 | }; 26 | 27 | $scope.showSidebar = false; 28 | $scope.toggleSidebar = function(){ 29 | $scope.showSidebar = !$scope.showSidebar; 30 | }; 31 | 32 | // oh god jQuery hack 33 | $('.item').on('click', function(){ 34 | $scope.showSidebar = false; 35 | }); 36 | 37 | }]); 38 | -------------------------------------------------------------------------------- /app/client/views/team/team.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Team 4 |
5 |
6 |
7 |
10 | {{error}} 11 |
12 | 13 |
14 |
16 |

17 | {{ TEAM.NO_TEAM_REG_CLOSED }} 18 |

19 |
20 |
23 | 24 |
25 | Join or Create a Team 26 |
27 | 28 |
29 | 30 |
31 | 36 |
37 | 38 |
39 | 44 |
45 | 46 |
47 | 48 |
50 |
51 |
52 | Your Team Name: 53 |
54 |
55 |
56 |
57 | {{user.teamCode}} 58 |
59 | 60 |
61 | 62 |
63 | 64 |
65 |
66 | Your Team: 67 |
68 |
69 | 70 |

73 | {{user.profile.name || 'Somebody (has not filled out their profile!)'}} 74 |

75 | 76 |
78 |
79 | 80 | 83 |
84 | 85 |
86 | 87 |
88 |
89 |
90 | 91 |
92 | -------------------------------------------------------------------------------- /app/client/views/team/teamCtrl.js: -------------------------------------------------------------------------------- 1 | angular.module('reg') 2 | .controller('TeamCtrl', [ 3 | '$scope', 4 | 'currentUser', 5 | 'settings', 6 | 'Utils', 7 | 'UserService', 8 | 'TEAM', 9 | function($scope, currentUser, settings, Utils, UserService, TEAM){ 10 | // Get the current user's most recent data. 11 | var Settings = settings.data; 12 | 13 | $scope.regIsOpen = Utils.isRegOpen(Settings); 14 | 15 | $scope.user = currentUser.data; 16 | 17 | $scope.TEAM = TEAM; 18 | 19 | function _populateTeammates() { 20 | UserService 21 | .getMyTeammates() 22 | .then(response => { 23 | $scope.error = null; 24 | $scope.teammates = response.data; 25 | }); 26 | } 27 | 28 | if ($scope.user.teamCode){ 29 | _populateTeammates(); 30 | } 31 | 32 | $scope.joinTeam = function(){ 33 | UserService 34 | .joinOrCreateTeam($scope.code) 35 | .then(response => { 36 | $scope.error = null; 37 | $scope.user = response.data; 38 | _populateTeammates(); 39 | }, response => { 40 | $scope.error = response.data.message; 41 | }); 42 | }; 43 | 44 | $scope.leaveTeam = function(){ 45 | UserService 46 | .leaveTeam() 47 | .then(response => { 48 | $scope.error = null; 49 | $scope.user = response.data; 50 | $scope.teammates = []; 51 | }, response => { 52 | $scope.error = response.data.message; 53 | }); 54 | }; 55 | 56 | }]); 57 | -------------------------------------------------------------------------------- /app/client/views/verify/verify.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | EMAIL VERIFICATION 4 |
5 |
6 |
7 | 8 |

9 | Your email has been verified! 10 |

11 |
12 |
14 | Continue to Dashboard 15 |
16 |
17 |
18 | 19 |

20 | This token is invalid. 21 |

22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /app/client/views/verify/verifyCtrl.js: -------------------------------------------------------------------------------- 1 | angular.module('reg') 2 | .controller('VerifyCtrl', [ 3 | '$scope', 4 | '$stateParams', 5 | 'AuthService', 6 | function($scope, $stateParams, AuthService){ 7 | var token = $stateParams.token; 8 | 9 | $scope.loading = true; 10 | 11 | if (token) { 12 | AuthService.verify(token, 13 | function(user){ 14 | $scope.success = true; 15 | $scope.loading = false; 16 | }, 17 | function(err){ 18 | $scope.loading = false; 19 | }); 20 | } 21 | }]); 22 | -------------------------------------------------------------------------------- /app/server/controllers/SettingsController.js: -------------------------------------------------------------------------------- 1 | var Settings = require('../models/Settings'); 2 | 3 | var SettingsController = {}; 4 | 5 | /** 6 | * Update any field in the settings. 7 | * @param {String} field Name of the field 8 | * @param {Any} value Value to replace it to 9 | * @param {Function} callback args(err, settings) 10 | */ 11 | SettingsController.updateField = function(field, value, callback){ 12 | var update = {}; 13 | update[field] = value; 14 | Settings 15 | .findOneAndUpdate({},{ 16 | $set: update 17 | }, {new: true}, callback); 18 | }; 19 | 20 | /** 21 | * Update the list of whitelisted emails and email extensions. 22 | * @param {[type]} emails [description] 23 | * @param {Function} callback args(err, settings) 24 | */ 25 | SettingsController.updateWhitelistedEmails = function(emails, callback){ 26 | Settings 27 | .findOneAndUpdate({},{ 28 | $set: { 29 | whitelistedEmails: emails 30 | } 31 | }, {new: true}) 32 | .select('whitelistedEmails') 33 | .exec(callback); 34 | }; 35 | 36 | /** 37 | * Get the list of whitelisted emails. 38 | * Whitelist emails are by default not included in settings. 39 | * @param {Function} callback args(err, emails) 40 | */ 41 | SettingsController.getWhitelistedEmails = function(callback){ 42 | Settings.getWhitelistedEmails(callback); 43 | }; 44 | 45 | /** 46 | * Set the time window for registrations. 47 | * If either open or close are null, do not change that time. 48 | * @param {Number} open Open time in ms 49 | * @param {Number} close Close time in ms 50 | * @param {Function} callback args(err, settings) 51 | */ 52 | SettingsController.updateRegistrationTimes = function(open, close, callback){ 53 | var updatedTimes = {}; 54 | 55 | if (close <= open){ 56 | return callback({ 57 | message: "Registration cannot close before or at exactly the same time it opens." 58 | }); 59 | } 60 | 61 | if (open){ 62 | updatedTimes.timeOpen = open; 63 | } 64 | 65 | if (close){ 66 | updatedTimes.timeClose = close; 67 | } 68 | 69 | Settings 70 | .findOneAndUpdate({},{ 71 | $set: updatedTimes 72 | }, {new: true}, callback); 73 | }; 74 | 75 | /** 76 | * Get the open and close time for registration. 77 | * @param {Function} callback args(err, times : {timeOpen, timeClose}) 78 | */ 79 | SettingsController.getRegistrationTimes = function(callback){ 80 | Settings.getRegistrationTimes(callback); 81 | }; 82 | 83 | /** 84 | * Get all public settings. 85 | * @param {Function} callback [description] 86 | * @return {[type]} [description] 87 | */ 88 | SettingsController.getPublicSettings = function(callback){ 89 | Settings.getPublicSettings(callback); 90 | }; 91 | 92 | module.exports = SettingsController; -------------------------------------------------------------------------------- /app/server/emails/email-basic/html.pug: -------------------------------------------------------------------------------- 1 | doctype transitional 2 | html(xmlns='http://www.w3.org/1999/xhtml') 3 | head 4 | meta(http-equiv='Content-Type', content='text/html; charset=UTF-8') 5 | title *|MC:SUBJECT|* 6 | style 7 | include style.css 8 | body(leftmargin='0', marginwidth='0', topmargin='0', marginheight='0', offset='0') 9 | center 10 | table#bodyTable(align='center', border='0', cellpadding='0', cellspacing='0', height='100%', width='100%') 11 | tbody 12 | tr 13 | td#bodyCell(align='center', valign='top') 14 | // BEGIN TEMPLATE // 15 | table#templateContainer(border='0', cellpadding='0', cellspacing='0') 16 | tbody 17 | tr 18 | td(align='center', valign='top') 19 | // BEGIN HEADER // 20 | table#templateHeader(border='0', cellpadding='0', cellspacing='0', width='100%') 21 | tbody 22 | tr 23 | td.headerContent(valign='top') 24 | img#headerImage(src=emailHeaderImage, style='max-width:600px;', mc:label='header_image', mc:edit='header_image', mc:allowdesigner='', mc:allowtext='') 25 | // // END HEADER 26 | tr 27 | td(align='center', valign='top') 28 | // BEGIN BODY // 29 | table#templateBody(border='0', cellpadding='0', cellspacing='0', width='100%') 30 | tbody 31 | tr 32 | td.bodyContent(valign='top', style='text-align: center', mc:edit='body_content00') 33 | h1 #{title} 34 | h3 #{subtitle} 35 | | #{body} 36 | tr 37 | td.bodyContent(valign='top', style='text-align: center', mc:edit='body_content00') 38 | | Thanks, 39 | br 40 | | The #{hackathonName} Team 41 | // // END BODY 42 | tr 43 | td(align='center', valign='top') 44 | // BEGIN FOOTER // 45 | table#templateFooter(border='0', cellpadding='0', cellspacing='0', width='100%') 46 | tbody 47 | tr 48 | td.footerContent(valign='top', mc:edit='footer_content00') 49 | a(href='https://twitter.com/' + twitterHandle) Follow on Twitter 50 | a(href='https://facebook.com/' + facebookHandle) Like on Facebook 51 | a(href='mailto:' + emailAddress) Email Us 52 | tr 53 | td.footerContent(valign='top', style='padding-top:0;', mc:edit='footer_content01') 54 | em Copyright © #{hackathonName} 2017, All rights reserved. 55 | br 56 | // // END FOOTER 57 | // // END TEMPLATE 58 | -------------------------------------------------------------------------------- /app/server/emails/email-basic/text.pug: -------------------------------------------------------------------------------- 1 | | #{title} 2 | | 3 | | #{subtitle} 4 | | 5 | | #{body} 6 | | 7 | | Thanks, 8 | | HackMIT Team 9 | -------------------------------------------------------------------------------- /app/server/emails/email-link-action/html.pug: -------------------------------------------------------------------------------- 1 | doctype transitional 2 | html(xmlns='http://www.w3.org/1999/xhtml') 3 | head 4 | meta(http-equiv='Content-Type', content='text/html; charset=UTF-8') 5 | title *|MC:SUBJECT|* 6 | style 7 | include style.css 8 | body(leftmargin='0', marginwidth='0', topmargin='0', marginheight='0', offset='0') 9 | center 10 | table#bodyTable(align='center', border='0', cellpadding='0', cellspacing='0', height='100%', width='100%') 11 | tbody 12 | tr 13 | td#bodyCell(align='center', valign='top') 14 | // BEGIN TEMPLATE // 15 | table#templateContainer(border='0', cellpadding='0', cellspacing='0') 16 | tbody 17 | tr 18 | td(align='center', valign='top') 19 | // BEGIN HEADER // 20 | table#templateHeader(border='0', cellpadding='0', cellspacing='0', width='100%') 21 | tbody 22 | tr 23 | td.headerContent(valign='top') 24 | img#headerImage(src=emailHeaderImage, mc:label='header_image', mc:edit='header_image', mc:allowdesigner='', mc:allowtext='') 25 | // // END HEADER 26 | tr 27 | td(align='center', valign='top') 28 | // BEGIN BODY // 29 | table#templateBody(border='0', cellpadding='0', cellspacing='0', width='100%') 30 | tbody 31 | tr 32 | td.bodyContent(valign='top', style='text-align: center', mc:edit='body_content00') 33 | h1 #{title} 34 | h3 #{subtitle} 35 | | #{description} 36 | br 37 | br 38 | br 39 | a.pink.button(href=actionUrl) 40 | | #{actionName} 41 | tr 42 | td.bodyContent(valign='top', style='text-align: center', mc:edit='body_content00') 43 | | Thanks, 44 | br 45 | | The #{hackathonName} Team 46 | // // END BODY 47 | tr 48 | td(align='center', valign='top') 49 | // BEGIN FOOTER // 50 | table#templateFooter(border='0', cellpadding='0', cellspacing='0', width='100%') 51 | tbody 52 | tr 53 | td.footerContent(valign='top', mc:edit='footer_content00') 54 | a(href='https://twitter.com/' + twitterHandle) Follow on Twitter 55 | a(href='https://facebook.com/' + facebookHandle) Like on Facebook 56 | a(href='mailto:' + emailAddress) Email Us 57 | tr 58 | td.footerContent(valign='top', style='padding-top:0;', mc:edit='footer_content01') 59 | em Copyright © #{hackathonName} 2017, All rights reserved. 60 | br 61 | // // END FOOTER 62 | // // END TEMPLATE 63 | -------------------------------------------------------------------------------- /app/server/emails/email-link-action/text.pug: -------------------------------------------------------------------------------- 1 | | Hi! Thanks for signing up for HackMIT 2015. To verify your email, follow this link: #{verifyUrl} 2 | | 3 | | Thanks, 4 | | HackMIT Team 5 | -------------------------------------------------------------------------------- /app/server/emails/email-verify/html.pug: -------------------------------------------------------------------------------- 1 | doctype transitional 2 | html(xmlns='http://www.w3.org/1999/xhtml') 3 | head 4 | meta(http-equiv='Content-Type', content='text/html; charset=UTF-8') 5 | title *|MC:SUBJECT|* 6 | style 7 | include style.css 8 | body(leftmargin='0', marginwidth='0', topmargin='0', marginheight='0', offset='0') 9 | center 10 | table#bodyTable(align='center', border='0', cellpadding='0', cellspacing='0', height='100%', width='100%') 11 | tbody 12 | tr 13 | td#bodyCell(align='center', valign='top') 14 | // BEGIN TEMPLATE // 15 | table#templateContainer(border='0', cellpadding='0', cellspacing='0') 16 | tbody 17 | tr 18 | td(align='center', valign='top') 19 | // BEGIN HEADER // 20 | table#templateHeader(border='0', cellpadding='0', cellspacing='0', width='100%') 21 | tbody 22 | tr 23 | td.headerContent(valign='top') 24 | img#headerImage(src=emailHeaderImage, style='max-width:600px;', mc:label='header_image', mc:edit='header_image', mc:allowdesigner='', mc:allowtext='') 25 | // // END HEADER 26 | tr 27 | td(align='center', valign='top') 28 | // BEGIN BODY // 29 | table#templateBody(border='0', cellpadding='0', cellspacing='0', width='100%') 30 | tbody 31 | tr 32 | td.bodyContent(valign='top', style='text-align: center', mc:edit='body_content00') 33 | h1 Verify Your Email 34 | h3 Thanks for signing up for #{hackathonName}! 35 | | To verify your email, click the button below. 36 | br 37 | br 38 | br 39 | a.pink.button(href=verifyUrl) 40 | | Verify Me 41 | tr 42 | td.bodyContent(valign='top', style='text-align: center', mc:edit='body_content00') 43 | | Thanks, 44 | br 45 | | The #{hackathonName} Team 46 | // // END BODY 47 | tr 48 | td(align='center', valign='top') 49 | // BEGIN FOOTER // 50 | table#templateFooter(border='0', cellpadding='0', cellspacing='0', width='100%') 51 | tbody 52 | tr 53 | td.footerContent(valign='top', mc:edit='footer_content00') 54 | a(href='https://twitter.com/' + twitterHandle) Follow on Twitter 55 | a(href='https://facebook.com/' + facebookHandle) Like on Facebook 56 | a(href='mailto:#{emailAddress}') Email Us 57 | tr 58 | td.footerContent(valign='top', style='padding-top:0;', mc:edit='footer_content01') 59 | em Copyright © #{hackathonName} 2017, All rights reserved. 60 | br 61 | // // END FOOTER 62 | // // END TEMPLATE 63 | -------------------------------------------------------------------------------- /app/server/emails/email-verify/text.pug: -------------------------------------------------------------------------------- 1 | | Hi! Thanks for signing up for HackMIT 2015. To verify your email, follow this link: #{verifyUrl} 2 | | 3 | | Thanks, 4 | | HackMIT Team 5 | -------------------------------------------------------------------------------- /app/server/models/Settings.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | /** 4 | * Settings Schema! 5 | * 6 | * Fields with select: false are not public. 7 | * These can be retrieved in controller methods. 8 | * 9 | * @type {mongoose} 10 | */ 11 | var schema = new mongoose.Schema({ 12 | status: String, 13 | timeOpen: { 14 | type: Number, 15 | default: 0 16 | }, 17 | timeClose: { 18 | type: Number, 19 | default: Date.now() + 31104000000 // Add a year from now. 20 | }, 21 | timeConfirm: { 22 | type: Number, 23 | default: 604800000 // Date of confirmation 24 | }, 25 | whitelistedEmails: { 26 | type: [String], 27 | select: false, 28 | default: ['.edu'], 29 | }, 30 | waitlistText: { 31 | type: String 32 | }, 33 | acceptanceText: { 34 | type: String, 35 | }, 36 | confirmationText: { 37 | type: String 38 | }, 39 | allowMinors: { 40 | type: Boolean 41 | } 42 | }); 43 | 44 | /** 45 | * Get the list of whitelisted emails. 46 | * Whitelist emails are by default not included in settings. 47 | * @param {Function} callback args(err, emails) 48 | */ 49 | schema.statics.getWhitelistedEmails = function(callback){ 50 | this 51 | .findOne({}) 52 | .select('whitelistedEmails') 53 | .exec(function(err, settings){ 54 | return callback(err, settings.whitelistedEmails); 55 | }); 56 | }; 57 | 58 | /** 59 | * Get the open and close time for registration. 60 | * @param {Function} callback args(err, times : {timeOpen, timeClose, timeConfirm}) 61 | */ 62 | schema.statics.getRegistrationTimes = function(callback){ 63 | this 64 | .findOne({}) 65 | .select('timeOpen timeClose timeConfirm') 66 | .exec(function(err, settings){ 67 | callback(err, { 68 | timeOpen: settings.timeOpen, 69 | timeClose: settings.timeClose, 70 | timeConfirm: settings.timeConfirm 71 | }); 72 | }); 73 | }; 74 | 75 | schema.statics.getPublicSettings = function(callback){ 76 | this 77 | .findOne({}) 78 | .exec(callback); 79 | }; 80 | 81 | module.exports = mongoose.model('Settings', schema); 82 | -------------------------------------------------------------------------------- /app/server/models/User.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'), 2 | bcrypt = require('bcrypt'), 3 | validator = require('validator'), 4 | jwt = require('jsonwebtoken'); 5 | JWT_SECRET = process.env.JWT_SECRET; 6 | 7 | var profile = { 8 | 9 | // Basic info 10 | name: { 11 | type: String, 12 | min: 1, 13 | max: 100, 14 | }, 15 | 16 | adult: { 17 | type: Boolean, 18 | required: true, 19 | default: false, 20 | }, 21 | 22 | school: { 23 | type: String, 24 | min: 1, 25 | max: 150, 26 | }, 27 | 28 | graduationYear: { 29 | type: String, 30 | enum: { 31 | values: '2016 2017 2018 2019'.split(' '), 32 | } 33 | }, 34 | 35 | description: { 36 | type: String, 37 | min: 0, 38 | max: 300 39 | }, 40 | 41 | essay: { 42 | type: String, 43 | min: 0, 44 | max: 1500 45 | }, 46 | 47 | // Optional info for demographics 48 | gender: { 49 | type: String, 50 | enum : { 51 | values: 'M F O N'.split(' ') 52 | } 53 | }, 54 | 55 | }; 56 | 57 | // Only after confirmed 58 | var confirmation = { 59 | phoneNumber: String, 60 | dietaryRestrictions: [String], 61 | shirtSize: { 62 | type: String, 63 | enum: { 64 | values: 'XS S M L XL XXL WXS WS WM WL WXL WXXL'.split(' ') 65 | } 66 | }, 67 | wantsHardware: Boolean, 68 | hardware: String, 69 | 70 | major: String, 71 | github: String, 72 | twitter: String, 73 | website: String, 74 | resume: String, 75 | 76 | needsReimbursement: Boolean, 77 | address: { 78 | name: String, 79 | line1: String, 80 | line2: String, 81 | city: String, 82 | state: String, 83 | zip: String, 84 | country: String 85 | }, 86 | receipt: String, 87 | 88 | hostNeededFri: Boolean, 89 | hostNeededSat: Boolean, 90 | genderNeutral: Boolean, 91 | catFriendly: Boolean, 92 | smokingFriendly: Boolean, 93 | hostNotes: String, 94 | 95 | notes: String, 96 | 97 | signatureLiability: String, 98 | signaturePhotoRelease: String, 99 | signatureCodeOfConduct: String, 100 | }; 101 | 102 | var status = { 103 | /** 104 | * Whether or not the user's profile has been completed. 105 | * @type {Object} 106 | */ 107 | completedProfile: { 108 | type: Boolean, 109 | required: true, 110 | default: false, 111 | }, 112 | admitted: { 113 | type: Boolean, 114 | required: true, 115 | default: false, 116 | }, 117 | admittedBy: { 118 | type: String, 119 | validate: [ 120 | validator.isEmail, 121 | 'Invalid Email', 122 | ], 123 | select: false 124 | }, 125 | confirmed: { 126 | type: Boolean, 127 | required: true, 128 | default: false, 129 | }, 130 | declined: { 131 | type: Boolean, 132 | required: true, 133 | default: false, 134 | }, 135 | checkedIn: { 136 | type: Boolean, 137 | required: true, 138 | default: false, 139 | }, 140 | checkInTime: { 141 | type: Number, 142 | }, 143 | confirmBy: { 144 | type: Number 145 | }, 146 | reimbursementGiven: { 147 | type: Boolean, 148 | default: false 149 | } 150 | }; 151 | 152 | // define the schema for our admin model 153 | var schema = new mongoose.Schema({ 154 | 155 | email: { 156 | type: String, 157 | required: true, 158 | unique: true, 159 | validate: [ 160 | validator.isEmail, 161 | 'Invalid Email', 162 | ] 163 | }, 164 | 165 | password: { 166 | type: String, 167 | required: true, 168 | select: false 169 | }, 170 | 171 | admin: { 172 | type: Boolean, 173 | required: true, 174 | default: false, 175 | }, 176 | 177 | timestamp: { 178 | type: Number, 179 | required: true, 180 | default: Date.now(), 181 | }, 182 | 183 | lastUpdated: { 184 | type: Number, 185 | default: Date.now(), 186 | }, 187 | 188 | teamCode: { 189 | type: String, 190 | min: 0, 191 | max: 140, 192 | }, 193 | 194 | verified: { 195 | type: Boolean, 196 | required: true, 197 | default: false 198 | }, 199 | 200 | salt: { 201 | type: Number, 202 | required: true, 203 | default: Date.now(), 204 | select: false 205 | }, 206 | 207 | /** 208 | * User Profile. 209 | * 210 | * This is the only part of the user that the user can edit. 211 | * 212 | * Profile validation will exist here. 213 | */ 214 | profile: profile, 215 | 216 | /** 217 | * Confirmation information 218 | * 219 | * Extension of the user model, but can only be edited after acceptance. 220 | */ 221 | confirmation: confirmation, 222 | 223 | status: status, 224 | 225 | }); 226 | 227 | schema.set('toJSON', { 228 | virtuals: true 229 | }); 230 | 231 | schema.set('toObject', { 232 | virtuals: true 233 | }); 234 | 235 | //========================================= 236 | // Instance Methods 237 | //========================================= 238 | 239 | // checking if this password matches 240 | schema.methods.checkPassword = function(password) { 241 | return bcrypt.compareSync(password, this.password); 242 | }; 243 | 244 | // Token stuff 245 | schema.methods.generateEmailVerificationToken = function(){ 246 | return jwt.sign(this.email, JWT_SECRET); 247 | }; 248 | 249 | schema.methods.generateAuthToken = function(){ 250 | return jwt.sign(this._id, JWT_SECRET); 251 | }; 252 | 253 | /** 254 | * Generate a temporary authentication token (for changing passwords) 255 | * @return JWT 256 | * payload: { 257 | * id: userId 258 | * iat: issued at ms 259 | * exp: expiration ms 260 | * } 261 | */ 262 | schema.methods.generateTempAuthToken = function(){ 263 | return jwt.sign({ 264 | id: this._id 265 | }, JWT_SECRET, { 266 | expiresInMinutes: 60, 267 | }); 268 | }; 269 | 270 | //========================================= 271 | // Static Methods 272 | //========================================= 273 | 274 | schema.statics.generateHash = function(password) { 275 | return bcrypt.hashSync(password, bcrypt.genSaltSync(8)); 276 | }; 277 | 278 | /** 279 | * Verify an an email verification token. 280 | * @param {[type]} token token 281 | * @param {Function} cb args(err, email) 282 | */ 283 | schema.statics.verifyEmailVerificationToken = function(token, callback){ 284 | jwt.verify(token, JWT_SECRET, function(err, email) { 285 | return callback(err, email); 286 | }); 287 | }; 288 | 289 | /** 290 | * Verify a temporary authentication token. 291 | * @param {[type]} token temporary auth token 292 | * @param {Function} callback args(err, id) 293 | */ 294 | schema.statics.verifyTempAuthToken = function(token, callback){ 295 | jwt.verify(token, JWT_SECRET, function(err, payload){ 296 | 297 | if (err || !payload){ 298 | return callback(err); 299 | } 300 | 301 | if (!payload.exp || Date.now() >= payload.exp * 1000){ 302 | return callback({ 303 | message: 'Token has expired.' 304 | }); 305 | } 306 | 307 | return callback(null, payload.id); 308 | }); 309 | }; 310 | 311 | schema.statics.findOneByEmail = function(email){ 312 | return this.findOne({ 313 | email: email.toLowerCase() 314 | }); 315 | }; 316 | 317 | /** 318 | * Get a single user using a signed token. 319 | * @param {String} token User's authentication token. 320 | * @param {Function} callback args(err, user) 321 | */ 322 | schema.statics.getByToken = function(token, callback){ 323 | jwt.verify(token, JWT_SECRET, function(err, id){ 324 | if (err) { 325 | return callback(err); 326 | } 327 | this.findOne({_id: id}, callback); 328 | }.bind(this)); 329 | }; 330 | 331 | schema.statics.validateProfile = function(profile, cb){ 332 | return cb(!( 333 | profile.name.length > 0 && 334 | profile.adult && 335 | profile.school.length > 0 && 336 | ['2016', '2017', '2018', '2019'].indexOf(profile.graduationYear) > -1 && 337 | ['M', 'F', 'O', 'N'].indexOf(profile.gender) > -1 338 | )); 339 | }; 340 | 341 | //========================================= 342 | // Virtuals 343 | //========================================= 344 | 345 | /** 346 | * Has the user completed their profile? 347 | * This provides a verbose explanation of their furthest state. 348 | */ 349 | schema.virtual('status.name').get(function(){ 350 | 351 | if (this.status.checkedIn) { 352 | return 'checked in'; 353 | } 354 | 355 | if (this.status.declined) { 356 | return "declined"; 357 | } 358 | 359 | if (this.status.confirmed) { 360 | return "confirmed"; 361 | } 362 | 363 | if (this.status.admitted) { 364 | return "admitted"; 365 | } 366 | 367 | if (this.status.completedProfile){ 368 | return "submitted"; 369 | } 370 | 371 | if (!this.verified){ 372 | return "unverified"; 373 | } 374 | 375 | return "incomplete"; 376 | 377 | }); 378 | 379 | module.exports = mongoose.model('User', schema); 380 | -------------------------------------------------------------------------------- /app/server/routes.js: -------------------------------------------------------------------------------- 1 | var User = require('./models/User'); 2 | 3 | module.exports = function(app) { 4 | 5 | // Application ------------------------------------------ 6 | app.get('/', function(req, res){ 7 | res.sendfile('./app/client/index.html'); 8 | }); 9 | 10 | // Wildcard all other GET requests to the angular app 11 | app.get('*', function(req, res){ 12 | res.sendfile('./app/client/index.html'); 13 | }); 14 | 15 | }; 16 | -------------------------------------------------------------------------------- /app/server/routes/auth.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | var SettingsController = require('../controllers/SettingsController'); 4 | var UserController = require('../controllers/UserController'); 5 | 6 | module.exports = function(router){ 7 | 8 | // --------------------------------------------- 9 | // AUTHENTICATION 10 | // --------------------------------------------- 11 | 12 | /** 13 | * Login a user with a username (email) and password. 14 | * Find em', check em'. 15 | * Pass them an authentication token on success. 16 | * Otherwise, 401. You fucked up. 17 | * 18 | * body { 19 | * email: email, 20 | * password: password 21 | * token: ? 22 | * } 23 | * 24 | */ 25 | router.post('/login', 26 | function(req, res, next){ 27 | var email = req.body.email; 28 | var password = req.body.password; 29 | var token = req.body.token; 30 | 31 | if (token) { 32 | UserController.loginWithToken(token, 33 | function(err, token, user){ 34 | if (err || !user) { 35 | return res.status(400).send(err); 36 | } 37 | return res.json({ 38 | token: token, 39 | user: user 40 | }); 41 | }); 42 | } else { 43 | UserController.loginWithPassword(email, password, 44 | function(err, token, user){ 45 | if (err || !user) { 46 | return res.status(400).send(err); 47 | } 48 | return res.json({ 49 | token: token, 50 | user: user 51 | }); 52 | }); 53 | } 54 | 55 | }); 56 | 57 | /** 58 | * Register a user with a username (email) and password. 59 | * If it already exists, then don't register, duh. 60 | * 61 | * body { 62 | * email: email, 63 | * password: password 64 | * } 65 | * 66 | */ 67 | router.post('/register', 68 | function(req, res, next){ 69 | // Register with an email and password 70 | var email = req.body.email; 71 | var password = req.body.password; 72 | 73 | UserController.createUser(email, password, 74 | function(err, user){ 75 | if (err){ 76 | return res.status(400).send(err); 77 | } 78 | return res.json(user); 79 | }); 80 | }); 81 | 82 | router.post('/reset', 83 | function(req, res, next){ 84 | var email = req.body.email; 85 | if (!email){ 86 | return res.status(400).send(); 87 | } 88 | 89 | UserController.sendPasswordResetEmail(email, function(err){ 90 | if(err){ 91 | return res.status(400).send(err); 92 | } 93 | return res.json({ 94 | message: 'Email Sent' 95 | }); 96 | }); 97 | }); 98 | 99 | /** 100 | * Reset user's password. 101 | * { 102 | * token: STRING 103 | * password: STRING, 104 | * } 105 | */ 106 | router.post('/reset/password', function(req, res){ 107 | var pass = req.body.password; 108 | var token = req.body.token; 109 | 110 | UserController.resetPassword(token, pass, function(err, user){ 111 | if (err || !user){ 112 | return res.status(400).send(err); 113 | } 114 | return res.json(user); 115 | }); 116 | }); 117 | 118 | /** 119 | * Resend a password verification email for this user. 120 | * 121 | * body { 122 | * id: user id 123 | * } 124 | */ 125 | router.post('/verify/resend', 126 | function(req, res, next){ 127 | var id = req.body.id; 128 | if (id){ 129 | UserController.sendVerificationEmailById(id, function(err, user){ 130 | if (err || !user){ 131 | return res.status(400).send(); 132 | } 133 | return res.status(200).send(); 134 | }); 135 | } else { 136 | return res.status(400).send(); 137 | } 138 | }); 139 | 140 | /** 141 | * Verify a user with a given token. 142 | */ 143 | router.get('/verify/:token', 144 | function(req, res, next){ 145 | var token = req.params.token; 146 | UserController.verifyByToken(token, function(err, user){ 147 | 148 | if (err || !user){ 149 | return res.status(400).send(err); 150 | } 151 | 152 | return res.json(user); 153 | 154 | }); 155 | }); 156 | 157 | }; 158 | -------------------------------------------------------------------------------- /app/server/services/email.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var nodemailer = require('nodemailer'); 3 | var smtpTransport = require('nodemailer-smtp-transport'); 4 | 5 | var templatesDir = path.join(__dirname, '../templates'); 6 | var Email = require('email-templates'); 7 | 8 | var ROOT_URL = process.env.ROOT_URL; 9 | 10 | var HACKATHON_NAME = process.env.HACKATHON_NAME; 11 | var EMAIL_ADDRESS = process.env.EMAIL_ADDRESS; 12 | var TWITTER_HANDLE = process.env.TWITTER_HANDLE; 13 | var FACEBOOK_HANDLE = process.env.FACEBOOK_HANDLE; 14 | 15 | var EMAIL_HOST = process.env.EMAIL_HOST; 16 | var EMAIL_USER = process.env.EMAIL_USER; 17 | var EMAIL_PASS = process.env.EMAIL_PASS; 18 | var EMAIL_PORT = process.env.EMAIL_PORT; 19 | var EMAIL_CONTACT = process.env.EMAIL_CONTACT; 20 | var EMAIL_HEADER_IMAGE = process.env.EMAIL_HEADER_IMAGE; 21 | if(EMAIL_HEADER_IMAGE.indexOf("https") == -1){ 22 | EMAIL_HEADER_IMAGE = ROOT_URL + EMAIL_HEADER_IMAGE; 23 | } 24 | 25 | var NODE_ENV = process.env.NODE_ENV; 26 | 27 | var options = { 28 | host: EMAIL_HOST, 29 | port: EMAIL_PORT, 30 | secure: true, 31 | auth: { 32 | user: EMAIL_USER, 33 | pass: EMAIL_PASS 34 | } 35 | }; 36 | 37 | var transporter = nodemailer.createTransport(smtpTransport(options)); 38 | 39 | var controller = {}; 40 | 41 | controller.transporter = transporter; 42 | 43 | function sendOne(templateName, options, data, callback) { 44 | if (NODE_ENV === "dev") { 45 | console.log(templateName); 46 | console.log(JSON.stringify(data, "", 2)); 47 | } 48 | 49 | const email = new Email({ 50 | message: { 51 | from: EMAIL_ADDRESS 52 | }, 53 | send: true, 54 | transport: transporter 55 | }); 56 | 57 | data.emailHeaderImage = EMAIL_HEADER_IMAGE; 58 | data.emailAddress = EMAIL_ADDRESS; 59 | data.hackathonName = HACKATHON_NAME; 60 | data.twitterHandle = TWITTER_HANDLE; 61 | data.facebookHandle = FACEBOOK_HANDLE; 62 | 63 | email.send({ 64 | locals: data, 65 | message: { 66 | subject: options.subject, 67 | to: options.to 68 | }, 69 | template: path.join(__dirname, "..", "emails", templateName), 70 | }).then(res => { 71 | if (callback) { 72 | callback(undefined, res); 73 | } 74 | }).catch(err => { 75 | if (callback) { 76 | callback(err, undefined); 77 | } 78 | }); 79 | } 80 | 81 | /** 82 | * Send a verification email to a user, with a verification token to enter. 83 | * @param {[type]} email [description] 84 | * @param {[type]} token [description] 85 | * @param {Function} callback [description] 86 | * @return {[type]} [description] 87 | */ 88 | controller.sendVerificationEmail = function(email, token, callback) { 89 | 90 | var options = { 91 | to: email, 92 | subject: "["+HACKATHON_NAME+"] - Verify your email" 93 | }; 94 | 95 | var locals = { 96 | verifyUrl: ROOT_URL + '/verify/' + token 97 | }; 98 | 99 | /** 100 | * Eamil-verify takes a few template values: 101 | * { 102 | * verifyUrl: the url that the user must visit to verify their account 103 | * } 104 | */ 105 | sendOne('email-verify', options, locals, function(err, info){ 106 | if (err){ 107 | console.log(err); 108 | } 109 | if (info){ 110 | console.log(info.message); 111 | } 112 | if (callback){ 113 | callback(err, info); 114 | } 115 | }); 116 | 117 | }; 118 | 119 | /** 120 | * Send a password recovery email. 121 | * @param {[type]} email [description] 122 | * @param {[type]} token [description] 123 | * @param {Function} callback [description] 124 | */ 125 | controller.sendPasswordResetEmail = function(email, token, callback) { 126 | 127 | var options = { 128 | to: email, 129 | subject: "["+HACKATHON_NAME+"] - Password reset requested!" 130 | }; 131 | 132 | var locals = { 133 | title: 'Password Reset Request', 134 | subtitle: '', 135 | description: 'Somebody (hopefully you!) has requested that your password be reset. If ' + 136 | 'this was not you, feel free to disregard this email. This link will expire in one hour.', 137 | actionUrl: ROOT_URL + '/reset/' + token, 138 | actionName: "Reset Password" 139 | }; 140 | 141 | /** 142 | * Eamil-verify takes a few template values: 143 | * { 144 | * verifyUrl: the url that the user must visit to verify their account 145 | * } 146 | */ 147 | sendOne('email-link-action', options, locals, function(err, info){ 148 | if (err){ 149 | console.log(err); 150 | } 151 | if (info){ 152 | console.log(info.message); 153 | } 154 | if (callback){ 155 | callback(err, info); 156 | } 157 | }); 158 | 159 | }; 160 | 161 | /** 162 | * Send a password recovery email. 163 | * @param {[type]} email [description] 164 | * @param {Function} callback [description] 165 | */ 166 | controller.sendPasswordChangedEmail = function(email, callback){ 167 | 168 | var options = { 169 | to: email, 170 | subject: "["+HACKATHON_NAME+"] - Your password has been changed!" 171 | }; 172 | 173 | var locals = { 174 | title: 'Password Updated', 175 | body: 'Somebody (hopefully you!) has successfully changed your password.', 176 | }; 177 | 178 | /** 179 | * Eamil-verify takes a few template values: 180 | * { 181 | * verifyUrl: the url that the user must visit to verify their account 182 | * } 183 | */ 184 | sendOne('email-basic', options, locals, function(err, info){ 185 | if (err){ 186 | console.log(err); 187 | } 188 | if (info){ 189 | console.log(info.message); 190 | } 191 | if (callback){ 192 | callback(err, info); 193 | } 194 | }); 195 | 196 | }; 197 | 198 | module.exports = controller; 199 | -------------------------------------------------------------------------------- /app/server/services/stats.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var async = require('async'); 3 | var User = require('../models/User'); 4 | 5 | // In memory stats. 6 | var stats = {}; 7 | function calculateStats(){ 8 | console.log('Calculating stats...'); 9 | var newStats = { 10 | lastUpdated: 0, 11 | 12 | total: 0, 13 | demo: { 14 | gender: { 15 | M: 0, 16 | F: 0, 17 | O: 0, 18 | N: 0 19 | }, 20 | schools: {}, 21 | year: { 22 | '2016': 0, 23 | '2017': 0, 24 | '2018': 0, 25 | '2019': 0, 26 | } 27 | }, 28 | 29 | teams: {}, 30 | verified: 0, 31 | submitted: 0, 32 | admitted: 0, 33 | confirmed: 0, 34 | confirmedMit: 0, 35 | declined: 0, 36 | 37 | confirmedFemale: 0, 38 | confirmedMale: 0, 39 | confirmedOther: 0, 40 | confirmedNone: 0, 41 | 42 | shirtSizes: { 43 | 'XS': 0, 44 | 'S': 0, 45 | 'M': 0, 46 | 'L': 0, 47 | 'XL': 0, 48 | 'XXL': 0, 49 | 'WXS': 0, 50 | 'WS': 0, 51 | 'WM': 0, 52 | 'WL': 0, 53 | 'WXL': 0, 54 | 'WXXL': 0, 55 | 'None': 0 56 | }, 57 | 58 | dietaryRestrictions: {}, 59 | 60 | hostNeededFri: 0, 61 | hostNeededSat: 0, 62 | hostNeededUnique: 0, 63 | 64 | hostNeededFemale: 0, 65 | hostNeededMale: 0, 66 | hostNeededOther: 0, 67 | hostNeededNone: 0, 68 | 69 | reimbursementTotal: 0, 70 | reimbursementMissing: 0, 71 | 72 | wantsHardware: 0, 73 | 74 | checkedIn: 0 75 | }; 76 | 77 | User 78 | .find({}) 79 | .exec(function(err, users){ 80 | if (err || !users){ 81 | throw err; 82 | } 83 | 84 | newStats.total = users.length; 85 | 86 | async.each(users, function(user, callback){ 87 | 88 | // Grab the email extension 89 | var email = user.email.split('@')[1]; 90 | 91 | // Add to the gender 92 | newStats.demo.gender[user.profile.gender] += 1; 93 | 94 | // Count verified 95 | newStats.verified += user.verified ? 1 : 0; 96 | 97 | // Count submitted 98 | newStats.submitted += user.status.completedProfile ? 1 : 0; 99 | 100 | // Count accepted 101 | newStats.admitted += user.status.admitted ? 1 : 0; 102 | 103 | // Count confirmed 104 | newStats.confirmed += user.status.confirmed ? 1 : 0; 105 | 106 | // Count confirmed that are mit 107 | newStats.confirmedMit += user.status.confirmed && email === "mit.edu" ? 1 : 0; 108 | 109 | newStats.confirmedFemale += user.status.confirmed && user.profile.gender == "F" ? 1 : 0; 110 | newStats.confirmedMale += user.status.confirmed && user.profile.gender == "M" ? 1 : 0; 111 | newStats.confirmedOther += user.status.confirmed && user.profile.gender == "O" ? 1 : 0; 112 | newStats.confirmedNone += user.status.confirmed && user.profile.gender == "N" ? 1 : 0; 113 | 114 | // Count declined 115 | newStats.declined += user.status.declined ? 1 : 0; 116 | 117 | // Count the number of people who need reimbursements 118 | newStats.reimbursementTotal += user.confirmation.needsReimbursement ? 1 : 0; 119 | 120 | // Count the number of people who still need to be reimbursed 121 | newStats.reimbursementMissing += user.confirmation.needsReimbursement && 122 | !user.status.reimbursementGiven ? 1 : 0; 123 | 124 | // Count the number of people who want hardware 125 | newStats.wantsHardware += user.confirmation.wantsHardware ? 1 : 0; 126 | 127 | // Count schools 128 | if (!newStats.demo.schools[email]){ 129 | newStats.demo.schools[email] = { 130 | submitted: 0, 131 | admitted: 0, 132 | confirmed: 0, 133 | declined: 0, 134 | }; 135 | } 136 | newStats.demo.schools[email].submitted += user.status.completedProfile ? 1 : 0; 137 | newStats.demo.schools[email].admitted += user.status.admitted ? 1 : 0; 138 | newStats.demo.schools[email].confirmed += user.status.confirmed ? 1 : 0; 139 | newStats.demo.schools[email].declined += user.status.declined ? 1 : 0; 140 | 141 | // Count graduation years 142 | if (user.profile.graduationYear){ 143 | newStats.demo.year[user.profile.graduationYear] += 1; 144 | } 145 | 146 | // Grab the team name if there is one 147 | // if (user.teamCode && user.teamCode.length > 0){ 148 | // if (!newStats.teams[user.teamCode]){ 149 | // newStats.teams[user.teamCode] = []; 150 | // } 151 | // newStats.teams[user.teamCode].push(user.profile.name); 152 | // } 153 | 154 | // Count shirt sizes 155 | if (user.confirmation.shirtSize in newStats.shirtSizes){ 156 | newStats.shirtSizes[user.confirmation.shirtSize] += 1; 157 | } 158 | 159 | // Host needed counts 160 | newStats.hostNeededFri += user.confirmation.hostNeededFri ? 1 : 0; 161 | newStats.hostNeededSat += user.confirmation.hostNeededSat ? 1 : 0; 162 | newStats.hostNeededUnique += user.confirmation.hostNeededFri || user.confirmation.hostNeededSat ? 1 : 0; 163 | 164 | newStats.hostNeededFemale 165 | += (user.confirmation.hostNeededFri || user.confirmation.hostNeededSat) && user.profile.gender == "F" ? 1 : 0; 166 | newStats.hostNeededMale 167 | += (user.confirmation.hostNeededFri || user.confirmation.hostNeededSat) && user.profile.gender == "M" ? 1 : 0; 168 | newStats.hostNeededOther 169 | += (user.confirmation.hostNeededFri || user.confirmation.hostNeededSat) && user.profile.gender == "O" ? 1 : 0; 170 | newStats.hostNeededNone 171 | += (user.confirmation.hostNeededFri || user.confirmation.hostNeededSat) && user.profile.gender == "N" ? 1 : 0; 172 | 173 | // Dietary restrictions 174 | if (user.confirmation.dietaryRestrictions){ 175 | user.confirmation.dietaryRestrictions.forEach(function(restriction){ 176 | if (!newStats.dietaryRestrictions[restriction]){ 177 | newStats.dietaryRestrictions[restriction] = 0; 178 | } 179 | newStats.dietaryRestrictions[restriction] += 1; 180 | }); 181 | } 182 | 183 | // Count checked in 184 | newStats.checkedIn += user.status.checkedIn ? 1 : 0; 185 | 186 | callback(); // let async know we've finished 187 | }, function() { 188 | // Transform dietary restrictions into a series of objects 189 | var restrictions = []; 190 | _.keys(newStats.dietaryRestrictions) 191 | .forEach(function(key){ 192 | restrictions.push({ 193 | name: key, 194 | count: newStats.dietaryRestrictions[key], 195 | }); 196 | }); 197 | newStats.dietaryRestrictions = restrictions; 198 | 199 | // Transform schools into an array of objects 200 | var schools = []; 201 | _.keys(newStats.demo.schools) 202 | .forEach(function(key){ 203 | schools.push({ 204 | email: key, 205 | count: newStats.demo.schools[key].submitted, 206 | stats: newStats.demo.schools[key] 207 | }); 208 | }); 209 | newStats.demo.schools = schools; 210 | 211 | // Likewise, transform the teams into an array of objects 212 | // var teams = []; 213 | // _.keys(newStats.teams) 214 | // .forEach(function(key){ 215 | // teams.push({ 216 | // name: key, 217 | // users: newStats.teams[key] 218 | // }); 219 | // }); 220 | // newStats.teams = teams; 221 | 222 | console.log('Stats updated!'); 223 | newStats.lastUpdated = new Date(); 224 | stats = newStats; 225 | }); 226 | }); 227 | 228 | } 229 | 230 | // Calculate once every five minutes. 231 | calculateStats(); 232 | setInterval(calculateStats, 300000); 233 | 234 | var Stats = {}; 235 | 236 | Stats.getUserStats = function(){ 237 | return stats; 238 | }; 239 | 240 | module.exports = Stats; 241 | -------------------------------------------------------------------------------- /config/admin.js: -------------------------------------------------------------------------------- 1 | ADMIN_EMAIL = process.env.ADMIN_EMAIL; 2 | ADMIN_PASSWORD = process.env.ADMIN_PASS; 3 | 4 | // Create a default admin user. 5 | var User = require('../app/server/models/User'); 6 | 7 | // If there is already a user 8 | User 9 | .findOne({ 10 | email: ADMIN_EMAIL 11 | }) 12 | .exec(function(err, user){ 13 | if (!user){ 14 | var u = new User(); 15 | u.email = ADMIN_EMAIL; 16 | u.password = User.generateHash(ADMIN_PASSWORD); 17 | u.admin = true; 18 | u.verified = true; 19 | u.save(function(err){ 20 | if (err){ 21 | console.log(err); 22 | } 23 | }); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /config/settings.js: -------------------------------------------------------------------------------- 1 | var Settings = require('../app/server/models/Settings'); 2 | 3 | Settings 4 | .findOne({}) 5 | .exec(function(err, settings){ 6 | if (!settings){ 7 | var settings = new Settings(); 8 | settings.save(); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /docs/images/screenshots/admin-users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techx/quill/3227d1f60409eb02d682e14dd63197cd3073f63a/docs/images/screenshots/admin-users.png -------------------------------------------------------------------------------- /docs/images/screenshots/application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techx/quill/3227d1f60409eb02d682e14dd63197cd3073f63a/docs/images/screenshots/application.png -------------------------------------------------------------------------------- /docs/images/screenshots/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techx/quill/3227d1f60409eb02d682e14dd63197cd3073f63a/docs/images/screenshots/dashboard.png -------------------------------------------------------------------------------- /docs/images/screenshots/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techx/quill/3227d1f60409eb02d682e14dd63197cd3073f63a/docs/images/screenshots/login.png -------------------------------------------------------------------------------- /docs/images/screenshots/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techx/quill/3227d1f60409eb02d682e14dd63197cd3073f63a/docs/images/screenshots/settings.png -------------------------------------------------------------------------------- /docs/images/screenshots/stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techx/quill/3227d1f60409eb02d682e14dd63197cd3073f63a/docs/images/screenshots/stats.png -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | var gulp = require('gulp'); 4 | var sass = require('gulp-sass'); 5 | var browserify = require('browserify'); 6 | var browserifyNgAnnotate = require('browserify-ngannotate'); 7 | var buffer = require('gulp-buffer'); 8 | var cleanCss = require('gulp-clean-css'); 9 | var concat = require('gulp-concat'); 10 | var source = require('vinyl-source-stream'); 11 | var sourcemaps = require('gulp-sourcemaps'); 12 | var uglify = require('gulp-uglify'); 13 | var ngAnnotate = require('gulp-ng-annotate'); 14 | 15 | var environment = process.env.NODE_ENV; 16 | 17 | var nodemon = require('gulp-nodemon'); 18 | 19 | function swallowError (error) { 20 | //If you want details of the error in the console 21 | console.log(error.toString()); 22 | this.emit('end'); 23 | } 24 | 25 | gulp.task('default', function(){ 26 | console.log('yo. use gulp watch or something'); 27 | }); 28 | 29 | gulp.task('js', function () { 30 | var b = browserify({ 31 | entries: 'app/client/src/app.js', 32 | debug: environment === "dev", 33 | transform: [browserifyNgAnnotate] 34 | }); 35 | 36 | // transform streaming contents into buffer contents (because gulp-sourcemaps does not support streaming contents) 37 | b.bundle() 38 | .pipe(source('app.js')) 39 | .pipe(buffer()) 40 | .pipe(sourcemaps.init({loadMaps: true})) 41 | .pipe(ngAnnotate()) 42 | .on('error', swallowError) 43 | .pipe(sourcemaps.write()) 44 | .pipe(gulp.dest('app/client/build')); 45 | }); 46 | 47 | gulp.task('sass', function() { 48 | gulp.src('app/client/stylesheets/site.scss') 49 | .pipe(sass()) 50 | .on('error', sass.logError) 51 | .pipe(cleanCss()) 52 | .pipe(gulp.dest('app/client/build')); 53 | }); 54 | 55 | gulp.task('build', ['js', 'sass'], function() { 56 | // Yup, build the js and sass. 57 | }); 58 | 59 | gulp.task('watch', ['js', 'sass'], function() { 60 | gulp.watch('app/client/src/**/*.js', ['js']); 61 | gulp.watch('app/client/views/**/*.js', ['js']); 62 | gulp.watch('app/client/stylesheets/**/*.scss', ['sass']); 63 | }); 64 | 65 | gulp.task('server', ['watch'], function() { 66 | nodemon({ 67 | script: 'app.js', 68 | env: { 'NODE_ENV': process.env.NODE_ENV || 'DEV' }, 69 | watch: [ 70 | 'app/server' 71 | ] 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quill", 3 | "description": "mean stack registration system", 4 | "version": "1.0.0", 5 | "private": "true", 6 | "dependencies": { 7 | "acorn": "^7.3.1", 8 | "angular": "^1.8.0", 9 | "angular-ui-router": "^1.0.26", 10 | "async": "^2.6.3", 11 | "bcrypt": "^3.0.8", 12 | "body-parser": "^1.19.0", 13 | "browserify": "^16.5.1", 14 | "browserify-ngannotate": "^2.0.0", 15 | "csv-express": "^1.2.2", 16 | "dotenv": "^6.2.0", 17 | "email-templates": "^5.1.0", 18 | "express": "^4.17.1", 19 | "gulp-buffer": "0.0.2", 20 | "gulp-tap": "^1.0.1", 21 | "jquery": "^3.5.1", 22 | "json2csv": "^4.5.4", 23 | "jsonwebtoken": "5.0.4", 24 | "method-override": "^3.0.0", 25 | "minimist": "^1.2.5", 26 | "moment": "^2.27.0", 27 | "mongoose": "^5.9.19", 28 | "morgan": "^1.10.0", 29 | "nodemailer": "^4.7.0", 30 | "nodemailer-smtp-transport": "^2.7.4", 31 | "request": "^2.88.2", 32 | "showdown": "^1.9.1", 33 | "sweetalert": "^2.1.2", 34 | "underscore": "^1.10.2", 35 | "validator": "^10.11.0", 36 | "vinyl-source-stream": "^2.0.0" 37 | }, 38 | "scripts": { 39 | "mongo": "mongod --dbpath db", 40 | "start": "node app.js", 41 | "dev": "nodemon app.js", 42 | "watch": "gulp server", 43 | "prod": "gulp build && node app.js", 44 | "config": "cp .env.config .env", 45 | "test": "jest --forceExit", 46 | "test:accessibility": "pa11y-ci", 47 | "eslint": "./node_modules/.bin/eslint './**/*.js'" 48 | }, 49 | "devDependencies": { 50 | "eslint": "^6.8.0", 51 | "gulp": "^3.9.1", 52 | "gulp-clean-css": "^4.3.0", 53 | "gulp-concat": "^2.6.1", 54 | "gulp-ng-annotate": "^2.1.0", 55 | "gulp-nodemon": "^2.5.0", 56 | "gulp-sass": "^4.1.0", 57 | "gulp-sourcemaps": "^2.6.5", 58 | "gulp-uglify": "^3.0.2", 59 | "jest": "^24.9.0", 60 | "nodemon": "^1.19.4", 61 | "pa11y-ci": "^2.3.0", 62 | "supertest": "^4.0.2" 63 | }, 64 | "engines": { 65 | "node": "10.17.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scripts/acceptUsers.js: -------------------------------------------------------------------------------- 1 | require('dotenv').load(); 2 | var mongoose = require('mongoose'); 3 | var database = process.env.DATABASE || "mongodb://localhost:27017"; 4 | var jwt = require('jsonwebtoken'); 5 | mongoose.connect(database); 6 | 7 | var UserController = require('../app/server/controllers/UserController'); 8 | 9 | var user = { email: process.env.ADMIN_EMAIL }; 10 | 11 | var userArray = require('fs').readFileSync('accepted.txt').toString().split('\n'); 12 | var count = 0; 13 | userArray.forEach(function (id) { 14 | UserController.admitUser( id, user, function() { 15 | count += 1; 16 | if (count == userArray.length) { 17 | console.log("Done"); 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /scripts/createHellaUsers.js: -------------------------------------------------------------------------------- 1 | // Connect to mongodb 2 | var mongoose = require('mongoose'); 3 | var database = process.env.DATABASE || { url: "mongodb://localhost:27017"}; 4 | mongoose.connect(database.url); 5 | 6 | var UserController = require('../app/server/controllers/UserController'); 7 | 8 | var users = 1000; 9 | var username = 'hacker'; 10 | 11 | for (var i = 0; i < users; i++){ 12 | console.log(username, i); 13 | UserController 14 | .createUser(username + i + '@school.edu', 'foobar', function(){ 15 | console.log(i); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /scripts/getVerifyToken.js: -------------------------------------------------------------------------------- 1 | require('dotenv').load(); 2 | var mongoose = require('mongoose'); 3 | var database = process.env.DATABASE || { url: "mongodb://localhost:27017"}; 4 | var jwt = require('jsonwebtoken'); 5 | mongoose.connect(database.url); 6 | 7 | var User = require('../app/server/models/User'); 8 | 9 | var email = 'hacker@school.edu'; 10 | 11 | User.findOne({ 12 | email: email 13 | }, function(err, user){ 14 | console.log(user.generateEmailVerificationToken()); 15 | console.log(user.generateAuthToken()); 16 | 17 | var temp = user.generateTempAuthToken(); 18 | console.log(temp); 19 | 20 | console.log(jwt.verify(temp, process.env.JWT_SECRET)); 21 | }); 22 | -------------------------------------------------------------------------------- /scripts/testChangePassword.js: -------------------------------------------------------------------------------- 1 | require('dotenv').load(); 2 | var mongoose = require('mongoose'); 3 | var database = process.env.DATABASE || { url: "mongodb://localhost:27017"}; 4 | var jwt = require('jsonwebtoken'); 5 | mongoose.connect(database.url); 6 | 7 | var User = require('../app/server/models/User'); 8 | var UserController = require('../app/server/controllers/UserController'); 9 | 10 | var email = 'hacker@school.edu'; 11 | 12 | User.findOne({ 13 | email: email 14 | }, function(err, user){ 15 | var id = user._id; 16 | 17 | /* Change with old password */ 18 | UserController.changePassword( 19 | id, 20 | 'foobar', 21 | 'hunter123', 22 | function (err, something){ 23 | console.log(!err ? 'Successfuly changed' : err); 24 | } 25 | ); 26 | 27 | /* Change with auth token */ 28 | // var token = user.generateTempAuthToken(); 29 | 30 | // UserController.resetPassword( 31 | // id, 32 | // token, 33 | // 'hunter123', 34 | // function (err, something){ 35 | // console.log(!err ? 'Successfully changed' : err); 36 | // } 37 | // ); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("./app.js"); 3 | 4 | 5 | describe("Get Login", () => { 6 | it("Should Get the login Page ", async () => { 7 | const res = await request(app) 8 | .get("/login"); 9 | expect(res.statusCode).toEqual(200); 10 | }); 11 | }); 12 | --------------------------------------------------------------------------------