Major changes
This commit is contained in:
parent
f03151c8a4
commit
68d10967db
21
Dockerfile
21
Dockerfile
|
|
@ -1,10 +1,19 @@
|
|||
# Use an official Python runtime as a parent image
|
||||
FROM python:3.11-slim
|
||||
FROM ubuntu:24.04
|
||||
|
||||
COPY requirements.txt .
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY backend/tcp_sia_server.bin ./tcp_sia_server.bin
|
||||
COPY backend/main.bin ./main.bin
|
||||
COPY backend/watchdog.bin ./watchdog.bin
|
||||
|
||||
EXPOSE 9000
|
||||
RUN chmod -R 777 /tmp
|
||||
|
||||
CMD ["python", "/backend/tcp_sia_server.py"]
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libstdc++6 libgcc-s1 libexpat1 zlib1g libffi8 \
|
||||
ca-certificates \
|
||||
default-mysql-client libmysqlclient21 \
|
||||
&& chmod +x /app/tcp_sia_server.bin /app/main.bin \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
#CMD ["/app/tcp_sia_server.bin", "9000"]
|
||||
CMD ["/app/main.bin"]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
FROM php:8.2-fpm
|
||||
|
||||
RUN docker-php-ext-install pdo pdo_mysql sockets
|
||||
|
||||
Binary file not shown.
|
|
@ -0,0 +1,211 @@
|
|||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import requests
|
||||
import os
|
||||
import pymysql
|
||||
import logging
|
||||
import bcrypt
|
||||
import configparser
|
||||
from functools import partial
|
||||
|
||||
CONFIG_PATH = "/app/config.ini"
|
||||
#CONFIG_PATH = "../config.ini"
|
||||
config = configparser.ConfigParser()
|
||||
config.read(CONFIG_PATH)
|
||||
loglevel_str = config["log"]["level"].upper()
|
||||
loglevel = getattr(logging, loglevel_str, logging.INFO)
|
||||
logging.basicConfig(level=loglevel)
|
||||
|
||||
DB_CONFIG = {
|
||||
'host': config["database"]["host"],
|
||||
'user': config["database"]["user"],
|
||||
'password': config["database"]["password"],
|
||||
'database': config["database"]["database"]
|
||||
}
|
||||
|
||||
LICENSE_SERVER = "https://sikkerdata.com/validate_license.php"
|
||||
LICENSE_KEY = config["license"]["license_key"]
|
||||
with open("/etc/machine-id", "r") as f:
|
||||
get_HWID = f.read().strip()
|
||||
|
||||
def validate_license():
|
||||
try:
|
||||
hwid = get_HWID
|
||||
response = requests.post(LICENSE_SERVER, json={
|
||||
"license_key": LICENSE_KEY,
|
||||
"hwid": hwid
|
||||
}, timeout=5)
|
||||
logging.info(f"[MAIN] License server response: {response.text}")
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
if result.get("status") == "valid":
|
||||
logging.info("[MAIN] License validated")
|
||||
return True
|
||||
else:
|
||||
logging.info(f"[MAIN] License check failed: {result.get('message')}")
|
||||
return False
|
||||
else:
|
||||
logging.info(f"[MAIN] License server error: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.info(f"[MAIN] License check exception: {e}")
|
||||
return False
|
||||
|
||||
def setup_database(db_config):
|
||||
conn = pymysql.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
|
||||
def table_exists(name):
|
||||
cursor.execute("SHOW TABLES LIKE %s", (name,))
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
# Create `users` table if missing
|
||||
if not table_exists('users'):
|
||||
print("[DB] Creating 'users' table...")
|
||||
cursor.execute("""
|
||||
CREATE TABLE users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
full_name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
phone VARCHAR(20),
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
totp_secret VARCHAR(64),
|
||||
user_role ENUM('Admin','Operator','Installer','Client') DEFAULT 'Client',
|
||||
access_group VARCHAR(100),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
""")
|
||||
|
||||
# Create `signals` table if missing
|
||||
if not table_exists('signals'):
|
||||
print("[DB] Creating 'signals' table...")
|
||||
cursor.execute("""
|
||||
CREATE TABLE signals (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
protocol VARCHAR(10) NOT NULL,
|
||||
raw_message TEXT NOT NULL,
|
||||
account VARCHAR(20),
|
||||
sequence VARCHAR(10),
|
||||
line_number VARCHAR(10),
|
||||
event_code VARCHAR(10),
|
||||
partition VARCHAR(10),
|
||||
zone VARCHAR(20),
|
||||
signal_time DATETIME,
|
||||
source_ip VARCHAR(45),
|
||||
signal_text VARCHAR(255),
|
||||
v TEXT,
|
||||
x TEXT,
|
||||
y TEXT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
""")
|
||||
if not table_exists('receivers'):
|
||||
print("[DB] Creating 'receivers' table...")
|
||||
cursor.execute("""
|
||||
CREATE TABLE receivers (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
type ENUM('SIA-DC09') NOT NULL DEFAULT 'SIA-DC09',
|
||||
tcpport INT NOT NULL,
|
||||
enabled BOOLEAN DEFAULT 1
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
""")
|
||||
cursor.execute("""INSERT INTO receivers (name, type, tcpport, enabled) VALUES ('Main Receiver', 'SIA-DC09', 9000, 1);""")
|
||||
|
||||
# If no users exist, create default admin
|
||||
cursor.execute("SELECT COUNT(*) FROM users")
|
||||
(user_count,) = cursor.fetchone()
|
||||
if user_count == 0:
|
||||
print("[DB] Inserting default admin user...")
|
||||
default_password = bcrypt.hashpw("admin123".encode(), bcrypt.gensalt()).decode('utf-8')
|
||||
cursor.execute("""
|
||||
INSERT INTO users (username, full_name, email, password_hash, user_role)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
""", ("admin", "Admin", "admin@example.com", default_password, "Admin"))
|
||||
print("[DB] Admin created with username 'admin' and password 'admin123'")
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
running_services = {}
|
||||
|
||||
def watchdog():
|
||||
bin_path = "/app/watchdog.bin"
|
||||
def run_watchdog():
|
||||
while True:
|
||||
cmd = [bin_path]
|
||||
logging.debug(f"[MAIN] Starting Watchdog...")
|
||||
process = subprocess.Popen(cmd)
|
||||
running_services["Watchdog"] = process
|
||||
process.wait()
|
||||
logging.debug(f"[MAIN] Watchdog finished. Restarting in 5s...")
|
||||
time.sleep(5)
|
||||
|
||||
thread = threading.Thread(target=run_watchdog, name="Watchdog", daemon=True)
|
||||
thread.start()
|
||||
|
||||
def is_watchdog_running():
|
||||
return any(thread.name == "Watchdog" and thread.is_alive() for thread in threading.enumerate())
|
||||
|
||||
def main():
|
||||
logging.info("[MAIN] Starting main function")
|
||||
try:
|
||||
if not validate_license():
|
||||
logging.info("[MAIN] Shutting down due to invalid license.")
|
||||
return
|
||||
setup_thread = threading.Thread(target=partial(setup_database, DB_CONFIG), daemon=True)
|
||||
setup_thread.start()
|
||||
setup_thread.join() # Wait for DB setup before continuing
|
||||
|
||||
while True:
|
||||
try:
|
||||
logging.debug(f"[MAIN] Main is running")
|
||||
|
||||
if is_watchdog_running():
|
||||
logging.debug("[MAIN] Watchdog thread is already running")
|
||||
else:
|
||||
watchdog()
|
||||
|
||||
time.sleep(10)
|
||||
except Exception as e:
|
||||
logging.exception(f"[MAIN] Exception while logging thread status: {e}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Graceful shutdown requested")
|
||||
|
||||
def wait_for_mariadb(host, port, user, password, database, timeout=5):
|
||||
logging.info(f"[WAIT] Waiting for MariaDB at {host}:{port} to become ready...")
|
||||
while True:
|
||||
try:
|
||||
conn = pymysql.connect(
|
||||
host=host,
|
||||
port=port,
|
||||
user=user,
|
||||
password=password,
|
||||
database=database,
|
||||
connect_timeout=timeout
|
||||
)
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("SELECT 1;")
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
logging.info("[WAIT] MariaDB is ready.")
|
||||
conn.close()
|
||||
break
|
||||
except pymysql.MySQLError as e:
|
||||
logging.warning(f"[WAIT] MariaDB not ready yet: {e}")
|
||||
time.sleep(timeout)
|
||||
|
||||
if __name__ == "__main__":
|
||||
wait_for_mariadb(
|
||||
host=config["database"]["host"],
|
||||
port=3306,
|
||||
user=config["database"]["user"],
|
||||
password=config["database"]["password"],
|
||||
database=config["database"]["database"]
|
||||
)
|
||||
main()
|
||||
|
||||
Binary file not shown.
|
|
@ -1,9 +1,19 @@
|
|||
import asyncio
|
||||
import sys
|
||||
from crccheck.crc import CrcArc
|
||||
import re
|
||||
import json
|
||||
import mysql.connector
|
||||
import pymysql
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import configparser
|
||||
|
||||
CONFIG_PATH = "/app/config.ini"
|
||||
config = configparser.ConfigParser()
|
||||
config.read(CONFIG_PATH)
|
||||
loglevel_str = config["log"]["level"].upper()
|
||||
loglevel = getattr(logging, loglevel_str, logging.INFO)
|
||||
logging.basicConfig(level=loglevel)
|
||||
|
||||
def verify_message(message: bytes) -> tuple[bool, str]:
|
||||
"""
|
||||
|
|
@ -12,11 +22,11 @@ def verify_message(message: bytes) -> tuple[bool, str]:
|
|||
"""
|
||||
|
||||
if len(message) < 10:
|
||||
print("Message too short")
|
||||
logging.info("Message too short")
|
||||
return False, None
|
||||
|
||||
if message[0] != 10 or message[-1] != 13:
|
||||
print("Message must start with LF (\\n) and end with CR (\\r)")
|
||||
logging.info("Message must start with LF (\\n) and end with CR (\\r)")
|
||||
return False, None
|
||||
|
||||
crc_bytes = message[1:5] # 4 hex chars
|
||||
|
|
@ -28,43 +38,42 @@ def verify_message(message: bytes) -> tuple[bool, str]:
|
|||
length_str = length_bytes.decode('ascii')
|
||||
payload_str = data_bytes.decode('ascii')
|
||||
except UnicodeDecodeError:
|
||||
print("Non-ASCII characters in message")
|
||||
logging.info("Non-ASCII characters in message")
|
||||
return False, None
|
||||
|
||||
try:
|
||||
crc_recv = int(crc_str, 16)
|
||||
length_recv = int(length_str, 16)
|
||||
except ValueError:
|
||||
print("CRC or length not hex")
|
||||
logging.info("CRC or length not hex")
|
||||
return False, None
|
||||
|
||||
if length_recv != len(data_bytes):
|
||||
print(f"Length mismatch: declared {length_recv}, actual {len(data_bytes)}")
|
||||
logging.info(f"Length mismatch: declared {length_recv}, actual {len(data_bytes)}")
|
||||
return False, None
|
||||
|
||||
crc_calc = CrcArc.calc(data_bytes)
|
||||
if crc_calc != crc_recv:
|
||||
print(f"CRC mismatch: received {crc_str} calculated {crc_calc:04X}")
|
||||
logging.info(f"CRC mismatch: received {crc_str} calculated {crc_calc:04X}")
|
||||
return False, None
|
||||
|
||||
print(f"Valid message received: {payload_str}")
|
||||
logging.info(f"Valid message received: {payload_str}")
|
||||
return True, payload_str
|
||||
|
||||
|
||||
def parse_sia_payload(payload: str) -> dict:
|
||||
"""
|
||||
Extracts fields from SIA-DC09 message according to the full protocol spec.
|
||||
Example:
|
||||
Extracts fields from SIA-DCS or ADM-CID message according to the protocol spec.
|
||||
Examples:
|
||||
"SIA-DCS"0005L0#1234[1234|Nri1/RP00][Vimage.jpg][X010E57.5698][Y59N12.8358]
|
||||
"ADM-CID"0000RF3L56789#1234[#1234|1131 01 015]
|
||||
"""
|
||||
|
||||
result = {
|
||||
"format": None,
|
||||
"seq": None,
|
||||
"line": None,
|
||||
"account": None,
|
||||
"receiver": None,
|
||||
"event": None,
|
||||
"zone": None,
|
||||
"partition": None,
|
||||
"code": None,
|
||||
|
|
@ -72,6 +81,7 @@ def parse_sia_payload(payload: str) -> dict:
|
|||
"y": None,
|
||||
"v": None,
|
||||
"raw": payload,
|
||||
"signal_text": None,
|
||||
}
|
||||
|
||||
# Extract format and header
|
||||
|
|
@ -82,132 +92,228 @@ def parse_sia_payload(payload: str) -> dict:
|
|||
result["line"] = header_match.group(3)
|
||||
result["account"] = header_match.group(4)
|
||||
|
||||
# Main event block: [account|receiver/eventcode]
|
||||
event_match = re.search(r'\[\#?(\d+)\|([^\]]+)\]', payload)
|
||||
if event_match:
|
||||
result["account_verify"] = event_match.group(1)
|
||||
receiver_event = event_match.group(2)
|
||||
# Parse based on message format
|
||||
format_type = result["format"]
|
||||
|
||||
if format_type == "SIA-DCS":
|
||||
# Main event block: [account|receiver/eventcode]
|
||||
event_match = re.search(r'\[\#?(\d+)\|([^\]]+)\]', payload)
|
||||
if event_match:
|
||||
result["account_verify"] = event_match.group(1)
|
||||
receiver_event = event_match.group(2)
|
||||
|
||||
# Handle both with and without slash
|
||||
if "/" in receiver_event:
|
||||
receiver, event = receiver_event.split("/", 1)
|
||||
else:
|
||||
match = re.match(r'(Nri\d+)([A-Z]{2}\d+.*)', receiver_event)
|
||||
if match:
|
||||
receiver = match.group(1)
|
||||
event = match.group(2)
|
||||
else:
|
||||
receiver = ""
|
||||
event = receiver_event # fallback
|
||||
|
||||
# Split by slash
|
||||
if "/" in receiver_event:
|
||||
receiver, event = receiver_event.split("/", 1)
|
||||
result["receiver"] = receiver
|
||||
result["event"] = event
|
||||
|
||||
# Optional: split event into type (2 chars), zone (2–3 digits)
|
||||
# Extract code and zone from event (e.g., OP01, BA05)
|
||||
event_code_match = re.match(r'([A-Z]{2})(\d+)', event)
|
||||
if event_code_match:
|
||||
result["code"] = event_code_match.group(1)
|
||||
result["zone"] = event_code_match.group(2)
|
||||
|
||||
# Extract partition from receiver (e.g., "Nri0" -> "0")
|
||||
# Extract partition from receiver (e.g., Nri1 → 1)
|
||||
partition_match = re.match(r'Nri(\d+)', receiver)
|
||||
if partition_match:
|
||||
result["partition"] = partition_match.group(1)
|
||||
|
||||
# Optional V block (usually image URL)
|
||||
v_match = re.search(r'\[V([^\]]*)\]', payload)
|
||||
if v_match:
|
||||
result["v"] = v_match.group(1)
|
||||
# Optional signal text (e.g., ^Fire alarm triggered^)
|
||||
text_match = re.search(r'\^([^^]+)\^', event)
|
||||
if text_match:
|
||||
result["signal_text"] = text_match.group(1)
|
||||
|
||||
# Optional GPS data
|
||||
x_match = re.search(r'\[X([^\]]+)\]', payload)
|
||||
y_match = re.search(r'\[Y([^\]]+)\]', payload)
|
||||
if x_match:
|
||||
result["x"] = x_match.group(1)
|
||||
if y_match:
|
||||
result["y"] = y_match.group(1)
|
||||
# Optional V block (usually image or file name)
|
||||
v_match = re.search(r'\[V([^\]]*)\]', payload)
|
||||
if v_match:
|
||||
result["v"] = v_match.group(1)
|
||||
|
||||
# Optional GPS data
|
||||
x_match = re.search(r'\[X([^\]]+)\]', payload)
|
||||
y_match = re.search(r'\[Y([^\]]+)\]', payload)
|
||||
if x_match:
|
||||
result["x"] = x_match.group(1)
|
||||
if y_match:
|
||||
result["y"] = y_match.group(1)
|
||||
|
||||
elif format_type == "ADM-CID":
|
||||
# Extract format
|
||||
format_match = re.search(r'"(ADM-CID)"(\d{4})[A-Z]*L(\d)#(\d+)', payload)
|
||||
if format_match:
|
||||
result["format"] = format_match.group(1)
|
||||
result["seq"] = format_match.group(2)
|
||||
result["line"] = format_match.group(3)
|
||||
result["account"] = format_match.group(4)
|
||||
|
||||
# Parse event block: [account|event_code partition zone]
|
||||
event_match = re.search(r'\[#?(\d+)\|([A-Z0-9]{3,4}) (\d{2}) (\d{3})\]', payload)
|
||||
if event_match:
|
||||
result["account_verify"] = event_match.group(1)
|
||||
result["code"] = event_match.group(2)
|
||||
result["partition"] = event_match.group(3)
|
||||
result["zone"] = event_match.group(4)
|
||||
|
||||
# Optional fields
|
||||
timestamp_match = re.search(r'_(\d{2}:\d{2}:\d{2},\d{2}-\d{2}-\d{4})', payload)
|
||||
if timestamp_match:
|
||||
result["timestamp"] = timestamp_match.group(1)
|
||||
|
||||
v_match = re.search(r'\[V([^\]]*)\]', payload)
|
||||
if v_match:
|
||||
result["v"] = v_match.group(1)
|
||||
|
||||
x_match = re.search(r'\[X([^\]]+)\]', payload)
|
||||
y_match = re.search(r'\[Y([^\]]+)\]', payload)
|
||||
if x_match:
|
||||
result["x"] = x_match.group(1)
|
||||
if y_match:
|
||||
result["y"] = y_match.group(1)
|
||||
|
||||
# Optional timestamp: _hh:mm:ss,mm-dd-yyyy
|
||||
time_match = re.search(r'_(\d{2}:\d{2}:\d{2}),(\d{2}-\d{2}-\d{4})', payload)
|
||||
if time_match:
|
||||
time_str = f"{time_match.group(2)} {time_match.group(1)}" # e.g., 07-25-2025 07:33:01
|
||||
time_str = f"{time_match.group(2)} {time_match.group(1)}"
|
||||
result["signal_time"] = datetime.strptime(time_str, "%m-%d-%Y %H:%M:%S").isoformat()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def build_nack(seq: str, account: str, receiver: str = '0') -> bytes:
|
||||
lf = chr(0x0A)
|
||||
cr = chr(0x0D)
|
||||
payload = f'"NACK"{seq}L{receiver}#{account}[]'
|
||||
crc = CrcArc.calc(payload.encode())
|
||||
length = str(len(payload)).zfill(4)
|
||||
return f"{lf}{crc:04X}{length}{payload}{cr}".encode()
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
||||
db_config = {
|
||||
'host': config["database"]["host"],
|
||||
'user': config["database"]["user"],
|
||||
'password': config["database"]["password"],
|
||||
'database': config["database"]["database"]
|
||||
}
|
||||
addr = writer.get_extra_info('peername')
|
||||
print(f"Connection from {addr}")
|
||||
lf = chr(0x0A) # LF
|
||||
cr = chr(0x0D) # CR
|
||||
logging.info(f"Connection from {addr}")
|
||||
|
||||
try:
|
||||
data = await reader.readuntil(b'\r')
|
||||
print(f"Raw received: {data}")
|
||||
while True:
|
||||
try:
|
||||
data = await reader.readuntil(b'\r')
|
||||
except asyncio.IncompleteReadError:
|
||||
logging.info("Client closed connection")
|
||||
break
|
||||
|
||||
is_valid, payload = verify_message(data)
|
||||
response = b"\n0004NACK\r" # fallback
|
||||
logging.info(f"Raw received: {data}")
|
||||
|
||||
if is_valid and payload:
|
||||
parsed = parse_sia_payload(payload)
|
||||
parsed["source_ip"] = str(addr[0])
|
||||
print("Parsed payload:")
|
||||
print(json.dumps(parsed, indent=2))
|
||||
sia_data = parsed
|
||||
# Connect to MariaDB
|
||||
conn = mysql.connector.connect(
|
||||
host="192.168.10.57",
|
||||
user="admin",
|
||||
password="yourpassword",
|
||||
database="superarc"
|
||||
)
|
||||
cursor = conn.cursor()
|
||||
# Insert data
|
||||
query = """
|
||||
INSERT INTO signals (
|
||||
protocol, raw_message, account, sequence,
|
||||
line_number, event_code, `partition`, zone, signal_time, source_ip
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
values = (
|
||||
sia_data["format"],
|
||||
sia_data["raw"],
|
||||
sia_data.get("account"),
|
||||
sia_data.get("seq"),
|
||||
sia_data.get("line"),
|
||||
sia_data.get("code"),
|
||||
sia_data.get("partition"),
|
||||
sia_data.get("zone"),
|
||||
sia_data.get("signal_time"),
|
||||
sia_data.get("source_ip")
|
||||
)
|
||||
is_valid, payload = verify_message(data)
|
||||
response = b'\n"NACK"0000L0#0000[]\r' # Default response
|
||||
|
||||
cursor.execute(query, values)
|
||||
conn.commit()
|
||||
print("Inserted signal into database.")
|
||||
cursor.close()
|
||||
conn.close()
|
||||
if is_valid and payload:
|
||||
parsed = parse_sia_payload(payload)
|
||||
parsed["source_ip"] = str(addr[0])
|
||||
logging.info("Parsed payload:")
|
||||
logging.info(json.dumps(parsed, indent=2))
|
||||
seq = parsed.get("seq", "0000")
|
||||
account = parsed.get("account", "0000")
|
||||
response = build_nack(seq=seq, account=account)
|
||||
|
||||
seq = parsed.get("seq", "0000")
|
||||
account = parsed.get("account", "0000")
|
||||
# Validate required fields
|
||||
required_format = parsed.get("format")
|
||||
required_fields = [parsed.get("account"), parsed.get("code"), parsed.get("zone")]
|
||||
|
||||
response_payload = f'"ACK"{seq}L0#{account}[]'
|
||||
crc = CrcArc.calc(response_payload.encode())
|
||||
length = str(hex(len(response_payload))).split('x')
|
||||
response = f"\n{crc:04X}00{length[1].upper()}{response_payload}\r".encode()
|
||||
if required_format in ["SIA-DCS","ADM-CID"]:
|
||||
if any(f is None for f in required_fields):
|
||||
logging.info("Missing required SIA fields — sending NACK.")
|
||||
# build and send NACK
|
||||
else:
|
||||
logging.info("Valid SIA-DCS signal.")
|
||||
# Connect to MariaDB
|
||||
conn = pymysql.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
query = """
|
||||
INSERT INTO signals (
|
||||
protocol, raw_message, account, sequence,
|
||||
line_number, event_code, `partition`, zone, signal_time, source_ip, signal_text, x, y, v
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
values = (
|
||||
parsed["format"],
|
||||
parsed["raw"],
|
||||
parsed.get("account"),
|
||||
parsed.get("seq"),
|
||||
parsed.get("line"),
|
||||
parsed.get("code"),
|
||||
parsed.get("partition"),
|
||||
parsed.get("zone"),
|
||||
parsed.get("signal_time"),
|
||||
parsed.get("source_ip"),
|
||||
parsed.get("signal_text"),
|
||||
parsed.get("x"),
|
||||
parsed.get("y"),
|
||||
parsed.get("v")
|
||||
)
|
||||
cursor.execute(query, values)
|
||||
conn.commit()
|
||||
logging.info("Inserted signal into database.")
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
print(f"Sending response: {response}")
|
||||
else:
|
||||
print("Invalid message. Sending fallback NACK.")
|
||||
# Build ACK response
|
||||
response_payload = f'"ACK"{seq}L0#{account}[]'
|
||||
crc = CrcArc.calc(response_payload.encode())
|
||||
length = str(len(response_payload)).zfill(4)
|
||||
response = f"{lf}{crc:04X}{length}{response_payload}{cr}".encode()
|
||||
logging.info(f"Sending response: {response}")
|
||||
elif required_format == "NULL":
|
||||
logging.info("Received NULL signal, skipping field validation.")
|
||||
# Build ACK response
|
||||
response_payload = f'"ACK"{seq}L0#{account}[]'
|
||||
crc = CrcArc.calc(response_payload.encode())
|
||||
length = str(len(response_payload)).zfill(4)
|
||||
response = f"{lf}{crc:04X}{length}{response_payload}{cr}".encode()
|
||||
logging.info(f"Sending response: {response}")
|
||||
# allow
|
||||
else:
|
||||
logging.info("Invalid CRC or malformed message — sending fallback NACK.")
|
||||
|
||||
writer.write(response)
|
||||
await writer.drain()
|
||||
writer.write(response)
|
||||
await writer.drain()
|
||||
|
||||
except asyncio.IncompleteReadError:
|
||||
print("Connection closed before end of message")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
logging.info(f"Unexpected error: {e}")
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
print(f"Connection with {addr} closed")
|
||||
logging.info(f"Connection with {addr} closed")
|
||||
|
||||
async def main():
|
||||
server = await asyncio.start_server(handle_client, host='0.0.0.0', port=9000)
|
||||
print("Listening on TCP port 9000 for SIA-DCS messages")
|
||||
async def main(port):
|
||||
server = await asyncio.start_server(handle_client, host='0.0.0.0', port=port)
|
||||
logging.info(f"Listening on TCP port {port} for SIA-DCS messages")
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: tcp_sia_server.bin <port>")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
port = int(sys.argv[1])
|
||||
asyncio.run(main(port))
|
||||
except Exception as e:
|
||||
logging.error(f"[SIA] Failed to start server: {e}")
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,137 @@
|
|||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import pymysql
|
||||
import logging
|
||||
import configparser
|
||||
import psutil
|
||||
|
||||
CONFIG_PATH = "/app/config.ini"
|
||||
config = configparser.ConfigParser()
|
||||
config.read(CONFIG_PATH)
|
||||
loglevel_str = config["log"]["level"].upper()
|
||||
loglevel = getattr(logging, loglevel_str, logging.INFO)
|
||||
logging.basicConfig(level=loglevel)
|
||||
|
||||
DB_CONFIG = {
|
||||
'host': config["database"]["host"],
|
||||
'user': config["database"]["user"],
|
||||
'password': config["database"]["password"],
|
||||
'database': config["database"]["database"]
|
||||
}
|
||||
|
||||
running_services = {}
|
||||
|
||||
def get_process_on_port(port):
|
||||
for conn in psutil.net_connections(kind='inet'):
|
||||
if conn.laddr.port == port and conn.status == psutil.CONN_LISTEN:
|
||||
pid = conn.pid
|
||||
if pid is None:
|
||||
continue
|
||||
try:
|
||||
proc = psutil.Process(pid)
|
||||
return {
|
||||
'pid': pid,
|
||||
'name': proc.name(),
|
||||
'cmdline': proc.cmdline()
|
||||
}
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
return None
|
||||
|
||||
def initialize_running_services_from_db(db_config):
|
||||
try:
|
||||
conn = pymysql.connect(**db_config)
|
||||
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
||||
cursor.execute("SELECT name, tcpport FROM receivers;")
|
||||
rows = cursor.fetchall()
|
||||
for row in rows:
|
||||
name = row['name']
|
||||
port = row['tcpport']
|
||||
proc_info = get_process_on_port(port)
|
||||
if proc_info:
|
||||
logging.info(f"[INIT] Found existing service '{name}' on port {port}, PID={proc_info['pid']}, Cmd={proc_info['cmdline']}")
|
||||
running_services[name] = {
|
||||
"pid": proc_info["pid"],
|
||||
"cmdline": proc_info["cmdline"]
|
||||
}
|
||||
cursor.close()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logging.exception("[INIT] Failed to detect existing services")
|
||||
|
||||
def watchdog(db_config):
|
||||
global running_services
|
||||
while True:
|
||||
try:
|
||||
thread_conn = pymysql.connect(**db_config)
|
||||
cursor = thread_conn.cursor(pymysql.cursors.DictCursor)
|
||||
cursor.execute("SELECT name, type, tcpport, enabled FROM receivers;")
|
||||
rows = cursor.fetchall()
|
||||
cursor.close()
|
||||
thread_conn.close()
|
||||
for row in rows:
|
||||
name = row["name"]
|
||||
receiver_type = row["type"]
|
||||
port = row["tcpport"]
|
||||
enabled = row["enabled"]
|
||||
|
||||
if receiver_type != "SIA-DC09":
|
||||
continue
|
||||
|
||||
if enabled == 1 and name not in running_services:
|
||||
binary_path = "/app/tcp_sia_server.bin"
|
||||
logging.info(f"[WATCHDOG] Starting receiver '{name}' on port {port}")
|
||||
start_service(name, binary_path, port)
|
||||
|
||||
elif enabled == 0:
|
||||
proc_info = running_services.get(name)
|
||||
if proc_info and isinstance(proc_info, subprocess.Popen):
|
||||
logging.info(f"[WATCHDOG] Stopping disabled receiver '{name}' (started by script)")
|
||||
proc_info.terminate()
|
||||
proc_info.wait()
|
||||
del running_services[name]
|
||||
else:
|
||||
# Kill external process using psutil
|
||||
port = row["tcpport"]
|
||||
try:
|
||||
for conn in psutil.net_connections(kind='inet'):
|
||||
if conn.laddr.port == port and conn.status == psutil.CONN_LISTEN:
|
||||
pid = conn.pid
|
||||
if pid:
|
||||
logging.warning(
|
||||
f"[WATCHDOG] Force killing external process on port {port} (PID {pid}) for receiver '{name}'")
|
||||
psutil.Process(pid).kill()
|
||||
if name in running_services:
|
||||
del running_services[name]
|
||||
except Exception as e:
|
||||
logging.exception(f"[WATCHDOG] Failed to kill external process for receiver '{name}'")
|
||||
|
||||
log_thread_status()
|
||||
time.sleep(3)
|
||||
except Exception as e:
|
||||
logging.exception(f"[WATCHDOG] Error during check: {e}")
|
||||
|
||||
def start_service(name, binary_path, port=None):
|
||||
def run():
|
||||
cmd = [binary_path]
|
||||
if port:
|
||||
cmd.append(str(port))
|
||||
logging.info(f"[WATCHDOG] Starting {name} on port {port}...")
|
||||
process = subprocess.Popen(cmd)
|
||||
running_services[name] = {
|
||||
"popen": process
|
||||
}
|
||||
process.wait()
|
||||
logging.warning(f"[WATCHDOG] {name} exited.")
|
||||
thread = threading.Thread(target=run, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def log_thread_status():
|
||||
running = list(running_services.keys())
|
||||
logging.debug(f"[STATUS] Running services: {running if running else 'None'}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
initialize_running_services_from_db(DB_CONFIG)
|
||||
logging.debug(f"[INIT] Active running_services: {list(running_services.keys())}")
|
||||
watchdog(DB_CONFIG)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
[license]
|
||||
license_key = ABCDEF-123456-XYZ789
|
||||
|
||||
[database]
|
||||
host = 127.0.0.1
|
||||
user = admin
|
||||
password = yourpassword
|
||||
database = superarc
|
||||
|
||||
[log]
|
||||
level = INFO
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
services:
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10.9
|
||||
environment:
|
||||
|
|
@ -18,26 +17,27 @@ services:
|
|||
volumes:
|
||||
- ./frontend:/usr/share/nginx/html:ro
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
ports:
|
||||
- "80:80"
|
||||
network_mode: "host"
|
||||
depends_on:
|
||||
- mariadb
|
||||
- php-fpm
|
||||
|
||||
php-fpm:
|
||||
image: php:8.2-fpm
|
||||
volumes:
|
||||
- ./frontend:/usr/share/nginx/html
|
||||
|
||||
tcp_sia_server:
|
||||
build:
|
||||
context: .
|
||||
ports:
|
||||
- "9000:9000"
|
||||
dockerfile: Dockerfile.php
|
||||
volumes:
|
||||
- ./backend/:/app
|
||||
working_dir: /app
|
||||
command: python -u /app/tcp_sia_server.py
|
||||
- ./frontend:/usr/share/nginx/html
|
||||
network_mode: "host"
|
||||
|
||||
main:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
network_mode: "host"
|
||||
volumes:
|
||||
- ./config.ini:/app/config.ini:ro
|
||||
- /etc/machine-id:/etc/machine-id:ro
|
||||
depends_on:
|
||||
- mariadb
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
error_reporting = E_ALL & ~E_DEPRECATED
|
||||
display_errors = On
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
require 'auth.php';
|
||||
requireAdmin();
|
||||
require 'db.php';
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
$message = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = $_POST['username'];
|
||||
$full_name = $_POST['full_name'];
|
||||
$email = $_POST['email'];
|
||||
$phone = $_POST['phone'];
|
||||
$password = password_hash($_POST['password'], PASSWORD_BCRYPT);
|
||||
$user_role = $_POST['user_role'];
|
||||
$access_group = $_POST['access_group'] ?? '';
|
||||
|
||||
// Generate 2FA secret
|
||||
//$ga = new PHPGangsta_GoogleAuthenticator();
|
||||
//$totp_secret = $ga->createSecret();
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, full_name, email, phone, password_hash, user_role, access_group)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$username, $full_name, $email, $phone, $password, $user_role, $access_group]);
|
||||
|
||||
$message = "User '$username' created successfully.";
|
||||
}
|
||||
?>
|
||||
|
||||
<?php include 'header.php'; ?>
|
||||
|
||||
<div class="container mt-5">
|
||||
<h2>Add New User</h2>
|
||||
<?php if ($message): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($message) ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" name="username" required class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Full Name</label>
|
||||
<input type="text" name="full_name" required class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" name="email" required class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Phone</label>
|
||||
<input type="text" name="phone" class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" name="password" required class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Role</label>
|
||||
<select name="user_role" class="form-select" required>
|
||||
<option value="Admin">Admin</option>
|
||||
<option value="Operator">Operator</option>
|
||||
<option value="Installer">Installer</option>
|
||||
<option value="Client">Client</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Access Group</label>
|
||||
<input type="text" name="access_group" class="form-control">
|
||||
</div>
|
||||
<button type="button" class="btn btn-danger ms-2" onclick="history.back()">Cancel</button> <button type="submit" class="btn btn-primary">Create User</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
session_start();
|
||||
require 'db.php';
|
||||
|
||||
function isLoggedIn() {
|
||||
return isset($_SESSION['user_id']) && isset($_SESSION['2fa_verified']) && $_SESSION['2fa_verified'] === true;
|
||||
}
|
||||
|
||||
function requireLogin() {
|
||||
if (!isLoggedIn()) {
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function requireLoginBefore2FA() {
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function requireAdmin() {
|
||||
requireLogin();
|
||||
if ($_SESSION['user_role'] !== 'Admin') {
|
||||
die('Access denied: Admins only.');
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"require": {
|
||||
"phpgangsta/googleauthenticator": "dev-master"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
require 'db.php';
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
$ga = new PHPGangsta_GoogleAuthenticator();
|
||||
$secret = $ga->createSecret();
|
||||
|
||||
$qrCodeUrl = $ga->getQRCodeGoogleUrl('SuperArc', $secret);
|
||||
|
||||
$username = 'anders';
|
||||
$fullname = 'Anders Ostensvik';
|
||||
$email = 'anders@ostsik.no';
|
||||
$phone = '1234567890';
|
||||
$password = password_hash('yourpassword', PASSWORD_BCRYPT);
|
||||
$user_role = 'Admin'; // must be one of Admin, Operator, Installer, Client
|
||||
$access_group = 'default';
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, full_name, email, phone, password_hash, totp_secret, user_role, access_group) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$username, $fullname, $email, $phone, $password, $secret, $user_role, $access_group]);
|
||||
|
||||
echo "User created. Scan this QR in Google Authenticator: <br>";
|
||||
echo "<img src='$qrCodeUrl'>";
|
||||
?>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
$pdo = new PDO('mysql:host=127.0.0.1;dbname=superarc;charset=utf8mb4', 'admin', 'yourpassword');
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
?>
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
require 'auth.php';
|
||||
requireAdmin();
|
||||
require 'db.php';
|
||||
|
||||
$message = '';
|
||||
$selectedUser = null;
|
||||
|
||||
// Fetch all users with full info for the list and modal
|
||||
$users = $pdo->query("SELECT id, username, full_name, email, user_role, phone, access_group FROM users ORDER BY username")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
|
||||
|
||||
// When a user is selected
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['select_user'])) {
|
||||
$userId = $_POST['user_id'];
|
||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$stmt->execute([$userId]);
|
||||
$selectedUser = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
// When user info is updated
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_user'])) {
|
||||
$userId = $_POST['user_id'];
|
||||
$full_name = $_POST['full_name'];
|
||||
$email = $_POST['email'];
|
||||
$phone = $_POST['phone'];
|
||||
$user_role = $_POST['user_role'];
|
||||
$access_group = $_POST['access_group'];
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE users SET full_name = ?, email = ?, phone = ?, user_role = ?, access_group = ? WHERE id = ?");
|
||||
$stmt->execute([$full_name, $email, $phone, $user_role, $access_group, $userId]);
|
||||
|
||||
$message = "User updated successfully.";
|
||||
|
||||
// Reload updated user
|
||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$stmt->execute([$userId]);
|
||||
$selectedUser = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
?>
|
||||
|
||||
<?php include 'header.php'; ?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<h2><?= t('Users') ?>
|
||||
<a href="add_user.php">
|
||||
<button name="add_user" class="btn btn-success"><?= t('Add user') ?></button>
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<?php if ($message): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars(t($message)) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="filterRole" class="form-label"><?= t('Filter by Role') ?></label>
|
||||
<select id="filterRole" class="form-select">
|
||||
<option value=""><?= t('All Roles') ?></option>
|
||||
<option value="Admin"><?= t('Admin') ?></option>
|
||||
<option value="Operator"><?= t('Operator') ?></option>
|
||||
<option value="Installer"><?= t('Installer') ?></option>
|
||||
<option value="Client"><?= t('Client') ?></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="searchUser" class="form-label"><?= t('Search User') ?></label>
|
||||
<input type="text" id="searchUser" class="form-control" placeholder="<?= t('Search by name, username, or email...') ?>">
|
||||
</div>
|
||||
|
||||
<table class="table table-hover" id="userTable" style="cursor: pointer;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= t('Full Name') ?></th>
|
||||
<th><?= t('Username') ?></th>
|
||||
<th><?= t('Email') ?></th>
|
||||
<th><?= t('Role') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<tr
|
||||
data-user='<?= json_encode($user, JSON_HEX_APOS | JSON_HEX_QUOT) ?>'
|
||||
data-role="<?= htmlspecialchars($user['user_role']) ?>"
|
||||
>
|
||||
<td><?= htmlspecialchars($user['full_name']) ?></td>
|
||||
<td><?= htmlspecialchars($user['username']) ?></td>
|
||||
<td><?= htmlspecialchars($user['email']) ?></td>
|
||||
<td><?= htmlspecialchars($user['user_role']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="editUserModal" tabindex="-1" aria-labelledby="editUserModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<form id="editUserForm" method="POST" class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editUserModalLabel"><?= t('Edit User') ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="user_id" id="modal_user_id">
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= t('Full Name') ?></label>
|
||||
<input type="text" name="full_name" id="modal_full_name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= t('Email') ?></label>
|
||||
<input type="email" name="email" id="modal_email" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= t('Phone') ?></label>
|
||||
<input type="text" name="phone" id="modal_phone" class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= t('Role') ?></label>
|
||||
<select name="user_role" id="modal_user_role" class="form-select" required>
|
||||
<?php foreach (['Admin', 'Operator', 'Installer', 'Client'] as $role): ?>
|
||||
<option value="<?= $role ?>"><?= $role ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= t('Access Group') ?></label>
|
||||
<input type="text" name="access_group" id="modal_access_group" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="update_user" class="btn btn-success"><?= t('Update User') ?></button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= t('Cancel') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const searchInput = document.getElementById('searchUser');
|
||||
const roleFilter = document.getElementById('filterRole');
|
||||
const userTable = document.getElementById('userTable');
|
||||
const rows = userTable.querySelectorAll('tbody tr');
|
||||
|
||||
function filterTable() {
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
const selectedRole = roleFilter.value;
|
||||
|
||||
rows.forEach(row => {
|
||||
const user = JSON.parse(row.getAttribute('data-user'));
|
||||
const matchesSearch =
|
||||
user.full_name.toLowerCase().includes(searchTerm) ||
|
||||
user.username.toLowerCase().includes(searchTerm) ||
|
||||
user.email.toLowerCase().includes(searchTerm);
|
||||
|
||||
const matchesRole = selectedRole === '' || user.user_role === selectedRole;
|
||||
|
||||
row.style.display = (matchesSearch && matchesRole) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
searchInput.addEventListener('input', filterTable);
|
||||
roleFilter.addEventListener('change', filterTable);
|
||||
|
||||
// Show modal on row click with user data
|
||||
rows.forEach(row => {
|
||||
row.addEventListener('click', () => {
|
||||
const user = JSON.parse(row.getAttribute('data-user'));
|
||||
document.getElementById('modal_user_id').value = user.id;
|
||||
document.getElementById('modal_full_name').value = user.full_name;
|
||||
document.getElementById('modal_email').value = user.email;
|
||||
document.getElementById('modal_phone').value = user.phone || '';
|
||||
document.getElementById('modal_user_role').value = user.user_role;
|
||||
document.getElementById('modal_access_group').value = user.access_group || '';
|
||||
new bootstrap.Modal(document.getElementById('editUserModal')).show();
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Footer -->
|
||||
<footer class="text-center text-muted py-3 mt-5 border-top bg-light">
|
||||
<div class="container">
|
||||
<small>© 2025 AlarmDrift — All rights reserved.</small><br>
|
||||
<small>Version 0.0.1 (Dev) | Last updated: August 2025</small>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
header("Cache-Control: no-cache, no-store, must-revalidate"); // HTTP 1.1
|
||||
header("Pragma: no-cache"); // HTTP 1.0
|
||||
header("Expires: 0"); // Proxies
|
||||
require 'auth.php';
|
||||
requireLogin();
|
||||
require 'db.php';
|
||||
require './lang/translate.php';
|
||||
|
||||
$stmt = $pdo->query("SELECT * FROM signals ORDER BY timestamp DESC LIMIT 100;");
|
||||
$signals = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($signals as $signal): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($signal['timestamp'] ?? '') ?></td>
|
||||
<td><?= htmlspecialchars($signal['account'] ?? '') ?></td>
|
||||
<td><?= htmlspecialchars($signal['event_code'] ?? '') ?></td>
|
||||
<td><?= htmlspecialchars($signal['zone'] ?? '') ?></td>
|
||||
<td><?= htmlspecialchars($signal['signal_text'] ?? '') ?></td>
|
||||
<td>
|
||||
<?php if (!empty($signal['v'])): ?>
|
||||
<button class="btn btn-primary w-100 openImageModalBtn" data-image="<?= htmlspecialchars($signal['v']) ?>">
|
||||
<?= htmlspecialchars($translations['image'] ?? 'Image') ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
|
||||
<td><?= htmlspecialchars($signal['source_ip'] ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
require './lang/translate.php';
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>AlarmDrift</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- Bootstrap 5 CDN -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- jQuery (must come before DataTables) -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="index.php"><?= t('brand') ?></a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="index.php"><?= t('home') ?></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="clients.php"><?= t('clients') ?></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="signal_log.php"><?= t('signal_log') ?></a></li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="maintenanceDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<?= t('maintenance') ?>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="maintenanceDropdown">
|
||||
<?php if (!empty($_SESSION['user_role']) && $_SESSION['user_role'] === 'Admin'): ?>
|
||||
<li><h6 class="dropdown-header"><?= t('clients_header') ?></h6></li>
|
||||
<li><a class="dropdown-item" href="edit_user.php"><?= t('dealers') ?></a></li>
|
||||
<li><a class="dropdown-item" href="edit_user.php"><?= t('client_templates') ?></a></li>
|
||||
<li><h6 class="dropdown-header"><?= t('event_handling_header') ?></h6></li>
|
||||
<li><a class="dropdown-item" href="edit_user.php"><?= t('event_templates') ?></a></li>
|
||||
<li><a class="dropdown-item" href="edit_user.php"><?= t('response_plans') ?></a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="systemDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<?= t('system') ?>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="systemDropdown">
|
||||
<?php if (!empty($_SESSION['user_role']) && $_SESSION['user_role'] === 'Admin'): ?>
|
||||
<li><h6 class="dropdown-header"><?= t('users_header') ?></h6></li>
|
||||
<li><a class="dropdown-item" href="edit_user.php"><?= t('users') ?></a></li>
|
||||
<li><a class="dropdown-item" href="edit_user.php"><?= t('access_groups') ?></a></li>
|
||||
<li><h6 class="dropdown-header"><?= t('system_header') ?></h6></li>
|
||||
<li><a class="dropdown-item" href="edit_user.php"><?= t('sms_email') ?></a></li>
|
||||
<li><a class="dropdown-item" href="receivers.php"><?= t('receivers') ?></a></li>
|
||||
<li><a class="dropdown-item" href="edit_user.php"><?= t('system_settings') ?></a></li>
|
||||
<li><a class="dropdown-item" href="edit_user.php"><?= t('backup') ?></a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown ms-3">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="logoutDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<?= t('logout') ?>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="logoutDropdown">
|
||||
<li><a class="dropdown-item" href="logout.php"><?= t('logout') ?></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
|
|
@ -1 +0,0 @@
|
|||
test
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
require 'auth.php';
|
||||
requireLogin();
|
||||
require 'header.php';
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Dashboard</title></head>
|
||||
<body>
|
||||
<h1>This will be a dashboard!</h1>
|
||||
<?php require 'footer.php'; ?>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
return [
|
||||
'brand' => 'AlarmDrift',
|
||||
'home' => 'Home',
|
||||
'clients' => 'Clients',
|
||||
'signal_log' => 'Signal Log',
|
||||
'maintenance' => 'Maintenance',
|
||||
'clients_header' => 'Clients',
|
||||
'dealers' => 'Dealers',
|
||||
'client_templates' => 'Client Templates',
|
||||
'event_handling_header' => 'Event handling',
|
||||
'event_templates' => 'Event Templates',
|
||||
'response_plans' => 'Response Plans',
|
||||
'system' => 'System',
|
||||
'users_header' => 'Users',
|
||||
'users' => 'Users',
|
||||
'access_groups' => 'Access Groups',
|
||||
'system_header' => 'System',
|
||||
'sms_email' => 'SMS/Email',
|
||||
'receivers' => 'Receivers',
|
||||
'system_settings' => 'System Settings',
|
||||
'backup' => 'Backup',
|
||||
'logout' => 'Logout',
|
||||
'signal_log_live' => 'Signal Log',
|
||||
'timestamp' => 'Timestamp',
|
||||
'account' => 'Account',
|
||||
'event' => 'Event',
|
||||
'zone' => 'Zone',
|
||||
'text' => 'Text',
|
||||
'image' => 'Image',
|
||||
'source_ip' => 'Source IP',
|
||||
'image_preview' => 'Image Preview',
|
||||
'close' => 'Close',
|
||||
'Users' => 'Users',
|
||||
'Add user' => 'Add user',
|
||||
'Filter by Role' => 'Filter by Role',
|
||||
'All Roles' => 'All Roles',
|
||||
'Admin' => 'Admin',
|
||||
'Operator' => 'Operator',
|
||||
'Installer' => 'Installer',
|
||||
'Client' => 'Client',
|
||||
'Search User' => 'Search User',
|
||||
'Search by name, username, or email...' => 'Search by name, username, or email...',
|
||||
'Full Name' => 'Full Name',
|
||||
'Username' => 'Username',
|
||||
'Email' => 'Email',
|
||||
'Role' => 'Role',
|
||||
'Edit User' => 'Edit User',
|
||||
'Phone' => 'Phone',
|
||||
'Access Group' => 'Access Group',
|
||||
'Update User' => 'Update User',
|
||||
'Cancel' => 'Cancel',
|
||||
'User updated successfully.' => 'User updated successfully.',
|
||||
'receivers' => 'Receivers',
|
||||
'add_receiver' => 'Add Receiver',
|
||||
'id' => 'ID',
|
||||
'name' => 'Name',
|
||||
'protocol' => 'Protocol',
|
||||
'port' => 'Port',
|
||||
'enabled' => 'Enabled',
|
||||
'status' => 'Status',
|
||||
'actions' => 'Actions',
|
||||
'edit' => 'Edit',
|
||||
'yes' => 'Yes',
|
||||
'no' => 'No',
|
||||
'running' => 'Running',
|
||||
'offline' => 'Offline',
|
||||
];
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
return [
|
||||
'brand' => 'AlarmDrift',
|
||||
'home' => 'Hjem',
|
||||
'clients' => 'Kunder',
|
||||
'signal_log' => 'Signal Logg',
|
||||
'maintenance' => 'Vedlikehold',
|
||||
'clients_header' => 'Kunder',
|
||||
'dealers' => 'Forhandlere',
|
||||
'client_templates' => 'Kundemaler',
|
||||
'event_handling_header' => 'Hendelseshåndtering',
|
||||
'event_templates' => 'Hendelsesmaler',
|
||||
'response_plans' => 'Responsplaner',
|
||||
'system' => 'System',
|
||||
'users_header' => 'Brukere',
|
||||
'users' => 'Brukere',
|
||||
'access_groups' => 'Tilgangsgrupper',
|
||||
'system_header' => 'System',
|
||||
'sms_email' => 'SMS/Epost',
|
||||
'receivers' => 'Mottakere',
|
||||
'system_settings' => 'Systeminnstillinger',
|
||||
'backup' => 'Backup',
|
||||
'logout' => 'Logg ut',
|
||||
'signal_log_live' => 'Signal Logg',
|
||||
'timestamp' => 'Tidsstempel',
|
||||
'account' => 'Konto',
|
||||
'event' => 'Hendelse',
|
||||
'zone' => 'Sone',
|
||||
'text' => 'Tekst',
|
||||
'image' => 'Bilde',
|
||||
'source_ip' => 'Kilde IP',
|
||||
'image_preview' => 'Bilde forhåndsvisning',
|
||||
'close' => 'Lukk',
|
||||
'Users' => 'Brukere',
|
||||
'Add user' => 'Legg til bruker',
|
||||
'Filter by Role' => 'Filtrer etter rolle',
|
||||
'All Roles' => 'Alle roller',
|
||||
'Admin' => 'Administrator',
|
||||
'Operator' => 'Operatør',
|
||||
'Installer' => 'Installatør',
|
||||
'Client' => 'Kunde',
|
||||
'Search User' => 'Søk bruker',
|
||||
'Search by name, username, or email...' => 'Søk etter navn, brukernavn eller e-post...',
|
||||
'Full Name' => 'Fullt navn',
|
||||
'Username' => 'Brukernavn',
|
||||
'Email' => 'E-post',
|
||||
'Role' => 'Rolle',
|
||||
'Edit User' => 'Rediger bruker',
|
||||
'Phone' => 'Telefon',
|
||||
'Access Group' => 'Tilgangsgruppe',
|
||||
'Update User' => 'Oppdater bruker',
|
||||
'Cancel' => 'Avbryt',
|
||||
'User updated successfully.' => 'Bruker oppdatert.'
|
||||
'receivers' => 'Mottakere',
|
||||
'add_receiver' => 'Legg til mottaker',
|
||||
'id' => 'ID',
|
||||
'name' => 'Navn',
|
||||
'protocol' => 'Protokoll',
|
||||
'port' => 'Port',
|
||||
'enabled' => 'Aktiv',
|
||||
'status' => 'Status',
|
||||
'actions' => 'Handlinger',
|
||||
'edit' => 'Rediger',
|
||||
'yes' => 'Ja',
|
||||
'no' => 'Nei',
|
||||
'running' => 'Kjører',
|
||||
'offline' => 'Av',
|
||||
];
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
function getPreferredLanguage(array $availableLanguages, string $httpAcceptLanguage): string {
|
||||
$langs = explode(',', $httpAcceptLanguage);
|
||||
|
||||
foreach ($langs as $lang) {
|
||||
$langCode = strtolower(substr(trim($lang), 0, 2));
|
||||
if (in_array($langCode, $availableLanguages)) {
|
||||
return $langCode;
|
||||
}
|
||||
}
|
||||
return 'en';
|
||||
}
|
||||
|
||||
$availableLanguages = ['en', 'no'];
|
||||
|
||||
// URL override has priority
|
||||
if (isset($_GET['lang']) && in_array($_GET['lang'], $availableLanguages)) {
|
||||
$_SESSION['lang'] = $_GET['lang'];
|
||||
}
|
||||
|
||||
// Set lang based on browser if not set already
|
||||
if (!isset($_SESSION['lang'])) {
|
||||
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
|
||||
$_SESSION['lang'] = getPreferredLanguage($availableLanguages, $_SERVER['HTTP_ACCEPT_LANGUAGE']);
|
||||
} else {
|
||||
$_SESSION['lang'] = 'en';
|
||||
}
|
||||
}
|
||||
|
||||
$lang = $_SESSION['lang'];
|
||||
$langFile = __DIR__ . "/{$lang}.php";
|
||||
$translations = [];
|
||||
|
||||
if (file_exists($langFile)) {
|
||||
$translations = include $langFile;
|
||||
} else {
|
||||
$translations = include __DIR__ . '/en.php';
|
||||
}
|
||||
|
||||
function t(string $key): string {
|
||||
global $translations;
|
||||
return htmlspecialchars($translations[$key] ?? $key);
|
||||
}
|
||||
?>
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
require 'db.php';
|
||||
require 'vendor/autoload.php';
|
||||
session_start();
|
||||
|
||||
$message = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
|
||||
$stmt->execute([$_POST['email']]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user && password_verify($_POST['password'], $user['password_hash'])) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['totp_secret'] = $user['totp_secret'];
|
||||
$_SESSION['2fa_verified'] = false;
|
||||
$_SESSION['user_role'] = $user['user_role'] ?? 'Client';
|
||||
header('Location: verify_2fa.php');
|
||||
exit;
|
||||
} else {
|
||||
$message = "Invalid credentials.";
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login - SuperArc</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- Bootstrap 5 CDN -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-light d-flex align-items-center justify-content-center vh-100">
|
||||
|
||||
<div class="card shadow p-4" style="width: 100%; max-width: 400px;">
|
||||
<h3 class="text-center mb-3">Login</h3>
|
||||
<?php if ($message): ?>
|
||||
<div class="alert alert-danger" role="alert"><?= htmlspecialchars($message) ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<input name="email" type="email" class="form-control" id="email" required autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input name="password" type="password" class="form-control" id="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Sign In</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
session_start();
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
// receiver_status.php
|
||||
require 'db.php'; // or wherever your DB connection is
|
||||
|
||||
$receivers = $pdo->query("SELECT id, tcpport, enabled FROM receivers")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$response = [];
|
||||
|
||||
function getServerIp(): string {
|
||||
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
|
||||
if (!$socket) return '127.0.0.1';
|
||||
|
||||
// This IP doesn't need to be reachable — it just helps detect the outbound IP
|
||||
socket_connect($socket, '8.8.8.8', 80);
|
||||
|
||||
socket_getsockname($socket, $localIp);
|
||||
socket_close($socket);
|
||||
|
||||
return $localIp ?? '127.0.0.1';
|
||||
}
|
||||
|
||||
foreach ($receivers as $r) {
|
||||
$status = 'down';
|
||||
|
||||
// Check if the port is open (basic socket check)
|
||||
$fp = @fsockopen(getServerIp(), $r['tcpport'], $errno, $errstr, 1);
|
||||
if ($fp) {
|
||||
fclose($fp);
|
||||
$status = 'up';
|
||||
}
|
||||
|
||||
$response[] = [
|
||||
'id' => $r['id'],
|
||||
'status' => $status
|
||||
];
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($response);
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
<?php
|
||||
|
||||
require 'db.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_receiver']) && !empty($_POST['id'])) {
|
||||
$deleteId = (int) $_POST['id'];
|
||||
$stmt = $pdo->prepare("DELETE FROM receivers WHERE id = ?");
|
||||
$stmt->execute([$deleteId]);
|
||||
header("Location: receivers.php");
|
||||
exit;
|
||||
}
|
||||
include 'header.php';
|
||||
|
||||
// Handle receiver update
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_receiver'])) {
|
||||
$stmt = $pdo->prepare("UPDATE receivers SET name = ?, type = ?, tcpport = ?, enabled = ? WHERE id = ?");
|
||||
$stmt->execute([
|
||||
$_POST['name'],
|
||||
$_POST['type'],
|
||||
$_POST['tcpport'],
|
||||
isset($_POST['enabled']) ? 1 : 0,
|
||||
$_POST['id']
|
||||
]);
|
||||
$message = t('Receiver updated successfully.');
|
||||
}
|
||||
|
||||
// Handle add receiver
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_receiver'])) {
|
||||
$stmt = $pdo->prepare("INSERT INTO receivers (name, type, tcpport, enabled) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([
|
||||
$_POST['name'],
|
||||
$_POST['type'],
|
||||
$_POST['tcpport'],
|
||||
isset($_POST['enabled']) ? 1 : 0
|
||||
]);
|
||||
$message = t('Receiver added successfully.');
|
||||
}
|
||||
|
||||
// Get all receivers
|
||||
$receivers = $pdo->query("SELECT * FROM receivers")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
function isPortOpen(string $host, int $port, int $timeout = 1): bool {
|
||||
$connection = @fsockopen($host, $port, $errno, $errstr, $timeout);
|
||||
if (is_resource($connection)) {
|
||||
fclose($connection);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
|
||||
|
||||
<h2><?= t('Receivers') ?>
|
||||
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addReceiverModal"><?= t('Add receiver') ?></button>
|
||||
</h2>
|
||||
|
||||
<?php if (!empty($message)): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($message) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<table id="receiverTable" class="table table-hover" style="cursor:pointer;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= t('Name') ?></th>
|
||||
<th><?= t('Protocol') ?></th>
|
||||
<th><?= t('Port') ?></th>
|
||||
<th><?= t('Enabled') ?></th>
|
||||
<th><?= t('status') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($receivers as $receiver): ?>
|
||||
<tr data-bs-toggle="modal" data-bs-target="#editReceiverModal"
|
||||
data-id="<?= $receiver['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($receiver['name']) ?>"
|
||||
data-type="<?= $receiver['type'] ?>"
|
||||
data-port="<?= $receiver['tcpport'] ?>"
|
||||
data-enabled="<?= $receiver['enabled'] ?>">
|
||||
<td><?= htmlspecialchars($receiver['name']) ?></td>
|
||||
<td><?= $receiver['type'] ?></td>
|
||||
<td><?= $receiver['tcpport'] ?></td>
|
||||
<td><?= $receiver['enabled'] ? t('Yes') : t('No') ?></td>
|
||||
<td id="status-<?= $receiver['id'] ?>">
|
||||
<?= $statusText ?> <!-- Initial status -->
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Edit Receiver Modal -->
|
||||
<div class="modal fade" id="editReceiverModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<form method="post" class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><?= t('Edit Receiver') ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="id" id="edit-id">
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= t('Name') ?></label>
|
||||
<input type="text" name="name" id="edit-name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= t('Protocol') ?></label>
|
||||
<select name="type" id="edit-type" class="form-select">
|
||||
<option value="SIA-DC09">SIA-DC09</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= t('Port') ?></label>
|
||||
<input type="number" name="tcpport" id="edit-port" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" name="enabled" id="edit-enabled" class="form-check-input">
|
||||
<label class="form-check-label" for="edit-enabled"><?= t('Enabled') ?></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="update_receiver" class="btn btn-success"><?= t('Update Receiver') ?></button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= t('Cancel') ?></button>
|
||||
<button type="submit" name="delete_receiver" id="deleteReceiverBtn" class="btn btn-danger"><?= t('Delete Receiver') ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Receiver Modal -->
|
||||
<div class="modal fade" id="addReceiverModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<form method="post" class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><?= t('Add Receiver') ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= t('Name') ?></label>
|
||||
<input type="text" name="name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= t('Protocol') ?></label>
|
||||
<select name="type" class="form-select">
|
||||
<option value="SIA-DC09">SIA-DC09</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= t('Port') ?></label>
|
||||
<input type="number" name="tcpport" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" name="enabled" class="form-check-input" checked>
|
||||
<label class="form-check-label"><?= t('Enabled') ?></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="add_receiver" class="btn btn-success"><?= t('Add Receiver') ?></button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= t('Cancel') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('tr[data-bs-toggle="modal"]').forEach(row => {
|
||||
row.addEventListener('click', () => {
|
||||
const id = row.dataset.id;
|
||||
const name = row.dataset.name;
|
||||
const type = row.dataset.type;
|
||||
const port = row.dataset.port;
|
||||
const enabled = row.dataset.enabled;
|
||||
|
||||
document.getElementById('edit-id').value = id;
|
||||
document.getElementById('edit-name').value = name;
|
||||
document.getElementById('edit-type').value = type;
|
||||
document.getElementById('edit-port').value = port;
|
||||
document.getElementById('edit-enabled').checked = (enabled === '1');
|
||||
|
||||
// Disable the delete button if receiver is enabled
|
||||
document.getElementById('deleteReceiverBtn').disabled = (enabled === '1');
|
||||
|
||||
// Show the modal (optional if data-bs-toggle="modal" does this automatically)
|
||||
// new bootstrap.Modal(document.getElementById('editReceiverModal')).show();
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
<script>
|
||||
function updateStatuses() {
|
||||
fetch('receiver_status.php')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
data.forEach(receiver => {
|
||||
const el = document.getElementById('status-' + receiver.id);
|
||||
if (el) {
|
||||
el.textContent = receiver.status === 'up' ? 'Online' : 'Offline';
|
||||
el.className = receiver.status === 'up' ? 'text-success' : 'text-danger';
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Status update failed:', error));
|
||||
}
|
||||
|
||||
// First run
|
||||
updateStatuses();
|
||||
|
||||
// Repeat every 5 seconds
|
||||
setInterval(updateStatuses, 5000);
|
||||
</script>
|
||||
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
require 'auth.php';
|
||||
requireLogin();
|
||||
?>
|
||||
|
||||
<?php include 'header.php'; ?>
|
||||
|
||||
<!-- DataTables CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
|
||||
<div class="container mt-4">
|
||||
<h2><?= htmlspecialchars($translations['signal_log_live'] ?? 'Signal Log') ?></h2>
|
||||
|
||||
<!-- Auto-refresh toggle button -->
|
||||
<button id="toggleRefreshBtn" class="btn btn-sm btn-primary mb-3">⏸️ Auto-Refresh: OFF</button>
|
||||
|
||||
<table id="signal-log-table" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= htmlspecialchars($translations['timestamp'] ?? 'Timestamp') ?></th>
|
||||
<th><?= htmlspecialchars($translations['account'] ?? 'Account') ?></th>
|
||||
<th><?= htmlspecialchars($translations['event'] ?? 'Event') ?></th>
|
||||
<th><?= htmlspecialchars($translations['zone'] ?? 'Zone') ?></th>
|
||||
<th><?= htmlspecialchars($translations['text'] ?? 'Text') ?></th>
|
||||
<th><?= htmlspecialchars($translations['image'] ?? 'Image') ?></th>
|
||||
<th><?= htmlspecialchars($translations['source_ip'] ?? 'Source IP') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="signal-log-body">
|
||||
<!-- Filled dynamically -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Image Preview Modal -->
|
||||
<div class="modal fade" id="imageModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><?= htmlspecialchars($translations['image_preview'] ?? 'Image Preview') ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?= htmlspecialchars($translations['close'] ?? 'Close') ?>"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img src="" alt="<?= htmlspecialchars($translations['image'] ?? 'Image') ?>" class="img-fluid" id="modalImage" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DataTables JS -->
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
|
||||
<script>
|
||||
let dataTable = null;
|
||||
let autoRefresh = false;
|
||||
let refreshInterval = null;
|
||||
|
||||
function fetchSignals() {
|
||||
fetch('get_signals.php?_ts=' + new Date().getTime())
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
const tbody = document.getElementById('signal-log-body');
|
||||
|
||||
// Destroy existing DataTable instance before updating HTML
|
||||
if (dataTable) {
|
||||
dataTable.destroy();
|
||||
}
|
||||
|
||||
tbody.innerHTML = data;
|
||||
|
||||
// Re-initialize DataTable
|
||||
dataTable = new DataTable('#signal-log-table', {
|
||||
paging: true,
|
||||
searching: true,
|
||||
ordering: true,
|
||||
order: [[0, 'desc']],
|
||||
lengthMenu: [ [50, 100, 200], [50, 100, 200] ]
|
||||
});
|
||||
})
|
||||
.catch(err => console.error('Fetch error:', err));
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchSignals();
|
||||
|
||||
const toggleBtn = document.getElementById('toggleRefreshBtn');
|
||||
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
autoRefresh = !autoRefresh;
|
||||
toggleBtn.innerHTML = autoRefresh ? '🔄 Auto-Refresh: ON' : '⏸️ Auto-Refresh: OFF';
|
||||
|
||||
if (autoRefresh) {
|
||||
fetchSignals();
|
||||
refreshInterval = setInterval(fetchSignals, 2000);
|
||||
} else {
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Delegate image modal opening for dynamically loaded buttons
|
||||
document.body.addEventListener('click', e => {
|
||||
if (e.target.classList.contains('openImageModalBtn')) {
|
||||
const imgSrc = e.target.getAttribute('data-image');
|
||||
document.getElementById('modalImage').src = imgSrc;
|
||||
|
||||
const myModal = new bootstrap.Modal(document.getElementById('imageModal'));
|
||||
myModal.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
require 'auth.php';
|
||||
requireLoginBefore2FA();
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
require 'db.php';
|
||||
|
||||
$ga = new PHPGangsta_GoogleAuthenticator();
|
||||
$message = '';
|
||||
$user_id = $_SESSION['user_id'] ?? null;
|
||||
|
||||
// Fetch user info
|
||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$stmt->execute([$user_id]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user) {
|
||||
die('User not found.');
|
||||
}
|
||||
|
||||
// Load or generate secret
|
||||
if (empty($user['totp_secret']) && empty($_SESSION['totp_secret_temp'])) {
|
||||
// First-time login: generate temp secret and store in session
|
||||
$_SESSION['totp_secret_temp'] = $ga->createSecret();
|
||||
}
|
||||
|
||||
$secret = $_SESSION['totp_secret'] ?? $_SESSION['totp_secret_temp'] ?? null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$code = $_POST['code'] ?? '';
|
||||
|
||||
if (!$secret) {
|
||||
die('No secret found.');
|
||||
}
|
||||
|
||||
if ($ga->verifyCode($secret, $code, 2)) {
|
||||
// First-time setup: store the verified secret
|
||||
if (empty($user['totp_secret'])) {
|
||||
$stmt = $pdo->prepare("UPDATE users SET totp_secret = ? WHERE id = ?");
|
||||
$stmt->execute([$secret, $user_id]);
|
||||
}
|
||||
|
||||
$_SESSION['2fa_verified'] = true;
|
||||
$_SESSION['totp_secret'] = $secret;
|
||||
unset($_SESSION['totp_secret_temp']);
|
||||
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
} else {
|
||||
$message = 'Invalid 2FA code.';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>2FA Verification - SuperArc</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-light d-flex align-items-center justify-content-center vh-100">
|
||||
<div class="card shadow p-4" style="width: 100%; max-width: 400px;">
|
||||
<h3 class="text-center mb-3">2FA Verification</h3>
|
||||
|
||||
<?php if ($message): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($message) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($_SESSION['totp_secret_temp'])): ?>
|
||||
<div class="mb-3 text-center">
|
||||
<p>Scan this QR code in your authenticator app:</p>
|
||||
<img src="<?= htmlspecialchars($ga->getQRCodeGoogleUrl('AlarmDrift', $_SESSION['totp_secret_temp'])) ?>" alt="QR Code">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="code" class="form-label">Enter your 2FA code</label>
|
||||
<input type="text" name="code" id="code" class="form-control" required autofocus>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Verify</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -12,7 +12,7 @@ http {
|
|||
|
||||
location ~ \.php$ {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass php-fpm:9000;
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
crccheck
|
||||
mysql-connector-python
|
||||
|
|
@ -25,25 +25,31 @@ def build_sia_dcs_message():
|
|||
if image_url:
|
||||
body += f'[V{image_url}]'
|
||||
|
||||
data_bytes = body.encode("ascii")
|
||||
crc = CrcArc.calc(data_bytes)
|
||||
length = len(data_bytes)
|
||||
body_bytes = body.encode("ascii")
|
||||
crc = CrcArc.calc(body_bytes)
|
||||
length = f"{len(body_bytes):04X}" # Hex, 4-digit, upper-case
|
||||
|
||||
full_message = f"\n{crc:04X}{length:04X}{body}\r".encode("ascii")
|
||||
full_message = f"\n{crc:04X}{length}{body}\r".encode("ascii")
|
||||
return full_message
|
||||
|
||||
def build_adm_cid_message():
|
||||
line = input("Enter phone line number (e.g. 01): ").zfill(2)
|
||||
seq = input("Enter sequence number (e.g. 0000): ").zfill(4)
|
||||
line = input("Enter line number (e.g. 3): ").zfill(1)
|
||||
account = input("Enter account number (e.g. 1234): ").zfill(4)
|
||||
code = input("Enter event code (e.g. 401): ").zfill(3)
|
||||
qualifier = input("Enter qualifier (1=New, 3=Restore): ").zfill(1)
|
||||
area = input("Enter partition/area (e.g. 1): ").zfill(2)
|
||||
zone = input("Enter zone/user (e.g. 005): ").zfill(3)
|
||||
code = input("Enter event code (e.g. 1131): ").zfill(4)
|
||||
area = input("Enter area/partition (e.g. 01): ").zfill(2)
|
||||
zone = input("Enter zone/user (e.g. 015): ").zfill(3)
|
||||
image_url = input("Optional Image URL (or leave blank): ")
|
||||
|
||||
# ADM-CID format: \r\nLLL:18QCCC[AAAA|EEMMZZ]\r
|
||||
cid_data = f'18{qualifier}{code}[{account}|{area}{area}{zone}]'
|
||||
msg = f"\r\n{line}{cid_data}\r".encode("ascii")
|
||||
return msg
|
||||
body = f'"ADM-CID"{seq}L{line}#{account}[#{account}|{code} {area} {zone}]'
|
||||
if image_url:
|
||||
body += f'[V{image_url}]'
|
||||
body_bytes = body.encode("ascii")
|
||||
crc = CrcArc.calc(body_bytes)
|
||||
length = f"{len(body_bytes):04X}"
|
||||
|
||||
full_message = f"\n{crc:04X}{length}{body}\r".encode("ascii")
|
||||
return full_message
|
||||
|
||||
def main():
|
||||
clear_screen()
|
||||
|
|
|
|||
Loading…
Reference in New Issue