Implement full multi-company architecture:
- Per-company directory structure (data/companies/{id}/)
- Automatic migration from single-tenant to multi-tenant
- Company management admin tab (create, edit, delete companies)
- Per-company IMAP mailbox configuration (multiple mailboxes per company)
- User access control per company (companies array on users)
- Company switcher in header (shown when user has access to >1 company)
- Session-based company context with check_auth fallback for old sessions
- Ticket list shows mailbox name instead of sender
- IMAP settings moved from global config to company-specific config
- All data endpoints protected with requireCompany() guard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2516 lines
94 KiB
PHP
2516 lines
94 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', 'Strict');
|
|
session_start();
|
|
|
|
header('Content-Type: application/json');
|
|
header('X-Content-Type-Options: nosniff');
|
|
|
|
define('DATA_DIR', __DIR__ . '/data');
|
|
define('USERS_FILE', DATA_DIR . '/users.json');
|
|
define('TOKENS_FILE', DATA_DIR . '/reset_tokens.json');
|
|
define('RATE_FILE', DATA_DIR . '/login_attempts.json');
|
|
define('CONFIG_FILE', DATA_DIR . '/config.json');
|
|
define('COMPANIES_FILE', DATA_DIR . '/companies.json');
|
|
define('SITE_URL', 'https://intra.cuitunet.fi');
|
|
|
|
// Sähköpostiasetukset (fallback)
|
|
define('MAIL_FROM', 'sivusto@cuitunet.fi');
|
|
define('MAIL_FROM_NAME', 'CuituNet Intra');
|
|
|
|
// Varmista data-kansio ja globaalit tiedostot
|
|
if (!file_exists(DATA_DIR)) mkdir(DATA_DIR, 0755, true);
|
|
foreach ([USERS_FILE, TOKENS_FILE, RATE_FILE] as $f) {
|
|
if (!file_exists($f)) file_put_contents($f, '[]');
|
|
}
|
|
if (!file_exists(CONFIG_FILE)) file_put_contents(CONFIG_FILE, '{}');
|
|
if (!file_exists(COMPANIES_FILE)) file_put_contents(COMPANIES_FILE, '[]');
|
|
|
|
initUsers();
|
|
runMigration();
|
|
|
|
$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 loadCompanies(): array {
|
|
if (!file_exists(COMPANIES_FILE)) return [];
|
|
return json_decode(file_get_contents(COMPANIES_FILE), true) ?: [];
|
|
}
|
|
|
|
function saveCompanies(array $companies): void {
|
|
file_put_contents(COMPANIES_FILE, json_encode($companies, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
function companyFile(string $filename): string {
|
|
return getCompanyDir() . '/' . $filename;
|
|
}
|
|
|
|
function loadCompanyConfig(): array {
|
|
$file = companyFile('config.json');
|
|
if (!file_exists($file)) return ['mailboxes' => [], 'ticket_rules' => []];
|
|
return json_decode(file_get_contents($file), true) ?: ['mailboxes' => [], 'ticket_rules' => []];
|
|
}
|
|
|
|
function saveCompanyConfig(array $config): void {
|
|
file_put_contents(companyFile('config.json'), json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
function runMigration(): void {
|
|
// Tarkista onko migraatio jo tehty
|
|
$companiesDir = DATA_DIR . '/companies';
|
|
if (file_exists($companiesDir) && is_dir($companiesDir)) return;
|
|
|
|
// Tarkista onko vanha data olemassa (pre-multitenant)
|
|
$oldCustomers = DATA_DIR . '/customers.json';
|
|
if (!file_exists($oldCustomers)) return;
|
|
|
|
// Luo yritykshakemisto
|
|
mkdir($companiesDir, 0755, true);
|
|
$cuitunetDir = $companiesDir . '/cuitunet';
|
|
mkdir($cuitunetDir, 0755, true);
|
|
|
|
// Luo companies.json
|
|
$companies = [[
|
|
'id' => 'cuitunet',
|
|
'nimi' => 'CuituNet',
|
|
'luotu' => date('Y-m-d H:i:s'),
|
|
'aktiivinen' => true,
|
|
]];
|
|
file_put_contents(COMPANIES_FILE, json_encode($companies, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
|
|
// Siirrä datatiedostot yrityksen alle
|
|
$filesToMove = ['customers.json', 'leads.json', 'archive.json', 'tickets.json', 'changelog.json'];
|
|
foreach ($filesToMove as $f) {
|
|
$src = DATA_DIR . '/' . $f;
|
|
if (file_exists($src)) {
|
|
copy($src, $cuitunetDir . '/' . $f);
|
|
unlink($src);
|
|
}
|
|
}
|
|
|
|
// Siirrä tiedostokansio
|
|
$oldFiles = DATA_DIR . '/files';
|
|
if (is_dir($oldFiles)) {
|
|
rename($oldFiles, $cuitunetDir . '/files');
|
|
}
|
|
|
|
// Siirrä backups-kansio
|
|
$oldBackups = DATA_DIR . '/backups';
|
|
if (is_dir($oldBackups)) {
|
|
rename($oldBackups, $cuitunetDir . '/backups');
|
|
}
|
|
|
|
// Luo yrityksen config IMAP-asetuksista
|
|
$globalConfig = json_decode(file_get_contents(CONFIG_FILE), true) ?: [];
|
|
$companyConfig = ['mailboxes' => [], 'ticket_rules' => $globalConfig['ticket_rules'] ?? []];
|
|
|
|
if (!empty($globalConfig['imap_host'])) {
|
|
$companyConfig['mailboxes'][] = [
|
|
'id' => generateId(),
|
|
'nimi' => 'Cuitunet-asiakaspalvelu',
|
|
'imap_host' => $globalConfig['imap_host'],
|
|
'imap_port' => intval($globalConfig['imap_port'] ?? 993),
|
|
'imap_user' => $globalConfig['imap_user'] ?? '',
|
|
'imap_password' => $globalConfig['imap_password'] ?? '',
|
|
'imap_encryption' => $globalConfig['imap_encryption'] ?? 'ssl',
|
|
'smtp_from_email' => $globalConfig['imap_user'] ?? '',
|
|
'smtp_from_name' => 'CuituNet Asiakaspalvelu',
|
|
'aktiivinen' => true,
|
|
];
|
|
}
|
|
|
|
file_put_contents($cuitunetDir . '/config.json', json_encode($companyConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
|
|
// Päivitä tiketteihin mailbox_id
|
|
$ticketsFile = $cuitunetDir . '/tickets.json';
|
|
if (file_exists($ticketsFile)) {
|
|
$tickets = json_decode(file_get_contents($ticketsFile), true) ?: [];
|
|
$mbId = !empty($companyConfig['mailboxes']) ? $companyConfig['mailboxes'][0]['id'] : '';
|
|
foreach ($tickets as &$t) {
|
|
if (!isset($t['mailbox_id'])) $t['mailbox_id'] = $mbId;
|
|
}
|
|
unset($t);
|
|
file_put_contents($ticketsFile, json_encode($tickets, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
// Lisää companies-array kaikkiin käyttäjiin
|
|
$users = json_decode(file_get_contents(USERS_FILE), true) ?: [];
|
|
foreach ($users as &$u) {
|
|
if (!isset($u['companies'])) $u['companies'] = ['cuitunet'];
|
|
}
|
|
unset($u);
|
|
file_put_contents(USERS_FILE, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
|
|
// Siivoa globaali config
|
|
unset($globalConfig['imap_host'], $globalConfig['imap_port'], $globalConfig['imap_user'],
|
|
$globalConfig['imap_password'], $globalConfig['imap_encryption'], $globalConfig['ticket_rules']);
|
|
file_put_contents(CONFIG_FILE, json_encode($globalConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
// ==================== RATE LIMITING ====================
|
|
|
|
function checkRateLimit(string $ip): bool {
|
|
$attempts = json_decode(file_get_contents(RATE_FILE), true) ?: [];
|
|
$now = time();
|
|
// Siivoa vanhat (yli 15 min)
|
|
$attempts = array_filter($attempts, fn($a) => ($now - $a['time']) < 900);
|
|
file_put_contents(RATE_FILE, json_encode(array_values($attempts)));
|
|
// Laske tämän IP:n yritykset viimeisen 15 min aikana
|
|
$ipAttempts = array_filter($attempts, fn($a) => $a['ip'] === $ip);
|
|
return count($ipAttempts) < 10; // Max 10 yritystä / 15 min
|
|
}
|
|
|
|
function recordLoginAttempt(string $ip): void {
|
|
$attempts = json_decode(file_get_contents(RATE_FILE), true) ?: [];
|
|
$attempts[] = ['ip' => $ip, 'time' => time()];
|
|
file_put_contents(RATE_FILE, json_encode($attempts));
|
|
}
|
|
|
|
function getClientIp(): string {
|
|
return $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
|
}
|
|
|
|
// ==================== CONFIG ====================
|
|
|
|
function loadConfig(): array {
|
|
if (!file_exists(CONFIG_FILE)) return [];
|
|
return json_decode(file_get_contents(CONFIG_FILE), true) ?: [];
|
|
}
|
|
|
|
function saveConfig(array $config): void {
|
|
file_put_contents(CONFIG_FILE, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
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 ====================
|
|
|
|
function loadTickets(): array {
|
|
$file = companyFile('tickets.json');
|
|
if (!file_exists($file)) { file_put_contents($file, '[]'); return []; }
|
|
return json_decode(file_get_contents($file), true) ?: [];
|
|
}
|
|
|
|
function saveTickets(array $tickets): void {
|
|
file_put_contents(companyFile('tickets.json'), json_encode($tickets, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
function findTicketByMessageId(array $tickets, string $messageId): ?int {
|
|
foreach ($tickets as $i => $t) {
|
|
if ($t['message_id'] === $messageId) return $i;
|
|
foreach ($t['messages'] ?? [] as $m) {
|
|
if (($m['message_id'] ?? '') === $messageId) return $i;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function findTicketByReferences(array $tickets, string $inReplyTo, string $references): ?int {
|
|
// Check In-Reply-To header
|
|
if ($inReplyTo) {
|
|
$idx = findTicketByMessageId($tickets, $inReplyTo);
|
|
if ($idx !== null) return $idx;
|
|
}
|
|
// Check References header
|
|
if ($references) {
|
|
$refs = preg_split('/\s+/', $references);
|
|
foreach ($refs as $ref) {
|
|
$ref = trim($ref);
|
|
if (!$ref) continue;
|
|
$idx = findTicketByMessageId($tickets, $ref);
|
|
if ($idx !== null) return $idx;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// ==================== USERS ====================
|
|
|
|
function initUsers(): void {
|
|
$users = json_decode(file_get_contents(USERS_FILE), true) ?: [];
|
|
if (empty($users)) {
|
|
$users[] = [
|
|
'id' => generateId(),
|
|
'username' => 'admin',
|
|
'password_hash' => password_hash('cuitunet2024', PASSWORD_DEFAULT),
|
|
'nimi' => 'Ylläpitäjä',
|
|
'email' => '',
|
|
'role' => 'admin',
|
|
'companies' => [],
|
|
'luotu' => date('Y-m-d H:i:s'),
|
|
];
|
|
file_put_contents(USERS_FILE, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
}
|
|
|
|
function loadUsers(): array {
|
|
return json_decode(file_get_contents(USERS_FILE), true) ?: [];
|
|
}
|
|
|
|
function saveUsers(array $users): void {
|
|
file_put_contents(USERS_FILE, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
// ==================== RESET TOKENS ====================
|
|
|
|
function saveToken(string $userId, string $token): void {
|
|
$tokens = json_decode(file_get_contents(TOKENS_FILE), true) ?: [];
|
|
// Poista vanhat tokenit tälle käyttäjälle
|
|
$tokens = array_filter($tokens, fn($t) => $t['user_id'] !== $userId);
|
|
$tokens[] = [
|
|
'user_id' => $userId,
|
|
'token' => hash('sha256', $token),
|
|
'expires' => time() + 3600, // 1 tunti
|
|
];
|
|
file_put_contents(TOKENS_FILE, json_encode(array_values($tokens)));
|
|
}
|
|
|
|
function validateToken(string $token): ?string {
|
|
$tokens = json_decode(file_get_contents(TOKENS_FILE), true) ?: [];
|
|
$hashed = hash('sha256', $token);
|
|
$now = time();
|
|
foreach ($tokens as $t) {
|
|
if ($t['token'] === $hashed && $t['expires'] > $now) {
|
|
return $t['user_id'];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function removeToken(string $token): void {
|
|
$tokens = json_decode(file_get_contents(TOKENS_FILE), true) ?: [];
|
|
$hashed = hash('sha256', $token);
|
|
$tokens = array_filter($tokens, fn($t) => $t['token'] !== $hashed);
|
|
file_put_contents(TOKENS_FILE, json_encode(array_values($tokens)));
|
|
}
|
|
|
|
// ==================== CHANGELOG ====================
|
|
|
|
function addLog(string $action, string $customerId = '', string $customerName = '', string $details = ''): void {
|
|
// Jos company-kontekstia ei ole (esim. globaalit asetukset), ohitetaan
|
|
if (empty($_SESSION['company_id'])) return;
|
|
$file = companyFile('changelog.json');
|
|
if (!file_exists($file)) file_put_contents($file, '[]');
|
|
$log = json_decode(file_get_contents($file), true) ?: [];
|
|
array_unshift($log, [
|
|
'id' => generateId(),
|
|
'timestamp' => date('Y-m-d H:i:s'),
|
|
'user' => currentUser(),
|
|
'action' => $action,
|
|
'customer_id' => $customerId,
|
|
'customer_name' => $customerName,
|
|
'details' => $details,
|
|
]);
|
|
if (count($log) > 500) $log = array_slice($log, 0, 500);
|
|
file_put_contents($file, json_encode($log, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
// ==================== CUSTOMERS ====================
|
|
|
|
function loadCustomers(): array {
|
|
$file = companyFile('customers.json');
|
|
if (!file_exists($file)) { file_put_contents($file, '[]'); return []; }
|
|
$data = file_get_contents($file);
|
|
$customers = json_decode($data, true) ?: [];
|
|
$migrated = false;
|
|
foreach ($customers as &$c) {
|
|
if (!isset($c['liittymat'])) {
|
|
$c['liittymat'] = [[
|
|
'asennusosoite' => $c['asennusosoite'] ?? '',
|
|
'postinumero' => $c['postinumero'] ?? '',
|
|
'kaupunki' => $c['kaupunki'] ?? '',
|
|
'liittymanopeus' => $c['liittymanopeus'] ?? '',
|
|
'hinta' => floatval($c['hinta'] ?? 0),
|
|
'sopimuskausi' => '',
|
|
'alkupvm' => '',
|
|
]];
|
|
unset($c['asennusosoite'], $c['postinumero'], $c['kaupunki'], $c['liittymanopeus'], $c['hinta']);
|
|
$migrated = true;
|
|
}
|
|
}
|
|
unset($c);
|
|
if ($migrated) {
|
|
file_put_contents($file, json_encode($customers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
return $customers;
|
|
}
|
|
|
|
function saveCustomers(array $customers): void {
|
|
$file = companyFile('customers.json');
|
|
if (file_exists($file) && filesize($file) > 2) {
|
|
$backupDir = getCompanyDir() . '/backups';
|
|
if (!file_exists($backupDir)) mkdir($backupDir, 0755, true);
|
|
copy($file, $backupDir . '/customers_' . date('Y-m-d_His') . '.json');
|
|
$backups = glob($backupDir . '/customers_*.json');
|
|
if (count($backups) > 30) {
|
|
sort($backups);
|
|
array_map('unlink', array_slice($backups, 0, count($backups) - 30));
|
|
}
|
|
}
|
|
file_put_contents($file, json_encode($customers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
function loadArchive(): array {
|
|
$file = companyFile('archive.json');
|
|
if (!file_exists($file)) { file_put_contents($file, '[]'); return []; }
|
|
return json_decode(file_get_contents($file), true) ?: [];
|
|
}
|
|
|
|
function saveArchive(array $archive): void {
|
|
file_put_contents(companyFile('archive.json'), json_encode($archive, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
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':
|
|
// CORS - salli cuitunet.fi
|
|
$config = loadConfig();
|
|
$allowedOrigins = $config['cors_origins'] ?? ['https://cuitunet.fi', 'https://www.cuitunet.fi'];
|
|
$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; }
|
|
|
|
// API-avain tarkistus
|
|
$apiKey = $config['api_key'] ?? '';
|
|
$providedKey = $_GET['key'] ?? ($_SERVER['HTTP_X_API_KEY'] ?? '');
|
|
if (empty($apiKey) || $providedKey !== $apiKey) {
|
|
http_response_code(403);
|
|
echo json_encode(['error' => 'Virheellinen API-avain']);
|
|
break;
|
|
}
|
|
|
|
// Parametrit: osoite (kadunnimi + numero), postinumero, kaupunki
|
|
$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 kaikista yrityksistä
|
|
$allCompanies = loadCompanies();
|
|
$found = false;
|
|
foreach ($allCompanies as $comp) {
|
|
$compDir = DATA_DIR . '/companies/' . $comp['id'];
|
|
$custFile = $compDir . '/customers.json';
|
|
if (!file_exists($custFile)) continue;
|
|
$customers = json_decode(file_get_contents($custFile), true) ?: [];
|
|
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 3;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Palauta VAIN true/false - ei osoitteita, nopeuksia tai muuta dataa
|
|
echo json_encode(['saatavilla' => $found]);
|
|
break;
|
|
|
|
// ---------- CONFIG (admin) ----------
|
|
case 'config':
|
|
requireAdmin();
|
|
echo json_encode(loadConfig());
|
|
break;
|
|
|
|
case 'config_update':
|
|
requireAdmin();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$config = loadConfig();
|
|
if (isset($input['api_key'])) $config['api_key'] = trim($input['api_key']);
|
|
if (isset($input['cors_origins'])) {
|
|
$origins = array_filter(array_map('trim', explode("\n", $input['cors_origins'])));
|
|
$config['cors_origins'] = array_values($origins);
|
|
}
|
|
saveConfig($config);
|
|
addLog('config_update', '', '', 'Päivitti asetukset');
|
|
echo json_encode($config);
|
|
break;
|
|
|
|
case 'generate_api_key':
|
|
requireAdmin();
|
|
if ($method !== 'POST') break;
|
|
$config = loadConfig();
|
|
$config['api_key'] = bin2hex(random_bytes(16));
|
|
saveConfig($config);
|
|
addLog('config_update', '', '', 'Generoi uuden API-avaimen');
|
|
echo json_encode($config);
|
|
break;
|
|
|
|
// ---------- CAPTCHA ----------
|
|
case 'captcha':
|
|
$a = rand(1, 20);
|
|
$b = rand(1, 20);
|
|
$_SESSION['captcha_answer'] = $a + $b;
|
|
echo json_encode(['question' => "$a + $b = ?"]);
|
|
break;
|
|
|
|
// ---------- AUTH ----------
|
|
case 'login':
|
|
if ($method !== 'POST') break;
|
|
$ip = getClientIp();
|
|
if (!checkRateLimit($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']) {
|
|
recordLoginAttempt($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'] ?? '';
|
|
$users = loadUsers();
|
|
$found = false;
|
|
foreach ($users as $u) {
|
|
if ($u['username'] === $username && password_verify($password, $u['password_hash'])) {
|
|
session_regenerate_id(true);
|
|
$_SESSION['user_id'] = $u['id'];
|
|
$_SESSION['username'] = $u['username'];
|
|
$_SESSION['nimi'] = $u['nimi'];
|
|
$_SESSION['role'] = $u['role'];
|
|
// Multi-company: aseta käyttäjän yritykset sessioon
|
|
$userCompanies = $u['companies'] ?? [];
|
|
$_SESSION['companies'] = $userCompanies;
|
|
// Valitse ensimmäinen yritys oletukseksi
|
|
$_SESSION['company_id'] = !empty($userCompanies) ? $userCompanies[0] : '';
|
|
// Hae yritysten nimet
|
|
$allCompanies = loadCompanies();
|
|
$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'],
|
|
]);
|
|
$found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$found) {
|
|
recordLoginAttempt($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'])) {
|
|
// Fallback: jos session ei sisällä company-dataa (vanha sessio ennen migraatiota)
|
|
if (empty($_SESSION['companies'])) {
|
|
$users = loadUsers();
|
|
foreach ($users as $u) {
|
|
if ($u['id'] === $_SESSION['user_id']) {
|
|
$_SESSION['companies'] = $u['companies'] ?? [];
|
|
if (!empty($u['companies'])) {
|
|
$_SESSION['company_id'] = $u['companies'][0];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Hae yritysten nimet
|
|
$userCompanyIds = $_SESSION['companies'] ?? [];
|
|
$allCompanies = loadCompanies();
|
|
$companyList = [];
|
|
foreach ($allCompanies as $comp) {
|
|
if (in_array($comp['id'], $userCompanyIds)) {
|
|
$companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']];
|
|
}
|
|
}
|
|
echo json_encode([
|
|
'authenticated' => true,
|
|
'username' => $_SESSION['username'],
|
|
'nimi' => $_SESSION['nimi'],
|
|
'role' => $_SESSION['role'],
|
|
'companies' => $companyList,
|
|
'company_id' => $_SESSION['company_id'] ?? '',
|
|
]);
|
|
} else {
|
|
echo json_encode(['authenticated' => false]);
|
|
}
|
|
break;
|
|
|
|
// ---------- PASSWORD RESET ----------
|
|
case 'password_reset_request':
|
|
if ($method !== 'POST') break;
|
|
$ip = getClientIp();
|
|
if (!checkRateLimit($ip)) {
|
|
http_response_code(429);
|
|
echo json_encode(['error' => 'Liian monta yritystä. Yritä uudelleen myöhemmin.']);
|
|
break;
|
|
}
|
|
recordLoginAttempt($ip);
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$username = trim($input['username'] ?? '');
|
|
$users = loadUsers();
|
|
$user = null;
|
|
foreach ($users as $u) {
|
|
if ($u['username'] === $username) { $user = $u; break; }
|
|
}
|
|
// Palauta aina sama viesti (ei paljasta onko tunnus olemassa)
|
|
if ($user && !empty($user['email'])) {
|
|
$token = generateToken();
|
|
saveToken($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;">CuituNet 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 = validateToken($token);
|
|
if (!$userId) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Palautuslinkki on vanhentunut tai virheellinen']);
|
|
break;
|
|
}
|
|
$users = loadUsers();
|
|
foreach ($users as &$u) {
|
|
if ($u['id'] === $userId) {
|
|
$u['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT);
|
|
break;
|
|
}
|
|
}
|
|
unset($u);
|
|
saveUsers($users);
|
|
removeToken($token);
|
|
echo json_encode(['success' => true, 'message' => 'Salasana vaihdettu onnistuneesti']);
|
|
break;
|
|
|
|
case 'validate_reset_token':
|
|
$token = $_GET['token'] ?? '';
|
|
$userId = validateToken($token);
|
|
echo json_encode(['valid' => $userId !== null]);
|
|
break;
|
|
|
|
// ---------- USERS ----------
|
|
case 'users':
|
|
requireAdmin();
|
|
$users = loadUsers();
|
|
$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;
|
|
}
|
|
$users = loadUsers();
|
|
foreach ($users as $u) {
|
|
if ($u['username'] === $username) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Käyttäjätunnus on jo käytössä']);
|
|
break 2;
|
|
}
|
|
}
|
|
$companies = $input['companies'] ?? [];
|
|
// Validoi yritys-IDt
|
|
$allCompanies = loadCompanies();
|
|
$validIds = array_column($allCompanies, 'id');
|
|
$companies = array_values(array_filter($companies, fn($c) => in_array($c, $validIds)));
|
|
$newUser = [
|
|
'id' => generateId(),
|
|
'username' => $username,
|
|
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
|
'nimi' => $nimi ?: $username,
|
|
'email' => $email,
|
|
'role' => $role,
|
|
'companies' => $companies,
|
|
'luotu' => date('Y-m-d H:i:s'),
|
|
];
|
|
$users[] = $newUser;
|
|
saveUsers($users);
|
|
addLog('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'] ?? '';
|
|
$users = loadUsers();
|
|
$found = false;
|
|
foreach ($users as &$u) {
|
|
if ($u['id'] === $id) {
|
|
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 = loadCompanies();
|
|
$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);
|
|
}
|
|
$found = true;
|
|
addLog('user_update', '', '', "Muokkasi käyttäjää: {$u['username']}");
|
|
$safe = $u;
|
|
unset($safe['password_hash']);
|
|
echo json_encode($safe);
|
|
break;
|
|
}
|
|
}
|
|
unset($u);
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Käyttäjää ei löydy']);
|
|
break;
|
|
}
|
|
saveUsers($users);
|
|
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;
|
|
}
|
|
$users = loadUsers();
|
|
$deleted = null;
|
|
foreach ($users as $u) {
|
|
if ($u['id'] === $id) { $deleted = $u; break; }
|
|
}
|
|
$users = array_values(array_filter($users, fn($u) => $u['id'] !== $id));
|
|
saveUsers($users);
|
|
if ($deleted) addLog('user_delete', '', '', "Poisti käyttäjän: {$deleted['username']}");
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
// ---------- CHANGELOG ----------
|
|
case 'changelog':
|
|
requireAuth();
|
|
requireCompany();
|
|
$logFile = companyFile('changelog.json');
|
|
if (!file_exists($logFile)) file_put_contents($logFile, '[]');
|
|
$log = json_decode(file_get_contents($logFile), true) ?: [];
|
|
$limit = intval($_GET['limit'] ?? 100);
|
|
echo json_encode(array_slice($log, 0, $limit));
|
|
break;
|
|
|
|
// ---------- CUSTOMERS ----------
|
|
case 'customers':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method === 'GET') {
|
|
echo json_encode(loadCustomers());
|
|
}
|
|
break;
|
|
|
|
case 'customer':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method === 'POST') {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$customers = loadCustomers();
|
|
$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;
|
|
}
|
|
$customers[] = $customer;
|
|
saveCustomers($customers);
|
|
addLog('customer_create', $customer['id'], $customer['yritys'], 'Lisäsi asiakkaan');
|
|
echo json_encode($customer);
|
|
}
|
|
break;
|
|
|
|
case 'customer_update':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$customers = loadCustomers();
|
|
$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');
|
|
$found = true;
|
|
addLog('customer_update', $c['id'], $c['yritys'], 'Muokkasi: ' . implode(', ', $changes));
|
|
echo json_encode($c);
|
|
break;
|
|
}
|
|
}
|
|
unset($c);
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Asiakasta ei löydy']);
|
|
break;
|
|
}
|
|
saveCustomers($customers);
|
|
break;
|
|
|
|
case 'customer_delete':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$customers = loadCustomers();
|
|
$archived = null;
|
|
$remaining = [];
|
|
foreach ($customers as $c) {
|
|
if ($c['id'] === $id) {
|
|
$c['arkistoitu'] = date('Y-m-d H:i:s');
|
|
$c['arkistoija'] = currentUser();
|
|
$archived = $c;
|
|
} else {
|
|
$remaining[] = $c;
|
|
}
|
|
}
|
|
if ($archived) {
|
|
$archive = loadArchive();
|
|
$archive[] = $archived;
|
|
saveArchive($archive);
|
|
saveCustomers($remaining);
|
|
addLog('customer_archive', $archived['id'], $archived['yritys'], 'Arkistoi asiakkaan');
|
|
}
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
// ---------- ARCHIVE ----------
|
|
case 'archived_customers':
|
|
requireAuth();
|
|
requireCompany();
|
|
echo json_encode(loadArchive());
|
|
break;
|
|
|
|
case 'customer_restore':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$archive = loadArchive();
|
|
$restored = null;
|
|
$remaining = [];
|
|
foreach ($archive as $c) {
|
|
if ($c['id'] === $id) {
|
|
unset($c['arkistoitu'], $c['arkistoija']);
|
|
$restored = $c;
|
|
} else {
|
|
$remaining[] = $c;
|
|
}
|
|
}
|
|
if ($restored) {
|
|
$customers = loadCustomers();
|
|
$customers[] = $restored;
|
|
saveCustomers($customers);
|
|
saveArchive($remaining);
|
|
addLog('customer_restore', $restored['id'], $restored['yritys'], 'Palautti asiakkaan arkistosta');
|
|
}
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
case 'customer_permanent_delete':
|
|
requireAdmin();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$archive = loadArchive();
|
|
$deleted = null;
|
|
foreach ($archive as $c) {
|
|
if ($c['id'] === $id) { $deleted = $c; break; }
|
|
}
|
|
$archive = array_values(array_filter($archive, fn($c) => $c['id'] !== $id));
|
|
saveArchive($archive);
|
|
$filesDir = getCompanyDir() . '/files/' . $id;
|
|
if (is_dir($filesDir)) {
|
|
array_map('unlink', glob($filesDir . '/*'));
|
|
rmdir($filesDir);
|
|
}
|
|
if ($deleted) addLog('customer_permanent_delete', $id, $deleted['yritys'] ?? '', 'Poisti pysyvästi');
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
// ---------- LEADS ----------
|
|
case 'leads':
|
|
requireAuth();
|
|
requireCompany();
|
|
$leadsFile = companyFile('leads.json');
|
|
if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]');
|
|
$leads = json_decode(file_get_contents($leadsFile), true) ?: [];
|
|
echo json_encode($leads);
|
|
break;
|
|
|
|
case 'lead_create':
|
|
requireAuth();
|
|
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;
|
|
}
|
|
$leadsFile = companyFile('leads.json');
|
|
if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]');
|
|
$leads = json_decode(file_get_contents($leadsFile), true) ?: [];
|
|
$leads[] = $lead;
|
|
file_put_contents($leadsFile, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
addLog('lead_create', $lead['id'], $lead['yritys'], 'Lisäsi liidin');
|
|
echo json_encode($lead);
|
|
break;
|
|
|
|
case 'lead_update':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$leadsFile = companyFile('leads.json');
|
|
if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]');
|
|
$leads = json_decode(file_get_contents($leadsFile), true) ?: [];
|
|
$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;
|
|
addLog('lead_update', $l['id'], $l['yritys'], 'Muokkasi liidiä');
|
|
echo json_encode($l);
|
|
break;
|
|
}
|
|
}
|
|
unset($l);
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Liidiä ei löydy']);
|
|
break;
|
|
}
|
|
file_put_contents($leadsFile, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
break;
|
|
|
|
case 'lead_delete':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$leadsFile = companyFile('leads.json');
|
|
if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]');
|
|
$leads = json_decode(file_get_contents($leadsFile), true) ?: [];
|
|
$deleted = null;
|
|
foreach ($leads as $l) {
|
|
if ($l['id'] === $id) { $deleted = $l; break; }
|
|
}
|
|
$leads = array_values(array_filter($leads, fn($l) => $l['id'] !== $id));
|
|
file_put_contents($leadsFile, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
if ($deleted) addLog('lead_delete', $id, $deleted['yritys'] ?? '', 'Poisti liidin');
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
case 'lead_to_customer':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$leadsFile = companyFile('leads.json');
|
|
if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]');
|
|
$leads = json_decode(file_get_contents($leadsFile), true) ?: [];
|
|
$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'),
|
|
];
|
|
$customers = loadCustomers();
|
|
$customers[] = $customer;
|
|
saveCustomers($customers);
|
|
// Poista liidi
|
|
$leads = array_values(array_filter($leads, fn($l) => $l['id'] !== $id));
|
|
file_put_contents($leadsFile, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
addLog('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();
|
|
requireCompany();
|
|
$tickets = loadTickets();
|
|
// Palauta ilman viestisisältöjä (lista-näkymä)
|
|
// Auto-close tarkistus: sulje tiketit joiden auto_close_at on ohitettu
|
|
$now = date('Y-m-d H:i:s');
|
|
$autoCloseCount = 0;
|
|
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;
|
|
$autoCloseCount++;
|
|
}
|
|
}
|
|
unset($tc);
|
|
if ($autoCloseCount > 0) {
|
|
saveTickets($tickets);
|
|
addLog('ticket_auto_close', '', '', "Automaattisulku: $autoCloseCount tikettiä");
|
|
}
|
|
|
|
// Resolve mailbox names
|
|
$companyConf = loadCompanyConfig();
|
|
$mailboxNames = [];
|
|
foreach ($companyConf['mailboxes'] ?? [] as $mb) {
|
|
$mailboxNames[$mb['id']] = $mb['nimi'];
|
|
}
|
|
|
|
$list = array_map(function($t) use ($mailboxNames) {
|
|
$msgCount = count($t['messages'] ?? []);
|
|
$lastMsg = $msgCount > 0 ? $t['messages'][$msgCount - 1] : null;
|
|
return [
|
|
'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'] ?? ''] ?? '',
|
|
'created' => $t['created'],
|
|
'updated' => $t['updated'],
|
|
'message_count' => $msgCount,
|
|
'last_message_type' => $lastMsg ? ($lastMsg['type'] ?? '') : '',
|
|
'last_message_time' => $lastMsg ? ($lastMsg['timestamp'] ?? '') : '',
|
|
];
|
|
}, $tickets);
|
|
echo json_encode($list);
|
|
break;
|
|
|
|
case 'ticket_detail':
|
|
requireAuth();
|
|
requireCompany();
|
|
$id = $_GET['id'] ?? '';
|
|
$tickets = loadTickets();
|
|
$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();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
|
|
$companyConf = loadCompanyConfig();
|
|
$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 = loadTickets();
|
|
$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'],
|
|
];
|
|
|
|
$ticketIdx = findTicketByReferences($tickets, $email['in_reply_to'], $email['references']);
|
|
|
|
if ($ticketIdx !== null) {
|
|
$tickets[$ticketIdx]['messages'][] = $msg;
|
|
$tickets[$ticketIdx]['updated'] = $email['date'];
|
|
if (in_array($tickets[$ticketIdx]['status'], ['ratkaistu', 'suljettu'])) {
|
|
$tickets[$ticketIdx]['status'] = 'kasittelyssa';
|
|
}
|
|
$threadedCount++;
|
|
} 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;
|
|
}
|
|
}
|
|
|
|
$tickets[] = $ticket;
|
|
$newCount++;
|
|
}
|
|
|
|
if ($email['message_id']) $existingMsgIds[$email['message_id']] = true;
|
|
}
|
|
}
|
|
|
|
usort($tickets, function($a, $b) {
|
|
return strcmp($b['updated'], $a['updated']);
|
|
});
|
|
|
|
saveTickets($tickets);
|
|
addLog('ticket_fetch', '', '', "Haettu sähköpostit: {$newCount} uutta tikettiä, {$threadedCount} ketjutettu");
|
|
$result = ['success' => true, 'new_tickets' => $newCount, 'threaded' => $threadedCount, 'total' => count($tickets)];
|
|
if (!empty($errors)) $result['errors'] = $errors;
|
|
echo json_encode($result);
|
|
break;
|
|
|
|
case 'ticket_reply':
|
|
requireAuth();
|
|
requireCompany();
|
|
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 = loadTickets();
|
|
$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 = loadCompanyConfig();
|
|
$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];
|
|
}
|
|
|
|
$subject = 'Re: ' . $t['subject'];
|
|
$sent = sendTicketMail($t['from_email'], $subject, $body, $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
|
|
$reply = [
|
|
'id' => generateId(),
|
|
'type' => 'reply_out',
|
|
'from' => currentUser(),
|
|
'from_name' => $_SESSION['nimi'] ?? currentUser(),
|
|
'body' => $body,
|
|
'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';
|
|
|
|
$found = true;
|
|
addLog('ticket_reply', $t['id'], $t['subject'], 'Vastasi tikettiin');
|
|
echo json_encode($t);
|
|
break;
|
|
}
|
|
}
|
|
unset($t);
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
break;
|
|
}
|
|
saveTickets($tickets);
|
|
break;
|
|
|
|
case 'ticket_status':
|
|
requireAuth();
|
|
requireCompany();
|
|
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 = loadTickets();
|
|
$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');
|
|
$found = true;
|
|
addLog('ticket_status', $t['id'], $t['subject'], "Tila: {$oldStatus} → {$status}");
|
|
echo json_encode($t);
|
|
break;
|
|
}
|
|
}
|
|
unset($t);
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
break;
|
|
}
|
|
saveTickets($tickets);
|
|
break;
|
|
|
|
case 'ticket_type':
|
|
requireAuth();
|
|
requireCompany();
|
|
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 = loadTickets();
|
|
$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');
|
|
$found = true;
|
|
addLog('ticket_type', $t['id'], $t['subject'], "Tyyppi: {$oldType} → {$type}");
|
|
echo json_encode($t);
|
|
break;
|
|
}
|
|
}
|
|
unset($t);
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
break;
|
|
}
|
|
saveTickets($tickets);
|
|
break;
|
|
|
|
case 'ticket_customer':
|
|
requireAuth();
|
|
requireCompany();
|
|
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 = loadTickets();
|
|
$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');
|
|
$found = true;
|
|
addLog('ticket_customer', $t['id'], $t['subject'], "Asiakkuus: {$customerName}");
|
|
echo json_encode($t);
|
|
break;
|
|
}
|
|
}
|
|
unset($t);
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
break;
|
|
}
|
|
saveTickets($tickets);
|
|
break;
|
|
|
|
case 'ticket_assign':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$assignTo = trim($input['assigned_to'] ?? '');
|
|
$tickets = loadTickets();
|
|
$found = false;
|
|
foreach ($tickets as &$t) {
|
|
if ($t['id'] === $id) {
|
|
$t['assigned_to'] = $assignTo;
|
|
$t['updated'] = date('Y-m-d H:i:s');
|
|
$found = true;
|
|
addLog('ticket_assign', $t['id'], $t['subject'], "Osoitettu: {$assignTo}");
|
|
echo json_encode($t);
|
|
break;
|
|
}
|
|
}
|
|
unset($t);
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
break;
|
|
}
|
|
saveTickets($tickets);
|
|
break;
|
|
|
|
case 'ticket_note':
|
|
requireAuth();
|
|
requireCompany();
|
|
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 = loadTickets();
|
|
$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');
|
|
$found = true;
|
|
addLog('ticket_note', $t['id'], $t['subject'], 'Lisäsi muistiinpanon');
|
|
echo json_encode($t);
|
|
break;
|
|
}
|
|
}
|
|
unset($t);
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
break;
|
|
}
|
|
saveTickets($tickets);
|
|
break;
|
|
|
|
case 'ticket_delete':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = $input['id'] ?? '';
|
|
$tickets = loadTickets();
|
|
$deleted = null;
|
|
foreach ($tickets as $t) {
|
|
if ($t['id'] === $id) { $deleted = $t; break; }
|
|
}
|
|
$tickets = array_values(array_filter($tickets, fn($t) => $t['id'] !== $id));
|
|
saveTickets($tickets);
|
|
if ($deleted) addLog('ticket_delete', $id, $deleted['subject'] ?? '', 'Poisti tiketin');
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
case 'ticket_tags':
|
|
requireAuth();
|
|
requireCompany();
|
|
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 = loadTickets();
|
|
$found = false;
|
|
foreach ($tickets as &$t) {
|
|
if ($t['id'] === $id) {
|
|
$t['tags'] = $tags;
|
|
$t['updated'] = date('Y-m-d H:i:s');
|
|
$found = true;
|
|
addLog('ticket_tags', $t['id'], $t['subject'], 'Tagit: ' . implode(', ', $tags));
|
|
echo json_encode($t);
|
|
break;
|
|
}
|
|
}
|
|
unset($t);
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
|
break;
|
|
}
|
|
saveTickets($tickets);
|
|
break;
|
|
|
|
case 'ticket_rules':
|
|
requireAuth();
|
|
requireCompany();
|
|
$companyConf = loadCompanyConfig();
|
|
echo json_encode($companyConf['ticket_rules'] ?? []);
|
|
break;
|
|
|
|
case 'ticket_rule_save':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$companyConf = loadCompanyConfig();
|
|
$rules = $companyConf['ticket_rules'] ?? [];
|
|
|
|
$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;
|
|
}
|
|
|
|
$found = false;
|
|
foreach ($rules as &$r) {
|
|
if ($r['id'] === $rule['id']) {
|
|
$r = $rule;
|
|
$found = true;
|
|
break;
|
|
}
|
|
}
|
|
unset($r);
|
|
if (!$found) $rules[] = $rule;
|
|
|
|
$companyConf['ticket_rules'] = $rules;
|
|
saveCompanyConfig($companyConf);
|
|
addLog('config_update', '', '', 'Tikettisääntö: ' . $rule['name']);
|
|
echo json_encode($rule);
|
|
break;
|
|
|
|
case 'ticket_bulk_status':
|
|
requireAuth();
|
|
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 = loadTickets();
|
|
$changed = 0;
|
|
foreach ($tickets as &$t) {
|
|
if (in_array($t['id'], $ids)) {
|
|
$t['status'] = $newStatus;
|
|
$t['updated'] = date('Y-m-d H:i:s');
|
|
$changed++;
|
|
}
|
|
}
|
|
unset($t);
|
|
saveTickets($tickets);
|
|
addLog('ticket_status', '', '', "Massapäivitys: $changed tikettiä → $newStatus");
|
|
echo json_encode(['success' => true, 'changed' => $changed]);
|
|
break;
|
|
|
|
case 'ticket_bulk_delete':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$ids = $input['ids'] ?? [];
|
|
$tickets = loadTickets();
|
|
$before = count($tickets);
|
|
$tickets = array_values(array_filter($tickets, fn($t) => !in_array($t['id'], $ids)));
|
|
$deleted = $before - count($tickets);
|
|
saveTickets($tickets);
|
|
addLog('ticket_delete', '', '', "Massapoisto: $deleted tikettiä");
|
|
echo json_encode(['success' => true, 'deleted' => $deleted]);
|
|
break;
|
|
|
|
case 'ticket_rule_delete':
|
|
requireAuth();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$ruleId = $input['id'] ?? '';
|
|
$companyConf = loadCompanyConfig();
|
|
$rules = $companyConf['ticket_rules'] ?? [];
|
|
$companyConf['ticket_rules'] = array_values(array_filter($rules, fn($r) => $r['id'] !== $ruleId));
|
|
saveCompanyConfig($companyConf);
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
// ---------- COMPANY MANAGEMENT ----------
|
|
case 'companies':
|
|
requireAuth();
|
|
$userCompanyIds = $_SESSION['companies'] ?? [];
|
|
$allCompanies = loadCompanies();
|
|
$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(loadCompanies());
|
|
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 = loadCompanies();
|
|
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;
|
|
}
|
|
}
|
|
$company = [
|
|
'id' => $id,
|
|
'nimi' => $nimi,
|
|
'luotu' => date('Y-m-d H:i:s'),
|
|
'aktiivinen' => true,
|
|
];
|
|
$companies[] = $company;
|
|
saveCompanies($companies);
|
|
// Luo hakemisto ja tyhjät tiedostot
|
|
$compDir = DATA_DIR . '/companies/' . $id;
|
|
if (!file_exists($compDir)) mkdir($compDir, 0755, true);
|
|
file_put_contents($compDir . '/config.json', json_encode(['mailboxes' => [], 'ticket_rules' => []], JSON_PRETTY_PRINT));
|
|
file_put_contents($compDir . '/customers.json', '[]');
|
|
file_put_contents($compDir . '/leads.json', '[]');
|
|
file_put_contents($compDir . '/archive.json', '[]');
|
|
file_put_contents($compDir . '/tickets.json', '[]');
|
|
file_put_contents($compDir . '/changelog.json', '[]');
|
|
// Lisää luoja yrityksen käyttäjäksi
|
|
$users = loadUsers();
|
|
foreach ($users as &$u) {
|
|
if ($u['id'] === $_SESSION['user_id']) {
|
|
$u['companies'] = array_unique(array_merge($u['companies'] ?? [], [$id]));
|
|
$_SESSION['companies'] = $u['companies'];
|
|
break;
|
|
}
|
|
}
|
|
unset($u);
|
|
saveUsers($users);
|
|
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 = loadCompanies();
|
|
$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'];
|
|
$found = true;
|
|
echo json_encode($c);
|
|
break;
|
|
}
|
|
}
|
|
unset($c);
|
|
if (!$found) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Yritystä ei löydy']);
|
|
break;
|
|
}
|
|
saveCompanies($companies);
|
|
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;
|
|
}
|
|
$companies = loadCompanies();
|
|
$companies = array_values(array_filter($companies, fn($c) => $c['id'] !== $id));
|
|
saveCompanies($companies);
|
|
// Poista yritys käyttäjiltä
|
|
$users = loadUsers();
|
|
foreach ($users as &$u) {
|
|
$u['companies'] = array_values(array_filter($u['companies'] ?? [], fn($c) => $c !== $id));
|
|
}
|
|
unset($u);
|
|
saveUsers($users);
|
|
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();
|
|
requireCompany();
|
|
echo json_encode(loadCompanyConfig());
|
|
break;
|
|
|
|
case 'company_config_update':
|
|
requireAdmin();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$companyConf = loadCompanyConfig();
|
|
if (isset($input['mailboxes'])) $companyConf['mailboxes'] = $input['mailboxes'];
|
|
if (isset($input['ticket_rules'])) $companyConf['ticket_rules'] = $input['ticket_rules'];
|
|
saveCompanyConfig($companyConf);
|
|
echo json_encode($companyConf);
|
|
break;
|
|
|
|
// ---------- MAILBOXES ----------
|
|
case 'mailboxes':
|
|
requireAuth();
|
|
requireCompany();
|
|
$companyConf = loadCompanyConfig();
|
|
// Palauta postilaatikot ilman salasanoja
|
|
$mbs = array_map(function($mb) {
|
|
$mb['imap_password'] = !empty($mb['imap_password']) ? '********' : '';
|
|
return $mb;
|
|
}, $companyConf['mailboxes'] ?? []);
|
|
echo json_encode($mbs);
|
|
break;
|
|
|
|
case 'mailbox_save':
|
|
requireAdmin();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$companyConf = loadCompanyConfig();
|
|
$mailboxes = $companyConf['mailboxes'] ?? [];
|
|
|
|
$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
|
|
foreach ($mailboxes as $existing) {
|
|
if ($existing['id'] === $mb['id']) {
|
|
$mb['imap_password'] = $existing['imap_password'] ?? '';
|
|
break;
|
|
}
|
|
}
|
|
if (!isset($mb['imap_password'])) $mb['imap_password'] = '';
|
|
}
|
|
|
|
if (empty($mb['nimi'])) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Postilaatikon nimi puuttuu']);
|
|
break;
|
|
}
|
|
|
|
$found = false;
|
|
foreach ($mailboxes as &$existing) {
|
|
if ($existing['id'] === $mb['id']) {
|
|
$existing = $mb;
|
|
$found = true;
|
|
break;
|
|
}
|
|
}
|
|
unset($existing);
|
|
if (!$found) $mailboxes[] = $mb;
|
|
|
|
$companyConf['mailboxes'] = $mailboxes;
|
|
saveCompanyConfig($companyConf);
|
|
addLog('mailbox_save', '', '', 'Postilaatikko: ' . $mb['nimi']);
|
|
// Palauta ilman salasanaa
|
|
$mb['imap_password'] = '********';
|
|
echo json_encode($mb);
|
|
break;
|
|
|
|
case 'mailbox_delete':
|
|
requireAdmin();
|
|
requireCompany();
|
|
if ($method !== 'POST') break;
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$mbId = $input['id'] ?? '';
|
|
$companyConf = loadCompanyConfig();
|
|
$companyConf['mailboxes'] = array_values(array_filter($companyConf['mailboxes'] ?? [], fn($m) => $m['id'] !== $mbId));
|
|
saveCompanyConfig($companyConf);
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
default:
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Tuntematon toiminto']);
|
|
break;
|
|
}
|