mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
signup codes: admin-issued invite codes + site /join redemption
While account creation is disabled, the only way in is now an admin-issued single-use signup code (Discord-invite style), redeemed on the public site's /join page. Because the 2020 watch login is device-id-bound with no code field, the flow captures it server-side instead: a new device refused at the signups-disabled gate (PlatformLogin / account create) is stashed in a PendingDevices table keyed by the real client IP (CF-Connecting-IP aware). The /join page then shows the player the device their own client just reported, they enter their code + username, and the account is minted bound to that device — so the next launch logs straight in via the existing device-id path. No watch changes. - SignupCodeEntity (code, descriptor, optional expiry, redeemer) + PendingDeviceEntity; idempotent CREATE TABLE in DatabaseBootstrap so both providers pick them up without a new migration. - SignupCodeService: generate / list / revoke / redeem + pending-device capture, single-use + expiry + revoke validation. - Admin: GET/POST signup-codes + revoke endpoints and a Signup codes admin-ui page (generate with descriptor/expiry, status table, revoke). - Public: anonymous /api/site/v1/join pending-devices + redeem, and a /join site page (+ header link). - README documents the flow + the post-Initial table-create approach.
This commit is contained in:
@@ -93,6 +93,14 @@ public static class DatabaseBootstrap
|
||||
await conn.CloseAsync();
|
||||
}
|
||||
|
||||
// Signup-code + pending-device tables. These post-date the
|
||||
// consolidated Initial migration, and the Postgres path is
|
||||
// EnsureCreated-only (no migration replay), so neither provider
|
||||
// gets them automatically on an existing DB. Create them with
|
||||
// idempotent raw SQL that works on a fresh DB too (CREATE TABLE
|
||||
// IF NOT EXISTS no-ops when EnsureCreated already built them).
|
||||
await EnsureSignupCodeTablesAsync(db);
|
||||
|
||||
// Coach system account at Player.Id=1. The RR-Original room seeder
|
||||
// below assigns CreatorPlayerId=1 to every canonical room
|
||||
// (RecCenter, Paintball, Dodgeball, etc.), so the FK target needs
|
||||
@@ -157,6 +165,77 @@ public static class DatabaseBootstrap
|
||||
Controllers.Health.HealthController.MigrationsComplete = true;
|
||||
}
|
||||
|
||||
/// <summary>Idempotently create the SignupCodes + PendingDevices
|
||||
/// tables on whichever provider is in use. Safe to run every boot:
|
||||
/// CREATE TABLE / INDEX IF NOT EXISTS no-op once they exist (whether
|
||||
/// created here or by a fresh-DB EnsureCreated).</summary>
|
||||
private static async Task EnsureSignupCodeTablesAsync(DorkNetDbContext db)
|
||||
{
|
||||
var pg = db.Database.IsNpgsql();
|
||||
var statements = pg
|
||||
? new[]
|
||||
{
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "SignupCodes" (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"Code" text NOT NULL,
|
||||
"Descriptor" text NOT NULL DEFAULT '',
|
||||
"CreatedByPlayerId" bigint NOT NULL DEFAULT 0,
|
||||
"CreatedAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"ExpiresAt" timestamp with time zone NULL,
|
||||
"RedeemedByPlayerId" bigint NULL,
|
||||
"RedeemedAt" timestamp with time zone NULL,
|
||||
"Revoked" boolean NOT NULL DEFAULT false
|
||||
);
|
||||
""",
|
||||
@"CREATE UNIQUE INDEX IF NOT EXISTS ""IX_SignupCodes_Code"" ON ""SignupCodes"" (""Code"");",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "PendingDevices" (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"DeviceId" text NOT NULL,
|
||||
"Platform" integer NOT NULL DEFAULT 0,
|
||||
"PlatformId" text NOT NULL DEFAULT '',
|
||||
"LastIp" text NULL,
|
||||
"FirstSeenAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"LastSeenAt" timestamp with time zone NOT NULL DEFAULT now()
|
||||
);
|
||||
""",
|
||||
@"CREATE UNIQUE INDEX IF NOT EXISTS ""IX_PendingDevices_DeviceId"" ON ""PendingDevices"" (""DeviceId"");",
|
||||
}
|
||||
: new[]
|
||||
{
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "SignupCodes" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_SignupCodes" PRIMARY KEY AUTOINCREMENT,
|
||||
"Code" TEXT NOT NULL,
|
||||
"Descriptor" TEXT NOT NULL DEFAULT '',
|
||||
"CreatedByPlayerId" INTEGER NOT NULL DEFAULT 0,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"ExpiresAt" TEXT NULL,
|
||||
"RedeemedByPlayerId" INTEGER NULL,
|
||||
"RedeemedAt" TEXT NULL,
|
||||
"Revoked" INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
""",
|
||||
@"CREATE UNIQUE INDEX IF NOT EXISTS ""IX_SignupCodes_Code"" ON ""SignupCodes"" (""Code"");",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "PendingDevices" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_PendingDevices" PRIMARY KEY AUTOINCREMENT,
|
||||
"DeviceId" TEXT NOT NULL,
|
||||
"Platform" INTEGER NOT NULL DEFAULT 0,
|
||||
"PlatformId" TEXT NOT NULL DEFAULT '',
|
||||
"LastIp" TEXT NULL,
|
||||
"FirstSeenAt" TEXT NOT NULL,
|
||||
"LastSeenAt" TEXT NOT NULL
|
||||
);
|
||||
""",
|
||||
@"CREATE UNIQUE INDEX IF NOT EXISTS ""IX_PendingDevices_DeviceId"" ON ""PendingDevices"" (""DeviceId"");",
|
||||
};
|
||||
|
||||
foreach (var sql in statements)
|
||||
await db.Database.ExecuteSqlRawAsync(sql);
|
||||
}
|
||||
|
||||
/// <summary>SQLite migration-history reconciliation. Handles two
|
||||
/// cutover scenarios so legacy DBs boot cleanly without manual SQL:
|
||||
/// <list type="number">
|
||||
|
||||
Reference in New Issue
Block a user