Skip to content

Commit c5cab7a

Browse files
committed
Merge pull request #21 from ThePiratePhone/add-suport-for-sms
## Pull Request Overview This PR adds support for sending and managing SMS messages within the admin area. - Introduces a singleton `Sms` class for gateway integration, token management, and sending messages. - Extends utility functions for phone normalization and validation. - Adds three new endpoints (`smsStatus`, `sendSms`, `setPhone`), updates routes, tests, model, and docs. ### Reviewed Changes Copilot reviewed 20 out of 22 changed files in this pull request and generated 6 comments. <details> <summary>Show a summary per file</summary> | File | Description | |------------------------------------|-------------------------------------------------------| | tools/utils.ts | Updated phone validation logic | | tools/sms.ts | New `Sms` class handling SMS gateway interactions | | router/admin/area/smsStatus.ts | Implement `/admin/area/smsStatus` endpoint | | router/admin/area/sendSms.ts | Implement `/admin/area/sendSms` endpoint | | router/admin/area/setPhone.ts | Implement `/admin/area/setPhone` endpoint | | routes.ts | Registered new SMS-related routes | | Models/Area.ts | Added `adminPhone` field to `Area` schema | | README.md | Added versioning section | </details> <details> <summary>Comments suppressed due to low confidence (1)</summary> **tools/sms.ts:10** * The property name 'gatway' appears to be a typo. Rename it to 'gateway' for clarity and consistency. ``` gatway: string; ``` </details>
2 parents 67c1784 + 55c824d commit c5cab7a

22 files changed

+731
-180
lines changed

Models/Area.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,6 @@ const AreaModel = new mongoose.Schema({
66
required: true,
77
index: true
88
},
9-
password: {
10-
type: String,
11-
required: true,
12-
index: true
13-
},
149
campaignList: {
1510
type: Array<typeof mongoose.Schema.ObjectId>,
1611
ref: 'Campaign',
@@ -23,6 +18,12 @@ const AreaModel = new mongoose.Schema({
2318
createdAt: {
2419
type: Date,
2520
default: Date.now()
21+
},
22+
adminPhone: {
23+
type: Array<[string, string | undefined]>, // [phone, name?]
24+
required: true,
25+
index: false,
26+
default: []
2627
}
2728
});
2829

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,10 @@ or
3939
```bash
4040
npm run test file
4141
```
42+
43+
## versioning
44+
major.minor.patch
45+
46+
- **major**: add or remove a model object for the database
47+
- **minor**: modification on a model, requires a database upgrade
48+
- **patch**: no modification on the database

package-lock.json

Lines changed: 55 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@types/cors": "^2.8.17",
3434
"@types/express": "^5.0.0",
3535
"@types/jest": "^29.5.14",
36+
"@types/node-fetch": "^2.6.12",
3637
"@types/supertest": "^6.0.2",
3738
"jest": "^29.7.0"
3839
},
@@ -46,6 +47,7 @@
4647
"js-sha512": "^0.9.0",
4748
"mongodb": "^6.13.1",
4849
"mongoose": "^8.10.2",
50+
"node-fetch": "^2.7.0",
4951
"supertest": "^7.0.0",
5052
"ts-jest": "^29.2.6",
5153
"ts-node": "^10.9.2"

router/admin/.DS_Store

8 KB
Binary file not shown.

router/admin/area/changeName.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default async function ChangeName(req: Request<any>, res: Response<any>)
3232
res,
3333
[
3434
['adminCode', 'string'],
35-
['area', 'string'],
35+
['area', 'ObjectId'],
3636
['newName', 'string'],
3737
['allreadyHaseded', 'boolean', true]
3838
],

router/admin/area/changePassword.ts

Lines changed: 0 additions & 75 deletions
This file was deleted.

router/admin/area/sendSms.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Request, Response } from 'express';
2+
3+
import { Area } from '../../../Models/Area';
4+
import { log } from '../../../tools/log';
5+
import sms from '../../../tools/sms';
6+
import { checkParameters, clearPhone, hashPasword, phoneNumberCheck } from '../../../tools/utils';
7+
8+
export default async function sendSms(req: Request<any>, res: Response<any>) {
9+
const ip =
10+
(Array.isArray(req.headers['x-forwarded-for'])
11+
? req.headers['x-forwarded-for'][0]
12+
: req.headers['x-forwarded-for']?.split(',')?.[0] ?? req.ip) ?? 'no IP';
13+
if (
14+
!checkParameters(
15+
req.body,
16+
res,
17+
[
18+
['adminCode', 'string'],
19+
['area', 'ObjectId'],
20+
['allreadyHaseded', 'boolean', true],
21+
['message', 'string']
22+
],
23+
__filename
24+
)
25+
)
26+
return;
27+
28+
if (req.body.phone && (!Array.isArray(req.body.phone) || req.body.phone.length === 0)) {
29+
res.status(400).send({ message: 'Invalid phone, phone must be a array<[phone, name]>', OK: false });
30+
log(`[!${req.body.area}, ${ip}] Invalid phone`, 'WARNING', __filename);
31+
return;
32+
}
33+
34+
let errored = false;
35+
req.body.phone.forEach((phone: [string, string | undefined]) => {
36+
if ((!errored && typeof phone[0] !== 'string') || !phoneNumberCheck(clearPhone(phone[0]))) {
37+
errored = true;
38+
res.status(400).send({ message: 'Invalid phone number', OK: false });
39+
log(`[!${req.body.area}, ${ip}] Invalid phone number: ${phone[0]}`, 'WARNING', __filename);
40+
return;
41+
}
42+
});
43+
if (errored) {
44+
return;
45+
}
46+
47+
//pass phone to [name, phone]
48+
req.body.phone = req.body.phone.map((phone: string | [string, string | undefined]) => {
49+
if (Array.isArray(phone) && phone.length === 2) {
50+
return [phone[1], clearPhone(phone[0])];
51+
}
52+
});
53+
54+
const password = hashPasword(req.body.adminCode, req.body.allreadyHaseded, res);
55+
const area = await Area.findOne({ _id: { $eq: req.body.area }, adminPassword: { $eq: password } }, ['adminPhone']);
56+
if (!area) {
57+
res.status(404).send({ message: 'no area found, or bad password', OK: false });
58+
log(`[!${req.body.area}, ${ip}] no area found, or bad password`, 'WARNING', __filename);
59+
return;
60+
}
61+
62+
if (sms.enabled) {
63+
sms.sendSms(req.body.phone, req.body.message)
64+
.then(() => {
65+
res.status(200).send({ OK: true, message: 'SMS sent successfully' });
66+
log(`[${req.body.area}, ${ip}] SMS sent to ${req.body.phone}`, 'INFO', __filename);
67+
})
68+
.catch(err => {
69+
res.status(500).send({ OK: false, message: `Failed to send SMS: ${err.message}` });
70+
log(`[${req.body.area}, ${ip}] Failed to send SMS: ${err.message}`, 'ERROR', __filename);
71+
});
72+
} else {
73+
res.status(503).send({ OK: false, message: 'SMS service is not enabled' });
74+
log(`[${req.body.area}, ${ip}] SMS service is not enabled`, 'WARNING', __filename);
75+
return;
76+
}
77+
}

router/admin/area/setPhone.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Request, Response } from 'express';
2+
3+
import { Area } from '../../../Models/Area';
4+
import { log } from '../../../tools/log';
5+
import { checkParameters, clearPhone, hashPasword, phoneNumberCheck } from '../../../tools/utils';
6+
7+
export default async function setPhone(req: Request<any>, res: Response<any>) {
8+
const ip =
9+
(Array.isArray(req.headers['x-forwarded-for'])
10+
? req.headers['x-forwarded-for'][0]
11+
: req.headers['x-forwarded-for']?.split(',')?.[0] ?? req.ip) ?? 'no IP';
12+
if (
13+
!checkParameters(
14+
req.body,
15+
res,
16+
[
17+
['adminCode', 'string'],
18+
['area', 'ObjectId'],
19+
['allreadyHaseded', 'boolean', true]
20+
],
21+
__filename
22+
)
23+
)
24+
return;
25+
26+
if (req.body.phone && (!Array.isArray(req.body.phone) || req.body.phone.length === 0)) {
27+
res.status(400).send({ message: 'Invalid phone, phone must be a array<[phone, name]>', OK: false });
28+
log(`[!${req.body.area}, ${ip}] Invalid phone`, 'WARNING', __filename);
29+
return;
30+
}
31+
32+
const errored = req.body.phone.some((phoneCombo: [string, string]) => {
33+
const [phone, name] = [clearPhone(phoneCombo[0]), phoneCombo[1].trim()];
34+
if (typeof phone !== 'string' || !phoneNumberCheck(phone) || typeof name != 'string') {
35+
res.status(400).send({ message: 'Invalid phone number', OK: false });
36+
log(`[!${req.body.area}, ${ip}] Invalid phone number: ${phone}`, 'WARNING', __filename);
37+
return true;
38+
}
39+
return false;
40+
});
41+
42+
if (errored) {
43+
return;
44+
}
45+
46+
req.body.phone = req.body.phone.map((phoneCombo: [string, string]) => [
47+
clearPhone(phoneCombo[0]),
48+
phoneCombo[1].trim()
49+
]);
50+
51+
const password = hashPasword(req.body.adminCode, req.body.allreadyHaseded, res);
52+
const area = await Area.updateOne(
53+
{ _id: { $eq: req.body.area }, adminPassword: { $eq: password } },
54+
{ adminPhone: req.body.phone },
55+
['adminPhone']
56+
);
57+
if (area.matchedCount === 0) {
58+
res.status(404).send({ message: 'no area found, or bad password', OK: false });
59+
log(`[!${req.body.area}, ${ip}] no area found, or bad password`, 'WARNING', __filename);
60+
return;
61+
}
62+
63+
res.status(200).send({ OK: true, message: 'admin phone number updated' });
64+
log(`[${req.body.area}, ${ip}] admin phone number updated`, 'INFO', __filename);
65+
}

0 commit comments

Comments
 (0)