151 lines
9.2 KiB
Plaintext
151 lines
9.2 KiB
Plaintext
---
|
|
// Admin: one user. Their role, their permissions (grants, for moderators) and account actions, each in its own section.
|
|
import Base from '../../../layouts/Base.astro';
|
|
import Icon from '../../../components/Icon.astro';
|
|
import AdminNav from '../../../components/AdminNav.astro';
|
|
import { api } from '../../../lib/api';
|
|
import { asUser, COOKIE } from '../../../lib/auth';
|
|
import { ud } from '../../../lib/urdu';
|
|
import { ROLE, ROLE_HINT, SCOPE, CONTENT, ACTION, ago } from '../../../lib/roles';
|
|
|
|
const me = Astro.locals.user;
|
|
if (!me) return Astro.redirect(`/signin?next=${Astro.url.pathname}`);
|
|
if (me.role !== 'admin') return new Response('صرف ایڈمن کے لیے', { status: 403 });
|
|
const token = Astro.cookies.get(COOKIE)!.value, id = Number(Astro.params.id) || 0;
|
|
const poets: any[] = (await api('/api/poets')) ?? [];
|
|
|
|
let error = '', done = '', tempPassword = '';
|
|
if (Astro.request.method === 'POST') {
|
|
const f = await Astro.request.formData(), act = String(f.get('act'));
|
|
let target = String(f.get('target') ?? '').trim();
|
|
// a poet may be picked by name; the API takes a page link
|
|
const byName = poets.find((p) => p.name === target || p.nickname === target);
|
|
if (byName) target = byName.url;
|
|
const r = act === 'reset' ? await asUser(token, `/api/admin/users/${id}/password`, {})
|
|
: act === 'disable' || act === 'enable' ? await asUser(token, `/api/admin/users/${id}/disable`, { disabled: act === 'disable' })
|
|
: act === 'role' ? await asUser(token, `/api/admin/users/${id}/role`, { role: f.get('role') })
|
|
: act === 'delete' ? await asUser(token, `/api/admin/users/${id}/delete`, {})
|
|
: act === 'revoke' ? await asUser(token, `/api/admin/grants/${Number(f.get('grant'))}/delete`, {})
|
|
: act === 'grant' ? await asUser(token, `/api/admin/users/${id}/grants`, { scope: f.get('scope'), target, content: f.getAll('content'), actions: f.getAll('actions') })
|
|
: { ok: false, data: { error: 'نامعلوم عمل' } };
|
|
if (!r.ok) error = r.data.error ?? 'کچھ غلط ہو گیا';
|
|
else if (act === 'delete') return Astro.redirect('/admin');
|
|
else if (act === 'reset') tempPassword = r.data.password;
|
|
else done = { disable: 'اکاؤنٹ معطل کر دیا گیا', enable: 'اکاؤنٹ بحال کر دیا گیا', role: 'کردار بدل دیا گیا', revoke: 'اجازت واپس لے لی گئی', grant: 'اجازت دے دی گئی' }[act] ?? '';
|
|
}
|
|
const res = await asUser(token, `/api/admin/users/${id}/grants`);
|
|
if (res.status === 404) return new Response('صارف نہیں ملا', { status: 404 });
|
|
const { user, grants } = res.data;
|
|
const isMod = user.role.startsWith('mod-'), self = user.id === me.id;
|
|
const initial = [...(user.full_name || user.email)][0]?.toUpperCase();
|
|
---
|
|
<Base title={`ایڈمن: ${user.full_name || user.email}`}>
|
|
<AdminNav current="users" />
|
|
<p class="back"><a href="/admin"><Icon name="prev" size={16} /> صارفین</a></p>
|
|
|
|
<header class="user-head">
|
|
<span class="avatar big" data-role={user.role} aria-hidden="true">{initial}</span>
|
|
<div>
|
|
<h1>{user.full_name || <bdi dir="ltr">{user.email}</bdi>}</h1>
|
|
{user.full_name && <bdi class="email" dir="ltr">{user.email}</bdi>}
|
|
<p class="tags">
|
|
<span class="badge" data-role={user.role}>{ROLE[user.role] ?? user.role}</span>
|
|
{user.disabled_at && <span class="badge danger">معطل</span>}
|
|
{self && <span class="badge plain">آپ</span>}
|
|
</p>
|
|
<p class="muted small-line">شمولیت {ago(user.created_at)} · آخری لاگ ان {ago(user.last_sign_in)}</p>
|
|
</div>
|
|
</header>
|
|
|
|
{error && <p class="form-error" role="alert">{error}</p>}
|
|
{done && <p class="form-done" role="status">{done}</p>}
|
|
{tempPassword && (
|
|
<div class="temp-password" role="status">
|
|
<p>عارضی پاس ورڈ (صرف ایک بار دکھایا جا رہا ہے):</p>
|
|
<p><code dir="ltr">{tempPassword}</code></p>
|
|
<p class="muted">یہ پاس ورڈ صارف کو دیں؛ وہ لاگ ان کر کے اپنے اکاؤنٹ کے صفحے سے اسے بدل لیں۔</p>
|
|
</div>
|
|
)}
|
|
|
|
<section class="panel">
|
|
<h2>کردار</h2>
|
|
{self ? <p class="muted">اپنا کردار خود نہیں بدل سکتے۔</p> : (
|
|
<form method="post" class="role-pick" onsubmit={`return confirm(${JSON.stringify(isMod ? 'کردار بدلیں؟ موڈریٹر نہ رہنے پر اس کی اجازتیں ختم ہو جائیں گی۔' : 'کردار بدلیں؟')})`}>
|
|
<input type="hidden" name="act" value="role" />
|
|
{Object.entries(ROLE).map(([k, v]) => (
|
|
<button class="btn" name="role" value={k} aria-pressed={k === user.role ? 'true' : 'false'} disabled={k === user.role}>
|
|
<span class="badge" data-role={k}>{v}</span><small>{ROLE_HINT[k]}</small>
|
|
</button>
|
|
))}
|
|
</form>
|
|
)}
|
|
</section>
|
|
|
|
{isMod && (
|
|
<section class="panel">
|
|
<h2>اجازتیں <small class="muted">{ud(grants.length)}</small></h2>
|
|
{grants.length === 0 && <p class="muted">ابھی کوئی اجازت نہیں: یہ موڈریٹر کچھ نہیں بدل سکتا۔ نیچے سے اجازت دیں۔</p>}
|
|
<ul class="grants">
|
|
{grants.map((g: any) => (
|
|
<li class="grant">
|
|
<div>
|
|
<p class="grant-scope"><strong>{SCOPE[g.scope]}</strong>{g.label && <>: {g.url ? <a href={g.url}>{g.label}</a> : g.label}</>}{g.poet && g.scope !== 'poet' && <small class="muted"> ({g.poet})</small>}</p>
|
|
<p class="tag-chips">{g.content.map((c: string) => <span class="badge plain">{CONTENT[c]}</span>)}<span class="sep" aria-hidden="true"></span>{g.actions.map((a: string) => <span class="badge" data-role="mod-l2">{ACTION[a]}</span>)}</p>
|
|
</div>
|
|
<form method="post" onsubmit="return confirm('یہ اجازت واپس لیں؟')">
|
|
<input type="hidden" name="grant" value={g.id} />
|
|
<button class="icon-btn small danger" name="act" value="revoke" title="اجازت واپس لیں" aria-label="اجازت واپس لیں"><Icon name="trash" size={16} /></button>
|
|
</form>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<details class="new-grant" open={grants.length === 0}>
|
|
<summary class="btn primary"><Icon name="plus" size={16} /> نئی اجازت</summary>
|
|
<form method="post" class="grant-form">
|
|
<input type="hidden" name="act" value="grant" />
|
|
<fieldset><legend>کہاں</legend>
|
|
<div class="chips">{Object.entries(SCOPE).map(([k, v], i) => <label class="chip"><input type="radio" name="scope" value={k} checked={i === 1} required /> {v}</label>)}</div>
|
|
</fieldset>
|
|
<label class="target">کون سا
|
|
<input name="target" list="poet-names" placeholder="شاعر کا نام، یا کتاب / کلام کا لنک" autocomplete="off" />
|
|
<small class="muted">شاعر نام سے چنیں؛ کتاب یا کلام کے لیے اس صفحے کا لنک دیں (جیسے <bdi dir="ltr">/p266/ghazal</bdi>)</small>
|
|
</label>
|
|
<datalist id="poet-names">{poets.map((p) => <option value={p.nickname || p.name} />)}</datalist>
|
|
<fieldset><legend>کیا</legend>
|
|
<div class="chips">{Object.entries(CONTENT).map(([k, v]) => <label class="chip"><input type="checkbox" name="content" value={k} /> {v}</label>)}</div>
|
|
<small class="muted">لغت صرف "تمام شعرا" کے ساتھ</small>
|
|
</fieldset>
|
|
<fieldset><legend>کون سے کام</legend>
|
|
<div class="chips">{Object.entries(ACTION).map(([k, v]) => <label class="chip"><input type="checkbox" name="actions" value={k} checked={k === 'edit'} /> {v}</label>)}</div>
|
|
</fieldset>
|
|
<p><button class="btn primary"><Icon name="check" size={16} /> اجازت دیں</button></p>
|
|
</form>
|
|
</details>
|
|
</section>
|
|
)}
|
|
{!isMod && !self && <p class="muted center small-line">اجازتیں موڈریٹرز کے لیے ہیں: اوپر سے کردار "موڈریٹر" کریں۔</p>}
|
|
|
|
<section class="panel">
|
|
<h2>اکاؤنٹ</h2>
|
|
<div class="account-actions">
|
|
<form method="post" onsubmit="return confirm('اس صارف کا پاس ورڈ ری سیٹ کریں؟ اس کے سب سیشن ختم ہو جائیں گے۔')">
|
|
<button class="btn" name="act" value="reset"><Icon name="key" size={16} /> پاس ورڈ ری سیٹ</button>
|
|
</form>
|
|
{!self && (
|
|
<form method="post">
|
|
{user.disabled_at
|
|
? <button class="btn ok" name="act" value="enable"><Icon name="undo" size={16} /> بحال کریں</button>
|
|
: <button class="btn" name="act" value="disable"><Icon name="ban" size={16} /> معطل کریں</button>}
|
|
</form>
|
|
)}
|
|
</div>
|
|
{!self && (
|
|
<form method="post" class="danger-zone" onsubmit="return confirm('یہ اکاؤنٹ اور اس کا تمام ڈیٹا مستقل طور پر حذف ہو جائے گا۔ جاری رکھیں؟')">
|
|
<p class="muted small-line">اکاؤنٹ اور اس کا تمام ڈیٹا (محفوظ شدہ، ذخیرۂ الفاظ) مستقل طور پر حذف۔ واپسی ممکن نہیں۔</p>
|
|
<button class="btn danger" name="act" value="delete"><Icon name="trash" size={16} /> اکاؤنٹ حذف کریں</button>
|
|
</form>
|
|
)}
|
|
</section>
|
|
</Base>
|