package api import ( "crypto/rand" "encoding/hex" "sync" "time" ) const wsTicketTTL = 2 * time.Minute type wsTicketEntry struct { Username string ExpiresAt time.Time } var ( wsTicketMu sync.Mutex wsTickets = map[string]wsTicketEntry{} ) // issueWSTicket mints a one-time, short-lived dashboard WebSocket credential. func issueWSTicket(username string) string { b := make([]byte, 24) _, _ = rand.Read(b) ticket := hex.EncodeToString(b) wsTicketMu.Lock() wsTickets[ticket] = wsTicketEntry{Username: username, ExpiresAt: time.Now().Add(wsTicketTTL)} if len(wsTickets) > 512 { now := time.Now() for k, v := range wsTickets { if now.After(v.ExpiresAt) { delete(wsTickets, k) } } } wsTicketMu.Unlock() return ticket } // consumeWSTicket validates and invalidates a ticket (one-time use). func consumeWSTicket(ticket string) (string, bool) { wsTicketMu.Lock() defer wsTicketMu.Unlock() entry, ok := wsTickets[ticket] if !ok || time.Now().After(entry.ExpiresAt) { if ok { delete(wsTickets, ticket) } return "", false } delete(wsTickets, ticket) return entry.Username, true }