├── App_Illustration.png ├── README.md ├── api ├── .gitignore ├── app.js ├── db │ ├── models │ │ ├── index.js │ │ ├── list.model.js │ │ ├── task.model.js │ │ └── user.model.js │ └── mongoose.js ├── package-lock.json └── package.json └── frontend ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── auth.service.spec.ts │ ├── auth.service.ts │ ├── models │ │ ├── list.model.ts │ │ └── task.model.ts │ ├── pages │ │ ├── edit-list │ │ │ ├── edit-list.component.html │ │ │ ├── edit-list.component.scss │ │ │ ├── edit-list.component.spec.ts │ │ │ └── edit-list.component.ts │ │ ├── edit-task │ │ │ ├── edit-task.component.html │ │ │ ├── edit-task.component.scss │ │ │ ├── edit-task.component.spec.ts │ │ │ └── edit-task.component.ts │ │ ├── login-page │ │ │ ├── login-page.component.html │ │ │ ├── login-page.component.scss │ │ │ ├── login-page.component.spec.ts │ │ │ └── login-page.component.ts │ │ ├── new-list │ │ │ ├── new-list.component.html │ │ │ ├── new-list.component.scss │ │ │ ├── new-list.component.spec.ts │ │ │ └── new-list.component.ts │ │ ├── new-task │ │ │ ├── new-task.component.html │ │ │ ├── new-task.component.scss │ │ │ ├── new-task.component.spec.ts │ │ │ └── new-task.component.ts │ │ ├── signup-page │ │ │ ├── signup-page.component.html │ │ │ ├── signup-page.component.scss │ │ │ ├── signup-page.component.spec.ts │ │ │ └── signup-page.component.ts │ │ └── task-view │ │ │ ├── task-view.component.html │ │ │ ├── task-view.component.scss │ │ │ ├── task-view.component.spec.ts │ │ │ └── task-view.component.ts │ ├── task.service.spec.ts │ ├── task.service.ts │ ├── web-req.interceptor.ts │ ├── web-request.service.spec.ts │ └── web-request.service.ts ├── assets │ ├── .gitkeep │ └── plus-icon.svg ├── browserslist ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main-styles.scss ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json └── tslint.json /App_Illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devstackr/task-manager-mean-stack/548d6f69cd4b226dc85cbacd85ede9cfdbc69131/App_Illustration.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Task Manager App 2 | 3 | [![Application Design Illustration](App_Illustration.png)](https://www.youtube.com/watch?v=V-CeWkz1MNQ&list=PLIjdNHWULhPSZFDzQU6AnbVQNNo1NTRpd) 4 | 5 | ##### Watch the full tutorial on [Youtube](https://www.youtube.com/watch?v=V-CeWkz1MNQ&list=PLIjdNHWULhPSZFDzQU6AnbVQNNo1NTRpd)! 6 | 7 | ##### This repository contains the code for both the Angular frontend application as well as the NodeJS API. 8 | 9 | > Don't forget to run `npm install` in both the **api** and **frontend** folders to install dependencies 10 | 11 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /api/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | 4 | const { mongoose } = require('./db/mongoose'); 5 | 6 | const bodyParser = require('body-parser'); 7 | 8 | // Load in the mongoose models 9 | const { List, Task, User } = require('./db/models'); 10 | 11 | const jwt = require('jsonwebtoken'); 12 | 13 | 14 | /* MIDDLEWARE */ 15 | 16 | // Load middleware 17 | app.use(bodyParser.json()); 18 | 19 | 20 | // CORS HEADERS MIDDLEWARE 21 | app.use(function (req, res, next) { 22 | res.header("Access-Control-Allow-Origin", "*"); 23 | res.header("Access-Control-Allow-Methods", "GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE"); 24 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, x-access-token, x-refresh-token, _id"); 25 | 26 | res.header( 27 | 'Access-Control-Expose-Headers', 28 | 'x-access-token, x-refresh-token' 29 | ); 30 | 31 | next(); 32 | }); 33 | 34 | 35 | // check whether the request has a valid JWT access token 36 | let authenticate = (req, res, next) => { 37 | let token = req.header('x-access-token'); 38 | 39 | // verify the JWT 40 | jwt.verify(token, User.getJWTSecret(), (err, decoded) => { 41 | if (err) { 42 | // there was an error 43 | // jwt is invalid - * DO NOT AUTHENTICATE * 44 | res.status(401).send(err); 45 | } else { 46 | // jwt is valid 47 | req.user_id = decoded._id; 48 | next(); 49 | } 50 | }); 51 | } 52 | 53 | // Verify Refresh Token Middleware (which will be verifying the session) 54 | let verifySession = (req, res, next) => { 55 | // grab the refresh token from the request header 56 | let refreshToken = req.header('x-refresh-token'); 57 | 58 | // grab the _id from the request header 59 | let _id = req.header('_id'); 60 | 61 | User.findByIdAndToken(_id, refreshToken).then((user) => { 62 | if (!user) { 63 | // user couldn't be found 64 | return Promise.reject({ 65 | 'error': 'User not found. Make sure that the refresh token and user id are correct' 66 | }); 67 | } 68 | 69 | 70 | // if the code reaches here - the user was found 71 | // therefore the refresh token exists in the database - but we still have to check if it has expired or not 72 | 73 | req.user_id = user._id; 74 | req.userObject = user; 75 | req.refreshToken = refreshToken; 76 | 77 | let isSessionValid = false; 78 | 79 | user.sessions.forEach((session) => { 80 | if (session.token === refreshToken) { 81 | // check if the session has expired 82 | if (User.hasRefreshTokenExpired(session.expiresAt) === false) { 83 | // refresh token has not expired 84 | isSessionValid = true; 85 | } 86 | } 87 | }); 88 | 89 | if (isSessionValid) { 90 | // the session is VALID - call next() to continue with processing this web request 91 | next(); 92 | } else { 93 | // the session is not valid 94 | return Promise.reject({ 95 | 'error': 'Refresh token has expired or the session is invalid' 96 | }) 97 | } 98 | 99 | }).catch((e) => { 100 | res.status(401).send(e); 101 | }) 102 | } 103 | 104 | /* END MIDDLEWARE */ 105 | 106 | 107 | 108 | 109 | /* ROUTE HANDLERS */ 110 | 111 | /* LIST ROUTES */ 112 | 113 | /** 114 | * GET /lists 115 | * Purpose: Get all lists 116 | */ 117 | app.get('/lists', authenticate, (req, res) => { 118 | // We want to return an array of all the lists that belong to the authenticated user 119 | List.find({ 120 | _userId: req.user_id 121 | }).then((lists) => { 122 | res.send(lists); 123 | }).catch((e) => { 124 | res.send(e); 125 | }); 126 | }) 127 | 128 | /** 129 | * POST /lists 130 | * Purpose: Create a list 131 | */ 132 | app.post('/lists', authenticate, (req, res) => { 133 | // We want to create a new list and return the new list document back to the user (which includes the id) 134 | // The list information (fields) will be passed in via the JSON request body 135 | let title = req.body.title; 136 | 137 | let newList = new List({ 138 | title, 139 | _userId: req.user_id 140 | }); 141 | newList.save().then((listDoc) => { 142 | // the full list document is returned (incl. id) 143 | res.send(listDoc); 144 | }) 145 | }); 146 | 147 | /** 148 | * PATCH /lists/:id 149 | * Purpose: Update a specified list 150 | */ 151 | app.patch('/lists/:id', authenticate, (req, res) => { 152 | // We want to update the specified list (list document with id in the URL) with the new values specified in the JSON body of the request 153 | List.findOneAndUpdate({ _id: req.params.id, _userId: req.user_id }, { 154 | $set: req.body 155 | }).then(() => { 156 | res.send({ 'message': 'updated successfully'}); 157 | }); 158 | }); 159 | 160 | /** 161 | * DELETE /lists/:id 162 | * Purpose: Delete a list 163 | */ 164 | app.delete('/lists/:id', authenticate, (req, res) => { 165 | // We want to delete the specified list (document with id in the URL) 166 | List.findOneAndRemove({ 167 | _id: req.params.id, 168 | _userId: req.user_id 169 | }).then((removedListDoc) => { 170 | res.send(removedListDoc); 171 | 172 | // delete all the tasks that are in the deleted list 173 | deleteTasksFromList(removedListDoc._id); 174 | }) 175 | }); 176 | 177 | 178 | /** 179 | * GET /lists/:listId/tasks 180 | * Purpose: Get all tasks in a specific list 181 | */ 182 | app.get('/lists/:listId/tasks', authenticate, (req, res) => { 183 | // We want to return all tasks that belong to a specific list (specified by listId) 184 | Task.find({ 185 | _listId: req.params.listId 186 | }).then((tasks) => { 187 | res.send(tasks); 188 | }) 189 | }); 190 | 191 | 192 | /** 193 | * POST /lists/:listId/tasks 194 | * Purpose: Create a new task in a specific list 195 | */ 196 | app.post('/lists/:listId/tasks', authenticate, (req, res) => { 197 | // We want to create a new task in a list specified by listId 198 | 199 | List.findOne({ 200 | _id: req.params.listId, 201 | _userId: req.user_id 202 | }).then((list) => { 203 | if (list) { 204 | // list object with the specified conditions was found 205 | // therefore the currently authenticated user can create new tasks 206 | return true; 207 | } 208 | 209 | // else - the list object is undefined 210 | return false; 211 | }).then((canCreateTask) => { 212 | if (canCreateTask) { 213 | let newTask = new Task({ 214 | title: req.body.title, 215 | _listId: req.params.listId 216 | }); 217 | newTask.save().then((newTaskDoc) => { 218 | res.send(newTaskDoc); 219 | }) 220 | } else { 221 | res.sendStatus(404); 222 | } 223 | }) 224 | }) 225 | 226 | /** 227 | * PATCH /lists/:listId/tasks/:taskId 228 | * Purpose: Update an existing task 229 | */ 230 | app.patch('/lists/:listId/tasks/:taskId', authenticate, (req, res) => { 231 | // We want to update an existing task (specified by taskId) 232 | 233 | List.findOne({ 234 | _id: req.params.listId, 235 | _userId: req.user_id 236 | }).then((list) => { 237 | if (list) { 238 | // list object with the specified conditions was found 239 | // therefore the currently authenticated user can make updates to tasks within this list 240 | return true; 241 | } 242 | 243 | // else - the list object is undefined 244 | return false; 245 | }).then((canUpdateTasks) => { 246 | if (canUpdateTasks) { 247 | // the currently authenticated user can update tasks 248 | Task.findOneAndUpdate({ 249 | _id: req.params.taskId, 250 | _listId: req.params.listId 251 | }, { 252 | $set: req.body 253 | } 254 | ).then(() => { 255 | res.send({ message: 'Updated successfully.' }) 256 | }) 257 | } else { 258 | res.sendStatus(404); 259 | } 260 | }) 261 | }); 262 | 263 | /** 264 | * DELETE /lists/:listId/tasks/:taskId 265 | * Purpose: Delete a task 266 | */ 267 | app.delete('/lists/:listId/tasks/:taskId', authenticate, (req, res) => { 268 | 269 | List.findOne({ 270 | _id: req.params.listId, 271 | _userId: req.user_id 272 | }).then((list) => { 273 | if (list) { 274 | // list object with the specified conditions was found 275 | // therefore the currently authenticated user can make updates to tasks within this list 276 | return true; 277 | } 278 | 279 | // else - the list object is undefined 280 | return false; 281 | }).then((canDeleteTasks) => { 282 | 283 | if (canDeleteTasks) { 284 | Task.findOneAndRemove({ 285 | _id: req.params.taskId, 286 | _listId: req.params.listId 287 | }).then((removedTaskDoc) => { 288 | res.send(removedTaskDoc); 289 | }) 290 | } else { 291 | res.sendStatus(404); 292 | } 293 | }); 294 | }); 295 | 296 | 297 | 298 | /* USER ROUTES */ 299 | 300 | /** 301 | * POST /users 302 | * Purpose: Sign up 303 | */ 304 | app.post('/users', (req, res) => { 305 | // User sign up 306 | 307 | let body = req.body; 308 | let newUser = new User(body); 309 | 310 | newUser.save().then(() => { 311 | return newUser.createSession(); 312 | }).then((refreshToken) => { 313 | // Session created successfully - refreshToken returned. 314 | // now we geneate an access auth token for the user 315 | 316 | return newUser.generateAccessAuthToken().then((accessToken) => { 317 | // access auth token generated successfully, now we return an object containing the auth tokens 318 | return { accessToken, refreshToken } 319 | }); 320 | }).then((authTokens) => { 321 | // Now we construct and send the response to the user with their auth tokens in the header and the user object in the body 322 | res 323 | .header('x-refresh-token', authTokens.refreshToken) 324 | .header('x-access-token', authTokens.accessToken) 325 | .send(newUser); 326 | }).catch((e) => { 327 | res.status(400).send(e); 328 | }) 329 | }) 330 | 331 | 332 | /** 333 | * POST /users/login 334 | * Purpose: Login 335 | */ 336 | app.post('/users/login', (req, res) => { 337 | let email = req.body.email; 338 | let password = req.body.password; 339 | 340 | User.findByCredentials(email, password).then((user) => { 341 | return user.createSession().then((refreshToken) => { 342 | // Session created successfully - refreshToken returned. 343 | // now we geneate an access auth token for the user 344 | 345 | return user.generateAccessAuthToken().then((accessToken) => { 346 | // access auth token generated successfully, now we return an object containing the auth tokens 347 | return { accessToken, refreshToken } 348 | }); 349 | }).then((authTokens) => { 350 | // Now we construct and send the response to the user with their auth tokens in the header and the user object in the body 351 | res 352 | .header('x-refresh-token', authTokens.refreshToken) 353 | .header('x-access-token', authTokens.accessToken) 354 | .send(user); 355 | }) 356 | }).catch((e) => { 357 | res.status(400).send(e); 358 | }); 359 | }) 360 | 361 | 362 | /** 363 | * GET /users/me/access-token 364 | * Purpose: generates and returns an access token 365 | */ 366 | app.get('/users/me/access-token', verifySession, (req, res) => { 367 | // we know that the user/caller is authenticated and we have the user_id and user object available to us 368 | req.userObject.generateAccessAuthToken().then((accessToken) => { 369 | res.header('x-access-token', accessToken).send({ accessToken }); 370 | }).catch((e) => { 371 | res.status(400).send(e); 372 | }); 373 | }) 374 | 375 | 376 | 377 | /* HELPER METHODS */ 378 | let deleteTasksFromList = (_listId) => { 379 | Task.deleteMany({ 380 | _listId 381 | }).then(() => { 382 | console.log("Tasks from " + _listId + " were deleted!"); 383 | }) 384 | } 385 | 386 | 387 | 388 | 389 | app.listen(3000, () => { 390 | console.log("Server is listening on port 3000"); 391 | }) -------------------------------------------------------------------------------- /api/db/models/index.js: -------------------------------------------------------------------------------- 1 | const { List } = require('./list.model'); 2 | const { Task } = require('./task.model'); 3 | const { User } = require('./user.model'); 4 | 5 | 6 | module.exports = { 7 | List, 8 | Task, 9 | User 10 | } -------------------------------------------------------------------------------- /api/db/models/list.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const ListSchema = new mongoose.Schema({ 4 | title: { 5 | type: String, 6 | required: true, 7 | minlength: 1, 8 | trim: true 9 | }, 10 | // with auth 11 | _userId: { 12 | type: mongoose.Types.ObjectId, 13 | required: true 14 | } 15 | 16 | }) 17 | 18 | const List = mongoose.model('List', ListSchema); 19 | 20 | module.exports = { List } -------------------------------------------------------------------------------- /api/db/models/task.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const TaskSchema = new mongoose.Schema({ 4 | title: { 5 | type: String, 6 | required: true, 7 | minlength: 1, 8 | trim: true 9 | }, 10 | _listId: { 11 | type: mongoose.Types.ObjectId, 12 | required: true 13 | }, 14 | completed: { 15 | type: Boolean, 16 | default: false 17 | } 18 | }) 19 | 20 | const Task = mongoose.model('Task', TaskSchema); 21 | 22 | module.exports = { Task } -------------------------------------------------------------------------------- /api/db/models/user.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const _ = require('lodash'); 3 | const jwt = require('jsonwebtoken'); 4 | const crypto = require('crypto'); 5 | const bcrypt = require('bcryptjs'); 6 | 7 | 8 | // JWT Secret 9 | const jwtSecret = "51778657246321226641fsdklafjasdkljfsklfjd7148924065"; 10 | 11 | const UserSchema = new mongoose.Schema({ 12 | email: { 13 | type: String, 14 | required: true, 15 | minlength: 1, 16 | trim: true, 17 | unique: true 18 | }, 19 | password: { 20 | type: String, 21 | required: true, 22 | minlength: 8 23 | }, 24 | sessions: [{ 25 | token: { 26 | type: String, 27 | required: true 28 | }, 29 | expiresAt: { 30 | type: Number, 31 | required: true 32 | } 33 | }] 34 | }); 35 | 36 | 37 | // *** Instance methods *** 38 | 39 | UserSchema.methods.toJSON = function () { 40 | const user = this; 41 | const userObject = user.toObject(); 42 | 43 | // return the document except the password and sessions (these shouldn't be made available) 44 | return _.omit(userObject, ['password', 'sessions']); 45 | } 46 | 47 | UserSchema.methods.generateAccessAuthToken = function () { 48 | const user = this; 49 | return new Promise((resolve, reject) => { 50 | // Create the JSON Web Token and return that 51 | jwt.sign({ _id: user._id.toHexString() }, jwtSecret, { expiresIn: "15m" }, (err, token) => { 52 | if (!err) { 53 | resolve(token); 54 | } else { 55 | // there is an error 56 | reject(); 57 | } 58 | }) 59 | }) 60 | } 61 | 62 | UserSchema.methods.generateRefreshAuthToken = function () { 63 | // This method simply generates a 64byte hex string - it doesn't save it to the database. saveSessionToDatabase() does that. 64 | return new Promise((resolve, reject) => { 65 | crypto.randomBytes(64, (err, buf) => { 66 | if (!err) { 67 | // no error 68 | let token = buf.toString('hex'); 69 | 70 | return resolve(token); 71 | } 72 | }) 73 | }) 74 | } 75 | 76 | UserSchema.methods.createSession = function () { 77 | let user = this; 78 | 79 | return user.generateRefreshAuthToken().then((refreshToken) => { 80 | return saveSessionToDatabase(user, refreshToken); 81 | }).then((refreshToken) => { 82 | // saved to database successfully 83 | // now return the refresh token 84 | return refreshToken; 85 | }).catch((e) => { 86 | return Promise.reject('Failed to save session to database.\n' + e); 87 | }) 88 | } 89 | 90 | 91 | 92 | /* MODEL METHODS (static methods) */ 93 | 94 | UserSchema.statics.getJWTSecret = () => { 95 | return jwtSecret; 96 | } 97 | 98 | 99 | 100 | UserSchema.statics.findByIdAndToken = function (_id, token) { 101 | // finds user by id and token 102 | // used in auth middleware (verifySession) 103 | 104 | const User = this; 105 | 106 | return User.findOne({ 107 | _id, 108 | 'sessions.token': token 109 | }); 110 | } 111 | 112 | 113 | UserSchema.statics.findByCredentials = function (email, password) { 114 | let User = this; 115 | return User.findOne({ email }).then((user) => { 116 | if (!user) return Promise.reject(); 117 | 118 | return new Promise((resolve, reject) => { 119 | bcrypt.compare(password, user.password, (err, res) => { 120 | if (res) { 121 | resolve(user); 122 | } 123 | else { 124 | reject(); 125 | } 126 | }) 127 | }) 128 | }) 129 | } 130 | 131 | UserSchema.statics.hasRefreshTokenExpired = (expiresAt) => { 132 | let secondsSinceEpoch = Date.now() / 1000; 133 | if (expiresAt > secondsSinceEpoch) { 134 | // hasn't expired 135 | return false; 136 | } else { 137 | // has expired 138 | return true; 139 | } 140 | } 141 | 142 | 143 | /* MIDDLEWARE */ 144 | // Before a user document is saved, this code runs 145 | UserSchema.pre('save', function (next) { 146 | let user = this; 147 | let costFactor = 10; 148 | 149 | if (user.isModified('password')) { 150 | // if the password field has been edited/changed then run this code. 151 | 152 | // Generate salt and hash password 153 | bcrypt.genSalt(costFactor, (err, salt) => { 154 | bcrypt.hash(user.password, salt, (err, hash) => { 155 | user.password = hash; 156 | next(); 157 | }) 158 | }) 159 | } else { 160 | next(); 161 | } 162 | }); 163 | 164 | 165 | /* HELPER METHODS */ 166 | let saveSessionToDatabase = (user, refreshToken) => { 167 | // Save session to database 168 | return new Promise((resolve, reject) => { 169 | let expiresAt = generateRefreshTokenExpiryTime(); 170 | 171 | user.sessions.push({ 'token': refreshToken, expiresAt }); 172 | 173 | user.save().then(() => { 174 | // saved session successfully 175 | return resolve(refreshToken); 176 | }).catch((e) => { 177 | reject(e); 178 | }); 179 | }) 180 | } 181 | 182 | let generateRefreshTokenExpiryTime = () => { 183 | let daysUntilExpire = "10"; 184 | let secondsUntilExpire = ((daysUntilExpire * 24) * 60) * 60; 185 | return ((Date.now() / 1000) + secondsUntilExpire); 186 | } 187 | 188 | const User = mongoose.model('User', UserSchema); 189 | 190 | module.exports = { User } -------------------------------------------------------------------------------- /api/db/mongoose.js: -------------------------------------------------------------------------------- 1 | // This file will handle connection logic to the MongoDB database 2 | 3 | const mongoose = require('mongoose'); 4 | 5 | mongoose.Promise = global.Promise; 6 | mongoose.connect('mongodb://localhost:27017/TaskManager', { useNewUrlParser: true }).then(() => { 7 | console.log("Connected to MongoDB successfully :)"); 8 | }).catch((e) => { 9 | console.log("Error while attempting to connect to MongoDB"); 10 | console.log(e); 11 | }); 12 | 13 | // To prevent deprectation warnings (from MongoDB native driver) 14 | mongoose.set('useCreateIndex', true); 15 | mongoose.set('useFindAndModify', false); 16 | 17 | 18 | module.exports = { 19 | mongoose 20 | }; -------------------------------------------------------------------------------- /api/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.5", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 11 | "requires": { 12 | "mime-types": "~2.1.18", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "array-flatten": { 17 | "version": "1.1.1", 18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 19 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 20 | }, 21 | "async": { 22 | "version": "2.6.1", 23 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", 24 | "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", 25 | "requires": { 26 | "lodash": "^4.17.10" 27 | } 28 | }, 29 | "bcryptjs": { 30 | "version": "2.4.3", 31 | "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", 32 | "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" 33 | }, 34 | "bluebird": { 35 | "version": "3.5.1", 36 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", 37 | "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" 38 | }, 39 | "body-parser": { 40 | "version": "1.18.3", 41 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", 42 | "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", 43 | "requires": { 44 | "bytes": "3.0.0", 45 | "content-type": "~1.0.4", 46 | "debug": "2.6.9", 47 | "depd": "~1.1.2", 48 | "http-errors": "~1.6.3", 49 | "iconv-lite": "0.4.23", 50 | "on-finished": "~2.3.0", 51 | "qs": "6.5.2", 52 | "raw-body": "2.3.3", 53 | "type-is": "~1.6.16" 54 | } 55 | }, 56 | "bson": { 57 | "version": "1.1.0", 58 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.0.tgz", 59 | "integrity": "sha512-9Aeai9TacfNtWXOYarkFJRW2CWo+dRon+fuLZYJmvLV3+MiUp0bEI6IAZfXEIg7/Pl/7IWlLaDnhzTsD81etQA==" 60 | }, 61 | "buffer-equal-constant-time": { 62 | "version": "1.0.1", 63 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 64 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 65 | }, 66 | "bytes": { 67 | "version": "3.0.0", 68 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 69 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 70 | }, 71 | "content-disposition": { 72 | "version": "0.5.2", 73 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 74 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 75 | }, 76 | "content-type": { 77 | "version": "1.0.4", 78 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 79 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 80 | }, 81 | "cookie": { 82 | "version": "0.3.1", 83 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 84 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 85 | }, 86 | "cookie-signature": { 87 | "version": "1.0.6", 88 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 89 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 90 | }, 91 | "debug": { 92 | "version": "2.6.9", 93 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 94 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 95 | "requires": { 96 | "ms": "2.0.0" 97 | } 98 | }, 99 | "depd": { 100 | "version": "1.1.2", 101 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 102 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 103 | }, 104 | "destroy": { 105 | "version": "1.0.4", 106 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 107 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 108 | }, 109 | "ecdsa-sig-formatter": { 110 | "version": "1.0.11", 111 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 112 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 113 | "requires": { 114 | "safe-buffer": "^5.0.1" 115 | } 116 | }, 117 | "ee-first": { 118 | "version": "1.1.1", 119 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 120 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 121 | }, 122 | "encodeurl": { 123 | "version": "1.0.2", 124 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 125 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 126 | }, 127 | "escape-html": { 128 | "version": "1.0.3", 129 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 130 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 131 | }, 132 | "etag": { 133 | "version": "1.8.1", 134 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 135 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 136 | }, 137 | "express": { 138 | "version": "4.16.4", 139 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", 140 | "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", 141 | "requires": { 142 | "accepts": "~1.3.5", 143 | "array-flatten": "1.1.1", 144 | "body-parser": "1.18.3", 145 | "content-disposition": "0.5.2", 146 | "content-type": "~1.0.4", 147 | "cookie": "0.3.1", 148 | "cookie-signature": "1.0.6", 149 | "debug": "2.6.9", 150 | "depd": "~1.1.2", 151 | "encodeurl": "~1.0.2", 152 | "escape-html": "~1.0.3", 153 | "etag": "~1.8.1", 154 | "finalhandler": "1.1.1", 155 | "fresh": "0.5.2", 156 | "merge-descriptors": "1.0.1", 157 | "methods": "~1.1.2", 158 | "on-finished": "~2.3.0", 159 | "parseurl": "~1.3.2", 160 | "path-to-regexp": "0.1.7", 161 | "proxy-addr": "~2.0.4", 162 | "qs": "6.5.2", 163 | "range-parser": "~1.2.0", 164 | "safe-buffer": "5.1.2", 165 | "send": "0.16.2", 166 | "serve-static": "1.13.2", 167 | "setprototypeof": "1.1.0", 168 | "statuses": "~1.4.0", 169 | "type-is": "~1.6.16", 170 | "utils-merge": "1.0.1", 171 | "vary": "~1.1.2" 172 | } 173 | }, 174 | "finalhandler": { 175 | "version": "1.1.1", 176 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", 177 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", 178 | "requires": { 179 | "debug": "2.6.9", 180 | "encodeurl": "~1.0.2", 181 | "escape-html": "~1.0.3", 182 | "on-finished": "~2.3.0", 183 | "parseurl": "~1.3.2", 184 | "statuses": "~1.4.0", 185 | "unpipe": "~1.0.0" 186 | } 187 | }, 188 | "forwarded": { 189 | "version": "0.1.2", 190 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 191 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 192 | }, 193 | "fresh": { 194 | "version": "0.5.2", 195 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 196 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 197 | }, 198 | "http-errors": { 199 | "version": "1.6.3", 200 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 201 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 202 | "requires": { 203 | "depd": "~1.1.2", 204 | "inherits": "2.0.3", 205 | "setprototypeof": "1.1.0", 206 | "statuses": ">= 1.4.0 < 2" 207 | } 208 | }, 209 | "iconv-lite": { 210 | "version": "0.4.23", 211 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", 212 | "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", 213 | "requires": { 214 | "safer-buffer": ">= 2.1.2 < 3" 215 | } 216 | }, 217 | "inherits": { 218 | "version": "2.0.3", 219 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 220 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 221 | }, 222 | "ipaddr.js": { 223 | "version": "1.8.0", 224 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", 225 | "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" 226 | }, 227 | "jsonwebtoken": { 228 | "version": "8.5.0", 229 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz", 230 | "integrity": "sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA==", 231 | "requires": { 232 | "jws": "^3.2.1", 233 | "lodash.includes": "^4.3.0", 234 | "lodash.isboolean": "^3.0.3", 235 | "lodash.isinteger": "^4.0.4", 236 | "lodash.isnumber": "^3.0.3", 237 | "lodash.isplainobject": "^4.0.6", 238 | "lodash.isstring": "^4.0.1", 239 | "lodash.once": "^4.0.0", 240 | "ms": "^2.1.1", 241 | "semver": "^5.6.0" 242 | }, 243 | "dependencies": { 244 | "ms": { 245 | "version": "2.1.1", 246 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 247 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 248 | } 249 | } 250 | }, 251 | "jwa": { 252 | "version": "1.3.0", 253 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.3.0.tgz", 254 | "integrity": "sha512-SxObIyzv9a6MYuZYaSN6DhSm9j3+qkokwvCB0/OTSV5ylPq1wUQiygZQcHT5Qlux0I5kmISx3J86TxKhuefItg==", 255 | "requires": { 256 | "buffer-equal-constant-time": "1.0.1", 257 | "ecdsa-sig-formatter": "1.0.11", 258 | "safe-buffer": "^5.0.1" 259 | } 260 | }, 261 | "jws": { 262 | "version": "3.2.1", 263 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.1.tgz", 264 | "integrity": "sha512-bGA2omSrFUkd72dhh05bIAN832znP4wOU3lfuXtRBuGTbsmNmDXMQg28f0Vsxaxgk4myF5YkKQpz6qeRpMgX9g==", 265 | "requires": { 266 | "jwa": "^1.2.0", 267 | "safe-buffer": "^5.0.1" 268 | } 269 | }, 270 | "kareem": { 271 | "version": "2.3.0", 272 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.0.tgz", 273 | "integrity": "sha512-6hHxsp9e6zQU8nXsP+02HGWXwTkOEw6IROhF2ZA28cYbUk4eJ6QbtZvdqZOdD9YPKghG3apk5eOCvs+tLl3lRg==" 274 | }, 275 | "lodash": { 276 | "version": "4.17.11", 277 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", 278 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" 279 | }, 280 | "lodash.includes": { 281 | "version": "4.3.0", 282 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 283 | "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" 284 | }, 285 | "lodash.isboolean": { 286 | "version": "3.0.3", 287 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 288 | "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" 289 | }, 290 | "lodash.isinteger": { 291 | "version": "4.0.4", 292 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 293 | "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" 294 | }, 295 | "lodash.isnumber": { 296 | "version": "3.0.3", 297 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 298 | "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" 299 | }, 300 | "lodash.isplainobject": { 301 | "version": "4.0.6", 302 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 303 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 304 | }, 305 | "lodash.isstring": { 306 | "version": "4.0.1", 307 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 308 | "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" 309 | }, 310 | "lodash.once": { 311 | "version": "4.1.1", 312 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 313 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" 314 | }, 315 | "media-typer": { 316 | "version": "0.3.0", 317 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 318 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 319 | }, 320 | "memory-pager": { 321 | "version": "1.5.0", 322 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 323 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", 324 | "optional": true 325 | }, 326 | "merge-descriptors": { 327 | "version": "1.0.1", 328 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 329 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 330 | }, 331 | "methods": { 332 | "version": "1.1.2", 333 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 334 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 335 | }, 336 | "mime": { 337 | "version": "1.4.1", 338 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 339 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 340 | }, 341 | "mime-db": { 342 | "version": "1.37.0", 343 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", 344 | "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" 345 | }, 346 | "mime-types": { 347 | "version": "2.1.21", 348 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", 349 | "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", 350 | "requires": { 351 | "mime-db": "~1.37.0" 352 | } 353 | }, 354 | "mongodb": { 355 | "version": "3.1.13", 356 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.13.tgz", 357 | "integrity": "sha512-sz2dhvBZQWf3LRNDhbd30KHVzdjZx9IKC0L+kSZ/gzYquCF5zPOgGqRz6sSCqYZtKP2ekB4nfLxhGtzGHnIKxA==", 358 | "requires": { 359 | "mongodb-core": "3.1.11", 360 | "safe-buffer": "^5.1.2" 361 | } 362 | }, 363 | "mongodb-core": { 364 | "version": "3.1.11", 365 | "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.11.tgz", 366 | "integrity": "sha512-rD2US2s5qk/ckbiiGFHeu+yKYDXdJ1G87F6CG3YdaZpzdOm5zpoAZd/EKbPmFO6cQZ+XVXBXBJ660sSI0gc6qg==", 367 | "requires": { 368 | "bson": "^1.1.0", 369 | "require_optional": "^1.0.1", 370 | "safe-buffer": "^5.1.2", 371 | "saslprep": "^1.0.0" 372 | } 373 | }, 374 | "mongoose": { 375 | "version": "5.4.11", 376 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.4.11.tgz", 377 | "integrity": "sha512-Ah/JaZj4vhYP2CEjIy1ictJguzGJczHMtUQmLrBUpnQ4WhhQl5jOo6JHzKiRJ/PdLV25y22kt8WOdsc0DIojww==", 378 | "requires": { 379 | "async": "2.6.1", 380 | "bson": "~1.1.0", 381 | "kareem": "2.3.0", 382 | "mongodb": "3.1.13", 383 | "mongodb-core": "3.1.11", 384 | "mongoose-legacy-pluralize": "1.0.2", 385 | "mpath": "0.5.1", 386 | "mquery": "3.2.0", 387 | "ms": "2.0.0", 388 | "regexp-clone": "0.0.1", 389 | "safe-buffer": "5.1.2", 390 | "sliced": "1.0.1" 391 | } 392 | }, 393 | "mongoose-legacy-pluralize": { 394 | "version": "1.0.2", 395 | "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", 396 | "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" 397 | }, 398 | "mpath": { 399 | "version": "0.5.1", 400 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.5.1.tgz", 401 | "integrity": "sha512-H8OVQ+QEz82sch4wbODFOz+3YQ61FYz/z3eJ5pIdbMEaUzDqA268Wd+Vt4Paw9TJfvDgVKaayC0gBzMIw2jhsg==" 402 | }, 403 | "mquery": { 404 | "version": "3.2.0", 405 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.0.tgz", 406 | "integrity": "sha512-qPJcdK/yqcbQiKoemAt62Y0BAc0fTEKo1IThodBD+O5meQRJT/2HSe5QpBNwaa4CjskoGrYWsEyjkqgiE0qjhg==", 407 | "requires": { 408 | "bluebird": "3.5.1", 409 | "debug": "3.1.0", 410 | "regexp-clone": "0.0.1", 411 | "safe-buffer": "5.1.2", 412 | "sliced": "1.0.1" 413 | }, 414 | "dependencies": { 415 | "debug": { 416 | "version": "3.1.0", 417 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 418 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 419 | "requires": { 420 | "ms": "2.0.0" 421 | } 422 | } 423 | } 424 | }, 425 | "ms": { 426 | "version": "2.0.0", 427 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 428 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 429 | }, 430 | "negotiator": { 431 | "version": "0.6.1", 432 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 433 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 434 | }, 435 | "on-finished": { 436 | "version": "2.3.0", 437 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 438 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 439 | "requires": { 440 | "ee-first": "1.1.1" 441 | } 442 | }, 443 | "parseurl": { 444 | "version": "1.3.2", 445 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 446 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 447 | }, 448 | "path-to-regexp": { 449 | "version": "0.1.7", 450 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 451 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 452 | }, 453 | "proxy-addr": { 454 | "version": "2.0.4", 455 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", 456 | "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", 457 | "requires": { 458 | "forwarded": "~0.1.2", 459 | "ipaddr.js": "1.8.0" 460 | } 461 | }, 462 | "qs": { 463 | "version": "6.5.2", 464 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 465 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 466 | }, 467 | "range-parser": { 468 | "version": "1.2.0", 469 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 470 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 471 | }, 472 | "raw-body": { 473 | "version": "2.3.3", 474 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", 475 | "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", 476 | "requires": { 477 | "bytes": "3.0.0", 478 | "http-errors": "1.6.3", 479 | "iconv-lite": "0.4.23", 480 | "unpipe": "1.0.0" 481 | } 482 | }, 483 | "regexp-clone": { 484 | "version": "0.0.1", 485 | "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", 486 | "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" 487 | }, 488 | "require_optional": { 489 | "version": "1.0.1", 490 | "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", 491 | "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", 492 | "requires": { 493 | "resolve-from": "^2.0.0", 494 | "semver": "^5.1.0" 495 | } 496 | }, 497 | "resolve-from": { 498 | "version": "2.0.0", 499 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", 500 | "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" 501 | }, 502 | "safe-buffer": { 503 | "version": "5.1.2", 504 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 505 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 506 | }, 507 | "safer-buffer": { 508 | "version": "2.1.2", 509 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 510 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 511 | }, 512 | "saslprep": { 513 | "version": "1.0.2", 514 | "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.2.tgz", 515 | "integrity": "sha512-4cDsYuAjXssUSjxHKRe4DTZC0agDwsCqcMqtJAQPzC74nJ7LfAJflAtC1Zed5hMzEQKj82d3tuzqdGNRsLJ4Gw==", 516 | "optional": true, 517 | "requires": { 518 | "sparse-bitfield": "^3.0.3" 519 | } 520 | }, 521 | "semver": { 522 | "version": "5.6.0", 523 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", 524 | "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" 525 | }, 526 | "send": { 527 | "version": "0.16.2", 528 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", 529 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", 530 | "requires": { 531 | "debug": "2.6.9", 532 | "depd": "~1.1.2", 533 | "destroy": "~1.0.4", 534 | "encodeurl": "~1.0.2", 535 | "escape-html": "~1.0.3", 536 | "etag": "~1.8.1", 537 | "fresh": "0.5.2", 538 | "http-errors": "~1.6.2", 539 | "mime": "1.4.1", 540 | "ms": "2.0.0", 541 | "on-finished": "~2.3.0", 542 | "range-parser": "~1.2.0", 543 | "statuses": "~1.4.0" 544 | } 545 | }, 546 | "serve-static": { 547 | "version": "1.13.2", 548 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", 549 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", 550 | "requires": { 551 | "encodeurl": "~1.0.2", 552 | "escape-html": "~1.0.3", 553 | "parseurl": "~1.3.2", 554 | "send": "0.16.2" 555 | } 556 | }, 557 | "setprototypeof": { 558 | "version": "1.1.0", 559 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 560 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 561 | }, 562 | "sliced": { 563 | "version": "1.0.1", 564 | "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", 565 | "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" 566 | }, 567 | "sparse-bitfield": { 568 | "version": "3.0.3", 569 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 570 | "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", 571 | "optional": true, 572 | "requires": { 573 | "memory-pager": "^1.0.2" 574 | } 575 | }, 576 | "statuses": { 577 | "version": "1.4.0", 578 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 579 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 580 | }, 581 | "type-is": { 582 | "version": "1.6.16", 583 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 584 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 585 | "requires": { 586 | "media-typer": "0.3.0", 587 | "mime-types": "~2.1.18" 588 | } 589 | }, 590 | "unpipe": { 591 | "version": "1.0.0", 592 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 593 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 594 | }, 595 | "utils-merge": { 596 | "version": "1.0.1", 597 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 598 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 599 | }, 600 | "vary": { 601 | "version": "1.1.2", 602 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 603 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 604 | } 605 | } 606 | } 607 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "bcryptjs": "^2.4.3", 13 | "body-parser": "^1.18.3", 14 | "express": "^4.16.4", 15 | "jsonwebtoken": "^8.5.0", 16 | "lodash": "^4.17.11", 17 | "mongoose": "^5.4.11" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # profiling files 12 | chrome-profiler-events.json 13 | speed-measure-plugin.json 14 | 15 | # IDEs and editors 16 | /.idea 17 | .project 18 | .classpath 19 | .c9/ 20 | *.launch 21 | .settings/ 22 | *.sublime-workspace 23 | 24 | # IDE - VSCode 25 | .vscode/* 26 | !.vscode/settings.json 27 | !.vscode/tasks.json 28 | !.vscode/launch.json 29 | !.vscode/extensions.json 30 | .history/* 31 | 32 | # misc 33 | /.sass-cache 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | npm-debug.log 38 | yarn-error.log 39 | testem.log 40 | /typings 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Frontend 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.2.1. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "frontend": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "styleext": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/frontend", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "src/tsconfig.app.json", 25 | "assets": [ 26 | "src/favicon.ico", 27 | "src/assets" 28 | ], 29 | "styles": [ 30 | "src/styles.scss" 31 | ], 32 | "scripts": [] 33 | }, 34 | "configurations": { 35 | "production": { 36 | "fileReplacements": [ 37 | { 38 | "replace": "src/environments/environment.ts", 39 | "with": "src/environments/environment.prod.ts" 40 | } 41 | ], 42 | "optimization": true, 43 | "outputHashing": "all", 44 | "sourceMap": false, 45 | "extractCss": true, 46 | "namedChunks": false, 47 | "aot": true, 48 | "extractLicenses": true, 49 | "vendorChunk": false, 50 | "buildOptimizer": true, 51 | "budgets": [ 52 | { 53 | "type": "initial", 54 | "maximumWarning": "2mb", 55 | "maximumError": "5mb" 56 | } 57 | ] 58 | } 59 | } 60 | }, 61 | "serve": { 62 | "builder": "@angular-devkit/build-angular:dev-server", 63 | "options": { 64 | "browserTarget": "frontend:build" 65 | }, 66 | "configurations": { 67 | "production": { 68 | "browserTarget": "frontend:build:production" 69 | } 70 | } 71 | }, 72 | "extract-i18n": { 73 | "builder": "@angular-devkit/build-angular:extract-i18n", 74 | "options": { 75 | "browserTarget": "frontend:build" 76 | } 77 | }, 78 | "test": { 79 | "builder": "@angular-devkit/build-angular:karma", 80 | "options": { 81 | "main": "src/test.ts", 82 | "polyfills": "src/polyfills.ts", 83 | "tsConfig": "src/tsconfig.spec.json", 84 | "karmaConfig": "src/karma.conf.js", 85 | "styles": [ 86 | "src/styles.scss" 87 | ], 88 | "scripts": [], 89 | "assets": [ 90 | "src/favicon.ico", 91 | "src/assets" 92 | ] 93 | } 94 | }, 95 | "lint": { 96 | "builder": "@angular-devkit/build-angular:tslint", 97 | "options": { 98 | "tsConfig": [ 99 | "src/tsconfig.app.json", 100 | "src/tsconfig.spec.json" 101 | ], 102 | "exclude": [ 103 | "**/node_modules/**" 104 | ] 105 | } 106 | } 107 | } 108 | }, 109 | "frontend-e2e": { 110 | "root": "e2e/", 111 | "projectType": "application", 112 | "prefix": "", 113 | "architect": { 114 | "e2e": { 115 | "builder": "@angular-devkit/build-angular:protractor", 116 | "options": { 117 | "protractorConfig": "e2e/protractor.conf.js", 118 | "devServerTarget": "frontend:serve" 119 | }, 120 | "configurations": { 121 | "production": { 122 | "devServerTarget": "frontend:serve:production" 123 | } 124 | } 125 | }, 126 | "lint": { 127 | "builder": "@angular-devkit/build-angular:tslint", 128 | "options": { 129 | "tsConfig": "e2e/tsconfig.e2e.json", 130 | "exclude": [ 131 | "**/node_modules/**" 132 | ] 133 | } 134 | } 135 | } 136 | } 137 | }, 138 | "defaultProject": "frontend" 139 | } -------------------------------------------------------------------------------- /frontend/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /frontend/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getTitleText()).toEqual('Welcome to frontend!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~7.2.0", 15 | "@angular/common": "~7.2.0", 16 | "@angular/compiler": "~7.2.0", 17 | "@angular/core": "~7.2.0", 18 | "@angular/forms": "~7.2.0", 19 | "@angular/platform-browser": "~7.2.0", 20 | "@angular/platform-browser-dynamic": "~7.2.0", 21 | "@angular/router": "~7.2.0", 22 | "bulma": "^0.7.4", 23 | "core-js": "^2.5.4", 24 | "rxjs": "~6.3.3", 25 | "tslib": "^1.9.0", 26 | "zone.js": "~0.8.26" 27 | }, 28 | "devDependencies": { 29 | "@angular-devkit/build-angular": "~0.12.0", 30 | "@angular/cli": "~7.2.1", 31 | "@angular/compiler-cli": "~7.2.0", 32 | "@angular/language-service": "~7.2.0", 33 | "@types/node": "~8.9.4", 34 | "@types/jasmine": "~2.8.8", 35 | "@types/jasminewd2": "~2.0.3", 36 | "codelyzer": "~4.5.0", 37 | "jasmine-core": "~2.99.1", 38 | "jasmine-spec-reporter": "~4.2.1", 39 | "karma": "~3.1.1", 40 | "karma-chrome-launcher": "~2.2.0", 41 | "karma-coverage-istanbul-reporter": "~2.0.1", 42 | "karma-jasmine": "~1.1.2", 43 | "karma-jasmine-html-reporter": "^0.2.2", 44 | "protractor": "~5.4.0", 45 | "ts-node": "~7.0.0", 46 | "tslint": "~5.11.0", 47 | "typescript": "~3.2.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /frontend/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { TaskViewComponent } from './pages/task-view/task-view.component'; 4 | import { NewListComponent } from './pages/new-list/new-list.component'; 5 | import { NewTaskComponent } from './pages/new-task/new-task.component'; 6 | import { LoginPageComponent } from './pages/login-page/login-page.component'; 7 | import { SignupPageComponent } from './pages/signup-page/signup-page.component'; 8 | import { EditListComponent } from './pages/edit-list/edit-list.component'; 9 | import { EditTaskComponent } from './pages/edit-task/edit-task.component'; 10 | 11 | const routes: Routes = [ 12 | { path: '', redirectTo: '/lists', pathMatch: 'full' }, 13 | { path: 'new-list', component: NewListComponent }, 14 | { path: 'edit-list/:listId', component: EditListComponent }, 15 | { path: 'login', component: LoginPageComponent }, 16 | { path: 'signup', component: SignupPageComponent }, 17 | { path: 'lists', component: TaskViewComponent }, 18 | { path: 'lists/:listId', component: TaskViewComponent }, 19 | { path: 'lists/:listId/new-task', component: NewTaskComponent }, 20 | { path: 'lists/:listId/edit-task/:taskId', component: EditTaskComponent }, 21 | ]; 22 | 23 | @NgModule({ 24 | imports: [RouterModule.forRoot(routes)], 25 | exports: [RouterModule] 26 | }) 27 | export class AppRoutingModule { } 28 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devstackr/task-manager-mean-stack/548d6f69cd4b226dc85cbacd85ede9cfdbc69131/frontend/src/app/app.component.scss -------------------------------------------------------------------------------- /frontend/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | })); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.debugElement.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'frontend'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.debugElement.componentInstance; 26 | expect(app.title).toEqual('frontend'); 27 | }); 28 | 29 | it('should render title in a h1 tag', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.debugElement.nativeElement; 33 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to frontend!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'frontend'; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | import { TaskViewComponent } from './pages/task-view/task-view.component'; 7 | 8 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 9 | import { NewListComponent } from './pages/new-list/new-list.component'; 10 | import { NewTaskComponent } from './pages/new-task/new-task.component'; 11 | import { LoginPageComponent } from './pages/login-page/login-page.component'; 12 | import { WebReqInterceptor } from './web-req.interceptor'; 13 | import { SignupPageComponent } from './pages/signup-page/signup-page.component'; 14 | import { EditListComponent } from './pages/edit-list/edit-list.component'; 15 | import { EditTaskComponent } from './pages/edit-task/edit-task.component'; 16 | 17 | @NgModule({ 18 | declarations: [ 19 | AppComponent, 20 | TaskViewComponent, 21 | NewListComponent, 22 | NewTaskComponent, 23 | LoginPageComponent, 24 | SignupPageComponent, 25 | EditListComponent, 26 | EditTaskComponent 27 | ], 28 | imports: [ 29 | BrowserModule, 30 | AppRoutingModule, 31 | HttpClientModule 32 | ], 33 | providers: [ 34 | { provide: HTTP_INTERCEPTORS, useClass: WebReqInterceptor, multi: true } 35 | ], 36 | bootstrap: [AppComponent] 37 | }) 38 | export class AppModule { } 39 | -------------------------------------------------------------------------------- /frontend/src/app/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: AuthService = TestBed.get(AuthService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/app/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpResponse } from '@angular/common/http'; 3 | import { WebRequestService } from './web-request.service'; 4 | import { Router } from '@angular/router'; 5 | import { shareReplay, tap } from 'rxjs/operators'; 6 | 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class AuthService { 12 | 13 | constructor(private webService: WebRequestService, private router: Router, private http: HttpClient) { } 14 | 15 | login(email: string, password: string) { 16 | return this.webService.login(email, password).pipe( 17 | shareReplay(), 18 | tap((res: HttpResponse) => { 19 | // the auth tokens will be in the header of this response 20 | this.setSession(res.body._id, res.headers.get('x-access-token'), res.headers.get('x-refresh-token')); 21 | console.log("LOGGED IN!"); 22 | }) 23 | ) 24 | } 25 | 26 | 27 | signup(email: string, password: string) { 28 | return this.webService.signup(email, password).pipe( 29 | shareReplay(), 30 | tap((res: HttpResponse) => { 31 | // the auth tokens will be in the header of this response 32 | this.setSession(res.body._id, res.headers.get('x-access-token'), res.headers.get('x-refresh-token')); 33 | console.log("Successfully signed up and now logged in!"); 34 | }) 35 | ) 36 | } 37 | 38 | 39 | 40 | logout() { 41 | this.removeSession(); 42 | 43 | this.router.navigate(['/login']); 44 | } 45 | 46 | getAccessToken() { 47 | return localStorage.getItem('x-access-token'); 48 | } 49 | 50 | getRefreshToken() { 51 | return localStorage.getItem('x-refresh-token'); 52 | } 53 | 54 | getUserId() { 55 | return localStorage.getItem('user-id'); 56 | } 57 | 58 | setAccessToken(accessToken: string) { 59 | localStorage.setItem('x-access-token', accessToken) 60 | } 61 | 62 | private setSession(userId: string, accessToken: string, refreshToken: string) { 63 | localStorage.setItem('user-id', userId); 64 | localStorage.setItem('x-access-token', accessToken); 65 | localStorage.setItem('x-refresh-token', refreshToken); 66 | } 67 | 68 | private removeSession() { 69 | localStorage.removeItem('user-id'); 70 | localStorage.removeItem('x-access-token'); 71 | localStorage.removeItem('x-refresh-token'); 72 | } 73 | 74 | getNewAccessToken() { 75 | return this.http.get(`${this.webService.ROOT_URL}/users/me/access-token`, { 76 | headers: { 77 | 'x-refresh-token': this.getRefreshToken(), 78 | '_id': this.getUserId() 79 | }, 80 | observe: 'response' 81 | }).pipe( 82 | tap((res: HttpResponse) => { 83 | this.setAccessToken(res.headers.get('x-access-token')); 84 | }) 85 | ) 86 | } 87 | } -------------------------------------------------------------------------------- /frontend/src/app/models/list.model.ts: -------------------------------------------------------------------------------- 1 | export class List { 2 | _id: string; 3 | title: string; 4 | } -------------------------------------------------------------------------------- /frontend/src/app/models/task.model.ts: -------------------------------------------------------------------------------- 1 | export class Task { 2 | _id: string; 3 | _listId: string; 4 | title: string; 5 | completed: boolean; 6 | } -------------------------------------------------------------------------------- /frontend/src/app/pages/edit-list/edit-list.component.html: -------------------------------------------------------------------------------- 1 |
2 | 16 |
-------------------------------------------------------------------------------- /frontend/src/app/pages/edit-list/edit-list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devstackr/task-manager-mean-stack/548d6f69cd4b226dc85cbacd85ede9cfdbc69131/frontend/src/app/pages/edit-list/edit-list.component.scss -------------------------------------------------------------------------------- /frontend/src/app/pages/edit-list/edit-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { EditListComponent } from './edit-list.component'; 4 | 5 | describe('EditListComponent', () => { 6 | let component: EditListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ EditListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(EditListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/pages/edit-list/edit-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Params, Router } from '@angular/router'; 3 | import { TaskService } from 'src/app/task.service'; 4 | 5 | @Component({ 6 | selector: 'app-edit-list', 7 | templateUrl: './edit-list.component.html', 8 | styleUrls: ['./edit-list.component.scss'] 9 | }) 10 | export class EditListComponent implements OnInit { 11 | 12 | constructor(private route: ActivatedRoute, private taskService: TaskService, private router: Router) { } 13 | 14 | listId: string; 15 | 16 | 17 | ngOnInit() { 18 | this.route.params.subscribe( 19 | (params: Params) => { 20 | this.listId = params.listId; 21 | console.log(params.listId); 22 | } 23 | ) 24 | } 25 | 26 | updateList(title: string) { 27 | this.taskService.updateList(this.listId, title).subscribe(() => { 28 | this.router.navigate(['/lists', this.listId]); 29 | }) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/app/pages/edit-task/edit-task.component.html: -------------------------------------------------------------------------------- 1 |
2 | 14 |
-------------------------------------------------------------------------------- /frontend/src/app/pages/edit-task/edit-task.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devstackr/task-manager-mean-stack/548d6f69cd4b226dc85cbacd85ede9cfdbc69131/frontend/src/app/pages/edit-task/edit-task.component.scss -------------------------------------------------------------------------------- /frontend/src/app/pages/edit-task/edit-task.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { EditTaskComponent } from './edit-task.component'; 4 | 5 | describe('EditTaskComponent', () => { 6 | let component: EditTaskComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ EditTaskComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(EditTaskComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/pages/edit-task/edit-task.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Params, ActivatedRoute, Router } from '@angular/router'; 3 | import { TaskService } from 'src/app/task.service'; 4 | 5 | @Component({ 6 | selector: 'app-edit-task', 7 | templateUrl: './edit-task.component.html', 8 | styleUrls: ['./edit-task.component.scss'] 9 | }) 10 | export class EditTaskComponent implements OnInit { 11 | 12 | constructor(private route: ActivatedRoute, private taskService: TaskService, private router: Router) { } 13 | 14 | taskId: string; 15 | listId: string; 16 | 17 | 18 | ngOnInit() { 19 | this.route.params.subscribe( 20 | (params: Params) => { 21 | this.taskId = params.taskId; 22 | this.listId = params.listId; 23 | } 24 | ) 25 | } 26 | 27 | updateTask(title: string) { 28 | this.taskService.updateTask(this.listId, this.taskId, title).subscribe(() => { 29 | this.router.navigate(['/lists', this.listId]); 30 | }) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/app/pages/login-page/login-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Login

5 | 6 |
7 |

8 | 9 | 10 | 11 | 12 |

13 |
14 |
15 |

16 | 17 | 18 | 19 | 20 |

21 |
22 |
23 |

24 | 27 |

28 |
29 | 30 |
31 | 32 |

Not got an account? Sign up now!

33 | 34 |
35 |
-------------------------------------------------------------------------------- /frontend/src/app/pages/login-page/login-page.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devstackr/task-manager-mean-stack/548d6f69cd4b226dc85cbacd85ede9cfdbc69131/frontend/src/app/pages/login-page/login-page.component.scss -------------------------------------------------------------------------------- /frontend/src/app/pages/login-page/login-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginPageComponent } from './login-page.component'; 4 | 5 | describe('LoginPageComponent', () => { 6 | let component: LoginPageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LoginPageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoginPageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/pages/login-page/login-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AuthService } from 'src/app/auth.service'; 3 | import { HttpResponse } from '@angular/common/http'; 4 | import { Router } from '@angular/router'; 5 | 6 | @Component({ 7 | selector: 'app-login-page', 8 | templateUrl: './login-page.component.html', 9 | styleUrls: ['./login-page.component.scss'] 10 | }) 11 | export class LoginPageComponent implements OnInit { 12 | 13 | constructor(private authService: AuthService, private router: Router) { } 14 | 15 | ngOnInit() { 16 | } 17 | 18 | onLoginButtonClicked(email: string, password: string) { 19 | this.authService.login(email, password).subscribe((res: HttpResponse) => { 20 | if (res.status === 200) { 21 | // we have logged in successfully 22 | this.router.navigate(['/lists']); 23 | } 24 | console.log(res); 25 | 26 | }); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/app/pages/new-list/new-list.component.html: -------------------------------------------------------------------------------- 1 |
2 | 16 |
-------------------------------------------------------------------------------- /frontend/src/app/pages/new-list/new-list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devstackr/task-manager-mean-stack/548d6f69cd4b226dc85cbacd85ede9cfdbc69131/frontend/src/app/pages/new-list/new-list.component.scss -------------------------------------------------------------------------------- /frontend/src/app/pages/new-list/new-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NewListComponent } from './new-list.component'; 4 | 5 | describe('NewListComponent', () => { 6 | let component: NewListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NewListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NewListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/pages/new-list/new-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { TaskService } from 'src/app/task.service'; 3 | import { Router } from '@angular/router'; 4 | import { List } from 'src/app/models/list.model'; 5 | 6 | @Component({ 7 | selector: 'app-new-list', 8 | templateUrl: './new-list.component.html', 9 | styleUrls: ['./new-list.component.scss'] 10 | }) 11 | export class NewListComponent implements OnInit { 12 | 13 | constructor(private taskService: TaskService, private router: Router) { } 14 | 15 | ngOnInit() { 16 | } 17 | 18 | createList(title: string) { 19 | this.taskService.createList(title).subscribe((list: List) => { 20 | console.log(list); 21 | // Now we navigate to /lists/task._id 22 | this.router.navigate([ '/lists', list._id ]); 23 | }); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/app/pages/new-task/new-task.component.html: -------------------------------------------------------------------------------- 1 |
2 | 16 |
-------------------------------------------------------------------------------- /frontend/src/app/pages/new-task/new-task.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devstackr/task-manager-mean-stack/548d6f69cd4b226dc85cbacd85ede9cfdbc69131/frontend/src/app/pages/new-task/new-task.component.scss -------------------------------------------------------------------------------- /frontend/src/app/pages/new-task/new-task.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NewTaskComponent } from './new-task.component'; 4 | 5 | describe('NewTaskComponent', () => { 6 | let component: NewTaskComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NewTaskComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NewTaskComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/pages/new-task/new-task.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { TaskService } from 'src/app/task.service'; 3 | import { ActivatedRoute, Params, Router } from '@angular/router'; 4 | import { Task } from 'src/app/models/task.model'; 5 | 6 | @Component({ 7 | selector: 'app-new-task', 8 | templateUrl: './new-task.component.html', 9 | styleUrls: ['./new-task.component.scss'] 10 | }) 11 | export class NewTaskComponent implements OnInit { 12 | 13 | constructor(private taskService: TaskService, private route: ActivatedRoute, private router: Router) { } 14 | 15 | listId: string; 16 | 17 | ngOnInit() { 18 | this.route.params.subscribe( 19 | (params: Params) => { 20 | this.listId = params['listId']; 21 | } 22 | ) 23 | } 24 | 25 | createTask(title: string) { 26 | this.taskService.createTask(title, this.listId).subscribe((newTask: Task) => { 27 | this.router.navigate(['../'], { relativeTo: this.route }); 28 | }) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/app/pages/signup-page/signup-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Sign up

5 | 6 |
7 |

8 | 9 | 10 | 11 | 12 |

13 |
14 |
15 |

16 | 17 | 18 | 19 | 20 |

21 |
22 |
23 |

24 | 27 |

28 |
29 |
30 | 31 |

Already got an account? Login now!

32 | 33 |
34 |
-------------------------------------------------------------------------------- /frontend/src/app/pages/signup-page/signup-page.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devstackr/task-manager-mean-stack/548d6f69cd4b226dc85cbacd85ede9cfdbc69131/frontend/src/app/pages/signup-page/signup-page.component.scss -------------------------------------------------------------------------------- /frontend/src/app/pages/signup-page/signup-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SignupPageComponent } from './signup-page.component'; 4 | 5 | describe('SignupPageComponent', () => { 6 | let component: SignupPageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SignupPageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SignupPageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/pages/signup-page/signup-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { HttpResponse } from '@angular/common/http'; 3 | import { AuthService } from 'src/app/auth.service'; 4 | import { Router } from '@angular/router'; 5 | 6 | @Component({ 7 | selector: 'app-signup-page', 8 | templateUrl: './signup-page.component.html', 9 | styleUrls: ['./signup-page.component.scss'] 10 | }) 11 | export class SignupPageComponent implements OnInit { 12 | 13 | constructor(private authService: AuthService, private router: Router) { } 14 | 15 | ngOnInit() { 16 | } 17 | 18 | onSignupButtonClicked(email: string, password: string) { 19 | this.authService.signup(email, password).subscribe((res: HttpResponse) => { 20 | console.log(res); 21 | this.router.navigate(['/lists']); 22 | }); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/app/pages/task-view/task-view.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 19 | 20 |
21 |
22 |
23 |

24 | Tasks 25 |

26 | 45 |
46 | 47 | 48 |
49 |
50 |

{{ task.title }}

51 |
52 |
53 | 54 | 59 | 60 | 65 | 66 |
67 |
68 | 69 |

There are no tasks here! Click the add button to create 70 | a new task.

71 | 72 | 75 |
76 | 77 |
78 |

Please select a list from the sidebar

79 |
80 | 81 |
82 | 83 |
84 |
-------------------------------------------------------------------------------- /frontend/src/app/pages/task-view/task-view.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../main-styles.scss'; 2 | 3 | .task-manager-container { 4 | display: flex; 5 | width: 100%; 6 | height: 100%; 7 | max-width: 1000px; 8 | max-height: 680px; 9 | } 10 | 11 | .sidebar { 12 | display: flex; 13 | flex-direction: column; 14 | 15 | width: 250px; 16 | background: white; 17 | 18 | padding: 42px; 19 | 20 | border-top-left-radius: 8px; 21 | border-bottom-left-radius: 8px; 22 | } 23 | 24 | .task-list-container { 25 | position: relative; 26 | display: flex; 27 | flex-direction: column; 28 | flex-grow: 1; 29 | padding: 42px; 30 | 31 | border-top-right-radius: 8px; 32 | border-bottom-right-radius: 8px; 33 | 34 | 35 | .top-bar { 36 | margin-bottom: 20px; 37 | display: flex; 38 | .title { 39 | flex-grow: 1; 40 | margin: 0; 41 | } 42 | } 43 | 44 | } 45 | 46 | .list-menu { 47 | display: flex; 48 | flex-direction: column; 49 | margin-top: 15px; 50 | flex-grow: 1; 51 | 52 | .list-menu-item { 53 | display: flex; 54 | 55 | align-items: center; 56 | padding: 10px 15px; 57 | border-radius: 5px; 58 | 59 | width: 100%; 60 | color: #5f5f5f; 61 | margin-bottom: 5px; 62 | 63 | &:hover { 64 | background: $light; 65 | } 66 | 67 | &.is-active { 68 | background-color: #cff8ef; 69 | color: $dark-green; 70 | } 71 | } 72 | } 73 | 74 | .task { 75 | display: flex; 76 | background: white; 77 | padding: 15px 20px; 78 | border-radius: 5px; 79 | margin-top: 10px; 80 | 81 | align-items: center; 82 | 83 | &:not(.complete) { 84 | cursor: pointer; 85 | transition: box-shadow 0.2s ease, transform 0.2s ease; 86 | 87 | &:active { 88 | transform: scale(0.95); 89 | } 90 | 91 | &:hover { 92 | box-shadow: 0 0 0 5px #eeeeee; 93 | 94 | .task-buttons { 95 | opacity: 1; 96 | transform: scale(1); 97 | } 98 | } 99 | } 100 | 101 | &.completed .task-text { 102 | text-decoration: line-through; 103 | opacity: 0.8; 104 | } 105 | 106 | 107 | .task-text { 108 | display: flex; 109 | flex-grow: 1; 110 | } 111 | 112 | .task-buttons { 113 | transition: opacity 0.2s, transform 0.2s; 114 | opacity: 0; 115 | transform: scale(0.85); 116 | } 117 | } 118 | 119 | .circle-add-button { 120 | position: absolute; 121 | bottom: 35px; 122 | right: 35px; 123 | height: 50px; 124 | width: 50px; 125 | border-radius: 50%; 126 | 127 | } 128 | 129 | .empty-state-text { 130 | font-size: 20px; 131 | color: #777; 132 | text-align: center; 133 | } 134 | 135 | -------------------------------------------------------------------------------- /frontend/src/app/pages/task-view/task-view.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TaskViewComponent } from './task-view.component'; 4 | 5 | describe('TaskViewComponent', () => { 6 | let component: TaskViewComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ TaskViewComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TaskViewComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/pages/task-view/task-view.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { TaskService } from 'src/app/task.service'; 3 | import { ActivatedRoute, Params, Router } from '@angular/router'; 4 | import { Task } from 'src/app/models/task.model'; 5 | import { List } from 'src/app/models/list.model'; 6 | 7 | @Component({ 8 | selector: 'app-task-view', 9 | templateUrl: './task-view.component.html', 10 | styleUrls: ['./task-view.component.scss'] 11 | }) 12 | export class TaskViewComponent implements OnInit { 13 | 14 | lists: List[]; 15 | tasks: Task[]; 16 | 17 | selectedListId: string; 18 | 19 | constructor(private taskService: TaskService, private route: ActivatedRoute, private router: Router) { } 20 | 21 | ngOnInit() { 22 | this.route.params.subscribe( 23 | (params: Params) => { 24 | if (params.listId) { 25 | this.selectedListId = params.listId; 26 | this.taskService.getTasks(params.listId).subscribe((tasks: Task[]) => { 27 | this.tasks = tasks; 28 | }) 29 | } else { 30 | this.tasks = undefined; 31 | } 32 | } 33 | ) 34 | 35 | this.taskService.getLists().subscribe((lists: List[]) => { 36 | this.lists = lists; 37 | }) 38 | 39 | } 40 | 41 | onTaskClick(task: Task) { 42 | // we want to set the task to completed 43 | this.taskService.complete(task).subscribe(() => { 44 | // the task has been set to completed successfully 45 | console.log("Completed successully!"); 46 | task.completed = !task.completed; 47 | }) 48 | } 49 | 50 | onDeleteListClick() { 51 | this.taskService.deleteList(this.selectedListId).subscribe((res: any) => { 52 | this.router.navigate(['/lists']); 53 | console.log(res); 54 | }) 55 | } 56 | 57 | onDeleteTaskClick(id: string) { 58 | this.taskService.deleteTask(this.selectedListId, id).subscribe((res: any) => { 59 | this.tasks = this.tasks.filter(val => val._id !== id); 60 | console.log(res); 61 | }) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /frontend/src/app/task.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { TaskService } from './task.service'; 4 | 5 | describe('TaskService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: TaskService = TestBed.get(TaskService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/app/task.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { WebRequestService } from './web-request.service'; 3 | import { Task } from './models/task.model'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class TaskService { 9 | 10 | constructor(private webReqService: WebRequestService) { } 11 | 12 | 13 | getLists() { 14 | return this.webReqService.get('lists'); 15 | } 16 | 17 | createList(title: string) { 18 | // We want to send a web request to create a list 19 | return this.webReqService.post('lists', { title }); 20 | } 21 | 22 | updateList(id: string, title: string) { 23 | // We want to send a web request to update a list 24 | return this.webReqService.patch(`lists/${id}`, { title }); 25 | } 26 | 27 | updateTask(listId: string, taskId: string, title: string) { 28 | // We want to send a web request to update a list 29 | return this.webReqService.patch(`lists/${listId}/tasks/${taskId}`, { title }); 30 | } 31 | 32 | deleteTask(listId: string, taskId: string) { 33 | return this.webReqService.delete(`lists/${listId}/tasks/${taskId}`); 34 | } 35 | 36 | deleteList(id: string) { 37 | return this.webReqService.delete(`lists/${id}`); 38 | } 39 | 40 | getTasks(listId: string) { 41 | return this.webReqService.get(`lists/${listId}/tasks`); 42 | } 43 | 44 | createTask(title: string, listId: string) { 45 | // We want to send a web request to create a task 46 | return this.webReqService.post(`lists/${listId}/tasks`, { title }); 47 | } 48 | 49 | complete(task: Task) { 50 | return this.webReqService.patch(`lists/${task._listId}/tasks/${task._id}`, { 51 | completed: !task.completed 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/app/web-req.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpInterceptor, HttpRequest, HttpHandler, HttpErrorResponse } from '@angular/common/http'; 3 | import { Observable, throwError, empty, Subject } from 'rxjs'; 4 | import { AuthService } from './auth.service'; 5 | import { catchError, tap, switchMap } from 'rxjs/operators'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class WebReqInterceptor implements HttpInterceptor { 11 | 12 | constructor(private authService: AuthService) { } 13 | 14 | refreshingAccessToken: boolean; 15 | 16 | accessTokenRefreshed: Subject = new Subject(); 17 | 18 | 19 | intercept(request: HttpRequest, next: HttpHandler): Observable { 20 | // Handle the request 21 | request = this.addAuthHeader(request); 22 | 23 | // call next() and handle the response 24 | return next.handle(request).pipe( 25 | catchError((error: HttpErrorResponse) => { 26 | console.log(error); 27 | 28 | if (error.status === 401) { 29 | // 401 error so we are unauthorized 30 | 31 | // refresh the access token 32 | return this.refreshAccessToken() 33 | .pipe( 34 | switchMap(() => { 35 | request = this.addAuthHeader(request); 36 | return next.handle(request); 37 | }), 38 | catchError((err: any) => { 39 | console.log(err); 40 | this.authService.logout(); 41 | return empty(); 42 | }) 43 | ) 44 | } 45 | 46 | return throwError(error); 47 | }) 48 | ) 49 | } 50 | 51 | refreshAccessToken() { 52 | if (this.refreshingAccessToken) { 53 | return new Observable(observer => { 54 | this.accessTokenRefreshed.subscribe(() => { 55 | // this code will run when the access token has been refreshed 56 | observer.next(); 57 | observer.complete(); 58 | }) 59 | }) 60 | } else { 61 | this.refreshingAccessToken = true; 62 | // we want to call a method in the auth service to send a request to refresh the access token 63 | return this.authService.getNewAccessToken().pipe( 64 | tap(() => { 65 | console.log("Access Token Refreshed!"); 66 | this.refreshingAccessToken = false; 67 | this.accessTokenRefreshed.next(); 68 | }) 69 | ) 70 | } 71 | 72 | } 73 | 74 | 75 | addAuthHeader(request: HttpRequest) { 76 | // get the access token 77 | const token = this.authService.getAccessToken(); 78 | 79 | if (token) { 80 | // append the access token to the request header 81 | return request.clone({ 82 | setHeaders: { 83 | 'x-access-token': token 84 | } 85 | }) 86 | } 87 | return request; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /frontend/src/app/web-request.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { WebRequestService } from './web-request.service'; 4 | 5 | describe('WebRequestService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: WebRequestService = TestBed.get(WebRequestService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/app/web-request.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class WebRequestService { 8 | 9 | readonly ROOT_URL; 10 | 11 | constructor(private http: HttpClient) { 12 | this.ROOT_URL = 'http://localhost:3000'; 13 | } 14 | 15 | get(uri: string) { 16 | return this.http.get(`${this.ROOT_URL}/${uri}`); 17 | } 18 | 19 | post(uri: string, payload: Object) { 20 | return this.http.post(`${this.ROOT_URL}/${uri}`, payload); 21 | } 22 | 23 | patch(uri: string, payload: Object) { 24 | return this.http.patch(`${this.ROOT_URL}/${uri}`, payload); 25 | } 26 | 27 | delete(uri: string) { 28 | return this.http.delete(`${this.ROOT_URL}/${uri}`); 29 | } 30 | 31 | login(email: string, password: string) { 32 | return this.http.post(`${this.ROOT_URL}/users/login`, { 33 | email, 34 | password 35 | }, { 36 | observe: 'response' 37 | }); 38 | } 39 | 40 | signup(email: string, password: string) { 41 | return this.http.post(`${this.ROOT_URL}/users`, { 42 | email, 43 | password 44 | }, { 45 | observe: 'response' 46 | }); 47 | } 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devstackr/task-manager-mean-stack/548d6f69cd4b226dc85cbacd85ede9cfdbc69131/frontend/src/assets/.gitkeep -------------------------------------------------------------------------------- /frontend/src/assets/plus-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | -------------------------------------------------------------------------------- /frontend/src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devstackr/task-manager-mean-stack/548d6f69cd4b226dc85cbacd85ede9cfdbc69131/frontend/src/favicon.ico -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Frontend 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /frontend/src/main-styles.scss: -------------------------------------------------------------------------------- 1 | // This is where we put global styles and change variable values 2 | 3 | // Import Fonts (Lato and Nunito) 4 | @import url('https://fonts.googleapis.com/css?family=Lato:400,700|Nunito:400,700'); 5 | 6 | $turquoise: #44ddd0; 7 | $green: #27d7a1; 8 | $light-green: #45f69e; 9 | $dark-green: rgb(0, 100, 80); 10 | 11 | $light: #f8f8f8; 12 | 13 | 14 | // Update Bulma's global variables 15 | $family-sans-serif: "Nunito", sans-serif; 16 | $primary: $green; 17 | 18 | $link: $turquoise; 19 | 20 | 21 | // Update some of Bulma's component variables 22 | 23 | $control-border-width: 2px; 24 | $input-border-color: transparent; 25 | $input-shadow: none; 26 | 27 | 28 | // Set custom global styles (will be applied to whole app) 29 | 30 | html { 31 | background: linear-gradient(to right top, $turquoise, $green); 32 | } 33 | 34 | html, body { 35 | height: 100%; 36 | } 37 | 38 | .centered-content { 39 | display: flex; 40 | justify-content: center; 41 | align-items: center; 42 | width: 100%; 43 | height: 100%; 44 | } 45 | 46 | .modal-box { 47 | min-height: 200px; 48 | width: 100%; 49 | max-width: 580px; 50 | background: white; 51 | border-radius: 5px; 52 | padding: 35px; 53 | } 54 | 55 | .title { 56 | font-family: 'Lato'; 57 | font-size: 24px; 58 | text-transform: uppercase; 59 | letter-spacing: 2px; 60 | } 61 | 62 | .white-box { 63 | padding: 35px; 64 | background: white; 65 | border-radius: 5px; 66 | 67 | width: 350px; 68 | max-width: 350px; 69 | 70 | } -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10, IE11, and Chrome <55 requires all of the following polyfills. 22 | * This also includes Android Emulators with older versions of Chrome and Google Search/Googlebot 23 | */ 24 | 25 | // import 'core-js/es6/symbol'; 26 | // import 'core-js/es6/object'; 27 | // import 'core-js/es6/function'; 28 | // import 'core-js/es6/parse-int'; 29 | // import 'core-js/es6/parse-float'; 30 | // import 'core-js/es6/number'; 31 | // import 'core-js/es6/math'; 32 | // import 'core-js/es6/string'; 33 | // import 'core-js/es6/date'; 34 | // import 'core-js/es6/array'; 35 | // import 'core-js/es6/regexp'; 36 | // import 'core-js/es6/map'; 37 | // import 'core-js/es6/weak-map'; 38 | // import 'core-js/es6/set'; 39 | 40 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 41 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 42 | 43 | /** IE10 and IE11 requires the following for the Reflect API. */ 44 | // import 'core-js/es6/reflect'; 45 | 46 | /** 47 | * Web Animations `@angular/platform-browser/animations` 48 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 49 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 50 | */ 51 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 52 | 53 | /** 54 | * By default, zone.js will patch all possible macroTask and DomEvents 55 | * user can disable parts of macroTask/DomEvents patch by setting following flags 56 | * because those flags need to be set before `zone.js` being loaded, and webpack 57 | * will put import in the top of bundle, so user need to create a separate file 58 | * in this directory (for example: zone-flags.ts), and put the following flags 59 | * into that file, and then add the following code before importing zone.js. 60 | * import './zone-flags.ts'; 61 | * 62 | * The flags allowed in zone-flags.ts are listed here. 63 | * 64 | * The following flags will work for all browsers. 65 | * 66 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 67 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 68 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 69 | * 70 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 71 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 72 | * 73 | * (window as any).__Zone_enable_cross_context_check = true; 74 | * 75 | */ 76 | 77 | /*************************************************************************************************** 78 | * Zone JS is required by default for Angular itself. 79 | */ 80 | import 'zone.js/dist/zone'; // Included with Angular CLI. 81 | 82 | 83 | /*************************************************************************************************** 84 | * APPLICATION IMPORTS 85 | */ 86 | -------------------------------------------------------------------------------- /frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | @import 'main-styles'; 4 | @import '../node_modules/bulma/bulma.sass'; -------------------------------------------------------------------------------- /frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "no-output-on-prefix": true, 121 | "use-input-property-decorator": true, 122 | "use-output-property-decorator": true, 123 | "use-host-property-decorator": true, 124 | "no-input-rename": true, 125 | "no-output-rename": true, 126 | "use-life-cycle-interface": true, 127 | "use-pipe-transform-interface": true, 128 | "component-class-suffix": true, 129 | "directive-class-suffix": true 130 | } 131 | } 132 | --------------------------------------------------------------------------------