Skip to content

Commit

Permalink
fix: additional sec fixes; feat: added pw change; feat: better email …
Browse files Browse the repository at this point in the history
…config;
  • Loading branch information
ncoonrod committed Jan 23, 2024
1 parent 677755f commit ecde79b
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 38 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@hotstaq/userroute",
"description": "A user route for HotStaq. Allows users to be created/edited/deleted securely.",
"version": "0.4.5",
"version": "0.4.6",
"main": "build/src/index.js",
"scripts": {
"test": "hotstaq --dev --env-file .env run --server-type api --api-test",
Expand Down
87 changes: 73 additions & 14 deletions src/AdminRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,25 @@ import { HotRouteMethodParameter, PassType } from "hotstaq/build/src/HotRouteMet
*/
export class AdminRoute extends UserRoute
{
/**
* Requires that each method requires authentication from a user of type.
* Set this to an empty string if you want to handle authentication yourself.
* Be warned, if you do this, you will need to check the user's permissions
* on EVERY method within this route.
*
* @default admin
*/
methodsRequireAuthType: string;
/**
* The database connection.
*/
db: HotDBMySQL;

constructor (api: HotAPI)
constructor (api: HotAPI, routeName: string = "admins")
{
super (api, "admins");
super (api, routeName);

this.methodsRequireAuthType = "admin";

this.onPreRegister = async () =>
{
Expand Down Expand Up @@ -58,6 +69,25 @@ export class AdminRoute extends UserRoute
}
]
});
this.addMethod ({
"name": "changePassword",
"onServerExecute": this.changePassword,
"description": `Change a user's password. The id set in the user object that is passed will be the id of the user that has it's password changed.`,
"parameters": {
"user": userObjectDesc,
"newPassword": {
"type": "string",
"description": "The new password to set."
}
},
"returns": "Returns true if the user's password was changed.",
"testCases": [
"changePasswordTest",
async (driver: HotTestDriver): Promise<any> =>
{
}
]
});
this.addMethod ({
"name": "listUsers",
"onServerExecute": this.listUsers,
Expand Down Expand Up @@ -97,26 +127,41 @@ export class AdminRoute extends UserRoute
}

/**
* The admin login. This checks to verify the user is an admin.
* Check a user's authentication.
*/
protected async checkAuth (req: ServerRequest): Promise<void>
{
if (this.methodsRequireAuthType !== "")
{
const jwtToken: string = HotStaq.getParam ("jwtToken", req.jsonObj);
const decoded: IJWTToken = await User.decodeJWTToken (jwtToken);
const authUser: IUser = decoded.user;

if (authUser.userType !== this.methodsRequireAuthType)
throw new Error (`Only user of type ${this.methodsRequireAuthType} is allowed to use this method.`);
}
}

/**
* The admin login. This checks to verify the user is of the user type set in
* this.methodsRequireAuthType.
*/
protected async login (req: ServerRequest): Promise<any>
{
let user: User = await super.login (req);

if (user.userType !== "admin")
throw new Error (`Only admins are allowed to login to this route.`);
await this.checkAuth (req);

return (user);
}

/**
* Edit a user.
*
* **WARNING:** By default, this method can be used by anyone. To
* prevent this, use "onServerPreExecute" to check the user's permissions.
*/
protected async editUser (req: ServerRequest): Promise<boolean>
{
await this.checkAuth (req);

const userObj: IUser = HotStaq.getParam ("user", req.jsonObj);
const user: User = new User (userObj);

Expand All @@ -127,12 +172,11 @@ export class AdminRoute extends UserRoute

/**
* Delete a user.
*
* **WARNING:** By default, this method can be used by anyone. To
* prevent this, use "onServerPreExecute" to check the user's permissions.
*/
protected async deleteUser (req: ServerRequest): Promise<boolean>
{
await this.checkAuth (req);

const userObj: IUser = HotStaq.getParam ("user", req.jsonObj);
const user: User = new User (userObj);

Expand All @@ -141,14 +185,29 @@ export class AdminRoute extends UserRoute
return (true);
}

/**
* Change a user's password.
*/
protected async changePassword (req: ServerRequest): Promise<any>
{
await this.checkAuth (req);

const userObj: IUser = HotStaq.getParam ("user", req.jsonObj);
const user: User = new User (userObj);
const newPassword: string = HotStaq.getParam ("newPassword", req.jsonObj);

await User.changePassword (this.db, user, newPassword);

return (true);
}

/**
* List users.
*
* **WARNING:** By default, this method can be used by anyone. To
* prevent this, use "onServerPreExecute" to check the user's permissions.
*/
protected async listUsers (req: ServerRequest): Promise<any>
{
await this.checkAuth (req);

const search: string = HotStaq.getParamDefault ("search", req.jsonObj, null);
const offset: number = HotStaq.getParamDefault ("offset", req.jsonObj, 0);
const limit: number = HotStaq.getParamDefault ("limit", req.jsonObj, 20);
Expand Down
70 changes: 58 additions & 12 deletions src/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,15 @@ export class User implements IUser
* the user's verifyCode in the database so the user can update their password.
*/
static onForgotPasswordUpdate: (user: User) => Promise<void> = null;
/**
* The event to fire when a user's password has changed. This updates
* the user's new password hash and salt in the database.
*/
static onChangePasswordUpdate: (user: User,
passwordHash: string, passwordSalt: string) => Promise<void> = null;
/**
* The event to fire when a user's forgotten password has been reset. This updates
* the user's new password has and salt in the database.
* the user's new password hash and salt in the database.
*/
static onResetForgottenPasswordUpdate: (user: User,
passwordHash: string, passwordSalt: string) => Promise<void> = null;
Expand Down Expand Up @@ -367,7 +373,7 @@ export class User implements IUser
/**
* Register a user.
*/
async register (db: HotDBMySQL): Promise<User>
async register (db: HotDBMySQL, emailConfig: EmailConfig = null, verifyCode: string = ""): Promise<User>
{
let tempUser: User | null = await User.getUser (db, this.email);

Expand All @@ -392,8 +398,16 @@ export class User implements IUser
verified = 1;
}

if (verifyCode !== "")
verificationCode = verifyCode;

if (verified === 0)
verificationCode = await User.createRandomHash (new Date ().toString ());
{
if (verifyCode !== "")
verificationCode = verifyCode;
else
verificationCode = await User.createRandomHash (new Date ().toString ());
}

this.verifyCode = verificationCode;

Expand All @@ -417,6 +431,13 @@ export class User implements IUser

this.id = userId;

if (emailConfig != null)
{
const body: string = emailConfig.body (this, this.verifyCode);

await User.sendEmail (this.email, emailConfig.subject, body, emailConfig);
}

return (this);
}

Expand Down Expand Up @@ -614,6 +635,28 @@ export class User implements IUser
}
}

/**
* Change password.
*/
static async changePassword (db: HotDBMySQL, user: User, newPassword: string): Promise<void>
{
const salt: string = await User.generateSalt ();
const hash: string = await User.generateHash (newPassword, salt);

if (User.onChangePasswordUpdate != null)
await User.onChangePasswordUpdate (user, hash, salt);
else
{
// Update the user's password in the database.
let result = await db.query (
`update users set password = ?, passwordSalt = ?, verifyCode = null where id = UNHEX(REPLACE(?,'-',''))`,
[hash, salt, user.id]);

if (result.error != null)
throw new Error (result.error);
}
}

/**
* Send the verification email.
*/
Expand All @@ -635,21 +678,24 @@ export class User implements IUser
/**
* Start the reset of a user's password.
*/
static async forgotPassword (db: HotDBMySQL, email: string, emailConfig: EmailConfig = null): Promise<string>
static async forgotPassword (db: HotDBMySQL, email: string, emailConfig: EmailConfig = null, verifyCode: string = ""): Promise<string>
{
let user: User = await User.getUser (db, email);
let user: User = await User.getUser (db, email, true);

if (user == null)
throw new Error (`User not found.`);

user.verifyCode = await User.createRandomHash (new Date ().toString ());
if (verifyCode !== "")
user.verifyCode = verifyCode;
else
user.verifyCode = await User.createRandomHash (new Date ().toString ());

if (User.onForgotPasswordUpdate != null)
await User.onForgotPasswordUpdate (user);
else
{
let result = await db.query (
`update users set verifyCode = ? where id = ?`,
`update users set verifyCode = ? where id = UNHEX(REPLACE(?,'-',''))`,
[user.verifyCode, user.id]);

if (result.error != null)
Expand All @@ -672,7 +718,7 @@ export class User implements IUser
static async resetForgottenPassword (db: HotDBMySQL, email: string,
verificationCode: string, newPassword: string): Promise<void>
{
let foundUser: User = await User.getUser (db, email);
let foundUser: User = await User.getUser (db, email, true);

if (foundUser == null)
throw new Error (`User not found.`);
Expand All @@ -689,7 +735,7 @@ export class User implements IUser
{
// Update the user's password in the database.
let result = await db.query (
`update users set password = ?, passwordSalt = ?, verifyCode = null where id = ?`,
`update users set password = ?, passwordSalt = ?, verifyCode = null where id = UNHEX(REPLACE(?,'-',''))`,
[hash, salt, foundUser.id]);

if (result.error != null)
Expand Down Expand Up @@ -841,7 +887,7 @@ export class User implements IUser
/**
* Verify and decode a JWT Token.
*/
static async decodeJWTToken (jwtToken: string): Promise<any>
static async decodeJWTToken (jwtToken: string): Promise<IJWTToken>
{
if (User.jwtSecretKey === "")
throw new Error (`A JWT secret key is required to run!`);
Expand All @@ -852,9 +898,9 @@ export class User implements IUser
throw new Error (`JWT token has been invalidated!`);
}

return (new Promise<string> ((resolve, reject) =>
return (new Promise<IJWTToken> ((resolve, reject) =>
{
jwt.verify (jwtToken, User.jwtSecretKey, (err: Error, decoded: string) =>
jwt.verify (jwtToken, User.jwtSecretKey, (err: Error, decoded: IJWTToken) =>
{
if (err != null)
throw new Error (`Unable to verify JWT token!`);
Expand Down

0 comments on commit ecde79b

Please sign in to comment.