mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
feat: Implement room restoration functionality and update room state management
This commit is contained in:
@@ -404,6 +404,7 @@ public class AdminController(
|
|||||||
r.IsAGRoom,
|
r.IsAGRoom,
|
||||||
r.IsDormRoom,
|
r.IsDormRoom,
|
||||||
r.CreatorPlayerId,
|
r.CreatorPlayerId,
|
||||||
|
r.State,
|
||||||
BlobCount = db.RoomDataBlobs.Count(b => b.RoomId == r.Id),
|
BlobCount = db.RoomDataBlobs.Count(b => b.RoomId == r.Id),
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@@ -448,6 +449,21 @@ public class AdminController(
|
|||||||
return Ok(new { room.Id, room.State });
|
return Ok(new { room.Id, room.State });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>POST <c>api/admin/v1/rooms/{id}/restore</c> — the inverse of
|
||||||
|
/// the soft-archive above: clears State back to 0 (Active) so the room
|
||||||
|
/// shows up in browse/search again. Idempotent; safe on a non-archived
|
||||||
|
/// room (just re-asserts State=0).</summary>
|
||||||
|
[HttpPost("rooms/{id:long}/restore")]
|
||||||
|
public async Task<ActionResult> RestoreRoom(long id)
|
||||||
|
{
|
||||||
|
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == id);
|
||||||
|
if (room is null) return NotFound();
|
||||||
|
room.State = 0; // Active
|
||||||
|
await LogAsync("restore_room", "room", id, "");
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return Ok(new { room.Id, room.State });
|
||||||
|
}
|
||||||
|
|
||||||
// The bulk "purge every custom room" endpoint was removed: a single
|
// The bulk "purge every custom room" endpoint was removed: a single
|
||||||
// wrong click could wipe every imported map in one shot. Per-room
|
// wrong click could wipe every imported map in one shot. Per-room
|
||||||
// purge (below) is the only hard-delete path, gated by two
|
// purge (below) is the only hard-delete path, gated by two
|
||||||
@@ -487,8 +503,11 @@ public class AdminController(
|
|||||||
{
|
{
|
||||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == id);
|
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == id);
|
||||||
if (room is null) return NotFound();
|
if (room is null) return NotFound();
|
||||||
if (room.IsDormRoom)
|
// Dorms are per-player state — purging one wipes that player's dorm
|
||||||
return BadRequest(new { error = "refuses_to_hard_delete_dorm", note = "Dorms are per-player state; never purge." });
|
// contents, but it's recoverable: EnsurePersonalDormAsync regenerates
|
||||||
|
// a fresh dorm on their next join. An admin sometimes needs to nuke a
|
||||||
|
// corrupted/test dorm, so we ALLOW it; the two typed-name
|
||||||
|
// confirmations below are the guard against an accidental wipe.
|
||||||
if (room.IsAGRoom && room.CreatorPlayerId == PlayerService.SystemAccountId)
|
if (room.IsAGRoom && room.CreatorPlayerId == PlayerService.SystemAccountId)
|
||||||
return BadRequest(new { error = "refuses_to_hard_delete_seeded_room", note = "This row is owned by the system seed account — soft-archive via DELETE /rooms/{id} instead." });
|
return BadRequest(new { error = "refuses_to_hard_delete_seeded_room", note = "This row is owned by the system seed account — soft-archive via DELETE /rooms/{id} instead." });
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export interface Room {
|
|||||||
isAGRoom: boolean;
|
isAGRoom: boolean;
|
||||||
isDormRoom: boolean;
|
isDormRoom: boolean;
|
||||||
creatorPlayerId: number;
|
creatorPlayerId: number;
|
||||||
|
state: number;
|
||||||
blobCount: number;
|
blobCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1006,7 +1006,11 @@ function DangerTab({ room, onAfter }: { room: RoomDetail; onAfter: () => void })
|
|||||||
const [confirm2, setConfirm2] = useState('');
|
const [confirm2, setConfirm2] = useState('');
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
const isCustom = !room.isDormRoom && room.owner.id !== 1;
|
// Anything not owned by the system seed account (id 1) can be purged —
|
||||||
|
// including dorms. Purging a dorm wipes its contents but the player's dorm
|
||||||
|
// regenerates on next join; the two typed-name confirmations are the guard.
|
||||||
|
const isCustom = room.owner.id !== 1;
|
||||||
|
const isArchived = room.state === 1;
|
||||||
const purgeReady = confirm1 === room.name && confirm2 === room.name && !busy;
|
const purgeReady = confirm1 === room.name && confirm2 === room.name && !busy;
|
||||||
|
|
||||||
const archive = async () => {
|
const archive = async () => {
|
||||||
@@ -1019,6 +1023,16 @@ function DangerTab({ room, onAfter }: { room: RoomDetail; onAfter: () => void })
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const restore = async () => {
|
||||||
|
try {
|
||||||
|
await api(`/rooms/${room.id}/restore`, { method: 'POST' });
|
||||||
|
toast.push(`Restored ${room.name}`, 'success');
|
||||||
|
onAfter();
|
||||||
|
} catch (e) {
|
||||||
|
toast.push((e as Error).message, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const purge = async () => {
|
const purge = async () => {
|
||||||
if (!purgeReady) return;
|
if (!purgeReady) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
@@ -1041,14 +1055,22 @@ function DangerTab({ room, onAfter }: { room: RoomDetail; onAfter: () => void })
|
|||||||
<div className="card p-4">
|
<div className="card p-4">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-ink-50">Archive</h3>
|
<h3 className="text-sm font-semibold text-ink-50">{isArchived ? 'Restore' : 'Archive'}</h3>
|
||||||
<p className="text-xs text-ink-400 mt-1">
|
<p className="text-xs text-ink-400 mt-1">
|
||||||
Soft delete — sets State=Archived. The room stops appearing in browse/search but stays restorable.
|
{isArchived
|
||||||
|
? 'This room is archived (State=1). Restore sets State=0 (Active) so it shows up in browse/search again.'
|
||||||
|
: 'Soft delete — sets State=Archived. The room stops appearing in browse/search but stays restorable.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{isArchived ? (
|
||||||
|
<button onClick={restore} className="btn-secondary text-xs" disabled={busy}>
|
||||||
|
Restore
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
<button onClick={() => setArchiveOpen(true)} className="btn-secondary text-xs text-danger">
|
<button onClick={() => setArchiveOpen(true)} className="btn-secondary text-xs text-danger">
|
||||||
<Trash /> Archive
|
<Trash /> Archive
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isCustom && (
|
{isCustom && (
|
||||||
|
|||||||
@@ -79,6 +79,16 @@ export function Rooms() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const restore = async (r: Room) => {
|
||||||
|
try {
|
||||||
|
await api(`/rooms/${r.id}/restore`, { method: 'POST' });
|
||||||
|
toast.push(`Restored ${r.name}`, 'success');
|
||||||
|
refresh();
|
||||||
|
} catch (e) {
|
||||||
|
toast.push((e as Error).message, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -172,10 +182,15 @@ export function Rooms() {
|
|||||||
<td className="px-4 py-2.5 text-right text-ink-200 tabular-nums">{r.blobCount}</td>
|
<td className="px-4 py-2.5 text-right text-ink-200 tabular-nums">{r.blobCount}</td>
|
||||||
<td className="px-4 py-2.5 text-right whitespace-nowrap">
|
<td className="px-4 py-2.5 text-right whitespace-nowrap">
|
||||||
<Link to={`/rooms/${r.id}`} className="btn-ghost text-xs">Manage</Link>
|
<Link to={`/rooms/${r.id}`} className="btn-ghost text-xs">Manage</Link>
|
||||||
|
{r.state === 1 ? (
|
||||||
|
<button onClick={() => restore(r)} className="btn-ghost text-xs ml-1" title="Unarchive — set State=Active">
|
||||||
|
Restore
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
<button onClick={() => setPendingDelete(r)} className="btn-ghost text-xs text-danger ml-1" title="Soft archive (reversible)">
|
<button onClick={() => setPendingDelete(r)} className="btn-ghost text-xs text-danger ml-1" title="Soft archive (reversible)">
|
||||||
<Trash />
|
<Trash />
|
||||||
</button>
|
</button>
|
||||||
{!r.isDormRoom && (
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => setPendingPurge(r)}
|
onClick={() => setPendingPurge(r)}
|
||||||
className="btn-ghost text-xs text-danger ml-1"
|
className="btn-ghost text-xs text-danger ml-1"
|
||||||
@@ -183,7 +198,6 @@ export function Rooms() {
|
|||||||
>
|
>
|
||||||
Purge
|
Purge
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user