mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user