UNPKG

@avonjs/avonjs

Version:

A fluent Node.js API generator.

603 lines (602 loc) 22.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const express_1 = __importDefault(require("express")); const express_jwt_1 = require("express-jwt"); const joi_1 = __importDefault(require("joi")); const jsonwebtoken_1 = require("jsonwebtoken"); const FieldCollection_1 = __importDefault(require("./Collections/FieldCollection")); const ResourceCollection_1 = require("./Collections/ResourceCollection"); const Exceptions_1 = require("./Exceptions"); const ValidationException_1 = __importDefault(require("./Exceptions/ValidationException")); const Fields_1 = require("./Fields"); const LoginRequest_1 = __importDefault(require("./Http/Requests/Auth/LoginRequest")); const LoginResponse_1 = __importDefault(require("./Http/Responses/Auth/LoginResponse")); const Models_1 = require("./Models"); const Resource_1 = __importDefault(require("./Resource")); const RouteRegistrar_1 = __importDefault(require("./Route/RouteRegistrar")); const helpers_1 = require("./helpers"); const debug_1 = __importDefault(require("./support/debug")); // TODO: may i have to export new class instance instead of static method // biome-ignore lint/complexity/noStaticOnlyClass: class Avon { /** * Indicates application current version. */ static VERSION = '3.5.1'; /** * Array of available resources. */ static resourceInstances = []; /** * Map of available resources. */ static resourceMap = {}; /** * The error handler callback. */ static errorHandler = (error) => console.error(error); /** * The user resolver callback. */ static userResolver = () => null; /** * Extended swagger paths. */ static paths = {}; /** * List of routes without authorization. */ static excepts = [ /.*\/schema/, /.*\/login/, ]; /** * The login attempt callback. */ static attemptCallback = async () => undefined; /** * Set application secret key. */ static appKey = 'Avon'; /** * Indicates JWT params. */ static jwtSignOptions = { algorithm: 'HS256', }; /** * Indicates JWT verify params. */ static jwtVerifyOptions; /** * The login attempt callback. */ static authFields = [ new Fields_1.Email().default(() => 'zarehesmaiel@gmail.com'), new Fields_1.Text('password').default(() => 'zarehesmaiel@gmail.com'), ]; /** * Extended swagger info. */ static info = { version: Avon.version(), title: 'My Application API', description: 'Another Avonjs Application', contact: { name: 'Ismail Zare', email: 'zarehesmaiel@gmail.com', }, }; /** * Get the Avon version. */ static version() { return Avon.VERSION; } /** * Register array of new resources. */ static resources(resources = []) { Avon.resourceInstances = [...Avon.resourceInstances, ...resources]; debug_1.default.dump('Resources registered: %O', Avon.resourceCollection().keys().implode(', ')); return Avon; } /** * Find resource for given uriKey. */ static resourceForKey(key) { if (Avon.resourceMap[key] === undefined) { Avon.resourceMap[key] = Avon.resourceCollection().first((resource) => resource.uriKey() === key); } return Avon.resourceMap[key]; } /** * Get collection of available resources. */ static resourceCollection() { return new ResourceCollection_1.ResourceCollection(Avon.resourceInstances); } /** * Get express instance. */ static routes(withAuthentication = false) { return Avon.express(withAuthentication); } /** * Get express instance. */ static express(withAuthentication = false, middlewares = []) { const app = (0, express_1.default)(); app.set('query parser', 'extended'); // Apply provided middlewares middlewares.forEach((middleware) => app.use(middleware)); // Define an auth-specific router const authRouter = express_1.default.Router(); if (withAuthentication) { // Login route should be available before authentication middleware authRouter.post('/login', Avon.login); // Apply authentication middleware only after login app.use(Avon.expressjwt()); app.use(helpers_1.handleAuthenticationError); } // Register other application routes new RouteRegistrar_1.default(authRouter).register(); // Register the authRouter under '/api' app.use(authRouter); return app; } /** * Get JWT middleware. */ static expressjwt() { const verifyOptions = Object.assign({ secret: Avon.appKey, algorithms: ['HS256'] }, Avon.jwtVerifyOptions); return (0, express_jwt_1.expressjwt)(verifyOptions).unless({ path: Avon.excepts }); } /** * Handle the given error. */ static handleError(error) { Avon.errorHandler(error); return Avon; } /** * Handle the given error. */ static handleErrorUsing(errorHandler) { Avon.errorHandler = errorHandler; return Avon; } /** * Get the user id. */ static userId(request) { return request.getRequest().auth?.id; } /** * Resolve the user for incoming request to share in the app. */ static resolveUser(request) { return Avon.userResolver(request); } /** * Set the user resolver callback. */ static resolveUserUsing(userResolver) { Avon.userResolver = userResolver; return Avon; } /** * Register resource from given path. */ static resourceIn(path) { debug_1.default.dump(`Searching resources in directory: ${path}`); const files = (0, node_fs_1.readdirSync)(path); const resources = []; // check paths for (const file of files) { const filePath = (0, node_path_1.join)(path, file); const stat = (0, node_fs_1.statSync)(filePath); // check sub directories if (stat.isDirectory()) { Avon.resourceIn(filePath); continue; } // check file type if (!['.js', '.ts'].includes((0, node_path_1.extname)(file))) { continue; } const resourceClass = require(filePath).default || require(filePath); // validate resource if (resourceClass.prototype instanceof Resource_1.default) { resources.push(new resourceClass()); } } // register resources if (resources.length) { debug_1.default.dump(`Registering resources from directory: ${path}`); Avon.resources(resources); } else { debug_1.default.dump(`No resources found in directory: ${path}`); } return Avon; } /** * Set login fields. */ static credentials(authFields) { Avon.authFields = authFields; return Avon; } /** * Get login fields. */ static fieldsForLogin() { return new FieldCollection_1.default(Avon.authFields); } /** * Set JWT secret. */ static key(appKey) { Avon.appKey = appKey; return Avon; } /** * Extend swagger paths. */ static extend(paths) { Avon.paths = paths; return Avon; } /** * Extend swagger paths. */ static describe(info) { Avon.info = { ...Avon.info, ...info }; return Avon; } /** * Set the JWT sign options. */ static signOptions(signOptions) { Avon.jwtSignOptions = { ...Avon.jwtSignOptions, ...signOptions }; return Avon; } /** * Set the JWT verify options. */ static verifyOptions(verifyOptions) { Avon.jwtVerifyOptions = { ...Avon.jwtVerifyOptions, ...verifyOptions }; return Avon; } /** * Set the JWT options. */ static except(path) { Avon.excepts.push(path); return Avon; } /** * Set attempt callback. */ static attemptUsing(attemptCallback) { Avon.attemptCallback = attemptCallback; return Avon; } /** * Handle login request. */ static async login(req, res) { const request = new LoginRequest_1.default(req); const payload = new Models_1.Fluent(); // validate credentials await Avon.performValidation(request) .then(() => { // resolve credentials Avon.fieldsForLogin().each((field) => field.fill(request, payload)); // attempt login Avon.attempt(payload.getAttributes()) .then((response) => (0, helpers_1.send)(res, response)) .catch((error) => { if (error instanceof Exceptions_1.ResponsableException) { (0, helpers_1.send)(res, error.toResponse()); } else { Avon.handleError(error); res .status(500) .send({ message: error.message, name: 'InternalServerError' }); } }); }) .catch((error) => { (0, helpers_1.send)(res, new ValidationException_1.default(error).toResponse()); }); } /** * Perform login request validation. */ static async performValidation(request) { await joi_1.default.object(Avon.fieldsForLogin() .map((field) => field.getRules(request)) .flatMap((rules) => Object.keys(rules).map((key) => [key, rules[key]])) .mapWithKeys((rules) => rules) .all()).validateAsync(request.all(), { abortEarly: false }); } /** * Set attempt callback. */ static async attempt(payload) { try { const user = await Avon.attemptCallback(payload); Exceptions_1.NotFoundException.unless(user); return new LoginResponse_1.default({ token: Avon.sign(user) }); } catch (err) { debug_1.default.error(err); throw new Exceptions_1.AuthenticationException(); } } /** * Make JWT token. */ static sign(payload) { return (0, jsonwebtoken_1.sign)(payload, Avon.appKey, Avon.jwtSignOptions); } /** * Get the schema for open API. */ static schema(request) { return { openapi: '3.0.0', security: [{ BearerAuth: [] }], paths: Avon.resourceCollection().reduce((paths, resource) => Object.assign({}, paths, resource.schema(request)), { ...Avon.paths, ...Avon.loginSchema(request) }), info: Avon.info, components: { securitySchemes: { BearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', }, }, responses: { Forbidden: { description: 'This action is unauthorized.', content: { 'application/json': { schema: { type: 'object', properties: { code: { type: 'number', default: 403 }, message: { type: 'string', default: 'This action is unauthorized.', }, name: { type: 'string', default: 'Forbidden' }, meta: { type: 'object', properties: { stack: { type: 'object' }, }, }, }, }, }, }, }, Unauthenticated: { description: 'The user is unauthenticated.', content: { 'application/json': { schema: { type: 'object', properties: { code: { type: 'number', default: 401 }, message: { type: 'string', default: 'The user is unauthenticated.', }, name: { type: 'string', default: 'Unauthenticated' }, meta: { type: 'object', properties: { stack: { type: 'object' }, }, }, }, }, }, }, }, NotFound: { description: 'Requested resource not found.', content: { 'application/json': { schema: { type: 'object', properties: { code: { type: 'number', default: 404 }, message: { type: 'string', default: 'Requested resource not found.', }, name: { type: 'string', default: 'NotFound' }, meta: { type: 'object', properties: { stack: { type: 'object' }, }, }, }, }, }, }, }, InternalServerError: { description: 'Internal server error.', content: { 'application/json': { schema: { type: 'object', properties: { code: { type: 'number', default: 500 }, message: { type: 'string', default: 'Something went wrong.', }, name: { type: 'string', default: 'InternalServerError' }, meta: { type: 'object', properties: { stack: { type: 'object' }, }, }, }, }, }, }, }, UnprocessableContent: { description: 'Validation failed.', content: { 'application/json': { schema: { type: 'object', properties: { code: { type: 'number', default: 422 }, message: { type: 'string', default: 'The given data was invalid.', }, name: { type: 'string', default: 'UnprocessableContent' }, meta: { type: 'object', properties: { errors: { type: 'object' }, }, }, }, }, }, }, }, EmptyResponse: { description: 'Nothing to show', content: { 'application/json': {}, }, }, BadRequest: { description: 'Bad request error.', content: { 'application/json': { schema: { type: 'object', properties: { code: { type: 'number', default: 400 }, message: { type: 'string', default: 'Request payload are invalid.', }, name: { type: 'string', default: 'BadRequest' }, meta: { type: 'object', properties: { stack: { type: 'object' }, }, }, }, }, }, }, }, MethodNotAllowed: { description: 'Method not allowed error.', content: { 'application/json': { schema: { type: 'object', properties: { code: { type: 'number', default: 405 }, message: { type: 'string', default: 'The requested method is not supported for this endpoint.', }, name: { type: 'string', default: 'MethodNotAllowed' }, meta: { type: 'object', properties: { stack: { type: 'object' }, }, }, }, }, }, }, }, }, schemas: { PrimaryKey: { anyOf: [{ type: 'number' }, { type: 'string' }], }, }, }, }; } /** * Get the login swagger schema. */ static loginSchema(request) { const fields = Avon.fieldsForLogin(); return { [`${request.getRequest().baseUrl}/login`]: { post: { tags: ['auth'], description: 'Login to get JWT token', operationId: 'attempt', requestBody: { content: { 'application/json': { schema: { type: 'object', required: fields.map((field) => field.attribute).all(), properties: fields.payloadSchemas(request), }, }, }, }, responses: { ...(0, helpers_1.errorsResponses)(), ...(0, helpers_1.validationResponses)(), 200: { description: 'Get JWT token', content: { 'application/json': { schema: { type: 'object', properties: { data: { type: 'object', properties: { token: { type: 'string' }, meta: { type: 'object' }, }, }, }, }, }, }, }, }, }, }, }; } } exports.default = Avon;