refactor(server): thread context.Context through the db layer and all callers

Fixes all 109 golangci-lint findings (106 contextcheck, 1 gocritic,
2 gosec) that accumulated after D2 wired dbgen (whose queries take ctx)
under ctx-less db.DB wrappers while CI lint was quota-dead. No nolint
comments added; every finding fixed by genuinely threading context.

- db: all 138 hand-written db.DB methods take ctx first; the dbCtx()
  Background shim is deleted; raw Query/QueryRow/Exec/Begin use their
  Context variants; the four redundant ctx-less passthroughs removed.
  db.Auditor/WriteAudit gain ctx.
- Seams: permissions.Checker (DB iface, HasChannelPerm,
  RequireChannelAccess) and the service.Store interface mirror the new
  signatures (ws.EventStore and plugin.PluginStore already did).
- Callers: api/admin handlers use r.Context(); ws per-message paths use
  the connection ctx via DispatchV2; hub loops and startup wiring use
  context.Background(); service methods thread ctx where they have one
  and Background where no ctx exists. Public service surface reached by
  ctx-holding chains (PermissionService.HasChannelPerm/GetRoleForUser/
  RequireChannelAccess, message/dm/block/invite/profile methods) is now
  ctx-first.
- Detached (context.WithoutCancel) where cancellation would break an
  invariant, found by a 3-lens adversarial review of the diff:
  * voice-leave background retries (a dead webhook/connection ctx killed
    retry 2 before it ran, leaving ghost capacity-holding voice rows)
  * rollbackVoiceJoin's compensating delete (its trigger IS the cancel)
  * post-2FA-change DeleteOtherSessions and logout DeleteSession (the
    security tail of a committed change must not die with the request)
  * all api/ws audit writes (a banned user could suppress their own
    login_blocked_banned row by aborting the request mid-bcrypt)
  * admin backup VACUUM INTO (an interrupt left a truncated .db that
    the backup list presented as restorable)
  * post-commit message/edit refetches (a committed message must still
    fan out when the sender disconnects)
  * hub settings-cache refresh (one dead connection could pin stale
    values for the 30s TTL)
- gocritic rangeValCopy fixed (index iteration); gosec G306 excluded in
  config with justification (generated source must stay world-readable)
  instead of flipping genprotocol output to 0o600.

Verified: gofmt/vet, all four build-tag variants, full suite, deadlock
pass, full -race pass, golangci-lint 0 issues uncapped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-07-23 17:03:52 +02:00
co-authored by Claude Fable 5
parent f4b20726ff
commit 6afa9e974c
124 changed files with 2442 additions and 2326 deletions
+3 -2
View File
@@ -1,6 +1,7 @@
package admin_test
import (
"context"
"net/http"
"net/http/httptest"
"os"
@@ -159,9 +160,9 @@ func TestOwnerOnlyMiddleware_AdminDenied(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
// Create admin user (role_id=2, position=80)
adminUID, _ := database.CreateUser("middlewareadmin", "hash", 2)
adminUID, _ := database.CreateUser(context.Background(), "middlewareadmin", "hash", 2)
token := "mw-admin-token"
_, _ = database.CreateSession(adminUID, auth.HashToken(token), "test", "127.0.0.1")
_, _ = database.CreateSession(context.Background(), adminUID, auth.HashToken(token), "test", "127.0.0.1")
w := doRequest(t, handler, http.MethodPost, "/backup", token, nil)
+18 -18
View File
@@ -41,8 +41,8 @@ func TestAdminAPI_PatchUser_UnbanUser(t *testing.T) {
token := createAdminUser(t, database)
// Create and ban a target user first.
targetUID, _ := database.CreateUser("unbanme", "hash", 3)
_ = database.BanUser(targetUID, "test ban", nil)
targetUID, _ := database.CreateUser(context.Background(), "unbanme", "hash", 3)
_ = database.BanUser(context.Background(), targetUID, "test ban", nil)
body := map[string]any{"banned": false}
w := doRequest(t, handler, http.MethodPatch, "/users/"+itoa(targetUID), token, body)
@@ -52,7 +52,7 @@ func TestAdminAPI_PatchUser_UnbanUser(t *testing.T) {
}
// Verify the user is now unbanned.
user, _ := database.GetUserByID(targetUID)
user, _ := database.GetUserByID(context.Background(), targetUID)
if user.Banned {
t.Error("user is still banned after unban request")
}
@@ -64,7 +64,7 @@ func TestAdminAPI_PatchUser_InvalidBody(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
targetUID, _ := database.CreateUser("invalidbody", "hash", 3)
targetUID, _ := database.CreateUser(context.Background(), "invalidbody", "hash", 3)
req := httptest.NewRequest(http.MethodPatch, "/users/"+itoa(targetUID), bytes.NewReader([]byte("not-json")))
req.Header.Set("Authorization", "Bearer "+token)
@@ -147,7 +147,7 @@ func TestAdminAPI_PatchChannel_InvalidBody(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
chID, _ := database.AdminCreateChannel("malformed", "text", "", "", 0)
chID, _ := database.AdminCreateChannel(context.Background(), "malformed", "text", "", "", 0)
req := httptest.NewRequest(http.MethodPatch, "/channels/"+itoa(chID), bytes.NewReader([]byte("not-json")))
req.Header.Set("Authorization", "Bearer "+token)
@@ -235,9 +235,9 @@ func TestAdminAPI_AuditLog_Pagination(t *testing.T) {
token := createAdminUser(t, database)
// Create several audit entries.
uid, _ := database.CreateUser("auditpager", "hash", 1)
uid, _ := database.CreateUser(context.Background(), "auditpager", "hash", 1)
for i := 0; i < 5; i++ {
_ = database.LogAudit(uid, "TEST", "test", int64(i), "")
_ = database.LogAudit(context.Background(), uid, "TEST", "test", int64(i), "")
}
// Fetch page 2 with limit=2, offset=2 — should return 2 entries.
@@ -324,7 +324,7 @@ func TestAdminAPI_PatchUser_BanNilHubDoesNotPanic(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
targetUID, _ := database.CreateUser("ban-nohub", "hash", 3)
targetUID, _ := database.CreateUser(context.Background(), "ban-nohub", "hash", 3)
body := map[string]any{"banned": true, "ban_reason": "nil hub test"}
w := doRequest(t, handler, http.MethodPatch, "/users/"+itoa(targetUID), token, body)
@@ -334,7 +334,7 @@ func TestAdminAPI_PatchUser_BanNilHubDoesNotPanic(t *testing.T) {
}
// Verify ban was still applied despite nil hub.
user, _ := database.GetUserByID(targetUID)
user, _ := database.GetUserByID(context.Background(), targetUID)
if !user.Banned {
t.Error("user should be banned even with nil hub")
}
@@ -363,7 +363,7 @@ func TestAdminAPI_LogStreamTicketFlow(t *testing.T) {
if payload.Ticket == "" {
t.Fatal("expected non-empty log stream ticket")
}
if err := database.DeleteSession(auth.HashToken(token)); err != nil {
if err := database.DeleteSession(context.Background(), auth.HashToken(token)); err != nil {
t.Fatalf("DeleteSession: %v", err)
}
@@ -412,7 +412,7 @@ func TestAdminAPI_LogStreamTicketFlow(t *testing.T) {
t.Fatalf("legacy token stream status = %d, want 401; body: %s", legacyResp.StatusCode, string(body))
}
if _, err := database.CreateSession(1, auth.HashToken(token), "test", "127.0.0.1"); err != nil {
if _, err := database.CreateSession(context.Background(), 1, auth.HashToken(token), "test", "127.0.0.1"); err != nil {
t.Fatalf("CreateSession: %v", err)
}
ticketResp = doRequest(t, handler, http.MethodPost, "/logs/ticket", token, nil)
@@ -422,7 +422,7 @@ func TestAdminAPI_LogStreamTicketFlow(t *testing.T) {
if err := json.Unmarshal(ticketResp.Body.Bytes(), &payload); err != nil {
t.Fatalf("unmarshal restored ticket response: %v", err)
}
if err := database.UpdateUserRole(1, 3); err != nil {
if err := database.UpdateUserRole(context.Background(), 1, 3); err != nil {
t.Fatalf("UpdateUserRole: %v", err)
}
demotedResp, err := http.Get(srv.URL + "/logs/stream?ticket=" + payload.Ticket)
@@ -444,7 +444,7 @@ func TestAdminAPI_PatchUser_RoleChangeNilHubDoesNotPanic(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
targetUID, _ := database.CreateUser("role-nohub", "hash", 3)
targetUID, _ := database.CreateUser(context.Background(), "role-nohub", "hash", 3)
body := map[string]any{"role_id": float64(2)}
w := doRequest(t, handler, http.MethodPatch, "/users/"+itoa(targetUID), token, body)
@@ -454,7 +454,7 @@ func TestAdminAPI_PatchUser_RoleChangeNilHubDoesNotPanic(t *testing.T) {
}
// Verify role was still changed despite nil hub.
user, _ := database.GetUserByID(targetUID)
user, _ := database.GetUserByID(context.Background(), targetUID)
if user.RoleID != 2 {
t.Errorf("RoleID = %d, want 2", user.RoleID)
}
@@ -469,7 +469,7 @@ func TestAdminAPI_PatchUser_BanWithoutReason(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
targetUID, _ := database.CreateUser("banwithout", "hash", 3)
targetUID, _ := database.CreateUser(context.Background(), "banwithout", "hash", 3)
// No ban_reason in body — the nil check in handlePatchUser uses empty string.
body := map[string]any{"banned": true}
@@ -490,7 +490,7 @@ func TestAdminAPI_PatchUser_RoleChangeBroadcast(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", hub, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
targetUID, _ := database.CreateUser("rolebroadcast", "hash", 3)
targetUID, _ := database.CreateUser(context.Background(), "rolebroadcast", "hash", 3)
body := map[string]any{"role_id": float64(2)}
w := doRequest(t, handler, http.MethodPatch, "/users/"+itoa(targetUID), token, body)
@@ -531,7 +531,7 @@ func TestAdminAPI_SetupStatus_AlreadySetup(t *testing.T) {
database := openAdminTestDB(t)
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil, newTestModService(database))
_, _ = database.CreateUser("existing", "hash", 1)
_, _ = database.CreateUser(context.Background(), "existing", "hash", 1)
w := doRequest(t, handler, http.MethodGet, "/setup/status", "", nil)
@@ -583,7 +583,7 @@ func TestAdminAPI_Setup_AlreadyCompleted(t *testing.T) {
database := openAdminTestDB(t)
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil, newTestModService(database))
_, _ = database.CreateUser("existing", "hash", 1)
_, _ = database.CreateUser(context.Background(), "existing", "hash", 1)
body := map[string]string{
"username": "hacker",
+43 -42
View File
@@ -2,6 +2,7 @@ package admin_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
@@ -160,14 +161,14 @@ func openAdminTestDB(t *testing.T) *db.DB {
func createAdminUser(t *testing.T, database *db.DB) string {
t.Helper()
// Owner role has permissions = 2147483647 (includes ADMINISTRATOR bit 0x40000000)
uid, err := database.CreateUser("adminuser", "$2a$12$placeholder", 1)
uid, err := database.CreateUser(context.Background(), "adminuser", "$2a$12$placeholder", 1)
if err != nil {
t.Fatalf("CreateUser admin: %v", err)
}
token := "test-admin-token-" + t.Name()
tokenHash := auth.HashToken(token)
if _, err := database.CreateSession(uid, tokenHash, "test", "127.0.0.1"); err != nil {
if _, err := database.CreateSession(context.Background(), uid, tokenHash, "test", "127.0.0.1"); err != nil {
t.Fatalf("CreateSession: %v", err)
}
return token
@@ -177,14 +178,14 @@ func createAdminUser(t *testing.T, database *db.DB) string {
func createMemberUser(t *testing.T, database *db.DB) string {
t.Helper()
// Member role (id=3) has limited permissions, not ADMINISTRATOR
uid, err := database.CreateUser("memberuser", "$2a$12$placeholder", 3)
uid, err := database.CreateUser(context.Background(), "memberuser", "$2a$12$placeholder", 3)
if err != nil {
t.Fatalf("CreateUser member: %v", err)
}
token := "test-member-token-" + t.Name()
tokenHash := auth.HashToken(token)
if _, err := database.CreateSession(uid, tokenHash, "test", "127.0.0.1"); err != nil {
if _, err := database.CreateSession(context.Background(), uid, tokenHash, "test", "127.0.0.1"); err != nil {
t.Fatalf("CreateSession: %v", err)
}
return token
@@ -322,7 +323,7 @@ func TestAdminAPI_PatchUser_BanHierarchy(t *testing.T) {
ownerToken := createAdminUser(t, database) // Owner role (pos 100)
// A second owner-rank user: equal position, cannot be banned.
peerUID, err := database.CreateUser("peerowner", "$2a$12$placeholder", 1)
peerUID, err := database.CreateUser(context.Background(), "peerowner", "$2a$12$placeholder", 1)
if err != nil {
t.Fatalf("CreateUser peerowner: %v", err)
}
@@ -331,27 +332,27 @@ func TestAdminAPI_PatchUser_BanHierarchy(t *testing.T) {
if w.Code != http.StatusForbidden {
t.Fatalf("equal-rank ban: status = %d, want 403; body: %s", w.Code, w.Body.String())
}
if u, _ := database.GetUserByID(peerUID); u.Banned {
if u, _ := database.GetUserByID(context.Background(), peerUID); u.Banned {
t.Fatal("equal-rank target must not be banned")
}
// A lower-positioned role that still holds ADMINISTRATOR (panel access):
// its holder must not be able to ban the higher-ranked owner.
if _, err := database.Exec(
if _, err := database.ExecContext(context.Background(),
`INSERT INTO roles (id, name, permissions, position, is_default) VALUES (9, 'JuniorAdmin', ?, 50, 0)`,
permissions.Administrator,
); err != nil {
t.Fatalf("inserting junior admin role: %v", err)
}
juniorUID, err := database.CreateUser("junioradmin", "$2a$12$placeholder", 9)
juniorUID, err := database.CreateUser(context.Background(), "junioradmin", "$2a$12$placeholder", 9)
if err != nil {
t.Fatalf("CreateUser junioradmin: %v", err)
}
juniorToken := "junior-token-" + t.Name()
if _, err := database.CreateSession(juniorUID, auth.HashToken(juniorToken), "test", "127.0.0.1"); err != nil {
if _, err := database.CreateSession(context.Background(), juniorUID, auth.HashToken(juniorToken), "test", "127.0.0.1"); err != nil {
t.Fatalf("CreateSession junior: %v", err)
}
ownerUser, err := database.GetUserByUsername("adminuser")
ownerUser, err := database.GetUserByUsername(context.Background(), "adminuser")
if err != nil || ownerUser == nil {
t.Fatalf("GetUserByUsername adminuser: %v", err)
}
@@ -360,12 +361,12 @@ func TestAdminAPI_PatchUser_BanHierarchy(t *testing.T) {
if w.Code != http.StatusForbidden {
t.Fatalf("junior bans owner: status = %d, want 403; body: %s", w.Code, w.Body.String())
}
if u, _ := database.GetUserByID(ownerUser.ID); u.Banned {
if u, _ := database.GetUserByID(context.Background(), ownerUser.ID); u.Banned {
t.Fatal("owner must not be banned by a lower rank")
}
// Downward ban still works: junior admin (pos 50) bans a member (pos 40).
memberUID, err := database.CreateUser("banme", "$2a$12$placeholder", 3)
memberUID, err := database.CreateUser(context.Background(), "banme", "$2a$12$placeholder", 3)
if err != nil {
t.Fatalf("CreateUser banme: %v", err)
}
@@ -374,7 +375,7 @@ func TestAdminAPI_PatchUser_BanHierarchy(t *testing.T) {
if w.Code != http.StatusOK {
t.Fatalf("junior bans member: status = %d, want 200; body: %s", w.Code, w.Body.String())
}
if u, _ := database.GetUserByID(memberUID); !u.Banned {
if u, _ := database.GetUserByID(context.Background(), memberUID); !u.Banned {
t.Fatal("member should be banned by higher-ranked actor")
}
}
@@ -385,7 +386,7 @@ func TestAdminAPI_PatchUser_BanUser(t *testing.T) {
token := createAdminUser(t, database)
// Create a target user
targetUID, _ := database.CreateUser("target", "hash", 3)
targetUID, _ := database.CreateUser(context.Background(), "target", "hash", 3)
body := map[string]any{
"banned": true,
@@ -398,7 +399,7 @@ func TestAdminAPI_PatchUser_BanUser(t *testing.T) {
}
// Verify user is banned in DB
user, err := database.GetUserByID(targetUID)
user, err := database.GetUserByID(context.Background(), targetUID)
if err != nil {
t.Fatalf("GetUserByID: %v", err)
}
@@ -412,7 +413,7 @@ func TestAdminAPI_PatchUser_ChangeRole(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
targetUID, _ := database.CreateUser("rolechange", "hash", 3)
targetUID, _ := database.CreateUser(context.Background(), "rolechange", "hash", 3)
body := map[string]any{
"role_id": float64(2),
@@ -423,7 +424,7 @@ func TestAdminAPI_PatchUser_ChangeRole(t *testing.T) {
t.Errorf("status = %d, want 200; body: %s", w.Code, w.Body.String())
}
user, _ := database.GetUserByID(targetUID)
user, _ := database.GetUserByID(context.Background(), targetUID)
if user.RoleID != 2 {
t.Errorf("RoleID = %d, want 2", user.RoleID)
}
@@ -461,8 +462,8 @@ func TestAdminAPI_ForceLogout_OK(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
targetUID, _ := database.CreateUser("logoutme", "hash", 3)
_, _ = database.CreateSession(targetUID, "victim-token-hash", "web", "1.2.3.4")
targetUID, _ := database.CreateUser(context.Background(), "logoutme", "hash", 3)
_, _ = database.CreateSession(context.Background(), targetUID, "victim-token-hash", "web", "1.2.3.4")
w := doRequest(t, handler, http.MethodDelete, "/users/"+itoa(targetUID)+"/sessions", token, nil)
@@ -470,7 +471,7 @@ func TestAdminAPI_ForceLogout_OK(t *testing.T) {
t.Errorf("status = %d, want 204", w.Code)
}
sessions, _ := database.GetUserSessions(targetUID)
sessions, _ := database.GetUserSessions(context.Background(), targetUID)
if len(sessions) != 0 {
t.Errorf("expected 0 sessions after force logout, got %d", len(sessions))
}
@@ -494,7 +495,7 @@ func TestAdminAPI_ListChannels_OK(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
_, _ = database.AdminCreateChannel("general", "text", "", "", 0)
_, _ = database.AdminCreateChannel(context.Background(), "general", "text", "", "", 0)
w := doRequest(t, handler, http.MethodGet, "/channels", token, nil)
@@ -562,7 +563,7 @@ func TestAdminAPI_UpdateChannel_OK(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
chID, _ := database.AdminCreateChannel("old", "text", "", "", 0)
chID, _ := database.AdminCreateChannel(context.Background(), "old", "text", "", "", 0)
body := map[string]any{
"name": "updated",
@@ -598,7 +599,7 @@ func TestAdminAPI_DeleteChannel_OK(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
chID, _ := database.AdminCreateChannel("del-me", "text", "", "", 0)
chID, _ := database.AdminCreateChannel(context.Background(), "del-me", "text", "", "", 0)
w := doRequest(t, handler, http.MethodDelete, "/channels/"+itoa(chID), token, nil)
@@ -626,8 +627,8 @@ func TestAdminAPI_AuditLog_OK(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
uid, _ := database.CreateUser("actor", "hash", 1)
_ = database.LogAudit(uid, "TEST_ACTION", "user", uid, "detail")
uid, _ := database.CreateUser(context.Background(), "actor", "hash", 1)
_ = database.LogAudit(context.Background(), uid, "TEST_ACTION", "user", uid, "detail")
w := doRequest(t, handler, http.MethodGet, "/audit-log?limit=10&offset=0", token, nil)
@@ -702,7 +703,7 @@ func TestAdminAPI_PatchSettings_OK(t *testing.T) {
}
// Verify the change was persisted
val, err := database.GetSetting("server_name")
val, err := database.GetSetting(context.Background(), "server_name")
if err != nil {
t.Fatalf("GetSetting: %v", err)
}
@@ -733,9 +734,9 @@ func TestAdminAPI_Backup_RequiresOwner(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
// Admin (role 2) can authenticate but is not Owner (role 1, position 100)
adminUID, _ := database.CreateUser("adminonly", "hash", 2)
adminUID, _ := database.CreateUser(context.Background(), "adminonly", "hash", 2)
token := "admin-only-token"
_, _ = database.CreateSession(adminUID, auth.HashToken(token), "test", "127.0.0.1")
_, _ = database.CreateSession(context.Background(), adminUID, auth.HashToken(token), "test", "127.0.0.1")
w := doRequest(t, handler, http.MethodPost, "/backup", token, nil)
@@ -768,7 +769,7 @@ func TestAdminAPI_ActorFromContext_AuditEntry(t *testing.T) {
token := createAdminUser(t, database)
// Create a target user to act on.
targetUID, _ := database.CreateUser("ctxtarget", "hash", 3)
targetUID, _ := database.CreateUser(context.Background(), "ctxtarget", "hash", 3)
body := map[string]any{"banned": true, "ban_reason": "context test"}
w := doRequest(t, handler, http.MethodPatch, "/users/"+itoa(targetUID), token, body)
@@ -779,7 +780,7 @@ func TestAdminAPI_ActorFromContext_AuditEntry(t *testing.T) {
// The audit log should have a non-zero actor_id showing the actor was
// resolved (not 0, which would indicate a failed context lookup).
entries, err := database.GetAuditLog(10, 0)
entries, err := database.GetAuditLog(context.Background(), 10, 0)
if err != nil {
t.Fatalf("GetAuditLog: %v", err)
}
@@ -801,8 +802,8 @@ func TestAdminAPI_ActorFromContext_ForceLogout(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
targetUID, _ := database.CreateUser("logoutctx", "hash", 3)
_, _ = database.CreateSession(targetUID, "victim-hash-ctx", "web", "1.2.3.4")
targetUID, _ := database.CreateUser(context.Background(), "logoutctx", "hash", 3)
_, _ = database.CreateSession(context.Background(), targetUID, "victim-hash-ctx", "web", "1.2.3.4")
w := doRequest(t, handler, http.MethodDelete, "/users/"+itoa(targetUID)+"/sessions", token, nil)
@@ -810,7 +811,7 @@ func TestAdminAPI_ActorFromContext_ForceLogout(t *testing.T) {
t.Fatalf("status = %d, want 204; body: %s", w.Code, w.Body.String())
}
entries, err := database.GetAuditLog(10, 0)
entries, err := database.GetAuditLog(context.Background(), 10, 0)
if err != nil {
t.Fatalf("GetAuditLog: %v", err)
}
@@ -870,7 +871,7 @@ func TestAdminAPI_PatchSettings_RejectsMixedKeys(t *testing.T) {
}
// The valid key must NOT have been written because the request was rejected.
val, err := database.GetSetting("server_name")
val, err := database.GetSetting(context.Background(), "server_name")
if err != nil {
t.Fatalf("GetSetting: %v", err)
}
@@ -953,7 +954,7 @@ func TestAdminAPI_PatchSettings_AllowsRequire2FAWhenAllUsersEnrolledAndRegistrat
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
if _, err := database.Exec(`UPDATE users SET totp_secret = ? WHERE id = 1`, "JBSWY3DPEHPK3PXP"); err != nil {
if _, err := database.ExecContext(context.Background(), `UPDATE users SET totp_secret = ? WHERE id = 1`, "JBSWY3DPEHPK3PXP"); err != nil {
t.Fatalf("enroll admin user: %v", err)
}
@@ -993,7 +994,7 @@ func TestAdminAPI_ListUsers_NoPasswordHash(t *testing.T) {
token := createAdminUser(t, database)
// Create a second user so the list is non-trivial.
_, _ = database.CreateUser("plainuser", "supersecretbcrypthash", 3)
_, _ = database.CreateUser(context.Background(), "plainuser", "supersecretbcrypthash", 3)
w := doRequest(t, handler, http.MethodGet, "/users", token, nil)
@@ -1067,7 +1068,7 @@ func TestAdminAPI_PatchUser_NoPasswordHash(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
targetUID, _ := database.CreateUser("patchvictim", "topsecretbcrypt", 3)
targetUID, _ := database.CreateUser(context.Background(), "patchvictim", "topsecretbcrypt", 3)
body := map[string]any{
"banned": true,
@@ -1095,7 +1096,7 @@ func TestAdminAPI_PatchUser_NoTOTPSecret(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
targetUID, _ := database.CreateUser("patchtotp", "hash", 3)
targetUID, _ := database.CreateUser(context.Background(), "patchtotp", "hash", 3)
w := doRequest(t, handler, http.MethodPatch, "/users/"+itoa(targetUID), token, map[string]any{
"banned": false,
@@ -1210,7 +1211,7 @@ func TestAdminAPI_UpdateChannel_BroadcastsChannelUpdate(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", hub, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
chID, _ := database.AdminCreateChannel("before", "text", "", "", 0)
chID, _ := database.AdminCreateChannel(context.Background(), "before", "text", "", "", 0)
body := map[string]any{"name": "after"}
w := doRequest(t, handler, http.MethodPatch, "/channels/"+itoa(chID), token, body)
@@ -1231,7 +1232,7 @@ func TestAdminAPI_UpdateChannel_NilHubDoesNotPanic(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
chID, _ := database.AdminCreateChannel("patchme", "text", "", "", 0)
chID, _ := database.AdminCreateChannel(context.Background(), "patchme", "text", "", "", 0)
body := map[string]any{"name": "patched"}
w := doRequest(t, handler, http.MethodPatch, "/channels/"+itoa(chID), token, body)
@@ -1246,7 +1247,7 @@ func TestAdminAPI_DeleteChannel_BroadcastsChannelDelete(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", hub, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
chID, _ := database.AdminCreateChannel("delete-me", "text", "", "", 0)
chID, _ := database.AdminCreateChannel(context.Background(), "delete-me", "text", "", "", 0)
w := doRequest(t, handler, http.MethodDelete, "/channels/"+itoa(chID), token, nil)
@@ -1266,7 +1267,7 @@ func TestAdminAPI_DeleteChannel_NilHubDoesNotPanic(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
chID, _ := database.AdminCreateChannel("del-no-hub", "text", "", "", 0)
chID, _ := database.AdminCreateChannel(context.Background(), "del-no-hub", "text", "", "", 0)
w := doRequest(t, handler, http.MethodDelete, "/channels/"+itoa(chID), token, nil)
if w.Code != http.StatusNoContent {
+13 -6
View File
@@ -1,6 +1,7 @@
package admin
import (
"context"
"fmt"
"io"
"log/slog"
@@ -41,7 +42,10 @@ func handleBackup(database *db.DB) http.Handler {
timestamp := time.Now().UTC().Format("20060102_150405")
backupPath := filepath.Join(backupDir, "chatserver_"+timestamp+".db")
if err := database.BackupTo(backupPath); err != nil {
// Detached like the restore path's safety backup: an interrupted
// VACUUM INTO leaves a truncated .db that handleListBackups would
// present as restorable.
if err := database.BackupTo(context.WithoutCancel(r.Context()), backupPath); err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "backup failed")
return
}
@@ -49,7 +53,7 @@ func handleBackup(database *db.DB) http.Handler {
actor := actorFromContext(r)
backupName := filepath.Base(backupPath)
slog.Info("database backup created", "actor_id", actor, "name", backupName)
db.WriteAudit(database, actor, "backup_create", "server", 0,
db.WriteAudit(context.WithoutCancel(r.Context()), database, actor, "backup_create", "server", 0,
fmt.Sprintf("backup saved: %s", backupName))
writeJSON(w, http.StatusOK, map[string]string{
@@ -133,7 +137,7 @@ func handleDeleteBackup(database *db.DB) http.Handler {
actor := actorFromContext(r)
slog.Info("backup deleted", "actor_id", actor, "name", name)
db.WriteAudit(database, actor, "backup_delete", "server", 0, "deleted backup "+name)
db.WriteAudit(context.WithoutCancel(r.Context()), database, actor, "backup_delete", "server", 0, "deleted backup "+name)
w.WriteHeader(http.StatusNoContent)
})
@@ -160,9 +164,12 @@ func handleRestoreBackup(database *db.DB, hub HubBroadcaster) http.Handler {
dbPath := filepath.Join("data", "chatserver.db")
// Safety: create a pre-restore backup before overwriting.
// Safety: create a pre-restore backup before overwriting. WithoutCancel:
// the restore proceeds regardless of client disconnect (Close/copyFile
// below are not ctx-aware), so the safety backup must not be skippable
// by a canceled request ctx.
preRestore := filepath.Join("data", "backups", "pre_restore_"+time.Now().UTC().Format("20060102_150405")+".db")
if err := database.BackupTo(preRestore); err != nil {
if err := database.BackupTo(context.WithoutCancel(r.Context()), preRestore); err != nil {
slog.Warn("pre-restore backup failed", "err", err)
}
@@ -171,7 +178,7 @@ func handleRestoreBackup(database *db.DB, hub HubBroadcaster) http.Handler {
// Checkpoint the WAL and close the database connection before overwriting
// to prevent corruption from concurrent writes (BUG-096).
if _, checkpointErr := database.SQLDb().Exec("PRAGMA wal_checkpoint(TRUNCATE)"); checkpointErr != nil {
if _, checkpointErr := database.SQLDb().ExecContext(context.WithoutCancel(r.Context()), "PRAGMA wal_checkpoint(TRUNCATE)"); checkpointErr != nil {
slog.Warn("pre-restore WAL checkpoint failed", "err", checkpointErr)
}
+7 -6
View File
@@ -1,6 +1,7 @@
package admin_test
import (
"context"
"encoding/json"
"net/http"
"os"
@@ -77,9 +78,9 @@ func TestHandleBackup_RequiresOwner(t *testing.T) {
database := openAdminTestDB(t)
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
adminUID, _ := database.CreateUser("backupadmin", "hash", 2)
adminUID, _ := database.CreateUser(context.Background(), "backupadmin", "hash", 2)
token := "backup-admin-token"
_, _ = database.CreateSession(adminUID, auth.HashToken(token), "test", "127.0.0.1")
_, _ = database.CreateSession(context.Background(), adminUID, auth.HashToken(token), "test", "127.0.0.1")
w := doRequest(t, handler, http.MethodPost, "/backup", token, nil)
@@ -226,9 +227,9 @@ func TestHandleDeleteBackup_RequiresOwner(t *testing.T) {
database := openAdminTestDB(t)
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
adminUID, _ := database.CreateUser("deladmin", "hash", 2)
adminUID, _ := database.CreateUser(context.Background(), "deladmin", "hash", 2)
token := "del-admin-token"
_, _ = database.CreateSession(adminUID, auth.HashToken(token), "test", "127.0.0.1")
_, _ = database.CreateSession(context.Background(), adminUID, auth.HashToken(token), "test", "127.0.0.1")
// Create the file so path validation doesn't return 404 before the 403.
backupDir := filepath.Join(tmpDir, "data", "backups")
@@ -354,9 +355,9 @@ func TestHandleRestoreBackup_RequiresOwner(t *testing.T) {
database := openAdminTestDB(t)
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
adminUID, _ := database.CreateUser("restoreadmin", "hash", 2)
adminUID, _ := database.CreateUser(context.Background(), "restoreadmin", "hash", 2)
token := "restore-admin-token"
_, _ = database.CreateSession(adminUID, auth.HashToken(token), "test", "127.0.0.1")
_, _ = database.CreateSession(context.Background(), adminUID, auth.HashToken(token), "test", "127.0.0.1")
// Create files so path checks pass before auth check.
backupDir := filepath.Join(tmpDir, "data", "backups")
+8 -7
View File
@@ -1,6 +1,7 @@
package admin
import (
"context"
"encoding/json"
"fmt"
"log/slog"
@@ -27,7 +28,7 @@ func getPermChannel(database *db.DB, w http.ResponseWriter, r *http.Request) *db
writeErr(w, http.StatusBadRequest, "BAD_REQUEST", "invalid channel id")
return nil
}
ch, err := database.GetChannel(id)
ch, err := database.GetChannel(r.Context(), id)
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to fetch channel")
return nil
@@ -55,7 +56,7 @@ func handleGetChannelPermissions(database *db.DB) http.HandlerFunc {
if ch == nil {
return
}
overrides, err := database.ListChannelRoleOverrides(ch.ID)
overrides, err := database.ListChannelRoleOverrides(r.Context(), ch.ID)
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to list channel permissions")
return
@@ -81,7 +82,7 @@ func handlePutChannelPermission(database *db.DB, hub HubBroadcaster, permInvalid
writeErr(w, http.StatusBadRequest, "BAD_REQUEST", "invalid role id")
return
}
role, err := database.GetRoleByID(roleID)
role, err := database.GetRoleByID(r.Context(), roleID)
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to fetch role")
return
@@ -100,7 +101,7 @@ func handlePutChannelPermission(database *db.DB, hub HubBroadcaster, permInvalid
allow := req.Allow & permissions.AllPerms
deny := req.Deny & permissions.AllPerms
if err := database.UpsertChannelOverride(ch.ID, roleID, allow, deny); err != nil {
if err := database.UpsertChannelOverride(r.Context(), ch.ID, roleID, allow, deny); err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to save channel permission")
return
}
@@ -108,7 +109,7 @@ func handlePutChannelPermission(database *db.DB, hub HubBroadcaster, permInvalid
actor := actorFromContext(r)
slog.Info("channel permissions updated", "actor_id", actor, "channel_id", ch.ID,
"role_id", roleID, "allow", allow, "deny", deny)
db.WriteAudit(database, actor, "channel_perms_update", "channel", ch.ID,
db.WriteAudit(context.WithoutCancel(r.Context()), database, actor, "channel_perms_update", "channel", ch.ID,
fmt.Sprintf("set overrides for role %s on #%s (allow=%#x deny=%#x)", role.Name, ch.Name, allow, deny))
if permInvalidator != nil {
@@ -140,14 +141,14 @@ func handleDeleteChannelPermission(database *db.DB, hub HubBroadcaster, permInva
return
}
if err := database.DeleteChannelOverride(ch.ID, roleID); err != nil {
if err := database.DeleteChannelOverride(r.Context(), ch.ID, roleID); err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to delete channel permission")
return
}
actor := actorFromContext(r)
slog.Info("channel permissions cleared", "actor_id", actor, "channel_id", ch.ID, "role_id", roleID)
db.WriteAudit(database, actor, "channel_perms_clear", "channel", ch.ID,
db.WriteAudit(context.WithoutCancel(r.Context()), database, actor, "channel_perms_clear", "channel", ch.ID,
fmt.Sprintf("cleared overrides for role %d on #%s", roleID, ch.Name))
if permInvalidator != nil {
+13 -12
View File
@@ -1,6 +1,7 @@
package admin_test
import (
"context"
"encoding/json"
"net/http"
"testing"
@@ -31,7 +32,7 @@ func TestGetChannelPermissions_ReturnsAllRoles(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
chID, err := database.CreateChannel("secret", "text", "", "", 0)
chID, err := database.CreateChannel(context.Background(), "secret", "text", "", "", 0)
if err != nil {
t.Fatalf("CreateChannel: %v", err)
}
@@ -80,7 +81,7 @@ func TestGetChannelPermissions_DMRejected(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
chID, err := database.CreateChannel("dm-chan", "dm", "", "", 0)
chID, err := database.CreateChannel(context.Background(), "dm-chan", "dm", "", "", 0)
if err != nil {
t.Fatalf("CreateChannel dm: %v", err)
}
@@ -101,7 +102,7 @@ func TestPutChannelPermission_PersistsAndPropagates(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", hub, nil, nil, nil, inv, newTestModService(database))
token := createAdminUser(t, database)
chID, err := database.CreateChannel("secret", "text", "", "", 0)
chID, err := database.CreateChannel(context.Background(), "secret", "text", "", "", 0)
if err != nil {
t.Fatalf("CreateChannel: %v", err)
}
@@ -114,7 +115,7 @@ func TestPutChannelPermission_PersistsAndPropagates(t *testing.T) {
t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
}
allow, deny, err := database.GetChannelPermissions(chID, 3)
allow, deny, err := database.GetChannelPermissions(context.Background(), chID, 3)
if err != nil {
t.Fatalf("GetChannelPermissions: %v", err)
}
@@ -129,7 +130,7 @@ func TestPutChannelPermission_PersistsAndPropagates(t *testing.T) {
t.Errorf("RefreshChannelVisibility not called for channel %d", chID)
}
entries, err := database.GetAuditLog(10, 0)
entries, err := database.GetAuditLog(context.Background(), 10, 0)
if err != nil {
t.Fatalf("GetAuditLog: %v", err)
}
@@ -149,7 +150,7 @@ func TestPutChannelPermission_MasksUnknownBits(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
chID, err := database.CreateChannel("secret2", "text", "", "", 0)
chID, err := database.CreateChannel(context.Background(), "secret2", "text", "", "", 0)
if err != nil {
t.Fatalf("CreateChannel: %v", err)
}
@@ -162,7 +163,7 @@ func TestPutChannelPermission_MasksUnknownBits(t *testing.T) {
t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
}
allow, deny, err := database.GetChannelPermissions(chID, 3)
allow, deny, err := database.GetChannelPermissions(context.Background(), chID, 3)
if err != nil {
t.Fatalf("GetChannelPermissions: %v", err)
}
@@ -179,7 +180,7 @@ func TestPutChannelPermission_UnknownRole(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil, newTestModService(database))
token := createAdminUser(t, database)
chID, err := database.CreateChannel("secret3", "text", "", "", 0)
chID, err := database.CreateChannel(context.Background(), "secret3", "text", "", "", 0)
if err != nil {
t.Fatalf("CreateChannel: %v", err)
}
@@ -197,7 +198,7 @@ func TestPutChannelPermission_NonAdminForbidden(t *testing.T) {
_ = createAdminUser(t, database)
memberToken := createMemberUser(t, database)
chID, err := database.CreateChannel("secret4", "text", "", "", 0)
chID, err := database.CreateChannel(context.Background(), "secret4", "text", "", "", 0)
if err != nil {
t.Fatalf("CreateChannel: %v", err)
}
@@ -218,11 +219,11 @@ func TestDeleteChannelPermission_ClearsOverride(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", hub, nil, nil, nil, inv, newTestModService(database))
token := createAdminUser(t, database)
chID, err := database.CreateChannel("secret5", "text", "", "", 0)
chID, err := database.CreateChannel(context.Background(), "secret5", "text", "", "", 0)
if err != nil {
t.Fatalf("CreateChannel: %v", err)
}
if err := database.UpsertChannelOverride(chID, 3, 0, permissions.ReadMessages); err != nil {
if err := database.UpsertChannelOverride(context.Background(), chID, 3, 0, permissions.ReadMessages); err != nil {
t.Fatalf("UpsertChannelOverride: %v", err)
}
@@ -232,7 +233,7 @@ func TestDeleteChannelPermission_ClearsOverride(t *testing.T) {
t.Fatalf("status = %d, want 204; body: %s", w.Code, w.Body.String())
}
allow, deny, err := database.GetChannelPermissions(chID, 3)
allow, deny, err := database.GetChannelPermissions(context.Background(), chID, 3)
if err != nil {
t.Fatalf("GetChannelPermissions: %v", err)
}
+13 -12
View File
@@ -1,6 +1,7 @@
package admin
import (
"context"
"encoding/json"
"fmt"
"log/slog"
@@ -63,7 +64,7 @@ func validateCategoryType(channelType, category string) string {
func handleListChannels(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
channels, err := database.ListChannels()
channels, err := database.ListChannels(r.Context())
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to list channels")
return
@@ -102,20 +103,20 @@ func handleCreateChannel(database *db.DB, hub HubBroadcaster) http.HandlerFunc {
return
}
id, err := database.AdminCreateChannel(req.Name, req.Type, req.Category, req.Topic, req.Position)
id, err := database.AdminCreateChannel(r.Context(), req.Name, req.Type, req.Category, req.Topic, req.Position)
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to create channel")
return
}
ch, err := database.GetChannel(id)
ch, err := database.GetChannel(r.Context(), id)
if err != nil || ch == nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to fetch created channel")
return
}
actor := actorFromContext(r)
slog.Info("channel created", "actor_id", actor, "channel", req.Name, "type", req.Type)
db.WriteAudit(database, actor, "channel_create", "channel", id,
db.WriteAudit(context.WithoutCancel(r.Context()), database, actor, "channel_create", "channel", id,
fmt.Sprintf("created #%s (%s)", req.Name, req.Type))
if hub != nil {
hub.BroadcastChannelCreate(ch)
@@ -141,7 +142,7 @@ func handlePatchChannel(database *db.DB, hub HubBroadcaster) http.HandlerFunc {
return
}
existing, err := database.GetChannel(id)
existing, err := database.GetChannel(r.Context(), id)
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to fetch channel")
return
@@ -164,17 +165,17 @@ func handlePatchChannel(database *db.DB, hub HubBroadcaster) http.HandlerFunc {
return
}
if err := database.AdminUpdateChannel(id, req.Name, req.Topic, req.SlowMode, req.Position, req.Archived); err != nil {
if err := database.AdminUpdateChannel(r.Context(), id, req.Name, req.Topic, req.SlowMode, req.Position, req.Archived); err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to update channel")
return
}
actor := actorFromContext(r)
slog.Info("channel updated", "actor_id", actor, "channel_id", id, "name", req.Name)
db.WriteAudit(database, actor, "channel_update", "channel", id,
db.WriteAudit(context.WithoutCancel(r.Context()), database, actor, "channel_update", "channel", id,
fmt.Sprintf("updated #%s", req.Name))
updated, err := database.GetChannel(id)
updated, err := database.GetChannel(r.Context(), id)
if err != nil || updated == nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to fetch updated channel")
return
@@ -194,7 +195,7 @@ func handleDeleteChannel(database *db.DB, hub HubBroadcaster) http.HandlerFunc {
return
}
existing, err := database.GetChannel(id)
existing, err := database.GetChannel(r.Context(), id)
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to fetch channel")
return
@@ -204,13 +205,13 @@ func handleDeleteChannel(database *db.DB, hub HubBroadcaster) http.HandlerFunc {
return
}
if err := database.AdminDeleteChannel(id); err != nil {
if err := database.AdminDeleteChannel(r.Context(), id); err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to delete channel")
return
}
actor := actorFromContext(r)
slog.Warn("channel deleted", "actor_id", actor, "channel_id", id, "name", existing.Name)
db.WriteAudit(database, actor, "channel_delete", "channel", id,
db.WriteAudit(context.WithoutCancel(r.Context()), database, actor, "channel_delete", "channel", id,
fmt.Sprintf("deleted #%s", existing.Name))
if hub != nil {
hub.BroadcastChannelDelete(id)
@@ -224,7 +225,7 @@ func handleGetAuditLog(database *db.DB) http.HandlerFunc {
limit := queryInt(r, "limit", 50, 1)
offset := queryInt(r, "offset", 0, 0)
entries, err := database.GetAuditLog(limit, offset)
entries, err := database.GetAuditLog(r.Context(), limit, offset)
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to get audit log")
return
+13 -12
View File
@@ -1,6 +1,7 @@
package admin
import (
"context"
"encoding/json"
"errors"
"fmt"
@@ -15,7 +16,7 @@ import (
func handleGetSettings(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
settings, err := database.GetAllSettings()
settings, err := database.GetAllSettings(r.Context())
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to get settings")
return
@@ -48,7 +49,7 @@ func handlePatchSettings(database *db.DB) http.HandlerFunc {
return
}
if err := validateRequire2FAUpdate(database, normalizedUpdates); err != nil {
if err := validateRequire2FAUpdate(r.Context(), database, normalizedUpdates); err != nil {
writeErr(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
@@ -57,13 +58,13 @@ func handlePatchSettings(database *db.DB) http.HandlerFunc {
// Apply all settings atomically so a mid-loop failure doesn't leave
// partial updates.
tx, err := database.Begin()
tx, err := database.BeginTx(r.Context(), nil)
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to start transaction")
return
}
for key, value := range normalizedUpdates {
if _, txErr := tx.Exec(
if _, txErr := tx.ExecContext(r.Context(),
`INSERT INTO settings (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
key, value,
@@ -79,11 +80,11 @@ func handlePatchSettings(database *db.DB) http.HandlerFunc {
}
for key := range normalizedUpdates {
slog.Info("setting changed", "actor_id", actor, "key", key)
db.WriteAudit(database, actor, "setting_change", "setting", 0,
db.WriteAudit(context.WithoutCancel(r.Context()), database, actor, "setting_change", "setting", 0,
fmt.Sprintf("%s updated", key))
}
settings, err := database.GetAllSettings()
settings, err := database.GetAllSettings(r.Context())
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to fetch settings")
return
@@ -112,8 +113,8 @@ func normalizeSettingUpdates(updates map[string]string) (map[string]string, erro
return normalized, nil
}
func validateRequire2FAUpdate(database *db.DB, updates map[string]string) error {
targetRequire2FA, err := targetBoolSetting(database, updates, "require_2fa")
func validateRequire2FAUpdate(ctx context.Context, database *db.DB, updates map[string]string) error {
targetRequire2FA, err := targetBoolSetting(ctx, database, updates, "require_2fa")
if err != nil {
return err
}
@@ -121,7 +122,7 @@ func validateRequire2FAUpdate(database *db.DB, updates map[string]string) error
return nil
}
registrationOpen, err := targetBoolSetting(database, updates, "registration_open")
registrationOpen, err := targetBoolSetting(ctx, database, updates, "registration_open")
if err != nil {
return err
}
@@ -129,7 +130,7 @@ func validateRequire2FAUpdate(database *db.DB, updates map[string]string) error
return fmt.Errorf("require_2fa cannot be enabled while registration is open")
}
count, err := database.CountUsersWithoutTOTP()
count, err := database.CountUsersWithoutTOTP(ctx)
if err != nil {
return fmt.Errorf("failed to validate 2FA enrollment")
}
@@ -139,11 +140,11 @@ func validateRequire2FAUpdate(database *db.DB, updates map[string]string) error
return nil
}
func targetBoolSetting(database *db.DB, updates map[string]string, key string) (bool, error) {
func targetBoolSetting(ctx context.Context, database *db.DB, updates map[string]string, key string) (bool, error) {
if value, ok := updates[key]; ok {
return parseBooleanSettingValue(value)
}
value, err := database.GetSetting(key)
value, err := database.GetSetting(ctx, key)
if errors.Is(err, db.ErrNotFound) {
return false, nil
}
+11 -10
View File
@@ -1,6 +1,7 @@
package admin
import (
"context"
"encoding/json"
"errors"
"fmt"
@@ -15,7 +16,7 @@ import (
func handleGetStats(database *db.DB, hub HubBroadcaster) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
stats, err := database.GetServerStats()
stats, err := database.GetServerStats(r.Context())
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to get stats")
return
@@ -32,7 +33,7 @@ func handleListUsers(database *db.DB) http.HandlerFunc {
limit := queryInt(r, "limit", 50, 1)
offset := queryInt(r, "offset", 0, 0)
users, err := database.ListAllUsers(limit, offset)
users, err := database.ListAllUsers(r.Context(), limit, offset)
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to list users")
return
@@ -81,7 +82,7 @@ func handlePatchUser(database *db.DB, hub HubBroadcaster, permInvalidator Permis
return
}
user, err := database.GetUserByID(id)
user, err := database.GetUserByID(r.Context(), id)
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to fetch user")
return
@@ -131,7 +132,7 @@ func handlePatchUser(database *db.DB, hub HubBroadcaster, permInvalidator Permis
}
if req.RoleID != nil {
if _, err := database.Exec(`UPDATE users SET role_id = ? WHERE id = ?`, *req.RoleID, id); err != nil {
if _, err := database.ExecContext(r.Context(), `UPDATE users SET role_id = ? WHERE id = ?`, *req.RoleID, id); err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to update role")
return
}
@@ -139,21 +140,21 @@ func handlePatchUser(database *db.DB, hub HubBroadcaster, permInvalidator Permis
if permInvalidator != nil {
permInvalidator.InvalidateUser(id)
}
db.WriteAudit(database, actor, "role_change", "user", id,
db.WriteAudit(context.WithoutCancel(r.Context()), database, actor, "role_change", "user", id,
fmt.Sprintf("changed %s role to %d", user.Username, *req.RoleID))
if role, err := database.GetRoleByID(*req.RoleID); err == nil && role != nil {
if role, err := database.GetRoleByID(r.Context(), *req.RoleID); err == nil && role != nil {
if hub != nil {
hub.BroadcastMemberUpdate(id, role.Name)
}
}
}
updated, err := database.GetUserByID(id)
updated, err := database.GetUserByID(r.Context(), id)
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to fetch updated user")
return
}
writeJSON(w, http.StatusOK, toAdminUserResponseFromUser(database, updated))
writeJSON(w, http.StatusOK, toAdminUserResponseFromUser(r.Context(), database, updated))
}
}
@@ -165,13 +166,13 @@ func handleForceLogout(database *db.DB) http.HandlerFunc {
return
}
if err := database.ForceLogoutUser(id); err != nil {
if err := database.ForceLogoutUser(r.Context(), id); err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to logout user")
return
}
actor := actorFromContext(r)
slog.Info("force logout", "actor_id", actor, "target_user_id", id)
db.WriteAudit(database, actor, "force_logout", "user", id, "all sessions terminated")
db.WriteAudit(context.WithoutCancel(r.Context()), database, actor, "force_logout", "user", id, "all sessions terminated")
w.WriteHeader(http.StatusNoContent)
}
}
+7 -5
View File
@@ -341,7 +341,10 @@ func handleLogStream(database *db.DB, ringBuf *RingBuffer) http.HandlerFunc {
http.Error(w, string(errResp), http.StatusUnauthorized)
return
}
sess, err := database.GetSessionByTokenHash(entry.tokenHash)
// Stream lifetime == request lifetime, so all session re-checks below
// use the stream request's context.
ctx := r.Context()
sess, err := database.GetSessionByTokenHash(ctx, entry.tokenHash)
if err != nil || sess == nil || auth.IsSessionExpired(sess.ExpiresAt) {
errResp, _ := json.Marshal(map[string]string{
"error": "UNAUTHORIZED",
@@ -351,15 +354,15 @@ func handleLogStream(database *db.DB, ringBuf *RingBuffer) http.HandlerFunc {
return
}
sessionStillAuthorized := func() bool {
current, currentErr := database.GetSessionByTokenHash(entry.tokenHash)
current, currentErr := database.GetSessionByTokenHash(ctx, entry.tokenHash)
if currentErr != nil || current == nil || auth.IsSessionExpired(current.ExpiresAt) {
return false
}
user, userErr := database.GetUserByID(current.UserID)
user, userErr := database.GetUserByID(ctx, current.UserID)
if userErr != nil || user == nil {
return false
}
role, roleErr := database.GetRoleByID(user.RoleID)
role, roleErr := database.GetRoleByID(ctx, user.RoleID)
if roleErr != nil || role == nil {
return false
}
@@ -408,7 +411,6 @@ func handleLogStream(database *db.DB, ringBuf *RingBuffer) http.HandlerFunc {
keepalive := time.NewTicker(15 * time.Second)
defer keepalive.Stop()
ctx := r.Context()
for {
select {
case entry := <-ch:
+3 -3
View File
@@ -73,7 +73,7 @@ func TestHandleLogStream_BackfillStopsAfterSessionRevocation(t *testing.T) {
logBuf.Write(LogEntry{Timestamp: "2026-03-29T10:00:00Z", Level: "info", Message: "first", Source: "test"})
logBuf.Write(LogEntry{Timestamp: "2026-03-29T10:00:01Z", Level: "info", Message: "second", Source: "test"})
userID, err := database.CreateUser("owner", "hash", 1)
userID, err := database.CreateUser(context.Background(), "owner", "hash", 1)
if err != nil {
t.Fatalf("CreateUser: %v", err)
}
@@ -83,7 +83,7 @@ func TestHandleLogStream_BackfillStopsAfterSessionRevocation(t *testing.T) {
t.Fatalf("GenerateToken: %v", err)
}
tokenHash := auth.HashToken(token)
if _, err := database.CreateSession(userID, tokenHash, "test", "127.0.0.1"); err != nil {
if _, err := database.CreateSession(context.Background(), userID, tokenHash, "test", "127.0.0.1"); err != nil {
t.Fatalf("CreateSession: %v", err)
}
@@ -99,7 +99,7 @@ func TestHandleLogStream_BackfillStopsAfterSessionRevocation(t *testing.T) {
writer := &revokingSSEWriter{
header: make(http.Header),
revoke: func() {
_ = database.DeleteSession(tokenHash)
_ = database.DeleteSession(context.Background(), tokenHash)
},
cancel: cancel,
}
+4 -4
View File
@@ -31,7 +31,7 @@ func adminAuthMiddleware(database *db.DB) func(http.Handler) http.Handler {
}
hash := auth.HashToken(token)
sess, err := database.GetSessionByTokenHash(hash)
sess, err := database.GetSessionByTokenHash(r.Context(), hash)
if err != nil || sess == nil {
writeErr(w, http.StatusUnauthorized, "UNAUTHORIZED", "invalid or expired session")
return
@@ -42,13 +42,13 @@ func adminAuthMiddleware(database *db.DB) func(http.Handler) http.Handler {
return
}
user, err := database.GetUserByID(sess.UserID)
user, err := database.GetUserByID(r.Context(), sess.UserID)
if err != nil || user == nil {
writeErr(w, http.StatusUnauthorized, "UNAUTHORIZED", "user not found")
return
}
role, err := database.GetRoleByID(user.RoleID)
role, err := database.GetRoleByID(r.Context(), user.RoleID)
if err != nil || role == nil {
writeErr(w, http.StatusUnauthorized, "UNAUTHORIZED", "role not found")
return
@@ -77,7 +77,7 @@ func ownerOnlyMiddleware(database *db.DB, next http.Handler) http.Handler {
return
}
role, err := database.GetRoleByID(user.RoleID)
role, err := database.GetRoleByID(r.Context(), user.RoleID)
if err != nil || role == nil {
writeErr(w, http.StatusForbidden, "FORBIDDEN", "role not found")
return
+12 -12
View File
@@ -157,23 +157,23 @@ func TestOwnerOnlyMiddleware_RoleNotFound(t *testing.T) {
// Create a user initially with a valid role, then mutate role_id to a
// nonexistent value (disabling FK checks temporarily so SQLite allows it).
uid, err := database.CreateUser("orphanuser", "$2a$12$x", 1)
uid, err := database.CreateUser(context.Background(), "orphanuser", "$2a$12$x", 1)
if err != nil {
t.Fatalf("CreateUser: %v", err)
}
user, err := database.GetUserByID(uid)
user, err := database.GetUserByID(context.Background(), uid)
if err != nil || user == nil {
t.Fatalf("GetUserByID: %v", err)
}
// Disable FK enforcement, update role_id, re-enable.
if _, err := database.Exec(`PRAGMA foreign_keys=OFF`); err != nil {
if _, err := database.ExecContext(context.Background(), `PRAGMA foreign_keys=OFF`); err != nil {
t.Fatalf("disable FK: %v", err)
}
if _, err := database.Exec(`UPDATE users SET role_id = 9999 WHERE id = ?`, uid); err != nil {
if _, err := database.ExecContext(context.Background(), `UPDATE users SET role_id = 9999 WHERE id = ?`, uid); err != nil {
t.Fatalf("UPDATE role_id: %v", err)
}
if _, err := database.Exec(`PRAGMA foreign_keys=ON`); err != nil {
if _, err := database.ExecContext(context.Background(), `PRAGMA foreign_keys=ON`); err != nil {
t.Fatalf("re-enable FK: %v", err)
}
user.RoleID = 9999 // mirror the DB value in our in-memory struct
@@ -213,11 +213,11 @@ func TestOwnerOnlyMiddleware_RoleNotFound(t *testing.T) {
func TestOwnerOnlyMiddleware_OwnerPassesThrough(t *testing.T) {
database := openWhiteboxTestDB(t)
uid, err := database.CreateUser("ownerpass", "$2a$12$x", 1)
uid, err := database.CreateUser(context.Background(), "ownerpass", "$2a$12$x", 1)
if err != nil {
t.Fatalf("CreateUser: %v", err)
}
user, err := database.GetUserByID(uid)
user, err := database.GetUserByID(context.Background(), uid)
if err != nil || user == nil {
t.Fatalf("GetUserByID: %v", err)
}
@@ -251,23 +251,23 @@ func TestAdminAuthMiddleware_RoleNotFound(t *testing.T) {
database := openWhiteboxTestDB(t)
handler := NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil, nil)
uid, err := database.CreateUser("noroleuser", "$2a$12$x", 1)
uid, err := database.CreateUser(context.Background(), "noroleuser", "$2a$12$x", 1)
if err != nil {
t.Fatalf("CreateUser: %v", err)
}
token := "norole-token"
if _, err := database.CreateSession(uid, auth.HashToken(token), "test", "127.0.0.1"); err != nil {
if _, err := database.CreateSession(context.Background(), uid, auth.HashToken(token), "test", "127.0.0.1"); err != nil {
t.Fatalf("CreateSession: %v", err)
}
// Disable FK enforcement, assign a non-existent role_id, re-enable.
if _, err := database.Exec(`PRAGMA foreign_keys=OFF`); err != nil {
if _, err := database.ExecContext(context.Background(), `PRAGMA foreign_keys=OFF`); err != nil {
t.Fatalf("disable FK: %v", err)
}
if _, err := database.Exec(`UPDATE users SET role_id = 9999 WHERE id = ?`, uid); err != nil {
if _, err := database.ExecContext(context.Background(), `UPDATE users SET role_id = 9999 WHERE id = ?`, uid); err != nil {
t.Fatalf("UPDATE role_id: %v", err)
}
if _, err := database.Exec(`PRAGMA foreign_keys=ON`); err != nil {
if _, err := database.ExecContext(context.Background(), `PRAGMA foreign_keys=ON`); err != nil {
t.Fatalf("re-enable FK: %v", err)
}
+4 -3
View File
@@ -4,6 +4,7 @@ package admin_test
// ownerOnlyMiddleware, and related helpers.
import (
"context"
"net/http"
"testing"
"time"
@@ -22,19 +23,19 @@ func TestAdminAuthMiddleware_ExpiredSession(t *testing.T) {
// Create a user and session, then manually expire the session by setting
// expires_at to a past timestamp via the exported Exec helper.
uid, err := database.CreateUser("expireduser", "$2a$12$x", 1)
uid, err := database.CreateUser(context.Background(), "expireduser", "$2a$12$x", 1)
if err != nil {
t.Fatalf("CreateUser: %v", err)
}
token := "expired-session-token"
tokenHash := auth.HashToken(token)
if _, err := database.CreateSession(uid, tokenHash, "test", "127.0.0.1"); err != nil {
if _, err := database.CreateSession(context.Background(), uid, tokenHash, "test", "127.0.0.1"); err != nil {
t.Fatalf("CreateSession: %v", err)
}
// Set expires_at to yesterday so the session is treated as expired.
pastTime := time.Now().Add(-24 * time.Hour).UTC().Format("2006-01-02T15:04:05Z")
if _, err := database.Exec(
if _, err := database.ExecContext(context.Background(),
`UPDATE sessions SET expires_at = ? WHERE token = ?`,
pastTime, tokenHash,
); err != nil {
+8 -7
View File
@@ -1,6 +1,7 @@
package admin
import (
"context"
"encoding/json"
"errors"
"log/slog"
@@ -42,7 +43,7 @@ type setupResponse struct {
// handleSetupStatus returns whether initial setup is needed (no users exist).
func handleSetupStatus(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
count, err := database.UserCount()
count, err := database.UserCount(r.Context())
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to check user count")
return
@@ -110,7 +111,7 @@ func handleSetup(database *db.DB, limiter *auth.RateLimiter, allowedOrigins []st
// Atomically check no users exist and create the owner (BUG-119).
// This closes the TOCTOU race between UserCount() and CreateUser().
uid, err := database.CreateOwnerIfEmpty(req.Username, hash, ownerRoleID)
uid, err := database.CreateOwnerIfEmpty(r.Context(), req.Username, hash, ownerRoleID)
if errors.Is(err, db.ErrConflict) {
writeErr(w, http.StatusForbidden, "FORBIDDEN", "setup has already been completed")
return
@@ -132,27 +133,27 @@ func handleSetup(database *db.DB, limiter *auth.RateLimiter, allowedOrigins []st
if len(device) > maxDeviceLen {
device = device[:maxDeviceLen]
}
if _, err := database.CreateSession(uid, auth.HashToken(token), device, host); err != nil {
if _, err := database.CreateSession(r.Context(), uid, auth.HashToken(token), device, host); err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to create session")
return
}
// Create default channels under canonical categories.
_, _ = database.CreateChannel("general", "text", "Text Channels", "Welcome to the server!", 0)
_, _ = database.CreateChannel("General", "voice", "Voice Channels", "", 0)
_, _ = database.CreateChannel(r.Context(), "general", "text", "Text Channels", "Welcome to the server!", 0)
_, _ = database.CreateChannel(r.Context(), "General", "voice", "Voice Channels", "", 0)
// Generate a bootstrap invite code so the owner can invite others.
// Bound it (5 uses / 24h) rather than minting an unlimited, non-expiring
// invite — the owner can create fresh invites once logged in.
bootstrapInviteExpiry := time.Now().Add(24 * time.Hour)
inviteCode, err := database.CreateInvite(uid, 5, &bootstrapInviteExpiry)
inviteCode, err := database.CreateInvite(r.Context(), uid, 5, &bootstrapInviteExpiry)
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to generate invite code")
return
}
slog.Info("server setup completed", "owner", req.Username, "user_id", uid)
db.WriteAudit(database, uid, "server_setup", "server", 0,
db.WriteAudit(context.WithoutCancel(r.Context()), database, uid, "server_setup", "server", 0,
"initial setup: owner account created, default channel and invite generated")
writeJSON(w, http.StatusCreated, setupResponse{
+3 -2
View File
@@ -1,6 +1,7 @@
package admin_test
import (
"context"
"encoding/json"
"fmt"
"net/http"
@@ -86,7 +87,7 @@ func TestSetup_CreatesOwner(t *testing.T) {
}
// Verify user was created with Owner role.
user, err := database.GetUserByUsername("myadmin")
user, err := database.GetUserByUsername(context.Background(), "myadmin")
if err != nil || user == nil {
t.Fatal("user not found in database after setup")
}
@@ -185,7 +186,7 @@ func TestSetup_ConcurrentRace(t *testing.T) {
}
// Verify only one user exists in the database.
count, err := database.UserCount()
count, err := database.UserCount(context.Background())
if err != nil {
t.Fatalf("UserCount: %v", err)
}
+7 -3
View File
@@ -1,6 +1,10 @@
package admin
import "github.com/owncord/server/db"
import (
"context"
"github.com/owncord/server/db"
)
// ─── Context keys ─────────────────────────────────────────────────────────────
@@ -93,9 +97,9 @@ func toAdminUserResponse(u db.UserWithRole) adminUserResponse {
// toAdminUserResponseFromUser converts a plain db.User to the safe response
// shape, resolving the role name via the database.
func toAdminUserResponseFromUser(database *db.DB, u *db.User) adminUserResponse {
func toAdminUserResponseFromUser(ctx context.Context, database *db.DB, u *db.User) adminUserResponse {
roleName := ""
if role, err := database.GetRoleByID(u.RoleID); err == nil && role != nil {
if role, err := database.GetRoleByID(ctx, u.RoleID); err == nil && role != nil {
roleName = role.Name
}
return adminUserResponse{
+3 -2
View File
@@ -1,6 +1,7 @@
package admin_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -136,9 +137,9 @@ func TestAdminAPI_ApplyUpdate_RequiresOwner(t *testing.T) {
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil, newTestModService(database))
// Create admin user (not owner - role 2)
adminUID, _ := database.CreateUser("adminonly2", "hash", 2)
adminUID, _ := database.CreateUser(context.Background(), "adminonly2", "hash", 2)
token := "admin-role-token"
_, _ = database.CreateSession(adminUID, auth.HashToken(token), "test", "127.0.0.1")
_, _ = database.CreateSession(context.Background(), adminUID, auth.HashToken(token), "test", "127.0.0.1")
w := doRequest(t, handler, http.MethodPost, "/updates/apply", token, nil)
if w.Code != http.StatusForbidden {