-
+
@@ -94,6 +96,7 @@ const mapStateToProps = (state) => {
categories: state.expenses.categories,
monthlyTotal: state.expenses.monthlyTotal,
yearlyTotal: state.expenses.yearlyTotal,
+ groups: state.groups.groups,
};
};
@@ -104,6 +107,7 @@ const mapDispatchToProps = (dispatch) => {
getMonthlyTotal: () => dispatch(getMonthlyTotal()),
getYearlyTotal: () => dispatch(getYearlyTotal()),
getCategoryAmounts: (options) => dispatch(getCategoryAmounts(options)),
+ getGroups: () => dispatch(getGroups()),
};
};
diff --git a/frontend/src/containers/expenses/expenses.css b/frontend/src/containers/expenses/expenses.css
new file mode 100644
index 0000000..c4eb2bf
--- /dev/null
+++ b/frontend/src/containers/expenses/expenses.css
@@ -0,0 +1,9 @@
+.manage {
+ margin: 0;
+ height: 100%;
+}
+
+.sidebar {
+ border-right: 1px solid rgba(0, 0, 0, 0.125);
+ padding: 0;
+}
diff --git a/frontend/src/containers/expenses/expenses.jsx b/frontend/src/containers/expenses/expenses.jsx
new file mode 100644
index 0000000..6455f45
--- /dev/null
+++ b/frontend/src/containers/expenses/expenses.jsx
@@ -0,0 +1,74 @@
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { Redirect } from 'react-router-dom';
+import {
+ getUserExpenses,
+ refreshExpenses,
+} from '../../store/actions/expensesActions';
+import { getGroups } from '../../store/actions/groupsActions';
+import GroupManager from '../../components/groupManager';
+import ExpensesTable from '../../components/expensesTable';
+
+import './expenses.css';
+import { Row, Col } from 'react-bootstrap';
+
+class Expenses extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ group_id: null,
+ group_name: 'All expenses',
+ };
+
+ this.props.getGroups();
+ }
+
+ onGroupChange = (value) => {
+ this.setState(
+ { group_id: value.id, group_name: value.name },
+ this.props.refreshExpenses
+ );
+ };
+
+ render() {
+ if (!this.props.isAuthenticated) {
+ return
;
+ }
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+ }
+}
+
+const mapStateToProps = (state) => {
+ return {
+ isAuthenticated: state.users.isAuthenticated,
+ groups: state.groups.groups,
+ };
+};
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ getGroups: () => dispatch(getGroups()),
+ getUserExpenses: (options) => dispatch(getUserExpenses(options)),
+ refreshExpenses: () => dispatch(refreshExpenses()),
+ };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(Expenses);
diff --git a/frontend/src/index.js b/frontend/src/index.js
index 11f3a47..dc9a1ac 100644
--- a/frontend/src/index.js
+++ b/frontend/src/index.js
@@ -7,12 +7,14 @@ import { createStore, applyMiddleware, combineReducers } from 'redux';
import usersReducer from './store/reducers/usersReducer';
import expensesReducer from './store/reducers/expensesReducer';
+import groupsReducer from './store/reducers/groupsReducer';
import App from './App';
const rootReducer = combineReducers({
users: usersReducer,
expenses: expensesReducer,
+ groups: groupsReducer,
});
const store = createStore(rootReducer, applyMiddleware(thunk));
diff --git a/frontend/src/store/actions/actionTypes.js b/frontend/src/store/actions/actionTypes.js
index 6db2ec9..19e38af 100644
--- a/frontend/src/store/actions/actionTypes.js
+++ b/frontend/src/store/actions/actionTypes.js
@@ -8,6 +8,7 @@ export const LOGIN_UNSUCCESSFUL = 'LOGIN_UNSUCCESSFUL';
export const LOGOUT_USER = 'LOGOUT_USER';
// Expenses
+export const REFRESH_EXPENSES = 'REFRESH_EXPENSES'
export const GET_USER_EXPENSES = 'GET_USER_EXPENSES';
export const GET_USER_CATEGORIES = 'GET_USER_CATEGORIES';
export const ADD_NEW_EXPENSE = 'ADD_NEW_EXPENSE';
@@ -16,3 +17,10 @@ export const DELETE_EXPENSE = 'DELETE_EXPENSE';
export const GET_MONTHLY_TOTAL = 'GET_MONTHLY_TOTAL';
export const GET_YEARLY_TOTAL = 'GET_YEARLY_TOTAL';
export const GET_CATEGORY_AMOUNTS = 'GET_CATEGORY_AMOUNTS';
+
+// Groups
+export const GET_GROUPS = 'GET_GROUPS';
+export const ADD_GROUP = 'ADD_GROUP';
+export const EDIT_GROUP = 'EDIT_GROUP';
+export const DELETE_GROUP = 'DELETE_GROUP';
+export const SET_DEFAULT_GROUP = 'SET_DEFAULT_GROUP';
diff --git a/frontend/src/store/actions/expensesActions.js b/frontend/src/store/actions/expensesActions.js
index 32b1912..2b21b51 100644
--- a/frontend/src/store/actions/expensesActions.js
+++ b/frontend/src/store/actions/expensesActions.js
@@ -4,6 +4,14 @@ import axiosAPI from '../../helpers/axiosAPI';
import querystring from 'querystring';
import moment from 'moment';
+export const refreshExpenses = () => {
+ return (dispatch) => {
+ dispatch({
+ type: actionTypes.REFRESH_EXPENSES,
+ });
+ };
+};
+
/**
* Get the expenses for the current user that follow the specified options
* like sort order, sort criteria, start date, end date, etc.
@@ -84,7 +92,7 @@ export const editExpense = (expense) => {
if (!token) return;
return axiosAPI
- .put('/expenses/' + expense.id, expense)
+ .put('/expenses/' + expense._id, expense)
.then((res) => {
dispatch({
type: actionTypes.EDIT_EXPENSE,
diff --git a/frontend/src/store/actions/groupsActions.js b/frontend/src/store/actions/groupsActions.js
new file mode 100644
index 0000000..9ad6e09
--- /dev/null
+++ b/frontend/src/store/actions/groupsActions.js
@@ -0,0 +1,111 @@
+import * as actionTypes from './actionTypes';
+import jwt from 'jsonwebtoken';
+import axiosAPI from '../../helpers/axiosAPI';
+
+export const getGroups = () => {
+ return (dispatch) => {
+ const token = localStorage.getItem('accessToken');
+ if (!token) return;
+ const username = jwt.decode(token).user.username;
+
+ return axiosAPI
+ .get('/users/' + username + '/groups')
+ .then((res) => {
+ dispatch({
+ type: actionTypes.GET_GROUPS,
+ groups: res.data.groups,
+ });
+ })
+ .catch((err) => {
+ // TODO
+ });
+ };
+};
+
+export const addGroup = (group_name) => {
+ return (dispatch) => {
+ const token = localStorage.getItem('accessToken');
+ if (!token) return;
+ const username = jwt.decode(token).user.username;
+
+ return axiosAPI
+ .get('/users/' + username)
+ .then((res) => {
+ return axiosAPI
+ .post('/groups/', { user_id: res.data._id, name: group_name })
+ .then((res) => {
+ dispatch({
+ type: actionTypes.ADD_GROUP,
+ });
+ })
+ .catch((err) => {
+ // TODO
+ });
+ })
+ .catch((err) => {
+ // TODO
+ });
+ };
+};
+
+export const editGroup = (group) => {
+ return (dispatch) => {
+ const token = localStorage.getItem('accessToken');
+ if (!token) return;
+
+ return axiosAPI
+ .put('/groups/' + group._id, { name: group.name })
+ .then((res) => {
+ dispatch({
+ type: actionTypes.EDIT_GROUP,
+ });
+ })
+ .catch((err) => {
+ // TODO
+ });
+ };
+};
+
+export const deleteGroup = (group_id) => {
+ return (dispatch) => {
+ const token = localStorage.getItem('accessToken');
+ if (!token) return;
+
+ return axiosAPI
+ .delete('/groups/' + group_id)
+ .then((res) => {
+ dispatch({
+ type: actionTypes.DELETE_GROUP,
+ });
+ })
+ .catch((err) => {
+ // TODO
+ });
+ };
+};
+
+export const setDefaultGroup = (group_id) => {
+ return (dispatch) => {
+ const token = localStorage.getItem('accessToken');
+ if (!token) return;
+ const username = jwt.decode(token).user.username;
+
+ return axiosAPI
+ .get('/users/' + username)
+ .then((res) => {
+ return axiosAPI
+ .put('/users/' + res.data._id, { default_group_id: group_id })
+ .then((res) => {
+ dispatch({
+ type: actionTypes.SET_DEFAULT_GROUP,
+ });
+ })
+ .catch((err) => {
+ // TODO
+ });
+ })
+ .catch((err) => {
+ // TODO
+ });
+ };
+};
diff --git a/frontend/src/store/actions/usersActions.js b/frontend/src/store/actions/usersActions.js
index 6a74bb5..6cb79d7 100644
--- a/frontend/src/store/actions/usersActions.js
+++ b/frontend/src/store/actions/usersActions.js
@@ -31,8 +31,6 @@ export const userRegisterRequest = (userInfo) => {
};
export const userLoginRequest = (userLogin) => {
- console.log('login requested');
-
return (dispatch) => {
axiosAuth
.post('/login', userLogin)
@@ -60,7 +58,6 @@ export const userLoginRequest = (userLogin) => {
};
export const userLogoutRequest = () => {
- console.log('logout requested');
return (dispatch) => {
axiosAuth
.delete('/logout', {
diff --git a/frontend/src/store/reducers/expensesReducer.js b/frontend/src/store/reducers/expensesReducer.js
index 080945f..312b088 100644
--- a/frontend/src/store/reducers/expensesReducer.js
+++ b/frontend/src/store/reducers/expensesReducer.js
@@ -22,6 +22,13 @@ const reducer = (state = initialState, action) => {
totalPages: action.totalPages,
};
}
+ case actionTypes.REFRESH_EXPENSES: {
+ return {
+ ...state,
+ update: state.update + 1,
+ updateAction: 'refresh',
+ };
+ }
case actionTypes.ADD_NEW_EXPENSE: {
return {
...state,
diff --git a/frontend/src/store/reducers/groupsReducer.js b/frontend/src/store/reducers/groupsReducer.js
new file mode 100644
index 0000000..12fe5f8
--- /dev/null
+++ b/frontend/src/store/reducers/groupsReducer.js
@@ -0,0 +1,35 @@
+import * as actionTypes from '../actions/actionTypes';
+
+const initialState = {
+ groups: null,
+};
+
+const reducer = (state = initialState, action) => {
+ switch (action.type) {
+ case actionTypes.GET_GROUPS: {
+ return {
+ ...state,
+ groups: action.groups,
+ };
+ }
+ case actionTypes.EDIT_GROUP: {
+ return state;
+ }
+ case actionTypes.SET_DEFAULT_GROUP: {
+ return state;
+ }
+ case actionTypes.ADD_GROUP: {
+ return state;
+ }
+ case actionTypes.DELETE_GROUP: {
+ return state;
+ }
+ case actionTypes.LOGOUT_USER: {
+ return initialState;
+ }
+ default:
+ return state;
+ }
+};
+
+export default reducer;
diff --git a/models/expenseModel.js b/models/expenseModel.js
index d385969..dba8455 100644
--- a/models/expenseModel.js
+++ b/models/expenseModel.js
@@ -21,6 +21,13 @@ const expenseSchema = new Schema(
},
category: {
type: String,
+ required: true,
+ },
+ group_id: {
+ type: mongoose.Types.ObjectId,
+ },
+ description: {
+ type: String,
},
},
{
diff --git a/models/groupModel.js b/models/groupModel.js
new file mode 100644
index 0000000..d9f46ee
--- /dev/null
+++ b/models/groupModel.js
@@ -0,0 +1,24 @@
+const mongoose = require('mongoose');
+const Schema = mongoose.Schema;
+
+const groupSchema = new Schema(
+ {
+ user_id: {
+ type: mongoose.Types.ObjectId,
+ required: true,
+ },
+ name: {
+ type: String,
+ required: true,
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+groupSchema.index({ user_id: 1, name: 1 }, { unique: true });
+
+const Group = mongoose.model('groups', groupSchema);
+
+module.exports = Group;
diff --git a/models/userModel.js b/models/userModel.js
index a6d3fbc..fce5323 100644
--- a/models/userModel.js
+++ b/models/userModel.js
@@ -1,4 +1,4 @@
-const mongoose = require("mongoose");
+const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const userSchema = new Schema(
@@ -25,6 +25,9 @@ const userSchema = new Schema(
},
},
},
+ default_group_id: {
+ type: mongoose.Types.ObjectId,
+ },
account_type: {
type: String,
lowercase: true,
@@ -40,6 +43,6 @@ const userSchema = new Schema(
}
);
-const User = mongoose.model("users", userSchema);
+const User = mongoose.model('users', userSchema);
module.exports = User;
diff --git a/routes/expenses.js b/routes/expenses.js
index 0da24c5..0f94c04 100644
--- a/routes/expenses.js
+++ b/routes/expenses.js
@@ -1,26 +1,23 @@
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
+const { auth, authAdmin } = require('./middleware/auth');
const { check, validationResult, query } = require('express-validator');
const User = require('../models/userModel');
const Expense = require('../models/expenseModel');
+const Group = require('../models/groupModel');
const validateGetExpenses = [
check('sort', 'Invalid sort option')
.optional()
- .isIn(['title', 'date', 'amount', 'category', 'user_id'])
- .not()
- .isArray(),
+ .isIn(['title', 'date', 'amount', 'category', 'user_id']),
check('direction', 'Invalid direction option')
.optional()
- .isIn(['asc', 'dsc'])
- .not()
- .isArray(),
- check(['start_date', 'end_date'], 'Invalid date format')
- .optional()
- .not()
- .isArray(),
+ .isIn(['asc', 'dsc']),
+ check(['start_date', 'end_date'], 'Invalid date format').optional(),
+ check('search').optional().isString(),
+ check('group_id').optional().isMongoId(),
check('per_page', 'Invalid number').optional().isInt({ min: 1, max: 100 }),
check('page', 'Invalid number').optional().isInt({ min: 1 }),
];
@@ -76,6 +73,11 @@ getExpenses = async (req, res) => {
options.skip = (req.query.page - 1) * options.limit;
}
+ // Group
+ if (req.query.group_id) {
+ query.group_id = req.query.group_id;
+ }
+
// handle username
if (req.params.username) {
query.user_id = req.user._id;
@@ -194,14 +196,20 @@ getExpenseCategories = async (req, res) => {
};
// Get all expenses
-router.get('/', validateGetExpenses, getExpenses);
+router.get('/', authAdmin, validateGetExpenses, getExpenses);
// Get all expense categories
-router.get('/categories', validateGetExpenseCategories, getExpenseCategories);
+router.get(
+ '/categories',
+ authAdmin,
+ validateGetExpenseCategories,
+ getExpenseCategories
+);
// Get details of a single expense
router.get(
'/:expense_id',
+ auth,
[check('expense_id', 'Invalid expense ID').isMongoId()],
(req, res) => {
let err = validationResult(req);
@@ -220,31 +228,36 @@ router.get(
// Create new expense
router.post(
'/',
+ auth,
[
check('user_id', 'User ID must be an ObjectID').isMongoId(),
check('title', 'Title is required').notEmpty().isString(),
check('amount', 'Amount must be a float').isFloat(),
check('date', 'Incorrect date format').isDate(),
check('category').notEmpty().isString(),
+ check('group_id').optional().isMongoId(),
+ check('description').optional().isString(),
],
(req, res) => {
let err = validationResult(req);
if (!err.isEmpty()) {
res.status(400).json(err.errors);
} else {
- Expense.create(
- {
- user_id: mongoose.Types.ObjectId(req.body.user_id),
- title: req.body.title,
- amount: req.body.amount,
- date: req.body.date,
- category: req.body.category,
- },
- (err) => {
- if (err) throw err;
- res.status(201).json({ message: 'Expense created' });
- }
- );
+ let query = {
+ user_id: mongoose.Types.ObjectId(req.body.user_id),
+ title: req.body.title,
+ amount: req.body.amount,
+ date: req.body.date,
+ category: req.body.category,
+ };
+ if (!req.body.group_id && req.user.default_group_id)
+ query.group_id = req.user.default_group_id;
+ else if (req.body.group_id) query.group_id = req.body.group_id;
+ if (req.body.description) query.description = req.body.description;
+ Expense.create(query, (err) => {
+ if (err) throw err;
+ res.status(201).json({ message: 'Expense created' });
+ });
}
}
);
@@ -259,6 +272,8 @@ router.put(
check('amount', 'Amount must be a float').optional().isFloat(),
check('date', 'Incorrect date format').optional().isDate(),
check('category').optional().notEmpty().isString(),
+ check('group_id').optional().isMongoId(),
+ check('description').optional().isString(),
],
(req, res) => {
let err = validationResult(req);
@@ -281,6 +296,7 @@ router.put(
// Delete an expense
router.delete(
'/:expense_id',
+ auth,
[check('expense_id', 'Expense ID must be an ObjectID').isMongoId()],
(req, res) => {
let err = validationResult(req);
diff --git a/routes/groups.js b/routes/groups.js
new file mode 100644
index 0000000..190823e
--- /dev/null
+++ b/routes/groups.js
@@ -0,0 +1,148 @@
+const express = require('express');
+const router = express.Router();
+const mongoose = require('mongoose');
+const { auth, authAdmin } = require('./middleware/auth');
+const { check, validationResult, query } = require('express-validator');
+
+const Group = require('../models/groupModel');
+const User = require('../models/userModel');
+const Expense = require('../models/expenseModel');
+
+getGroups = (req, res) => {
+ let err = validationResult(req);
+ if (!err.isEmpty()) return res.status(400).json(err.errors);
+
+ let query = {};
+ if (req.params.username) query.user_id = req.user._id;
+
+ Group.find(query, null, { sort: { name: 1 } }, (err, groups) => {
+ if (err) throw err;
+ res.status(200).json({
+ groups: groups.map((group) => {
+ return {
+ ...group.toObject(),
+ default: String(group._id) === String(req.user.default_group_id),
+ };
+ }),
+ });
+ });
+};
+
+/**
+ * Get all groups
+ */
+router.get('/', authAdmin, getGroups);
+
+/**
+ * Create a new group
+ */
+router.post(
+ '/',
+ auth,
+ [check('user_id').isMongoId(), check('name').notEmpty().isString()],
+ (req, res) => {
+ let err = validationResult(req);
+ if (!err.isEmpty()) return res.status(400).json(err.errors);
+
+ Group.create(
+ {
+ user_id: req.body.user_id,
+ name: req.body.name,
+ },
+ (err, group) => {
+ if (err) {
+ if (err.code === 11000)
+ res.status(409).json({ message: 'Group name already exists' });
+ else throw err;
+ }
+ if (!req.user.default_group_id) {
+ // make the created group the default for the user if they do not have a default
+ User.findOneAndUpdate(
+ { _id: group.user_id },
+ { default_group_id: group._id },
+ (err, user) => {
+ if (err) throw err;
+ res.sendStatus(200);
+ }
+ );
+ } else {
+ res.sendStatus(200);
+ }
+ }
+ );
+ }
+);
+
+/**
+ * Edit a group
+ */
+router.put(
+ '/:group_id',
+ auth,
+ [check('group_id').isMongoId(), check('name').isString()],
+ (req, res) => {
+ let err = validationResult(req);
+ if (!err.isEmpty()) return res.status(400).json(err.errors);
+
+ Group.findOneAndUpdate(
+ { _id: req.params.group_id },
+ req.body,
+ (err, group) => {
+ if (err) {
+ if (err.code === 11000)
+ res.status(409).json({ message: 'Group name already exists' });
+ else throw err;
+ }
+ if (!group) return res.sendStatus(500);
+ res.sendStatus(200);
+ }
+ );
+ }
+);
+
+/**
+ * Delete an group
+ */
+router.delete(
+ '/:group_id',
+ auth,
+ [check('group_id').isMongoId()],
+ (req, res) => {
+ let err = validationResult(req);
+ if (!err.isEmpty()) return res.status(400).json(err.errors);
+
+ Group.findOneAndDelete({ _id: req.params.group_id }, (err, group) => {
+ if (err) throw err;
+ if (!group) return res.sendStatus(500);
+ if (
+ req.user.default_group_id &&
+ String(req.user.default_group_id) === String(group._id)
+ ) {
+ Group.find({ user_id: group.user_id }, (err, groups) => {
+ if (err) throw err;
+ let defaultQuery;
+ if (groups.length > 0) {
+ // set first result as default group
+ defaultQuery = { default_group_id: groups[0]._id };
+ } else {
+ // remove default group from user
+ defaultQuery = { $unset: { default_group_id: 1 } };
+ }
+
+ User.findOneAndUpdate(
+ { _id: group.user_id },
+ defaultQuery,
+ (err, user) => {
+ if (err) throw err;
+ res.sendStatus(200);
+ }
+ );
+ });
+ } else {
+ res.sendStatus(200);
+ }
+ });
+ }
+);
+
+module.exports = { router: router, getGroups: getGroups };
diff --git a/routes/middleware/auth.js b/routes/middleware/auth.js
index bdbdd51..b40225e 100644
--- a/routes/middleware/auth.js
+++ b/routes/middleware/auth.js
@@ -3,6 +3,7 @@ const config = require('../../config');
const User = require('../../models/userModel');
const Expense = require('../../models/expenseModel');
+const Group = require('../../models/groupModel');
auth = (req, res, next) => {
const authHeader = req.headers['authorization'];
@@ -15,33 +16,66 @@ auth = (req, res, next) => {
if (err) return res.sendStatus(401);
User.findOne({ username: decoded.user.username }, (userErr, user) => {
- Expense.findOne(
- { _id: req.params.expense_id },
- (expenseErr, expense) => {
- if (userErr || expenseErr || !user) return res.sendSatus(401);
+ if (userErr || !user) return res.sendStatus(401);
- // add user from payload
- req.user = user;
+ // add user from payload
+ req.user = user;
- // If username is in the URL, confirm token auth
- if (req.params.username && req.params.username !== user.username)
- return res.sendStatus(401);
+ let queries = [];
+
+ // TODO : use custom sanitizer with express-validator ?
+ if (req.params.expense_id)
+ queries.push(Expense.findOne({ _id: req.params.expense_id }).exec());
+ if (req.params.group_id)
+ queries.push(Group.findOne({ _id: req.params.group_id }).exec());
+ if (req.body.expense_id)
+ queries.push(Expense.findOne({ _id: req.body.expense_id }).exec());
+ if (req.body.group_id)
+ queries.push(Group.findOne({ _id: req.body.group_id }).exec());
+ if (req.body.default_group_id)
+ queries.push(
+ Group.findOne({ _id: req.body.default_group_id }).exec()
+ );
- // If user_id is in the body, confirm token auth
- if (req.body.user_id && req.body.user_id !== user._id)
+ Promise.all(queries).then((results) => {
+ if (results.includes(null)) return res.sendStatus(500);
+ results.forEach((result) => {
+ if (String(result.user_id) !== String(user._id))
return res.sendStatus(401);
+ });
+
+ // If username is in the URL, confirm token auth
+ if (req.params.username && req.params.username !== user.username)
+ return res.sendStatus(401);
+
+ // If user_id is in the body, confirm token auth
+ if (req.body.user_id && String(req.body.user_id) !== String(user._id))
+ return res.sendStatus(401);
+
+ next();
+ });
+ });
+ });
+ } catch (e) {
+ return res.sendStatus(401);
+ }
+};
- // If expense_id is in the URL, confirm token auth (check expense owner)
- if (req.params.expense_id) {
- if (!expense) return res.sendStatus(401);
- if (String(expense.user_id) !== String(user._id)) {
- return res.sendStatus(401);
- }
- }
-
- next();
- }
- );
+authAdmin = (req, res, next) => {
+ const authHeader = req.headers['authorization'];
+ const token = authHeader && authHeader.split(' ')[1];
+ if (token == null) return res.sendStatus(401);
+
+ try {
+ // verify token
+ jwt.verify(token, config.jwt_access_secret, (err, decoded) => {
+ if (err) return res.sendStatus(401);
+
+ User.findOne({ username: decoded.user.username }, (userErr, user) => {
+ if (userErr || !user) return res.sendStatus(401);
+ if (user.account_type !== 'admin' && user.account_type !== 'owner')
+ return res.sendStatus(401);
+ next();
});
});
} catch (e) {
@@ -49,4 +83,4 @@ auth = (req, res, next) => {
}
};
-module.exports = auth;
+module.exports = { auth: auth, authAdmin: authAdmin };
diff --git a/routes/users.js b/routes/users.js
index 033bd50..aa48208 100644
--- a/routes/users.js
+++ b/routes/users.js
@@ -2,7 +2,7 @@ const express = require('express');
const bcrypt = require('bcrypt');
const router = express.Router();
const jwt = require('jsonwebtoken');
-const auth = require('./middleware/auth');
+const { auth, authAdmin } = require('./middleware/auth');
const { check, validationResult } = require('express-validator');
const {
validateGetExpenses,
@@ -10,6 +10,7 @@ const {
validateGetExpenseCategories,
getExpenseCategories,
} = require('./expenses');
+const { getGroups } = require('./groups');
const config = require('../config');
@@ -19,6 +20,7 @@ const Expense = require('../models/expenseModel');
// Get list of users
router.get(
'/',
+ authAdmin,
[
check('type', 'Invalid account type')
.optional()
@@ -152,8 +154,9 @@ router.post(
// Edit user
router.put(
- '/:id',
- [check('id', 'Invalid user ID').isMongoId()],
+ '/:user_id',
+ auth,
+ [check('user_id', 'Invalid user ID').isMongoId()],
async (req, res) => {
let err = validationResult(req);
if (!err.isEmpty()) {
@@ -171,9 +174,17 @@ router.put(
delete req.body['name'];
}
+ if (req.body.account_type) {
+ delete req.body['account_type'];
+ }
+
+ if (req.body.disabled) {
+ delete req.body['disabled'];
+ }
+
User.findOneAndUpdate(
{
- _id: req.params.id,
+ _id: req.params.user_id,
},
req.body,
(err, user) => {
@@ -195,8 +206,9 @@ router.put(
// Delete user
router.delete(
- '/:id',
- [check('id', 'Invalid user ID').isMongoId()],
+ '/:user_id',
+ auth,
+ [check('user_id', 'Invalid user ID').isMongoId()],
(req, res) => {
let err = validationResult(req);
if (!err.isEmpty()) {
@@ -204,7 +216,7 @@ router.delete(
} else {
User.findOneAndUpdate(
{
- _id: req.params.id,
+ _id: req.params.user_id,
},
{
disabled: true,
@@ -237,4 +249,11 @@ router.get(
getExpenseCategories
);
+router.get(
+ '/:username/groups',
+ auth,
+ [check('username').isAlphanumeric()],
+ getGroups
+);
+
module.exports = router;