- Reverse DNS -haku tallentaa hostnamen IP:n rinnalle (paljastaa operaattorin ja alueen, esim. dsl-hel-123.elisa.fi) - Duplikaattikyselyn (sama osoite+postinumero+kaupunki) ei tallenneta uudelleen samalle yritykselle - IP/hostname -sarake lisätty taulukkoon Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5455 lines
225 KiB
PHP
5455 lines
225 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 HUB');
|
|
|
|
// 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();
|
|
if (!isCompanyAdmin()) {
|
|
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 {
|
|
if (($_SESSION['role'] ?? '') === 'superadmin') return true;
|
|
return ($_SESSION['company_role'] ?? '') === 'admin';
|
|
}
|
|
|
|
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'] ?? [];
|
|
$isSuperadmin = ($_SESSION['role'] ?? '') === 'superadmin';
|
|
if (!$isSuperadmin && !in_array($companyId, $userCompanies)) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']);
|
|
exit;
|
|
}
|
|
// IP-rajoitus: tarkista vasta kun yrityksen dataa käytetään (superadmin ohittaa)
|
|
if (($_SESSION['role'] ?? '') !== 'superadmin') {
|
|
$allCompanies = dbLoadCompanies();
|
|
foreach ($allCompanies as $comp) {
|
|
if ($comp['id'] === $companyId) {
|
|
if (!isIpAllowed(getClientIp(), $comp['allowed_ips'] ?? '')) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'IP-osoitteesi ei ole sallittu tälle yritykselle.']);
|
|
exit;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
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 {
|
|
$xff = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
|
|
if ($xff) {
|
|
// X-Forwarded-For voi sisältää useita IP:itä: "client, proxy1, proxy2" — otetaan ensimmäinen
|
|
$parts = explode(',', $xff);
|
|
$ip = trim($parts[0]);
|
|
if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
|
|
}
|
|
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
|
}
|
|
|
|
/**
|
|
* Tarkista onko IP sallittujen listalla.
|
|
* Tyhjä lista = ei rajoitusta (kaikki sallittu).
|
|
* Tukee IPv4 ja IPv6, yksittäisiä osoitteita ja CIDR-alueita.
|
|
*/
|
|
function isIpAllowed(string $ip, string $allowedIps): bool {
|
|
$allowedIps = trim($allowedIps);
|
|
if ($allowedIps === '' || strtolower($allowedIps) === 'kaikki') return true;
|
|
$entries = preg_split('/[\s,]+/', $allowedIps, -1, PREG_SPLIT_NO_EMPTY);
|
|
|
|
// Normalisoi IP: IPv4-mapped IPv6 (::ffff:1.2.3.4) → myös IPv4 muotoon
|
|
$ipBin = @inet_pton($ip);
|
|
if ($ipBin === false) return false;
|
|
|
|
// Jos IP on IPv4-mapped IPv6 (::ffff:x.x.x.x), kokeile myös puhtaana IPv4:nä
|
|
$ipv4Equivalent = null;
|
|
if (strlen($ipBin) === 16 && substr($ipBin, 0, 12) === "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff") {
|
|
$ipv4Equivalent = inet_ntop(substr($ipBin, 12));
|
|
}
|
|
// Jos IP on puhdas IPv4, kokeile myös mapped-muodossa
|
|
$ipv6MappedBin = null;
|
|
if (strlen($ipBin) === 4) {
|
|
$ipv6MappedBin = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" . $ipBin;
|
|
}
|
|
|
|
foreach ($entries as $entry) {
|
|
$entry = trim($entry);
|
|
if ($entry === '') continue;
|
|
if (strpos($entry, '/') !== false) {
|
|
// CIDR-alue (IPv4 tai IPv6)
|
|
[$subnet, $bits] = explode('/', $entry, 2);
|
|
$bits = (int)$bits;
|
|
$subnetBin = @inet_pton($subnet);
|
|
if ($subnetBin === false) continue;
|
|
$maxBits = strlen($subnetBin) * 8;
|
|
if ($bits < 0 || $bits > $maxBits) continue;
|
|
// Rakenna bittimask
|
|
$mask = str_repeat("\xff", intdiv($bits, 8));
|
|
if ($bits % 8) $mask .= chr(0xff << (8 - ($bits % 8)));
|
|
$mask = str_pad($mask, strlen($subnetBin), "\x00");
|
|
|
|
// Tarkista suoraan
|
|
if (strlen($ipBin) === strlen($subnetBin) && ($ipBin & $mask) === ($subnetBin & $mask)) return true;
|
|
// Tarkista IPv4-equivalent
|
|
if ($ipv4Equivalent) {
|
|
$ipv4Bin = @inet_pton($ipv4Equivalent);
|
|
if ($ipv4Bin && strlen($ipv4Bin) === strlen($subnetBin) && ($ipv4Bin & $mask) === ($subnetBin & $mask)) return true;
|
|
}
|
|
// Tarkista IPv6-mapped
|
|
if ($ipv6MappedBin && strlen($ipv6MappedBin) === strlen($subnetBin) && ($ipv6MappedBin & $mask) === ($subnetBin & $mask)) return true;
|
|
} else {
|
|
// Yksittäinen IP (IPv4 tai IPv6)
|
|
$entryBin = @inet_pton($entry);
|
|
if ($entryBin === false) continue;
|
|
if ($ipBin === $entryBin) return true;
|
|
// Tarkista IPv4-equivalent
|
|
if ($ipv4Equivalent) {
|
|
$ipv4Bin = @inet_pton($ipv4Equivalent);
|
|
if ($ipv4Bin && $ipv4Bin === $entryBin) return true;
|
|
}
|
|
// Tarkista IPv6-mapped
|
|
if ($ipv6MappedBin && $ipv6MappedBin === $entryBin) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// ==================== ZAMMAD CLIENT ====================
|
|
|
|
class ZammadClient {
|
|
private string $url;
|
|
private string $token;
|
|
|
|
public function __construct(string $url, string $token) {
|
|
$url = rtrim($url, '/');
|
|
// Lisää https:// jos protokolla puuttuu
|
|
if (!preg_match('#^https?://#i', $url)) {
|
|
$url = 'https://' . $url;
|
|
}
|
|
$this->url = $url;
|
|
$this->token = $token;
|
|
}
|
|
|
|
private function request(string $method, string $endpoint, ?array $data = null): array {
|
|
$url = $this->url . '/api/v1/' . ltrim($endpoint, '/');
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 30,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Authorization: Token token=' . $this->token,
|
|
'Content-Type: application/json',
|
|
],
|
|
]);
|
|
if ($method === 'POST') {
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
|
} elseif ($method === 'PUT') {
|
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
|
|
if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
|
}
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($httpCode >= 400) {
|
|
$err = json_decode($response, true);
|
|
throw new \RuntimeException('Zammad API error (' . $httpCode . '): ' . ($err['error'] ?? $response));
|
|
}
|
|
return json_decode($response, true) ?: [];
|
|
}
|
|
|
|
/** Hae tikettejä (search API palauttaa kaikki joihin on oikeus) */
|
|
public function getTickets(array $groupIds = [], int $page = 1, int $perPage = 100, ?string $updatedSince = null): array {
|
|
if (!empty($groupIds)) {
|
|
$parts = array_map(fn($id) => 'group_id:' . $id, $groupIds);
|
|
$query = '(' . implode(' OR ', $parts) . ')';
|
|
} else {
|
|
$query = '*';
|
|
}
|
|
if ($updatedSince) {
|
|
$query .= ' AND updated_at:>=' . $updatedSince;
|
|
}
|
|
return $this->request('GET', 'tickets/search?query=' . urlencode($query) . '&per_page=' . $perPage . '&page=' . $page . '&expand=true');
|
|
}
|
|
|
|
/** Hae yksittäinen tiketti */
|
|
public function getTicket(int $id): array {
|
|
return $this->request('GET', 'tickets/' . $id . '?expand=true');
|
|
}
|
|
|
|
/** Hae tiketin artikkelit */
|
|
public function getArticles(int $ticketId): array {
|
|
return $this->request('GET', 'ticket_articles/by_ticket/' . $ticketId . '?expand=true');
|
|
}
|
|
|
|
/** Lähetä vastaus tikettiin */
|
|
public function createArticle(int $ticketId, string $body, string $to = '', string $subject = '', string $type = 'email', string $cc = ''): array {
|
|
// Muunna plain-text HTML:ksi jos body ei sisällä HTML-tageja
|
|
$htmlBody = $body;
|
|
if (strip_tags($body) === $body) {
|
|
$htmlBody = nl2br(htmlspecialchars($body, ENT_QUOTES, 'UTF-8'));
|
|
}
|
|
$data = [
|
|
'ticket_id' => $ticketId,
|
|
'body' => $htmlBody,
|
|
'content_type' => 'text/html',
|
|
'type' => $type,
|
|
'subtype' => 'reply',
|
|
'internal' => false,
|
|
'sender' => 'Agent',
|
|
];
|
|
if ($to) $data['to'] = $to;
|
|
if ($cc) $data['cc'] = $cc;
|
|
if ($subject) $data['subject'] = 'Re: ' . preg_replace('/^Re:\s*/i', '', $subject);
|
|
return $this->request('POST', 'ticket_articles', $data);
|
|
}
|
|
|
|
/** Päivitä tiketin tila */
|
|
public function updateTicket(int $ticketId, array $fields): array {
|
|
return $this->request('PUT', 'tickets/' . $ticketId, $fields);
|
|
}
|
|
|
|
/** Hae ryhmät */
|
|
public function getGroups(): array {
|
|
return $this->request('GET', 'groups?expand=true');
|
|
}
|
|
|
|
/** Testaa yhteys */
|
|
public function testConnection(): array {
|
|
$user = $this->request('GET', 'users/me');
|
|
$groups = $this->getGroups();
|
|
return ['user' => $user['login'] ?? '?', 'groups' => count($groups), 'ok' => true];
|
|
}
|
|
}
|
|
|
|
// ==================== 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 To
|
|
$toRaw = $this->decodeMimeHeader($headers['to'] ?? '');
|
|
$toEmails = $this->parseCcAddresses($toRaw);
|
|
|
|
// 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']),
|
|
'to' => $toEmails,
|
|
'message_id' => $messageId,
|
|
'in_reply_to' => $inReplyTo,
|
|
'references' => $references,
|
|
'date' => $date,
|
|
'body' => $body,
|
|
'cc' => $ccEmails,
|
|
];
|
|
}
|
|
|
|
private function fetchBody(int $num): string {
|
|
// Hae BODYSTRUCTURE rakenteen selvittämiseksi
|
|
$resp = $this->command("FETCH {$num} BODYSTRUCTURE");
|
|
$struct = implode(' ', $resp);
|
|
error_log("IMAP BODYSTRUCTURE msg#{$num}: " . substr($struct, 0, 500));
|
|
|
|
// Etsi text/plain osa ja sen koodaus BODYSTRUCTURE:sta
|
|
// Joustava regex: param=(list|NIL), body-id=(NIL|"str"), body-desc=(NIL|"str"), encoding="str"
|
|
$pParam = '(?:\([^)]*\)|NIL)';
|
|
$pNStr = '(?:NIL|"[^"]*")';
|
|
$plainRx = '/"TEXT"\s+"PLAIN"\s+' . $pParam . '\s+' . $pNStr . '\s+' . $pNStr . '\s+"([^"]+)"/i';
|
|
$htmlRx = '/"TEXT"\s+"HTML"\s+' . $pParam . '\s+' . $pNStr . '\s+' . $pNStr . '\s+"([^"]+)"/i';
|
|
|
|
$plainEncoding = '';
|
|
$htmlEncoding = '';
|
|
if (preg_match($plainRx, $struct, $em)) $plainEncoding = strtoupper($em[1]);
|
|
if (preg_match($htmlRx, $struct, $em)) $htmlEncoding = strtoupper($em[1]);
|
|
|
|
// Charset text/plain -osasta
|
|
$charset = 'utf-8';
|
|
if (preg_match('/"TEXT"\s+"PLAIN"\s+\([^)]*"CHARSET"\s+"([^"]+)"/i', $struct, $cm)) {
|
|
$charset = strtolower($cm[1]);
|
|
} elseif (preg_match('/charset[="\s]+([^\s;"\\)]+)/i', $struct, $cm)) {
|
|
$charset = strtolower(trim($cm[1], '"'));
|
|
}
|
|
|
|
// Päättele oikea section-numero BODYSTRUCTURE:n rakenteesta
|
|
// Yksiosainen: BODYSTRUCTURE ("TEXT" "PLAIN" ...) → BODY[TEXT]
|
|
// Multipart: BODYSTRUCTURE (("TEXT" "PLAIN" ...) ...) → BODY[1]
|
|
// Sisäkkäinen: BODYSTRUCTURE ((("TEXT" "PLAIN" ...) ...) ...) → BODY[1.1]
|
|
$sections = [];
|
|
$plainPos = stripos($struct, '"TEXT" "PLAIN"');
|
|
if ($plainPos !== false) {
|
|
$bsPos = stripos($struct, 'BODYSTRUCTURE');
|
|
$after = ($bsPos !== false) ? substr($struct, $bsPos + 13) : $struct;
|
|
$plainInAfter = stripos($after, '"TEXT" "PLAIN"');
|
|
if ($plainInAfter !== false) {
|
|
$beforePlain = substr($after, 0, $plainInAfter);
|
|
$depth = substr_count($beforePlain, '(') - substr_count($beforePlain, ')');
|
|
if ($depth <= 1) {
|
|
$sections[] = 'TEXT'; // yksiosainen viesti
|
|
} elseif ($depth === 2) {
|
|
$sections[] = '1'; // suora lapsi multipartissa
|
|
} elseif ($depth >= 3) {
|
|
$sections[] = '1.1'; // sisäkkäinen multipart
|
|
}
|
|
}
|
|
}
|
|
// Lisää fallbackit
|
|
foreach (['1', '1.1', 'TEXT'] as $fb) {
|
|
if (!in_array($fb, $sections)) $sections[] = $fb;
|
|
}
|
|
|
|
error_log("IMAP sections to try for msg#{$num}: " . implode(', ', $sections) . " | plainEnc={$plainEncoding} htmlEnc={$htmlEncoding} charset={$charset}");
|
|
|
|
// Kokeile osioita järjestyksessä
|
|
$body = '';
|
|
$usedSection = '';
|
|
foreach ($sections as $sec) {
|
|
$resp = $this->command("FETCH {$num} BODY.PEEK[{$sec}]");
|
|
$data = $this->extractLiteral($resp);
|
|
if ($data && strlen(trim($data)) > 0) {
|
|
$body = $data;
|
|
$usedSection = $sec;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$body) return '';
|
|
|
|
// Päättele käytettävä koodaus
|
|
$encoding = $plainEncoding;
|
|
// Jos BODYSTRUCTURE ei löytänyt text/plain koodausta, kokeile raakaa hakua
|
|
if (!$encoding) {
|
|
if (preg_match('/"(BASE64|QUOTED-PRINTABLE|7BIT|8BIT)"/i', $struct, $em)) {
|
|
$encoding = strtoupper($em[1]);
|
|
}
|
|
}
|
|
|
|
error_log("IMAP body msg#{$num}: section={$usedSection} encoding={$encoding} bodyLen=" . strlen($body) . " first100=" . substr($body, 0, 100));
|
|
|
|
// Dekoodaa sisältö
|
|
if ($encoding === 'BASE64') {
|
|
$decoded = @base64_decode($body);
|
|
if ($decoded !== false) $body = $decoded;
|
|
} elseif ($encoding === 'QUOTED-PRINTABLE') {
|
|
$body = quoted_printable_decode($body);
|
|
}
|
|
|
|
// Jos koodausta ei tunnistettu, yritä automaattinen tunnistus
|
|
if (!$encoding || $encoding === '7BIT' || $encoding === '8BIT') {
|
|
$trimmed = trim($body);
|
|
// Tarkista näyttääkö base64:ltä
|
|
if (preg_match('/^[A-Za-z0-9+\/=\s]+$/', $trimmed) && strlen($trimmed) > 50) {
|
|
$decoded = @base64_decode($trimmed, true);
|
|
if ($decoded !== false && strlen($decoded) > 0 && preg_match('/[\x20-\x7E\xC0-\xFF]/', $decoded)) {
|
|
$body = $decoded;
|
|
}
|
|
}
|
|
// Tarkista näyttääkö quoted-printable:lta (sisältää =XX koodeja)
|
|
elseif (preg_match('/=[0-9A-Fa-f]{2}/', $body) && substr_count($body, '=') > 3) {
|
|
$decoded = quoted_printable_decode($body);
|
|
if (strlen($decoded) < strlen($body)) {
|
|
$body = $decoded;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Jos body sisältää multipart-rajoja (haettiin väärä osio), yritä parsia plain text
|
|
if (preg_match('/^--[^\r\n]+\r?\n/m', $body) && preg_match('/Content-Type:/i', $body)) {
|
|
error_log("IMAP msg#{$num}: body contains MIME boundaries, trying to extract text/plain");
|
|
$extracted = $this->extractPlainFromMultipart($body);
|
|
if ($extracted) $body = $extracted;
|
|
}
|
|
|
|
// Riisu HTML jos sisältö on HTML:ää
|
|
if (preg_match('/<html|<body|<div|<p\b/i', $body)) {
|
|
// Yritä ensin hakea text/html -osan koodaus
|
|
if (!$plainEncoding && $htmlEncoding === 'BASE64') {
|
|
// Body saattaa olla vielä base64-koodattua HTML:ää
|
|
$decoded = @base64_decode($body);
|
|
if ($decoded !== false && preg_match('/<html|<body|<div|<p\b/i', $decoded)) {
|
|
$body = $decoded;
|
|
}
|
|
}
|
|
$body = strip_tags($body);
|
|
$body = preg_replace('/\n{3,}/', "\n\n", $body);
|
|
$body = preg_replace('/[ \t]+\n/', "\n", $body);
|
|
}
|
|
|
|
// Charset-muunnos
|
|
if ($charset && $charset !== 'utf-8') {
|
|
$converted = @iconv($charset, 'UTF-8//IGNORE', $body);
|
|
if ($converted !== false) $body = $converted;
|
|
}
|
|
|
|
return trim($body);
|
|
}
|
|
|
|
/**
|
|
* Jos fetchBody palautti raakaa multipart-dataa, yritetään parsia text/plain -osa siitä.
|
|
*/
|
|
private function extractPlainFromMultipart(string $raw): string {
|
|
// Etsi boundary
|
|
if (!preg_match('/^--([^\r\n]+)/m', $raw, $bm)) return '';
|
|
$boundary = $bm[1];
|
|
$parts = explode('--' . $boundary, $raw);
|
|
|
|
foreach ($parts as $part) {
|
|
$part = trim($part);
|
|
if (!$part || $part === '--') continue;
|
|
// Etsi Content-Type header
|
|
if (preg_match('/Content-Type:\s*text\/plain/i', $part)) {
|
|
// Erota headerit ja body
|
|
$split = preg_split('/\r?\n\r?\n/', $part, 2);
|
|
if (count($split) < 2) continue;
|
|
$headers = $split[0];
|
|
$body = $split[1];
|
|
// Tarkista Transfer-Encoding
|
|
if (preg_match('/Content-Transfer-Encoding:\s*base64/i', $headers)) {
|
|
$body = base64_decode($body);
|
|
} elseif (preg_match('/Content-Transfer-Encoding:\s*quoted-printable/i', $headers)) {
|
|
$body = quoted_printable_decode($body);
|
|
}
|
|
// Charset
|
|
if (preg_match('/charset[="\s]+([^\s;"\\)]+)/i', $headers, $cm)) {
|
|
$cs = strtolower(trim($cm[1], '"'));
|
|
if ($cs && $cs !== 'utf-8') {
|
|
$converted = @iconv($cs, 'UTF-8//IGNORE', $body);
|
|
if ($converted !== false) $body = $converted;
|
|
}
|
|
}
|
|
return trim($body);
|
|
}
|
|
}
|
|
|
|
// Jos text/plain ei löydy, yritä text/html ja riisu tagit
|
|
foreach ($parts as $part) {
|
|
$part = trim($part);
|
|
if (!$part || $part === '--') continue;
|
|
if (preg_match('/Content-Type:\s*text\/html/i', $part)) {
|
|
$split = preg_split('/\r?\n\r?\n/', $part, 2);
|
|
if (count($split) < 2) continue;
|
|
$headers = $split[0];
|
|
$body = $split[1];
|
|
if (preg_match('/Content-Transfer-Encoding:\s*base64/i', $headers)) {
|
|
$body = base64_decode($body);
|
|
} elseif (preg_match('/Content-Transfer-Encoding:\s*quoted-printable/i', $headers)) {
|
|
$body = quoted_printable_decode($body);
|
|
}
|
|
if (preg_match('/charset[="\s]+([^\s;"\\)]+)/i', $headers, $cm)) {
|
|
$cs = strtolower(trim($cm[1], '"'));
|
|
if ($cs && $cs !== 'utf-8') {
|
|
$converted = @iconv($cs, 'UTF-8//IGNORE', $body);
|
|
if ($converted !== false) $body = $converted;
|
|
}
|
|
}
|
|
$body = strip_tags($body);
|
|
$body = preg_replace('/\n{3,}/', "\n\n", $body);
|
|
return trim($body);
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Generoi oletusallekirjoitukset käyttäjälle niille mailboxeille joille ei ole omaa.
|
|
* Palauttaa yhdistetyn allekirjoitukset-arrayn (omat + generoitut oletukset).
|
|
*/
|
|
function buildSignaturesWithDefaults(array $user, array $userCompanyIds): array {
|
|
$sigs = $user['signatures'] ?? [];
|
|
$allCompanies = dbLoadCompanies();
|
|
foreach ($allCompanies as $comp) {
|
|
if (!in_array($comp['id'], $userCompanyIds)) continue;
|
|
$mailboxes = dbLoadMailboxes($comp['id']);
|
|
foreach ($mailboxes as $mb) {
|
|
if (!empty($sigs[$mb['id']])) continue; // käyttäjällä on jo oma allekirjoitus
|
|
// Generoi oletus: Etunimi \n Yritys \n sähköposti
|
|
$etunimi = trim(explode(' ', $user['nimi'] ?? '')[0]);
|
|
$yritys = ($comp['nimi'] ?? '') . ' Oy';
|
|
$email = $mb['smtp_from_email'] ?? $mb['imap_user'] ?? '';
|
|
$parts = array_filter([$etunimi, $yritys, $email]);
|
|
if (!empty($parts)) {
|
|
$sigs[$mb['id']] = implode("\n", $parts);
|
|
}
|
|
}
|
|
}
|
|
return $sigs;
|
|
}
|
|
|
|
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';
|
|
|
|
// Jos mailboxilla on SMTP-asetukset, käytä SMTP:tä
|
|
$smtpHost = $mailbox['smtp_host'] ?? '';
|
|
error_log("MAIL DEBUG: to={$to} smtpHost={$smtpHost} from={$fromEmail} mailbox_keys=" . implode(',', array_keys($mailbox ?? [])));
|
|
if ($smtpHost !== '') {
|
|
return sendViaSMTP($to, $subject, $body, $fromEmail, $fromName, $inReplyTo, $references, $mailbox, $cc);
|
|
}
|
|
|
|
// Fallback: PHP mail()
|
|
$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);
|
|
}
|
|
|
|
/** @var string|null Viimeisin SMTP-virhe (palautetaan frontendille) */
|
|
$GLOBALS['smtp_last_error'] = null;
|
|
|
|
function smtpReadResponse($fp): string {
|
|
$full = '';
|
|
while ($line = @fgets($fp, 512)) {
|
|
$full .= $line;
|
|
// Viimeinen rivi: koodi + välilyönti (ei -)
|
|
if (isset($line[3]) && $line[3] !== '-') break;
|
|
// Timeout / EOF
|
|
if ($line === false) break;
|
|
}
|
|
return $full;
|
|
}
|
|
|
|
function smtpCommand($fp, string $cmd): string {
|
|
fwrite($fp, $cmd . "\r\n");
|
|
return smtpReadResponse($fp);
|
|
}
|
|
|
|
function smtpCode(string $resp): string {
|
|
return substr(trim($resp), 0, 3);
|
|
}
|
|
|
|
function sendViaSMTP(string $to, string $subject, string $body, string $fromEmail, string $fromName, string $inReplyTo, string $references, array $mailbox, string $cc): bool {
|
|
$host = $mailbox['smtp_host'];
|
|
$port = (int)($mailbox['smtp_port'] ?? 587);
|
|
// Fallback-ketju käyttäjälle: smtp_user → imap_user → smtp_from_email
|
|
$user = $mailbox['smtp_user'] ?? '';
|
|
if ($user === '') $user = $mailbox['imap_user'] ?? '';
|
|
if ($user === '') $user = $fromEmail;
|
|
// Fallback salasanalle: smtp_password → imap_password
|
|
$pass = $mailbox['smtp_password'] ?? '';
|
|
if ($pass === '') $pass = $mailbox['imap_password'] ?? '';
|
|
$encryption = $mailbox['smtp_encryption'] ?? 'tls';
|
|
|
|
$log = []; // Debug-loki
|
|
|
|
$fail = function(string $step, string $detail) use (&$fp, &$log) {
|
|
$log[] = "FAIL @ {$step}: {$detail}";
|
|
$msg = "SMTP {$step}: {$detail} | log: " . implode(' → ', $log);
|
|
error_log($msg);
|
|
$GLOBALS['smtp_last_error'] = "SMTP {$step}: {$detail}";
|
|
if (isset($fp) && is_resource($fp)) fclose($fp);
|
|
return false;
|
|
};
|
|
|
|
// 1. Yhteys
|
|
$timeout = 15;
|
|
$errno = 0; $errstr = '';
|
|
$connStr = ($encryption === 'ssl' ? "ssl" : "tcp") . "://{$host}:{$port}";
|
|
$log[] = "connect {$connStr}";
|
|
|
|
$ctx = stream_context_create(['ssl' => [
|
|
'verify_peer' => false,
|
|
'verify_peer_name' => false,
|
|
'allow_self_signed' => true,
|
|
]]);
|
|
$fp = @stream_socket_client($connStr, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $ctx);
|
|
if (!$fp) return $fail('connect', "{$errstr} ({$errno})");
|
|
stream_set_timeout($fp, $timeout);
|
|
|
|
// 2. Banner
|
|
$resp = smtpReadResponse($fp);
|
|
$log[] = "banner:" . smtpCode($resp);
|
|
if (smtpCode($resp) !== '220') return $fail('banner', trim($resp));
|
|
|
|
// 3. EHLO
|
|
$ehlo = smtpCommand($fp, "EHLO " . gethostname());
|
|
$log[] = "ehlo:" . smtpCode($ehlo);
|
|
if (smtpCode($ehlo) !== '250') return $fail('EHLO', trim($ehlo));
|
|
|
|
// 4. STARTTLS
|
|
if ($encryption === 'tls') {
|
|
$resp = smtpCommand($fp, "STARTTLS");
|
|
$log[] = "starttls:" . smtpCode($resp);
|
|
if (smtpCode($resp) !== '220') return $fail('STARTTLS', trim($resp));
|
|
$crypto = @stream_socket_enable_crypto($fp, true,
|
|
STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT);
|
|
if (!$crypto) return $fail('TLS', 'negotiation failed');
|
|
$log[] = "tls:ok";
|
|
// EHLO opnieuw na TLS
|
|
$ehlo = smtpCommand($fp, "EHLO " . gethostname());
|
|
$log[] = "ehlo2:" . smtpCode($ehlo);
|
|
}
|
|
|
|
// 5. AUTH — probeer eerst PLAIN, dan LOGIN
|
|
if ($user !== '') {
|
|
$authOk = false;
|
|
|
|
// AUTH PLAIN
|
|
$cred = base64_encode("\0{$user}\0{$pass}");
|
|
$resp = smtpCommand($fp, "AUTH PLAIN {$cred}");
|
|
$log[] = "auth_plain:" . smtpCode($resp);
|
|
if (smtpCode($resp) === '235') {
|
|
$authOk = true;
|
|
} else {
|
|
// AUTH LOGIN fallback
|
|
$resp = smtpCommand($fp, "AUTH LOGIN");
|
|
$log[] = "auth_login:" . smtpCode($resp);
|
|
if (smtpCode($resp) === '334') {
|
|
$resp = smtpCommand($fp, base64_encode($user));
|
|
$log[] = "auth_user:" . smtpCode($resp);
|
|
if (smtpCode($resp) === '334') {
|
|
$resp = smtpCommand($fp, base64_encode($pass));
|
|
$log[] = "auth_pass:" . smtpCode($resp);
|
|
if (smtpCode($resp) === '235') {
|
|
$authOk = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$authOk) {
|
|
$passHint = strlen($pass) > 0 ? strlen($pass) . ' chars' : 'EMPTY';
|
|
return $fail('AUTH', trim($resp) . " (user={$user}, pass={$passHint})");
|
|
}
|
|
$log[] = "auth:ok";
|
|
}
|
|
|
|
// 6. MAIL FROM
|
|
$resp = smtpCommand($fp, "MAIL FROM:<{$fromEmail}>");
|
|
$log[] = "from:" . smtpCode($resp);
|
|
if (smtpCode($resp) !== '250') return $fail('MAIL FROM', trim($resp));
|
|
|
|
// 7. RCPT TO
|
|
$allRecipients = array_filter(array_map('trim', explode(',', $to)));
|
|
if ($cc) $allRecipients = array_merge($allRecipients, array_filter(array_map('trim', explode(',', $cc))));
|
|
foreach ($allRecipients as $rcpt) {
|
|
$resp = smtpCommand($fp, "RCPT TO:<{$rcpt}>");
|
|
$log[] = "rcpt:" . smtpCode($resp);
|
|
if (!in_array(smtpCode($resp), ['250', '251'])) return $fail('RCPT TO', trim($resp) . " ({$rcpt})");
|
|
}
|
|
|
|
// 8. DATA
|
|
$resp = smtpCommand($fp, "DATA");
|
|
$log[] = "data:" . smtpCode($resp);
|
|
if (smtpCode($resp) !== '354') return $fail('DATA', trim($resp));
|
|
|
|
// 9. Viesti
|
|
$messageId = '<' . uniqid('msg_', true) . '@' . (explode('@', $fromEmail)[1] ?? 'localhost') . '>';
|
|
$msg = "From: {$fromName} <{$fromEmail}>\r\n";
|
|
$msg .= "To: {$to}\r\n";
|
|
if ($cc) $msg .= "Cc: {$cc}\r\n";
|
|
$msg .= "Subject: =?UTF-8?B?" . base64_encode($subject) . "?=\r\n";
|
|
$msg .= "Message-ID: {$messageId}\r\n";
|
|
if ($inReplyTo) {
|
|
$msg .= "In-Reply-To: {$inReplyTo}\r\n";
|
|
$msg .= "References: " . ($references ? $references . ' ' : '') . $inReplyTo . "\r\n";
|
|
}
|
|
$msg .= "MIME-Version: 1.0\r\n";
|
|
$msg .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
|
$msg .= "Content-Transfer-Encoding: base64\r\n";
|
|
$msg .= "Date: " . date('r') . "\r\n";
|
|
$msg .= "\r\n";
|
|
$msg .= chunk_split(base64_encode($body));
|
|
// Lopeta piste omalla rivillä
|
|
fwrite($fp, $msg . "\r\n.\r\n");
|
|
$resp = smtpReadResponse($fp);
|
|
$log[] = "send:" . smtpCode($resp);
|
|
if (smtpCode($resp) !== '250') return $fail('send', trim($resp));
|
|
|
|
// QUIT
|
|
fwrite($fp, "QUIT\r\n");
|
|
fclose($fp);
|
|
error_log("SMTP OK: " . implode(' → ', $log));
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tallenna kysely tietokantaan (ohita duplikaatit: sama osoite+postinumero+kaupunki+yritys)
|
|
try {
|
|
$rawOsoite = $_GET['osoite'] ?? '';
|
|
$rawPostinumero = $_GET['postinumero'] ?? '';
|
|
$rawKaupunki = $_GET['kaupunki'] ?? '';
|
|
$exists = _dbFetchScalar(
|
|
"SELECT COUNT(*) FROM availability_queries WHERE company_id = ? AND LOWER(osoite) = LOWER(?) AND postinumero = ? AND LOWER(kaupunki) = LOWER(?)",
|
|
[$matchedCompany['id'], $rawOsoite, $rawPostinumero, $rawKaupunki]
|
|
);
|
|
if (!$exists) {
|
|
$ip = getClientIp();
|
|
$hostname = @gethostbyaddr($ip) ?: '';
|
|
if ($hostname === $ip) $hostname = ''; // gethostbyaddr palauttaa IP:n jos ei löydy
|
|
_dbExecute(
|
|
"INSERT INTO availability_queries (company_id, osoite, postinumero, kaupunki, saatavilla, ip_address, hostname, user_agent, referer, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
[
|
|
$matchedCompany['id'],
|
|
$rawOsoite,
|
|
$rawPostinumero,
|
|
$rawKaupunki,
|
|
$found ? 1 : 0,
|
|
$ip,
|
|
$hostname,
|
|
substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 500),
|
|
substr($_SERVER['HTTP_REFERER'] ?? '', 0, 500),
|
|
date('Y-m-d H:i:s'),
|
|
]
|
|
);
|
|
}
|
|
} catch (\Throwable $e) { /* logitus ei saa kaataa API-vastausta */ }
|
|
|
|
echo json_encode(['saatavilla' => $found]);
|
|
break;
|
|
|
|
// ---------- SAATAVUUSKYSELYT ----------
|
|
case 'availability_queries':
|
|
requireAuth();
|
|
$limit = (int)($_GET['limit'] ?? 100);
|
|
$offset = (int)($_GET['offset'] ?? 0);
|
|
if ($limit > 500) $limit = 500;
|
|
|
|
// Näytä kaikkien käyttäjän yritysten kyselyt
|
|
$userCompanyIds = $_SESSION['companies'] ?? [];
|
|
if (empty($userCompanyIds)) {
|
|
echo json_encode(['total' => 0, 'queries' => []]);
|
|
break;
|
|
}
|
|
$placeholders = implode(',', array_fill(0, count($userCompanyIds), '?'));
|
|
$total = (int)_dbFetchScalar("SELECT COUNT(*) FROM availability_queries WHERE company_id IN ($placeholders)", $userCompanyIds);
|
|
$params = array_merge($userCompanyIds, [$limit, $offset]);
|
|
$rows = _dbFetchAll(
|
|
"SELECT aq.id, aq.company_id, c.nimi as company_nimi, aq.osoite, aq.postinumero, aq.kaupunki, aq.saatavilla, aq.ip_address, aq.hostname, aq.referer, aq.created_at
|
|
FROM availability_queries aq LEFT JOIN companies c ON c.id = aq.company_id
|
|
WHERE aq.company_id IN ($placeholders) ORDER BY aq.created_at DESC LIMIT ? OFFSET ?",
|
|
$params
|
|
);
|
|
echo json_encode(['total' => $total, 'queries' => $rows]);
|
|
break;
|
|
|
|
// ---------- CONFIG (admin, yrityskohtainen) ----------
|
|
case 'config':
|
|
requireAuth();
|
|
$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 HUB 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;
|
|
}
|
|
$allCompanies = dbLoadCompanies();
|
|
|
|
// IP-rajoitus: selvitä mihin yrityksiin on pääsy (superadmin ohittaa)
|
|
$allowedCompanyIds = $userCompanies;
|
|
if ($u['role'] !== 'superadmin') {
|
|
$allowedCompanyIds = [];
|
|
foreach ($allCompanies as $comp) {
|
|
if (in_array($comp['id'], $userCompanies) && isIpAllowed($ip, $comp['allowed_ips'] ?? '')) {
|
|
$allowedCompanyIds[] = $comp['id'];
|
|
}
|
|
}
|
|
// Jos kaikki yritykset IP-estetty → estä kirjautuminen
|
|
if (empty($allowedCompanyIds) && !empty($userCompanies)) {
|
|
dbRecordLoginAttempt($ip);
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'IP-osoitteesi (' . $ip . ') ei ole sallittu. Ota yhteyttä ylläpitoon.']);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// false = säilytä vanha sessio hetken (estää race condition kun frontend lataa heti dataa)
|
|
session_regenerate_id(false);
|
|
$_SESSION['user_id'] = $u['id'];
|
|
$_SESSION['username'] = $u['username'];
|
|
$_SESSION['nimi'] = $u['nimi'];
|
|
$_SESSION['role'] = $u['role'];
|
|
// Superadmin saa pääsyn kaikkiin yrityksiin
|
|
if ($u['role'] === 'superadmin') {
|
|
$allIds = array_map(fn($c) => $c['id'], $allCompanies);
|
|
$_SESSION['companies'] = $allIds;
|
|
} else {
|
|
$_SESSION['companies'] = $userCompanies;
|
|
}
|
|
$_SESSION['company_roles'] = $u['company_roles'] ?? [];
|
|
// Valitse aktiivinen yritys: domain-match > ensimmäinen sallittu
|
|
if ($domainCompanyId && in_array($domainCompanyId, $allowedCompanyIds)) {
|
|
$_SESSION['company_id'] = $domainCompanyId;
|
|
} elseif (!empty($allowedCompanyIds)) {
|
|
$_SESSION['company_id'] = $allowedCompanyIds[0];
|
|
} else {
|
|
$_SESSION['company_id'] = !empty($userCompanies) ? $userCompanies[0] : '';
|
|
}
|
|
// Aseta aktiivisen yrityksen rooli
|
|
$_SESSION['company_role'] = $_SESSION['company_roles'][$_SESSION['company_id']] ?? 'user';
|
|
// Hae yritysten nimet + IP-status
|
|
$companyList = [];
|
|
foreach ($allCompanies as $comp) {
|
|
// Superadmin näkee kaikki yritykset
|
|
if ($u['role'] === 'superadmin' || in_array($comp['id'], $userCompanies)) {
|
|
$entry = ['id' => $comp['id'], 'nimi' => $comp['nimi'], 'phone' => $comp['phone'] ?? ''];
|
|
// Merkitse IP-estetyt yritykset (superadmin ohittaa)
|
|
if ($u['role'] !== 'superadmin' && !isIpAllowed($ip, $comp['allowed_ips'] ?? '')) {
|
|
$entry['ip_blocked'] = true;
|
|
}
|
|
$companyList[] = $entry;
|
|
}
|
|
}
|
|
echo json_encode([
|
|
'success' => true,
|
|
'username' => $u['username'],
|
|
'nimi' => $u['nimi'],
|
|
'role' => $u['role'],
|
|
'company_role' => $_SESSION['company_role'],
|
|
'companies' => $companyList,
|
|
'company_id' => $_SESSION['company_id'],
|
|
'signatures' => buildSignaturesWithDefaults($u, $u['companies'] ?? []),
|
|
]);
|
|
} 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) {
|
|
// Superadmin saa pääsyn kaikkiin yrityksiin
|
|
if (($u['role'] ?? '') === 'superadmin') {
|
|
$allComps = dbLoadCompanies();
|
|
$_SESSION['companies'] = array_map(fn($c) => $c['id'], $allComps);
|
|
} else {
|
|
$_SESSION['companies'] = $u['companies'] ?? [];
|
|
}
|
|
$_SESSION['company_roles'] = $u['company_roles'] ?? [];
|
|
// Varmista aktiivinen yritys on sallittu
|
|
if (!in_array($_SESSION['company_id'] ?? '', $_SESSION['companies'])) {
|
|
$_SESSION['company_id'] = !empty($_SESSION['companies']) ? $_SESSION['companies'][0] : '';
|
|
}
|
|
// Päivitä aktiivisen yrityksen rooli
|
|
$_SESSION['company_role'] = $_SESSION['company_roles'][$_SESSION['company_id'] ?? ''] ?? 'user';
|
|
}
|
|
// Hae yritysten nimet ja IP-status
|
|
$userCompanyIds = $_SESSION['companies'] ?? [];
|
|
$allCompanies = dbLoadCompanies();
|
|
$ip = getClientIp();
|
|
$isSuperAdmin = ($_SESSION['role'] ?? '') === 'superadmin';
|
|
$companyList = [];
|
|
$firstAllowedId = null;
|
|
foreach ($allCompanies as $comp) {
|
|
// Superadmin näkee kaikki yritykset
|
|
if ($isSuperAdmin || in_array($comp['id'], $userCompanyIds)) {
|
|
$entry = ['id' => $comp['id'], 'nimi' => $comp['nimi'], 'phone' => $comp['phone'] ?? ''];
|
|
if (!$isSuperAdmin && !isIpAllowed($ip, $comp['allowed_ips'] ?? '')) {
|
|
$entry['ip_blocked'] = true;
|
|
} else {
|
|
if (!$firstAllowedId) $firstAllowedId = $comp['id'];
|
|
}
|
|
$companyList[] = $entry;
|
|
}
|
|
}
|
|
// Jos aktiivinen yritys on IP-estetty → vaihda sallittuun
|
|
if (!$isSuperAdmin) {
|
|
$activeBlocked = false;
|
|
$activeId = $_SESSION['company_id'] ?? '';
|
|
foreach ($companyList as $c) {
|
|
if ($c['id'] === $activeId && !empty($c['ip_blocked'])) {
|
|
$activeBlocked = true;
|
|
break;
|
|
}
|
|
}
|
|
if ($activeBlocked) {
|
|
if ($firstAllowedId) {
|
|
$_SESSION['company_id'] = $firstAllowedId;
|
|
$_SESSION['company_role'] = $_SESSION['company_roles'][$firstAllowedId] ?? 'user';
|
|
} else {
|
|
// Kaikki yritykset IP-estetty → kirjaa ulos
|
|
session_destroy();
|
|
echo json_encode(['authenticated' => false, 'reason' => 'ip_blocked']);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Hae allekirjoitukset (oletus generoituna jos omaa ei ole)
|
|
$userSignatures = $u ? buildSignaturesWithDefaults($u, $u['companies'] ?? []) : [];
|
|
// Brändäystiedot aktiivisen yrityksen mukaan (ei domain-pohjainen)
|
|
$activeCompanyId = $_SESSION['company_id'] ?? '';
|
|
$branding = null;
|
|
$enabledModules = [];
|
|
foreach ($allCompanies as $comp) {
|
|
if ($comp['id'] === $activeCompanyId) {
|
|
$logoUrl = !empty($comp['logo_file'])
|
|
? "api.php?action=company_logo&company_id=" . urlencode($comp['id'])
|
|
: '';
|
|
$branding = [
|
|
'found' => true,
|
|
'company_id' => $comp['id'],
|
|
'nimi' => $comp['nimi'],
|
|
'primary_color' => $comp['primary_color'] ?? '#0f3460',
|
|
'subtitle' => $comp['subtitle'] ?? '',
|
|
'logo_url' => $logoUrl,
|
|
];
|
|
$enabledModules = $comp['enabled_modules'] ?? [];
|
|
break;
|
|
}
|
|
}
|
|
if (!$branding) {
|
|
$branding = ['found' => false, 'nimi' => 'Noxus HUB', 'primary_color' => '#0f3460', 'subtitle' => 'Hallintapaneeli', 'logo_url' => ''];
|
|
}
|
|
// Tarkista onko yrityksellä integraatioita päällä
|
|
$hasIntegrations = false;
|
|
if ($activeCompanyId) {
|
|
$integrations = dbLoadIntegrations($activeCompanyId);
|
|
foreach ($integrations as $integ) {
|
|
if (!empty($integ['enabled'])) { $hasIntegrations = true; break; }
|
|
}
|
|
}
|
|
echo json_encode([
|
|
'authenticated' => true,
|
|
'user_id' => $_SESSION['user_id'],
|
|
'username' => $_SESSION['username'],
|
|
'nimi' => $_SESSION['nimi'],
|
|
'email' => $u['email'] ?? '',
|
|
'role' => $_SESSION['role'],
|
|
'company_role' => $_SESSION['company_role'] ?? 'user',
|
|
'companies' => $companyList,
|
|
'company_id' => $_SESSION['company_id'] ?? '',
|
|
'signatures' => $userSignatures,
|
|
'branding' => $branding,
|
|
'enabled_modules' => $enabledModules,
|
|
'hidden_mailboxes' => $u ? ($u['hidden_mailboxes'] ?? []) : [],
|
|
'has_integrations' => $hasIntegrations,
|
|
]);
|
|
} 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 HUB</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 - Noxus HUB', $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':
|
|
requireAdmin();
|
|
$users = dbLoadUsers();
|
|
// Admin näkee vain oman yrityksensä käyttäjät, superadmin näkee kaikki
|
|
$role = $_SESSION['role'] ?? '';
|
|
$companyId = $_SESSION['company_id'] ?? '';
|
|
if ($role !== 'superadmin') {
|
|
$users = array_filter($users, function($u) use ($companyId) {
|
|
return in_array($companyId, $u['companies'] ?? []);
|
|
});
|
|
}
|
|
$safe = array_map(function($u) {
|
|
unset($u['password_hash']);
|
|
return $u;
|
|
}, $users);
|
|
echo json_encode(array_values($safe));
|
|
break;
|
|
|
|
case 'user_create':
|
|
requireAdmin();
|
|
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'] ?? '');
|
|
$isSA = ($_SESSION['role'] ?? '') === 'superadmin';
|
|
// Globaali rooli: user tai superadmin (admin on nyt yrityskohtainen)
|
|
$validRoles = $isSA ? ['superadmin', 'user'] : ['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'] ?? [];
|
|
// Admin voi lisätä käyttäjiä vain omaan yritykseensä
|
|
if (!$isSA) {
|
|
$myCompanyId = $_SESSION['company_id'] ?? '';
|
|
$companies = [$myCompanyId];
|
|
}
|
|
// 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;
|
|
}
|
|
}
|
|
// Yrityskohtaiset roolit
|
|
$companyRoles = [];
|
|
if ($isSA && isset($input['company_roles']) && is_array($input['company_roles'])) {
|
|
foreach ($input['company_roles'] as $cid => $crole) {
|
|
if (in_array($cid, $companies) && in_array($crole, ['admin', 'user'])) {
|
|
$companyRoles[$cid] = $crole;
|
|
}
|
|
}
|
|
} elseif (!$isSA) {
|
|
// Admin luo käyttäjiä vain omaan yritykseensä -> oletusrooli user
|
|
$myCompanyId = $_SESSION['company_id'] ?? '';
|
|
$companyRoles[$myCompanyId] = 'user';
|
|
}
|
|
$newUser = [
|
|
'id' => generateId(),
|
|
'username' => $username,
|
|
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
|
'nimi' => $nimi ?: $username,
|
|
'email' => $email,
|
|
'role' => $role,
|
|
'companies' => $companies,
|
|
'company_roles' => $companyRoles,
|
|
'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':
|
|
requireAdmin();
|
|
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;
|
|
}
|
|
$isSA = ($_SESSION['role'] ?? '') === 'superadmin';
|
|
$myCompanyId = $_SESSION['company_id'] ?? '';
|
|
// Admin voi muokata vain oman yrityksensä käyttäjiä
|
|
if (!$isSA && !in_array($myCompanyId, $u['companies'] ?? [])) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Ei oikeutta muokata tätä käyttäjää']);
|
|
break;
|
|
}
|
|
if (isset($input['nimi'])) $u['nimi'] = trim($input['nimi']);
|
|
if (isset($input['email'])) $u['email'] = trim($input['email']);
|
|
if (isset($input['role'])) {
|
|
// Globaali rooli: user tai superadmin (admin on nyt yrityskohtainen)
|
|
$validRoles = $isSA ? ['superadmin', 'user'] : ['user'];
|
|
// Admin ei voi muuttaa superadminia
|
|
if (!$isSA && ($u['role'] === 'superadmin')) {
|
|
// Älä muuta roolia
|
|
} else {
|
|
$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)));
|
|
}
|
|
// Yrityskohtaiset roolit
|
|
if (isset($input['company_roles']) && is_array($input['company_roles'])) {
|
|
$companyRoles = $u['company_roles'] ?? [];
|
|
foreach ($input['company_roles'] as $cid => $crole) {
|
|
if (in_array($cid, $u['companies'] ?? []) && in_array($crole, ['admin', 'user'])) {
|
|
$companyRoles[$cid] = $crole;
|
|
}
|
|
}
|
|
$u['company_roles'] = $companyRoles;
|
|
}
|
|
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'] ?? [];
|
|
$_SESSION['company_roles'] = $u['company_roles'] ?? [];
|
|
if (!empty($u['companies']) && !in_array($_SESSION['company_id'] ?? '', $u['companies'])) {
|
|
$_SESSION['company_id'] = $u['companies'][0];
|
|
}
|
|
if (empty($u['companies'])) {
|
|
$_SESSION['company_id'] = '';
|
|
}
|
|
// Päivitä aktiivisen yrityksen rooli
|
|
$_SESSION['company_role'] = ($_SESSION['company_roles'][$_SESSION['company_id'] ?? '']) ?? 'user';
|
|
}
|
|
$safe = $u;
|
|
unset($safe['password_hash']);
|
|
echo json_encode($safe);
|
|
break;
|
|
|
|
case 'user_delete':
|
|
requireAdmin();
|
|
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);
|
|
$isSA = ($_SESSION['role'] ?? '') === 'superadmin';
|
|
$myCompanyId = $_SESSION['company_id'] ?? '';
|
|
// Admin ei voi poistaa superadmineja eikä toisen yrityksen käyttäjiä
|
|
if (!$isSA) {
|
|
if ($deleted && ($deleted['role'] === 'superadmin' || !in_array($myCompanyId, $deleted['companies'] ?? []))) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Ei oikeutta poistaa tätä käyttäjää']);
|
|
break;
|
|
}
|
|
}
|
|
$companyId = $_SESSION['company_id'] ?? '';
|
|
if ($isSA) {
|
|
// Superadmin poistaa käyttäjän kokonaan
|
|
dbDeleteUser($id);
|
|
if ($deleted) dbAddLog($companyId, currentUser(), 'user_delete', '', '', "Poisti käyttäjän kokonaan: {$deleted['username']}");
|
|
} else {
|
|
// Admin poistaa käyttäjän vain nykyisestä yrityksestä
|
|
dbRemoveUserFromCompany($id, $companyId);
|
|
if ($deleted) dbAddLog($companyId, currentUser(), 'user_delete', '', '', "Poisti käyttäjän yrityksestä: {$deleted['username']}");
|
|
// Jos käyttäjällä ei ole enää yhtään yritystä, poista kokonaan
|
|
$remaining = dbGetUserCompanies($id);
|
|
if (empty($remaining)) {
|
|
dbDeleteUser($id);
|
|
}
|
|
}
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
// ---------- PROFILE (oma profiili) ----------
|
|
case 'profile_update':
|
|
requireAuth();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$userId = $_SESSION['user_id'];
|
|
$u = dbGetUser($userId);
|
|
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 (!empty($input['password'])) {
|
|
if (strlen($input['password']) < 4) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Salasanan pitää olla vähintään 4 merkkiä']);
|
|
break;
|
|
}
|
|
$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;
|
|
}
|
|
if (isset($input['hidden_mailboxes']) && is_array($input['hidden_mailboxes'])) {
|
|
$u['hidden_mailboxes'] = array_map('strval', $input['hidden_mailboxes']);
|
|
}
|
|
dbSaveUser($u);
|
|
// Päivitä session nimi
|
|
$_SESSION['nimi'] = $u['nimi'];
|
|
$safe = $u;
|
|
unset($safe['password_hash']);
|
|
echo json_encode($safe);
|
|
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) — YHDISTETTY LAITETILOIHIN ----------
|
|
// sites-endpoint palauttaa nyt laitetilat (taaksepäin yhteensopivuus)
|
|
case 'sites':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
echo json_encode(dbLoadLaitetilat($companyId));
|
|
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' => null,
|
|
'laitetila_id' => $input['laitetila_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);
|
|
// Auto-IPAM: varaa hallintaosoite laitteelle IPAM:iin
|
|
$mgmtIp = $device['hallintaosoite'];
|
|
if ($mgmtIp) {
|
|
// Normalisoi: poista mahdollinen /32 suffix
|
|
$cleanIp = explode('/', $mgmtIp)[0];
|
|
// Etsi löytyykö jo IPAM:ista
|
|
$ipamEntries = dbLoadIpam($companyId);
|
|
$existing = null;
|
|
foreach ($ipamEntries as $ie) {
|
|
$ieClean = explode('/', $ie['verkko'])[0];
|
|
if ($ieClean === $cleanIp && $ie['tyyppi'] === 'ip') {
|
|
$existing = $ie;
|
|
break;
|
|
}
|
|
}
|
|
if ($existing) {
|
|
// Päivitä olemassa oleva: merkitse varatuksi laitteelle
|
|
$existing['tila'] = 'varattu';
|
|
$existing['nimi'] = $device['nimi'];
|
|
$existing['site_id'] = $device['laitetila_id'];
|
|
$existing['muokattu'] = date('Y-m-d H:i:s');
|
|
$existing['muokkaaja'] = currentUser();
|
|
dbSaveIpam($companyId, $existing);
|
|
} else {
|
|
// Luo uusi IPAM-merkintä
|
|
$newIpam = [
|
|
'id' => generateId(),
|
|
'tyyppi' => 'ip',
|
|
'nimi' => $device['nimi'],
|
|
'verkko' => $cleanIp,
|
|
'vlan_id' => null,
|
|
'site_id' => $device['laitetila_id'],
|
|
'tila' => 'varattu',
|
|
'asiakas' => '',
|
|
'lisatiedot' => 'Automaattinen varaus laitteelta: ' . $device['nimi'],
|
|
'luotu' => date('Y-m-d H:i:s'),
|
|
'muokattu' => null,
|
|
'muokkaaja' => currentUser(),
|
|
];
|
|
dbSaveIpam($companyId, $newIpam);
|
|
}
|
|
}
|
|
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(),
|
|
];
|
|
// Duplikaatti-tarkistus
|
|
$skipDuplicateCheck = !empty($input['force']);
|
|
if (!$skipDuplicateCheck) {
|
|
$existingAll = dbLoadIpam($companyId);
|
|
// Verkko/IP duplikaatti — estä kokonaan
|
|
if ($entry['verkko'] !== '' && $entry['tyyppi'] !== 'vlan') {
|
|
foreach ($existingAll as $ex) {
|
|
if ($ex['verkko'] === $entry['verkko'] && $ex['id'] !== $entry['id'] && $ex['tyyppi'] !== 'vlan') {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'IP-osoite tai verkko "' . $entry['verkko'] . '" on jo olemassa (' . ($ex['nimi'] ?: 'nimetön') . ')']);
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
// VLAN duplikaatti — varoitus (409 = confirm)
|
|
if ($entry['tyyppi'] === 'vlan' && !empty($entry['vlan_id'])) {
|
|
$vlanNum = (int)$entry['vlan_id'];
|
|
foreach ($existingAll as $ex) {
|
|
if ($ex['tyyppi'] === 'vlan' && (int)$ex['vlan_id'] === $vlanNum && $ex['id'] !== $entry['id']) {
|
|
http_response_code(409);
|
|
echo json_encode(['warning' => 'VLAN ' . $vlanNum . ' on jo olemassa (' . ($ex['nimi'] ?: 'nimetön') . '). Lisätäänkö silti?']);
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
dbSaveIpam($companyId, $entry);
|
|
// Auto-VLAN: jos subnet/ip:llä on vlan_id, luo VLAN automaattisesti jos ei vielä ole
|
|
if ($entry['tyyppi'] !== 'vlan' && !empty($entry['vlan_id'])) {
|
|
$vlanNum = (int)$entry['vlan_id'];
|
|
$existingIpam = dbLoadIpam($companyId);
|
|
$vlanExists = false;
|
|
foreach ($existingIpam as $ie) {
|
|
if ($ie['tyyppi'] === 'vlan' && (int)$ie['vlan_id'] === $vlanNum) {
|
|
$vlanExists = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$vlanExists) {
|
|
$newVlan = [
|
|
'id' => generateId(),
|
|
'tyyppi' => 'vlan',
|
|
'nimi' => trim($entry['nimi']) ?: ('VLAN ' . $vlanNum),
|
|
'verkko' => '',
|
|
'vlan_id' => $vlanNum,
|
|
'site_id' => $entry['site_id'],
|
|
'tila' => 'varattu',
|
|
'asiakas' => '',
|
|
'lisatiedot' => 'Luotu automaattisesti',
|
|
'luotu' => date('Y-m-d H:i:s'),
|
|
'muokattu' => null,
|
|
'muokkaaja' => currentUser(),
|
|
];
|
|
dbSaveIpam($companyId, $newVlan);
|
|
}
|
|
}
|
|
$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;
|
|
|
|
// ---------- OHJEET (GUIDES) ----------
|
|
case 'guides':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
echo json_encode(dbLoadGuides($companyId));
|
|
break;
|
|
|
|
case 'guide':
|
|
requireAuth();
|
|
requireCompany();
|
|
$id = $_GET['id'] ?? '';
|
|
$guide = dbLoadGuide($id);
|
|
if (!$guide) { http_response_code(404); echo json_encode(['error' => 'Ohjetta ei löydy']); exit; }
|
|
echo json_encode($guide);
|
|
break;
|
|
|
|
case 'guide_save':
|
|
requireAuth();
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$isNew = empty($input['id']);
|
|
$guide = [
|
|
'id' => $input['id'] ?? generateId(),
|
|
'category_id' => $input['category_id'] ?? null,
|
|
'title' => trim($input['title'] ?? ''),
|
|
'content' => $input['content'] ?? '',
|
|
'tags' => trim($input['tags'] ?? ''),
|
|
'author' => $isNew ? currentUser() : ($input['author'] ?? currentUser()),
|
|
'pinned' => !empty($input['pinned']),
|
|
'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($guide['title'])) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Otsikko vaaditaan']);
|
|
exit;
|
|
}
|
|
dbSaveGuide($companyId, $guide);
|
|
dbAddLog($companyId, currentUser(), $isNew ? 'guide_create' : 'guide_update', $guide['id'], $guide['title'], ($isNew ? 'Loi' : 'Muokkasi') . ' ohjeen');
|
|
echo json_encode($guide);
|
|
break;
|
|
|
|
case 'guide_delete':
|
|
requireAuth();
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$guide = dbLoadGuide($id);
|
|
dbDeleteGuide($id);
|
|
dbAddLog($companyId, currentUser(), 'guide_delete', $id, $guide ? $guide['title'] : '', 'Poisti ohjeen');
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
case 'guide_categories':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
echo json_encode(dbLoadGuideCategories($companyId));
|
|
break;
|
|
|
|
case 'guide_category_save':
|
|
requireAuth();
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$cat = [
|
|
'id' => $input['id'] ?? generateId(),
|
|
'nimi' => trim($input['nimi'] ?? ''),
|
|
'sort_order' => (int)($input['sort_order'] ?? 0),
|
|
];
|
|
if (empty($cat['nimi'])) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Kategorian nimi vaaditaan']);
|
|
exit;
|
|
}
|
|
dbSaveGuideCategory($companyId, $cat);
|
|
echo json_encode($cat);
|
|
break;
|
|
|
|
case 'guide_category_delete':
|
|
requireAuth();
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
dbDeleteGuideCategory($input['id'] ?? '');
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
case 'guide_image_upload':
|
|
requireAuth();
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
if (empty($_FILES['image'])) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Kuva puuttuu']);
|
|
break;
|
|
}
|
|
$file = $_FILES['image'];
|
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Kuvan lähetys epäonnistui']);
|
|
break;
|
|
}
|
|
if ($file['size'] > 5 * 1024 * 1024) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Kuva on liian suuri (max 5 MB)']);
|
|
break;
|
|
}
|
|
$allowedExt = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
|
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
|
if (!in_array($ext, $allowedExt)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Sallitut tiedostotyypit: PNG, JPG, GIF, WebP']);
|
|
break;
|
|
}
|
|
$imgDir = getCompanyDir($companyId) . '/guide_images';
|
|
if (!file_exists($imgDir)) mkdir($imgDir, 0755, true);
|
|
$filename = uniqid() . '.' . ($ext === 'jpeg' ? 'jpg' : $ext);
|
|
if (move_uploaded_file($file['tmp_name'], $imgDir . '/' . $filename)) {
|
|
$url = 'api.php?action=guide_image&file=' . urlencode($filename);
|
|
echo json_encode(['success' => true, 'url' => $url, 'filename' => $filename]);
|
|
} else {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Tallennusvirhe']);
|
|
}
|
|
break;
|
|
|
|
case 'guide_image':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
$filename = basename($_GET['file'] ?? '');
|
|
if (!$filename || !preg_match('/^[a-f0-9]+\.(png|jpg|gif|webp)$/', $filename)) {
|
|
http_response_code(400);
|
|
echo 'Virheellinen tiedostonimi';
|
|
break;
|
|
}
|
|
$path = getCompanyDir($companyId) . '/guide_images/' . $filename;
|
|
if (!file_exists($path)) {
|
|
http_response_code(404);
|
|
echo 'Kuvaa ei löydy';
|
|
break;
|
|
}
|
|
$mimes = ['png' => 'image/png', 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'webp' => 'image/webp'];
|
|
$ext = pathinfo($filename, PATHINFO_EXTENSION);
|
|
header('Content-Type: ' . ($mimes[$ext] ?? 'application/octet-stream'));
|
|
header('Cache-Control: public, max-age=86400');
|
|
readfile($path);
|
|
exit;
|
|
|
|
// ---------- TEHTÄVÄT (TODOS) ----------
|
|
case 'todos':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
echo json_encode(dbLoadTodos($companyId));
|
|
break;
|
|
|
|
case 'todo_detail':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
$id = $_GET['id'] ?? '';
|
|
$todo = dbLoadTodo($id);
|
|
if (!$todo || $todo['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tehtävää ei löydy']);
|
|
break;
|
|
}
|
|
unset($todo['company_id']);
|
|
echo json_encode($todo);
|
|
break;
|
|
|
|
case 'todo_save':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$type = $input['type'] ?? 'task';
|
|
$isNew = empty($input['id']);
|
|
// Task: vain admin. Feature request: kaikki voivat luoda, mutta muokata vain omia (tai admin)
|
|
if ($type === 'task') {
|
|
requireAdmin();
|
|
} elseif (!$isNew) {
|
|
$existing = dbLoadTodo($input['id']);
|
|
if ($existing && $existing['created_by'] !== currentUser() && !isCompanyAdmin()) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Voit muokata vain omia ehdotuksiasi']);
|
|
break;
|
|
}
|
|
}
|
|
$todo = [
|
|
'id' => $input['id'] ?? generateId(),
|
|
'type' => $type,
|
|
'title' => trim($input['title'] ?? ''),
|
|
'description' => $input['description'] ?? '',
|
|
'status' => $input['status'] ?? ($type === 'task' ? 'avoin' : 'ehdotettu'),
|
|
'priority' => $input['priority'] ?? 'normaali',
|
|
'category' => $input['category'] ?? '',
|
|
'assigned_to' => $input['assigned_to'] ?? '',
|
|
'created_by' => $isNew ? currentUser() : ($input['created_by'] ?? currentUser()),
|
|
'deadline' => $input['deadline'] ?? null,
|
|
'luotu' => $isNew ? date('Y-m-d H:i:s') : ($input['luotu'] ?? date('Y-m-d H:i:s')),
|
|
'muokkaaja' => currentUser(),
|
|
];
|
|
if (empty($todo['title'])) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Otsikko on pakollinen']);
|
|
break;
|
|
}
|
|
dbSaveTodo($companyId, $todo);
|
|
dbAddLog($companyId, currentUser(), $isNew ? 'todo_create' : 'todo_update', $todo['id'], $todo['title'], '');
|
|
echo json_encode($todo);
|
|
break;
|
|
|
|
case 'todo_delete':
|
|
requireAuth();
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$todo = dbLoadTodo($id);
|
|
if ($todo) {
|
|
dbDeleteTodo($id);
|
|
dbAddLog($companyId, currentUser(), 'todo_delete', $id, $todo['title'] ?? '', '');
|
|
}
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
case 'todo_status':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') { echo json_encode(['error' => 'POST required']); break; }
|
|
try {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$status = $input['status'] ?? '';
|
|
if (!$id || !$status) { echo json_encode(['error' => 'id ja status vaaditaan']); break; }
|
|
// Tarkista oikeudet kevyellä querylla (ei dbLoadTodo joka voi kaatua)
|
|
$rows = _dbFetchAll("SELECT type, company_id FROM todos WHERE id = ?", [$id]);
|
|
if (empty($rows) || $rows[0]['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tehtävää ei löytynyt']);
|
|
break;
|
|
}
|
|
$type = $rows[0]['type'];
|
|
if ($type === 'feature_request' && !isCompanyAdmin()) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Vain admin voi muuttaa ehdotuksen statusta']);
|
|
break;
|
|
}
|
|
if ($type === 'task' && !isCompanyAdmin()) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Vain admin voi muuttaa tehtävän statusta']);
|
|
break;
|
|
}
|
|
_dbExecute("UPDATE todos SET status = ?, muokattu = NOW(), muokkaaja = ? WHERE id = ?", [$status, currentUser(), $id]);
|
|
echo json_encode(['success' => true]);
|
|
} catch (\Throwable $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Virhe: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'todo_assign':
|
|
requireAuth();
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') { echo json_encode(['error' => 'POST required']); break; }
|
|
try {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$assignedTo = $input['assigned_to'] ?? '';
|
|
$rows = _dbFetchAll("SELECT company_id FROM todos WHERE id = ?", [$id]);
|
|
if (empty($rows) || $rows[0]['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tehtävää ei löytynyt']);
|
|
break;
|
|
}
|
|
_dbExecute("UPDATE todos SET assigned_to = ?, muokattu = NOW(), muokkaaja = ? WHERE id = ?", [$assignedTo, currentUser(), $id]);
|
|
echo json_encode(['success' => true]);
|
|
} catch (\Throwable $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Virhe: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'todo_comment':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$todoId = $input['todo_id'] ?? '';
|
|
$body = trim($input['body'] ?? '');
|
|
if (empty($body)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Kommentti ei voi olla tyhjä']);
|
|
break;
|
|
}
|
|
$comment = [
|
|
'id' => generateId(),
|
|
'author' => currentUser(),
|
|
'body' => $body,
|
|
'luotu' => date('Y-m-d H:i:s'),
|
|
];
|
|
dbAddTodoComment($todoId, $comment);
|
|
echo json_encode($comment);
|
|
break;
|
|
|
|
case 'todo_comment_delete':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$commentId = $input['id'] ?? '';
|
|
// Tarkista onko oma kommentti tai admin
|
|
$rows = _dbFetchAll("SELECT author FROM todo_comments WHERE id = ?", [$commentId]);
|
|
if (!empty($rows) && ($rows[0]['author'] === currentUser() || isCompanyAdmin())) {
|
|
dbDeleteTodoComment($commentId);
|
|
echo json_encode(['success' => true]);
|
|
} else {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Ei oikeutta']);
|
|
}
|
|
break;
|
|
|
|
case 'todo_time_add':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$todoId = $input['todo_id'] ?? '';
|
|
$hours = floatval($input['hours'] ?? 0);
|
|
if ($hours <= 0) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Tunnit pitää olla > 0']);
|
|
break;
|
|
}
|
|
$entry = [
|
|
'id' => generateId(),
|
|
'user' => currentUser(),
|
|
'hours' => $hours,
|
|
'description' => trim($input['description'] ?? ''),
|
|
'work_date' => $input['work_date'] ?? date('Y-m-d'),
|
|
'luotu' => date('Y-m-d H:i:s'),
|
|
];
|
|
dbAddTodoTimeEntry($todoId, $entry);
|
|
echo json_encode($entry);
|
|
break;
|
|
|
|
case 'todo_time_delete':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$entryId = $input['id'] ?? '';
|
|
$rows = _dbFetchAll("SELECT user FROM todo_time_entries WHERE id = ?", [$entryId]);
|
|
if (!empty($rows) && ($rows[0]['user'] === currentUser() || isCompanyAdmin())) {
|
|
dbDeleteTodoTimeEntry($entryId);
|
|
echo json_encode(['success' => true]);
|
|
} else {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Ei oikeutta']);
|
|
}
|
|
break;
|
|
|
|
case 'todo_subtask_add':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') { echo json_encode(['error' => 'POST required']); break; }
|
|
try {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$todoId = $input['todo_id'] ?? '';
|
|
$title = trim($input['title'] ?? '');
|
|
if (!$todoId || !$title) { echo json_encode(['error' => 'todo_id ja title vaaditaan']); break; }
|
|
$rows = _dbFetchAll("SELECT company_id FROM todos WHERE id = ?", [$todoId]);
|
|
if (empty($rows) || $rows[0]['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tehtävää ei löytynyt']);
|
|
break;
|
|
}
|
|
$id = generateId();
|
|
dbAddTodoSubtask($todoId, ['id' => $id, 'title' => $title, 'created_by' => currentUser()]);
|
|
echo json_encode(['success' => true, 'id' => $id]);
|
|
} catch (\Throwable $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Virhe: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'todo_subtask_toggle':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method !== 'POST') { echo json_encode(['error' => 'POST required']); break; }
|
|
try {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$subtaskId = $input['id'] ?? '';
|
|
if (!$subtaskId) { echo json_encode(['error' => 'id vaaditaan']); break; }
|
|
$completed = dbToggleTodoSubtask($subtaskId);
|
|
echo json_encode(['success' => true, 'completed' => $completed]);
|
|
} catch (\Throwable $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Virhe: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'todo_subtask_delete':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method !== 'POST') { echo json_encode(['error' => 'POST required']); break; }
|
|
try {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$subtaskId = $input['id'] ?? '';
|
|
if (!$subtaskId) { echo json_encode(['error' => 'id vaaditaan']); break; }
|
|
$rows = _dbFetchAll("SELECT created_by FROM todo_subtasks WHERE id = ?", [$subtaskId]);
|
|
if (!empty($rows) && ($rows[0]['created_by'] === currentUser() || isCompanyAdmin())) {
|
|
dbDeleteTodoSubtask($subtaskId);
|
|
echo json_encode(['success' => true]);
|
|
} else {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Ei oikeutta']);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Virhe: ' . $e->getMessage()]);
|
|
}
|
|
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;
|
|
}
|
|
// Zammad-tiketti: hae artikkelit on-demand jos ei vielä haettu
|
|
if (!empty($ticket['zammad_ticket_id']) && empty($ticket['messages'])) {
|
|
try {
|
|
$integ = dbGetIntegration($companyId, 'zammad');
|
|
if ($integ && $integ['enabled']) {
|
|
$z = new ZammadClient($integ['config']['url'], $integ['config']['token']);
|
|
$articles = $z->getArticles((int)$ticket['zammad_ticket_id']);
|
|
$toEmail = '';
|
|
foreach ($articles as $art) {
|
|
if (($art['internal'] ?? false)) continue;
|
|
// Tallenna ensimmäisen saapuneen viestin to-osoite
|
|
if (!$toEmail && ($art['sender'] ?? '') === 'Customer' && !empty($art['to'])) {
|
|
$toEmail = $art['to'];
|
|
}
|
|
$artId = (int)$art['id'];
|
|
$existingMsg = dbGetMessageByZammadArticleId($ticket['id'], $artId);
|
|
if ($existingMsg) continue;
|
|
$msgId = substr(uniqid(), -8) . bin2hex(random_bytes(2));
|
|
$msgType = ($art['sender'] ?? '') === 'Customer' ? 'incoming' : 'outgoing';
|
|
$body = $art['body'] ?? '';
|
|
if (($art['content_type'] ?? '') === 'text/html') {
|
|
$body = strip_tags($body, '<br><p><div><a><b><i><strong><em><ul><ol><li>');
|
|
}
|
|
_dbExecute(
|
|
"INSERT INTO ticket_messages (id, ticket_id, type, from_email, from_name, body, timestamp, message_id, zammad_article_id)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
[$msgId, $ticket['id'], $msgType, $art['from'] ?? '', $art['from'] ?? '', $body,
|
|
$art['created_at'] ? date('Y-m-d H:i:s', strtotime($art['created_at'])) : date('Y-m-d H:i:s'),
|
|
$art['message_id'] ?? '', $artId]
|
|
);
|
|
}
|
|
// Tallenna Zammad to-osoite tikettiin
|
|
if ($toEmail) {
|
|
_dbExecute("UPDATE tickets SET zammad_to_email = ? WHERE id = ?", [$toEmail, $ticket['id']]);
|
|
}
|
|
// Lataa tiketti uudelleen viestien kanssa
|
|
$tickets = dbLoadTickets($companyId);
|
|
foreach ($tickets as $t) {
|
|
if ($t['id'] === $id) { $ticket = $t; break; }
|
|
}
|
|
}
|
|
} catch (\Throwable $e) { /* Zammad ei saatavilla — näytä tiketti ilman viestejä */ }
|
|
}
|
|
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 previously fetched message IDs (persists even after ticket deletion)
|
|
$existingMsgIds = dbGetFetchedMessageIds($companyId);
|
|
// Also include message IDs from current tickets (for backward compatibility)
|
|
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'] = 'uusi';
|
|
}
|
|
dbSaveTicket($companyId, $ft);
|
|
$threadedCount++;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// Tarkista onko lähettäjä priority-listalla
|
|
$ticketPriority = 'normaali';
|
|
if (dbIsPriorityEmail($companyId, $email['from_email'])) {
|
|
$ticketPriority = 'tärkeä';
|
|
}
|
|
|
|
// Generoi tikettinumero (VVNKKNN)
|
|
$ticketNumber = dbNextTicketNumber($companyId);
|
|
$originalSubject = $email['subject'] ?: '(Ei aihetta)';
|
|
$numberedSubject = "Tiketti #{$ticketNumber}: {$originalSubject}";
|
|
|
|
$ticket = [
|
|
'id' => generateId(),
|
|
'ticket_number' => $ticketNumber,
|
|
'subject' => $numberedSubject,
|
|
'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
|
|
$toAddresses = $email['to'] ?? '';
|
|
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 (!empty($rule['to_contains'])) {
|
|
$needle = strtolower($rule['to_contains']);
|
|
if (strpos(strtolower($toAddresses), $needle) === false) {
|
|
$match = false;
|
|
}
|
|
}
|
|
if ($match) {
|
|
if (!empty($rule['status_set'])) $ticket['status'] = $rule['status_set'];
|
|
if (!empty($rule['type_set'])) $ticket['type'] = $rule['type_set'];
|
|
if (!empty($rule['set_priority'])) $ticket['priority'] = $rule['set_priority'];
|
|
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);
|
|
|
|
// Autoreply — lähetä automaattinen vastaus asiakkaalle
|
|
if (!empty($mailbox['auto_reply_enabled']) && !empty($mailbox['auto_reply_body'])) {
|
|
$arSubject = 'Re: ' . $ticket['subject'];
|
|
$arBody = $mailbox['auto_reply_body'];
|
|
$arSent = sendTicketMail(
|
|
$email['from_email'],
|
|
$arSubject,
|
|
$arBody,
|
|
$email['message_id'], // In-Reply-To
|
|
$email['message_id'], // References
|
|
$mailbox,
|
|
'' // ei CC
|
|
);
|
|
if ($arSent) {
|
|
// Tallenna autoreply tiketin viestiksi
|
|
$arMsg = [
|
|
'id' => generateId(),
|
|
'type' => 'auto_reply',
|
|
'from' => $mailbox['smtp_from_email'] ?? $mailbox['imap_user'] ?? '',
|
|
'from_name' => $mailbox['smtp_from_name'] ?? $mailbox['nimi'] ?? '',
|
|
'body' => $arBody,
|
|
'timestamp' => date('Y-m-d H:i:s'),
|
|
'message_id' => '',
|
|
];
|
|
$ticket['messages'][] = $arMsg;
|
|
dbSaveTicket($companyId, $ticket);
|
|
}
|
|
}
|
|
|
|
// Telegram-hälytys tärkeille/urgentille
|
|
if ($ticket['priority'] === 'urgent' || $ticket['priority'] === 'tärkeä') {
|
|
sendTelegramAlert($companyId, $ticket);
|
|
}
|
|
$newCount++;
|
|
}
|
|
|
|
// Merkitse haetuksi pysyvästi
|
|
if ($email['message_id']) {
|
|
dbMarkMessageIdFetched($companyId, $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'] ?? '';
|
|
$replyTo = trim($input['to'] ?? '');
|
|
$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 (ellei estetty)
|
|
$noSignature = !empty($input['no_signature']);
|
|
$mailboxId = $replyMailbox['id'] ?? '';
|
|
$signature = '';
|
|
if (!$noSignature) {
|
|
$sigUser = dbGetUser($_SESSION['user_id']);
|
|
if ($sigUser) {
|
|
$allSigs = buildSignaturesWithDefaults($sigUser, $sigUser['companies'] ?? []);
|
|
$signature = trim($allSigs[$mailboxId] ?? '');
|
|
}
|
|
}
|
|
$emailBody = $signature ? $body . "\n\n-- \n" . $signature : $body;
|
|
|
|
// Rakenna viestiketju (quoted thread) vastaukseen
|
|
$threadMessages = _dbFetchAll(
|
|
"SELECT from_name, from_email, timestamp, body, type FROM ticket_messages WHERE ticket_id = ? ORDER BY timestamp DESC",
|
|
[$t['id']]
|
|
);
|
|
if (!empty($threadMessages)) {
|
|
$emailBody .= "\n";
|
|
foreach ($threadMessages as $tm) {
|
|
$tmSender = $tm['from_name'] ?: $tm['from_email'];
|
|
$tmDate = date('d.m.Y H:i', strtotime($tm['timestamp']));
|
|
$tmBody = strip_tags(str_replace(['<br>', '<br/>', '<br />', '</p>', '</div>'], "\n", $tm['body'] ?: ''));
|
|
$tmBody = html_entity_decode($tmBody, ENT_QUOTES, 'UTF-8');
|
|
$tmBody = trim(preg_replace('/\n{3,}/', "\n\n", $tmBody));
|
|
$quoted = implode("\n", array_map(fn($l) => "> " . $l, explode("\n", $tmBody)));
|
|
$emailBody .= "\n{$tmSender} — {$tmDate}:\n{$quoted}\n";
|
|
}
|
|
}
|
|
|
|
// CC: käytä frontendistä annettua CC:tä, tai tiketin alkuperäistä CC:tä
|
|
$ccToSend = $replyCc !== '' ? $replyCc : ($t['cc'] ?? '');
|
|
|
|
$subject = 'Re: ' . $t['subject'];
|
|
$toAddress = $replyTo !== '' ? $replyTo : $t['from_email'];
|
|
$sent = sendTicketMail($toAddress, $subject, $emailBody, $lastMsgId, trim($allRefs), $replyMailbox, $ccToSend);
|
|
|
|
if (!$sent) {
|
|
http_response_code(500);
|
|
$smtpErr = $GLOBALS['smtp_last_error'] ?? '';
|
|
$detail = $smtpErr ? " ({$smtpErr})" : '';
|
|
$mbDebug = $replyMailbox ? " [smtp_host=" . ($replyMailbox['smtp_host'] ?? 'EMPTY')
|
|
. " smtp_port=" . ($replyMailbox['smtp_port'] ?? '?')
|
|
. " smtp_user=" . ($replyMailbox['smtp_user'] ?? 'EMPTY')
|
|
. " smtp_pass_len=" . strlen($replyMailbox['smtp_password'] ?? '')
|
|
. " smtp_enc=" . ($replyMailbox['smtp_encryption'] ?? '?')
|
|
. " imap_host=" . ($replyMailbox['imap_host'] ?? '')
|
|
. " imap_user=" . ($replyMailbox['imap_user'] ?? '')
|
|
. " imap_pass_len=" . strlen($replyMailbox['imap_password'] ?? '') . "]" : ' [no mailbox]';
|
|
echo json_encode(['error' => "Sähköpostin lähetys epäonnistui{$detail}{$mbDebug}"]);
|
|
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', '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 = array_map(fn($t) => $t['value'], dbLoadTicketTypes($companyId));
|
|
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'] ?? ''),
|
|
'to_contains' => trim($input['to_contains'] ?? ''),
|
|
'set_status' => $input['set_status'] ?? '',
|
|
'set_type' => $input['set_type'] ?? '',
|
|
'set_priority' => $input['set_priority'] ?? '',
|
|
'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','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_types':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
echo json_encode(dbLoadTicketTypes($companyId));
|
|
break;
|
|
|
|
case 'ticket_type_save':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$value = preg_replace('/[^a-z0-9_-]/', '', strtolower(trim($input['value'] ?? '')));
|
|
$label = trim($input['label'] ?? '');
|
|
if (!$value || !$label) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Tunnus ja nimi vaaditaan']);
|
|
break;
|
|
}
|
|
dbSaveTicketType($companyId, [
|
|
'id' => $input['id'] ?? null,
|
|
'value' => $value,
|
|
'label' => $label,
|
|
'color' => $input['color'] ?? '',
|
|
'sort_order' => intval($input['sort_order'] ?? 0),
|
|
]);
|
|
dbAddLog($companyId, currentUser(), 'config_update', '', '', 'Tikettityyppi: ' . $label);
|
|
echo json_encode(dbLoadTicketTypes($companyId));
|
|
break;
|
|
|
|
case 'ticket_type_delete':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$value = $input['value'] ?? '';
|
|
if (!$value) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Tyyppi puuttuu']);
|
|
break;
|
|
}
|
|
// Tarkista onko käytössä
|
|
$tickets = dbLoadTickets($companyId);
|
|
$inUse = 0;
|
|
foreach ($tickets as $t) {
|
|
if (($t['type'] ?? '') === $value) $inUse++;
|
|
}
|
|
if ($inUse > 0) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => "Tyyppiä käytetään {$inUse} tiketissä, ei voi poistaa"]);
|
|
break;
|
|
}
|
|
dbDeleteTicketType($companyId, $value);
|
|
echo json_encode(dbLoadTicketTypes($companyId));
|
|
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'] ?? '',
|
|
'smtp_from_email' => $mb['smtp_from_email'] ?? $mb['imap_user'] ?? '',
|
|
'company_id' => $comp['id'],
|
|
'company_nimi' => $comp['nimi'],
|
|
'company_phone' => $comp['phone'] ?? '',
|
|
];
|
|
}
|
|
}
|
|
echo json_encode($result);
|
|
break;
|
|
|
|
case 'ticket_zammad_groups':
|
|
requireAuth();
|
|
$userCompanyIds = $_SESSION['companies'] ?? [];
|
|
$groups = [];
|
|
foreach ($userCompanyIds as $cid) {
|
|
$rows = _dbFetchAll("SELECT DISTINCT zammad_group FROM tickets WHERE company_id = ? AND source = 'zammad' AND zammad_group IS NOT NULL AND zammad_group != ''", [$cid]);
|
|
foreach ($rows as $r) {
|
|
if (!in_array($r['zammad_group'], $groups)) $groups[] = $r['zammad_group'];
|
|
}
|
|
}
|
|
sort($groups);
|
|
echo json_encode($groups);
|
|
break;
|
|
|
|
case 'ticket_zammad_emails':
|
|
requireAuth();
|
|
$userCompanyIds = $_SESSION['companies'] ?? [];
|
|
$emails = [];
|
|
foreach ($userCompanyIds as $cid) {
|
|
$rows = _dbFetchAll("SELECT DISTINCT zammad_to_email FROM tickets WHERE company_id = ? AND source = 'zammad' AND zammad_to_email IS NOT NULL AND zammad_to_email != ''", [$cid]);
|
|
foreach ($rows as $r) {
|
|
if (!in_array($r['zammad_to_email'], $emails)) $emails[] = $r['zammad_to_email'];
|
|
}
|
|
}
|
|
sort($emails);
|
|
echo json_encode($emails);
|
|
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['phone'])) $c['phone'] = trim($input['phone']);
|
|
if (isset($input['enabled_modules']) && is_array($input['enabled_modules'])) {
|
|
$c['enabled_modules'] = array_values($input['enabled_modules']);
|
|
}
|
|
// Vain superadmin saa muuttaa IP-rajoituksia
|
|
if (isset($input['allowed_ips']) && isSuperAdmin()) {
|
|
$c['allowed_ips'] = trim($input['allowed_ips']);
|
|
}
|
|
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'] ?? [];
|
|
$isSuperadmin = ($_SESSION['role'] ?? '') === 'superadmin';
|
|
if (!$isSuperadmin && !in_array($companyId, $userCompanies)) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']);
|
|
break;
|
|
}
|
|
// IP-rajoitus yritystä vaihdettaessa (superadmin ohittaa)
|
|
if (($_SESSION['role'] ?? '') !== 'superadmin') {
|
|
$companies = dbLoadCompanies();
|
|
foreach ($companies as $comp) {
|
|
if ($comp['id'] === $companyId) {
|
|
if (!isIpAllowed(getClientIp(), $comp['allowed_ips'] ?? '')) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'IP-osoitteesi ei ole sallittu tälle yritykselle.']);
|
|
break 2;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$_SESSION['company_id'] = $companyId;
|
|
// Päivitä aktiivisen yrityksen rooli
|
|
$_SESSION['company_role'] = ($_SESSION['company_roles'] ?? [])[$companyId] ?? 'user';
|
|
echo json_encode([
|
|
'success' => true,
|
|
'company_id' => $companyId,
|
|
'company_role' => $_SESSION['company_role'],
|
|
]);
|
|
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']) ? '********' : '';
|
|
$mb['smtp_password'] = !empty($mb['smtp_password']) ? '********' : '';
|
|
return $mb;
|
|
}, $mailboxes);
|
|
echo json_encode($mbs);
|
|
break;
|
|
|
|
case 'smtp_test':
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$mailboxId = $input['mailbox_id'] ?? '';
|
|
$mailbox = dbGetMailbox($mailboxId);
|
|
if (!$mailbox) {
|
|
echo json_encode(['error' => 'Postilaatikkoa ei löydy']);
|
|
break;
|
|
}
|
|
// Näytä tietokannan raakatiedot
|
|
$dbInfo = [
|
|
'imap_user' => $mailbox['imap_user'] ?? '',
|
|
'imap_pass_len' => strlen($mailbox['imap_password'] ?? ''),
|
|
'smtp_host' => $mailbox['smtp_host'] ?? '',
|
|
'smtp_port' => $mailbox['smtp_port'] ?? '',
|
|
'smtp_user' => $mailbox['smtp_user'] ?? '',
|
|
'smtp_pass_len' => strlen($mailbox['smtp_password'] ?? ''),
|
|
'smtp_encryption' => $mailbox['smtp_encryption'] ?? '',
|
|
'smtp_from_email' => $mailbox['smtp_from_email'] ?? '',
|
|
];
|
|
// Laske fallback-arvot
|
|
$effectiveUser = $mailbox['smtp_user'] ?? '';
|
|
if ($effectiveUser === '') $effectiveUser = $mailbox['imap_user'] ?? '';
|
|
if ($effectiveUser === '') $effectiveUser = $mailbox['smtp_from_email'] ?? '';
|
|
$effectivePass = $mailbox['smtp_password'] ?? '';
|
|
if ($effectivePass === '') $effectivePass = $mailbox['imap_password'] ?? '';
|
|
$host = $mailbox['smtp_host'] ?? '';
|
|
$port = (int)($mailbox['smtp_port'] ?? 587);
|
|
$encryption = $mailbox['smtp_encryption'] ?? 'tls';
|
|
// Salasana-vihje: näytä ensimmäiset 3 ja viimeiset 2 merkkiä
|
|
$passHint = strlen($effectivePass) > 5
|
|
? substr($effectivePass, 0, 3) . '***' . substr($effectivePass, -2)
|
|
: (strlen($effectivePass) > 0 ? '***(' . strlen($effectivePass) . ' merkkiä)' : '(tyhjä)');
|
|
|
|
$result = [
|
|
'db_values' => $dbInfo,
|
|
'effective_user' => $effectiveUser,
|
|
'effective_pass_hint' => $passHint,
|
|
'effective_pass_len' => strlen($effectivePass),
|
|
'steps' => [],
|
|
];
|
|
|
|
if (empty($host)) {
|
|
$result['steps'][] = '❌ SMTP-palvelin puuttuu';
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
break;
|
|
}
|
|
if (empty($effectivePass)) {
|
|
$result['steps'][] = '❌ Salasana puuttuu (tietokannassa ei SMTP- eikä IMAP-salasanaa)';
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
break;
|
|
}
|
|
|
|
// Testaa yhteys
|
|
$connStr = ($encryption === 'ssl' ? 'ssl' : 'tcp') . "://{$host}:{$port}";
|
|
$result['steps'][] = "🔌 Yhdistetään: {$connStr}";
|
|
$ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true]]);
|
|
$fp = @stream_socket_client($connStr, $errno, $errstr, 15, STREAM_CLIENT_CONNECT, $ctx);
|
|
if (!$fp) {
|
|
$result['steps'][] = "❌ Yhteys epäonnistui: {$errstr} ({$errno})";
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
break;
|
|
}
|
|
stream_set_timeout($fp, 15);
|
|
$result['steps'][] = '✅ Yhteys muodostettu';
|
|
|
|
// Banner
|
|
$resp = smtpReadResponse($fp);
|
|
$result['steps'][] = '📨 Banner: ' . trim($resp);
|
|
if (smtpCode($resp) !== '220') {
|
|
$result['steps'][] = '❌ Virheellinen banner';
|
|
fclose($fp);
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
break;
|
|
}
|
|
|
|
// EHLO
|
|
$ehlo = smtpCommand($fp, "EHLO " . gethostname());
|
|
$result['steps'][] = '📨 EHLO: ' . smtpCode($ehlo);
|
|
$result['ehlo_capabilities'] = trim($ehlo);
|
|
if (smtpCode($ehlo) !== '250') {
|
|
$result['steps'][] = '❌ EHLO hylätty';
|
|
fclose($fp);
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
break;
|
|
}
|
|
|
|
// STARTTLS
|
|
if ($encryption === 'tls') {
|
|
$resp = smtpCommand($fp, "STARTTLS");
|
|
$result['steps'][] = '🔒 STARTTLS: ' . smtpCode($resp);
|
|
if (smtpCode($resp) !== '220') {
|
|
$result['steps'][] = '❌ STARTTLS hylätty: ' . trim($resp);
|
|
fclose($fp);
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
break;
|
|
}
|
|
$crypto = @stream_socket_enable_crypto($fp, true,
|
|
STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT);
|
|
if (!$crypto) {
|
|
$result['steps'][] = '❌ TLS-neuvottelu epäonnistui';
|
|
fclose($fp);
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
break;
|
|
}
|
|
$result['steps'][] = '✅ TLS OK';
|
|
// Re-EHLO
|
|
$ehlo = smtpCommand($fp, "EHLO " . gethostname());
|
|
$result['steps'][] = '📨 EHLO2: ' . smtpCode($ehlo);
|
|
$result['ehlo_capabilities_tls'] = trim($ehlo);
|
|
}
|
|
|
|
// AUTH PLAIN
|
|
$cred = base64_encode("\0{$effectiveUser}\0{$effectivePass}");
|
|
$resp = smtpCommand($fp, "AUTH PLAIN {$cred}");
|
|
$result['steps'][] = '🔑 AUTH PLAIN: ' . trim($resp);
|
|
if (smtpCode($resp) === '235') {
|
|
$result['steps'][] = '✅ Kirjautuminen onnistui (AUTH PLAIN)!';
|
|
smtpCommand($fp, "QUIT");
|
|
fclose($fp);
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
break;
|
|
}
|
|
|
|
// AUTH LOGIN fallback
|
|
$resp = smtpCommand($fp, "AUTH LOGIN");
|
|
$result['steps'][] = '🔑 AUTH LOGIN: ' . smtpCode($resp);
|
|
if (smtpCode($resp) === '334') {
|
|
$resp = smtpCommand($fp, base64_encode($effectiveUser));
|
|
$result['steps'][] = '🔑 Käyttäjä: ' . smtpCode($resp);
|
|
if (smtpCode($resp) === '334') {
|
|
$resp = smtpCommand($fp, base64_encode($effectivePass));
|
|
$result['steps'][] = '🔑 Salasana: ' . trim($resp);
|
|
if (smtpCode($resp) === '235') {
|
|
$result['steps'][] = '✅ Kirjautuminen onnistui (AUTH LOGIN)!';
|
|
smtpCommand($fp, "QUIT");
|
|
fclose($fp);
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
$result['steps'][] = "❌ Kirjautuminen epäonnistui (käyttäjä: {$effectiveUser}, salasanan pituus: " . strlen($effectivePass) . ")";
|
|
smtpCommand($fp, "QUIT");
|
|
fclose($fp);
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
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'] ?? ''),
|
|
'smtp_host' => trim($input['smtp_host'] ?? ''),
|
|
'smtp_port' => intval($input['smtp_port'] ?? 587),
|
|
'smtp_user' => trim($input['smtp_user'] ?? ''),
|
|
'smtp_encryption' => trim($input['smtp_encryption'] ?? 'tls'),
|
|
'aktiivinen' => $input['aktiivinen'] ?? true,
|
|
'auto_reply_enabled' => !empty($input['auto_reply_enabled']),
|
|
'auto_reply_body' => trim($input['auto_reply_body'] ?? ''),
|
|
];
|
|
// Hae vanha mailbox salasanojen vertailua varten
|
|
$existingMb = dbGetMailbox($mb['id']);
|
|
// IMAP-salasana: jos ******** -> pidä vanha, muuten päivitä
|
|
if (isset($input['imap_password']) && $input['imap_password'] !== '********') {
|
|
$mb['imap_password'] = $input['imap_password'];
|
|
} else {
|
|
$mb['imap_password'] = $existingMb ? ($existingMb['imap_password'] ?? '') : '';
|
|
}
|
|
// SMTP-salasana: jos ******** -> pidä vanha, muuten päivitä
|
|
if (isset($input['smtp_password']) && $input['smtp_password'] !== '********') {
|
|
$mb['smtp_password'] = $input['smtp_password'];
|
|
} else {
|
|
$mb['smtp_password'] = $existingMb ? ($existingMb['smtp_password'] ?? '') : '';
|
|
}
|
|
// Jos SMTP-salasana on tyhjä mutta käyttäjä on sama kuin IMAP → kopioi IMAP-salasana
|
|
if (empty($mb['smtp_password']) && $mb['smtp_user'] === $mb['imap_user']) {
|
|
$mb['smtp_password'] = $mb['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;
|
|
|
|
// ==================== DOKUMENTIT ====================
|
|
|
|
case 'documents':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
try {
|
|
$customerId = $_GET['customer_id'] ?? null;
|
|
$docs = dbLoadDocuments($companyId, $customerId ?: null);
|
|
echo json_encode($docs);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Dokumenttien haku epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'document':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
try {
|
|
$docId = $_GET['id'] ?? '';
|
|
if (empty($docId)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Dokumentin ID puuttuu']);
|
|
break;
|
|
}
|
|
$doc = dbLoadDocument($docId);
|
|
if (!$doc || $doc['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Dokumenttia ei löytynyt']);
|
|
break;
|
|
}
|
|
echo json_encode($doc);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Dokumentin haku epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'document_save':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
try {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$input['created_by'] = $input['created_by'] ?? currentUser();
|
|
$input['muokkaaja'] = currentUser();
|
|
$id = dbSaveDocument($companyId, $input);
|
|
$doc = dbLoadDocument($id);
|
|
echo json_encode($doc);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Dokumentin tallennus epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'document_upload':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
try {
|
|
$docId = $_POST['document_id'] ?? '';
|
|
if (empty($docId)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Dokumentin ID puuttuu']);
|
|
break;
|
|
}
|
|
// Tarkista dokumentin olemassaolo
|
|
$doc = dbLoadDocument($docId);
|
|
if (!$doc || $doc['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Dokumenttia ei löytynyt']);
|
|
break;
|
|
}
|
|
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Tiedosto puuttuu tai lähetys epäonnistui']);
|
|
break;
|
|
}
|
|
$file = $_FILES['file'];
|
|
// Max 50MB
|
|
if ($file['size'] > 50 * 1024 * 1024) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Tiedosto on liian suuri (max 50MB)']);
|
|
break;
|
|
}
|
|
// Luo hakemisto
|
|
$docDir = DATA_DIR . '/companies/' . $companyId . '/documents/' . $docId;
|
|
if (!file_exists($docDir)) mkdir($docDir, 0755, true);
|
|
|
|
$nextVersion = (int)$doc['current_version'] + 1;
|
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
|
$safeFilename = $nextVersion . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $file['name']);
|
|
|
|
move_uploaded_file($file['tmp_name'], $docDir . '/' . $safeFilename);
|
|
|
|
$mimeType = '';
|
|
if (function_exists('finfo_open')) {
|
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
$mimeType = finfo_file($finfo, $docDir . '/' . $safeFilename);
|
|
finfo_close($finfo);
|
|
}
|
|
|
|
dbAddDocumentVersion($docId, [
|
|
'filename' => $safeFilename,
|
|
'original_name' => $file['name'],
|
|
'file_size' => $file['size'],
|
|
'mime_type' => $mimeType ?: ($file['type'] ?? ''),
|
|
'change_notes' => $_POST['change_notes'] ?? '',
|
|
'created_by' => currentUser()
|
|
]);
|
|
|
|
$updatedDoc = dbLoadDocument($docId);
|
|
echo json_encode($updatedDoc);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Tiedoston lataus epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'document_download':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
try {
|
|
$docId = $_GET['id'] ?? '';
|
|
$versionNum = (int)($_GET['version'] ?? 0);
|
|
if (empty($docId)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Dokumentin ID puuttuu']);
|
|
break;
|
|
}
|
|
$doc = dbLoadDocument($docId);
|
|
if (!$doc || $doc['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Dokumenttia ei löytynyt']);
|
|
break;
|
|
}
|
|
// Jos versiota ei annettu, käytä nykyistä
|
|
if ($versionNum <= 0) $versionNum = (int)$doc['current_version'];
|
|
$version = dbGetDocumentVersion($docId, $versionNum);
|
|
if (!$version) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Versiota ei löytynyt']);
|
|
break;
|
|
}
|
|
$filePath = DATA_DIR . '/companies/' . $companyId . '/documents/' . $docId . '/' . $version['filename'];
|
|
if (!file_exists($filePath)) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tiedostoa ei löytynyt palvelimelta']);
|
|
break;
|
|
}
|
|
header('Content-Type: ' . ($version['mime_type'] ?: 'application/octet-stream'));
|
|
header('Content-Disposition: attachment; filename="' . $version['original_name'] . '"');
|
|
header('Content-Length: ' . filesize($filePath));
|
|
readfile($filePath);
|
|
exit;
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Lataus epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'document_restore':
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
try {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$docId = $input['document_id'] ?? '';
|
|
$versionId = $input['version_id'] ?? '';
|
|
if (empty($docId) || empty($versionId)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Parametrit puuttuvat']);
|
|
break;
|
|
}
|
|
$doc = dbLoadDocument($docId);
|
|
if (!$doc || $doc['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Dokumenttia ei löytynyt']);
|
|
break;
|
|
}
|
|
// Hae vanha versio ja kopioi tiedosto
|
|
$oldVersion = null;
|
|
foreach ($doc['versions'] as $v) {
|
|
if ($v['id'] === $versionId) { $oldVersion = $v; break; }
|
|
}
|
|
if (!$oldVersion) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Versiota ei löytynyt']);
|
|
break;
|
|
}
|
|
// Kopioi tiedosto uudella nimellä
|
|
$docDir = DATA_DIR . '/companies/' . $companyId . '/documents/' . $docId;
|
|
$oldFilePath = $docDir . '/' . $oldVersion['filename'];
|
|
if (file_exists($oldFilePath)) {
|
|
$nextVersion = (int)$doc['current_version'] + 1;
|
|
$newFilename = $nextVersion . '_' . $oldVersion['original_name'];
|
|
$newFilename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $newFilename);
|
|
copy($oldFilePath, $docDir . '/' . $newFilename);
|
|
}
|
|
$result = dbRestoreDocumentVersion($docId, $versionId, currentUser());
|
|
if ($result === null) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Palautus epäonnistui']);
|
|
break;
|
|
}
|
|
$updatedDoc = dbLoadDocument($docId);
|
|
echo json_encode($updatedDoc);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Palautus epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'document_delete':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
try {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$docId = $input['id'] ?? '';
|
|
$doc = dbLoadDocument($docId);
|
|
if (!$doc || $doc['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Dokumenttia ei löytynyt']);
|
|
break;
|
|
}
|
|
// Salli poisto adminille tai dokumentin luojalle
|
|
if (!isCompanyAdmin() && $doc['created_by'] !== currentUser()) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Ei oikeutta poistaa']);
|
|
break;
|
|
}
|
|
// Poista tiedostot levyltä
|
|
$docDir = DATA_DIR . '/companies/' . $companyId . '/documents/' . $docId;
|
|
if (is_dir($docDir)) {
|
|
$files = glob($docDir . '/*');
|
|
foreach ($files as $f) { if (is_file($f)) unlink($f); }
|
|
rmdir($docDir);
|
|
}
|
|
dbDeleteDocument($docId);
|
|
echo json_encode(['success' => true]);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Poisto epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
// ==================== DOKUMENTTIKANSIOT ====================
|
|
|
|
case 'document_folders':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
$customerId = $_GET['customer_id'] ?? null;
|
|
echo json_encode(dbLoadFolders($companyId, $customerId));
|
|
break;
|
|
|
|
case 'document_folder_save':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
if (empty($input['name'])) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Kansion nimi puuttuu']);
|
|
break;
|
|
}
|
|
$input['created_by'] = currentUser();
|
|
$id = dbSaveFolder($companyId, $input);
|
|
echo json_encode(['id' => $id, 'name' => $input['name']]);
|
|
break;
|
|
|
|
case 'document_folder_delete':
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$result = dbDeleteFolder($companyId, $input['id'] ?? '');
|
|
echo json_encode(['ok' => $result]);
|
|
break;
|
|
|
|
case 'document_content_save':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
try {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$docId = $input['document_id'] ?? '';
|
|
$doc = dbLoadDocument($docId);
|
|
if (!$doc || $doc['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Dokumenttia ei löytynyt']);
|
|
break;
|
|
}
|
|
dbAddDocumentVersion($docId, [
|
|
'content' => $input['content'] ?? '',
|
|
'change_notes' => $input['change_notes'] ?? '',
|
|
'created_by' => currentUser(),
|
|
'filename' => '',
|
|
'original_name' => '',
|
|
'file_size' => strlen($input['content'] ?? ''),
|
|
'mime_type' => 'text/plain'
|
|
]);
|
|
$updated = dbLoadDocument($docId);
|
|
echo json_encode($updated);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Tallennus epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'document_move':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
try {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$docId = $input['document_id'] ?? '';
|
|
$folderId = $input['folder_id'] ?? null;
|
|
$doc = dbLoadDocument($docId);
|
|
if (!$doc || $doc['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Dokumenttia ei löytynyt']);
|
|
break;
|
|
}
|
|
_dbExecute("UPDATE documents SET folder_id = ? WHERE id = ?", [$folderId ?: null, $docId]);
|
|
echo json_encode(['ok' => true]);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Siirto epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
// ==================== NETADMIN ====================
|
|
|
|
case 'netadmin_connections':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
try {
|
|
$connections = dbLoadAllConnections($companyId);
|
|
// Hae myös laitteet ja IPAM-tiedot aggregointia varten
|
|
$devices = _dbFetchAll("SELECT id, nimi, hallintaosoite, site_id, malli, ping_status FROM devices WHERE company_id = ?", [$companyId]);
|
|
$deviceMap = [];
|
|
foreach ($devices as $d) { $deviceMap[$d['nimi']] = $d; }
|
|
|
|
// Hae IPAM VLANit, IP:t ja subnetit valikkoja varten
|
|
$ipamAll = dbLoadIpam($companyId);
|
|
$vlans = array_values(array_filter($ipamAll, fn($e) => $e['tyyppi'] === 'vlan'));
|
|
$ips = array_values(array_filter($ipamAll, fn($e) => $e['tyyppi'] === 'ip' || $e['tyyppi'] === 'subnet'));
|
|
|
|
// Rikasta liittymädata laitetiedoilla ja IPAM-tiedoilla
|
|
// Rakenna IPAM-lookup asiakkaan nimen perusteella
|
|
$ipamByCustomer = [];
|
|
foreach ($ipamAll as $entry) {
|
|
$asiakas = $entry['asiakas'] ?? '';
|
|
if ($asiakas) {
|
|
$ipamByCustomer[$asiakas][] = $entry;
|
|
}
|
|
}
|
|
|
|
foreach ($connections as &$conn) {
|
|
$deviceName = $conn['laite'] ?? '';
|
|
if ($deviceName && isset($deviceMap[$deviceName])) {
|
|
$conn['device_info'] = $deviceMap[$deviceName];
|
|
}
|
|
// Auto-populate VLAN ja IP IPAMista asiakkaan nimen perusteella
|
|
$custName = $conn['customer_name'] ?? '';
|
|
$ipamEntries = $ipamByCustomer[$custName] ?? [];
|
|
$conn['ipam_vlans'] = [];
|
|
$conn['ipam_ips'] = [];
|
|
foreach ($ipamEntries as $ie) {
|
|
if ($ie['tyyppi'] === 'vlan') {
|
|
$conn['ipam_vlans'][] = ['vlan_id' => $ie['vlan_id'], 'nimi' => $ie['nimi'], 'site_name' => $ie['site_name'] ?? ''];
|
|
} elseif ($ie['tyyppi'] === 'ip' || $ie['tyyppi'] === 'subnet') {
|
|
$conn['ipam_ips'][] = ['verkko' => $ie['verkko'], 'nimi' => $ie['nimi'], 'tila' => $ie['tila'], 'site_name' => $ie['site_name'] ?? ''];
|
|
}
|
|
}
|
|
}
|
|
|
|
echo json_encode([
|
|
'connections' => $connections,
|
|
'total' => count($connections),
|
|
'devices' => $devices,
|
|
'vlans' => $vlans,
|
|
'ips' => $ips
|
|
]);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Liittymien haku epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'netadmin_connection':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
try {
|
|
$connId = (int)($_GET['id'] ?? 0);
|
|
if (!$connId) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Liittymän ID puuttuu']);
|
|
break;
|
|
}
|
|
$conn = dbLoadConnection($connId);
|
|
if (!$conn || $conn['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Liittymää ei löytynyt']);
|
|
break;
|
|
}
|
|
// Lisää IPAM-data asiakkaan nimen perusteella
|
|
$ipamAll = dbLoadIpam($companyId);
|
|
$custName = $conn['customer_name'] ?? '';
|
|
$conn['ipam_vlans'] = [];
|
|
$conn['ipam_ips'] = [];
|
|
if ($custName) {
|
|
foreach ($ipamAll as $ie) {
|
|
$asiakas = $ie['asiakas'] ?? '';
|
|
if ($asiakas !== $custName) continue;
|
|
if ($ie['tyyppi'] === 'vlan') {
|
|
$conn['ipam_vlans'][] = ['vlan_id' => $ie['vlan_id'], 'nimi' => $ie['nimi'], 'site_name' => $ie['site_name'] ?? ''];
|
|
} elseif ($ie['tyyppi'] === 'ip' || $ie['tyyppi'] === 'subnet') {
|
|
$conn['ipam_ips'][] = ['verkko' => $ie['verkko'], 'nimi' => $ie['nimi'], 'tila' => $ie['tila'], 'site_name' => $ie['site_name'] ?? ''];
|
|
}
|
|
}
|
|
}
|
|
echo json_encode($conn);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Liittymän haku epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'netadmin_connection_update':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
try {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$connId = (int)($input['id'] ?? 0);
|
|
if (!$connId) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Liittymän ID puuttuu']);
|
|
break;
|
|
}
|
|
$conn = dbLoadConnection($connId);
|
|
if (!$conn || $conn['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Liittymää ei löytynyt']);
|
|
break;
|
|
}
|
|
// Automaattinen IPAM-tilan päivitys kun IP muuttuu
|
|
$oldIp = trim($conn['ip'] ?? '');
|
|
$newIp = trim($input['ip'] ?? '');
|
|
if ($oldIp !== $newIp) {
|
|
$ipamAll = dbLoadIpam($companyId);
|
|
// Vapauta vanha IP
|
|
if ($oldIp) {
|
|
foreach ($ipamAll as $entry) {
|
|
if ($entry['tyyppi'] === 'ip' && $entry['verkko'] === $oldIp && $entry['tila'] === 'varattu') {
|
|
$entry['tila'] = 'vapaa';
|
|
$entry['asiakas'] = '';
|
|
$entry['muokattu'] = date('Y-m-d H:i:s');
|
|
$entry['muokkaaja'] = currentUser();
|
|
dbSaveIpam($companyId, $entry);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Varaa uusi IP
|
|
if ($newIp) {
|
|
$customerName = $conn['customer_name'] ?? '';
|
|
foreach ($ipamAll as $entry) {
|
|
if ($entry['tyyppi'] === 'ip' && $entry['verkko'] === $newIp) {
|
|
$entry['tila'] = 'varattu';
|
|
$entry['asiakas'] = $customerName;
|
|
$entry['muokattu'] = date('Y-m-d H:i:s');
|
|
$entry['muokkaaja'] = currentUser();
|
|
dbSaveIpam($companyId, $entry);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
dbUpdateConnection($connId, $input);
|
|
$updated = dbLoadConnection($connId);
|
|
echo json_encode($updated);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Liittymän päivitys epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
// ==================== LAITETILAT ====================
|
|
|
|
case 'laitetilat':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
try {
|
|
$tilat = dbLoadLaitetilat($companyId);
|
|
echo json_encode($tilat);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Laitetilojen haku epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'laitetila':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
try {
|
|
$tilaId = $_GET['id'] ?? '';
|
|
if (empty($tilaId)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Laitetilan ID puuttuu']);
|
|
break;
|
|
}
|
|
$tila = dbLoadLaitetila($tilaId);
|
|
if (!$tila || $tila['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Laitetilaa ei löytynyt']);
|
|
break;
|
|
}
|
|
echo json_encode($tila);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Laitetilan haku epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'laitetila_save':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
try {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
if (empty($input['nimi'])) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Laitetilan nimi puuttuu']);
|
|
break;
|
|
}
|
|
$input['muokkaaja'] = currentUser();
|
|
$id = dbSaveLaitetila($companyId, $input);
|
|
$tila = dbLoadLaitetila($id);
|
|
echo json_encode($tila);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Laitetilan tallennus epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'laitetila_delete':
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
try {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$tilaId = $input['id'] ?? '';
|
|
if (empty($tilaId)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Laitetilan ID puuttuu']);
|
|
break;
|
|
}
|
|
$tila = dbLoadLaitetila($tilaId);
|
|
if (!$tila || $tila['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Laitetilaa ei löytynyt']);
|
|
break;
|
|
}
|
|
// Poista tiedostot levyltä
|
|
$tilaDir = DATA_DIR . '/companies/' . $companyId . '/laitetilat/' . $tilaId;
|
|
if (is_dir($tilaDir)) {
|
|
$items = glob($tilaDir . '/{,.}*', GLOB_BRACE);
|
|
foreach ($items as $item) {
|
|
if (is_file($item)) @unlink($item);
|
|
}
|
|
@rmdir($tilaDir);
|
|
}
|
|
dbDeleteLaitetila($tilaId);
|
|
dbAddLog($companyId, currentUser(), 'laitetila_delete', $tilaId, $tila['nimi'] ?? '', 'Poisti laitetilan');
|
|
echo json_encode(['success' => true]);
|
|
} catch (\Throwable $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Poisto epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'laitetila_file_upload':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
try {
|
|
$tilaId = $_POST['laitetila_id'] ?? '';
|
|
if (empty($tilaId)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Laitetilan ID puuttuu']);
|
|
break;
|
|
}
|
|
$tila = dbLoadLaitetila($tilaId);
|
|
if (!$tila || $tila['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Laitetilaa ei löytynyt']);
|
|
break;
|
|
}
|
|
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Tiedosto puuttuu tai lähetys epäonnistui']);
|
|
break;
|
|
}
|
|
$file = $_FILES['file'];
|
|
if ($file['size'] > 50 * 1024 * 1024) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Tiedosto on liian suuri (max 50MB)']);
|
|
break;
|
|
}
|
|
$tilaDir = DATA_DIR . '/companies/' . $companyId . '/laitetilat/' . $tilaId;
|
|
if (!file_exists($tilaDir)) mkdir($tilaDir, 0755, true);
|
|
|
|
$safeFilename = time() . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $file['name']);
|
|
move_uploaded_file($file['tmp_name'], $tilaDir . '/' . $safeFilename);
|
|
|
|
$mimeType = '';
|
|
if (function_exists('finfo_open')) {
|
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
$mimeType = finfo_file($finfo, $tilaDir . '/' . $safeFilename);
|
|
finfo_close($finfo);
|
|
}
|
|
|
|
dbAddLaitetilaFile($tilaId, [
|
|
'filename' => $safeFilename,
|
|
'original_name' => $file['name'],
|
|
'file_size' => $file['size'],
|
|
'mime_type' => $mimeType ?: ($file['type'] ?? ''),
|
|
'description' => $_POST['description'] ?? '',
|
|
'created_by' => currentUser()
|
|
]);
|
|
|
|
$updatedTila = dbLoadLaitetila($tilaId);
|
|
echo json_encode($updatedTila);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Tiedoston lataus epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'laitetila_file_delete':
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
try {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$fileId = $input['id'] ?? '';
|
|
$file = dbDeleteLaitetilaFile($fileId);
|
|
if ($file) {
|
|
$filePath = DATA_DIR . '/companies/' . $file['company_id'] . '/laitetilat/' . $file['laitetila_id'] . '/' . $file['filename'];
|
|
if (file_exists($filePath)) unlink($filePath);
|
|
}
|
|
echo json_encode(['success' => true]);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Tiedoston poisto epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'laitetila_file_download':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
try {
|
|
$tilaId = $_GET['laitetila_id'] ?? '';
|
|
$fileId = $_GET['file_id'] ?? '';
|
|
if (empty($tilaId) || empty($fileId)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Parametrit puuttuvat']);
|
|
break;
|
|
}
|
|
$tila = dbLoadLaitetila($tilaId);
|
|
if (!$tila || $tila['company_id'] !== $companyId) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Laitetilaa ei löytynyt']);
|
|
break;
|
|
}
|
|
$targetFile = null;
|
|
foreach ($tila['files'] as $f) {
|
|
if ($f['id'] === $fileId) { $targetFile = $f; break; }
|
|
}
|
|
if (!$targetFile) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tiedostoa ei löytynyt']);
|
|
break;
|
|
}
|
|
$filePath = DATA_DIR . '/companies/' . $companyId . '/laitetilat/' . $tilaId . '/' . $targetFile['filename'];
|
|
if (!file_exists($filePath)) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tiedostoa ei löytynyt palvelimelta']);
|
|
break;
|
|
}
|
|
header('Content-Type: ' . ($targetFile['mime_type'] ?: 'application/octet-stream'));
|
|
header('Content-Disposition: attachment; filename="' . $targetFile['original_name'] . '"');
|
|
header('Content-Length: ' . filesize($filePath));
|
|
readfile($filePath);
|
|
exit;
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Lataus epäonnistui: ' . $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
// ==================== INTEGRAATIOT ====================
|
|
|
|
case 'integrations':
|
|
$companyId = requireCompany();
|
|
$integrations = dbLoadIntegrations($companyId);
|
|
// Piilota tokenit/salasanat
|
|
foreach ($integrations as &$integ) {
|
|
if ($integ['config']) {
|
|
$cfg = is_string($integ['config']) ? json_decode($integ['config'], true) : $integ['config'];
|
|
if (isset($cfg['token'])) $cfg['token'] = str_repeat('*', 8);
|
|
$integ['config'] = $cfg;
|
|
}
|
|
}
|
|
echo json_encode($integrations);
|
|
break;
|
|
|
|
case 'integration_save':
|
|
$companyId = requireCompany();
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$type = $input['type'] ?? '';
|
|
$enabled = (bool)($input['enabled'] ?? false);
|
|
$config = $input['config'] ?? null;
|
|
if (!$type) { http_response_code(400); echo json_encode(['error' => 'Tyyppi puuttuu']); break; }
|
|
|
|
// Jos config ei lähetetty tai on tyhjä, säilytä vanha config (toggle ei ylikirjoita asetuksia)
|
|
$old = dbGetIntegration($companyId, $type);
|
|
if ($config === null || (is_array($config) && empty($config))) {
|
|
$config = ($old && isset($old['config'])) ? (is_string($old['config']) ? json_decode($old['config'], true) : $old['config']) : [];
|
|
} else {
|
|
// Jos token on maskattua (********), säilytä vanha
|
|
if (isset($config['token']) && preg_match('/^\*+$/', $config['token'])) {
|
|
if ($old && isset($old['config']['token'])) {
|
|
$config['token'] = is_string($old['config']) ? json_decode($old['config'], true)['token'] : $old['config']['token'];
|
|
}
|
|
}
|
|
}
|
|
|
|
dbSaveIntegration($companyId, $type, $enabled, $config);
|
|
echo json_encode(['ok' => true]);
|
|
break;
|
|
|
|
case 'integration_test':
|
|
$companyId = requireCompany();
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$type = $input['type'] ?? $_GET['type'] ?? '';
|
|
$integ = dbGetIntegration($companyId, $type);
|
|
if (!$integ) { http_response_code(404); echo json_encode(['error' => 'Integraatiota ei löydy']); break; }
|
|
|
|
try {
|
|
if ($type === 'zammad') {
|
|
$cfg = $integ['config'];
|
|
$z = new ZammadClient($cfg['url'], $cfg['token']);
|
|
$result = $z->testConnection();
|
|
echo json_encode($result);
|
|
} else {
|
|
echo json_encode(['error' => 'Tuntematon tyyppi']);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'zammad_groups':
|
|
$companyId = requireCompany();
|
|
$integ = dbGetIntegration($companyId, 'zammad');
|
|
if (!$integ || !$integ['enabled']) { http_response_code(400); echo json_encode(['error' => 'Zammad ei käytössä']); break; }
|
|
try {
|
|
$z = new ZammadClient($integ['config']['url'], $integ['config']['token']);
|
|
echo json_encode($z->getGroups());
|
|
} catch (\Throwable $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'zammad_sync':
|
|
$companyId = requireCompany();
|
|
$integ = dbGetIntegration($companyId, 'zammad');
|
|
if (!$integ || !$integ['enabled']) { http_response_code(400); echo json_encode(['error' => 'Zammad ei käytössä']); break; }
|
|
|
|
// full=1 pakottaa täyden synkkauksen (esim. "Hae postit" -napista)
|
|
$fullSync = ($_GET['full'] ?? $_POST['full'] ?? '') === '1';
|
|
$input = json_decode(file_get_contents('php://input'), true) ?: [];
|
|
if (!empty($input['full'])) $fullSync = true;
|
|
|
|
try {
|
|
$cfg = $integ['config'];
|
|
$z = new ZammadClient($cfg['url'], $cfg['token']);
|
|
$groupIds = $cfg['group_ids'] ?? [];
|
|
$syncedGroupNames = $cfg['group_names'] ?? [];
|
|
|
|
// Hae viimeisin synkkausaika — inkrementaalinen haku
|
|
$lastSync = null;
|
|
if (!$fullSync) {
|
|
$row = _dbFetchOne("SELECT MAX(updated) as last_updated FROM tickets WHERE company_id = ? AND source = 'zammad'", [$companyId]);
|
|
if ($row && $row['last_updated']) {
|
|
// Hae 5 min marginaalilla ettei menetä mitään
|
|
$lastSync = date('Y-m-d\TH:i:s', strtotime($row['last_updated']) - 300);
|
|
}
|
|
}
|
|
|
|
// Hae tikettejä Zammadista
|
|
$allTickets = [];
|
|
$page = 1;
|
|
$maxPages = $fullSync ? 10 : 3;
|
|
do {
|
|
$batch = $z->getTickets($groupIds, $page, 100, $lastSync);
|
|
if (empty($batch)) break;
|
|
$allTickets = array_merge($allTickets, $batch);
|
|
$page++;
|
|
} while (count($batch) >= 100 && $page <= $maxPages);
|
|
|
|
$created = 0;
|
|
$updated = 0;
|
|
$messagesAdded = 0;
|
|
|
|
// Zammad state → intran status
|
|
$stateMap = [
|
|
'new' => 'uusi', 'open' => 'avoin', 'pending reminder' => 'odottaa',
|
|
'pending close' => 'odottaa', 'closed' => 'suljettu',
|
|
'merged' => 'suljettu', 'removed' => 'suljettu',
|
|
];
|
|
// Zammad priority → intran priority
|
|
$priorityMap = [
|
|
'1 low' => 'matala', '2 normal' => 'normaali', '3 high' => 'korkea',
|
|
];
|
|
|
|
foreach ($allTickets as $zt) {
|
|
$zammadId = (int)$zt['id'];
|
|
$existing = dbGetTicketByZammadId($companyId, $zammadId);
|
|
|
|
// Mäppää Zammad-ryhmä → intran tikettityyppi
|
|
$group = $zt['group'] ?? '';
|
|
$type = 'muu';
|
|
if (stripos($group, 'support') !== false || stripos($group, 'tech') !== false) $type = 'tekniikka';
|
|
elseif (stripos($group, 'sales') !== false) $type = 'laskutus';
|
|
elseif (stripos($group, 'abuse') !== false) $type = 'abuse';
|
|
elseif (stripos($group, 'domain') !== false) $type = 'muu';
|
|
elseif (stripos($group, 'noc') !== false) $type = 'vika';
|
|
|
|
$status = $stateMap[strtolower($zt['state'] ?? '')] ?? 'uusi';
|
|
$priority = $priorityMap[strtolower($zt['priority'] ?? '')] ?? 'normaali';
|
|
|
|
if (!$existing) {
|
|
// Uusi tiketti
|
|
$ticketId = substr(uniqid(), -8) . bin2hex(random_bytes(2));
|
|
$now = date('Y-m-d H:i:s');
|
|
_dbExecute(
|
|
"INSERT INTO tickets (id, company_id, subject, from_email, from_name, status, type, priority, zammad_ticket_id, zammad_group, source, ticket_number, created, updated)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
[$ticketId, $companyId, $zt['title'] ?? '', $zt['customer'] ?? '', $zt['customer'] ?? '',
|
|
$status, $type, $priority, $zammadId, $group, 'zammad', (int)($zt['number'] ?? 0),
|
|
$zt['created_at'] ? date('Y-m-d H:i:s', strtotime($zt['created_at'])) : $now,
|
|
$zt['updated_at'] ? date('Y-m-d H:i:s', strtotime($zt['updated_at'])) : $now]
|
|
);
|
|
$created++;
|
|
} else {
|
|
$ticketId = $existing['id'];
|
|
$zammadUpdated = date('Y-m-d H:i:s', strtotime($zt['updated_at'] ?? 'now'));
|
|
// Päivitä status/priority/group
|
|
_dbExecute(
|
|
"UPDATE tickets SET status = ?, type = ?, priority = ?, subject = ?, zammad_group = ?, updated = ? WHERE id = ?",
|
|
[$status, $type, $priority, $zt['title'] ?? '', $group, $zammadUpdated, $ticketId]
|
|
);
|
|
$updated++;
|
|
}
|
|
|
|
// Synkkaa artikkelit vain uusille tiketeille (ei jokaiselle — liian hidas)
|
|
// Olemassa olevien tikettien artikkelit haetaan on-demand kun käyttäjä avaa tiketin
|
|
if ($existing) continue;
|
|
|
|
try {
|
|
$articles = $z->getArticles($zammadId);
|
|
foreach ($articles as $art) {
|
|
if (($art['internal'] ?? false)) continue;
|
|
$artId = (int)$art['id'];
|
|
$existingMsg = dbGetMessageByZammadArticleId($ticketId, $artId);
|
|
if ($existingMsg) continue;
|
|
|
|
$msgId = substr(uniqid(), -8) . bin2hex(random_bytes(2));
|
|
$msgType = ($art['sender'] ?? '') === 'Customer' ? 'incoming' : 'outgoing';
|
|
$body = $art['body'] ?? '';
|
|
if (($art['content_type'] ?? '') === 'text/html') {
|
|
$body = strip_tags($body, '<br><p><div><a><b><i><strong><em><ul><ol><li>');
|
|
}
|
|
|
|
_dbExecute(
|
|
"INSERT INTO ticket_messages (id, ticket_id, type, from_email, from_name, body, timestamp, message_id, zammad_article_id)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
[$msgId, $ticketId, $msgType,
|
|
$art['from'] ?? '', $art['from'] ?? '',
|
|
$body,
|
|
$art['created_at'] ? date('Y-m-d H:i:s', strtotime($art['created_at'])) : date('Y-m-d H:i:s'),
|
|
$art['message_id'] ?? '',
|
|
$artId]
|
|
);
|
|
$messagesAdded++;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// Artikkelihaku epäonnistui — jatka silti
|
|
error_log("Zammad articles error for ticket $zammadId: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
echo json_encode([
|
|
'ok' => true,
|
|
'tickets_found' => count($allTickets),
|
|
'created' => $created,
|
|
'updated' => $updated,
|
|
'messages_added' => $messagesAdded,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
case 'zammad_reply':
|
|
$companyId = requireCompany();
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$ticketId = $input['ticket_id'] ?? '';
|
|
$body = $input['body'] ?? '';
|
|
if (!$ticketId || !$body) { http_response_code(400); echo json_encode(['error' => 'Tiketti ja viesti vaaditaan']); break; }
|
|
|
|
$ticket = _dbFetchOne("SELECT * FROM tickets WHERE id = ? AND company_id = ?", [$ticketId, $companyId]);
|
|
if (!$ticket || !$ticket['zammad_ticket_id']) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Tiketti ei ole Zammad-tiketti']);
|
|
break;
|
|
}
|
|
|
|
$integ = dbGetIntegration($companyId, 'zammad');
|
|
if (!$integ || !$integ['enabled']) { http_response_code(400); echo json_encode(['error' => 'Zammad ei käytössä']); break; }
|
|
|
|
try {
|
|
$z = new ZammadClient($integ['config']['url'], $integ['config']['token']);
|
|
$to = !empty($input['to']) ? trim($input['to']) : ($ticket['from_email'] ?? '');
|
|
$cc = !empty($input['cc']) ? trim($input['cc']) : '';
|
|
|
|
// Muunna uusi viesti HTML:ksi
|
|
$newMsgHtml = nl2br(htmlspecialchars($body, ENT_QUOTES, 'UTF-8'));
|
|
|
|
// Rakenna viestiketju (quoted thread) vastaukseen
|
|
$messages = _dbFetchAll(
|
|
"SELECT from_name, from_email, timestamp, body, type FROM ticket_messages WHERE ticket_id = ? ORDER BY timestamp DESC",
|
|
[$ticketId]
|
|
);
|
|
$quotedThread = '';
|
|
foreach ($messages as $msg) {
|
|
$sender = $msg['from_name'] ?: $msg['from_email'];
|
|
$date = date('d.m.Y H:i', strtotime($msg['timestamp']));
|
|
$msgBody = $msg['body'] ?: '';
|
|
$quotedThread .= '<br><div style="padding-left:0.5em;border-left:2px solid #ccc;color:#555;">'
|
|
. '<small><strong>' . htmlspecialchars($sender) . '</strong> — ' . $date . '</small><br>'
|
|
. '<div>' . $msgBody . '</div>'
|
|
. '</div>';
|
|
}
|
|
|
|
// Yhdistä: uusi viesti (HTML) + viestiketju
|
|
$fullBody = $newMsgHtml . $quotedThread;
|
|
|
|
$result = $z->createArticle(
|
|
(int)$ticket['zammad_ticket_id'],
|
|
$fullBody,
|
|
$to,
|
|
$ticket['subject'] ?? '',
|
|
'email',
|
|
$cc
|
|
);
|
|
|
|
// Tallenna myös paikalliseen tietokantaan (vain uusi viesti, ei ketjua)
|
|
$msgId = substr(uniqid(), -8) . bin2hex(random_bytes(2));
|
|
_dbExecute(
|
|
"INSERT INTO ticket_messages (id, ticket_id, type, from_email, from_name, body, timestamp, zammad_article_id)
|
|
VALUES (?, ?, 'outgoing', ?, ?, ?, ?, ?)",
|
|
[$msgId, $ticketId,
|
|
$result['from'] ?? '', $result['from'] ?? '',
|
|
$body, date('Y-m-d H:i:s'),
|
|
(int)($result['id'] ?? 0)]
|
|
);
|
|
|
|
echo json_encode(['ok' => true, 'message_id' => $msgId]);
|
|
} catch (\Throwable $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => $e->getMessage()]);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tuntematon toiminto']);
|
|
break;
|
|
}
|