Skip to content

Commit

Permalink
v1.3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
luwol03 authored Jun 13, 2022
2 parents ac5dbfd + 6dcd5e9 commit ae77ac8
Show file tree
Hide file tree
Showing 41 changed files with 5,503 additions and 2,927 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ vocascan.config.*
!docker/**/vocascan.config.*
docker/**/database
*.log
/staticPages
*.log.gz
/staticPages
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
This changelog goes through all the changes that have been made in each release on the
[vocascan-server](https://github.com/vocascan/vocascan-server).

## [v1.3.0](https://github.com/vocascan/vocascan-server/releases/tag/v1.3.0) - 2022.06.13

In this version of the vocascan-server, we added the function to prevent unverified users from logging in. You can choose between different modes to adjust the security level of your server directly to your intentions.

- Features
- Vocabulary-Search in group route (#85)
- Email verification (#78)
- Refactor
- Email verification adjustment (#86)
- Improved run programmatically (#84)

## [v1.2.1](https://github.com/vocascan/vocascan-server/releases/tag/v1.2.1) - 2022.01.29

In this release of vocascan-server we have added new security features to force secure passwords to the users.
Expand Down
37 changes: 31 additions & 6 deletions app/Controllers/AuthController.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ const {
validatePassword,
destroyUser,
changePassword,
sendAccountVerificationEmail,
} = require('../Services/AuthServiceProvider');
const { useInviteCode } = require('../Services/InviteCodeProvider');
const { generateJWT, deleteKeysFromObject } = require('../utils');
const catchAsync = require('../utils/catchAsync');
const { tokenTypes } = require('../utils/constants');
const ApiError = require('../utils/ApiError');

const register = catchAsync(async (req, res) => {
Expand All @@ -21,32 +23,55 @@ const register = catchAsync(async (req, res) => {
throw new ApiError(httpStatus.BAD_REQUEST, 'password complexity failed', 'password');
}

const user = await createUser(req.body);
const token = generateJWT({ id: user.id }, config.server.jwt_secret);
const user = await createUser({
...req.body,
emailVerified: !config.service.email_confirm,
disabled: false,
});

const token = generateJWT(
{
id: user.id,
type: tokenTypes.ACCESS,
},
config.server.jwt_secret,
{ expiresIn: config.service.access_live_time }
);

// after everything is registered redeem the code
if (config.server.registration_locked) {
if (config.service.invite_code) {
await useInviteCode(req.query.inviteCode);
}

if (config.service.email_confirm) {
await sendAccountVerificationEmail({ ...user, email: req.body.email });
}

res.send({ token, user });
});

const login = catchAsync(async (req, res) => {
validateLogin(req, res);

const user = await loginUser(req.body, res);
const user = await loginUser(req.body);

if (user) {
// generate JWT with userId
const token = generateJWT({ id: user.id }, config.server.jwt_secret);
const token = generateJWT(
{
id: user.id,
type: tokenTypes.ACCESS,
},
config.server.jwt_secret,
{ expiresIn: config.service.access_live_time }
);

res.send({ token, user });
}
});

const profile = catchAsync(async (req, res) => {
res.send(deleteKeysFromObject(['roleId', 'password', 'createdAt', 'updatedAt'], req.user.toJSON()));
res.send(deleteKeysFromObject(['roleId', 'email', 'password', 'updatedAt'], req.user.toJSON()));
});

const deleteUser = catchAsync(async (req, res) => {
Expand Down
7 changes: 6 additions & 1 deletion app/Controllers/InfoController.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ const sendInfo = catchAsync(async (_req, res) => {
res.send({
identifier: 'vocascan-server',
version: getVersion(),
locked: config.server.registration_locked,
locked: config.service.invite_code,
commitRef: gitDescribe === undefined ? (gitDescribe = await getGitDescribe()) : gitDescribe,
email_confirm: {
enabled: config.service.email_confirm,
level: config.service.email_confirm_level,
time: config.service.email_confirm_time,
},
});
});

Expand Down
83 changes: 83 additions & 0 deletions app/Controllers/VerificationController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const httpStatus = require('http-status');
const config = require('../config/config/index.js');
const ApiError = require('../utils/ApiError.js');
const catchAsync = require('../utils/catchAsync.js');
const { verifyJWT, hashEmail } = require('../utils/index.js');
const { verifyUser, sendAccountVerificationEmail } = require('../Services/AuthServiceProvider.js');
const { tokenTypes } = require('../utils/constants.js');
const logger = require('../config/logger');

const requestEmailVerification = catchAsync(async (req, res) => {
if (!config.service.email_confirm) {
throw new ApiError(httpStatus.NOT_FOUND);
}

if (req.user.emailVerified) {
throw new ApiError(httpStatus.GONE, 'User is already verified');
}

if (hashEmail(req.body.email) !== req.user.email) {
throw new ApiError(httpStatus.BAD_REQUEST, 'Email not valid', 'email');
}

await sendAccountVerificationEmail({ ...req.user.toJSON(), email: req.body.email });

res.status(httpStatus.NO_CONTENT).end();
});

const verifyEmail = catchAsync(async (req, res) => {
const { token } = req.query;

if (!token) {
return res.render('accountVerification', {
status: httpStatus.BAD_REQUEST,
error: 'No verification token provided',
});
}

let payload = '';

try {
payload = await verifyJWT(token, config.server.jwt_secret);

if (payload.type !== tokenTypes.VERIFY_EMAIL) {
throw new Error();
}
} catch (error) {
return res.render('accountVerification', {
status: httpStatus.UNAUTHORIZED,
error: 'No valid verification token provided',
});
}

let user = null;

try {
user = await verifyUser({ id: payload.id });
} catch (error) {
if (error instanceof ApiError) {
return res.render('accountVerification', {
status: httpStatus.GONE,
error: 'User is already verified',
});
}

logger.error(error);

return res.render('accountVerification', {
status: httpStatus.INTERNAL_SERVER_ERROR,
error: 'Internal Server Error',
});
}

return res.render('accountVerification', {
status: httpStatus.OK,
error: null,
user,
});
});

module.exports = {
requestEmailVerification,
verifyEmail,
};
3 changes: 2 additions & 1 deletion app/Controllers/VocabularyController.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ const sendGroupVocabulary = catchAsync(async (req, res) => {
// get userId from request
const userId = req.user.id;
const { groupId } = req.params;
const { search } = req.query;

const vocabulary = await getGroupVocabulary(userId, groupId);
const vocabulary = await getGroupVocabulary(userId, groupId, search);

res.send(vocabulary);
});
Expand Down
82 changes: 52 additions & 30 deletions app/Middleware/ProtectMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,64 @@
const { parseTokenUserId } = require('../utils');
const { verifyJWT } = require('../utils');
const { User } = require('../../database');
const ApiError = require('../utils/ApiError.js');
const httpStatus = require('http-status');
const catchAsync = require('../utils/catchAsync');
const config = require('../config/config');
const { tokenTypes } = require('../utils/constants');

// Check for Authorization header and add user attribute to request object
const ProtectMiddleware = catchAsync(async (req, _res, next) => {
// Break if no Authorization header is set
if (!req.header('Authorization')) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Not Authorized');
}

let userId;

try {
// Read userId from token
userId = await parseTokenUserId(req, config.server.jwt_secret);
} catch (err) {
// Handle broken token
throw new ApiError(httpStatus.BAD_REQUEST, 'Invalid auth token');
}

// Get user from database
const user = await User.findOne({
where: {
id: userId,
},
});
const ProtectMiddleware = (emailVerified = true) =>
catchAsync(async (req, _res, next) => {
// Break if no Authorization header is set
if (!req.header('Authorization')) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Not Authorized');
}

let payload = null;

try {
// Read userId from token
const token = req.header('Authorization').split(' ')[1];
payload = await verifyJWT(token, config.server.jwt_secret);

if (payload.type !== tokenTypes.ACCESS) {
throw new Error();
}
} catch (err) {
// Handle broken token
throw new ApiError(httpStatus.BAD_REQUEST, 'Invalid auth token');
}

if (!user) {
throw new ApiError(httpStatus.BAD_REQUEST, 'Invalid auth token');
}
// Get user from database
const user = await User.findOne({
where: {
id: payload.id,
},
});

// Inject user into request object
req.user = user;
if (!user || user.disabled) {
throw new ApiError(httpStatus.BAD_REQUEST, 'Invalid auth token');
}

next();
});
// Inject user into request object
req.user = user;

// check verification level
if (emailVerified && !req.user.emailVerified) {
// allow operations in time range
if (config.service.email_confirm_level === 'medium') {
if (new Date() - req.user.createdAt > config.service.email_confirm_time) {
throw new ApiError(httpStatus.FORBIDDEN, 'Email not verified');
}
}

// dont allow any operations
else if (config.service.email_confirm_level === 'high') {
throw new ApiError(httpStatus.FORBIDDEN, 'Email not verified');
}
}

next();
});

module.exports = ProtectMiddleware;
Loading

0 comments on commit ae77ac8

Please sign in to comment.