├── .env ├── .gitignore ├── .vscode └── extensions.json ├── Makefile ├── NodeJs Prisma.postman_collection.json ├── config ├── custom-environment-variables.ts └── default.ts ├── docker-compose.yml ├── package.json ├── pnpm-lock.yaml ├── prisma ├── migrations │ ├── 20220516201725_user_model │ │ └── migration.sql │ ├── 20220522190602_user_entity │ │ └── migration.sql │ ├── 20220523141621_user_entity │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── readMe.md ├── src ├── app.ts ├── controllers │ ├── auth.controller.ts │ └── user.controller.ts ├── middleware │ ├── deserializeUser.ts │ ├── requireUser.ts │ └── validate.ts ├── routes │ ├── auth.routes.ts │ └── user.routes.ts ├── schemas │ └── user.schema.ts ├── services │ └── user.service.ts ├── utils │ ├── appError.ts │ ├── connectRedis.ts │ ├── email.ts │ ├── jwt.ts │ └── validateEnv.ts └── views │ ├── _styles.pug │ ├── base.pug │ ├── resetPassword.pug │ └── verificationCode.pug └── tsconfig.json /.env: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB (Preview). 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL=postgresql://admin:password123@localhost:6500/node_prisma?schema=public 8 | 9 | PORT=8000 10 | NODE_ENV=development 11 | 12 | POSTGRES_HOST=127.0.0.1 13 | POSTGRES_PORT=6500 14 | POSTGRES_USER=admin 15 | POSTGRES_PASSWORD=password123 16 | POSTGRES_DB=node_prisma 17 | 18 | EMAIL_USER=uwb5bcxb2vbgvcz7@ethereal.email 19 | EMAIL_PASS=BXNStFGGWcnetC8MVM 20 | EMAIL_HOST=smtp.ethereal.email 21 | EMAIL_PORT=587 22 | 23 | JWT_ACCESS_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKSndJQkFBS0NBZ0VBb040aGtCOHJGYjhtVENNNm1tdVpHT0Rjekl4RnB0Q21SU1VJSlM3N0V2RkZUalg5CjdOQTVtMU41M0VLU1JVUGoxYmsxdEZvaHhpbEM3Y2p4Q2FhZStXay9PUFBsVW9hREtpcEVTNE1ZaUdZbVErT0kKc2NpN2tFTmYvd1FvMkhySFc2bzZJcWJ0RFVmRGZTdGJkRmxGaFNXZU5lUnpPanhGdHFrUFk4ejlod0grRWJBYQo4RFpJTzF5MnBucTllajV6cXFJUTZNWXZQOHcyakhjRmtpSmMxbVJlemNDR3pOcldmN1JwdExnYWdoN0tGM296CnB6VUVXTHZLSjJKTjVEeWdHbFl5dzBGVE5tZ0FXNjRkL1N1b0FRMWtERnQ4RzdHYXB0Mk0wdDVsaFBRaWdXZ1kKSHZ0ZHJjL24vSGhJQ0JMWFFJZW5tZWFpYWRKWGZiU1QwbnowbzF1MEthbnNMUVJMU2o0SHRHVGJDWEsxTlliaAorcnFqeTUvcHduSWZlOW5QV0Q1WUZoTHBlTytWNTFZbHJxV2xVLzhFb1J3WVE4RWZwMWpMaEpSYnRWTkFYczVjCnpyMXlkQ3N0L1BkOU5DQ2w3ZU9LWGw0Nk5icEVWTUpnVHp0WUd2R29JeE9td2Y1SmhrZDFlUFBnTlRtVmJ2WkkKeGU3ZkxXMHRYNHc3RHV4VHAzWlpUNXNxaW1QODVuU3dMNmlTQmg3eDIxSmp2ck94TkVGREl4aVJyZjNrZFgyUwpCcFlhTmV0MzFIVnJ4Y1l1WGtXcGlLdld6ekVOUlQ4Vno2MkNZc0dBVmF2UmFQRCtGMmFpQXQ1cFNMY0k4dmtGCkhwUFNRTzc4amdrNTNjeGZkdzR4TlJZZ1hzNVMwMFBuVmpGZ0d5SEpYaXFGS1g4Vk1hMFdYS1Rxc0tjQ0F3RUEKQVFLQ0FnQmdEbU9pS0VwVTBPL3dWTFRna2xFM25zV1pTa1VvNTd2dzI3enlYd3hDOGxoODdCWjB1cDA3VjJ3VApqcGpDelZnVlBXT0pEcXpPSThNNE0xMWljdURJY3dUeVlBaFY3OFlRZm02TFhIdGdyd1k0VVh4RkZHU1RMdmY2ClVhU0dKMEkxUlA4enc5TVdzL3hTR0hxaWpjL0tRbjBuRndPTENZUmpIcTMyM09laUh4bHp2R0ZKZ0ZBL1Nnd3kKZW1YN3dhRFZpYkpwSERZSWRVczVNclRiM3JMcXZjSXlQN3orUThvRE5WcE5OQVY0SHlSelFsL25mdXZhaEQrZwpwcjEyQkpGaFNJbEZPQk9WRUNMdW01aUNuUGZUc1FoRnJMYjkzbDVITjFHb2RHM1FKdXNYdEcxNnlvUFVqcW1xCmdyUVlSZy9NRTNTTG5FRHN5VkcrMlAzUG1SZUdKVVQzL3N0cXVrb1ZyZW9DUkxvL2RkR3IxU3NpWjcxaVhWam8KKzdrNFhuYWZKNWJ0QUdQd1h5b1JyUW1aaGQ1eE8rMkpRK1A4bDA0SlFtRU5CWkNNQVlGZmNpTXRLY0x0SmZzeQpkenBoNXFqSjdXSnlkVkdHdGlrT3JXcHVHTWJuVVRoZW8zamQxWkg1eEVXbkhiWlZ6N2hMWTdJQWlXN0k1OHlaCkZYakEzbnRQL01RTGM2aUJmRlloTU1yTUhOMVFqWll5Y2FENU95VWN3S3lXU1ZWUnNiTm0xMzNvS3dxVkQ2aWwKeWtIcDdlUUlOaXBVQ3hVT0RHRGIwZDlGNURCS0xlTHRvRXhIMVliYnh6bDJPZXZlb3VJcmMxVE02TzRtUGJmVgprUWhDaDBBT1g0aVNEZ3gvenRQQmlxNEJNWlowWXBma2RneXo5UEQxVHhqQjd1N0pNUUtDQVFFQTFjRzdZK2JhCnhsUUlzTnJoNlhDck5CZURCSUJWSXBLT0ZwWVJiZTZYaDVmeGhjOTVtOEFDdndQN1R3c0ZuUCt2WGVaZUV6algKRUhMOUdySFhlTjBGbmtIVjhqNEMvUk1jUElNNVpGRTFwMzcrdzJFZzRNNUZlbU1ScmVmRUYxZWxhV3dISkFCVgpZZS9jYksrZnNlQkZiaDVPZUxneCtSRkNIaGllb1N2UzJzUGdqditSaEFBUERtTkhPR2tJVzdzNVA1amltbUFqClNvN3lmNzU3Nk91WlBMUUVuR1NDT1ZkSWVjYm9rdHp6dE5rWkJ5SDc1QzVlbVRuVWhqak5sakU3OHRvVURpTkwKemJQUzU0NGxwSWhsSklFekhnQk1LUDFUUlJTUkxzaUhNOU8vc0JXdlJtYjZHbHlucktmc0o5VXRQUE42cDhBYwpBWlRtQ0xENi9udEtTUUtDQVFFQXdLaXA3RkxVNElZckZKWHc2bzZrSXYwNURJTDVPYUdpL3E5VlVvd1kwalRuCm9DQXNsRUR5d2dSeGsraHNGTERTaElNditCVzVqNU5QMXJ6cE0reThOdTV5MjBDeGlmVUw1Y3RMR3VBMDNqU0gKMTdpcmgrMkgvUXpzU3g3TXdvL1F1d0g1SHlxWHQrTEtWVDB3SkwzdE5ERTFINVV4NksrYjNJNmd3aDg5ZkpQZwpKNDdjU0daV2x4UlVXODJNZWswK1JtdnkrRDdIVEgzWlk3bnNVNjFQUUZDMVF2eGRQaVhBODZpbi9RUE11VzVuClJFOFViWWZ5Sk5kbzdyOVpac0M0THpGSkl6U05xRy91NXl6eThUemVsU1gwT0NWckNnTklTMmpWUUFtMHY2SzkKUTdMYlVLM1BEUCtPS284TVhJakFRN3FiN0tKdStNblJLcFpmYStHamJ3S0NBUUFIVW9TZHpubXVFNURjcGhMWgpFVS84RDlZZ0lGbEtQdVQ1SnhqZndDMldxb0RPaWFYcnpPOExKaExFeVdiZTB1OXA4d3REODAyQ3RiMnYzV201CkxURVZFOEpGY3hSZElQdDFvelVqRjVrVDdNakRYcDltdEFvelFCT0o5b2VFR1MrRVpmdjBvc0pLQVpKMmNxbXcKeVRMM3E3Z2FjSXpkc0Zhb2UwVnIxZmw2SHYyaFNHWHV0S1hQWTEwOE5IVGgxNDhGZTdJL2dZMlQ3ME9sVnB3ZQpCWHdSVlVWWEtpWS8ycHFUK1ZsY05vOXVvWVUyOWxZUG9mc0JtcmFLQm9ZY1lBRml0MCs2a0VEM2dmSjJsaXI1CjhPWXVFdkx5VXlaSXZTek9ELzlOTC9kWnNpWTkzUEJOSkJmck1VTlpLY053TmVBLzEwTmJYYnhYM1libncxQkYKeVdscEFvSUJBRHpLL0JPZWI1NEp1RDVlc2c3UncrbG1qa0hzY2toRlFQR3MwZmVpVFpMSEx1L1FOMER3M2FscgpUWXVtbzdSRHQ2b28yMUlsU05DTlkzd1hVRjhhTEtvSXhDVjdYSXB6MExWS1dRQzZEdlN2RHM1N2IyVU5Cd1JLCnQ2VmNxckFhUVh0QlBDTWhoWTU0K2UrN3h4MVE4VEN1OWx4bzc3N0U2eUhGditTQmh4MXdiQ01QYVdOUEo3TzAKcm9ucGFwS05IVUdMZnJmbTEyekRxWDFXaGNSQncrZy9qM0w3djFDRkNVWFp5bWlFY20xdzhSVVUveFV3K0srMwphVjBzOUVFQVB4NWxKdmI4OWxaWU84S201R0pMR3RGbGtKTEcrK2ZFOFdNYVNYQlRuNzE0MU5BditCU1ZRK2lPCkc0WkNiVndscXNXMGRNOU8wa1dmS1dtTzJiMXBEZkVDZ2dFQUlrOElESjVkSWF2b0FlekFLQjdPTVV1OXNaMVkKRDNCMWJheDNWd2t2bGRxR3VseGxwWUdwTnJUV29MUjBBS05MLys2dkRYTllETXVEN3I3SkZ1UThHVDgyRGp6OQpaU29iS2VCVmNHd0F0VVZ3eG92ZzVIVllmbW1YRFk2b3dRdEFGYzJkV0NSalpCeThuM1NVTlBLd3BycHBwQWpmCk5JWDlyUEZrazBVWUFram10YUVVYkNwNExndjdmWmJPZXh1dkRnMnpsWHErend1ZzZnNDkwTjEzQk1GMExveEsKY0xoQ3MzTlI3NlFLYlYzMU1xZHFoeUhEZTR6UHd6ZzVxQUVKY1JEV2JaT2c2Qy9ncVUyODE5ajJVWGFtN2hYeQpYYWpaRHlaUDNFRks0ZUw4QllSV1VJK0FIeDZrT29aSXYyR0pwbTM4MkZqNGZRTzR3NkdaUHpoM1BnPT0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0= 24 | JWT_ACCESS_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFvTjRoa0I4ckZiOG1UQ002bW11WgpHT0Rjekl4RnB0Q21SU1VJSlM3N0V2RkZUalg5N05BNW0xTjUzRUtTUlVQajFiazF0Rm9oeGlsQzdjanhDYWFlCitXay9PUFBsVW9hREtpcEVTNE1ZaUdZbVErT0lzY2k3a0VOZi93UW8ySHJIVzZvNklxYnREVWZEZlN0YmRGbEYKaFNXZU5lUnpPanhGdHFrUFk4ejlod0grRWJBYThEWklPMXkycG5xOWVqNXpxcUlRNk1ZdlA4dzJqSGNGa2lKYwoxbVJlemNDR3pOcldmN1JwdExnYWdoN0tGM296cHpVRVdMdktKMkpONUR5Z0dsWXl3MEZUTm1nQVc2NGQvU3VvCkFRMWtERnQ4RzdHYXB0Mk0wdDVsaFBRaWdXZ1lIdnRkcmMvbi9IaElDQkxYUUllbm1lYWlhZEpYZmJTVDBuejAKbzF1MEthbnNMUVJMU2o0SHRHVGJDWEsxTlliaCtycWp5NS9wd25JZmU5blBXRDVZRmhMcGVPK1Y1MVlscnFXbApVLzhFb1J3WVE4RWZwMWpMaEpSYnRWTkFYczVjenIxeWRDc3QvUGQ5TkNDbDdlT0tYbDQ2TmJwRVZNSmdUenRZCkd2R29JeE9td2Y1SmhrZDFlUFBnTlRtVmJ2Wkl4ZTdmTFcwdFg0dzdEdXhUcDNaWlQ1c3FpbVA4NW5Td0w2aVMKQmg3eDIxSmp2ck94TkVGREl4aVJyZjNrZFgyU0JwWWFOZXQzMUhWcnhjWXVYa1dwaUt2V3p6RU5SVDhWejYyQwpZc0dBVmF2UmFQRCtGMmFpQXQ1cFNMY0k4dmtGSHBQU1FPNzhqZ2s1M2N4ZmR3NHhOUllnWHM1UzAwUG5WakZnCkd5SEpYaXFGS1g4Vk1hMFdYS1Rxc0tjQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ== 25 | JWT_REFRESH_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKSmdJQkFBS0NBZ0IvcXhhMjAwNmFUQmNiSjZKV2pjb2tFTFUvcUtmcjNxSnhHQW5KV2FqSWx4N1ExOWRjCkFHakUzNkhYWVl4aW5hNis4TzZxdEJ1TVhuU3lwV1JvTDh0bE9XYXowaFE3RWxienpGNUVUMmErVkkrRGtic0wKTHhOUWoyWlJkRmhhYXk0bzZQNVFFSHg3OEVnZmU1MGpHMllOMnBFSk5ZRHdpUDlsblJVdy9zOGhzVFVCSkhsYwpHcEJ2b0ZMc0ZZMml2WWZqbzFxMk9GY00vR3M2VktmQ2RFR2RNdmRxUm56Ni83dGcvRUMrNjhnRzZ6bTIwN2dFCkhEb0Q3QXFHQVVwRjg3TEsrTFgyNFZ6UVhSZiswVzYxbkh4MWtyeE8xcWFURCttbENsVUl4MnNnbmZwdFQ2WHQKa2hOYms2VVZVSkxoalZiTzRJeG54aXBCY215ejFhWU1QZGFwOVNsNGE2TDIxaFAzeWpONnNvZVQ1ZGNBQ0NBdApCNnpzK0h0VUhqdnlJOGw4NThoMS9SZEQ5UThQVEJOWVd5VWRNN0Q3eHpVU1FIQnBiWmhRc0FOUHdJRWJiRGo2ClpOQXJDOXVpVGZ6M0Y0L1ZrVDZkbHZDcnlJdVRPL2pZMUErMmpVUVNoeUVQd3FQUnRjbWRKNHN6cC9JS1YxMW8KTmVrNjNUeFhrbURWKytObHFCcDBxeGpEYUdtMGhidzVQM1ZUNExtaWkzSUIzQjYzYytidk9HNEc0TWZOakQvUQp6MTk5ZDVvSU9mSThDNFhBUzhmdG1rVGdEb3NTWG5Ya3orVW94aWRtc0k1TzBPa2xwSGxCM2psVFVaekJOQ1dvClRBdWN4bFJ3MDlkZFVhSVpVckZlckhETjBSQ0VWQkJsSTBKeUozQkdHN2hKUFFDUy96NjlnK0RBc3dJREFRQUIKQW9JQ0FEeDhpTFJ5Qk1taHgwaThUSE9KbGYza1ZKc21ndmlEY0o3NGJwcVEraUNmU0tCVnd0ajU4S2xsOHIyaQpuK3FSSHB1a2RzODFxZCtsV05EUy8zRkZRNjVmdGNMNGcxRkJiU1R3N0xDY2xlY1RGVE9XdGZHa2loZ2UrTEVhCjJFbHVnbDRybFdIRmpkVUNrSWtwL2RjR1FpSEtMZkVaQWsveitGemlRWW9IRDRlODk0bmxDQUtWSzExWHJqSGcKUEV5Z1UyT01uNHU1d2JTTFd4bzF4WTJobXZ5cGl3TTFCajI5TDcvUmc3Tnc3a0lvN0Iwc3pnQlVoMEFZY3h2dQpQSXhMMjZmQ2tpTzN2a3VSNzY1dzkyemdDUytESitpS1ZqMVN5clhBNFd6WlhtNERxd1JqT2hBR3AxazU2SHJpCnc5Uk5MSld6V1pKSkZ2Z1BDR1Zqa2RvMjZoN0dsdFlhbCtXakFHbWhqSDFHWStwR2ZVcVdjeGZRNjVSM0gvbDEKOUYwOTFOK0tKUEJFMUxTNTZMem1DUk9hTXJDR2tZRFAwdUR5MEhTTDY1Q2xvUlpma0Y1KzJZT25GeURlV0k2RQpBMjFGZ2pMbVljZ1VNdHh3M3YrWm9OSkdlS1FSUW9VVXh3RVludEIzbVA3eEhmRUNtT1NjWVpienY0dzNEblNEClBZODg5VTU4MWEzRzFLcDhzODVHMkF0RlJKYlh6VkgxNFFNQndkV0VsM1lYTENwZ1lsWmZ3WWhWMHFNMnpnYTAKWXJmMWRsQ3VEenJpOE1qQXlCRFgwNVArUXJMS2UzaXVCQnYvTGhScHpoZVVwUDFSNHlUV1NHazVGdGZJb2YrYwpWa1pNWnRhcTZFMmdoRzVxSmtMSElwM1VlWUFDUjNraVBvRnFoQ0krbEg3TVd2TUJBb0lCQVFEclZLK2NXbEN4Cld6ckx0OUQzMUs4ZE8xeHM4SmxIdzM4QUEzUGlUQXQwVXpSa3NvelFBb05zT1JQOEVCTGc3YzJGc0NnQU1NRmYKU20yVngza0VLMHp4ZEdNNXdtL05yYWV2MDBJaDlpVnlYTlVQTTU4UTRWSXV5akc3QklaU3k4RmNyaWhybjhnWgpaUFpvTHZ0TTI3bDJIK0QvWlc4bEx0NnhFSVlaVitHZ3RIbTVCa21WSGp2S3BKbGU2K0p2bEgwRDYvRUsydUtvCnkxOHYwSGNoVEI0VHVsb3BtRFZkb2pKUFBZTldINXB2cStIMTArQzkyeWR0UDhqVWMvQUI1Q2xkejk3Wld0eVgKQ2x4V3RuMlJ6d0lEQjhQeWc1ZElHZ0E2Q01YTURNUTFiaG11bVcwYW5BS05YaGV3TTU3Q0c0K1N3RjgxMmQzaQovdGFrdGtzbTNxbHpBb0lCQVFDSzRhZ3l0eldjaUwrZHdTU2lrSmNJT21odGFkMk9nNkRWd1JGd2dkY1JkVms1ClcyQzZKQmRNSURYK0QxeDRSSlJRdElrSk1TRE1iUXJjcFhRdUdNZ21jQ2FpUzRRYTRUY0ViYmFXdzR3cXpVSVoKT3RmU3FaN3N3SGhXS1JzbnlTT29lZVZkUDRFVXJQTERyR3VGTWttcFZuOGNscE1oN0NScW5JdFVMaWFsSGl3OQo3aXZmaHN1Ym0zNnhqZG8wejlaeWNkVUhBVVl4SUNKZjZmMi8wWG9tMVFpbDJhOW01ZFV5TmtBOW1MZWF0Z1RpCm5CcVArRi9yZ2JqcENJbDkvYkRzbGZlSFNVTUJnaWJJcTJ3MGtSVzA0UlVmRi95NGpaaGxvU0dlMGVvSkJEUC8KVW5Yb2F0TVhtVEFRQVhrQWFjL21aYU5QUmR4cGNkZUJpb2VQMUx2QkFvSUJBRWQ1bkREUDJZeFVtaUgvdlc5WQpZS3R2T1I0NVlVVERvdElkT2xnMElwSjlKditpbjFRYVAxSmliNmh5TlZtdHdHdG5XbTdjWUE4U0t3Qi82a2lTCjRqYS8wOWRhTDl6M2JtUGZ0VWxkV25oR3E4dUhNVHZPUThqRVRxQnZ1bUJ4VkZSUzdTSHJFTHB0eU0xUjM2K3oKV0x3QTZMU0pXVjJndzdCaHhXanpJU3RQUlYwbUJMbE95bmc4ei94ZGhwTy9OTTA1SHlmZDNWUEpHU3NMbFBNLwpEQytQdlF5L0Z3NU9VaHR4SW0vSS8zNEZaWWJsaUhBQm5STDRlWVhWaE1MK1NuWWN5YUdZa0VFcnUybjIyV0tzCnpmNmZvQ3NMUHFwK2d5MGROdjVIcmx4UUNXclhYS1RsVjMwYVkvVjNhRE9xYlV2ME03YkdhamNra1lyTGR6M2kKQ1BNQ2dnRUFkQkFMVVg3Y2xkbU9VUmJsTjlvZjdlZHA5KzRlRXdpYnM2Y3FuTnhWVUtqQ0RQWDVpdlc0Wm8wUQpzaVpDUDl3SUdyNFZPNUpMOTZzMUJacGIwbDR4ZmpZQmcyWWFtNkFWSzJWSXp3dlNxcjVvZTg4NktEcEtyMDVXCkFxTk5wMTBucUpkZUFDU2ZDaGxTdEFYMndSUjZqTUxWYlozRWpmZmJveUQ2S3pwNlgwa3M3Z0NKdDc3ZGowaE8KZzdHcFpyTStZdHdRakU4YlN0aXhBWGo1NzREdlZWNTVZS2pnTmR6aTQ1aUU3VFVnVTNIRCtXRTlyWTJERTd5UQpQR2MvQ0FTb0E3OThZaG51bm1jWFpHSW1CblF0TU1GZHRtT0Vkdk1XSmZDWHVUTnlJMkRZMTRvaE1UOTg4OTY0ClZOZjJPRXMxVERVcVdvaG1uOVJjcGt5UDU3cC93UUtDQVFBSHJVODBreHZ4MkVyNFlOYW5YYnRSNDlWbHFUQWIKSzkwNm1qUFRRL0NlVjVBaVpLZ1pUZVhhZS9BYUhLYUdQTXBHZE5yQkN1eWt5UjJ3dWpXTXZkTDUyUkdLVnEzNQplQ2NuTjF3L0RWZnRYUzhWSlZtd1JCRkd5K1hGZnAwWmRzUnpGWWJTa1c3QzNLbXJhMU9MMTZia2FaRDJQaEdMClN4QVpjaFk2Qml5YW9vSUM5YlhpZXpxeVQ2R3YwZUpuM2U1djNlSjBtZit5a3RzcmVtaWxIYkZaY0ora1FvakMKWG5ZOVFVNTU3TmxBVWhhZmwwaEtWSGNPN0wwWGI5ajBZeEZmSTZoa1R1QVJwZFpYa28wQnNrcE45ZjA5OFhHcwp1OHVHdHBaR000Wkk0NEFZZkN0bXBrOVM1OTJDeEQ2U0I3SENJaXJFN3JvTk1RTVF6YzhwTjkreAotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ== 26 | JWT_REFRESH_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lUQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnNEFNSUlDQ1FLQ0FnQi9xeGEyMDA2YVRCY2JKNkpXamNvawpFTFUvcUtmcjNxSnhHQW5KV2FqSWx4N1ExOWRjQUdqRTM2SFhZWXhpbmE2KzhPNnF0QnVNWG5TeXBXUm9MOHRsCk9XYXowaFE3RWxienpGNUVUMmErVkkrRGtic0xMeE5RajJaUmRGaGFheTRvNlA1UUVIeDc4RWdmZTUwakcyWU4KMnBFSk5ZRHdpUDlsblJVdy9zOGhzVFVCSkhsY0dwQnZvRkxzRlkyaXZZZmpvMXEyT0ZjTS9HczZWS2ZDZEVHZApNdmRxUm56Ni83dGcvRUMrNjhnRzZ6bTIwN2dFSERvRDdBcUdBVXBGODdMSytMWDI0VnpRWFJmKzBXNjFuSHgxCmtyeE8xcWFURCttbENsVUl4MnNnbmZwdFQ2WHRraE5iazZVVlVKTGhqVmJPNEl4bnhpcEJjbXl6MWFZTVBkYXAKOVNsNGE2TDIxaFAzeWpONnNvZVQ1ZGNBQ0NBdEI2enMrSHRVSGp2eUk4bDg1OGgxL1JkRDlROFBUQk5ZV3lVZApNN0Q3eHpVU1FIQnBiWmhRc0FOUHdJRWJiRGo2Wk5BckM5dWlUZnozRjQvVmtUNmRsdkNyeUl1VE8valkxQSsyCmpVUVNoeUVQd3FQUnRjbWRKNHN6cC9JS1YxMW9OZWs2M1R4WGttRFYrK05scUJwMHF4akRhR20waGJ3NVAzVlQKNExtaWkzSUIzQjYzYytidk9HNEc0TWZOakQvUXoxOTlkNW9JT2ZJOEM0WEFTOGZ0bWtUZ0Rvc1NYblhreitVbwp4aWRtc0k1TzBPa2xwSGxCM2psVFVaekJOQ1dvVEF1Y3hsUncwOWRkVWFJWlVyRmVySEROMFJDRVZCQmxJMEp5CkozQkdHN2hKUFFDUy96NjlnK0RBc3dJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | # .env 4 | 5 | build/ 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Prisma.prisma"] 3 | } 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker-compose up -d 3 | stop: 4 | docker-compose down 5 | stop-v: 6 | docker-compose down -v -------------------------------------------------------------------------------- /NodeJs Prisma.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "fe997961-5444-4a90-9ed2-46da1ff1b2d2", 4 | "name": "NodeJs Prisma", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "14791724" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Auth", 11 | "item": [ 12 | { 13 | "name": "Register", 14 | "request": { 15 | "method": "POST", 16 | "header": [], 17 | "body": { 18 | "mode": "raw", 19 | "raw": "{\r\n \"email\": \"admin@admin.com\",\r\n \"name\": \"Admin\",\r\n \"password\": \"password123\",\r\n \"passwordConfirm\": \"password123\",\r\n \"photo\": \"default.png\"\r\n}", 20 | "options": { 21 | "raw": { 22 | "language": "json" 23 | } 24 | } 25 | }, 26 | "url": { 27 | "raw": "{{host}}/api/auth/register", 28 | "host": [ 29 | "{{host}}" 30 | ], 31 | "path": [ 32 | "api", 33 | "auth", 34 | "register" 35 | ] 36 | } 37 | }, 38 | "response": [] 39 | }, 40 | { 41 | "name": "Refresh Token", 42 | "request": { 43 | "method": "GET", 44 | "header": [], 45 | "url": { 46 | "raw": "{{host}}/api/auth/refresh", 47 | "host": [ 48 | "{{host}}" 49 | ], 50 | "path": [ 51 | "api", 52 | "auth", 53 | "refresh" 54 | ] 55 | } 56 | }, 57 | "response": [] 58 | }, 59 | { 60 | "name": "Forgot Password", 61 | "request": { 62 | "method": "POST", 63 | "header": [], 64 | "body": { 65 | "mode": "raw", 66 | "raw": "{\r\n \"email\": \"admin@admin.com\"\r\n}", 67 | "options": { 68 | "raw": { 69 | "language": "json" 70 | } 71 | } 72 | }, 73 | "url": { 74 | "raw": "{{host}}/api/auth/forgotpassword", 75 | "host": [ 76 | "{{host}}" 77 | ], 78 | "path": [ 79 | "api", 80 | "auth", 81 | "forgotpassword" 82 | ] 83 | } 84 | }, 85 | "response": [] 86 | }, 87 | { 88 | "name": "Reset Password", 89 | "request": { 90 | "method": "PATCH", 91 | "header": [], 92 | "body": { 93 | "mode": "raw", 94 | "raw": "{\r\n \"password\": \"mynewpassword123\",\r\n \"passwordConfirm\": \"mynewpassword123\"\r\n}", 95 | "options": { 96 | "raw": { 97 | "language": "json" 98 | } 99 | } 100 | }, 101 | "url": { 102 | "raw": "{{host}}/api/auth/resetpassword/9720a8d38a8ab24f04cfa27ca4979f0b63f5ed1501272a2b2f7b250ea66ac502", 103 | "host": [ 104 | "{{host}}" 105 | ], 106 | "path": [ 107 | "api", 108 | "auth", 109 | "resetpassword", 110 | "9720a8d38a8ab24f04cfa27ca4979f0b63f5ed1501272a2b2f7b250ea66ac502" 111 | ] 112 | } 113 | }, 114 | "response": [] 115 | }, 116 | { 117 | "name": "Logout", 118 | "request": { 119 | "method": "GET", 120 | "header": [], 121 | "url": { 122 | "raw": "{{host}}/api/auth/logout", 123 | "host": [ 124 | "{{host}}" 125 | ], 126 | "path": [ 127 | "api", 128 | "auth", 129 | "logout" 130 | ] 131 | } 132 | }, 133 | "response": [] 134 | }, 135 | { 136 | "name": "Login", 137 | "request": { 138 | "method": "POST", 139 | "header": [], 140 | "body": { 141 | "mode": "raw", 142 | "raw": "{\r\n \"email\": \"admin@admin.com\",\r\n \"password\": \"password123\"\r\n}", 143 | "options": { 144 | "raw": { 145 | "language": "json" 146 | } 147 | } 148 | }, 149 | "url": { 150 | "raw": "{{host}}/api/auth/login", 151 | "host": [ 152 | "{{host}}" 153 | ], 154 | "path": [ 155 | "api", 156 | "auth", 157 | "login" 158 | ] 159 | } 160 | }, 161 | "response": [] 162 | }, 163 | { 164 | "name": "Verify Email Address", 165 | "request": { 166 | "method": "GET", 167 | "header": [], 168 | "url": { 169 | "raw": "{{host}}/api/auth/verifyemail/ee3ed38963942a7450a08565917c09f182b6844d93f26d6254873095c678e964", 170 | "host": [ 171 | "{{host}}" 172 | ], 173 | "path": [ 174 | "api", 175 | "auth", 176 | "verifyemail", 177 | "ee3ed38963942a7450a08565917c09f182b6844d93f26d6254873095c678e964" 178 | ] 179 | } 180 | }, 181 | "response": [] 182 | } 183 | ] 184 | }, 185 | { 186 | "name": "User", 187 | "item": [ 188 | { 189 | "name": "Get Me", 190 | "request": { 191 | "auth": { 192 | "type": "noauth" 193 | }, 194 | "method": "GET", 195 | "header": [], 196 | "url": { 197 | "raw": "{{host}}/api/users/me", 198 | "host": [ 199 | "{{host}}" 200 | ], 201 | "path": [ 202 | "api", 203 | "users", 204 | "me" 205 | ] 206 | } 207 | }, 208 | "response": [] 209 | } 210 | ] 211 | }, 212 | { 213 | "name": "Post", 214 | "item": [ 215 | { 216 | "name": "Create Post", 217 | "request": { 218 | "method": "POST", 219 | "header": [], 220 | "body": { 221 | "mode": "raw", 222 | "raw": "{\r\n \"title\": \"My first 7 demo post always\",\r\n \"content\": \"My content haha My content haha\",\r\n \"image\": \"default.png\"\r\n}", 223 | "options": { 224 | "raw": { 225 | "language": "json" 226 | } 227 | } 228 | }, 229 | "url": { 230 | "raw": "{{host}}/api/posts", 231 | "host": [ 232 | "{{host}}" 233 | ], 234 | "path": [ 235 | "api", 236 | "posts" 237 | ] 238 | } 239 | }, 240 | "response": [] 241 | }, 242 | { 243 | "name": "Get Post", 244 | "request": { 245 | "method": "GET", 246 | "header": [], 247 | "url": { 248 | "raw": "{{host}}/api/posts/3b680b7d-dc05-4df4-937d-fe61d663ef40", 249 | "host": [ 250 | "{{host}}" 251 | ], 252 | "path": [ 253 | "api", 254 | "posts", 255 | "3b680b7d-dc05-4df4-937d-fe61d663ef40" 256 | ] 257 | } 258 | }, 259 | "response": [] 260 | }, 261 | { 262 | "name": "Update Post", 263 | "request": { 264 | "method": "PATCH", 265 | "header": [], 266 | "body": { 267 | "mode": "raw", 268 | "raw": "{\r\n \"title\": \"Update post 6 with new tile\"\r\n}", 269 | "options": { 270 | "raw": { 271 | "language": "json" 272 | } 273 | } 274 | }, 275 | "url": { 276 | "raw": "{{host}}/api/posts/3b680b7d-dc05-4df4-937d-fe61d663ef40", 277 | "host": [ 278 | "{{host}}" 279 | ], 280 | "path": [ 281 | "api", 282 | "posts", 283 | "3b680b7d-dc05-4df4-937d-fe61d663ef40" 284 | ] 285 | } 286 | }, 287 | "response": [] 288 | }, 289 | { 290 | "name": "Delete Post", 291 | "request": { 292 | "method": "DELETE", 293 | "header": [], 294 | "url": { 295 | "raw": "{{host}}/api/posts/80746953-a5c3-48ec-bb20-326ede6e0af1", 296 | "host": [ 297 | "{{host}}" 298 | ], 299 | "path": [ 300 | "api", 301 | "posts", 302 | "80746953-a5c3-48ec-bb20-326ede6e0af1" 303 | ] 304 | } 305 | }, 306 | "response": [] 307 | }, 308 | { 309 | "name": "Get All Posts", 310 | "request": { 311 | "method": "GET", 312 | "header": [], 313 | "url": { 314 | "raw": "{{host}}/api/posts?page=1&limit=10", 315 | "host": [ 316 | "{{host}}" 317 | ], 318 | "path": [ 319 | "api", 320 | "posts" 321 | ], 322 | "query": [ 323 | { 324 | "key": "page", 325 | "value": "1" 326 | }, 327 | { 328 | "key": "limit", 329 | "value": "10" 330 | } 331 | ] 332 | } 333 | }, 334 | "response": [] 335 | } 336 | ] 337 | } 338 | ] 339 | } -------------------------------------------------------------------------------- /config/custom-environment-variables.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | port: 'PORT', 3 | 4 | accessTokenPrivateKey: 'JWT_ACCESS_TOKEN_PRIVATE_KEY', 5 | accessTokenPublicKey: 'JWT_ACCESS_TOKEN_PUBLIC_KEY', 6 | refreshTokenPrivateKey: 'JWT_REFRESH_TOKEN_PRIVATE_KEY', 7 | refreshTokenPublicKey: 'JWT_REFRESH_TOKEN_PUBLIC_KEY', 8 | 9 | smtp: { 10 | host: 'EMAIL_HOST', 11 | pass: 'EMAIL_PASS', 12 | port: 'EMAIL_PORT', 13 | user: 'EMAIL_USER', 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /config/default.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | redisCacheExpiresIn: 60, 3 | refreshTokenExpiresIn: 60, 4 | accessTokenExpiresIn: 15, 5 | origin: 'http://localhost:3000', 6 | }; 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | postgres: 4 | image: postgres:latest 5 | container_name: postgres 6 | ports: 7 | - '6500:5432' 8 | volumes: 9 | - progresDB:/data/postgres 10 | env_file: 11 | - ./.env 12 | 13 | redis: 14 | image: redis:alpine 15 | container_name: redis 16 | ports: 17 | - '6379:6379' 18 | volumes: 19 | - redisDB:/data 20 | volumes: 21 | progresDB: 22 | redisDB: 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node_prisma", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts", 8 | "db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate", 9 | "db:push": "npx prisma db push", 10 | "build": "tsc . -p" 11 | }, 12 | "devDependencies": { 13 | "@types/bcryptjs": "^2.4.6", 14 | "@types/config": "^3.3.3", 15 | "@types/cookie-parser": "^1.4.6", 16 | "@types/cors": "^2.8.17", 17 | "@types/express": "^4.17.21", 18 | "@types/html-to-text": "^9.0.4", 19 | "@types/jsonwebtoken": "^9.0.5", 20 | "@types/lodash": "^4.14.202", 21 | "@types/morgan": "^1.9.9", 22 | "@types/node": "^20.11.18", 23 | "@types/nodemailer": "^6.4.14", 24 | "@types/pug": "^2.0.10", 25 | "prisma": "^5.9.1", 26 | "typescript": "^5.3.3" 27 | }, 28 | "dependencies": { 29 | "@prisma/client": "^5.9.1", 30 | "bcryptjs": "^2.4.3", 31 | "config": "^3.3.11", 32 | "cookie-parser": "^1.4.6", 33 | "cors": "^2.8.5", 34 | "dotenv": "^16.4.4", 35 | "envalid": "^8.0.0", 36 | "express": "^4.18.2", 37 | "html-to-text": "^9.0.5", 38 | "jsonwebtoken": "^9.0.2", 39 | "lodash": "^4.17.21", 40 | "morgan": "^1.10.0", 41 | "nodemailer": "^6.9.9", 42 | "pug": "^3.0.2", 43 | "redis": "^4.6.13", 44 | "ts-node-dev": "^2.0.0", 45 | "zod": "^3.22.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | '@prisma/client': 9 | specifier: ^5.9.1 10 | version: 5.9.1(prisma@5.9.1) 11 | bcryptjs: 12 | specifier: ^2.4.3 13 | version: 2.4.3 14 | config: 15 | specifier: ^3.3.11 16 | version: 3.3.11 17 | cookie-parser: 18 | specifier: ^1.4.6 19 | version: 1.4.6 20 | cors: 21 | specifier: ^2.8.5 22 | version: 2.8.5 23 | dotenv: 24 | specifier: ^16.4.4 25 | version: 16.4.4 26 | envalid: 27 | specifier: ^8.0.0 28 | version: 8.0.0 29 | express: 30 | specifier: ^4.18.2 31 | version: 4.18.2 32 | html-to-text: 33 | specifier: ^9.0.5 34 | version: 9.0.5 35 | jsonwebtoken: 36 | specifier: ^9.0.2 37 | version: 9.0.2 38 | lodash: 39 | specifier: ^4.17.21 40 | version: 4.17.21 41 | morgan: 42 | specifier: ^1.10.0 43 | version: 1.10.0 44 | nodemailer: 45 | specifier: ^6.9.9 46 | version: 6.9.9 47 | pug: 48 | specifier: ^3.0.2 49 | version: 3.0.2 50 | redis: 51 | specifier: ^4.6.13 52 | version: 4.6.13 53 | ts-node-dev: 54 | specifier: ^2.0.0 55 | version: 2.0.0(@types/node@20.11.18)(typescript@5.3.3) 56 | zod: 57 | specifier: ^3.22.4 58 | version: 3.22.4 59 | 60 | devDependencies: 61 | '@types/bcryptjs': 62 | specifier: ^2.4.6 63 | version: 2.4.6 64 | '@types/config': 65 | specifier: ^3.3.3 66 | version: 3.3.3 67 | '@types/cookie-parser': 68 | specifier: ^1.4.6 69 | version: 1.4.6 70 | '@types/cors': 71 | specifier: ^2.8.17 72 | version: 2.8.17 73 | '@types/express': 74 | specifier: ^4.17.21 75 | version: 4.17.21 76 | '@types/html-to-text': 77 | specifier: ^9.0.4 78 | version: 9.0.4 79 | '@types/jsonwebtoken': 80 | specifier: ^9.0.5 81 | version: 9.0.5 82 | '@types/lodash': 83 | specifier: ^4.14.202 84 | version: 4.14.202 85 | '@types/morgan': 86 | specifier: ^1.9.9 87 | version: 1.9.9 88 | '@types/node': 89 | specifier: ^20.11.18 90 | version: 20.11.18 91 | '@types/nodemailer': 92 | specifier: ^6.4.14 93 | version: 6.4.14 94 | '@types/pug': 95 | specifier: ^2.0.10 96 | version: 2.0.10 97 | prisma: 98 | specifier: ^5.9.1 99 | version: 5.9.1 100 | typescript: 101 | specifier: ^5.3.3 102 | version: 5.3.3 103 | 104 | packages: 105 | 106 | /@babel/helper-string-parser@7.23.4: 107 | resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} 108 | engines: {node: '>=6.9.0'} 109 | dev: false 110 | 111 | /@babel/helper-validator-identifier@7.22.20: 112 | resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} 113 | engines: {node: '>=6.9.0'} 114 | dev: false 115 | 116 | /@babel/parser@7.23.9: 117 | resolution: {integrity: sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==} 118 | engines: {node: '>=6.0.0'} 119 | hasBin: true 120 | dependencies: 121 | '@babel/types': 7.23.9 122 | dev: false 123 | 124 | /@babel/types@7.23.9: 125 | resolution: {integrity: sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==} 126 | engines: {node: '>=6.9.0'} 127 | dependencies: 128 | '@babel/helper-string-parser': 7.23.4 129 | '@babel/helper-validator-identifier': 7.22.20 130 | to-fast-properties: 2.0.0 131 | dev: false 132 | 133 | /@cspotcode/source-map-support@0.8.1: 134 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 135 | engines: {node: '>=12'} 136 | dependencies: 137 | '@jridgewell/trace-mapping': 0.3.9 138 | dev: false 139 | 140 | /@jridgewell/resolve-uri@3.1.2: 141 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 142 | engines: {node: '>=6.0.0'} 143 | dev: false 144 | 145 | /@jridgewell/sourcemap-codec@1.4.15: 146 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 147 | dev: false 148 | 149 | /@jridgewell/trace-mapping@0.3.9: 150 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 151 | dependencies: 152 | '@jridgewell/resolve-uri': 3.1.2 153 | '@jridgewell/sourcemap-codec': 1.4.15 154 | dev: false 155 | 156 | /@prisma/client@5.9.1(prisma@5.9.1): 157 | resolution: {integrity: sha512-caSOnG4kxcSkhqC/2ShV7rEoWwd3XrftokxJqOCMVvia4NYV/TPtJlS9C2os3Igxw/Qyxumj9GBQzcStzECvtQ==} 158 | engines: {node: '>=16.13'} 159 | requiresBuild: true 160 | peerDependencies: 161 | prisma: '*' 162 | peerDependenciesMeta: 163 | prisma: 164 | optional: true 165 | dependencies: 166 | prisma: 5.9.1 167 | dev: false 168 | 169 | /@prisma/debug@5.9.1: 170 | resolution: {integrity: sha512-yAHFSFCg8KVoL0oRUno3m60GAjsUKYUDkQ+9BA2X2JfVR3kRVSJFc/GpQ2fSORi4pSHZR9orfM4UC9OVXIFFTA==} 171 | 172 | /@prisma/engines-version@5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64: 173 | resolution: {integrity: sha512-HFl7275yF0FWbdcNvcSRbbu9JCBSLMcurYwvWc8WGDnpu7APxQo2ONtZrUggU3WxLxUJ2uBX+0GOFIcJeVeOOQ==} 174 | 175 | /@prisma/engines@5.9.1: 176 | resolution: {integrity: sha512-gkdXmjxQ5jktxWNdDA5aZZ6R8rH74JkoKq6LD5mACSvxd2vbqWeWIOV0Py5wFC8vofOYShbt6XUeCIUmrOzOnQ==} 177 | requiresBuild: true 178 | dependencies: 179 | '@prisma/debug': 5.9.1 180 | '@prisma/engines-version': 5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64 181 | '@prisma/fetch-engine': 5.9.1 182 | '@prisma/get-platform': 5.9.1 183 | 184 | /@prisma/fetch-engine@5.9.1: 185 | resolution: {integrity: sha512-l0goQOMcNVOJs1kAcwqpKq3ylvkD9F04Ioe1oJoCqmz05mw22bNAKKGWuDd3zTUoUZr97va0c/UfLNru+PDmNA==} 186 | dependencies: 187 | '@prisma/debug': 5.9.1 188 | '@prisma/engines-version': 5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64 189 | '@prisma/get-platform': 5.9.1 190 | 191 | /@prisma/get-platform@5.9.1: 192 | resolution: {integrity: sha512-6OQsNxTyhvG+T2Ksr8FPFpuPeL4r9u0JF0OZHUBI/Uy9SS43sPyAIutt4ZEAyqWQt104ERh70EZedkHZKsnNbg==} 193 | dependencies: 194 | '@prisma/debug': 5.9.1 195 | 196 | /@redis/bloom@1.2.0(@redis/client@1.5.14): 197 | resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} 198 | peerDependencies: 199 | '@redis/client': ^1.0.0 200 | dependencies: 201 | '@redis/client': 1.5.14 202 | dev: false 203 | 204 | /@redis/client@1.5.14: 205 | resolution: {integrity: sha512-YGn0GqsRBFUQxklhY7v562VMOP0DcmlrHHs3IV1mFE3cbxe31IITUkqhBcIhVSI/2JqtWAJXg5mjV4aU+zD0HA==} 206 | engines: {node: '>=14'} 207 | dependencies: 208 | cluster-key-slot: 1.1.2 209 | generic-pool: 3.9.0 210 | yallist: 4.0.0 211 | dev: false 212 | 213 | /@redis/graph@1.1.1(@redis/client@1.5.14): 214 | resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} 215 | peerDependencies: 216 | '@redis/client': ^1.0.0 217 | dependencies: 218 | '@redis/client': 1.5.14 219 | dev: false 220 | 221 | /@redis/json@1.0.6(@redis/client@1.5.14): 222 | resolution: {integrity: sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==} 223 | peerDependencies: 224 | '@redis/client': ^1.0.0 225 | dependencies: 226 | '@redis/client': 1.5.14 227 | dev: false 228 | 229 | /@redis/search@1.1.6(@redis/client@1.5.14): 230 | resolution: {integrity: sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw==} 231 | peerDependencies: 232 | '@redis/client': ^1.0.0 233 | dependencies: 234 | '@redis/client': 1.5.14 235 | dev: false 236 | 237 | /@redis/time-series@1.0.5(@redis/client@1.5.14): 238 | resolution: {integrity: sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==} 239 | peerDependencies: 240 | '@redis/client': ^1.0.0 241 | dependencies: 242 | '@redis/client': 1.5.14 243 | dev: false 244 | 245 | /@selderee/plugin-htmlparser2@0.11.0: 246 | resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} 247 | dependencies: 248 | domhandler: 5.0.3 249 | selderee: 0.11.0 250 | dev: false 251 | 252 | /@tsconfig/node10@1.0.9: 253 | resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} 254 | dev: false 255 | 256 | /@tsconfig/node12@1.0.11: 257 | resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} 258 | dev: false 259 | 260 | /@tsconfig/node14@1.0.3: 261 | resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} 262 | dev: false 263 | 264 | /@tsconfig/node16@1.0.4: 265 | resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} 266 | dev: false 267 | 268 | /@types/bcryptjs@2.4.6: 269 | resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} 270 | dev: true 271 | 272 | /@types/body-parser@1.19.5: 273 | resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} 274 | dependencies: 275 | '@types/connect': 3.4.38 276 | '@types/node': 20.11.18 277 | dev: true 278 | 279 | /@types/config@3.3.3: 280 | resolution: {integrity: sha512-BB8DBAud88EgiAKlz8WQStzI771Kb6F3j4dioRJ4GD+tP4tzcZyMlz86aXuZT4s9hyesFORehMQE6eqtA5O+Vg==} 281 | dev: true 282 | 283 | /@types/connect@3.4.38: 284 | resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} 285 | dependencies: 286 | '@types/node': 20.11.18 287 | dev: true 288 | 289 | /@types/cookie-parser@1.4.6: 290 | resolution: {integrity: sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==} 291 | dependencies: 292 | '@types/express': 4.17.21 293 | dev: true 294 | 295 | /@types/cors@2.8.17: 296 | resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} 297 | dependencies: 298 | '@types/node': 20.11.18 299 | dev: true 300 | 301 | /@types/express-serve-static-core@4.17.43: 302 | resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} 303 | dependencies: 304 | '@types/node': 20.11.18 305 | '@types/qs': 6.9.11 306 | '@types/range-parser': 1.2.7 307 | '@types/send': 0.17.4 308 | dev: true 309 | 310 | /@types/express@4.17.21: 311 | resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} 312 | dependencies: 313 | '@types/body-parser': 1.19.5 314 | '@types/express-serve-static-core': 4.17.43 315 | '@types/qs': 6.9.11 316 | '@types/serve-static': 1.15.5 317 | dev: true 318 | 319 | /@types/html-to-text@9.0.4: 320 | resolution: {integrity: sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==} 321 | dev: true 322 | 323 | /@types/http-errors@2.0.4: 324 | resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} 325 | dev: true 326 | 327 | /@types/jsonwebtoken@9.0.5: 328 | resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} 329 | dependencies: 330 | '@types/node': 20.11.18 331 | dev: true 332 | 333 | /@types/lodash@4.14.202: 334 | resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} 335 | dev: true 336 | 337 | /@types/mime@1.3.5: 338 | resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} 339 | dev: true 340 | 341 | /@types/mime@3.0.4: 342 | resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} 343 | dev: true 344 | 345 | /@types/morgan@1.9.9: 346 | resolution: {integrity: sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==} 347 | dependencies: 348 | '@types/node': 20.11.18 349 | dev: true 350 | 351 | /@types/node@20.11.18: 352 | resolution: {integrity: sha512-ABT5VWnnYneSBcNWYSCuR05M826RoMyMSGiFivXGx6ZUIsXb9vn4643IEwkg2zbEOSgAiSogtapN2fgc4mAPlw==} 353 | dependencies: 354 | undici-types: 5.26.5 355 | 356 | /@types/nodemailer@6.4.14: 357 | resolution: {integrity: sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==} 358 | dependencies: 359 | '@types/node': 20.11.18 360 | dev: true 361 | 362 | /@types/pug@2.0.10: 363 | resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} 364 | dev: true 365 | 366 | /@types/qs@6.9.11: 367 | resolution: {integrity: sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==} 368 | dev: true 369 | 370 | /@types/range-parser@1.2.7: 371 | resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} 372 | dev: true 373 | 374 | /@types/send@0.17.4: 375 | resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} 376 | dependencies: 377 | '@types/mime': 1.3.5 378 | '@types/node': 20.11.18 379 | dev: true 380 | 381 | /@types/serve-static@1.15.5: 382 | resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==} 383 | dependencies: 384 | '@types/http-errors': 2.0.4 385 | '@types/mime': 3.0.4 386 | '@types/node': 20.11.18 387 | dev: true 388 | 389 | /@types/strip-bom@3.0.0: 390 | resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} 391 | dev: false 392 | 393 | /@types/strip-json-comments@0.0.30: 394 | resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} 395 | dev: false 396 | 397 | /accepts@1.3.8: 398 | resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} 399 | engines: {node: '>= 0.6'} 400 | dependencies: 401 | mime-types: 2.1.35 402 | negotiator: 0.6.3 403 | dev: false 404 | 405 | /acorn-walk@8.3.2: 406 | resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} 407 | engines: {node: '>=0.4.0'} 408 | dev: false 409 | 410 | /acorn@7.4.1: 411 | resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} 412 | engines: {node: '>=0.4.0'} 413 | hasBin: true 414 | dev: false 415 | 416 | /acorn@8.11.3: 417 | resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} 418 | engines: {node: '>=0.4.0'} 419 | hasBin: true 420 | dev: false 421 | 422 | /anymatch@3.1.3: 423 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 424 | engines: {node: '>= 8'} 425 | dependencies: 426 | normalize-path: 3.0.0 427 | picomatch: 2.3.1 428 | dev: false 429 | 430 | /arg@4.1.3: 431 | resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} 432 | dev: false 433 | 434 | /array-flatten@1.1.1: 435 | resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} 436 | dev: false 437 | 438 | /asap@2.0.6: 439 | resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} 440 | dev: false 441 | 442 | /assert-never@1.2.1: 443 | resolution: {integrity: sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==} 444 | dev: false 445 | 446 | /babel-walk@3.0.0-canary-5: 447 | resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} 448 | engines: {node: '>= 10.0.0'} 449 | dependencies: 450 | '@babel/types': 7.23.9 451 | dev: false 452 | 453 | /balanced-match@1.0.2: 454 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 455 | dev: false 456 | 457 | /basic-auth@2.0.1: 458 | resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} 459 | engines: {node: '>= 0.8'} 460 | dependencies: 461 | safe-buffer: 5.1.2 462 | dev: false 463 | 464 | /bcryptjs@2.4.3: 465 | resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} 466 | dev: false 467 | 468 | /binary-extensions@2.2.0: 469 | resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} 470 | engines: {node: '>=8'} 471 | dev: false 472 | 473 | /body-parser@1.20.1: 474 | resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} 475 | engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 476 | dependencies: 477 | bytes: 3.1.2 478 | content-type: 1.0.5 479 | debug: 2.6.9 480 | depd: 2.0.0 481 | destroy: 1.2.0 482 | http-errors: 2.0.0 483 | iconv-lite: 0.4.24 484 | on-finished: 2.4.1 485 | qs: 6.11.0 486 | raw-body: 2.5.1 487 | type-is: 1.6.18 488 | unpipe: 1.0.0 489 | transitivePeerDependencies: 490 | - supports-color 491 | dev: false 492 | 493 | /brace-expansion@1.1.11: 494 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 495 | dependencies: 496 | balanced-match: 1.0.2 497 | concat-map: 0.0.1 498 | dev: false 499 | 500 | /braces@3.0.2: 501 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 502 | engines: {node: '>=8'} 503 | dependencies: 504 | fill-range: 7.0.1 505 | dev: false 506 | 507 | /buffer-equal-constant-time@1.0.1: 508 | resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} 509 | dev: false 510 | 511 | /buffer-from@1.1.2: 512 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 513 | dev: false 514 | 515 | /bytes@3.1.2: 516 | resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} 517 | engines: {node: '>= 0.8'} 518 | dev: false 519 | 520 | /call-bind@1.0.7: 521 | resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} 522 | engines: {node: '>= 0.4'} 523 | dependencies: 524 | es-define-property: 1.0.0 525 | es-errors: 1.3.0 526 | function-bind: 1.1.2 527 | get-intrinsic: 1.2.4 528 | set-function-length: 1.2.1 529 | dev: false 530 | 531 | /character-parser@2.2.0: 532 | resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} 533 | dependencies: 534 | is-regex: 1.1.4 535 | dev: false 536 | 537 | /chokidar@3.6.0: 538 | resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 539 | engines: {node: '>= 8.10.0'} 540 | dependencies: 541 | anymatch: 3.1.3 542 | braces: 3.0.2 543 | glob-parent: 5.1.2 544 | is-binary-path: 2.1.0 545 | is-glob: 4.0.3 546 | normalize-path: 3.0.0 547 | readdirp: 3.6.0 548 | optionalDependencies: 549 | fsevents: 2.3.3 550 | dev: false 551 | 552 | /cluster-key-slot@1.1.2: 553 | resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} 554 | engines: {node: '>=0.10.0'} 555 | dev: false 556 | 557 | /concat-map@0.0.1: 558 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 559 | dev: false 560 | 561 | /config@3.3.11: 562 | resolution: {integrity: sha512-Dhn63ZoWCW5EMg4P0Sl/XNsj/7RLiUIA1x1npCy+m2cRwRHzLnt3UtYtxRDMZW/6oOMdWhCzaGYkOcajGgrAOA==} 563 | engines: {node: '>= 10.0.0'} 564 | dependencies: 565 | json5: 2.2.3 566 | dev: false 567 | 568 | /constantinople@4.0.1: 569 | resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} 570 | dependencies: 571 | '@babel/parser': 7.23.9 572 | '@babel/types': 7.23.9 573 | dev: false 574 | 575 | /content-disposition@0.5.4: 576 | resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} 577 | engines: {node: '>= 0.6'} 578 | dependencies: 579 | safe-buffer: 5.2.1 580 | dev: false 581 | 582 | /content-type@1.0.5: 583 | resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 584 | engines: {node: '>= 0.6'} 585 | dev: false 586 | 587 | /cookie-parser@1.4.6: 588 | resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==} 589 | engines: {node: '>= 0.8.0'} 590 | dependencies: 591 | cookie: 0.4.1 592 | cookie-signature: 1.0.6 593 | dev: false 594 | 595 | /cookie-signature@1.0.6: 596 | resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} 597 | dev: false 598 | 599 | /cookie@0.4.1: 600 | resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} 601 | engines: {node: '>= 0.6'} 602 | dev: false 603 | 604 | /cookie@0.5.0: 605 | resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} 606 | engines: {node: '>= 0.6'} 607 | dev: false 608 | 609 | /cors@2.8.5: 610 | resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} 611 | engines: {node: '>= 0.10'} 612 | dependencies: 613 | object-assign: 4.1.1 614 | vary: 1.1.2 615 | dev: false 616 | 617 | /create-require@1.1.1: 618 | resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} 619 | dev: false 620 | 621 | /debug@2.6.9: 622 | resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} 623 | peerDependencies: 624 | supports-color: '*' 625 | peerDependenciesMeta: 626 | supports-color: 627 | optional: true 628 | dependencies: 629 | ms: 2.0.0 630 | dev: false 631 | 632 | /deepmerge@4.3.1: 633 | resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 634 | engines: {node: '>=0.10.0'} 635 | dev: false 636 | 637 | /define-data-property@1.1.4: 638 | resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} 639 | engines: {node: '>= 0.4'} 640 | dependencies: 641 | es-define-property: 1.0.0 642 | es-errors: 1.3.0 643 | gopd: 1.0.1 644 | dev: false 645 | 646 | /depd@2.0.0: 647 | resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 648 | engines: {node: '>= 0.8'} 649 | dev: false 650 | 651 | /destroy@1.2.0: 652 | resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} 653 | engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 654 | dev: false 655 | 656 | /diff@4.0.2: 657 | resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} 658 | engines: {node: '>=0.3.1'} 659 | dev: false 660 | 661 | /doctypes@1.1.0: 662 | resolution: {integrity: sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==} 663 | dev: false 664 | 665 | /dom-serializer@2.0.0: 666 | resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} 667 | dependencies: 668 | domelementtype: 2.3.0 669 | domhandler: 5.0.3 670 | entities: 4.5.0 671 | dev: false 672 | 673 | /domelementtype@2.3.0: 674 | resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} 675 | dev: false 676 | 677 | /domhandler@5.0.3: 678 | resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} 679 | engines: {node: '>= 4'} 680 | dependencies: 681 | domelementtype: 2.3.0 682 | dev: false 683 | 684 | /domutils@3.1.0: 685 | resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} 686 | dependencies: 687 | dom-serializer: 2.0.0 688 | domelementtype: 2.3.0 689 | domhandler: 5.0.3 690 | dev: false 691 | 692 | /dotenv@16.4.4: 693 | resolution: {integrity: sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg==} 694 | engines: {node: '>=12'} 695 | dev: false 696 | 697 | /dynamic-dedupe@0.3.0: 698 | resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} 699 | dependencies: 700 | xtend: 4.0.2 701 | dev: false 702 | 703 | /ecdsa-sig-formatter@1.0.11: 704 | resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} 705 | dependencies: 706 | safe-buffer: 5.2.1 707 | dev: false 708 | 709 | /ee-first@1.1.1: 710 | resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} 711 | dev: false 712 | 713 | /encodeurl@1.0.2: 714 | resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} 715 | engines: {node: '>= 0.8'} 716 | dev: false 717 | 718 | /entities@4.5.0: 719 | resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 720 | engines: {node: '>=0.12'} 721 | dev: false 722 | 723 | /envalid@8.0.0: 724 | resolution: {integrity: sha512-PGeYJnJB5naN0ME6SH8nFcDj9HVbLpYIfg1p5lAyM9T4cH2lwtu2fLbozC/bq+HUUOIFxhX/LP0/GmlqPHT4tQ==} 725 | engines: {node: '>=8.12'} 726 | dependencies: 727 | tslib: 2.6.2 728 | dev: false 729 | 730 | /es-define-property@1.0.0: 731 | resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} 732 | engines: {node: '>= 0.4'} 733 | dependencies: 734 | get-intrinsic: 1.2.4 735 | dev: false 736 | 737 | /es-errors@1.3.0: 738 | resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 739 | engines: {node: '>= 0.4'} 740 | dev: false 741 | 742 | /escape-html@1.0.3: 743 | resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 744 | dev: false 745 | 746 | /etag@1.8.1: 747 | resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 748 | engines: {node: '>= 0.6'} 749 | dev: false 750 | 751 | /express@4.18.2: 752 | resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} 753 | engines: {node: '>= 0.10.0'} 754 | dependencies: 755 | accepts: 1.3.8 756 | array-flatten: 1.1.1 757 | body-parser: 1.20.1 758 | content-disposition: 0.5.4 759 | content-type: 1.0.5 760 | cookie: 0.5.0 761 | cookie-signature: 1.0.6 762 | debug: 2.6.9 763 | depd: 2.0.0 764 | encodeurl: 1.0.2 765 | escape-html: 1.0.3 766 | etag: 1.8.1 767 | finalhandler: 1.2.0 768 | fresh: 0.5.2 769 | http-errors: 2.0.0 770 | merge-descriptors: 1.0.1 771 | methods: 1.1.2 772 | on-finished: 2.4.1 773 | parseurl: 1.3.3 774 | path-to-regexp: 0.1.7 775 | proxy-addr: 2.0.7 776 | qs: 6.11.0 777 | range-parser: 1.2.1 778 | safe-buffer: 5.2.1 779 | send: 0.18.0 780 | serve-static: 1.15.0 781 | setprototypeof: 1.2.0 782 | statuses: 2.0.1 783 | type-is: 1.6.18 784 | utils-merge: 1.0.1 785 | vary: 1.1.2 786 | transitivePeerDependencies: 787 | - supports-color 788 | dev: false 789 | 790 | /fill-range@7.0.1: 791 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 792 | engines: {node: '>=8'} 793 | dependencies: 794 | to-regex-range: 5.0.1 795 | dev: false 796 | 797 | /finalhandler@1.2.0: 798 | resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} 799 | engines: {node: '>= 0.8'} 800 | dependencies: 801 | debug: 2.6.9 802 | encodeurl: 1.0.2 803 | escape-html: 1.0.3 804 | on-finished: 2.4.1 805 | parseurl: 1.3.3 806 | statuses: 2.0.1 807 | unpipe: 1.0.0 808 | transitivePeerDependencies: 809 | - supports-color 810 | dev: false 811 | 812 | /forwarded@0.2.0: 813 | resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} 814 | engines: {node: '>= 0.6'} 815 | dev: false 816 | 817 | /fresh@0.5.2: 818 | resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} 819 | engines: {node: '>= 0.6'} 820 | dev: false 821 | 822 | /fs.realpath@1.0.0: 823 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 824 | dev: false 825 | 826 | /fsevents@2.3.3: 827 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 828 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 829 | os: [darwin] 830 | requiresBuild: true 831 | dev: false 832 | optional: true 833 | 834 | /function-bind@1.1.2: 835 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 836 | dev: false 837 | 838 | /generic-pool@3.9.0: 839 | resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} 840 | engines: {node: '>= 4'} 841 | dev: false 842 | 843 | /get-intrinsic@1.2.4: 844 | resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} 845 | engines: {node: '>= 0.4'} 846 | dependencies: 847 | es-errors: 1.3.0 848 | function-bind: 1.1.2 849 | has-proto: 1.0.1 850 | has-symbols: 1.0.3 851 | hasown: 2.0.1 852 | dev: false 853 | 854 | /glob-parent@5.1.2: 855 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 856 | engines: {node: '>= 6'} 857 | dependencies: 858 | is-glob: 4.0.3 859 | dev: false 860 | 861 | /glob@7.2.3: 862 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 863 | dependencies: 864 | fs.realpath: 1.0.0 865 | inflight: 1.0.6 866 | inherits: 2.0.4 867 | minimatch: 3.1.2 868 | once: 1.4.0 869 | path-is-absolute: 1.0.1 870 | dev: false 871 | 872 | /gopd@1.0.1: 873 | resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} 874 | dependencies: 875 | get-intrinsic: 1.2.4 876 | dev: false 877 | 878 | /has-property-descriptors@1.0.2: 879 | resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} 880 | dependencies: 881 | es-define-property: 1.0.0 882 | dev: false 883 | 884 | /has-proto@1.0.1: 885 | resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} 886 | engines: {node: '>= 0.4'} 887 | dev: false 888 | 889 | /has-symbols@1.0.3: 890 | resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} 891 | engines: {node: '>= 0.4'} 892 | dev: false 893 | 894 | /has-tostringtag@1.0.2: 895 | resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} 896 | engines: {node: '>= 0.4'} 897 | dependencies: 898 | has-symbols: 1.0.3 899 | dev: false 900 | 901 | /hasown@2.0.1: 902 | resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==} 903 | engines: {node: '>= 0.4'} 904 | dependencies: 905 | function-bind: 1.1.2 906 | dev: false 907 | 908 | /html-to-text@9.0.5: 909 | resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} 910 | engines: {node: '>=14'} 911 | dependencies: 912 | '@selderee/plugin-htmlparser2': 0.11.0 913 | deepmerge: 4.3.1 914 | dom-serializer: 2.0.0 915 | htmlparser2: 8.0.2 916 | selderee: 0.11.0 917 | dev: false 918 | 919 | /htmlparser2@8.0.2: 920 | resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} 921 | dependencies: 922 | domelementtype: 2.3.0 923 | domhandler: 5.0.3 924 | domutils: 3.1.0 925 | entities: 4.5.0 926 | dev: false 927 | 928 | /http-errors@2.0.0: 929 | resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} 930 | engines: {node: '>= 0.8'} 931 | dependencies: 932 | depd: 2.0.0 933 | inherits: 2.0.4 934 | setprototypeof: 1.2.0 935 | statuses: 2.0.1 936 | toidentifier: 1.0.1 937 | dev: false 938 | 939 | /iconv-lite@0.4.24: 940 | resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} 941 | engines: {node: '>=0.10.0'} 942 | dependencies: 943 | safer-buffer: 2.1.2 944 | dev: false 945 | 946 | /inflight@1.0.6: 947 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 948 | dependencies: 949 | once: 1.4.0 950 | wrappy: 1.0.2 951 | dev: false 952 | 953 | /inherits@2.0.4: 954 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 955 | dev: false 956 | 957 | /ipaddr.js@1.9.1: 958 | resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} 959 | engines: {node: '>= 0.10'} 960 | dev: false 961 | 962 | /is-binary-path@2.1.0: 963 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 964 | engines: {node: '>=8'} 965 | dependencies: 966 | binary-extensions: 2.2.0 967 | dev: false 968 | 969 | /is-core-module@2.13.1: 970 | resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} 971 | dependencies: 972 | hasown: 2.0.1 973 | dev: false 974 | 975 | /is-expression@4.0.0: 976 | resolution: {integrity: sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==} 977 | dependencies: 978 | acorn: 7.4.1 979 | object-assign: 4.1.1 980 | dev: false 981 | 982 | /is-extglob@2.1.1: 983 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 984 | engines: {node: '>=0.10.0'} 985 | dev: false 986 | 987 | /is-glob@4.0.3: 988 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 989 | engines: {node: '>=0.10.0'} 990 | dependencies: 991 | is-extglob: 2.1.1 992 | dev: false 993 | 994 | /is-number@7.0.0: 995 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 996 | engines: {node: '>=0.12.0'} 997 | dev: false 998 | 999 | /is-promise@2.2.2: 1000 | resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} 1001 | dev: false 1002 | 1003 | /is-regex@1.1.4: 1004 | resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} 1005 | engines: {node: '>= 0.4'} 1006 | dependencies: 1007 | call-bind: 1.0.7 1008 | has-tostringtag: 1.0.2 1009 | dev: false 1010 | 1011 | /js-stringify@1.0.2: 1012 | resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==} 1013 | dev: false 1014 | 1015 | /json5@2.2.3: 1016 | resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 1017 | engines: {node: '>=6'} 1018 | hasBin: true 1019 | dev: false 1020 | 1021 | /jsonwebtoken@9.0.2: 1022 | resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} 1023 | engines: {node: '>=12', npm: '>=6'} 1024 | dependencies: 1025 | jws: 3.2.2 1026 | lodash.includes: 4.3.0 1027 | lodash.isboolean: 3.0.3 1028 | lodash.isinteger: 4.0.4 1029 | lodash.isnumber: 3.0.3 1030 | lodash.isplainobject: 4.0.6 1031 | lodash.isstring: 4.0.1 1032 | lodash.once: 4.1.1 1033 | ms: 2.1.3 1034 | semver: 7.6.0 1035 | dev: false 1036 | 1037 | /jstransformer@1.0.0: 1038 | resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} 1039 | dependencies: 1040 | is-promise: 2.2.2 1041 | promise: 7.3.1 1042 | dev: false 1043 | 1044 | /jwa@1.4.1: 1045 | resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} 1046 | dependencies: 1047 | buffer-equal-constant-time: 1.0.1 1048 | ecdsa-sig-formatter: 1.0.11 1049 | safe-buffer: 5.2.1 1050 | dev: false 1051 | 1052 | /jws@3.2.2: 1053 | resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} 1054 | dependencies: 1055 | jwa: 1.4.1 1056 | safe-buffer: 5.2.1 1057 | dev: false 1058 | 1059 | /leac@0.6.0: 1060 | resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} 1061 | dev: false 1062 | 1063 | /lodash.includes@4.3.0: 1064 | resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} 1065 | dev: false 1066 | 1067 | /lodash.isboolean@3.0.3: 1068 | resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} 1069 | dev: false 1070 | 1071 | /lodash.isinteger@4.0.4: 1072 | resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} 1073 | dev: false 1074 | 1075 | /lodash.isnumber@3.0.3: 1076 | resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} 1077 | dev: false 1078 | 1079 | /lodash.isplainobject@4.0.6: 1080 | resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} 1081 | dev: false 1082 | 1083 | /lodash.isstring@4.0.1: 1084 | resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} 1085 | dev: false 1086 | 1087 | /lodash.once@4.1.1: 1088 | resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} 1089 | dev: false 1090 | 1091 | /lodash@4.17.21: 1092 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 1093 | dev: false 1094 | 1095 | /lru-cache@6.0.0: 1096 | resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} 1097 | engines: {node: '>=10'} 1098 | dependencies: 1099 | yallist: 4.0.0 1100 | dev: false 1101 | 1102 | /make-error@1.3.6: 1103 | resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} 1104 | dev: false 1105 | 1106 | /media-typer@0.3.0: 1107 | resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} 1108 | engines: {node: '>= 0.6'} 1109 | dev: false 1110 | 1111 | /merge-descriptors@1.0.1: 1112 | resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} 1113 | dev: false 1114 | 1115 | /methods@1.1.2: 1116 | resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} 1117 | engines: {node: '>= 0.6'} 1118 | dev: false 1119 | 1120 | /mime-db@1.52.0: 1121 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 1122 | engines: {node: '>= 0.6'} 1123 | dev: false 1124 | 1125 | /mime-types@2.1.35: 1126 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 1127 | engines: {node: '>= 0.6'} 1128 | dependencies: 1129 | mime-db: 1.52.0 1130 | dev: false 1131 | 1132 | /mime@1.6.0: 1133 | resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} 1134 | engines: {node: '>=4'} 1135 | hasBin: true 1136 | dev: false 1137 | 1138 | /minimatch@3.1.2: 1139 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 1140 | dependencies: 1141 | brace-expansion: 1.1.11 1142 | dev: false 1143 | 1144 | /minimist@1.2.8: 1145 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 1146 | dev: false 1147 | 1148 | /mkdirp@1.0.4: 1149 | resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} 1150 | engines: {node: '>=10'} 1151 | hasBin: true 1152 | dev: false 1153 | 1154 | /morgan@1.10.0: 1155 | resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} 1156 | engines: {node: '>= 0.8.0'} 1157 | dependencies: 1158 | basic-auth: 2.0.1 1159 | debug: 2.6.9 1160 | depd: 2.0.0 1161 | on-finished: 2.3.0 1162 | on-headers: 1.0.2 1163 | transitivePeerDependencies: 1164 | - supports-color 1165 | dev: false 1166 | 1167 | /ms@2.0.0: 1168 | resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} 1169 | dev: false 1170 | 1171 | /ms@2.1.3: 1172 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 1173 | dev: false 1174 | 1175 | /negotiator@0.6.3: 1176 | resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} 1177 | engines: {node: '>= 0.6'} 1178 | dev: false 1179 | 1180 | /nodemailer@6.9.9: 1181 | resolution: {integrity: sha512-dexTll8zqQoVJEZPwQAKzxxtFn0qTnjdQTchoU6Re9BUUGBJiOy3YMn/0ShTW6J5M0dfQ1NeDeRTTl4oIWgQMA==} 1182 | engines: {node: '>=6.0.0'} 1183 | dev: false 1184 | 1185 | /normalize-path@3.0.0: 1186 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 1187 | engines: {node: '>=0.10.0'} 1188 | dev: false 1189 | 1190 | /object-assign@4.1.1: 1191 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 1192 | engines: {node: '>=0.10.0'} 1193 | dev: false 1194 | 1195 | /object-inspect@1.13.1: 1196 | resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} 1197 | dev: false 1198 | 1199 | /on-finished@2.3.0: 1200 | resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} 1201 | engines: {node: '>= 0.8'} 1202 | dependencies: 1203 | ee-first: 1.1.1 1204 | dev: false 1205 | 1206 | /on-finished@2.4.1: 1207 | resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} 1208 | engines: {node: '>= 0.8'} 1209 | dependencies: 1210 | ee-first: 1.1.1 1211 | dev: false 1212 | 1213 | /on-headers@1.0.2: 1214 | resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} 1215 | engines: {node: '>= 0.8'} 1216 | dev: false 1217 | 1218 | /once@1.4.0: 1219 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 1220 | dependencies: 1221 | wrappy: 1.0.2 1222 | dev: false 1223 | 1224 | /parseley@0.12.1: 1225 | resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} 1226 | dependencies: 1227 | leac: 0.6.0 1228 | peberminta: 0.9.0 1229 | dev: false 1230 | 1231 | /parseurl@1.3.3: 1232 | resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} 1233 | engines: {node: '>= 0.8'} 1234 | dev: false 1235 | 1236 | /path-is-absolute@1.0.1: 1237 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 1238 | engines: {node: '>=0.10.0'} 1239 | dev: false 1240 | 1241 | /path-parse@1.0.7: 1242 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 1243 | dev: false 1244 | 1245 | /path-to-regexp@0.1.7: 1246 | resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} 1247 | dev: false 1248 | 1249 | /peberminta@0.9.0: 1250 | resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} 1251 | dev: false 1252 | 1253 | /picomatch@2.3.1: 1254 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 1255 | engines: {node: '>=8.6'} 1256 | dev: false 1257 | 1258 | /prisma@5.9.1: 1259 | resolution: {integrity: sha512-Hy/8KJZz0ELtkw4FnG9MS9rNWlXcJhf98Z2QMqi0QiVMoS8PzsBkpla0/Y5hTlob8F3HeECYphBjqmBxrluUrQ==} 1260 | engines: {node: '>=16.13'} 1261 | hasBin: true 1262 | requiresBuild: true 1263 | dependencies: 1264 | '@prisma/engines': 5.9.1 1265 | 1266 | /promise@7.3.1: 1267 | resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} 1268 | dependencies: 1269 | asap: 2.0.6 1270 | dev: false 1271 | 1272 | /proxy-addr@2.0.7: 1273 | resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} 1274 | engines: {node: '>= 0.10'} 1275 | dependencies: 1276 | forwarded: 0.2.0 1277 | ipaddr.js: 1.9.1 1278 | dev: false 1279 | 1280 | /pug-attrs@3.0.0: 1281 | resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==} 1282 | dependencies: 1283 | constantinople: 4.0.1 1284 | js-stringify: 1.0.2 1285 | pug-runtime: 3.0.1 1286 | dev: false 1287 | 1288 | /pug-code-gen@3.0.2: 1289 | resolution: {integrity: sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==} 1290 | dependencies: 1291 | constantinople: 4.0.1 1292 | doctypes: 1.1.0 1293 | js-stringify: 1.0.2 1294 | pug-attrs: 3.0.0 1295 | pug-error: 2.0.0 1296 | pug-runtime: 3.0.1 1297 | void-elements: 3.1.0 1298 | with: 7.0.2 1299 | dev: false 1300 | 1301 | /pug-error@2.0.0: 1302 | resolution: {integrity: sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==} 1303 | dev: false 1304 | 1305 | /pug-filters@4.0.0: 1306 | resolution: {integrity: sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==} 1307 | dependencies: 1308 | constantinople: 4.0.1 1309 | jstransformer: 1.0.0 1310 | pug-error: 2.0.0 1311 | pug-walk: 2.0.0 1312 | resolve: 1.22.8 1313 | dev: false 1314 | 1315 | /pug-lexer@5.0.1: 1316 | resolution: {integrity: sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==} 1317 | dependencies: 1318 | character-parser: 2.2.0 1319 | is-expression: 4.0.0 1320 | pug-error: 2.0.0 1321 | dev: false 1322 | 1323 | /pug-linker@4.0.0: 1324 | resolution: {integrity: sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==} 1325 | dependencies: 1326 | pug-error: 2.0.0 1327 | pug-walk: 2.0.0 1328 | dev: false 1329 | 1330 | /pug-load@3.0.0: 1331 | resolution: {integrity: sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==} 1332 | dependencies: 1333 | object-assign: 4.1.1 1334 | pug-walk: 2.0.0 1335 | dev: false 1336 | 1337 | /pug-parser@6.0.0: 1338 | resolution: {integrity: sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==} 1339 | dependencies: 1340 | pug-error: 2.0.0 1341 | token-stream: 1.0.0 1342 | dev: false 1343 | 1344 | /pug-runtime@3.0.1: 1345 | resolution: {integrity: sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==} 1346 | dev: false 1347 | 1348 | /pug-strip-comments@2.0.0: 1349 | resolution: {integrity: sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==} 1350 | dependencies: 1351 | pug-error: 2.0.0 1352 | dev: false 1353 | 1354 | /pug-walk@2.0.0: 1355 | resolution: {integrity: sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==} 1356 | dev: false 1357 | 1358 | /pug@3.0.2: 1359 | resolution: {integrity: sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==} 1360 | dependencies: 1361 | pug-code-gen: 3.0.2 1362 | pug-filters: 4.0.0 1363 | pug-lexer: 5.0.1 1364 | pug-linker: 4.0.0 1365 | pug-load: 3.0.0 1366 | pug-parser: 6.0.0 1367 | pug-runtime: 3.0.1 1368 | pug-strip-comments: 2.0.0 1369 | dev: false 1370 | 1371 | /qs@6.11.0: 1372 | resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} 1373 | engines: {node: '>=0.6'} 1374 | dependencies: 1375 | side-channel: 1.0.5 1376 | dev: false 1377 | 1378 | /range-parser@1.2.1: 1379 | resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} 1380 | engines: {node: '>= 0.6'} 1381 | dev: false 1382 | 1383 | /raw-body@2.5.1: 1384 | resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} 1385 | engines: {node: '>= 0.8'} 1386 | dependencies: 1387 | bytes: 3.1.2 1388 | http-errors: 2.0.0 1389 | iconv-lite: 0.4.24 1390 | unpipe: 1.0.0 1391 | dev: false 1392 | 1393 | /readdirp@3.6.0: 1394 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 1395 | engines: {node: '>=8.10.0'} 1396 | dependencies: 1397 | picomatch: 2.3.1 1398 | dev: false 1399 | 1400 | /redis@4.6.13: 1401 | resolution: {integrity: sha512-MHgkS4B+sPjCXpf+HfdetBwbRz6vCtsceTmw1pHNYJAsYxrfpOP6dz+piJWGos8wqG7qb3vj/Rrc5qOlmInUuA==} 1402 | dependencies: 1403 | '@redis/bloom': 1.2.0(@redis/client@1.5.14) 1404 | '@redis/client': 1.5.14 1405 | '@redis/graph': 1.1.1(@redis/client@1.5.14) 1406 | '@redis/json': 1.0.6(@redis/client@1.5.14) 1407 | '@redis/search': 1.1.6(@redis/client@1.5.14) 1408 | '@redis/time-series': 1.0.5(@redis/client@1.5.14) 1409 | dev: false 1410 | 1411 | /resolve@1.22.8: 1412 | resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} 1413 | hasBin: true 1414 | dependencies: 1415 | is-core-module: 2.13.1 1416 | path-parse: 1.0.7 1417 | supports-preserve-symlinks-flag: 1.0.0 1418 | dev: false 1419 | 1420 | /rimraf@2.7.1: 1421 | resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} 1422 | hasBin: true 1423 | dependencies: 1424 | glob: 7.2.3 1425 | dev: false 1426 | 1427 | /safe-buffer@5.1.2: 1428 | resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} 1429 | dev: false 1430 | 1431 | /safe-buffer@5.2.1: 1432 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 1433 | dev: false 1434 | 1435 | /safer-buffer@2.1.2: 1436 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 1437 | dev: false 1438 | 1439 | /selderee@0.11.0: 1440 | resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} 1441 | dependencies: 1442 | parseley: 0.12.1 1443 | dev: false 1444 | 1445 | /semver@7.6.0: 1446 | resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} 1447 | engines: {node: '>=10'} 1448 | hasBin: true 1449 | dependencies: 1450 | lru-cache: 6.0.0 1451 | dev: false 1452 | 1453 | /send@0.18.0: 1454 | resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} 1455 | engines: {node: '>= 0.8.0'} 1456 | dependencies: 1457 | debug: 2.6.9 1458 | depd: 2.0.0 1459 | destroy: 1.2.0 1460 | encodeurl: 1.0.2 1461 | escape-html: 1.0.3 1462 | etag: 1.8.1 1463 | fresh: 0.5.2 1464 | http-errors: 2.0.0 1465 | mime: 1.6.0 1466 | ms: 2.1.3 1467 | on-finished: 2.4.1 1468 | range-parser: 1.2.1 1469 | statuses: 2.0.1 1470 | transitivePeerDependencies: 1471 | - supports-color 1472 | dev: false 1473 | 1474 | /serve-static@1.15.0: 1475 | resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} 1476 | engines: {node: '>= 0.8.0'} 1477 | dependencies: 1478 | encodeurl: 1.0.2 1479 | escape-html: 1.0.3 1480 | parseurl: 1.3.3 1481 | send: 0.18.0 1482 | transitivePeerDependencies: 1483 | - supports-color 1484 | dev: false 1485 | 1486 | /set-function-length@1.2.1: 1487 | resolution: {integrity: sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==} 1488 | engines: {node: '>= 0.4'} 1489 | dependencies: 1490 | define-data-property: 1.1.4 1491 | es-errors: 1.3.0 1492 | function-bind: 1.1.2 1493 | get-intrinsic: 1.2.4 1494 | gopd: 1.0.1 1495 | has-property-descriptors: 1.0.2 1496 | dev: false 1497 | 1498 | /setprototypeof@1.2.0: 1499 | resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} 1500 | dev: false 1501 | 1502 | /side-channel@1.0.5: 1503 | resolution: {integrity: sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==} 1504 | engines: {node: '>= 0.4'} 1505 | dependencies: 1506 | call-bind: 1.0.7 1507 | es-errors: 1.3.0 1508 | get-intrinsic: 1.2.4 1509 | object-inspect: 1.13.1 1510 | dev: false 1511 | 1512 | /source-map-support@0.5.21: 1513 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 1514 | dependencies: 1515 | buffer-from: 1.1.2 1516 | source-map: 0.6.1 1517 | dev: false 1518 | 1519 | /source-map@0.6.1: 1520 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 1521 | engines: {node: '>=0.10.0'} 1522 | dev: false 1523 | 1524 | /statuses@2.0.1: 1525 | resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} 1526 | engines: {node: '>= 0.8'} 1527 | dev: false 1528 | 1529 | /strip-bom@3.0.0: 1530 | resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} 1531 | engines: {node: '>=4'} 1532 | dev: false 1533 | 1534 | /strip-json-comments@2.0.1: 1535 | resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} 1536 | engines: {node: '>=0.10.0'} 1537 | dev: false 1538 | 1539 | /supports-preserve-symlinks-flag@1.0.0: 1540 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 1541 | engines: {node: '>= 0.4'} 1542 | dev: false 1543 | 1544 | /to-fast-properties@2.0.0: 1545 | resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} 1546 | engines: {node: '>=4'} 1547 | dev: false 1548 | 1549 | /to-regex-range@5.0.1: 1550 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1551 | engines: {node: '>=8.0'} 1552 | dependencies: 1553 | is-number: 7.0.0 1554 | dev: false 1555 | 1556 | /toidentifier@1.0.1: 1557 | resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 1558 | engines: {node: '>=0.6'} 1559 | dev: false 1560 | 1561 | /token-stream@1.0.0: 1562 | resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} 1563 | dev: false 1564 | 1565 | /tree-kill@1.2.2: 1566 | resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 1567 | hasBin: true 1568 | dev: false 1569 | 1570 | /ts-node-dev@2.0.0(@types/node@20.11.18)(typescript@5.3.3): 1571 | resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} 1572 | engines: {node: '>=0.8.0'} 1573 | hasBin: true 1574 | peerDependencies: 1575 | node-notifier: '*' 1576 | typescript: '*' 1577 | peerDependenciesMeta: 1578 | node-notifier: 1579 | optional: true 1580 | dependencies: 1581 | chokidar: 3.6.0 1582 | dynamic-dedupe: 0.3.0 1583 | minimist: 1.2.8 1584 | mkdirp: 1.0.4 1585 | resolve: 1.22.8 1586 | rimraf: 2.7.1 1587 | source-map-support: 0.5.21 1588 | tree-kill: 1.2.2 1589 | ts-node: 10.9.2(@types/node@20.11.18)(typescript@5.3.3) 1590 | tsconfig: 7.0.0 1591 | typescript: 5.3.3 1592 | transitivePeerDependencies: 1593 | - '@swc/core' 1594 | - '@swc/wasm' 1595 | - '@types/node' 1596 | dev: false 1597 | 1598 | /ts-node@10.9.2(@types/node@20.11.18)(typescript@5.3.3): 1599 | resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} 1600 | hasBin: true 1601 | peerDependencies: 1602 | '@swc/core': '>=1.2.50' 1603 | '@swc/wasm': '>=1.2.50' 1604 | '@types/node': '*' 1605 | typescript: '>=2.7' 1606 | peerDependenciesMeta: 1607 | '@swc/core': 1608 | optional: true 1609 | '@swc/wasm': 1610 | optional: true 1611 | dependencies: 1612 | '@cspotcode/source-map-support': 0.8.1 1613 | '@tsconfig/node10': 1.0.9 1614 | '@tsconfig/node12': 1.0.11 1615 | '@tsconfig/node14': 1.0.3 1616 | '@tsconfig/node16': 1.0.4 1617 | '@types/node': 20.11.18 1618 | acorn: 8.11.3 1619 | acorn-walk: 8.3.2 1620 | arg: 4.1.3 1621 | create-require: 1.1.1 1622 | diff: 4.0.2 1623 | make-error: 1.3.6 1624 | typescript: 5.3.3 1625 | v8-compile-cache-lib: 3.0.1 1626 | yn: 3.1.1 1627 | dev: false 1628 | 1629 | /tsconfig@7.0.0: 1630 | resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} 1631 | dependencies: 1632 | '@types/strip-bom': 3.0.0 1633 | '@types/strip-json-comments': 0.0.30 1634 | strip-bom: 3.0.0 1635 | strip-json-comments: 2.0.1 1636 | dev: false 1637 | 1638 | /tslib@2.6.2: 1639 | resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} 1640 | dev: false 1641 | 1642 | /type-is@1.6.18: 1643 | resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} 1644 | engines: {node: '>= 0.6'} 1645 | dependencies: 1646 | media-typer: 0.3.0 1647 | mime-types: 2.1.35 1648 | dev: false 1649 | 1650 | /typescript@5.3.3: 1651 | resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} 1652 | engines: {node: '>=14.17'} 1653 | hasBin: true 1654 | 1655 | /undici-types@5.26.5: 1656 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 1657 | 1658 | /unpipe@1.0.0: 1659 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} 1660 | engines: {node: '>= 0.8'} 1661 | dev: false 1662 | 1663 | /utils-merge@1.0.1: 1664 | resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} 1665 | engines: {node: '>= 0.4.0'} 1666 | dev: false 1667 | 1668 | /v8-compile-cache-lib@3.0.1: 1669 | resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} 1670 | dev: false 1671 | 1672 | /vary@1.1.2: 1673 | resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} 1674 | engines: {node: '>= 0.8'} 1675 | dev: false 1676 | 1677 | /void-elements@3.1.0: 1678 | resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} 1679 | engines: {node: '>=0.10.0'} 1680 | dev: false 1681 | 1682 | /with@7.0.2: 1683 | resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} 1684 | engines: {node: '>= 10.0.0'} 1685 | dependencies: 1686 | '@babel/parser': 7.23.9 1687 | '@babel/types': 7.23.9 1688 | assert-never: 1.2.1 1689 | babel-walk: 3.0.0-canary-5 1690 | dev: false 1691 | 1692 | /wrappy@1.0.2: 1693 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 1694 | dev: false 1695 | 1696 | /xtend@4.0.2: 1697 | resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} 1698 | engines: {node: '>=0.4'} 1699 | dev: false 1700 | 1701 | /yallist@4.0.0: 1702 | resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} 1703 | dev: false 1704 | 1705 | /yn@3.1.1: 1706 | resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} 1707 | engines: {node: '>=6'} 1708 | dev: false 1709 | 1710 | /zod@3.22.4: 1711 | resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} 1712 | dev: false 1713 | -------------------------------------------------------------------------------- /prisma/migrations/20220516201725_user_model/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "RoleEnumType" AS ENUM ('user', 'admin'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "users" ( 6 | "id" TEXT NOT NULL, 7 | "name" VARCHAR(255) NOT NULL, 8 | "email" TEXT NOT NULL, 9 | "photo" TEXT DEFAULT E'default.png', 10 | "verified" BOOLEAN DEFAULT false, 11 | "password" TEXT NOT NULL, 12 | "role" "RoleEnumType" DEFAULT E'user', 13 | "verificationCode" TEXT, 14 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | "updatedAt" TIMESTAMP(3) NOT NULL, 16 | 17 | CONSTRAINT "users_pkey" PRIMARY KEY ("id") 18 | ); 19 | 20 | -- CreateIndex 21 | CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); 22 | 23 | -- CreateIndex 24 | CREATE UNIQUE INDEX "users_verificationCode_key" ON "users"("verificationCode"); 25 | 26 | -- CreateIndex 27 | CREATE INDEX "users_email_verificationCode_idx" ON "users"("email", "verificationCode"); 28 | 29 | -- CreateIndex 30 | CREATE UNIQUE INDEX "users_email_verificationCode_key" ON "users"("email", "verificationCode"); 31 | -------------------------------------------------------------------------------- /prisma/migrations/20220522190602_user_entity/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[email,verificationCode,passwordResetToken]` on the table `users` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- DropIndex 8 | DROP INDEX "users_email_verificationCode_idx"; 9 | 10 | -- DropIndex 11 | DROP INDEX "users_email_verificationCode_key"; 12 | 13 | -- AlterTable 14 | ALTER TABLE "users" ADD COLUMN "passwordResetAt" TIMESTAMP(3), 15 | ADD COLUMN "passwordResetToken" TEXT, 16 | ADD COLUMN "provider" TEXT; 17 | 18 | -- CreateIndex 19 | CREATE INDEX "users_email_verificationCode_passwordResetToken_idx" ON "users"("email", "verificationCode", "passwordResetToken"); 20 | 21 | -- CreateIndex 22 | CREATE UNIQUE INDEX "users_email_verificationCode_passwordResetToken_key" ON "users"("email", "verificationCode", "passwordResetToken"); 23 | -------------------------------------------------------------------------------- /prisma/migrations/20220523141621_user_entity/migration.sql: -------------------------------------------------------------------------------- 1 | -- This is an empty migration. -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | 11 | model User{ 12 | @@map(name: "users") 13 | 14 | id String @id @default(uuid()) 15 | name String @db.VarChar(255) 16 | email String @unique 17 | photo String? @default("default.png") 18 | verified Boolean? @default(false) 19 | 20 | password String 21 | role RoleEnumType? @default(user) 22 | 23 | verificationCode String? @db.Text @unique 24 | 25 | createdAt DateTime @default(now()) 26 | updatedAt DateTime @updatedAt 27 | 28 | provider String? 29 | passwordResetToken String? 30 | passwordResetAt DateTime? 31 | 32 | @@unique([email, verificationCode, passwordResetToken]) 33 | @@index([email, verificationCode,passwordResetToken]) 34 | } 35 | 36 | enum RoleEnumType { 37 | user 38 | admin 39 | } -------------------------------------------------------------------------------- /readMe.md: -------------------------------------------------------------------------------- 1 | # Node.js API with Prisma, TypeScript, and PostgreSQL 2 | 3 | ## Setup Node.js API with Prisma, TypeScript, and PostgreSQL 4 | 5 | ![Setup Node.js API with Prisma, TypeScript, and PostgreSQL](https://codevoweb.com/wp-content/uploads/2022/05/API-Node.js-TypeScript-Prisma-PostgreSQL-Project-Setup.webp) 6 | 7 | In this comprehensive article, you'll learn how to set up a Node.js API Project with ExpressJs, Prisma, PostgreSQL, Redis, and Docker-compose. The API will run on an Express server and use Postgres for data storage. 8 | 9 | ### Topics Covered 10 | 11 | - Initialize a Prisma Project with Express.js 12 | - Generate a database schema with the Prisma CLI 13 | - Defining the User Model with Prisma 14 | - Database Migration with Prisma 15 | - Connect the ExpressJs App to the Redis Server 16 | - Connecting to PostgreSQL and Redis with MySQL VS Code Extension 17 | 18 | Read the entire article here: [https://codevoweb.com/api-node-typescript-prisma-postgresql-project-setup](https://codevoweb.com/api-node-typescript-prisma-postgresql-project-setup) 19 | 20 | ## Node.js + Prisma + PostgreSQL: Access & Refresh Tokens 21 | 22 | ![Node.js + Prisma + PostgreSQL: Access & Refresh Tokens](https://codevoweb.com/wp-content/uploads/2022/05/Node.js-Prisma-PostgreSQL-Access-Refresh-Tokens.webp) 23 | 24 | In this article, you’ll learn how to implement JWT authentication with access and refresh tokens using Node.js, ExpressJs, Prisma, PostgreSQL, Redis, and Docker-compose. 25 | 26 | ### Topics Covered 27 | 28 | - JWT Authentication Example with Node.js, Prisma, and PostgreSQL 29 | - User Login and Registration Flow with JSON Web Tokens 30 | - Create the User Model with Prisma 31 | - Create Validation Schemas with Zod 32 | - Create Middleware to Parse Zod Schema 33 | - Password Management with Bcryptjs 34 | - Create Services to Interact with the Database 35 | - Authentication with JSON Web Tokens (JWT) 36 | - Create Authentication Controllers 37 | - Create User Controller 38 | - Create Authentication Middleware 39 | - Create Authentication Routes 40 | - Create User Routes 41 | - Add the Routes to the Middleware Stack 42 | - Database Migration with Prisma 43 | - Testing the JSON Web Token Authentication REST API 44 | 45 | Read the entire article here: [https://codevoweb.com/node-prisma-postgresql-access-refresh-tokens](https://codevoweb.com/node-prisma-postgresql-access-refresh-tokens) 46 | 47 | ## CRUD API with Node.js and PostgreSQL: Send HTML Emails 48 | 49 | ![CRUD API with Node.js and PostgreSQL: Send HTML Emails](https://codevoweb.com/wp-content/uploads/2022/05/CRUD-API-with-Node.js-and-PostgreSQL-Send-HTML-Emails.webp) 50 | 51 | In this article, you'll learn how to send HTML Emails with Node.js, Nodemailer, Prisma, PostgreSQL, Express, and Docker-compose. We'll create a generic class that can be used to send single or multiple emails at once by chaining the methods together. 52 | 53 | ### Topics Covered 54 | 55 | - Send Emails with Node.js, Nodemailer, Prisma Overview 56 | - Setting up ExpressJs Templating Engine with Pug 57 | - Create a Utility Class to the Send Emails 58 | - Get the Nodemailer Credentials 59 | - Define the Email Class Attributes 60 | - Create a Nodemailer Transporter 61 | - Create a Method to Generate the Email Templates 62 | - Create a Method to Send the Emails 63 | - Creating the Email Templates with Pug 64 | - Run Prisma Migration to Update PostgreSQL Schema 65 | - Update the User Registration Controller to Send the Emails 66 | - Create a Route Handler to Verify the Email 67 | - Update the Login Controller 68 | 69 | Read the entire article here: [https://codevoweb.com/crud-api-node-js-and-postgresql-send-html-emails](https://codevoweb.com/crud-api-node-js-and-postgresql-send-html-emails) 70 | 71 | ## API with Node.js, Prisma & PostgreSQL: Forget/Reset Password 72 | 73 | ![API with Node.js, Prisma & PostgreSQL: Forget/Reset Password](https://codevoweb.com/wp-content/uploads/2022/05/API-with-Node.js-Prisma-PostgreSQL-Forget-Reset-Password.webp) 74 | 75 | In this article, you'll learn how to implement forget/reset passwords with Node.js, Prisma, PostgreSQL, Nodemailer, Redis, Docker-compose, and Pug. Also, you will learn how to dynamically generate HTML Email templates with Pug and send them via SMTP to the user's Email inbox. 76 | 77 | ### Topics Covered 78 | 79 | - Forget/Reset Password Flow in Node.js 80 | - Update the Prisma User Model 81 | - Run the Prisma Migration Command to Update the Database 82 | - Update the Zod User Schema 83 | - Create Services to Query and Mutate the Database 84 | - Create a Utility Class to Send Emails 85 | - Create Controllers 86 | - Forgot Password Controller 87 | - Create the Password Reset Controller 88 | - Add the Routes to the Middleware Pipeline 89 | 90 | Read the entire article here: [https://codevoweb.com/crud-api-node-prisma-postgresql-reset-password](https://codevoweb.com/crud-api-node-prisma-postgresql-reset-password) 91 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | import express, { NextFunction, Request, Response, response } from 'express'; 3 | import config from 'config'; 4 | import cors from 'cors'; 5 | import morgan from 'morgan'; 6 | import cookieParser from 'cookie-parser'; 7 | import validateEnv from './utils/validateEnv'; 8 | import { PrismaClient } from '@prisma/client'; 9 | import authRouter from './routes/auth.routes'; 10 | import userRouter from './routes/user.routes'; 11 | import AppError from './utils/appError'; 12 | 13 | // import nodemailer from 'nodemailer'; 14 | // (async function () { 15 | // const credentials = await nodemailer.createTestAccount(); 16 | // console.log(credentials); 17 | // })(); 18 | 19 | validateEnv(); 20 | 21 | const prisma = new PrismaClient(); 22 | const app = express(); 23 | 24 | async function bootstrap() { 25 | // TEMPLATE ENGINE 26 | app.set('view engine', 'pug'); 27 | app.set('views', `${__dirname}/views`); 28 | 29 | // MIDDLEWARE 30 | 31 | // 1.Body Parser 32 | app.use(express.json({ limit: '10kb' })); 33 | 34 | // 2. Cookie Parser 35 | app.use(cookieParser()); 36 | 37 | // 2. Cors 38 | app.use( 39 | cors({ 40 | origin: [config.get('origin')], 41 | credentials: true, 42 | }) 43 | ); 44 | 45 | // 3. Logger 46 | if (process.env.NODE_ENV === 'development') app.use(morgan('dev')); 47 | 48 | // ROUTES 49 | app.use('/api/auth', authRouter); 50 | app.use('/api/users', userRouter); 51 | 52 | // Testing 53 | app.get('/api/healthchecker', (_, res: Response) => { 54 | res.status(200).json({ 55 | status: 'success', 56 | message: 'Welcome to NodeJs with Prisma and PostgreSQL', 57 | }); 58 | }); 59 | 60 | // UNHANDLED ROUTES 61 | app.all('*', (req: Request, res: Response, next: NextFunction) => { 62 | next(new AppError(404, `Route ${req.originalUrl} not found`)); 63 | }); 64 | 65 | // GLOBAL ERROR HANDLER 66 | app.use((err: AppError, req: Request, res: Response, next: NextFunction) => { 67 | err.status = err.status || 'error'; 68 | err.statusCode = err.statusCode || 500; 69 | 70 | res.status(err.statusCode).json({ 71 | status: err.status, 72 | message: err.message, 73 | }); 74 | }); 75 | 76 | const port = config.get('port'); 77 | app.listen(port, () => { 78 | console.log(`Server on port: ${port}`); 79 | }); 80 | } 81 | 82 | bootstrap() 83 | .catch((err) => { 84 | throw err; 85 | }) 86 | .finally(async () => { 87 | await prisma.$disconnect(); 88 | }); 89 | -------------------------------------------------------------------------------- /src/controllers/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | import { CookieOptions, NextFunction, Request, Response } from 'express'; 3 | import bcrypt from 'bcryptjs'; 4 | import { 5 | ForgotPasswordInput, 6 | LoginUserInput, 7 | RegisterUserInput, 8 | ResetPasswordInput, 9 | VerifyEmailInput, 10 | } from '../schemas/user.schema'; 11 | import { 12 | createUser, 13 | findUniqueUser, 14 | findUser, 15 | signTokens, 16 | updateUser, 17 | } from '../services/user.service'; 18 | import { Prisma } from '@prisma/client'; 19 | import config from 'config'; 20 | import AppError from '../utils/appError'; 21 | import redisClient from '../utils/connectRedis'; 22 | import { signJwt, verifyJwt } from '../utils/jwt'; 23 | import Email from '../utils/email'; 24 | 25 | const cookiesOptions: CookieOptions = { 26 | httpOnly: true, 27 | sameSite: 'lax', 28 | }; 29 | 30 | if (process.env.NODE_ENV === 'production') cookiesOptions.secure = true; 31 | 32 | const accessTokenCookieOptions: CookieOptions = { 33 | ...cookiesOptions, 34 | expires: new Date( 35 | Date.now() + config.get('accessTokenExpiresIn') * 60 * 1000 36 | ), 37 | maxAge: config.get('accessTokenExpiresIn') * 60 * 1000, 38 | }; 39 | 40 | const refreshTokenCookieOptions: CookieOptions = { 41 | ...cookiesOptions, 42 | expires: new Date( 43 | Date.now() + config.get('refreshTokenExpiresIn') * 60 * 1000 44 | ), 45 | maxAge: config.get('refreshTokenExpiresIn') * 60 * 1000, 46 | }; 47 | 48 | export const registerUserHandler = async ( 49 | req: Request<{}, {}, RegisterUserInput>, 50 | res: Response, 51 | next: NextFunction 52 | ) => { 53 | try { 54 | const hashedPassword = await bcrypt.hash(req.body.password, 12); 55 | 56 | const verifyCode = crypto.randomBytes(32).toString('hex'); 57 | const verificationCode = crypto 58 | .createHash('sha256') 59 | .update(verifyCode) 60 | .digest('hex'); 61 | 62 | const user = await createUser({ 63 | name: req.body.name, 64 | email: req.body.email.toLowerCase(), 65 | password: hashedPassword, 66 | verificationCode, 67 | }); 68 | 69 | const redirectUrl = `${config.get( 70 | 'origin' 71 | )}/verifyemail/${verifyCode}`; 72 | try { 73 | await new Email(user, redirectUrl).sendVerificationCode(); 74 | await updateUser({ id: user.id }, { verificationCode }); 75 | 76 | res.status(201).json({ 77 | status: 'success', 78 | message: 79 | 'An email with a verification code has been sent to your email', 80 | }); 81 | } catch (error) { 82 | await updateUser({ id: user.id }, { verificationCode: null }); 83 | return res.status(500).json({ 84 | status: 'error', 85 | message: 'There was an error sending email, please try again', 86 | }); 87 | } 88 | } catch (err: any) { 89 | if (err instanceof Prisma.PrismaClientKnownRequestError) { 90 | if (err.code === 'P2002') { 91 | return res.status(409).json({ 92 | status: 'fail', 93 | message: 'Email already exist, please use another email address', 94 | }); 95 | } 96 | } 97 | next(err); 98 | } 99 | }; 100 | 101 | export const loginUserHandler = async ( 102 | req: Request<{}, {}, LoginUserInput>, 103 | res: Response, 104 | next: NextFunction 105 | ) => { 106 | try { 107 | const { email, password } = req.body; 108 | 109 | const user = await findUniqueUser( 110 | { email: email.toLowerCase() }, 111 | { id: true, email: true, verified: true, password: true } 112 | ); 113 | 114 | if (!user) { 115 | return next(new AppError(400, 'Invalid email or password')); 116 | } 117 | 118 | // Check if user is verified 119 | if (!user.verified) { 120 | return next( 121 | new AppError( 122 | 401, 123 | 'You are not verified, please verify your email to login' 124 | ) 125 | ); 126 | } 127 | 128 | if (!user || !(await bcrypt.compare(password, user.password))) { 129 | return next(new AppError(400, 'Invalid email or password')); 130 | } 131 | 132 | // Sign Tokens 133 | const { access_token, refresh_token } = await signTokens(user); 134 | res.cookie('access_token', access_token, accessTokenCookieOptions); 135 | res.cookie('refresh_token', refresh_token, refreshTokenCookieOptions); 136 | res.cookie('logged_in', true, { 137 | ...accessTokenCookieOptions, 138 | httpOnly: false, 139 | }); 140 | 141 | res.status(200).json({ 142 | status: 'success', 143 | access_token, 144 | }); 145 | } catch (err: any) { 146 | next(err); 147 | } 148 | }; 149 | 150 | export const refreshAccessTokenHandler = async ( 151 | req: Request, 152 | res: Response, 153 | next: NextFunction 154 | ) => { 155 | try { 156 | const refresh_token = req.cookies.refresh_token; 157 | 158 | const message = 'Could not refresh access token'; 159 | 160 | if (!refresh_token) { 161 | return next(new AppError(403, message)); 162 | } 163 | 164 | // Validate refresh token 165 | const decoded = verifyJwt<{ sub: string }>( 166 | refresh_token, 167 | 'refreshTokenPublicKey' 168 | ); 169 | 170 | if (!decoded) { 171 | return next(new AppError(403, message)); 172 | } 173 | 174 | // Check if user has a valid session 175 | const session = await redisClient.get(decoded.sub); 176 | 177 | if (!session) { 178 | return next(new AppError(403, message)); 179 | } 180 | 181 | // Check if user still exist 182 | const user = await findUniqueUser({ id: JSON.parse(session).id }); 183 | 184 | if (!user) { 185 | return next(new AppError(403, message)); 186 | } 187 | 188 | // Sign new access token 189 | const access_token = signJwt({ sub: user.id }, 'accessTokenPrivateKey', { 190 | expiresIn: `${config.get('accessTokenExpiresIn')}m`, 191 | }); 192 | 193 | // 4. Add Cookies 194 | res.cookie('access_token', access_token, accessTokenCookieOptions); 195 | res.cookie('logged_in', true, { 196 | ...accessTokenCookieOptions, 197 | httpOnly: false, 198 | }); 199 | 200 | // 5. Send response 201 | res.status(200).json({ 202 | status: 'success', 203 | access_token, 204 | }); 205 | } catch (err: any) { 206 | next(err); 207 | } 208 | }; 209 | 210 | function logout(res: Response) { 211 | res.cookie('access_token', '', { maxAge: 1 }); 212 | res.cookie('refresh_token', '', { maxAge: 1 }); 213 | res.cookie('logged_in', '', { maxAge: 1 }); 214 | } 215 | 216 | export const logoutUserHandler = async ( 217 | req: Request, 218 | res: Response, 219 | next: NextFunction 220 | ) => { 221 | try { 222 | await redisClient.del(res.locals.user.id); 223 | logout(res); 224 | 225 | res.status(200).json({ 226 | status: 'success', 227 | }); 228 | } catch (err: any) { 229 | next(err); 230 | } 231 | }; 232 | 233 | export const verifyEmailHandler = async ( 234 | req: Request, 235 | res: Response, 236 | next: NextFunction 237 | ) => { 238 | try { 239 | const verificationCode = crypto 240 | .createHash('sha256') 241 | .update(req.params.verificationCode) 242 | .digest('hex'); 243 | 244 | const user = await updateUser( 245 | { verificationCode }, 246 | { verified: true, verificationCode: null }, 247 | { email: true } 248 | ); 249 | 250 | if (!user) { 251 | return next(new AppError(401, 'Could not verify email')); 252 | } 253 | 254 | res.status(200).json({ 255 | status: 'success', 256 | message: 'Email verified successfully', 257 | }); 258 | } catch (err: any) { 259 | if (err.code === 'P2025') { 260 | return res.status(403).json({ 261 | status: 'fail', 262 | message: `Verification code is invalid or user doesn't exist`, 263 | }); 264 | } 265 | next(err); 266 | } 267 | }; 268 | 269 | export const forgotPasswordHandler = async ( 270 | req: Request< 271 | Record, 272 | Record, 273 | ForgotPasswordInput 274 | >, 275 | res: Response, 276 | next: NextFunction 277 | ) => { 278 | try { 279 | // Get the user from the collection 280 | const user = await findUser({ email: req.body.email.toLowerCase() }); 281 | const message = 282 | 'You will receive a reset email if user with that email exist'; 283 | if (!user) { 284 | return res.status(200).json({ 285 | status: 'success', 286 | message, 287 | }); 288 | } 289 | 290 | if (!user.verified) { 291 | return res.status(403).json({ 292 | status: 'fail', 293 | message: 'Account not verified', 294 | }); 295 | } 296 | 297 | if (user.provider) { 298 | return res.status(403).json({ 299 | status: 'fail', 300 | message: 301 | 'We found your account. It looks like you registered with a social auth account. Try signing in with social auth.', 302 | }); 303 | } 304 | 305 | const resetToken = crypto.randomBytes(32).toString('hex'); 306 | const passwordResetToken = crypto 307 | .createHash('sha256') 308 | .update(resetToken) 309 | .digest('hex'); 310 | 311 | await updateUser( 312 | { id: user.id }, 313 | { 314 | passwordResetToken, 315 | passwordResetAt: new Date(Date.now() + 10 * 60 * 1000), 316 | }, 317 | { email: true } 318 | ); 319 | 320 | try { 321 | const url = `${config.get('origin')}/resetpassword/${resetToken}`; 322 | await new Email(user, url).sendPasswordResetToken(); 323 | 324 | res.status(200).json({ 325 | status: 'success', 326 | message, 327 | }); 328 | } catch (err: any) { 329 | await updateUser( 330 | { id: user.id }, 331 | { passwordResetToken: null, passwordResetAt: null }, 332 | {} 333 | ); 334 | return res.status(500).json({ 335 | status: 'error', 336 | message: 'There was an error sending email', 337 | }); 338 | } 339 | } catch (err: any) { 340 | next(err); 341 | } 342 | }; 343 | 344 | export const resetPasswordHandler = async ( 345 | req: Request< 346 | ResetPasswordInput['params'], 347 | Record, 348 | ResetPasswordInput['body'] 349 | >, 350 | res: Response, 351 | next: NextFunction 352 | ) => { 353 | try { 354 | // Get the user from the collection 355 | const passwordResetToken = crypto 356 | .createHash('sha256') 357 | .update(req.params.resetToken) 358 | .digest('hex'); 359 | 360 | const user = await findUser({ 361 | passwordResetToken, 362 | passwordResetAt: { 363 | gt: new Date(), 364 | }, 365 | }); 366 | 367 | if (!user) { 368 | return res.status(403).json({ 369 | status: 'fail', 370 | message: 'Invalid token or token has expired', 371 | }); 372 | } 373 | 374 | const hashedPassword = await bcrypt.hash(req.body.password, 12); 375 | // Change password data 376 | await updateUser( 377 | { 378 | id: user.id, 379 | }, 380 | { 381 | password: hashedPassword, 382 | passwordResetToken: null, 383 | passwordResetAt: null, 384 | }, 385 | { email: true } 386 | ); 387 | 388 | logout(res); 389 | res.status(200).json({ 390 | status: 'success', 391 | message: 'Password data updated successfully', 392 | }); 393 | } catch (err: any) { 394 | next(err); 395 | } 396 | }; 397 | -------------------------------------------------------------------------------- /src/controllers/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | 3 | export const getMeHandler = async ( 4 | req: Request, 5 | res: Response, 6 | next: NextFunction 7 | ) => { 8 | try { 9 | const user = res.locals.user; 10 | 11 | res.status(200).status(200).json({ 12 | status: 'success', 13 | data: { 14 | user, 15 | }, 16 | }); 17 | } catch (err: any) { 18 | next(err); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/middleware/deserializeUser.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { omit } from 'lodash'; 3 | import { excludedFields, findUniqueUser } from '../services/user.service'; 4 | import AppError from '../utils/appError'; 5 | import redisClient from '../utils/connectRedis'; 6 | import { verifyJwt } from '../utils/jwt'; 7 | 8 | export const deserializeUser = async ( 9 | req: Request, 10 | res: Response, 11 | next: NextFunction 12 | ) => { 13 | try { 14 | let access_token; 15 | 16 | if ( 17 | req.headers.authorization && 18 | req.headers.authorization.startsWith('Bearer') 19 | ) { 20 | access_token = req.headers.authorization.split(' ')[1]; 21 | } else if (req.cookies.access_token) { 22 | access_token = req.cookies.access_token; 23 | } 24 | 25 | if (!access_token) { 26 | return next(new AppError(401, 'You are not logged in')); 27 | } 28 | 29 | // Validate the access token 30 | const decoded = verifyJwt<{ sub: string }>( 31 | access_token, 32 | 'accessTokenPublicKey' 33 | ); 34 | 35 | if (!decoded) { 36 | return next(new AppError(401, `Invalid token or user doesn't exist`)); 37 | } 38 | 39 | // Check if the user has a valid session 40 | const session = await redisClient.get(decoded.sub); 41 | 42 | if (!session) { 43 | return next(new AppError(401, `Invalid token or session has expired`)); 44 | } 45 | 46 | // Check if the user still exist 47 | const user = await findUniqueUser({ id: JSON.parse(session).id }); 48 | 49 | if (!user) { 50 | return next(new AppError(401, `Invalid token or session has expired`)); 51 | } 52 | 53 | // Add user to res.locals 54 | res.locals.user = omit(user, excludedFields); 55 | 56 | next(); 57 | } catch (err: any) { 58 | next(err); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/middleware/requireUser.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import AppError from '../utils/appError'; 3 | 4 | export const requireUser = ( 5 | req: Request, 6 | res: Response, 7 | next: NextFunction 8 | ) => { 9 | try { 10 | const user = res.locals.user; 11 | 12 | if (!user) { 13 | return next( 14 | new AppError(400, `Session has expired or user doesn't exist`) 15 | ); 16 | } 17 | 18 | next(); 19 | } catch (err: any) { 20 | next(err); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/middleware/validate.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { AnyZodObject, ZodError } from 'zod'; 3 | 4 | export const validate = 5 | (schema: AnyZodObject) => 6 | (req: Request, res: Response, next: NextFunction) => { 7 | try { 8 | schema.parse({ 9 | params: req.params, 10 | query: req.query, 11 | body: req.body, 12 | }); 13 | 14 | next(); 15 | } catch (error) { 16 | if (error instanceof ZodError) { 17 | return res.status(400).json({ 18 | status: 'fail', 19 | errors: error.errors, 20 | }); 21 | } 22 | next(error); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/routes/auth.routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { 3 | forgotPasswordHandler, 4 | loginUserHandler, 5 | logoutUserHandler, 6 | refreshAccessTokenHandler, 7 | registerUserHandler, 8 | resetPasswordHandler, 9 | verifyEmailHandler, 10 | } from '../controllers/auth.controller'; 11 | import { deserializeUser } from '../middleware/deserializeUser'; 12 | import { requireUser } from '../middleware/requireUser'; 13 | import { validate } from '../middleware/validate'; 14 | import { 15 | forgotPasswordSchema, 16 | loginUserSchema, 17 | registerUserSchema, 18 | resetPasswordSchema, 19 | verifyEmailSchema, 20 | } from '../schemas/user.schema'; 21 | 22 | const router = express.Router(); 23 | 24 | router.post('/register', validate(registerUserSchema), registerUserHandler); 25 | 26 | router.post('/login', validate(loginUserSchema), loginUserHandler); 27 | 28 | router.get('/refresh', refreshAccessTokenHandler); 29 | 30 | router.get( 31 | '/verifyemail/:verificationCode', 32 | validate(verifyEmailSchema), 33 | verifyEmailHandler 34 | ); 35 | 36 | router.get('/logout', deserializeUser, requireUser, logoutUserHandler); 37 | 38 | router.post( 39 | '/forgotpassword', 40 | validate(forgotPasswordSchema), 41 | forgotPasswordHandler 42 | ); 43 | 44 | router.patch( 45 | '/resetpassword/:resetToken', 46 | validate(resetPasswordSchema), 47 | resetPasswordHandler 48 | ); 49 | 50 | export default router; 51 | -------------------------------------------------------------------------------- /src/routes/user.routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { getMeHandler } from '../controllers/user.controller'; 3 | import { deserializeUser } from '../middleware/deserializeUser'; 4 | import { requireUser } from '../middleware/requireUser'; 5 | 6 | const router = express.Router(); 7 | 8 | router.use(deserializeUser, requireUser); 9 | 10 | router.get('/me', getMeHandler); 11 | 12 | export default router; 13 | -------------------------------------------------------------------------------- /src/schemas/user.schema.ts: -------------------------------------------------------------------------------- 1 | import { object, string, TypeOf, z } from 'zod'; 2 | 3 | enum RoleEnumType { 4 | ADMIN = 'admin', 5 | USER = 'user', 6 | } 7 | 8 | export const registerUserSchema = object({ 9 | body: object({ 10 | name: string({ 11 | required_error: 'Name is required', 12 | }), 13 | email: string({ 14 | required_error: 'Email address is required', 15 | }).email('Invalid email address'), 16 | password: string({ 17 | required_error: 'Password is required', 18 | }) 19 | .min(8, 'Password must be more than 8 characters') 20 | .max(32, 'Password must be less than 32 characters'), 21 | passwordConfirm: string({ 22 | required_error: 'Please confirm your password', 23 | }), 24 | role: z.optional(z.nativeEnum(RoleEnumType)), 25 | }).refine((data) => data.password === data.passwordConfirm, { 26 | path: ['passwordConfirm'], 27 | message: 'Passwords do not match', 28 | }), 29 | }); 30 | 31 | export const loginUserSchema = object({ 32 | body: object({ 33 | email: string({ 34 | required_error: 'Email address is required', 35 | }).email('Invalid email address'), 36 | password: string({ 37 | required_error: 'Password is required', 38 | }).min(8, 'Invalid email or password'), 39 | }), 40 | }); 41 | 42 | export const verifyEmailSchema = object({ 43 | params: object({ 44 | verificationCode: string(), 45 | }), 46 | }); 47 | 48 | export const updateUserSchema = object({ 49 | body: object({ 50 | name: string({}), 51 | email: string({}).email('Invalid email address'), 52 | password: string({}) 53 | .min(8, 'Password must be more than 8 characters') 54 | .max(32, 'Password must be less than 32 characters'), 55 | passwordConfirm: string({}), 56 | role: z.optional(z.nativeEnum(RoleEnumType)), 57 | }) 58 | .partial() 59 | .refine((data) => data.password === data.passwordConfirm, { 60 | path: ['passwordConfirm'], 61 | message: 'Passwords do not match', 62 | }), 63 | }); 64 | 65 | export const forgotPasswordSchema = object({ 66 | body: object({ 67 | email: string({ 68 | required_error: 'Email is required', 69 | }).email('Email is invalid'), 70 | }), 71 | }); 72 | 73 | export const resetPasswordSchema = object({ 74 | params: object({ 75 | resetToken: string(), 76 | }), 77 | body: object({ 78 | password: string({ 79 | required_error: 'Password is required', 80 | }).min(8, 'Password must be more than 8 characters'), 81 | passwordConfirm: string({ 82 | required_error: 'Please confirm your password', 83 | }), 84 | }).refine((data) => data.password === data.passwordConfirm, { 85 | message: 'Passwords do not match', 86 | path: ['passwordConfirm'], 87 | }), 88 | }); 89 | 90 | export type RegisterUserInput = Omit< 91 | TypeOf['body'], 92 | 'passwordConfirm' 93 | >; 94 | 95 | export type LoginUserInput = TypeOf['body']; 96 | export type VerifyEmailInput = TypeOf['params']; 97 | export type UpdateUserInput = TypeOf['body']; 98 | 99 | export type ForgotPasswordInput = TypeOf['body']; 100 | export type ResetPasswordInput = TypeOf; 101 | -------------------------------------------------------------------------------- /src/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Prisma, User } from "@prisma/client"; 2 | import config from "config"; 3 | import { omit } from "lodash"; 4 | import redisClient from "../utils/connectRedis"; 5 | import { signJwt } from "../utils/jwt"; 6 | 7 | export const excludedFields = [ 8 | "password", 9 | "verified", 10 | "verificationCode", 11 | "passwordResetAt", 12 | "passwordResetToken", 13 | ]; 14 | 15 | const prisma = new PrismaClient(); 16 | 17 | export const createUser = async (input: Prisma.UserCreateInput) => { 18 | return (await prisma.user.create({ 19 | data: input, 20 | })) as User; 21 | }; 22 | 23 | export const findUser = async ( 24 | where: Partial, 25 | select?: Prisma.UserSelect 26 | ) => { 27 | return (await prisma.user.findFirst({ 28 | where, 29 | select, 30 | })) as User; 31 | }; 32 | 33 | export const findUniqueUser = async ( 34 | where: Prisma.UserWhereUniqueInput, 35 | select?: Prisma.UserSelect 36 | ) => { 37 | return (await prisma.user.findUnique({ 38 | where, 39 | select, 40 | })) as User; 41 | }; 42 | 43 | export const updateUser = async ( 44 | where: Partial, 45 | data: Prisma.UserUpdateInput, 46 | select?: Prisma.UserSelect 47 | ) => { 48 | return (await prisma.user.update({ where, data, select })) as User; 49 | }; 50 | 51 | export const signTokens = async (user: Prisma.UserCreateInput) => { 52 | // 1. Create Session 53 | redisClient.set(`${user.id}`, JSON.stringify(omit(user, excludedFields)), { 54 | EX: config.get("redisCacheExpiresIn") * 60, 55 | }); 56 | 57 | // 2. Create Access and Refresh tokens 58 | const access_token = signJwt({ sub: user.id }, "accessTokenPrivateKey", { 59 | expiresIn: `${config.get("accessTokenExpiresIn")}m`, 60 | }); 61 | 62 | const refresh_token = signJwt({ sub: user.id }, "refreshTokenPrivateKey", { 63 | expiresIn: `${config.get("refreshTokenExpiresIn")}m`, 64 | }); 65 | 66 | return { access_token, refresh_token }; 67 | }; 68 | -------------------------------------------------------------------------------- /src/utils/appError.ts: -------------------------------------------------------------------------------- 1 | export default class AppError extends Error { 2 | status: string; 3 | isOperational: boolean; 4 | constructor(public statusCode: number = 500, public message: string) { 5 | super(message); 6 | this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; 7 | this.isOperational = true; 8 | 9 | Error.captureStackTrace(this, this.constructor); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/connectRedis.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis'; 2 | 3 | const redisUrl = 'redis://localhost:6379'; 4 | 5 | const redisClient = createClient({ 6 | url: redisUrl, 7 | }); 8 | 9 | const connectRedis = async () => { 10 | try { 11 | await redisClient.connect(); 12 | console.log('Redis client connect successfully'); 13 | redisClient.set('try', 'Hello Welcome to Express with TypeORM'); 14 | } catch (error) { 15 | console.log(error); 16 | setTimeout(connectRedis, 5000); 17 | } 18 | }; 19 | 20 | connectRedis(); 21 | 22 | export default redisClient; 23 | -------------------------------------------------------------------------------- /src/utils/email.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | import config from 'config'; 3 | import pug from 'pug'; 4 | import { convert } from 'html-to-text'; 5 | import { Prisma } from '@prisma/client'; 6 | 7 | const smtp = config.get<{ 8 | host: string; 9 | port: number; 10 | user: string; 11 | pass: string; 12 | }>('smtp'); 13 | 14 | export default class Email { 15 | #firstName: string; 16 | #to: string; 17 | #from: string; 18 | constructor(private user: Prisma.UserCreateInput, private url: string) { 19 | this.#firstName = user.name.split(' ')[0]; 20 | this.#to = user.email; 21 | this.#from = `Codevo `; 22 | } 23 | 24 | private newTransport() { 25 | // if (process.env.NODE_ENV === 'production') { 26 | // } 27 | 28 | return nodemailer.createTransport({ 29 | ...smtp, 30 | auth: { 31 | user: smtp.user, 32 | pass: smtp.pass, 33 | }, 34 | }); 35 | } 36 | 37 | private async send(template: string, subject: string) { 38 | try { 39 | // Generate HTML template based on the template string 40 | const html = pug.renderFile(`${__dirname}/../views/${template}.pug`, { 41 | firstName: this.#firstName, 42 | subject, 43 | url: this.url, 44 | }); 45 | 46 | // Create mailOptions 47 | const mailOptions = { 48 | from: this.#from, 49 | to: this.#to, 50 | subject, 51 | text: convert(html), 52 | html, 53 | }; 54 | 55 | // Send email 56 | const info = await this.newTransport().sendMail(mailOptions); 57 | console.log(nodemailer.getTestMessageUrl(info)); 58 | } catch (error) { 59 | console.error('Error during send mail :', error); 60 | } 61 | } 62 | 63 | async sendVerificationCode() { 64 | await this.send('verificationCode', 'Your account verification code'); 65 | } 66 | 67 | async sendPasswordResetToken() { 68 | await this.send( 69 | 'resetPassword', 70 | 'Your password reset token (valid for only 10 minutes)' 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/jwt.ts: -------------------------------------------------------------------------------- 1 | import jwt, { SignOptions } from 'jsonwebtoken'; 2 | import config from 'config'; 3 | 4 | export const signJwt = ( 5 | payload: Object, 6 | keyName: 'accessTokenPrivateKey' | 'refreshTokenPrivateKey', 7 | options: SignOptions 8 | ) => { 9 | const privateKey = Buffer.from( 10 | config.get(keyName), 11 | 'base64' 12 | ).toString('ascii'); 13 | return jwt.sign(payload, privateKey, { 14 | ...(options && options), 15 | algorithm: 'RS256', 16 | }); 17 | }; 18 | 19 | export const verifyJwt = ( 20 | token: string, 21 | keyName: 'accessTokenPublicKey' | 'refreshTokenPublicKey' 22 | ): T | null => { 23 | try { 24 | const publicKey = Buffer.from( 25 | config.get(keyName), 26 | 'base64' 27 | ).toString('ascii'); 28 | const decoded = jwt.verify(token, publicKey) as T; 29 | 30 | return decoded; 31 | } catch (error) { 32 | return null; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/utils/validateEnv.ts: -------------------------------------------------------------------------------- 1 | import { cleanEnv, port, str } from 'envalid'; 2 | 3 | function validateEnv() { 4 | cleanEnv(process.env, { 5 | DATABASE_URL: str(), 6 | 7 | PORT: port(), 8 | NODE_ENV: str(), 9 | 10 | POSTGRES_HOST: str(), 11 | POSTGRES_PORT: port(), 12 | POSTGRES_USER: str(), 13 | POSTGRES_PASSWORD: str(), 14 | POSTGRES_DB: str(), 15 | 16 | JWT_ACCESS_TOKEN_PRIVATE_KEY: str(), 17 | JWT_ACCESS_TOKEN_PUBLIC_KEY: str(), 18 | JWT_REFRESH_TOKEN_PRIVATE_KEY: str(), 19 | JWT_REFRESH_TOKEN_PUBLIC_KEY: str(), 20 | 21 | EMAIL_USER: str(), 22 | EMAIL_PASS: str(), 23 | EMAIL_HOST: str(), 24 | EMAIL_PORT: port(), 25 | }); 26 | } 27 | 28 | export default validateEnv; 29 | -------------------------------------------------------------------------------- /src/views/_styles.pug: -------------------------------------------------------------------------------- 1 | style. 2 | /* ------------------------------------- 3 | GLOBAL RESETS 4 | ------------------------------------- */ 5 | /*All the styling goes here*/ 6 | img { 7 | border: none; 8 | -ms-interpolation-mode: bicubic; 9 | max-width: 100%; 10 | } 11 | body { 12 | background-color: #f6f6f6; 13 | font-family: sans-serif; 14 | -webkit-font-smoothing: antialiased; 15 | font-size: 14px; 16 | line-height: 1.4; 17 | margin: 0; 18 | padding: 0; 19 | -ms-text-size-adjust: 100%; 20 | -webkit-text-size-adjust: 100%; 21 | } 22 | table { 23 | border-collapse: separate; 24 | mso-table-lspace: 0pt; 25 | mso-table-rspace: 0pt; 26 | width: 100%; } 27 | table td { 28 | font-family: sans-serif; 29 | font-size: 14px; 30 | vertical-align: top; 31 | } 32 | /* ------------------------------------- 33 | BODY & CONTAINER 34 | ------------------------------------- */ 35 | .body { 36 | background-color: #f6f6f6; 37 | width: 100%; 38 | } 39 | /* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */ 40 | .container { 41 | display: block; 42 | margin: 0 auto !important; 43 | /* makes it centered */ 44 | max-width: 580px; 45 | padding: 10px; 46 | width: 580px; 47 | } 48 | /* This should also be a block element, so that it will fill 100% of the .container */ 49 | .content { 50 | box-sizing: border-box; 51 | display: block; 52 | margin: 0 auto; 53 | max-width: 580px; 54 | padding: 10px; 55 | } 56 | /* ------------------------------------- 57 | HEADER, FOOTER, MAIN 58 | ------------------------------------- */ 59 | .main { 60 | background: #ffffff; 61 | border-radius: 3px; 62 | width: 100%; 63 | } 64 | .wrapper { 65 | box-sizing: border-box; 66 | padding: 20px; 67 | } 68 | .content-block { 69 | padding-bottom: 10px; 70 | padding-top: 10px; 71 | } 72 | .footer { 73 | clear: both; 74 | margin-top: 10px; 75 | text-align: center; 76 | width: 100%; 77 | } 78 | .footer td, 79 | .footer p, 80 | .footer span, 81 | .footer a { 82 | color: #999999; 83 | font-size: 12px; 84 | text-align: center; 85 | } 86 | /* ------------------------------------- 87 | TYPOGRAPHY 88 | ------------------------------------- */ 89 | h1, 90 | h2, 91 | h3, 92 | h4 { 93 | color: #000000; 94 | font-family: sans-serif; 95 | font-weight: 400; 96 | line-height: 1.4; 97 | margin: 0; 98 | margin-bottom: 30px; 99 | } 100 | h1 { 101 | font-size: 35px; 102 | font-weight: 300; 103 | text-align: center; 104 | text-transform: capitalize; 105 | } 106 | p, 107 | ul, 108 | ol { 109 | font-family: sans-serif; 110 | font-size: 14px; 111 | font-weight: normal; 112 | margin: 0; 113 | margin-bottom: 15px; 114 | } 115 | p li, 116 | ul li, 117 | ol li { 118 | list-style-position: inside; 119 | margin-left: 5px; 120 | } 121 | a { 122 | color: #3498db; 123 | text-decoration: underline; 124 | } 125 | /* ------------------------------------- 126 | BUTTONS 127 | ------------------------------------- */ 128 | .btn { 129 | box-sizing: border-box; 130 | width: 100%; } 131 | .btn > tbody > tr > td { 132 | padding-bottom: 15px; } 133 | .btn table { 134 | width: auto; 135 | } 136 | .btn table td { 137 | background-color: #ffffff; 138 | border-radius: 5px; 139 | text-align: center; 140 | } 141 | .btn a { 142 | background-color: #ffffff; 143 | border: solid 1px #3498db; 144 | border-radius: 5px; 145 | box-sizing: border-box; 146 | color: #3498db; 147 | cursor: pointer; 148 | display: inline-block; 149 | font-size: 14px; 150 | font-weight: bold; 151 | margin: 0; 152 | padding: 12px 25px; 153 | text-decoration: none; 154 | text-transform: capitalize; 155 | } 156 | .btn-primary table td { 157 | background-color: #3498db; 158 | } 159 | .btn-primary a { 160 | background-color: #3498db; 161 | border-color: #3498db; 162 | color: #ffffff; 163 | } 164 | /* ------------------------------------- 165 | OTHER STYLES THAT MIGHT BE USEFUL 166 | ------------------------------------- */ 167 | .last { 168 | margin-bottom: 0; 169 | } 170 | .first { 171 | margin-top: 0; 172 | } 173 | .align-center { 174 | text-align: center; 175 | } 176 | .align-right { 177 | text-align: right; 178 | } 179 | .align-left { 180 | text-align: left; 181 | } 182 | .clear { 183 | clear: both; 184 | } 185 | .mt0 { 186 | margin-top: 0; 187 | } 188 | .mb0 { 189 | margin-bottom: 0; 190 | } 191 | .preheader { 192 | color: transparent; 193 | display: none; 194 | height: 0; 195 | max-height: 0; 196 | max-width: 0; 197 | opacity: 0; 198 | overflow: hidden; 199 | mso-hide: all; 200 | visibility: hidden; 201 | width: 0; 202 | } 203 | .powered-by a { 204 | text-decoration: none; 205 | } 206 | hr { 207 | border: 0; 208 | border-bottom: 1px solid #f6f6f6; 209 | margin: 20px 0; 210 | } 211 | /* ------------------------------------- 212 | RESPONSIVE AND MOBILE FRIENDLY STYLES 213 | ------------------------------------- */ 214 | @media only screen and (max-width: 620px) { 215 | table.body h1 { 216 | font-size: 28px !important; 217 | margin-bottom: 10px !important; 218 | } 219 | table.body p, 220 | table.body ul, 221 | table.body ol, 222 | table.body td, 223 | table.body span, 224 | table.body a { 225 | font-size: 16px !important; 226 | } 227 | table.body .wrapper, 228 | table.body .article { 229 | padding: 10px !important; 230 | } 231 | table.body .content { 232 | padding: 0 !important; 233 | } 234 | table.body .container { 235 | padding: 0 !important; 236 | width: 100% !important; 237 | } 238 | table.body .main { 239 | border-left-width: 0 !important; 240 | border-radius: 0 !important; 241 | border-right-width: 0 !important; 242 | } 243 | table.body .btn table { 244 | width: 100% !important; 245 | } 246 | table.body .btn a { 247 | width: 100% !important; 248 | } 249 | table.body .img-responsive { 250 | height: auto !important; 251 | max-width: 100% !important; 252 | width: auto !important; 253 | } 254 | } 255 | /* ------------------------------------- 256 | PRESERVE THESE STYLES IN THE HEAD 257 | ------------------------------------- */ 258 | @media all { 259 | .ExternalClass { 260 | width: 100%; 261 | } 262 | .ExternalClass, 263 | .ExternalClass p, 264 | .ExternalClass span, 265 | .ExternalClass font, 266 | .ExternalClass td, 267 | .ExternalClass div { 268 | line-height: 100%; 269 | } 270 | .apple-link a { 271 | color: inherit !important; 272 | font-family: inherit !important; 273 | font-size: inherit !important; 274 | font-weight: inherit !important; 275 | line-height: inherit !important; 276 | text-decoration: none !important; 277 | } 278 | #MessageViewBody a { 279 | color: inherit; 280 | text-decoration: none; 281 | font-size: inherit; 282 | font-family: inherit; 283 | font-weight: inherit; 284 | line-height: inherit; 285 | } 286 | .btn-primary table td:hover { 287 | background-color: #34495e !important; 288 | } 289 | .btn-primary a:hover { 290 | background-color: #34495e !important; 291 | border-color: #34495e !important; 292 | } 293 | } -------------------------------------------------------------------------------- /src/views/base.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(name='viewport' content='width=device-width, initial-scale=1.0') 5 | meta(http-equiv='Content-Type' content='text/html; charset=UTF-8') 6 | title #{subject} 7 | include _styles 8 | body 9 | table.body(role='presentation' border='0' cellpadding='0' cellspacing='0') 10 | tbody 11 | tr 12 | td   13 | td.container 14 | .content 15 | // START CENTERED WHITE CONTAINER 16 | table.main(role='presentation') 17 | // START MAIN CONTENT AREA 18 | tbody 19 | tr 20 | td.wrapper 21 | table(role='presentation' border='0' cellpadding='0' cellspacing='0') 22 | tbody 23 | tr 24 | td 25 | block content 26 | // END MAIN CONTENT AREA 27 | // END CENTERED WHITE CONTAINER 28 | // START FOOTER 29 | .footer 30 | table(role='presentation' border='0' cellpadding='0' cellspacing='0') 31 | tbody 32 | tr 33 | td.content-block 34 | span.apple-link Company Inc, 3 Abbey Road, San Francisco CA 94102 35 | br 36 | | Don't like these emails? 37 | a(href='http://i.imgur.com/CScmqnj.gif') Unsubscribe 38 | | . 39 | tr 40 | td.content-block.powered-by 41 | | Powered by 42 | a(href='http://htmlemail.io') HTMLemail 43 | | . 44 | // END FOOTER 45 | td   46 | -------------------------------------------------------------------------------- /src/views/resetPassword.pug: -------------------------------------------------------------------------------- 1 | extends base 2 | 3 | block content 4 | p Hi #{firstName}, 5 | p Forgot password? Send a PATCH request to with your password and passwordConfirm to #{url} 6 | table.btn.btn-primary(role='presentation' border='0' cellpadding='0' cellspacing='0') 7 | tbody 8 | tr 9 | td(align='left') 10 | table(role='presentation' border='0' cellpadding='0' cellspacing='0') 11 | tbody 12 | tr 13 | td 14 | a(href=`${url}` target='_blank') Reset password 15 | p If you didn't forget your password, please ignore this email 16 | p Good luck! Codevo CEO. -------------------------------------------------------------------------------- /src/views/verificationCode.pug: -------------------------------------------------------------------------------- 1 | extends base 2 | 3 | block content 4 | p Hi #{firstName}, 5 | p Please verify your account to be able to login 6 | table.btn.btn-primary(role='presentation' border='0' cellpadding='0' cellspacing='0') 7 | tbody 8 | tr 9 | td(align='left') 10 | table(role='presentation' border='0' cellpadding='0' cellspacing='0') 11 | tbody 12 | tr 13 | td 14 | a(href=`${url}` target='_blank') Verify your account 15 | p Good luck! Codevo CEO. -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "experimentalDecorators": true, 5 | "emitDecoratorMetadata": true, 6 | "module": "commonjs", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "strictPropertyInitialization": false, 11 | "skipLibCheck": true, 12 | "outDir": "./build", 13 | "rootDir": "." 14 | } 15 | } 16 | --------------------------------------------------------------------------------