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