Files
intra.noxus.fi/api.php
Jukka Lampikoski 9140c912cd feat: Tekniikka-moduuli sub-tabeilla (Laitteet + Sijainnit + IPAM)
- Laitteet-tabi → Tekniikka (sub-tabit: Laitteet, Sijainnit, IPAM)
- Sijainnit siirretty omaksi taulukkonäkymäksi (+ "Lisää sijainti" laitteiden yhteydessä)
- Uusi IPAM-näkymä: IP-osoitteet, subnetit ja VLANit hallintaan
- IPAM: tyyppi (subnet/vlan/ip), verkko, VLAN-nro, sijainti, tila, asiakas
- Sub-tab-tyylit ja logiikka
- Yhteensopivuus: vanha 'devices' moduuli → 'tekniikka'

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 20:18:56 +02:00

2737 lines
106 KiB
PHP

<?php
// Turvalliset session-asetukset
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.use_strict_mode', 1);
ini_set('session.cookie_samesite', 'Lax');
session_start();
require_once __DIR__ . '/db.php';
initDatabase();
header('Content-Type: application/json');
header('X-Content-Type-Options: nosniff');
define('DATA_DIR', __DIR__ . '/data');
// Dynaaminen SITE_URL domainin mukaan
define('SITE_URL', 'https://' . ($_SERVER['HTTP_HOST'] ?? 'intra.noxus.fi'));
// Sähköpostiasetukset (fallback)
define('MAIL_FROM', 'noreply@noxus.fi');
define('MAIL_FROM_NAME', 'Noxus Intra');
// Varmista data-kansio (tiedostoja varten)
if (!file_exists(DATA_DIR)) mkdir(DATA_DIR, 0755, true);
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
// ==================== HELPERS ====================
function requireAuth() {
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => 'Kirjaudu sisään']);
exit;
}
}
function requireAdmin() {
requireAuth();
$role = $_SESSION['role'] ?? '';
if ($role !== 'admin' && $role !== 'superadmin') {
http_response_code(403);
echo json_encode(['error' => 'Vain ylläpitäjä voi tehdä tämän']);
exit;
}
}
function requireSuperAdmin() {
requireAuth();
if (($_SESSION['role'] ?? '') !== 'superadmin') {
http_response_code(403);
echo json_encode(['error' => 'Vain pääkäyttäjä voi tehdä tämän']);
exit;
}
}
function isSuperAdmin(): bool {
return ($_SESSION['role'] ?? '') === 'superadmin';
}
function isCompanyAdmin(): bool {
$role = $_SESSION['role'] ?? '';
return $role === 'admin' || $role === 'superadmin';
}
function currentUser(): string {
return $_SESSION['username'] ?? 'tuntematon';
}
function generateId(): string {
return bin2hex(random_bytes(8));
}
function generateToken(): string {
return bin2hex(random_bytes(32));
}
// ==================== MULTI-COMPANY ====================
function getCompanyDir(?string $companyId = null): string {
$id = $companyId ?? ($_SESSION['company_id'] ?? '');
if (empty($id) || !preg_match('/^[a-z0-9-]+$/', $id)) {
http_response_code(400);
echo json_encode(['error' => 'Yritystä ei ole valittu']);
exit;
}
$dir = DATA_DIR . '/companies/' . $id;
if (!file_exists($dir)) mkdir($dir, 0755, true);
return $dir;
}
function requireCompany(): string {
$companyId = $_SESSION['company_id'] ?? '';
if (empty($companyId)) {
http_response_code(400);
echo json_encode(['error' => 'Valitse ensin yritys']);
exit;
}
$userCompanies = $_SESSION['companies'] ?? [];
if (!in_array($companyId, $userCompanies)) {
http_response_code(403);
echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']);
exit;
}
return $companyId;
}
// Kuten requireCompany(), mutta sallii company_id:n overriden GET-parametrista
// Käytetään tiketti-endpointeissa jotta toisen yrityksen tikettejä voi avata
function requireCompanyOrParam(): string {
$paramCompany = $_GET['company_id'] ?? '';
if (!empty($paramCompany)) {
$userCompanies = $_SESSION['companies'] ?? [];
if (!in_array($paramCompany, $userCompanies)) {
http_response_code(403);
echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']);
exit;
}
$_SESSION['company_id'] = $paramCompany;
}
return requireCompany();
}
function companyFile(string $filename): string {
return getCompanyDir() . '/' . $filename;
}
function getClientIp(): string {
return $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
function normalizeAddress(string $addr): string {
$addr = strtolower(trim($addr));
$addr = preg_replace('/\s+/', ' ', $addr);
return $addr;
}
// ==================== EMAIL ====================
function sendMail(string $to, string $subject, string $htmlBody): bool {
$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: text/html; charset=UTF-8\r\n";
$headers .= "From: " . MAIL_FROM_NAME . " <" . MAIL_FROM . ">\r\n";
$headers .= "Reply-To: " . MAIL_FROM . "\r\n";
return mail($to, $subject, $htmlBody, $headers, '-f ' . MAIL_FROM);
}
// ==================== IMAP CLIENT (socket-pohjainen, ei vaadi php-imap) ====================
class ImapClient {
private $socket = null;
private int $tagCounter = 0;
public string $lastError = '';
public function connect(array $config): bool {
$host = $config['imap_host'] ?? '';
$port = intval($config['imap_port'] ?? 993);
$user = $config['imap_user'] ?? '';
$pass = $config['imap_password'] ?? '';
$encryption = $config['imap_encryption'] ?? 'ssl';
if (empty($host) || empty($user) || empty($pass)) {
$this->lastError = 'IMAP-asetukset puuttuvat';
return false;
}
$prefix = ($encryption === 'ssl') ? 'ssl://' : 'tcp://';
$context = stream_context_create([
'ssl' => ['verify_peer' => false, 'verify_peer_name' => false]
]);
$this->socket = @stream_socket_client(
$prefix . $host . ':' . $port,
$errno, $errstr, 15,
STREAM_CLIENT_CONNECT, $context
);
if (!$this->socket) {
$this->lastError = "Yhteys epäonnistui: {$errstr} ({$errno})";
return false;
}
stream_set_timeout($this->socket, 30);
// Read greeting
$greeting = $this->readLine();
if (!$greeting || strpos($greeting, '* OK') === false) {
$this->lastError = 'Palvelin ei vastannut oikein: ' . $greeting;
$this->disconnect();
return false;
}
// STARTTLS if needed
if ($encryption === 'tls') {
$resp = $this->command('STARTTLS');
if (!$this->isOk($resp)) {
$this->lastError = 'STARTTLS epäonnistui';
$this->disconnect();
return false;
}
if (!stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
$this->lastError = 'TLS-neuvottelu epäonnistui';
$this->disconnect();
return false;
}
}
// Login
$resp = $this->command('LOGIN "' . $this->escape($user) . '" "' . $this->escape($pass) . '"');
if (!$this->isOk($resp)) {
$this->lastError = 'Kirjautuminen epäonnistui: väärä tunnus tai salasana';
$this->disconnect();
return false;
}
// Select INBOX
$resp = $this->command('SELECT INBOX');
if (!$this->isOk($resp)) {
$this->lastError = 'INBOX:n avaus epäonnistui';
$this->disconnect();
return false;
}
return true;
}
public function fetchMessages(int $limit = 50): array {
if (!$this->socket) return [];
// Get message count from STATUS
$resp = $this->command('STATUS INBOX (MESSAGES)');
$totalMessages = 0;
foreach ($resp as $line) {
if (preg_match('/MESSAGES\s+(\d+)/i', $line, $m)) {
$totalMessages = intval($m[1]);
}
}
if ($totalMessages === 0) return [];
$start = max(1, $totalMessages - $limit + 1);
$range = $start . ':' . $totalMessages;
// Fetch headers for range
$resp = $this->command("FETCH {$range} (BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE MESSAGE-ID IN-REPLY-TO REFERENCES)] BODY.PEEK[TEXT] FLAGS)");
$messages = [];
$current = null;
$headerBuf = '';
$bodyBuf = '';
$readingHeader = false;
$readingBody = false;
$headerBytesLeft = 0;
$bodyBytesLeft = 0;
// Simpler approach: fetch one-by-one for reliability
$messages = [];
for ($i = $totalMessages; $i >= $start; $i--) {
$msg = $this->fetchSingleMessage($i);
if ($msg) $messages[] = $msg;
}
return $messages;
}
private function fetchSingleMessage(int $num): ?array {
// Fetch headers
$resp = $this->command("FETCH {$num} BODY.PEEK[HEADER]");
$headerRaw = $this->extractLiteral($resp);
if (!$headerRaw) return null;
$headers = $this->parseHeaders($headerRaw);
$subject = $this->decodeMimeHeader($headers['subject'] ?? '');
$fromRaw = $headers['from'] ?? '';
$fromParsed = $this->parseFrom($fromRaw);
$messageId = trim($headers['message-id'] ?? '');
$inReplyTo = trim($headers['in-reply-to'] ?? '');
$references = trim($headers['references'] ?? '');
$dateStr = $headers['date'] ?? '';
$date = $dateStr ? @date('Y-m-d H:i:s', strtotime($dateStr)) : date('Y-m-d H:i:s');
if (!$date) $date = date('Y-m-d H:i:s');
// Parse CC
$ccRaw = $this->decodeMimeHeader($headers['cc'] ?? '');
$ccEmails = $this->parseCcAddresses($ccRaw);
// Fetch body (text part)
$body = $this->fetchBody($num);
return [
'subject' => $subject,
'from_email' => $fromParsed['email'],
'from_name' => $this->decodeMimeHeader($fromParsed['name']),
'message_id' => $messageId,
'in_reply_to' => $inReplyTo,
'references' => $references,
'date' => $date,
'body' => $body,
'cc' => $ccEmails,
];
}
private function fetchBody(int $num): string {
// Try text/plain first via BODYSTRUCTURE
$resp = $this->command("FETCH {$num} BODYSTRUCTURE");
$structLine = implode(' ', $resp);
// Simple approach: fetch BODY[1] (usually text/plain in multipart)
// or BODY[TEXT] for simple messages
$resp = $this->command("FETCH {$num} BODY.PEEK[1]");
$body = $this->extractLiteral($resp);
if (!$body) {
// Fallback: full text
$resp = $this->command("FETCH {$num} BODY.PEEK[TEXT]");
$body = $this->extractLiteral($resp);
}
if (!$body) return '';
// Detect encoding from BODYSTRUCTURE
$encoding = '';
// Parse BODYSTRUCTURE for encoding (7BIT, BASE64, QUOTED-PRINTABLE)
if (preg_match('/"TEXT"\s+"PLAIN"\s+\([^)]*\)\s+NIL\s+NIL\s+"([^"]+)"/i', $structLine, $em)) {
$encoding = strtoupper($em[1]);
} elseif (preg_match('/BODY\[1\].*?"([^"]+)"/i', $structLine, $em)) {
$encoding = strtoupper($em[1]);
}
// Try to detect encoding from body content if not found
if (!$encoding) {
// Check if it looks like base64
if (preg_match('/^[A-Za-z0-9+\/=\s]+$/', trim($body)) && strlen(trim($body)) > 50) {
$decoded = @base64_decode($body, true);
if ($decoded !== false && strlen($decoded) > 0) {
// Verify it produces readable text
if (preg_match('/[\x20-\x7E\xC0-\xFF]/', $decoded)) {
$body = $decoded;
}
}
}
} else {
if ($encoding === 'BASE64') {
$body = base64_decode($body);
} elseif ($encoding === 'QUOTED-PRINTABLE') {
$body = quoted_printable_decode($body);
}
}
// Strip HTML if it looks like HTML
if (preg_match('/<html|<body|<div|<p\b/i', $body)) {
$body = strip_tags($body);
// Clean up whitespace
$body = preg_replace('/\n{3,}/', "\n\n", $body);
}
// Try charset conversion
if (preg_match('/charset[="\s]+([^\s;"]+)/i', $structLine, $cm)) {
$charset = strtolower(trim($cm[1], '"'));
if ($charset && $charset !== 'utf-8') {
$converted = @iconv($charset, 'UTF-8//IGNORE', $body);
if ($converted !== false) $body = $converted;
}
}
return trim($body);
}
private function parseHeaders(string $raw): array {
$headers = [];
$lines = explode("\n", str_replace("\r\n", "\n", $raw));
$lastKey = '';
foreach ($lines as $line) {
if ($line === '' || $line === "\r") continue;
// Continuation line (starts with space/tab)
if (preg_match('/^[\s\t]+(.+)/', $line, $m)) {
if ($lastKey && isset($headers[$lastKey])) {
$headers[$lastKey] .= ' ' . trim($m[1]);
}
continue;
}
if (preg_match('/^([A-Za-z\-]+):\s*(.*)$/', $line, $m)) {
$key = strtolower($m[1]);
$headers[$key] = trim($m[2]);
$lastKey = $key;
}
}
return $headers;
}
private function parseFrom(string $from): array {
$from = trim($from);
if (preg_match('/^"?([^"<]*)"?\s*<([^>]+)>/', $from, $m)) {
return ['name' => trim($m[1], ' "'), 'email' => trim($m[2])];
}
if (preg_match('/^([^\s@]+@[^\s@]+)/', $from, $m)) {
return ['name' => '', 'email' => $m[1]];
}
return ['name' => '', 'email' => $from];
}
private function parseCcAddresses(string $cc): string {
$cc = trim($cc);
if (!$cc) return '';
// Parse "Name <email>, Name2 <email2>" or "email1, email2"
$emails = [];
// Split on comma, but be careful with quoted strings
$parts = preg_split('/,\s*(?=(?:[^"]*"[^"]*")*[^"]*$)/', $cc);
foreach ($parts as $part) {
$part = trim($part);
if (!$part) continue;
$parsed = $this->parseFrom($part);
if ($parsed['email']) $emails[] = $parsed['email'];
}
return implode(', ', $emails);
}
private function decodeMimeHeader(string $str): string {
if (strpos($str, '=?') === false) return trim($str);
$decoded = '';
$parts = preg_split('/(=\?[^\?]+\?[BbQq]\?[^\?]*\?=)/', $str, -1, PREG_SPLIT_DELIM_CAPTURE);
foreach ($parts as $part) {
if (preg_match('/^=\?([^\?]+)\?([BbQq])\?([^\?]*)\?=$/', $part, $m)) {
$charset = $m[1];
$encoding = strtoupper($m[2]);
$text = $m[3];
if ($encoding === 'B') {
$text = base64_decode($text);
} elseif ($encoding === 'Q') {
$text = quoted_printable_decode(str_replace('_', ' ', $text));
}
if (strtolower($charset) !== 'utf-8') {
$converted = @iconv($charset, 'UTF-8//IGNORE', $text);
if ($converted !== false) $text = $converted;
}
$decoded .= $text;
} else {
// Remove whitespace between encoded words
if (trim($part) === '') continue;
$decoded .= $part;
}
}
return trim($decoded);
}
private function command(string $cmd): array {
$tag = 'A' . (++$this->tagCounter);
$this->writeLine("{$tag} {$cmd}");
$response = [];
while (true) {
$line = $this->readLine();
if ($line === false || $line === null) break;
$response[] = $line;
// Check for literal {N} — read N bytes
if (preg_match('/\{(\d+)\}$/', $line, $m)) {
$bytes = intval($m[1]);
$data = $this->readBytes($bytes);
$response[] = $data;
// Read the closing line after literal
$closingLine = $this->readLine();
if ($closingLine !== false && $closingLine !== null) {
$response[] = $closingLine;
}
continue;
}
// Tagged response = done
if (strpos($line, $tag . ' ') === 0) break;
}
return $response;
}
private function extractLiteral(array $resp): string {
$result = '';
for ($i = 0; $i < count($resp); $i++) {
if (preg_match('/\{(\d+)\}$/', $resp[$i], $m)) {
// Next element should be the literal data
if (isset($resp[$i + 1])) {
$result .= $resp[$i + 1];
}
}
}
return $result;
}
private function isOk(array $resp): bool {
foreach ($resp as $line) {
if (preg_match('/^A\d+\s+OK/i', $line)) return true;
if (preg_match('/^A\d+\s+(NO|BAD)/i', $line)) return false;
}
return false;
}
private function escape(string $str): string {
return str_replace(['\\', '"'], ['\\\\', '\\"'], $str);
}
private function writeLine(string $line): void {
if (!$this->socket) return;
fwrite($this->socket, $line . "\r\n");
}
private function readLine(): ?string {
if (!$this->socket) return null;
$line = fgets($this->socket, 8192);
if ($line === false) return null;
return rtrim($line, "\r\n");
}
private function readBytes(int $n): string {
if (!$this->socket) return '';
$data = '';
$remaining = $n;
while ($remaining > 0) {
$chunk = fread($this->socket, min($remaining, 8192));
if ($chunk === false || $chunk === '') break;
$data .= $chunk;
$remaining -= strlen($chunk);
}
return $data;
}
public function disconnect(): void {
if ($this->socket) {
try {
$this->command('LOGOUT');
} catch (\Throwable $e) {}
@fclose($this->socket);
$this->socket = null;
}
}
}
// ==================== TICKETS HELPER ====================
function sendTelegramAlert(string $companyId, array $ticket): void {
$config = dbLoadConfig();
$botToken = $config['telegram_bot_token'] ?? '';
$chatId = $config['telegram_chat_id'] ?? '';
if (!$botToken || !$chatId) return;
$text = "🚨 *URGENT TIKETTI*\n\n";
$text .= "📋 *" . ($ticket['subject'] ?? '(Ei aihetta)') . "*\n";
$text .= "👤 " . ($ticket['from_name'] ?? $ticket['from_email'] ?? 'Tuntematon') . "\n";
$text .= "📧 " . ($ticket['from_email'] ?? '') . "\n";
$text .= "🏢 " . $companyId . "\n";
$text .= "🕐 " . date('d.m.Y H:i');
$url = "https://api.telegram.org/bot{$botToken}/sendMessage";
$data = [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'Markdown',
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
curl_exec($ch);
curl_close($ch);
}
function sendTicketMail(string $to, string $subject, string $body, string $inReplyTo = '', string $references = '', ?array $mailbox = null, string $cc = ''): bool {
$fromEmail = $mailbox['smtp_from_email'] ?? $mailbox['imap_user'] ?? MAIL_FROM;
$fromName = $mailbox['smtp_from_name'] ?? $mailbox['nimi'] ?? 'Asiakaspalvelu';
$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
$headers .= "From: {$fromName} <{$fromEmail}>\r\n";
$headers .= "Reply-To: {$fromEmail}\r\n";
if ($cc) {
$headers .= "Cc: {$cc}\r\n";
}
if ($inReplyTo) {
$headers .= "In-Reply-To: {$inReplyTo}\r\n";
$headers .= "References: " . ($references ? $references . ' ' : '') . $inReplyTo . "\r\n";
}
return mail($to, $subject, $body, $headers, '-f ' . $fromEmail);
}
function parseLiittymat(array $input): array {
$liittymat = [];
foreach (($input['liittymat'] ?? []) as $l) {
$liittymat[] = [
'asennusosoite' => trim($l['asennusosoite'] ?? ''),
'postinumero' => trim($l['postinumero'] ?? ''),
'kaupunki' => trim($l['kaupunki'] ?? ''),
'liittymanopeus' => trim($l['liittymanopeus'] ?? ''),
'hinta' => floatval($l['hinta'] ?? 0),
'sopimuskausi' => trim($l['sopimuskausi'] ?? ''),
'alkupvm' => trim($l['alkupvm'] ?? ''),
'vlan' => trim($l['vlan'] ?? ''),
'laite' => trim($l['laite'] ?? ''),
'portti' => trim($l['portti'] ?? ''),
'ip' => trim($l['ip'] ?? ''),
];
}
if (empty($liittymat)) {
$liittymat[] = ['asennusosoite' => '', 'postinumero' => '', 'kaupunki' => '', 'liittymanopeus' => '', 'hinta' => 0, 'sopimuskausi' => '', 'alkupvm' => '', 'vlan' => '', 'laite' => '', 'portti' => '', 'ip' => ''];
}
return $liittymat;
}
// ==================== ROUTES ====================
switch ($action) {
// ---------- SAATAVUUS (julkinen API) ----------
case 'saatavuus':
$providedKey = $_GET['key'] ?? ($_SERVER['HTTP_X_API_KEY'] ?? '');
if (empty($providedKey)) {
http_response_code(403);
echo json_encode(['error' => 'API-avain puuttuu']);
break;
}
// Etsi yritys jonka API-avain täsmää
$matchedCompany = dbGetCompanyByApiKey($providedKey);
if (!$matchedCompany) {
http_response_code(403);
echo json_encode(['error' => 'Virheellinen API-avain']);
break;
}
// CORS - yrityskohtaiset originit
$allowedOrigins = dbGetCompanyCorsOrigins($matchedCompany['id']);
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Api-Key');
}
if ($method === 'OPTIONS') { http_response_code(204); break; }
// Parametrit
$queryOsoite = normalizeAddress($_GET['osoite'] ?? '');
$queryPostinumero = trim($_GET['postinumero'] ?? '');
$queryKaupunki = strtolower(trim($_GET['kaupunki'] ?? ''));
if (empty($queryOsoite) || empty($queryPostinumero) || empty($queryKaupunki)) {
http_response_code(400);
echo json_encode(['error' => 'Anna osoite, postinumero ja kaupunki']);
break;
}
// Hae VAIN tämän yrityksen asiakkaista
$customers = dbLoadCustomers($matchedCompany['id']);
$found = false;
foreach ($customers as $c) {
foreach ($c['liittymat'] ?? [] as $l) {
$addr = normalizeAddress($l['asennusosoite'] ?? '');
$zip = trim($l['postinumero'] ?? '');
$city = strtolower(trim($l['kaupunki'] ?? ''));
if ($zip === $queryPostinumero && $city === $queryKaupunki) {
if (!empty($addr) && !empty($queryOsoite)) {
if (strpos($addr, $queryOsoite) !== false || strpos($queryOsoite, $addr) !== false) {
$found = true;
break 2;
}
}
}
}
}
echo json_encode(['saatavilla' => $found]);
break;
// ---------- CONFIG (admin, yrityskohtainen) ----------
case 'config':
requireAdmin();
$companyId = requireCompany();
$globalConf = dbLoadConfig();
echo json_encode([
'api_key' => dbGetCompanyApiKey($companyId),
'cors_origins' => dbGetCompanyCorsOrigins($companyId),
'telegram_bot_token' => $globalConf['telegram_bot_token'] ?? '',
'telegram_chat_id' => $globalConf['telegram_chat_id'] ?? '',
]);
break;
case 'config_update':
requireAdmin();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['api_key'])) {
dbSetCompanyApiKey($companyId, trim($input['api_key']));
}
if (isset($input['cors_origins'])) {
$origins = array_filter(array_map('trim', explode("\n", $input['cors_origins'])));
dbSetCompanyCorsOrigins($companyId, array_values($origins));
}
// Telegram-asetukset (globaalit, tallennetaan config-tauluun)
if (isset($input['telegram_bot_token'])) {
dbSaveConfig(['telegram_bot_token' => trim($input['telegram_bot_token'])]);
}
if (isset($input['telegram_chat_id'])) {
dbSaveConfig(['telegram_chat_id' => trim($input['telegram_chat_id'])]);
}
dbAddLog($companyId, currentUser(), 'config_update', '', '', 'Päivitti asetuksia');
echo json_encode([
'api_key' => dbGetCompanyApiKey($companyId),
'cors_origins' => dbGetCompanyCorsOrigins($companyId),
]);
break;
case 'telegram_test':
requireAdmin();
if ($method !== 'POST') break;
$config = dbLoadConfig();
$botToken = $config['telegram_bot_token'] ?? '';
$chatId = $config['telegram_chat_id'] ?? '';
if (!$botToken || !$chatId) {
http_response_code(400);
echo json_encode(['error' => 'Telegram Bot Token ja Chat ID vaaditaan']);
break;
}
$url = "https://api.telegram.org/bot{$botToken}/sendMessage";
$data = ['chat_id' => $chatId, 'text' => '✅ Noxus Intra Telegram-hälytys toimii!', 'parse_mode' => 'Markdown'];
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($data), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5]);
$resp = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
echo json_encode(['success' => true]);
} else {
http_response_code(400);
echo json_encode(['error' => 'Telegram virhe: ' . $resp]);
}
break;
case 'generate_api_key':
requireAdmin();
$companyId = requireCompany();
if ($method !== 'POST') break;
$newApiKey = bin2hex(random_bytes(16));
dbSetCompanyApiKey($companyId, $newApiKey);
dbAddLog($companyId, currentUser(), 'config_update', '', '', 'Generoi uuden API-avaimen');
echo json_encode([
'api_key' => $newApiKey,
'cors_origins' => dbGetCompanyCorsOrigins($companyId),
]);
break;
// ---------- CAPTCHA ----------
case 'captcha':
$a = rand(1, 20);
$b = rand(1, 20);
$_SESSION['captcha_answer'] = $a + $b;
echo json_encode(['question' => "$a + $b = ?"]);
break;
// ---------- BRANDING (julkinen) ----------
case 'branding':
$host = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]);
echo json_encode(dbGetBranding($host));
break;
case 'company_logo':
$companyId = $_GET['company_id'] ?? '';
if (empty($companyId) || !preg_match('/^[a-z0-9-]+$/', $companyId)) {
http_response_code(400);
echo json_encode(['error' => 'Virheellinen company_id']);
break;
}
$companies = dbLoadCompanies();
$logoFile = '';
foreach ($companies as $comp) {
if ($comp['id'] === $companyId) {
$logoFile = $comp['logo_file'] ?? '';
break;
}
}
if (empty($logoFile)) {
http_response_code(404);
echo json_encode(['error' => 'Logoa ei löydy']);
break;
}
$logoPath = DATA_DIR . '/companies/' . $companyId . '/' . $logoFile;
if (!file_exists($logoPath)) {
http_response_code(404);
echo json_encode(['error' => 'Logotiedostoa ei löydy']);
break;
}
$ext = strtolower(pathinfo($logoFile, PATHINFO_EXTENSION));
$mimeTypes = ['png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'svg' => 'image/svg+xml', 'webp' => 'image/webp'];
$mime = $mimeTypes[$ext] ?? 'application/octet-stream';
header('Content-Type: ' . $mime);
header('Cache-Control: public, max-age=3600');
readfile($logoPath);
exit;
case 'company_logo_upload':
requireAdmin();
if ($method !== 'POST') break;
$companyId = $_POST['company_id'] ?? '';
if (empty($companyId) || !preg_match('/^[a-z0-9-]+$/', $companyId)) {
http_response_code(400);
echo json_encode(['error' => 'Virheellinen company_id']);
break;
}
if (!isset($_FILES['logo']) || $_FILES['logo']['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo json_encode(['error' => 'Logotiedosto puuttuu tai virhe uploadissa']);
break;
}
$file = $_FILES['logo'];
// Validoi koko (max 2MB)
if ($file['size'] > 2 * 1024 * 1024) {
http_response_code(400);
echo json_encode(['error' => 'Logo on liian suuri (max 2MB)']);
break;
}
// Validoi tyyppi (tiedostopäätteen + mahdollisen finfo:n perusteella)
$allowedExtensions = ['png', 'jpg', 'jpeg', 'svg', 'webp'];
$origExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($origExt, $allowedExtensions)) {
http_response_code(400);
echo json_encode(['error' => 'Sallitut tiedostotyypit: PNG, JPG, SVG, WebP']);
break;
}
// Käytä finfo:a jos saatavilla, muuten luota tiedostopäätteeseen
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$detectedType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
$allowedMimes = ['image/png', 'image/jpeg', 'image/svg+xml', 'image/webp'];
if (!in_array($detectedType, $allowedMimes)) {
http_response_code(400);
echo json_encode(['error' => 'Sallitut tiedostotyypit: PNG, JPG, SVG, WebP']);
break;
}
}
$extNormalize = ['jpeg' => 'jpg'];
$ext = $extNormalize[$origExt] ?? $origExt;
$newFilename = 'logo.' . $ext;
$compDir = DATA_DIR . '/companies/' . $companyId;
// Luo kansio tarvittaessa (data on nyt MySQL:ssä, kansio vain logoille)
if (!file_exists($compDir)) {
mkdir($compDir, 0755, true);
}
// Poista vanha logo ja päivitä kantaan
$companies = dbLoadCompanies();
$found = false;
foreach ($companies as $comp) {
if ($comp['id'] === $companyId) {
$oldLogo = $comp['logo_file'] ?? '';
if ($oldLogo && $oldLogo !== $newFilename && file_exists($compDir . '/' . $oldLogo)) {
unlink($compDir . '/' . $oldLogo);
}
$comp['logo_file'] = $newFilename;
dbSaveCompany($comp);
$found = true;
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Yritystä ei löydy']);
break;
}
move_uploaded_file($file['tmp_name'], $compDir . '/' . $newFilename);
echo json_encode([
'success' => true,
'logo_file' => $newFilename,
'logo_url' => "api.php?action=company_logo&company_id=" . urlencode($companyId),
]);
break;
// ---------- AUTH ----------
case 'login':
if ($method !== 'POST') break;
$ip = getClientIp();
if (!dbCheckRateLimit($ip)) {
http_response_code(429);
echo json_encode(['error' => 'Liian monta kirjautumisyritystä. Yritä uudelleen 15 minuutin kuluttua.']);
break;
}
$input = json_decode(file_get_contents('php://input'), true);
// Captcha-tarkistus
$captchaAnswer = intval($input['captcha'] ?? 0);
if (!isset($_SESSION['captcha_answer']) || $captchaAnswer !== $_SESSION['captcha_answer']) {
dbRecordLoginAttempt($ip);
http_response_code(400);
echo json_encode(['error' => 'Virheellinen captcha-vastaus']);
unset($_SESSION['captcha_answer']);
break;
}
unset($_SESSION['captcha_answer']);
$username = trim($input['username'] ?? '');
$password = $input['password'] ?? '';
$u = dbGetUserByUsername($username);
if ($u && password_verify($password, $u['password_hash'])) {
$userCompanies = $u['companies'] ?? [];
// Domain-pohjainen kirjautumisrajoitus
$host = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]);
$domainCompany = dbGetCompanyByDomain($host);
$domainCompanyId = $domainCompany ? $domainCompany['id'] : '';
// Jos domain kuuluu tietylle yritykselle, vain sen yrityksen käyttäjät + adminit pääsevät sisään
if ($domainCompanyId && $u['role'] !== 'superadmin' && !in_array($domainCompanyId, $userCompanies)) {
dbRecordLoginAttempt($ip);
http_response_code(403);
echo json_encode(['error' => 'Sinulla ei ole oikeutta kirjautua tälle sivustolle.']);
break;
}
session_regenerate_id(true);
$_SESSION['user_id'] = $u['id'];
$_SESSION['username'] = $u['username'];
$_SESSION['nimi'] = $u['nimi'];
$_SESSION['role'] = $u['role'];
$_SESSION['companies'] = $userCompanies;
// Jos domain matchaa ja käyttäjällä on oikeus -> käytä sitä
if ($domainCompanyId && in_array($domainCompanyId, $userCompanies)) {
$_SESSION['company_id'] = $domainCompanyId;
} else {
$_SESSION['company_id'] = !empty($userCompanies) ? $userCompanies[0] : '';
}
// Hae yritysten nimet
$allCompanies = dbLoadCompanies();
$companyList = [];
foreach ($allCompanies as $comp) {
if (in_array($comp['id'], $userCompanies)) {
$companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']];
}
}
echo json_encode([
'success' => true,
'username' => $u['username'],
'nimi' => $u['nimi'],
'role' => $u['role'],
'companies' => $companyList,
'company_id' => $_SESSION['company_id'],
'signatures' => $u['signatures'] ?? [],
]);
} else {
dbRecordLoginAttempt($ip);
http_response_code(401);
echo json_encode(['error' => 'Väärä käyttäjätunnus tai salasana']);
}
break;
case 'logout':
session_destroy();
echo json_encode(['success' => true]);
break;
case 'check_auth':
if (isset($_SESSION['user_id'])) {
// Synkronoi aina tuoreet yritysoikeudet tietokannasta sessioon
$u = dbGetUser($_SESSION['user_id']);
if ($u) {
$_SESSION['companies'] = $u['companies'] ?? [];
// Varmista aktiivinen yritys on sallittu
if (!in_array($_SESSION['company_id'] ?? '', $_SESSION['companies'])) {
$_SESSION['company_id'] = !empty($_SESSION['companies']) ? $_SESSION['companies'][0] : '';
}
}
// Hae yritysten nimet
$userCompanyIds = $_SESSION['companies'] ?? [];
$allCompanies = dbLoadCompanies();
$companyList = [];
foreach ($allCompanies as $comp) {
if (in_array($comp['id'], $userCompanyIds)) {
$companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']];
}
}
// Hae allekirjoitukset
$userSignatures = $u ? ($u['signatures'] ?? []) : [];
// Brändäystiedot domain-pohjaisesti (sama kuin branding-endpoint)
$host = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]);
$branding = dbGetBranding($host);
// Aktiivisen yrityksen enabled_modules
$activeCompanyId = $_SESSION['company_id'] ?? '';
$enabledModules = [];
foreach ($allCompanies as $comp) {
if ($comp['id'] === $activeCompanyId) {
$enabledModules = $comp['enabled_modules'] ?? [];
break;
}
}
echo json_encode([
'authenticated' => true,
'user_id' => $_SESSION['user_id'],
'username' => $_SESSION['username'],
'nimi' => $_SESSION['nimi'],
'role' => $_SESSION['role'],
'companies' => $companyList,
'company_id' => $_SESSION['company_id'] ?? '',
'signatures' => $userSignatures,
'branding' => $branding,
'enabled_modules' => $enabledModules,
]);
} else {
echo json_encode(['authenticated' => false]);
}
break;
// ---------- PASSWORD RESET ----------
case 'password_reset_request':
if ($method !== 'POST') break;
$ip = getClientIp();
if (!dbCheckRateLimit($ip)) {
http_response_code(429);
echo json_encode(['error' => 'Liian monta yritystä. Yritä uudelleen myöhemmin.']);
break;
}
dbRecordLoginAttempt($ip);
$input = json_decode(file_get_contents('php://input'), true);
$username = trim($input['username'] ?? '');
$user = dbGetUserByUsername($username);
// Palauta aina sama viesti (ei paljasta onko tunnus olemassa)
if ($user && !empty($user['email'])) {
$token = generateToken();
dbSaveToken($user['id'], $token);
$resetUrl = SITE_URL . '/?reset=' . $token;
$html = '<div style="font-family:sans-serif;max-width:500px;margin:0 auto;">';
$html .= '<h2 style="color:#0f3460;">Noxus Intra</h2>';
$html .= '<p>Hei ' . htmlspecialchars($user['nimi'] ?: $user['username']) . ',</p>';
$html .= '<p>Sait tämän viestin koska salasanan palautusta pyydettiin tilillesi.</p>';
$html .= '<p><a href="' . $resetUrl . '" style="display:inline-block;background:#0f3460;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:600;">Vaihda salasana</a></p>';
$html .= '<p style="color:#888;font-size:0.9em;">Linkki on voimassa 1 tunnin. Jos et pyytänyt salasanan vaihtoa, voit jättää tämän viestin huomiotta.</p>';
$html .= '</div>';
sendMail($user['email'], 'Salasanan palautus - CuituNet Intra', $html);
}
echo json_encode(['success' => true, 'message' => 'Jos käyttäjätunnus löytyy ja sillä on sähköposti, palautuslinkki lähetetään.']);
break;
case 'password_reset':
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$token = $input['token'] ?? '';
$newPassword = $input['password'] ?? '';
if (strlen($newPassword) < 4) {
http_response_code(400);
echo json_encode(['error' => 'Salasanan pitää olla vähintään 4 merkkiä']);
break;
}
$userId = dbValidateToken($token);
if (!$userId) {
http_response_code(400);
echo json_encode(['error' => 'Palautuslinkki on vanhentunut tai virheellinen']);
break;
}
$user = dbGetUser($userId);
if ($user) {
$user['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT);
dbSaveUser($user);
}
dbRemoveToken($token);
echo json_encode(['success' => true, 'message' => 'Salasana vaihdettu onnistuneesti']);
break;
case 'validate_reset_token':
$token = $_GET['token'] ?? '';
$userId = dbValidateToken($token);
echo json_encode(['valid' => $userId !== null]);
break;
// ---------- USERS ----------
case 'users':
requireSuperAdmin();
$users = dbLoadUsers();
$safe = array_map(function($u) {
unset($u['password_hash']);
return $u;
}, $users);
echo json_encode(array_values($safe));
break;
case 'user_create':
requireSuperAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$username = trim($input['username'] ?? '');
$password = $input['password'] ?? '';
$nimi = trim($input['nimi'] ?? '');
$email = trim($input['email'] ?? '');
$validRoles = ['superadmin', 'admin', 'user'];
$role = in_array($input['role'] ?? '', $validRoles) ? $input['role'] : 'user';
if (empty($username) || empty($password)) {
http_response_code(400);
echo json_encode(['error' => 'Käyttäjätunnus ja salasana vaaditaan']);
break;
}
if (strlen($password) < 4) {
http_response_code(400);
echo json_encode(['error' => 'Salasanan pitää olla vähintään 4 merkkiä']);
break;
}
$existingUser = dbGetUserByUsername($username);
if ($existingUser) {
http_response_code(400);
echo json_encode(['error' => 'Käyttäjätunnus on jo käytössä']);
break;
}
$companies = $input['companies'] ?? [];
// Validoi yritys-IDt
$allCompanies = dbLoadCompanies();
$validIds = array_column($allCompanies, 'id');
$companies = array_values(array_filter($companies, fn($c) => in_array($c, $validIds)));
$signatures = [];
if (isset($input['signatures']) && is_array($input['signatures'])) {
foreach ($input['signatures'] as $mbId => $sig) {
$signatures[(string)$mbId] = (string)$sig;
}
}
$newUser = [
'id' => generateId(),
'username' => $username,
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
'nimi' => $nimi ?: $username,
'email' => $email,
'role' => $role,
'companies' => $companies,
'signatures' => $signatures,
'luotu' => date('Y-m-d H:i:s'),
];
dbSaveUser($newUser);
$companyId = $_SESSION['company_id'] ?? '';
dbAddLog($companyId, currentUser(), 'user_create', '', '', "Lisäsi käyttäjän: {$username} ({$role})");
unset($newUser['password_hash']);
echo json_encode($newUser);
break;
case 'user_update':
requireSuperAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$u = dbGetUser($id);
if (!$u) {
http_response_code(404);
echo json_encode(['error' => 'Käyttäjää ei löydy']);
break;
}
if (isset($input['nimi'])) $u['nimi'] = trim($input['nimi']);
if (isset($input['email'])) $u['email'] = trim($input['email']);
if (isset($input['role'])) {
$validRoles = ['superadmin', 'admin', 'user'];
$u['role'] = in_array($input['role'], $validRoles) ? $input['role'] : 'user';
}
if (isset($input['companies'])) {
$allCompanies = dbLoadCompanies();
$validIds = array_column($allCompanies, 'id');
$u['companies'] = array_values(array_filter($input['companies'], fn($c) => in_array($c, $validIds)));
}
if (!empty($input['password'])) {
$u['password_hash'] = password_hash($input['password'], PASSWORD_DEFAULT);
}
if (isset($input['signatures']) && is_array($input['signatures'])) {
$sigs = [];
foreach ($input['signatures'] as $mbId => $sig) {
$sigs[(string)$mbId] = (string)$sig;
}
$u['signatures'] = $sigs;
}
dbSaveUser($u);
$companyId = $_SESSION['company_id'] ?? '';
dbAddLog($companyId, currentUser(), 'user_update', '', '', "Muokkasi käyttäjää: {$u['username']}");
// Päivitä sessio jos muokattiin kirjautunutta käyttäjää
if ($u['id'] === $_SESSION['user_id']) {
$_SESSION['companies'] = $u['companies'] ?? [];
if (!empty($u['companies']) && !in_array($_SESSION['company_id'] ?? '', $u['companies'])) {
$_SESSION['company_id'] = $u['companies'][0];
}
if (empty($u['companies'])) {
$_SESSION['company_id'] = '';
}
}
$safe = $u;
unset($safe['password_hash']);
echo json_encode($safe);
break;
case 'user_delete':
requireSuperAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
if ($id === $_SESSION['user_id']) {
http_response_code(400);
echo json_encode(['error' => 'Et voi poistaa itseäsi']);
break;
}
$deleted = dbGetUser($id);
dbDeleteUser($id);
$companyId = $_SESSION['company_id'] ?? '';
if ($deleted) dbAddLog($companyId, currentUser(), 'user_delete', '', '', "Poisti käyttäjän: {$deleted['username']}");
echo json_encode(['success' => true]);
break;
// ---------- CHANGELOG ----------
case 'changelog':
requireAuth();
$companyId = requireCompany();
$limit = intval($_GET['limit'] ?? 100);
echo json_encode(dbLoadChangelog($companyId, $limit));
break;
// ---------- CUSTOMERS ----------
case 'customers':
requireAuth();
$companyId = requireCompany();
if ($method === 'GET') {
echo json_encode(dbLoadCustomers($companyId));
}
break;
case 'customer':
requireAuth();
$companyId = requireCompany();
if ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$customer = [
'id' => generateId(),
'yritys' => trim($input['yritys'] ?? ''),
'yhteyshenkilö' => trim($input['yhteyshenkilö'] ?? ''),
'puhelin' => trim($input['puhelin'] ?? ''),
'sahkoposti' => trim($input['sahkoposti'] ?? ''),
'laskutusosoite' => trim($input['laskutusosoite'] ?? ''),
'laskutuspostinumero' => trim($input['laskutuspostinumero'] ?? ''),
'laskutuskaupunki' => trim($input['laskutuskaupunki'] ?? ''),
'laskutussahkoposti' => trim($input['laskutussahkoposti'] ?? ''),
'elaskuosoite' => trim($input['elaskuosoite'] ?? ''),
'elaskuvalittaja' => trim($input['elaskuvalittaja'] ?? ''),
'ytunnus' => trim($input['ytunnus'] ?? ''),
'lisatiedot' => trim($input['lisatiedot'] ?? ''),
'priority_emails' => trim($input['priority_emails'] ?? ''),
'liittymat' => parseLiittymat($input),
'luotu' => date('Y-m-d H:i:s'),
];
if (empty($customer['yritys'])) {
http_response_code(400);
echo json_encode(['error' => 'Yrityksen nimi vaaditaan']);
break;
}
dbSaveCustomer($companyId, $customer);
dbAddLog($companyId, currentUser(), 'customer_create', $customer['id'], $customer['yritys'], 'Lisäsi asiakkaan');
echo json_encode($customer);
}
break;
case 'customer_update':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$customers = dbLoadCustomers($companyId);
$found = false;
foreach ($customers as $c) {
if ($c['id'] === $id) {
$changes = [];
$fields = ['yritys','yhteyshenkilö','puhelin','sahkoposti','laskutusosoite','laskutuspostinumero','laskutuskaupunki','laskutussahkoposti','elaskuosoite','elaskuvalittaja','ytunnus','lisatiedot','priority_emails'];
foreach ($fields as $f) {
if (isset($input[$f])) {
$old = $c[$f] ?? '';
$new = trim($input[$f]);
if ($old !== $new) $changes[] = $f;
$c[$f] = $new;
}
}
if (isset($input['liittymat'])) {
$c['liittymat'] = parseLiittymat($input);
$changes[] = 'liittymat';
}
$c['muokattu'] = date('Y-m-d H:i:s');
$c['muokkaaja'] = currentUser();
$found = true;
dbSaveCustomer($companyId, $c);
dbAddLog($companyId, currentUser(), 'customer_update', $c['id'], $c['yritys'], 'Muokkasi: ' . implode(', ', $changes));
echo json_encode($c);
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Asiakasta ei löydy']);
}
break;
case 'customer_delete':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$customers = dbLoadCustomers($companyId);
$archived = null;
foreach ($customers as $c) {
if ($c['id'] === $id) {
$c['arkistoitu'] = date('Y-m-d H:i:s');
$c['arkistoija'] = currentUser();
$archived = $c;
break;
}
}
if ($archived) {
dbArchiveCustomer($companyId, $archived);
dbDeleteCustomer($id);
dbAddLog($companyId, currentUser(), 'customer_archive', $archived['id'], $archived['yritys'], 'Arkistoi asiakkaan');
}
echo json_encode(['success' => true]);
break;
// ---------- SIJAINNIT (SITES) ----------
case 'sites':
requireAuth();
$companyId = requireCompany();
echo json_encode(dbLoadSites($companyId));
break;
case 'site_save':
requireAdmin();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$site = [
'id' => $input['id'] ?? generateId(),
'nimi' => trim($input['nimi'] ?? ''),
'osoite' => trim($input['osoite'] ?? ''),
'kaupunki' => trim($input['kaupunki'] ?? ''),
];
if (empty($site['nimi'])) {
http_response_code(400);
echo json_encode(['error' => 'Sijainnin nimi vaaditaan']);
break;
}
dbSaveSite($companyId, $site);
dbAddLog($companyId, currentUser(), 'site_save', $site['id'], $site['nimi'], (isset($input['id']) ? 'Muokkasi' : 'Lisäsi') . ' sijainnin');
echo json_encode($site);
break;
case 'site_delete':
requireAdmin();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
// Hae nimi logitusta varten
$sites = dbLoadSites($companyId);
$siteName = '';
foreach ($sites as $s) { if ($s['id'] === $id) { $siteName = $s['nimi']; break; } }
dbDeleteSite($id);
dbAddLog($companyId, currentUser(), 'site_delete', $id, $siteName, 'Poisti sijainnin');
echo json_encode(['success' => true]);
break;
// ---------- LAITTEET (DEVICES) ----------
case 'devices':
requireAuth();
$companyId = requireCompany();
echo json_encode(dbLoadDevices($companyId));
break;
case 'device_save':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$isNew = empty($input['id']);
$device = [
'id' => $input['id'] ?? generateId(),
'nimi' => trim($input['nimi'] ?? ''),
'hallintaosoite' => trim($input['hallintaosoite'] ?? ''),
'serial' => trim($input['serial'] ?? ''),
'site_id' => $input['site_id'] ?? null,
'funktio' => trim($input['funktio'] ?? ''),
'tyyppi' => trim($input['tyyppi'] ?? ''),
'malli' => trim($input['malli'] ?? ''),
'ping_check' => !empty($input['ping_check']),
'lisatiedot' => trim($input['lisatiedot'] ?? ''),
'luotu' => $isNew ? date('Y-m-d H:i:s') : ($input['luotu'] ?? date('Y-m-d H:i:s')),
'muokattu' => $isNew ? null : date('Y-m-d H:i:s'),
'muokkaaja' => $isNew ? '' : currentUser(),
];
if (empty($device['nimi'])) {
http_response_code(400);
echo json_encode(['error' => 'Laitteen nimi vaaditaan']);
break;
}
dbSaveDevice($companyId, $device);
dbAddLog($companyId, currentUser(), $isNew ? 'device_create' : 'device_update', $device['id'], $device['nimi'], ($isNew ? 'Lisäsi' : 'Muokkasi') . ' laitteen');
echo json_encode($device);
break;
case 'device_delete':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$devices = dbLoadDevices($companyId);
$deviceName = '';
foreach ($devices as $d) { if ($d['id'] === $id) { $deviceName = $d['nimi']; break; } }
dbDeleteDevice($id);
dbAddLog($companyId, currentUser(), 'device_delete', $id, $deviceName, 'Poisti laitteen');
echo json_encode(['success' => true]);
break;
// ---------- IPAM ----------
case 'ipam':
requireAuth();
$companyId = requireCompany();
echo json_encode(dbLoadIpam($companyId));
break;
case 'ipam_save':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$entry = [
'id' => $input['id'] ?? generateId(),
'tyyppi' => $input['tyyppi'] ?? 'ip',
'nimi' => trim($input['nimi'] ?? ''),
'verkko' => trim($input['verkko'] ?? ''),
'vlan_id' => $input['vlan_id'] ?? null,
'site_id' => $input['site_id'] ?? null,
'tila' => $input['tila'] ?? 'vapaa',
'asiakas' => trim($input['asiakas'] ?? ''),
'lisatiedot' => trim($input['lisatiedot'] ?? ''),
'luotu' => $input['luotu'] ?? date('Y-m-d H:i:s'),
'muokattu' => date('Y-m-d H:i:s'),
'muokkaaja' => currentUser(),
];
dbSaveIpam($companyId, $entry);
$action_label = isset($input['id']) && !empty($input['id']) ? 'ipam_update' : 'ipam_create';
$desc = ($entry['tyyppi'] === 'vlan' ? 'VLAN ' . ($entry['vlan_id'] ?? '') : $entry['verkko']) . ' ' . $entry['nimi'];
dbAddLog($companyId, currentUser(), $action_label, $entry['id'], $desc, $action_label === 'ipam_create' ? 'Lisäsi IPAM-merkinnän' : 'Muokkasi IPAM-merkintää');
echo json_encode($entry);
break;
case 'ipam_delete':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$all = dbLoadIpam($companyId);
$entryName = '';
foreach ($all as $e) { if ($e['id'] === $id) { $entryName = ($e['tyyppi'] === 'vlan' ? 'VLAN ' . $e['vlan_id'] : $e['verkko']) . ' ' . $e['nimi']; break; } }
dbDeleteIpam($id);
dbAddLog($companyId, currentUser(), 'ipam_delete', $id, $entryName, 'Poisti IPAM-merkinnän');
echo json_encode(['success' => true]);
break;
// ---------- ARCHIVE ----------
case 'archived_customers':
requireAuth();
$companyId = requireCompany();
echo json_encode(dbLoadArchive($companyId));
break;
case 'customer_restore':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$restored = dbRestoreArchive($id);
if ($restored) {
unset($restored['arkistoitu'], $restored['arkistoija'], $restored['archived_at']);
dbSaveCustomer($companyId, $restored);
dbAddLog($companyId, currentUser(), 'customer_restore', $restored['id'], $restored['yritys'] ?? '', 'Palautti asiakkaan arkistosta');
}
echo json_encode(['success' => true]);
break;
case 'customer_permanent_delete':
requireAdmin();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
// Hae arkistoidun tiedot ennen poistoa
$archive = dbLoadArchive($companyId);
$deleted = null;
foreach ($archive as $c) {
if ($c['id'] === $id) { $deleted = $c; break; }
}
dbDeleteArchive($id);
$filesDir = getCompanyDir() . '/files/' . $id;
if (is_dir($filesDir)) {
array_map('unlink', glob($filesDir . '/*'));
rmdir($filesDir);
}
if ($deleted) dbAddLog($companyId, currentUser(), 'customer_permanent_delete', $id, $deleted['yritys'] ?? '', 'Poisti pysyvästi');
echo json_encode(['success' => true]);
break;
// ---------- LEADS ----------
case 'leads':
requireAuth();
$companyId = requireCompany();
echo json_encode(dbLoadLeads($companyId));
break;
case 'lead_create':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$lead = [
'id' => generateId(),
'yritys' => trim($input['yritys'] ?? ''),
'yhteyshenkilo' => trim($input['yhteyshenkilo'] ?? ''),
'puhelin' => trim($input['puhelin'] ?? ''),
'sahkoposti' => trim($input['sahkoposti'] ?? ''),
'osoite' => trim($input['osoite'] ?? ''),
'kaupunki' => trim($input['kaupunki'] ?? ''),
'tila' => trim($input['tila'] ?? 'uusi'),
'muistiinpanot' => trim($input['muistiinpanot'] ?? ''),
'luotu' => date('Y-m-d H:i:s'),
'luoja' => currentUser(),
];
if (empty($lead['yritys'])) {
http_response_code(400);
echo json_encode(['error' => 'Yrityksen nimi vaaditaan']);
break;
}
dbSaveLead($companyId, $lead);
dbAddLog($companyId, currentUser(), 'lead_create', $lead['id'], $lead['yritys'], 'Lisäsi liidin');
echo json_encode($lead);
break;
case 'lead_update':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$leads = dbLoadLeads($companyId);
$found = false;
foreach ($leads as $l) {
if ($l['id'] === $id) {
$fields = ['yritys','yhteyshenkilo','puhelin','sahkoposti','osoite','kaupunki','tila','muistiinpanot'];
foreach ($fields as $f) {
if (isset($input[$f])) $l[$f] = trim($input[$f]);
}
$l['muokattu'] = date('Y-m-d H:i:s');
$l['muokkaaja'] = currentUser();
$found = true;
dbSaveLead($companyId, $l);
dbAddLog($companyId, currentUser(), 'lead_update', $l['id'], $l['yritys'], 'Muokkasi liidiä');
echo json_encode($l);
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Liidiä ei löydy']);
}
break;
case 'lead_delete':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$leads = dbLoadLeads($companyId);
$deleted = null;
foreach ($leads as $l) {
if ($l['id'] === $id) { $deleted = $l; break; }
}
dbDeleteLead($id);
if ($deleted) dbAddLog($companyId, currentUser(), 'lead_delete', $id, $deleted['yritys'] ?? '', 'Poisti liidin');
echo json_encode(['success' => true]);
break;
case 'lead_to_customer':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$leads = dbLoadLeads($companyId);
$lead = null;
foreach ($leads as $l) {
if ($l['id'] === $id) { $lead = $l; break; }
}
if (!$lead) {
http_response_code(404);
echo json_encode(['error' => 'Liidiä ei löydy']);
break;
}
// Luo asiakas liidistä
$customer = [
'id' => generateId(),
'yritys' => $lead['yritys'],
'yhteyshenkilö' => $lead['yhteyshenkilo'] ?? '',
'puhelin' => $lead['puhelin'] ?? '',
'sahkoposti' => $lead['sahkoposti'] ?? '',
'laskutusosoite' => '',
'laskutuspostinumero' => '',
'laskutuskaupunki' => '',
'laskutussahkoposti' => '',
'elaskuosoite' => '',
'elaskuvalittaja' => '',
'ytunnus' => '',
'lisatiedot' => $lead['muistiinpanot'] ?? '',
'liittymat' => [['asennusosoite' => $lead['osoite'] ?? '', 'postinumero' => '', 'kaupunki' => $lead['kaupunki'] ?? '', 'liittymanopeus' => '', 'hinta' => 0, 'sopimuskausi' => '', 'alkupvm' => '', 'vlan' => '', 'laite' => '', 'portti' => '', 'ip' => '']],
'luotu' => date('Y-m-d H:i:s'),
];
dbSaveCustomer($companyId, $customer);
// Poista liidi
dbDeleteLead($id);
dbAddLog($companyId, currentUser(), 'lead_to_customer', $customer['id'], $customer['yritys'], 'Muutti liidin asiakkaaksi');
echo json_encode($customer);
break;
// ---------- FILES ----------
case 'file_upload':
requireAuth();
requireCompany();
if ($method !== 'POST') break;
$customerId = $_POST['customer_id'] ?? '';
if (!$customerId || !preg_match('/^[a-f0-9]+$/', $customerId)) {
http_response_code(400);
echo json_encode(['error' => 'Virheellinen asiakas-ID']);
break;
}
if (empty($_FILES['file'])) {
http_response_code(400);
echo json_encode(['error' => 'Tiedosto puuttuu']);
break;
}
$file = $_FILES['file'];
if ($file['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo json_encode(['error' => 'Tiedoston lähetys epäonnistui']);
break;
}
if ($file['size'] > 20 * 1024 * 1024) {
http_response_code(400);
echo json_encode(['error' => 'Tiedosto on liian suuri (max 20 MB)']);
break;
}
$uploadDir = getCompanyDir() . '/files/' . $customerId;
if (!file_exists($uploadDir)) mkdir($uploadDir, 0755, true);
$safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($file['name']));
$dest = $uploadDir . '/' . $safeName;
if (file_exists($dest)) {
$ext = pathinfo($safeName, PATHINFO_EXTENSION);
$base = pathinfo($safeName, PATHINFO_FILENAME);
$safeName = $base . '_' . date('His') . ($ext ? '.' . $ext : '');
$dest = $uploadDir . '/' . $safeName;
}
if (move_uploaded_file($file['tmp_name'], $dest)) {
echo json_encode(['success' => true, 'filename' => $safeName, 'size' => $file['size']]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Tallennusvirhe']);
}
break;
case 'file_list':
requireAuth();
requireCompany();
$customerId = $_GET['customer_id'] ?? '';
if (!$customerId || !preg_match('/^[a-f0-9]+$/', $customerId)) {
echo json_encode([]);
break;
}
$dir = getCompanyDir() . '/files/' . $customerId;
$files = [];
if (is_dir($dir)) {
foreach (scandir($dir) as $f) {
if ($f === '.' || $f === '..') continue;
$path = $dir . '/' . $f;
$files[] = ['filename' => $f, 'size' => filesize($path), 'modified' => date('Y-m-d H:i', filemtime($path))];
}
}
usort($files, fn($a, $b) => strcmp($b['modified'], $a['modified']));
echo json_encode($files);
break;
case 'file_download':
requireAuth();
requireCompany();
$customerId = $_GET['customer_id'] ?? '';
$filename = $_GET['filename'] ?? '';
if (!$customerId || !preg_match('/^[a-f0-9]+$/', $customerId) || !$filename) {
http_response_code(400);
echo json_encode(['error' => 'Virheelliset parametrit']);
break;
}
$safeName = basename($filename);
$path = getCompanyDir() . '/files/' . $customerId . '/' . $safeName;
if (!file_exists($path)) {
http_response_code(404);
echo json_encode(['error' => 'Tiedostoa ei löydy']);
break;
}
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $safeName . '"');
header('Content-Length: ' . filesize($path));
readfile($path);
exit;
case 'file_delete':
requireAuth();
requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$customerId = $input['customer_id'] ?? '';
$filename = $input['filename'] ?? '';
if (!$customerId || !preg_match('/^[a-f0-9]+$/', $customerId) || !$filename) {
http_response_code(400);
echo json_encode(['error' => 'Virheelliset parametrit']);
break;
}
$safeName = basename($filename);
$path = getCompanyDir() . '/files/' . $customerId . '/' . $safeName;
if (file_exists($path)) unlink($path);
echo json_encode(['success' => true]);
break;
// ---------- TICKETS ----------
case 'tickets':
requireAuth();
$allCompaniesMode = !empty($_GET['all']);
$userCompanyIds = $_SESSION['companies'] ?? [];
// Kerää yritykset joista haetaan
$companiesToQuery = [];
if ($allCompaniesMode && count($userCompanyIds) > 1) {
$allComps = dbLoadCompanies();
foreach ($allComps as $c) {
if (in_array($c['id'], $userCompanyIds)) {
$companiesToQuery[] = $c;
}
}
} else {
requireCompany();
$companiesToQuery[] = ['id' => $_SESSION['company_id'], 'nimi' => ''];
}
$list = [];
foreach ($companiesToQuery as $comp) {
$tickets = dbLoadTickets($comp['id']);
// Auto-close tarkistus
$now = date('Y-m-d H:i:s');
foreach ($tickets as &$tc) {
if (!empty($tc['auto_close_at']) && $tc['auto_close_at'] <= $now && !in_array($tc['status'], ['suljettu'])) {
$tc['status'] = 'suljettu';
$tc['updated'] = $now;
dbSaveTicket($comp['id'], $tc);
}
}
unset($tc);
// Resolve mailbox names for this company
$mailboxes = dbLoadMailboxes($comp['id']);
$mailboxNames = [];
foreach ($mailboxes as $mb) {
$mailboxNames[$mb['id']] = $mb['nimi'];
}
foreach ($tickets as $t) {
$msgCount = count($t['messages'] ?? []);
$lastMsg = $msgCount > 0 ? $t['messages'][$msgCount - 1] : null;
$list[] = [
'id' => $t['id'],
'subject' => $t['subject'],
'from_email' => $t['from_email'],
'from_name' => $t['from_name'],
'status' => $t['status'],
'type' => $t['type'] ?? 'muu',
'assigned_to' => $t['assigned_to'] ?? '',
'customer_id' => $t['customer_id'] ?? '',
'customer_name' => $t['customer_name'] ?? '',
'tags' => $t['tags'] ?? [],
'priority' => $t['priority'] ?? 'normaali',
'auto_close_at' => $t['auto_close_at'] ?? '',
'mailbox_id' => $t['mailbox_id'] ?? '',
'mailbox_name' => $mailboxNames[$t['mailbox_id'] ?? ''] ?? '',
'company_id' => $comp['id'],
'company_name' => $comp['nimi'] ?? '',
'created' => $t['created'],
'updated' => $t['updated'],
'message_count' => $msgCount,
'last_message_type' => $lastMsg ? ($lastMsg['type'] ?? '') : '',
'last_message_time' => $lastMsg ? ($lastMsg['timestamp'] ?? '') : '',
];
}
}
echo json_encode($list);
break;
case 'ticket_detail':
requireAuth();
$companyId = requireCompanyOrParam();
$id = $_GET['id'] ?? '';
$tickets = dbLoadTickets($companyId);
$ticket = null;
foreach ($tickets as $t) {
if ($t['id'] === $id) { $ticket = $t; break; }
}
if (!$ticket) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
break;
}
echo json_encode($ticket);
break;
case 'ticket_fetch':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$companyConf = dbGetCompanyConfig($companyId);
$mailboxes = array_filter($companyConf['mailboxes'] ?? [], fn($mb) => !empty($mb['aktiivinen']));
if (empty($mailboxes)) {
http_response_code(400);
echo json_encode(['error' => 'Postilaatikoita ei ole määritetty. Lisää ne Yritykset-välilehdellä.']);
break;
}
$tickets = dbLoadTickets($companyId);
$newCount = 0;
$threadedCount = 0;
$errors = [];
// Collect all existing message IDs for duplicate detection
$existingMsgIds = [];
foreach ($tickets as $t) {
if ($t['message_id']) $existingMsgIds[$t['message_id']] = true;
foreach ($t['messages'] ?? [] as $m) {
if (!empty($m['message_id'])) $existingMsgIds[$m['message_id']] = true;
}
}
// Hae kaikista aktiivisista postilaatikoista
foreach ($mailboxes as $mailbox) {
$imapConfig = [
'imap_host' => $mailbox['imap_host'] ?? '',
'imap_port' => $mailbox['imap_port'] ?? 993,
'imap_user' => $mailbox['imap_user'] ?? '',
'imap_password' => $mailbox['imap_password'] ?? '',
'imap_encryption' => $mailbox['imap_encryption'] ?? 'ssl',
];
$imap = new ImapClient();
if (!$imap->connect($imapConfig)) {
$errors[] = ($mailbox['nimi'] ?? 'Tuntematon') . ': ' . $imap->lastError;
continue;
}
$emails = $imap->fetchMessages(100);
$imap->disconnect();
$rules = $companyConf['ticket_rules'] ?? [];
foreach ($emails as $email) {
if (!empty($email['message_id']) && isset($existingMsgIds[$email['message_id']])) {
continue;
}
$msg = [
'id' => generateId(),
'type' => 'email_in',
'from' => $email['from_email'],
'from_name' => $email['from_name'],
'body' => $email['body'],
'timestamp' => $email['date'],
'message_id' => $email['message_id'],
];
// Threading: find existing ticket by references
$existingTicket = null;
if ($email['in_reply_to']) {
$existingTicket = dbFindTicketByMessageId($companyId, $email['in_reply_to']);
}
if (!$existingTicket && $email['references']) {
$refs = preg_split('/\s+/', $email['references']);
foreach ($refs as $ref) {
$ref = trim($ref);
if (!$ref) continue;
$existingTicket = dbFindTicketByMessageId($companyId, $ref);
if ($existingTicket) break;
}
}
if ($existingTicket) {
// Load full ticket with messages
$fullTickets = dbLoadTickets($companyId);
foreach ($fullTickets as $ft) {
if ($ft['id'] === $existingTicket['id']) {
$ft['messages'][] = $msg;
$ft['updated'] = $email['date'];
if (in_array($ft['status'], ['ratkaistu', 'suljettu'])) {
$ft['status'] = 'kasittelyssa';
}
dbSaveTicket($companyId, $ft);
$threadedCount++;
break;
}
}
} else {
// Tarkista onko lähettäjä priority-listalla
$ticketPriority = 'normaali';
if (dbIsPriorityEmail($companyId, $email['from_email'])) {
$ticketPriority = 'tärkeä';
}
$ticket = [
'id' => generateId(),
'subject' => $email['subject'] ?: '(Ei aihetta)',
'from_email' => $email['from_email'],
'from_name' => $email['from_name'],
'status' => 'uusi',
'type' => 'muu',
'assigned_to' => '',
'customer_id' => '',
'customer_name' => '',
'tags' => [],
'cc' => $email['cc'] ?? '',
'priority' => $ticketPriority,
'auto_close_at' => '',
'mailbox_id' => $mailbox['id'],
'created' => $email['date'],
'updated' => $email['date'],
'message_id' => $email['message_id'],
'messages' => [$msg],
];
// Apply auto-rules
foreach ($rules as $rule) {
if (empty($rule['enabled'])) continue;
$match = true;
if (!empty($rule['from_contains'])) {
$needle = strtolower($rule['from_contains']);
if (strpos(strtolower($email['from_email'] . ' ' . $email['from_name']), $needle) === false) {
$match = false;
}
}
if (!empty($rule['subject_contains'])) {
$needle = strtolower($rule['subject_contains']);
if (strpos(strtolower($email['subject'] ?? ''), $needle) === false) {
$match = false;
}
}
if ($match) {
if (!empty($rule['set_status'])) $ticket['status'] = $rule['set_status'];
if (!empty($rule['set_type'])) $ticket['type'] = $rule['set_type'];
if (!empty($rule['set_tags'])) {
$ruleTags = array_map('trim', explode(',', $rule['set_tags']));
$ticket['tags'] = array_values(array_unique(array_merge($ticket['tags'], $ruleTags)));
}
if (!empty($rule['auto_close_days'])) {
$days = intval($rule['auto_close_days']);
if ($days > 0) {
$ticket['auto_close_at'] = date('Y-m-d H:i:s', strtotime("+{$days} days"));
}
}
break;
}
}
dbSaveTicket($companyId, $ticket);
// Telegram-hälytys tärkeille/urgentille
if ($ticket['priority'] === 'urgent' || $ticket['priority'] === 'tärkeä') {
sendTelegramAlert($companyId, $ticket);
}
$newCount++;
}
if ($email['message_id']) $existingMsgIds[$email['message_id']] = true;
}
}
// Reload for total count
$allTickets = dbLoadTickets($companyId);
dbAddLog($companyId, currentUser(), 'ticket_fetch', '', '', "Haettu sähköpostit: {$newCount} uutta tikettiä, {$threadedCount} ketjutettu");
$result = ['success' => true, 'new_tickets' => $newCount, 'threaded' => $threadedCount, 'total' => count($allTickets)];
if (!empty($errors)) $result['errors'] = $errors;
echo json_encode($result);
break;
case 'ticket_reply':
requireAuth();
$companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$body = trim($input['body'] ?? '');
$replyMailboxId = $input['mailbox_id'] ?? '';
$replyCc = trim($input['cc'] ?? '');
if (empty($body)) {
http_response_code(400);
echo json_encode(['error' => 'Viesti ei voi olla tyhjä']);
break;
}
$tickets = dbLoadTickets($companyId);
$found = false;
foreach ($tickets as $t) {
if ($t['id'] === $id) {
// Find last message_id for threading
$lastMsgId = $t['message_id'] ?? '';
$allRefs = $lastMsgId;
foreach ($t['messages'] as $m) {
if (!empty($m['message_id'])) {
$lastMsgId = $m['message_id'];
$allRefs .= ' ' . $m['message_id'];
}
}
// Send email — hae postilaatikon asetukset
// Käytä frontendistä valittua mailboxia tai tiketin oletusta
$companyConf = dbGetCompanyConfig($companyId);
$useMailboxId = $replyMailboxId ?: ($t['mailbox_id'] ?? '');
$replyMailbox = null;
foreach ($companyConf['mailboxes'] ?? [] as $mb) {
if ($mb['id'] === $useMailboxId) { $replyMailbox = $mb; break; }
}
// Fallback: käytä ensimmäistä postilaatikkoa
if (!$replyMailbox && !empty($companyConf['mailboxes'])) {
$replyMailbox = $companyConf['mailboxes'][0];
}
// Hae käyttäjän allekirjoitus tälle postilaatikolle
$mailboxId = $replyMailbox['id'] ?? '';
$signature = '';
$sigUser = dbGetUser($_SESSION['user_id']);
if ($sigUser) {
$signature = trim($sigUser['signatures'][$mailboxId] ?? '');
}
$emailBody = $signature ? $body . "\n\n-- \n" . $signature : $body;
// CC: käytä frontendistä annettua CC:tä, tai tiketin alkuperäistä CC:tä
$ccToSend = $replyCc !== '' ? $replyCc : ($t['cc'] ?? '');
$subject = 'Re: ' . $t['subject'];
$sent = sendTicketMail($t['from_email'], $subject, $emailBody, $lastMsgId, trim($allRefs), $replyMailbox, $ccToSend);
if (!$sent) {
http_response_code(500);
echo json_encode(['error' => 'Sähköpostin lähetys epäonnistui']);
break 2;
}
// Päivitä tiketin CC jos muuttunut
if ($replyCc !== '' && $replyCc !== ($t['cc'] ?? '')) {
$t['cc'] = $replyCc;
}
// Add reply to ticket (tallennetaan allekirjoituksen kanssa)
$reply = [
'id' => generateId(),
'type' => 'reply_out',
'from' => currentUser(),
'from_name' => $_SESSION['nimi'] ?? currentUser(),
'body' => $emailBody,
'timestamp' => date('Y-m-d H:i:s'),
'message_id' => '',
];
$t['messages'][] = $reply;
$t['updated'] = date('Y-m-d H:i:s');
if ($t['status'] === 'uusi') $t['status'] = 'kasittelyssa';
dbSaveTicket($companyId, $t);
$found = true;
dbAddLog($companyId, currentUser(), 'ticket_reply', $t['id'], $t['subject'], 'Vastasi tikettiin');
echo json_encode($t);
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
}
break;
case 'ticket_status':
requireAuth();
$companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$status = $input['status'] ?? '';
$validStatuses = ['uusi', 'kasittelyssa', 'odottaa', 'ratkaistu', 'suljettu'];
if (!in_array($status, $validStatuses)) {
http_response_code(400);
echo json_encode(['error' => 'Virheellinen tila']);
break;
}
$tickets = dbLoadTickets($companyId);
$found = false;
foreach ($tickets as $t) {
if ($t['id'] === $id) {
$oldStatus = $t['status'];
$t['status'] = $status;
$t['updated'] = date('Y-m-d H:i:s');
dbSaveTicket($companyId, $t);
$found = true;
dbAddLog($companyId, currentUser(), 'ticket_status', $t['id'], $t['subject'], "Tila: {$oldStatus}{$status}");
echo json_encode($t);
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
}
break;
case 'ticket_type':
requireAuth();
$companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$type = $input['type'] ?? '';
$validTypes = ['laskutus', 'tekniikka', 'vika', 'muu'];
if (!in_array($type, $validTypes)) {
http_response_code(400);
echo json_encode(['error' => 'Virheellinen tyyppi']);
break;
}
$tickets = dbLoadTickets($companyId);
$found = false;
foreach ($tickets as $t) {
if ($t['id'] === $id) {
$oldType = $t['type'] ?? 'muu';
$t['type'] = $type;
$t['updated'] = date('Y-m-d H:i:s');
dbSaveTicket($companyId, $t);
$found = true;
dbAddLog($companyId, currentUser(), 'ticket_type', $t['id'], $t['subject'], "Tyyppi: {$oldType}{$type}");
echo json_encode($t);
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
}
break;
case 'ticket_customer':
requireAuth();
$companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$customerId = $input['customer_id'] ?? '';
$customerName = $input['customer_name'] ?? '';
$tickets = dbLoadTickets($companyId);
$found = false;
foreach ($tickets as $t) {
if ($t['id'] === $id) {
$t['customer_id'] = $customerId;
$t['customer_name'] = $customerName;
$t['updated'] = date('Y-m-d H:i:s');
dbSaveTicket($companyId, $t);
$found = true;
dbAddLog($companyId, currentUser(), 'ticket_customer', $t['id'], $t['subject'], "Asiakkuus: {$customerName}");
echo json_encode($t);
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
}
break;
case 'ticket_assign':
requireAuth();
$companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$assignTo = trim($input['assigned_to'] ?? '');
$tickets = dbLoadTickets($companyId);
$found = false;
foreach ($tickets as $t) {
if ($t['id'] === $id) {
$t['assigned_to'] = $assignTo;
$t['updated'] = date('Y-m-d H:i:s');
dbSaveTicket($companyId, $t);
$found = true;
dbAddLog($companyId, currentUser(), 'ticket_assign', $t['id'], $t['subject'], "Osoitettu: {$assignTo}");
echo json_encode($t);
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
}
break;
case 'ticket_note':
requireAuth();
$companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$body = trim($input['body'] ?? '');
if (empty($body)) {
http_response_code(400);
echo json_encode(['error' => 'Muistiinpano ei voi olla tyhjä']);
break;
}
$tickets = dbLoadTickets($companyId);
$found = false;
foreach ($tickets as $t) {
if ($t['id'] === $id) {
$note = [
'id' => generateId(),
'type' => 'note',
'from' => currentUser(),
'from_name' => $_SESSION['nimi'] ?? currentUser(),
'body' => $body,
'timestamp' => date('Y-m-d H:i:s'),
'message_id' => '',
];
$t['messages'][] = $note;
$t['updated'] = date('Y-m-d H:i:s');
dbSaveTicket($companyId, $t);
$found = true;
dbAddLog($companyId, currentUser(), 'ticket_note', $t['id'], $t['subject'], 'Lisäsi muistiinpanon');
echo json_encode($t);
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
}
break;
case 'ticket_delete':
requireAuth();
$companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$tickets = dbLoadTickets($companyId);
$deleted = null;
foreach ($tickets as $t) {
if ($t['id'] === $id) { $deleted = $t; break; }
}
dbDeleteTicket($id);
if ($deleted) dbAddLog($companyId, currentUser(), 'ticket_delete', $id, $deleted['subject'] ?? '', 'Poisti tiketin');
echo json_encode(['success' => true]);
break;
case 'ticket_tags':
requireAuth();
$companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$tags = $input['tags'] ?? [];
// Sanitize tags: trim, lowercase, remove empty
$tags = array_values(array_filter(array_map(function($t) {
return trim(strtolower($t));
}, $tags)));
$tickets = dbLoadTickets($companyId);
$found = false;
foreach ($tickets as $t) {
if ($t['id'] === $id) {
$t['tags'] = $tags;
$t['updated'] = date('Y-m-d H:i:s');
dbSaveTicket($companyId, $t);
$found = true;
dbAddLog($companyId, currentUser(), 'ticket_tags', $t['id'], $t['subject'], 'Tagit: ' . implode(', ', $tags));
echo json_encode($t);
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
}
break;
case 'ticket_rules':
requireAuth();
$companyId = requireCompany();
echo json_encode(dbLoadTicketRules($companyId));
break;
case 'ticket_rule_save':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$rule = [
'id' => $input['id'] ?? generateId(),
'name' => trim($input['name'] ?? ''),
'from_contains' => trim($input['from_contains'] ?? ''),
'subject_contains' => trim($input['subject_contains'] ?? ''),
'set_status' => $input['set_status'] ?? '',
'set_type' => $input['set_type'] ?? '',
'set_tags' => trim($input['set_tags'] ?? ''),
'auto_close_days' => intval($input['auto_close_days'] ?? 0),
'enabled' => $input['enabled'] ?? true,
];
if (empty($rule['name'])) {
http_response_code(400);
echo json_encode(['error' => 'Säännön nimi puuttuu']);
break;
}
dbSaveTicketRule($companyId, $rule);
dbAddLog($companyId, currentUser(), 'config_update', '', '', 'Tikettisääntö: ' . $rule['name']);
echo json_encode($rule);
break;
case 'ticket_bulk_status':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$ids = $input['ids'] ?? [];
$newStatus = $input['status'] ?? '';
$validStatuses = ['uusi','kasittelyssa','odottaa','ratkaistu','suljettu'];
if (!in_array($newStatus, $validStatuses)) {
http_response_code(400);
echo json_encode(['error' => 'Virheellinen tila']);
break;
}
$tickets = dbLoadTickets($companyId);
$changed = 0;
foreach ($tickets as $t) {
if (in_array($t['id'], $ids)) {
$t['status'] = $newStatus;
$t['updated'] = date('Y-m-d H:i:s');
dbSaveTicket($companyId, $t);
$changed++;
}
}
dbAddLog($companyId, currentUser(), 'ticket_status', '', '', "Massapäivitys: $changed tikettiä → $newStatus");
echo json_encode(['success' => true, 'changed' => $changed]);
break;
case 'ticket_bulk_delete':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$ids = $input['ids'] ?? [];
$deleted = 0;
foreach ($ids as $ticketId) {
dbDeleteTicket($ticketId);
$deleted++;
}
dbAddLog($companyId, currentUser(), 'ticket_delete', '', '', "Massapoisto: $deleted tikettiä");
echo json_encode(['success' => true, 'deleted' => $deleted]);
break;
case 'ticket_rule_delete':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$ruleId = $input['id'] ?? '';
dbDeleteTicketRule($ruleId);
echo json_encode(['success' => true]);
break;
case 'ticket_priority':
requireAuth();
$companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$priority = $input['priority'] ?? 'normaali';
if (!in_array($priority, ['normaali', 'tärkeä', 'urgent'])) $priority = 'normaali';
$tickets = dbLoadTickets($companyId);
foreach ($tickets as $t) {
if ($t['id'] === $id) {
$t['priority'] = $priority;
$t['updated'] = date('Y-m-d H:i:s');
dbSaveTicket($companyId, $t);
// Telegram-hälytys urgentille
if ($priority === 'urgent') {
sendTelegramAlert($companyId, $t);
}
echo json_encode($t);
break 2;
}
}
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
break;
// ---------- VASTAUSPOHJAT ----------
case 'reply_templates':
requireAuth();
$companyId = requireCompany();
echo json_encode(dbLoadTemplates($companyId));
break;
case 'reply_template_save':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
if (empty($input['nimi']) || empty($input['body'])) {
http_response_code(400);
echo json_encode(['error' => 'Nimi ja sisältö vaaditaan']);
break;
}
$tpl = [
'id' => $input['id'] ?? generateId(),
'nimi' => trim($input['nimi']),
'body' => trim($input['body']),
'sort_order' => intval($input['sort_order'] ?? 0),
];
dbSaveTemplate($companyId, $tpl);
echo json_encode(['success' => true, 'template' => $tpl]);
break;
case 'reply_template_delete':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
dbDeleteTemplate($input['id'] ?? '');
echo json_encode(['success' => true]);
break;
// ---------- PRIORITY EMAILS ----------
case 'priority_emails':
requireAuth();
$companyId = requireCompany();
echo json_encode(dbLoadPriorityEmails($companyId));
break;
case 'priority_emails_save':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$emails = $input['emails'] ?? [];
// Poista vanhat ja lisää uudet
_dbExecute("DELETE FROM customer_priority_emails WHERE company_id = ?", [$companyId]);
foreach ($emails as $email) {
$email = strtolower(trim($email));
if ($email) {
_dbExecute("INSERT IGNORE INTO customer_priority_emails (company_id, email) VALUES (?, ?)", [$companyId, $email]);
}
}
echo json_encode(['success' => true]);
break;
// ---------- COMPANY MANAGEMENT ----------
case 'companies':
requireAuth();
$userCompanyIds = $_SESSION['companies'] ?? [];
$allCompanies = dbLoadCompanies();
$result = array_values(array_filter($allCompanies, fn($c) => in_array($c['id'], $userCompanyIds)));
echo json_encode($result);
break;
case 'companies_all':
requireAdmin();
$all = dbLoadCompanies();
if (isSuperAdmin()) {
echo json_encode($all);
} else {
// Yritysadmin näkee vain omat yrityksensä
$userCompanyIds = $_SESSION['companies'] ?? [];
$filtered = array_values(array_filter($all, fn($c) => in_array($c['id'], $userCompanyIds)));
echo json_encode($filtered);
}
break;
case 'all_mailboxes':
requireAuth();
// Palauttaa kaikki postilaatikot käyttäjän yrityksistä (allekirjoituksia varten)
$userCompanyIds = $_SESSION['companies'] ?? [];
$allCompanies = dbLoadCompanies();
$result = [];
foreach ($allCompanies as $comp) {
if (!in_array($comp['id'], $userCompanyIds)) continue;
$mailboxes = dbLoadMailboxes($comp['id']);
foreach ($mailboxes as $mb) {
$result[] = [
'id' => $mb['id'],
'nimi' => $mb['nimi'] ?? $mb['imap_user'] ?? '',
'company_id' => $comp['id'],
'company_nimi' => $comp['nimi'],
];
}
}
echo json_encode($result);
break;
case 'company_create':
requireSuperAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = preg_replace('/[^a-z0-9-]/', '', strtolower(trim($input['id'] ?? '')));
$nimi = trim($input['nimi'] ?? '');
if (empty($id) || empty($nimi)) {
http_response_code(400);
echo json_encode(['error' => 'ID ja nimi vaaditaan']);
break;
}
$companies = dbLoadCompanies();
foreach ($companies as $c) {
if ($c['id'] === $id) {
http_response_code(400);
echo json_encode(['error' => 'Yritys-ID on jo käytössä']);
break 2;
}
}
// Brändäyskentät
$domains = [];
if (isset($input['domains']) && is_array($input['domains'])) {
$domains = array_values(array_filter(array_map('trim', $input['domains'])));
}
$company = [
'id' => $id,
'nimi' => $nimi,
'domains' => $domains,
'primary_color' => trim($input['primary_color'] ?? '#0f3460'),
'subtitle' => trim($input['subtitle'] ?? ''),
'logo_file' => '',
'luotu' => date('Y-m-d H:i:s'),
'aktiivinen' => true,
];
dbSaveCompany($company);
// Luo hakemisto (tiedostoja varten)
$compDir = DATA_DIR . '/companies/' . $id;
if (!file_exists($compDir)) mkdir($compDir, 0755, true);
// Lisää luoja yrityksen käyttäjäksi
$u = dbGetUser($_SESSION['user_id']);
if ($u) {
$u['companies'] = array_unique(array_merge($u['companies'] ?? [], [$id]));
dbSaveUser($u);
$_SESSION['companies'] = $u['companies'];
}
echo json_encode($company);
break;
case 'company_update':
requireAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
// Yritysadmin saa muokata vain omia yrityksiään
if (!isSuperAdmin()) {
$userCompanyIds = $_SESSION['companies'] ?? [];
if (!in_array($id, $userCompanyIds)) {
http_response_code(403);
echo json_encode(['error' => 'Ei oikeuksia muokata tätä yritystä']);
break;
}
}
$companies = dbLoadCompanies();
$found = false;
foreach ($companies as $c) {
if ($c['id'] === $id) {
if (isset($input['nimi'])) $c['nimi'] = trim($input['nimi']);
if (isset($input['aktiivinen'])) $c['aktiivinen'] = (bool)$input['aktiivinen'];
if (isset($input['domains']) && is_array($input['domains'])) {
$c['domains'] = array_values(array_filter(array_map('trim', $input['domains'])));
}
if (isset($input['primary_color'])) $c['primary_color'] = trim($input['primary_color']);
if (isset($input['subtitle'])) $c['subtitle'] = trim($input['subtitle']);
if (isset($input['enabled_modules']) && is_array($input['enabled_modules'])) {
$c['enabled_modules'] = array_values($input['enabled_modules']);
}
dbSaveCompany($c);
$found = true;
echo json_encode($c);
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Yritystä ei löydy']);
}
break;
case 'company_delete':
requireSuperAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
if (empty($id) || !preg_match('/^[a-z0-9-]+$/', $id)) {
http_response_code(400);
echo json_encode(['error' => 'Virheellinen yritys-ID']);
break;
}
dbDeleteCompany($id);
// Poista yritys käyttäjiltä (CASCADE hoitaa tietokannan puolella, mutta päivitetään sessio)
$users = dbLoadUsers();
foreach ($users as $u) {
$u['companies'] = array_values(array_filter($u['companies'] ?? [], fn($c) => $c !== $id));
dbSaveUser($u);
}
echo json_encode(['success' => true]);
break;
case 'company_switch':
requireAuth();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$companyId = $input['company_id'] ?? '';
$userCompanies = $_SESSION['companies'] ?? [];
if (!in_array($companyId, $userCompanies)) {
http_response_code(403);
echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']);
break;
}
$_SESSION['company_id'] = $companyId;
echo json_encode(['success' => true, 'company_id' => $companyId]);
break;
case 'company_config':
requireAdmin();
$companyId = requireCompany();
echo json_encode(dbGetCompanyConfig($companyId));
break;
case 'company_config_update':
requireAdmin();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['mailboxes'])) {
// Delete all existing mailboxes and re-save
$existingMailboxes = dbLoadMailboxes($companyId);
foreach ($existingMailboxes as $existing) {
dbDeleteMailbox($existing['id']);
}
foreach ($input['mailboxes'] as $mb) {
if (empty($mb['id'])) $mb['id'] = generateId();
dbSaveMailbox($companyId, $mb);
}
}
if (isset($input['ticket_rules'])) {
// Delete all existing rules and re-save
$existingRules = dbLoadTicketRules($companyId);
foreach ($existingRules as $existing) {
dbDeleteTicketRule($existing['id']);
}
foreach ($input['ticket_rules'] as $rule) {
if (empty($rule['id'])) $rule['id'] = generateId();
dbSaveTicketRule($companyId, $rule);
}
}
echo json_encode(dbGetCompanyConfig($companyId));
break;
// ---------- MAILBOXES ----------
case 'mailboxes':
requireAuth();
$companyId = requireCompany();
$mailboxes = dbLoadMailboxes($companyId);
// Palauta postilaatikot ilman salasanoja
$mbs = array_map(function($mb) {
$mb['imap_password'] = !empty($mb['imap_password']) ? '********' : '';
return $mb;
}, $mailboxes);
echo json_encode($mbs);
break;
case 'mailbox_save':
requireAdmin();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$mb = [
'id' => $input['id'] ?? generateId(),
'nimi' => trim($input['nimi'] ?? ''),
'imap_host' => trim($input['imap_host'] ?? ''),
'imap_port' => intval($input['imap_port'] ?? 993),
'imap_user' => trim($input['imap_user'] ?? ''),
'imap_encryption' => trim($input['imap_encryption'] ?? 'ssl'),
'smtp_from_email' => trim($input['smtp_from_email'] ?? ''),
'smtp_from_name' => trim($input['smtp_from_name'] ?? ''),
'aktiivinen' => $input['aktiivinen'] ?? true,
];
// Salasana: jos ******** -> pidä vanha, muuten päivitä
if (isset($input['imap_password']) && $input['imap_password'] !== '********') {
$mb['imap_password'] = $input['imap_password'];
} else {
// Hae vanha salasana
$existingMb = dbGetMailbox($mb['id']);
$mb['imap_password'] = $existingMb ? ($existingMb['imap_password'] ?? '') : '';
}
if (empty($mb['nimi'])) {
http_response_code(400);
echo json_encode(['error' => 'Postilaatikon nimi puuttuu']);
break;
}
dbSaveMailbox($companyId, $mb);
dbAddLog($companyId, currentUser(), 'mailbox_save', '', '', 'Postilaatikko: ' . $mb['nimi']);
// Palauta ilman salasanaa
$mb['imap_password'] = '********';
echo json_encode($mb);
break;
case 'mailbox_delete':
requireAdmin();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$mbId = $input['id'] ?? '';
dbDeleteMailbox($mbId);
echo json_encode(['success' => true]);
break;
default:
http_response_code(404);
echo json_encode(['error' => 'Tuntematon toiminto']);
break;
}