mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 15:08:47 -07:00
admin: change a player's username from the Players panel
Add POST players/{id}/username + a Username field in the player detail's
Profile tab. Changes only the unique @handle (not the display name) and
rejects a collision outright rather than auto-suffixing, so the admin
gets the exact handle they typed. Same 2-24 char letters/digits/_/-
rule as the account API; logged to the audit trail as old -> new.
This commit is contained in:
@@ -1725,6 +1725,35 @@ public class AdminController(
|
||||
return Ok(new { p.Id, p.DisplayName });
|
||||
}
|
||||
|
||||
public sealed record UsernameRequest(string Username);
|
||||
|
||||
/// <summary>Force-set a player's username (the unique @handle).
|
||||
/// Unlike the normal account flow this changes ONLY the username, not
|
||||
/// the display name, and rejects a collision outright rather than
|
||||
/// auto-suffixing — the admin types the exact handle they want.
|
||||
/// Username rules mirror the account API: 2–24 chars, letters /
|
||||
/// digits / underscore / hyphen.</summary>
|
||||
[HttpPost("players/{id:long}/username")]
|
||||
public async Task<ActionResult> SetUsername(long id, [FromBody] UsernameRequest body)
|
||||
{
|
||||
var p = await db.Players.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (p is null) return NotFound();
|
||||
|
||||
var name = (body.Username ?? string.Empty).Trim();
|
||||
if (name.Length is < 2 or > 24
|
||||
|| !name.All(ch => char.IsLetterOrDigit(ch) || ch is '_' or '-'))
|
||||
return BadRequest(new { Error = "Username must be 2–24 chars: letters, numbers, _ or -." });
|
||||
|
||||
if (await db.Players.AnyAsync(x => x.Username == name && x.Id != id))
|
||||
return BadRequest(new { Error = "That username is already taken." });
|
||||
|
||||
var previous = p.Username;
|
||||
p.Username = name;
|
||||
await LogAsync("set_username", "player", id, $"{previous} -> {name}");
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(new { p.Id, p.Username });
|
||||
}
|
||||
|
||||
private sealed class InventoryEntry
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("itemId")]
|
||||
|
||||
@@ -446,6 +446,7 @@ function GrantsTab({ data, onChanged }: { data: PlayerDetail; onChanged: () => v
|
||||
function ProfileTab({ data, onChanged }: { data: PlayerDetail; onChanged: () => void }) {
|
||||
const toast = useToast();
|
||||
const [displayName, setDisplayName] = useState(data.displayName);
|
||||
const [username, setUsername] = useState(data.username);
|
||||
const [flags, setFlags] = useState({
|
||||
isVerified: data.isVerified,
|
||||
isDeveloper: data.isDeveloper,
|
||||
@@ -465,6 +466,19 @@ function ProfileTab({ data, onChanged }: { data: PlayerDetail; onChanged: () =>
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="card !p-4">
|
||||
<h3 className="text-sm font-semibold text-ink-50 mb-1">Username</h3>
|
||||
<p className="text-xs text-ink-400 mb-3">The unique @handle. 2–24 chars: letters, numbers, _ or -. Changing it doesn't touch the display name.</p>
|
||||
<div className="flex gap-2">
|
||||
<input value={username} onChange={e => setUsername(e.target.value)} className="input flex-1 font-mono text-xs" />
|
||||
<button
|
||||
onClick={() => save('Username', () => api(`/players/${data.id}/username`, { method: 'POST', body: { Username: username.trim() } }))}
|
||||
disabled={!username.trim() || username.trim() === data.username}
|
||||
className="btn-primary text-xs"
|
||||
>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card !p-4">
|
||||
<h3 className="text-sm font-semibold text-ink-50 mb-1">Display name</h3>
|
||||
<p className="text-xs text-ink-400 mb-3">Force-override the player's display name.</p>
|
||||
|
||||
Reference in New Issue
Block a user