fix(auth): close bootstrap gate hole between sign-up and user mirror

Sign-ups through the Better Auth proxy write to auth_users, but the
bootstrap gate counted users — which only mirrors on the first
authenticated call. In that window a second public registration was
accepted on both the proxied sign-up and the native register endpoint.

countLocalUsers now unions users with auth_users (guarded by
to_regclass so backend-only installs still work), and handleRegister
uses it instead of its own inline count.

e2e-verified on a live stack: users=0/auth_users=1 already reports
mode=login and 403s both public paths; internal-token bypass works,
wrong token refused, authed POST /users creates users (201), and the
users mirror still populates on first authenticated call.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-17 16:22:26 +02:00
1 parent 572ff8dce3
commit e127c62b0f
2 files changed
+32 -5

No files matched your search

+20 -2
View File
@@ -264,8 +264,7 @@ func handleRegister(c *gin.Context) {
db := c.MustGet("db").(*database.DB)
jwtSecret := c.MustGet("jwt_secret").(string)
var count int
err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
count, err := countLocalUsers(db)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
@@ -432,6 +431,25 @@ func countLocalUsers(db *database.DB) (int, error) {
if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count); err != nil {
return 0, err
}
// Better Auth writes to auth_users; a row there only mirrors into users on
// the first authenticated call. Count it too or a second public sign-up
// slips through the bootstrap window. UNION by email avoids double-counting
// mirrored users.
var authTable *string
if err := db.QueryRow("SELECT to_regclass('auth_users')").Scan(&authTable); err != nil {
return 0, err
}
if authTable != nil && *authTable != "" {
if err := db.QueryRow(`
SELECT COUNT(*) FROM (
SELECT email FROM users
UNION
SELECT email FROM auth_users
) u
`).Scan(&count); err != nil {
return 0, err
}
}
return count, nil
}