Skip to content
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
APP_PORT=5000

APP_KEY=
JWT_SECRET=
JWT_EXPIRES_IN_MINUTES=60

Expand Down
6 changes: 3 additions & 3 deletions src/app/factory/UserFactory.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import User from "@src/app/models/auth/User";
import { GROUPS, ROLES } from "@src/config/acl";
import Factory from "@src/core/base/Factory";
import hashPassword from "@src/core/domains/auth/utils/hashPassword";
import { cryptoService } from "@src/core/domains/crypto/service/CryptoService";
import { IModelAttributes } from "@src/core/interfaces/IModel";
import User from "@src/app/models/auth/User";

class UserFactory extends Factory {

Expand All @@ -11,7 +11,7 @@ class UserFactory extends Factory {
getDefinition(): IModelAttributes | null {
return {
email: this.faker.internet.email(),
hashedPassword: hashPassword(this.faker.internet.password()),
hashedPassword: cryptoService().hash(this.faker.internet.password()),
roles: [ROLES.USER],
groups: [GROUPS.User],
firstName: this.faker.person.firstName(),
Expand Down
6 changes: 5 additions & 1 deletion src/config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import { EnvironmentDevelopment, EnvironmentType } from '@src/core/consts/Enviro

// App configuration type definition
export type IAppConfig = {
env: EnvironmentType
appKey: string;
env: EnvironmentType;
}

// App configuration
const appConfig: IAppConfig = {

// App key
appKey: process.env.APP_KEY ?? '',

// Environment
env: (process.env.APP_ENV as EnvironmentType) ?? EnvironmentDevelopment,

Expand Down
7 changes: 5 additions & 2 deletions src/core/domains/auth/observers/UserObserver.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { UserCreatedListener } from "@src/app/events/listeners/UserCreatedListener";
import { UserAttributes } from "@src/app/models/auth/User";
import hashPassword from "@src/core/domains/auth/utils/hashPassword";
import { IBaseEvent } from "@src/core/domains/events/interfaces/IBaseEvent";
import Observer from "@src/core/domains/observer/services/Observer";
import { ICtor } from "@src/core/interfaces/ICtor";
import { App } from "@src/core/services/App";
import { cryptoService } from "@src/core/domains/crypto/service/CryptoService";

/**
* Observer for the User model.
Expand Down Expand Up @@ -81,7 +81,10 @@ export default class UserObserver extends Observer<UserAttributes> {
return data
}

data.hashedPassword = hashPassword(data.password);
// Hash the password
data.hashedPassword = cryptoService().hash(data.password);

// Delete the password from the data
delete data.password;

return data
Expand Down
4 changes: 2 additions & 2 deletions src/core/domains/auth/services/JwtAuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import ApiToken from "@src/core/domains/auth/models/ApiToken";
import ApiTokenRepository from "@src/core/domains/auth/repository/ApiTokenRepitory";
import UserRepository from "@src/core/domains/auth/repository/UserRepository";
import ACLService from "@src/core/domains/auth/services/ACLService";
import comparePassword from "@src/core/domains/auth/utils/comparePassword";
import createJwt from "@src/core/domains/auth/utils/createJwt";
import decodeJwt from "@src/core/domains/auth/utils/decodeJwt";
import generateToken from "@src/core/domains/auth/utils/generateToken";
Expand All @@ -26,6 +25,7 @@ import Router from "@src/core/domains/http/router/Router";
import { app } from "@src/core/services/App";
import { JsonWebTokenError } from "jsonwebtoken";
import { DataTypes } from "sequelize";
import { cryptoService } from "@src/core/domains/crypto/service/CryptoService";

/**
* Short hand for app('auth.jwt')
Expand Down Expand Up @@ -108,7 +108,7 @@ class JwtAuthService extends BaseAuthAdapter<IJwtConfig> implements IJwtAuthServ
throw new UnauthorizedError()
}

if (!comparePassword(password, hashedPassword)) {
if (!cryptoService().verifyHash(password, hashedPassword)) {
throw new UnauthorizedError()
}

Expand Down
12 changes: 3 additions & 9 deletions src/core/domains/auth/usecase/LoginUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@

import HttpContext from "@src/core/domains/http/context/HttpContext";
import ApiResponse from "@src/core/domains/http/response/ApiResponse";
import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError";
import { authJwt } from "@src/core/domains/auth/services/JwtAuthService";
import comparePassword from "@src/core/domains/auth/utils/comparePassword";
import HttpContext from "@src/core/domains/http/context/HttpContext";
import ApiResponse from "@src/core/domains/http/response/ApiResponse";


/**
* LoginUseCase handles user authentication by validating credentials and generating JWT tokens
Expand Down Expand Up @@ -40,12 +40,6 @@ class LoginUseCase {
return this.unauthorized('Email or password is incorrect');
}

const hashedPassword = user.getHashedPassword();

if(!hashedPassword || !comparePassword(password, hashedPassword)) {
return this.unauthorized('Email or password is incorrect');
}

let jwtToken!: string;

try {
Expand Down
6 changes: 2 additions & 4 deletions src/core/domains/auth/usecase/RegisterUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { IUserModel } from "@src/core/domains/auth/interfaces/models/IUserModel";
import { acl, auth } from "@src/core/domains/auth/services/AuthService";
import { authJwt } from "@src/core/domains/auth/services/JwtAuthService";
import hashPassword from "@src/core/domains/auth/utils/hashPassword";
import HttpContext from "@src/core/domains/http/context/HttpContext";
import ApiResponse from "@src/core/domains/http/response/ApiResponse";
import ValidatorResult from "@src/core/domains/validator/data/ValidatorResult";
import { IValidatorResult } from "@src/core/domains/validator/interfaces/IValidatorResult";
import { cryptoService } from "@src/core/domains/crypto/service/CryptoService";

/**
* RegisterUseCase handles new user registration
Expand Down Expand Up @@ -52,12 +52,10 @@ class RegisterUseCase {
async createUser(context: HttpContext): Promise<IUserModel> {
const userAttributes = {
email: context.getBody().email,

hashedPassword: hashPassword(context.getBody().password),
hashedPassword: cryptoService().hash(context.getBody().password),
groups: [acl().getDefaultGroup().name],
roles: [acl().getGroupRoles(acl().getDefaultGroup()).map(role => role.name)],
...context.getBody()

}

// Create and save the user
Expand Down
1 change: 1 addition & 0 deletions src/core/domains/auth/utils/comparePassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ import bcrypt from 'bcryptjs'
* @param password The plain text password
* @param hashedPassword The hashed password
* @returns true if the password matches the hashed password, false otherwise
* @deprecated Use cryptoService().verifyHash instead
*/
export default (password: string, hashedPassword: string): boolean => bcrypt.compareSync(password, hashedPassword)
1 change: 1 addition & 0 deletions src/core/domains/auth/utils/hashPassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import bcryptjs from 'bcryptjs'
* @param password The password to hash
* @param salt The salt to use for hashing (optional, default is 10)
* @returns The hashed password
* @deprecated Use cryptoService().hash instead
*/
export default (password: string, salt: number = 10): string => bcryptjs.hashSync(password, salt)

35 changes: 35 additions & 0 deletions src/core/domains/crypto/commands/GenerateAppKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import EnvService from "@src/core/services/EnvService";
import BaseCommand from "@src/core/domains/console/base/BaseCommand";
import { cryptoService } from "@src/core/domains/crypto/service/CryptoService";

class GenerateAppKey extends BaseCommand {

signature = 'app:generate-key'

description = 'Generate a new app key'

envService = new EnvService();

async execute() {

const confirm = await this.input.askQuestion('Are you sure you want to generate a new app key? (y/n)')

if (confirm !== 'y') {
console.log('App key generation cancelled.')
return
}

console.log('Generating app key...')

const appKey = cryptoService().generateAppKey()

await this.envService.updateValues({
APP_KEY: appKey
})

console.log(`App key generated: ${appKey}`)
}

}

export default GenerateAppKey
13 changes: 13 additions & 0 deletions src/core/domains/crypto/interfaces/BufferingEncoding.t.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type BufferEncoding =
| "ascii"
| "utf8"
| "utf-8"
| "utf16le"
| "utf-16le"
| "ucs2"
| "ucs-2"
| "base64"
| "base64url"
| "latin1"
| "binary"
| "hex";
4 changes: 4 additions & 0 deletions src/core/domains/crypto/interfaces/ICryptoConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ICryptoConfig {
appKey: string
}

11 changes: 11 additions & 0 deletions src/core/domains/crypto/interfaces/ICryptoService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable no-unused-vars */
import { BufferEncoding } from "@src/core/domains/crypto/interfaces/BufferingEncoding.t"

export interface ICryptoService {
generateBytesAsString(length?: number, encoding?: BufferEncoding): string
encrypt(string: string): string
decrypt(string: string): string
hash(string: string): string
verifyHash(string: string, hashWithSalt: string): boolean
generateAppKey(): string
}
29 changes: 29 additions & 0 deletions src/core/domains/crypto/providers/CryptoProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import appConfig, { IAppConfig } from "@src/config/app";
import BaseProvider from "@src/core/base/Provider";
import { app } from "@src/core/services/App";
import GenerateAppKey from "@src/core/domains/crypto/commands/GenerateAppKey";
import CryptoService from "@src/core/domains/crypto/service/CryptoService";

class CryptoProvider extends BaseProvider {

config: IAppConfig = appConfig;

async register(): Promise<void> {

const config = {
appKey: this.config.appKey
}
const cryptoService = new CryptoService(config)

// Bind the crypto service
this.bind('crypto', cryptoService)

// Register commands
app('console').register().registerAll([
GenerateAppKey
])
}

}

export default CryptoProvider
105 changes: 105 additions & 0 deletions src/core/domains/crypto/service/CryptoService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { app } from "@src/core/services/App"
import crypto from 'crypto'
import { BufferEncoding } from "@src/core/domains/crypto/interfaces/BufferingEncoding.t"
import { ICryptoConfig } from "@src/core/domains/crypto/interfaces/ICryptoConfig"
import { ICryptoService } from "@src/core/domains/crypto/interfaces/ICryptoService"

// Alias for app('crypto')
export const cryptoService = () => app('crypto')

class CryptoService implements ICryptoService {

protected config!: ICryptoConfig;

constructor(config: ICryptoConfig) {
this.config = config
}

/**
* Generate a new app key
*/
generateBytesAsString(length: number = 32, encoding: BufferEncoding = 'hex') {
return crypto.randomBytes(length).toString(encoding)
}

/**
* Encrypt a string
*/
encrypt(toEncrypt: string): string {
this.validateAppKey()

const iv = crypto.randomBytes(16);
const key = crypto.pbkdf2Sync(this.config.appKey, 'salt', 100000, 32, 'sha256');
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);

let encrypted = cipher.update(String(toEncrypt), 'utf-8', 'hex')
encrypted += cipher.final('hex')
return iv.toString('hex') + '|' + encrypted
}

/**
* Decrypt a string
*/
decrypt(encryptedData: string): string {
this.validateAppKey()

const [ivHex, encryptedText] = encryptedData.split('|');
const iv = Buffer.from(ivHex, 'hex');
const key = crypto.pbkdf2Sync(this.config.appKey, 'salt', 100000, 32, 'sha256');

const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encryptedText, 'hex', 'utf-8');
decrypted += decipher.final('utf-8');
return decrypted;
}

/**
* Hash a string using PBKDF2
* @param string The string to hash
* @param salt Optional salt (if not provided, a random one will be generated)
* @returns The hashed string with salt, format: 'salt|hash'
*/
hash(string: string, salt?: string): string {
const useSalt = salt || crypto.randomBytes(16).toString('hex');
const hashedString = crypto.pbkdf2Sync(
string,
useSalt,
100000, // iterations
64, // key length
'sha512'
).toString('hex');

return `${useSalt}|${hashedString}`;
}

/**
* Verify a string against a hash
* @param string The string to verify
* @param hashWithSalt The hash with salt (format: 'salt|hash')
* @returns boolean
*/
verifyHash(string: string, hashWithSalt: string): boolean {
const [salt] = hashWithSalt.split('|');
const verifyHash = this.hash(string, salt);
return verifyHash === hashWithSalt;
}

/**
* Generate a new app key
*/
generateAppKey(): string {
return crypto.randomBytes(32).toString('hex');
}

/**
* Validate the app key
*/
private validateAppKey() {
if (!this.config.appKey || this.config.appKey.length === 0) {
throw new Error('App key is not set')
}
}

}

export default CryptoService
6 changes: 6 additions & 0 deletions src/core/domains/database/interfaces/IDatabaseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { IDatabaseConfig } from "@src/core/domains/database/interfaces/IDatabase
import { IDatabaseSchema } from "@src/core/domains/database/interfaces/IDatabaseSchema";
import { IHasConfigConcern } from "@src/core/interfaces/concerns/IHasConfigConcern";
import { ICtor } from "@src/core/interfaces/ICtor";
import MongoDbAdapter from "@src/core/domains/mongodb/adapters/MongoDbAdapter";
import PostgresAdapter from "@src/core/domains/postgres/adapters/PostgresAdapter";


export interface IDatabaseService extends IHasConfigConcern<IDatabaseConfig>
Expand All @@ -30,4 +32,8 @@ export interface IDatabaseService extends IHasConfigConcern<IDatabaseConfig>
schema<TSchema extends IDatabaseSchema = IDatabaseSchema>(connectionName?: string): TSchema;

createMigrationSchema(tableName: string, connectionName?: string): Promise<unknown>;

postgres(): PostgresAdapter;

mongodb(): MongoDbAdapter;
}
Loading