├── .adonisrc.json ├── .editorconfig ├── .env.example ├── .env.test ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── inspectionProfiles │ └── Project_Default.xml ├── mockos-back.iml ├── modules.xml ├── prettier.xml └── vcs.xml ├── .prettierignore ├── Dockerfile ├── README.md ├── ace ├── ace-manifest.json ├── app ├── Controllers │ └── Http │ │ ├── ApiController.ts │ │ ├── HeadersController.ts │ │ ├── InvitationsController.ts │ │ ├── ProjectsController.ts │ │ ├── ResponsesController.ts │ │ ├── RoutesController.ts │ │ ├── SwaggerController.ts │ │ ├── TokensController.ts │ │ └── UserController.ts ├── Exceptions │ └── Handler.ts ├── Helpers │ ├── Shared │ │ ├── array.helper.ts │ │ ├── file.helper.ts │ │ ├── sort.helper.ts │ │ └── string.helper.ts │ └── Swagger │ │ ├── common │ │ └── common │ │ │ ├── constants │ │ │ └── environment-schema.constants.ts │ │ │ ├── index.ts │ │ │ ├── libs │ │ │ └── schema-builder.ts │ │ │ ├── models │ │ │ ├── environment.model.ts │ │ │ ├── folder.model.ts │ │ │ └── route.model.ts │ │ │ └── utils │ │ │ ├── mappers.ts │ │ │ └── utils.ts │ │ └── swagger.helper.ts ├── Interfaces │ ├── HeaderInterface.ts │ ├── ResponseInterface.ts │ └── RouteInterface.ts ├── Middleware │ ├── Auth.ts │ ├── DetectUserLocale.ts │ ├── DisabledVerification.ts │ └── SilentAuth.ts ├── Models │ ├── ApiToken.ts │ ├── Header.ts │ ├── HttpError.ts │ ├── Member.ts │ ├── Processor.ts │ ├── Project.ts │ ├── Response.ts │ ├── Route.ts │ ├── Token.ts │ └── User.ts ├── Policies │ ├── GlobalPolicy.ts │ ├── InvitationPolicy.ts │ ├── ProjectPolicy.ts │ └── RoutePolicy.ts ├── Services │ └── Ws.ts └── Validators │ ├── Header │ ├── CreateHeaderValidator.ts │ └── UpdateHeaderValidator.ts │ ├── Processor │ └── CreateProcessorValidator.ts │ ├── Project │ ├── CreateProjectValidator.ts │ ├── EditProjectValidator.ts │ └── GetProjectsValidator.ts │ ├── Response │ ├── CreateResponseValidator.ts │ ├── DeleteMultipleResponseValidator.ts │ ├── DuplicateResponseValidator.ts │ └── EditResponseValidator.ts │ ├── Route │ ├── CreateRouteValidator.ts │ ├── EditFolderValidator.ts │ ├── EditRouteValidator.ts │ └── MoveAndSortValidator.ts │ ├── Swagger │ └── ImportSwaggerValidator.ts │ ├── Token │ └── CreateTokenValidator.ts │ └── User │ ├── EditUserEmailValidator.ts │ ├── EditUserValidator.ts │ ├── LoginValidator.ts │ ├── RegisterValidator.ts │ └── ResendEmailValidator.ts ├── commands └── index.ts ├── config ├── app.ts ├── auth.ts ├── bodyparser.ts ├── cors.ts ├── database.ts ├── drive.ts ├── hash.ts ├── i18n.ts └── mail.ts ├── contracts ├── auth.ts ├── bouncer.ts ├── drive.ts ├── env.ts ├── events.ts ├── hash.ts ├── mail.ts └── tests.ts ├── database ├── factories │ └── index.ts └── migrations │ ├── 1669234334619_users.ts │ ├── 1669234334622_api_tokens.ts │ ├── 1669464855581_projects.ts │ ├── 1669584873001_members.ts │ ├── 1670583028612_routes.ts │ ├── 1670591159570_responses.ts │ ├── 1689104187739_headers.ts │ ├── 1689635396291_unique_headers.ts │ ├── 1690714833453_user_verify_locks.ts │ ├── 1691319933218_tokens.ts │ ├── 1693469175128_folders.ts │ ├── 1695735082689_contracts.ts │ ├── 1740136305100_processors.ts │ └── 1741125580372_remove_contracts.ts ├── docker-compose.yml ├── env.ts ├── package-lock.json ├── package.json ├── providers └── AppProvider.ts ├── resources ├── images │ └── graphics │ │ └── mockos-banner.png └── lang │ ├── en │ ├── bouncer.json │ ├── middleware.json │ ├── responses.json │ └── validator.json │ └── es │ ├── bouncer.json │ ├── middleware.json │ ├── responses.json │ └── validator.json ├── server.ts ├── start ├── bouncer.ts ├── kernel.ts ├── routes.ts ├── routes │ ├── api.ts │ ├── headers.ts │ ├── invitations.ts │ ├── mock.ts │ ├── projects.ts │ ├── responses.ts │ ├── routes.ts │ ├── tokens.ts │ └── user.ts └── socket.ts ├── test.ts ├── tests ├── bootstrap.ts └── functional │ └── hello_world.spec.ts └── tsconfig.json /.adonisrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript": true, 3 | "commands": [ 4 | "./commands", 5 | "@adonisjs/core/build/commands/index.js", 6 | "@adonisjs/repl/build/commands", 7 | "@adonisjs/lucid/build/commands", 8 | "@adonisjs/mail/build/commands", 9 | "@adonisjs/bouncer/build/commands" 10 | ], 11 | "exceptionHandlerNamespace": "App/Exceptions/Handler", 12 | "aliases": { 13 | "App": "app", 14 | "Config": "config", 15 | "Database": "database", 16 | "Contracts": "contracts" 17 | }, 18 | "preloads": ["./start/routes", "./start/kernel", "./start/bouncer"], 19 | "providers": [ 20 | "./providers/AppProvider", 21 | "@adonisjs/core", 22 | "@adonisjs/lucid", 23 | "@adonisjs/auth", 24 | "@adonisjs/mail", 25 | "@adonisjs/bouncer", 26 | "@adonisjs/i18n", 27 | "@adonisjs/drive-s3" 28 | ], 29 | "aceProviders": ["@adonisjs/repl"], 30 | "tests": { 31 | "suites": [ 32 | { 33 | "name": "functional", 34 | "files": ["tests/functional/**/*.spec(.ts|.js)"], 35 | "timeout": 60000 36 | } 37 | ] 38 | }, 39 | "testProviders": ["@japa/preset-adonis/TestsProvider"], 40 | "metaFiles": ["resources/lang/**/*.(json|yaml)"] 41 | } 42 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = false 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3333 2 | HOST=0.0.0.0 3 | BACK_URL=http://localhost:3333 4 | FRONT_URL=https://mockos.io 5 | NODE_ENV=development 6 | APP_KEY=nQn9FKVrvg0wjJGwaJTx_TCMfBkVoBw4 7 | 8 | DRIVE_DISK=local 9 | 10 | DB_CONNECTION=pg 11 | PG_HOST=localhost 12 | PG_PORT=5432 13 | PG_USER=mockos 14 | PG_PASSWORD= 15 | PG_DB_NAME=mockos 16 | 17 | DISABLE_VERIFICATION=true 18 | 19 | SMTP_EMAIL= 20 | SMTP_HOST=localhost 21 | SMTP_PORT=587 22 | SMTP_USERNAME= 23 | SMTP_PASSWORD= 24 | 25 | S3_KEY= 26 | S3_SECRET= 27 | S3_BUCKET= 28 | S3_REGION=us-east-1 29 | S3_ENDPOINT= 30 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | NODE_ENV=test 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | coverage 4 | .vscode 5 | .DS_STORE 6 | .env 7 | tmp 8 | .idea 9 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 15 | 16 | 23 | 24 | 27 | 28 | 35 | 36 | 43 | 44 | 51 | 52 | 57 | 58 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/mockos-back.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts as build 2 | 3 | WORKDIR /usr/local/app 4 | 5 | COPY ./app /usr/local/app/app 6 | COPY ./commands /usr/local/app/commands 7 | COPY ./config /usr/local/app/config 8 | COPY ./contracts /usr/local/app/contracts 9 | COPY ./database /usr/local/app/database 10 | COPY ./providers /usr/local/app/providers 11 | COPY ./resources /usr/local/app/resources 12 | COPY ./start /usr/local/app/start 13 | COPY ./tests /usr/local/app/tests 14 | COPY ./.adonisrc.json /usr/local/app/ 15 | COPY ./ace /usr/local/app/ 16 | COPY ./ace-manifest.json /usr/local/app/ 17 | COPY ./env.ts /usr/local/app/ 18 | COPY ./package.json /usr/local/app/ 19 | COPY ./package-lock.json /usr/local/app/ 20 | COPY ./server.ts /usr/local/app/ 21 | COPY ./test.ts /usr/local/app/ 22 | COPY ./tsconfig.json /usr/local/app/ 23 | 24 | ENV NODE_ENV=development 25 | RUN npm install 26 | RUN node ace build --production --ignore-ts-errors 27 | 28 | FROM node:lts 29 | WORKDIR /usr/local/app 30 | COPY --from=build /usr/local/app/build/ /usr/local/app/ 31 | ENV NODE_ENV=production 32 | RUN npm ci 33 | CMD node ace migration:run --force && node server.js 34 | EXPOSE 3333 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | mockos 3 |
4 | 5 | ## Mockos 6 | 7 | Create API mocks **collaboratively**, **fully online** and with **custom code logic**. 8 | 9 | ## Deployments 10 | 11 | - **Application** - https://mockos.io 12 | - **Documentation** - https://docs.mockos.io 13 | 14 | If you find any bugs/vulnerabilities feel free to open a new issue or/and contact me at undernightcore@gmail.com. 15 | 16 | ## Features 17 | 18 | - Create API responses using a **simple JSON editor**. 19 | - Create **fully dynamic responses** with our **JS sandbox**, like a **real API**! 20 | - Stay in sync with other people thanks to **realtime and compare capabilities**. 21 | - Create **multiple projects** and invite **multiple members** to help you mock API responses. 22 | - **Import OpenAPI/Swagger** contracts and _create mocks automatically_. 23 | 24 | ## Host your own instance using Docker 25 | 26 | - Check our [self-hosting guide.](https://docs.mockos.io/docs/getting-started/self-hosting) 27 | 28 | ## Screenshots 29 | 30 | ![Projects](https://github.com/undernightcore/mockos-ui/blob/assets/projects.png?raw=true) 31 | 32 | ![Routes](https://github.com/undernightcore/mockos-ui/blob/assets/routes.png?raw=true) 33 | 34 | ![Response](https://github.com/undernightcore/mockos-ui/blob/assets/response.png?raw=true) 35 | 36 | ![Live](https://github.com/undernightcore/mockos-ui/blob/assets/live.png?raw=true) 37 | -------------------------------------------------------------------------------- /ace: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Ace Commands 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This file is the entry point for running ace commands. 7 | | 8 | */ 9 | 10 | require('reflect-metadata') 11 | require('source-map-support').install({ handleUncaughtExceptions: false }) 12 | 13 | const { Ignitor } = require('@adonisjs/core/build/standalone') 14 | new Ignitor(__dirname) 15 | .ace() 16 | .handle(process.argv.slice(2)) 17 | -------------------------------------------------------------------------------- /app/Controllers/Http/ApiController.ts: -------------------------------------------------------------------------------- 1 | import { I18nContract } from '@ioc:Adonis/Addons/I18n' 2 | import Drive from '@ioc:Adonis/Core/Drive' 3 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 4 | import { RequestContract } from '@ioc:Adonis/Core/Request' 5 | import { ResponseContract } from '@ioc:Adonis/Core/Response' 6 | import { HttpError } from 'App/Models/HttpError' 7 | import Project from 'App/Models/Project' 8 | import Response from 'App/Models/Response' 9 | import Route from 'App/Models/Route' 10 | import Token from 'App/Models/Token' 11 | import { Buffer } from 'buffer' 12 | import Sandbox from 'v8-sandbox' 13 | 14 | export default class ApiController { 15 | public async mock({ request, params, response, i18n }: HttpContextContract) { 16 | const project = await this.#authenticateProject(request, i18n) 17 | const method = request.method().toLowerCase() 18 | const endpoint = '/' + (params['*']?.join('/') ?? '') 19 | const routes = await project 20 | .related('routes') 21 | .query() 22 | .where('method', '=', method) 23 | .andWhere('enabled', '=', true) 24 | .andWhere('is_folder', '=', false) 25 | .orderBy('order') 26 | const { enabledResponse, routeParams } = await this.#getFirstMatchingRouteOrFail( 27 | routes, 28 | endpoint, 29 | i18n 30 | ) 31 | if (!enabledResponse) 32 | throw new HttpError(404, i18n.formatMessage('responses.api.mock.missing_response')) 33 | 34 | const file = enabledResponse.isFile 35 | ? await Drive.get(`responses/${enabledResponse.body}`) 36 | : undefined 37 | const headers = await this.#getHeadersOrDefault(enabledResponse, Boolean(file)) 38 | return this.#prepareResponse(headers, enabledResponse, routeParams, file, request, response) 39 | } 40 | 41 | // Helper functions 42 | async #authenticateProject(request: RequestContract, i18n: I18nContract) { 43 | const requestToken: string | undefined = request.param('token') || request.header('token') 44 | if (!requestToken) { 45 | throw new HttpError(400, i18n.formatMessage('responses.api.mock.missing_token')) 46 | } 47 | 48 | const token = await Token.findBy('token', requestToken) 49 | if (!token) { 50 | throw new HttpError(400, i18n.formatMessage('responses.api.mock.wrong_token')) 51 | } 52 | 53 | const project = await Project.find(token.projectId) 54 | if (!project) { 55 | throw new HttpError(400, i18n.formatMessage('responses.api.mock.wrong_token')) 56 | } 57 | 58 | return project 59 | } 60 | 61 | async #getFirstMatchingRouteOrFail(routes: Route[], endpoint: string, i18n: I18nContract) { 62 | const regexList = routes.map( 63 | (route) => 64 | // Replace {} dynamic values in path to regex dynamic value 65 | new RegExp( 66 | `^${route.endpoint.replace('/', '\\/').replace(new RegExp('{(.+?)}', 'g'), '([^/]+)')}$` 67 | ) 68 | ) 69 | const routeIndex = regexList.findIndex((regex) => regex.test(endpoint)) 70 | if (routeIndex === -1) { 71 | throw new HttpError(400, i18n.formatMessage('responses.api.mock.missing_route')) 72 | } else { 73 | return { 74 | enabledResponse: await routes[routeIndex] 75 | .related('responses') 76 | .query() 77 | .preload('processor') 78 | .where('enabled', '=', true) 79 | .first(), 80 | routeParams: this.#getHydratedRouteParams( 81 | routes[routeIndex], 82 | endpoint, 83 | regexList[routeIndex] 84 | ), 85 | } 86 | } 87 | } 88 | 89 | #getHydratedRouteParams(route: Route, endpoint: string, regex: RegExp) { 90 | const keys = 91 | route.endpoint 92 | .match(regex) 93 | ?.filter((value) => typeof value === 'string') 94 | .map((key) => key.replace(/[{}]/g, '')) 95 | .slice(1) ?? [] 96 | 97 | const values = 98 | endpoint 99 | .match(regex) 100 | ?.filter((value) => typeof value === 'string') 101 | .slice(1) ?? [] 102 | 103 | return Object.fromEntries(keys.map((key, index) => [key, values[index]])) 104 | } 105 | 106 | async #getHeadersOrDefault(response: Response, isFile: boolean) { 107 | const dbHeaders = await response.related('headers').query() 108 | const headers = dbHeaders.map(({ key, value }) => ({ key, value })) 109 | const foundContentType = headers.find((header) => header.key === 'content-type') 110 | const defaultContentType = { 111 | key: 'content-type', 112 | value: isFile ? 'application/octet-stream' : 'application/json', 113 | } 114 | return foundContentType ? headers : [...headers, defaultContentType] 115 | } 116 | 117 | async #prepareResponse( 118 | headers: { key: string; value: string }[], 119 | enabledResponse: Response, 120 | params: { [key: string]: string }, 121 | file: Buffer | undefined, 122 | request: RequestContract, 123 | response: ResponseContract 124 | ) { 125 | const processed = await this.#preprocessRequest(enabledResponse, params, request, file) 126 | 127 | return headers 128 | .reduce( 129 | (acc, { key, value }) => acc.safeHeader(key, value), 130 | response.status(enabledResponse.status) 131 | ) 132 | .send(processed) 133 | } 134 | 135 | async #preprocessRequest( 136 | enabledResponse: Response, 137 | params: { [key: string]: string }, 138 | request: RequestContract, 139 | file: Buffer | undefined 140 | ) { 141 | if (enabledResponse.processor?.enabled && !file) { 142 | const sandbox = new Sandbox() 143 | const { error, value } = await sandbox.execute({ 144 | code: enabledResponse.processor.code, 145 | timeout: 2000, 146 | globals: { 147 | queryParams: request.qs(), 148 | url: request.url(), 149 | params, 150 | body: request.body(), 151 | headers: request.headers(), 152 | content: enabledResponse.body, 153 | }, 154 | }) 155 | sandbox.shutdown() 156 | 157 | if (error) { 158 | throw new HttpError(400, `The preprocessor code crashed: ${error.message}`) 159 | } 160 | 161 | return String(value) 162 | } 163 | 164 | return enabledResponse.body 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /app/Controllers/Http/HeadersController.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Response from 'App/Models/Response' 3 | import Route from 'App/Models/Route' 4 | import Project from 'App/Models/Project' 5 | import CreateHeaderValidator from 'App/Validators/Header/CreateHeaderValidator' 6 | import Header from 'App/Models/Header' 7 | import Ws from 'App/Services/Ws' 8 | import UpdateHeaderValidator from 'App/Validators/Header/UpdateHeaderValidator' 9 | 10 | export default class HeadersController { 11 | public async getList({ params, request, response, auth, bouncer, i18n }: HttpContextContract) { 12 | await auth.authenticate() 13 | const { res, route, project } = await this.#getProjectByResponse(params.id) 14 | await bouncer.with('RoutePolicy').authorize('isNotFolder', route, i18n) 15 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 16 | const page = request.input('page', 1) 17 | const perPage = request.input('perPage', 10) 18 | const headers = await res.related('headers').query().paginate(page, perPage) 19 | return response.ok(headers) 20 | } 21 | 22 | public async create({ params, request, response, auth, bouncer, i18n }: HttpContextContract) { 23 | await auth.authenticate() 24 | const { res, project, route } = await this.#getProjectByResponse(params.id) 25 | await bouncer.with('RoutePolicy').authorize('isNotFolder', route, i18n) 26 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 27 | const { key, value } = await request.validate(CreateHeaderValidator) 28 | await res.related('headers').create({ key: key.toLowerCase(), value: value }) 29 | Ws.io.emit(`response:${res.id}`, `headers`) 30 | return response.created({ 31 | message: i18n.formatMessage('responses.header.create.header_created'), 32 | }) 33 | } 34 | 35 | public async edit({ params, request, response, auth, bouncer, i18n }: HttpContextContract) { 36 | await auth.authenticate() 37 | const { header, project, res, route } = await this.#getProjectByHeader(params.id) 38 | params['responseId'] = res.id // Pass context to validator 39 | await bouncer.with('RoutePolicy').authorize('isNotFolder', route, i18n) 40 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 41 | const { key, value } = await request.validate(UpdateHeaderValidator) 42 | await header.merge({ key: key.toLowerCase(), value: value }).save() 43 | Ws.io.emit(`response:${res.id}`, `headers`) 44 | return response.created({ 45 | message: i18n.formatMessage('responses.header.update.header_updated'), 46 | }) 47 | } 48 | 49 | public async delete({ params, response, auth, bouncer, i18n }: HttpContextContract) { 50 | await auth.authenticate() 51 | const { header, res, project, route } = await this.#getProjectByHeader(params.id) 52 | await bouncer.with('RoutePolicy').authorize('isNotFolder', route, i18n) 53 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 54 | await header.delete() 55 | Ws.io.emit(`response:${res.id}`, `headers`) 56 | return response.ok({ 57 | message: i18n.formatMessage('responses.header.delete.header_deleted'), 58 | }) 59 | } 60 | 61 | // Helper functions 62 | 63 | async #getProjectByHeader(id: string) { 64 | const header = await Header.findOrFail(id) 65 | const res = await Response.findOrFail(header.responseId) 66 | const route = await Route.findOrFail(res.routeId) 67 | const project = await Project.findOrFail(route.projectId) 68 | return { header, res, route, project } 69 | } 70 | 71 | async #getProjectByResponse(id: string) { 72 | const res = await Response.findOrFail(id) 73 | const route = await Route.findOrFail(res.routeId) 74 | const project = await Project.findOrFail(route.projectId) 75 | return { res, route, project } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/Controllers/Http/InvitationsController.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Member from 'App/Models/Member' 3 | import Project from 'App/Models/Project' 4 | import User from 'App/Models/User' 5 | import Ws from 'App/Services/Ws' 6 | 7 | export default class InvitationsController { 8 | public async getList({ request, response, auth }: HttpContextContract) { 9 | const user = await auth.authenticate() 10 | const page = await request.input('page') 11 | const perPage = await request.input('perPage') 12 | const invitations = await user 13 | .related('invitations') 14 | .query() 15 | .where('verified', false) 16 | .preload('project') 17 | .paginate(page ?? 1, perPage ?? 10) 18 | return response.ok(invitations) 19 | } 20 | 21 | public async accept({ response, auth, params, bouncer, i18n }: HttpContextContract) { 22 | const user = await auth.authenticate() 23 | const invitation = await Member.findOrFail(params.id) 24 | await bouncer.with('InvitationPolicy').authorize('isInvited', invitation, i18n) 25 | await invitation.load('project') 26 | invitation.verified = true 27 | await invitation.save() 28 | Ws.io.emit(`projects:${user.id}`, `updated`) 29 | return response.ok({ 30 | message: i18n.formatMessage('responses.invitation.accept.welcome_user', { 31 | project: invitation.project.name, 32 | name: user.name, 33 | }), 34 | }) 35 | } 36 | 37 | public async reject({ response, auth, params, bouncer, i18n }: HttpContextContract) { 38 | await auth.authenticate() 39 | const invitation = await Member.findOrFail(params.id) 40 | await bouncer.with('InvitationPolicy').authorize('isInvited', invitation, i18n) 41 | await invitation.load('project') 42 | const count = 43 | (await invitation.project.related('members').query().count('* as total'))[0].$extras.total - 1 44 | if (count) { 45 | await invitation.delete() 46 | } else { 47 | await invitation.project.delete() 48 | } 49 | return response.ok({ 50 | message: i18n.formatMessage('responses.invitation.reject.invitation_rejected'), 51 | }) 52 | } 53 | 54 | public async invite({ response, params, auth, bouncer, i18n }: HttpContextContract) { 55 | await auth.authenticate() 56 | const project = await Project.findOrFail(params.projectId) 57 | const user = await User.findByOrFail('email', params.email) 58 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 59 | await bouncer.forUser(user).with('ProjectPolicy').authorize('isAlreadyMember', project, i18n) 60 | 61 | if (!user.verified) { 62 | return response.ok({ 63 | message: i18n.formatMessage('bouncer.global.is_verified'), 64 | }) 65 | } 66 | 67 | await project.related('members').attach({ 68 | [user.id]: { 69 | verified: false, 70 | }, 71 | }) 72 | Ws.io.emit(`invitations:${user.id}`, `updated`) 73 | return response.ok({ 74 | message: i18n.formatMessage('responses.invitation.invite.user_invited', { 75 | name: user.name, 76 | }), 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/Controllers/Http/ProjectsController.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Project from 'App/Models/Project' 3 | import EditProjectValidator from 'App/Validators/Project/EditProjectValidator' 4 | import CreateProjectValidator from 'App/Validators/Project/CreateProjectValidator' 5 | import Member from 'App/Models/Member' 6 | import Database from '@ioc:Adonis/Lucid/Database' 7 | import GetProjectsValidator from 'App/Validators/Project/GetProjectsValidator' 8 | import Ws from 'App/Services/Ws' 9 | 10 | export default class ProjectsController { 11 | public async create({ request, response, auth }: HttpContextContract) { 12 | const user = await auth.authenticate() 13 | const data = await request.validate(CreateProjectValidator) 14 | const project = await Project.create(data) 15 | await project.related('members').attach({ [user.id]: { verified: true } }) 16 | Ws.io.emit(`projects:${user.id}`, `updated`) 17 | return response.created(project) 18 | } 19 | 20 | public async edit({ request, params, response, auth, bouncer, i18n }: HttpContextContract) { 21 | const user = await auth.authenticate() 22 | const data = await request.validate(EditProjectValidator) 23 | const project = await Project.findOrFail(params.id) 24 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 25 | const newProject = await project?.merge(data).save() 26 | Ws.io.emit(`projects:${user.id}`, `updated`) 27 | return response.ok(newProject) 28 | } 29 | 30 | public async get({ params, response, auth, bouncer, i18n }: HttpContextContract) { 31 | await auth.authenticate() 32 | const project = await Project.findOrFail(params.id) 33 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 34 | return response.ok(project) 35 | } 36 | 37 | public async getList({ response, request, auth }: HttpContextContract) { 38 | const user = await auth.authenticate() 39 | const options = await request.validate(GetProjectsValidator) 40 | 41 | const projectList = await user 42 | .related('projects') 43 | .query() 44 | .preload('forkedProject') 45 | .wherePivot('verified', true) 46 | [options.onlyBranches ? 'whereNotNull' : 'whereNull']('forkedProjectId') 47 | .orderBy(options.sortBy ?? 'created_at', options.direction as 'asc' | 'desc') 48 | .paginate(options.page ?? 1, options.perPage ?? 10) 49 | 50 | return response.ok(projectList) 51 | } 52 | 53 | public async getMemberList({ 54 | response, 55 | request, 56 | params, 57 | auth, 58 | bouncer, 59 | i18n, 60 | }: HttpContextContract) { 61 | await auth.authenticate() 62 | const page = await request.input('page') 63 | const perPage = await request.input('perPage') 64 | const project = await Project.findOrFail(params.id) 65 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 66 | const memberList = await Member.query() 67 | .where('project_id', project.id) 68 | .preload('user') 69 | .paginate(page ?? 1, perPage ?? 10) 70 | return response.ok(memberList) 71 | } 72 | 73 | public async fork({ response, request, params, auth, bouncer, i18n }: HttpContextContract) { 74 | const user = await auth.authenticate() 75 | const data = await request.validate(CreateProjectValidator) 76 | const project = await Project.findOrFail(params.id) 77 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 78 | await Database.transaction(async (trx) => { 79 | const previousIds = new Map() 80 | const newProject = await Project.create( 81 | { ...data, forkedProjectId: project.id }, 82 | { client: trx } 83 | ) 84 | const oldRoutes = await project 85 | .related('routes') 86 | .query() 87 | .orderBy('order') 88 | .useTransaction(trx) 89 | .preload('responses') 90 | 91 | for (const oldRoute of oldRoutes) { 92 | const { name, endpoint, method, enabled, order, responses, parentFolderId, isFolder } = 93 | oldRoute 94 | const newRoute = await newProject.related('routes').create( 95 | { 96 | name, 97 | endpoint, 98 | method, 99 | enabled, 100 | order, 101 | parentFolderId: parentFolderId !== null ? previousIds.get(parentFolderId) : null, 102 | isFolder, 103 | }, 104 | { client: trx } 105 | ) 106 | 107 | previousIds.set(oldRoute.id, newRoute.id) 108 | 109 | const newResponses = responses.map(({ name, body, status, enabled, isFile }) => ({ 110 | name, 111 | body, 112 | status, 113 | enabled, 114 | isFile, 115 | })) 116 | await newRoute.related('responses').createMany(newResponses, { client: trx }) 117 | } 118 | 119 | await newProject.related('members').attach({ [user.id]: { verified: true } }, trx) 120 | }) 121 | 122 | Ws.io.emit(`projects:${user.id}`, `updated`) 123 | return response.created({ message: i18n.formatMessage('responses.project.fork.fork_created') }) 124 | } 125 | 126 | public async leave({ response, auth, params, bouncer, i18n }: HttpContextContract) { 127 | const user = await auth.authenticate() 128 | const project = await Project.findOrFail(params.id) 129 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 130 | const count = 131 | (await project.related('members').query().count('* as total'))[0].$extras.total - 1 132 | if (count) { 133 | await project.related('members').detach([user.id]) 134 | } else { 135 | await project.delete() 136 | } 137 | 138 | Ws.io.emit(`projects:${user.id}`, `updated`) 139 | return response.ok({ 140 | message: i18n.formatMessage('responses.project.leave.left_project', { 141 | project: project.name, 142 | }), 143 | }) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /app/Controllers/Http/RoutesController.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Project from 'App/Models/Project' 3 | import Route from 'App/Models/Route' 4 | import CreateRouteValidator from 'App/Validators/Route/CreateRouteValidator' 5 | import EditRouteValidator from 'App/Validators/Route/EditRouteValidator' 6 | import Database from '@ioc:Adonis/Lucid/Database' 7 | import Ws from 'App/Services/Ws' 8 | import { recalculateRouteOrder } from 'App/Helpers/Shared/sort.helper' 9 | import { getLastIndex } from 'App/Helpers/Shared/array.helper' 10 | import { HttpError } from 'App/Models/HttpError' 11 | import EditFolderValidator from 'App/Validators/Route/EditFolderValidator' 12 | import MoveAndSortValidator from 'App/Validators/Route/MoveAndSortValidator' 13 | 14 | export default class RoutesController { 15 | public async create({ request, response, auth, params, bouncer, i18n }: HttpContextContract) { 16 | await auth.authenticate() 17 | const project = await Project.findOrFail(params.id) 18 | const isFolder = Boolean(request.input('isFolder', false)) 19 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 20 | const data = await request.validate(CreateRouteValidator) 21 | 22 | const route = await (data.parentFolderId 23 | ? this.createNewRouteInFolder(project, data.parentFolderId, data) 24 | : this.createNewRouteInRoot(project, isFolder, data)) 25 | 26 | Ws.io.emit(`project:${project.id}`, `updated`) 27 | return response.created(route) 28 | } 29 | 30 | public async edit({ request, response, auth, params, bouncer, i18n }: HttpContextContract) { 31 | await auth.authenticate() 32 | const route = await Route.findOrFail(params.id) 33 | const project = await Project.findOrFail(route.projectId) 34 | params.projectId = route.projectId 35 | const data = await request.validate(route.isFolder ? EditFolderValidator : EditRouteValidator) 36 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 37 | const newRoute = await route.merge(data).save() 38 | Ws.io.emit(`project:${project.id}`, `updated`) 39 | Ws.io.emit(`route:${route.id}`, `updated`) 40 | return response.ok(newRoute) 41 | } 42 | 43 | public async delete({ response, auth, params, bouncer, i18n }: HttpContextContract) { 44 | await auth.authenticate() 45 | const route = await Route.findOrFail(params.id) 46 | const project = await Project.findOrFail(route.projectId) 47 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 48 | await Database.transaction(async (trx) => { 49 | await route.useTransaction(trx) 50 | await route.delete() 51 | const routes = await project.related('routes').query().useTransaction(trx).orderBy('order') 52 | await recalculateRouteOrder(routes, trx) 53 | }) 54 | Ws.io.emit(`project:${project.id}`, `updated`) 55 | Ws.io.emit(`route:${route.id}`, `deleted`) 56 | return response.ok({ message: i18n.formatMessage('responses.route.delete.route_deleted') }) 57 | } 58 | 59 | public async get({ response, auth, params, bouncer, i18n }: HttpContextContract) { 60 | await auth.authenticate() 61 | const route = await Route.findOrFail(params.id) 62 | const project = await Project.findOrFail(route.projectId) 63 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 64 | return response.ok(route) 65 | } 66 | 67 | public async getList({ response, auth, params, bouncer, i18n }: HttpContextContract) { 68 | await auth.authenticate() 69 | const project = await Project.findOrFail(params.id) 70 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 71 | const routes = await project.related('routes').query().orderBy('order') 72 | return response.ok(routes) 73 | } 74 | 75 | public async moveAndSort({ 76 | auth, 77 | params, 78 | request, 79 | response, 80 | bouncer, 81 | i18n, 82 | }: HttpContextContract) { 83 | await auth.authenticate() 84 | 85 | const project = await Project.findOrFail(params.id) 86 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 87 | 88 | const data = await request.validate(MoveAndSortValidator) 89 | 90 | const what = await Route.findOrFail(data.what) 91 | if (what.isFolder && data.into !== null) 92 | throw new HttpError(400, i18n.formatMessage('responses.route.moveandsort.folder_in_folder')) 93 | 94 | await Database.transaction( 95 | async (trx) => { 96 | const routes = await project.related('routes').query().orderBy('order').useTransaction(trx) 97 | 98 | const whatIndex = routes.findIndex((route) => route.id === data.what) 99 | if (whatIndex === -1) 100 | throw new HttpError(500, i18n.formatMessage('responses.route.moveandsort.missing_data')) 101 | const whatChildren = routes.reduce( 102 | (acc, route) => (route.parentFolderId === data.what ? acc + 1 : acc), 103 | 0 104 | ) 105 | const what = routes.splice(whatIndex, whatChildren + 1) 106 | 107 | const siblingIndex = 108 | data.before !== null 109 | ? routes.findIndex((route) => route.id === data.before) 110 | : data.into === null 111 | ? routes.length 112 | : routes.reduce( 113 | (last, current, index) => 114 | current.id === data.into || current.parentFolderId === data.into 115 | ? index + 1 116 | : last, 117 | -1 118 | ) 119 | 120 | if (siblingIndex === -1) 121 | throw new HttpError(500, i18n.formatMessage('responses.route.moveandsort.missing_data')) 122 | 123 | routes.splice(siblingIndex, 0, ...what) 124 | 125 | if (!what[0].isFolder) what[0].parentFolderId = data.into 126 | 127 | await recalculateRouteOrder(routes, trx) 128 | }, 129 | { isolationLevel: 'repeatable read' } 130 | ) 131 | 132 | Ws.io.emit(`project:${project.id}`, `updated`) 133 | return response.ok({ message: i18n.formatMessage('responses.route.moveandsort.successfully') }) 134 | } 135 | 136 | // Helper functions 137 | 138 | private async createNewRouteInRoot( 139 | project: Project, 140 | isFolder: boolean, 141 | data: { [p: string]: any } 142 | ) { 143 | const lastOrder = await project.related('routes').query().orderBy('order', 'desc').first() 144 | return project 145 | .related('routes') 146 | .create({ ...data, isFolder, order: (lastOrder?.order ?? 0) + 1 }) 147 | } 148 | 149 | private async createNewRouteInFolder( 150 | project: Project, 151 | parentFolderId: number, 152 | data: { [p: string]: any } 153 | ) { 154 | return Database.transaction(async (trx) => { 155 | const newRoute = new Route().fill({ 156 | ...data, 157 | isFolder: false, 158 | projectId: project.id, 159 | parentFolderId: parentFolderId, 160 | }) 161 | const allRoutes = await project.related('routes').query().useTransaction(trx).orderBy('order') 162 | const lastFolderChildIndex = getLastIndex( 163 | allRoutes, 164 | (route: Route) => route.parentFolderId === parentFolderId || route.id === parentFolderId 165 | ) 166 | allRoutes.splice(lastFolderChildIndex + 1, 0, newRoute) 167 | await recalculateRouteOrder(allRoutes, trx) 168 | return newRoute 169 | }) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /app/Controllers/Http/SwaggerController.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Database, { TransactionClientContract } from '@ioc:Adonis/Lucid/Database' 3 | import { toMap } from 'App/Helpers/Shared/array.helper' 4 | import { parseSwagger } from 'App/Helpers/Swagger/swagger.helper' 5 | import { ParsedHeaderInterface } from 'App/Interfaces/HeaderInterface' 6 | import { ParsedResponseInterface } from 'App/Interfaces/ResponseInterface' 7 | import { ParsedRouteInterface } from 'App/Interfaces/RouteInterface' 8 | import Header from 'App/Models/Header' 9 | import Project from 'App/Models/Project' 10 | import Response from 'App/Models/Response' 11 | import Route from 'App/Models/Route' 12 | import Ws from 'App/Services/Ws' 13 | import ImportSwaggerValidator from 'App/Validators/Swagger/ImportSwaggerValidator' 14 | 15 | export default class SwaggerController { 16 | public async parse({ auth, params, request, response, i18n }: HttpContextContract) { 17 | await auth.authenticate() 18 | 19 | const data = await request.validate(ImportSwaggerValidator) 20 | 21 | const result = await parseSwagger(data.swagger, data.basePath) 22 | 23 | await this.insertRoutes(result, params.id, data.reset) 24 | 25 | Ws.io.emit(`project:${params.id}`, `updated`) 26 | 27 | return response.created({ message: i18n.formatMessage('responses.swagger.parse.success') }) 28 | } 29 | 30 | private async insertRoutes(routes: ParsedRouteInterface[], projectId: number, reset: boolean) { 31 | const project = await Project.findOrFail(projectId) 32 | 33 | await Database.transaction( 34 | async (trx) => { 35 | if (reset) { 36 | await Route.query().where('projectId', project.id).useTransaction(trx).delete() 37 | } 38 | 39 | const existingRoutes = await project.related('routes').query().useTransaction(trx) 40 | const existingEndpoints = toMap( 41 | existingRoutes, 42 | (route) => `${route.method} ${route.endpoint}` 43 | ) 44 | 45 | const parsedNewRoutes = routes.filter( 46 | (route) => !existingEndpoints.has(`${route.method} ${route.endpoint}`) 47 | ) 48 | const parsedExistingRoutes = routes 49 | .map((route) => ({ 50 | id: existingEndpoints.get(`${route.method} ${route.endpoint}`)?.id, 51 | ...route, 52 | })) 53 | .filter((route) => route.id !== undefined) 54 | 55 | const lastOrder = await project 56 | .related('routes') 57 | .query() 58 | .useTransaction(trx) 59 | .orderBy('order', 'desc') 60 | .first() 61 | 62 | await Promise.all( 63 | parsedNewRoutes.map(async (route, index) => { 64 | const newRoute = await project 65 | .related('routes') 66 | .create( 67 | { ...route, enabled: true, order: (lastOrder?.order ?? 0) + index + 1 }, 68 | { client: trx } 69 | ) 70 | 71 | if (route.responses && route.responses.length > 0) { 72 | await this.insertResponses(newRoute.id, route.responses, trx, true) 73 | } 74 | }) 75 | ) 76 | 77 | await Promise.all( 78 | parsedExistingRoutes.map(async (route) => { 79 | if (route.id !== undefined && route.responses && route.responses.length > 0) { 80 | await this.insertResponses(route.id, route.responses, trx, false) 81 | } 82 | }) 83 | ) 84 | }, 85 | { isolationLevel: 'repeatable read' } 86 | ) 87 | } 88 | 89 | private async insertResponses( 90 | routeId: number, 91 | responses: ParsedResponseInterface[], 92 | trx: TransactionClientContract, 93 | enabled: boolean 94 | ) { 95 | await Promise.all( 96 | responses.map(async (response, index) => { 97 | const newResponse = await Response.create( 98 | { 99 | ...response, 100 | name: `${response.name} - Imported from Swagger ${new Date().getTime()}`, 101 | routeId, 102 | isFile: false, 103 | enabled: index === 0 && enabled, 104 | body: response.body, 105 | }, 106 | { client: trx } 107 | ) 108 | 109 | if (response.headers && response.headers.length > 0) { 110 | await this.insertHeaders(response.headers, newResponse.id, trx) 111 | } 112 | }) 113 | ) 114 | } 115 | 116 | private async insertHeaders( 117 | headers: ParsedHeaderInterface[], 118 | responseId: number, 119 | trx: TransactionClientContract 120 | ) { 121 | await Header.createMany( 122 | headers.map((header) => ({ ...header, responseId })), 123 | { client: trx } 124 | ) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /app/Controllers/Http/TokensController.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Project from 'App/Models/Project' 3 | import { randomString } from 'App/Helpers/Shared/string.helper' 4 | import Token from 'App/Models/Token' 5 | import CreateTokenValidator from 'App/Validators/Token/CreateTokenValidator' 6 | 7 | export default class TokensController { 8 | public async create({ request, response, params, bouncer, i18n, auth }: HttpContextContract) { 9 | await auth.authenticate() 10 | const data = await request.validate(CreateTokenValidator) 11 | const project = await Project.findOrFail(params.id) 12 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 13 | let token = randomString(40) 14 | while (await Token.findBy('token', token)) { 15 | token = randomString(40) 16 | } 17 | const newToken = await project.related('tokens').create({ token, ...data }) 18 | return response.created(newToken) 19 | } 20 | 21 | public async delete({ response, params, bouncer, i18n, auth }: HttpContextContract) { 22 | await auth.authenticate() 23 | const token = await Token.findOrFail(params.id) 24 | const project = await Project.findOrFail(token.projectId) 25 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 26 | await token.delete() 27 | return response.ok({ message: i18n.formatMessage('responses.token.delete.token_deleted') }) 28 | } 29 | 30 | public async getList({ response, request, params, bouncer, i18n, auth }: HttpContextContract) { 31 | await auth.authenticate() 32 | const project = await Project.findOrFail(params.id) 33 | const page = request.input('page', 1) 34 | const perPage = request.input('perPage', 10) 35 | await bouncer.with('ProjectPolicy').authorize('isMember', project, i18n) 36 | const tokenList = await project.related('tokens').query().paginate(page, perPage) 37 | return response.ok(tokenList) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Controllers/Http/UserController.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import User from 'App/Models/User' 3 | import Mail from '@ioc:Adonis/Addons/Mail' 4 | import Route from '@ioc:Adonis/Core/Route' 5 | import RegisterValidator from 'App/Validators/User/RegisterValidator' 6 | import LoginValidator from 'App/Validators/User/LoginValidator' 7 | import Env from '@ioc:Adonis/Core/Env' 8 | import EditUserValidator from 'App/Validators/User/EditUserValidator' 9 | import EditUserEmailValidator from 'App/Validators/User/EditUserEmailValidator' 10 | import ResendEmailValidator from 'App/Validators/User/ResendEmailValidator' 11 | 12 | export default class UserController { 13 | public async get({ response, auth }: HttpContextContract) { 14 | const user = await auth.authenticate() 15 | return response.ok(user) 16 | } 17 | 18 | public async register({ request, response, i18n }: HttpContextContract) { 19 | const isVerifyDisabled = Env.get('DISABLE_VERIFICATION') 20 | const data = await request.validate(RegisterValidator) 21 | const { id, email, name } = await User.create({ ...data, verified: isVerifyDisabled }) 22 | if (!isVerifyDisabled) { 23 | const verificationUrl = this.#createVerificationUrl(id, 0) 24 | await this.#sendEmail( 25 | email, 26 | i18n.formatMessage('responses.user.register.verify_subject'), 27 | i18n.formatMessage('responses.user.register.verify_message', { 28 | url: `${Env.get('BACK_URL')}${verificationUrl}`, 29 | }) 30 | ) 31 | } 32 | return response.created({ 33 | message: i18n.formatMessage( 34 | `responses.user.register.${isVerifyDisabled ? 'login' : 'verify_email'}`, 35 | { name } 36 | ), 37 | }) 38 | } 39 | 40 | public async verify({ request, response, params }: HttpContextContract) { 41 | if (!request.hasValidSignature()) return response.redirect(Env.get('VERIFY_FAILURE_URL')) 42 | const id = Number(params.id) 43 | const verifyLock = Number(request.qs().verifyLock) 44 | const email = request.qs().email 45 | const user = await User.find(id) 46 | if (!user || user.verifyLock !== verifyLock) { 47 | return response.redirect(`${Env.get('FRONT_URL')}/verify/failure`) 48 | } 49 | if (email && (await User.findBy('email', email))) { 50 | return response.redirect(`${Env.get('FRONT_URL')}/verify/failure`) 51 | } 52 | user.email = email ?? user.email 53 | user.verified = true 54 | user.verifyLock = user.verifyLock + 1 55 | await user.save() 56 | return response.redirect(`${Env.get('FRONT_URL')}/verify/success`) 57 | } 58 | 59 | public async login({ request, response, auth, i18n }: HttpContextContract) { 60 | const data = await request.validate(LoginValidator) 61 | try { 62 | const token = await auth.attempt(data.email, data.password, { 63 | expiresIn: '7 days', 64 | }) 65 | const user = await User.findByOrFail('email', data.email) 66 | await this.#flushExpiredTokens(user) 67 | return user?.verified 68 | ? response.ok(token) 69 | : response.forbidden({ errors: [i18n.formatMessage('responses.user.login.verify_first')] }) 70 | } catch { 71 | return response.unauthorized({ 72 | errors: [i18n.formatMessage('responses.user.login.wrong_credentials')], 73 | }) 74 | } 75 | } 76 | 77 | public async edit({ request, response, auth, i18n }: HttpContextContract) { 78 | const user = await auth.authenticate() 79 | const data = await request.validate(EditUserValidator) 80 | await user.merge(data).save() 81 | return response.ok({ 82 | message: i18n.formatMessage('responses.user.edit.user_edited'), 83 | }) 84 | } 85 | 86 | public async editEmail({ request, response, auth, i18n, bouncer }: HttpContextContract) { 87 | const { id, verifyLock } = await auth.authenticate() 88 | await bouncer.with('GlobalPolicy').authorize('isVerified', i18n) 89 | const { email } = await request.validate(EditUserEmailValidator) 90 | const verificationUrl = this.#createVerificationUrl(id, verifyLock, email) 91 | await this.#sendEmail( 92 | email, 93 | i18n.formatMessage('responses.user.email.verify_subject'), 94 | i18n.formatMessage('responses.user.email.verify_message', { 95 | url: `${Env.get('BACK_URL')}${verificationUrl}`, 96 | }) 97 | ) 98 | return response.ok({ message: i18n.formatMessage('responses.user.email.verify_email') }) 99 | } 100 | 101 | public async resendEmail({ request, response, i18n }: HttpContextContract) { 102 | const { email } = await request.validate(ResendEmailValidator) 103 | const user = await User.findByOrFail('email', email) 104 | if (user.verified) 105 | return response 106 | .status(400) 107 | .send({ errors: [i18n.formatMessage('responses.user.resend_email.already_verified')] }) 108 | const verificationUrl = this.#createVerificationUrl(user.id, user.verifyLock) 109 | await this.#sendEmail( 110 | email, 111 | i18n.formatMessage('responses.user.register.verify_subject'), 112 | i18n.formatMessage('responses.user.register.verify_message', { 113 | url: `${Env.get('BACK_URL')}${verificationUrl}`, 114 | }) 115 | ) 116 | return response.ok({ 117 | message: i18n.formatMessage('responses.user.resend_email.verify_email'), 118 | }) 119 | } 120 | 121 | // Helper functions 122 | 123 | async #sendEmail(email: string, subject: string, body: string) { 124 | await Mail.send((message) => { 125 | message.from(Env.get('SMTP_EMAIL')).to(email).subject(subject).text(body) 126 | }) 127 | } 128 | 129 | async #flushExpiredTokens(user: User) { 130 | await user.related('tokens').query().delete().where('expires_at', '<', new Date()) 131 | } 132 | 133 | #createVerificationUrl(id: number, verifyLock: number, email?: string) { 134 | return Route.makeSignedUrl('verifyEmail', { id }, { qs: { email, verifyLock } }) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.ts: -------------------------------------------------------------------------------- 1 | import Logger from '@ioc:Adonis/Core/Logger' 2 | import HttpExceptionHandler from '@ioc:Adonis/Core/HttpExceptionHandler' 3 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 4 | 5 | export default class ExceptionHandler extends HttpExceptionHandler { 6 | constructor() { 7 | super(Logger) 8 | } 9 | public async handle(error: any, ctx: HttpContextContract) { 10 | const code = error.status ?? 500 11 | const messages = error.messages?.errors?.map((e) => e.message) ?? [error.message] 12 | Logger.info(error) 13 | return ctx.response.status(code).send({ 14 | errors: code === 500 || !messages ? ['Error inesperado'] : messages, 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Helpers/Shared/array.helper.ts: -------------------------------------------------------------------------------- 1 | export function move(arr, old_index, new_index) { 2 | arr.splice(new_index, 0, arr.splice(old_index, 1)[0]) 3 | } 4 | 5 | export function getLastIndex(arr: any[], condition: (item: any) => boolean) { 6 | const reversedIndex = arr.slice().reverse().findIndex(condition) 7 | return reversedIndex === -1 ? -1 : arr.length - reversedIndex - 1 8 | } 9 | 10 | export function toMap(list: T[], getter: (item: T) => string | number): Map { 11 | const map = new Map() 12 | 13 | for (const item of list) { 14 | map.set(getter(item), item) 15 | } 16 | 17 | return map 18 | } 19 | -------------------------------------------------------------------------------- /app/Helpers/Shared/file.helper.ts: -------------------------------------------------------------------------------- 1 | import Response from 'App/Models/Response' 2 | import Drive from '@ioc:Adonis/Core/Drive' 3 | 4 | export async function deleteIfOnceUsed(location: string, fileName: string) { 5 | const amountUsed = 6 | ( 7 | await Response.query().where('body', fileName).andWhere('is_file', true).count('* as total') 8 | )[0].$extras.total - 1 9 | if (!amountUsed) await Drive.delete(`${location}/${fileName}`) 10 | } 11 | 12 | export function getFileName(path: string) { 13 | return path.split('/').at(-1)! 14 | } 15 | -------------------------------------------------------------------------------- /app/Helpers/Shared/sort.helper.ts: -------------------------------------------------------------------------------- 1 | import { TransactionClientContract } from '@ioc:Adonis/Lucid/Database' 2 | import Route from 'App/Models/Route' 3 | 4 | export async function recalculateRouteOrder(sortedRoutes: Route[], trx: TransactionClientContract) { 5 | const lastRouteOrder = sortedRoutes.reduce( 6 | (acc, value) => (value.order > acc ? value.order : acc), 7 | 1 8 | ) 9 | await Promise.all( 10 | sortedRoutes.map(async (r, index) => { 11 | r.order = index + lastRouteOrder + 1 12 | await r.useTransaction(trx).save() 13 | }) 14 | ) 15 | await Promise.all( 16 | sortedRoutes.map(async (r, index) => { 17 | r.order = index + 1 18 | await r.useTransaction(trx).save() 19 | }) 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /app/Helpers/Shared/string.helper.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto' 2 | import { load } from 'js-yaml' 3 | 4 | export function compressJson(value: string) { 5 | return isValidJson(value) ? JSON.stringify(JSON.parse(value)) : value 6 | } 7 | 8 | export function isValidJson(value: string) { 9 | try { 10 | JSON.parse(value) 11 | return true 12 | } catch { 13 | return false 14 | } 15 | } 16 | 17 | export function parseFromJsonOrYaml(value: string) { 18 | try { 19 | return JSON.parse(value) 20 | } catch { 21 | return load(value) 22 | } 23 | } 24 | 25 | export function randomString(size: number) { 26 | return randomBytes(size / 2).toString('hex') 27 | } 28 | -------------------------------------------------------------------------------- /app/Helpers/Swagger/common/common/constants/environment-schema.constants.ts: -------------------------------------------------------------------------------- 1 | import { BodyTypes, Methods, Route, RouteResponse, RouteType } from '../models/route.model' 2 | import { generateUUID } from '../utils/utils' 3 | 4 | export const RouteDefault: Route = { 5 | get uuid() { 6 | return generateUUID() 7 | }, 8 | name: '', 9 | type: RouteType.HTTP, 10 | documentation: '', 11 | method: Methods.get, 12 | endpoint: '', 13 | responses: [], 14 | responseMode: null, 15 | streamingMode: null, 16 | streamingInterval: 0, 17 | tags: [], 18 | } 19 | 20 | export const RouteResponseDefault: RouteResponse = { 21 | get uuid() { 22 | return generateUUID() 23 | }, 24 | body: '{}', 25 | latency: 0, 26 | statusCode: 200, 27 | label: '', 28 | headers: [], 29 | bodyType: BodyTypes.INLINE, 30 | filePath: '', 31 | databucketID: '', 32 | sendFileAsBody: false, 33 | rules: [], 34 | rulesOperator: 'OR', 35 | disableTemplating: false, 36 | fallbackTo404: false, 37 | default: false, 38 | crudKey: 'id', 39 | callbacks: [], 40 | } 41 | -------------------------------------------------------------------------------- /app/Helpers/Swagger/common/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants/environment-schema.constants' 2 | export * from './libs/schema-builder' 3 | export * from './models/environment.model' 4 | export * from './models/folder.model' 5 | export * from './models/route.model' 6 | export * from './utils/utils' 7 | -------------------------------------------------------------------------------- /app/Helpers/Swagger/common/common/libs/schema-builder.ts: -------------------------------------------------------------------------------- 1 | import { RouteDefault, RouteResponseDefault } from 'App/Helpers/Swagger/common/common' 2 | import { Header, Methods, Route, RouteResponse, RouteType } from '../models/route.model' 3 | 4 | /** 5 | * Build a new environment or route response header 6 | */ 7 | export const BuildHeader = (key = '', value = ''): Header => ({ key, value }) 8 | 9 | /** 10 | * Build a new route response 11 | */ 12 | export const BuildRouteResponse = (): RouteResponse => ({ 13 | ...RouteResponseDefault, 14 | }) 15 | 16 | /** 17 | * Build a new HTTP route 18 | */ 19 | export const BuildHTTPRoute = ( 20 | hasDefaultRouteResponse = true, 21 | options: { 22 | endpoint: typeof RouteDefault.endpoint 23 | body: typeof RouteResponseDefault.body 24 | } = { 25 | endpoint: RouteDefault.endpoint, 26 | body: RouteResponseDefault.body, 27 | } 28 | ): Route => { 29 | const defaultResponse = { 30 | ...BuildRouteResponse(), 31 | default: true, 32 | body: options.body, 33 | } 34 | 35 | return { 36 | ...RouteDefault, 37 | type: RouteType.HTTP, 38 | method: Methods.get, 39 | endpoint: options.endpoint, 40 | responses: hasDefaultRouteResponse ? [defaultResponse] : [], 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Helpers/Swagger/common/common/models/environment.model.ts: -------------------------------------------------------------------------------- 1 | import { Folder, FolderChild } from './folder.model' 2 | import { BodyTypes, Header, Methods, Route } from './route.model' 3 | 4 | export type TLSOptionsType = 'PFX' | 'CERT' 5 | 6 | export type DataBucket = { 7 | uuid: string 8 | // unique short id to be used in templating helpers or rules 9 | id: string 10 | name: string 11 | documentation: string 12 | value: string 13 | } 14 | 15 | export type Callback = { 16 | uuid: string 17 | id: string 18 | name: string 19 | documentation: string 20 | method: Methods 21 | uri: string 22 | headers: Header[] 23 | body?: string 24 | filePath?: string 25 | sendFileAsBody?: boolean 26 | bodyType: BodyTypes 27 | databucketID?: string 28 | } 29 | 30 | /** 31 | * Node.js TLS options https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tlscreatesecurecontextoptions 32 | * Use pfx or cert+key. 33 | */ 34 | export type EnvironmentTLSOptions = { 35 | // TLS enabled, old `https` flag 36 | enabled: boolean 37 | type: TLSOptionsType 38 | // path to PFX or PKCS12 encoded private key and certificate 39 | pfxPath: string 40 | // Path to cert chains in PEM format 41 | certPath: string 42 | // Path to private keys in PEM format 43 | keyPath: string 44 | // Path to CA certificates override 45 | caPath: string 46 | // Password for `pfx` or `!` 47 | passphrase: string 48 | } 49 | 50 | export type Environment = { 51 | uuid: string 52 | lastMigration: number 53 | name: string 54 | port: number 55 | hostname: string 56 | endpointPrefix: string 57 | latency: number 58 | folders: Folder[] 59 | routes: Route[] 60 | rootChildren: FolderChild[] 61 | proxyMode: boolean 62 | proxyRemovePrefix: boolean 63 | proxyHost: string 64 | proxyReqHeaders: Header[] 65 | proxyResHeaders: Header[] 66 | tlsOptions: EnvironmentTLSOptions 67 | cors: boolean 68 | headers: Header[] 69 | data: DataBucket[] 70 | callbacks: Callback[] 71 | } 72 | -------------------------------------------------------------------------------- /app/Helpers/Swagger/common/common/models/folder.model.ts: -------------------------------------------------------------------------------- 1 | export type FolderChild = { type: 'route' | 'folder'; uuid: string } 2 | 3 | export type Folder = { 4 | uuid: string 5 | name: string 6 | children: FolderChild[] 7 | } 8 | -------------------------------------------------------------------------------- /app/Helpers/Swagger/common/common/models/route.model.ts: -------------------------------------------------------------------------------- 1 | export type LogicalOperators = 'AND' | 'OR' 2 | 3 | export enum BodyTypes { 4 | INLINE = 'INLINE', 5 | FILE = 'FILE', 6 | DATABUCKET = 'DATABUCKET', 7 | } 8 | 9 | export type CallbackInvocation = { 10 | uuid: string 11 | latency: number 12 | } 13 | 14 | export type RouteResponse = { 15 | uuid: string 16 | rules: ResponseRule[] 17 | rulesOperator: LogicalOperators 18 | statusCode: number 19 | label: string 20 | headers: Header[] 21 | body: any 22 | latency: number 23 | bodyType: BodyTypes 24 | filePath: string 25 | databucketID: string 26 | sendFileAsBody: boolean 27 | disableTemplating: boolean 28 | fallbackTo404: boolean 29 | // default is always true for CRUD routes first response 30 | default: boolean 31 | crudKey: string 32 | callbacks: CallbackInvocation[] 33 | } 34 | 35 | export enum ResponseMode { 36 | RANDOM = 'RANDOM', 37 | SEQUENTIAL = 'SEQUENTIAL', 38 | DISABLE_RULES = 'DISABLE_RULES', 39 | FALLBACK = 'FALLBACK', 40 | } 41 | 42 | export enum StreamingMode { 43 | UNICAST = 'UNICAST', 44 | BROADCAST = 'BROADCAST', 45 | } 46 | 47 | export type ResponseRuleOperators = 48 | | 'equals' 49 | | 'regex' 50 | | 'regex_i' 51 | | 'null' 52 | | 'empty_array' 53 | | 'array_includes' 54 | | 'valid_json_schema' 55 | 56 | export type ResponseRule = { 57 | target: ResponseRuleTargets 58 | modifier: string 59 | value: string 60 | invert: boolean 61 | operator: ResponseRuleOperators 62 | } 63 | 64 | export type ResponseRuleTargets = 65 | | 'body' 66 | | 'query' 67 | | 'header' 68 | | 'cookie' 69 | | 'params' 70 | | 'path' 71 | | 'method' 72 | | 'request_number' 73 | | 'global_var' 74 | | 'data_bucket' 75 | | 'templating' 76 | 77 | export enum RouteType { 78 | HTTP = 'http', 79 | CRUD = 'crud', 80 | WS = 'ws', 81 | } 82 | 83 | export type Route = { 84 | uuid: string 85 | type: RouteType 86 | documentation: string 87 | name: string 88 | method: keyof typeof Methods | '' 89 | endpoint: string 90 | tags: string[] 91 | responses: RouteResponse[] 92 | responseMode: ResponseMode | null 93 | // used in websocket routes 94 | streamingMode: StreamingMode | null 95 | streamingInterval: number 96 | } 97 | 98 | export type Header = { key: string; value: string } 99 | 100 | export enum Methods { 101 | all = 'all', 102 | get = 'get', 103 | post = 'post', 104 | put = 'put', 105 | patch = 'patch', 106 | delete = 'delete', 107 | head = 'head', 108 | options = 'options', 109 | propfind = 'propfind', 110 | proppatch = 'proppatch', 111 | move = 'move', 112 | copy = 'copy', 113 | mkcol = 'mkcol', 114 | lock = 'lock', 115 | unlock = 'unlock', 116 | } 117 | -------------------------------------------------------------------------------- /app/Helpers/Swagger/common/common/utils/mappers.ts: -------------------------------------------------------------------------------- 1 | import { Header, Route, RouteResponse } from '../models/route.model' 2 | import { ParsedHeaderInterface } from 'App/Interfaces/HeaderInterface' 3 | import { ParsedResponseInterface } from 'App/Interfaces/ResponseInterface' 4 | import { ParsedRouteInterface } from 'App/Interfaces/RouteInterface' 5 | 6 | export function mapHeader(header: Header): ParsedHeaderInterface { 7 | return { 8 | key: header.key, 9 | value: header.value, 10 | } 11 | } 12 | 13 | export function mapResponse(response: RouteResponse): ParsedResponseInterface { 14 | return { 15 | status: response.statusCode, 16 | body: JSON.stringify(response.body), 17 | name: response.label, 18 | headers: response.headers.map(mapHeader), 19 | } 20 | } 21 | 22 | export function mapRoute(route: Route): ParsedRouteInterface { 23 | return { 24 | name: route.name, 25 | endpoint: route.endpoint, 26 | method: route.method, 27 | responses: route.responses.map(mapResponse), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Helpers/Swagger/common/common/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export const generateUUID = (): string => 'uuid' 2 | 3 | export function getRandomEnumValue(enumObj: T): T[keyof T] { 4 | // @ts-ignore 5 | const values = Object.values(enumObj) as T[keyof T][] // Get all values of the enum 6 | const randomIndex = Math.floor(Math.random() * values.length) // Generate a random index 7 | return values[randomIndex] // Return the random value 8 | } 9 | -------------------------------------------------------------------------------- /app/Helpers/Swagger/swagger.helper.ts: -------------------------------------------------------------------------------- 1 | import openAPI from '@apidevtools/swagger-parser' 2 | import { faker } from '@faker-js/faker' 3 | import { 4 | BuildHeader, 5 | BuildHTTPRoute, 6 | BuildRouteResponse, 7 | getRandomEnumValue, 8 | Header, 9 | Methods, 10 | Route, 11 | RouteResponse, 12 | } from 'App/Helpers/Swagger/common/common' 13 | import { mapRoute } from 'App/Helpers/Swagger/common/common/utils/mappers' 14 | import { ParsedRouteInterface } from 'App/Interfaces/RouteInterface' 15 | import { load } from 'js-yaml' 16 | import { OpenAPI, OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types' 17 | import TagObject = OpenAPIV3_1.TagObject 18 | 19 | type SpecificationVersions = 'SWAGGER' | 'OPENAPI_V3' 20 | 21 | export function parseSwagger(swagger: string, basePath?: string): Promise { 22 | return convertFromOpenAPI(swagger, basePath) 23 | } 24 | 25 | async function convertFromOpenAPI( 26 | swagger: string, 27 | basePath?: string 28 | ): Promise { 29 | let routes: Route[] = [] 30 | let api: OpenAPIV3.Document | undefined 31 | 32 | try { 33 | api = JSON.parse(swagger) as OpenAPIV3.Document 34 | } catch { 35 | api = load(swagger) as OpenAPIV3.Document 36 | } 37 | 38 | const parsedAPI: OpenAPI.Document = await openAPI.dereference.bind(openAPI)(api, { 39 | dereference: { circular: 'ignore' }, 40 | }) 41 | 42 | if (isSwagger(parsedAPI)) { 43 | routes = createRoutes(parsedAPI, 'SWAGGER', basePath) 44 | } else if (isOpenAPIV3(parsedAPI)) { 45 | routes = createRoutes(parsedAPI, 'OPENAPI_V3', basePath) 46 | } 47 | 48 | return routes.map(mapRoute) 49 | } 50 | 51 | function createRoutes(parsedAPI: OpenAPIV2.Document, version: 'SWAGGER', basePath?: string): Route[] 52 | function createRoutes( 53 | parsedAPI: OpenAPIV3.Document, 54 | version: 'OPENAPI_V3', 55 | basePath?: string 56 | ): Route[] 57 | function createRoutes( 58 | parsedAPI: OpenAPIV2.Document & OpenAPIV3.Document, 59 | version: SpecificationVersions, 60 | basePath?: string 61 | ): Route[] { 62 | const routes: Route[] = [] 63 | const tags: TagObject[] = parsedAPI.tags ?? [] 64 | 65 | Object.keys(parsedAPI.paths).forEach((routePath) => { 66 | Object.keys(parsedAPI.paths[routePath]).forEach((routeMethod) => { 67 | const parsedRoute: OpenAPIV2.OperationObject & OpenAPIV3.OperationObject = 68 | parsedAPI.paths[routePath][routeMethod] 69 | 70 | if (routeMethod in Methods) { 71 | const routeResponses: RouteResponse[] = [] 72 | 73 | Object.keys(parsedRoute.responses).forEach((responseStatus) => { 74 | const statusCode = parseInt(responseStatus, 10) 75 | 76 | if ((statusCode >= 100 && statusCode <= 999) || responseStatus === 'default') { 77 | const routeResponse: OpenAPIV2.ResponseObject & OpenAPIV3.ResponseObject = parsedRoute 78 | .responses[responseStatus] as OpenAPIV2.ResponseObject & OpenAPIV3.ResponseObject 79 | 80 | let contentTypeHeaders: string[] = [] 81 | let schema: OpenAPIV2.SchemaObject | OpenAPIV3.SchemaObject | undefined 82 | let examples: OpenAPIV2.ExampleObject | OpenAPIV3.ExampleObject | undefined 83 | let example: OpenAPIV2.ExampleObject | OpenAPIV3.ExampleObject | undefined 84 | 85 | if (version === 'SWAGGER') { 86 | contentTypeHeaders = 87 | parsedRoute.produces ?? 88 | parsedRoute.consumes ?? 89 | parsedAPI.produces ?? 90 | parsedAPI.consumes ?? 91 | [] 92 | } else if (version === 'OPENAPI_V3' && routeResponse.content) { 93 | contentTypeHeaders = Object.keys(routeResponse.content) 94 | } 95 | 96 | const contentTypeHeader = contentTypeHeaders.find((header) => 97 | header.includes('application/json') 98 | ) 99 | 100 | if (contentTypeHeader) { 101 | if (version === 'SWAGGER') { 102 | schema = routeResponse.schema 103 | examples = routeResponse.examples 104 | } else if (version === 'OPENAPI_V3') { 105 | schema = routeResponse.content?.[contentTypeHeader].schema 106 | examples = routeResponse.content?.[contentTypeHeader].examples 107 | example = routeResponse.content?.[contentTypeHeader].example 108 | } 109 | } 110 | 111 | const headers = buildResponseHeaders(contentTypeHeaders, routeResponse.headers) 112 | 113 | if (examples) { 114 | const routeResponseExamples = parseOpenAPIExamples(examples).map((example) => 115 | buildResponse( 116 | example.body, 117 | example.label, 118 | responseStatus === 'default' ? 200 : statusCode, 119 | headers 120 | ) 121 | ) 122 | routeResponses.push(...routeResponseExamples) 123 | } else if (example) { 124 | routeResponses.push( 125 | buildResponse(example, '', responseStatus === 'default' ? 200 : statusCode, headers) 126 | ) 127 | } else { 128 | routeResponses.push( 129 | buildResponse( 130 | schema ? generateSchema(schema) : undefined, 131 | routeResponse.description || '', 132 | responseStatus === 'default' ? 200 : statusCode, 133 | headers 134 | ) 135 | ) 136 | } 137 | } 138 | }) 139 | 140 | if (!routeResponses.length) { 141 | routeResponses.push({ 142 | ...BuildRouteResponse(), 143 | headers: [BuildHeader('Content-Type', 'application/json')], 144 | body: '', 145 | }) 146 | } 147 | 148 | routeResponses[0].default = true 149 | 150 | const newRoute: Route = { 151 | ...BuildHTTPRoute(false), 152 | name: parsedRoute.operationId || '', 153 | documentation: parsedRoute.summary || parsedRoute.description || '', 154 | method: routeMethod as Methods, 155 | endpoint: basePath ? basePath + routePath : routePath, 156 | responses: routeResponses, 157 | tags: tags.map((tag) => tag.name), 158 | } 159 | 160 | routes.push(newRoute) 161 | } 162 | }) 163 | }) 164 | 165 | return routes 166 | } 167 | 168 | /** 169 | * Build route response headers from 'content' (v3) or 'produces' (v2), and 'headers' objects 170 | * 171 | * @param contentTypes 172 | * @param responseHeaders 173 | */ 174 | function buildResponseHeaders( 175 | contentTypes: string[], 176 | responseHeaders: 177 | | undefined 178 | | OpenAPIV2.HeadersObject 179 | | Record 180 | ): Header[] { 181 | const routeContentTypeHeader = BuildHeader('Content-Type', 'application/json') 182 | 183 | if (contentTypes?.length && !contentTypes.includes('application/json')) { 184 | routeContentTypeHeader.value = contentTypes[0] 185 | } 186 | 187 | if (responseHeaders) { 188 | return [ 189 | routeContentTypeHeader, 190 | ...Object.keys(responseHeaders).map((headerName) => { 191 | let headerValue = '' 192 | 193 | if (responseHeaders[headerName] !== null) { 194 | if (responseHeaders[headerName]['example'] !== null) { 195 | headerValue = responseHeaders[headerName]['example'] 196 | } else if (responseHeaders[headerName]['examples'] !== null) { 197 | headerValue = 198 | responseHeaders[headerName]['examples'][ 199 | Object.keys(responseHeaders[headerName]['examples'])[0] 200 | ]['value'] 201 | } else if (responseHeaders[headerName]['schema'] !== null) { 202 | headerValue = generateSchema(responseHeaders[headerName]['schema']) 203 | } 204 | } 205 | 206 | return BuildHeader(headerName, headerValue) 207 | }), 208 | ] 209 | } 210 | 211 | return [routeContentTypeHeader] 212 | } 213 | 214 | function buildResponse( 215 | body: object | undefined, 216 | label: string, 217 | statusCode: number, 218 | headers: Header[] 219 | ) { 220 | return { 221 | ...BuildRouteResponse(), 222 | body: body ?? '', 223 | label, 224 | statusCode, 225 | headers, 226 | } 227 | } 228 | 229 | /** 230 | * Swagger specification type guard 231 | * 232 | * @param parsedAPI 233 | */ 234 | function isSwagger(parsedAPI: any): parsedAPI is OpenAPIV2.Document { 235 | return parsedAPI.swagger !== undefined 236 | } 237 | 238 | /** 239 | * OpenAPI v3 specification type guard 240 | * 241 | * @param parsedAPI 242 | */ 243 | function isOpenAPIV3(parsedAPI: any): parsedAPI is OpenAPIV3.Document { 244 | return parsedAPI.openapi !== undefined && parsedAPI.openapi.startsWith('3.') 245 | } 246 | 247 | /** 248 | * Generate a JSON object from a schema 249 | * 250 | */ 251 | function generateSchema(schema: OpenAPIV2.SchemaObject | OpenAPIV3.SchemaObject) { 252 | const typeFactories = { 253 | 'integer': () => faker.number.int({ max: 99999 }), 254 | 'number': () => faker.number.int({ max: 99999 }), 255 | 'number_float': () => faker.number.float({ fractionDigits: 2 }), 256 | 'number_double': () => faker.number.float({ fractionDigits: 2 }), 257 | 'string': () => faker.string.alpha({ length: { min: 1, max: 15 } }), 258 | 'string_date': () => faker.date.between({ from: '2024-01-01', to: Date.now() }), 259 | 'string_date-time': () => faker.date.between({ from: '2024-01-01', to: Date.now() }), 260 | 'string_email': () => faker.internet.email(), 261 | 'string_uuid': () => faker.string.uuid(), 262 | 'boolean': () => faker.datatype.boolean(), 263 | 264 | 'array': (arraySchema) => { 265 | const newObject = generateSchema(arraySchema.items) 266 | 267 | return arraySchema.collectionFormat === 'csv' ? newObject : [newObject] 268 | }, 269 | 'object': (objectSchema) => { 270 | const newObject = {} 271 | const { properties } = objectSchema 272 | 273 | if (properties) { 274 | Object.keys(properties).forEach((propertyName) => { 275 | newObject[propertyName] = generateSchema(properties[propertyName]) 276 | }) 277 | } 278 | 279 | return newObject 280 | }, 281 | } 282 | 283 | if (schema instanceof Object) { 284 | let type: string = 285 | Array.isArray(schema.type) && schema.type.length >= 1 286 | ? schema.type[0] 287 | : (schema.type as string) 288 | 289 | // use enum property if present 290 | if (schema.enum) { 291 | return getRandomEnumValue(schema.enum) 292 | } 293 | 294 | // return example if any 295 | if (schema.example) { 296 | return schema.example 297 | } 298 | 299 | // return default value if any 300 | if (schema.default) { 301 | return schema.default 302 | } 303 | 304 | const schemaToBuild = schema 305 | 306 | // check if we have an array of schemas, and take first item 307 | for (const propertyName of ['allOf', 'oneOf', 'anyOf']) { 308 | if ( 309 | Object.prototype.hasOwnProperty.call(schema, propertyName) && 310 | schema[propertyName].length > 0 311 | ) { 312 | return generateSchema(schema[propertyName][0]) 313 | } 314 | } 315 | 316 | // sometimes we have no type but only 'properties' (=object) 317 | if (!type && schemaToBuild.properties && schemaToBuild.properties instanceof Object) { 318 | type = 'object' 319 | } 320 | 321 | const typeFactory = typeFactories[`${type}_${schemaToBuild.format}`] || typeFactories[type] 322 | 323 | if (typeFactory) { 324 | return typeFactory(schemaToBuild) 325 | } 326 | 327 | return '' 328 | } 329 | } 330 | 331 | /** 332 | * Extract bodies and labels from OpenAPI examples 333 | * @param examples 334 | * @ 335 | */ 336 | function parseOpenAPIExamples(examples: OpenAPIV2.ExampleObject | OpenAPIV3.ExampleObject) { 337 | return Object.entries(examples) 338 | .map(([label, example]) => ({ label, example })) 339 | .filter(({ example }) => example?.value?.data) 340 | .map(({ label, example }) => ({ 341 | body: example.value.data, 342 | label, 343 | })) 344 | } 345 | -------------------------------------------------------------------------------- /app/Interfaces/HeaderInterface.ts: -------------------------------------------------------------------------------- 1 | export interface ParsedHeaderInterface { 2 | key: string 3 | value: string 4 | } 5 | -------------------------------------------------------------------------------- /app/Interfaces/ResponseInterface.ts: -------------------------------------------------------------------------------- 1 | import { ParsedHeaderInterface } from 'App/Interfaces/HeaderInterface' 2 | 3 | export interface ParsedResponseInterface { 4 | status: number 5 | body: string 6 | name: string 7 | headers: ParsedHeaderInterface[] 8 | } 9 | -------------------------------------------------------------------------------- /app/Interfaces/RouteInterface.ts: -------------------------------------------------------------------------------- 1 | import { ParsedResponseInterface } from 'App/Interfaces/ResponseInterface' 2 | 3 | export interface ParsedRouteInterface { 4 | name: string 5 | endpoint: string 6 | method: string 7 | responses: ParsedResponseInterface[] 8 | } 9 | -------------------------------------------------------------------------------- /app/Middleware/Auth.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticationException } from '@adonisjs/auth/build/standalone' 2 | import type { GuardsList } from '@ioc:Adonis/Addons/Auth' 3 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 4 | 5 | /** 6 | * Auth middleware is meant to restrict un-authenticated access to a given route 7 | * or a group of routes. 8 | * 9 | * You must register this middleware inside `start/kernel.ts` file under the list 10 | * of named middleware. 11 | */ 12 | export default class AuthMiddleware { 13 | /** 14 | * The URL to redirect to when request is Unauthorized 15 | */ 16 | protected redirectTo = '/login' 17 | 18 | /** 19 | * Authenticates the current HTTP request against a custom set of defined 20 | * guards. 21 | * 22 | * The authentication loop stops as soon as the user is authenticated using any 23 | * of the mentioned guards and that guard will be used by the rest of the code 24 | * during the current request. 25 | */ 26 | protected async authenticate(auth: HttpContextContract['auth'], guards: (keyof GuardsList)[]) { 27 | /** 28 | * Hold reference to the guard last attempted within the for loop. We pass 29 | * the reference of the guard to the "AuthenticationException", so that 30 | * it can decide the correct response behavior based upon the guard 31 | * driver 32 | */ 33 | let guardLastAttempted: string | undefined 34 | 35 | for (let guard of guards) { 36 | guardLastAttempted = guard 37 | 38 | if (await auth.use(guard).check()) { 39 | /** 40 | * Instruct auth to use the given guard as the default guard for 41 | * the rest of the request, since the user authenticated 42 | * succeeded here 43 | */ 44 | auth.defaultGuard = guard 45 | return true 46 | } 47 | } 48 | 49 | /** 50 | * Unable to authenticate using any guard 51 | */ 52 | throw new AuthenticationException( 53 | 'Unauthorized access', 54 | 'E_UNAUTHORIZED_ACCESS', 55 | guardLastAttempted, 56 | this.redirectTo 57 | ) 58 | } 59 | 60 | /** 61 | * Handle request 62 | */ 63 | public async handle( 64 | { auth }: HttpContextContract, 65 | next: () => Promise, 66 | customGuards: (keyof GuardsList)[] 67 | ) { 68 | /** 69 | * Uses the user defined guards or the default guard mentioned in 70 | * the config file 71 | */ 72 | const guards = customGuards.length ? customGuards : [auth.name] 73 | await this.authenticate(auth, guards) 74 | await next() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/Middleware/DetectUserLocale.ts: -------------------------------------------------------------------------------- 1 | import I18n from '@ioc:Adonis/Addons/I18n' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | /** 5 | * The middleware detects the user language using the "Accept-language" HTTP header 6 | * or the "lang" query string parameter. 7 | * 8 | * Feel free to change the middleware implementation to what suits your needs. Just 9 | * make sure 10 | * 11 | * - You always ensure the user selected language is supported by your app. 12 | * - Only call "switchLocale" when the detected language is valid string value and 13 | * not "null" or "undefined" 14 | */ 15 | export default class DetectUserLocale { 16 | /** 17 | * Detect user language using "Accept-language" header or 18 | * the "lang" query string parameter. 19 | * 20 | * The user language must be part of the "supportedLocales", otherwise 21 | * this method should return null. 22 | */ 23 | protected getUserLanguage(ctx: HttpContextContract) { 24 | const availableLocales = I18n.supportedLocales() 25 | return ctx.request.language(availableLocales) || ctx.request.input('lang') 26 | } 27 | 28 | /** 29 | * Handle method is called by AdonisJS automatically on every middleware 30 | * class. 31 | */ 32 | public async handle(ctx: HttpContextContract, next: () => Promise) { 33 | const language = this.getUserLanguage(ctx) 34 | 35 | /** 36 | * Switch locale when we are able to detect the user language and it 37 | * is supported by the application 38 | */ 39 | if (language) { 40 | ctx.i18n.switchLocale(language) 41 | } 42 | 43 | /** 44 | * Share i18n with view 45 | */ 46 | if ('view' in ctx) { 47 | // @ts-ignore 48 | ctx.view.share({ i18n: ctx.i18n }) 49 | } 50 | 51 | await next() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Middleware/DisabledVerification.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import Env from '@ioc:Adonis/Core/Env' 3 | 4 | export default class DisabledVerification { 5 | public async handle({ response, i18n }: HttpContextContract, next: () => Promise) { 6 | if (Env.get('DISABLE_VERIFICATION')) { 7 | response 8 | .status(400) 9 | .send({ errors: [i18n.formatMessage('middleware.disabled_verification.disabled')] }) 10 | } else { 11 | await next() 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/Middleware/SilentAuth.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | 3 | /** 4 | * Silent auth middleware can be used as a global middleware to silent check 5 | * if the user is logged-in or not. 6 | * 7 | * The request continues as usual, even when the user is not logged-in. 8 | */ 9 | export default class SilentAuthMiddleware { 10 | /** 11 | * Handle request 12 | */ 13 | public async handle({ auth }: HttpContextContract, next: () => Promise) { 14 | /** 15 | * Check if user is logged-in or not. If yes, then `ctx.auth.user` will be 16 | * set to the instance of the currently logged in user. 17 | */ 18 | await auth.check() 19 | await next() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Models/ApiToken.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { BaseModel, column, BelongsTo, belongsTo } from '@ioc:Adonis/Lucid/Orm' 3 | import User from 'App/Models/User' 4 | 5 | export default class ApiToken extends BaseModel { 6 | @column({ isPrimary: true }) 7 | public id: number 8 | 9 | @belongsTo(() => User) 10 | public user: BelongsTo 11 | 12 | @column({ serializeAs: null }) 13 | public userId: number 14 | 15 | @column() 16 | public name: string 17 | 18 | @column() 19 | public type: string 20 | 21 | @column() 22 | public token: string 23 | 24 | @column.dateTime() 25 | public createdAt: DateTime 26 | 27 | @column.dateTime() 28 | public expiresAt?: DateTime 29 | } 30 | -------------------------------------------------------------------------------- /app/Models/Header.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm' 3 | import Response from 'App/Models/Response' 4 | 5 | export default class Header extends BaseModel { 6 | @column({ isPrimary: true }) 7 | public id: number 8 | 9 | @column() 10 | public key: string 11 | 12 | @column() 13 | public value: string 14 | 15 | @column({ serializeAs: null }) 16 | public responseId: number 17 | 18 | @belongsTo(() => Response, { 19 | foreignKey: 'responseId', 20 | }) 21 | public response: BelongsTo 22 | 23 | @column.dateTime({ autoCreate: true }) 24 | public createdAt: DateTime 25 | 26 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 27 | public updatedAt: DateTime 28 | } 29 | -------------------------------------------------------------------------------- /app/Models/HttpError.ts: -------------------------------------------------------------------------------- 1 | export class HttpError { 2 | public readonly status: number 3 | public readonly message: string 4 | 5 | constructor(status: number, message: string) { 6 | this.status = status 7 | this.message = message 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/Models/Member.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm' 2 | import Project from 'App/Models/Project' 3 | import User from 'App/Models/User' 4 | 5 | export default class Member extends BaseModel { 6 | @column({ isPrimary: true }) 7 | public id: number 8 | 9 | @belongsTo(() => Project) 10 | public project: BelongsTo 11 | 12 | @column({ serializeAs: null }) 13 | public projectId: number 14 | 15 | @belongsTo(() => User) 16 | public user: BelongsTo 17 | 18 | @column({ serializeAs: null }) 19 | public userId: number 20 | 21 | @column() 22 | public verified: boolean 23 | } 24 | -------------------------------------------------------------------------------- /app/Models/Processor.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm' 3 | import Response from 'App/Models/Response' 4 | 5 | export default class Processor extends BaseModel { 6 | @column({ isPrimary: true }) 7 | public id: number 8 | 9 | @column() 10 | public enabled: boolean 11 | 12 | @column() 13 | public code: string 14 | 15 | @belongsTo(() => Response, { 16 | foreignKey: 'responseId', 17 | }) 18 | public response: BelongsTo 19 | 20 | @column({ serializeAs: 'responseId' }) 21 | public responseId: number 22 | 23 | @column.dateTime({ autoCreate: true }) 24 | public createdAt: DateTime 25 | 26 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 27 | public updatedAt: DateTime 28 | } 29 | -------------------------------------------------------------------------------- /app/Models/Project.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { 3 | BaseModel, 4 | BelongsTo, 5 | belongsTo, 6 | column, 7 | HasMany, 8 | hasMany, 9 | ManyToMany, 10 | manyToMany, 11 | } from '@ioc:Adonis/Lucid/Orm' 12 | import User from 'App/Models/User' 13 | import Route from 'App/Models/Route' 14 | import Token from 'App/Models/Token' 15 | 16 | export default class Project extends BaseModel { 17 | @column({ isPrimary: true }) 18 | public id: number 19 | 20 | @column() 21 | public name: string 22 | 23 | @column() 24 | public description: string | null 25 | 26 | @belongsTo(() => Project, { 27 | foreignKey: 'forkedProjectId', 28 | }) 29 | public forkedProject: BelongsTo 30 | 31 | @hasMany(() => Token) 32 | public tokens: HasMany 33 | 34 | @column({ serializeAs: null }) 35 | public forkedProjectId: number | null 36 | 37 | @hasMany(() => Route) 38 | public routes: HasMany 39 | 40 | @manyToMany(() => User, { 41 | pivotTable: 'members', 42 | }) 43 | public members: ManyToMany 44 | 45 | @column.dateTime({ autoCreate: true }) 46 | public createdAt: DateTime 47 | 48 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 49 | public updatedAt: DateTime 50 | } 51 | -------------------------------------------------------------------------------- /app/Models/Response.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { 3 | BaseModel, 4 | BelongsTo, 5 | belongsTo, 6 | column, 7 | HasMany, 8 | hasMany, 9 | HasOne, 10 | hasOne, 11 | } from '@ioc:Adonis/Lucid/Orm' 12 | import Route from 'App/Models/Route' 13 | import Header from 'App/Models/Header' 14 | import Processor from './Processor' 15 | 16 | export default class Response extends BaseModel { 17 | @column({ isPrimary: true }) 18 | public id: number 19 | 20 | @column() 21 | public status: number 22 | 23 | @column() 24 | public body: string 25 | 26 | @column() 27 | public name: string 28 | 29 | @column() 30 | public isFile: boolean 31 | 32 | @column() 33 | public enabled: boolean 34 | 35 | @belongsTo(() => Route) 36 | public route: BelongsTo 37 | 38 | @hasMany(() => Header) 39 | public headers: HasMany 40 | 41 | @hasOne(() => Processor) 42 | public processor: HasOne 43 | 44 | @column({ serializeAs: null }) 45 | public routeId: number 46 | 47 | @column.dateTime({ autoCreate: true }) 48 | public createdAt: DateTime 49 | 50 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 51 | public updatedAt: DateTime 52 | } 53 | -------------------------------------------------------------------------------- /app/Models/Route.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { BaseModel, BelongsTo, belongsTo, column, HasMany, hasMany } from '@ioc:Adonis/Lucid/Orm' 3 | import Project from 'App/Models/Project' 4 | import Response from 'App/Models/Response' 5 | 6 | export default class Route extends BaseModel { 7 | @column({ isPrimary: true }) 8 | public id: number 9 | 10 | @column() 11 | public name: string 12 | 13 | @column() 14 | public endpoint: string 15 | 16 | @column() 17 | public method: string 18 | 19 | @column() 20 | public enabled: boolean 21 | 22 | @column() 23 | public isFolder: boolean 24 | 25 | @column({ serializeAs: null }) 26 | public order: number 27 | 28 | @belongsTo(() => Project) 29 | public project: BelongsTo 30 | 31 | @belongsTo(() => Route, { foreignKey: 'parentFolderId' }) 32 | public parentFolder: BelongsTo 33 | 34 | @hasMany(() => Response) 35 | public responses: HasMany 36 | 37 | @column({ serializeAs: null }) 38 | public projectId: number 39 | 40 | @column() 41 | public parentFolderId: number | null 42 | 43 | @column.dateTime({ autoCreate: true }) 44 | public createdAt: DateTime 45 | 46 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 47 | public updatedAt: DateTime 48 | } 49 | -------------------------------------------------------------------------------- /app/Models/Token.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm' 3 | import Project from 'App/Models/Project' 4 | 5 | export default class Token extends BaseModel { 6 | @column({ isPrimary: true }) 7 | public id: number 8 | 9 | @column() 10 | public token: string 11 | 12 | @column() 13 | public name: string 14 | 15 | @belongsTo(() => Project, { 16 | foreignKey: 'projectId', 17 | }) 18 | public project: BelongsTo 19 | 20 | @column({ serializeAs: null }) 21 | public projectId: number 22 | 23 | @column.dateTime({ autoCreate: true }) 24 | public createdAt: DateTime 25 | 26 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 27 | public updatedAt: DateTime 28 | } 29 | -------------------------------------------------------------------------------- /app/Models/User.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import Hash from '@ioc:Adonis/Core/Hash' 3 | import { 4 | column, 5 | beforeSave, 6 | BaseModel, 7 | manyToMany, 8 | ManyToMany, 9 | hasMany, 10 | HasMany, 11 | } from '@ioc:Adonis/Lucid/Orm' 12 | import Project from 'App/Models/Project' 13 | import Member from 'App/Models/Member' 14 | import ApiToken from 'App/Models/ApiToken' 15 | 16 | export default class User extends BaseModel { 17 | @column({ isPrimary: true }) 18 | public id: number 19 | 20 | @column() 21 | public name: string 22 | 23 | @column() 24 | public email: string 25 | 26 | @column() 27 | public verified: boolean 28 | 29 | @hasMany(() => Member) 30 | public invitations: HasMany 31 | 32 | @hasMany(() => ApiToken) 33 | public tokens: HasMany 34 | 35 | @manyToMany(() => Project, { 36 | pivotTable: 'members', 37 | }) 38 | public projects: ManyToMany 39 | 40 | @column({ serializeAs: null }) 41 | public password: string 42 | 43 | @column({ serializeAs: null }) 44 | public rememberMeToken: string | null 45 | 46 | @column({ serializeAs: null }) 47 | public verifyLock: number 48 | 49 | @column.dateTime({ autoCreate: true }) 50 | public createdAt: DateTime 51 | 52 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 53 | public updatedAt: DateTime 54 | 55 | @beforeSave() 56 | public static async hashPassword(user: User) { 57 | if (user.$dirty.password) { 58 | user.password = await Hash.make(user.password) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Policies/GlobalPolicy.ts: -------------------------------------------------------------------------------- 1 | import Bouncer, { BasePolicy } from '@ioc:Adonis/Addons/Bouncer' 2 | import User from 'App/Models/User' 3 | import { I18nContract } from '@ioc:Adonis/Addons/I18n' 4 | 5 | export default class GlobalPolicy extends BasePolicy { 6 | public async isVerified(user: User, i18n: I18nContract) { 7 | return !user.verified ? Bouncer.deny(i18n.formatMessage('bouncer.global.is_verified')) : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/Policies/InvitationPolicy.ts: -------------------------------------------------------------------------------- 1 | import Bouncer, { BasePolicy } from '@ioc:Adonis/Addons/Bouncer' 2 | import User from 'App/Models/User' 3 | import Member from 'App/Models/Member' 4 | import { I18nContract } from '@ioc:Adonis/Addons/I18n' 5 | 6 | export default class InvitationPolicy extends BasePolicy { 7 | public async isInvited(user: User, invitation: Member, i18n: I18nContract) { 8 | const isUserInvited = invitation.userId === user.id 9 | return !isUserInvited 10 | ? Bouncer.deny(i18n.formatMessage('bouncer.invitation.is_invited'), 403) 11 | : true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/Policies/ProjectPolicy.ts: -------------------------------------------------------------------------------- 1 | import Bouncer, { BasePolicy } from '@ioc:Adonis/Addons/Bouncer' 2 | import User from 'App/Models/User' 3 | import Project from 'App/Models/Project' 4 | import { I18nContract } from '@ioc:Adonis/Addons/I18n' 5 | 6 | export default class ProjectPolicy extends BasePolicy { 7 | public async isMember(user: User, project: Project, i18n: I18nContract) { 8 | return ( 9 | Boolean( 10 | await project 11 | .related('members') 12 | .query() 13 | .where('user_id', user.id) 14 | .andWherePivot('verified', true) 15 | .first() 16 | ) || Bouncer.deny(i18n.formatMessage('bouncer.project.is_member'), 403) 17 | ) 18 | } 19 | 20 | public async isAlreadyMember(user: User, project: Project, i18n: I18nContract) { 21 | const member = await project.related('members').query().where('user_id', user.id).first() 22 | return member 23 | ? Bouncer.deny( 24 | member.verified 25 | ? i18n.formatMessage('bouncer.project.is_already_member.is_verified') 26 | : i18n.formatMessage('bouncer.project.is_already_member.is_not_verified'), 27 | 400 28 | ) 29 | : true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Policies/RoutePolicy.ts: -------------------------------------------------------------------------------- 1 | import Bouncer, { BasePolicy } from '@ioc:Adonis/Addons/Bouncer' 2 | import User from 'App/Models/User' 3 | import Route from 'App/Models/Route' 4 | import { I18nContract } from '@ioc:Adonis/Addons/I18n' 5 | 6 | export default class RoutePolicy extends BasePolicy { 7 | public async isNotFolder(_: User, route: Route, i18n: I18nContract) { 8 | return route.isFolder ? Bouncer.deny(i18n.formatMessage('bouncer.route.is_folder'), 400) : true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/Services/Ws.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'socket.io' 2 | import AdonisServer from '@ioc:Adonis/Core/Server' 3 | 4 | class Ws { 5 | public io: Server 6 | public boot() { 7 | if (this.io) return 8 | this.io = new Server(AdonisServer.instance, { 9 | cors: { 10 | origin: '*', 11 | }, 12 | }) 13 | } 14 | } 15 | 16 | export default new Ws() 17 | -------------------------------------------------------------------------------- /app/Validators/Header/CreateHeaderValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator' 2 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class CreateHeaderValidator { 5 | constructor(protected ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | key: schema.string({ trim: true }, [ 9 | rules.maxLength(255), 10 | rules.unique({ 11 | table: 'headers', 12 | column: 'key', 13 | where: { response_id: this.ctx.params.id }, 14 | }), 15 | ]), 16 | value: schema.string({ trim: true }, [rules.maxLength(255)]), 17 | }) 18 | 19 | public messages: CustomMessages = this.ctx.i18n.validatorMessages('validator.header.create') 20 | } 21 | -------------------------------------------------------------------------------- /app/Validators/Header/UpdateHeaderValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator' 2 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class UpdateHeaderValidator { 5 | constructor(protected ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | key: schema.string({ trim: true }, [ 9 | rules.maxLength(255), 10 | rules.unique({ 11 | table: 'headers', 12 | column: 'key', 13 | whereNot: { id: this.ctx.params.id }, 14 | where: { response_id: this.ctx.params.responseId }, 15 | }), 16 | ]), 17 | value: schema.string({ trim: true }, [rules.maxLength(255)]), 18 | }) 19 | 20 | public messages: CustomMessages = this.ctx.i18n.validatorMessages('validator.header.update') 21 | } 22 | -------------------------------------------------------------------------------- /app/Validators/Processor/CreateProcessorValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, CustomMessages } from '@ioc:Adonis/Core/Validator' 2 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class CreateProcessorValidator { 5 | constructor(protected ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | enabled: schema.boolean(), 9 | code: schema.string(), 10 | }) 11 | 12 | public messages: CustomMessages = this.ctx.i18n.validatorMessages('validator.processor.create') 13 | } 14 | -------------------------------------------------------------------------------- /app/Validators/Project/CreateProjectValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class CreateProjectValidator { 5 | constructor(private ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | name: schema.string({}, [ 9 | rules.minLength(3), 10 | rules.maxLength(200), 11 | rules.unique({ table: 'projects', column: 'name' }), 12 | ]), 13 | description: schema.string.nullable({}, [rules.minLength(3), rules.maxLength(2000)]), 14 | }) 15 | 16 | public messages = this.ctx.i18n.validatorMessages('validator.project.create') 17 | } 18 | -------------------------------------------------------------------------------- /app/Validators/Project/EditProjectValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class EditProjectValidator { 5 | constructor(private ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | name: schema.string({}, [ 9 | rules.minLength(3), 10 | rules.maxLength(200), 11 | rules.unique({ table: 'projects', column: 'name', whereNot: { id: this.ctx.params.id } }), 12 | ]), 13 | description: schema.string.nullable({}, [rules.minLength(3), rules.maxLength(2000)]), 14 | }) 15 | public messages = this.ctx.i18n.validatorMessages('validator.project.edit') 16 | } 17 | -------------------------------------------------------------------------------- /app/Validators/Project/GetProjectsValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema } from '@ioc:Adonis/Core/Validator' 2 | 3 | export default class GetProjectsValidator { 4 | public schema = schema.create({ 5 | page: schema.number.optional(), 6 | perPage: schema.number.optional(), 7 | sortBy: schema.enum.optional(['created_at', 'updated_at', 'name']), 8 | onlyBranches: schema.boolean.optional(), 9 | direction: schema.enum.optional(['desc', 'asc']), 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /app/Validators/Response/CreateResponseValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class CreateResponseValidator { 5 | #isFile = Boolean(this.ctx.request.input('isFile', false)) 6 | 7 | constructor(private ctx: HttpContextContract) {} 8 | 9 | public schema = schema.create({ 10 | enabled: schema.boolean(), 11 | status: schema.number([rules.range(100, 599)]), 12 | body: this.#isFile 13 | ? schema.file({ size: '8MB' }) 14 | : schema.string({}, [rules.maxLength(500000)]), 15 | name: schema.string({}, [ 16 | rules.unique({ table: 'responses', column: 'name', where: { route_id: this.ctx.params.id } }), 17 | rules.maxLength(200), 18 | ]), 19 | }) 20 | 21 | public messages = this.ctx.i18n.validatorMessages('validator.response.create') 22 | } 23 | -------------------------------------------------------------------------------- /app/Validators/Response/DeleteMultipleResponseValidator.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import { rules, schema } from '@ioc:Adonis/Core/Validator' 3 | 4 | export default class DeleteMultipleResponseValidator { 5 | constructor(private ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | ids: schema.array([rules.minLength(1)]).members( 9 | schema.number([ 10 | rules.exists({ 11 | column: 'id', 12 | table: 'responses', 13 | where: { route_id: this.ctx.params.id }, 14 | }), 15 | ]) 16 | ), 17 | }) 18 | 19 | public messages = this.ctx.i18n.validatorMessages('validator.response.deleteMany') 20 | } 21 | -------------------------------------------------------------------------------- /app/Validators/Response/DuplicateResponseValidator.ts: -------------------------------------------------------------------------------- 1 | import { rules, schema } from '@ioc:Adonis/Core/Validator' 2 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class DuplicateResponseValidator { 5 | constructor(private ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | name: schema.string({}, [ 9 | rules.unique({ 10 | table: 'responses', 11 | column: 'name', 12 | where: { route_id: this.ctx.params.routeId }, 13 | }), 14 | rules.maxLength(200), 15 | ]), 16 | }) 17 | 18 | public messages = this.ctx.i18n.validatorMessages('validator.response.duplicate') 19 | } 20 | -------------------------------------------------------------------------------- /app/Validators/Response/EditResponseValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class EditResponseValidator { 5 | #isFile = Boolean(this.ctx.request.input('isFile', false)) 6 | 7 | constructor(private ctx: HttpContextContract) {} 8 | 9 | public schema = schema.create({ 10 | enabled: schema.boolean(), 11 | status: schema.number([rules.range(100, 599)]), 12 | body: this.#isFile 13 | ? schema.file.optional({ size: '8MB' }) 14 | : schema.string({}, [rules.maxLength(500000)]), 15 | name: schema.string({}, [ 16 | rules.unique({ 17 | table: 'responses', 18 | column: 'name', 19 | where: { route_id: this.ctx.params.routeId }, 20 | whereNot: { id: this.ctx.params.id }, 21 | }), 22 | rules.maxLength(200), 23 | ]), 24 | }) 25 | 26 | public messages = this.ctx.i18n.validatorMessages('validator.response.edit') 27 | } 28 | -------------------------------------------------------------------------------- /app/Validators/Route/CreateRouteValidator.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import { rules, schema } from '@ioc:Adonis/Core/Validator' 3 | 4 | export default class CreateRouteValidator { 5 | private isFolder = Boolean(this.ctx.request.input('isFolder', false)) 6 | 7 | constructor(private ctx: HttpContextContract) {} 8 | 9 | public schema = schema.create( 10 | this.isFolder 11 | ? { 12 | name: schema.string({}, [rules.minLength(3), rules.maxLength(200)]), 13 | } 14 | : { 15 | name: schema.string({}, [rules.minLength(3), rules.maxLength(200)]), 16 | method: schema.enum( 17 | ['get', 'post', 'put', 'delete', 'patch'], 18 | [ 19 | rules.unique({ 20 | table: 'routes', 21 | column: 'method', 22 | where: { 23 | project_id: this.ctx.params.id ?? 0, 24 | endpoint: this.ctx.request.body().endpoint ?? '', 25 | }, 26 | }), 27 | ] 28 | ), 29 | endpoint: schema.string({}, [ 30 | rules.regex(new RegExp('^/([a-zA-Z0-9{}_-]+)*(/[a-zA-Z0-9{}_-]+)*$')), 31 | rules.maxLength(2000), 32 | ]), 33 | parentFolderId: schema.number.nullable([ 34 | rules.exists({ table: 'routes', column: 'id', where: { is_folder: true } }), 35 | ]), 36 | enabled: schema.boolean(), 37 | } 38 | ) 39 | 40 | public messages = this.ctx.i18n.validatorMessages( 41 | this.isFolder ? 'validator.route.create_folder' : 'validator.route.create' 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /app/Validators/Route/EditFolderValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class EditFolderValidator { 5 | constructor(private ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | name: schema.string({}, [rules.minLength(3), rules.maxLength(200)]), 9 | }) 10 | 11 | public messages = this.ctx.i18n.validatorMessages('validator.route.edit_folder') 12 | } 13 | -------------------------------------------------------------------------------- /app/Validators/Route/EditRouteValidator.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 2 | import { rules, schema } from '@ioc:Adonis/Core/Validator' 3 | 4 | export default class EditRouteValidator { 5 | constructor(private ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | name: schema.string({}, [rules.minLength(3), rules.maxLength(200)]), 9 | method: schema.enum( 10 | ['get', 'post', 'put', 'delete', 'patch'], 11 | [ 12 | rules.unique({ 13 | table: 'routes', 14 | column: 'method', 15 | where: { 16 | project_id: this.ctx.params.projectId ?? 0, 17 | endpoint: this.ctx.request.body().endpoint ?? '', 18 | }, 19 | whereNot: { 20 | id: this.ctx.params.id, 21 | }, 22 | }), 23 | ] 24 | ), 25 | endpoint: schema.string({}, [ 26 | rules.regex(new RegExp('^/([a-zA-Z0-9{}_-]+)*(/[a-zA-Z0-9{}_-]+)*$')), 27 | rules.maxLength(2000), 28 | ]), 29 | enabled: schema.boolean(), 30 | }) 31 | 32 | public messages = this.ctx.i18n.validatorMessages('validator.route.edit') 33 | } 34 | -------------------------------------------------------------------------------- /app/Validators/Route/MoveAndSortValidator.ts: -------------------------------------------------------------------------------- 1 | import { rules, schema } from '@ioc:Adonis/Core/Validator' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class MoveAndSortValidator { 5 | constructor(private ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | what: schema.number([ 9 | rules.exists({ 10 | table: 'routes', 11 | column: 'id', 12 | where: { project_id: this.ctx.request.params().id }, 13 | }), 14 | ]), 15 | into: schema.number.nullable([ 16 | rules.exists({ 17 | table: 'routes', 18 | column: 'id', 19 | where: { is_folder: true, project_id: this.ctx.request.params().id }, 20 | }), 21 | ]), 22 | before: schema.number.nullable([ 23 | rules.exists({ 24 | table: 'routes', 25 | column: 'id', 26 | where: { 27 | parent_folder_id: this.ctx.request.body().into, 28 | project_id: this.ctx.request.params().id, 29 | }, 30 | }), 31 | ]), 32 | }) 33 | 34 | public messages = this.ctx.i18n.validatorMessages('validator.route.moveandsort') 35 | } 36 | -------------------------------------------------------------------------------- /app/Validators/Swagger/ImportSwaggerValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator' 2 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class ImportSwaggerValidator { 5 | constructor(protected ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | reset: schema.boolean(), 9 | basePath: schema.string.optional({}, [ 10 | rules.regex(new RegExp('^/([a-zA-Z0-9{}-]+)*(/[a-zA-Z0-9{}-]+)*$')), 11 | rules.maxLength(2000), 12 | ]), 13 | swagger: schema.string({}), 14 | }) 15 | 16 | public messages: CustomMessages = this.ctx.i18n.validatorMessages('validator.swagger.parse') 17 | } 18 | -------------------------------------------------------------------------------- /app/Validators/Token/CreateTokenValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator' 2 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class CreateTokenValidator { 5 | constructor(protected ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | name: schema.string({ trim: true }, [rules.minLength(3), rules.maxLength(30)]), 9 | }) 10 | 11 | public messages: CustomMessages = this.ctx.i18n.validatorMessages('validator.token.create') 12 | } 13 | -------------------------------------------------------------------------------- /app/Validators/User/EditUserEmailValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class EditUserEmailValidator { 5 | constructor(private ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | email: schema.string({}, [rules.email(), rules.unique({ table: 'users', column: 'email' })]), 9 | }) 10 | 11 | public messages = this.ctx.i18n.validatorMessages('validator.user.email') 12 | } 13 | -------------------------------------------------------------------------------- /app/Validators/User/EditUserValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class EditUserValidator { 5 | constructor(private ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | name: schema.string({}, [rules.minLength(3)]), 9 | }) 10 | 11 | public messages = this.ctx.i18n.validatorMessages('validator.user.edit') 12 | } 13 | -------------------------------------------------------------------------------- /app/Validators/User/LoginValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class LoginValidator { 5 | constructor(private ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | email: schema.string({}, [rules.email()]), 9 | password: schema.string(), 10 | }) 11 | 12 | public messages = this.ctx.i18n.validatorMessages('validator.user.login') 13 | } 14 | -------------------------------------------------------------------------------- /app/Validators/User/RegisterValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class RegisterValidator { 5 | constructor(private ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | email: schema.string({}, [rules.email(), rules.unique({ table: 'users', column: 'email' })]), 9 | name: schema.string({}, [rules.minLength(3)]), 10 | password: schema.string({}, [rules.minLength(8)]), 11 | }) 12 | 13 | public messages = this.ctx.i18n.validatorMessages('validator.user.register') 14 | } 15 | -------------------------------------------------------------------------------- /app/Validators/User/ResendEmailValidator.ts: -------------------------------------------------------------------------------- 1 | import { schema, rules } from '@ioc:Adonis/Core/Validator' 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 3 | 4 | export default class ResendEmailValidator { 5 | constructor(private ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | email: schema.string({}, [rules.email(), rules.exists({ table: 'users', column: 'email' })]), 9 | }) 10 | 11 | public messages = this.ctx.i18n.validatorMessages('validator.user.resend_email') 12 | } 13 | -------------------------------------------------------------------------------- /commands/index.ts: -------------------------------------------------------------------------------- 1 | import { listDirectoryFiles } from '@adonisjs/core/build/standalone' 2 | import Application from '@ioc:Adonis/Core/Application' 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Exporting an array of commands 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Instead of manually exporting each file from this directory, we use the 10 | | helper `listDirectoryFiles` to recursively collect and export an array 11 | | of filenames. 12 | | 13 | | Couple of things to note: 14 | | 15 | | 1. The file path must be relative from the project root and not this directory. 16 | | 2. We must ignore this file to avoid getting into an infinite loop 17 | | 18 | */ 19 | export default listDirectoryFiles(__dirname, Application.appRoot, ['./commands/index']) 20 | -------------------------------------------------------------------------------- /config/app.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JfefZ 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import proxyAddr from 'proxy-addr' 9 | import Env from '@ioc:Adonis/Core/Env' 10 | import type { ServerConfig } from '@ioc:Adonis/Core/Server' 11 | import type { LoggerConfig } from '@ioc:Adonis/Core/Logger' 12 | import type { ProfilerConfig } from '@ioc:Adonis/Core/Profiler' 13 | import type { ValidatorConfig } from '@ioc:Adonis/Core/Validator' 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | Application secret key 18 | |-------------------------------------------------------------------------- 19 | | 20 | | The secret to encrypt and sign different values in your application. 21 | | Make sure to keep the `APP_KEY` as an environment variable and secure. 22 | | 23 | | Note: Changing the application key for an existing app will make all 24 | | the cookies invalid and also the existing encrypted data will not 25 | | be decrypted. 26 | | 27 | */ 28 | export const appKey: string = Env.get('APP_KEY') 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Http server configuration 33 | |-------------------------------------------------------------------------- 34 | | 35 | | The configuration for the HTTP(s) server. Make sure to go through all 36 | | the config properties to make keep server secure. 37 | | 38 | */ 39 | export const http: ServerConfig = { 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Allow method spoofing 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Method spoofing enables defining custom HTTP methods using a query string 46 | | `_method`. This is usually required when you are making traditional 47 | | form requests and wants to use HTTP verbs like `PUT`, `DELETE` and 48 | | so on. 49 | | 50 | */ 51 | allowMethodSpoofing: false, 52 | 53 | /* 54 | |-------------------------------------------------------------------------- 55 | | Subdomain offset 56 | |-------------------------------------------------------------------------- 57 | */ 58 | subdomainOffset: 2, 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Request Ids 63 | |-------------------------------------------------------------------------- 64 | | 65 | | Setting this value to `true` will generate a unique request id for each 66 | | HTTP request and set it as `x-request-id` header. 67 | | 68 | */ 69 | generateRequestId: false, 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Trusting proxy servers 74 | |-------------------------------------------------------------------------- 75 | | 76 | | Define the proxy servers that AdonisJs must trust for reading `X-Forwarded` 77 | | headers. 78 | | 79 | */ 80 | trustProxy: proxyAddr.compile('loopback'), 81 | 82 | /* 83 | |-------------------------------------------------------------------------- 84 | | Generating Etag 85 | |-------------------------------------------------------------------------- 86 | | 87 | | Whether or not to generate an etag for every response. 88 | | 89 | */ 90 | etag: false, 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | JSONP Callback 95 | |-------------------------------------------------------------------------- 96 | */ 97 | jsonpCallbackName: 'callback', 98 | 99 | /* 100 | |-------------------------------------------------------------------------- 101 | | Cookie settings 102 | |-------------------------------------------------------------------------- 103 | */ 104 | cookie: { 105 | domain: '', 106 | path: '/', 107 | maxAge: '2h', 108 | httpOnly: true, 109 | secure: false, 110 | sameSite: false, 111 | }, 112 | 113 | /* 114 | |-------------------------------------------------------------------------- 115 | | Force Content Negotiation 116 | |-------------------------------------------------------------------------- 117 | | 118 | | The internals of the framework relies on the content negotiation to 119 | | detect the best possible response type for a given HTTP request. 120 | | 121 | | However, it is a very common these days that API servers always wants to 122 | | make response in JSON regardless of the existence of the `Accept` header. 123 | | 124 | | By setting `forceContentNegotiationTo = 'application/json'`, you negotiate 125 | | with the server in advance to always return JSON without relying on the 126 | | client to set the header explicitly. 127 | | 128 | */ 129 | forceContentNegotiationTo: 'application/json', 130 | } 131 | 132 | /* 133 | |-------------------------------------------------------------------------- 134 | | Logger 135 | |-------------------------------------------------------------------------- 136 | */ 137 | export const logger: LoggerConfig = { 138 | /* 139 | |-------------------------------------------------------------------------- 140 | | Application name 141 | |-------------------------------------------------------------------------- 142 | | 143 | | The name of the application you want to add to the log. It is recommended 144 | | to always have app name in every log line. 145 | | 146 | | The `APP_NAME` environment variable is automatically set by AdonisJS by 147 | | reading the `name` property from the `package.json` file. 148 | | 149 | */ 150 | name: Env.get('APP_NAME'), 151 | 152 | /* 153 | |-------------------------------------------------------------------------- 154 | | Toggle logger 155 | |-------------------------------------------------------------------------- 156 | | 157 | | Enable or disable logger application wide 158 | | 159 | */ 160 | enabled: true, 161 | 162 | /* 163 | |-------------------------------------------------------------------------- 164 | | Logging level 165 | |-------------------------------------------------------------------------- 166 | | 167 | | The level from which you want the logger to flush logs. It is recommended 168 | | to make use of the environment variable, so that you can define log levels 169 | | at deployment level and not code level. 170 | | 171 | */ 172 | level: Env.get('LOG_LEVEL', 'info'), 173 | 174 | /* 175 | |-------------------------------------------------------------------------- 176 | | Pretty print 177 | |-------------------------------------------------------------------------- 178 | | 179 | | It is highly advised NOT to use `prettyPrint` in production, since it 180 | | can have huge impact on performance. 181 | | 182 | */ 183 | prettyPrint: Env.get('NODE_ENV') === 'development', 184 | } 185 | 186 | /* 187 | |-------------------------------------------------------------------------- 188 | | Profiler 189 | |-------------------------------------------------------------------------- 190 | */ 191 | export const profiler: ProfilerConfig = { 192 | /* 193 | |-------------------------------------------------------------------------- 194 | | Toggle profiler 195 | |-------------------------------------------------------------------------- 196 | | 197 | | Enable or disable profiler 198 | | 199 | */ 200 | enabled: true, 201 | 202 | /* 203 | |-------------------------------------------------------------------------- 204 | | Blacklist actions/row labels 205 | |-------------------------------------------------------------------------- 206 | | 207 | | Define an array of actions or row labels that you want to disable from 208 | | getting profiled. 209 | | 210 | */ 211 | blacklist: [], 212 | 213 | /* 214 | |-------------------------------------------------------------------------- 215 | | Whitelist actions/row labels 216 | |-------------------------------------------------------------------------- 217 | | 218 | | Define an array of actions or row labels that you want to whitelist for 219 | | the profiler. When whitelist is defined, then `blacklist` is ignored. 220 | | 221 | */ 222 | whitelist: [], 223 | } 224 | 225 | /* 226 | |-------------------------------------------------------------------------- 227 | | Validator 228 | |-------------------------------------------------------------------------- 229 | | 230 | | Configure the global configuration for the validator. Here's the reference 231 | | to the default config https://git.io/JT0WE 232 | | 233 | */ 234 | export const validator: ValidatorConfig = {} 235 | -------------------------------------------------------------------------------- /config/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JY0mp 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import type { AuthConfig } from '@ioc:Adonis/Addons/Auth' 9 | 10 | /* 11 | |-------------------------------------------------------------------------- 12 | | Authentication Mapping 13 | |-------------------------------------------------------------------------- 14 | | 15 | | List of available authentication mapping. You must first define them 16 | | inside the `contracts/auth.ts` file before mentioning them here. 17 | | 18 | */ 19 | const authConfig: AuthConfig = { 20 | guard: 'api', 21 | guards: { 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | OAT Guard 25 | |-------------------------------------------------------------------------- 26 | | 27 | | OAT (Opaque access tokens) guard uses database backed tokens to authenticate 28 | | HTTP request. This guard DOES NOT rely on sessions or cookies and uses 29 | | Authorization header value for authentication. 30 | | 31 | | Use this guard to authenticate mobile apps or web clients that cannot rely 32 | | on cookies/sessions. 33 | | 34 | */ 35 | api: { 36 | driver: 'oat', 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Tokens provider 41 | |-------------------------------------------------------------------------- 42 | | 43 | | Uses SQL database for managing tokens. Use the "database" driver, when 44 | | tokens are the secondary mode of authentication. 45 | | For example: The Github personal tokens 46 | | 47 | | The foreignKey column is used to make the relationship between the user 48 | | and the token. You are free to use any column name here. 49 | | 50 | */ 51 | tokenProvider: { 52 | type: 'api', 53 | driver: 'database', 54 | table: 'api_tokens', 55 | foreignKey: 'user_id', 56 | }, 57 | 58 | provider: { 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Driver 62 | |-------------------------------------------------------------------------- 63 | | 64 | | Name of the driver 65 | | 66 | */ 67 | driver: 'lucid', 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | Identifier key 72 | |-------------------------------------------------------------------------- 73 | | 74 | | The identifier key is the unique key on the model. In most cases specifying 75 | | the primary key is the right choice. 76 | | 77 | */ 78 | identifierKey: 'id', 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Uids 83 | |-------------------------------------------------------------------------- 84 | | 85 | | Uids are used to search a user against one of the mentioned columns. During 86 | | login, the auth module will search the user mentioned value against one 87 | | of the mentioned columns to find their user record. 88 | | 89 | */ 90 | uids: ['email'], 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Model 95 | |-------------------------------------------------------------------------- 96 | | 97 | | The model to use for fetching or finding users. The model is imported 98 | | lazily since the config files are read way earlier in the lifecycle 99 | | of booting the app and the models may not be in a usable state at 100 | | that time. 101 | | 102 | */ 103 | model: () => import('App/Models/User'), 104 | }, 105 | }, 106 | }, 107 | } 108 | 109 | export default authConfig 110 | -------------------------------------------------------------------------------- /config/bodyparser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/Jfefn 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import type { BodyParserConfig } from '@ioc:Adonis/Core/BodyParser' 9 | 10 | const bodyParserConfig: BodyParserConfig = { 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | White listed methods 14 | |-------------------------------------------------------------------------- 15 | | 16 | | HTTP methods for which body parsing must be performed. It is a good practice 17 | | to avoid body parsing for `GET` requests. 18 | | 19 | */ 20 | whitelistedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'], 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | JSON parser settings 25 | |-------------------------------------------------------------------------- 26 | | 27 | | The settings for the JSON parser. The types defines the request content 28 | | types which gets processed by the JSON parser. 29 | | 30 | */ 31 | json: { 32 | encoding: 'utf-8', 33 | limit: '1mb', 34 | strict: true, 35 | types: [ 36 | 'application/json', 37 | 'application/json-patch+json', 38 | 'application/vnd.api+json', 39 | 'application/csp-report', 40 | ], 41 | }, 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Form parser settings 46 | |-------------------------------------------------------------------------- 47 | | 48 | | The settings for the `application/x-www-form-urlencoded` parser. The types 49 | | defines the request content types which gets processed by the form parser. 50 | | 51 | */ 52 | form: { 53 | encoding: 'utf-8', 54 | limit: '1mb', 55 | queryString: {}, 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Convert empty strings to null 60 | |-------------------------------------------------------------------------- 61 | | 62 | | Convert empty form fields to null. HTML forms results in field string 63 | | value when the field is left blank. This option normalizes all the blank 64 | | field values to "null" 65 | | 66 | */ 67 | convertEmptyStringsToNull: true, 68 | 69 | types: ['application/x-www-form-urlencoded'], 70 | }, 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Raw body parser settings 75 | |-------------------------------------------------------------------------- 76 | | 77 | | Raw body just reads the request body stream as a plain text, which you 78 | | can process by hand. This must be used when request body type is not 79 | | supported by the body parser. 80 | | 81 | */ 82 | raw: { 83 | encoding: 'utf-8', 84 | limit: '1mb', 85 | queryString: {}, 86 | types: ['text/*'], 87 | }, 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Multipart parser settings 92 | |-------------------------------------------------------------------------- 93 | | 94 | | The settings for the `multipart/form-data` parser. The types defines the 95 | | request content types which gets processed by the form parser. 96 | | 97 | */ 98 | multipart: { 99 | /* 100 | |-------------------------------------------------------------------------- 101 | | Auto process 102 | |-------------------------------------------------------------------------- 103 | | 104 | | The auto process option will process uploaded files and writes them to 105 | | the `tmp` folder. You can turn it off and then manually use the stream 106 | | to pipe stream to a different destination. 107 | | 108 | | It is recommended to keep `autoProcess=true`. Unless you are processing bigger 109 | | file sizes. 110 | | 111 | */ 112 | autoProcess: true, 113 | 114 | /* 115 | |-------------------------------------------------------------------------- 116 | | Files to be processed manually 117 | |-------------------------------------------------------------------------- 118 | | 119 | | You can turn off `autoProcess` for certain routes by defining 120 | | routes inside the following array. 121 | | 122 | | NOTE: Make sure the route pattern starts with a leading slash. 123 | | 124 | | Correct 125 | | ```js 126 | | /projects/:id/file 127 | | ``` 128 | | 129 | | Incorrect 130 | | ```js 131 | | projects/:id/file 132 | | ``` 133 | */ 134 | processManually: [], 135 | 136 | /* 137 | |-------------------------------------------------------------------------- 138 | | Temporary file name 139 | |-------------------------------------------------------------------------- 140 | | 141 | | When auto processing is on. We will use this method to compute the temporary 142 | | file name. AdonisJs will compute a unique `tmpPath` for you automatically, 143 | | However, you can also define your own custom method. 144 | | 145 | */ 146 | // tmpFileName () { 147 | // }, 148 | 149 | /* 150 | |-------------------------------------------------------------------------- 151 | | Encoding 152 | |-------------------------------------------------------------------------- 153 | | 154 | | Request body encoding 155 | | 156 | */ 157 | encoding: 'utf-8', 158 | 159 | /* 160 | |-------------------------------------------------------------------------- 161 | | Convert empty strings to null 162 | |-------------------------------------------------------------------------- 163 | | 164 | | Convert empty form fields to null. HTML forms results in field string 165 | | value when the field is left blank. This option normalizes all the blank 166 | | field values to "null" 167 | | 168 | */ 169 | convertEmptyStringsToNull: true, 170 | 171 | /* 172 | |-------------------------------------------------------------------------- 173 | | Max Fields 174 | |-------------------------------------------------------------------------- 175 | | 176 | | The maximum number of fields allowed in the request body. The field includes 177 | | text inputs and files both. 178 | | 179 | */ 180 | maxFields: 1000, 181 | 182 | /* 183 | |-------------------------------------------------------------------------- 184 | | Request body limit 185 | |-------------------------------------------------------------------------- 186 | | 187 | | The total limit to the multipart body. This includes all request files 188 | | and fields data. 189 | | 190 | */ 191 | limit: '20mb', 192 | 193 | /* 194 | |-------------------------------------------------------------------------- 195 | | Types 196 | |-------------------------------------------------------------------------- 197 | | 198 | | The types that will be considered and parsed as multipart body. 199 | | 200 | */ 201 | types: ['multipart/form-data'], 202 | }, 203 | } 204 | 205 | export default bodyParserConfig 206 | -------------------------------------------------------------------------------- /config/cors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JfefC 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import type { CorsConfig } from '@ioc:Adonis/Core/Cors' 9 | 10 | const corsConfig: CorsConfig = { 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Enabled 14 | |-------------------------------------------------------------------------- 15 | | 16 | | A boolean to enable or disable CORS integration from your AdonisJs 17 | | application. 18 | | 19 | | Setting the value to `true` will enable the CORS for all HTTP request. However, 20 | | you can define a function to enable/disable it on per request basis as well. 21 | | 22 | */ 23 | enabled: true, 24 | 25 | // You can also use a function that return true or false. 26 | // enabled: (request) => request.url().startsWith('/api') 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Origin 31 | |-------------------------------------------------------------------------- 32 | | 33 | | Set a list of origins to be allowed for `Access-Control-Allow-Origin`. 34 | | The value can be one of the following: 35 | | 36 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin 37 | | 38 | | Boolean (true) - Allow current request origin. 39 | | Boolean (false) - Disallow all. 40 | | String - Comma separated list of allowed origins. 41 | | Array - An array of allowed origins. 42 | | String (*) - A wildcard (*) to allow all request origins. 43 | | Function - Receives the current origin string and should return 44 | | one of the above values. 45 | | 46 | */ 47 | origin: '*', 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Methods 52 | |-------------------------------------------------------------------------- 53 | | 54 | | An array of allowed HTTP methods for CORS. The `Access-Control-Request-Method` 55 | | is checked against the following list. 56 | | 57 | | Following is the list of default methods. Feel free to add more. 58 | */ 59 | methods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'], 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Headers 64 | |-------------------------------------------------------------------------- 65 | | 66 | | List of headers to be allowed for `Access-Control-Allow-Headers` header. 67 | | The value can be one of the following: 68 | | 69 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers 70 | | 71 | | Boolean(true) - Allow all headers mentioned in `Access-Control-Request-Headers`. 72 | | Boolean(false) - Disallow all headers. 73 | | String - Comma separated list of allowed headers. 74 | | Array - An array of allowed headers. 75 | | Function - Receives the current header and should return one of the above values. 76 | | 77 | */ 78 | headers: true, 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Expose Headers 83 | |-------------------------------------------------------------------------- 84 | | 85 | | A list of headers to be exposed by setting `Access-Control-Expose-Headers`. 86 | | header. By default following 6 simple response headers are exposed. 87 | | 88 | | Cache-Control 89 | | Content-Language 90 | | Content-Type 91 | | Expires 92 | | Last-Modified 93 | | Pragma 94 | | 95 | | In order to add more headers, simply define them inside the following array. 96 | | 97 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers 98 | | 99 | */ 100 | exposeHeaders: [ 101 | 'cache-control', 102 | 'content-language', 103 | 'content-type', 104 | 'expires', 105 | 'last-modified', 106 | 'pragma', 107 | ], 108 | 109 | /* 110 | |-------------------------------------------------------------------------- 111 | | Credentials 112 | |-------------------------------------------------------------------------- 113 | | 114 | | Toggle `Access-Control-Allow-Credentials` header. If value is set to `true`, 115 | | then header will be set, otherwise not. 116 | | 117 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials 118 | | 119 | */ 120 | credentials: true, 121 | 122 | /* 123 | |-------------------------------------------------------------------------- 124 | | MaxAge 125 | |-------------------------------------------------------------------------- 126 | | 127 | | Define `Access-Control-Max-Age` header in seconds. 128 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age 129 | | 130 | */ 131 | maxAge: 90, 132 | } 133 | 134 | export default corsConfig 135 | -------------------------------------------------------------------------------- /config/database.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JesV9 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import Env from '@ioc:Adonis/Core/Env' 9 | import { DatabaseConfig } from '@ioc:Adonis/Lucid/Database' 10 | 11 | const databaseConfig: DatabaseConfig = { 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Connection 15 | |-------------------------------------------------------------------------- 16 | | 17 | | The primary connection for making database queries across the application 18 | | You can use any key from the `connections` object defined in this same 19 | | file. 20 | | 21 | */ 22 | connection: Env.get('DB_CONNECTION'), 23 | 24 | connections: { 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | PostgreSQL config 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Configuration for PostgreSQL database. Make sure to install the driver 31 | | from npm when using this connection 32 | | 33 | | npm i pg 34 | | 35 | */ 36 | pg: { 37 | client: 'pg', 38 | connection: { 39 | host: Env.get('PG_HOST'), 40 | port: Env.get('PG_PORT'), 41 | user: Env.get('PG_USER'), 42 | password: Env.get('PG_PASSWORD', ''), 43 | database: Env.get('PG_DB_NAME'), 44 | }, 45 | migrations: { 46 | naturalSort: true, 47 | }, 48 | healthCheck: false, 49 | debug: false, 50 | }, 51 | 52 | } 53 | } 54 | 55 | export default databaseConfig 56 | -------------------------------------------------------------------------------- /config/drive.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JBt3o 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import Env from '@ioc:Adonis/Core/Env' 9 | import { driveConfig } from '@adonisjs/core/build/config' 10 | import Application from '@ioc:Adonis/Core/Application' 11 | 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Drive Config 15 | |-------------------------------------------------------------------------- 16 | | 17 | | The `DriveConfig` relies on the `DisksList` interface which is 18 | | defined inside the `contracts` directory. 19 | | 20 | */ 21 | export default driveConfig({ 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Default disk 25 | |-------------------------------------------------------------------------- 26 | | 27 | | The default disk to use for managing file uploads. The value is driven by 28 | | the `DRIVE_DISK` environment variable. 29 | | 30 | */ 31 | disk: Env.get('DRIVE_DISK'), 32 | 33 | disks: { 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Local 37 | |-------------------------------------------------------------------------- 38 | | 39 | | Uses the local file system to manage files. Make sure to turn off serving 40 | | files when not using this disk. 41 | | 42 | */ 43 | local: { 44 | driver: 'local', 45 | visibility: 'private', 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Storage root - Local driver only 50 | |-------------------------------------------------------------------------- 51 | | 52 | | Define an absolute path to the storage directory from where to read the 53 | | files. 54 | | 55 | */ 56 | root: Application.tmpPath('uploads'), 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | Serve files - Local driver only 61 | |-------------------------------------------------------------------------- 62 | | 63 | | When this is set to true, AdonisJS will configure a files server to serve 64 | | files from the disk root. This is done to mimic the behavior of cloud 65 | | storage services that has inbuilt capabilities to serve files. 66 | | 67 | */ 68 | serveFiles: false, 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Base path - Local driver only 73 | |-------------------------------------------------------------------------- 74 | | 75 | | Base path is always required when "serveFiles = true". Also make sure 76 | | the `basePath` is unique across all the disks using "local" driver and 77 | | you are not registering routes with this prefix. 78 | | 79 | */ 80 | basePath: '/uploads', 81 | }, 82 | 83 | /* 84 | |-------------------------------------------------------------------------- 85 | | S3 Driver 86 | |-------------------------------------------------------------------------- 87 | | 88 | | Uses the S3 cloud storage to manage files. Make sure to install the s3 89 | | drive separately when using it. 90 | | 91 | |************************************************************************** 92 | | npm i @adonisjs/drive-s3 93 | |************************************************************************** 94 | | 95 | */ 96 | s3: { 97 | driver: 's3', 98 | visibility: 'private', 99 | key: Env.get('S3_KEY'), 100 | secret: Env.get('S3_SECRET'), 101 | region: Env.get('S3_REGION'), 102 | bucket: Env.get('S3_BUCKET'), 103 | endpoint: Env.get('S3_ENDPOINT'), 104 | forcePathStyle: true, 105 | }, 106 | 107 | /* 108 | |-------------------------------------------------------------------------- 109 | | GCS Driver 110 | |-------------------------------------------------------------------------- 111 | | 112 | | Uses the Google cloud storage to manage files. Make sure to install the GCS 113 | | drive separately when using it. 114 | | 115 | |************************************************************************** 116 | | npm i @adonisjs/drive-gcs 117 | |************************************************************************** 118 | | 119 | */ 120 | // gcs: { 121 | // driver: 'gcs', 122 | // visibility: 'public', 123 | // keyFilename: Env.get('GCS_KEY_FILENAME'), 124 | // bucket: Env.get('GCS_BUCKET'), 125 | 126 | /* 127 | |-------------------------------------------------------------------------- 128 | | Uniform ACL - Google cloud storage only 129 | |-------------------------------------------------------------------------- 130 | | 131 | | When using the Uniform ACL on the bucket, the "visibility" option is 132 | | ignored. Since, the files ACL is managed by the google bucket policies 133 | | directly. 134 | | 135 | |************************************************************************** 136 | | Learn more: https://cloud.google.com/storage/docs/uniform-bucket-level-access 137 | |************************************************************************** 138 | | 139 | | The following option just informs drive whether your bucket is using uniform 140 | | ACL or not. The actual setting needs to be toggled within the Google cloud 141 | | console. 142 | | 143 | */ 144 | // usingUniformAcl: false, 145 | // }, 146 | }, 147 | }) 148 | -------------------------------------------------------------------------------- /config/hash.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JfefW 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import Env from '@ioc:Adonis/Core/Env' 9 | import { hashConfig } from '@adonisjs/core/build/config' 10 | 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Hash Config 14 | |-------------------------------------------------------------------------- 15 | | 16 | | The `HashConfig` relies on the `HashList` interface which is 17 | | defined inside `contracts` directory. 18 | | 19 | */ 20 | export default hashConfig({ 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Default hasher 24 | |-------------------------------------------------------------------------- 25 | | 26 | | By default we make use of the argon hasher to hash values. However, feel 27 | | free to change the default value 28 | | 29 | */ 30 | default: Env.get('HASH_DRIVER', 'scrypt'), 31 | 32 | list: { 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | scrypt 36 | |-------------------------------------------------------------------------- 37 | | 38 | | Scrypt mapping uses the Node.js inbuilt crypto module for creating 39 | | hashes. 40 | | 41 | | We are using the default configuration recommended within the Node.js 42 | | documentation. 43 | | https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback 44 | | 45 | */ 46 | scrypt: { 47 | driver: 'scrypt', 48 | cost: 16384, 49 | blockSize: 8, 50 | parallelization: 1, 51 | saltSize: 16, 52 | keyLength: 64, 53 | maxMemory: 32 * 1024 * 1024, 54 | }, 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Argon 59 | |-------------------------------------------------------------------------- 60 | | 61 | | Argon mapping uses the `argon2` driver to hash values. 62 | | 63 | | Make sure you install the underlying dependency for this driver to work. 64 | | https://www.npmjs.com/package/phc-argon2. 65 | | 66 | | npm install phc-argon2 67 | | 68 | */ 69 | argon: { 70 | driver: 'argon2', 71 | variant: 'id', 72 | iterations: 3, 73 | memory: 4096, 74 | parallelism: 1, 75 | saltSize: 16, 76 | }, 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Bcrypt 81 | |-------------------------------------------------------------------------- 82 | | 83 | | Bcrypt mapping uses the `bcrypt` driver to hash values. 84 | | 85 | | Make sure you install the underlying dependency for this driver to work. 86 | | https://www.npmjs.com/package/phc-bcrypt. 87 | | 88 | | npm install phc-bcrypt 89 | | 90 | */ 91 | bcrypt: { 92 | driver: 'bcrypt', 93 | rounds: 10, 94 | }, 95 | }, 96 | }) 97 | -------------------------------------------------------------------------------- /config/i18n.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/Jw53K 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import Application from '@ioc:Adonis/Core/Application' 9 | import { I18nConfig } from '@ioc:Adonis/Addons/I18n' 10 | 11 | const i18nConfig: I18nConfig = { 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Translations format 15 | |-------------------------------------------------------------------------- 16 | | 17 | | The format in which the translation are written. By default only the 18 | | ICU message syntax is supported. However, you can register custom 19 | | formatters too and please reference the documentation for that. 20 | | 21 | */ 22 | translationsFormat: 'icu', 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Default locale 27 | |-------------------------------------------------------------------------- 28 | | 29 | | The default locale represents the language for which all the translations 30 | | are always available. 31 | | 32 | | Having a default locale allows you to incrementally add translations for 33 | | other languages. If a specific language does not have a translation, 34 | | then the default locale translation will be used. 35 | | 36 | | Also, we switch to default locale for HTTP requests where the user language 37 | | is not supported by the your app 38 | | 39 | */ 40 | defaultLocale: 'en', 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Supported locales 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Optionally define an array of locales that your application supports. If 48 | | not defined, we will derive this value from the translations stored 49 | | inside the `resources/lang` directory. 50 | | 51 | */ 52 | // supportedLocales: [], 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Fallback locales 57 | |-------------------------------------------------------------------------- 58 | | 59 | | Here you can configure per language fallbacks. For example, you can set 60 | | "es" as the fallback locale for the Catalan language. 61 | | 62 | | If not configured, all languages will fallback to the defaultLocale 63 | | 64 | */ 65 | // fallbackLocales: {}, 66 | 67 | /* 68 | |-------------------------------------------------------------------------- 69 | | Provide validator messages 70 | |-------------------------------------------------------------------------- 71 | | 72 | | Set the following option to "true" if you want to use "i18n" for defining 73 | | the validation messages. 74 | | 75 | | The validation messages will be loaded from the "validator.shared" prefix. 76 | | 77 | */ 78 | provideValidatorMessages: false, 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Loaders 83 | |-------------------------------------------------------------------------- 84 | | 85 | | Loaders from which to load the translations. You can configure multiple 86 | | loaders as well and AdonisJS will merge the translations from all the 87 | | loaders to have a unified collection of messages. 88 | | 89 | | By default, only the "fs" loader is supported. However, you can add custom 90 | | loaders too and please reference the documentation for that. 91 | | 92 | */ 93 | loaders: { 94 | fs: { 95 | enabled: true, 96 | location: Application.resourcesPath('lang'), 97 | }, 98 | }, 99 | } 100 | 101 | export default i18nConfig 102 | -------------------------------------------------------------------------------- /config/mail.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JvgAf 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import Env from '@ioc:Adonis/Core/Env' 9 | import { mailConfig } from '@adonisjs/mail/build/config' 10 | 11 | export default mailConfig({ 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Default mailer 15 | |-------------------------------------------------------------------------- 16 | | 17 | | The following mailer will be used to send emails, when you don't specify 18 | | a mailer 19 | | 20 | */ 21 | mailer: 'smtp', 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Mailers 26 | |-------------------------------------------------------------------------- 27 | | 28 | | You can define or more mailers to send emails from your application. A 29 | | single `driver` can be used to define multiple mailers with different 30 | | config. 31 | | 32 | | For example: Postmark driver can be used to have different mailers for 33 | | sending transactional and promotional emails 34 | | 35 | */ 36 | mailers: { 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Smtp 40 | |-------------------------------------------------------------------------- 41 | | 42 | | Uses SMTP protocol for sending email 43 | | 44 | */ 45 | smtp: { 46 | driver: 'smtp', 47 | host: Env.get('SMTP_HOST'), 48 | port: Env.get('SMTP_PORT'), 49 | auth: { 50 | user: Env.get('SMTP_USERNAME'), 51 | pass: Env.get('SMTP_PASSWORD'), 52 | type: 'login', 53 | }, 54 | }, 55 | }, 56 | }) 57 | -------------------------------------------------------------------------------- /contracts/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/JOdz5 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this 5 | * file. 6 | */ 7 | 8 | import User from 'App/Models/User' 9 | 10 | declare module '@ioc:Adonis/Addons/Auth' { 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Providers 14 | |-------------------------------------------------------------------------- 15 | | 16 | | The providers are used to fetch users. The Auth module comes pre-bundled 17 | | with two providers that are `Lucid` and `Database`. Both uses database 18 | | to fetch user details. 19 | | 20 | | You can also create and register your own custom providers. 21 | | 22 | */ 23 | interface ProvidersList { 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | User Provider 27 | |-------------------------------------------------------------------------- 28 | | 29 | | The following provider uses Lucid models as a driver for fetching user 30 | | details from the database for authentication. 31 | | 32 | | You can create multiple providers using the same underlying driver with 33 | | different Lucid models. 34 | | 35 | */ 36 | user: { 37 | implementation: LucidProviderContract 38 | config: LucidProviderConfig 39 | } 40 | } 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Guards 45 | |-------------------------------------------------------------------------- 46 | | 47 | | The guards are used for authenticating users using different drivers. 48 | | The auth module comes with 3 different guards. 49 | | 50 | | - SessionGuardContract 51 | | - BasicAuthGuardContract 52 | | - OATGuardContract ( Opaque access token ) 53 | | 54 | | Every guard needs a provider for looking up users from the database. 55 | | 56 | */ 57 | interface GuardsList { 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | OAT Guard 61 | |-------------------------------------------------------------------------- 62 | | 63 | | OAT, stands for (Opaque access tokens) guard uses database backed tokens 64 | | to authenticate requests. 65 | | 66 | */ 67 | api: { 68 | implementation: OATGuardContract<'user', 'api'> 69 | config: OATGuardConfig<'user'> 70 | client: OATClientContract<'user'> 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /contracts/bouncer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/Jte3v 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import { actions, policies } from '../start/bouncer' 9 | 10 | declare module '@ioc:Adonis/Addons/Bouncer' { 11 | type ApplicationActions = ExtractActionsTypes 12 | type ApplicationPolicies = ExtractPoliciesTypes 13 | 14 | interface ActionsList extends ApplicationActions {} 15 | interface PoliciesList extends ApplicationPolicies {} 16 | } 17 | -------------------------------------------------------------------------------- /contracts/drive.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/JBt3I 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import type { InferDisksFromConfig } from '@adonisjs/core/build/config' 9 | import type driveConfig from '../config/drive' 10 | 11 | declare module '@ioc:Adonis/Core/Drive' { 12 | interface DisksList extends InferDisksFromConfig {} 13 | } 14 | -------------------------------------------------------------------------------- /contracts/env.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/JTm6U 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | declare module '@ioc:Adonis/Core/Env' { 9 | /* 10 | |-------------------------------------------------------------------------- 11 | | Getting types for validated environment variables 12 | |-------------------------------------------------------------------------- 13 | | 14 | | The `default` export from the "../env.ts" file exports types for the 15 | | validated environment variables. Here we merge them with the `EnvTypes` 16 | | interface so that you can enjoy intellisense when using the "Env" 17 | | module. 18 | | 19 | */ 20 | 21 | type CustomTypes = typeof import('../env').default 22 | interface EnvTypes extends CustomTypes {} 23 | } 24 | -------------------------------------------------------------------------------- /contracts/events.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/JfefG 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | declare module '@ioc:Adonis/Core/Event' { 9 | /* 10 | |-------------------------------------------------------------------------- 11 | | Define typed events 12 | |-------------------------------------------------------------------------- 13 | | 14 | | You can define types for events inside the following interface and 15 | | AdonisJS will make sure that all listeners and emit calls adheres 16 | | to the defined types. 17 | | 18 | | For example: 19 | | 20 | | interface EventsList { 21 | | 'new:user': UserModel 22 | | } 23 | | 24 | | Now calling `Event.emit('new:user')` will statically ensure that passed value is 25 | | an instance of the the UserModel only. 26 | | 27 | */ 28 | interface EventsList { 29 | // 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/hash.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/Jfefs 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import type { InferListFromConfig } from '@adonisjs/core/build/config' 9 | import type hashConfig from '../config/hash' 10 | 11 | declare module '@ioc:Adonis/Core/Hash' { 12 | interface HashersList extends InferListFromConfig {} 13 | } 14 | -------------------------------------------------------------------------------- /contracts/mail.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/JvgAT 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import { InferMailersFromConfig } from '@adonisjs/mail/build/config' 9 | import mailConfig from '../config/mail' 10 | 11 | declare module '@ioc:Adonis/Addons/Mail' { 12 | interface MailersList extends InferMailersFromConfig {} 13 | } 14 | -------------------------------------------------------------------------------- /contracts/tests.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://bit.ly/3DP1ypf 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import '@japa/runner' 9 | 10 | declare module '@japa/runner' { 11 | interface TestContext { 12 | // Extend context 13 | } 14 | 15 | interface Test { 16 | // Extend test 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /database/factories/index.ts: -------------------------------------------------------------------------------- 1 | // import Factory from '@ioc:Adonis/Lucid/Factory' 2 | -------------------------------------------------------------------------------- /database/migrations/1669234334619_users.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'users' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id').primary() 9 | table.string('email', 255).notNullable().unique() 10 | table.string('password', 180).notNullable() 11 | table.boolean('verified').defaultTo(false) 12 | table.string('name', 255).notNullable() 13 | table.string('remember_me_token').nullable() 14 | table.timestamp('created_at', { useTz: true }).notNullable() 15 | table.timestamp('updated_at', { useTz: true }).notNullable() 16 | }) 17 | } 18 | 19 | public async down() { 20 | this.schema.dropTable(this.tableName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/migrations/1669234334622_api_tokens.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'api_tokens' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id').primary() 9 | table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE') 10 | table.string('name').notNullable() 11 | table.string('type').notNullable() 12 | table.string('token', 64).notNullable().unique() 13 | 14 | /** 15 | * Uses timestampz for PostgreSQL and DATETIME2 for MSSQL 16 | */ 17 | table.timestamp('expires_at', { useTz: true }).nullable() 18 | table.timestamp('created_at', { useTz: true }).notNullable() 19 | }) 20 | } 21 | 22 | public async down() { 23 | this.schema.dropTable(this.tableName) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/migrations/1669464855581_projects.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'projects' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id') 9 | table.text('name').notNullable().unique() 10 | table.text('description').nullable() 11 | table 12 | .integer('forked_project_id') 13 | .unsigned() 14 | .references('projects.id') 15 | .nullable() 16 | .onDelete('CASCADE') 17 | table.timestamp('created_at', { useTz: true }) 18 | table.timestamp('updated_at', { useTz: true }) 19 | }) 20 | } 21 | 22 | public async down() { 23 | this.schema.dropTable(this.tableName) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/migrations/1669584873001_members.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'members' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id') 9 | table.unique(['project_id', 'user_id']) 10 | table 11 | .integer('project_id') 12 | .unsigned() 13 | .notNullable() 14 | .references('projects.id') 15 | .onDelete('CASCADE') 16 | table.integer('user_id').unsigned().notNullable().references('users.id').onDelete('CASCADE') 17 | table.boolean('verified').defaultTo(false) 18 | }) 19 | } 20 | 21 | public async down() { 22 | this.schema.dropTable(this.tableName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/migrations/1670583028612_routes.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'routes' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id') 9 | table.text('name').notNullable() 10 | table.enum('method', ['get', 'post', 'put', 'delete', 'patch']).notNullable() 11 | table.text('endpoint').notNullable() 12 | table.boolean('enabled').defaultTo(false) 13 | table.unique(['project_id', 'endpoint', 'method']) 14 | table.unique(['project_id', 'order']) 15 | table 16 | .integer('project_id') 17 | .unsigned() 18 | .notNullable() 19 | .references('projects.id') 20 | .onDelete('CASCADE') 21 | table.integer('order').notNullable() 22 | table.timestamp('created_at', { useTz: true }) 23 | table.timestamp('updated_at', { useTz: true }) 24 | }) 25 | } 26 | 27 | public async down() { 28 | this.schema.dropTable(this.tableName) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/1670591159570_responses.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'responses' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id') 9 | table.text('name').notNullable() 10 | table.integer('status').notNullable() 11 | table.text('body').notNullable() 12 | table.boolean('is_file').notNullable() 13 | table.boolean('enabled').defaultTo(false) 14 | table.integer('route_id').unsigned().notNullable().references('routes.id').onDelete('CASCADE') 15 | table.unique(['route_id', 'name']) 16 | table.timestamp('created_at', { useTz: true }) 17 | table.timestamp('updated_at', { useTz: true }) 18 | }) 19 | } 20 | 21 | public async down() { 22 | this.schema.dropTable(this.tableName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/migrations/1689104187739_headers.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'headers' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id') 9 | table.text('key').notNullable() 10 | table.text('value').notNullable() 11 | table 12 | .integer('response_id') 13 | .unsigned() 14 | .references('responses.id') 15 | .onDelete('CASCADE') 16 | .notNullable() 17 | table.timestamp('created_at', { useTz: true }) 18 | table.timestamp('updated_at', { useTz: true }) 19 | }) 20 | } 21 | 22 | public async down() { 23 | this.schema.dropTable(this.tableName) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/migrations/1689635396291_unique_headers.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'headers' 5 | 6 | public async up() { 7 | this.schema.alterTable(this.tableName, (table) => { 8 | table.unique(['key', 'response_id']) 9 | }) 10 | } 11 | 12 | public async down() { 13 | this.schema.alterTable(this.tableName, (table) => { 14 | table.dropUnique(['key', 'response_id']) 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /database/migrations/1690714833453_user_verify_locks.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'users' 5 | 6 | public async up() { 7 | this.schema.alterTable(this.tableName, (table) => { 8 | table.integer('verify_lock').unsigned().defaultTo(0) 9 | }) 10 | } 11 | 12 | public async down() { 13 | this.schema.alterTable(this.tableName, (table) => { 14 | table.dropColumn('verify_lock') 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /database/migrations/1691319933218_tokens.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'tokens' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id') 9 | table.text('token').notNullable().unique() 10 | table.text('name').notNullable() 11 | table 12 | .integer('project_id') 13 | .unsigned() 14 | .references('projects.id') 15 | .onDelete('CASCADE') 16 | .notNullable() 17 | table.timestamp('created_at', { useTz: true }) 18 | table.timestamp('updated_at', { useTz: true }) 19 | }) 20 | } 21 | 22 | public async down() { 23 | this.schema.dropTable(this.tableName) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/migrations/1693469175128_folders.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'routes' 5 | 6 | public async up() { 7 | this.schema.alterTable(this.tableName, (table) => { 8 | table.dropUnique(['project_id', 'endpoint', 'method']) 9 | 10 | table.enum('method', ['get', 'post', 'put', 'delete', 'patch']).alter({ alterType: false }) 11 | table.text('endpoint').nullable().alter() 12 | 13 | table.boolean('is_folder').notNullable().defaultTo(false) 14 | table 15 | .integer('parent_folder_id') 16 | .unsigned() 17 | .nullable() 18 | .references('routes.id') 19 | .onDelete('SET NULL') 20 | }) 21 | } 22 | 23 | public async down() { 24 | this.defer(async (db) => { 25 | await db.from('routes').whereNull('endpoint').orWhereNull('method').delete() 26 | }) 27 | 28 | this.schema.alterTable(this.tableName, (table) => { 29 | table 30 | .enum('method', ['get', 'post', 'put', 'delete', 'patch']) 31 | .notNullable() 32 | .alter({ alterType: false }) 33 | table.text('endpoint').notNullable().alter() 34 | 35 | table.dropColumn('is_folder') 36 | table.dropColumn('parent_folder_id') 37 | 38 | table.unique(['project_id', 'endpoint', 'method']) 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /database/migrations/1695735082689_contracts.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'contracts' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id') 9 | 10 | table.text('version').notNullable() 11 | table 12 | .integer('project_id') 13 | .references('projects.id') 14 | .unsigned() 15 | .notNullable() 16 | .onDelete('CASCADE') 17 | table.unique(['version', 'project_id']) 18 | 19 | table.text('swagger') 20 | 21 | table.integer('user_id').references('users.id').unsigned().nullable().onDelete('SET NULL') 22 | table.timestamp('created_at', { useTz: true }) 23 | table.timestamp('updated_at', { useTz: true }) 24 | }) 25 | } 26 | 27 | public async down() { 28 | this.schema.dropTable(this.tableName) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/1740136305100_processors.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'processors' 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments('id') 9 | table 10 | .integer('response_id') 11 | .unsigned() 12 | .notNullable() 13 | .references('responses.id') 14 | .onDelete('CASCADE') 15 | table.boolean('enabled') 16 | table.text('code') 17 | table.timestamp('created_at', { useTz: true }) 18 | table.timestamp('updated_at', { useTz: true }) 19 | }) 20 | } 21 | 22 | public async down() { 23 | this.schema.dropTable(this.tableName) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/migrations/1741125580372_remove_contracts.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from '@ioc:Adonis/Lucid/Schema' 2 | 3 | export default class extends BaseSchema { 4 | protected tableName = 'contracts' 5 | 6 | public async up() { 7 | this.schema.dropTable(this.tableName) 8 | } 9 | 10 | public async down() { 11 | this.schema.createTable(this.tableName, (table) => { 12 | table.increments('id') 13 | 14 | table.text('version').notNullable() 15 | table 16 | .integer('project_id') 17 | .references('projects.id') 18 | .unsigned() 19 | .notNullable() 20 | .onDelete('CASCADE') 21 | table.unique(['version', 'project_id']) 22 | 23 | table.text('swagger') 24 | 25 | table.integer('user_id').references('users.id').unsigned().nullable().onDelete('SET NULL') 26 | table.timestamp('created_at', { useTz: true }) 27 | table.timestamp('updated_at', { useTz: true }) 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | frontend: 4 | image: 'ghcr.io/undernightcore/mockos-front:latest' 5 | restart: unless-stopped 6 | env_file: 7 | - .env 8 | ports: 9 | - '8090:80' 10 | api: 11 | image: 'ghcr.io/undernightcore/mockos-back:latest' 12 | restart: unless-stopped 13 | env_file: 14 | - .env 15 | ports: 16 | - '8091:3333' 17 | s3: 18 | image: 'minio/minio' 19 | restart: unless-stopped 20 | volumes: 21 | - ./storage:/data 22 | environment: 23 | MINIO_ROOT_USER: ${S3_KEY} 24 | MINIO_ROOT_PASSWORD: ${S3_SECRET} 25 | entrypoint: sh 26 | command: -c 'mkdir -p /data/mockos && minio server /data' 27 | db: 28 | image: 'postgres' 29 | restart: unless-stopped 30 | environment: 31 | POSTGRES_USER: ${PG_USER} 32 | POSTGRES_PASSWORD: ${PG_PASSWORD} 33 | volumes: 34 | - ./database:/var/lib/postgresql/data 35 | -------------------------------------------------------------------------------- /env.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Validating Environment Variables 4 | |-------------------------------------------------------------------------- 5 | | 6 | | In this file we define the rules for validating environment variables. 7 | | By performing validation we ensure that your application is running in 8 | | a stable environment with correct configuration values. 9 | | 10 | | This file is read automatically by the framework during the boot lifecycle 11 | | and hence do not rename or move this file to a different location. 12 | | 13 | */ 14 | 15 | import Env from '@ioc:Adonis/Core/Env' 16 | 17 | export default Env.rules({ 18 | HOST: Env.schema.string({ format: 'host' }), 19 | PORT: Env.schema.number(), 20 | BACK_URL: Env.schema.string(), 21 | APP_KEY: Env.schema.string(), 22 | APP_NAME: Env.schema.string(), 23 | DRIVE_DISK: Env.schema.enum(['local', 's3'] as const), 24 | S3_KEY: Env.schema.string(), 25 | S3_SECRET: Env.schema.string(), 26 | S3_BUCKET: Env.schema.string(), 27 | S3_REGION: Env.schema.string(), 28 | S3_ENDPOINT: Env.schema.string.optional(), 29 | NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const), 30 | DB_CONNECTION: Env.schema.string(), 31 | PG_HOST: Env.schema.string({ format: 'host' }), 32 | PG_PORT: Env.schema.number(), 33 | PG_USER: Env.schema.string(), 34 | PG_PASSWORD: Env.schema.string.optional(), 35 | PG_DB_NAME: Env.schema.string(), 36 | SMTP_EMAIL: Env.schema.string(), 37 | SMTP_HOST: Env.schema.string({ format: 'host' }), 38 | SMTP_PORT: Env.schema.number(), 39 | SMTP_USERNAME: Env.schema.string(), 40 | SMTP_PASSWORD: Env.schema.string(), 41 | FRONT_URL: Env.schema.string(), 42 | DISABLE_VERIFICATION: Env.schema.boolean(), 43 | }) 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mockos-back", 3 | "version": "2.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "node ace serve --watch", 7 | "build": "node ace build --production", 8 | "start": "node server.js", 9 | "lint": "eslint . --ext=.ts", 10 | "format": "prettier --write ." 11 | }, 12 | "eslintConfig": { 13 | "extends": [ 14 | "plugin:adonis/typescriptApp", 15 | "prettier" 16 | ], 17 | "plugins": [ 18 | "prettier" 19 | ], 20 | "rules": { 21 | "prettier/prettier": [ 22 | "error" 23 | ] 24 | } 25 | }, 26 | "eslintIgnore": [ 27 | "build" 28 | ], 29 | "prettier": { 30 | "trailingComma": "es5", 31 | "semi": false, 32 | "singleQuote": true, 33 | "useTabs": false, 34 | "quoteProps": "consistent", 35 | "bracketSpacing": true, 36 | "arrowParens": "always", 37 | "printWidth": 100 38 | }, 39 | "devDependencies": { 40 | "@adonisjs/assembler": "^5.9.5", 41 | "@japa/preset-adonis": "^1.2.0", 42 | "@japa/runner": "^2.2.2", 43 | "@types/js-yaml": "^4.0.6", 44 | "@types/proxy-addr": "^2.0.0", 45 | "@types/source-map-support": "^0.5.6", 46 | "adonis-preset-ts": "^2.1.0", 47 | "eslint": "^8.28.0", 48 | "eslint-config-prettier": "^8.5.0", 49 | "eslint-plugin-adonis": "^2.1.1", 50 | "eslint-plugin-prettier": "^4.2.1", 51 | "pino-pretty": "^9.1.1", 52 | "prettier": "^2.8.0", 53 | "typescript": "~4.6", 54 | "youch": "^3.2.2", 55 | "youch-terminal": "^2.1.5" 56 | }, 57 | "dependencies": { 58 | "@adonisjs/auth": "^8.2.3", 59 | "@adonisjs/bouncer": "^2.3.0", 60 | "@adonisjs/core": "^5.9.0", 61 | "@adonisjs/drive-s3": "^1.3.2", 62 | "@adonisjs/i18n": "^1.5.6", 63 | "@adonisjs/lucid": "^18.3.0", 64 | "@adonisjs/mail": "^8.1.2", 65 | "@adonisjs/repl": "^3.1.11", 66 | "@apidevtools/swagger-parser": "^10.1.0", 67 | "@faker-js/faker": "^8.4.1", 68 | "js-yaml": "^4.1.0", 69 | "luxon": "^3.1.0", 70 | "pg": "^8.8.0", 71 | "proxy-addr": "^2.0.7", 72 | "reflect-metadata": "^0.1.13", 73 | "socket.io": "^4.6.1", 74 | "source-map-support": "^0.5.21", 75 | "v8-sandbox": "^3.2.12" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /providers/AppProvider.ts: -------------------------------------------------------------------------------- 1 | import type { ApplicationContract } from '@ioc:Adonis/Core/Application' 2 | 3 | export default class AppProvider { 4 | constructor(protected app: ApplicationContract) {} 5 | 6 | public register() { 7 | // Register your own bindings 8 | } 9 | 10 | public async boot() { 11 | // IoC container is ready 12 | } 13 | 14 | public async ready() { 15 | await import('../start/socket') 16 | } 17 | 18 | public async shutdown() { 19 | // Cleanup, since app is going down 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /resources/images/graphics/mockos-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/undernightcore/mockos/797c29ee1c1244313a6893f4e8fb6c0056ab139f/resources/images/graphics/mockos-banner.png -------------------------------------------------------------------------------- /resources/lang/en/bouncer.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "is_verified": "The user must verify their email first" 4 | }, 5 | "invitation": { 6 | "is_invited": "You haven't been invited to this party!" 7 | }, 8 | "project": { 9 | "is_already_member": { 10 | "is_verified": "This member already exists", 11 | "is_not_verified": "This member is pending to accept the invitation" 12 | }, 13 | "is_member": "You are not a member of this project" 14 | }, 15 | "route": { 16 | "is_folder": "You can't add responses/headers to a folder!" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/lang/en/middleware.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled_verification": { 3 | "disabled": "Email verification is disabled. Try again later." 4 | } 5 | } -------------------------------------------------------------------------------- /resources/lang/en/responses.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "mock": { 4 | "missing_token": "You need to authenticate using a token", 5 | "wrong_token": "Invalid token", 6 | "missing_route": "No route found with that endpoint and methods", 7 | "missing_response": "No response available for that route and method", 8 | "invalid_json": "JSON response has invalid structure, please fix response body" 9 | } 10 | }, 11 | "user": { 12 | "register": { 13 | "verify_subject": "Welcome to Mockos!", 14 | "verify_message": "Please verify your email here {url}", 15 | "verify_email": "Hi {name}, please verify your email address", 16 | "login": "Welcome {name}! Please log in!" 17 | }, 18 | "login": { 19 | "verify_first": "You need to verify your email address first", 20 | "wrong_credentials": "Sorry, these credentials are incorrect" 21 | }, 22 | "edit": { 23 | "user_edited": "Your user info has been edited" 24 | }, 25 | "email": { 26 | "verify_subject": "Please validate your new Mockos email here", 27 | "verify_message": "You just requested an email change, the change will take effect if you click here {url}", 28 | "verify_email": "Please, verify this change in the inbox of the email you just sent us" 29 | }, 30 | "resend_email": { 31 | "already_verified": "No need to verify your account, you're already verified", 32 | "verify_email": "We sent you a new email with a verification link" 33 | } 34 | }, 35 | "invitation": { 36 | "accept": { 37 | "welcome_user": "Welcome to {project}, {name}!" 38 | }, 39 | "reject": { 40 | "invitation_rejected": "You have rejected the invitation" 41 | }, 42 | "invite": { 43 | "user_invited": "{name} has been invited" 44 | } 45 | }, 46 | "project": { 47 | "fork": { 48 | "fork_created": "Branch created successfully" 49 | }, 50 | "leave": { 51 | "left_project": "You have left the {project} project" 52 | } 53 | }, 54 | "response": { 55 | "create": { 56 | "response_created": "The response has been created" 57 | }, 58 | "edit": { 59 | "response_edited": "The response has been updated", 60 | "missing_file_body": "You have to upload a file" 61 | }, 62 | "delete": { 63 | "response_deleted": "The response has been deleted" 64 | }, 65 | "enable": { 66 | "response_enabled": "The response is now enabled" 67 | }, 68 | "duplicate": { 69 | "response_duplicated": "The response has been duplicated" 70 | } 71 | }, 72 | "route": { 73 | "delete": { 74 | "route_deleted": "The route has been deleted" 75 | }, 76 | "moveandsort": { 77 | "successfully": "Moved successfully", 78 | "folder_in_folder": "You can't put a folder into another folder", 79 | "missing_data": "The route you were trying to move has dissapeared" 80 | } 81 | }, 82 | "header": { 83 | "create": { 84 | "header_created": "Header created successfully" 85 | }, 86 | "update": { 87 | "header_updated": "Header updated successfully" 88 | }, 89 | "delete": { 90 | "header_deleted": "Header deleted successfully" 91 | } 92 | }, 93 | "token": { 94 | "delete": { 95 | "token_deleted": "Token deleted successfully" 96 | } 97 | }, 98 | "swagger": { 99 | "parse": { 100 | "success": "Swagger imported successfully" 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /resources/lang/en/validator.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "login": { 4 | "email.required": "We need an email so we know who you are", 5 | "email.email": "This email does not look right...", 6 | "password.required": "We need your password to verify that it's actually you" 7 | }, 8 | "register": { 9 | "email.required": "We need an email to register you", 10 | "email.unique": "You already have an account with this email", 11 | "email.email": "This email is not valid", 12 | "name.required": "We need a name to know how to address you", 13 | "name.minLength": "We need a name that is at least 3 characters, you can also use your surname", 14 | "password.required": "We need a password to give you access", 15 | "password.minLength": "Please use a password of at least 8 characters" 16 | }, 17 | "edit": { 18 | "name.required": "We need a name to know how to address you", 19 | "name.minLength": "We need a name that is at least 3 characters, you can also use your surname" 20 | }, 21 | "email": { 22 | "email.required": "We need to know your new email", 23 | "email.email": "This email is not valid", 24 | "email.unique": "This email is already in use" 25 | }, 26 | "resend_email": { 27 | "email.required": "We need to know what is your email", 28 | "email.email": "This email is not valid", 29 | "email.exists": "There is no user with this email" 30 | } 31 | }, 32 | "project": { 33 | "create": { 34 | "name.required": "I need a name for the project", 35 | "name.unique": "A project with this name already exists", 36 | "name.minLength": "The name must be at least 3 characters long", 37 | "name.maxLength": "The name can't be more than 200 characters long", 38 | "description.minLength": "The description must be at least 3 characters long", 39 | "description.maxLength": "The description can't be more than 2000 characters long", 40 | "description.nullable": "A description is required, if you don't want to send one you can send null" 41 | }, 42 | "edit": { 43 | "name.required": "I need a name to modify", 44 | "name.unique": "A project with this name already exists", 45 | "name.minLength": "The name must be at least 3 characters long", 46 | "name.maxLength": "The name can't be more than 200 characters long", 47 | "description.minLength": "The description must be at least 3 characters long", 48 | "description.maxLength": "The description can't be more than 2000 characters long", 49 | "description.nullable": "A description is required, if you don't want to send one you can send null" 50 | } 51 | }, 52 | "response": { 53 | "create": { 54 | "enabled.required": "You need to indicate if the response is enabled", 55 | "status.required": "You need an HTTP code for your response", 56 | "status.range": "The HTTP code must be valid", 57 | "body.required": "A body is necessary for your response", 58 | "body.size": "Your file can't be bigger than 8MB", 59 | "body.json": "This JSON body is invalid", 60 | "body.maxLength": "The body can't be more than 500000 characters long", 61 | "name.required": "A name is necessary for your response", 62 | "name.maxLength": "The name can't be more than 200 characters long", 63 | "name.unique": "This response name already exists" 64 | }, 65 | "edit": { 66 | "enabled.required": "You need to indicate if the response is enabled", 67 | "status.required": "You need an HTTP code for your response", 68 | "status.range": "The HTTP code must be valid", 69 | "body.required": "A body is necessary for your response", 70 | "body.json": "This JSON body is invalid", 71 | "body.maxLength": "The body can't be more than 20000 characters long", 72 | "name.required": "A name is necessary for your response", 73 | "name.maxLength": "The name can't be more than 200 characters long", 74 | "name.unique": "This response name already exists" 75 | }, 76 | "duplicate": { 77 | "name.required": "A name is necessary for your response", 78 | "name.maxLength": "The name can't be more than 200 characters long", 79 | "name.unique": "This response name already exists" 80 | }, 81 | "deleteMany": { 82 | "ids.required": "You need to indicate which responses you want to delete", 83 | "ids.minLength": "You need to indicate at least one response", 84 | "ids.exists": "There is a response that does not exist" 85 | } 86 | }, 87 | "processor": { 88 | "create": { 89 | "enabled.required": "You need to indicate if the processor is enabled", 90 | "code.required": "You need to send the code the processor will execute" 91 | } 92 | }, 93 | "route": { 94 | "create": { 95 | "name.required": "A brief description for the route is needed", 96 | "name.minLength": "The name must have at least 3 characters", 97 | "name.maxLength": "The name can't be more than 200 characters long", 98 | "method.required": "An HTTP verb must be specified", 99 | "method.enum": "This is not a valid verb.", 100 | "method.unique": "There is already a route with this method and endpoint in this project", 101 | "endpoint.required": "The endpoint URL to create is required", 102 | "endpoint.regex": "The endpoint must start with / and not end in /", 103 | "endpoint.maxLength": "The endpoint can't be more than 2000 characters long", 104 | "enabled.required": "You have to indicate the state of the route", 105 | "parentFolderId.exists": "The folder you are trying to create the route in does not exist" 106 | }, 107 | "create_folder": { 108 | "name.required": "A brief description for the folder is needed", 109 | "name.minLength": "The name must have at least 3 characters", 110 | "name.maxLength": "The name can't be more than 200 characters long" 111 | }, 112 | "edit": { 113 | "name.required": "A brief description for the route is needed", 114 | "name.minLength": "The name must have at least 3 characters", 115 | "name.maxLength": "The name can't be more than 200 characters long", 116 | "method.required": "An HTTP verb must be specified", 117 | "method.enum": "This is not a valid verb.", 118 | "method.unique": "There is already a route with this method and endpoint in this project", 119 | "endpoint.required": "The endpoint URL to create is required", 120 | "endpoint.regex": "The endpoint must start with / and not end in /", 121 | "endpoint.maxLength": "The endpoint can't be more than 2000 characters long", 122 | "enabled.required": "You have to indicate the state of the route" 123 | }, 124 | "edit_folder": { 125 | "name.required": "A brief description for the folder is needed", 126 | "name.minLength": "The name must have at least 3 characters", 127 | "name.maxLength": "The name can't be more than 200 characters long" 128 | }, 129 | "moveandsort": { 130 | "what.required": "You must specify the route you want to move", 131 | "what.exists": "This route does not exist", 132 | "into.required": "You must specify the folder where you want to drop the route", 133 | "into.exists": "The folder you are trying to drop the route into does not exist", 134 | "before.required": "You must specify before which route you want to drop the item", 135 | "before.exists": "The route you indicate that should come after yours does not exist" 136 | } 137 | }, 138 | "header": { 139 | "create": { 140 | "key.required": "A key is required", 141 | "key.maxLength": "The key can't be more than 255 characters long", 142 | "value.required": "A value is required", 143 | "value.maxLength": "The value can't be more than 255 characters long" 144 | }, 145 | "update": { 146 | "key.required": "A key is required", 147 | "key.maxLength": "The key can't be more than 255 characters long", 148 | "value.required": "A value is required", 149 | "value.maxLength": "The value can't be more than 255 characters long", 150 | "key.unique": "There is already a header with that key" 151 | } 152 | }, 153 | "token": { 154 | "create": { 155 | "name.required": "A name is required for this token", 156 | "name.minLength": "The name must be at least 3 characters long", 157 | "name.maxLength": "The name can't be more than 30 characters long" 158 | } 159 | }, 160 | "swagger": { 161 | "parse": { 162 | "swagger.required": "You need to indicate the swagger content to parse", 163 | "reset.required": "You need to indicate if you want to reset the project", 164 | "basePath.regex": "The basePath must begin with / and not end with /" 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /resources/lang/es/bouncer.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "is_verified": "El usuario debe verificar primero su correo electrónico" 4 | }, 5 | "invitation": { 6 | "is_invited": "No te han invitado a esta fiesta!" 7 | }, 8 | "project": { 9 | "is_already_member": { 10 | "is_verified": "Ya existe este miembro", 11 | "is_not_verified": "Este miembro está pendiente de aceptar la invitación" 12 | }, 13 | "is_member": "No eres miembro de este proyecto" 14 | }, 15 | "route": { 16 | "is_folder": "¡No puedes agregar respuestas/cabeceras a una carpeta!" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/lang/es/middleware.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled_verification": { 3 | "disabled": "La verificación por correo electrónico está deshabilitada. Vuelve a intentarlo más tarde." 4 | } 5 | } -------------------------------------------------------------------------------- /resources/lang/es/responses.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "mock": { 4 | "missing_token": "Necesitamos que te autentiques usando un token", 5 | "wrong_token": "Este token no es válido", 6 | "missing_route": "No se ha encontrado ninguna ruta con ese endpoint y métodos", 7 | "missing_response": "No se ha encontrado ninguna respuesta disponible para esa ruta y método", 8 | "invalid_json": "La respuesta JSON tiene una estructura inválida, por favor corrige el cuerpo de la respuesta" 9 | } 10 | }, 11 | "user": { 12 | "register": { 13 | "verify_subject": "¡Bienvenido a Mockos!", 14 | "verify_message": "Porfi, verifica tu correo aquí {url}", 15 | "verify_email": "Holi {name}, verifica tu correo electrónico porfa", 16 | "login": "¡Bienvenido {name}! Por favor inicia sesión" 17 | }, 18 | "login": { 19 | "verify_first": "Tienes que verificar tu correo electrónico primero", 20 | "wrong_credentials": "Bro estas credenciales no están bien" 21 | }, 22 | "edit": { 23 | "user_edited": "Tus datos de usuario han sido editados" 24 | }, 25 | "email": { 26 | "verify_subject": "Por favor, valida tu nuevo correo electrónico de Mockos aquí", 27 | "verify_message": "Acabas de solicitar un cambio de correo electrónico, el cambio surtirá efecto si haces clic aquí {url}", 28 | "verify_email": "Por favor, verifica este cambio en la bandeja de entrada del correo electrónico que nos acabas de enviar" 29 | }, 30 | "resend_email": { 31 | "already_verified": "No hace falta que verifiques tu cuenta, ya estás verificado", 32 | "verify_email": "Te hemos enviado un nuevo correo electrónico con un enlace de verificación" 33 | } 34 | }, 35 | "invitation": { 36 | "accept": { 37 | "welcome_user": "¡Bienvenido a {project}, {name}!" 38 | }, 39 | "reject": { 40 | "invitation_rejected": "Has rechazado la invitación" 41 | }, 42 | "invite": { 43 | "user_invited": "Se ha invitado a {name}" 44 | } 45 | }, 46 | "project": { 47 | "fork": { 48 | "fork_created": "Rama creada correctamente" 49 | }, 50 | "leave": { 51 | "left_project": "Te has ido del projecto {project}" 52 | } 53 | }, 54 | "response": { 55 | "create": { 56 | "response_created": "Se ha creado la respuesta" 57 | }, 58 | "edit": { 59 | "response_edited": "Se ha actualizado la respuesta", 60 | "missing_file_body": "Tienes que subir un archivo" 61 | }, 62 | "delete": { 63 | "response_deleted": "Se ha eliminado la respuesta" 64 | }, 65 | "enable": { 66 | "response_enabled": "Se ha activado la respuesta" 67 | }, 68 | "duplicate": { 69 | "response_duplicated": "Se ha duplicado la respuesta" 70 | } 71 | }, 72 | "route": { 73 | "delete": { 74 | "route_deleted": "Se ha eliminado la ruta" 75 | }, 76 | "moveandsort": { 77 | "successfully": "Se ha movido correctamente", 78 | "folder_in_folder": "No puedes mover una carpeta dentro de otra carpeta", 79 | "missing_data": "La ruta que intentabas mover ha desaparecido" 80 | } 81 | }, 82 | "header": { 83 | "create": { 84 | "header_created": "Cabecera creada correctamente" 85 | }, 86 | "delete": { 87 | "header_deleted": "Cabecera eliminada correctamente" 88 | } 89 | }, 90 | "token": { 91 | "delete": { 92 | "token_deleted": "Token eliminado correctamente" 93 | } 94 | }, 95 | "swagger": { 96 | "parse": { 97 | "success": "El swagger ha sido importado correctamente" 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /resources/lang/es/validator.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "login": { 4 | "email.required": "Necesitamos un correo para saber quien eres", 5 | "email.email": "El correo electrónico no tiene un formato correcto", 6 | "password.required": "Necesitamos tu contraseña para verificar que eres tu" 7 | }, 8 | "register": { 9 | "email.required": "Necesitamos un correo para poder registrarte", 10 | "email.unique": "Ya tienes una cuenta con este correo", 11 | "email.email": "Este correo no es valido", 12 | "name.required": "Necesitamos un nombre para saber como dirigirnos a ti", 13 | "name.minLength": "Tu nombre tiene que tener al menos 3 caracteres, podrías usar tu apellido", 14 | "password.required": "Necesitamos una contraseña para poderte dar acceso", 15 | "password.minLength": "Porfa, usa una contraseña de al menos 8 caracteres" 16 | }, 17 | "edit": { 18 | "name.required": "Necesitamos un nombre para saber como dirigirnos a ti", 19 | "name.minLength": "Tu nombre tiene que tener al menos 3 caracteres, podrías usar tu apellido" 20 | }, 21 | "email": { 22 | "email.required": "Necesitamos conocer tu nuevo email", 23 | "email.email": "Este email no es valido", 24 | "email.unique": "Este email ya está en uso" 25 | }, 26 | "resend_email": { 27 | "email.required": "Necesitamos saber cuál es tu correo electrónico", 28 | "email.email": "Este correo electrónico no es válido", 29 | "email.exists": "No hay ningún usuario con este correo electrónico" 30 | } 31 | }, 32 | "project": { 33 | "create": { 34 | "name.required": "Necesito un nombre para el proyecto", 35 | "name.unique": "Ya existe un proyecto con este nombre", 36 | "name.minLength": "El nombre debe tener como mínimo 3 carácteres", 37 | "name.maxLength": "El nombre no puede tener más de 200 carácteres", 38 | "description.minLength": "La descripción debe tener como mínimo 3 caracteres", 39 | "description.maxLength": "La descripción no puede tener más de 2000 carácteres", 40 | "description.nullable": "Es necesaria una descripción, si no quieres enviar una puede pasar null" 41 | }, 42 | "edit": { 43 | "name.required": "Necesito un nombre a modificar", 44 | "name.unique": "Ya existe un proyecto con este nombre", 45 | "name.minLength": "El nombre debe tener como mínimo 3 carácteres", 46 | "name.maxLength": "El nombre no puede tener más de 200 carácteres", 47 | "description.minLength": "La descripción debe tener como mínimo 3 caracteres", 48 | "description.maxLength": "La descripción no puede tener más de 2000 carácteres", 49 | "description.nullable": "Es necesaria una descripción, si no quieres enviar una puede pasar null" 50 | } 51 | }, 52 | "response": { 53 | "create": { 54 | "enabled.required": "Es necesario que indiques si la respuesta está activada", 55 | "status.required": "Es necesario un código HTTP para tu respuesta", 56 | "status.range": "El código HTTP debe ser valido", 57 | "body.required": "Es necesaria un cuerpo para tu respuesta", 58 | "body.size": "Tu fichero no debe pesar más de 8MB", 59 | "body.json": "Este JSON es invalido", 60 | "body.maxLength": "El cuerpo no puede tener más de 20000 carácteres", 61 | "name.required": "Es necesario un nombre para tu respuesta", 62 | "name.maxLength": "El nombre no puede tener más de 200 carácteres", 63 | "name.unique": "Este nombre de respuesta ya existe" 64 | }, 65 | "edit": { 66 | "enabled.required": "Es necesario que indiques si la respuesta está activada", 67 | "status.required": "Es necesario un código HTTP para tu respuesta", 68 | "status.range": "El código HTTP debe ser valido", 69 | "body.required": "Es necesaria un cuerpo para tu respuesta", 70 | "body.json": "Este JSON es invalido", 71 | "body.maxLength": "El cuerpo no puede tener más de 20000 carácteres", 72 | "name.required": "Es necesario un nombre para tu respuesta", 73 | "name.maxLength": "El nombre no puede tener más de 200 carácteres", 74 | "name.unique": "Este nombre de respuesta ya existe" 75 | }, 76 | "duplicate": { 77 | "name.required": "Es necesario un nombre para tu respuesta", 78 | "name.maxLength": "El nombre no puede tener más de 200 carácteres", 79 | "name.unique": "Este nombre de respuesta ya existe" 80 | }, 81 | "deleteMany": { 82 | "ids.required": "Es necesario que indiques el listado de respuestas a eliminar", 83 | "ids.minLength": "Tienes que indicar al menos una respuesta", 84 | "ids.exists": "Hay una respuesta que no existe" 85 | } 86 | }, 87 | "processor": { 88 | "create": { 89 | "enabled.required": "Es necesario que indiques si el procesado está activado", 90 | "code.required": "Es necesario que envies el código a ejecutar en el procesado" 91 | } 92 | }, 93 | "route": { 94 | "create": { 95 | "name.required": "Se necesita una pequeña descripción para la ruta", 96 | "name.minLength": "Es necesario que el nombre tenga como mínimo 3 carácteres", 97 | "name.maxLength": "El nombre no puede tener más de 200 carácteres", 98 | "method.required": "Es necesario especificar un verbo HTTP", 99 | "method.enum": "No es un verbo válido.", 100 | "method.unique": "Ya existe una ruta con este método y endpoint en este proyecto", 101 | "endpoint.required": "Es necesaria la URL del endpoint a crear", 102 | "endpoint.regex": "El endpoint debe comenzar con / y no terminar en /", 103 | "endpoint.maxLength": "El endpoint no puede tener más de 2000 carácteres", 104 | "enabled.required": "Tienes que indicar el estado de la ruta", 105 | "parentFolderId.exists": "No existe la carpeta donde intentas crear la ruta" 106 | }, 107 | "create_folder": { 108 | "name.required": "Se necesita una pequeña descripción para la carpeta", 109 | "name.minLength": "Es necesario que el nombre tenga como mínimo 3 carácteres", 110 | "name.maxLength": "El nombre no puede tener más de 200 carácteres" 111 | }, 112 | "edit": { 113 | "name.required": "Se necesita una pequeña descripción para la ruta", 114 | "name.minLength": "Es necesario que el nombre tenga como mínimo 3 carácteres", 115 | "name.maxLength": "El nombre no puede tener más de 200 carácteres", 116 | "method.required": "Es necesario especificar un verbo HTTP", 117 | "method.enum": "No es un verbo válido.", 118 | "method.unique": "Ya existe una ruta con este método y endpoint en este proyecto", 119 | "endpoint.required": "Es necesaria la URL del endpoint a crear", 120 | "endpoint.regex": "El endpoint debe comenzar con / y no terminar en /", 121 | "endpoint.maxLength": "El endpoint no puede tener más de 500000 carácteres", 122 | "enabled.required": "Tienes que indicar el estado de la ruta" 123 | }, 124 | "edit_folder": { 125 | "name.required": "Se necesita una pequeña descripción para la carpeta", 126 | "name.minLength": "Es necesario que el nombre tenga como mínimo 3 carácteres", 127 | "name.maxLength": "El nombre no puede tener más de 200 carácteres" 128 | }, 129 | "moveandsort": { 130 | "what.required": "Debes indicar la ruta que quieres mover", 131 | "what.exists": "Esta ruta no existe", 132 | "into.required": "Debes indicar en que carpeta quieres soltar la ruta", 133 | "into.exists": "La carpeta donde estás intentado soltar la ruta no existe", 134 | "before.required": "Debes indicar antes de que ruta quieres soltar el elemento", 135 | "before.exists": "La ruta que indicas que quieres que sea la posterior a la tuya no existe" 136 | } 137 | }, 138 | "header": { 139 | "create": { 140 | "key.required": "Es necesario que indiques que clave necesitas", 141 | "key.maxLength": "La clave no puede tener más de 255 carácteres", 142 | "value.required": "Es necesario que indiques que valor tiene esa clave", 143 | "value.maxLength": "El valor no puede tener más de 255 carácteres" 144 | }, 145 | "update": { 146 | "key.required": "Es necesario que indiques que clave necesitas", 147 | "key.maxLength": "La clave no puede tener más de 255 carácteres", 148 | "value.required": "Es necesario que indiques que valor tiene esa clave", 149 | "value.maxLength": "El valor no puede tener más de 255 carácteres", 150 | "key.unique": "Ya hay una cabecera con esa clave" 151 | } 152 | }, 153 | "token": { 154 | "create": { 155 | "name.required": "Se require un nombre para este token", 156 | "name.minLength": "El nombre tiene que tener al menos 3 carácteres", 157 | "name.maxLength": "El nombre no puede tener más de 30 carácteres" 158 | } 159 | }, 160 | "swagger": { 161 | "parse": { 162 | "swagger.required": "Es necesario indicar el contenido swagger a parsear", 163 | "reset.required": "Debe indicar si desea restablecer el proyecto", 164 | "basePath.regex": "La ruta base debe empezar por / y no terminar por /" 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | AdonisJs Server 4 | |-------------------------------------------------------------------------- 5 | | 6 | | The contents in this file is meant to bootstrap the AdonisJs application 7 | | and start the HTTP server to accept incoming connections. You must avoid 8 | | making this file dirty and instead make use of `lifecycle hooks` provided 9 | | by AdonisJs service providers for custom code. 10 | | 11 | */ 12 | 13 | import 'reflect-metadata' 14 | import sourceMapSupport from 'source-map-support' 15 | import { Ignitor } from '@adonisjs/core/build/standalone' 16 | 17 | sourceMapSupport.install({ handleUncaughtExceptions: false }) 18 | 19 | new Ignitor(__dirname).httpServer().start() 20 | -------------------------------------------------------------------------------- /start/bouncer.ts: -------------------------------------------------------------------------------- 1 | import Bouncer from '@ioc:Adonis/Addons/Bouncer' 2 | export const { actions } = Bouncer 3 | 4 | export const { policies } = Bouncer.registerPolicies({ 5 | GlobalPolicy: () => import('App/Policies/GlobalPolicy'), 6 | ProjectPolicy: () => import('App/Policies/ProjectPolicy'), 7 | InvitationPolicy: () => import('App/Policies/InvitationPolicy'), 8 | RoutePolicy: () => import('App/Policies/RoutePolicy'), 9 | }) 10 | -------------------------------------------------------------------------------- /start/kernel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Application middleware 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This file is used to define middleware for HTTP requests. You can register 7 | | middleware as a `closure` or an IoC container binding. The bindings are 8 | | preferred, since they keep this file clean. 9 | | 10 | */ 11 | 12 | import Server from '@ioc:Adonis/Core/Server' 13 | 14 | /* 15 | |-------------------------------------------------------------------------- 16 | | Global middleware 17 | |-------------------------------------------------------------------------- 18 | | 19 | | An array of global middleware, that will be executed in the order they 20 | | are defined for every HTTP requests. 21 | | 22 | */ 23 | Server.middleware.register([ 24 | () => import('@ioc:Adonis/Core/BodyParser'), 25 | () => import('App/Middleware/DetectUserLocale'), 26 | ]) 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Named middleware 31 | |-------------------------------------------------------------------------- 32 | | 33 | | Named middleware are defined as key-value pair. The value is the namespace 34 | | or middleware function and key is the alias. Later you can use these 35 | | alias on individual routes. For example: 36 | | 37 | | { auth: () => import('App/Middleware/Auth') } 38 | | 39 | | and then use it as follows 40 | | 41 | | Route.get('dashboard', 'UserController.dashboard').middleware('auth') 42 | | 43 | */ 44 | Server.middleware.registerNamed({ 45 | verificationDisabled: () => import('App/Middleware/DisabledVerification'), 46 | }) 47 | -------------------------------------------------------------------------------- /start/routes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Routes 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This file is dedicated for defining HTTP routes. A single file is enough 7 | | for majority of projects, however you can define routes in different 8 | | files and just make sure to import them inside this file. For example 9 | | 10 | | Define routes in following two files 11 | | ├── start/routes/cart.ts 12 | | ├── start/routes/customer.ts 13 | | 14 | | and then import them inside `start/routes.ts` as follows 15 | | 16 | | import './routes/cart' 17 | | import './routes/customer' 18 | | 19 | */ 20 | 21 | import './routes/api' 22 | import './routes/routes' 23 | import './routes/invitations' 24 | import './routes/user' 25 | import './routes/projects' 26 | import './routes/responses' 27 | import './routes/headers' 28 | import './routes/tokens' 29 | import './routes/mock' 30 | -------------------------------------------------------------------------------- /start/routes/api.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | 3 | Route.group(() => { 4 | Route.any('', 'ApiController.mock') 5 | Route.any('*', 'ApiController.mock') 6 | }).prefix('api') 7 | -------------------------------------------------------------------------------- /start/routes/headers.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | 3 | Route.group(() => { 4 | Route.put(':id', 'HeadersController.edit') 5 | Route.delete(':id', 'HeadersController.delete') 6 | }).prefix('headers') 7 | -------------------------------------------------------------------------------- /start/routes/invitations.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | 3 | Route.group(() => { 4 | Route.get('', 'InvitationsController.getList') 5 | Route.post(':id/accept', 'InvitationsController.accept') 6 | Route.post(':id/reject', 'InvitationsController.reject') 7 | }).prefix('invitations') 8 | -------------------------------------------------------------------------------- /start/routes/mock.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | 3 | Route.group(() => { 4 | Route.any(':token', 'ApiController.mock') 5 | Route.any(':token/*', 'ApiController.mock') 6 | }).prefix('mock') 7 | -------------------------------------------------------------------------------- /start/routes/projects.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | 3 | Route.group(() => { 4 | Route.get('', 'ProjectsController.getList') 5 | Route.post('', 'ProjectsController.create') 6 | Route.delete(':id', 'ProjectsController.leave') 7 | Route.put(':id', 'ProjectsController.edit') 8 | Route.get(':id', 'ProjectsController.get') 9 | Route.get(':id/members', 'ProjectsController.getMemberList') 10 | Route.post(':id/fork', 'ProjectsController.fork') 11 | Route.post(':id/leave', 'ProjectsController.leave') 12 | Route.post(':projectId/invite/:email', 'InvitationsController.invite') 13 | Route.post(':id/routes', 'RoutesController.create') 14 | Route.get(':id/routes', 'RoutesController.getList') 15 | Route.post(':id/move', 'RoutesController.moveAndSort') 16 | Route.post(':id/tokens', 'TokensController.create') 17 | Route.get(':id/tokens', 'TokensController.getList') 18 | Route.post(':id/swagger', 'SwaggerController.parse') 19 | }).prefix('projects') 20 | -------------------------------------------------------------------------------- /start/routes/responses.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | 3 | Route.group(() => { 4 | Route.get(':id', 'ResponsesController.get') 5 | Route.put(':id', 'ResponsesController.edit') 6 | Route.delete(':id', 'ResponsesController.delete') 7 | Route.post(':id/enable', 'ResponsesController.enable') 8 | Route.post(':id/duplicate', 'ResponsesController.duplicate') 9 | Route.get(':id/headers', 'HeadersController.getList') 10 | Route.post(':id/headers', 'HeadersController.create') 11 | Route.get(':id/processor', 'ResponsesController.getProcessor') 12 | Route.post(':id/processor', 'ResponsesController.editProcessor') 13 | }).prefix('responses') 14 | -------------------------------------------------------------------------------- /start/routes/routes.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | 3 | Route.group(() => { 4 | Route.put(':id', 'RoutesController.edit') 5 | Route.get(':id', 'RoutesController.get') 6 | Route.delete(':id', 'RoutesController.delete') 7 | Route.post(':id/responses', 'ResponsesController.create') 8 | Route.get(':id/responses', 'ResponsesController.getList') 9 | Route.delete(':id/responses', 'ResponsesController.deleteMultiple') 10 | }).prefix('routes') 11 | -------------------------------------------------------------------------------- /start/routes/tokens.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | 3 | Route.group(() => { 4 | Route.delete(':id', 'TokensController.delete') 5 | }).prefix('tokens') 6 | -------------------------------------------------------------------------------- /start/routes/user.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | 3 | Route.group(() => { 4 | Route.get('', 'UserController.get') 5 | Route.post('login', 'UserController.login') 6 | Route.post('register', 'UserController.register') 7 | Route.get(':id/verify', 'UserController.verify').as('verifyEmail') 8 | Route.put('', 'UserController.edit') 9 | Route.put('email', 'UserController.editEmail').middleware('verificationDisabled') 10 | Route.post('resend', 'UserController.resendEmail').middleware('verificationDisabled') 11 | }).prefix('user') 12 | -------------------------------------------------------------------------------- /start/socket.ts: -------------------------------------------------------------------------------- 1 | import Ws from 'App/Services/Ws' 2 | Ws.boot() 3 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Tests 4 | |-------------------------------------------------------------------------- 5 | | 6 | | The contents in this file boots the AdonisJS application and configures 7 | | the Japa tests runner. 8 | | 9 | | For the most part you will never edit this file. The configuration 10 | | for the tests can be controlled via ".adonisrc.json" and 11 | | "tests/bootstrap.ts" files. 12 | | 13 | */ 14 | 15 | process.env.NODE_ENV = 'test' 16 | 17 | import 'reflect-metadata' 18 | import sourceMapSupport from 'source-map-support' 19 | import { Ignitor } from '@adonisjs/core/build/standalone' 20 | import { configure, processCliArgs, run, RunnerHooksHandler } from '@japa/runner' 21 | 22 | sourceMapSupport.install({ handleUncaughtExceptions: false }) 23 | 24 | const kernel = new Ignitor(__dirname).kernel('test') 25 | 26 | kernel 27 | .boot() 28 | .then(() => import('./tests/bootstrap')) 29 | .then(({ runnerHooks, ...config }) => { 30 | const app: RunnerHooksHandler[] = [() => kernel.start()] 31 | 32 | configure({ 33 | ...kernel.application.rcFile.tests, 34 | ...processCliArgs(process.argv.slice(2)), 35 | ...config, 36 | ...{ 37 | importer: (filePath) => import(filePath), 38 | setup: app.concat(runnerHooks.setup), 39 | teardown: runnerHooks.teardown, 40 | }, 41 | cwd: kernel.application.appRoot, 42 | }) 43 | 44 | run() 45 | }) 46 | -------------------------------------------------------------------------------- /tests/bootstrap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File source: https://bit.ly/3ukaHTz 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import type { Config } from '@japa/runner' 9 | import TestUtils from '@ioc:Adonis/Core/TestUtils' 10 | import { assert, runFailedTests, specReporter, apiClient } from '@japa/preset-adonis' 11 | 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Japa Plugins 15 | |-------------------------------------------------------------------------- 16 | | 17 | | Japa plugins allows you to add additional features to Japa. By default 18 | | we register the assertion plugin. 19 | | 20 | | Feel free to remove existing plugins or add more. 21 | | 22 | */ 23 | export const plugins: Required['plugins'] = [assert(), runFailedTests(), apiClient()] 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Japa Reporters 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Japa reporters displays/saves the progress of tests as they are executed. 31 | | By default, we register the spec reporter to show a detailed report 32 | | of tests on the terminal. 33 | | 34 | */ 35 | export const reporters: Required['reporters'] = [specReporter()] 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Runner hooks 40 | |-------------------------------------------------------------------------- 41 | | 42 | | Runner hooks are executed after booting the AdonisJS app and 43 | | before the test files are imported. 44 | | 45 | | You can perform actions like starting the HTTP server or running migrations 46 | | within the runner hooks 47 | | 48 | */ 49 | export const runnerHooks: Pick, 'setup' | 'teardown'> = { 50 | setup: [() => TestUtils.ace().loadCommands()], 51 | teardown: [], 52 | } 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Configure individual suites 57 | |-------------------------------------------------------------------------- 58 | | 59 | | The configureSuite method gets called for every test suite registered 60 | | within ".adonisrc.json" file. 61 | | 62 | | You can use this method to configure suites. For example: Only start 63 | | the HTTP server when it is a functional suite. 64 | */ 65 | export const configureSuite: Required['configureSuite'] = (suite) => { 66 | if (suite.name === 'functional') { 67 | suite.setup(() => TestUtils.httpServer().start()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/functional/hello_world.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@japa/runner' 2 | 3 | test('display welcome page', async ({ client }) => { 4 | const response = await client.get('/') 5 | 6 | response.assertStatus(200) 7 | response.assertBodyContains({ hello: 'world' }) 8 | }) 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "adonis-preset-ts/tsconfig.json", 3 | "include": [ 4 | "**/*" 5 | ], 6 | "exclude": [ 7 | "node_modules", 8 | "build" 9 | ], 10 | "compilerOptions": { 11 | "outDir": "build", 12 | "rootDir": "./", 13 | "sourceMap": true, 14 | "paths": { 15 | "App/*": [ 16 | "./app/*" 17 | ], 18 | "Config/*": [ 19 | "./config/*" 20 | ], 21 | "Contracts/*": [ 22 | "./contracts/*" 23 | ], 24 | "Database/*": [ 25 | "./database/*" 26 | ] 27 | }, 28 | "types": [ 29 | "@adonisjs/core", 30 | "@adonisjs/repl", 31 | "@japa/preset-adonis/build/adonis-typings", 32 | "@adonisjs/lucid", 33 | "@adonisjs/auth", 34 | "@adonisjs/mail", 35 | "@adonisjs/bouncer", 36 | "@adonisjs/i18n", 37 | "@adonisjs/drive-s3" 38 | ] 39 | } 40 | } 41 | --------------------------------------------------------------------------------