Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ export default tseslint.config(
},
sourceType: 'commonjs',
parserOptions: {
projectService: {
allowDefaultProject: ['src/**/*.spec.ts', 'test/**/*.ts'],
},
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
Expand All @@ -33,4 +31,4 @@ export default tseslint.config(
'@typescript-eslint/no-unsafe-argument': 'warn'
},
},
);
);
8 changes: 8 additions & 0 deletions libs/shared-communication/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": ".."
},
"include": ["./**/*"],
"exclude": ["node_modules", "dist"]
}
39 changes: 27 additions & 12 deletions microservices/sms-service/.env.example
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
NODE_ENV=development
SERVICE_PORT=3010
SERVICE_PORT=3007
SERVICE_VERSION=1.0.0

DB_HOST=localhost
DB_TYPE=postgres
DB_HOST=127.0.0.1
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=sms_service
DB_SYNCHRONIZE=true
DB_SYNC=true

SMS_PROVIDER=console
SMS_DEFAULT_FROM=Quest
SMS_PROVIDER=mock
SMS_DEFAULT_COUNTRY=US
SMS_SENDER_ID=Quest
SMS_STATUS_CALLBACK_URL=http://localhost:3007/receipts/webhook
SMS_RATE_LIMIT_WINDOW_MINUTES=10
SMS_RATE_LIMIT_MAX_PER_WINDOW=20
SMS_OTP_RATE_LIMIT_WINDOW_MINUTES=10
SMS_OTP_RATE_LIMIT_MAX_PER_WINDOW=3
SMS_DISPATCH_INTERVAL_MS=5000
SMS_DISPATCH_BATCH_SIZE=25
SMS_RETRY_BASE_DELAY_MS=60000
SMS_MAX_RETRIES=3
SMS_OTP_LENGTH=6
SMS_OTP_EXPIRY_MINUTES=10
SMS_OTP_MAX_ATTEMPTS=5
SMS_OTP_SECRET=replace-me
SMS_OTP_TEMPLATE_NAME=otp-verification
SMS_DEBUG_EXPOSE_CODES=false

TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_FROM_NUMBER=
TWILIO_STATUS_CALLBACK_URL=
TWILIO_MESSAGING_SERVICE_SID=

AWS_REGION=us-east-1
AWS_SNS_SENDER_ID=Quest

OTP_TTL_SECONDS=300
OTP_LENGTH=6
SMS_RATE_LIMIT_MAX=5
SMS_RATE_LIMIT_WINDOW_SECONDS=60
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_SNS_SENDER_ID=
4 changes: 4 additions & 0 deletions microservices/sms-service/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist
node_modules
.env
coverage
4 changes: 4 additions & 0 deletions microservices/sms-service/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
37 changes: 37 additions & 0 deletions microservices/sms-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Build stage
FROM node:18-alpine AS builder

WORKDIR /app

COPY package*.json ./
COPY tsconfig*.json ./
COPY nest-cli.json ./

RUN npm ci

COPY src ./src

RUN npm run build

FROM node:18-alpine

WORKDIR /app

RUN apk add --no-cache dumb-init

COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

COPY --from=builder /app/dist ./dist

RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001

USER nodejs

EXPOSE 3007

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3007/health || exit 1

ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/main.js"]
130 changes: 104 additions & 26 deletions microservices/sms-service/README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,123 @@
# SMS Service

NestJS microservice for sending verification codes, alerts, and time-sensitive text messages.
A NestJS microservice for sending SMS verification codes, alerts, reminders, and other time-sensitive notifications.

## Features

- SMS, message, and delivery receipt entities backed by TypeORM/PostgreSQL
- Provider abstraction with local console sending and Twilio HTTP support
- Secure OTP generation with hashed verification codes and expiry
- Handlebars message templates
- Scheduled message dispatch through `@nestjs/schedule`
- Delivery receipt webhook endpoint
- Phone validation, per-phone rate limiting, history, and analytics
- Docker and docker-compose configuration
- Twilio, AWS SNS, and built-in mock provider support
- Secure OTP generation and verification
- Message templates with Handlebars rendering
- Scheduled delivery with automatic retry backoff
- Delivery receipts and webhook/manual confirmation endpoints
- Phone number normalization and validation
- Message history and analytics
- Rate limiting per phone number and stricter OTP throttling

## Local Run
## Quick Start

```bash
cd microservices/sms-service
npm install
cp .env.example .env
npm run start:dev
```

The default `SMS_PROVIDER=console` logs outbound SMS messages and marks them sent. Set `SMS_PROVIDER=twilio` with `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, and `TWILIO_FROM_NUMBER` to send real SMS.
The default configuration uses the `mock` provider, so the service can run locally without Twilio or AWS credentials.

## API
## Key Environment Variables

- `GET /health`
- `POST /sms/send`
- `POST /sms/send-template`
- `POST /sms/otp`
- `POST /sms/otp/verify`
- `POST /sms/receipts`
- `GET /sms`
- `GET /sms/:id`
- `POST /sms/:id/cancel`
- `GET /sms-analytics`
- `GET /sms-templates`
| Variable | Description | Default |
|---|---|---|
| `SERVICE_PORT` | HTTP port | `3007` |
| `DB_TYPE` | `postgres` or `sqljs` | `postgres` |
| `SMS_PROVIDER` | `mock`, `twilio`, or `sns` | `mock` |
| `SMS_DEFAULT_COUNTRY` | Phone parsing fallback region | `US` |
| `SMS_DISPATCH_INTERVAL_MS` | Poll interval for scheduled/retry messages | `5000` |
| `SMS_RATE_LIMIT_MAX_PER_WINDOW` | Max SMS per phone per window | `20` |
| `SMS_OTP_RATE_LIMIT_MAX_PER_WINDOW` | Max OTP sends per phone per window | `3` |
| `SMS_OTP_EXPIRY_MINUTES` | OTP expiration | `10` |
| `SMS_DEBUG_EXPOSE_CODES` | Include OTP codes in responses for local testing | `false` |

## Docker
## Main Endpoints

### Service

- `GET /` - service info
- `GET /health` - health check

### SMS

- `POST /sms/send` - send plain SMS
- `POST /sms/send-templated` - send SMS from template
- `GET /sms/history` - list message history
- `GET /sms/stats` - aggregated delivery analytics
- `POST /sms/:id/cancel` - cancel pending/scheduled SMS
- `POST /sms/:id/retry` - retry failed SMS

### Templates

- `POST /templates` - create template
- `GET /templates` - list templates
- `GET /templates/:id` - fetch template
- `GET /templates/name/:name` - fetch template by name
- `PUT /templates/:id` - update template
- `DELETE /templates/:id` - delete template
- `POST /templates/render` - render a template payload

### OTP

- `POST /otp/send` - generate and send verification code
- `POST /otp/verify` - verify a code

### Receipts

- `POST /receipts/confirm` - manually confirm/update delivery status
- `POST /receipts/webhook` - provider webhook target
- `GET /receipts/message/:messageId` - list receipt events for a message

## Example Requests

### Send an Alert

```bash
cd microservices/sms-service
docker compose -f docker/docker-compose.yml up --build
curl -X POST http://localhost:3007/sms/send \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "+15555550123",
"body": "Your Quest account password was changed.",
"type": "alert",
"priority": "high"
}'
```

### Create a Template

```bash
curl -X POST http://localhost:3007/templates \
-H "Content-Type: application/json" \
-d '{
"name": "otp-verification",
"body": "Your {{purpose}} code is {{code}}. It expires in {{expiryMinutes}} minutes.",
"category": "otp"
}'
```

### Send OTP

```bash
curl -X POST http://localhost:3007/otp/send \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "+15555550123",
"purpose": "login"
}'
```

## Testing

```bash
npm run build
npm run test:e2e
```

The e2e test suite runs against `sqljs` and the mock provider, so it does not require PostgreSQL or a real SMS account.
34 changes: 19 additions & 15 deletions microservices/sms-service/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,32 @@ services:
container_name: sms-service
build:
context: ..
dockerfile: docker/Dockerfile
dockerfile: Dockerfile
ports:
- '3010:3010'
- '3007:3007'
environment:
- NODE_ENV=development
- SERVICE_PORT=3010
- SERVICE_PORT=3007
- DB_TYPE=postgres
- DB_HOST=postgres
- DB_PORT=5432
- DB_USER=${DB_USER:-postgres}
- DB_PASSWORD=${DB_PASSWORD:-postgres}
- DB_NAME=${DB_NAME:-sms_service}
- SMS_PROVIDER=${SMS_PROVIDER:-console}
- SMS_DEFAULT_FROM=${SMS_DEFAULT_FROM:-Quest}
- DB_SYNC=true
- SMS_PROVIDER=${SMS_PROVIDER:-mock}
- SMS_DEFAULT_COUNTRY=${SMS_DEFAULT_COUNTRY:-US}
- SMS_SENDER_ID=${SMS_SENDER_ID:-Quest}
- SMS_STATUS_CALLBACK_URL=${SMS_STATUS_CALLBACK_URL:-http://localhost:3007/receipts/webhook}
- SMS_OTP_SECRET=${SMS_OTP_SECRET:-replace-me}
- TWILIO_ACCOUNT_SID=${TWILIO_ACCOUNT_SID:-}
- TWILIO_AUTH_TOKEN=${TWILIO_AUTH_TOKEN:-}
- TWILIO_FROM_NUMBER=${TWILIO_FROM_NUMBER:-}
- TWILIO_STATUS_CALLBACK_URL=${TWILIO_STATUS_CALLBACK_URL:-}
- OTP_TTL_SECONDS=${OTP_TTL_SECONDS:-300}
- OTP_LENGTH=${OTP_LENGTH:-6}
- SMS_RATE_LIMIT_MAX=${SMS_RATE_LIMIT_MAX:-5}
- SMS_RATE_LIMIT_WINDOW_SECONDS=${SMS_RATE_LIMIT_WINDOW_SECONDS:-60}
- TWILIO_MESSAGING_SERVICE_SID=${TWILIO_MESSAGING_SERVICE_SID:-}
- AWS_REGION=${AWS_REGION:-us-east-1}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
- AWS_SNS_SENDER_ID=${AWS_SNS_SENDER_ID:-Quest}
depends_on:
postgres:
condition: service_healthy
Expand All @@ -39,19 +44,18 @@ services:
environment:
- POSTGRES_USER=${DB_USER:-postgres}
- POSTGRES_PASSWORD=${DB_PASSWORD:-postgres}
- POSTGRES_DB=sms_service
- POSTGRES_DB=${DB_NAME:-sms_service}
ports:
- '5440:5432'
- '5437:5432'
volumes:
- postgres_sms_data:/var/lib/postgresql/data
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
networks:
- sms-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- sms-network
restart: unless-stopped

volumes:
Expand Down
Loading
Loading