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:
114
script.js
114
script.js
@@ -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">↺ Palauta</button>
|
||||
${currentUser.role === 'admin' ? `<button onclick="permanentDelete('${c.id}','${esc(c.yritys)}')" class="btn-small btn-perm-delete" title="Poista pysyvästi">✕ Poista</button>` : ''}
|
||||
${isCurrentUserAdmin() ? `<button onclick="permanentDelete('${c.id}','${esc(c.yritys)}')" class="btn-small btn-perm-delete" title="Poista pysyvästi">✕ 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">✎</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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user