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

114
script.js
View File

@@ -3,7 +3,7 @@ let customers = [];
let sortField = 'yritys';
let sortAsc = true;
let currentDetailId = null;
let currentUser = { username: '', nimi: '', role: '' };
let currentUser = { username: '', nimi: '', role: '', company_role: '' };
let currentCompany = null; // {id, nimi}
let availableCompanies = []; // [{id, nimi}, ...]
let currentTicketCompanyId = ''; // Avatun tiketin yritys (cross-company tuki)
@@ -138,7 +138,7 @@ async function checkAuth() {
try {
const data = await apiCall('check_auth');
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 || [];
currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null;
currentUserSignatures = data.signatures || {};
@@ -160,7 +160,7 @@ loginForm.addEventListener('submit', async (e) => {
try {
const data = await apiCall('login', 'POST', { username, password, captcha: parseInt(captcha) });
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 || [];
currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null;
currentUserSignatures = data.signatures || {};
@@ -185,16 +185,23 @@ document.getElementById('btn-logout').addEventListener('click', async () => {
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() {
loginScreen.style.display = 'none';
dashboard.style.display = 'block';
document.getElementById('user-info').textContent = currentUser.nimi || currentUser.username;
const isSuperAdmin = currentUser.role === 'superadmin';
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';
updateAdminVisibility();
// Yritysvalitsin
populateCompanySelector();
// Avaa oikea tabi URL-hashin perusteella (tai customers oletuks)
@@ -212,21 +219,30 @@ function populateCompanySelector() {
return;
}
sel.style.display = '';
sel.innerHTML = availableCompanies.map(c =>
`<option value="${c.id}" ${currentCompany && c.id === currentCompany.id ? 'selected' : ''}>${esc(c.nimi)}</option>`
).join('');
sel.innerHTML = availableCompanies.map(c => {
const blocked = c.ip_blocked ? ' (IP-rajoitus)' : '';
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) {
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;
// Päivitä yrityskohtainen rooli
if (result.company_role) {
currentUser.company_role = result.company_role;
}
// Päivitä brändäys vaihdetun yrityksen mukaan
try {
const auth = await apiCall('check_auth');
if (auth.branding) applyBranding(auth.branding);
applyModules(auth.enabled_modules || []);
currentUser.company_role = auth.company_role || '';
} catch (e2) {}
// Päivitä admin-näkyvyys yritysroolin mukaan
updateAdminVisibility();
// Lataa uudelleen aktiivinen tab
const hash = window.location.hash.replace('#', '') || 'customers';
const [mainTab, subTab] = hash.split('/');
@@ -966,7 +982,7 @@ async function loadArchive() {
<td>${esc(c.arkistoija || '')}</td>
<td class="actions-cell">
<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>
</tr>`).join('');
}
@@ -1045,7 +1061,10 @@ async function loadUsers() {
<td><strong>${esc(u.username)}</strong></td>
<td>${esc(u.nimi)}</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 class="actions-cell">
<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-password').value = '';
document.getElementById('user-pw-hint').textContent = user ? '(jätä tyhjäksi jos ei muuteta)' : '*';
document.getElementById('user-form-role').value = user ? user.role : 'user';
// Piilota superadmin-vaihtoehto ellei ole superadmin
const saOption = document.querySelector('#user-form-role option[value="superadmin"]');
if (saOption) saOption.style.display = currentUser?.role === 'superadmin' ? '' : 'none';
// Globaali rooli: user vs superadmin
document.getElementById('user-form-role').value = (user && user.role === 'superadmin') ? 'superadmin' : 'user';
// Piilota superadmin-kenttä ellei ole superadmin
const roleGroup = document.getElementById('user-role-group');
if (roleGroup) roleGroup.style.display = currentUser?.role === 'superadmin' ? '' : 'none';
// Piilota yrityscheckboxit adminilta (näkee vain oman yrityksen)
const compSection = document.getElementById('user-company-checkboxes')?.closest('.form-group');
if (compSection) compSection.style.display = currentUser?.role === 'superadmin' ? '' : 'none';
// Yrityscheckboxit
// Yrityscheckboxit + yrityskohtaiset roolit
const allComps = availableCompanies.length > 0 ? availableCompanies : [];
const userComps = user ? (user.companies || []) : [];
const companyRoles = user ? (user.company_roles || {}) : {};
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
apiCall('companies_all').then(companies => {
container.innerHTML = companies.map(c =>
`<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('');
renderCompanyCheckboxes(companies);
}).catch(() => {
container.innerHTML = allComps.map(c =>
`<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('');
renderCompanyCheckboxes(allComps);
});
// Allekirjoitukset per postilaatikko
const sigSection = document.getElementById('user-signatures-section');
@@ -1140,6 +1177,14 @@ document.getElementById('user-form').addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('user-form-id').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
const signatures = {};
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,
role: document.getElementById('user-form-role').value,
companies,
company_roles,
signatures,
};
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)
const auth = await apiCall('check_auth');
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 || {};
}
} catch (e) { alert(e.message); }
@@ -1233,7 +1279,7 @@ document.getElementById('profile-form').addEventListener('submit', async (e) =>
// Päivitä UI
const auth = await apiCall('check_auth');
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 || {};
document.getElementById('user-info').textContent = auth.nimi || auth.username;
}