White-label multi-domain tuki (Noxus Intra)

- CSS-muuttujat: kaikki kovakoodatut #0f3460/#16213e korvattu var(--primary-color)/var(--primary-dark)
- Uudet API-endpointit: branding (julkinen, domain-pohjainen), company_logo, company_logo_upload
- Domain-pohjainen brändäys: HTTP_HOST → yrityksen domains-arrayn matchaus
- Login: domain asettaa oletusyrityksen sessioon
- check_auth: palauttaa branding-objektin (primary_color, subtitle, logo_url)
- company_create/update: käsittelee domains, primary_color, subtitle, logo_file
- Dynaaminen login-sivu, header ja footer (logo, nimi, alaotsikko, värit)
- JS: loadBranding(), applyBranding(), yritysvaihdon brändäyspäivitys
- Admin-paneeli: brändäysasetukset (logo-upload, väri, alaotsikko, domainit)
- Git-repo siirretty intra.noxus.fi:hin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 13:44:49 +02:00
parent 918a5ff120
commit 095dc90b6f
4 changed files with 381 additions and 54 deletions

186
api.php
View File

@@ -1016,6 +1016,143 @@ switch ($action) {
echo json_encode(['question' => "$a + $b = ?"]);
break;
// ---------- BRANDING (julkinen) ----------
case 'branding':
$host = $_SERVER['HTTP_HOST'] ?? '';
// Stripaa portti pois (localhost:3001 → localhost)
$host = strtolower(explode(':', $host)[0]);
$companies = loadCompanies();
$matchedCompany = null;
foreach ($companies as $comp) {
$domains = $comp['domains'] ?? [];
foreach ($domains as $d) {
if (strtolower(trim($d)) === strtolower($host)) {
$matchedCompany = $comp;
break 2;
}
}
}
if ($matchedCompany) {
$logoUrl = !empty($matchedCompany['logo_file'])
? "api.php?action=company_logo&company_id=" . urlencode($matchedCompany['id'])
: '';
echo json_encode([
'found' => true,
'company_id' => $matchedCompany['id'],
'nimi' => $matchedCompany['nimi'],
'primary_color' => $matchedCompany['primary_color'] ?? '#0f3460',
'subtitle' => $matchedCompany['subtitle'] ?? '',
'logo_url' => $logoUrl,
]);
} else {
// Noxus Intra -oletusbrändäys
echo json_encode([
'found' => false,
'company_id' => '',
'nimi' => 'Noxus Intra',
'primary_color' => '#0f3460',
'subtitle' => 'Hallintapaneeli',
'logo_url' => '',
]);
}
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 = loadCompanies();
$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 = loadCompanies();
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;
break;
}
}
unset($comp);
saveCompanies($companies);
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;
@@ -1050,8 +1187,24 @@ switch ($action) {
// 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] : '';
// Domain-pohjainen oletusyritys
$host = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]);
$domainCompanyId = '';
$allComps = loadCompanies();
foreach ($allComps as $dc) {
foreach ($dc['domains'] ?? [] as $d) {
if (strtolower(trim($d)) === strtolower($host)) {
$domainCompanyId = $dc['id'];
break 2;
}
}
}
// 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 = loadCompanies();
$companyList = [];
@@ -1116,6 +1269,20 @@ switch ($action) {
break;
}
}
// 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'],
@@ -1125,6 +1292,7 @@ switch ($action) {
'companies' => $companyList,
'company_id' => $_SESSION['company_id'] ?? '',
'signatures' => $userSignatures,
'branding' => $branding,
]);
} else {
echo json_encode(['authenticated' => false]);
@@ -2479,9 +2647,18 @@ switch ($action) {
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,
];
@@ -2521,6 +2698,11 @@ switch ($action) {
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']);
$found = true;
echo json_encode($c);
break;