feat: OTP rate limit, private-room invite UX, in-game UI fixes
Auth / security
- Rate-limit real SMS OTP sends (dev mode unlimited): 60s resend cooldown,
5 per phone/hour, 300/hour global backstop. OtpService.CheckAndRecordRate;
POST /api/auth/otp/request returns 429 {error,retryAfter}; AuthScreen shows
auth.rateLimited. Knobs in appsettings Sms (Sms__* env).
Private rooms (invite)
- Cancel-invite button on pending seats; friend picker shows presence
(online/offline/in-game, sorted online-first) and flags in-game players.
- Mock invite stays pending ~3.5s and a cancel truly stops the auto-accept
(was a bug that re-seated cancelled invites).
In-game UI
- Scoreboard is compact + shrink-safe (no overflow on narrow screens).
- Played trick cards land dead-center (were ~2px off the corner anchor).
Plus the in-flight typing-indicator work (GameHub, ChatScreen).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,7 @@ export class SignalrService implements OnlineService {
|
||||
private rewardCbs = new Set<(r: RewardResult) => void>();
|
||||
private friendCbs = new Set<(f: Friend[]) => void>();
|
||||
private chatCbs = new Set<(id: string, m: ChatMessage[]) => void>();
|
||||
private typingCbs = new Set<(fromId: string) => void>();
|
||||
private forfeitCbs = new Set<(r: ForfeitRequest | null) => void>();
|
||||
private cachedProfile: UserProfile | null = null;
|
||||
|
||||
@@ -120,6 +121,8 @@ export class SignalrService implements OnlineService {
|
||||
conn.on("state", (s: ServerGameState) => this.stateCbs.forEach((cb) => cb(s)));
|
||||
conn.on("reaction", (r: { seat: number; reaction: string }) =>
|
||||
this.reactionCbs.forEach((cb) => cb(r.seat, r.reaction)));
|
||||
conn.on("typing", (m: { from: string }) =>
|
||||
this.typingCbs.forEach((cb) => cb(m.from)));
|
||||
conn.on("notification", (n: AppNotification) =>
|
||||
this.notifCbs.forEach((cb) => cb(n)));
|
||||
conn.on("profile", (p: UserProfile) =>
|
||||
@@ -404,6 +407,8 @@ export class SignalrService implements OnlineService {
|
||||
}
|
||||
async markRead() { /* server marks read when messages are fetched */ }
|
||||
onChat(cb: (id: string, m: ChatMessage[]) => void) { this.chatCbs.add(cb); return () => this.chatCbs.delete(cb); }
|
||||
sendTyping(id: string) { this.conn?.invoke("Typing", id).catch(() => {}); }
|
||||
onTyping(cb: (fromId: string) => void) { this.typingCbs.add(cb); return () => this.typingCbs.delete(cb); }
|
||||
|
||||
onNotification(cb: (n: AppNotification) => void): Unsubscribe {
|
||||
// Real notifications only — server hub "notification" events + app-generated
|
||||
|
||||
Reference in New Issue
Block a user