Files
DorkNet/DorkNet.Server/Controllers/Auth/AuthAccountController.cs
Alexa d471b418b9 Import march-2020-03-10 from private repo (scrubbed 2026-05-27)
Targets Rec Room 2020.03.10 client (also serves 2020.03.06).

Re-import after adding:
- DorkNet.ClientMod/dorknet-clientmod.json.template — config schema
  the launcher's manifest-driven patcher renders into UserData/.
- tools/build-patcher-zip.ps1 — produces dorknet-clientpatch-{key}.zip
  for v1-march-* Release uploads.
- .github/workflows/release.yml — on tag push publishes both
  dorknet-server-march_2020_03_10-win-x64.zip and
  dorknet-clientpatch-march_2020_03_10.zip as Release assets.

Same scrub rules as december; tools/build-patcher-zip.ps1 added to
the maintainer-side allow-list so it survives.
2026-05-27 01:06:12 +03:00

94 lines
4.1 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using DorkNet.Server.Auth;
using DorkNet.Server.Data;
namespace DorkNet.Server.Controllers.Auth;
/// <summary>
/// auth.rec.net/account/me/* — password and account-recovery endpoints
/// the client invokes from the in-game settings screen. Real persistence:
/// the password is stored as a BCrypt hash on
/// <see cref="DorkNet.Server.Data.Entities.PlayerEntity.PasswordHash"/>;
/// HasPassword reflects whether the column is non-null; ChangePassword
/// requires the old password to match (or for the column to be null —
/// first-time set); ResetPassword is admin-gated for now.
/// </summary>
[ApiController]
public class AuthAccountController(DorkNetDbContext db) : ControllerBase
{
/// <summary>GET /account/me/haspassword — returns whether the
/// caller has a password set. Client uses
/// <c>ExpectPrimitiveResponse&lt;bool&gt;</c> so the body must be
/// the literal JSON <c>true</c> or <c>false</c>.</summary>
[HttpGet("/account/me/haspassword")]
[HttpGet("/api/platformlogin/haspassword")]
[Authorize]
public async Task<IActionResult> HasPassword()
{
var pid = this.RequireCurrentPlayerId();
var hasPwd = await db.Players
.Where(p => p.Id == pid)
.Select(p => p.PasswordHash != null)
.FirstOrDefaultAsync();
return Content(hasPwd ? "true" : "false", "application/json");
}
/// <summary>POST /account/me/changepassword (and aliases). When the
/// account already has a password the caller must supply the old
/// one; first-time creation skips that check. Form-encoded body.</summary>
[HttpPost("/account/me/changepassword")]
[HttpPost("/account/me/createpassword")]
[HttpPost("/api/platformlogin/password")]
[HttpPut("/api/platformlogin/password")]
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
[Authorize]
public async Task<IActionResult> ChangePassword(
[FromForm] string? oldPassword,
[FromForm] string? newPassword)
{
if (string.IsNullOrEmpty(newPassword) || newPassword.Length < 6)
return Ok(new { success = false, error = "password_too_short" });
var pid = this.RequireCurrentPlayerId();
var player = await db.Players.FirstOrDefaultAsync(p => p.Id == pid);
if (player is null) return Unauthorized();
// First-time create: PasswordHash is null and oldPassword is
// ignored. Subsequent changes require the existing password to
// verify so a stolen JWT can't silently rotate the password.
if (!string.IsNullOrEmpty(player.PasswordHash))
{
if (string.IsNullOrEmpty(oldPassword) ||
!BCrypt.Net.BCrypt.Verify(oldPassword, player.PasswordHash))
return Ok(new { success = false, error = "old_password_mismatch" });
}
player.PasswordHash = BCrypt.Net.BCrypt.HashPassword(newPassword, workFactor: 11);
await db.SaveChangesAsync();
return Ok(new { success = true, error = "" });
}
/// <summary>POST /account/me/resetpassword — sends a reset link.
/// Without an SMTP integration we have nothing to send to, so this
/// is admin-only: an admin can clear the password column on any
/// account from the admin tab and the user re-creates it via
/// changepassword on next login. Anonymous calls 401.</summary>
[HttpPost("/account/me/resetpassword")]
[HttpPost("/api/platformlogin/resetpassword")]
[Authorize]
[AdminOnly]
public async Task<IActionResult> AdminResetPassword(
[FromForm] long? targetPlayerId)
{
if (targetPlayerId is null)
return Ok(new { success = false, error = "missing_target" });
var player = await db.Players.FirstOrDefaultAsync(p => p.Id == targetPlayerId);
if (player is null) return Ok(new { success = false, error = "unknown_player" });
player.PasswordHash = null;
await db.SaveChangesAsync();
return Ok(new { success = true, error = "" });
}
}