feat: backend service

This commit is contained in:
Ilia Denisov
2026-05-06 10:14:55 +03:00
committed by GitHub
parent 3e2622757e
commit f446c6a2ac
1486 changed files with 49720 additions and 266401 deletions
@@ -0,0 +1,131 @@
// Package basicauth gates a route group behind HTTP Basic authentication.
//
// The middleware delegates the credential check to a Verifier.
// Production wires `*admin.Service` (Postgres-backed, bcrypt cost 12).
// The bundled StaticVerifier is a test utility — it accepts any
// non-empty username together with a fixed password so the contract
// test can exercise the admin route group without booting a database.
// Production wiring never references StaticVerifier.
package basicauth
import (
"context"
"crypto/subtle"
"net/http"
"strings"
"galaxy/backend/internal/server/httperr"
"github.com/gin-gonic/gin"
)
// DefaultRealm is the realm advertised on `WWW-Authenticate` for the admin
// surface.
const DefaultRealm = "galaxy-admin"
// usernameContextKey is the unexported context key used to expose the
// authenticated admin username to downstream handlers (e.g. for
// soft-delete audit trails). The unexported value type prevents
// accidental collisions with keys defined in unrelated packages.
type usernameContextKey struct{}
// Verifier validates a username/password pair. Implementations must run in
// constant time relative to the credential bytes.
type Verifier interface {
// Verify reports whether the supplied credentials are accepted. A non-nil
// error indicates an unexpected verifier failure, distinct from a clean
// rejection (false, nil).
Verify(ctx context.Context, username, password string) (bool, error)
}
// UsernameFromContext returns the authenticated admin username stored on
// ctx by Middleware. The boolean reports whether a value was found.
func UsernameFromContext(ctx context.Context) (string, bool) {
if ctx == nil {
return "", false
}
value, ok := ctx.Value(usernameContextKey{}).(string)
if !ok {
return "", false
}
return value, true
}
// WithUsername stores username on ctx under the package-private context
// key. Exposed for tests that need to build a context outside the
// middleware.
func WithUsername(ctx context.Context, username string) context.Context {
return context.WithValue(ctx, usernameContextKey{}, username)
}
// Middleware returns a gin middleware that enforces Basic authentication via
// verifier. realm is advertised on `WWW-Authenticate`. A nil verifier behaves
// as a deny-all verifier, suitable for the operating mode where the admin
// surface must remain mounted but inaccessible.
func Middleware(verifier Verifier, realm string) gin.HandlerFunc {
if realm == "" {
realm = DefaultRealm
}
challenge := `Basic realm="` + realm + `"`
return func(c *gin.Context) {
username, password, ok := c.Request.BasicAuth()
if !ok {
c.Header("WWW-Authenticate", challenge)
httperr.Abort(c, http.StatusUnauthorized, httperr.CodeUnauthorized, "basic authentication is required")
return
}
if verifier == nil {
c.Header("WWW-Authenticate", challenge)
httperr.Abort(c, http.StatusUnauthorized, httperr.CodeUnauthorized, "credentials were rejected")
return
}
accepted, err := verifier.Verify(c.Request.Context(), username, password)
if err != nil {
httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "credential verification failed")
return
}
if !accepted {
c.Header("WWW-Authenticate", challenge)
httperr.Abort(c, http.StatusUnauthorized, httperr.CodeUnauthorized, "credentials were rejected")
return
}
c.Request = c.Request.WithContext(WithUsername(c.Request.Context(), username))
c.Next()
}
}
// StaticVerifier accepts any non-empty username together with a
// fixed shared password. It is a test-only utility: the OpenAPI
// contract test wires it to exercise the admin route group without
// booting a database. Production wiring uses the Postgres-backed
// `*backend/internal/admin.Service`.
type StaticVerifier struct {
// Password is the shared secret. An empty value disables the verifier
// (every request is rejected).
Password string
}
// NewStaticVerifier returns a StaticVerifier with the supplied password.
func NewStaticVerifier(password string) StaticVerifier {
return StaticVerifier{Password: password}
}
// Verify accepts any non-empty username together with the configured password.
// The password comparison runs in constant time. An empty configured password
// rejects every request.
func (v StaticVerifier) Verify(_ context.Context, username, password string) (bool, error) {
if strings.TrimSpace(username) == "" {
return false, nil
}
if v.Password == "" {
return false, nil
}
if subtle.ConstantTimeCompare([]byte(password), []byte(v.Password)) != 1 {
return false, nil
}
return true, nil
}