İlk commit: Stepstead web (statik panel + db)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
dea01dd6a8
18 changed files with 3764 additions and 0 deletions
233
assets/users.html
Normal file
233
assets/users.html
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
<!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=>({"&":"&","<":"<",">":">",'"':"""}[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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue