Skip to content

Commit

Permalink
Merge pull request #12 from MattPlayGamez/dev
Browse files Browse the repository at this point in the history
Adding changes from dev to main branch
  • Loading branch information
MattPlayGamez authored Dec 23, 2024
2 parents 037158c + 61bb06b commit 70fcb08
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 225 deletions.
45 changes: 33 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ You can add as many fields as you need. (e.g., phone number, address)

```javascript
const DB_SCHEMA = {
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
loginAttempts: { type: Number, default: 0 },
Expand All @@ -54,32 +55,52 @@ const DB_SCHEMA = {
Initialize the authenticator with the required parameters:

```javascript
// File / Memory Storage
const auth = new Authenticator();

// MongoDB Storage
const auth = new Authenticator(
QR_LABEL,
SALT,
JWT_SECRET_KEY,
JWT_OPTIONS,
MAX_LOGIN_ATTEMPTS,
USER_OBJECT // Only for memory authentication
DB_CONNECTION_STRING, //for MONGODB or DB_FILE_PATH for file storage
DB_SCHEMA, // for MONGODB schema
DB_PASSWORD // only for file storage
);
```
MONGODB_STRING,
USER_SCHEMA
)

// There are a lot more options available below which are not required.
```
## Options
These contain the default inputs and CAN be changed by `auth.QR_LABEL = "something else";`
- `this.QR_LABEL = "Authenticator";`
- `this.rounds = 12;`
- `this.JWT_SECRET_KEY = "changeme";`
- `this.JWT_OPTIONS = { expiresIn: "1h" };`
- `this.maxLoginAttempts = 13;`
- `this.maxLoginAttempts = this.maxLoginAttempts - 2;`
- `this.DB_FILE_PATH = "./users.db";`
- `this.DB_PASSWORD = "changeme";`
- `this.users = [];`
- `this.OTP_ENCODING = 'base32';`
- `this.lockedText = "User is locked";`
- `this.OTP_WINDOW = 1;` // How many OTP codes can be used before and after the current one (usefull for slower people, recommended 1)
- `this.INVALID_2FA_CODE_TEXT = "Invalid 2FA code";`
- `this.REMOVED_USER_TEXT = "User has been removed";`
- `this.USERNAME_ALREADY_EXISTS_TEXT = "This username already exists";`
- `this.EMAIL_ALREADY_EXISTS_TEXT = "This email already exists";`
- `this.USERNAME_IS_REQUIRED="Username is required";`
- `this.ALLOW_DB_DUMP = false;` // Allowing DB Dumping is disabled by default can be enabled by setting ALLOW_DB_DUMP to true after initializing your class

## API

### `register(userObject)`
Registers a new user.

### `login(email, password, twoFactorCode || null)`
### `login(username, password, twoFactorCode || null)`
Logs in a user.

### `getInfoFromUser(userId)`
Retrieves user information.

### `getInfoFromCustom(searchType, value)`
Retrieves user information based on a custom search criteria (like email, username,...)

### `verifyToken(token)`
Verifies a JWT token.

Expand Down
101 changes: 55 additions & 46 deletions file.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Local file is not written to disk
// Local file is written to disk
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
const uuid = require('uuid')
Expand Down Expand Up @@ -48,35 +48,30 @@ function loadUsersFromFile(filePath, password) {

class Authenticator {

/**
* Constructor for the Authenticator class
* @param {string} QR_LABEL - label for the QR code
* @param {number} rounds - number of rounds for bcrypt
* @param {string} JWT_SECRET_KEY - secret key for signing JWTs
* @param {object} JWT_OPTIONS - options for JWTs such as expiresIn
* @param {number} maxLoginAttempts - maximum number of login attempts
* @param {string} DB_FILE_PATH - path to the file where the users are stored
* @param {string} DB_PASSWORD - password to decrypt the file
*/
constructor(QR_LABEL, rounds, JWT_SECRET_KEY, JWT_OPTIONS, maxLoginAttempts, DB_FILE_PATH, DB_PASSWORD) {
this.QR_LABEL = QR_LABEL;
this.rounds = rounds;
this.JWT_SECRET_KEY = JWT_SECRET_KEY;
this.JWT_OPTIONS = JWT_OPTIONS;
this.maxLoginAttempts = maxLoginAttempts - 2;
this.users = loadUsersFromFile(DB_FILE_PATH, DB_PASSWORD);
this.DB_FILE_PATH = DB_FILE_PATH
this.DB_PASSWORD = DB_PASSWORD

constructor() {
this.QR_LABEL = "Authenticator";
this.rounds = 12;
this.JWT_SECRET_KEY = "changeme";
this.JWT_OPTIONS = { expiresIn: "1h" };
this.maxLoginAttempts = 13
this.maxLoginAttempts = this.maxLoginAttempts - 2;
this.DB_FILE_PATH = "./users.db"
this.DB_PASSWORD = "changeme"
this.users = loadUsersFromFile(this.DB_FILE_PATH, this.DB_PASSWORD);
this.OTP_ENCODING = 'base32'
this.lockedText = "User is locked"
this.OTP_WINDOW = 1 // How many OTP codes can be used before and after the current one (usefull for slower people, recommended 1)
this.INVALID_2FA_CODE_TEXT = "Invalid 2FA code"
this.REMOVED_USER_TEXT = "User has been removed"
this.USER_ALREADY_EXISTS_TEXT = "User already exists"
this.USERNAME_ALREADY_EXISTS_TEXT = "This username already exists"
this.EMAIL_ALREADY_EXISTS_TEXT = "This email already exists"
this.USERNAME_IS_REQUIRED="Username is required"
this.ALLOW_DB_DUMP = false // Allowing DB Dumping is disabled by default can be enabled by setting ALLOW_DB_DUMP to true after initializing your class

// Override methods to update file when users array changes
const originalPush = this.users.push;

this.users.push = (...args) => {
const result = originalPush.apply(this.users, args);
saveUsersToFile(this.users, this.DB_FILE_PATH, this.DB_PASSWORD);
Expand All @@ -87,12 +82,22 @@ class Authenticator {



/**
* Registers a new user
* @param {object} userObject - object with required keys: email, password, wants2FA, you can add custom keys too
* @returns {object} - registered user object, or "User already exists" if user already exists
* @throws {Error} - any other error
*/

/**
* Registers a new user.
*
* Initializes user object with default values if not provided, including login attempts,
* locked status, and unique ID. ashes the password and optionally generates a 2FA secret
* and QR code if 2FA is requested. Checks for existing user by email and returns an
* appropriate message if user already exists. Updates users list and returns the
* registered user object.
*
* @param {object} userObject - The user details containing required keys:
* username, email, password, wants2FA. Custom keys can be added like.
* If email is null or undefined, they can't use login by email.
* @returns {object|string} - The registered user object or a string "User already exists".
* @throws {Error} - Logs any error encountered during registration process.
*/
async register(userObject) {
if (!userObject.loginAttempts) userObject.loginAttempts = 0
if (!userObject.locked) userObject.locked = false
Expand All @@ -117,38 +122,41 @@ class Authenticator {
userObject.password = hash;
userObject.jwt_version = 1

if (!userObject.username) return this.USERNAME_IS_REQUIRED

if (this.users.find(u => u.email === userObject.email)) return this.USER_ALREADY_EXISTS_TEXT
if (this.users.find(u => u.username === userObject.username)) return this.USERNAME_ALREADY_EXISTS_TEXT
if (this.users.find(u => u.email === userObject.email)) return this.EMAIL_ALREADY_EXISTS_TEXT
this.users.push(userObject);
return returnedUser;
} catch (err) {
console.log(err)

}

}

/**
* Logs in a user
* @param {string} email - email address of user
* @param {string} username - Username of user
* @param {string} password - password of user
* @param {number} twoFactorCode - 2FA code of user or put null if user didn't provide a 2FA
* @returns {object} - user object with jwt_token, or null if login was unsuccessful, or "User is locked" if user is locked
* @throws {Error} - any other error
*/
async login(email, password, twoFactorCode) {
const account = this.users.find(u => u.email === email);
if (!email) return null;
async login(username, password, twoFactorCode) {
const account = this.users.find(u => u.username === username);
if (!username) return null;
if (!password) return null;

try {
const result = await bcrypt.compare(password, account.password);

if (!result) {
(account.loginAttempts >= this.maxLoginAttempts) ? this.lockUser(account.id) : await this.changeLoginAttempts(account._id, account.loginAttempts + 1)

(account.loginAttempts >= this.maxLoginAttempts) ? await this.lockUser(account.id) : await this.changeLoginAttempts(account._id, account.loginAttempts + 1)

return null
};
}
if (account) {
if (account.locked) return this.lockedText
if (account.wants2FA) {
Expand All @@ -167,7 +175,7 @@ class Authenticator {

}
const jwt_token = jwt.sign({ _id: account._id, version: account.jwt_version }, this.JWT_SECRET_KEY, this.JWT_OPTIONS);
this.changeLoginAttempts(account._id, 0)
await this.changeLoginAttempts(account._id, 0)

return { ...account, jwt_token };
}
Expand Down Expand Up @@ -206,7 +214,7 @@ class Authenticator {
*/
async verifyEmailSignin(emailCode) {
if (emailCode === null) return null
const user = await this.users.find(user => user.emailCode == emailCode);
const user = await this.users.find(user => user.emailCode === emailCode);
if (!user) return null;
const userIndex = this.users.findIndex(u => u.emailCode === emailCode);
if (userIndex !== -1) {
Expand All @@ -227,15 +235,16 @@ class Authenticator {
if (!user) return null;
return user
}

/**
* Retrieves user information based on the user email
* @param {string} email - the email to retrieve information
* @returns {object} - an object with the user information
* @throws {Error} - any error that occurs during the process
* Retrieves user information based on a custom search criteria
* @param {string} searchType - the field name to search by (e.g. username, email, etc.).
* It will only find the first element that corresponds to the specified value
* @param {string} value - the value to match in the specified field
* @returns {object} - an object with the user information or null if not found
*/

getInfoFromEmail(email) {
const user = this.users.find(u => u.email === email);
getInfoFromCustom(searchType, value) {
const user = this.users.find(u => u[searchType] === value);
if (!user) return null;
return user
}
Expand Down
Loading

0 comments on commit 70fcb08

Please sign in to comment.