Users can only log in from domains belonging to their assigned company. E.g. cuitunet users can only log in via intra.cuitunet.fi. Admin users can still log in from any domain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2329 lines
90 KiB
PHP
2329 lines
90 KiB
PHP
<?php
|
|
// Turvalliset session-asetukset
|
|
ini_set('session.cookie_httponly', 1);
|
|
ini_set('session.cookie_secure', 1);
|
|
ini_set('session.use_strict_mode', 1);
|
|
ini_set('session.cookie_samesite', 'Lax');
|
|
session_start();
|
|
|
|
require_once __DIR__ . '/db.php';
|
|
initDatabase();
|
|
|
|
header('Content-Type: application/json');
|
|
header('X-Content-Type-Options: nosniff');
|
|
|
|
define('DATA_DIR', __DIR__ . '/data');
|
|
// Dynaaminen SITE_URL domainin mukaan
|
|
define('SITE_URL', 'https://' . ($_SERVER['HTTP_HOST'] ?? 'intra.noxus.fi'));
|
|
|
|
// Sähköpostiasetukset (fallback)
|
|
define('MAIL_FROM', 'noreply@noxus.fi');
|
|
define('MAIL_FROM_NAME', 'Noxus Intra');
|
|
|
|
// Varmista data-kansio (tiedostoja varten)
|
|
if (!file_exists(DATA_DIR)) mkdir(DATA_DIR, 0755, true);
|
|
|
|
$method = $_SERVER['REQUEST_METHOD'];
|
|
$action = $_GET['action'] ?? '';
|
|
|
|
// ==================== HELPERS ====================
|
|
|
|
function requireAuth() {
|
|
if (!isset($_SESSION['user_id'])) {
|
|
http_response_code(401);
|
|
echo json_encode(['error' => 'Kirjaudu sisään']);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
function requireAdmin() {
|
|
requireAuth();
|
|
if (($_SESSION['role'] ?? '') !== 'admin') {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Vain ylläpitäjä voi tehdä tämän']);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
function currentUser(): string {
|
|
return $_SESSION['username'] ?? 'tuntematon';
|
|
}
|
|
|
|
function generateId(): string {
|
|
return bin2hex(random_bytes(8));
|
|
}
|
|
|
|
function generateToken(): string {
|
|
return bin2hex(random_bytes(32));
|
|
}
|
|
|
|
// ==================== MULTI-COMPANY ====================
|
|
|
|
function getCompanyDir(?string $companyId = null): string {
|
|
$id = $companyId ?? ($_SESSION['company_id'] ?? '');
|
|
if (empty($id) || !preg_match('/^[a-z0-9-]+$/', $id)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Yritystä ei ole valittu']);
|
|
exit;
|
|
}
|
|
$dir = DATA_DIR . '/companies/' . $id;
|
|
if (!file_exists($dir)) mkdir($dir, 0755, true);
|
|
return $dir;
|
|
}
|
|
|
|
function requireCompany(): string {
|
|
$companyId = $_SESSION['company_id'] ?? '';
|
|
if (empty($companyId)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Valitse ensin yritys']);
|
|
exit;
|
|
}
|
|
$userCompanies = $_SESSION['companies'] ?? [];
|
|
if (!in_array($companyId, $userCompanies)) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']);
|
|
exit;
|
|
}
|
|
return $companyId;
|
|
}
|
|
|
|
// Kuten requireCompany(), mutta sallii company_id:n overriden GET-parametrista
|
|
// Käytetään tiketti-endpointeissa jotta toisen yrityksen tikettejä voi avata
|
|
function requireCompanyOrParam(): string {
|
|
$paramCompany = $_GET['company_id'] ?? '';
|
|
if (!empty($paramCompany)) {
|
|
$userCompanies = $_SESSION['companies'] ?? [];
|
|
if (!in_array($paramCompany, $userCompanies)) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']);
|
|
exit;
|
|
}
|
|
$_SESSION['company_id'] = $paramCompany;
|
|
}
|
|
return requireCompany();
|
|
}
|
|
|
|
function companyFile(string $filename): string {
|
|
return getCompanyDir() . '/' . $filename;
|
|
}
|
|
|
|
function getClientIp(): string {
|
|
return $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
|
}
|
|
|
|
function normalizeAddress(string $addr): string {
|
|
$addr = strtolower(trim($addr));
|
|
$addr = preg_replace('/\s+/', ' ', $addr);
|
|
return $addr;
|
|
}
|
|
|
|
// ==================== EMAIL ====================
|
|
|
|
function sendMail(string $to, string $subject, string $htmlBody): bool {
|
|
$headers = "MIME-Version: 1.0\r\n";
|
|
$headers .= "Content-Type: text/html; charset=UTF-8\r\n";
|
|
$headers .= "From: " . MAIL_FROM_NAME . " <" . MAIL_FROM . ">\r\n";
|
|
$headers .= "Reply-To: " . MAIL_FROM . "\r\n";
|
|
return mail($to, $subject, $htmlBody, $headers, '-f ' . MAIL_FROM);
|
|
}
|
|
|
|
// ==================== IMAP CLIENT (socket-pohjainen, ei vaadi php-imap) ====================
|
|
|
|
class ImapClient {
|
|
private $socket = null;
|
|
private int $tagCounter = 0;
|
|
public string $lastError = '';
|
|
|
|
public function connect(array $config): bool {
|
|
$host = $config['imap_host'] ?? '';
|
|
$port = intval($config['imap_port'] ?? 993);
|
|
$user = $config['imap_user'] ?? '';
|
|
$pass = $config['imap_password'] ?? '';
|
|
$encryption = $config['imap_encryption'] ?? 'ssl';
|
|
|
|
if (empty($host) || empty($user) || empty($pass)) {
|
|
$this->lastError = 'IMAP-asetukset puuttuvat';
|
|
return false;
|
|
}
|
|
|
|
$prefix = ($encryption === 'ssl') ? 'ssl://' : 'tcp://';
|
|
$context = stream_context_create([
|
|
'ssl' => ['verify_peer' => false, 'verify_peer_name' => false]
|
|
]);
|
|
|
|
$this->socket = @stream_socket_client(
|
|
$prefix . $host . ':' . $port,
|
|
$errno, $errstr, 15,
|
|
STREAM_CLIENT_CONNECT, $context
|
|
);
|
|
|
|
if (!$this->socket) {
|
|
$this->lastError = "Yhteys epäonnistui: {$errstr} ({$errno})";
|
|
return false;
|
|
}
|
|
|
|
stream_set_timeout($this->socket, 30);
|
|
|
|
// Read greeting
|
|
$greeting = $this->readLine();
|
|
if (!$greeting || strpos($greeting, '* OK') === false) {
|
|
$this->lastError = 'Palvelin ei vastannut oikein: ' . $greeting;
|
|
$this->disconnect();
|
|
return false;
|
|
}
|
|
|
|
// STARTTLS if needed
|
|
if ($encryption === 'tls') {
|
|
$resp = $this->command('STARTTLS');
|
|
if (!$this->isOk($resp)) {
|
|
$this->lastError = 'STARTTLS epäonnistui';
|
|
$this->disconnect();
|
|
return false;
|
|
}
|
|
if (!stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
|
|
$this->lastError = 'TLS-neuvottelu epäonnistui';
|
|
$this->disconnect();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Login
|
|
$resp = $this->command('LOGIN "' . $this->escape($user) . '" "' . $this->escape($pass) . '"');
|
|
if (!$this->isOk($resp)) {
|
|
$this->lastError = 'Kirjautuminen epäonnistui: väärä tunnus tai salasana';
|
|
$this->disconnect();
|
|
return false;
|
|
}
|
|
|
|
// Select INBOX
|
|
$resp = $this->command('SELECT INBOX');
|
|
if (!$this->isOk($resp)) {
|
|
$this->lastError = 'INBOX:n avaus epäonnistui';
|
|
$this->disconnect();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function fetchMessages(int $limit = 50): array {
|
|
if (!$this->socket) return [];
|
|
|
|
// Get message count from STATUS
|
|
$resp = $this->command('STATUS INBOX (MESSAGES)');
|
|
$totalMessages = 0;
|
|
foreach ($resp as $line) {
|
|
if (preg_match('/MESSAGES\s+(\d+)/i', $line, $m)) {
|
|
$totalMessages = intval($m[1]);
|
|
}
|
|
}
|
|
if ($totalMessages === 0) return [];
|
|
|
|
$start = max(1, $totalMessages - $limit + 1);
|
|
$range = $start . ':' . $totalMessages;
|
|
|
|
// Fetch headers for range
|
|
$resp = $this->command("FETCH {$range} (BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE MESSAGE-ID IN-REPLY-TO REFERENCES)] BODY.PEEK[TEXT] FLAGS)");
|
|
|
|
$messages = [];
|
|
$current = null;
|
|
$headerBuf = '';
|
|
$bodyBuf = '';
|
|
$readingHeader = false;
|
|
$readingBody = false;
|
|
$headerBytesLeft = 0;
|
|
$bodyBytesLeft = 0;
|
|
|
|
// Simpler approach: fetch one-by-one for reliability
|
|
$messages = [];
|
|
for ($i = $totalMessages; $i >= $start; $i--) {
|
|
$msg = $this->fetchSingleMessage($i);
|
|
if ($msg) $messages[] = $msg;
|
|
}
|
|
|
|
return $messages;
|
|
}
|
|
|
|
private function fetchSingleMessage(int $num): ?array {
|
|
// Fetch headers
|
|
$resp = $this->command("FETCH {$num} BODY.PEEK[HEADER]");
|
|
$headerRaw = $this->extractLiteral($resp);
|
|
|
|
if (!$headerRaw) return null;
|
|
|
|
$headers = $this->parseHeaders($headerRaw);
|
|
$subject = $this->decodeMimeHeader($headers['subject'] ?? '');
|
|
$fromRaw = $headers['from'] ?? '';
|
|
$fromParsed = $this->parseFrom($fromRaw);
|
|
$messageId = trim($headers['message-id'] ?? '');
|
|
$inReplyTo = trim($headers['in-reply-to'] ?? '');
|
|
$references = trim($headers['references'] ?? '');
|
|
$dateStr = $headers['date'] ?? '';
|
|
$date = $dateStr ? @date('Y-m-d H:i:s', strtotime($dateStr)) : date('Y-m-d H:i:s');
|
|
if (!$date) $date = date('Y-m-d H:i:s');
|
|
|
|
// Fetch body (text part)
|
|
$body = $this->fetchBody($num);
|
|
|
|
return [
|
|
'subject' => $subject,
|
|
'from_email' => $fromParsed['email'],
|
|
'from_name' => $this->decodeMimeHeader($fromParsed['name']),
|
|
'message_id' => $messageId,
|
|
'in_reply_to' => $inReplyTo,
|
|
'references' => $references,
|
|
'date' => $date,
|
|
'body' => $body,
|
|
];
|
|
}
|
|
|
|
private function fetchBody(int $num): string {
|
|
// Try text/plain first via BODYSTRUCTURE
|
|
$resp = $this->command("FETCH {$num} BODYSTRUCTURE");
|
|
$structLine = implode(' ', $resp);
|
|
|
|
// Simple approach: fetch BODY[1] (usually text/plain in multipart)
|
|
// or BODY[TEXT] for simple messages
|
|
$resp = $this->command("FETCH {$num} BODY.PEEK[1]");
|
|
$body = $this->extractLiteral($resp);
|
|
|
|
if (!$body) {
|
|
// Fallback: full text
|
|
$resp = $this->command("FETCH {$num} BODY.PEEK[TEXT]");
|
|
$body = $this->extractLiteral($resp);
|
|
}
|
|
|
|
if (!$body) return '';
|
|
|
|
// Detect encoding from BODYSTRUCTURE
|
|
$encoding = '';
|
|
// Parse BODYSTRUCTURE for encoding (7BIT, BASE64, QUOTED-PRINTABLE)
|
|
if (preg_match('/"TEXT"\s+"PLAIN"\s+\([^)]*\)\s+NIL\s+NIL\s+"([^"]+)"/i', $structLine, $em)) {
|
|
$encoding = strtoupper($em[1]);
|
|
} elseif (preg_match('/BODY\[1\].*?"([^"]+)"/i', $structLine, $em)) {
|
|
$encoding = strtoupper($em[1]);
|
|
}
|
|
|
|
// Try to detect encoding from body content if not found
|
|
if (!$encoding) {
|
|
// Check if it looks like base64
|
|
if (preg_match('/^[A-Za-z0-9+\/=\s]+$/', trim($body)) && strlen(trim($body)) > 50) {
|
|
$decoded = @base64_decode($body, true);
|
|
if ($decoded !== false && strlen($decoded) > 0) {
|
|
// Verify it produces readable text
|
|
if (preg_match('/[\x20-\x7E\xC0-\xFF]/', $decoded)) {
|
|
$body = $decoded;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if ($encoding === 'BASE64') {
|
|
$body = base64_decode($body);
|
|
} elseif ($encoding === 'QUOTED-PRINTABLE') {
|
|
$body = quoted_printable_decode($body);
|
|
}
|
|
}
|
|
|
|
// Strip HTML if it looks like HTML
|
|
if (preg_match('/<html|<body|<div|<p\b/i', $body)) {
|
|
$body = strip_tags($body);
|
|
// Clean up whitespace
|
|
$body = preg_replace('/\n{3,}/', "\n\n", $body);
|
|
}
|
|
|
|
// Try charset conversion
|
|
if (preg_match('/charset[="\s]+([^\s;"]+)/i', $structLine, $cm)) {
|
|
$charset = strtolower(trim($cm[1], '"'));
|
|
if ($charset && $charset !== 'utf-8') {
|
|
$converted = @iconv($charset, 'UTF-8//IGNORE', $body);
|
|
if ($converted !== false) $body = $converted;
|
|
}
|
|
}
|
|
|
|
return trim($body);
|
|
}
|
|
|
|
private function parseHeaders(string $raw): array {
|
|
$headers = [];
|
|
$lines = explode("\n", str_replace("\r\n", "\n", $raw));
|
|
$lastKey = '';
|
|
foreach ($lines as $line) {
|
|
if ($line === '' || $line === "\r") continue;
|
|
// Continuation line (starts with space/tab)
|
|
if (preg_match('/^[\s\t]+(.+)/', $line, $m)) {
|
|
if ($lastKey && isset($headers[$lastKey])) {
|
|
$headers[$lastKey] .= ' ' . trim($m[1]);
|
|
}
|
|
continue;
|
|
}
|
|
if (preg_match('/^([A-Za-z\-]+):\s*(.*)$/', $line, $m)) {
|
|
$key = strtolower($m[1]);
|
|
$headers[$key] = trim($m[2]);
|
|
$lastKey = $key;
|
|
}
|
|
}
|
|
return $headers;
|
|
}
|
|
|
|
private function parseFrom(string $from): array {
|
|
$from = trim($from);
|
|
if (preg_match('/^"?([^"<]*)"?\s*<([^>]+)>/', $from, $m)) {
|
|
return ['name' => trim($m[1], ' "'), 'email' => trim($m[2])];
|
|
}
|
|
if (preg_match('/^([^\s@]+@[^\s@]+)/', $from, $m)) {
|
|
return ['name' => '', 'email' => $m[1]];
|
|
}
|
|
return ['name' => '', 'email' => $from];
|
|
}
|
|
|
|
private function 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 sendTicketMail(string $to, string $subject, string $body, string $inReplyTo = '', string $references = '', ?array $mailbox = null): bool {
|
|
$fromEmail = $mailbox['smtp_from_email'] ?? $mailbox['imap_user'] ?? MAIL_FROM;
|
|
$fromName = $mailbox['smtp_from_name'] ?? $mailbox['nimi'] ?? 'Asiakaspalvelu';
|
|
|
|
$headers = "MIME-Version: 1.0\r\n";
|
|
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
|
$headers .= "From: {$fromName} <{$fromEmail}>\r\n";
|
|
$headers .= "Reply-To: {$fromEmail}\r\n";
|
|
if ($inReplyTo) {
|
|
$headers .= "In-Reply-To: {$inReplyTo}\r\n";
|
|
$headers .= "References: " . ($references ? $references . ' ' : '') . $inReplyTo . "\r\n";
|
|
}
|
|
return mail($to, $subject, $body, $headers, '-f ' . $fromEmail);
|
|
}
|
|
|
|
function parseLiittymat(array $input): array {
|
|
$liittymat = [];
|
|
foreach (($input['liittymat'] ?? []) as $l) {
|
|
$liittymat[] = [
|
|
'asennusosoite' => trim($l['asennusosoite'] ?? ''),
|
|
'postinumero' => trim($l['postinumero'] ?? ''),
|
|
'kaupunki' => trim($l['kaupunki'] ?? ''),
|
|
'liittymanopeus' => trim($l['liittymanopeus'] ?? ''),
|
|
'hinta' => floatval($l['hinta'] ?? 0),
|
|
'sopimuskausi' => trim($l['sopimuskausi'] ?? ''),
|
|
'alkupvm' => trim($l['alkupvm'] ?? ''),
|
|
];
|
|
}
|
|
if (empty($liittymat)) {
|
|
$liittymat[] = ['asennusosoite' => '', 'postinumero' => '', 'kaupunki' => '', 'liittymanopeus' => '', 'hinta' => 0, 'sopimuskausi' => '', 'alkupvm' => ''];
|
|
}
|
|
return $liittymat;
|
|
}
|
|
|
|
// ==================== ROUTES ====================
|
|
|
|
switch ($action) {
|
|
|
|
// ---------- SAATAVUUS (julkinen API) ----------
|
|
case 'saatavuus':
|
|
$providedKey = $_GET['key'] ?? ($_SERVER['HTTP_X_API_KEY'] ?? '');
|
|
if (empty($providedKey)) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'API-avain puuttuu']);
|
|
break;
|
|
}
|
|
|
|
// Etsi yritys jonka API-avain täsmää
|
|
$matchedCompany = dbGetCompanyByApiKey($providedKey);
|
|
|
|
if (!$matchedCompany) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Virheellinen API-avain']);
|
|
break;
|
|
}
|
|
|
|
// CORS - yrityskohtaiset originit
|
|
$allowedOrigins = dbGetCompanyCorsOrigins($matchedCompany['id']);
|
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
if (in_array($origin, $allowedOrigins)) {
|
|
header("Access-Control-Allow-Origin: $origin");
|
|
header('Access-Control-Allow-Methods: GET, OPTIONS');
|
|
header('Access-Control-Allow-Headers: Content-Type, X-Api-Key');
|
|
}
|
|
if ($method === 'OPTIONS') { http_response_code(204); break; }
|
|
|
|
// Parametrit
|
|
$queryOsoite = normalizeAddress($_GET['osoite'] ?? '');
|
|
$queryPostinumero = trim($_GET['postinumero'] ?? '');
|
|
$queryKaupunki = strtolower(trim($_GET['kaupunki'] ?? ''));
|
|
|
|
if (empty($queryOsoite) || empty($queryPostinumero) || empty($queryKaupunki)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Anna osoite, postinumero ja kaupunki']);
|
|
break;
|
|
}
|
|
|
|
// Hae VAIN tämän yrityksen asiakkaista
|
|
$customers = dbLoadCustomers($matchedCompany['id']);
|
|
$found = false;
|
|
foreach ($customers as $c) {
|
|
foreach ($c['liittymat'] ?? [] as $l) {
|
|
$addr = normalizeAddress($l['asennusosoite'] ?? '');
|
|
$zip = trim($l['postinumero'] ?? '');
|
|
$city = strtolower(trim($l['kaupunki'] ?? ''));
|
|
if ($zip === $queryPostinumero && $city === $queryKaupunki) {
|
|
if (!empty($addr) && !empty($queryOsoite)) {
|
|
if (strpos($addr, $queryOsoite) !== false || strpos($queryOsoite, $addr) !== false) {
|
|
$found = true;
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
echo json_encode(['saatavilla' => $found]);
|
|
break;
|
|
|
|
// ---------- CONFIG (admin, yrityskohtainen) ----------
|
|
case 'config':
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
echo json_encode([
|
|
'api_key' => dbGetCompanyApiKey($companyId),
|
|
'cors_origins' => dbGetCompanyCorsOrigins($companyId),
|
|
]);
|
|
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));
|
|
}
|
|
dbAddLog($companyId, currentUser(), 'config_update', '', '', 'Päivitti API-asetukset');
|
|
echo json_encode([
|
|
'api_key' => dbGetCompanyApiKey($companyId),
|
|
'cors_origins' => dbGetCompanyCorsOrigins($companyId),
|
|
]);
|
|
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
|
|
$allowedTypes = ['image/png', 'image/jpeg', 'image/svg+xml', 'image/webp'];
|
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
$detectedType = finfo_file($finfo, $file['tmp_name']);
|
|
finfo_close($finfo);
|
|
if (!in_array($detectedType, $allowedTypes)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Sallitut tiedostotyypit: PNG, JPG, SVG, WebP']);
|
|
break;
|
|
}
|
|
$extMap = ['image/png' => 'png', 'image/jpeg' => 'jpg', 'image/svg+xml' => 'svg', 'image/webp' => 'webp'];
|
|
$ext = $extMap[$detectedType] ?? 'png';
|
|
$newFilename = 'logo.' . $ext;
|
|
$compDir = DATA_DIR . '/companies/' . $companyId;
|
|
if (!file_exists($compDir)) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Yritystä ei löydy']);
|
|
break;
|
|
}
|
|
// Poista vanha logo
|
|
$companies = dbLoadCompanies();
|
|
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);
|
|
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'] !== 'admin' && !in_array($domainCompanyId, $userCompanies)) {
|
|
dbRecordLoginAttempt($ip);
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Sinulla ei ole oikeutta kirjautua tälle sivustolle.']);
|
|
break;
|
|
}
|
|
session_regenerate_id(true);
|
|
$_SESSION['user_id'] = $u['id'];
|
|
$_SESSION['username'] = $u['username'];
|
|
$_SESSION['nimi'] = $u['nimi'];
|
|
$_SESSION['role'] = $u['role'];
|
|
$_SESSION['companies'] = $userCompanies;
|
|
// Jos domain matchaa ja käyttäjällä on oikeus -> käytä sitä
|
|
if ($domainCompanyId && in_array($domainCompanyId, $userCompanies)) {
|
|
$_SESSION['company_id'] = $domainCompanyId;
|
|
} else {
|
|
$_SESSION['company_id'] = !empty($userCompanies) ? $userCompanies[0] : '';
|
|
}
|
|
// Hae yritysten nimet
|
|
$allCompanies = dbLoadCompanies();
|
|
$companyList = [];
|
|
foreach ($allCompanies as $comp) {
|
|
if (in_array($comp['id'], $userCompanies)) {
|
|
$companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']];
|
|
}
|
|
}
|
|
echo json_encode([
|
|
'success' => true,
|
|
'username' => $u['username'],
|
|
'nimi' => $u['nimi'],
|
|
'role' => $u['role'],
|
|
'companies' => $companyList,
|
|
'company_id' => $_SESSION['company_id'],
|
|
'signatures' => $u['signatures'] ?? [],
|
|
]);
|
|
} else {
|
|
dbRecordLoginAttempt($ip);
|
|
http_response_code(401);
|
|
echo json_encode(['error' => 'Väärä käyttäjätunnus tai salasana']);
|
|
}
|
|
break;
|
|
|
|
case 'logout':
|
|
session_destroy();
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
case 'check_auth':
|
|
if (isset($_SESSION['user_id'])) {
|
|
// Synkronoi aina tuoreet yritysoikeudet tietokannasta sessioon
|
|
$u = dbGetUser($_SESSION['user_id']);
|
|
if ($u) {
|
|
$_SESSION['companies'] = $u['companies'] ?? [];
|
|
// Varmista aktiivinen yritys on sallittu
|
|
if (!in_array($_SESSION['company_id'] ?? '', $_SESSION['companies'])) {
|
|
$_SESSION['company_id'] = !empty($_SESSION['companies']) ? $_SESSION['companies'][0] : '';
|
|
}
|
|
}
|
|
// Hae yritysten nimet
|
|
$userCompanyIds = $_SESSION['companies'] ?? [];
|
|
$allCompanies = dbLoadCompanies();
|
|
$companyList = [];
|
|
foreach ($allCompanies as $comp) {
|
|
if (in_array($comp['id'], $userCompanyIds)) {
|
|
$companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']];
|
|
}
|
|
}
|
|
// Hae allekirjoitukset
|
|
$userSignatures = $u ? ($u['signatures'] ?? []) : [];
|
|
// Brändäystiedot aktiivisesta yrityksestä
|
|
$branding = ['primary_color' => '#0f3460', 'subtitle' => '', 'logo_url' => '', 'company_nimi' => ''];
|
|
$activeCompanyId = $_SESSION['company_id'] ?? '';
|
|
foreach ($allCompanies as $bc) {
|
|
if ($bc['id'] === $activeCompanyId) {
|
|
$branding['primary_color'] = $bc['primary_color'] ?? '#0f3460';
|
|
$branding['subtitle'] = $bc['subtitle'] ?? '';
|
|
$branding['company_nimi'] = $bc['nimi'] ?? '';
|
|
$branding['logo_url'] = !empty($bc['logo_file'])
|
|
? "api.php?action=company_logo&company_id=" . urlencode($bc['id'])
|
|
: '';
|
|
break;
|
|
}
|
|
}
|
|
echo json_encode([
|
|
'authenticated' => true,
|
|
'user_id' => $_SESSION['user_id'],
|
|
'username' => $_SESSION['username'],
|
|
'nimi' => $_SESSION['nimi'],
|
|
'role' => $_SESSION['role'],
|
|
'companies' => $companyList,
|
|
'company_id' => $_SESSION['company_id'] ?? '',
|
|
'signatures' => $userSignatures,
|
|
'branding' => $branding,
|
|
]);
|
|
} else {
|
|
echo json_encode(['authenticated' => false]);
|
|
}
|
|
break;
|
|
|
|
// ---------- PASSWORD RESET ----------
|
|
case 'password_reset_request':
|
|
if ($method !== 'POST') break;
|
|
$ip = getClientIp();
|
|
if (!dbCheckRateLimit($ip)) {
|
|
http_response_code(429);
|
|
echo json_encode(['error' => 'Liian monta yritystä. Yritä uudelleen myöhemmin.']);
|
|
break;
|
|
}
|
|
dbRecordLoginAttempt($ip);
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$username = trim($input['username'] ?? '');
|
|
$user = dbGetUserByUsername($username);
|
|
// Palauta aina sama viesti (ei paljasta onko tunnus olemassa)
|
|
if ($user && !empty($user['email'])) {
|
|
$token = generateToken();
|
|
dbSaveToken($user['id'], $token);
|
|
$resetUrl = SITE_URL . '/?reset=' . $token;
|
|
$html = '<div style="font-family:sans-serif;max-width:500px;margin:0 auto;">';
|
|
$html .= '<h2 style="color:#0f3460;">Noxus Intra</h2>';
|
|
$html .= '<p>Hei ' . htmlspecialchars($user['nimi'] ?: $user['username']) . ',</p>';
|
|
$html .= '<p>Sait tämän viestin koska salasanan palautusta pyydettiin tilillesi.</p>';
|
|
$html .= '<p><a href="' . $resetUrl . '" style="display:inline-block;background:#0f3460;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:600;">Vaihda salasana</a></p>';
|
|
$html .= '<p style="color:#888;font-size:0.9em;">Linkki on voimassa 1 tunnin. Jos et pyytänyt salasanan vaihtoa, voit jättää tämän viestin huomiotta.</p>';
|
|
$html .= '</div>';
|
|
sendMail($user['email'], 'Salasanan palautus - CuituNet Intra', $html);
|
|
}
|
|
echo json_encode(['success' => true, 'message' => 'Jos käyttäjätunnus löytyy ja sillä on sähköposti, palautuslinkki lähetetään.']);
|
|
break;
|
|
|
|
case 'password_reset':
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$token = $input['token'] ?? '';
|
|
$newPassword = $input['password'] ?? '';
|
|
if (strlen($newPassword) < 4) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Salasanan pitää olla vähintään 4 merkkiä']);
|
|
break;
|
|
}
|
|
$userId = dbValidateToken($token);
|
|
if (!$userId) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Palautuslinkki on vanhentunut tai virheellinen']);
|
|
break;
|
|
}
|
|
$user = dbGetUser($userId);
|
|
if ($user) {
|
|
$user['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT);
|
|
dbSaveUser($user);
|
|
}
|
|
dbRemoveToken($token);
|
|
echo json_encode(['success' => true, 'message' => 'Salasana vaihdettu onnistuneesti']);
|
|
break;
|
|
|
|
case 'validate_reset_token':
|
|
$token = $_GET['token'] ?? '';
|
|
$userId = dbValidateToken($token);
|
|
echo json_encode(['valid' => $userId !== null]);
|
|
break;
|
|
|
|
// ---------- USERS ----------
|
|
case 'users':
|
|
requireAdmin();
|
|
$users = dbLoadUsers();
|
|
$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'] ?? '');
|
|
$role = ($input['role'] ?? 'user') === 'admin' ? 'admin' : 'user';
|
|
if (empty($username) || empty($password)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Käyttäjätunnus ja salasana vaaditaan']);
|
|
break;
|
|
}
|
|
if (strlen($password) < 4) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Salasanan pitää olla vähintään 4 merkkiä']);
|
|
break;
|
|
}
|
|
$existingUser = dbGetUserByUsername($username);
|
|
if ($existingUser) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Käyttäjätunnus on jo käytössä']);
|
|
break;
|
|
}
|
|
$companies = $input['companies'] ?? [];
|
|
// Validoi yritys-IDt
|
|
$allCompanies = dbLoadCompanies();
|
|
$validIds = array_column($allCompanies, 'id');
|
|
$companies = array_values(array_filter($companies, fn($c) => in_array($c, $validIds)));
|
|
$signatures = [];
|
|
if (isset($input['signatures']) && is_array($input['signatures'])) {
|
|
foreach ($input['signatures'] as $mbId => $sig) {
|
|
$signatures[(string)$mbId] = (string)$sig;
|
|
}
|
|
}
|
|
$newUser = [
|
|
'id' => generateId(),
|
|
'username' => $username,
|
|
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
|
'nimi' => $nimi ?: $username,
|
|
'email' => $email,
|
|
'role' => $role,
|
|
'companies' => $companies,
|
|
'signatures' => $signatures,
|
|
'luotu' => date('Y-m-d H:i:s'),
|
|
];
|
|
dbSaveUser($newUser);
|
|
$companyId = $_SESSION['company_id'] ?? '';
|
|
dbAddLog($companyId, currentUser(), 'user_create', '', '', "Lisäsi käyttäjän: {$username} ({$role})");
|
|
unset($newUser['password_hash']);
|
|
echo json_encode($newUser);
|
|
break;
|
|
|
|
case 'user_update':
|
|
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;
|
|
}
|
|
if (isset($input['nimi'])) $u['nimi'] = trim($input['nimi']);
|
|
if (isset($input['email'])) $u['email'] = trim($input['email']);
|
|
if (isset($input['role'])) $u['role'] = $input['role'] === 'admin' ? 'admin' : 'user';
|
|
if (isset($input['companies'])) {
|
|
$allCompanies = dbLoadCompanies();
|
|
$validIds = array_column($allCompanies, 'id');
|
|
$u['companies'] = array_values(array_filter($input['companies'], fn($c) => in_array($c, $validIds)));
|
|
}
|
|
if (!empty($input['password'])) {
|
|
$u['password_hash'] = password_hash($input['password'], PASSWORD_DEFAULT);
|
|
}
|
|
if (isset($input['signatures']) && is_array($input['signatures'])) {
|
|
$sigs = [];
|
|
foreach ($input['signatures'] as $mbId => $sig) {
|
|
$sigs[(string)$mbId] = (string)$sig;
|
|
}
|
|
$u['signatures'] = $sigs;
|
|
}
|
|
dbSaveUser($u);
|
|
$companyId = $_SESSION['company_id'] ?? '';
|
|
dbAddLog($companyId, currentUser(), 'user_update', '', '', "Muokkasi käyttäjää: {$u['username']}");
|
|
// Päivitä sessio jos muokattiin kirjautunutta käyttäjää
|
|
if ($u['id'] === $_SESSION['user_id']) {
|
|
$_SESSION['companies'] = $u['companies'] ?? [];
|
|
if (!empty($u['companies']) && !in_array($_SESSION['company_id'] ?? '', $u['companies'])) {
|
|
$_SESSION['company_id'] = $u['companies'][0];
|
|
}
|
|
if (empty($u['companies'])) {
|
|
$_SESSION['company_id'] = '';
|
|
}
|
|
}
|
|
$safe = $u;
|
|
unset($safe['password_hash']);
|
|
echo json_encode($safe);
|
|
break;
|
|
|
|
case 'user_delete':
|
|
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);
|
|
dbDeleteUser($id);
|
|
$companyId = $_SESSION['company_id'] ?? '';
|
|
if ($deleted) dbAddLog($companyId, currentUser(), 'user_delete', '', '', "Poisti käyttäjän: {$deleted['username']}");
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
// ---------- CHANGELOG ----------
|
|
case 'changelog':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
$limit = intval($_GET['limit'] ?? 100);
|
|
echo json_encode(dbLoadChangelog($companyId, $limit));
|
|
break;
|
|
|
|
// ---------- CUSTOMERS ----------
|
|
case 'customers':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method === 'GET') {
|
|
echo json_encode(dbLoadCustomers($companyId));
|
|
}
|
|
break;
|
|
|
|
case 'customer':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method === 'POST') {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$customer = [
|
|
'id' => generateId(),
|
|
'yritys' => trim($input['yritys'] ?? ''),
|
|
'yhteyshenkilö' => trim($input['yhteyshenkilö'] ?? ''),
|
|
'puhelin' => trim($input['puhelin'] ?? ''),
|
|
'sahkoposti' => trim($input['sahkoposti'] ?? ''),
|
|
'laskutusosoite' => trim($input['laskutusosoite'] ?? ''),
|
|
'laskutuspostinumero' => trim($input['laskutuspostinumero'] ?? ''),
|
|
'laskutuskaupunki' => trim($input['laskutuskaupunki'] ?? ''),
|
|
'laskutussahkoposti' => trim($input['laskutussahkoposti'] ?? ''),
|
|
'elaskuosoite' => trim($input['elaskuosoite'] ?? ''),
|
|
'elaskuvalittaja' => trim($input['elaskuvalittaja'] ?? ''),
|
|
'ytunnus' => trim($input['ytunnus'] ?? ''),
|
|
'lisatiedot' => trim($input['lisatiedot'] ?? ''),
|
|
'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'];
|
|
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;
|
|
|
|
// ---------- 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' => '']],
|
|
'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'] ?? [],
|
|
'auto_close_at' => $t['auto_close_at'] ?? '',
|
|
'mailbox_id' => $t['mailbox_id'] ?? '',
|
|
'mailbox_name' => $mailboxNames[$t['mailbox_id'] ?? ''] ?? '',
|
|
'company_id' => $comp['id'],
|
|
'company_name' => $comp['nimi'] ?? '',
|
|
'created' => $t['created'],
|
|
'updated' => $t['updated'],
|
|
'message_count' => $msgCount,
|
|
'last_message_type' => $lastMsg ? ($lastMsg['type'] ?? '') : '',
|
|
'last_message_time' => $lastMsg ? ($lastMsg['timestamp'] ?? '') : '',
|
|
];
|
|
}
|
|
}
|
|
echo json_encode($list);
|
|
break;
|
|
|
|
case 'ticket_detail':
|
|
requireAuth();
|
|
$companyId = requireCompanyOrParam();
|
|
$id = $_GET['id'] ?? '';
|
|
$tickets = dbLoadTickets($companyId);
|
|
$ticket = null;
|
|
foreach ($tickets as $t) {
|
|
if ($t['id'] === $id) { $ticket = $t; break; }
|
|
}
|
|
if (!$ticket) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
break;
|
|
}
|
|
echo json_encode($ticket);
|
|
break;
|
|
|
|
case 'ticket_fetch':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
|
|
$companyConf = dbGetCompanyConfig($companyId);
|
|
$mailboxes = array_filter($companyConf['mailboxes'] ?? [], fn($mb) => !empty($mb['aktiivinen']));
|
|
|
|
if (empty($mailboxes)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Postilaatikoita ei ole määritetty. Lisää ne Yritykset-välilehdellä.']);
|
|
break;
|
|
}
|
|
|
|
$tickets = dbLoadTickets($companyId);
|
|
$newCount = 0;
|
|
$threadedCount = 0;
|
|
$errors = [];
|
|
|
|
// Collect all existing message IDs for duplicate detection
|
|
$existingMsgIds = [];
|
|
foreach ($tickets as $t) {
|
|
if ($t['message_id']) $existingMsgIds[$t['message_id']] = true;
|
|
foreach ($t['messages'] ?? [] as $m) {
|
|
if (!empty($m['message_id'])) $existingMsgIds[$m['message_id']] = true;
|
|
}
|
|
}
|
|
|
|
// Hae kaikista aktiivisista postilaatikoista
|
|
foreach ($mailboxes as $mailbox) {
|
|
$imapConfig = [
|
|
'imap_host' => $mailbox['imap_host'] ?? '',
|
|
'imap_port' => $mailbox['imap_port'] ?? 993,
|
|
'imap_user' => $mailbox['imap_user'] ?? '',
|
|
'imap_password' => $mailbox['imap_password'] ?? '',
|
|
'imap_encryption' => $mailbox['imap_encryption'] ?? 'ssl',
|
|
];
|
|
|
|
$imap = new ImapClient();
|
|
if (!$imap->connect($imapConfig)) {
|
|
$errors[] = ($mailbox['nimi'] ?? 'Tuntematon') . ': ' . $imap->lastError;
|
|
continue;
|
|
}
|
|
|
|
$emails = $imap->fetchMessages(100);
|
|
$imap->disconnect();
|
|
|
|
$rules = $companyConf['ticket_rules'] ?? [];
|
|
|
|
foreach ($emails as $email) {
|
|
if (!empty($email['message_id']) && isset($existingMsgIds[$email['message_id']])) {
|
|
continue;
|
|
}
|
|
|
|
$msg = [
|
|
'id' => generateId(),
|
|
'type' => 'email_in',
|
|
'from' => $email['from_email'],
|
|
'from_name' => $email['from_name'],
|
|
'body' => $email['body'],
|
|
'timestamp' => $email['date'],
|
|
'message_id' => $email['message_id'],
|
|
];
|
|
|
|
// Threading: find existing ticket by references
|
|
$existingTicket = null;
|
|
if ($email['in_reply_to']) {
|
|
$existingTicket = dbFindTicketByMessageId($companyId, $email['in_reply_to']);
|
|
}
|
|
if (!$existingTicket && $email['references']) {
|
|
$refs = preg_split('/\s+/', $email['references']);
|
|
foreach ($refs as $ref) {
|
|
$ref = trim($ref);
|
|
if (!$ref) continue;
|
|
$existingTicket = dbFindTicketByMessageId($companyId, $ref);
|
|
if ($existingTicket) break;
|
|
}
|
|
}
|
|
|
|
if ($existingTicket) {
|
|
// Load full ticket with messages
|
|
$fullTickets = dbLoadTickets($companyId);
|
|
foreach ($fullTickets as $ft) {
|
|
if ($ft['id'] === $existingTicket['id']) {
|
|
$ft['messages'][] = $msg;
|
|
$ft['updated'] = $email['date'];
|
|
if (in_array($ft['status'], ['ratkaistu', 'suljettu'])) {
|
|
$ft['status'] = 'kasittelyssa';
|
|
}
|
|
dbSaveTicket($companyId, $ft);
|
|
$threadedCount++;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
$ticket = [
|
|
'id' => generateId(),
|
|
'subject' => $email['subject'] ?: '(Ei aihetta)',
|
|
'from_email' => $email['from_email'],
|
|
'from_name' => $email['from_name'],
|
|
'status' => 'uusi',
|
|
'type' => 'muu',
|
|
'assigned_to' => '',
|
|
'customer_id' => '',
|
|
'customer_name' => '',
|
|
'tags' => [],
|
|
'auto_close_at' => '',
|
|
'mailbox_id' => $mailbox['id'],
|
|
'created' => $email['date'],
|
|
'updated' => $email['date'],
|
|
'message_id' => $email['message_id'],
|
|
'messages' => [$msg],
|
|
];
|
|
|
|
// Apply auto-rules
|
|
foreach ($rules as $rule) {
|
|
if (empty($rule['enabled'])) continue;
|
|
$match = true;
|
|
if (!empty($rule['from_contains'])) {
|
|
$needle = strtolower($rule['from_contains']);
|
|
if (strpos(strtolower($email['from_email'] . ' ' . $email['from_name']), $needle) === false) {
|
|
$match = false;
|
|
}
|
|
}
|
|
if (!empty($rule['subject_contains'])) {
|
|
$needle = strtolower($rule['subject_contains']);
|
|
if (strpos(strtolower($email['subject'] ?? ''), $needle) === false) {
|
|
$match = false;
|
|
}
|
|
}
|
|
if ($match) {
|
|
if (!empty($rule['set_status'])) $ticket['status'] = $rule['set_status'];
|
|
if (!empty($rule['set_type'])) $ticket['type'] = $rule['set_type'];
|
|
if (!empty($rule['set_tags'])) {
|
|
$ruleTags = array_map('trim', explode(',', $rule['set_tags']));
|
|
$ticket['tags'] = array_values(array_unique(array_merge($ticket['tags'], $ruleTags)));
|
|
}
|
|
if (!empty($rule['auto_close_days'])) {
|
|
$days = intval($rule['auto_close_days']);
|
|
if ($days > 0) {
|
|
$ticket['auto_close_at'] = date('Y-m-d H:i:s', strtotime("+{$days} days"));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
dbSaveTicket($companyId, $ticket);
|
|
$newCount++;
|
|
}
|
|
|
|
if ($email['message_id']) $existingMsgIds[$email['message_id']] = true;
|
|
}
|
|
}
|
|
|
|
// Reload for total count
|
|
$allTickets = dbLoadTickets($companyId);
|
|
dbAddLog($companyId, currentUser(), 'ticket_fetch', '', '', "Haettu sähköpostit: {$newCount} uutta tikettiä, {$threadedCount} ketjutettu");
|
|
$result = ['success' => true, 'new_tickets' => $newCount, 'threaded' => $threadedCount, 'total' => count($allTickets)];
|
|
if (!empty($errors)) $result['errors'] = $errors;
|
|
echo json_encode($result);
|
|
break;
|
|
|
|
case 'ticket_reply':
|
|
requireAuth();
|
|
$companyId = requireCompanyOrParam();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$body = trim($input['body'] ?? '');
|
|
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
|
|
$companyConf = dbGetCompanyConfig($companyId);
|
|
$replyMailbox = null;
|
|
foreach ($companyConf['mailboxes'] ?? [] as $mb) {
|
|
if ($mb['id'] === ($t['mailbox_id'] ?? '')) { $replyMailbox = $mb; break; }
|
|
}
|
|
// Fallback: käytä ensimmäistä postilaatikkoa
|
|
if (!$replyMailbox && !empty($companyConf['mailboxes'])) {
|
|
$replyMailbox = $companyConf['mailboxes'][0];
|
|
}
|
|
|
|
// Hae käyttäjän allekirjoitus tälle postilaatikolle
|
|
$mailboxId = $t['mailbox_id'] ?? '';
|
|
$signature = '';
|
|
$sigUser = dbGetUser($_SESSION['user_id']);
|
|
if ($sigUser) {
|
|
$signature = trim($sigUser['signatures'][$mailboxId] ?? '');
|
|
}
|
|
$emailBody = $signature ? $body . "\n\n-- \n" . $signature : $body;
|
|
|
|
$subject = 'Re: ' . $t['subject'];
|
|
$sent = sendTicketMail($t['from_email'], $subject, $emailBody, $lastMsgId, trim($allRefs), $replyMailbox);
|
|
|
|
if (!$sent) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Sähköpostin lähetys epäonnistui']);
|
|
break 2;
|
|
}
|
|
|
|
// Add reply to ticket (tallennetaan allekirjoituksen kanssa)
|
|
$reply = [
|
|
'id' => generateId(),
|
|
'type' => 'reply_out',
|
|
'from' => currentUser(),
|
|
'from_name' => $_SESSION['nimi'] ?? currentUser(),
|
|
'body' => $emailBody,
|
|
'timestamp' => date('Y-m-d H:i:s'),
|
|
'message_id' => '',
|
|
];
|
|
$t['messages'][] = $reply;
|
|
$t['updated'] = date('Y-m-d H:i:s');
|
|
if ($t['status'] === 'uusi') $t['status'] = 'kasittelyssa';
|
|
|
|
dbSaveTicket($companyId, $t);
|
|
$found = true;
|
|
dbAddLog($companyId, currentUser(), 'ticket_reply', $t['id'], $t['subject'], 'Vastasi tikettiin');
|
|
echo json_encode($t);
|
|
break;
|
|
}
|
|
}
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
}
|
|
break;
|
|
|
|
case 'ticket_status':
|
|
requireAuth();
|
|
$companyId = requireCompanyOrParam();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$status = $input['status'] ?? '';
|
|
$validStatuses = ['uusi', 'kasittelyssa', 'odottaa', 'ratkaistu', 'suljettu'];
|
|
if (!in_array($status, $validStatuses)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Virheellinen tila']);
|
|
break;
|
|
}
|
|
$tickets = dbLoadTickets($companyId);
|
|
$found = false;
|
|
foreach ($tickets as $t) {
|
|
if ($t['id'] === $id) {
|
|
$oldStatus = $t['status'];
|
|
$t['status'] = $status;
|
|
$t['updated'] = date('Y-m-d H:i:s');
|
|
dbSaveTicket($companyId, $t);
|
|
$found = true;
|
|
dbAddLog($companyId, currentUser(), 'ticket_status', $t['id'], $t['subject'], "Tila: {$oldStatus} → {$status}");
|
|
echo json_encode($t);
|
|
break;
|
|
}
|
|
}
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
}
|
|
break;
|
|
|
|
case 'ticket_type':
|
|
requireAuth();
|
|
$companyId = requireCompanyOrParam();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$type = $input['type'] ?? '';
|
|
$validTypes = ['laskutus', 'tekniikka', 'vika', 'muu'];
|
|
if (!in_array($type, $validTypes)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Virheellinen tyyppi']);
|
|
break;
|
|
}
|
|
$tickets = dbLoadTickets($companyId);
|
|
$found = false;
|
|
foreach ($tickets as $t) {
|
|
if ($t['id'] === $id) {
|
|
$oldType = $t['type'] ?? 'muu';
|
|
$t['type'] = $type;
|
|
$t['updated'] = date('Y-m-d H:i:s');
|
|
dbSaveTicket($companyId, $t);
|
|
$found = true;
|
|
dbAddLog($companyId, currentUser(), 'ticket_type', $t['id'], $t['subject'], "Tyyppi: {$oldType} → {$type}");
|
|
echo json_encode($t);
|
|
break;
|
|
}
|
|
}
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
}
|
|
break;
|
|
|
|
case 'ticket_customer':
|
|
requireAuth();
|
|
$companyId = requireCompanyOrParam();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$customerId = $input['customer_id'] ?? '';
|
|
$customerName = $input['customer_name'] ?? '';
|
|
$tickets = dbLoadTickets($companyId);
|
|
$found = false;
|
|
foreach ($tickets as $t) {
|
|
if ($t['id'] === $id) {
|
|
$t['customer_id'] = $customerId;
|
|
$t['customer_name'] = $customerName;
|
|
$t['updated'] = date('Y-m-d H:i:s');
|
|
dbSaveTicket($companyId, $t);
|
|
$found = true;
|
|
dbAddLog($companyId, currentUser(), 'ticket_customer', $t['id'], $t['subject'], "Asiakkuus: {$customerName}");
|
|
echo json_encode($t);
|
|
break;
|
|
}
|
|
}
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
}
|
|
break;
|
|
|
|
case 'ticket_assign':
|
|
requireAuth();
|
|
$companyId = requireCompanyOrParam();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$assignTo = trim($input['assigned_to'] ?? '');
|
|
$tickets = dbLoadTickets($companyId);
|
|
$found = false;
|
|
foreach ($tickets as $t) {
|
|
if ($t['id'] === $id) {
|
|
$t['assigned_to'] = $assignTo;
|
|
$t['updated'] = date('Y-m-d H:i:s');
|
|
dbSaveTicket($companyId, $t);
|
|
$found = true;
|
|
dbAddLog($companyId, currentUser(), 'ticket_assign', $t['id'], $t['subject'], "Osoitettu: {$assignTo}");
|
|
echo json_encode($t);
|
|
break;
|
|
}
|
|
}
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
}
|
|
break;
|
|
|
|
case 'ticket_note':
|
|
requireAuth();
|
|
$companyId = requireCompanyOrParam();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$body = trim($input['body'] ?? '');
|
|
if (empty($body)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Muistiinpano ei voi olla tyhjä']);
|
|
break;
|
|
}
|
|
$tickets = dbLoadTickets($companyId);
|
|
$found = false;
|
|
foreach ($tickets as $t) {
|
|
if ($t['id'] === $id) {
|
|
$note = [
|
|
'id' => generateId(),
|
|
'type' => 'note',
|
|
'from' => currentUser(),
|
|
'from_name' => $_SESSION['nimi'] ?? currentUser(),
|
|
'body' => $body,
|
|
'timestamp' => date('Y-m-d H:i:s'),
|
|
'message_id' => '',
|
|
];
|
|
$t['messages'][] = $note;
|
|
$t['updated'] = date('Y-m-d H:i:s');
|
|
dbSaveTicket($companyId, $t);
|
|
$found = true;
|
|
dbAddLog($companyId, currentUser(), 'ticket_note', $t['id'], $t['subject'], 'Lisäsi muistiinpanon');
|
|
echo json_encode($t);
|
|
break;
|
|
}
|
|
}
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
}
|
|
break;
|
|
|
|
case 'ticket_delete':
|
|
requireAuth();
|
|
$companyId = requireCompanyOrParam();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$tickets = dbLoadTickets($companyId);
|
|
$deleted = null;
|
|
foreach ($tickets as $t) {
|
|
if ($t['id'] === $id) { $deleted = $t; break; }
|
|
}
|
|
dbDeleteTicket($id);
|
|
if ($deleted) dbAddLog($companyId, currentUser(), 'ticket_delete', $id, $deleted['subject'] ?? '', 'Poisti tiketin');
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
case 'ticket_tags':
|
|
requireAuth();
|
|
$companyId = requireCompanyOrParam();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$tags = $input['tags'] ?? [];
|
|
// Sanitize tags: trim, lowercase, remove empty
|
|
$tags = array_values(array_filter(array_map(function($t) {
|
|
return trim(strtolower($t));
|
|
}, $tags)));
|
|
$tickets = dbLoadTickets($companyId);
|
|
$found = false;
|
|
foreach ($tickets as $t) {
|
|
if ($t['id'] === $id) {
|
|
$t['tags'] = $tags;
|
|
$t['updated'] = date('Y-m-d H:i:s');
|
|
dbSaveTicket($companyId, $t);
|
|
$found = true;
|
|
dbAddLog($companyId, currentUser(), 'ticket_tags', $t['id'], $t['subject'], 'Tagit: ' . implode(', ', $tags));
|
|
echo json_encode($t);
|
|
break;
|
|
}
|
|
}
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
}
|
|
break;
|
|
|
|
case 'ticket_rules':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
echo json_encode(dbLoadTicketRules($companyId));
|
|
break;
|
|
|
|
case 'ticket_rule_save':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
|
|
$rule = [
|
|
'id' => $input['id'] ?? generateId(),
|
|
'name' => trim($input['name'] ?? ''),
|
|
'from_contains' => trim($input['from_contains'] ?? ''),
|
|
'subject_contains' => trim($input['subject_contains'] ?? ''),
|
|
'set_status' => $input['set_status'] ?? '',
|
|
'set_type' => $input['set_type'] ?? '',
|
|
'set_tags' => trim($input['set_tags'] ?? ''),
|
|
'auto_close_days' => intval($input['auto_close_days'] ?? 0),
|
|
'enabled' => $input['enabled'] ?? true,
|
|
];
|
|
|
|
if (empty($rule['name'])) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Säännön nimi puuttuu']);
|
|
break;
|
|
}
|
|
|
|
dbSaveTicketRule($companyId, $rule);
|
|
dbAddLog($companyId, currentUser(), 'config_update', '', '', 'Tikettisääntö: ' . $rule['name']);
|
|
echo json_encode($rule);
|
|
break;
|
|
|
|
case 'ticket_bulk_status':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$ids = $input['ids'] ?? [];
|
|
$newStatus = $input['status'] ?? '';
|
|
$validStatuses = ['uusi','kasittelyssa','odottaa','ratkaistu','suljettu'];
|
|
if (!in_array($newStatus, $validStatuses)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Virheellinen tila']);
|
|
break;
|
|
}
|
|
$tickets = dbLoadTickets($companyId);
|
|
$changed = 0;
|
|
foreach ($tickets as $t) {
|
|
if (in_array($t['id'], $ids)) {
|
|
$t['status'] = $newStatus;
|
|
$t['updated'] = date('Y-m-d H:i:s');
|
|
dbSaveTicket($companyId, $t);
|
|
$changed++;
|
|
}
|
|
}
|
|
dbAddLog($companyId, currentUser(), 'ticket_status', '', '', "Massapäivitys: $changed tikettiä → $newStatus");
|
|
echo json_encode(['success' => true, 'changed' => $changed]);
|
|
break;
|
|
|
|
case 'ticket_bulk_delete':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$ids = $input['ids'] ?? [];
|
|
$deleted = 0;
|
|
foreach ($ids as $ticketId) {
|
|
dbDeleteTicket($ticketId);
|
|
$deleted++;
|
|
}
|
|
dbAddLog($companyId, currentUser(), 'ticket_delete', '', '', "Massapoisto: $deleted tikettiä");
|
|
echo json_encode(['success' => true, 'deleted' => $deleted]);
|
|
break;
|
|
|
|
case 'ticket_rule_delete':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$ruleId = $input['id'] ?? '';
|
|
dbDeleteTicketRule($ruleId);
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
// ---------- 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();
|
|
echo json_encode(dbLoadCompanies());
|
|
break;
|
|
|
|
case 'all_mailboxes':
|
|
requireAuth();
|
|
// Palauttaa kaikki postilaatikot käyttäjän yrityksistä (allekirjoituksia varten)
|
|
$userCompanyIds = $_SESSION['companies'] ?? [];
|
|
$allCompanies = dbLoadCompanies();
|
|
$result = [];
|
|
foreach ($allCompanies as $comp) {
|
|
if (!in_array($comp['id'], $userCompanyIds)) continue;
|
|
$mailboxes = dbLoadMailboxes($comp['id']);
|
|
foreach ($mailboxes as $mb) {
|
|
$result[] = [
|
|
'id' => $mb['id'],
|
|
'nimi' => $mb['nimi'] ?? $mb['imap_user'] ?? '',
|
|
'company_id' => $comp['id'],
|
|
'company_nimi' => $comp['nimi'],
|
|
];
|
|
}
|
|
}
|
|
echo json_encode($result);
|
|
break;
|
|
|
|
case 'company_create':
|
|
requireAdmin();
|
|
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'] ?? '';
|
|
$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']);
|
|
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':
|
|
requireAdmin();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
if (empty($id) || !preg_match('/^[a-z0-9-]+$/', $id)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Virheellinen yritys-ID']);
|
|
break;
|
|
}
|
|
dbDeleteCompany($id);
|
|
// Poista yritys käyttäjiltä (CASCADE hoitaa tietokannan puolella, mutta päivitetään sessio)
|
|
$users = dbLoadUsers();
|
|
foreach ($users as $u) {
|
|
$u['companies'] = array_values(array_filter($u['companies'] ?? [], fn($c) => $c !== $id));
|
|
dbSaveUser($u);
|
|
}
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
case 'company_switch':
|
|
requireAuth();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$companyId = $input['company_id'] ?? '';
|
|
$userCompanies = $_SESSION['companies'] ?? [];
|
|
if (!in_array($companyId, $userCompanies)) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']);
|
|
break;
|
|
}
|
|
$_SESSION['company_id'] = $companyId;
|
|
echo json_encode(['success' => true, 'company_id' => $companyId]);
|
|
break;
|
|
|
|
case 'company_config':
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
echo json_encode(dbGetCompanyConfig($companyId));
|
|
break;
|
|
|
|
case 'company_config_update':
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
if (isset($input['mailboxes'])) {
|
|
// Delete all existing mailboxes and re-save
|
|
$existingMailboxes = dbLoadMailboxes($companyId);
|
|
foreach ($existingMailboxes as $existing) {
|
|
dbDeleteMailbox($existing['id']);
|
|
}
|
|
foreach ($input['mailboxes'] as $mb) {
|
|
if (empty($mb['id'])) $mb['id'] = generateId();
|
|
dbSaveMailbox($companyId, $mb);
|
|
}
|
|
}
|
|
if (isset($input['ticket_rules'])) {
|
|
// Delete all existing rules and re-save
|
|
$existingRules = dbLoadTicketRules($companyId);
|
|
foreach ($existingRules as $existing) {
|
|
dbDeleteTicketRule($existing['id']);
|
|
}
|
|
foreach ($input['ticket_rules'] as $rule) {
|
|
if (empty($rule['id'])) $rule['id'] = generateId();
|
|
dbSaveTicketRule($companyId, $rule);
|
|
}
|
|
}
|
|
echo json_encode(dbGetCompanyConfig($companyId));
|
|
break;
|
|
|
|
// ---------- MAILBOXES ----------
|
|
case 'mailboxes':
|
|
requireAuth();
|
|
$companyId = requireCompany();
|
|
$mailboxes = dbLoadMailboxes($companyId);
|
|
// Palauta postilaatikot ilman salasanoja
|
|
$mbs = array_map(function($mb) {
|
|
$mb['imap_password'] = !empty($mb['imap_password']) ? '********' : '';
|
|
return $mb;
|
|
}, $mailboxes);
|
|
echo json_encode($mbs);
|
|
break;
|
|
|
|
case 'mailbox_save':
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
|
|
$mb = [
|
|
'id' => $input['id'] ?? generateId(),
|
|
'nimi' => trim($input['nimi'] ?? ''),
|
|
'imap_host' => trim($input['imap_host'] ?? ''),
|
|
'imap_port' => intval($input['imap_port'] ?? 993),
|
|
'imap_user' => trim($input['imap_user'] ?? ''),
|
|
'imap_encryption' => trim($input['imap_encryption'] ?? 'ssl'),
|
|
'smtp_from_email' => trim($input['smtp_from_email'] ?? ''),
|
|
'smtp_from_name' => trim($input['smtp_from_name'] ?? ''),
|
|
'aktiivinen' => $input['aktiivinen'] ?? true,
|
|
];
|
|
// Salasana: jos ******** -> pidä vanha, muuten päivitä
|
|
if (isset($input['imap_password']) && $input['imap_password'] !== '********') {
|
|
$mb['imap_password'] = $input['imap_password'];
|
|
} else {
|
|
// Hae vanha salasana
|
|
$existingMb = dbGetMailbox($mb['id']);
|
|
$mb['imap_password'] = $existingMb ? ($existingMb['imap_password'] ?? '') : '';
|
|
}
|
|
|
|
if (empty($mb['nimi'])) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Postilaatikon nimi puuttuu']);
|
|
break;
|
|
}
|
|
|
|
dbSaveMailbox($companyId, $mb);
|
|
dbAddLog($companyId, currentUser(), 'mailbox_save', '', '', 'Postilaatikko: ' . $mb['nimi']);
|
|
// Palauta ilman salasanaa
|
|
$mb['imap_password'] = '********';
|
|
echo json_encode($mb);
|
|
break;
|
|
|
|
case 'mailbox_delete':
|
|
requireAdmin();
|
|
$companyId = requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$mbId = $input['id'] ?? '';
|
|
dbDeleteMailbox($mbId);
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
default:
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tuntematon toiminto']);
|
|
break;
|
|
}
|