diff --git a/src/app/migrations/2024-09-06-create-api-token-table.ts b/src/app/migrations/2024-09-06-create-api-token-table.ts index 82767598f..45e39500c 100644 --- a/src/app/migrations/2024-09-06-create-api-token-table.ts +++ b/src/app/migrations/2024-09-06-create-api-token-table.ts @@ -1,4 +1,4 @@ -import ApiToken from "@src/app/models/auth/ApiToken"; +import ApiToken from "@src/core/domains/auth/models/ApiToken"; import BaseMigration from "@src/core/domains/migrations/base/BaseMigration"; import { DataTypes } from "sequelize"; diff --git a/src/app/migrations/2024-09-06-create-user-table.ts b/src/app/migrations/2024-09-06-create-user-table.ts index 515630c67..de4c95c84 100644 --- a/src/app/migrations/2024-09-06-create-user-table.ts +++ b/src/app/migrations/2024-09-06-create-user-table.ts @@ -22,12 +22,13 @@ export class CreateUserModelMigration extends BaseMigration { await this.schema.createTable(this.table, { email: DataTypes.STRING, hashedPassword: DataTypes.STRING, - groups: DataTypes.JSON, - roles: DataTypes.JSON, + groups: DataTypes.ARRAY(DataTypes.STRING), + roles: DataTypes.ARRAY(DataTypes.STRING), firstName: stringNullable, lastName: stringNullable, createdAt: DataTypes.DATE, updatedAt: DataTypes.DATE + }) } diff --git a/src/app/models/auth/ApiToken.ts b/src/app/models/auth/ApiToken.ts deleted file mode 100644 index 3c4e43eef..000000000 --- a/src/app/models/auth/ApiToken.ts +++ /dev/null @@ -1,103 +0,0 @@ -import User from '@src/app/models/auth/User'; -import ApiTokenObserver from '@src/app/observers/ApiTokenObserver'; -import IApiTokenModel, { ApiTokenAttributes } from '@src/core/domains/auth/interfaces/IApitokenModel'; -import IUserModel from '@src/core/domains/auth/interfaces/IUserModel'; -import Scopes from '@src/core/domains/auth/services/Scopes'; -import BelongsTo from '@src/core/domains/eloquent/relational/BelongsTo'; -import { ModelConstructor } from '@src/core/interfaces/IModel'; -import Model from '@src/core/models/base/Model'; - -/** - * ApiToken model - * - * Represents an API token that can be used to authenticate a user. - */ -class ApiToken extends Model implements IApiTokenModel { - - /** - * The user model constructor - */ - protected userModelCtor: ModelConstructor = User - - /** - * Required ApiToken fields - * - * @field userId The user this token belongs to - * @field token The token itself - * @field revokedAt The date and time the token was revoked (null if not revoked) - */ - public fields: string[] = [ - 'userId', - 'token', - 'scopes', - 'revokedAt' - ] - - public json: string[] = [ - 'scopes' - ] - - public relationships: string[] = [ - 'user' - ] - - public timestamps: boolean = false; - - /** - * Construct an ApiToken model from the given data. - * - * @param {ApiTokenAttributes} [data=null] The data to construct the model from. - * - * @constructor - */ - constructor(data: ApiTokenAttributes | null = null) { - super(data) - this.setObserverConstructor(ApiTokenObserver) - } - - /** - * Sets the user model constructor to use for fetching the user of this ApiToken - * @param {ModelConstructor} userModelCtor The user model constructor - */ - setUserModelCtor(userModelCtor: ModelConstructor): void { - this.userModelCtor = userModelCtor - } - - /** - * Retrieves the constructor for the user model associated with this ApiToken. - * @returns {ModelConstructor} The user model constructor. - */ - getUserModelCtor(): ModelConstructor { - return this.userModelCtor - } - - /** - * Fetches the user that this ApiToken belongs to. - * - * @returns A BelongsTo relationship that resolves to the user model. - */ - user(): BelongsTo { - return this.belongsTo(this.userModelCtor, { - localKey: 'userId', - foreignKey: 'id', - }) - } - - /** - * Checks if the given scope(s) are present in the scopes of this ApiToken - * @param scopes The scope(s) to check - * @returns True if all scopes are present, false otherwise - */ - hasScope(scopes: string | string[], exactMatch: boolean = true): boolean { - const currentScopes = this.getAttributeSync('scopes') ?? []; - - if(exactMatch) { - return Scopes.exactMatch(currentScopes, scopes); - } - - return Scopes.partialMatch(currentScopes, scopes); - } - -} - -export default ApiToken diff --git a/src/app/models/auth/User.ts b/src/app/models/auth/User.ts index 44ebd3faa..6b33af833 100644 --- a/src/app/models/auth/User.ts +++ b/src/app/models/auth/User.ts @@ -1,7 +1,6 @@ -import UserObserver from "@src/app/observers/UserObserver"; -import IUserModel from "@src/core/domains/auth/interfaces/IUserModel"; +import AuthUser from "@src/core/domains/auth/models/AuthUser"; +import UserObserver from "@src/core/domains/auth/observers/UserObserver"; import { IModelAttributes } from "@src/core/interfaces/IModel"; -import Model from "@src/core/models/base/Model"; /** * User structure @@ -23,7 +22,7 @@ export interface UserAttributes extends IModelAttributes { * * Represents a user in the database. */ -export default class User extends Model implements IUserModel { +export default class User extends AuthUser { /** * Table name @@ -38,6 +37,7 @@ export default class User extends Model implements IUserModel { this.setObserverConstructor(UserObserver); } + /** * Guarded fields * @@ -66,16 +66,6 @@ export default class User extends Model implements IUserModel { 'updatedAt', ] - /** - * Fields that should be returned as JSON - * - * These fields will be returned as JSON when the model is serialized. - */ - json = [ - 'groups', - 'roles' - ] - /** * Retrieves the fields defined on the model, minus the password field. * As this is a temporary field and shouldn't be saved to the database. @@ -86,38 +76,4 @@ export default class User extends Model implements IUserModel { return super.getFields().filter(field => !['password'].includes(field)); } - /** - * Checks if the user has the given role - * - * @param role The role to check - * @returns True if the user has the role, false otherwise - */ - hasRole(roles: string | string[]): boolean { - roles = typeof roles === 'string' ? [roles] : roles; - const userRoles = this.getAttributeSync('roles') ?? []; - - for(const role of roles) { - if(!userRoles.includes(role)) return false; - } - - return true; - } - - /** - * Checks if the user has the given role - * - * @param role The role to check - * @returns True if the user has the role, false otherwise - */ - hasGroup(groups: string | string[]): boolean { - groups = typeof groups === 'string' ? [groups] : groups; - const userGroups = this.getAttributeSync('groups') ?? []; - - for(const group of groups) { - if(!userGroups.includes(group)) return false; - } - - return true; - } - } diff --git a/src/app/observers/.gitkeep b/src/app/observers/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/providers/RoutesProvider.ts b/src/app/providers/RoutesProvider.ts index 727f7f12e..8dc964834 100644 --- a/src/app/providers/RoutesProvider.ts +++ b/src/app/providers/RoutesProvider.ts @@ -15,7 +15,7 @@ class RoutesProvider extends BaseProvider { const httpService = app('http'); // Bind routes - httpService.bindRoutes(app('auth').getAuthRoutes()) + httpService.bindRoutes(app('auth.jwt').getRouter()) httpService.bindRoutes(healthRoutes); httpService.bindRoutes(apiRoutes); diff --git a/src/app/repositories/auth/ApiTokenRepository.ts b/src/app/repositories/auth/ApiTokenRepository.ts deleted file mode 100644 index f402dd751..000000000 --- a/src/app/repositories/auth/ApiTokenRepository.ts +++ /dev/null @@ -1,31 +0,0 @@ -import ApiToken from "@src/app/models/auth/ApiToken"; -import Repository from "@src/core/base/Repository"; -import IApiTokenModel from "@src/core/domains/auth/interfaces/IApitokenModel"; -import IApiTokenRepository from "@src/core/domains/auth/interfaces/IApiTokenRepository"; - - -export default class ApiTokenRepository extends Repository implements IApiTokenRepository { - - constructor() { - super(ApiToken) - } - - /** - * Finds one token - * @param token - * @returns - */ - async findOneToken(token: string): Promise { - return await this.query().where('token', token).first() - } - - /** - * Finds one token that is not currently revoked - * @param token - * @returns - */ - async findOneActiveToken(token: string): Promise { - return await this.query().where('token', token).whereNull('revokedAt').first() - } - -} \ No newline at end of file diff --git a/src/app/repositories/auth/UserRepository.ts b/src/app/repositories/auth/UserRepository.ts index 01aa0164d..6b1dba05b 100644 --- a/src/app/repositories/auth/UserRepository.ts +++ b/src/app/repositories/auth/UserRepository.ts @@ -1,21 +1,11 @@ import User from "@src/app/models/auth/User"; -import Repository from "@src/core/base/Repository"; -import IUserModel from "@src/core/domains/auth/interfaces/IUserModel"; -import IUserRepository from "@src/core/domains/auth/interfaces/IUserRepository"; +import AuthUserRepository from '@src/core/domains/auth/repository/UserRepository'; -export default class UserRepository extends Repository implements IUserRepository { + +export default class UserRepository extends AuthUserRepository { constructor() { super(User) } - - /** - * Finds a User by their email address - * @param email - * @returns - */ - public async findOneByEmail(email: string): Promise { - return this.query().where('email', email).first() - } } \ No newline at end of file diff --git a/src/app/routes/api.ts b/src/app/routes/api.ts index 532565d54..673830db3 100644 --- a/src/app/routes/api.ts +++ b/src/app/routes/api.ts @@ -1,6 +1,5 @@ import Route from "@src/core/domains/http/router/Route" - -import ExampleController from "../controllers/ExampleController" +import ExampleController from "@src/app/controllers/ExampleController" export default Route.group(router => { diff --git a/src/app/validators/user/CreateUserValidator.ts b/src/app/validators/user/CreateUserValidator.ts index 481c4b109..1c8b98da2 100644 --- a/src/app/validators/user/CreateUserValidator.ts +++ b/src/app/validators/user/CreateUserValidator.ts @@ -1,4 +1,5 @@ -import { auth } from "@src/core/domains/auth/services/AuthService"; +import User from "@src/app/models/auth/User"; +import { queryBuilder } from "@src/core/domains/eloquent/services/EloquentQueryBuilderService"; import BaseValidator from "@src/core/domains/validator/base/BaseValidator"; import { ValidatorPayload } from "@src/core/domains/validator/interfaces/IValidator"; import Joi, { ObjectSchema } from "joi"; @@ -15,8 +16,7 @@ class CreateUserValidator extends BaseValidator { */ async validateEmailAvailability(payload: ValidatorPayload) { - const repository = auth().getUserRepository(); - const user = await repository.findOneByEmail(payload.email as string); + const user = await queryBuilder(User).where('email', payload.email as string).first(); if(user) { this.setErrorMessage({ email: 'User already exists' }); diff --git a/src/config/acl.ts b/src/config/acl.ts new file mode 100644 index 000000000..b428d3ef0 --- /dev/null +++ b/src/config/acl.ts @@ -0,0 +1,47 @@ +import { IAclConfig } from "@src/core/domains/auth/interfaces/acl/IAclConfig"; + +// Define available groups +export const GROUPS = { + User: 'group_user', + Admin: 'group_admin', +} as const + +// Define available roles +export const ROLES = { + USER: 'role_user', + ADMIN: 'role_admin' +} as const + +/** + * ACL configuration + */ +export const aclConfig: IAclConfig = { + + // Default user group + defaultGroup: GROUPS.User, + + // List of groups + groups: [ + { + name: GROUPS.User, + roles: [ROLES.USER] + }, + { + name: GROUPS.Admin, + roles: [ROLES.USER, ROLES.ADMIN] + } + ], + + // List of roles, scopes and other permissions + roles: [ + { + name: ROLES.ADMIN, + scopes: [] + }, + { + name: ROLES.USER, + scopes: [] + }, + ], + +} \ No newline at end of file diff --git a/src/config/auth.ts b/src/config/auth.ts index 891d8fca9..0d6ccc3b9 100644 --- a/src/config/auth.ts +++ b/src/config/auth.ts @@ -1,124 +1,64 @@ -import ApiToken from '@src/app/models/auth/ApiToken'; import User from '@src/app/models/auth/User'; -import ApiTokenRepository from '@src/app/repositories/auth/ApiTokenRepository'; -import UserRepository from '@src/app/repositories/auth/UserRepository'; import CreateUserValidator from '@src/app/validators/user/CreateUserValidator'; import UpdateUserValidator from '@src/app/validators/user/UpdateUserValidator'; -import ApiTokenFactory from '@src/core/domains/auth/factory/apiTokenFactory'; -import UserFactory from '@src/core/domains/auth/factory/userFactory'; -import { IAuthConfig } from '@src/core/domains/auth/interfaces/IAuthConfig'; -import AuthService from '@src/core/domains/auth/services/AuthService'; +import { BaseAuthAdapterTypes } from '@src/core/domains/auth/interfaces/adapter/AuthAdapterTypes.t'; +import ApiToken from '@src/core/domains/auth/models/ApiToken'; +import AuthConfig from '@src/core/domains/auth/services/AuthConfig'; +import JwtAuthService from '@src/core/domains/auth/services/JwtAuthService'; import parseBooleanFromString from '@src/core/util/parseBooleanFromString'; /** - * Available groups + * Auth Configuration Module + * + * This module configures authentication adapters and settings for the application. + * It defines available auth adapters (currently JWT) and their configurations. + * + * Auth adapters, and related services can be retrieved in the application using: + * + * ```ts + * // Get auth service + * const authService = app('auth') + * const aclService = app('auth.acl') + * const jwtAdapter = app('auth.jwt') + * + * + * // Get specific adapter + * app('auth').getAdapter('jwt') + * ``` + * */ -export const GROUPS = { - User: 'group_user', - Admin: 'group_admin', -} as const -/** - * Available roles - */ -export const ROLES = { - USER: 'role_user', - ADMIN: 'role_admin' -} as const - -/** - * Auth configuration - */ -const config: IAuthConfig = { - service: { - authService: AuthService - }, - models: { - user: User, - apiToken: ApiToken - }, - repositories: { - user: UserRepository, - apiToken: ApiTokenRepository - }, - factory: { - userFactory: UserFactory, - apiTokenFactory: ApiTokenFactory - }, - validators: { - createUser: CreateUserValidator, - updateUser: UpdateUserValidator, - }, - - /** - * JWT secret - */ - jwtSecret: process.env.JWT_SECRET as string ?? '', - - /** - * JWT expiration time in minutes - */ - expiresInMinutes: process.env.JWT_EXPIRES_IN_MINUTES ? parseInt(process.env.JWT_EXPIRES_IN_MINUTES) : 60, - - /** - * Enable or disable auth routes - */ - enableAuthRoutes: parseBooleanFromString(process.env.ENABLE_AUTH_ROUTES, 'true'), - - /** - * Enable or disable create a new user endpoint - */ - enableAuthRoutesAllowCreate: parseBooleanFromString(process.env.ENABLE_AUTH_ROUTES_ALLOW_CREATE, 'true'), +// Type helper for auth adapters +export interface AuthAdapters extends BaseAuthAdapterTypes { + default: JwtAuthService + jwt: JwtAuthService +} - /** - * Permissions configuration - * - user.defaultGroup - The group will be the default group for new user accounts - * - groups - The list of groups - * - groups.roles will auto populate the roles on creation - * - groups.scopes will be automatically added to new ApiTokenModels - * - * - * You can use ModelScopes to generate scopes for models. These scopes will be - * added to any routes created with RouteResource. For example: - * - * Example: - * { - * name: GROUPS.User, - * roles: [ROLES.USER], - * scopes: [ - * ...ModelScopes.getScopes(ExampleModel, ['read', 'write']) - * ] - * }, - */ - permissions: { +// Define auth configs +export const authConfig = AuthConfig.define([ - /** - * The default user group - */ - user: { - defaultGroup: GROUPS.User, + // Default JWT Authentication + AuthConfig.config(JwtAuthService, { + name: 'jwt', + models: { + user: User, + apiToken: ApiToken + }, + validators: { + createUser: CreateUserValidator, + updateUser: UpdateUserValidator }, - /** - * The list of groups - */ - groups: [ - { - name: GROUPS.User, - roles: [ROLES.USER], - scopes: [ + routes: { + enableAuthRoutes: parseBooleanFromString(process.env.ENABLE_AUTH_ROUTES, 'true'), + enableAuthRoutesAllowCreate: parseBooleanFromString(process.env.ENABLE_AUTH_ROUTES_ALLOW_CREATE, 'true'), + }, + settings: { + secret: process.env.JWT_SECRET as string ?? '', + expiresInMinutes: process.env.JWT_EXPIRES_IN_MINUTES ? parseInt(process.env.JWT_EXPIRES_IN_MINUTES) : 60, + } + }) - ] - }, - { - name: GROUPS.Admin, - roles: [ROLES.USER, ROLES.ADMIN], - scopes: [ - - ] - } - ] - } -} + // Define more auth adapters here +]) -export default config; \ No newline at end of file diff --git a/src/core/base/BaseAdapter.ts b/src/core/base/BaseAdapter.ts index 3ed4097d4..6cdc05b66 100644 --- a/src/core/base/BaseAdapter.ts +++ b/src/core/base/BaseAdapter.ts @@ -1,47 +1,64 @@ import AdapterException from "@src/core/exceptions/AdapterException"; -export type AdapterTypes = { - [key: string]: Adapter; +/** + * @template T type of the adapter + */ +export type BaseAdapterTypes = { + [key: string]: T; } /** - * @abstract - * @class BaseAdapter - * @template AdapterType + * BaseAdapter is a generic abstract class that provides a foundation for implementing adapter patterns. + * It manages a collection of typed adapters and provides methods for safely adding and retrieving them. + * + * The class uses a generic type parameter AdapterTypes that extends BaseAdapterTypes, allowing for + * type-safe adapter management where each adapter must conform to the specified interface. + * + * Key features: + * - Type-safe adapter storage and retrieval + * - Prevention of duplicate adapter registration + * - Centralized adapter management + * - Error handling for missing or duplicate adapters + * + * This class is typically extended by service classes that need to manage multiple implementations + * of a particular interface, such as authentication adapters, storage adapters, or payment providers. + * + * @example + * class AuthService extends BaseAdapter { + * // Implements specific auth service functionality while inheriting adapter management + * } */ -abstract class BaseAdapter { +abstract class BaseAdapter { /** - * @type {AdapterTypes} + * @type {AdapterTypes} */ - protected adapters: AdapterTypes = {}; + protected adapters: AdapterTypes = {} as AdapterTypes; /** * @param {string} name - * @param {AdapterType} adapter + * @param {TAdapterType} adapter */ - public addAdapter(name: string, adapter: AdapterType): void { - this.adapters[name] = adapter; + public addAdapterOnce(name: string, adapter: AdapterTypes[keyof AdapterTypes]): void { + if(this.adapters[name as keyof AdapterTypes]) { + throw new AdapterException(`Adapter ${name} already exists`); + } + + this.adapters[name as keyof AdapterTypes] = adapter; } /** - * @param {keyof AdapterTypes} name - * @returns {AdapterType} + * @param {keyof AdapterTypes} name + * @returns {AdapterTypes[keyof AdapterTypes]} */ - public getAdapter(name: keyof AdapterTypes): AdapterType { + public getAdapter(name: K): AdapterTypes[K] { if(!this.adapters[name]) { - throw new AdapterException(`Adapter ${name} not found`); + throw new AdapterException(`Adapter ${name as string} not found`); } return this.adapters[name]; } - /** - * @returns {AdapterTypes} - */ - public getAdapters(): AdapterTypes { - return this.adapters; - } } diff --git a/src/core/base/Provider.ts b/src/core/base/Provider.ts index 94d97b24d..a844382c6 100644 --- a/src/core/base/Provider.ts +++ b/src/core/base/Provider.ts @@ -22,11 +22,23 @@ export default abstract class BaseProvider implements IProvider { */ protected config: any = {}; + /** + * Bind a value to the container + * + * @protected + * @param {string} key - The key to bind the value to + * @param {any} value - The value to bind to the key + */ + protected bind(key: string, value: any): void { + App.setContainer(key, value); + } + /** * Registers the provider * * @abstract * @returns {Promise} + */ register(): Promise { return Promise.resolve(); diff --git a/src/core/domains/auth/actions/create.ts b/src/core/domains/auth/actions/create.ts deleted file mode 100644 index 8f2089149..000000000 --- a/src/core/domains/auth/actions/create.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { UserAttributes } from '@src/app/models/auth/User'; -import UserFactory from '@src/core/domains/auth/factory/userFactory'; -import { auth } from '@src/core/domains/auth/services/AuthService'; -import hashPassword from '@src/core/domains/auth/utils/hashPassword'; -import responseError from '@src/core/domains/http/handlers/responseError'; -import ValidationError from '@src/core/exceptions/ValidationError'; -import { App } from '@src/core/services/App'; -import { Request, Response } from 'express'; - -/** - * Creates a new user - * - * @param {Request} req - The request object - * @param {Response} res - The response object - * @returns {Promise} - */ -export default async (req: Request, res: Response): Promise => { - - const { email, password, firstName, lastName } = req.body as Pick; - - try { - // Check if the user already exists - const repository = auth().getUserRepository(); - const existingUser = await repository.findOneByEmail(email); - - if (existingUser) { - // If the user already exists, throw a validation error - throw new ValidationError('User already exists'); - } - - // Create a new user - const user = new UserFactory().create({ - email, - password, - hashedPassword: hashPassword(password ?? ''), - groups: [ - App.container('auth').config.permissions.user.defaultGroup - ], - roles: [], - firstName, - lastName - }); - - // Save the user to the database - await user.save(); - - // Generate a JWT token for the user - const token = await App.container('auth').createJwtFromUser(user); - - // Return the user data and the JWT token - res.send({ - success: true, - token, - user: await user.getData({ excludeGuarded: true }) - }); - } - catch (error) { - // Handle validation errors - if (error instanceof ValidationError) { - res.status(400).send({ error: error.message, stack: error.stack }); - return; - } - - // Handle other errors - if (error instanceof Error) { - responseError(req, res, error); - return; - } - } - -} diff --git a/src/core/domains/auth/actions/getUser.ts b/src/core/domains/auth/actions/getUser.ts deleted file mode 100644 index 69fa5d2d3..000000000 --- a/src/core/domains/auth/actions/getUser.ts +++ /dev/null @@ -1,24 +0,0 @@ -import IAuthorizedRequest from '@src/core/domains/auth/interfaces/IAuthorizedRequest'; -import responseError from '@src/core/domains/http/handlers/responseError'; -import { Response } from 'express'; - -/** - * Gets the currently logged in user - * - * @param {IAuthorizedRequest} req - * @param {Response} res - * @returns {Promise} - */ -export default async (req: IAuthorizedRequest, res: Response) => { - try { - // Send the user data without the password - res.send({ success: true, user: await req.user?.getData({ excludeGuarded: true }) }); - } - catch (error) { - // If there is an error, send the error response - if (error instanceof Error) { - responseError(req, res, error); - return; - } - } -}; diff --git a/src/core/domains/auth/actions/login.ts b/src/core/domains/auth/actions/login.ts deleted file mode 100644 index ab49beedc..000000000 --- a/src/core/domains/auth/actions/login.ts +++ /dev/null @@ -1,44 +0,0 @@ -import unauthorizedError from '@src/core/domains/auth/exceptions/UnauthorizedError'; -import responseError from '@src/core/domains/http/handlers/responseError'; -import { App } from '@src/core/services/App'; -import { Request, Response } from 'express'; - -/** - * Logs in a user - * - * @param {Request} req - The request object - * @param {Response} res - The response object - * @returns {Promise} - */ -export default async (req: Request, res: Response): Promise => { - try { - // Get the email and password from the request body - const { email, password } = req?.body ?? {}; - - // Attempt to log in the user - const token = await App.container('auth').attemptCredentials(email, password); - - // Get the user from the database - const user = await App.container('auth').userRepository.findOneByEmail(email); - - // Send the user data and the token back to the client - res.send({ - success: true, - token, - user: await user?.getData({ excludeGuarded: true }) - }) - } - catch (error) { - // Handle unauthorized errors - if (error instanceof unauthorizedError) { - res.status(401).send({ error: error.message },) - return; - } - - // Handle other errors - if (error instanceof Error) { - responseError(req, res, error) - return; - } - } -} diff --git a/src/core/domains/auth/actions/revoke.ts b/src/core/domains/auth/actions/revoke.ts deleted file mode 100644 index 5eeb7d59c..000000000 --- a/src/core/domains/auth/actions/revoke.ts +++ /dev/null @@ -1,32 +0,0 @@ -import ApiToken from '@src/app/models/auth/ApiToken'; -import IAuthorizedRequest from '@src/core/domains/auth/interfaces/IAuthorizedRequest'; -import responseError from '@src/core/domains/http/handlers/responseError'; -import { App } from '@src/core/services/App'; -import { Response } from 'express'; - -/** - * Revokes the API token associated with the request - * - * @param {IAuthorizedRequest} req - The request object - * @param {Response} res - The response object - * @returns {Promise} - */ -export default async (req: IAuthorizedRequest, res: Response) => { - try { - // Get the auth service - const auth = App.container('auth'); - - // Revoke the API token - await auth.revokeToken(req.apiToken as ApiToken); - - // Send a success response - res.send({ success: true }); - } - catch (error) { - // Handle any errors - if (error instanceof Error) { - responseError(req, res, error); - return; - } - } -}; diff --git a/src/core/domains/auth/actions/update.ts b/src/core/domains/auth/actions/update.ts deleted file mode 100644 index ed0016d63..000000000 --- a/src/core/domains/auth/actions/update.ts +++ /dev/null @@ -1,43 +0,0 @@ -import User, { UserAttributes } from '@src/app/models/auth/User'; -import hashPassword from '@src/core/domains/auth/utils/hashPassword'; -import responseError from '@src/core/domains/http/handlers/responseError'; -import { TBaseRequest } from '@src/core/domains/http/interfaces/BaseRequest'; -import { Response } from 'express'; - -/** - * Updates the currently logged in user - * - * @param {TBaseRequest} req - The request object - * @param {Response} res - The response object - * @returns {Promise} - */ -export default async (req: TBaseRequest, res: Response) => { - try { - const user = req.user as User; - const { password, firstName, lastName } = req.body as Pick; - - // If the user provided a new password, hash it and update the user object - if(password) { - await user.setAttribute('hashedPassword', hashPassword(password)); - } - - // Update the user object with the new first and last name - await user.fill({ - firstName, - lastName - }); - - // Save the changes to the database - await user.save(); - - // Return the updated user data - res.send({ success: true, user: await req.user?.getData({ excludeGuarded: true }) }) - } - catch (error) { - // If there is an error, send the error response - if(error instanceof Error) { - responseError(req, res, error) - return; - } - } -} diff --git a/src/core/domains/auth/actions/user.ts b/src/core/domains/auth/actions/user.ts deleted file mode 100644 index 6328525e5..000000000 --- a/src/core/domains/auth/actions/user.ts +++ /dev/null @@ -1,26 +0,0 @@ -import IAuthorizedRequest from '@src/core/domains/auth/interfaces/IAuthorizedRequest'; -import responseError from '@src/core/domains/http/handlers/responseError'; -import { Response } from 'express'; - - - -/** - * Returns the currently logged in user - * - * @param {IAuthorizedRequest} req - The request object - * @param {Response} res - The response object - * @returns {Promise} - */ -export default async (req: IAuthorizedRequest, res: Response) => { - try { - // Send the user data without the password - res.send({ success: true, user: await req.user?.getData({ excludeGuarded: true }) }); - } - catch (error) { - // Handle any errors - if (error instanceof Error) { - responseError(req, res, error); - return; - } - } -}; diff --git a/src/core/domains/auth/base/BaseAuthAdapter.ts b/src/core/domains/auth/base/BaseAuthAdapter.ts new file mode 100644 index 000000000..cf92a51e0 --- /dev/null +++ b/src/core/domains/auth/base/BaseAuthAdapter.ts @@ -0,0 +1,60 @@ +import { IRouter } from "@src/core/domains/http/interfaces/IRouter"; +import Router from "@src/core/domains/http/router/Router"; +import { IAclConfig } from "@src/core/domains/auth/interfaces/acl/IAclConfig"; +import { IAuthAdapter } from "@src/core/domains/auth/interfaces/adapter/IAuthAdapter"; +import { IBaseAuthConfig } from "@src/core/domains/auth/interfaces/config/IAuth"; + +/** + * Base authentication adapter class that implements the IAuthAdapter interface. + * Provides core functionality for authentication adapters. + * @template Config - The configuration type that extends IBaseAuthConfig + */ +class BaseAuthAdapter implements IAuthAdapter { + + public config!: Config; + + protected aclConfig!: IAclConfig + + constructor(config: Config, aclConfig: IAclConfig) { + this.config = config; + this.aclConfig = aclConfig; + } + + /** + * Boots the adapter + * @returns A promise that resolves when the adapter is booted + */ + public async boot(): Promise { + return Promise.resolve(); + } + + + /** + * Retrieves the current configuration + * @returns The current configuration object + */ + + getConfig(): Config { + return this.config; + } + + /** + * Updates the configuration + * @param config - The new configuration object to set + */ + setConfig(config: Config): void { + this.config = config; + } + + /** + * Creates and returns a new router instance + * @returns A new IRouter instance + */ + getRouter(): IRouter { + return new Router(); + } + +} + +export default BaseAuthAdapter; + diff --git a/src/core/domains/auth/commands/GenerateJWTSecret.ts b/src/core/domains/auth/commands/GenerateJwtSecret.ts similarity index 100% rename from src/core/domains/auth/commands/GenerateJWTSecret.ts rename to src/core/domains/auth/commands/GenerateJwtSecret.ts diff --git a/src/core/domains/auth/consts/authConsts.ts b/src/core/domains/auth/consts/authConsts.ts deleted file mode 100644 index 3903f38db..000000000 --- a/src/core/domains/auth/consts/authConsts.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Constants for auth routes - */ -const authConsts = { - - /** - * Route for creating a new user - */ - routes: { - - /** - * Route for creating a new user - */ - authCreate: 'authCreate', - - /** - * Route for logging in - */ - authLogin: 'authLogin', - - /** - * Route for retrieving the current user - */ - authUser: 'authUser', - - /** - * Route for revoking a token - */ - authRevoke: 'authRevoke', - - /** - * Route for updating the current user - */ - authUpdate: 'authUpdate' - } -} - -export default authConsts diff --git a/src/core/domains/auth/controllers/AuthController.ts b/src/core/domains/auth/controllers/AuthController.ts new file mode 100644 index 000000000..aa3c52620 --- /dev/null +++ b/src/core/domains/auth/controllers/AuthController.ts @@ -0,0 +1,150 @@ +import ForbiddenResourceError from "@src/core/domains/auth/exceptions/ForbiddenResourceError"; +import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; +import LoginUseCase from "@src/core/domains/auth/usecase/LoginUseCase"; +import LogoutUseCase from "@src/core/domains/auth/usecase/LogoutUseCase"; +import RefreshUseCase from "@src/core/domains/auth/usecase/RefreshUseCase"; +import RegisterUseCase from "@src/core/domains/auth/usecase/RegisterUseCase"; +import UpdateUseCase from "@src/core/domains/auth/usecase/UpdateUseCase"; +import UserUseCase from "@src/core/domains/auth/usecase/UserUseCase"; +import Controller from "@src/core/domains/http/base/Controller"; +import HttpContext from "@src/core/domains/http/context/HttpContext"; +import responseError from "@src/core/domains/http/handlers/responseError"; +import ApiResponse from "@src/core/domains/http/response/ApiResponse"; +import ValidationError from "@src/core/domains/validator/exceptions/ValidationError"; + +/** + * Controller handling authentication-related HTTP endpoints. + * + * This controller manages user authentication operations including: + * - User registration + * - Login/authentication + * - User profile retrieval + * - Logout + * - Token refresh + * + * Each method handles its respective HTTP endpoint and delegates the business logic + * to appropriate use cases while handling response formatting and error cases. + */ +class AuthController extends Controller { + + protected loginUseCase = new LoginUseCase(); + + protected registerUseCase = new RegisterUseCase(); + + protected userUseCase = new UserUseCase(); + + protected logoutUseCase = new LogoutUseCase(); + + protected refreshUseCase = new RefreshUseCase(); + + protected updateUseCase = new UpdateUseCase(); + + /** + * Handle the login endpoint + + * @param context The HTTP context + * @returns The API response + */ + async login(context: HttpContext) { + this.handler(context, async () => { + return await this.loginUseCase.handle(context) + }) + } + + + /** + * Handle the register endpoint + * @param context The HTTP context + * @returns The API response + */ + async register(context: HttpContext) { + this.handler(context, async () => { + return await this.registerUseCase.handle(context) + }) + } + + /** + * Handle the user endpoint + * @param context The HTTP context + * @returns The API response + */ + async user(context: HttpContext) { + this.handler(context, async () => { + return await this.userUseCase.handle(context) + }) + } + + + /** + * Handle the logout endpoint + * @param context The HTTP context + * @returns The API response + */ + async logout(context: HttpContext) { + this.handler(context, async () => { + return await this.logoutUseCase.handle(context) + }) + } + + /** + * Handle the refresh endpoint + * @param context The HTTP context + * @returns The API response + */ + async refresh(context: HttpContext) { + this.handler(context, async () => { + return await this.refreshUseCase.handle(context) + }) + } + + /** + * Handle the update endpoint + * @param context The HTTP context + * @returns The API response + */ + async update(context: HttpContext) { + this.handler(context, async () => { + return await this.updateUseCase.handle(context) + }) + } + + /** + * Handle the request + * @param context The HTTP context + * @param callback The callback to handle the request + * @returns The API response + + */ + protected async handler(context: HttpContext, callback: () => Promise) { + try { + const apiResponse = await callback(); + + return this.jsonResponse( + apiResponse.toObject(), + apiResponse.getCode() + ) + } + catch (error) { + if(Error instanceof UnauthorizedError) { + responseError(context.getRequest(), context.getResponse(), error as Error, 401) + return; + } + + if(error instanceof ValidationError) { + responseError(context.getRequest(), context.getResponse(), error as Error, 422) + return; + } + + if(error instanceof ForbiddenResourceError) { + responseError(context.getRequest(), context.getResponse(), error as Error, 403) + return; + } + + responseError(context.getRequest(), context.getResponse(), error as Error) + } + + } + +} + +export default AuthController; diff --git a/src/core/domains/auth/enums/RolesEnum.ts b/src/core/domains/auth/enums/RolesEnum.ts deleted file mode 100644 index 196cc3702..000000000 --- a/src/core/domains/auth/enums/RolesEnum.ts +++ /dev/null @@ -1,6 +0,0 @@ -const ROLES = Object.freeze({ - USER: 'user', - ADMIN: 'admin' -}) - -export default ROLES \ No newline at end of file diff --git a/src/core/domains/auth/exceptions/InvalidJwtSettings.ts b/src/core/domains/auth/exceptions/InvalidJwtSettings.ts new file mode 100644 index 000000000..48c46c818 --- /dev/null +++ b/src/core/domains/auth/exceptions/InvalidJwtSettings.ts @@ -0,0 +1,9 @@ +export default class InvalidJwtSettingsException extends Error { + + constructor(message: string = 'Invalid JWT settings') { + super(message); + this.name = 'InvalidJwtSettingsException'; + } + + +} \ No newline at end of file diff --git a/src/core/domains/auth/factory/jwtTokenFactory.ts b/src/core/domains/auth/factory/JwtFactory.ts similarity index 76% rename from src/core/domains/auth/factory/jwtTokenFactory.ts rename to src/core/domains/auth/factory/JwtFactory.ts index 7743a7d17..8df10f0f4 100644 --- a/src/core/domains/auth/factory/jwtTokenFactory.ts +++ b/src/core/domains/auth/factory/JwtFactory.ts @@ -1,11 +1,12 @@ -import { IJSonWebToken } from "@src/core/domains/auth/interfaces/IJSonWebToken" +import { IJSonWebToken } from "@src/core/domains/auth/interfaces/jwt/IJsonWebToken" + /** * Factory for creating JWT tokens. * * @class JWTTokenFactory */ -export default class JWTTokenFactory { +export default class JwtFactory { /** * Creates a new JWT token from a user ID and token. @@ -14,7 +15,7 @@ export default class JWTTokenFactory { * @param {string} token - The token. * @returns {IJSonWebToken} A new JWT token. */ - static create(userId: string, token: string): IJSonWebToken { + static createUserIdAndPayload(userId: string, token: string): IJSonWebToken { return { uid: userId, token diff --git a/src/core/domains/auth/factory/apiTokenFactory.ts b/src/core/domains/auth/factory/apiTokenFactory.ts deleted file mode 100644 index b7ef7357a..000000000 --- a/src/core/domains/auth/factory/apiTokenFactory.ts +++ /dev/null @@ -1,34 +0,0 @@ -import ApiToken from '@src/app/models/auth/ApiToken' -import Factory from '@src/core/base/Factory' -import { IApiTokenFactory } from '@src/core/domains/auth/interfaces/IApiTokenFactory' -import IApiTokenModel from '@src/core/domains/auth/interfaces/IApitokenModel' -import IUserModel from '@src/core/domains/auth/interfaces/IUserModel' -import tokenFactory from '@src/core/domains/auth/utils/generateToken' -import { IModel, IModelAttributes, ModelConstructor } from '@src/core/interfaces/IModel' - -/** - * Factory for creating ApiToken models. - */ -class ApiTokenFactory extends Factory implements IApiTokenFactory { - - protected model: ModelConstructor> = ApiToken; - - /** - * Creates a new ApiToken model from the User - * - * @param {IUserModel} user - * @returns {IApiTokenModel} - - */ - createFromUser(user: IUserModel, scopes: string[] = []): IApiTokenModel { - return this.create({ - userId: user?.id, - token: tokenFactory(), - scopes: scopes, - revokedAt: null, - }) - } - -} - -export default ApiTokenFactory diff --git a/src/core/domains/auth/factory/userFactory.ts b/src/core/domains/auth/factory/userFactory.ts deleted file mode 100644 index d7a77ee01..000000000 --- a/src/core/domains/auth/factory/userFactory.ts +++ /dev/null @@ -1,15 +0,0 @@ -import User from '@src/app/models/auth/User'; -import Factory from '@src/core/base/Factory'; -import { IModel, IModelAttributes, ModelConstructor } from '@src/core/interfaces/IModel'; - -/** - * Factory for creating User models. - * - * @class UserFactory - * @extends {Factory} - */ -export default class UserFactory extends Factory { - - protected model: ModelConstructor> = User; - -} diff --git a/src/core/domains/auth/interfaces/IApiTokenFactory.ts b/src/core/domains/auth/interfaces/IApiTokenFactory.ts deleted file mode 100644 index 32e75f814..000000000 --- a/src/core/domains/auth/interfaces/IApiTokenFactory.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* eslint-disable no-unused-vars */ -import IApiTokenModel from "@src/core/domains/auth/interfaces/IApitokenModel"; -import IUserModel from "@src/core/domains/auth/interfaces/IUserModel"; - -export interface IApiTokenFactory -{ - createFromUser(user: IUserModel, scopes: string[]): IApiTokenModel -} \ No newline at end of file diff --git a/src/core/domains/auth/interfaces/IApiTokenRepository.ts b/src/core/domains/auth/interfaces/IApiTokenRepository.ts deleted file mode 100644 index 54fb03a4c..000000000 --- a/src/core/domains/auth/interfaces/IApiTokenRepository.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* eslint-disable no-unused-vars */ -import IApiTokenModel from "@src/core/domains/auth/interfaces/IApitokenModel"; -import { IRepository } from "@src/core/interfaces/IRepository"; - -export default interface IApiTokenRepository extends IRepository { - findOneToken(...args: any[]): Promise; - findOneActiveToken(...args: any[]): Promise; -} \ No newline at end of file diff --git a/src/core/domains/auth/interfaces/IApitokenModel.ts b/src/core/domains/auth/interfaces/IApitokenModel.ts deleted file mode 100644 index 2a4ce6e54..000000000 --- a/src/core/domains/auth/interfaces/IApitokenModel.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable no-unused-vars */ -import IUserModel from "@src/core/domains/auth/interfaces/IUserModel"; -import BelongsTo from "@src/core/domains/eloquent/relational/BelongsTo"; -import { ICtor } from "@src/core/interfaces/ICtor"; -import { IModel, IModelAttributes } from "@src/core/interfaces/IModel"; - -export interface ApiTokenAttributes extends IModelAttributes { - userId: string; - token: string; - scopes: string[]; - revokedAt: Date | null; - user: IUserModel | null; -} - -export default interface IApiTokenModel extends IModel { - setUserModelCtor(userModelCtor: ICtor): void; - getUserModelCtor(): ICtor; - user(): BelongsTo; - hasScope(scopes: string | string[], exactMatch?: boolean): boolean; -} \ No newline at end of file diff --git a/src/core/domains/auth/interfaces/IAuthConfig.ts b/src/core/domains/auth/interfaces/IAuthConfig.ts deleted file mode 100644 index eb8830797..000000000 --- a/src/core/domains/auth/interfaces/IAuthConfig.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { IApiTokenFactory } from "@src/core/domains/auth/interfaces/IApiTokenFactory"; -import IApiTokenModel from "@src/core/domains/auth/interfaces/IApitokenModel"; -import IApiTokenRepository from "@src/core/domains/auth/interfaces/IApiTokenRepository"; -import { IAuthService } from "@src/core/domains/auth/interfaces/IAuthService"; -import { IPermissionsConfig } from "@src/core/domains/auth/interfaces/IPermissionsConfig"; -import IUserModel from "@src/core/domains/auth/interfaces/IUserModel"; -import IUserRepository from "@src/core/domains/auth/interfaces/IUserRepository"; -import { ValidatorConstructor } from "@src/core/domains/validator/interfaces/IValidator"; -import { ICtor } from "@src/core/interfaces/ICtor"; -import IFactory from "@src/core/interfaces/IFactory"; -import { ModelConstructor } from "@src/core/interfaces/IModel"; -import { RepositoryConstructor } from "@src/core/interfaces/IRepository"; -import { ServiceConstructor } from "@src/core/interfaces/IService"; - -export interface IAuthConfig { - service: { - authService: ServiceConstructor; - }; - models: { - user: ModelConstructor; - apiToken: ModelConstructor; - }; - repositories: { - user: RepositoryConstructor; - apiToken: RepositoryConstructor; - }; - factory: { - userFactory: ICtor; - apiTokenFactory: ICtor; - } - validators: { - createUser: ValidatorConstructor; - updateUser: ValidatorConstructor; - }; - jwtSecret: string, - expiresInMinutes: number; - enableAuthRoutes: boolean; - enableAuthRoutesAllowCreate: boolean; - permissions: IPermissionsConfig; -} \ No newline at end of file diff --git a/src/core/domains/auth/interfaces/IAuthService.ts b/src/core/domains/auth/interfaces/IAuthService.ts deleted file mode 100644 index 962d1e5c0..000000000 --- a/src/core/domains/auth/interfaces/IAuthService.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* eslint-disable no-unused-vars */ -import IApiTokenModel from "@src/core/domains/auth/interfaces/IApitokenModel"; -import IApiTokenRepository from "@src/core/domains/auth/interfaces/IApiTokenRepository"; -import { IAuthConfig } from "@src/core/domains/auth/interfaces/IAuthConfig"; -import IUserModel from "@src/core/domains/auth/interfaces/IUserModel"; -import IUserRepository from "@src/core/domains/auth/interfaces/IUserRepository"; -import { IRouter } from "@src/core/domains/http/interfaces/IRouter"; -import IService from "@src/core/interfaces/IService"; - - -/** - * The service that handles authentication. - * - * @export - * @interface IAuthService - * @extends {IService} - */ -export interface IAuthService extends IService { - - /** - * The auth config - * - * @type {any} - * @memberof IAuthService - */ - config: IAuthConfig; - - /** - * The user repository - * - * @type {IUserRepository} - * @memberof IAuthService - */ - userRepository: IUserRepository; - - /** - * The api token repository - * - * @type {IApiTokenRepository} - * @memberof IAuthService - */ - apiTokenRepository: IApiTokenRepository; - - /** - * Attempt to authenticate a user using a JWT token. - * - * @param {string} token The JWT token - * @returns {Promise} The authenticated user, or null if not authenticated - * @memberof IAuthService - */ - attemptAuthenticateToken: (token: string) => Promise; - - /** - * Creates a JWT for the user. - * - * @param {IUserModel} user The user - * @returns {Promise} The JWT token - * @memberof IAuthService - */ - createJwtFromUser: (user: IUserModel, scopes?: string[]) => Promise; - - /** - * Creates a new ApiToken model from the User - * - * @param {IUserModel} user The user - * @returns {Promise} The new ApiToken model - * @memberof IAuthService - */ - createApiTokenFromUser: (user: IUserModel, scopes?: string[]) => Promise; - - /** - * Revokes a token. - * - * @param {IApiTokenModel} apiToken The ApiToken model - * @returns {Promise} - * @memberof IAuthService - */ - revokeToken: (apiToken: IApiTokenModel) => Promise; - - /** - * Attempt to authenticate a user using their credentials. - * - * @param {string} email The user's email - * @param {string} password The user's password - * @returns {Promise} The JWT token - * @memberof IAuthService - */ - attemptCredentials: (email: string, password: string, scopes?: string[]) => Promise; - - /** - * Generates a JWT. - * - * @param {IApiTokenModel} apiToken The ApiToken model - * @returns {string} The JWT token - * @memberof IAuthService - */ - jwt: (apiToken: IApiTokenModel) => string; - - /** - * Returns the auth routes. - * - * @returns {IRoute[] | null} An array of routes, or null if auth routes are disabled - * @memberof IAuthService - */ - getAuthRoutes(): IRouter; - - /** - * Gets the user repository. - * - * @returns {IUserRepository} The user repository - * @memberof IAuthService - */ - getUserRepository(): IUserRepository; -} diff --git a/src/core/domains/auth/interfaces/IAuthorizedRequest.ts b/src/core/domains/auth/interfaces/IAuthorizedRequest.ts deleted file mode 100644 index c437fcc99..000000000 --- a/src/core/domains/auth/interfaces/IAuthorizedRequest.ts +++ /dev/null @@ -1,8 +0,0 @@ -import IApiTokenModel from '@src/core/domains/auth/interfaces/IApitokenModel'; -import IUserModel from '@src/core/domains/auth/interfaces/IUserModel'; -import { Request } from 'express'; - -export default interface IAuthorizedRequest extends Request { - user?: IUserModel | null; - apiToken?: IApiTokenModel | null; -} \ No newline at end of file diff --git a/src/core/domains/auth/interfaces/IPermissionsConfig.ts b/src/core/domains/auth/interfaces/IPermissionsConfig.ts deleted file mode 100644 index cbc7af21c..000000000 --- a/src/core/domains/auth/interfaces/IPermissionsConfig.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface IPermissionGroup { - name: string; - scopes?: string[]; - roles?: string[]; -} - -export interface IPermissionUser { - defaultGroup: string; -} - -export interface IPermissionsConfig { - user: IPermissionUser; - groups: IPermissionGroup[] -} \ No newline at end of file diff --git a/src/core/domains/auth/interfaces/IRequestIdentifiable.ts b/src/core/domains/auth/interfaces/IRequestIdentifiable.ts deleted file mode 100644 index b959f395b..000000000 --- a/src/core/domains/auth/interfaces/IRequestIdentifiable.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Request } from 'express'; - -export default interface IRequestIdentifiable extends Request { - id?: string; -} \ No newline at end of file diff --git a/src/core/domains/auth/interfaces/IUserModel.ts b/src/core/domains/auth/interfaces/IUserModel.ts deleted file mode 100644 index c9bd36ae7..000000000 --- a/src/core/domains/auth/interfaces/IUserModel.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* eslint-disable no-unused-vars */ -import { UserAttributes } from "@src/app/models/auth/User"; -import { IModel } from "@src/core/interfaces/IModel"; - -export default interface IUserModel extends IModel { - hasRole(...args: any[]): any; - hasGroup(...args: any[]): any; -} \ No newline at end of file diff --git a/src/core/domains/auth/interfaces/IUserRepository.ts b/src/core/domains/auth/interfaces/IUserRepository.ts deleted file mode 100644 index a1e8713d1..000000000 --- a/src/core/domains/auth/interfaces/IUserRepository.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* eslint-disable no-unused-vars */ -import IUserModel from "@src/core/domains/auth/interfaces/IUserModel"; -import { IRepository } from "@src/core/interfaces/IRepository"; - -export default interface IUserRepository extends IRepository -{ - findOneByEmail(email: string): Promise -} \ No newline at end of file diff --git a/src/core/domains/auth/interfaces/acl/IACLService.ts b/src/core/domains/auth/interfaces/acl/IACLService.ts new file mode 100644 index 000000000..b31f520be --- /dev/null +++ b/src/core/domains/auth/interfaces/acl/IACLService.ts @@ -0,0 +1,19 @@ +/* eslint-disable no-unused-vars */ +import { IUserModel } from "@src/core/domains/auth/interfaces/models/IUserModel" +import { IAclConfig, IAclGroup, IAclRole } from "@src/core/domains/auth/interfaces/acl/IAclConfig" + +export interface IACLService { + getConfig(): IAclConfig + getDefaultGroup(): IAclGroup + getGroup(group: string | IAclGroup): IAclGroup + getGroupRoles(group: string | IAclGroup): IAclRole[] + getGroupScopes(group: string | IAclGroup): string[] + getRoleScopesFromUser(user: IUserModel): string[] + getRoleScopes(role: string | string[]): string[] + getRole(role: string): IAclRole + + +} + + + diff --git a/src/core/domains/auth/interfaces/acl/IAclConfig.ts b/src/core/domains/auth/interfaces/acl/IAclConfig.ts new file mode 100644 index 000000000..1098f9577 --- /dev/null +++ b/src/core/domains/auth/interfaces/acl/IAclConfig.ts @@ -0,0 +1,24 @@ +export interface IAclConfig { + defaultGroup: string; + groups: IAclGroup[]; + roles: IAclRole[]; +} + +export interface IAclGroup { + name: string; + roles: string[]; +} + +export interface IAclRole { + name: string; + scopes: string[]; +} + + + + + + + + + diff --git a/src/core/domains/auth/interfaces/adapter/AuthAdapterTypes.t.ts b/src/core/domains/auth/interfaces/adapter/AuthAdapterTypes.t.ts new file mode 100644 index 000000000..8582057f8 --- /dev/null +++ b/src/core/domains/auth/interfaces/adapter/AuthAdapterTypes.t.ts @@ -0,0 +1,7 @@ +import { BaseAdapterTypes } from "@src/core/base/BaseAdapter"; +import { IJwtAuthService } from "@src/core/domains/auth/interfaces/jwt/IJwtAuthService"; +import { IAuthAdapter } from "@src/core/domains/auth/interfaces/adapter/IAuthAdapter"; + +export type BaseAuthAdapterTypes = BaseAdapterTypes & { + default: IJwtAuthService +} \ No newline at end of file diff --git a/src/core/domains/auth/interfaces/adapter/IAuthAdapter.ts b/src/core/domains/auth/interfaces/adapter/IAuthAdapter.ts new file mode 100644 index 000000000..e74db0408 --- /dev/null +++ b/src/core/domains/auth/interfaces/adapter/IAuthAdapter.ts @@ -0,0 +1,20 @@ +/* eslint-disable no-unused-vars */ +import { IRouter } from "@src/core/domains/http/interfaces/IRouter"; +import { IAclConfig } from "@src/core/domains/auth/interfaces/acl/IAclConfig"; +import { IBaseAuthConfig } from "@src/core/domains/auth/interfaces/config/IAuth"; + +export interface AuthAdapterConstructor { + new (config: Adapter['config'], aclConfig: IAclConfig): Adapter; +} + +export interface IAuthAdapter { + boot(): Promise; + config: TConfig; + getConfig(): TConfig; + setConfig(config: TConfig): void; + getRouter(): IRouter; +} + + + + diff --git a/src/core/domains/auth/interfaces/config/IAuth.ts b/src/core/domains/auth/interfaces/config/IAuth.ts new file mode 100644 index 000000000..d42aa58e0 --- /dev/null +++ b/src/core/domains/auth/interfaces/config/IAuth.ts @@ -0,0 +1,6 @@ +import { AuthAdapterConstructor } from "@src/core/domains/auth/interfaces/adapter/IAuthAdapter"; + +export interface IBaseAuthConfig { + name: string; + adapter: AuthAdapterConstructor +} diff --git a/src/core/domains/auth/interfaces/IJSonWebToken.ts b/src/core/domains/auth/interfaces/jwt/IJsonWebToken.ts similarity index 100% rename from src/core/domains/auth/interfaces/IJSonWebToken.ts rename to src/core/domains/auth/interfaces/jwt/IJsonWebToken.ts diff --git a/src/core/domains/auth/interfaces/jwt/IJwtAuthService.ts b/src/core/domains/auth/interfaces/jwt/IJwtAuthService.ts new file mode 100644 index 000000000..ae17af4ba --- /dev/null +++ b/src/core/domains/auth/interfaces/jwt/IJwtAuthService.ts @@ -0,0 +1,17 @@ +/* eslint-disable no-unused-vars */ +import { IApiTokenModel } from "@src/core/domains/auth/interfaces/models/IApiTokenModel"; +import { IUserRepository } from "@src/core/domains/auth/interfaces/repository/IUserRepository"; +import { IRouter } from "@src/core/domains/http/interfaces/IRouter"; +import { IUserModel } from "@src/core/domains/auth/interfaces/models/IUserModel"; + + +export interface IJwtAuthService { + attemptCredentials(email: string, password: string, scopes?: string[]): Promise + attemptAuthenticateToken(token: string): Promise + refreshToken(apiToken: IApiTokenModel): string; + revokeToken(apiToken: IApiTokenModel): Promise + revokeAllTokens(userId: string | number): Promise + getRouter(): IRouter + getUserRepository(): IUserRepository + createJwtFromUser(user: IUserModel): Promise +} diff --git a/src/core/domains/auth/interfaces/jwt/IJwtConfig.ts b/src/core/domains/auth/interfaces/jwt/IJwtConfig.ts new file mode 100644 index 000000000..f7ada1057 --- /dev/null +++ b/src/core/domains/auth/interfaces/jwt/IJwtConfig.ts @@ -0,0 +1,26 @@ +import { AuthAdapterConstructor } from "@src/core/domains/auth/interfaces/adapter/IAuthAdapter"; +import { IBaseAuthConfig } from "@src/core/domains/auth/interfaces/config/IAuth"; +import { UserConstructor } from "@src/core/domains/auth/interfaces/models/IUserModel"; +import { ValidatorConstructor } from "@src/core/domains/validator/interfaces/IValidator"; +import { ApiTokenConstructor } from "@src/core/domains/auth/interfaces/models/IApiTokenModel"; + +export interface IJwtConfig extends IBaseAuthConfig { + name: string; + adapter: AuthAdapterConstructor + models?: { + user?: UserConstructor; + apiToken?: ApiTokenConstructor; + } + validators: { + createUser: ValidatorConstructor; + updateUser: ValidatorConstructor; + }; + settings: { + secret: string, + expiresInMinutes: number; + }, + routes: { + enableAuthRoutes: boolean; + enableAuthRoutesAllowCreate: boolean; + } +} diff --git a/src/core/domains/auth/interfaces/models/IApiTokenModel.ts b/src/core/domains/auth/interfaces/models/IApiTokenModel.ts new file mode 100644 index 000000000..833c27200 --- /dev/null +++ b/src/core/domains/auth/interfaces/models/IApiTokenModel.ts @@ -0,0 +1,20 @@ +/* eslint-disable no-unused-vars */ +import { IUserModel } from "@src/core/domains/auth/interfaces/models/IUserModel"; +import { IModel, ModelConstructor } from "@src/core/interfaces/IModel"; + +export interface ApiTokenConstructor extends ModelConstructor {} + +export interface IApiTokenModel extends IModel { + getUserId(): string + setUserId(userId: string): Promise + getUser(): Promise + getToken(): string + setToken(token: string): Promise + getScopes(): string[] + setScopes(scopes: string[]): Promise + hasScope(scopes: string | string[], exactMatch?: boolean): boolean + getRevokedAt(): Date | null + setRevokedAt(revokedAt: Date | null): Promise + + +} \ No newline at end of file diff --git a/src/core/domains/auth/interfaces/models/IUserModel.ts b/src/core/domains/auth/interfaces/models/IUserModel.ts new file mode 100644 index 000000000..00f1f8442 --- /dev/null +++ b/src/core/domains/auth/interfaces/models/IUserModel.ts @@ -0,0 +1,16 @@ +/* eslint-disable no-unused-vars */ +import { IModel, ModelConstructor } from "@src/core/interfaces/IModel"; + +export interface UserConstructor extends ModelConstructor {} + +export interface IUserModel extends IModel { + getEmail(): string | null; + setEmail(email: string): Promise; + getHashedPassword(): string | null; + setHashedPassword(hashedPassword: string): Promise; + getRoles(): string[]; + setRoles(roles: string[]): Promise; + hasRole(role: string | string[]): boolean; + getGroups(): string[]; + setGroups(groups: string[]): Promise; +} diff --git a/src/core/domains/auth/interfaces/repository/IApiTokenRepository.ts b/src/core/domains/auth/interfaces/repository/IApiTokenRepository.ts new file mode 100644 index 000000000..aab0ea8c3 --- /dev/null +++ b/src/core/domains/auth/interfaces/repository/IApiTokenRepository.ts @@ -0,0 +1,8 @@ +/* eslint-disable no-unused-vars */ +import { IApiTokenModel } from "@src/core/domains/auth/interfaces/models/IApiTokenModel"; + +export interface IApiTokenRepository { + findOneActiveToken(token: string): Promise + revokeToken(apiToken: IApiTokenModel): Promise + revokeAllTokens(userId: string | number): Promise +} \ No newline at end of file diff --git a/src/core/domains/auth/interfaces/repository/IUserRepository.ts b/src/core/domains/auth/interfaces/repository/IUserRepository.ts new file mode 100644 index 000000000..f29a37636 --- /dev/null +++ b/src/core/domains/auth/interfaces/repository/IUserRepository.ts @@ -0,0 +1,9 @@ +/* eslint-disable no-unused-vars */ +import { IUserModel } from "@src/core/domains/auth/interfaces/models/IUserModel"; + +export interface IUserRepository { + create(attributes?: IUserModel): IUserModel + findById(id: string | number): Promise + findByIdOrFail(id: string | number): Promise + findByEmail(email: string): Promise +} \ No newline at end of file diff --git a/src/core/domains/auth/interfaces/IScope.ts b/src/core/domains/auth/interfaces/scope/Scope.t.ts similarity index 100% rename from src/core/domains/auth/interfaces/IScope.ts rename to src/core/domains/auth/interfaces/scope/Scope.t.ts diff --git a/src/core/domains/auth/interfaces/service/IAuthService.ts b/src/core/domains/auth/interfaces/service/IAuthService.ts new file mode 100644 index 000000000..2338a7e82 --- /dev/null +++ b/src/core/domains/auth/interfaces/service/IAuthService.ts @@ -0,0 +1,10 @@ + +import { AuthAdapters } from "@src/config/auth"; +import { IACLService } from "@src/core/domains/auth/interfaces/acl/IACLService"; + +export interface IAuthService { + acl(): IACLService; + boot(): Promise + getDefaultAdapter(): AuthAdapters['default'] + getJwtAdapter(): AuthAdapters['jwt'] +} diff --git a/src/core/domains/http/middleware/AuthorizeMiddleware.ts b/src/core/domains/auth/middleware/AuthorizeMiddleware.ts similarity index 67% rename from src/core/domains/http/middleware/AuthorizeMiddleware.ts rename to src/core/domains/auth/middleware/AuthorizeMiddleware.ts index e36903ba8..82713b595 100644 --- a/src/core/domains/http/middleware/AuthorizeMiddleware.ts +++ b/src/core/domains/auth/middleware/AuthorizeMiddleware.ts @@ -1,14 +1,16 @@ -import ForbiddenResourceError from '@src/core/domains/auth/exceptions/ForbiddenResourceError'; -import UnauthorizedError from '@src/core/domains/auth/exceptions/UnauthorizedError'; -import AuthRequest from '@src/core/domains/auth/services/AuthRequest'; import Middleware from '@src/core/domains/http/base/Middleware'; import HttpContext from '@src/core/domains/http/context/HttpContext'; import responseError from '@src/core/domains/http/handlers/responseError'; -import { ray } from 'node-ray'; +import { requestContext } from '@src/core/domains/http/context/RequestContext'; +import { TBaseRequest } from '@src/core/domains/http/interfaces/BaseRequest'; +import ForbiddenResourceError from '@src/core/domains/auth/exceptions/ForbiddenResourceError'; +import UnauthorizedError from '@src/core/domains/auth/exceptions/UnauthorizedError'; +import { auth } from '@src/core/domains/auth/services/AuthService'; /** * AuthorizeMiddleware handles authentication and authorization for HTTP requests * + * This middleware: * - Validates the authorization header and authenticates the request * - Attaches the authenticated user and API token to the request context @@ -24,27 +26,21 @@ import { ray } from 'node-ray'; * Used as middleware on routes requiring authentication. Can be configured with * required scopes that are validated against the API token's allowed scopes. */ - class AuthorizeMiddleware extends Middleware<{ scopes: string[] }> { async execute(context: HttpContext): Promise { try { - // Authorize the request - // Parses the authorization header - // If successful, attaches the user and apiToken to the request - // and sets the user in the App - await AuthRequest.attemptAuthorizeRequest(context.getRequest()); - + // Attempt to authorize the request + await this.attemptAuthorizeRequest(context.getRequest()); + // Validate the scopes if the authorization was successful this.validateScopes(context) - this.next(); - ray('AuthorizeMiddleware executed') + // Continue to the next middleware + this.next(); } catch (error) { - ray('AuthorizeMiddleware error', error) - if(error instanceof UnauthorizedError) { responseError(context.getRequest(), context.getResponse(), error, 401) return; @@ -61,6 +57,36 @@ class AuthorizeMiddleware extends Middleware<{ scopes: string[] }> { } } + /** + * Attempts to authorize a request with a Bearer token. + * + * If successful, attaches the user and apiToken to the request. Sets the user in the App. + * + * @param req The request to authorize + * @returns The authorized request + * @throws UnauthorizedError if the token is invalid + */ + public async attemptAuthorizeRequest(req: TBaseRequest): Promise { + const authorization = (req.headers.authorization ?? '').replace('Bearer ', ''); + + const apiToken = await auth().getJwtAdapter().attemptAuthenticateToken(authorization) + + const user = await apiToken?.getUser() + + if(!user || !apiToken) { + throw new UnauthorizedError(); + } + + // Set the user and apiToken in the request + req.user = user; + req.apiToken = apiToken + + // Set the user id in the request context + requestContext().setByRequest(req, 'userId', user?.getId()) + + return req; + } + /** * Validates that the scopes in the api token match the required scopes for the request. * If the scopes do not match, it will throw a ForbiddenResourceError. diff --git a/src/core/domains/auth/models/ApiToken.ts b/src/core/domains/auth/models/ApiToken.ts new file mode 100644 index 000000000..aadaba9d7 --- /dev/null +++ b/src/core/domains/auth/models/ApiToken.ts @@ -0,0 +1,193 @@ +import User from '@src/app/models/auth/User'; +import { IApiTokenModel } from '@src/core/domains/auth/interfaces/models/IApiTokenModel'; +import { IUserModel } from '@src/core/domains/auth/interfaces/models/IUserModel'; +import ApiTokenObserver from '@src/core/domains/auth/observers/ApiTokenObserver'; +import UserRepository from '@src/core/domains/auth/repository/UserRepository'; +import ScopeMatcher from '@src/core/domains/auth/utils/ScopeMatcher'; +import BelongsTo from '@src/core/domains/eloquent/relational/BelongsTo'; +import { IModelAttributes, ModelConstructor } from '@src/core/interfaces/IModel'; +import Model from '@src/core/models/base/Model'; + + + + +export interface ApiTokenAttributes extends IModelAttributes { + userId: string; + token: string; + scopes: string[]; + revokedAt: Date | null; + user: IUserModel | null; +} + +/** + * ApiToken model + * + * Represents an API token that can be used to authenticate a user. + */ +class ApiToken extends Model implements IApiTokenModel { + + /** + * The user model constructor + */ + protected userModelCtor: ModelConstructor = User + + /** + * Required ApiToken fields + * + * @field userId The user this token belongs to + * @field token The token itself + * @field revokedAt The date and time the token was revoked (null if not revoked) + */ + public fields: string[] = [ + 'userId', + 'token', + 'scopes', + 'revokedAt' + ] + + public json: string[] = [ + 'scopes' + ] + + public relationships: string[] = [ + 'user' + ] + + public timestamps: boolean = false; + + /** + * Construct an ApiToken model from the given data. + * + * @param {ApiTokenAttributes} [data=null] The data to construct the model from. + * + * @constructor + */ + constructor(data: ApiTokenAttributes | null = null) { + super(data) + this.setObserverConstructor(ApiTokenObserver) + } + + /** + * Get the user id + * @returns {string} The user id + */ + getUserId(): string { + return this.getAttributeSync('userId') as string + } + + /** + * Set the user id + * @param {string} userId The user id + * @returns {Promise} A promise that resolves when the user id is set + */ + setUserId(userId: string): Promise { + return this.setAttribute('userId', userId) + } + + /** + * Get the user + * @returns {IUserModel} The user + * @deprecated Use `auth().getUserRepository().findByIdOrFail(this.getUserId())` instead + */ + async getUser(): Promise { + return await new UserRepository().findByIdOrFail(this.getUserId()) + } + + /** + * Get the token + * @returns {string} The token + */ + getToken(): string { + return this.getAttributeSync('token') as string + } + + /** + * Set the token + * @param {string} token The token + * @returns {Promise} A promise that resolves when the token is set + */ + setToken(token: string): Promise { + return this.setAttribute('token', token) + } + + /** + * Get the revoked at + * @returns {Date | null} The revoked at + */ + getRevokedAt(): Date | null { + return this.getAttributeSync('revokedAt') as Date | null + } + + /** + * Set the revoked at + * @param {Date | null} revokedAt The revoked at + * @returns {Promise} A promise that resolves when the revoked at is set + */ + setRevokedAt(revokedAt: Date | null): Promise { + return this.setAttribute('revokedAt', revokedAt) + } + + /** + * Get the scopes + * @returns {string[]} The scopes + */ + getScopes(): string[] { + return this.getAttributeSync('scopes') as string[] + } + + /** + * Set the scopes + * @param {string[]} scopes The scopes + * @returns {Promise} A promise that resolves when the scopes are set + */ + setScopes(scopes: string[]): Promise { + return this.setAttribute('scopes', scopes) + } + + /** + * Sets the user model constructor to use for fetching the user of this ApiToken + + * @param {ModelConstructor} userModelCtor The user model constructor + */ + setUserModelCtor(userModelCtor: ModelConstructor): void { + this.userModelCtor = userModelCtor + } + + /** + * Retrieves the constructor for the user model associated with this ApiToken. + * @returns {ModelConstructor} The user model constructor. + */ + getUserModelCtor(): ModelConstructor { + return this.userModelCtor + } + + /** + * Fetches the user that this ApiToken belongs to. + * + * @returns A BelongsTo relationship that resolves to the user model. + */ + user(): BelongsTo { + return this.belongsTo(this.userModelCtor, { + localKey: 'userId', + foreignKey: 'id', + }) + } + + /** + * Checks if the given scope(s) are present in the scopes of this ApiToken + * @param scopes The scope(s) to check + * @returns True if all scopes are present, false otherwise + */ + hasScope(scopes: string | string[], exactMatch: boolean = true): boolean { + const currentScopes = this.getAttributeSync('scopes') ?? []; + + if(exactMatch) { + return ScopeMatcher.exactMatch(currentScopes, scopes); + } + + return ScopeMatcher.partialMatch(currentScopes, scopes); + } + +} + +export default ApiToken diff --git a/src/core/domains/auth/models/AuthUser.ts b/src/core/domains/auth/models/AuthUser.ts new file mode 100644 index 000000000..69f4ba7c3 --- /dev/null +++ b/src/core/domains/auth/models/AuthUser.ts @@ -0,0 +1,188 @@ +import { IUserModel } from "@src/core/domains/auth/interfaces/models/IUserModel"; +import { IModelAttributes } from "@src/core/interfaces/IModel"; +import Model from "@src/core/models/base/Model"; +import UserObserver from "@src/core/domains/auth/observers/UserObserver"; + +/** + * User structure + */ +export interface AuthUserAttributes extends IModelAttributes { + email: string; + password?: string; + hashedPassword: string; + roles: string[]; + groups: string[]; +} + +/** + * User model + * + * Represents a user in the database. + */ +export default class AuthUser extends Model implements IUserModel { + + /** + * Table name + */ + public table: string = 'users'; + + /** + * @param data User data + */ + constructor(data: AuthUserAttributes | null = null) { + super(data); + this.setObserverConstructor(UserObserver); + } + + + /** + * Guarded fields + * + * These fields cannot be set directly. + */ + guarded: string[] = [ + 'hashedPassword', + 'password', + 'roles', + 'groups', + ]; + + /** + * The fields that are allowed to be set directly + * + * These fields can be set directly on the model. + */ + fields: string[] = [ + 'email', + 'password', + 'hashedPassword', + 'roles', + 'groups', + 'createdAt', + 'updatedAt', + ] + + /** + * Retrieves the fields defined on the model, minus the password field. + * As this is a temporary field and shouldn't be saved to the database. + * + * @returns The list of fields defined on the model. + */ + getFields(): string[] { + return super.getFields().filter(field => !['password'].includes(field)); + } + + /** + * Checks if the user has the given role + * + * @param role The role to check + * @returns True if the user has the role, false otherwise + */ + hasRole(roles: string | string[]): boolean { + roles = typeof roles === 'string' ? [roles] : roles; + const userRoles = this.getAttributeSync('roles') ?? []; + + for(const role of roles) { + if(!userRoles.includes(role)) return false; + } + + return true; + } + + /** + * Checks if the user has the given role + * + * @param role The role to check + * @returns True if the user has the role, false otherwise + */ + hasGroup(groups: string | string[]): boolean { + groups = typeof groups === 'string' ? [groups] : groups; + const userGroups = this.getAttributeSync('groups') ?? []; + + for(const group of groups) { + if(!userGroups.includes(group)) return false; + } + + return true; + } + + /** + * Get the email of the user + * + * @returns The email of the user + */ + getEmail(): string | null { + return this.getAttributeSync('email'); + } + + + /** + * Set the email of the user + * + * @param email The email to set + * @returns The email of the user + */ + setEmail(email: string): Promise { + return this.setAttribute('email', email); + } + + /** + * Get the hashed password of the user + * + * @returns The hashed password of the user + */ + + getHashedPassword(): string | null { + return this.getAttributeSync('hashedPassword'); + } + + /** + * Set the hashed password of the user + * + * @param hashedPassword The hashed password to set + * @returns The hashed password of the user + */ + setHashedPassword(hashedPassword: string): Promise { + return this.setAttribute('hashedPassword', hashedPassword); + } + + /** + * Get the roles of the user + * + * @returns The roles of the user + */ + getRoles(): string[] { + return this.getAttributeSync('roles') ?? []; + } + + /** + * Set the roles of the user + * + * @param roles The roles to set + * @returns The roles of the user + */ + setRoles(roles: string[]): Promise { + return this.setAttribute('roles', roles); + } + + /** + * Get the groups of the user + * + * @returns The groups of the user + */ + getGroups(): string[] { + return this.getAttributeSync('groups') ?? []; + } + + /** + * Set the groups of the user + * + * @param groups The groups to set + * @returns The groups of the user + */ + setGroups(groups: string[]): Promise { + return this.setAttribute('groups', groups); + } + + +} diff --git a/src/app/observers/ApiTokenObserver.ts b/src/core/domains/auth/observers/ApiTokenObserver.ts similarity index 69% rename from src/app/observers/ApiTokenObserver.ts rename to src/core/domains/auth/observers/ApiTokenObserver.ts index 6df56c05b..e9285dfa6 100644 --- a/src/app/observers/ApiTokenObserver.ts +++ b/src/core/domains/auth/observers/ApiTokenObserver.ts @@ -1,11 +1,10 @@ import UserRepository from "@src/app/repositories/auth/UserRepository"; -import { ApiTokenAttributes } from "@src/core/domains/auth/interfaces/IApitokenModel"; +import { ApiTokenAttributes } from "@src/core/domains/auth/models/ApiToken"; +import { auth } from "@src/core/domains/auth/services/AuthService"; import Observer from "@src/core/domains/observer/services/Observer"; -import { App } from "@src/core/services/App"; +import { authJwt } from "@src/core/domains/auth/services/JwtAuthService"; -interface IApiTokenObserverData extends ApiTokenAttributes { - -} +interface IApiTokenObserverData extends ApiTokenAttributes {} export default class ApiTokenObserver extends Observer { @@ -28,18 +27,17 @@ export default class ApiTokenObserver extends Observer { */ async addGroupScopes(data: IApiTokenObserverData): Promise { - const user = await this.userRepository.findById(data.userId); + const user = await authJwt().getUserRepository().findByIdOrFail(data.userId) if(!user) { return data } - const userGroups = user.getAttributeSync('groups') ?? []; + const userGroups = user.getGroups() for(const userGroup of userGroups) { - const group = App.container('auth').config.permissions.groups.find(g => g.name === userGroup); - const scopes = group?.scopes ?? []; - + const scopes = auth().acl().getGroupScopes(userGroup) + data.scopes = [ ...data.scopes, ...scopes diff --git a/src/app/observers/UserObserver.ts b/src/core/domains/auth/observers/UserObserver.ts similarity index 92% rename from src/app/observers/UserObserver.ts rename to src/core/domains/auth/observers/UserObserver.ts index 9e7814999..8ba102efc 100644 --- a/src/app/observers/UserObserver.ts +++ b/src/core/domains/auth/observers/UserObserver.ts @@ -1,88 +1,90 @@ -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"; - -/** - * Observer for the User model. - * - * Automatically hashes the password on create/update if it is provided. - */ -export default class UserObserver extends Observer { - - protected userCreatedListener: ICtor = UserCreatedListener; - - /** - * Sets the listener to use after a User has been created. - * @param listener The listener to use after a User has been created. - * @returns The UserObserver instance. - */ - setUserCreatedListener(listener: ICtor) { - this.userCreatedListener = listener - } - - /** - * Called when the User model is being created. - * Automatically hashes the password if it is provided. - * @param data The User data being created. - * @returns The processed User data. - */ - async creating(data: UserAttributes): Promise { - data = this.onPasswordChange(data) - data = await this.updateRoles(data) - return data - } - - /** - * Called after the User model has been created. - * @param data The User data that has been created. - * @returns The processed User data. - */ - async created(data: UserAttributes): Promise { - await App.container('events').dispatch(new this.userCreatedListener(data)) - return data - } - - /** - * Updates the roles of the user based on the groups they belong to. - * Retrieves the roles associated with each group the user belongs to from the permissions configuration. - * @param data The User data being created/updated. - * @returns The processed User data with the updated roles. - */ - async updateRoles(data: UserAttributes): Promise { - let updatedRoles: string[] = []; - - for(const group of data.groups) { - const relatedRoles = App.container('auth').config.permissions.groups.find(g => g.name === group)?.roles ?? []; - - updatedRoles = [ - ...updatedRoles, - ...relatedRoles - ] - } - - data.roles = updatedRoles - - return data - } - - /** - * Automatically hashes the password if it is provided. - * @param data The User data being created/updated. - * @returns The processed User data. - */ - onPasswordChange(data: UserAttributes): UserAttributes { - if(!data.password) { - return data - } - - data.hashedPassword = hashPassword(data.password); - delete data.password; - - return data - } - -} +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"; + +/** + * Observer for the User model. + * + * Automatically hashes the password on create/update if it is provided. + */ +export default class UserObserver extends Observer { + + protected userCreatedListener: ICtor = UserCreatedListener; + + /** + * Sets the listener to use after a User has been created. + * @param listener The listener to use after a User has been created. + * @returns The UserObserver instance. + */ + setUserCreatedListener(listener: ICtor) { + this.userCreatedListener = listener + } + + /** + * Called when the User model is being created. + * Automatically hashes the password if it is provided. + * @param data The User data being created. + * @returns The processed User data. + */ + async creating(data: UserAttributes): Promise { + data = this.onPasswordChange(data) + data = await this.updateRoles(data) + return data + } + + /** + * Called after the User model has been created. + * @param data The User data that has been created. + * @returns The processed User data. + */ + async created(data: UserAttributes): Promise { + await App.container('events').dispatch(new this.userCreatedListener(data)) + return data + } + + /** + * Updates the roles of the user based on the groups they belong to. + * Retrieves the roles associated with each group the user belongs to from the permissions configuration. + * @param data The User data being created/updated. + * @returns The processed User data with the updated roles. + */ + async updateRoles(data: UserAttributes): Promise { + let updatedRoles: string[] = []; + + for(const group of data.groups) { + const relatedRoles = App.container('auth.acl').getGroupRoles(group) + const relatedRolesNames = relatedRoles.map(role => role.name) + + updatedRoles = [ + ...updatedRoles, + ...relatedRolesNames + ] + } + + data.roles = updatedRoles + + + return data + } + + /** + * Automatically hashes the password if it is provided. + * @param data The User data being created/updated. + * @returns The processed User data. + */ + onPasswordChange(data: UserAttributes): UserAttributes { + if(!data.password) { + return data + } + + data.hashedPassword = hashPassword(data.password); + delete data.password; + + return data + } + +} diff --git a/src/core/domains/auth/providers/AuthProvider.ts b/src/core/domains/auth/providers/AuthProvider.ts index a355ce295..06e4ce578 100644 --- a/src/core/domains/auth/providers/AuthProvider.ts +++ b/src/core/domains/auth/providers/AuthProvider.ts @@ -1,47 +1,33 @@ -import authConfig from "@src/config/auth"; + + +import { aclConfig } from "@src/config/acl"; +import { authConfig } from "@src/config/auth"; import BaseProvider from "@src/core/base/Provider"; -import GenerateJwtSecret from "@src/core/domains/auth/commands/GenerateJWTSecret"; -import { IAuthConfig } from "@src/core/domains/auth/interfaces/IAuthConfig"; -import { App } from "@src/core/services/App"; - -export default class AuthProvider extends BaseProvider { - - /** - * The configuration for the auth service - */ - protected config: IAuthConfig = authConfig; - - /** - * Register method - * - * Called when the provider is being registered - * Use this method to set up any initial configurations or services - * - * @returns Promise - */ - public async register(): Promise { - - this.log('Registering AuthProvider'); - - /** - * Setup the registed authService - */ - const authServiceCtor = this.config.service.authService; - const authService = new authServiceCtor(this.config); - - /** - * Setup the container - */ - App.setContainer('auth', authService); - - /** - * Register internal commands - */ - App.container('console').register().registerAll([ - GenerateJwtSecret - ]) +import GenerateJwtSecret from "@src/core/domains/auth/commands/GenerateJwtSecret"; +import Auth from "@src/core/domains/auth/services/AuthService"; +import { app } from "@src/core/services/App"; + +class AuthProvider extends BaseProvider{ + + protected config = authConfig + + protected aclConfig = aclConfig + + async register() { + const authService = new Auth(this.config, this.aclConfig); + await authService.boot(); + + // Bind services + this.bind('auth', authService); + this.bind('auth.jwt', (() => authService.getDefaultAdapter())()) + this.bind('auth.acl', (() => authService.acl())()) + + // Register commands + app('console').register().register(GenerateJwtSecret) } - public async boot(): Promise {} } + +export default AuthProvider; + diff --git a/src/core/domains/auth/repository/ApiTokenRepitory.ts b/src/core/domains/auth/repository/ApiTokenRepitory.ts new file mode 100644 index 000000000..1dceec072 --- /dev/null +++ b/src/core/domains/auth/repository/ApiTokenRepitory.ts @@ -0,0 +1,59 @@ +import Repository from "@src/core/base/Repository"; +import { IApiTokenModel } from "@src/core/domains/auth/interfaces/models/IApiTokenModel"; +import { IApiTokenRepository } from "@src/core/domains/auth/interfaces/repository/IApiTokenRepository"; +import ApiToken from "@src/core/domains/auth/models/ApiToken"; +import { queryBuilder } from "@src/core/domains/eloquent/services/EloquentQueryBuilderService"; +import { ModelConstructor } from "@src/core/interfaces/IModel"; + +class ApiTokenRepository extends Repository implements IApiTokenRepository { + + constructor(modelConstructor?: ModelConstructor) { + super(modelConstructor ?? ApiToken) + } + + /** + * Find one active token + * @param token + * @returns + */ + + async findOneActiveToken(token: string): Promise { + const builder = queryBuilder(this.modelConstructor) + + builder.where('token', token) + builder.whereNull('revokedAt') + + return await builder.first() + } + + /** + * Revokes a token + * @param apiToken + * @returns + */ + async revokeToken(apiToken: IApiTokenModel): Promise { + await queryBuilder(this.modelConstructor) + .where('id', apiToken.id as string) + .update({ revokedAt: new Date() }); + } + + + /** + * Revokes all tokens for a user + * @param userId + * @returns + */ + async revokeAllTokens(userId: string | number): Promise { + await queryBuilder(this.modelConstructor) + .where('userId', userId) + .update({ revokedAt: new Date() }); + } + + + + +} + +export default ApiTokenRepository; + + diff --git a/src/core/domains/auth/repository/UserRepository.ts b/src/core/domains/auth/repository/UserRepository.ts new file mode 100644 index 000000000..a52b72024 --- /dev/null +++ b/src/core/domains/auth/repository/UserRepository.ts @@ -0,0 +1,74 @@ +import Repository from "@src/core/base/Repository"; +import { IUserModel } from "@src/core/domains/auth/interfaces/models/IUserModel"; +import { IUserRepository } from "@src/core/domains/auth/interfaces/repository/IUserRepository"; +import AuthUser from "@src/core/domains/auth/models/AuthUser"; +import { queryBuilder } from "@src/core/domains/eloquent/services/EloquentQueryBuilderService"; +import { ModelConstructor } from "@src/core/interfaces/IModel"; + +/** + * Repository class for managing user data operations + * + * This repository extends the base Repository class and implements IUserRepository interface. + * It provides methods for creating, finding and managing user records in the database. + * + * Key features: + * - Create new user records + * - Find users by ID with optional fail behavior + * - Find users by email + * - Uses Eloquent query builder for database operations + * + * @extends Repository + * @implements IUserRepository + */ + +class UserRepository extends Repository implements IUserRepository { + + constructor(userModel?: ModelConstructor) { + super(userModel ?? AuthUser); + } + + /** + * Create a new user record + * + * @param attributes - The attributes for the new user record + * @returns The newly created user record + */ + create(attributes: IUserModel | null = null): IUserModel { + return this.modelConstructor.create(attributes) + } + + /** + * Find a user by their ID + * + * @param id - The ID of the user to find + * @returns The user record or null if not found + */ + async findById(id: string | number): Promise { + return await queryBuilder(this.modelConstructor).find(id) + } + + /** + * Find a user by their ID and fail if not found + * + * @param id - The ID of the user to find + * @returns The user record + */ + async findByIdOrFail(id: string | number): Promise { + return await queryBuilder(this.modelConstructor).findOrFail(id) + } + + /** + * Find a user by their email + * + * @param email - The email of the user to find + * @returns The user record or null if not found + */ + async findByEmail(email: string): Promise { + return await queryBuilder(this.modelConstructor).where('email', email).first() + } + +} + +export default UserRepository; + + diff --git a/src/core/domains/auth/routes/auth.ts b/src/core/domains/auth/routes/auth.ts deleted file mode 100644 index f9b140304..000000000 --- a/src/core/domains/auth/routes/auth.ts +++ /dev/null @@ -1,34 +0,0 @@ -import create from "@src/core/domains/auth/actions/create"; -import login from "@src/core/domains/auth/actions/login"; -import revoke from "@src/core/domains/auth/actions/revoke"; -import update from "@src/core/domains/auth/actions/update"; -import user from "@src/core/domains/auth/actions/user"; -import { IAuthConfig } from "@src/core/domains/auth/interfaces/IAuthConfig"; -import AuthorizeMiddleware from "@src/core/domains/http/middleware/AuthorizeMiddleware"; -import Route from "@src/core/domains/http/router/Route"; - -/** - * todo: missing validator - */ -const authRouter = (config: IAuthConfig) => Route.group({ - prefix: '/auth', -}, (router) => { - - router.post('/login', login) - - router.post('/create', create) - - router.patch('/update', update, { - middlewares: [AuthorizeMiddleware] - }) - - router.get('/user', user, { - middlewares: [AuthorizeMiddleware] - }) - - router.post('/revoke', revoke, { - middlewares: [AuthorizeMiddleware] - }) -}) - -export default authRouter; \ No newline at end of file diff --git a/src/core/domains/auth/services/ACLService.ts b/src/core/domains/auth/services/ACLService.ts new file mode 100644 index 000000000..daea59391 --- /dev/null +++ b/src/core/domains/auth/services/ACLService.ts @@ -0,0 +1,136 @@ +import { IACLService } from "@src/core/domains/auth/interfaces/acl/IACLService"; +import { IAclConfig, IAclGroup, IAclRole } from "@src/core/domains/auth/interfaces/acl/IAclConfig"; +import { IUserModel } from "@src/core/domains/auth/interfaces/models/IUserModel"; + +/** + * Access Control List (ACL) Service + * + * This service manages role-based access control (RBAC) by: + * - Managing user groups and their associated roles + * - Managing roles and their associated permissions/scopes + * - Providing methods to retrieve and validate permissions + * + * The service works with a configuration object that defines: + * - Groups (e.g. 'Admin', 'User') + * - Roles (e.g. 'role_admin', 'role_user') + * - Scopes/permissions for each role + */ +class ACLService implements IACLService { + + private aclConfig: IAclConfig; + + constructor(aclConfig: IAclConfig) { + this.aclConfig = aclConfig; + } + + /** + * Get the ACL config + * @returns + */ + getConfig(): IAclConfig { + return this.aclConfig; + } + + /** + * Get the default group + * @returns + */ + getDefaultGroup(): IAclGroup { + return this.getGroup(this.aclConfig.defaultGroup); + } + + /** + * Get the group + * @param group + * @returns + */ + getGroup(group: string): IAclGroup { + const result = this.aclConfig.groups.find(g => g.name === group); + + if(!result) { + throw new Error(`Group ${group} not found`); + } + + return result; + } + + /** + * Get the roles from the group + * @param group + * @returns + */ + getGroupRoles(group: string | IAclGroup): IAclRole[] { + const groupResult = typeof group === 'string' ? this.getGroup(group) : group; + return groupResult.roles.map(role => this.getRole(role)); + } + + /** + * Get the scopes from the group + * @param group + * @returns + */ + getGroupScopes(group: string | IAclGroup): string[] { + const roles = this.getGroupRoles(group); + return roles.map(role => role.scopes).flat(); + } + + /** + * Retrieves the scopes from the roles + + * @param user + * @returns + */ + getRoleScopesFromUser(user: IUserModel): string[] { + const roles = user.getAttributeSync('roles') as string[] | null; + + if(!roles) { + return []; + } + + let scopes: string[] = []; + + for(const roleString of roles) { + const role = this.getRole(roleString); + scopes = [...scopes, ...role.scopes]; + } + + return scopes; + } + + /** + * Retrieves the role from the config + * @param role + * @returns + */ + getRole(role: string): IAclRole { + const result = this.aclConfig.roles.find(r => r.name === role); + + if(!result) { + throw new Error(`Role ${role} not found`); + } + + return result; + } + + + /** + * Retrieves the scopes from the roles + * @param role + * @returns + */ + getRoleScopes(role: string | string[]): string[] { + const rolesArray = typeof role === 'string' ? [role] : role; + let scopes: string[] = []; + + for(const roleStr of rolesArray) { + const role = this.getRole(roleStr); + scopes = [...scopes, ...role.scopes]; + } + + return scopes; + + } + +} + +export default ACLService; diff --git a/src/core/domains/auth/services/AuthConfig.ts b/src/core/domains/auth/services/AuthConfig.ts new file mode 100644 index 000000000..6439151de --- /dev/null +++ b/src/core/domains/auth/services/AuthConfig.ts @@ -0,0 +1,55 @@ + +import { AuthAdapterConstructor, IAuthAdapter } from "@src/core/domains/auth/interfaces/adapter/IAuthAdapter"; +import { IBaseAuthConfig } from "@src/core/domains/auth/interfaces/config/IAuth"; + +/** + * AuthConfig is a configuration service for managing authentication adapters. + * It provides a centralized way to define and configure different authentication + * strategies (like JWT, OAuth, etc.) through a consistent interface. + * + * The class offers static helper methods to: + * - Define multiple auth configurations using the `define()` method + * - Create individual adapter configs using the `config()` method with type safety + * + * Example usage: + * ```ts + * const authConfig = AuthConfig.define([ + * AuthConfig.config(JwtAuthAdapter, { + * name: 'jwt', + * settings: { + * secret: 'xxx', + * expiresIn: 60 + * } + * }) + * ]); + * ``` + */ +class AuthConfig { + + /** + * Define a new auth config + * @param config - The config for the adapter + * @returns The adapter config + */ + + public static define(config: IBaseAuthConfig[]): IBaseAuthConfig[] { + return config + } + + /** + * Create a new auth adapter config + * @param adapter - The auth adapter constructor + * @param config - The config for the adapter + * @returns The adapter config + */ + public static config(adapter: AuthAdapterConstructor, config: Omit): Adapter['config'] { + return { + adapter: adapter, + ...config + } as unknown as Adapter['config']; + } + + +} + +export default AuthConfig; \ No newline at end of file diff --git a/src/core/domains/auth/services/AuthRequest.ts b/src/core/domains/auth/services/AuthRequest.ts deleted file mode 100644 index d38867bb0..000000000 --- a/src/core/domains/auth/services/AuthRequest.ts +++ /dev/null @@ -1,39 +0,0 @@ -import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; -import IUserModel from "@src/core/domains/auth/interfaces/IUserModel"; -import { auth } from "@src/core/domains/auth/services/AuthService"; -import { TBaseRequest } from "@src/core/domains/http/interfaces/BaseRequest"; -import { App } from "@src/core/services/App"; - -class AuthRequest { - - /** - * Attempts to authorize a request with a Bearer token. - * - * If successful, attaches the user and apiToken to the request. Sets the user in the App. - * - * @param req The request to authorize - * @returns The authorized request - * @throws UnauthorizedError if the token is invalid - */ - public static async attemptAuthorizeRequest(req: TBaseRequest): Promise { - const authorization = (req.headers.authorization ?? '').replace('Bearer ', ''); - - const apiToken = await auth().attemptAuthenticateToken(authorization) - - const user = await apiToken?.getAttribute('user') as IUserModel | null; - - if(!user || !apiToken) { - throw new UnauthorizedError(); - } - - req.user = user; - req.apiToken = apiToken - - App.container('requestContext').setByRequest(req, 'userId', user?.getId()) - - return req; - } - -} - -export default AuthRequest \ No newline at end of file diff --git a/src/core/domains/auth/services/AuthService.ts b/src/core/domains/auth/services/AuthService.ts index 6577889e4..2ff8016ec 100644 --- a/src/core/domains/auth/services/AuthService.ts +++ b/src/core/domains/auth/services/AuthService.ts @@ -1,241 +1,111 @@ -import Service from '@src/core/base/Service'; -import InvalidJWTSecret from '@src/core/domains/auth/exceptions/InvalidJWTSecret'; -import UnauthorizedError from '@src/core/domains/auth/exceptions/UnauthorizedError'; -import JWTTokenFactory from '@src/core/domains/auth/factory/jwtTokenFactory'; -import IApiTokenModel from '@src/core/domains/auth/interfaces/IApitokenModel'; -import IApiTokenRepository from '@src/core/domains/auth/interfaces/IApiTokenRepository'; -import { IAuthConfig } from '@src/core/domains/auth/interfaces/IAuthConfig'; -import { IAuthService } from '@src/core/domains/auth/interfaces/IAuthService'; -import { IPermissionGroup } from '@src/core/domains/auth/interfaces/IPermissionsConfig'; -import IUserModel from '@src/core/domains/auth/interfaces/IUserModel'; -import IUserRepository from '@src/core/domains/auth/interfaces/IUserRepository'; -import authRoutes from '@src/core/domains/auth/routes/auth'; -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 { queryBuilder } from '@src/core/domains/eloquent/services/EloquentQueryBuilderService'; -import { IRouter } from '@src/core/domains/http/interfaces/IRouter'; -import { app } from '@src/core/services/App'; -import { JsonWebTokenError } from 'jsonwebtoken'; - -import Router from '../../http/router/Router'; - +import { AuthAdapters } from "@src/config/auth"; +import BaseAdapter from "@src/core/base/BaseAdapter"; +import { app } from "@src/core/services/App"; +import { IACLService } from "@src/core/domains/auth/interfaces/acl/IACLService"; +import { IAclConfig } from "@src/core/domains/auth/interfaces/acl/IAclConfig"; +import { IBaseAuthConfig } from "@src/core/domains/auth/interfaces/config/IAuth"; +import { IAuthService } from "@src/core/domains/auth/interfaces/service/IAuthService"; +import ACLService from "@src/core/domains/auth/services/ACLService"; /** - * Shorthand for accessing the auth service - * @returns + * Short hand for app('auth') */ export const auth = () => app('auth'); /** - * Auth service + * Short hand for app('auth').acl() */ -export default class AuthService extends Service implements IAuthService { +export const acl = () => app('auth.acl'); - /** - * Auth config - */ - public config: IAuthConfig; +/** + * Auth Service + * + * This is the main authentication service that manages different authentication adapters + * (like JWT, Session etc) and integrates with ACL (Access Control List). + * + * Key responsibilities: + * - Manages multiple authentication adapters (JWT by default) + * - Integrates with ACL service for role/permission management + * - Provides helper methods to access default and specific adapters + * - Handles adapter registration and initialization + * + * The service works with: + * - AuthAdapters: Different authentication implementations (JWT etc) + * - ACLService: For managing roles and permissions + * - IBaseAuthConfig: Configuration for each auth adapter + * + * Usage: + * - Use auth() helper to access the service + * - Use acl() helper to access ACL functionality + */ - /** - * Repository for accessing user data - */ - public userRepository: IUserRepository; +class Auth extends BaseAdapter implements IAuthService { - /** - * Repository for accessing api tokens - */ - public apiTokenRepository: IApiTokenRepository; + private config!: IBaseAuthConfig[]; + private aclService!: IACLService; - constructor( - config: IAuthConfig - ) { - super() + constructor(config: IBaseAuthConfig[], aclConfig: IAclConfig) { + super(); this.config = config; - this.userRepository = new config.repositories.user; - this.apiTokenRepository = new config.repositories.apiToken; - this.validateJwtSecret(); + this.aclService = new ACLService(aclConfig); } /** - * Validate jwt secret + * Get the default adapter + * @returns The default adapter */ - private validateJwtSecret() { - if (!this.config.jwtSecret || this.config.jwtSecret === '') { - throw new InvalidJWTSecret(); - } + public getDefaultAdapter(): AuthAdapters['default'] { + return this.getAdapter('jwt') as AuthAdapters['default']; } /** - * Create a new ApiToken model from the User - * @param user - * @returns + * Get the JWT adapter + * @returns The JWT adapter */ - public async createApiTokenFromUser(user: IUserModel, scopes: string[] = []): Promise { - const userScopes = this.getRoleScopes(user); - const factory = new this.config.factory.apiTokenFactory(this.config.models.apiToken); - const apiToken = factory.createFromUser(user, [...userScopes, ...scopes]); - await apiToken.save(); - return apiToken + public getJwtAdapter(): AuthAdapters['jwt'] { + return this.getAdapter('jwt') as AuthAdapters['jwt']; } /** - * Creates a JWT from a user model - * @param user - * @returns + * Boots the auth service + * @returns A promise that resolves when the auth service is booted */ - async createJwtFromUser(user: IUserModel, scopes: string[] = []): Promise { - const apiToken = await this.createApiTokenFromUser(user, scopes); - return this.jwt(apiToken) + public async boot(): Promise { + await this.registerAdapters(); + await this.bootAdapters(); } /** - * Generates a JWT - * @param apiToken - * @returns + * Registers the adapters */ - jwt(apiToken: IApiTokenModel): string { - if (!apiToken?.userId) { - throw new Error('Invalid token'); - } - - const userId = apiToken.getAttributeSync('userId')?.toString() ?? ''; - const token = apiToken.getAttributeSync('token') ?? ''; - - const payload = JWTTokenFactory.create(userId, token); - return createJwt(this.config.jwtSecret, payload, `${this.config.expiresInMinutes}m`); - } + protected registerAdapters(): void { + const aclConfig = this.aclService.getConfig(); - /** - * Revokes a token - * @param apiToken - * @returns - */ - async revokeToken(apiToken: IApiTokenModel): Promise { - if (apiToken?.revokedAt) { - return; + for(const adapter of this.config) { + const adapterInstance = new adapter.adapter(adapter, aclConfig); + this.addAdapterOnce(adapter.name, adapterInstance); } - await queryBuilder(this.apiTokenRepository.modelConstructor) - .where('userId', apiToken.userId as string) - .update({ revokedAt: new Date() }); } /** - * Attempt authentication against a JWT - * @param token - * @returns + * Boots the adapters */ - async attemptAuthenticateToken(token: string): Promise { - try { - const decoded = decodeJwt(this.config.jwtSecret, token); - - const apiToken = await this.apiTokenRepository.findOneActiveToken(decoded.token) - - if (!apiToken) { - throw new UnauthorizedError() - } - - const user = await this.userRepository.findById(decoded.uid) - - if (!user) { - throw new UnauthorizedError() - } - - return apiToken + protected async bootAdapters(): Promise { + for(const adapterInstance of Object.values(this.adapters)) { + await adapterInstance.boot(); } - catch (err) { - if(err instanceof JsonWebTokenError) { - throw new UnauthorizedError() - } - } - - return null } /** - * Attempt login with credentials - * @param email - * @param password - * @returns + * Get the ACL service + * @returns The ACL service */ - async attemptCredentials(email: string, password: string, scopes: string[] = []): Promise { - const user = await this.userRepository.findOneByEmail(email); - - if (!user) { - throw new UnauthorizedError() - } - - const hashedPassword = user.getAttributeSync('hashedPassword') - - if (hashedPassword && !comparePassword(password, hashedPassword)) { - throw new UnauthorizedError() - } - - return await this.createJwtFromUser(user, scopes) - } - - /** - * Returns the auth routes - * - * @returns an array of IRoute objects, or a blank router if auth routes are disabled - */ - getAuthRoutes(): IRouter { - if (!this.config.enableAuthRoutes) { - return new Router() - } - - const router = authRoutes(this.config); - - /** - * todo - */ - if (!this.config.enableAuthRoutesAllowCreate) { - router.setRegisteredRoutes(router.getRegisteredRoutes().filter((route) => route.name !== 'authCreate')); - } - - return router; - } - - /** - * Retrieves the user repository instance. - * @returns {IUserRepository} The user repository instance. - */ - getUserRepository(): IUserRepository { - return new this.config.repositories.user(this.config.models.user); - } - - /** - * Retrieves the groups from the roles - * @param roles - * @returns - */ - getGroupsFromRoles(roles: string[] | string): IPermissionGroup[] { - const rolesArray = typeof roles === 'string' ? [roles] : roles; - const groups = this.config.permissions.groups; - - return groups.filter((group) => { - const groupRoles = group.roles ?? []; - return groupRoles.some((role) => rolesArray.includes(role)) - }); - } - - /** - * Retrieves the scopes from the roles - * @param user - * @returns - */ - getRoleScopes(user: IUserModel): string[] { - const roles = user.getAttributeSync('roles') ?? []; - const groups = this.getGroupsFromRoles(roles); - let scopes: string[] = []; - - for(const group of groups) { - scopes = [...scopes, ...(group.scopes ?? [])]; - } - - return scopes; + public acl(): IACLService { + return this.aclService; } + +} -} \ No newline at end of file +export default Auth; \ No newline at end of file diff --git a/src/core/domains/auth/services/JwtAuthService.ts b/src/core/domains/auth/services/JwtAuthService.ts new file mode 100644 index 000000000..6fb4372e8 --- /dev/null +++ b/src/core/domains/auth/services/JwtAuthService.ts @@ -0,0 +1,282 @@ +import BaseAuthAdapter from "@src/core/domains/auth/base/BaseAuthAdapter"; +import AuthController from "@src/core/domains/auth/controllers/AuthController"; +import InvalidSecretException from "@src/core/domains/auth/exceptions/InvalidJwtSettings"; +import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; +import JwtFactory from "@src/core/domains/auth/factory/JwtFactory"; +import { IAclConfig } from "@src/core/domains/auth/interfaces/acl/IAclConfig"; +import { IACLService } from "@src/core/domains/auth/interfaces/acl/IACLService"; +import { IJwtAuthService } from "@src/core/domains/auth/interfaces/jwt/IJwtAuthService"; +import { IJwtConfig } from "@src/core/domains/auth/interfaces/jwt/IJwtConfig"; +import { IApiTokenModel } from "@src/core/domains/auth/interfaces/models/IApiTokenModel"; +import { IUserModel } from "@src/core/domains/auth/interfaces/models/IUserModel"; +import { IApiTokenRepository } from "@src/core/domains/auth/interfaces/repository/IApiTokenRepository"; +import { IUserRepository } from "@src/core/domains/auth/interfaces/repository/IUserRepository"; +import AuthorizeMiddleware from "@src/core/domains/auth/middleware/AuthorizeMiddleware"; +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"; +import { IRouter } from "@src/core/domains/http/interfaces/IRouter"; +import Route from "@src/core/domains/http/router/Route"; +import Router from "@src/core/domains/http/router/Router"; +import { app } from "@src/core/services/App"; +import { JsonWebTokenError } from "jsonwebtoken"; + +/** + * Short hand for app('auth.jwt') + */ +export const authJwt = () => app('auth.jwt') + +/** + * JwtAuthService is an authentication adapter that implements JWT (JSON Web Token) based authentication. + * It extends BaseAuthAdapter and provides JWT-specific authentication functionality. + * + * This service: + * - Handles JWT token creation and validation + * - Manages user authentication via email/password credentials + * - Provides API token management for machine-to-machine auth + * - Configures auth-related routes and middleware + * - Integrates with the application's ACL (Access Control List) system + * + * The service can be accessed via the 'auth.jwt' helper: + * ```ts + * const jwtAuth = authJwt(); + * const token = await jwtAuth.attemptCredentials(email, password); + * ``` + */ +class JwtAuthService extends BaseAuthAdapter implements IJwtAuthService { + + private apiTokenRepository!: IApiTokenRepository + + private userRepository!: IUserRepository + + protected aclService!: IACLService + + constructor(config: IJwtConfig, aclConfig: IAclConfig) { + super(config, aclConfig); + this.apiTokenRepository = new ApiTokenRepository(config.models?.apiToken) + this.userRepository = new UserRepository(config.models?.user) + this.aclService = new ACLService(aclConfig) + } + + /** + * Get the JWT secret + * + * @returns + */ + private getJwtSecret(): string { + if(!this.config.settings.secret) { + throw new InvalidSecretException() + } + return this.config.settings.secret + } + + /** + * Get the JWT expires in minutes + * + * @returns + */ + + private getJwtExpiresInMinutes(): number { + if(!this.config.settings.expiresInMinutes) { + throw new InvalidSecretException() + } + return this.config.settings.expiresInMinutes + } + + /** + * Attempt login with credentials + * @param email + * @param password + * @returns + */ + async attemptCredentials(email: string, password: string, scopes: string[] = []): Promise { + const user = await this.userRepository.findByEmail(email); + + if (!user) { + throw new UnauthorizedError() + } + + const hashedPassword = user.getAttributeSync('hashedPassword') as string | null + + if(!hashedPassword) { + throw new UnauthorizedError() + } + + if (!comparePassword(password, hashedPassword)) { + throw new UnauthorizedError() + } + + // Generate the api token + const apiToken = await this.buildApiTokenByUser(user, scopes) + await apiToken.save() + + // Generate the JWT token + return this.generateJwt(apiToken) + } + + /** + * Create a new ApiToken model from the User + * @param user + * @returns + */ + protected async buildApiTokenByUser(user: IUserModel, scopes: string[] = []): Promise { + const apiToken = ApiToken.create() + apiToken.setUserId(user.id as string) + apiToken.setToken(generateToken()) + apiToken.setScopes([...this.aclService.getRoleScopesFromUser(user), ...scopes]) + apiToken.setRevokedAt(null) + return apiToken + } + + /** + * Generate a JWT token + * @param apiToken + * @returns + */ + protected generateJwt(apiToken: IApiTokenModel) { + if (!apiToken?.userId) { + throw new Error('Invalid token'); + } + + // Create the payload + const payload = JwtFactory.createUserIdAndPayload(apiToken.getUserId(), apiToken.getToken()); + + // Get the expires in minutes. Example: 1m + const expiresIn = `${this.getJwtExpiresInMinutes()}m` + + // Create the JWT token + return createJwt(this.getJwtSecret(), payload, expiresIn) + } + + /** + * Attempt authentication against a JWT + + * @param token + * @returns + */ + async attemptAuthenticateToken(token: string): Promise { + try { + const { token: decodedToken, uid: decodedUserId } = decodeJwt(this.getJwtSecret(), token); + + const apiToken = await this.apiTokenRepository.findOneActiveToken(decodedToken) + + if (!apiToken) { + throw new UnauthorizedError() + } + + const user = await this.userRepository.findById(decodedUserId) + + if (!user) { + throw new UnauthorizedError() + } + + return apiToken + } + catch (err) { + if(err instanceof JsonWebTokenError) { + throw new UnauthorizedError() + } + } + + return null + } + + /** + * Create a JWT token from a user + * @param user + * @returns + */ + public async createJwtFromUser(user: IUserModel): Promise { + const apiToken = await this.buildApiTokenByUser(user) + await apiToken.save() + return this.generateJwt(apiToken) + } + + /** + * Refresh a token + * @param apiToken + * @returns + */ + + refreshToken(apiToken: IApiTokenModel): string { + return this.generateJwt(apiToken) + } + + /** + * Revokes a token + * @param apiToken + * @returns + */ + + async revokeToken(apiToken: IApiTokenModel): Promise { + if (apiToken?.revokedAt) { + return; + } + + await this.apiTokenRepository.revokeToken(apiToken) + } + + /** + * Revokes all tokens for a user + * @param userId + * @returns + */ + async revokeAllTokens(userId: string | number): Promise { + await this.apiTokenRepository.revokeAllTokens(userId) + } + + /** + * Get the router + * @returns + */ + getRouter(): IRouter { + if(!this.config.routes.enableAuthRoutes) { + return new Router(); + } + + return Route.group({ + prefix: '/auth', + controller: AuthController, + config: { + adapter: 'jwt' + } + }, (router) => { + + router.post('/login', 'login'); + + if(this.config.routes.enableAuthRoutesAllowCreate) { + router.post('/register', 'register'); + } + + router.group({ + middlewares: [AuthorizeMiddleware] + }, (router) => { + router.get('/user', 'user'); + router.patch('/update', 'update'); + router.post('/refresh', 'refresh'); + router.post('/logout', 'logout'); + + }) + + }) + } + + /** + * Get the user repository + * @returns The user repository + */ + public getUserRepository(): IUserRepository { + return new UserRepository(this.config.models?.user); + } + +} + + + + +export default JwtAuthService; diff --git a/src/core/domains/auth/services/ModelScopes.ts b/src/core/domains/auth/services/ModelScopes.ts deleted file mode 100644 index fe6c54e80..000000000 --- a/src/core/domains/auth/services/ModelScopes.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Scope } from "@src/core/domains/auth/interfaces/IScope"; -import { ModelConstructor } from "@src/core/interfaces/IModel"; - -class ModelScopes { - - /** - * Generates a list of scopes, given a model name and some scope types. - * - * Available Scopes - * - all - All scopes - * - read - Read scopes - * - write - Write scopes - * - delete - Delete scopes - * - create - Create scopes - * - * @param model The model as a constructor - * @param scopes The scope type(s) to generate scopes for. If a string, it will be an array of only that type. - * @returns A list of scopes in the format of 'modelName:scopeType' - * - * Example: - * - * const scopes = ModelScopes.getScopes(BlogModel, ['write', 'read']) - * - * // Output - * [ - * 'BlogModel:write', - * 'BlogModel:read' - * ] - */ - public static getScopes(model: ModelConstructor, scopes: Scope[] = ['all'], additionalScopes: string[] = []): string[] { - if(scopes?.[0] === 'all') { - scopes = ['read', 'write', 'delete', 'create']; - } - return [...scopes.map((scope) => `${(model.name)}:${scope}`), ...additionalScopes]; - } - -} - -export default ModelScopes \ No newline at end of file diff --git a/src/core/domains/auth/usecase/LoginUseCase.ts b/src/core/domains/auth/usecase/LoginUseCase.ts new file mode 100644 index 000000000..ce70cf7d6 --- /dev/null +++ b/src/core/domains/auth/usecase/LoginUseCase.ts @@ -0,0 +1,88 @@ + +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"; + +/** + * LoginUseCase handles user authentication by validating credentials and generating JWT tokens + * + * This class is responsible for: + * - Validating user email and password credentials + * - Generating JWT tokens for authenticated users + * - Returning user data and token on successful login + * - Handling authentication errors and unauthorized access + * + * The handle() method processes the login request by: + * 1. Extracting credentials from the request body + * 2. Looking up the user by email + * 3. Verifying the password hash matches + * 4. Generating a JWT token via JwtAuthService + * 5. Returning the token and user data in the response + */ +class LoginUseCase { + + /** + * Handle the login use case + + * @param context The HTTP context + * @returns The API response + */ + async handle(context: HttpContext): Promise { + const apiResponse = new ApiResponse(); + + const { email = '', password = '' } = context.getBody(); + + const user = await authJwt().getUserRepository().findByEmail(email); + + if(!user) { + 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 { + jwtToken = await authJwt().attemptCredentials(email, password); + } + + catch (error) { + if(error instanceof UnauthorizedError) { + return this.unauthorized('Email or password is incorrect'); + } + throw error; + } + + const userAttributes = await user.toObject({ excludeGuarded: true }); + + return apiResponse.setData({ + token: jwtToken, + user: userAttributes + }).setCode(201); + + } + + /** + * Unauthorized response + * @param message The message + * @returns The API response + */ + unauthorized(message = 'Unauthorized') { + return new ApiResponse().setCode(401).setData({ + message + }); + } + + +} + + +export default LoginUseCase; + + diff --git a/src/core/domains/auth/usecase/LogoutUseCase.ts b/src/core/domains/auth/usecase/LogoutUseCase.ts new file mode 100644 index 000000000..6eb31660a --- /dev/null +++ b/src/core/domains/auth/usecase/LogoutUseCase.ts @@ -0,0 +1,40 @@ + +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"; + +/** + * LogoutUseCase handles user logout by revoking their JWT token + * + * This class is responsible for: + * - Validating the user has a valid API token + * - Revoking/invalidating the JWT token via JwtAuthService + * - Returning a successful empty response + */ +class LogoutUseCase { + + /** + * Handle the user use case + * @param context The HTTP context + * @returns The API response + */ + + async handle(context: HttpContext): Promise { + const apiToken = context.getApiToken(); + + if(!apiToken) { + throw new UnauthorizedError(); + } + + await authJwt().revokeToken(apiToken); + + return new ApiResponse().setCode(204) + } + +} + + +export default LogoutUseCase; + + diff --git a/src/core/domains/auth/usecase/RefreshUseCase.ts b/src/core/domains/auth/usecase/RefreshUseCase.ts new file mode 100644 index 000000000..c695420bb --- /dev/null +++ b/src/core/domains/auth/usecase/RefreshUseCase.ts @@ -0,0 +1,45 @@ + +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"; + +/** + * RefreshUseCase handles JWT token refresh requests + * + * This class is responsible for: + * - Validating the user has a valid existing API token + * - Generating a new JWT token via JwtAuthService's refresh mechanism + * - Returning the new token in the response + * + */ + +class RefreshUseCase { + + /** + * Handle the user use case + * @param context The HTTP context + * @returns The API response + */ + + async handle(context: HttpContext): Promise { + const apiToken = context.getApiToken(); + + if(!apiToken) { + throw new UnauthorizedError(); + } + + const refreshedToken = authJwt().refreshToken(apiToken); + + return new ApiResponse().setData({ + token: refreshedToken + }).setCode(200) + + } + +} + + +export default RefreshUseCase; + + diff --git a/src/core/domains/auth/usecase/RegisterUseCase.ts b/src/core/domains/auth/usecase/RegisterUseCase.ts new file mode 100644 index 000000000..a0d73dc43 --- /dev/null +++ b/src/core/domains/auth/usecase/RegisterUseCase.ts @@ -0,0 +1,111 @@ +import HttpContext from "@src/core/domains/http/context/HttpContext"; +import ApiResponse from "@src/core/domains/http/response/ApiResponse"; +import IValidatorResult from "@src/core/domains/validator/interfaces/IValidatorResult"; +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"; + +/** + * RegisterUseCase handles new user registration + * + * This class is responsible for: + * - Validating user registration data via configured validator + * - Creating new user records with hashed passwords + * - Assigning default groups and roles to new users + * - Saving user data to the configured user repository + * + */ +class RegisterUseCase { + + /** + * Handle the register use case + * @param context The HTTP context + * @returns The API response + */ + async handle(context: HttpContext): Promise { + const apiResponse = new ApiResponse(); + const validationResult = await this.validate(context); + + if(!validationResult.success) { + return apiResponse.setCode(422).setData({ + errors: validationResult.joi.error?.details ?? [] + }); + } + + if(this.validateUserAndPasswordPresent(context, apiResponse).getCode() !== 200) { + return apiResponse; + } + + const createdUser = await this.createUser(context); + const userAttributes = await createdUser.toObject({ excludeGuarded: true }); + + return apiResponse.setData(userAttributes).setCode(201); + } + + /** + * Create a user + * @param context The HTTP context + * @returns The user + */ + async createUser(context: HttpContext): Promise { + const userAttributes = { + email: context.getBody().email, + + hashedPassword: hashPassword(context.getBody().password), + groups: [acl().getDefaultGroup().name], + roles: [acl().getGroupRoles(acl().getDefaultGroup()).map(role => role.name)], + ...context.getBody() + + } + + // Create and save the user + const user = authJwt().getUserRepository().create(userAttributes); + await user.save(); + + return user; + } + + + /** + * Validate the user and password are present + * @param context The HTTP context + * @param apiResponse The API response + * @returns The API response + */ + validateUserAndPasswordPresent(context: HttpContext, apiResponse: ApiResponse): ApiResponse { + const { + email = null, + + password = null + } = context.getBody(); + + if(!email || !password) { + apiResponse.setCode(422).setData({ + errors: [{ + message: 'Email and password are required' + }] + }); + } + + return apiResponse; + } + + /** + * Validate the request body + * @param context The HTTP context + * @returns The validation result + */ + + async validate(context: HttpContext): Promise> { + const validatorConstructor = auth().getJwtAdapter().config.validators.createUser + const validator = new validatorConstructor(); + return await validator.validate(context.getBody()); + } + + +} + + +export default RegisterUseCase; + diff --git a/src/core/domains/auth/usecase/UpdateUseCase.ts b/src/core/domains/auth/usecase/UpdateUseCase.ts new file mode 100644 index 000000000..eefd9007b --- /dev/null +++ b/src/core/domains/auth/usecase/UpdateUseCase.ts @@ -0,0 +1,73 @@ + +import HttpContext from "@src/core/domains/http/context/HttpContext"; +import ApiResponse from "@src/core/domains/http/response/ApiResponse"; +import IValidatorResult from "@src/core/domains/validator/interfaces/IValidatorResult"; +import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; +import { auth } from "@src/core/domains/auth/services/AuthService"; + +/** + * UpdateUseCase handles user profile updates + * + * This class is responsible for: + * - Validating that the user is authenticated + * - Validating the update data via configured validator + * - Updating the user's profile information + * - Saving changes to the user repository + * - Returning the updated user data + * + */ +class UpdateUseCase { + + /** + * Handle the user update use case + * @param context The HTTP context + * @returns The API response + */ + + async handle(context: HttpContext): Promise { + const userId = context.getUser()?.getId(); + + if(!userId) { + throw new UnauthorizedError(); + } + + const validationResult = await this.validate(context); + + if(!validationResult.success) { + return new ApiResponse().setCode(422).setData({ + errors: validationResult.joi.error?.details + }) + } + + const user = await auth().getJwtAdapter().getUserRepository().findByIdOrFail(userId); + + // Update the user and save + user.fill(context.getBody()); + await user.save(); + + + // Get the user attributes + const userAttributes = await user.toObject({ excludeGuarded: true}) + + return new ApiResponse().setData({ + user: userAttributes + }).setCode(200) + } + + /** + * Validate the request body + * @param context The HTTP context + * @returns The validation result + */ + async validate(context: HttpContext): Promise> { + const validatorConstructor = auth().getJwtAdapter().config.validators.updateUser + const validator = new validatorConstructor(); + return await validator.validate(context.getBody()); + } + +} + + +export default UpdateUseCase; + + diff --git a/src/core/domains/auth/usecase/UserUseCase.ts b/src/core/domains/auth/usecase/UserUseCase.ts new file mode 100644 index 000000000..0d03e737d --- /dev/null +++ b/src/core/domains/auth/usecase/UserUseCase.ts @@ -0,0 +1,47 @@ + +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"; + +/** + * UserUseCase handles retrieving the authenticated user's profile + * + * This class is responsible for: + * - Validating that the user is authenticated via their JWT token + * - Retrieving the user's data from the user repository + * - Returning the user's profile data, excluding guarded attributes + * + */ +class UserUseCase { + + /** + * Handle the user use case + * @param context The HTTP context + * @returns The API response + */ + + async handle(context: HttpContext): Promise { + const userId = context.getUser()?.getId(); + + if(!userId) { + throw new UnauthorizedError(); + } + + const user = await authJwt().getUserRepository().findById(userId); + + if(!user) { + throw new UnauthorizedError(); + } + + const userAttributes = await user.toObject({ excludeGuarded: true }); + + return new ApiResponse().setData(userAttributes); + } + +} + + +export default UserUseCase; + + diff --git a/src/core/domains/auth/services/Scopes.ts b/src/core/domains/auth/utils/ScopeMatcher.ts similarity index 97% rename from src/core/domains/auth/services/Scopes.ts rename to src/core/domains/auth/utils/ScopeMatcher.ts index d6d724230..8b0292c84 100644 --- a/src/core/domains/auth/services/Scopes.ts +++ b/src/core/domains/auth/utils/ScopeMatcher.ts @@ -1,4 +1,4 @@ -class Scopes { +class ScopeMatcher { /** * Returns an object of default scopes that can be used in the system. @@ -54,4 +54,4 @@ class Scopes { } } -export default Scopes \ No newline at end of file +export default ScopeMatcher \ No newline at end of file diff --git a/src/core/domains/auth/utils/decodeJwt.ts b/src/core/domains/auth/utils/decodeJwt.ts index 58b07a377..07d65cd40 100644 --- a/src/core/domains/auth/utils/decodeJwt.ts +++ b/src/core/domains/auth/utils/decodeJwt.ts @@ -1,5 +1,5 @@ -import { IJSonWebToken } from '@src/core/domains/auth/interfaces/IJSonWebToken' import jwt from 'jsonwebtoken' +import { IJSonWebToken } from '@src/core/domains/auth/interfaces/jwt/IJsonWebToken' /** * Decodes a JWT token using the provided secret. diff --git a/src/core/domains/http/base/Controller.ts b/src/core/domains/http/base/Controller.ts index 644cdaee5..da7131209 100644 --- a/src/core/domains/http/base/Controller.ts +++ b/src/core/domains/http/base/Controller.ts @@ -27,8 +27,13 @@ class Controller implements IController { static executeAction(action: string, context: HttpContext): Promise { // Inject URL params as action arguments + // Add the context as the last argument const params = context.getParams() - const actionArgs = Object.keys(params).map(key => params[key]) + const actionArgs = [ + ...Object.keys(params).map(key => params[key]), + context + ] + const controller = new this(context) return controller[action](...actionArgs) diff --git a/src/core/domains/http/base/Middleware.ts b/src/core/domains/http/base/Middleware.ts index 630b2043a..55d9e0f8a 100644 --- a/src/core/domains/http/base/Middleware.ts +++ b/src/core/domains/http/base/Middleware.ts @@ -4,8 +4,7 @@ import { IExpressable } from "@src/core/domains/http/interfaces/IExpressable"; import { IMiddleware, MiddlewareConstructor, TExpressMiddlewareFn } from "@src/core/domains/http/interfaces/IMiddleware"; import { TRouteItem } from "@src/core/domains/http/interfaces/IRouter"; import { NextFunction, Response } from "express"; - -import responseError from "../../http/handlers/responseError"; +import responseError from "@src/core/domains/http/handlers/responseError"; /** * Abstract base class that transforms Express middleware into a class-based format. diff --git a/src/core/domains/http/context/HttpContext.ts b/src/core/domains/http/context/HttpContext.ts index 229bb47a5..1da0e65f8 100644 --- a/src/core/domains/http/context/HttpContext.ts +++ b/src/core/domains/http/context/HttpContext.ts @@ -1,10 +1,12 @@ -import IApiTokenModel from '@src/core/domains/auth/interfaces/IApitokenModel'; -import IUserModel from '@src/core/domains/auth/interfaces/IUserModel'; import HttpContextException from '@src/core/domains/express/exceptions/HttpContextException'; import { requestContext } from '@src/core/domains/http/context/RequestContext'; import { TBaseRequest } from '@src/core/domains/http/interfaces/BaseRequest'; import { TRouteItem } from '@src/core/domains/http/interfaces/IRouter'; import { NextFunction, Response } from 'express'; +import { IUserModel } from '@src/core/domains/auth/interfaces/models/IUserModel'; +import { IApiTokenModel } from '@src/core/domains/auth/interfaces/models/IApiTokenModel'; + + class HttpContext { diff --git a/src/core/domains/http/interfaces/BaseRequest.ts b/src/core/domains/http/interfaces/BaseRequest.ts index d94ea7921..5ffc221b7 100644 --- a/src/core/domains/http/interfaces/BaseRequest.ts +++ b/src/core/domains/http/interfaces/BaseRequest.ts @@ -1,10 +1,16 @@ -import IAuthorizedRequest from "@src/core/domains/auth/interfaces/IAuthorizedRequest"; -import IRequestIdentifiable from "@src/core/domains/auth/interfaces/IRequestIdentifiable"; import { ISecurityRequest } from "@src/core/domains/http/interfaces/ISecurity"; import IValidatorRequest from "@src/core/domains/http/interfaces/IValidatorRequest"; import { Request } from "express"; +import IAuthorizedRequest from "./IAuthorizedRequest"; +import { IRequestIdentifiable } from "./IRequestIdentifable"; + /** - * Extends the express Request object with auth and validator properties. + * TBaseRequest combines multiple request interfaces to create a comprehensive request type. + * It extends Express's Request and includes: + * - Authorization capabilities (IAuthorizedRequest) + * - Request validation (IValidatorRequest) + * - Security features (ISecurityRequest) + * - Request identification (IRequestIdentifiable) */ export type TBaseRequest = Request & IAuthorizedRequest & IValidatorRequest & ISecurityRequest & IRequestIdentifiable; \ No newline at end of file diff --git a/src/core/domains/http/interfaces/IAuthorizedRequest.ts b/src/core/domains/http/interfaces/IAuthorizedRequest.ts index fb0d084c0..22c54595e 100644 --- a/src/core/domains/http/interfaces/IAuthorizedRequest.ts +++ b/src/core/domains/http/interfaces/IAuthorizedRequest.ts @@ -1,6 +1,6 @@ -import ApiToken from '@src/app/models/auth/ApiToken'; import User from '@src/app/models/auth/User'; +import ApiToken from '@src/core/domains/auth/models/ApiToken'; export default interface IAuthorizedRequest { user?: User | null; diff --git a/src/core/domains/http/interfaces/IRequestIdentifable.ts b/src/core/domains/http/interfaces/IRequestIdentifable.ts new file mode 100644 index 000000000..448528d88 --- /dev/null +++ b/src/core/domains/http/interfaces/IRequestIdentifable.ts @@ -0,0 +1,3 @@ +export interface IRequestIdentifiable { + id: string; +} diff --git a/src/core/domains/http/interfaces/IResourceService.ts b/src/core/domains/http/interfaces/IResourceService.ts index 7cc232588..5e22a6cf8 100644 --- a/src/core/domains/http/interfaces/IResourceService.ts +++ b/src/core/domains/http/interfaces/IResourceService.ts @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-vars */ + export interface IPageOptions { page: number; diff --git a/src/core/domains/http/interfaces/IRouter.ts b/src/core/domains/http/interfaces/IRouter.ts index 0c76f0e3a..1f4fa59fe 100644 --- a/src/core/domains/http/interfaces/IRouter.ts +++ b/src/core/domains/http/interfaces/IRouter.ts @@ -3,10 +3,9 @@ import { ControllerConstructor } from "@src/core/domains/http/interfaces/IContro import { TExpressMiddlewareFnOrClass } from "@src/core/domains/http/interfaces/IMiddleware"; import { ISecurityRule } from "@src/core/domains/http/interfaces/ISecurity"; import { IModel, ModelConstructor } from "@src/core/interfaces/IModel"; - -import { ValidatorConstructor } from "../../validator/interfaces/IValidator"; -import SecurityRules from "../security/services/SecurityRules"; -import { TSortDirection } from "../utils/SortOptions"; +import { ValidatorConstructor } from "@src/core/domains/validator/interfaces/IValidator"; +import SecurityRules from "@src/core/domains/http/security/services/SecurityRules"; +import { TSortDirection } from "@src/core/domains/http/utils/SortOptions"; export type RouteConstructor = { new (...args: any[]): IRouter; @@ -19,8 +18,10 @@ export interface IRouteGroupOptions { middlewares?: TExpressMiddlewareFnOrClass | TExpressMiddlewareFnOrClass[]; controller?: ControllerConstructor; security?: ISecurityRule[] + config?: Record; } + export type TRouteGroupFn = (routes: IRouter) => void; export type TPartialRouteItemOptions = Omit; @@ -67,6 +68,7 @@ export type TRouteItem = { controller?: ControllerConstructor; security?: ISecurityRule[]; scopes?: string[]; + config?: Record; resource?: { type: TResourceType modelConstructor: ModelConstructor; diff --git a/src/core/domains/http/interfaces/ISecurity.ts b/src/core/domains/http/interfaces/ISecurity.ts index a8f2845fd..8cb28f1f7 100644 --- a/src/core/domains/http/interfaces/ISecurity.ts +++ b/src/core/domains/http/interfaces/ISecurity.ts @@ -1,7 +1,6 @@ /* eslint-disable no-unused-vars */ import { Request } from 'express'; - -import HttpContext from '../context/HttpContext'; +import HttpContext from '@src/core/domains/http/context/HttpContext'; export type TSecurityRuleOptions = { id: string; diff --git a/src/core/domains/http/middleware/BasicLoggerMiddleware.ts b/src/core/domains/http/middleware/BasicLoggerMiddleware.ts index b029ec01c..10c3a863b 100644 --- a/src/core/domains/http/middleware/BasicLoggerMiddleware.ts +++ b/src/core/domains/http/middleware/BasicLoggerMiddleware.ts @@ -1,7 +1,6 @@ import Middleware from '@src/core/domains/http/base/Middleware'; import HttpContext from '@src/core/domains/http/context/HttpContext'; - -import { logger } from '../../logger/services/LoggerService'; +import { logger } from '@src/core/domains/logger/services/LoggerService'; /** * BasicLoggerMiddleware logs basic information about incoming requests diff --git a/src/core/domains/http/middleware/EndRequestContextMiddleware.ts b/src/core/domains/http/middleware/EndRequestContextMiddleware.ts index 24a4a4cb7..07a0a9012 100644 --- a/src/core/domains/http/middleware/EndRequestContextMiddleware.ts +++ b/src/core/domains/http/middleware/EndRequestContextMiddleware.ts @@ -1,7 +1,6 @@ import HttpContext from "@src/core/domains/http/context/HttpContext"; import { App } from "@src/core/services/App"; - -import Middleware from "../base/Middleware"; +import Middleware from "@src/core/domains/http/base/Middleware"; /** * Middleware that ends the current request context and removes all associated values. diff --git a/src/core/domains/http/middleware/SecurityMiddleware.ts b/src/core/domains/http/middleware/SecurityMiddleware.ts index b018574ed..d5489d1ac 100644 --- a/src/core/domains/http/middleware/SecurityMiddleware.ts +++ b/src/core/domains/http/middleware/SecurityMiddleware.ts @@ -1,12 +1,11 @@ import Middleware from "@src/core/domains/http/base/Middleware"; import HttpContext from "@src/core/domains/http/context/HttpContext"; - -import ForbiddenResourceError from "../../auth/exceptions/ForbiddenResourceError"; -import RateLimitedExceededError from "../../auth/exceptions/RateLimitedExceededError"; -import SecurityException from "../../express/exceptions/SecurityException"; -import { SecurityEnum } from "../enums/SecurityEnum"; -import responseError from "../handlers/responseError"; -import SecurityReader from "../security/services/SecurityReader"; +import ForbiddenResourceError from "@src/core/domains/auth/exceptions/ForbiddenResourceError"; +import RateLimitedExceededError from "@src/core/domains/auth/exceptions/RateLimitedExceededError"; +import SecurityException from "@src/core/domains/express/exceptions/SecurityException"; +import { SecurityEnum } from "@src/core/domains/http/enums/SecurityEnum"; +import responseError from "@src/core/domains/http/handlers/responseError"; +import SecurityReader from "@src/core/domains/http/security/services/SecurityReader"; class SecurityMiddleware extends Middleware { diff --git a/src/core/domains/http/resources/abstract/AbastractBaseResourceService.ts b/src/core/domains/http/resources/abstract/AbastractBaseResourceService.ts index 49067b981..5ce31f4ff 100644 --- a/src/core/domains/http/resources/abstract/AbastractBaseResourceService.ts +++ b/src/core/domains/http/resources/abstract/AbastractBaseResourceService.ts @@ -2,18 +2,16 @@ import ResourceException from "@src/core/domains/express/exceptions/ResourceException"; import HttpContext from "@src/core/domains/http/context/HttpContext"; import { requestContext } from "@src/core/domains/http/context/RequestContext"; +import { SecurityEnum } from "@src/core/domains/http/enums/SecurityEnum"; +import { IApiResponse } from "@src/core/domains/http/interfaces/IApiResponse"; import { TRouteItem } from "@src/core/domains/http/interfaces/IRouter"; +import ApiResponse from "@src/core/domains/http/response/ApiResponse"; import ResourceOwnerRule from "@src/core/domains/http/security/rules/ResourceOwnerRule"; import SecurityReader from "@src/core/domains/http/security/services/SecurityReader"; -import ValidationError from "@src/core/domains/validator/exceptions/ValidationError"; +import Paginate from "@src/core/domains/http/utils/Paginate"; import { ValidatorConstructor } from "@src/core/domains/validator/interfaces/IValidator"; import { IModel, ModelConstructor } from "@src/core/interfaces/IModel"; -import { SecurityEnum } from "../../enums/SecurityEnum"; -import { IApiResponse } from "../../interfaces/IApiResponse"; -import ApiResponse from "../../response/ApiResponse"; -import Paginate from "../../utils/Paginate"; - type TResponseOptions = { showPagination: boolean; } diff --git a/src/core/domains/http/resources/controller/ResourceController.ts b/src/core/domains/http/resources/controller/ResourceController.ts index 619a1767f..f78827f17 100644 --- a/src/core/domains/http/resources/controller/ResourceController.ts +++ b/src/core/domains/http/resources/controller/ResourceController.ts @@ -6,18 +6,9 @@ import ResourceDeleteService from "@src/core/domains/http/resources/services/Res import ResourceIndexService from "@src/core/domains/http/resources/services/ResourceIndexService"; import ResourceShowService from "@src/core/domains/http/resources/services/ResourceShowService"; import ResourceUpdateService from "@src/core/domains/http/resources/services/ResourceUpdateService"; - -import HttpContext from "../../context/HttpContext"; -import responseError from "../../handlers/responseError"; -import AbastractBaseResourceService from "../abstract/AbastractBaseResourceService"; - -type THandlerOptions = { - showPagination: boolean; -} - -const DEFAULT_HANDLER_OPTIONS: THandlerOptions = { - showPagination: true -} as const; +import HttpContext from "@src/core/domains/http/context/HttpContext"; +import responseError from "@src/core/domains/http/handlers/responseError"; +import AbastractBaseResourceService from "@src/core/domains/http/resources/abstract/AbastractBaseResourceService"; /** * ResourceController handles CRUD operations for resources (database models) diff --git a/src/core/domains/http/resources/services/ResourceCreateService.ts b/src/core/domains/http/resources/services/ResourceCreateService.ts index 5698be880..f49fa03d5 100644 --- a/src/core/domains/http/resources/services/ResourceCreateService.ts +++ b/src/core/domains/http/resources/services/ResourceCreateService.ts @@ -7,9 +7,8 @@ import { RouteResourceTypes } from "@src/core/domains/http/router/RouterResource import stripGuardedResourceProperties from "@src/core/domains/http/utils/stripGuardedResourceProperties"; import { IModelAttributes } from "@src/core/interfaces/IModel"; import { App } from "@src/core/services/App"; - -import { TResponseErrorMessages } from "../../interfaces/ErrorResponse.t"; -import ApiResponse from "../../response/ApiResponse"; +import { TResponseErrorMessages } from "@src/core/domains/http/interfaces/ErrorResponse.t"; +import ApiResponse from "@src/core/domains/http/response/ApiResponse"; /** * Service class that handles creating new resources through HTTP requests diff --git a/src/core/domains/http/resources/services/ResourceDeleteService.ts b/src/core/domains/http/resources/services/ResourceDeleteService.ts index 815382455..bb7ec24d1 100644 --- a/src/core/domains/http/resources/services/ResourceDeleteService.ts +++ b/src/core/domains/http/resources/services/ResourceDeleteService.ts @@ -5,8 +5,7 @@ import ResourceException from "@src/core/domains/express/exceptions/ResourceExce import HttpContext from "@src/core/domains/http/context/HttpContext"; import AbastractBaseResourceService from "@src/core/domains/http/resources/abstract/AbastractBaseResourceService"; import { RouteResourceTypes } from "@src/core/domains/http/router/RouterResource"; - -import ApiResponse from "../../response/ApiResponse"; +import ApiResponse from "@src/core/domains/http/response/ApiResponse"; /** * Service class that handles deleting resources through HTTP requests diff --git a/src/core/domains/http/resources/services/ResourceIndexService.ts b/src/core/domains/http/resources/services/ResourceIndexService.ts index e7cea52d6..105da492b 100644 --- a/src/core/domains/http/resources/services/ResourceIndexService.ts +++ b/src/core/domains/http/resources/services/ResourceIndexService.ts @@ -9,9 +9,8 @@ import Paginate from "@src/core/domains/http/utils/Paginate"; import QueryFilters from "@src/core/domains/http/utils/QueryFilters"; import stripGuardedResourceProperties from "@src/core/domains/http/utils/stripGuardedResourceProperties"; import { IModelAttributes } from "@src/core/interfaces/IModel"; - -import ApiResponse from "../../response/ApiResponse"; -import SortOptions from "../../utils/SortOptions"; +import ApiResponse from "@src/core/domains/http/response/ApiResponse"; +import SortOptions from "@src/core/domains/http/utils/SortOptions"; /** * Service class that handles retrieving collections of resources through HTTP requests diff --git a/src/core/domains/http/resources/services/ResourceShowService.ts b/src/core/domains/http/resources/services/ResourceShowService.ts index 156b1c0c4..4045d0f1a 100644 --- a/src/core/domains/http/resources/services/ResourceShowService.ts +++ b/src/core/domains/http/resources/services/ResourceShowService.ts @@ -6,8 +6,7 @@ import AbastractBaseResourceService from "@src/core/domains/http/resources/abstr import { RouteResourceTypes } from "@src/core/domains/http/router/RouterResource"; import stripGuardedResourceProperties from "@src/core/domains/http/utils/stripGuardedResourceProperties"; import { IModelAttributes } from "@src/core/interfaces/IModel"; - -import ApiResponse from "../../response/ApiResponse"; +import ApiResponse from "@src/core/domains/http/response/ApiResponse"; /** * Service class that handles retrieving individual resources through HTTP requests diff --git a/src/core/domains/http/resources/services/ResourceUpdateService.ts b/src/core/domains/http/resources/services/ResourceUpdateService.ts index 6d82e3212..e18358c58 100644 --- a/src/core/domains/http/resources/services/ResourceUpdateService.ts +++ b/src/core/domains/http/resources/services/ResourceUpdateService.ts @@ -7,9 +7,8 @@ import AbastractBaseResourceService from "@src/core/domains/http/resources/abstr import { RouteResourceTypes } from "@src/core/domains/http/router/RouterResource"; import stripGuardedResourceProperties from "@src/core/domains/http/utils/stripGuardedResourceProperties"; import { IModelAttributes } from "@src/core/interfaces/IModel"; - -import { TResponseErrorMessages } from "../../interfaces/ErrorResponse.t"; -import ApiResponse from "../../response/ApiResponse"; +import { TResponseErrorMessages } from "@src/core/domains/http/interfaces/ErrorResponse.t"; +import ApiResponse from "@src/core/domains/http/response/ApiResponse"; /** * Service class that handles updating existing resources through HTTP requests diff --git a/src/core/domains/http/response/ApiResponse.ts b/src/core/domains/http/response/ApiResponse.ts index 8d0e1fb17..fdf4076c3 100644 --- a/src/core/domains/http/response/ApiResponse.ts +++ b/src/core/domains/http/response/ApiResponse.ts @@ -1,5 +1,5 @@ -import { IApiResponse, TApiResponse } from "../interfaces/IApiResponse"; -import { TPagination } from "../interfaces/Pagination.t"; +import { IApiResponse, TApiResponse } from "@src/core/domains/http/interfaces/IApiResponse"; +import { TPagination } from "@src/core/domains/http/interfaces/Pagination.t"; /** * ApiResponse is a class that builds standardized HTTP API responses. @@ -145,6 +145,10 @@ class ApiResponse implements IApiResponse { return this; } + /** + * Returns the HTTP status code of the response + * @returns {number} The HTTP status code + */ getCode(): number { return this.code; } @@ -167,8 +171,18 @@ class ApiResponse implements IApiResponse { return this.additionalMeta; } + /** + * Returns the response as an object + * @returns {TApiResponse} The response as an object + */ + toObject(): TApiResponse { + return this.build(); + } + + } export default ApiResponse; + diff --git a/src/core/domains/http/router/Router.ts b/src/core/domains/http/router/Router.ts index aad68f1d0..54e540e51 100644 --- a/src/core/domains/http/router/Router.ts +++ b/src/core/domains/http/router/Router.ts @@ -1,8 +1,7 @@ import { TExpressMiddlewareFnOrClass } from "@src/core/domains/http/interfaces/IMiddleware"; import { IRouteGroupOptions, IRouter, TPartialRouteItemOptions, TRouteGroupFn, TRouteItem, TRouteResourceOptions } from "@src/core/domains/http/interfaces/IRouter"; import ResourceRouter from "@src/core/domains/http/router/RouterResource"; - -import SecurityRules from "../security/services/SecurityRules"; +import SecurityRules from "@src/core/domains/http/security/services/SecurityRules"; /** * Router handles registration and organization of Express routes @@ -190,7 +189,7 @@ class Router implements IRouter { const currentMiddlewareOrDefault = this.baseOptions?.middlewares ?? [] as TExpressMiddlewareFnOrClass[]; const currentMiddleware = (Array.isArray(currentMiddlewareOrDefault) ? currentMiddlewareOrDefault : [currentMiddlewareOrDefault]) as TExpressMiddlewareFnOrClass[]; - const optionsMiddlewareOrDefault = route.baseOptions?.middlewares ?? [] as TExpressMiddlewareFnOrClass[]; + const optionsMiddlewareOrDefault = options?.middlewares ?? [] as TExpressMiddlewareFnOrClass[]; const optionsMiddleware = (Array.isArray(optionsMiddlewareOrDefault) ? optionsMiddlewareOrDefault : [optionsMiddlewareOrDefault]) as TExpressMiddlewareFnOrClass[]; options.middlewares = [...currentMiddleware, ...optionsMiddleware] as TExpressMiddlewareFnOrClass[]; diff --git a/src/core/domains/http/router/RouterBindService.ts b/src/core/domains/http/router/RouterBindService.ts index 4c164e0a1..2c49bdb2b 100644 --- a/src/core/domains/http/router/RouterBindService.ts +++ b/src/core/domains/http/router/RouterBindService.ts @@ -5,11 +5,10 @@ import { IRouter, TRouteItem } from "@src/core/domains/http/interfaces/IRouter"; import MiddlewareUtil from '@src/core/domains/http/utils/middlewareUtil'; import { logger } from '@src/core/domains/logger/services/LoggerService'; import expressClient from 'express'; - -import Controller from '../base/Controller'; -import Middleware from '../base/Middleware'; -import { ControllerConstructor } from '../interfaces/IController'; -import IExpressConfig from '../interfaces/IHttpConfig'; +import Controller from '@src/core/domains/http/base/Controller'; +import Middleware from '@src/core/domains/http/base/Middleware'; +import { ControllerConstructor } from '@src/core/domains/http/interfaces/IController'; +import IExpressConfig from '@src/core/domains/http/interfaces/IHttpConfig'; // eslint-disable-next-line no-unused-vars type ExecuteFn = (context: HttpContext) => Promise; @@ -238,7 +237,9 @@ class RouterBindService { path: route.path, method: route.method, security: route?.security, - resource: route?.resource + resource: route?.resource, + middlewares: route?.middlewares, + config: route?.config }) } diff --git a/src/core/domains/http/routes/healthRoutes.ts b/src/core/domains/http/routes/healthRoutes.ts index e24e603ae..c8eb8ee20 100644 --- a/src/core/domains/http/routes/healthRoutes.ts +++ b/src/core/domains/http/routes/healthRoutes.ts @@ -1,6 +1,5 @@ import health from '@src/core/actions/health'; - -import Route from '../router/Route'; +import Route from '@src/core/domains/http/router/Route'; /** * Health routes diff --git a/src/core/domains/http/security/rules/HasRoleRule.ts b/src/core/domains/http/security/rules/HasRoleRule.ts index aa13378f2..d32e23764 100644 --- a/src/core/domains/http/security/rules/HasRoleRule.ts +++ b/src/core/domains/http/security/rules/HasRoleRule.ts @@ -1,8 +1,8 @@ -import SecurityException from "../../../express/exceptions/SecurityException"; -import HttpContext from "../../context/HttpContext"; -import { SecurityEnum } from "../../enums/SecurityEnum"; -import AbstractSecurityRule from "../abstract/AbstractSecurityRule"; +import SecurityException from "@src/core/domains/express/exceptions/SecurityException"; +import HttpContext from "@src/core/domains/http/context/HttpContext"; +import { SecurityEnum } from "@src/core/domains/http/enums/SecurityEnum"; +import AbstractSecurityRule from "@src/core/domains/http/security/abstract/AbstractSecurityRule"; type THasRoleRuleOptions = { roles: string | string[]; diff --git a/src/core/domains/http/security/rules/RateLimitedRule.ts b/src/core/domains/http/security/rules/RateLimitedRule.ts index b2502b64a..70cd6afa7 100644 --- a/src/core/domains/http/security/rules/RateLimitedRule.ts +++ b/src/core/domains/http/security/rules/RateLimitedRule.ts @@ -1,11 +1,10 @@ import RateLimitedExceededError from "@src/core/domains/auth/exceptions/RateLimitedExceededError"; import { Request } from "express"; - -import HttpContext from "../../context/HttpContext"; -import { requestContext } from "../../context/RequestContext"; -import { SecurityEnum } from "../../enums/SecurityEnum"; -import { IPDatesArrayTTL } from "../../interfaces/IRequestContext"; -import AbstractSecurityRule from "../abstract/AbstractSecurityRule"; +import HttpContext from "@src/core/domains/http/context/HttpContext"; +import { requestContext } from "@src/core/domains/http/context/RequestContext"; +import { SecurityEnum } from "@src/core/domains/http/enums/SecurityEnum"; +import { IPDatesArrayTTL } from "@src/core/domains/http/interfaces/IRequestContext"; +import AbstractSecurityRule from "@src/core/domains/http/security/abstract/AbstractSecurityRule"; type TRateLimitedRuleOptions = { limit: number; diff --git a/src/core/domains/http/security/rules/ResourceOwnerRule.ts b/src/core/domains/http/security/rules/ResourceOwnerRule.ts index fbef9233f..b2417455f 100644 --- a/src/core/domains/http/security/rules/ResourceOwnerRule.ts +++ b/src/core/domains/http/security/rules/ResourceOwnerRule.ts @@ -3,9 +3,8 @@ import ResourceException from "@src/core/domains/express/exceptions/ResourceExce import HttpContext from "@src/core/domains/http/context/HttpContext"; import AbstractSecurityRule from "@src/core/domains/http/security/abstract/AbstractSecurityRule"; import { IModel } from "@src/core/interfaces/IModel"; - -import { SecurityEnum } from "../../enums/SecurityEnum"; -import { RouteResourceTypes } from "../../router/RouterResource"; +import { SecurityEnum } from "@src/core/domains/http/enums/SecurityEnum"; +import { RouteResourceTypes } from "@src/core/domains/http/router/RouterResource"; type TResourceOwnerRuleOptions = { attribute: string; diff --git a/src/core/domains/http/security/rules/ResourceScopeRule.ts b/src/core/domains/http/security/rules/ResourceScopeRule.ts index af7910bd5..68d8decb3 100644 --- a/src/core/domains/http/security/rules/ResourceScopeRule.ts +++ b/src/core/domains/http/security/rules/ResourceScopeRule.ts @@ -1,7 +1,7 @@ -import SecurityException from "../../../express/exceptions/SecurityException"; -import HttpContext from "../../context/HttpContext"; -import { SecurityEnum } from "../../enums/SecurityEnum"; -import AbstractSecurityRule from "../abstract/AbstractSecurityRule"; +import SecurityException from "@src/core/domains/express/exceptions/SecurityException"; +import HttpContext from "@src/core/domains/http/context/HttpContext"; +import { SecurityEnum } from "@src/core/domains/http/enums/SecurityEnum"; +import AbstractSecurityRule from "@src/core/domains/http/security/abstract/AbstractSecurityRule"; class ResourceScopeRule extends AbstractSecurityRule { diff --git a/src/core/domains/http/security/rules/ScopeRule.ts b/src/core/domains/http/security/rules/ScopeRule.ts index 28bd0ff1c..010bfc29f 100644 --- a/src/core/domains/http/security/rules/ScopeRule.ts +++ b/src/core/domains/http/security/rules/ScopeRule.ts @@ -1,8 +1,7 @@ import SecurityException from "@src/core/domains/express/exceptions/SecurityException"; - -import HttpContext from "../../context/HttpContext"; -import { SecurityEnum } from "../../enums/SecurityEnum"; -import AbstractSecurityRule from "../abstract/AbstractSecurityRule"; +import HttpContext from "@src/core/domains/http/context/HttpContext"; +import { SecurityEnum } from "@src/core/domains/http/enums/SecurityEnum"; +import AbstractSecurityRule from "@src/core/domains/http/security/abstract/AbstractSecurityRule"; type TEnableScopeRuleOptions = { scopes: string | string[]; diff --git a/src/core/domains/http/security/services/SecurityRules.ts b/src/core/domains/http/security/services/SecurityRules.ts index 2f4561764..de467015c 100644 --- a/src/core/domains/http/security/services/SecurityRules.ts +++ b/src/core/domains/http/security/services/SecurityRules.ts @@ -2,11 +2,10 @@ import { TSecurityRuleConstructor } from "@src/core/domains/http/interfaces/ISecurity"; import AbstractSecurityRule from "@src/core/domains/http/security/abstract/AbstractSecurityRule"; import ResourceOwnerRule from "@src/core/domains/http/security/rules/ResourceOwnerRule"; - -import HasRoleRule from "../rules/HasRoleRule"; -import RateLimitedRule from "../rules/RateLimitedRule"; -import ResourceScopeRule from "../rules/ResourceScopeRule"; -import ScopeRule from "../rules/ScopeRule"; +import HasRoleRule from "@src/core/domains/http/security/rules/HasRoleRule"; +import RateLimitedRule from "@src/core/domains/http/security/rules/RateLimitedRule"; +import ResourceScopeRule from "@src/core/domains/http/security/rules/ResourceScopeRule"; +import ScopeRule from "@src/core/domains/http/security/rules/ScopeRule"; class SecurityRules { diff --git a/src/core/domains/http/services/HttpService.ts b/src/core/domains/http/services/HttpService.ts index 5eab4b2bd..d68981ed1 100644 --- a/src/core/domains/http/services/HttpService.ts +++ b/src/core/domains/http/services/HttpService.ts @@ -11,9 +11,8 @@ import RouterBindService from '@src/core/domains/http/router/RouterBindService'; import { logger } from '@src/core/domains/logger/services/LoggerService'; import { app } from '@src/core/services/App'; import expressClient from 'express'; - -import Middleware from '../base/Middleware'; -import BasicLoggerMiddleware from '../middleware/BasicLoggerMiddleware'; +import Middleware from '@src/core/domains/http/base/Middleware'; +import BasicLoggerMiddleware from '@src/core/domains/http/middleware/BasicLoggerMiddleware'; /** * Short hand for `app('http')` diff --git a/src/core/domains/http/utils/middlewareUtil.ts b/src/core/domains/http/utils/middlewareUtil.ts index b5f9b1946..3aacd453e 100644 --- a/src/core/domains/http/utils/middlewareUtil.ts +++ b/src/core/domains/http/utils/middlewareUtil.ts @@ -1,8 +1,7 @@ import { MiddlewareConstructor, TExpressMiddlewareFn, TExpressMiddlewareFnOrClass } from '@src/core/domains/http/interfaces/IMiddleware'; import { TRouteItem } from '@src/core/domains/http/interfaces/IRouter'; - -import Middleware from '../base/Middleware'; +import Middleware from '@src/core/domains/http/base/Middleware'; /** * Utility class for handling middleware conversions and transformations. diff --git a/src/core/domains/models/utils/ModelScope.ts b/src/core/domains/models/utils/ModelScope.ts new file mode 100644 index 000000000..243a9fe81 --- /dev/null +++ b/src/core/domains/models/utils/ModelScope.ts @@ -0,0 +1,61 @@ +import { ModelConstructor } from "@src/core/interfaces/IModel"; + +export type TModelScope = 'read' | 'write' | 'create' | 'delete' | 'all'; + +/** + * ModelScopes is a utility class that helps generate standardized scope strings for model-based permissions. + * + * The class provides functionality to create scopes in the format of 'modelName:scopeType' where: + * - modelName: The name of the model class (e.g., 'User', 'BlogPost') + * - scopeType: The type of permission ('read', 'write', 'create', 'delete', or 'all') + * + * This standardized format is used throughout the application for: + * - API token permissions + * - Role-based access control + * - Permission validation + * + * The scope format allows for granular control over what actions can be performed on specific models. + * For example: + * - 'User:read' allows reading user data + * - 'BlogPost:write' allows updating blog posts + * - 'Comment:delete' allows deleting comments + * + * The 'all' scope type is a special case that expands to include all basic operations + * (read, write, create, and delete) for the specified model. + */ + +class ModelScopes { + + /** + * Generates an array of scope strings for a given model and scope types. + * + * @param {ModelConstructor} model - The model constructor to generate scopes for + * @param {TModelScope[]} [scopes=['all']] - Array of scope types to generate. Defaults to ['all'] + * @param {string[]} [additionalScopes=[]] - Additional custom scopes to include + * @returns {string[]} Array of scope strings in format 'modelName:scopeType' + * + * @example + * // Generate all scopes for User model + * ModelScopes.getScopes(User) + * // Returns: ['User:read', 'User:write', 'User:create', 'User:delete'] + * + * @example + * // Generate specific scopes for BlogPost model + * ModelScopes.getScopes(BlogPost, ['read', 'write']) + * // Returns: ['BlogPost:read', 'BlogPost:write'] + * + * @example + * // Generate scopes with additional custom scopes + * ModelScopes.getScopes(Comment, ['read'], ['comment:moderate']) + * // Returns: ['Comment:read', 'comment:moderate'] + */ + public static getScopes(model: ModelConstructor, scopes: TModelScope[] = ['all'], additionalScopes: string[] = []): string[] { + if(scopes?.[0] === 'all') { + scopes = ['read', 'write', 'delete', 'create']; + } + return [...scopes.map((scope) => `${(model.name)}:${scope}`), ...additionalScopes]; + } + +} + +export default ModelScopes \ No newline at end of file diff --git a/src/core/domains/validator/base/BaseValidator.ts b/src/core/domains/validator/base/BaseValidator.ts index a2ab20ef5..e14326f5a 100644 --- a/src/core/domains/validator/base/BaseValidator.ts +++ b/src/core/domains/validator/base/BaseValidator.ts @@ -39,7 +39,7 @@ abstract class BaseValidator

impl if(result.error) { return { success: !result.error, - joi: result + joi: result, } } diff --git a/src/core/interfaces/IFactory.ts b/src/core/interfaces/IFactory.ts index 7a3060dcf..403417bce 100644 --- a/src/core/interfaces/IFactory.ts +++ b/src/core/interfaces/IFactory.ts @@ -1,5 +1,5 @@ /* eslint-disable no-unused-vars */ -import { IModel } from "./IModel"; +import { IModel } from "@src/core/interfaces/IModel"; export type FactoryConstructor = { new (...args: any[]): IFactory diff --git a/src/core/interfaces/ILarascriptProviders.ts b/src/core/interfaces/ILarascriptProviders.ts index e1705de8a..291115e4f 100644 --- a/src/core/interfaces/ILarascriptProviders.ts +++ b/src/core/interfaces/ILarascriptProviders.ts @@ -1,4 +1,3 @@ -import { IAuthService } from '@src/core/domains/auth/interfaces/IAuthService'; import ICommandService from '@src/core/domains/console/interfaces/ICommandService'; import { IDatabaseService } from '@src/core/domains/database/interfaces/IDatabaseService'; import { IEloquentQueryBuilderService } from '@src/core/domains/eloquent/interfaces/IEloquentQueryBuilderService'; @@ -8,6 +7,9 @@ import { IRequestContext } from '@src/core/domains/http/interfaces/IRequestConte import { ILoggerService } from '@src/core/domains/logger/interfaces/ILoggerService'; import IValidatorService from '@src/core/domains/validator/interfaces/IValidatorService'; import readline from 'node:readline'; +import { IACLService } from '@src/core/domains/auth/interfaces/acl/IACLService'; +import { IJwtAuthService } from '@src/core/domains/auth/interfaces/jwt/IJwtAuthService'; +import { IAuthService } from '@src/core/domains/auth/interfaces/service/IAuthService'; export interface ILarascriptProviders { @@ -26,9 +28,23 @@ export interface ILarascriptProviders { auth: IAuthService; /** + * JWT Auth service + * Provided by '@src/core/domains/auth/providers/AuthProvider' + */ + 'auth.jwt': IJwtAuthService; + + /** + * ACL service + * Provided by '@src/core/domains/auth/providers/AuthProvider' + */ + 'auth.acl': IACLService; + + /** + * Database Service * Provided by '@src/core/domains/database/providers/DatabaseProvider' */ + db: IDatabaseService; /** diff --git a/src/core/interfaces/IModel.ts b/src/core/interfaces/IModel.ts index 354c4ab85..7875f4839 100644 --- a/src/core/interfaces/IModel.ts +++ b/src/core/interfaces/IModel.ts @@ -1,9 +1,8 @@ /* eslint-disable no-unused-vars */ import { IdGeneratorFn } from "@src/core/domains/eloquent/interfaces/IEloquent"; +import { TModelScope } from "@src/core/domains/models/utils/ModelScope"; import IHasObserver from "@src/core/domains/observer/interfaces/IHasObserver"; - -import { Scope } from "../domains/auth/interfaces/IScope"; -import IFactory from "./IFactory"; +import IFactory from "@src/core/interfaces/IFactory"; export type GetAttributesOptions = {excludeGuarded: boolean} @@ -13,7 +12,7 @@ export type ModelConstructor = { getTable(): string; getPrimaryKey(): string; getConnectionName(): string; - getScopes(scopes: Scope[], additionalScopes?: string[]): string[]; + getScopes(scopes: TModelScope[], additionalScopes?: string[]): string[]; getFields(): string[]; factory(): IFactory; } @@ -33,7 +32,6 @@ export interface IModelAttributes { [key: string]: unknown; } - export interface IModel extends IHasObserver { [key: string]: unknown; connection: string; diff --git a/src/core/models/base/Model.ts b/src/core/models/base/Model.ts index 2548547a7..008e16711 100644 --- a/src/core/models/base/Model.ts +++ b/src/core/models/base/Model.ts @@ -1,12 +1,10 @@ - -import { Scope } from '@src/core/domains/auth/interfaces/IScope'; -import ModelScopes from '@src/core/domains/auth/services/ModelScopes'; import { IDatabaseSchema } from '@src/core/domains/database/interfaces/IDatabaseSchema'; import { db } from '@src/core/domains/database/services/Database'; import BaseRelationshipResolver from '@src/core/domains/eloquent/base/BaseRelationshipResolver'; import { IBelongsToOptions, IEloquent, IHasManyOptions, IRelationship, IdGeneratorFn } from '@src/core/domains/eloquent/interfaces/IEloquent'; import BelongsTo from '@src/core/domains/eloquent/relational/BelongsTo'; import HasMany from '@src/core/domains/eloquent/relational/HasMany'; +import ModelScopes, { TModelScope } from '@src/core/domains/models/utils/ModelScope'; import { ObserveConstructor } from '@src/core/domains/observer/interfaces/IHasObserver'; import { IObserver, IObserverEvent } from '@src/core/domains/observer/interfaces/IObserver'; import { ICtor } from '@src/core/interfaces/ICtor'; @@ -116,7 +114,6 @@ export default abstract class Model impleme * The factory instance for the model. */ protected factory!: FactoryConstructor>; - /** * Constructs a new instance of the Model class. @@ -236,7 +233,7 @@ export default abstract class Model impleme * Retrieves the scopes associated with the model. * @returns {string[]} The scopes associated with the model. */ - static getScopes(scopes: Scope[] = ['all'], additionalScopes: string[] = []): string[] { + static getScopes(scopes: TModelScope[] = ['all'], additionalScopes: string[] = []): string[] { return ModelScopes.getScopes(this as unknown as ModelConstructor, scopes, additionalScopes) } @@ -639,6 +636,16 @@ export default abstract class Model impleme * @deprecated use `toObject` instead */ async getData(options: GetAttributesOptions = { excludeGuarded: true }): Promise { + return this.toObject(options); + } + + + /** + * Retrieves the entire model's data as an object. + * + * @returns {Promise} The model's data as an object, or null if no data is set. + */ + async toObject(options: GetAttributesOptions = { excludeGuarded: true }): Promise { let data = this.getAttributes(); if (data && options.excludeGuarded) { @@ -650,15 +657,6 @@ export default abstract class Model impleme return data as Attributes; } - - /** - * Retrieves the entire model's data as an object. - * - * @returns {Promise} The model's data as an object, or null if no data is set. - */ - async toObject(): Promise { - return this.getData({ excludeGuarded: false }); - } /** * Refreshes the model's data from the database. diff --git a/src/core/providers/LarascriptProviders.ts b/src/core/providers/LarascriptProviders.ts index dda6998a1..ae7a3216e 100644 --- a/src/core/providers/LarascriptProviders.ts +++ b/src/core/providers/LarascriptProviders.ts @@ -10,6 +10,8 @@ import MigrationProvider from "@src/core/domains/migrations/providers/MigrationP import SetupProvider from "@src/core/domains/setup/providers/SetupProvider"; import ValidatorProvider from "@src/core/domains/validator/providers/ValidatorProvider"; import { IProvider } from "@src/core/interfaces/IProvider"; +// eslint-disable-next-line no-unused-vars +import { ILarascriptProviders } from "@src/core/interfaces/ILarascriptProviders"; /** * Core providers for the framework @@ -17,6 +19,7 @@ import { IProvider } from "@src/core/interfaces/IProvider"; * These providers are loaded by default when the application boots * * @see {@link IProvider} for more information about providers + * @see {@link ILarascriptProviders} for providing type hints for providers */ const LarascriptProviders: IProvider[] = [ diff --git a/src/tests/auth/authLoginUser.test.ts b/src/tests/auth/authLoginUser.test.ts index 9f25ba415..2e1e8d1ef 100644 --- a/src/tests/auth/authLoginUser.test.ts +++ b/src/tests/auth/authLoginUser.test.ts @@ -1,15 +1,15 @@ /* eslint-disable no-undef */ import { describe } from '@jest/globals'; -import authConfig from '@src/config/auth'; -import IApiTokenModel from '@src/core/domains/auth/interfaces/IApitokenModel'; -import IUserModel from '@src/core/domains/auth/interfaces/IUserModel'; +import { IApiTokenModel } from '@src/core/domains/auth/interfaces/models/IApiTokenModel'; +import { IUserModel } from '@src/core/domains/auth/interfaces/models/IUserModel'; import { auth } from '@src/core/domains/auth/services/AuthService'; import hashPassword from '@src/core/domains/auth/utils/hashPassword'; import { logger } from '@src/core/domains/logger/services/LoggerService'; -import TestUserFactory from '@src/tests/factory/TestUserFactory'; import TestApiTokenModel from '@src/tests/models/models/TestApiTokenModel'; import TestUser from '@src/tests/models/models/TestUser'; import testHelper from '@src/tests/testHelper'; +import TestCreateUserValidator from '@src/tests/validator/TestCreateUserValidator'; +import TestUpdateUserValidator from '@src/tests/validator/TestUpdateUserValidator'; describe('attempt to run app with normal appConfig', () => { @@ -35,7 +35,7 @@ describe('attempt to run app with normal appConfig', () => { /** * Create a test user */ - testUser = new TestUserFactory().create({ + testUser = TestUser.create({ email, hashedPassword, roles: [], @@ -57,7 +57,7 @@ describe('attempt to run app with normal appConfig', () => { }) test('test create user validator (email already exists, validator should fail)', async () => { - const validator = new authConfig.validators.createUser() + const validator = new TestCreateUserValidator() const result = await validator.validate({ email, password, @@ -69,12 +69,13 @@ describe('attempt to run app with normal appConfig', () => { }) test('test create user validator', async () => { - const validator = new authConfig.validators.createUser() + const validator = new TestCreateUserValidator() const result = await validator.validate({ email: 'testUser2@test.com', password, firstName: 'Tony', lastName: 'Stark' + }); if(!result.success) { @@ -86,9 +87,10 @@ describe('attempt to run app with normal appConfig', () => { test('test update user validator', async () => { - const validator = new authConfig.validators.updateUser() + const validator = new TestUpdateUserValidator() const result = await validator.validate({ password, + firstName: 'Tony', lastName: 'Stark' }); @@ -101,25 +103,20 @@ describe('attempt to run app with normal appConfig', () => { }) test('attempt credentials', async () => { - jwtToken = await auth().attemptCredentials(email, password); + jwtToken = await auth().getJwtAdapter().attemptCredentials(email, password); logger().info('[jwtToken]', jwtToken); expect(jwtToken).toBeTruthy(); }) - test('create api token from user', async () => { - apiToken = await auth().createApiTokenFromUser(testUser); - logger().info('[apiToken]', apiToken); - expect(apiToken).toBeInstanceOf(TestApiTokenModel); - }) test('create jwt from user', async () => { - const jwt = await auth().createJwtFromUser(testUser); + const jwt = await auth().getJwtAdapter().createJwtFromUser(testUser); logger().info('[jwt]', jwt); expect(jwt).toBeTruthy(); }) test('verify token', async () => { - apiToken = await auth().attemptAuthenticateToken(jwtToken); + apiToken = await auth().getJwtAdapter().attemptAuthenticateToken(jwtToken); expect(apiToken).toBeInstanceOf(TestApiTokenModel); const user = await apiToken?.getAttribute('user'); @@ -129,7 +126,7 @@ describe('attempt to run app with normal appConfig', () => { test('revoke token', async () => { expect(apiToken).toBeInstanceOf(TestApiTokenModel); - apiToken && await auth().revokeToken(apiToken); + apiToken && await auth().getJwtAdapter().revokeToken(apiToken); await apiToken?.refresh(); expect(apiToken?.revokedAt).toBeTruthy(); diff --git a/src/tests/factory/TestApiTokenFactory.ts b/src/tests/factory/TestApiTokenFactory.ts deleted file mode 100644 index becb68dd7..000000000 --- a/src/tests/factory/TestApiTokenFactory.ts +++ /dev/null @@ -1,17 +0,0 @@ -import ApiTokenFactory from '@src/core/domains/auth/factory/apiTokenFactory'; -import { IModel, IModelAttributes, ModelConstructor } from '@src/core/interfaces/IModel'; - -import TestApiTokenModel from '../models/models/TestApiTokenModel'; - -/** - * Factory for creating ApiToken models. - */ -class TestApiTokenFactory extends ApiTokenFactory { - - protected model: ModelConstructor> = TestApiTokenModel; - -} - - - -export default TestApiTokenFactory diff --git a/src/tests/factory/factory.test.ts b/src/tests/factory/factory.test.ts index 97deabd13..541363468 100644 --- a/src/tests/factory/factory.test.ts +++ b/src/tests/factory/factory.test.ts @@ -1,8 +1,7 @@ /* eslint-disable no-undef */ import { describe } from '@jest/globals'; import testHelper from '@src/tests/testHelper'; - -import { TestMovieModel } from '../models/models/TestMovie'; +import { TestMovieModel } from '@src/tests/models/models/TestMovie'; describe('create a movie model using factories', () => { diff --git a/src/tests/migration/migrations/test-create-api-token-table.ts b/src/tests/migration/migrations/test-create-api-token-table.ts index a362859f1..681b3248d 100644 --- a/src/tests/migration/migrations/test-create-api-token-table.ts +++ b/src/tests/migration/migrations/test-create-api-token-table.ts @@ -1,4 +1,4 @@ -import ApiToken from "@src/app/models/auth/ApiToken"; +import ApiToken from "@src/core/domains/auth/models/ApiToken"; import BaseMigration from "@src/core/domains/migrations/base/BaseMigration"; import { DataTypes } from "sequelize"; diff --git a/src/tests/models/models/TestApiTokenModel.ts b/src/tests/models/models/TestApiTokenModel.ts index 3a87dbdfc..b7506c017 100644 --- a/src/tests/models/models/TestApiTokenModel.ts +++ b/src/tests/models/models/TestApiTokenModel.ts @@ -1,12 +1,10 @@ -import ApiToken from '@src/app/models/auth/ApiToken'; -import { ApiTokenAttributes } from '@src/core/domains/auth/interfaces/IApitokenModel'; +import ApiToken, { ApiTokenAttributes } from '@src/core/domains/auth/models/ApiToken'; import TestUser from '@src/tests/models/models/TestUser'; class TestApiTokenModel extends ApiToken { table: string = 'api_tokens'; - constructor(data: ApiTokenAttributes | null = null) { super(data); this.setUserModelCtor(TestUser) diff --git a/src/tests/observers/TestUserObserver.ts b/src/tests/observers/TestUserObserver.ts index c42cf207b..ccbf222d1 100644 --- a/src/tests/observers/TestUserObserver.ts +++ b/src/tests/observers/TestUserObserver.ts @@ -1,4 +1,4 @@ -import UserObserver from "@src/app/observers/UserObserver"; +import UserObserver from "@src/core/domains/auth/observers/UserObserver"; import { TestUserCreatedListener } from "@src/tests/events/events/auth/TestUserCreatedListener"; /** diff --git a/src/tests/providers/TestAuthProvider.ts b/src/tests/providers/TestAuthProvider.ts index eb1d01088..1dc007fde 100644 --- a/src/tests/providers/TestAuthProvider.ts +++ b/src/tests/providers/TestAuthProvider.ts @@ -1,39 +1,36 @@ -import authConfig from "@src/config/auth"; -import { IAuthConfig } from "@src/core/domains/auth/interfaces/IAuthConfig"; import AuthProvider from "@src/core/domains/auth/providers/AuthProvider"; -import AuthService from "@src/core/domains/auth/services/AuthService"; -import TestApiTokenFactory from "@src/tests/factory/TestApiTokenFactory"; -import TestUserFactory from "@src/tests/factory/TestUserFactory"; +import AuthConfig from "@src/core/domains/auth/services/AuthConfig"; +import JwtAuthService from "@src/core/domains/auth/services/JwtAuthService"; +import parseBooleanFromString from "@src/core/util/parseBooleanFromString"; import TestApiTokenModel from "@src/tests/models/models/TestApiTokenModel"; import TestUser from "@src/tests/models/models/TestUser"; -import TestApiTokenRepository from "@src/tests/repositories/TestApiTokenRepository"; -import TestUserRepository from "@src/tests/repositories/TestUserRepository"; import TestCreateUserValidator from "@src/tests/validator/TestCreateUserValidator"; import TestUpdateUserValidator from "@src/tests/validator/TestUpdateUserValidator"; export default class TestAuthProvider extends AuthProvider { - protected config: IAuthConfig = { - ...authConfig, - service: { - authService: AuthService - }, - models: { - user: TestUser, - apiToken: TestApiTokenModel - }, - repositories: { - user: TestUserRepository, - apiToken: TestApiTokenRepository - }, - factory: { - userFactory: TestUserFactory, - apiTokenFactory: TestApiTokenFactory - }, - validators: { - createUser: TestCreateUserValidator, - updateUser: TestUpdateUserValidator, - }, - } + protected config = AuthConfig.define([ + AuthConfig.config(JwtAuthService, { + name: 'jwt', + models: { + user: TestUser, + apiToken: TestApiTokenModel + }, + validators: { + createUser: TestCreateUserValidator, + updateUser: TestUpdateUserValidator + }, + + + routes: { + enableAuthRoutes: parseBooleanFromString(process.env.ENABLE_AUTH_ROUTES, 'true'), + enableAuthRoutesAllowCreate: parseBooleanFromString(process.env.ENABLE_AUTH_ROUTES_ALLOW_CREATE, 'true'), + }, + settings: { + secret: process.env.JWT_SECRET as string ?? '', + expiresInMinutes: process.env.JWT_EXPIRES_IN_MINUTES ? parseInt(process.env.JWT_EXPIRES_IN_MINUTES) : 60, + } + }) + ]) } diff --git a/src/tests/runApp.test.ts b/src/tests/runApp.test.ts index 057e0a76b..15ea7403c 100644 --- a/src/tests/runApp.test.ts +++ b/src/tests/runApp.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from '@jest/globals'; import AuthService from '@src/core/domains/auth/services/AuthService'; +import JwtAuthService from '@src/core/domains/auth/services/JwtAuthService'; import ConsoleService from '@src/core/domains/console/service/ConsoleService'; import Database from '@src/core/domains/database/services/Database'; import EventService from '@src/core/domains/events/services/EventService'; @@ -19,8 +20,10 @@ describe('attempt to run app with normal appConfig', () => { expect(App.container('db')).toBeInstanceOf(Database); expect(App.container('console')).toBeInstanceOf(ConsoleService); expect(App.container('auth')).toBeInstanceOf(AuthService); + expect(App.container('auth.jwt')).toBeInstanceOf(JwtAuthService); expect(Kernel.getInstance().booted()).toBe(true); + }, 10000) }); \ No newline at end of file diff --git a/src/tests/testHelper.ts b/src/tests/testHelper.ts index e47eb2db1..f757ee097 100644 --- a/src/tests/testHelper.ts +++ b/src/tests/testHelper.ts @@ -63,12 +63,13 @@ export const createAuthTables = async(connectionName?: string) => { await schema.createTable(userTable, { email: DataTypes.STRING, hashedPassword: DataTypes.STRING, - groups: DataTypes.JSON, - roles: DataTypes.JSON, + groups: DataTypes.ARRAY(DataTypes.STRING), + roles: DataTypes.ARRAY(DataTypes.STRING), firstName: stringNullable, lastName: stringNullable, createdAt: DataTypes.DATE, updatedAt: DataTypes.DATE + }) await schema.createTable(apiTokenTable, { diff --git a/src/tests/validator/TestCreateUserValidator.ts b/src/tests/validator/TestCreateUserValidator.ts index a384f28d1..4c1074c33 100644 --- a/src/tests/validator/TestCreateUserValidator.ts +++ b/src/tests/validator/TestCreateUserValidator.ts @@ -1,7 +1,8 @@ +import { queryBuilder } from "@src/core/domains/eloquent/services/EloquentQueryBuilderService"; import BaseValidator from "@src/core/domains/validator/base/BaseValidator"; import { ValidatorPayload } from "@src/core/domains/validator/interfaces/IValidator"; -import { App } from "@src/core/services/App"; import Joi, { ObjectSchema } from "joi"; +import TestUser from "@src/tests/models/models/TestUser"; class TestCreateUserValidator extends BaseValidator { @@ -14,9 +15,8 @@ class TestCreateUserValidator extends BaseValidator { * @param payload */ async validateEmailAvailability(payload: ValidatorPayload) { - const repository = App.container('auth').userRepository; - const user = await repository.findOneByEmail(payload.email as string); + const user = await queryBuilder(TestUser).where('email', payload.email as string).first(); if(user) { this.setErrorMessage({ email: 'User already exists' }); diff --git a/src/tinker.ts b/src/tinker.ts index 3533078e9..ec34910d0 100644 --- a/src/tinker.ts +++ b/src/tinker.ts @@ -5,9 +5,9 @@ import 'tsconfig-paths/register'; import appConfig from '@src/config/app'; import Kernel from '@src/core/Kernel'; -import LarascriptProviders from '@src/core/providers/LarascriptProviders'; import { app } from '@src/core/services/App'; import testHelper from '@src/tests/testHelper'; +import providers from '@src/config/providers'; const USE_TEST_DB = false; @@ -20,7 +20,7 @@ const USE_TEST_DB = false; else { await Kernel.boot({ environment: appConfig.env, - providers: LarascriptProviders + providers: providers }, {}); }