- SEC-1: nahrazen exec('fsutil') za PHP-native is_link()+realpath() v NasFileManager - eliminace command injection
- SEC-2: přidáno ověření aktuálního hesla při změně hesla (profile.php + DashProfile.jsx)
- BUG-1: attendance punch obalen do transakce s SELECT FOR UPDATE - prevence race condition při dvojkliku
- BUG-2: eliminován N+1 SQL dotaz pro VAT v invoice listu - výpočet přesunut do subquery
- BUG-5/6: delete a update attendance záznamů obaleny do transakcí - prevence nekonzistentního stavu
- BUG-7: opravena duplikace nabídky - přidáno chybějící pole unit v offer items
ESLint: 0 errors | PHPCS: 0 errors | Build: OK
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2 lines
12 KiB
JavaScript
2 lines
12 KiB
JavaScript
import{j as e,m as v,A as re}from"./vendor-animation-0s3FMHwK.js";import{g as le,r as n}from"./vendor-react-BVs3cwbi.js";import{a as oe,u as de,b as ce,c as f,F as A,C as me}from"./index-CCZhiEoc.js";import"./vendor-utils-Dyr8OjFr.js";const y="/api/admin",pe={attendance:"Docházka",trips:"Kniha jízd",offers:"Nabídky",orders:"Objednávky",projects:"Projekty",invoices:"Faktury",users:"Uživatelé",settings:"Nastavení"};function je(){const l=oe(),{hasPermission:_}=de(),F=le(),[U,P]=n.useState(!0),[V,W]=n.useState([]),[,Z]=n.useState([]),[M,I]=n.useState({}),[m,R]=n.useState(!1),[q,E]=n.useState(!0),[T,B]=n.useState(!1),[D,N]=n.useState(!1),[a,k]=n.useState(null),[w,L]=n.useState(!1),[o,p]=n.useState({name:"",display_name:"",description:"",permissions:[]}),[g,C]=n.useState({show:!1,role:null}),[G,O]=n.useState(!1),u=_("settings.roles"),j=_("settings.security");n.useEffect(()=>{!u&&!j&&F("/")},[u,j,F]),ce(D);const b=n.useCallback(async()=>{if(!u){P(!1);return}try{const s=await f(`${y}/roles.php`);if(s.status===401)return;const i=await s.json();i.success?(W(i.data.roles),Z(i.data.permissions),I(i.data.permission_groups)):l.error(i.error||"Nepodařilo se načíst role")}catch{l.error("Chyba připojení")}finally{P(!1)}},[l,u]);n.useEffect(()=>{b()},[b]);const $=n.useCallback(async()=>{if(!j){E(!1);return}try{const i=await(await f(`${y}/totp.php?action=get_required`)).json();i.success&&R(i.data.require_2fa)}catch{}finally{E(!1)}},[j]);n.useEffect(()=>{$()},[$]);const H=async()=>{B(!0);try{const i=await(await f(`${y}/totp.php?action=set_required`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({required:!m})})).json();i.success?(R(i.data.require_2fa),l.success(i.message)):l.error(i.error||"Nepodařilo se uložit nastavení")}catch{l.error("Chyba připojení")}finally{B(!1)}},J=s=>s.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,""),K=()=>{k(null),p({name:"",display_name:"",description:"",permissions:[]}),N(!0)},Q=s=>{k(s),p({name:s.name,display_name:s.display_name,description:s.description||"",permissions:s.permissions||[]}),N(!0)},S=()=>{N(!1),k(null)},X=s=>{const i={display_name:s};a||(i.name=J(s)),p(d=>({...d,...i}))},Y=s=>{p(i=>({...i,permissions:i.permissions.includes(s)?i.permissions.filter(d=>d!==s):[...i.permissions,s]}))},ee=s=>{const i=(M[s]||[]).map(t=>t.name),d=i.every(t=>o.permissions.includes(t));p(t=>({...t,permissions:d?t.permissions.filter(c=>!i.includes(c)):[...new Set([...t.permissions,...i])]}))},se=async s=>{if(s?.preventDefault(),!o.display_name.trim()){l.error("Zobrazovaný název je povinný");return}if(!a&&!o.name.trim()){l.error("Název role je povinný");return}L(!0);try{const i=a?`${y}/roles.php?id=${a.id}`:`${y}/roles.php`,t=await(await f(i,{method:a?"PUT":"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();t.success?(S(),await new Promise(c=>setTimeout(c,300)),l.success(t.message||(a?"Role byla aktualizována":"Role byla vytvořena")),b()):l.error(t.error||"Nepodařilo se uložit roli")}catch{l.error("Chyba připojení")}finally{L(!1)}},ie=async()=>{if(g.role){O(!0);try{const i=await(await f(`${y}/roles.php?id=${g.role.id}`,{method:"DELETE"})).json();i.success?(C({show:!1,role:null}),l.success(i.message||"Role byla smazána"),b()):l.error(i.error||"Nepodařilo se smazat roli")}catch{l.error("Chyba připojení")}finally{O(!1)}}};if(U)return e.jsxs("div",{className:"admin-skeleton",style:{padding:0,gap:"1.5rem"},children:[e.jsx("div",{className:"admin-skeleton-row",style:{justifyContent:"space-between"},children:e.jsxs("div",{children:[e.jsx("div",{className:"admin-skeleton-line h-8",style:{width:"200px",marginBottom:"0.5rem"}}),e.jsx("div",{className:"admin-skeleton-line",style:{width:"140px"}})]})}),e.jsx("div",{className:"admin-card",children:e.jsx("div",{className:"admin-skeleton",style:{gap:"1.25rem"},children:[0,1,2,3,4].map(s=>e.jsxs("div",{className:"admin-skeleton-row",children:[e.jsx("div",{className:"admin-skeleton-line circle"}),e.jsxs("div",{className:"flex-1",children:[e.jsx("div",{className:"admin-skeleton-line w-1/3 mb-2"}),e.jsx("div",{className:"admin-skeleton-line w-1/4",style:{height:"10px"}})]}),e.jsx("div",{className:"admin-skeleton-line w-1/4"})]},s))})})]});const h=s=>s.name==="admin";function ae(){return q?e.jsx("div",{className:"admin-skeleton-line",style:{width:"200px",height:"12px"}}):m?"Všichni uživatelé musí mít aktivní 2FA pro přístup do systému":"2FA je volitelná - uživatelé si ji mohou aktivovat v profilu"}function ne(){return T?"Ukládání...":m?"Vypnout":"Zapnout"}function te(){return w?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"admin-spinner admin-spinner-sm"}),"Ukládání..."]}):a?"Uložit změny":"Vytvořit roli"}return e.jsxs("div",{children:[e.jsxs(v.div,{className:"admin-page-header",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4},children:[e.jsxs("div",{children:[e.jsx("h1",{className:"admin-page-title",children:"Nastavení"}),e.jsx("p",{className:"admin-page-subtitle",children:"Zabezpečení a správa rolí"})]}),u&&e.jsxs("button",{onClick:K,className:"admin-btn admin-btn-primary",children:[e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("line",{x1:"12",y1:"5",x2:"12",y2:"19"}),e.jsx("line",{x1:"5",y1:"12",x2:"19",y2:"12"})]}),"Přidat roli"]})]}),j&&e.jsxs(v.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.1},children:[e.jsx("div",{className:"admin-card-header",children:e.jsx("h2",{className:"admin-card-title",children:"Zabezpečení"})}),e.jsx("div",{className:"admin-card-body",children:e.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"1rem"},children:[e.jsxs("div",{className:"flex-row-gap",children:[e.jsx("div",{style:{width:36,height:36,borderRadius:"50%",display:"flex",alignItems:"center",justifyContent:"center",background:m?"var(--success-light)":"rgba(var(--text-secondary-rgb, 107, 114, 128), 0.1)",color:m?"var(--success)":"var(--text-secondary)",flexShrink:0},children:e.jsxs("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("rect",{x:"3",y:"11",width:"18",height:"11",rx:"2",ry:"2"}),e.jsx("path",{d:"M7 11V7a5 5 0 0 1 10 0v4"})]})}),e.jsxs("div",{children:[e.jsx("div",{style:{fontWeight:500,color:"var(--text-primary)",fontSize:"0.875rem"},children:"Povinné dvoufaktorové ověření (2FA)"}),e.jsx("div",{style:{fontSize:"0.75rem",color:"var(--text-secondary)"},children:ae()})]})]}),!q&&e.jsx("button",{onClick:H,disabled:T,className:`admin-btn admin-btn-sm ${m?"admin-btn-secondary":"admin-btn-primary"}`,style:m?{color:"var(--danger)"}:{},children:ne()})]})})]}),u&&e.jsx(v.div,{className:"admin-card",initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{duration:.4,delay:.2},children:e.jsx("div",{className:"admin-card-body",children:e.jsx("div",{className:"admin-table-responsive",children:e.jsxs("table",{className:"admin-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Název"}),e.jsx("th",{children:"Popis"}),e.jsx("th",{children:"Oprávnění"}),e.jsx("th",{children:"Uživatelé"}),e.jsx("th",{children:"Akce"})]})}),e.jsx("tbody",{children:V.map(s=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("div",{style:{fontWeight:500,color:"var(--text-primary)"},children:s.display_name}),e.jsx("div",{style:{fontSize:"0.75rem",color:"var(--text-tertiary)"},children:s.name})]}),e.jsx("td",{style:{color:"var(--text-secondary)"},children:s.description||"—"}),e.jsx("td",{children:e.jsx("span",{className:"admin-badge admin-badge-info",children:h(s)?"Vše":s.permission_count})}),e.jsx("td",{children:e.jsx("span",{className:"admin-badge admin-badge-secondary",children:s.user_count})}),e.jsx("td",{children:!h(s)&&e.jsxs("div",{className:"flex-row gap-2",children:[e.jsx("button",{onClick:()=>Q(s),className:"admin-btn-icon",title:"Upravit","aria-label":"Upravit",children:e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e.jsx("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})]})}),e.jsx("button",{onClick:()=>C({show:!0,role:s}),className:"admin-btn-icon danger",title:s.user_count>0?"Nelze smazat roli s přiřazenými uživateli":"Smazat","aria-label":s.user_count>0?"Nelze smazat roli s přiřazenými uživateli":"Smazat",disabled:s.user_count>0,children:e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})})]})})]},s.id))})]})})})}),e.jsx(re,{children:D&&e.jsxs(v.div,{className:"admin-modal-overlay",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-backdrop",onClick:S}),e.jsxs(v.div,{className:"admin-modal admin-modal-lg",initial:{opacity:0,scale:.95,y:20},animate:{opacity:1,scale:1,y:0},exit:{opacity:0,scale:.95,y:20},transition:{duration:.2},children:[e.jsx("div",{className:"admin-modal-header",children:e.jsx("h2",{className:"admin-modal-title",children:a?"Upravit roli":"Nová role"})}),e.jsx("div",{className:"admin-modal-body",children:e.jsxs("div",{className:"admin-form",children:[a&&h(a)&&e.jsxs("div",{className:"admin-role-locked-notice",children:[e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[e.jsx("circle",{cx:"12",cy:"12",r:"10"}),e.jsx("line",{x1:"12",y1:"16",x2:"12",y2:"12"}),e.jsx("line",{x1:"12",y1:"8",x2:"12.01",y2:"8"})]}),"Administrátor má vždy plný přístup ke všem funkcím"]}),e.jsx(A,{label:"Zobrazovaný název",children:e.jsx("input",{type:"text",value:o.display_name,onChange:s=>X(s.target.value),className:"admin-form-input",placeholder:"např. Manažer",disabled:a&&h(a)})}),e.jsxs(A,{label:"Systémový název (slug)",children:[e.jsx("input",{type:"text",value:o.name,onChange:s=>p(i=>({...i,name:s.target.value})),className:"admin-form-input",placeholder:"např. manager",disabled:!!a}),!a&&e.jsx("small",{style:{color:"var(--text-tertiary)",fontSize:"0.75rem"},children:"Pouze malá písmena, čísla a pomlčky. Nelze později změnit."})]}),e.jsx(A,{label:"Popis",children:e.jsx("textarea",{value:o.description,onChange:s=>p(i=>({...i,description:s.target.value})),className:"admin-form-input",rows:2,placeholder:"Volitelný popis role",disabled:a&&h(a)})}),e.jsxs("div",{className:"admin-form-group",children:[e.jsx("label",{className:"admin-form-label",style:{marginBottom:"0.75rem"},children:"Oprávnění"}),Object.entries(M).sort(([s,i],[d,t])=>{if(s==="settings")return 1;if(d==="settings")return-1;const c=Math.min(...i.map(x=>x.id)),z=Math.min(...t.map(x=>x.id));return c-z}).map(([s,i],d)=>{const t=i.map(r=>r.name),c=t.every(r=>o.permissions.includes(r)),z=t.some(r=>o.permissions.includes(r)),x=a&&h(a);return e.jsxs("div",{children:[d>0&&e.jsx("hr",{style:{border:"none",borderTop:"1px solid var(--border-color, #e0e0e0)",margin:"0.75rem 0"}}),e.jsxs("div",{className:"admin-permission-group",children:[e.jsx("div",{className:"admin-permission-group-title",children:e.jsxs("label",{className:"admin-form-checkbox",children:[e.jsx("input",{type:"checkbox",checked:c,ref:r=>{r&&(r.indeterminate=z&&!c)},onChange:()=>ee(s),disabled:x}),e.jsx("span",{children:pe[s]||s})]})}),e.jsx("div",{className:"admin-permission-list",children:i.map(r=>e.jsxs("div",{className:"admin-permission-item",children:[e.jsxs("label",{className:"admin-form-checkbox",children:[e.jsx("input",{type:"checkbox",checked:o.permissions.includes(r.name),onChange:()=>Y(r.name),disabled:x}),e.jsx("span",{children:r.display_name})]}),r.description&&e.jsx("div",{className:"admin-permission-desc",children:r.description})]},r.id))})]})]},s)})]})]})}),e.jsxs("div",{className:"admin-modal-footer",children:[e.jsx("button",{type:"button",onClick:S,className:"admin-btn admin-btn-secondary",disabled:w,children:"Zrušit"}),!(a&&h(a))&&e.jsx("button",{type:"button",onClick:se,className:"admin-btn admin-btn-primary",disabled:w,children:te()})]})]})]})}),e.jsx(me,{isOpen:g.show,onClose:()=>C({show:!1,role:null}),onConfirm:ie,title:"Smazat roli",message:`Opravdu chcete smazat roli "${g.role?.display_name}"? Tato akce je nevratná.`,confirmText:"Smazat",cancelText:"Zrušit",type:"danger",loading:G})]})}export{je as default};
|