init
All checks were successful
Build SMS Gateway / Build image (push) Successful in 19s

This commit is contained in:
2026-01-19 18:44:24 +07:00
commit c49851de8b
11 changed files with 514 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"mcp__acp__Write",
"mcp__acp__Bash",
"mcp__acp__Edit"
]
}
}

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
node_modules
npm-debug.log
data
.git
.gitignore

View 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
View 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
View 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
```

1
VERSION Normal file
View File

@@ -0,0 +1 @@
0.1.0

3
data/base.json Normal file
View File

@@ -0,0 +1,3 @@
{
"messages": []
}

12
docker-compose.yml Normal file
View 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
View 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
View 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
View 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}`);
});