urit/internal/server/tokens.go
2026-05-02 21:11:50 -04:00

100 lines
2.3 KiB
Go

package server
import (
"crypto/rand"
"encoding/hex"
"sync"
"time"
)
// tokenTTL is how long a web access token remains valid.
const tokenTTL = 1 * time.Hour
// tokenInfo holds the identity associated with a web access token.
// When a telnet user generates a token, their security levels are
// captured so the HTTP server can enforce access control without
// needing to query the database on every request.
type tokenInfo struct {
UserID int64
UserName string
SecLibrary int
ExpiresAt time.Time
}
// TokenStore manages time-limited web access tokens.
//
// Tokens are generated by telnet users and used in the browser to
// authenticate file downloads. This is the bridge between the telnet
// BBS session and the HTTP file server — conceptually similar to how
// some BBSes generated one-time download passwords.
//
// Thread-safe: tokens can be created from telnet goroutines and
// validated from HTTP handler goroutines concurrently.
type TokenStore struct {
mu sync.Mutex
tokens map[string]*tokenInfo
}
// NewTokenStore creates an empty token store.
func NewTokenStore() *TokenStore {
return &TokenStore{
tokens: make(map[string]*tokenInfo),
}
}
// Generate creates a new token for the given user and returns it.
// The token is a 16-byte hex string (32 characters).
func (ts *TokenStore) Generate(userID int64, userName string, secLibrary int) (string, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
token := hex.EncodeToString(b)
ts.mu.Lock()
ts.tokens[token] = &tokenInfo{
UserID: userID,
UserName: userName,
SecLibrary: secLibrary,
ExpiresAt: time.Now().Add(tokenTTL),
}
ts.mu.Unlock()
// Opportunistic cleanup of expired tokens
go ts.cleanup()
return token, nil
}
// Validate checks a token and returns the associated info if valid.
// Returns nil if the token is missing, expired, or invalid.
func (ts *TokenStore) Validate(token string) *tokenInfo {
ts.mu.Lock()
defer ts.mu.Unlock()
info, ok := ts.tokens[token]
if !ok {
return nil
}
if time.Now().After(info.ExpiresAt) {
delete(ts.tokens, token)
return nil
}
return info
}
// cleanup removes all expired tokens.
func (ts *TokenStore) cleanup() {
ts.mu.Lock()
defer ts.mu.Unlock()
now := time.Now()
for token, info := range ts.tokens {
if now.After(info.ExpiresAt) {
delete(ts.tokens, token)
}
}
}