Adds the account-provisioning plane from docs/PROTOCOL_2.md Part A: the website can create game accounts and unlink them, gated by a shard-wide signup mode. The existing [link flow is unchanged. Overlay: - BridgeConfig: SignupMode (website|game|hybrid, default hybrid; unrecognized falls back to game), AccountCreateEnabled (mode-following default), RequireIpForCreate, name/password caps, and a boot warning when the core Accounts.AutoCreateAccounts setting contradicts the mode. - BridgeAccounts (new): account.create (mode gate, actor required, char-safety mirrored from AccountHandler, collision check, per-IP cap via CanCreate/ LogAccess with fail-closed missing/loopback IP, create + WebsiteUserId link, account.audit; password never logged or echoed) and account.unlink (Owner floor via BridgeAdmin.Protected, clears the tag). - BridgeAccountLink: in-game [unlink command, emits account.unlinked. - BridgeAdmin: Protected / ResolveTargetAccount promoted to public for reuse. Sidecar: - POST /accounts/create, DELETE /link/:account, respond_account status mapping (409 collision / 429 ip cap / 403 disabled|protected / 404 not-linked / 400). - store.record_unlink drops the mirrored link row. - PROTOCOL_VERSION -> 2 (outbound events additive; new endpoints need v2). Docs: INTEGRATION.md protocol bump, account.* events, endpoints, 409/429; PROTOCOL_2.md Part A marked built. Verified: sidecar cargo check clean; overlay compiles in the full ServUO Scripts tree (0 errors, 0 warnings). Live end-to-end run still pending. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
179 lines
8.8 KiB
C#
179 lines
8.8 KiB
C#
using System;
|
|
|
|
namespace Server.Custom.Bridge
|
|
{
|
|
/// <summary>
|
|
/// Which side may mint game accounts. Governs the bridge's inbound account.create verb;
|
|
/// the in-game first-login auto-create is a separate core setting (Accounts.AutoCreateAccounts)
|
|
/// the operator pairs with this (docs/PROTOCOL_2.md §2).
|
|
/// </summary>
|
|
public enum SignupMode
|
|
{
|
|
Website, // website is the account authority; in-game auto-create should be off
|
|
Game, // game server is the authority; account.create is refused
|
|
Hybrid // either side may create
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tunables from Config/Bridge.cfg. Key scope is the filename, so `Port=7788` there
|
|
/// reads as "Bridge.Port" here.
|
|
///
|
|
/// Loaded in Configure(), which ScriptCompiler invokes before World.Load.
|
|
/// </summary>
|
|
public static class BridgeConfig
|
|
{
|
|
public static string Host { get; private set; }
|
|
public static int Port { get; private set; }
|
|
public static int QueueCap { get; private set; }
|
|
|
|
public static int StatSweepSeconds { get; private set; }
|
|
public static int DecaySweepSeconds { get; private set; }
|
|
public static int EconomySweepSeconds { get; private set; }
|
|
public static int PageSweepSeconds { get; private set; }
|
|
public static int ChampSweepSeconds { get; private set; }
|
|
|
|
public static string LinkUrl { get; private set; }
|
|
|
|
public static int TownCrierMaxLines { get; private set; }
|
|
public static int TownCrierMaxLineLength { get; private set; }
|
|
public static int TownCrierMaxActive { get; private set; }
|
|
public static int TownCrierMaxDurationSec { get; private set; }
|
|
|
|
public static bool AdminWriteEnabled { get; private set; }
|
|
public static AccessLevel AdminAccessFloor { get; private set; }
|
|
public static int AdminBroadcastMaxLength { get; private set; }
|
|
public static int AdminReasonMaxLength { get; private set; }
|
|
public static int AdminBanMaxDurationSec { get; private set; }
|
|
|
|
// ---- account provisioning (docs/PROTOCOL_2.md Part A) ----
|
|
public static SignupMode Signup { get; private set; }
|
|
public static bool AccountCreateEnabled { get; private set; }
|
|
public static bool RequireIpForCreate { get; private set; }
|
|
public static int AccountNameMaxLength { get; private set; }
|
|
public static int AccountPasswordMaxLength { get; private set; }
|
|
|
|
public static bool Enabled { get; private set; }
|
|
|
|
public static void Configure()
|
|
{
|
|
Load();
|
|
}
|
|
|
|
/// <summary>Re-readable at runtime via `[bridge reload`.</summary>
|
|
public static void Load()
|
|
{
|
|
Enabled = Config.Get("Bridge.Enabled", true);
|
|
|
|
Host = Config.Get("Bridge.Host", "127.0.0.1");
|
|
Port = Config.Get("Bridge.Port", 7788);
|
|
QueueCap = Config.Get("Bridge.QueueCap", 10000);
|
|
|
|
StatSweepSeconds = Config.Get("Bridge.StatSweepSeconds", 30);
|
|
DecaySweepSeconds = Config.Get("Bridge.DecaySweepSeconds", 60);
|
|
EconomySweepSeconds = Config.Get("Bridge.EconomySweepSeconds", 300);
|
|
PageSweepSeconds = Config.Get("Bridge.PageSweepSeconds", 5);
|
|
if (PageSweepSeconds < 1)
|
|
PageSweepSeconds = 1;
|
|
|
|
ChampSweepSeconds = Config.Get("Bridge.ChampSweepSeconds", 10);
|
|
if (ChampSweepSeconds < 1)
|
|
ChampSweepSeconds = 1;
|
|
|
|
LinkUrl = Config.Get("Bridge.LinkUrl", "https://yoursite/link");
|
|
|
|
TownCrierMaxLines = Config.Get("Bridge.TownCrierMaxLines", 6);
|
|
TownCrierMaxLineLength = Config.Get("Bridge.TownCrierMaxLineLength", 200);
|
|
TownCrierMaxActive = Config.Get("Bridge.TownCrierMaxActive", 20);
|
|
TownCrierMaxDurationSec = Config.Get("Bridge.TownCrierMaxDurationSec", 86400);
|
|
|
|
AdminWriteEnabled = Config.Get("Bridge.AdminWriteEnabled", false);
|
|
AdminAccessFloor = ParseAccessLevel(Config.Get("Bridge.AdminAccessFloor", "CoOwner"), AccessLevel.CoOwner);
|
|
AdminBroadcastMaxLength = Config.Get("Bridge.AdminBroadcastMaxLength", 300);
|
|
AdminReasonMaxLength = Config.Get("Bridge.AdminReasonMaxLength", 400);
|
|
AdminBanMaxDurationSec = Config.Get("Bridge.AdminBanMaxDurationSec", 31536000);
|
|
|
|
// Account provisioning. An absent SignupMode defaults to Hybrid; a *present but
|
|
// unrecognized* value falls back to Game (the safest — no website creation), so a
|
|
// typo can never accidentally open provisioning.
|
|
Signup = ParseSignupMode(Config.Get("Bridge.SignupMode", "hybrid"), SignupMode.Game);
|
|
// Default follows the mode: creation is on unless the shard is game-authority.
|
|
AccountCreateEnabled = Config.Get("Bridge.AccountCreateEnabled", Signup != SignupMode.Game);
|
|
RequireIpForCreate = Config.Get("Bridge.RequireIpForCreate", true);
|
|
AccountNameMaxLength = Config.Get("Bridge.AccountNameMaxLength", 16);
|
|
AccountPasswordMaxLength = Config.Get("Bridge.AccountPasswordMaxLength", 30);
|
|
if (AccountNameMaxLength < 1)
|
|
AccountNameMaxLength = 1;
|
|
if (AccountPasswordMaxLength < 1)
|
|
AccountPasswordMaxLength = 1;
|
|
|
|
if (QueueCap < 16)
|
|
QueueCap = 16;
|
|
|
|
WarnOnSignupMismatch();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The bridge governs only the account.create verb; ServUO's in-game first-login
|
|
/// auto-create is the core Accounts.AutoCreateAccounts setting. A shard whose two halves
|
|
/// disagree is quietly broken (website-only that still auto-creates in game, or a mode
|
|
/// that expects in-game creation with it switched off), so surface the contradiction
|
|
/// loudly rather than silently doing the permissive thing.
|
|
/// </summary>
|
|
private static void WarnOnSignupMismatch()
|
|
{
|
|
var autoCreate = Config.Get("Accounts.AutoCreateAccounts", true);
|
|
|
|
if (Signup == SignupMode.Website && autoCreate)
|
|
Console.WriteLine(
|
|
"[Bridge] WARNING: SignupMode=website but Accounts.AutoCreateAccounts=true; "
|
|
+ "an in-game login of any new name still mints an account. Set it false for website-only.");
|
|
else if (Signup == SignupMode.Game && !autoCreate)
|
|
Console.WriteLine(
|
|
"[Bridge] WARNING: SignupMode=game but Accounts.AutoCreateAccounts=false; "
|
|
+ "in-game creation is off and account.create is refused, so no account can be created.");
|
|
else if (Signup == SignupMode.Hybrid && !autoCreate)
|
|
Console.WriteLine(
|
|
"[Bridge] WARNING: SignupMode=hybrid but Accounts.AutoCreateAccounts=false; "
|
|
+ "in-game first-login creation is off. Only website account.create will work.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a SignupMode name, case-insensitively, falling back to <paramref name="fallback"/>
|
|
/// on anything unrecognized so a typo can never open provisioning wider than intended.
|
|
/// </summary>
|
|
private static SignupMode ParseSignupMode(string value, SignupMode fallback)
|
|
{
|
|
SignupMode parsed;
|
|
if (!String.IsNullOrEmpty(value) && Enum.TryParse(value.Trim(), true, out parsed) &&
|
|
Enum.IsDefined(typeof(SignupMode), parsed))
|
|
return parsed;
|
|
|
|
Console.WriteLine("[Bridge] unrecognized SignupMode '{0}', using {1}", value, fallback);
|
|
return fallback;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses an AccessLevel name from config, case-insensitively, falling back to the given
|
|
/// default on anything unrecognized so a typo can never open the floor wider than intended.
|
|
/// </summary>
|
|
private static AccessLevel ParseAccessLevel(string value, AccessLevel fallback)
|
|
{
|
|
AccessLevel parsed;
|
|
if (!String.IsNullOrEmpty(value) && Enum.TryParse(value.Trim(), true, out parsed) &&
|
|
Enum.IsDefined(typeof(AccessLevel), parsed))
|
|
return parsed;
|
|
|
|
Console.WriteLine("[Bridge] unrecognized AdminAccessFloor '{0}', using {1}", value, fallback);
|
|
return fallback;
|
|
}
|
|
|
|
public static string Describe()
|
|
{
|
|
return String.Format(
|
|
"enabled={0} endpoint={1}:{2} queueCap={3} sweeps(stat={4}s decay={5}s econ={6}s champ={7}s) adminWrite={8}(floor={9}) signup={10}(create={11})",
|
|
Enabled, Host, Port, QueueCap, StatSweepSeconds, DecaySweepSeconds, EconomySweepSeconds,
|
|
ChampSweepSeconds, AdminWriteEnabled, AdminAccessFloor, Signup, AccountCreateEnabled);
|
|
}
|
|
}
|
|
}
|