feat: add NFT usernames and bot verification (#22)

Implements collectible usernames, official verification workflows, and third-party bot verification after maintainer protocol and migration review.

The composite activity/moderation rating remains an admin-only read model; Telegram Stars Rating wire fields stay unset pending a dedicated official-semantics implementation.

Reviewed-Head: 2796345775ea0f908fb7734601e5e1dee4b653b9
Original-Head: fa082b892fd5180c9c9bc53c81c21cf5d250a75b

Co-authored-by: Egor Egorov <business.egor.sg@gmail.com>
This commit is contained in:
Egor Egorov 2026-07-27 20:18:00 +03:00 • committed by GitHub
parent b0fd3976f1
commit fff8de783a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
169 changed files with 55769 additions and 282 deletions

View file

@ -2,6 +2,7 @@ package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
@ -13,10 +14,29 @@ import (
const sessionCookieName = "telesrv_admin_session"
// csrfCookieName is the double-submit cookie. It is deliberately NOT HttpOnly:
// the panel's own JavaScript has to read it back to echo it in the X-CSRF-Token
// header, which is the whole mechanism.
const csrfCookieName = "telesrv_admin_csrf"
// csrfHeaderName is the header the panel echoes the cookie in.
const csrfHeaderName = "X-CSRF-Token"
type sessionClaims struct {
Actor string `json:"actor"`
Exp int64 `json:"exp"`
Nonce string `json:"nonce"`
// Permissions is the right set granted to this session, taken from
// TELESRV_ADMIN_UI_PERMISSIONS at login. It travels inside the signed cookie
// rather than being re-read per request, so a session keeps the rights it was
// issued with, and it cannot be edited by the browser: the HMAC covers it.
Permissions []string `json:"permissions,omitempty"`
// CSRF is the double-submit token bound to this session. Binding it into the
// signed claims is what makes the cookie/header pair unforgeable by a sibling
// origin that can only *write* cookies (a subdomain, say): such an attacker
// can set both the cookie and the header to a value they know, but they cannot
// produce a session cookie that agrees with it.
CSRF string `json:"csrf,omitempty"`
}
func signSession(key []byte, claims sessionClaims) (string, error) {
@ -56,6 +76,28 @@ func verifySession(key []byte, value string, now time.Time) (sessionClaims, bool
return claims, true
}
// newCSRFToken mints a fresh double-submit token.
func newCSRFToken() (string, error) {
var raw [32]byte
if _, err := rand.Read(raw[:]); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(raw[:]), nil
}
// setCSRFCookie publishes the token to the browser.
func setCSRFCookie(w http.ResponseWriter, token string, ttl time.Duration) {
http.SetCookie(w, &http.Cookie{
Name: csrfCookieName,
Value: token,
Path: "/",
MaxAge: int(ttl.Seconds()),
// Readable by the panel's script on purpose; see csrfCookieName.
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
})
}
func clearSessionCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
@ -65,4 +107,12 @@ func clearSessionCookie(w http.ResponseWriter) {
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
http.SetCookie(w, &http.Cookie{
Name: csrfCookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
})
}