This commit is contained in:
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"mcp__acp__Write",
|
||||||
|
"mcp__acp__Bash",
|
||||||
|
"mcp__acp__Edit"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
data
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
39
.gitea/workflows/build.yaml
Normal file
39
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Build SMS Gateway
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.realmanual.ru
|
||||||
|
IMAGE_PREFIX: ${{ gitea.repository }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build image
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Read Version
|
||||||
|
id: version
|
||||||
|
run: echo "VERSION=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||||
|
- name: Log in to Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.TOKEN }}
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/kb-admin:${{ steps.version.outputs.VERSION }}
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/kb-admin:latest
|
||||||
|
|
||||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm install --production
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["npm", "start"]
|
||||||
171
README.md
Normal file
171
README.md
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# SMS OTP Gateway
|
||||||
|
|
||||||
|
Легковесный сервис для логирования SMS-сообщений. Принимает запросы в формате SMS-провайдера и сохраняет их в локальный файл для просмотра.
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- Docker и Docker Compose
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Сборка и запуск
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Просмотр логов
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Остановка
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
Сервис будет доступен на `http://localhost:3000`
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### POST /send-msg
|
||||||
|
|
||||||
|
Принимает SMS-сообщение и сохраняет в протокол.
|
||||||
|
|
||||||
|
**Content-Type:** `application/x-www-form-urlencoded`
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
|----------|--------|-------------------|
|
||||||
|
| login | string | Логин отправителя |
|
||||||
|
| psw | string | Пароль |
|
||||||
|
| phones | string | Номер телефона |
|
||||||
|
| mes | string | Текст сообщения |
|
||||||
|
|
||||||
|
**Пример запроса:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/send-msg \
|
||||||
|
-d "login=admin&psw=secret&phones=+79001234567&mes=Your code: 1234"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ (успех):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"id": 1705678901234
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ (неверные credentials):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Invalid credentials"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /view-all-sms
|
||||||
|
|
||||||
|
Возвращает все сохраненные сообщения.
|
||||||
|
|
||||||
|
**Пример запроса:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/view-all-sms
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"id": 1705678901234,
|
||||||
|
"timestamp": "2025-01-19T12:00:00.000Z",
|
||||||
|
"login": "user",
|
||||||
|
"phone": "+79001234567",
|
||||||
|
"message": "Your code: 1234"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /clear-all-sms
|
||||||
|
|
||||||
|
Очищает все сохраненные сообщения.
|
||||||
|
|
||||||
|
**Пример запроса:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/clear-all-sms
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"message": "All messages cleared"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Веб-интерфейс
|
||||||
|
|
||||||
|
Доступен по адресу `http://localhost:3000`
|
||||||
|
|
||||||
|
Функции:
|
||||||
|
- Просмотр всех сообщений в реальном времени (автообновление каждые 5 сек)
|
||||||
|
- Ручное обновление списка
|
||||||
|
- Очистка всех сообщений
|
||||||
|
|
||||||
|
## Хранение данных
|
||||||
|
|
||||||
|
Сообщения сохраняются в файл `data/base.json`. Директория `data/` монтируется как volume, данные сохраняются между перезапусками контейнера.
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
| Переменная | По умолчанию | Описание |
|
||||||
|
|--------------|--------------|------------------------------|
|
||||||
|
| PORT | 3000 | Порт сервера |
|
||||||
|
| SMS_LOGIN | - | Логин для авторизации (обяз) |
|
||||||
|
| SMS_PASSWORD | - | Пароль для авторизации (обяз)|
|
||||||
|
|
||||||
|
Переменные `SMS_LOGIN` и `SMS_PASSWORD` задаются в `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- SMS_LOGIN=admin
|
||||||
|
- SMS_PASSWORD=secret
|
||||||
|
```
|
||||||
|
|
||||||
|
Изменение порта в docker-compose.yml:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
sms-gateway:
|
||||||
|
ports:
|
||||||
|
- "8080:3000" # внешний:внутренний
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
sms-opt-gateway/
|
||||||
|
├── server.js # Express сервер
|
||||||
|
├── package.json # Зависимости
|
||||||
|
├── Dockerfile # Docker образ
|
||||||
|
├── docker-compose.yml # Compose конфигурация
|
||||||
|
├── .dockerignore
|
||||||
|
├── public/
|
||||||
|
│ └── index.html # Веб-интерфейс
|
||||||
|
└── data/
|
||||||
|
└── base.json # Хранилище сообщений
|
||||||
|
```
|
||||||
|
|
||||||
|
## Локальная разработка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установка зависимостей
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Запуск
|
||||||
|
npm start
|
||||||
|
```
|
||||||
3
data/base.json
Normal file
3
data/base.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"messages": []
|
||||||
|
}
|
||||||
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
sms-gateway:
|
||||||
|
build: .
|
||||||
|
container_name: sms-otp-gateway
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
environment:
|
||||||
|
- SMS_LOGIN=admin
|
||||||
|
- SMS_PASSWORD=secret
|
||||||
|
restart: unless-stopped
|
||||||
12
package.json
Normal file
12
package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "sms-otp-gateway",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Simple SMS OTP Gateway Service",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
153
public/index.html
Normal file
153
public/index.html
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>SMS OTP Gateway</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #0a1628;
|
||||||
|
color: #e0e6ed;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #4a9eff;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 300;
|
||||||
|
border-bottom: 1px solid #1e3a5f;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #1e3a5f;
|
||||||
|
color: #4a9eff;
|
||||||
|
border: 1px solid #2d4a6f;
|
||||||
|
padding: 10px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #2d4a6f;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
}
|
||||||
|
button.danger {
|
||||||
|
color: #ff6b6b;
|
||||||
|
border-color: #5f1e1e;
|
||||||
|
}
|
||||||
|
button.danger:hover {
|
||||||
|
background: #3f1e1e;
|
||||||
|
border-color: #ff6b6b;
|
||||||
|
}
|
||||||
|
.messages {
|
||||||
|
background: #0d1f35;
|
||||||
|
border: 1px solid #1e3a5f;
|
||||||
|
padding: 15px;
|
||||||
|
min-height: 400px;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #1e3a5f;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.message:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.message .time {
|
||||||
|
color: #6b8aaa;
|
||||||
|
}
|
||||||
|
.message .phone {
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
.message .text {
|
||||||
|
color: #7dcea0;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
color: #4a6a8a;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b8aaa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>SMS OTP Gateway</h1>
|
||||||
|
<div class="controls">
|
||||||
|
<button onclick="loadMessages()">Refresh</button>
|
||||||
|
<button class="danger" onclick="clearMessages()">Clear All</button>
|
||||||
|
</div>
|
||||||
|
<div class="messages" id="messages">
|
||||||
|
<div class="empty">Loading...</div>
|
||||||
|
</div>
|
||||||
|
<div class="status" id="status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function loadMessages() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/view-all-sms');
|
||||||
|
const data = await res.json();
|
||||||
|
const container = document.getElementById('messages');
|
||||||
|
|
||||||
|
if (data.messages.length === 0) {
|
||||||
|
container.innerHTML = '<div class="empty">No messages yet</div>';
|
||||||
|
} else {
|
||||||
|
container.innerHTML = data.messages.map(m => `
|
||||||
|
<div class="message">
|
||||||
|
<span class="time">[${m.timestamp}]</span>
|
||||||
|
<span class="phone">${m.phone}</span>:
|
||||||
|
<span class="text">${escapeHtml(m.message)}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('status').textContent = `Last updated: ${new Date().toLocaleTimeString()} | Total: ${data.messages.length} messages`;
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById('messages').innerHTML = '<div class="empty">Error loading messages</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearMessages() {
|
||||||
|
try {
|
||||||
|
await fetch('/clear-all-sms', { method: 'POST' });
|
||||||
|
loadMessages();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Error clearing messages');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMessages();
|
||||||
|
setInterval(loadMessages, 5000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
94
server.js
Normal file
94
server.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const BASE_FILE = path.join(__dirname, "data", "base.json");
|
||||||
|
|
||||||
|
// Auth credentials from environment
|
||||||
|
const AUTH_LOGIN = process.env.SMS_LOGIN;
|
||||||
|
const AUTH_PASSWORD = process.env.SMS_PASSWORD;
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.static("public"));
|
||||||
|
|
||||||
|
// Ensure data directory and base.json exist
|
||||||
|
function ensureBaseFile() {
|
||||||
|
const dataDir = path.dirname(BASE_FILE);
|
||||||
|
if (!fs.existsSync(dataDir)) {
|
||||||
|
fs.mkdirSync(dataDir, { recursive: true });
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(BASE_FILE)) {
|
||||||
|
fs.writeFileSync(BASE_FILE, JSON.stringify({ messages: [] }, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read messages from base.json
|
||||||
|
function readMessages() {
|
||||||
|
ensureBaseFile();
|
||||||
|
const data = fs.readFileSync(BASE_FILE, "utf8");
|
||||||
|
return JSON.parse(data).messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write messages to base.json
|
||||||
|
function writeMessages(messages) {
|
||||||
|
ensureBaseFile();
|
||||||
|
fs.writeFileSync(BASE_FILE, JSON.stringify({ messages }, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /send-msg - receive and log SMS
|
||||||
|
app.post("/send-msg", (req, res) => {
|
||||||
|
const { login, psw, phones, mes } = req.body;
|
||||||
|
|
||||||
|
if (!login || !psw || !phones || !mes) {
|
||||||
|
return res.status(400).json({ error: "Missing required parameters" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate credentials
|
||||||
|
if (login !== AUTH_LOGIN || psw !== AUTH_PASSWORD) {
|
||||||
|
return res.status(401).json({ error: "Invalid credentials" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
id: Date.now(),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
login,
|
||||||
|
phone: phones,
|
||||||
|
message: mes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const messages = readMessages();
|
||||||
|
messages.push(message);
|
||||||
|
writeMessages(messages);
|
||||||
|
|
||||||
|
console.log(`[${message.timestamp}] SMS to ${phones}: ${mes}`);
|
||||||
|
|
||||||
|
res.json({ status: "ok", id: message.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /view-all-sms - view all logged messages
|
||||||
|
app.get("/view-all-sms", (req, res) => {
|
||||||
|
const messages = readMessages();
|
||||||
|
res.json({ messages });
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /clear-all-sms - clear all messages
|
||||||
|
app.post("/clear-all-sms", (req, res) => {
|
||||||
|
writeMessages([]);
|
||||||
|
console.log("All messages cleared");
|
||||||
|
res.json({ status: "ok", message: "All messages cleared" });
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET / - serve main page
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, "public", "index.html"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize and start server
|
||||||
|
ensureBaseFile();
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`SMS OTP Gateway running on port ${PORT}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user