stepstead-web/assets/users.html
jts dea01dd6a8 İlk commit: Stepstead web (statik panel + db)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:30:59 +03:00

233 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex,nofollow">
<title>Stepstead — Kullanıcılar</title>
<style>
:root{--bg:#0f1419;--panel:#1a2230;--line:#2c3a4f;--txt:#e6edf3;--muted:#8b99a8;--gold:#e8b923;--ok:#2ea043;--err:#e5534b}
*{box-sizing:border-box}
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--txt);line-height:1.5}
.wrap{max-width:960px;margin:0 auto;padding:20px 16px}
h1{font-size:20px;color:var(--gold);margin:0 0 4px}
h2{font-size:15px;color:var(--gold);margin:0 0 10px}
.sub{color:var(--muted);font-size:13px;margin-bottom:16px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:16px}
input,button,select{font-size:13px;border-radius:6px;font-family:inherit}
input,select{background:#222d3d;color:var(--txt);border:1px solid var(--line);padding:7px 9px}
input[type=email],input[type=password]{width:100%;margin-bottom:10px}
.btn{background:var(--gold);color:#1a1300;border:none;padding:8px 16px;font-weight:600;cursor:pointer}
.btn.sec{background:#2a3648;color:var(--txt)}
.btn.sm{padding:5px 10px;font-size:12px}
.btn.danger{background:none;border:1px solid var(--line);color:var(--muted)}
.btn.danger:hover{color:var(--err);border-color:var(--err)}
.msg{font-size:13px;margin-top:8px;min-height:16px}
.msg.ok{color:var(--ok)} .msg.err{color:var(--err)}
.hide{display:none}
table{width:100%;border-collapse:collapse;font-size:13px}
th,td{text-align:left;padding:8px 6px;border-top:1px solid var(--line);vertical-align:middle}
th{color:var(--muted);font-weight:500;font-size:12px;text-transform:uppercase;letter-spacing:.04em;border-top:none}
tr.inactive td{opacity:.45}
.pill{font-size:11px;padding:1px 8px;border-radius:10px;background:#243247;color:var(--gold);font-weight:600}
.permbox{display:flex;gap:10px;flex-wrap:wrap;margin:6px 0}
.permbox label{display:flex;gap:5px;align-items:center;font-size:12px;color:var(--muted);background:#141c28;border:1px solid var(--line);border-radius:6px;padding:5px 9px;cursor:pointer}
.permbox label:has(input:checked){color:var(--gold);border-color:var(--gold)}
.rolerow{border-top:1px solid var(--line);padding:12px 0}
.rolerow:first-of-type{border-top:none}
.rolehead{display:flex;gap:10px;align-items:center}
.rolehead b{font-size:14px}
.rolehead .cnt{color:var(--muted);font-size:12px}
.rolehead .acts{margin-left:auto;display:flex;gap:6px}
.newrow{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:8px}
.newrow input{flex:1 1 140px}
#toast{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);background:#1a2230;border:1px solid var(--gold);color:var(--txt);padding:9px 16px;border-radius:8px;font-size:13px;opacity:0;pointer-events:none;transition:.3s;z-index:9}
#toast.on{opacity:1}
</style>
<script src="/nav.js?v=8" data-theme="dark" defer></script>
</head>
<body>
<div class="wrap">
<h1>👥 Kullanıcılar & Roller</h1>
<div class="sub">Kullanıcı oluştur, rol ata; rollerin hangi sayfalara/aksiyonlara erişeceğini seç.</div>
<div class="card" id="loginCard">
<input type="email" id="email" placeholder="E-posta" autocomplete="username">
<input type="password" id="pass" placeholder="Şifre" autocomplete="current-password">
<button class="btn" id="loginBtn">Giriş</button>
<div class="msg" id="loginMsg"></div>
</div>
<div id="app" class="hide">
<div class="card">
<h2>Kullanıcılar</h2>
<table>
<thead><tr><th>E-posta</th><th>İsim</th><th>Rol</th><th>Durum</th><th></th></tr></thead>
<tbody id="userRows"></tbody>
</table>
<div class="newrow">
<input type="email" id="nuEmail" placeholder="E-posta">
<input type="text" id="nuName" placeholder="İsim (ops.)">
<input type="password" id="nuPass" placeholder="Şifre (min 8)">
<select id="nuRole"></select>
<button class="btn" id="nuBtn">+ Kullanıcı Oluştur</button>
</div>
<div class="msg" id="uMsg"></div>
</div>
<div class="card">
<h2>Roller</h2>
<div id="roleList"></div>
<div class="newrow">
<input type="text" id="nrName" placeholder="Yeni rol adı">
<button class="btn" id="nrBtn">+ Rol Oluştur</button>
</div>
<div class="msg" id="rMsg"></div>
</div>
</div>
</div>
<div id="toast"></div>
<script>
const API="https://api.stepstead.com";
let TOKEN=localStorage.getItem("ss_upload_token")||"";
let ROLES=[],USERS=[],PERMS={};
const $=id=>document.getElementById(id);
function esc(s){return (s??"").toString().replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));}
function toast(m,ms=3000){const t=$("toast");t.textContent=m;t.classList.add("on");clearTimeout(t._t);t._t=setTimeout(()=>t.classList.remove("on"),ms);}
async function api(path,opts={}){
opts.headers=Object.assign({"Content-Type":"application/json"},opts.headers,{Authorization:"Bearer "+TOKEN});
const r=await fetch(API+path,opts);
let body={};try{body=await r.json();}catch(e){}
return {ok:r.ok,status:r.status,body};
}
async function login(){
$("loginMsg").textContent="…";
const r=await fetch(API+"/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({email:$("email").value.trim(),password:$("pass").value})});
const b=await r.json().catch(()=>({}));
if(r.ok&&b.access_token){TOKEN=b.access_token;localStorage.setItem("ss_upload_token",TOKEN);
if(window.ssNavRefresh)ssNavRefresh();showApp();}
else{$("loginMsg").className="msg err";$("loginMsg").textContent=b.detail||"Giriş başarısız";}
}
async function showApp(){
const p=await api("/admin/permissions");
if(!p.ok){
$("loginMsg").className="msg err";
$("loginMsg").textContent=p.status===403?"Bu hesabın kullanıcı yönetimi yetkisi yok":"Giriş gerekli / oturum bitti";
return;
}
PERMS=p.body;
$("loginCard").classList.add("hide");$("app").classList.remove("hide");
await loadAll();
}
async function loadAll(){
const [r,u]=await Promise.all([api("/admin/roles"),api("/admin/users")]);
ROLES=r.ok?r.body:[];USERS=u.ok?u.body:[];
render();
}
function roleSelect(id,sel){
const opts=['<option value="">— rol yok (oyuncu) —</option>']
.concat(ROLES.map(r=>`<option value="${r.id}" ${r.id===sel?"selected":""}>${esc(r.name)}</option>`));
return `<select data-uid="${id}" class="uRole">${opts.join("")}</select>`;
}
function render(){
// kullanıcılar
const tb=$("userRows");tb.innerHTML="";
for(const u of USERS){
const tr=document.createElement("tr");
if(!u.is_active)tr.className="inactive";
tr.innerHTML=`<td>${esc(u.email)}</td><td>${esc(u.display_name||"")}</td>
<td>${roleSelect(u.id,u.role_id)}</td>
<td><button class="btn sm sec uAct" data-uid="${u.id}" data-on="${u.is_active?1:0}">${u.is_active?"Aktif":"Pasif"}</button></td>
<td></td>`;
tb.appendChild(tr);
}
// yeni kullanıcı rol seçimi
$("nuRole").innerHTML=['<option value="">— rol yok —</option>']
.concat(ROLES.map(r=>`<option value="${r.id}">${esc(r.name)}</option>`)).join("");
// roller
const box=$("roleList");box.innerHTML="";
for(const r of ROLES){
const div=document.createElement("div");div.className="rolerow";
const permHtml=r.is_system
? `<div class="permbox"><span class="pill">tüm izinler (*)</span></div>`
: `<div class="permbox">`+Object.entries(PERMS).map(([code,lbl])=>
`<label title="${esc(lbl)}"><input type="checkbox" data-rid="${r.id}" value="${code}" ${r.permissions.includes(code)?"checked":""}>${code}</label>`
).join("")+`</div>`;
div.innerHTML=`<div class="rolehead">
<b>${esc(r.name)}</b><span class="pill">${r.is_system?"sistem":"özel"}</span>
<span class="cnt">${r.user_count} kullanıcı</span>
<span class="acts">
${r.is_system?"":`<button class="btn sm rSave" data-rid="${r.id}">Kaydet</button>
<button class="btn sm danger rDel" data-rid="${r.id}">Sil</button>`}
</span>
</div>${permHtml}`;
box.appendChild(div);
}
}
document.addEventListener("change",async e=>{
if(e.target.classList.contains("uRole")){
const uid=+e.target.dataset.uid;
const val=e.target.value?+e.target.value:null;
const r=await api("/admin/users/"+uid,{method:"POST",body:JSON.stringify({role_id:val})});
if(r.ok){toast("✅ Rol güncellendi");await loadAll();}
else{toast("⚠ "+(r.body.detail||"Hata"));render();}
}
});
document.addEventListener("click",async e=>{
if(e.target.classList.contains("uAct")){
const uid=+e.target.dataset.uid, on=e.target.dataset.on==="1";
const r=await api("/admin/users/"+uid,{method:"POST",body:JSON.stringify({is_active:!on})});
if(r.ok){toast(on?"Kullanıcı pasifleştirildi":"Kullanıcı aktifleştirildi");await loadAll();}
else toast("⚠ "+(r.body.detail||"Hata"));
}
if(e.target.classList.contains("rSave")){
const rid=+e.target.dataset.rid;
const role=ROLES.find(x=>x.id===rid);
const perms=[...document.querySelectorAll(`input[data-rid="${rid}"]:checked`)].map(c=>c.value);
const r=await api("/admin/roles/"+rid,{method:"POST",body:JSON.stringify({name:role.name,permissions:perms})});
if(r.ok){toast("✅ Rol kaydedildi");await loadAll();}
else toast("⚠ "+(r.body.detail||"Hata"));
}
if(e.target.classList.contains("rDel")){
const rid=+e.target.dataset.rid;
const role=ROLES.find(x=>x.id===rid);
if(!confirm(`"${role.name}" rolü silinsin mi?`))return;
const r=await api("/admin/roles/"+rid+"/delete",{method:"POST"});
if(r.ok){toast("Rol silindi");await loadAll();}
else toast("⚠ "+(r.body.detail||"Hata"));
}
});
$("nuBtn").onclick=async()=>{
const email=$("nuEmail").value.trim(),pass=$("nuPass").value;
if(!email||pass.length<8){$("uMsg").className="msg err";$("uMsg").textContent="E-posta + min 8 karakter şifre gerekli";return;}
$("uMsg").className="msg";$("uMsg").textContent="…";
const r=await api("/admin/users",{method:"POST",body:JSON.stringify({
email,password:pass,display_name:$("nuName").value.trim()||null,
role_id:$("nuRole").value?+$("nuRole").value:null})});
if(r.ok){$("uMsg").className="msg ok";$("uMsg").textContent="✅ Oluşturuldu: "+r.body.email;
$("nuEmail").value=$("nuName").value=$("nuPass").value="";await loadAll();}
else{$("uMsg").className="msg err";$("uMsg").textContent=r.body.detail||"Hata";}
};
$("nrBtn").onclick=async()=>{
const name=$("nrName").value.trim();
if(name.length<2){$("rMsg").className="msg err";$("rMsg").textContent="Rol adı gerekli";return;}
const r=await api("/admin/roles",{method:"POST",body:JSON.stringify({name,permissions:[]})});
if(r.ok){$("rMsg").className="msg ok";$("rMsg").textContent="✅ Rol oluşturuldu — izinlerini seçip Kaydet'e bas";
$("nrName").value="";await loadAll();}
else{$("rMsg").className="msg err";$("rMsg").textContent=r.body.detail||"Hata";}
};
$("loginBtn").onclick=login;
$("pass").addEventListener("keydown",e=>{if(e.key==="Enter")login();});
if(TOKEN)showApp();
</script>
</body>
</html>