Yrityskohtaiset käyttäjäroolit + IP-rajoitus bugikorjaus

- Lisää role-sarake user_companies-tauluun (admin/user per yritys)
- Migraatio: kopioi vanhat admin-roolit user_companies-tauluun, muuta globaali admin → user
- Päivitä dbSaveUser/dbLoadUsers/dbGetUser/dbGetUserByUsername käsittelemään company_roles
- isCompanyAdmin() tarkistaa nyt yrityskohtaisen roolin (session company_role)
- requireAdmin() käyttää isCompanyAdmin():ia
- requireCompany() tarkistaa IP-rajoituksen (siirretty login/check_auth:sta)
- Login ei enää estä kirjautumista IP:n perusteella, vaan merkitsee ip_blocked
- check_auth näyttää kaikki yritykset, IP-estetyt merkitään ip_blocked:lla
- company_switch palauttaa company_role ja päivittää session
- Frontend: käyttäjälomakkeessa yrityskohtaiset rooli-dropdownit (admin/käyttäjä)
- Frontend: yritysvaihto päivittää admin-näkyvyyden company_rolen mukaan
- Frontend: yritysvalitsimessa IP-estetyt yritykset näkyvät "(IP-rajoitus)" -tekstillä

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 20:45:18 +02:00
parent 4c128f5c71
commit 68c9075676
4 changed files with 194 additions and 89 deletions

122
api.php
View File

@@ -38,8 +38,7 @@ function requireAuth() {
function requireAdmin() { function requireAdmin() {
requireAuth(); requireAuth();
$role = $_SESSION['role'] ?? ''; if (!isCompanyAdmin()) {
if ($role !== 'admin' && $role !== 'superadmin') {
http_response_code(403); http_response_code(403);
echo json_encode(['error' => 'Vain ylläpitäjä voi tehdä tämän']); echo json_encode(['error' => 'Vain ylläpitäjä voi tehdä tämän']);
exit; exit;
@@ -60,8 +59,8 @@ function isSuperAdmin(): bool {
} }
function isCompanyAdmin(): bool { function isCompanyAdmin(): bool {
$role = $_SESSION['role'] ?? ''; if (($_SESSION['role'] ?? '') === 'superadmin') return true;
return $role === 'admin' || $role === 'superadmin'; return ($_SESSION['company_role'] ?? '') === 'admin';
} }
function currentUser(): string { function currentUser(): string {
@@ -103,6 +102,20 @@ function requireCompany(): string {
echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']); echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']);
exit; exit;
} }
// IP-rajoitus: tarkista vasta kun yrityksen dataa käytetään (superadmin ohittaa)
if (($_SESSION['role'] ?? '') !== 'superadmin') {
$allCompanies = dbLoadCompanies();
foreach ($allCompanies as $comp) {
if ($comp['id'] === $companyId) {
if (!isIpAllowed(getClientIp(), $comp['allowed_ips'] ?? '')) {
http_response_code(403);
echo json_encode(['error' => 'IP-osoitteesi ei ole sallittu tälle yritykselle.']);
exit;
}
break;
}
}
}
return $companyId; return $companyId;
} }
@@ -1329,45 +1342,33 @@ switch ($action) {
echo json_encode(['error' => 'Sinulla ei ole oikeutta kirjautua tälle sivustolle.']); echo json_encode(['error' => 'Sinulla ei ole oikeutta kirjautua tälle sivustolle.']);
break; break;
} }
// IP-rajoitus: superadmin ohittaa aina // IP-rajoitus EI enää estä kirjautumista — tarkistetaan vasta requireCompany():ssa
$allCompanies = dbLoadCompanies(); $allCompanies = dbLoadCompanies();
if ($u['role'] !== 'superadmin') {
$allowedCompanies = [];
foreach ($userCompanies as $ucId) {
foreach ($allCompanies as $comp) {
if ($comp['id'] === $ucId) {
if (isIpAllowed($ip, $comp['allowed_ips'] ?? '')) {
$allowedCompanies[] = $ucId;
}
break;
}
}
}
if (empty($allowedCompanies)) {
dbRecordLoginAttempt($ip);
http_response_code(403);
echo json_encode(['error' => 'IP-osoitteesi (' . $ip . ') ei ole sallittu.']);
break;
}
$userCompanies = $allowedCompanies;
}
session_regenerate_id(true); session_regenerate_id(true);
$_SESSION['user_id'] = $u['id']; $_SESSION['user_id'] = $u['id'];
$_SESSION['username'] = $u['username']; $_SESSION['username'] = $u['username'];
$_SESSION['nimi'] = $u['nimi']; $_SESSION['nimi'] = $u['nimi'];
$_SESSION['role'] = $u['role']; $_SESSION['role'] = $u['role'];
$_SESSION['companies'] = $userCompanies; $_SESSION['companies'] = $userCompanies;
$_SESSION['company_roles'] = $u['company_roles'] ?? [];
// Jos domain matchaa ja käyttäjällä on oikeus -> käytä sitä // Jos domain matchaa ja käyttäjällä on oikeus -> käytä sitä
if ($domainCompanyId && in_array($domainCompanyId, $userCompanies)) { if ($domainCompanyId && in_array($domainCompanyId, $userCompanies)) {
$_SESSION['company_id'] = $domainCompanyId; $_SESSION['company_id'] = $domainCompanyId;
} else { } else {
$_SESSION['company_id'] = !empty($userCompanies) ? $userCompanies[0] : ''; $_SESSION['company_id'] = !empty($userCompanies) ? $userCompanies[0] : '';
} }
// Hae yritysten nimet // Aseta aktiivisen yrityksen rooli
$_SESSION['company_role'] = $_SESSION['company_roles'][$_SESSION['company_id']] ?? 'user';
// Hae yritysten nimet + IP-status
$companyList = []; $companyList = [];
foreach ($allCompanies as $comp) { foreach ($allCompanies as $comp) {
if (in_array($comp['id'], $userCompanies)) { if (in_array($comp['id'], $userCompanies)) {
$companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']]; $entry = ['id' => $comp['id'], 'nimi' => $comp['nimi']];
// Merkitse IP-estetyt yritykset (superadmin ohittaa)
if ($u['role'] !== 'superadmin' && !isIpAllowed($ip, $comp['allowed_ips'] ?? '')) {
$entry['ip_blocked'] = true;
}
$companyList[] = $entry;
} }
} }
echo json_encode([ echo json_encode([
@@ -1375,6 +1376,7 @@ switch ($action) {
'username' => $u['username'], 'username' => $u['username'],
'nimi' => $u['nimi'], 'nimi' => $u['nimi'],
'role' => $u['role'], 'role' => $u['role'],
'company_role' => $_SESSION['company_role'],
'companies' => $companyList, 'companies' => $companyList,
'company_id' => $_SESSION['company_id'], 'company_id' => $_SESSION['company_id'],
'signatures' => buildSignaturesWithDefaults($u, $u['companies'] ?? []), 'signatures' => buildSignaturesWithDefaults($u, $u['companies'] ?? []),
@@ -1397,37 +1399,29 @@ switch ($action) {
$u = dbGetUser($_SESSION['user_id']); $u = dbGetUser($_SESSION['user_id']);
if ($u) { if ($u) {
$_SESSION['companies'] = $u['companies'] ?? []; $_SESSION['companies'] = $u['companies'] ?? [];
$_SESSION['company_roles'] = $u['company_roles'] ?? [];
// Varmista aktiivinen yritys on sallittu // Varmista aktiivinen yritys on sallittu
if (!in_array($_SESSION['company_id'] ?? '', $_SESSION['companies'])) { if (!in_array($_SESSION['company_id'] ?? '', $_SESSION['companies'])) {
$_SESSION['company_id'] = !empty($_SESSION['companies']) ? $_SESSION['companies'][0] : ''; $_SESSION['company_id'] = !empty($_SESSION['companies']) ? $_SESSION['companies'][0] : '';
} }
// Päivitä aktiivisen yrityksen rooli
$_SESSION['company_role'] = $_SESSION['company_roles'][$_SESSION['company_id'] ?? ''] ?? 'user';
} }
// Hae yritysten nimet + IP-rajoitus // Hae yritysten nimet — EI suodata IP:n perusteella pois, vaan merkitään ip_blocked
$userCompanyIds = $_SESSION['companies'] ?? []; $userCompanyIds = $_SESSION['companies'] ?? [];
$allCompanies = dbLoadCompanies(); $allCompanies = dbLoadCompanies();
$ip = getClientIp(); $ip = getClientIp();
$companyList = []; $companyList = [];
foreach ($allCompanies as $comp) { foreach ($allCompanies as $comp) {
if (in_array($comp['id'], $userCompanyIds)) { if (in_array($comp['id'], $userCompanyIds)) {
// IP-rajoitus: superadmin ohittaa aina $entry = ['id' => $comp['id'], 'nimi' => $comp['nimi']];
// Merkitse IP-estetyt yritykset (superadmin ohittaa)
if (($_SESSION['role'] ?? '') !== 'superadmin' && !isIpAllowed($ip, $comp['allowed_ips'] ?? '')) { if (($_SESSION['role'] ?? '') !== 'superadmin' && !isIpAllowed($ip, $comp['allowed_ips'] ?? '')) {
continue; $entry['ip_blocked'] = true;
} }
$companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']]; $companyList[] = $entry;
} }
} }
// Jos IP-rajoitus poistaa kaikki yritykset (ei superadmin) → kirjaa ulos
if (empty($companyList) && ($_SESSION['role'] ?? '') !== 'superadmin') {
session_destroy();
echo json_encode(['authenticated' => false]);
break;
}
// Päivitä session companies IP-suodatuksen mukaan
$allowedIds = array_column($companyList, 'id');
$_SESSION['companies'] = $allowedIds;
if (!in_array($_SESSION['company_id'] ?? '', $allowedIds) && !empty($allowedIds)) {
$_SESSION['company_id'] = $allowedIds[0];
}
// Hae allekirjoitukset (oletus generoituna jos omaa ei ole) // Hae allekirjoitukset (oletus generoituna jos omaa ei ole)
$userSignatures = $u ? buildSignaturesWithDefaults($u, $u['companies'] ?? []) : []; $userSignatures = $u ? buildSignaturesWithDefaults($u, $u['companies'] ?? []) : [];
// Brändäystiedot domain-pohjaisesti (sama kuin branding-endpoint) // Brändäystiedot domain-pohjaisesti (sama kuin branding-endpoint)
@@ -1449,6 +1443,7 @@ switch ($action) {
'nimi' => $_SESSION['nimi'], 'nimi' => $_SESSION['nimi'],
'email' => $u['email'] ?? '', 'email' => $u['email'] ?? '',
'role' => $_SESSION['role'], 'role' => $_SESSION['role'],
'company_role' => $_SESSION['company_role'] ?? 'user',
'companies' => $companyList, 'companies' => $companyList,
'company_id' => $_SESSION['company_id'] ?? '', 'company_id' => $_SESSION['company_id'] ?? '',
'signatures' => $userSignatures, 'signatures' => $userSignatures,
@@ -1549,7 +1544,8 @@ switch ($action) {
$nimi = trim($input['nimi'] ?? ''); $nimi = trim($input['nimi'] ?? '');
$email = trim($input['email'] ?? ''); $email = trim($input['email'] ?? '');
$isSA = ($_SESSION['role'] ?? '') === 'superadmin'; $isSA = ($_SESSION['role'] ?? '') === 'superadmin';
$validRoles = $isSA ? ['superadmin', 'admin', 'user'] : ['admin', 'user']; // Globaali rooli: user tai superadmin (admin on nyt yrityskohtainen)
$validRoles = $isSA ? ['superadmin', 'user'] : ['user'];
$role = in_array($input['role'] ?? '', $validRoles) ? $input['role'] : 'user'; $role = in_array($input['role'] ?? '', $validRoles) ? $input['role'] : 'user';
if (empty($username) || empty($password)) { if (empty($username) || empty($password)) {
http_response_code(400); http_response_code(400);
@@ -1583,6 +1579,19 @@ switch ($action) {
$signatures[(string)$mbId] = (string)$sig; $signatures[(string)$mbId] = (string)$sig;
} }
} }
// Yrityskohtaiset roolit
$companyRoles = [];
if ($isSA && isset($input['company_roles']) && is_array($input['company_roles'])) {
foreach ($input['company_roles'] as $cid => $crole) {
if (in_array($cid, $companies) && in_array($crole, ['admin', 'user'])) {
$companyRoles[$cid] = $crole;
}
}
} elseif (!$isSA) {
// Admin luo käyttäjiä vain omaan yritykseensä -> oletusrooli user
$myCompanyId = $_SESSION['company_id'] ?? '';
$companyRoles[$myCompanyId] = 'user';
}
$newUser = [ $newUser = [
'id' => generateId(), 'id' => generateId(),
'username' => $username, 'username' => $username,
@@ -1591,6 +1600,7 @@ switch ($action) {
'email' => $email, 'email' => $email,
'role' => $role, 'role' => $role,
'companies' => $companies, 'companies' => $companies,
'company_roles' => $companyRoles,
'signatures' => $signatures, 'signatures' => $signatures,
'luotu' => date('Y-m-d H:i:s'), 'luotu' => date('Y-m-d H:i:s'),
]; ];
@@ -1623,7 +1633,8 @@ switch ($action) {
if (isset($input['nimi'])) $u['nimi'] = trim($input['nimi']); if (isset($input['nimi'])) $u['nimi'] = trim($input['nimi']);
if (isset($input['email'])) $u['email'] = trim($input['email']); if (isset($input['email'])) $u['email'] = trim($input['email']);
if (isset($input['role'])) { if (isset($input['role'])) {
$validRoles = $isSA ? ['superadmin', 'admin', 'user'] : ['admin', 'user']; // Globaali rooli: user tai superadmin (admin on nyt yrityskohtainen)
$validRoles = $isSA ? ['superadmin', 'user'] : ['user'];
// Admin ei voi muuttaa superadminia // Admin ei voi muuttaa superadminia
if (!$isSA && ($u['role'] === 'superadmin')) { if (!$isSA && ($u['role'] === 'superadmin')) {
// Älä muuta roolia // Älä muuta roolia
@@ -1636,6 +1647,16 @@ switch ($action) {
$validIds = array_column($allCompanies, 'id'); $validIds = array_column($allCompanies, 'id');
$u['companies'] = array_values(array_filter($input['companies'], fn($c) => in_array($c, $validIds))); $u['companies'] = array_values(array_filter($input['companies'], fn($c) => in_array($c, $validIds)));
} }
// Yrityskohtaiset roolit
if (isset($input['company_roles']) && is_array($input['company_roles'])) {
$companyRoles = $u['company_roles'] ?? [];
foreach ($input['company_roles'] as $cid => $crole) {
if (in_array($cid, $u['companies'] ?? []) && in_array($crole, ['admin', 'user'])) {
$companyRoles[$cid] = $crole;
}
}
$u['company_roles'] = $companyRoles;
}
if (!empty($input['password'])) { if (!empty($input['password'])) {
$u['password_hash'] = password_hash($input['password'], PASSWORD_DEFAULT); $u['password_hash'] = password_hash($input['password'], PASSWORD_DEFAULT);
} }
@@ -1652,12 +1673,15 @@ switch ($action) {
// Päivitä sessio jos muokattiin kirjautunutta käyttäjää // Päivitä sessio jos muokattiin kirjautunutta käyttäjää
if ($u['id'] === $_SESSION['user_id']) { if ($u['id'] === $_SESSION['user_id']) {
$_SESSION['companies'] = $u['companies'] ?? []; $_SESSION['companies'] = $u['companies'] ?? [];
$_SESSION['company_roles'] = $u['company_roles'] ?? [];
if (!empty($u['companies']) && !in_array($_SESSION['company_id'] ?? '', $u['companies'])) { if (!empty($u['companies']) && !in_array($_SESSION['company_id'] ?? '', $u['companies'])) {
$_SESSION['company_id'] = $u['companies'][0]; $_SESSION['company_id'] = $u['companies'][0];
} }
if (empty($u['companies'])) { if (empty($u['companies'])) {
$_SESSION['company_id'] = ''; $_SESSION['company_id'] = '';
} }
// Päivitä aktiivisen yrityksen rooli
$_SESSION['company_role'] = ($_SESSION['company_roles'][$_SESSION['company_id'] ?? '']) ?? 'user';
} }
$safe = $u; $safe = $u;
unset($safe['password_hash']); unset($safe['password_hash']);
@@ -3755,7 +3779,13 @@ switch ($action) {
} }
} }
$_SESSION['company_id'] = $companyId; $_SESSION['company_id'] = $companyId;
echo json_encode(['success' => true, 'company_id' => $companyId]); // Päivitä aktiivisen yrityksen rooli
$_SESSION['company_role'] = ($_SESSION['company_roles'] ?? [])[$companyId] ?? 'user';
echo json_encode([
'success' => true,
'company_id' => $companyId,
'company_role' => $_SESSION['company_role'],
]);
break; break;
case 'company_config': case 'company_config':

34
db.php
View File

@@ -591,6 +591,7 @@ function initDatabase(): void {
"ALTER TABLE mailboxes ADD COLUMN auto_reply_body TEXT AFTER auto_reply_enabled", "ALTER TABLE mailboxes ADD COLUMN auto_reply_body TEXT AFTER auto_reply_enabled",
"ALTER TABLE companies ADD COLUMN allowed_ips TEXT DEFAULT '' AFTER enabled_modules", "ALTER TABLE companies ADD COLUMN allowed_ips TEXT DEFAULT '' AFTER enabled_modules",
"ALTER TABLE todos ADD COLUMN category VARCHAR(30) DEFAULT '' AFTER priority", "ALTER TABLE todos ADD COLUMN category VARCHAR(30) DEFAULT '' AFTER priority",
"ALTER TABLE user_companies ADD COLUMN role VARCHAR(20) DEFAULT 'user' AFTER company_id",
]; ];
foreach ($alters as $sql) { foreach ($alters as $sql) {
try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ } try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ }
@@ -604,6 +605,17 @@ function initDatabase(): void {
$db->query("UPDATE users SET role = 'superadmin' WHERE role = 'admin'"); $db->query("UPDATE users SET role = 'superadmin' WHERE role = 'admin'");
} }
} catch (\Throwable $e) { /* ohitetaan */ } } catch (\Throwable $e) { /* ohitetaan */ }
// Migraatio: kopioi admin-käyttäjien rooli user_companies-tauluun
// (kun role-sarake lisätty, olemassa olevat admin-käyttäjät saavat admin-roolin kaikkiin yrityksiinsä)
try {
$db->query("UPDATE user_companies uc JOIN users u ON u.id = uc.user_id SET uc.role = 'admin' WHERE u.role = 'admin' AND uc.role = 'user'");
} catch (\Throwable $e) { /* ohitetaan */ }
// Migraatio: muuta globaali 'admin' → 'user' (admin on nyt yrityskohtainen user_companies.role)
try {
$db->query("UPDATE users SET role = 'user' WHERE role = 'admin'");
} catch (\Throwable $e) { /* ohitetaan */ }
} }
// ==================== YRITYKSET ==================== // ==================== YRITYKSET ====================
@@ -730,6 +742,12 @@ function dbLoadUsers(): array {
foreach ($users as &$u) { foreach ($users as &$u) {
$u['companies'] = _dbFetchColumn("SELECT company_id FROM user_companies WHERE user_id = ?", [$u['id']]); $u['companies'] = _dbFetchColumn("SELECT company_id FROM user_companies WHERE user_id = ?", [$u['id']]);
// Yrityskohtaiset roolit
$roleRows = _dbFetchAll("SELECT company_id, role FROM user_companies WHERE user_id = ?", [$u['id']]);
$companyRoles = [];
foreach ($roleRows as $rr) { $companyRoles[$rr['company_id']] = $rr['role'] ?? 'user'; }
$u['company_roles'] = $companyRoles;
$sigRows = _dbFetchAll("SELECT mailbox_id, signature FROM user_signatures WHERE user_id = ?", [$u['id']]); $sigRows = _dbFetchAll("SELECT mailbox_id, signature FROM user_signatures WHERE user_id = ?", [$u['id']]);
$sigs = []; $sigs = [];
foreach ($sigRows as $row) { foreach ($sigRows as $row) {
@@ -746,6 +764,11 @@ function dbGetUser(string $id): ?array {
$u['companies'] = _dbFetchColumn("SELECT company_id FROM user_companies WHERE user_id = ?", [$id]); $u['companies'] = _dbFetchColumn("SELECT company_id FROM user_companies WHERE user_id = ?", [$id]);
$roleRows = _dbFetchAll("SELECT company_id, role FROM user_companies WHERE user_id = ?", [$id]);
$companyRoles = [];
foreach ($roleRows as $rr) { $companyRoles[$rr['company_id']] = $rr['role'] ?? 'user'; }
$u['company_roles'] = $companyRoles;
$sigRows = _dbFetchAll("SELECT mailbox_id, signature FROM user_signatures WHERE user_id = ?", [$id]); $sigRows = _dbFetchAll("SELECT mailbox_id, signature FROM user_signatures WHERE user_id = ?", [$id]);
$sigs = []; $sigs = [];
foreach ($sigRows as $row) { foreach ($sigRows as $row) {
@@ -762,6 +785,11 @@ function dbGetUserByUsername(string $username): ?array {
$u['companies'] = _dbFetchColumn("SELECT company_id FROM user_companies WHERE user_id = ?", [$u['id']]); $u['companies'] = _dbFetchColumn("SELECT company_id FROM user_companies WHERE user_id = ?", [$u['id']]);
$roleRows = _dbFetchAll("SELECT company_id, role FROM user_companies WHERE user_id = ?", [$u['id']]);
$companyRoles = [];
foreach ($roleRows as $rr) { $companyRoles[$rr['company_id']] = $rr['role'] ?? 'user'; }
$u['company_roles'] = $companyRoles;
$sigRows = _dbFetchAll("SELECT mailbox_id, signature FROM user_signatures WHERE user_id = ?", [$u['id']]); $sigRows = _dbFetchAll("SELECT mailbox_id, signature FROM user_signatures WHERE user_id = ?", [$u['id']]);
$sigs = []; $sigs = [];
foreach ($sigRows as $row) { foreach ($sigRows as $row) {
@@ -792,11 +820,13 @@ function dbSaveUser(array $user): void {
'luotu' => $user['luotu'] ?? date('Y-m-d H:i:s'), 'luotu' => $user['luotu'] ?? date('Y-m-d H:i:s'),
]); ]);
// Yritykset // Yritykset + yrityskohtaiset roolit
_dbExecute("DELETE FROM user_companies WHERE user_id = ?", [$user['id']]); _dbExecute("DELETE FROM user_companies WHERE user_id = ?", [$user['id']]);
if (!empty($user['companies'])) { if (!empty($user['companies'])) {
$companyRoles = $user['company_roles'] ?? [];
foreach ($user['companies'] as $cid) { foreach ($user['companies'] as $cid) {
_dbExecute("INSERT IGNORE INTO user_companies (user_id, company_id) VALUES (?, ?)", [$user['id'], $cid]); $role = $companyRoles[$cid] ?? 'user';
_dbExecute("INSERT IGNORE INTO user_companies (user_id, company_id, role) VALUES (?, ?, ?)", [$user['id'], $cid, $role]);
} }
} }

View File

@@ -1943,17 +1943,16 @@
<label for="user-form-password">Salasana <span id="user-pw-hint"></span></label> <label for="user-form-password">Salasana <span id="user-pw-hint"></span></label>
<input type="password" id="user-form-password"> <input type="password" id="user-form-password">
</div> </div>
<div class="form-group"> <div class="form-group" id="user-role-group">
<label for="user-form-role">Rooli</label> <label for="user-form-role">Pääkäyttäjä</label>
<select id="user-form-role"> <select id="user-form-role">
<option value="user">Käyttäjä</option> <option value="user">Ei</option>
<option value="admin">Yritysadmin</option> <option value="superadmin">Kyllä (Superadmin)</option>
<option value="superadmin">Pääkäyttäjä</option>
</select> </select>
</div> </div>
<div class="form-group full-width"> <div class="form-group full-width">
<label>Yritysoikeudet</label> <label>Yritykset ja roolit</label>
<div id="user-company-checkboxes" style="display:flex;flex-wrap:wrap;gap:0.75rem;margin-top:0.25rem;"></div> <div id="user-company-checkboxes" style="display:flex;flex-direction:column;gap:0.5rem;margin-top:0.25rem;"></div>
</div> </div>
</div> </div>
<div id="user-signatures-section" style="display:none;margin-top:1rem;border-top:1px solid #e5e7eb;padding-top:1rem;"> <div id="user-signatures-section" style="display:none;margin-top:1rem;border-top:1px solid #e5e7eb;padding-top:1rem;">

114
script.js
View File

@@ -3,7 +3,7 @@ let customers = [];
let sortField = 'yritys'; let sortField = 'yritys';
let sortAsc = true; let sortAsc = true;
let currentDetailId = null; let currentDetailId = null;
let currentUser = { username: '', nimi: '', role: '' }; let currentUser = { username: '', nimi: '', role: '', company_role: '' };
let currentCompany = null; // {id, nimi} let currentCompany = null; // {id, nimi}
let availableCompanies = []; // [{id, nimi}, ...] let availableCompanies = []; // [{id, nimi}, ...]
let currentTicketCompanyId = ''; // Avatun tiketin yritys (cross-company tuki) let currentTicketCompanyId = ''; // Avatun tiketin yritys (cross-company tuki)
@@ -138,7 +138,7 @@ async function checkAuth() {
try { try {
const data = await apiCall('check_auth'); const data = await apiCall('check_auth');
if (data.authenticated) { if (data.authenticated) {
currentUser = { username: data.username, nimi: data.nimi, role: data.role, id: data.user_id }; currentUser = { username: data.username, nimi: data.nimi, role: data.role, company_role: data.company_role || '', id: data.user_id };
availableCompanies = data.companies || []; availableCompanies = data.companies || [];
currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null; currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null;
currentUserSignatures = data.signatures || {}; currentUserSignatures = data.signatures || {};
@@ -160,7 +160,7 @@ loginForm.addEventListener('submit', async (e) => {
try { try {
const data = await apiCall('login', 'POST', { username, password, captcha: parseInt(captcha) }); const data = await apiCall('login', 'POST', { username, password, captcha: parseInt(captcha) });
loginError.style.display = 'none'; loginError.style.display = 'none';
currentUser = { username: data.username, nimi: data.nimi, role: data.role, id: data.user_id }; currentUser = { username: data.username, nimi: data.nimi, role: data.role, company_role: data.company_role || '', id: data.user_id };
availableCompanies = data.companies || []; availableCompanies = data.companies || [];
currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null; currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null;
currentUserSignatures = data.signatures || {}; currentUserSignatures = data.signatures || {};
@@ -185,16 +185,23 @@ document.getElementById('btn-logout').addEventListener('click', async () => {
loadBranding(); // Domain-pohjainen brändäys uudelleen loadBranding(); // Domain-pohjainen brändäys uudelleen
}); });
function isCurrentUserAdmin() {
if (currentUser.role === 'superadmin') return true;
return currentUser.company_role === 'admin';
}
function updateAdminVisibility() {
const isAdmin = isCurrentUserAdmin();
document.getElementById('btn-users').style.display = isAdmin ? '' : 'none';
document.getElementById('tab-settings').style.display = isAdmin ? '' : 'none';
document.getElementById('btn-companies').style.display = isAdmin ? '' : 'none';
}
async function showDashboard() { async function showDashboard() {
loginScreen.style.display = 'none'; loginScreen.style.display = 'none';
dashboard.style.display = 'block'; dashboard.style.display = 'block';
document.getElementById('user-info').textContent = currentUser.nimi || currentUser.username; document.getElementById('user-info').textContent = currentUser.nimi || currentUser.username;
const isSuperAdmin = currentUser.role === 'superadmin'; updateAdminVisibility();
const isAdmin = currentUser.role === 'admin' || isSuperAdmin;
// Näytä admin-toiminnot roolin mukaan
document.getElementById('btn-users').style.display = isAdmin ? '' : 'none';
document.getElementById('tab-settings').style.display = isAdmin ? '' : 'none';
document.getElementById('btn-companies').style.display = isAdmin ? '' : 'none';
// Yritysvalitsin // Yritysvalitsin
populateCompanySelector(); populateCompanySelector();
// Avaa oikea tabi URL-hashin perusteella (tai customers oletuks) // Avaa oikea tabi URL-hashin perusteella (tai customers oletuks)
@@ -212,21 +219,30 @@ function populateCompanySelector() {
return; return;
} }
sel.style.display = ''; sel.style.display = '';
sel.innerHTML = availableCompanies.map(c => sel.innerHTML = availableCompanies.map(c => {
`<option value="${c.id}" ${currentCompany && c.id === currentCompany.id ? 'selected' : ''}>${esc(c.nimi)}</option>` const blocked = c.ip_blocked ? ' (IP-rajoitus)' : '';
).join(''); const disabled = c.ip_blocked ? ' disabled' : '';
return `<option value="${c.id}" ${currentCompany && c.id === currentCompany.id ? 'selected' : ''}${disabled}>${esc(c.nimi)}${blocked}</option>`;
}).join('');
} }
async function switchCompany(companyId) { async function switchCompany(companyId) {
try { try {
await apiCall('company_switch', 'POST', { company_id: companyId }); const result = await apiCall('company_switch', 'POST', { company_id: companyId });
currentCompany = availableCompanies.find(c => c.id === companyId) || null; currentCompany = availableCompanies.find(c => c.id === companyId) || null;
// Päivitä yrityskohtainen rooli
if (result.company_role) {
currentUser.company_role = result.company_role;
}
// Päivitä brändäys vaihdetun yrityksen mukaan // Päivitä brändäys vaihdetun yrityksen mukaan
try { try {
const auth = await apiCall('check_auth'); const auth = await apiCall('check_auth');
if (auth.branding) applyBranding(auth.branding); if (auth.branding) applyBranding(auth.branding);
applyModules(auth.enabled_modules || []); applyModules(auth.enabled_modules || []);
currentUser.company_role = auth.company_role || '';
} catch (e2) {} } catch (e2) {}
// Päivitä admin-näkyvyys yritysroolin mukaan
updateAdminVisibility();
// Lataa uudelleen aktiivinen tab // Lataa uudelleen aktiivinen tab
const hash = window.location.hash.replace('#', '') || 'customers'; const hash = window.location.hash.replace('#', '') || 'customers';
const [mainTab, subTab] = hash.split('/'); const [mainTab, subTab] = hash.split('/');
@@ -966,7 +982,7 @@ async function loadArchive() {
<td>${esc(c.arkistoija || '')}</td> <td>${esc(c.arkistoija || '')}</td>
<td class="actions-cell"> <td class="actions-cell">
<button onclick="restoreCustomer('${c.id}')" class="btn-small btn-restore" title="Palauta">&#8634; Palauta</button> <button onclick="restoreCustomer('${c.id}')" class="btn-small btn-restore" title="Palauta">&#8634; Palauta</button>
${currentUser.role === 'admin' ? `<button onclick="permanentDelete('${c.id}','${esc(c.yritys)}')" class="btn-small btn-perm-delete" title="Poista pysyvästi">&#10005; Poista</button>` : ''} ${isCurrentUserAdmin() ? `<button onclick="permanentDelete('${c.id}','${esc(c.yritys)}')" class="btn-small btn-perm-delete" title="Poista pysyvästi">&#10005; Poista</button>` : ''}
</td> </td>
</tr>`).join(''); </tr>`).join('');
} }
@@ -1045,7 +1061,10 @@ async function loadUsers() {
<td><strong>${esc(u.username)}</strong></td> <td><strong>${esc(u.username)}</strong></td>
<td>${esc(u.nimi)}</td> <td>${esc(u.nimi)}</td>
<td>${esc(u.email || '')}</td> <td>${esc(u.email || '')}</td>
<td><span class="role-badge role-${u.role}">${u.role === 'superadmin' ? 'Pääkäyttäjä' : (u.role === 'admin' ? 'Yritysadmin' : 'Käyttäjä')}</span></td> <td>${u.role === 'superadmin' ? '<span class="role-badge role-superadmin">Pääkäyttäjä</span>' :
Object.entries(u.company_roles || {}).filter(([,r]) => r === 'admin').length > 0
? Object.entries(u.company_roles).map(([cid, r]) => r === 'admin' ? `<span class="role-badge role-admin" title="${cid}">Admin</span>` : '').filter(Boolean).join(' ')
: '<span class="role-badge role-user">Käyttäjä</span>'}</td>
<td>${esc(u.luotu)}</td> <td>${esc(u.luotu)}</td>
<td class="actions-cell"> <td class="actions-cell">
<button onclick="editUser('${u.id}')" title="Muokkaa">&#9998;</button> <button onclick="editUser('${u.id}')" title="Muokkaa">&#9998;</button>
@@ -1069,32 +1088,50 @@ function openUserForm(user = null) {
document.getElementById('user-form-email').value = user ? (user.email || '') : ''; document.getElementById('user-form-email').value = user ? (user.email || '') : '';
document.getElementById('user-form-password').value = ''; document.getElementById('user-form-password').value = '';
document.getElementById('user-pw-hint').textContent = user ? '(jätä tyhjäksi jos ei muuteta)' : '*'; document.getElementById('user-pw-hint').textContent = user ? '(jätä tyhjäksi jos ei muuteta)' : '*';
document.getElementById('user-form-role').value = user ? user.role : 'user'; // Globaali rooli: user vs superadmin
// Piilota superadmin-vaihtoehto ellei ole superadmin document.getElementById('user-form-role').value = (user && user.role === 'superadmin') ? 'superadmin' : 'user';
const saOption = document.querySelector('#user-form-role option[value="superadmin"]'); // Piilota superadmin-kenttä ellei ole superadmin
if (saOption) saOption.style.display = currentUser?.role === 'superadmin' ? '' : 'none'; const roleGroup = document.getElementById('user-role-group');
if (roleGroup) roleGroup.style.display = currentUser?.role === 'superadmin' ? '' : 'none';
// Piilota yrityscheckboxit adminilta (näkee vain oman yrityksen) // Piilota yrityscheckboxit adminilta (näkee vain oman yrityksen)
const compSection = document.getElementById('user-company-checkboxes')?.closest('.form-group'); const compSection = document.getElementById('user-company-checkboxes')?.closest('.form-group');
if (compSection) compSection.style.display = currentUser?.role === 'superadmin' ? '' : 'none'; if (compSection) compSection.style.display = currentUser?.role === 'superadmin' ? '' : 'none';
// Yrityscheckboxit // Yrityscheckboxit + yrityskohtaiset roolit
const allComps = availableCompanies.length > 0 ? availableCompanies : []; const allComps = availableCompanies.length > 0 ? availableCompanies : [];
const userComps = user ? (user.companies || []) : []; const userComps = user ? (user.companies || []) : [];
const companyRoles = user ? (user.company_roles || {}) : {};
const container = document.getElementById('user-company-checkboxes'); const container = document.getElementById('user-company-checkboxes');
function renderCompanyCheckboxes(companies) {
container.innerHTML = companies.map(c => {
const checked = userComps.includes(c.id);
const role = companyRoles[c.id] || 'user';
return `<div style="display:flex;align-items:center;gap:0.5rem;">
<label style="display:flex;align-items:center;gap:0.3rem;cursor:pointer;min-width:150px;">
<input type="checkbox" class="user-company-cb" value="${c.id}" ${checked ? 'checked' : ''}>
${esc(c.nimi)}
</label>
<select class="user-company-role" data-company-id="${c.id}" style="padding:4px 8px;border:1px solid #ddd;border-radius:4px;font-size:0.85rem;${checked ? '' : 'opacity:0.4;pointer-events:none;'}">
<option value="user" ${role === 'user' ? 'selected' : ''}>Käyttäjä</option>
<option value="admin" ${role === 'admin' ? 'selected' : ''}>Admin</option>
</select>
</div>`;
}).join('');
// Checkbox toggle: näytä/piilota rooli-dropdown
container.querySelectorAll('.user-company-cb').forEach(cb => {
cb.addEventListener('change', () => {
const sel = container.querySelector(`.user-company-role[data-company-id="${cb.value}"]`);
if (sel) {
sel.style.opacity = cb.checked ? '1' : '0.4';
sel.style.pointerEvents = cb.checked ? '' : 'none';
}
});
});
}
// Hae kaikki yritykset admin-näkymää varten // Hae kaikki yritykset admin-näkymää varten
apiCall('companies_all').then(companies => { apiCall('companies_all').then(companies => {
container.innerHTML = companies.map(c => renderCompanyCheckboxes(companies);
`<label style="display:flex;align-items:center;gap:0.3rem;cursor:pointer;">
<input type="checkbox" class="user-company-cb" value="${c.id}" ${userComps.includes(c.id) ? 'checked' : ''}>
${esc(c.nimi)}
</label>`
).join('');
}).catch(() => { }).catch(() => {
container.innerHTML = allComps.map(c => renderCompanyCheckboxes(allComps);
`<label style="display:flex;align-items:center;gap:0.3rem;cursor:pointer;">
<input type="checkbox" class="user-company-cb" value="${c.id}" ${userComps.includes(c.id) ? 'checked' : ''}>
${esc(c.nimi)}
</label>`
).join('');
}); });
// Allekirjoitukset per postilaatikko // Allekirjoitukset per postilaatikko
const sigSection = document.getElementById('user-signatures-section'); const sigSection = document.getElementById('user-signatures-section');
@@ -1140,6 +1177,14 @@ document.getElementById('user-form').addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const id = document.getElementById('user-form-id').value; const id = document.getElementById('user-form-id').value;
const companies = [...document.querySelectorAll('.user-company-cb:checked')].map(cb => cb.value); const companies = [...document.querySelectorAll('.user-company-cb:checked')].map(cb => cb.value);
// Kerää yrityskohtaiset roolit
const company_roles = {};
document.querySelectorAll('.user-company-role').forEach(sel => {
const cid = sel.dataset.companyId;
if (companies.includes(cid)) {
company_roles[cid] = sel.value;
}
});
// Kerää allekirjoitukset // Kerää allekirjoitukset
const signatures = {}; const signatures = {};
document.querySelectorAll('.sig-textarea').forEach(ta => { document.querySelectorAll('.sig-textarea').forEach(ta => {
@@ -1153,6 +1198,7 @@ document.getElementById('user-form').addEventListener('submit', async (e) => {
email: document.getElementById('user-form-email').value, email: document.getElementById('user-form-email').value,
role: document.getElementById('user-form-role').value, role: document.getElementById('user-form-role').value,
companies, companies,
company_roles,
signatures, signatures,
}; };
const pw = document.getElementById('user-form-password').value; const pw = document.getElementById('user-form-password').value;
@@ -1166,7 +1212,7 @@ document.getElementById('user-form').addEventListener('submit', async (e) => {
// Päivitä omat allekirjoitukset (check_auth palauttaa tuoreet) // Päivitä omat allekirjoitukset (check_auth palauttaa tuoreet)
const auth = await apiCall('check_auth'); const auth = await apiCall('check_auth');
if (auth.authenticated) { if (auth.authenticated) {
currentUser = { username: auth.username, nimi: auth.nimi, role: auth.role, id: auth.user_id }; currentUser = { username: auth.username, nimi: auth.nimi, role: auth.role, company_role: auth.company_role || '', id: auth.user_id };
currentUserSignatures = auth.signatures || {}; currentUserSignatures = auth.signatures || {};
} }
} catch (e) { alert(e.message); } } catch (e) { alert(e.message); }
@@ -1233,7 +1279,7 @@ document.getElementById('profile-form').addEventListener('submit', async (e) =>
// Päivitä UI // Päivitä UI
const auth = await apiCall('check_auth'); const auth = await apiCall('check_auth');
if (auth.authenticated) { if (auth.authenticated) {
currentUser = { username: auth.username, nimi: auth.nimi, role: auth.role, id: auth.user_id }; currentUser = { username: auth.username, nimi: auth.nimi, role: auth.role, company_role: auth.company_role || '', id: auth.user_id };
currentUserSignatures = auth.signatures || {}; currentUserSignatures = auth.signatures || {};
document.getElementById('user-info').textContent = auth.nimi || auth.username; document.getElementById('user-info').textContent = auth.nimi || auth.username;
} }