refactor(api): 删掉没有调用方的 QuotaAvailable,修正 passkey last_used_at 的过时注释

This commit is contained in:
Lemon-miaow committed 2026-09-27 14:20:20 +08:00
1 parent 4839d52f88
commit 56a4bbcf4c
5 files changed
+8 -44

No files matched your search

+2 -1
View File
@@ -80,7 +80,8 @@ sequenceDiagram
API-->>Panel: 412 not_linked API-->>Panel: 412 not_linked
else linked else linked
Repo-->>API: true 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 alt quota exhausted
Repo-->>API: false Repo-->>API: false
API-->>Panel: 403 quota_exceeded API-->>Panel: 403 quota_exceeded
+4 -5
View File
@@ -336,14 +336,13 @@ func (f *fakeRepo) ServerByName(_ context.Context, n string) (*ServerRecord, err
return nil, ErrNotFound return nil, ErrNotFound
} }
func (f *fakeRepo) IsLinked(_ context.Context, u string) (bool, error) { return f.linked[u], nil } 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) QuotaCheck(_ context.Context, userID string, _ string, incoming ResourceSpec) (bool, error) { func (f *fakeRepo) QuotaCheck(_ context.Context, userID string, _ string, incoming ResourceSpec) (bool, error) {
f.quotaChecked = append(f.quotaChecked, incoming) f.quotaChecked = append(f.quotaChecked, incoming)
// For hermetic tests, QuotaCheck delegates to the same QuotaAvailable // For hermetic tests, QuotaCheck answers from the per-user quota flag —
// store — tests that care about per-dimension checks should use // tests that care about per-dimension checks should use fakeQuotas with
// fakeQuotas with direct inspection. // direct inspection.
return f.QuotaAvailable(context.TODO(), userID) return f.quota[userID], nil
} }
func (f *fakeRepo) UpdateServerResources(ctx context.Context, name string, cpu, mem, stor int) error { func (f *fakeRepo) UpdateServerResources(ctx context.Context, name string, cpu, mem, stor int) error {
-26
View File
@@ -346,32 +346,6 @@ func (p *PGRepo) CompleteOwnerSetup(ctx context.Context, newUserID, code string,
return userID, mcUUID, authSource, nil 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` // QuotaCheck reports whether accepting a server with resource spec `incoming`
// would push userID over any quota cap. excludeName is the server row whose own // 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 // cached resources should be excluded ("" for a fresh claim where the row
+2 -6
View File
@@ -166,9 +166,8 @@ type StaffUser struct {
// be public (unlike a session token), so it is safe at rest. CredentialID is the // 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 // authenticator's globally-unique handle (base64url) and PublicKey the COSE key
// (base64); SignCount is the uint32 signature counter captured at registration. // (base64); SignCount is the uint32 signature counter captured at registration.
// LastUsedAt is nil until an assertion stamps it. The passkey login door now exists // LastUsedAt is nil until a passkey sign-in or step-up stamps it
// (Public /auth/passkey/login/{begin,finish}, #72), but no path yet writes // (AdvanceCredentialSignCount, with the advanced counter).
// last_used_at, so in practice it stays nil; wiring the stamp is a follow-up there.
type PasskeyCredential struct { type PasskeyCredential struct {
ID string ID string
UserID string UserID string
@@ -326,9 +325,6 @@ type Repo interface {
// the API clock so expiry is testable. It returns the effective userID plus the // the API clock so expiry is testable. It returns the effective userID plus the
// bound mc_uuid and authSource (for the response + audit). // 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) 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 // QuotaCheck reports whether claiming a server with the given resource spec
// would push the user over any of their four quota caps: max_servers, // 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 // max_cpu_milli, max_memory_mb, and max_storage_gb (spec §9.3 / §22). A nil
-6
View File
@@ -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") set(api.QuotaInput{MaxServers: n(0), MaxCPUMilli: n(2000), MaxMemoryMB: n(4096), MaxStorageGB: n(20)}, "0/2000/4096/20")
gate(false) 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) { if _, err := repo.ClaimServer(ctx, name, u.ID); !errors.Is(err, api.ErrQuotaExceeded) {
t.Fatalf("claim at max_servers 0 = %v, want ErrQuotaExceeded", err) t.Fatalf("claim at max_servers 0 = %v, want ErrQuotaExceeded", err)
} }
@@ -618,9 +615,6 @@ func TestSetQuotasReplacesEveryCap(t *testing.T) {
gate(false) gate(false)
set(api.QuotaInput{}, "-/-/-/-") set(api.QuotaInput{}, "-/-/-/-")
gate(true) 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 { 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) t.Fatalf("claim with every cap lifted = %v, %v; want claimed", claimed, err)
} }