feat: moduulijärjestelmä + käyttäjäroolit + suhteellinen aika

- Moduulijärjestelmä: yrityskohtaiset tabit (customers, support, leads, archive,
  changelog, settings) valittavissa checkboxeina yrityksen asetuksissa
- Käyttäjäroolit: superadmin (pääkäyttäjä), admin (yritysadmin), user (käyttäjä)
  - Superadmin: kaikki oikeudet kuten ennen
  - Yritysadmin: muokkaa oman yrityksen asetuksia, moduuleita, postilaatikoita
  - Käyttäjä: peruskäyttö ilman hallintaoikeuksia
- Päivitetty-kenttä näyttää suhteellista aikaa (15min sitten, 2h sitten, 3pv sitten)
- DB: enabled_modules sarake companies-tauluun, role ENUM laajennettu
- Automaattinen migraatio: vanhat admin → superadmin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 18:42:07 +02:00
parent 86ffcc88de
commit a135aaaaef
5 changed files with 225 additions and 50 deletions

75
api.php
View File

@@ -38,13 +38,32 @@ function requireAuth() {
function requireAdmin() {
requireAuth();
if (($_SESSION['role'] ?? '') !== 'admin') {
$role = $_SESSION['role'] ?? '';
if ($role !== 'admin' && $role !== 'superadmin') {
http_response_code(403);
echo json_encode(['error' => 'Vain ylläpitäjä voi tehdä tämän']);
exit;
}
}
function requireSuperAdmin() {
requireAuth();
if (($_SESSION['role'] ?? '') !== 'superadmin') {
http_response_code(403);
echo json_encode(['error' => 'Vain pääkäyttäjä voi tehdä tämän']);
exit;
}
}
function isSuperAdmin(): bool {
return ($_SESSION['role'] ?? '') === 'superadmin';
}
function isCompanyAdmin(): bool {
$role = $_SESSION['role'] ?? '';
return $role === 'admin' || $role === 'superadmin';
}
function currentUser(): string {
return $_SESSION['username'] ?? 'tuntematon';
}
@@ -884,7 +903,7 @@ switch ($action) {
$domainCompany = dbGetCompanyByDomain($host);
$domainCompanyId = $domainCompany ? $domainCompany['id'] : '';
// Jos domain kuuluu tietylle yritykselle, vain sen yrityksen käyttäjät + adminit pääsevät sisään
if ($domainCompanyId && $u['role'] !== 'admin' && !in_array($domainCompanyId, $userCompanies)) {
if ($domainCompanyId && $u['role'] !== 'superadmin' && !in_array($domainCompanyId, $userCompanies)) {
dbRecordLoginAttempt($ip);
http_response_code(403);
echo json_encode(['error' => 'Sinulla ei ole oikeutta kirjautua tälle sivustolle.']);
@@ -956,6 +975,15 @@ switch ($action) {
// Brändäystiedot domain-pohjaisesti (sama kuin branding-endpoint)
$host = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]);
$branding = dbGetBranding($host);
// Aktiivisen yrityksen enabled_modules
$activeCompanyId = $_SESSION['company_id'] ?? '';
$enabledModules = [];
foreach ($allCompanies as $comp) {
if ($comp['id'] === $activeCompanyId) {
$enabledModules = $comp['enabled_modules'] ?? [];
break;
}
}
echo json_encode([
'authenticated' => true,
'user_id' => $_SESSION['user_id'],
@@ -966,6 +994,7 @@ switch ($action) {
'company_id' => $_SESSION['company_id'] ?? '',
'signatures' => $userSignatures,
'branding' => $branding,
'enabled_modules' => $enabledModules,
]);
} else {
echo json_encode(['authenticated' => false]);
@@ -1035,7 +1064,7 @@ switch ($action) {
// ---------- USERS ----------
case 'users':
requireAdmin();
requireSuperAdmin();
$users = dbLoadUsers();
$safe = array_map(function($u) {
unset($u['password_hash']);
@@ -1045,14 +1074,15 @@ switch ($action) {
break;
case 'user_create':
requireAdmin();
requireSuperAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$username = trim($input['username'] ?? '');
$password = $input['password'] ?? '';
$nimi = trim($input['nimi'] ?? '');
$email = trim($input['email'] ?? '');
$role = ($input['role'] ?? 'user') === 'admin' ? 'admin' : 'user';
$validRoles = ['superadmin', 'admin', 'user'];
$role = in_array($input['role'] ?? '', $validRoles) ? $input['role'] : 'user';
if (empty($username) || empty($password)) {
http_response_code(400);
echo json_encode(['error' => 'Käyttäjätunnus ja salasana vaaditaan']);
@@ -1099,7 +1129,7 @@ switch ($action) {
break;
case 'user_update':
requireAdmin();
requireSuperAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
@@ -1111,7 +1141,10 @@ switch ($action) {
}
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['role'])) {
$validRoles = ['superadmin', 'admin', 'user'];
$u['role'] = in_array($input['role'], $validRoles) ? $input['role'] : 'user';
}
if (isset($input['companies'])) {
$allCompanies = dbLoadCompanies();
$validIds = array_column($allCompanies, 'id');
@@ -1146,7 +1179,7 @@ switch ($action) {
break;
case 'user_delete':
requireAdmin();
requireSuperAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
@@ -2297,7 +2330,15 @@ switch ($action) {
case 'companies_all':
requireAdmin();
echo json_encode(dbLoadCompanies());
$all = dbLoadCompanies();
if (isSuperAdmin()) {
echo json_encode($all);
} else {
// Yritysadmin näkee vain omat yrityksensä
$userCompanyIds = $_SESSION['companies'] ?? [];
$filtered = array_values(array_filter($all, fn($c) => in_array($c['id'], $userCompanyIds)));
echo json_encode($filtered);
}
break;
case 'all_mailboxes':
@@ -2322,7 +2363,7 @@ switch ($action) {
break;
case 'company_create':
requireAdmin();
requireSuperAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = preg_replace('/[^a-z0-9-]/', '', strtolower(trim($input['id'] ?? '')));
@@ -2374,6 +2415,15 @@ switch ($action) {
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
// Yritysadmin saa muokata vain omia yrityksiään
if (!isSuperAdmin()) {
$userCompanyIds = $_SESSION['companies'] ?? [];
if (!in_array($id, $userCompanyIds)) {
http_response_code(403);
echo json_encode(['error' => 'Ei oikeuksia muokata tätä yritystä']);
break;
}
}
$companies = dbLoadCompanies();
$found = false;
foreach ($companies as $c) {
@@ -2385,6 +2435,9 @@ switch ($action) {
}
if (isset($input['primary_color'])) $c['primary_color'] = trim($input['primary_color']);
if (isset($input['subtitle'])) $c['subtitle'] = trim($input['subtitle']);
if (isset($input['enabled_modules']) && is_array($input['enabled_modules'])) {
$c['enabled_modules'] = array_values($input['enabled_modules']);
}
dbSaveCompany($c);
$found = true;
echo json_encode($c);
@@ -2398,7 +2451,7 @@ switch ($action) {
break;
case 'company_delete':
requireAdmin();
requireSuperAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';