From 56a4bbcf4cf63115d6dec233f7ae607750dcda20 Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sun, 27 Sep 2026 14:20:20 +0800 Subject: [PATCH] =?UTF-8?q?refactor(api):=20=E5=88=A0=E6=8E=89=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E8=B0=83=E7=94=A8=E6=96=B9=E7=9A=84=20QuotaAvailable?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E6=AD=A3=20passkey=20last=5Fused=5Fat=20?= =?UTF-8?q?=E7=9A=84=E8=BF=87=E6=97=B6=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/sequence-diagrams.md | 3 ++- internal/api/api_test.go | 11 +++++------ internal/api/pgrepo.go | 26 -------------------------- internal/api/repo.go | 8 ++------ internal/pgint/pgint_test.go | 6 ------ 5 files changed, 9 insertions(+), 45 deletions(-) diff --git a/docs/sequence-diagrams.md b/docs/sequence-diagrams.md index 2d3bafb..82c3dec 100644 --- a/docs/sequence-diagrams.md +++ b/docs/sequence-diagrams.md @@ -80,7 +80,8 @@ sequenceDiagram API-->>Panel: 412 not_linked else linked Repo-->>API: true - API->>Repo: QuotaAvailable(user_id) + API->>Repo: QuotaCheck(user_id, the server's real size) + Note over API,Repo: all four caps: servers, CPU, memory, storage alt quota exhausted Repo-->>API: false API-->>Panel: 403 quota_exceeded diff --git a/internal/api/api_test.go b/internal/api/api_test.go index 3636326..8d8ba5e 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -335,15 +335,14 @@ func (f *fakeRepo) ServerByName(_ context.Context, n string) (*ServerRecord, err } return nil, ErrNotFound } -func (f *fakeRepo) IsLinked(_ context.Context, u string) (bool, error) { return f.linked[u], nil } -func (f *fakeRepo) QuotaAvailable(_ context.Context, u string) (bool, error) { return f.quota[u], nil } +func (f *fakeRepo) IsLinked(_ context.Context, u string) (bool, error) { return f.linked[u], nil } func (f *fakeRepo) QuotaCheck(_ context.Context, userID string, _ string, incoming ResourceSpec) (bool, error) { f.quotaChecked = append(f.quotaChecked, incoming) - // For hermetic tests, QuotaCheck delegates to the same QuotaAvailable - // store — tests that care about per-dimension checks should use - // fakeQuotas with direct inspection. - return f.QuotaAvailable(context.TODO(), userID) + // For hermetic tests, QuotaCheck answers from the per-user quota flag — + // tests that care about per-dimension checks should use fakeQuotas with + // direct inspection. + return f.quota[userID], nil } func (f *fakeRepo) UpdateServerResources(ctx context.Context, name string, cpu, mem, stor int) error { diff --git a/internal/api/pgrepo.go b/internal/api/pgrepo.go index 205fe34..63e18dc 100644 --- a/internal/api/pgrepo.go +++ b/internal/api/pgrepo.go @@ -346,32 +346,6 @@ func (p *PGRepo) CompleteOwnerSetup(ctx context.Context, newUserID, code string, return userID, mcUUID, authSource, nil } -// QuotaAvailable treats a missing quota row or a NULL max_servers as unlimited; -// otherwise it compares the live owned-server count against the cap (spec §9.3). -// It is the single-dimension convenience read; handlers use the four-dimension -// QuotaCheck. The former audit-#4 TOCTOU (check and claim in separate statements) -// is closed inside ClaimServer, which re-runs the four-dimension gate under a -// per-user advisory lock in the SAME transaction as the ownership write. -func (p *PGRepo) QuotaAvailable(ctx context.Context, userID string) (bool, error) { - var maxServers sql.NullInt64 - switch err := p.db.QueryRowContext(ctx, - `SELECT max_servers FROM quotas WHERE user_id = $1`, userID).Scan(&maxServers); { - case errors.Is(err, sql.ErrNoRows): - return true, nil - case err != nil: - return false, err - } - if !maxServers.Valid { - return true, nil - } - var n int64 - if err := p.db.QueryRowContext(ctx, - `SELECT count(*) FROM servers WHERE owner_id = $1 AND deleted_at IS NULL`, userID).Scan(&n); err != nil { - return false, err - } - return n < maxServers.Int64, nil -} - // QuotaCheck reports whether accepting a server with resource spec `incoming` // would push userID over any quota cap. excludeName is the server row whose own // cached resources should be excluded ("" for a fresh claim where the row diff --git a/internal/api/repo.go b/internal/api/repo.go index ec6c92a..1e0c35c 100644 --- a/internal/api/repo.go +++ b/internal/api/repo.go @@ -166,9 +166,8 @@ type StaffUser struct { // be public (unlike a session token), so it is safe at rest. CredentialID is the // authenticator's globally-unique handle (base64url) and PublicKey the COSE key // (base64); SignCount is the uint32 signature counter captured at registration. -// LastUsedAt is nil until an assertion stamps it. The passkey login door now exists -// (Public /auth/passkey/login/{begin,finish}, #72), but no path yet writes -// last_used_at, so in practice it stays nil; wiring the stamp is a follow-up there. +// LastUsedAt is nil until a passkey sign-in or step-up stamps it +// (AdvanceCredentialSignCount, with the advanced counter). type PasskeyCredential struct { ID string UserID string @@ -326,9 +325,6 @@ type Repo interface { // the API clock so expiry is testable. It returns the effective userID plus the // bound mc_uuid and authSource (for the response + audit). RedeemPlayerBindCode(ctx context.Context, newUserID, code string, now time.Time) (userID, mcUUID, authSource string, err error) - // QuotaAvailable reports whether the user is under their max_servers quota - // (spec §9.3 step ②, evaluated before provisioning). - QuotaAvailable(ctx context.Context, userID string) (bool, error) // QuotaCheck reports whether claiming a server with the given resource spec // would push the user over any of their four quota caps: max_servers, // max_cpu_milli, max_memory_mb, and max_storage_gb (spec §9.3 / §22). A nil diff --git a/internal/pgint/pgint_test.go b/internal/pgint/pgint_test.go index 87725ba..a7347d6 100644 --- a/internal/pgint/pgint_test.go +++ b/internal/pgint/pgint_test.go @@ -605,9 +605,6 @@ func TestSetQuotasReplacesEveryCap(t *testing.T) { set(api.QuotaInput{MaxServers: n(0), MaxCPUMilli: n(2000), MaxMemoryMB: n(4096), MaxStorageGB: n(20)}, "0/2000/4096/20") gate(false) - if ok, err := repo.QuotaAvailable(ctx, u.ID); err != nil || ok { - t.Fatalf("QuotaAvailable at max_servers 0 = %v, %v; want false", ok, err) - } if _, err := repo.ClaimServer(ctx, name, u.ID); !errors.Is(err, api.ErrQuotaExceeded) { t.Fatalf("claim at max_servers 0 = %v, want ErrQuotaExceeded", err) } @@ -618,9 +615,6 @@ func TestSetQuotasReplacesEveryCap(t *testing.T) { gate(false) set(api.QuotaInput{}, "-/-/-/-") gate(true) - if ok, err := repo.QuotaAvailable(ctx, u.ID); err != nil || !ok { - t.Fatalf("QuotaAvailable with every cap lifted = %v, %v; want true", ok, err) - } if claimed, err := repo.ClaimServer(ctx, name, u.ID); err != nil || !claimed { t.Fatalf("claim with every cap lifted = %v, %v; want claimed", claimed, err) }