R6 moderation in decePub: users and personas, accounts, report actions, deletions, suspended personas
Administration's Users & personas (administrators): logins searched and opened into their personas and groups; roles, bans, two-factor reset and deletion of a login; silence, suspension and deletion of a persona; a group deleted. Accounts (moderators): local personas and remote accounts searched or listed as moderated, silenced and suspended with a staff note and a warning. Reports resolve with an action. ActDialog asks for the note, the warning or the password and is the act's confirmation. A login deletes its own persona and the groups it owns with its password. Suspended personas can't be chosen; one refused a token mid-session moves the session to another, or to none, once. Warnings show in notifications, limited accounts on their profile, and moderators remove a post from its card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
1 parent
4c88980374
commit
c572471b88
30 files changed
+2604
-67
No files matched your search
@@ -4,10 +4,11 @@
|
||||
|
||||
<Title>@Localizer["Administration"]</Title>
|
||||
|
||||
@* Moderators handle reports and read the staff's log. Administrators also say what the server is (Instance), set its
|
||||
rules, write its announcements, block servers for everyone, choose its relays and media limits, read the federation
|
||||
statistics, and back up and restore the server. PrivaPub checks the role on every call; the sections only follow it.
|
||||
Each section is a collapse marked data-admin-section; its panel loads when the page opens. *@
|
||||
@* Moderators handle reports, moderate accounts here and elsewhere (Accounts) and read the staff's log. Administrators
|
||||
also change the logins and their personas (Users & personas), say what the server is (Instance), set its rules, write
|
||||
its announcements, block servers for everyone, choose its relays and media limits, read the federation statistics, and
|
||||
back up and restore the server. PrivaPub checks the role on every call; the sections only follow it. Each section is
|
||||
a collapse marked data-admin-section; its panel loads when the page opens. *@
|
||||
<section class="block relative w-full h-full neo-inset neo-xs rounded-xl">
|
||||
|
||||
<div class="flex flex-col gap-4 p-4 md:p-5 w-full h-full absolute overflow-y-auto">
|
||||
@@ -25,6 +26,19 @@
|
||||
</ChildContent>
|
||||
</DCollapse>
|
||||
|
||||
<DCollapse Icon="DCollapseIcon.Arrow" data-admin-section="accounts">
|
||||
<Header>
|
||||
<p class="inline-flex items-center gap-2">
|
||||
<i class="ion-md-contacts text-xl"></i> <span>@Localizer["Accounts"]</span>
|
||||
</p>
|
||||
</Header>
|
||||
<ChildContent>
|
||||
<div class="block w-full p-3 md:p-4">
|
||||
<AccountsPanel />
|
||||
</div>
|
||||
</ChildContent>
|
||||
</DCollapse>
|
||||
|
||||
<DCollapse Icon="DCollapseIcon.Arrow" data-admin-section="log">
|
||||
<Header>
|
||||
<p class="inline-flex items-center gap-2">
|
||||
@@ -39,6 +53,19 @@
|
||||
</DCollapse>
|
||||
|
||||
<AuthorizeView Policy="@Policies.IsAdmin">
|
||||
<DCollapse Icon="DCollapseIcon.Arrow" data-admin-section="users">
|
||||
<Header>
|
||||
<p class="inline-flex items-center gap-2">
|
||||
<i class="ion-md-people text-xl"></i> <span>@Localizer["Users & personas"]</span>
|
||||
</p>
|
||||
</Header>
|
||||
<ChildContent>
|
||||
<div class="block w-full p-3 md:p-4">
|
||||
<UsersPanel />
|
||||
</div>
|
||||
</ChildContent>
|
||||
</DCollapse>
|
||||
|
||||
<DCollapse Icon="DCollapseIcon.Arrow" data-admin-section="instance">
|
||||
<Header>
|
||||
<p class="inline-flex items-center gap-2">
|
||||
|
||||
+36
-1
@@ -4,7 +4,8 @@
|
||||
<Title>@Localizer["Groups"]</Title>
|
||||
|
||||
@* The current persona's communities and circles (/clientapi/group): made, joined by invitation, and left; for the ones
|
||||
it runs, edited, their invitation shown, the requests to join answered and members taken out. A circle's members are
|
||||
it runs, edited, their invitation shown, the requests to join answered and members taken out; for the ones it owns,
|
||||
deleted with the login's password, asked first (its members are told, its name stays taken). A circle's members are
|
||||
never shown to anyone but its owner and moderators, here as on every server. *@
|
||||
<section class="block relative w-full h-full neo-inset neo-xs rounded-xl">
|
||||
<div class="flex flex-col gap-4 p-4 md:p-5 w-full h-full absolute overflow-y-auto">
|
||||
@@ -179,6 +180,13 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-2 pt-2 border-t border-base-300">
|
||||
<span class="text-xs opacity-70 flex-1 min-w-48">@Localizer["Deleting the group tells its members, and its name stays taken."]</span>
|
||||
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || isSaving)" OnClick="() => deleting = group" data-group-delete-own>
|
||||
<i class="ion-md-trash text-error" aria-hidden="true"></i><span>@Localizer["Delete the group"]</span>
|
||||
</DButton>
|
||||
</div>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
@@ -186,6 +194,14 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (deleting is not null)
|
||||
{
|
||||
<ActDialog Question="@Localizer["Delete the group {0}?", deleting.Name ?? deleting.UserName]"
|
||||
Text="@Localizer["Its members and followers are told, and its name stays taken. It cannot be undone."]"
|
||||
ConfirmText="@Localizer["Delete for good"]" Danger="true" Act="group-delete" AskPassword="true" OnConfirm="Delete"
|
||||
OnClosed="() => deleting = null" />
|
||||
}
|
||||
|
||||
@code {
|
||||
static readonly string[] PostingPolicies = ["followers", "anyone", "moderators"];
|
||||
|
||||
@@ -200,6 +216,8 @@
|
||||
bool editDiscoverable;
|
||||
bool editApproves;
|
||||
ViewGroup open;
|
||||
// the group whose deletion the dialog asks about
|
||||
ViewGroup deleting;
|
||||
string loadedPersonaId;
|
||||
bool isSaving;
|
||||
|
||||
@@ -300,6 +318,23 @@
|
||||
Toast.AddResponseSuccess(Localizer["You left {0}.", group.Name ?? group.UserName]);
|
||||
}
|
||||
|
||||
async Task<WebResult> Delete(ActDialog.Answer answer)
|
||||
{
|
||||
var group = deleting;
|
||||
var result = await GroupService.Delete(CascadingState.PersonaId, group.Id, answer.Password);
|
||||
if (result.IsInvalid)
|
||||
return result;
|
||||
|
||||
groups = [.. groups.Where(g => g.Id != group.Id)];
|
||||
if (open?.Id == group.Id)
|
||||
{
|
||||
open = null;
|
||||
members = [];
|
||||
}
|
||||
Toast.AddResponseSuccess(Localizer["{0} is deleted.", group.Name ?? group.UserName]);
|
||||
return result;
|
||||
}
|
||||
|
||||
async Task Toggle(ViewGroup group)
|
||||
{
|
||||
if (open?.Id == group.Id)
|
||||
|
||||
+15
-2
@@ -114,14 +114,27 @@
|
||||
<div class="flex flex-col gap-4 w-full max-w-md mt-6 md:mt-16">
|
||||
<h1 class="text-xl font-bold text-center">@Localizer["Who are you today?"]</h1>
|
||||
<p class="text-sm opacity-70 text-center">@Localizer["Each persona has its own token; PrivaPub never tells anyone they share a login."]</p>
|
||||
@if (CascadingState.Personas.All(p => !CascadingState.CanUse(p)))
|
||||
{
|
||||
<p class="text-sm text-center" data-personas-suspended>
|
||||
@Localizer["Every persona of this login is suspended by this server's staff. Settings can still make a new one."]
|
||||
</p>
|
||||
<DButton Href="settings">@Localizer["Settings"]</DButton>
|
||||
}
|
||||
@foreach (var persona in CascadingState.Personas)
|
||||
{
|
||||
<button type="button" class="flex items-center gap-3 p-3 rounded-xl text-left neo-raised neo-xs hover:neo-sm transition-shadow @(persona.Id == CascadingState.PersonaId ? "ring-2 ring-primary" : null)"
|
||||
disabled="@(IsDefaultDisabled || isWorking)" @onclick="() => Choose(persona.Id)">
|
||||
@* a persona the staff suspended can't be chosen until that is lifted *@
|
||||
<button type="button" class="flex items-center gap-3 p-3 rounded-xl text-left neo-raised neo-xs hover:neo-sm transition-shadow disabled:opacity-60 @(persona.Id == CascadingState.PersonaId ? "ring-2 ring-primary" : null)"
|
||||
disabled="@(IsDefaultDisabled || isWorking || !CascadingState.CanUse(persona))" @onclick="() => Choose(persona.Id)"
|
||||
data-persona-choice="@persona.UserName" data-persona-state="@(CascadingState.CanUse(persona) ? "normal" : "suspended")">
|
||||
<img alt="" class="h-12 w-12 flex-none object-cover rounded-full neo-raised neo-2xs" src="@(persona.PictureURL ?? "imgs/icon-192.png")" />
|
||||
<span class="flex flex-col min-w-0">
|
||||
<b class="truncate">@(persona.Name ?? persona.UserName)</b>
|
||||
<span class="text-xs opacity-70 truncate">@(persona.Handle ?? "@" + persona.UserName)</span>
|
||||
@if (!CascadingState.CanUse(persona))
|
||||
{
|
||||
<span class="text-xs text-error">@Localizer["Suspended by this server's staff: it can't act until that is lifted."]</span>
|
||||
}
|
||||
</span>
|
||||
@if (persona.Id == CascadingState.PersonaId)
|
||||
{
|
||||
|
||||
@@ -126,6 +126,13 @@
|
||||
{
|
||||
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft"><i class="ion-md-people"></i> @Localizer["Groups"]</DBadge>
|
||||
}
|
||||
@if (account.Limited == true)
|
||||
{
|
||||
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Warning" data-account-limited
|
||||
title="@Localizer["This server's staff limited it: its posts stay out of public timelines, trends and search."]">
|
||||
<i class="ion-md-volume-off"></i> @Localizer["Limited"]
|
||||
</DBadge>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(account.Note))
|
||||
|
||||
Reference in new issue
Block a user