SMS gone, approvals in
This commit is contained in:
+1
-2
@@ -1,7 +1,6 @@
|
||||
DATABASE_URL="file:../data/app.db"
|
||||
JWT_SECRET="replace-with-a-long-random-string"
|
||||
TEXTLK_API_TOKEN=
|
||||
TEXTLK_SENDER_ID=TextLKDemo
|
||||
SEED_ADMIN_NAME="Super Admin"
|
||||
SEED_ADMIN_PHONE="0715127342"
|
||||
SEED_ADMIN_PASSWORD="change-this-password"
|
||||
PORT=3000
|
||||
|
||||
Generated
+13
-38
@@ -11,14 +11,14 @@
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.16.2",
|
||||
"axios": "^1.12.2",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.6.1",
|
||||
"express": "^4.21.2",
|
||||
"jsonwebtoken": "^9.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"prisma": "^6.16.2"
|
||||
}
|
||||
},
|
||||
"devDependencies": {}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "6.19.3",
|
||||
@@ -46,7 +46,6 @@
|
||||
"version": "6.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz",
|
||||
"integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"c12": "3.1.0",
|
||||
@@ -59,14 +58,12 @@
|
||||
"version": "6.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz",
|
||||
"integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "6.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz",
|
||||
"integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -80,14 +77,12 @@
|
||||
"version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
|
||||
"integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "6.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz",
|
||||
"integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.19.3",
|
||||
@@ -99,7 +94,6 @@
|
||||
"version": "6.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz",
|
||||
"integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.19.3"
|
||||
@@ -109,7 +103,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
@@ -184,6 +177,15 @@
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.8",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",
|
||||
@@ -227,7 +229,6 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
|
||||
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.3",
|
||||
@@ -285,7 +286,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
@@ -301,7 +301,6 @@
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
||||
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"consola": "^3.2.3"
|
||||
@@ -323,14 +322,12 @@
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
||||
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.10.0"
|
||||
@@ -402,7 +399,6 @@
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.2.tgz",
|
||||
"integrity": "sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "ko-fi",
|
||||
@@ -422,7 +418,6 @@
|
||||
"version": "6.1.7",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
||||
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
@@ -447,7 +442,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
@@ -505,7 +499,6 @@
|
||||
"version": "3.21.0",
|
||||
"resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz",
|
||||
"integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
@@ -516,7 +509,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
||||
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -641,14 +633,12 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz",
|
||||
"integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-check": {
|
||||
"version": "3.23.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
||||
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -789,7 +779,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
|
||||
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"citty": "^0.1.6",
|
||||
@@ -941,7 +930,6 @@
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
@@ -1126,14 +1114,12 @@
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.6.10",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.10.tgz",
|
||||
"integrity": "sha512-W72Hrj1petq+b3Hk2aAC+9zswetlIFTnDW4s5djseZh2nYqBbyQLOtj472HwcbcWykPBUW1WpWpjOd4nK6gMRw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"citty": "^0.2.2",
|
||||
@@ -1151,7 +1137,6 @@
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz",
|
||||
"integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
@@ -1179,7 +1164,6 @@
|
||||
"version": "2.0.12",
|
||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz",
|
||||
"integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
@@ -1213,21 +1197,18 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.3.tgz",
|
||||
"integrity": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.3.1",
|
||||
@@ -1239,14 +1220,12 @@
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz",
|
||||
"integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "6.19.3",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz",
|
||||
"integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -1298,7 +1277,6 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
||||
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -1355,7 +1333,6 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
||||
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"defu": "^6.1.4",
|
||||
@@ -1366,7 +1343,6 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
@@ -1550,7 +1526,6 @@
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
|
||||
"integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
||||
+3
-4
@@ -5,7 +5,7 @@
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"dev": "node --watch src/index.js",
|
||||
"start": "node src/index.js",
|
||||
"start": "prisma migrate deploy && node prisma/seed.js && node src/index.js",
|
||||
"seed": "node prisma/seed.js",
|
||||
"check": "node scripts/api-check.js",
|
||||
"postinstall": "prisma generate"
|
||||
@@ -16,12 +16,11 @@
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.16.2",
|
||||
"axios": "^1.12.2",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.6.1",
|
||||
"express": "^4.21.2",
|
||||
"jsonwebtoken": "^9.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"prisma": "^6.16.2"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "LoginRequest" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"secretHash" TEXT NOT NULL,
|
||||
"expiresAt" DATETIME NOT NULL,
|
||||
"decidedById" TEXT,
|
||||
"decidedAt" DATETIME,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "LoginRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "LoginRequest_decidedById_fkey" FOREIGN KEY ("decidedById") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_User" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"phone" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL,
|
||||
"dealerId" TEXT,
|
||||
"passwordHash" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "User_dealerId_fkey" FOREIGN KEY ("dealerId") REFERENCES "Dealer" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_User" ("createdAt", "dealerId", "id", "name", "phone", "role", "updatedAt") SELECT "createdAt", "dealerId", "id", "name", "phone", "role", "updatedAt" FROM "User";
|
||||
DROP TABLE "User";
|
||||
ALTER TABLE "new_User" RENAME TO "User";
|
||||
CREATE UNIQUE INDEX "User_phone_key" ON "User"("phone");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
+21
-7
@@ -14,16 +14,30 @@ model User {
|
||||
role String
|
||||
dealerId String?
|
||||
dealer Dealer? @relation("DealerStaff", fields: [dealerId], references: [id])
|
||||
currentOtp String?
|
||||
otpExpiresAt DateTime?
|
||||
passwordHash String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
assignments DealerAssignment[] @relation("SalesRepAssignments")
|
||||
stageLogs StageLog[]
|
||||
transfers StockTransfer[]
|
||||
events Event[]
|
||||
servicePoints ServicePoint[]
|
||||
assignments DealerAssignment[] @relation("SalesRepAssignments")
|
||||
stageLogs StageLog[]
|
||||
transfers StockTransfer[]
|
||||
events Event[]
|
||||
servicePoints ServicePoint[]
|
||||
loginRequests LoginRequest[] @relation("DealerLoginRequests")
|
||||
loginDecisions LoginRequest[] @relation("LoginDecisions")
|
||||
}
|
||||
|
||||
model LoginRequest {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation("DealerLoginRequests", fields: [userId], references: [id], onDelete: Cascade)
|
||||
status String
|
||||
secretHash String
|
||||
expiresAt DateTime
|
||||
decidedById String?
|
||||
decidedBy User? @relation("LoginDecisions", fields: [decidedById], references: [id])
|
||||
decidedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Dealer {
|
||||
|
||||
+9
-3
@@ -3,24 +3,30 @@ require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||
|
||||
const prisma = require('../src/db');
|
||||
const { isValidPhone, normalizePhone } = require('../src/lib/phone');
|
||||
const { hashPassword } = require('../src/lib/passwords');
|
||||
|
||||
async function main() {
|
||||
const name = String(process.env.SEED_ADMIN_NAME || 'Super Admin').trim();
|
||||
const phone = normalizePhone(process.env.SEED_ADMIN_PHONE);
|
||||
const password = String(process.env.SEED_ADMIN_PASSWORD || '');
|
||||
if (!isValidPhone(phone)) {
|
||||
throw new Error('SEED_ADMIN_PHONE must be a Sri Lankan mobile number');
|
||||
}
|
||||
if (password.length < 8) {
|
||||
throw new Error('SEED_ADMIN_PASSWORD must be at least 8 characters');
|
||||
}
|
||||
|
||||
const passwordHash = await hashPassword(password);
|
||||
const existingAdmin = await prisma.user.findFirst({ where: { role: 'superAdmin' } });
|
||||
const admin = existingAdmin
|
||||
? await prisma.user.update({
|
||||
where: { id: existingAdmin.id },
|
||||
data: { name, phone, role: 'superAdmin', dealerId: null },
|
||||
data: { name, phone, role: 'superAdmin', dealerId: null, passwordHash },
|
||||
})
|
||||
: await prisma.user.upsert({
|
||||
where: { phone },
|
||||
update: { name, role: 'superAdmin', dealerId: null },
|
||||
create: { name, phone, role: 'superAdmin' },
|
||||
update: { name, role: 'superAdmin', dealerId: null, passwordHash },
|
||||
create: { name, phone, role: 'superAdmin', passwordHash },
|
||||
});
|
||||
console.log(`Seeded super admin ${admin.name} (${admin.phone})`);
|
||||
}
|
||||
|
||||
+71
-25
@@ -22,6 +22,7 @@ execSync('npx prisma db push --skip-generate --accept-data-loss', {
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
const bcrypt = require('bcryptjs');
|
||||
const app = require('../src/app');
|
||||
const prisma = require('../src/db');
|
||||
const { ageBadge } = require('../src/lib/stockRules');
|
||||
@@ -61,31 +62,30 @@ async function run(port) {
|
||||
assert(ageBadge(45) === 'yellow', '45 days is yellow');
|
||||
assert(ageBadge(46) === 'red', '46 days is red');
|
||||
|
||||
const unknown = await request(port, 'POST', '/api/auth/send-otp', {
|
||||
const unknown = await request(port, 'POST', '/api/auth/dealer-login', {
|
||||
body: { phone: '94770000099' },
|
||||
});
|
||||
assert(unknown.status === 404, `unknown phone should be 404, got ${unknown.status}`);
|
||||
|
||||
const adminHash = await bcrypt.hash('admin-pass-123', 10);
|
||||
await prisma.user.create({
|
||||
data: { name: 'Super Admin', phone: '94770000001', role: 'superAdmin' },
|
||||
data: {
|
||||
name: 'Super Admin',
|
||||
phone: '94770000001',
|
||||
role: 'superAdmin',
|
||||
passwordHash: adminHash,
|
||||
},
|
||||
});
|
||||
|
||||
const sent = await request(port, 'POST', '/api/auth/send-otp', {
|
||||
body: { phone: '94770000001' },
|
||||
const badPassword = await request(port, 'POST', '/api/auth/staff-login', {
|
||||
body: { phone: '94770000001', password: 'wrong-password' },
|
||||
});
|
||||
assert(sent.status === 200 && sent.json.success === true, 'OTP send failed');
|
||||
assert(badPassword.status === 401, 'wrong password should be rejected');
|
||||
|
||||
const adminRow = await prisma.user.findUnique({ where: { phone: '94770000001' } });
|
||||
const wrongCode = adminRow.currentOtp === '000000' ? '111111' : '000000';
|
||||
const badOtp = await request(port, 'POST', '/api/auth/verify-otp', {
|
||||
body: { phone: '94770000001', otp: wrongCode },
|
||||
const verified = await request(port, 'POST', '/api/auth/staff-login', {
|
||||
body: { phone: '94770000001', password: 'admin-pass-123' },
|
||||
});
|
||||
assert(badOtp.status === 401, 'wrong OTP should be rejected');
|
||||
|
||||
const verified = await request(port, 'POST', '/api/auth/verify-otp', {
|
||||
body: { phone: '94770000001', otp: adminRow.currentOtp },
|
||||
});
|
||||
assert(verified.status === 200 && verified.json.token, 'OTP verify failed');
|
||||
assert(verified.status === 200 && verified.json.token, 'staff login failed');
|
||||
const adminToken = verified.json.token;
|
||||
|
||||
const me = await request(port, 'GET', '/api/auth/me', { token: adminToken });
|
||||
@@ -112,6 +112,7 @@ async function run(port) {
|
||||
body: {
|
||||
name: 'Nimal Perera',
|
||||
phone: '94771111111',
|
||||
password: 'rep-pass-123',
|
||||
dealerIds: [dealerA.json.data.id, dealerB.json.data.id],
|
||||
},
|
||||
});
|
||||
@@ -123,19 +124,64 @@ async function run(port) {
|
||||
});
|
||||
assert(dealerUser.status === 201, 'dealer user create failed');
|
||||
|
||||
async function login(phone) {
|
||||
const otpSend = await request(port, 'POST', '/api/auth/send-otp', { body: { phone } });
|
||||
assert(otpSend.status === 200, `otp send failed for ${phone}`);
|
||||
const row = await prisma.user.findUnique({ where: { phone } });
|
||||
const otpVerify = await request(port, 'POST', '/api/auth/verify-otp', {
|
||||
body: { phone, otp: row.currentOtp },
|
||||
async function staffLogin(phone, password) {
|
||||
const result = await request(port, 'POST', '/api/auth/staff-login', {
|
||||
body: { phone, password },
|
||||
});
|
||||
assert(otpVerify.status === 200, `otp verify failed for ${phone}`);
|
||||
return otpVerify.json.token;
|
||||
assert(result.status === 200 && result.json.token, `staff login failed for ${phone}`);
|
||||
return result.json.token;
|
||||
}
|
||||
|
||||
const repToken = await login('94771111111');
|
||||
const dealerToken = await login('94772222222');
|
||||
async function approveDealer(phone, token) {
|
||||
const started = await request(port, 'POST', '/api/auth/dealer-login', { body: { phone } });
|
||||
assert(started.status === 201, `dealer login failed for ${phone}: ${started.status}`);
|
||||
const approved = await request(
|
||||
port,
|
||||
'POST',
|
||||
`/api/auth/login-requests/${started.json.requestId}/approve`,
|
||||
{ token }
|
||||
);
|
||||
assert(approved.status === 200, `approve failed for ${phone}: ${approved.status}`);
|
||||
const ready = await request(
|
||||
port,
|
||||
'GET',
|
||||
`/api/auth/dealer-login/${started.json.requestId}?secret=${encodeURIComponent(started.json.secret)}`
|
||||
);
|
||||
assert(ready.status === 200 && ready.json.token, `dealer token missing for ${phone}`);
|
||||
return ready.json.token;
|
||||
}
|
||||
|
||||
const repToken = await staffLogin('94771111111', 'rep-pass-123');
|
||||
|
||||
const dealerStaff = await request(port, 'POST', '/api/auth/staff-login', {
|
||||
body: { phone: '94772222222', password: 'rep-pass-123' },
|
||||
});
|
||||
assert(dealerStaff.status === 401, 'dealer should not use staff login');
|
||||
|
||||
const otherDealer = await request(port, 'POST', '/api/admin/dealer-users', {
|
||||
token: adminToken,
|
||||
body: { name: 'Jaffna Desk', phone: '94773333333', dealerId: dealerC.json.data.id },
|
||||
});
|
||||
assert(otherDealer.status === 201, 'second dealer user create failed');
|
||||
const outsideRequest = await request(port, 'POST', '/api/auth/dealer-login', {
|
||||
body: { phone: '94773333333' },
|
||||
});
|
||||
assert(outsideRequest.status === 201, 'outside dealer login failed');
|
||||
const repQueue = await request(port, 'GET', '/api/auth/login-requests', { token: repToken });
|
||||
assert(
|
||||
repQueue.status === 200 &&
|
||||
!repQueue.json.data.some((item) => item.id === outsideRequest.json.requestId),
|
||||
'sales rep should not see an unassigned dealer login'
|
||||
);
|
||||
const repDenied = await request(
|
||||
port,
|
||||
'POST',
|
||||
`/api/auth/login-requests/${outsideRequest.json.requestId}/approve`,
|
||||
{ token: repToken }
|
||||
);
|
||||
assert(repDenied.status === 403, 'sales rep should not approve an unassigned dealer');
|
||||
|
||||
const dealerToken = await approveDealer('94772222222', adminToken);
|
||||
|
||||
const forbiddenStock = await request(port, 'POST', '/api/stocks', {
|
||||
token: dealerToken,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const ROUNDS = 10;
|
||||
|
||||
function hashPassword(password) {
|
||||
return bcrypt.hash(password, ROUNDS);
|
||||
}
|
||||
|
||||
function checkPassword(password, passwordHash) {
|
||||
if (!passwordHash) return false;
|
||||
return bcrypt.compare(password, passwordHash);
|
||||
}
|
||||
|
||||
module.exports = { hashPassword, checkPassword };
|
||||
+7
-1
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const prisma = require('../db');
|
||||
const { asyncHandler, httpError } = require('../lib/errors');
|
||||
const { isValidPhone, normalizePhone } = require('../lib/phone');
|
||||
const { hashPassword } = require('../lib/passwords');
|
||||
const { canAccessDealer } = require('../lib/access');
|
||||
const { userPublicSelect, publicUser } = require('../lib/users');
|
||||
const { requireAuth, requireRole } = require('../middleware/auth');
|
||||
@@ -60,9 +61,13 @@ router.post(
|
||||
asyncHandler(async (req, res) => {
|
||||
const name = String(req.body?.name || '').trim();
|
||||
const phone = normalizePhone(req.body?.phone);
|
||||
const password = String(req.body?.password || '');
|
||||
const dealerIds = Array.isArray(req.body?.dealerIds) ? req.body.dealerIds : [];
|
||||
if (!name || !isValidPhone(phone)) {
|
||||
throw httpError(400, 'Name and a 947XXXXXXXX phone are required');
|
||||
throw httpError(400, 'Name and a Sri Lankan mobile number are required');
|
||||
}
|
||||
if (password.length < 8) {
|
||||
throw httpError(400, 'Password must be at least 8 characters');
|
||||
}
|
||||
if (!dealerIds.every((id) => typeof id === 'string' && id.trim())) {
|
||||
throw httpError(400, 'dealerIds must be a list of ids');
|
||||
@@ -78,6 +83,7 @@ router.post(
|
||||
name,
|
||||
phone,
|
||||
role: 'salesRep',
|
||||
passwordHash: await hashPassword(password),
|
||||
assignments: {
|
||||
create: uniqueIds.map((dealerId) => ({ dealerId })),
|
||||
},
|
||||
|
||||
+177
-62
@@ -4,16 +4,18 @@ const jwt = require('jsonwebtoken');
|
||||
const prisma = require('../db');
|
||||
const { asyncHandler, httpError } = require('../lib/errors');
|
||||
const { isValidPhone, normalizePhone } = require('../lib/phone');
|
||||
const { checkPassword } = require('../lib/passwords');
|
||||
const { publicUser, userPublicSelect } = require('../lib/users');
|
||||
const { assignedDealerIds } = require('../lib/access');
|
||||
const { sendSmsOtp } = require('../services/smsService');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const { assignedDealerIds, canAccessDealer } = require('../lib/access');
|
||||
const { requireAuth, requireRole } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
const OTP_TTL_MS = 5 * 60 * 1000;
|
||||
const REQUEST_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
function generateOtp() {
|
||||
return String(crypto.randomInt(0, 1000000)).padStart(6, '0');
|
||||
function signToken(user) {
|
||||
return jwt.sign({ sub: user.id, role: user.role }, process.env.JWT_SECRET, {
|
||||
expiresIn: '10y',
|
||||
});
|
||||
}
|
||||
|
||||
async function accessibleDealers(user) {
|
||||
@@ -33,81 +35,194 @@ async function accessibleDealers(user) {
|
||||
});
|
||||
}
|
||||
|
||||
router.post(
|
||||
'/send-otp',
|
||||
asyncHandler(async (req, res) => {
|
||||
const phone = normalizePhone(req.body?.phone);
|
||||
if (!isValidPhone(phone)) {
|
||||
throw httpError(400, 'Phone must be in 947XXXXXXXX format');
|
||||
}
|
||||
function hashSecret(secret) {
|
||||
return crypto.createHash('sha256').update(secret).digest('hex');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { phone } });
|
||||
if (!user) {
|
||||
return res.status(404).json({ success: false, message: 'Not Registered' });
|
||||
}
|
||||
function secretsMatch(storedHash, secret) {
|
||||
const given = Buffer.from(hashSecret(secret));
|
||||
const stored = Buffer.from(storedHash);
|
||||
return stored.length === given.length && crypto.timingSafeEqual(stored, given);
|
||||
}
|
||||
|
||||
const otp = generateOtp();
|
||||
const otpExpiresAt = new Date(Date.now() + OTP_TTL_MS);
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { currentOtp: otp, otpExpiresAt },
|
||||
});
|
||||
|
||||
try {
|
||||
await sendSmsOtp(phone, otp);
|
||||
} catch (error) {
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { currentOtp: null, otpExpiresAt: null },
|
||||
});
|
||||
throw httpError(502, error.message || 'Failed to send OTP SMS');
|
||||
}
|
||||
|
||||
return res.json({ success: true, message: 'OTP sent via SMS' });
|
||||
})
|
||||
);
|
||||
async function expireStaleRequests() {
|
||||
await prisma.loginRequest.updateMany({
|
||||
where: { status: 'pending', expiresAt: { lt: new Date() } },
|
||||
data: { status: 'expired' },
|
||||
});
|
||||
}
|
||||
|
||||
router.post(
|
||||
'/verify-otp',
|
||||
'/staff-login',
|
||||
asyncHandler(async (req, res) => {
|
||||
const phone = normalizePhone(req.body?.phone);
|
||||
const otp = String(req.body?.otp || '').trim();
|
||||
if (!isValidPhone(phone) || !/^\d{6}$/.test(otp)) {
|
||||
throw httpError(400, 'Phone and a 6-digit OTP are required');
|
||||
const password = String(req.body?.password || '');
|
||||
if (!isValidPhone(phone) || !password) {
|
||||
throw httpError(400, 'Phone and password are required');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { phone },
|
||||
select: {
|
||||
...userPublicSelect,
|
||||
currentOtp: true,
|
||||
otpExpiresAt: true,
|
||||
select: { ...userPublicSelect, passwordHash: true },
|
||||
});
|
||||
const allowed = user && (user.role === 'superAdmin' || user.role === 'salesRep');
|
||||
const matches = allowed && (await checkPassword(password, user.passwordHash));
|
||||
if (!matches) {
|
||||
throw httpError(401, 'Invalid phone or password');
|
||||
}
|
||||
|
||||
const dealers = await accessibleDealers(user);
|
||||
return res.json({ token: signToken(user), user: publicUser(user, dealers) });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/dealer-login',
|
||||
asyncHandler(async (req, res) => {
|
||||
const phone = normalizePhone(req.body?.phone);
|
||||
if (!isValidPhone(phone)) {
|
||||
throw httpError(400, 'Phone must be a Sri Lankan mobile number');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { phone } });
|
||||
if (!user || user.role !== 'dealer') {
|
||||
return res.status(404).json({ success: false, message: 'Not Registered' });
|
||||
}
|
||||
|
||||
await prisma.loginRequest.updateMany({
|
||||
where: { userId: user.id, status: 'pending' },
|
||||
data: { status: 'expired' },
|
||||
});
|
||||
|
||||
const secret = crypto.randomBytes(32).toString('hex');
|
||||
const request = await prisma.loginRequest.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
status: 'pending',
|
||||
secretHash: hashSecret(secret),
|
||||
expiresAt: new Date(Date.now() + REQUEST_TTL_MS),
|
||||
},
|
||||
});
|
||||
if (!user || !user.currentOtp || !user.otpExpiresAt) {
|
||||
throw httpError(401, 'Invalid OTP');
|
||||
}
|
||||
if (user.otpExpiresAt.getTime() < Date.now()) {
|
||||
throw httpError(401, 'OTP expired');
|
||||
|
||||
return res.status(201).json({ requestId: request.id, secret });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/dealer-login/:id',
|
||||
asyncHandler(async (req, res) => {
|
||||
const secret = String(req.query.secret || '');
|
||||
const request = await prisma.loginRequest.findUnique({
|
||||
where: { id: req.params.id },
|
||||
include: { user: { select: userPublicSelect } },
|
||||
});
|
||||
if (!request || !secret || !secretsMatch(request.secretHash, secret)) {
|
||||
throw httpError(404, 'Login request not found');
|
||||
}
|
||||
|
||||
const stored = Buffer.from(user.currentOtp);
|
||||
const given = Buffer.from(otp);
|
||||
const matches = stored.length === given.length && crypto.timingSafeEqual(stored, given);
|
||||
if (!matches) {
|
||||
throw httpError(401, 'Invalid OTP');
|
||||
if (request.status === 'pending' && request.expiresAt.getTime() < Date.now()) {
|
||||
await prisma.loginRequest.update({
|
||||
where: { id: request.id },
|
||||
data: { status: 'expired' },
|
||||
});
|
||||
return res.json({ status: 'expired' });
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { currentOtp: null, otpExpiresAt: null },
|
||||
if (request.status !== 'approved') {
|
||||
return res.json({ status: request.status });
|
||||
}
|
||||
|
||||
const consumed = await prisma.loginRequest.updateMany({
|
||||
where: { id: request.id, status: 'approved' },
|
||||
data: { status: 'consumed' },
|
||||
});
|
||||
if (consumed.count !== 1) {
|
||||
return res.json({ status: 'consumed' });
|
||||
}
|
||||
|
||||
const dealers = await accessibleDealers(request.user);
|
||||
return res.json({
|
||||
status: 'approved',
|
||||
token: signToken(request.user),
|
||||
user: publicUser(request.user, dealers),
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/login-requests',
|
||||
requireAuth,
|
||||
requireRole('superAdmin', 'salesRep'),
|
||||
asyncHandler(async (req, res) => {
|
||||
await expireStaleRequests();
|
||||
const where = { status: 'pending' };
|
||||
if (req.user.role === 'salesRep') {
|
||||
const ids = assignedDealerIds(req.user);
|
||||
where.user = { dealerId: { in: ids.length ? ids : ['__none__'] } };
|
||||
}
|
||||
|
||||
const requests = await prisma.loginRequest.findMany({
|
||||
where,
|
||||
include: { user: { include: { dealer: true } } },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
const token = jwt.sign({ sub: user.id, role: user.role }, process.env.JWT_SECRET, {
|
||||
expiresIn: '10y',
|
||||
return res.json({
|
||||
success: true,
|
||||
data: requests.map((request) => ({
|
||||
id: request.id,
|
||||
status: request.status,
|
||||
createdAt: request.createdAt,
|
||||
expiresAt: request.expiresAt,
|
||||
user: {
|
||||
id: request.user.id,
|
||||
name: request.user.name,
|
||||
phone: request.user.phone,
|
||||
dealerId: request.user.dealerId,
|
||||
},
|
||||
dealerName: request.user.dealer ? request.user.dealer.name : null,
|
||||
})),
|
||||
});
|
||||
const dealers = await accessibleDealers(user);
|
||||
return res.json({ token, user: publicUser(user, dealers) });
|
||||
})
|
||||
);
|
||||
|
||||
async function decideRequest(req, status) {
|
||||
await expireStaleRequests();
|
||||
const request = await prisma.loginRequest.findUnique({
|
||||
where: { id: req.params.id },
|
||||
include: { user: true },
|
||||
});
|
||||
if (!request || request.status !== 'pending') {
|
||||
throw httpError(404, 'Login request not found');
|
||||
}
|
||||
if (!canAccessDealer(req.user, request.user.dealerId)) {
|
||||
throw httpError(403, 'Forbidden');
|
||||
}
|
||||
|
||||
await prisma.loginRequest.update({
|
||||
where: { id: request.id },
|
||||
data: { status, decidedById: req.user.id, decidedAt: new Date() },
|
||||
});
|
||||
return { id: request.id, status };
|
||||
}
|
||||
|
||||
router.post(
|
||||
'/login-requests/:id/approve',
|
||||
requireAuth,
|
||||
requireRole('superAdmin', 'salesRep'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const result = await decideRequest(req, 'approved');
|
||||
return res.json({ success: true, data: result });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/login-requests/:id/reject',
|
||||
requireAuth,
|
||||
requireRole('superAdmin', 'salesRep'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const result = await decideRequest(req, 'rejected');
|
||||
return res.json({ success: true, data: result });
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
const axios = require('axios');
|
||||
|
||||
const sendSmsOtp = async (phoneNumber, otpCode) => {
|
||||
if (!process.env.TEXTLK_API_TOKEN) {
|
||||
console.log(`DEV OTP for ${phoneNumber}: ${otpCode}`);
|
||||
return { dev: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
'https://app.text.lk/api/v3/sms/send',
|
||||
{
|
||||
recipient: phoneNumber,
|
||||
sender_id: process.env.TEXTLK_SENDER_ID || 'TextLKDemo',
|
||||
type: 'plain',
|
||||
message: `Your login OTP code is: ${otpCode}. Valid for 5 minutes.`,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.TEXTLK_API_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Text.lk SMS Error:', error.response?.data || error.message);
|
||||
throw new Error('Failed to send OTP SMS');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { sendSmsOtp };
|
||||
Reference in New Issue
Block a user