mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
Enhance signup process: increase item fetch limit, update Join API to use password, and improve UI for account creation
This commit is contained in:
@@ -2582,7 +2582,13 @@ public class AdminController(
|
|||||||
[FromQuery] string? storefront, [FromQuery] string? category,
|
[FromQuery] string? storefront, [FromQuery] string? category,
|
||||||
[FromQuery] int take = 200, [FromQuery] int skip = 0)
|
[FromQuery] int take = 200, [FromQuery] int skip = 0)
|
||||||
{
|
{
|
||||||
take = Math.Clamp(take, 1, 500);
|
// Ceiling raised from 500: the wardrobe + color-variant seed pushes
|
||||||
|
// the live catalog past 800 rows, all under storefront "main". A
|
||||||
|
// 500-row window (ordered Storefront→Category→Price) silently hid
|
||||||
|
// every item that sorted past it — e.g. torso SKUs like the Sea
|
||||||
|
// Captain Contest shirt — so the admin UI couldn't find them even
|
||||||
|
// by search (it filters the fetched page client-side).
|
||||||
|
take = Math.Clamp(take, 1, 5000);
|
||||||
skip = Math.Max(0, skip);
|
skip = Math.Max(0, skip);
|
||||||
IQueryable<StoreItemEntity> q = db.StoreItems;
|
IQueryable<StoreItemEntity> q = db.StoreItems;
|
||||||
if (!string.IsNullOrWhiteSpace(storefront)) q = q.Where(i => i.Storefront == storefront);
|
if (!string.IsNullOrWhiteSpace(storefront)) q = q.Where(i => i.Storefront == storefront);
|
||||||
|
|||||||
@@ -267,16 +267,15 @@ public class PublicSiteController(
|
|||||||
}).ToList());
|
}).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record JoinRedeemRequest(string? Code, string? Username, string? DeviceId);
|
public sealed record JoinRedeemRequest(string? Code, string? Username, string? Password);
|
||||||
|
|
||||||
/// <summary>POST /api/site/v1/join/redeem — redeem a signup code,
|
/// <summary>POST /api/site/v1/join/redeem — redeem a signup code,
|
||||||
/// minting an account bound to the supplied device id. On success the
|
/// minting a username/password account. Returns {ok, error?,
|
||||||
/// player relaunches the game and the watch's device-id login finds
|
/// username?}.</summary>
|
||||||
/// the new account. Returns {ok, error?, username?}.</summary>
|
|
||||||
[HttpPost("join/redeem")]
|
[HttpPost("join/redeem")]
|
||||||
public async Task<IActionResult> JoinRedeem([FromBody] JoinRedeemRequest body)
|
public async Task<IActionResult> JoinRedeem([FromBody] JoinRedeemRequest body)
|
||||||
{
|
{
|
||||||
var result = await signupCodes.RedeemAsync(body.Code, body.Username, body.DeviceId);
|
var result = await signupCodes.RedeemAsync(body.Code, body.Username, body.Password);
|
||||||
if (!result.Ok)
|
if (!result.Ok)
|
||||||
return BadRequest(new { ok = false, error = result.Error });
|
return BadRequest(new { ok = false, error = result.Error });
|
||||||
return Ok(new { ok = true, username = result.Username, playerId = result.PlayerId });
|
return Ok(new { ok = true, username = result.Username, playerId = result.PlayerId });
|
||||||
|
|||||||
@@ -1773,6 +1773,105 @@
|
|||||||
"item_outfit_type": 0,
|
"item_outfit_type": 0,
|
||||||
"mask_guid": "a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9"
|
"mask_guid": "a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"swatch_name": "Hat_SeaCaptainDarkDarkGold_Swatch",
|
||||||
|
"swatch_guid": "70112887-dc8d-49d5-96ed-0845204e0b58",
|
||||||
|
"color": "DarkDarkGold",
|
||||||
|
"item_name": "(Hat_SeaCaptain)",
|
||||||
|
"item_guid": "f5c9743f-e77c-42f2-ba63-9843342cbf8e",
|
||||||
|
"item_outfit_type": 0,
|
||||||
|
"mask_guid": "a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"swatch_name": "Hat_SeaCaptainDarkGold_Swatch",
|
||||||
|
"swatch_guid": "f5403c89-9769-479b-8a05-c71607f9b189",
|
||||||
|
"color": "DarkGold",
|
||||||
|
"item_name": "(Hat_SeaCaptain)",
|
||||||
|
"item_guid": "f5c9743f-e77c-42f2-ba63-9843342cbf8e",
|
||||||
|
"item_outfit_type": 0,
|
||||||
|
"mask_guid": "a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"swatch_name": "Hat_SeaCaptainDarkSilver_Swatch",
|
||||||
|
"swatch_guid": "36e63797-5a42-4779-a5cc-ef5c2e7d17d4",
|
||||||
|
"color": "DarkSilver",
|
||||||
|
"item_name": "(Hat_SeaCaptain)",
|
||||||
|
"item_guid": "f5c9743f-e77c-42f2-ba63-9843342cbf8e",
|
||||||
|
"item_outfit_type": 0,
|
||||||
|
"mask_guid": "a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"swatch_name": "Hat_SeaCaptainFushia_Swatch",
|
||||||
|
"swatch_guid": "2738ab5f-8c94-46b3-bac6-4a8111c41c21",
|
||||||
|
"color": "Fushia",
|
||||||
|
"item_name": "(Hat_SeaCaptain)",
|
||||||
|
"item_guid": "f5c9743f-e77c-42f2-ba63-9843342cbf8e",
|
||||||
|
"item_outfit_type": 0,
|
||||||
|
"mask_guid": "a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"swatch_name": "Torso_SeaCaptainBlue_Swatch",
|
||||||
|
"swatch_guid": "27af51bc-8226-4911-8d47-22d28ea8e378",
|
||||||
|
"color": "Blue",
|
||||||
|
"item_name": "(SeaCaptainTorso)",
|
||||||
|
"item_guid": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318",
|
||||||
|
"item_outfit_type": 101,
|
||||||
|
"mask_guid": "8ec5de40-73fc-4f09-9bf3-ddf4701107bc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"swatch_name": "Torso_SeaCaptainBlueDark_Swatch",
|
||||||
|
"swatch_guid": "7c1aaa7e-503b-46ef-b1c5-0c61a75b916e",
|
||||||
|
"color": "BlueDark",
|
||||||
|
"item_name": "(SeaCaptainTorso)",
|
||||||
|
"item_guid": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318",
|
||||||
|
"item_outfit_type": 101,
|
||||||
|
"mask_guid": "8ec5de40-73fc-4f09-9bf3-ddf4701107bc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"swatch_name": "Torso_SeaCaptainBlueDark2_Swatch",
|
||||||
|
"swatch_guid": "3df5d13b-ba77-4b51-90b2-7a946e4d0be7",
|
||||||
|
"color": "BlueDark2",
|
||||||
|
"item_name": "(SeaCaptainTorso)",
|
||||||
|
"item_guid": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318",
|
||||||
|
"item_outfit_type": 101,
|
||||||
|
"mask_guid": "8ec5de40-73fc-4f09-9bf3-ddf4701107bc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"swatch_name": "Torso_SeaCaptainBlueWhite_Swatch",
|
||||||
|
"swatch_guid": "857fdcd8-704f-43d0-92b9-bca26619b153",
|
||||||
|
"color": "BlueWhite",
|
||||||
|
"item_name": "(SeaCaptainTorso)",
|
||||||
|
"item_guid": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318",
|
||||||
|
"item_outfit_type": 101,
|
||||||
|
"mask_guid": "8ec5de40-73fc-4f09-9bf3-ddf4701107bc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"swatch_name": "Torso_SeaCaptainBlackDark_Swatch",
|
||||||
|
"swatch_guid": "6145292a-9002-41c6-8cd5-43679b6f542f",
|
||||||
|
"color": "BlackDark",
|
||||||
|
"item_name": "(SeaCaptainTorso)",
|
||||||
|
"item_guid": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318",
|
||||||
|
"item_outfit_type": 101,
|
||||||
|
"mask_guid": "8ec5de40-73fc-4f09-9bf3-ddf4701107bc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"swatch_name": "Torso_SeaCaptainDark_Swatch",
|
||||||
|
"swatch_guid": "b9f51861-ad10-4878-a9b1-d849bccf532a",
|
||||||
|
"color": "Dark",
|
||||||
|
"item_name": "(SeaCaptainTorso)",
|
||||||
|
"item_guid": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318",
|
||||||
|
"item_outfit_type": 101,
|
||||||
|
"mask_guid": "8ec5de40-73fc-4f09-9bf3-ddf4701107bc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"swatch_name": "Torso_SeaCaptainWhiteDark_Swatch",
|
||||||
|
"swatch_guid": "b5d62931-e756-4a8a-8f3b-760beba49319",
|
||||||
|
"color": "WhiteDark",
|
||||||
|
"item_name": "(SeaCaptainTorso)",
|
||||||
|
"item_guid": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318",
|
||||||
|
"item_outfit_type": 101,
|
||||||
|
"mask_guid": "8ec5de40-73fc-4f09-9bf3-ddf4701107bc"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"swatch_name": "LaserTagGlove_Orange_Swatch",
|
"swatch_name": "LaserTagGlove_Orange_Swatch",
|
||||||
"swatch_guid": "EC0ttvk6eU2HEMacLAx2Iw",
|
"swatch_guid": "EC0ttvk6eU2HEMacLAx2Iw",
|
||||||
|
|||||||
@@ -10,10 +10,8 @@ namespace DorkNet.Server.Services;
|
|||||||
/// Backs the admin-issued signup-code flow. While the server has signups
|
/// Backs the admin-issued signup-code flow. While the server has signups
|
||||||
/// disabled, the only path to a new account is redeeming one of these on
|
/// disabled, the only path to a new account is redeeming one of these on
|
||||||
/// the public <c>/join</c> page. Codes are single-use, carry an admin
|
/// the public <c>/join</c> page. Codes are single-use, carry an admin
|
||||||
/// descriptor, and can have an optional expiry. Redemption mints an
|
/// descriptor, and can have an optional expiry. Redemption mints a normal
|
||||||
/// account bound to the device id the player's game client reported (see
|
/// username/password account.
|
||||||
/// <see cref="PendingDeviceEntity"/>), so the next launch logs straight
|
|
||||||
/// in via the watch's device-id login.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SignupCodeService(DorkNetDbContext db, PlayerService players)
|
public class SignupCodeService(DorkNetDbContext db, PlayerService players)
|
||||||
{
|
{
|
||||||
@@ -61,10 +59,10 @@ public class SignupCodeService(DorkNetDbContext db, PlayerService players)
|
|||||||
|
|
||||||
public sealed record RedeemResult(bool Ok, string? Error, long? PlayerId, string? Username);
|
public sealed record RedeemResult(bool Ok, string? Error, long? PlayerId, string? Username);
|
||||||
|
|
||||||
/// <summary>Redeem a code: validate it, then mint an account bound to
|
/// <summary>Redeem a code: validate it, then mint a password account
|
||||||
/// <paramref name="deviceId"/> with the requested username. The code
|
/// with the requested username. The code is consumed atomically with
|
||||||
/// is consumed atomically with account creation.</summary>
|
/// account creation.</summary>
|
||||||
public async Task<RedeemResult> RedeemAsync(string? rawCode, string? rawUsername, string? deviceId)
|
public async Task<RedeemResult> RedeemAsync(string? rawCode, string? rawUsername, string? rawPassword)
|
||||||
{
|
{
|
||||||
var code = NormalizeCode(rawCode);
|
var code = NormalizeCode(rawCode);
|
||||||
if (string.IsNullOrEmpty(code)) return new(false, "missing_code", null, null);
|
if (string.IsNullOrEmpty(code)) return new(false, "missing_code", null, null);
|
||||||
@@ -72,7 +70,9 @@ public class SignupCodeService(DorkNetDbContext db, PlayerService players)
|
|||||||
var username = (rawUsername ?? string.Empty).Trim();
|
var username = (rawUsername ?? string.Empty).Trim();
|
||||||
if (!IsValidUsername(username)) return new(false, "invalid_username", null, null);
|
if (!IsValidUsername(username)) return new(false, "invalid_username", null, null);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(deviceId)) return new(false, "missing_device", null, null);
|
var password = rawPassword ?? string.Empty;
|
||||||
|
if (string.IsNullOrEmpty(password)) return new(false, "missing_password", null, null);
|
||||||
|
if (password.Length < 8) return new(false, "password_too_short", null, null);
|
||||||
|
|
||||||
var entity = await db.SignupCodes.FirstOrDefaultAsync(c => c.Code == code);
|
var entity = await db.SignupCodes.FirstOrDefaultAsync(c => c.Code == code);
|
||||||
if (entity is null) return new(false, "invalid_code", null, null);
|
if (entity is null) return new(false, "invalid_code", null, null);
|
||||||
@@ -83,31 +83,17 @@ public class SignupCodeService(DorkNetDbContext db, PlayerService players)
|
|||||||
if (await db.Players.AnyAsync(p => p.Username == username))
|
if (await db.Players.AnyAsync(p => p.Username == username))
|
||||||
return new(false, "username_taken", null, null);
|
return new(false, "username_taken", null, null);
|
||||||
|
|
||||||
// The device must not already own an account (otherwise the
|
var player = await players.CreateNewAccountAsync(
|
||||||
// player would launch into the existing one, not this fresh
|
deviceId: $"join-{Guid.NewGuid():N}",
|
||||||
// account). Guides them to a different device / to just log in.
|
|
||||||
if (await players.GetByDeviceAsync(deviceId) is not null)
|
|
||||||
return new(false, "device_in_use", null, null);
|
|
||||||
|
|
||||||
var player = await players.GetOrCreateByDeviceAsync(
|
|
||||||
deviceId: deviceId,
|
|
||||||
platform: 0,
|
platform: 0,
|
||||||
platformId: null,
|
platformId: null,
|
||||||
displayName: username);
|
displayName: username);
|
||||||
|
player.PasswordHash = BCrypt.Net.BCrypt.HashPassword(password, workFactor: 11);
|
||||||
|
|
||||||
entity.RedeemedByPlayerId = player.Id;
|
entity.RedeemedByPlayerId = player.Id;
|
||||||
entity.RedeemedAt = DateTime.UtcNow;
|
entity.RedeemedAt = DateTime.UtcNow;
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// The device is now bound to a real account — drop any pending
|
|
||||||
// refusal row so it stops showing on the /join picker.
|
|
||||||
var pending = await db.PendingDevices.FirstOrDefaultAsync(d => d.DeviceId == deviceId);
|
|
||||||
if (pending is not null)
|
|
||||||
{
|
|
||||||
db.PendingDevices.Remove(pending);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new(true, null, player.Id, player.Username);
|
return new(true, null, player.Id, player.Username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,12 +69,14 @@ export function Store() {
|
|||||||
const [bulkBusy, setBulkBusy] = useState(false);
|
const [bulkBusy, setBulkBusy] = useState(false);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
// Server-side pagination is overkill for this dataset (low hundreds
|
// Pull the whole catalog and filter / sort in the browser so the
|
||||||
// of rows typically); pull a generous slice and filter / sort in the
|
// storefront/category dropdowns don't trigger network round-trips on
|
||||||
// browser so the storefront/category dropdowns don't trigger network
|
// every change. The wardrobe + color-variant seed makes this ~800-1000
|
||||||
// round-trips on every change.
|
// rows, so the slice must clear that — a 500 cap previously hid every
|
||||||
|
// item that sorted past row 500 (e.g. torso SKUs like the Sea Captain
|
||||||
|
// Contest shirt).
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
qs.set('take', '500');
|
qs.set('take', '5000');
|
||||||
const { data, loading, error, refresh } = useApi<StoreItem[]>(`/storeitems?${qs}`);
|
const { data, loading, error, refresh } = useApi<StoreItem[]>(`/storeitems?${qs}`);
|
||||||
const { data: storefrontData } = useApi<StorefrontDefinition[]>('/storefronts');
|
const { data: storefrontData } = useApi<StorefrontDefinition[]>('/storefronts');
|
||||||
const storefronts = storefrontData ?? FALLBACK_STOREFRONTS;
|
const storefronts = storefrontData ?? FALLBACK_STOREFRONTS;
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { get, post } from '../lib/api';
|
import { post } from '../lib/api';
|
||||||
|
|
||||||
interface PendingDevice {
|
|
||||||
deviceId: string;
|
|
||||||
platform: number;
|
|
||||||
lastSeenAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server error code -> friendly copy.
|
// Server error code -> friendly copy.
|
||||||
const ERRORS: Record<string, string> = {
|
const ERRORS: Record<string, string> = {
|
||||||
@@ -16,40 +10,34 @@ const ERRORS: Record<string, string> = {
|
|||||||
code_expired: 'That code has expired. Ask for a new one.',
|
code_expired: 'That code has expired. Ask for a new one.',
|
||||||
invalid_username: 'Pick a username 2–24 characters long (letters, numbers, _ or -).',
|
invalid_username: 'Pick a username 2–24 characters long (letters, numbers, _ or -).',
|
||||||
username_taken: 'That username is taken — try another.',
|
username_taken: 'That username is taken — try another.',
|
||||||
missing_device: 'Pick or paste the device id from your game client.',
|
missing_password: 'Enter a password.',
|
||||||
device_in_use: 'That device already has an account — just launch the game to log in.',
|
password_too_short: 'Use at least 8 characters for your password.',
|
||||||
|
password_mismatch: 'The passwords do not match.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Join() {
|
export function Join() {
|
||||||
const [code, setCode] = useState('');
|
const [code, setCode] = useState('');
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [deviceId, setDeviceId] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [pending, setPending] = useState<PendingDevice[]>([]);
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [err, setErr] = useState<string | null>(null);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
const [doneUsername, setDoneUsername] = useState<string | null>(null);
|
const [doneUsername, setDoneUsername] = useState<string | null>(null);
|
||||||
|
|
||||||
// Pull the devices our caller's IP was recently refused from, so the
|
|
||||||
// player can pick the one their own game client reported instead of
|
|
||||||
// hunting for the Unity device id by hand.
|
|
||||||
useEffect(() => {
|
|
||||||
get<PendingDevice[]>('/join/pending-devices')
|
|
||||||
.then((rows) => {
|
|
||||||
setPending(rows);
|
|
||||||
if (rows.length > 0) setDeviceId((cur) => cur || rows[0].deviceId);
|
|
||||||
})
|
|
||||||
.catch(() => { /* picker is a convenience; manual paste still works */ });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const submit = async (e: React.FormEvent) => {
|
const submit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setErr(null);
|
setErr(null);
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setErr(ERRORS.password_mismatch);
|
||||||
|
setBusy(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const res = await post<{ ok: boolean; username: string }>('/join/redeem', {
|
const res = await post<{ ok: boolean; username: string }>('/join/redeem', {
|
||||||
Code: code.trim(),
|
Code: code.trim(),
|
||||||
Username: username.trim(),
|
Username: username.trim(),
|
||||||
DeviceId: deviceId.trim(),
|
Password: password,
|
||||||
});
|
});
|
||||||
setDoneUsername(res.username);
|
setDoneUsername(res.username);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -66,8 +54,8 @@ export function Join() {
|
|||||||
<div className="card !p-6 text-center">
|
<div className="card !p-6 text-center">
|
||||||
<h1 className="text-2xl font-semibold text-ink-50">You're in, {doneUsername}! 🎉</h1>
|
<h1 className="text-2xl font-semibold text-ink-50">You're in, {doneUsername}! 🎉</h1>
|
||||||
<p className="mt-3 text-sm text-ink-300">
|
<p className="mt-3 text-sm text-ink-300">
|
||||||
Your account is ready. Launch the game through the DorkNet launcher on this
|
Your account is ready. Launch the game through the DorkNet launcher and sign in
|
||||||
same device and you'll be logged straight in — no signup step needed.
|
with your username and password.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +67,7 @@ export function Join() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-semibold text-ink-50">Join the server</h1>
|
<h1 className="text-2xl font-semibold text-ink-50">Join the server</h1>
|
||||||
<p className="text-sm text-ink-400">
|
<p className="text-sm text-ink-400">
|
||||||
Got a signup code? Redeem it here to create your account, then launch the game.
|
Got a signup code? Redeem it here to create your account, then sign in from the game.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -102,52 +90,33 @@ export function Join() {
|
|||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
placeholder="2–24 chars: letters, numbers, _ or -"
|
placeholder="2–24 chars: letters, numbers, _ or -"
|
||||||
|
autoComplete="username"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="block">
|
<label className="block">
|
||||||
<span className="label">Your device</span>
|
<span className="label">Password</span>
|
||||||
{pending.length > 0 ? (
|
<input
|
||||||
<>
|
|
||||||
<select
|
|
||||||
className="input mt-1"
|
className="input mt-1"
|
||||||
value={pending.some((d) => d.deviceId === deviceId) ? deviceId : '__manual__'}
|
type="password"
|
||||||
onChange={(e) => setDeviceId(e.target.value === '__manual__' ? '' : e.target.value)}
|
value={password}
|
||||||
>
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
{pending.map((d) => (
|
placeholder="at least 8 characters"
|
||||||
<option key={d.deviceId} value={d.deviceId}>
|
autoComplete="new-password"
|
||||||
{d.deviceId.slice(0, 16)}… · last seen {new Date(d.lastSeenAt).toLocaleTimeString()}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
<option value="__manual__">Enter device id manually…</option>
|
|
||||||
</select>
|
|
||||||
{!pending.some((d) => d.deviceId === deviceId) && (
|
|
||||||
<input
|
|
||||||
className="input mt-2 font-mono text-xs"
|
|
||||||
value={deviceId}
|
|
||||||
onChange={(e) => setDeviceId(e.target.value)}
|
|
||||||
placeholder="paste your device id"
|
|
||||||
/>
|
/>
|
||||||
)}
|
</label>
|
||||||
<p className="mt-1 text-[11px] text-ink-500">
|
|
||||||
These are devices that just tried to sign in from your network. Pick yours.
|
<label className="block">
|
||||||
</p>
|
<span className="label">Confirm password</span>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<input
|
<input
|
||||||
className="input mt-1 font-mono text-xs"
|
className="input mt-1"
|
||||||
value={deviceId}
|
type="password"
|
||||||
onChange={(e) => setDeviceId(e.target.value)}
|
value={confirmPassword}
|
||||||
placeholder="paste your device id"
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder="type it again"
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-[11px] text-ink-500">
|
</label>
|
||||||
Launch the game once first — when it says signups are disabled, come back here
|
|
||||||
and your device should appear automatically.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{err && (
|
{err && (
|
||||||
<div className="rounded-lg border border-danger/30 bg-danger/5 px-3 py-2 text-sm text-danger">{err}</div>
|
<div className="rounded-lg border border-danger/30 bg-danger/5 px-3 py-2 text-sm text-danger">{err}</div>
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ export default defineConfig({
|
|||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
target: 'es2022',
|
target: 'es2022',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: 'assets/[name]-join-password-[hash].js',
|
||||||
|
chunkFileNames: 'assets/[name]-join-password-[hash].js',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5174,
|
port: 5174,
|
||||||
|
|||||||
Reference in New Issue
Block a user