Skip to content

Commit a2b18f8

Browse files
committed
More database fixes.
1 parent 7ea9e22 commit a2b18f8

6 files changed

Lines changed: 83 additions & 10 deletions

File tree

go-relay/internal/database/db.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,73 @@ func backfillEmailVerified(ctx context.Context, sqlDB *sqlx.DB, dbType string) {
779779
_, _ = sqlDB.ExecContext(ctx, fmt.Sprintf(`INSERT INTO %s (name) VALUES ($1)`, smTable), migrationName)
780780
}
781781

782+
// backfillUserAPIKeyHashes generates a fresh master API key hash for any Users row
783+
// whose apiKeyHash is NULL. This happens for Sequelize-era accounts where the
784+
// plaintext apiKey column was dropped before its SHA-256 hash was stored.
785+
// Without apiKeyHash, WebSocket connection-token validation always fails because
786+
// the user lookup returns an empty identifier.
787+
//
788+
// The generated key is random — the user never knew it and doesn't need to. It
789+
// exists only so the relay can use apiKeyHash as a stable per-account identifier
790+
// in ClientManager and Redis. If the user later needs to use their master API key
791+
// directly, they can regenerate it from the dashboard (which shows it once).
792+
func backfillUserAPIKeyHashes(ctx context.Context, sqlDB *sqlx.DB, dbType string) {
793+
const migrationName = "backfill_user_api_key_hashes_v3_1"
794+
smTable := "SchemaMigrations"
795+
usersTable := "Users"
796+
if dbType != "sqlite" {
797+
smTable = `"SchemaMigrations"`
798+
usersTable = `"Users"`
799+
}
800+
801+
var count int
802+
_ = sqlDB.GetContext(ctx, &count, fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE name = $1`, smTable), migrationName)
803+
if count > 0 {
804+
return
805+
}
806+
807+
// Find all users with no apiKeyHash.
808+
rows, err := sqlDB.QueryContext(ctx, fmt.Sprintf(`SELECT id FROM %s WHERE "apiKeyHash" IS NULL OR "apiKeyHash" = ''`, usersTable))
809+
if err != nil {
810+
log.Warn().Err(err).Msg("backfillUserAPIKeyHashes: failed to query users")
811+
return
812+
}
813+
defer rows.Close()
814+
815+
var ids []int64
816+
for rows.Next() {
817+
var id int64
818+
if err := rows.Scan(&id); err == nil {
819+
ids = append(ids, id)
820+
}
821+
}
822+
_ = rows.Close()
823+
824+
for _, id := range ids {
825+
// GenerateAPIKey returns 64 random hex chars (32 bytes via crypto/rand).
826+
rawKey, err := model.GenerateAPIKey()
827+
if err != nil {
828+
log.Warn().Err(err).Int64("userId", id).Msg("backfillUserAPIKeyHashes: failed to generate key")
829+
continue
830+
}
831+
sum := sha256.Sum256([]byte(rawKey))
832+
hash := hex.EncodeToString(sum[:])
833+
if _, err := sqlDB.ExecContext(ctx,
834+
fmt.Sprintf(`UPDATE %s SET "apiKeyHash" = $1, "apiKeyRotationRequired" = TRUE WHERE id = $2`, usersTable),
835+
hash, id,
836+
); err != nil {
837+
log.Warn().Err(err).Int64("userId", id).Msg("backfillUserAPIKeyHashes: failed to update user")
838+
} else {
839+
log.Info().Int64("userId", id).Msg("backfillUserAPIKeyHashes: generated apiKeyHash for user")
840+
}
841+
}
842+
843+
if len(ids) > 0 {
844+
log.Info().Int("count", len(ids)).Msg("backfillUserAPIKeyHashes: populated missing apiKeyHash values")
845+
}
846+
_, _ = sqlDB.ExecContext(ctx, fmt.Sprintf(`INSERT INTO %s (name) VALUES ($1)`, smTable), migrationName)
847+
}
848+
782849
// addWorldIdUniqueConstraint is the v3.0 migration that:
783850
// 1. Deletes all KnownClients rows with NULL worldId (unknown-world orphans from v2.x)
784851
// 2. Creates a UNIQUE index on (userId, worldId) so each world can only have one row per user
@@ -1143,6 +1210,7 @@ func (db *DB) migratePostgres(ctx context.Context) error {
11431210
`ALTER TABLE "ConnectionTokens" ADD COLUMN IF NOT EXISTS "remoteRequestsPerHour" INTEGER DEFAULT 0`,
11441211
`ALTER TABLE "ConnectionTokens" ADD COLUMN IF NOT EXISTS source VARCHAR(20) DEFAULT 'dashboard'`,
11451212
`ALTER TABLE "ConnectionTokens" ADD COLUMN IF NOT EXISTS "clientId" VARCHAR(255) DEFAULT ''`,
1213+
`ALTER TABLE "ConnectionTokens" ADD COLUMN IF NOT EXISTS "lastUsedAt" TIMESTAMPTZ`,
11461214
// Pairing code can be bound to an existing clientId for "add browser" flows
11471215
`ALTER TABLE "PairingCodes" ADD COLUMN IF NOT EXISTS "clientId" VARCHAR(255)`,
11481216
`ALTER TABLE "PairingCodes" ADD COLUMN IF NOT EXISTS "allowedTargetClients" TEXT DEFAULT ''`,
@@ -1413,6 +1481,10 @@ func (db *DB) migratePostgres(ctx context.Context) error {
14131481
// since email verification was introduced after these accounts were created.
14141482
backfillEmailVerified(ctx, db.sqlDB, "postgres")
14151483

1484+
// v3.1: generate apiKeyHash for Sequelize-era accounts that had their
1485+
// plaintext apiKey column dropped before the hash was computed.
1486+
backfillUserAPIKeyHashes(ctx, db.sqlDB, "postgres")
1487+
14161488
return nil
14171489
}
14181490

go-relay/internal/handler/connection_tokens.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ func RegisterConnectionTokenRoutes(r chi.Router, db *database.DB, cfg *config.Co
404404
"relayUrl": relayURL,
405405
})
406406

407-
log.Info().Int64("userId", pairingCode.UserID).Str("clientId", clientID).Str("worldId", body.WorldID).Msg("Pairing completed")
407+
log.Info().Int64("userId", pairingCode.UserID).Str("clientId", clientID).Str("worldId", body.WorldID).Int64("tokenId", connToken.ID).Str("tokenHashPrefix", tokenHash[:8]+"…").Msg("Pairing completed")
408408
})
409409

410410
// GET /auth/remote-request-logs — View cross-world audit log (authenticated, master key only)

go-relay/internal/model/known_client.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ func (s *SQLKnownClientStore) Upsert(ctx context.Context, client *KnownClient) e
226226
return s.DB.QueryRowContext(ctx, query,
227227
client.UserID, client.ClientID, client.WorldID, client.WorldTitle,
228228
client.SystemID, client.SystemTitle, client.SystemVersion,
229-
client.FoundryVersion, client.CustomName, client.IsOnline,
229+
client.FoundryVersion, client.CustomName, bool(client.IsOnline),
230230
now, now,
231231
).Scan(&client.ID)
232232
}
@@ -321,13 +321,9 @@ func (s *SQLKnownClientStore) SetCredentialID(ctx context.Context, id int64, cre
321321

322322
func (s *SQLKnownClientStore) SetAutoStartOnRemoteRequest(ctx context.Context, id int64, enabled bool) error {
323323
now := NewSQLiteTime(time.Now())
324-
val := 0
325-
if enabled {
326-
val = 1
327-
}
328324
query := fmt.Sprintf(`UPDATE %s SET %s = $1, %s = $2 WHERE id = $3`,
329325
s.tableName(), s.col("auto_start_on_remote_request"), s.col("updated_at"))
330-
_, err := s.DB.ExecContext(ctx, query, val, now, id)
326+
_, err := s.DB.ExecContext(ctx, query, enabled, now, id)
331327
return err
332328
}
333329

go-relay/internal/model/remote_request_log.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (s *SQLRemoteRequestLogStore) Create(ctx context.Context, l *RemoteRequestL
7070
query += " RETURNING id"
7171
return s.DB.QueryRowContext(ctx, query,
7272
l.UserID, l.SourceClientID, l.SourceTokenID, l.TargetClientID,
73-
l.Action, l.Success, l.ErrorMessage, l.SourceIP, now,
73+
l.Action, bool(l.Success), l.ErrorMessage, l.SourceIP, now,
7474
).Scan(&l.ID)
7575
}
7676

go-relay/internal/server/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ func (s *Server) setupRouter() *chi.Mux {
729729
ValidateConnectionToken: service.MakeWSValidateConnectionToken(s.db),
730730
ValidateHeadless: func(clientID, token string) (bool, error) {
731731
if s.Headless == nil {
732-
return false, nil
732+
return true, nil
733733
}
734734
return s.Headless.ValidateHeadlessSession(clientID, token)
735735
},

go-relay/internal/service/api_key_validation.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,12 @@ func MakeWSValidateConnectionToken(db *database.DB) func(token string) (string,
108108

109109
ctx := context.Background()
110110
ct, err := db.ConnectionTokenStore().FindByTokenHash(ctx, tokenHash)
111-
if err != nil || ct == nil {
111+
if err != nil {
112+
log.Warn().Err(err).Str("hash", tokenHash[:8]+"…").Msg("Connection token lookup DB error")
113+
return "", "", "", 0, fmt.Errorf("invalid connection token")
114+
}
115+
if ct == nil {
116+
log.Warn().Str("hash", tokenHash[:8]+"…").Msg("Connection token not found in DB")
112117
return "", "", "", 0, fmt.Errorf("invalid connection token")
113118
}
114119

0 commit comments

Comments
 (0)