Allow admin email edits and cap the viewer list to 12 rows without a scrollbar.
This commit is contained in:
parent
9efcfff484
commit
f6d484aa56
3 changed files with 107 additions and 3 deletions
|
|
@ -167,6 +167,7 @@ export default function ViewersPage() {
|
||||||
const [addonInstallUrls, setAddonInstallUrls] = useState<Record<string, string | null>>({});
|
const [addonInstallUrls, setAddonInstallUrls] = useState<Record<string, string | null>>({});
|
||||||
const [claimCodes, setClaimCodes] = useState<Record<string, string>>({});
|
const [claimCodes, setClaimCodes] = useState<Record<string, string>>({});
|
||||||
const [newPasswords, setNewPasswords] = useState<Record<string, string>>({});
|
const [newPasswords, setNewPasswords] = useState<Record<string, string>>({});
|
||||||
|
const [editEmails, setEditEmails] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const load = useCallback(() => {
|
const load = useCallback(() => {
|
||||||
fetch("/api/v1/admin/viewers", { credentials: "include" })
|
fetch("/api/v1/admin/viewers", { credentials: "include" })
|
||||||
|
|
@ -633,6 +634,39 @@ export default function ViewersPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function changeViewerEmail(v: Viewer) {
|
||||||
|
const next = (editEmails[v.id] ?? v.email).trim().toLowerCase();
|
||||||
|
if (!next || !next.includes("@")) {
|
||||||
|
setErr("Ongeldig e-mailadres");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (next === v.email.toLowerCase()) {
|
||||||
|
setMsg("E-mailadres is ongewijzigd");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setActionId(`email-${v.id}`);
|
||||||
|
setMsg(null);
|
||||||
|
setErr(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/v1/admin/viewers/${v.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ email: next }),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
setErr(data.error?.message ?? "E-mail wijzigen mislukt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEditEmails((prev) => ({ ...prev, [v.id]: next }));
|
||||||
|
setMsg(`E-mail gewijzigd naar ${next}`);
|
||||||
|
load();
|
||||||
|
} finally {
|
||||||
|
setActionId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteViewer(v: Viewer) {
|
async function deleteViewer(v: Viewer) {
|
||||||
const label = v.name || v.email;
|
const label = v.name || v.email;
|
||||||
if (
|
if (
|
||||||
|
|
@ -979,6 +1013,41 @@ export default function ViewersPage() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group password-change">
|
<div className="form-group password-change">
|
||||||
|
<label className="field">
|
||||||
|
E-mailadres
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
value={editEmails[selected.id] ?? selected.email}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditEmails((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[selected.id]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
void changeViewerEmail(selected);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-primary"
|
||||||
|
disabled={
|
||||||
|
actionId === `email-${selected.id}` ||
|
||||||
|
(editEmails[selected.id] ?? selected.email).trim().toLowerCase() ===
|
||||||
|
selected.email.toLowerCase()
|
||||||
|
}
|
||||||
|
onClick={() => void changeViewerEmail(selected)}
|
||||||
|
>
|
||||||
|
{actionId === `email-${selected.id}`
|
||||||
|
? "Opslaan…"
|
||||||
|
: "E-mail wijzigen"}
|
||||||
|
</button>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
Nieuw wachtwoord
|
Nieuw wachtwoord
|
||||||
<input
|
<input
|
||||||
|
|
@ -1468,19 +1537,31 @@ export default function ViewersPage() {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.user-list {
|
.user-list {
|
||||||
|
--user-gap: 0.3rem;
|
||||||
|
--user-row-h: 4.45rem;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.3rem;
|
gap: var(--user-gap);
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
max-height: calc(12 * var(--user-row-h) + 11 * var(--user-gap));
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
.user-list::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
.user-row {
|
.user-row {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-height: var(--user-row-h);
|
||||||
|
box-sizing: border-box;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
padding: 0.7rem 0.75rem;
|
padding: 0.55rem 0.75rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export function registerViewerRoutes(
|
||||||
const { id } = request.params as { id: string };
|
const { id } = request.params as { id: string };
|
||||||
const body = request.body as {
|
const body = request.body as {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
|
email?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
appAccess?: boolean;
|
appAccess?: boolean;
|
||||||
iptvAccess?: boolean;
|
iptvAccess?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,7 @@ export class ViewerService {
|
||||||
id: string,
|
id: string,
|
||||||
data: {
|
data: {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
|
email?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
appAccess?: boolean;
|
appAccess?: boolean;
|
||||||
iptvAccess?: boolean;
|
iptvAccess?: boolean;
|
||||||
|
|
@ -231,8 +232,15 @@ export class ViewerService {
|
||||||
password?: string;
|
password?: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
const existing = await prisma.viewerUser.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { id: true, email: true },
|
||||||
|
});
|
||||||
|
if (!existing) throw new AppError("NOT_FOUND", "Gebruiker niet gevonden", 404);
|
||||||
|
|
||||||
const patch: {
|
const patch: {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
|
email?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
appAccess?: boolean;
|
appAccess?: boolean;
|
||||||
iptvAccess?: boolean;
|
iptvAccess?: boolean;
|
||||||
|
|
@ -246,6 +254,17 @@ export class ViewerService {
|
||||||
if (data.iptvAddonTokenId !== undefined) {
|
if (data.iptvAddonTokenId !== undefined) {
|
||||||
patch.iptvAddonTokenId = data.iptvAddonTokenId?.trim() || null;
|
patch.iptvAddonTokenId = data.iptvAddonTokenId?.trim() || null;
|
||||||
}
|
}
|
||||||
|
if (data.email !== undefined) {
|
||||||
|
const email = data.email.trim().toLowerCase();
|
||||||
|
if (!email || !email.includes("@")) {
|
||||||
|
throw new AppError("INVALID_REQUEST", "Ongeldig e-mailadres", 400);
|
||||||
|
}
|
||||||
|
if (email !== existing.email.toLowerCase()) {
|
||||||
|
const clash = await prisma.viewerUser.findUnique({ where: { email } });
|
||||||
|
if (clash) throw new AppError("CONFLICT", "E-mail bestaat al", 409);
|
||||||
|
patch.email = email;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (data.password?.trim()) {
|
if (data.password?.trim()) {
|
||||||
if (data.password.length < 8) {
|
if (data.password.length < 8) {
|
||||||
throw new AppError("INVALID_REQUEST", "Wachtwoord minstens 8 tekens", 400);
|
throw new AppError("INVALID_REQUEST", "Wachtwoord minstens 8 tekens", 400);
|
||||||
|
|
@ -266,7 +285,10 @@ export class ViewerService {
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (patch.passwordHash) {
|
if (patch.email) {
|
||||||
|
await this.clearLoginLock(existing.email.toLowerCase());
|
||||||
|
await this.clearLoginLock(updated.email.toLowerCase());
|
||||||
|
} else if (patch.passwordHash) {
|
||||||
await this.clearLoginLock(updated.email.toLowerCase());
|
await this.clearLoginLock(updated.email.toLowerCase());
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue