mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-02 19:43:10 +03:00
fix compile errors and wire permission cache invalidation
Critical fixes: - Move svc creation in router.go above MountInviteRoutes/MountChannelRoutes (was used before definition — compile error) - Restore hasChannelPermREST in channel_handler.go for upload_handler.go (was removed but still referenced — compile error) Permission cache invalidation: - Add PermissionInvalidator interface to admin package - Wire through NewHandler → NewAdminAPI → handlePatchUser - Call InvalidateUser(userID) after role changes in admin panel - Update all admin test files to pass nil as new parameter Also clarifies WithTx documentation for SQLite single-writer semantics. https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
This commit is contained in:
@@ -22,11 +22,11 @@ var staticFiles embed.FS
|
||||
//
|
||||
// /api/* — admin REST API (all require ADMINISTRATOR permission)
|
||||
// /* — embedded static files (SPA; index.html for unknown paths)
|
||||
func NewHandler(database *db.DB, version string, hub HubBroadcaster, u *updater.Updater, logBuf *RingBuffer, allowedOrigins []string) http.Handler {
|
||||
func NewHandler(database *db.DB, version string, hub HubBroadcaster, u *updater.Updater, logBuf *RingBuffer, allowedOrigins []string, permInvalidator PermissionInvalidator) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Admin REST API mounted at /api
|
||||
r.Mount("/api", NewAdminAPI(database, version, hub, u, logBuf, allowedOrigins))
|
||||
r.Mount("/api", NewAdminAPI(database, version, hub, u, logBuf, allowedOrigins, permInvalidator))
|
||||
|
||||
// Static files — serve from the "static" sub-tree of the embedded FS.
|
||||
// The //go:embed static directive in this package embeds as "static/…",
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
// http.Handler with all dependencies wired.
|
||||
func TestNewHandler_ReturnsNonNilHandler(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
h := admin.NewHandler(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
h := admin.NewHandler(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
if h == nil {
|
||||
t.Fatal("NewHandler returned nil handler")
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func TestNewHandler_ReturnsNonNilHandler(t *testing.T) {
|
||||
// responds with 200 and HTML content (the embedded admin SPA).
|
||||
func TestNewHandler_ServesStaticRoot(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
h := admin.NewHandler(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
h := admin.NewHandler(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
@@ -60,7 +60,7 @@ func TestNewHandler_ServesStaticRoot(t *testing.T) {
|
||||
// Content-Security-Policy header allowing inline scripts and styles.
|
||||
func TestNewHandler_SetsCSPOnRoot(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
h := admin.NewHandler(database, "1.0.0", nil, nil, nil, nil)
|
||||
h := admin.NewHandler(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
@@ -76,7 +76,7 @@ func TestNewHandler_SetsCSPOnRoot(t *testing.T) {
|
||||
// through the NewHandler-returned handler (setup/status endpoint is unauthenticated).
|
||||
func TestNewHandler_APIRoutesMounted(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
h := admin.NewHandler(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
h := admin.NewHandler(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/setup/status", nil)
|
||||
w := httptest.NewRecorder()
|
||||
@@ -92,7 +92,7 @@ func TestNewHandler_APIRoutesMounted(t *testing.T) {
|
||||
// /api require a valid token.
|
||||
func TestNewHandler_AuthProtectedRoute(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
h := admin.NewHandler(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
h := admin.NewHandler(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
// /api/stats requires authentication
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/stats", nil)
|
||||
@@ -109,7 +109,7 @@ func TestNewHandler_AuthProtectedRoute(t *testing.T) {
|
||||
func TestNewHandler_WithUpdater(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
u := updater.NewUpdater("1.0.0", "", "J3vb", "OwnCord")
|
||||
h := admin.NewHandler(database, "1.0.0", &mockHub{}, u, nil, nil)
|
||||
h := admin.NewHandler(database, "1.0.0", &mockHub{}, u, nil, nil, nil)
|
||||
if h == nil {
|
||||
t.Fatal("NewHandler with updater returned nil handler")
|
||||
}
|
||||
@@ -148,7 +148,7 @@ func TestHandler_ServesEmbeddedFiles(t *testing.T) {
|
||||
// (position == 100) can reach backup endpoints.
|
||||
func TestOwnerOnlyMiddleware_OwnerAllowed(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
// createAdminUser creates an Owner-role user (role_id=1, position=100)
|
||||
ownerToken := createAdminUser(t, database)
|
||||
@@ -183,7 +183,7 @@ func TestOwnerOnlyMiddleware_OwnerAllowed(t *testing.T) {
|
||||
// (position < 100) cannot reach owner-only endpoints.
|
||||
func TestOwnerOnlyMiddleware_AdminDenied(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
// Create admin user (role_id=2, position=80)
|
||||
adminUID, _ := database.CreateUser("middlewareadmin", "hash", 2)
|
||||
@@ -201,7 +201,7 @@ func TestOwnerOnlyMiddleware_AdminDenied(t *testing.T) {
|
||||
// reach owner-only endpoints.
|
||||
func TestOwnerOnlyMiddleware_MemberDenied(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
memberToken := createMemberUser(t, database)
|
||||
|
||||
@@ -218,7 +218,7 @@ func TestOwnerOnlyMiddleware_MemberDenied(t *testing.T) {
|
||||
// rejected before reaching ownerOnlyMiddleware.
|
||||
func TestOwnerOnlyMiddleware_Unauthenticated(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
w := doRequest(t, handler, http.MethodPost, "/backup", "", nil)
|
||||
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ import (
|
||||
// NewAdminAPI returns a chi router with all /admin/api/* routes. All routes
|
||||
// are protected by adminAuthMiddleware which requires the ADMINISTRATOR bit,
|
||||
// except for the setup endpoints which are unauthenticated.
|
||||
func NewAdminAPI(database *db.DB, version string, hub HubBroadcaster, u *updater.Updater, logBuf *RingBuffer, allowedOrigins []string) http.Handler {
|
||||
func NewAdminAPI(database *db.DB, version string, hub HubBroadcaster, u *updater.Updater, logBuf *RingBuffer, allowedOrigins []string, permInvalidator PermissionInvalidator) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Setup endpoints — unauthenticated, only functional when no users exist.
|
||||
@@ -39,7 +39,7 @@ func NewAdminAPI(database *db.DB, version string, hub HubBroadcaster, u *updater
|
||||
|
||||
r.Get("/stats", handleGetStats(database, hub))
|
||||
r.Get("/users", handleListUsers(database))
|
||||
r.Patch("/users/{id}", handlePatchUser(database, hub))
|
||||
r.Patch("/users/{id}", handlePatchUser(database, hub, permInvalidator))
|
||||
r.Delete("/users/{id}/sessions", handleForceLogout(database))
|
||||
r.Get("/channels", handleListChannels(database))
|
||||
r.Post("/channels", handleCreateChannel(database, hub))
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// their own account via the admin panel.
|
||||
func TestAdminAPI_PatchUser_CannotModifySelf(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// The admin user created by createAdminUser has id=1. We try to patch id=1.
|
||||
@@ -37,7 +37,7 @@ func TestAdminAPI_PatchUser_CannotModifySelf(t *testing.T) {
|
||||
// banned user unbans them and returns 200.
|
||||
func TestAdminAPI_PatchUser_UnbanUser(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// Create and ban a target user first.
|
||||
@@ -61,7 +61,7 @@ func TestAdminAPI_PatchUser_UnbanUser(t *testing.T) {
|
||||
// TestAdminAPI_PatchUser_InvalidBody verifies that a non-JSON body returns 400.
|
||||
func TestAdminAPI_PatchUser_InvalidBody(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
targetUID, _ := database.CreateUser("invalidbody", "hash", 3)
|
||||
@@ -83,7 +83,7 @@ func TestAdminAPI_PatchUser_InvalidBody(t *testing.T) {
|
||||
// "type" field causes the channel to be created with type "text".
|
||||
func TestAdminAPI_CreateChannel_DefaultsTypeToText(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]any{
|
||||
@@ -108,7 +108,7 @@ func TestAdminAPI_CreateChannel_DefaultsTypeToText(t *testing.T) {
|
||||
// TestAdminAPI_CreateChannel_InvalidBody verifies that a malformed body returns 400.
|
||||
func TestAdminAPI_CreateChannel_InvalidBody(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/channels", bytes.NewReader([]byte("not-json")))
|
||||
@@ -128,7 +128,7 @@ func TestAdminAPI_CreateChannel_InvalidBody(t *testing.T) {
|
||||
// the URL returns 400.
|
||||
func TestAdminAPI_ForceLogout_InvalidID(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodDelete, "/users/notanumber/sessions", token, nil)
|
||||
@@ -144,7 +144,7 @@ func TestAdminAPI_ForceLogout_InvalidID(t *testing.T) {
|
||||
// returns 400.
|
||||
func TestAdminAPI_PatchChannel_InvalidBody(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
chID, _ := database.AdminCreateChannel("malformed", "text", "", "", 0)
|
||||
@@ -166,7 +166,7 @@ func TestAdminAPI_PatchChannel_InvalidBody(t *testing.T) {
|
||||
// to 500 (testing the queryInt cap branch).
|
||||
func TestAdminAPI_ListUsers_CapLargeLimit(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// Passing limit=9999 should be silently capped to 500.
|
||||
@@ -183,7 +183,7 @@ func TestAdminAPI_ListUsers_CapLargeLimit(t *testing.T) {
|
||||
// when no updater is configured.
|
||||
func TestAdminAPI_CheckUpdate_NilUpdater(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/updates", token, nil)
|
||||
@@ -199,7 +199,7 @@ func TestAdminAPI_CheckUpdate_NilUpdater(t *testing.T) {
|
||||
// returns 400.
|
||||
func TestAdminAPI_DeleteChannel_InvalidID(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodDelete, "/channels/notanumber", token, nil)
|
||||
@@ -215,7 +215,7 @@ func TestAdminAPI_DeleteChannel_InvalidID(t *testing.T) {
|
||||
// returns 400.
|
||||
func TestAdminAPI_PatchChannel_InvalidID(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]any{"name": "x"}
|
||||
@@ -231,7 +231,7 @@ func TestAdminAPI_PatchChannel_InvalidID(t *testing.T) {
|
||||
// TestAdminAPI_AuditLog_Pagination verifies that limit and offset params work.
|
||||
func TestAdminAPI_AuditLog_Pagination(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// Create several audit entries.
|
||||
@@ -262,7 +262,7 @@ func TestAdminAPI_AuditLog_Pagination(t *testing.T) {
|
||||
// hub is nil (the OnlineCount field defaults to 0).
|
||||
func TestAdminAPI_Stats_NilHub(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/stats", token, nil)
|
||||
@@ -289,7 +289,7 @@ func TestAdminAPI_Stats_NilHub(t *testing.T) {
|
||||
// falls back to the default (testing the queryInt error-fallback branch).
|
||||
func TestAdminAPI_AuditLog_InvalidLimitParam(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/audit-log?limit=notanumber", token, nil)
|
||||
@@ -303,7 +303,7 @@ func TestAdminAPI_AuditLog_InvalidLimitParam(t *testing.T) {
|
||||
// the default (testing the n < 1 branch of queryInt).
|
||||
func TestAdminAPI_ListUsers_InvalidLimitParam(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// limit=0 triggers the n < 1 fallback in queryInt
|
||||
@@ -321,7 +321,7 @@ func TestAdminAPI_ListUsers_InvalidLimitParam(t *testing.T) {
|
||||
// BroadcastMemberBan).
|
||||
func TestAdminAPI_PatchUser_BanNilHubDoesNotPanic(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
targetUID, _ := database.CreateUser("ban-nohub", "hash", 3)
|
||||
@@ -346,7 +346,7 @@ func TestAdminAPI_PatchUser_BanNilHubDoesNotPanic(t *testing.T) {
|
||||
func TestAdminAPI_LogStreamTicketFlow(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
logBuf := admin.NewRingBuffer(8)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, logBuf, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, logBuf, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
ticketResp := doRequest(t, handler, http.MethodPost, "/logs/ticket", token, nil)
|
||||
@@ -441,7 +441,7 @@ func TestAdminAPI_LogStreamTicketFlow(t *testing.T) {
|
||||
// around BroadcastMemberUpdate).
|
||||
func TestAdminAPI_PatchUser_RoleChangeNilHubDoesNotPanic(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
targetUID, _ := database.CreateUser("role-nohub", "hash", 3)
|
||||
@@ -466,7 +466,7 @@ func TestAdminAPI_PatchUser_RoleChangeNilHubDoesNotPanic(t *testing.T) {
|
||||
// providing ban_reason is accepted (reason defaults to empty string).
|
||||
func TestAdminAPI_PatchUser_BanWithoutReason(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
targetUID, _ := database.CreateUser("banwithout", "hash", 3)
|
||||
@@ -487,7 +487,7 @@ func TestAdminAPI_PatchUser_BanWithoutReason(t *testing.T) {
|
||||
func TestAdminAPI_PatchUser_RoleChangeBroadcast(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
hub := &mockHub{}
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", hub, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", hub, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
targetUID, _ := database.CreateUser("rolebroadcast", "hash", 3)
|
||||
@@ -509,7 +509,7 @@ func TestAdminAPI_PatchUser_RoleChangeBroadcast(t *testing.T) {
|
||||
// needs_setup=true when the database has no users.
|
||||
func TestAdminAPI_SetupStatus_NeedsSetup(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/setup/status", "", nil)
|
||||
|
||||
@@ -529,7 +529,7 @@ func TestAdminAPI_SetupStatus_NeedsSetup(t *testing.T) {
|
||||
// TestAdminAPI_SetupStatus_AlreadySetup verifies needs_setup=false when users exist.
|
||||
func TestAdminAPI_SetupStatus_AlreadySetup(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
_, _ = database.CreateUser("existing", "hash", 1)
|
||||
|
||||
@@ -550,7 +550,7 @@ func TestAdminAPI_SetupStatus_AlreadySetup(t *testing.T) {
|
||||
// session, channel, and invite.
|
||||
func TestAdminAPI_Setup_Success(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
body := map[string]string{
|
||||
"username": "owner",
|
||||
@@ -581,7 +581,7 @@ func TestAdminAPI_Setup_Success(t *testing.T) {
|
||||
// when users already exist.
|
||||
func TestAdminAPI_Setup_AlreadyCompleted(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
_, _ = database.CreateUser("existing", "hash", 1)
|
||||
|
||||
@@ -600,7 +600,7 @@ func TestAdminAPI_Setup_AlreadyCompleted(t *testing.T) {
|
||||
// username or password returns 400.
|
||||
func TestAdminAPI_Setup_MissingFields(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
body := map[string]string{
|
||||
"username": "",
|
||||
@@ -616,7 +616,7 @@ func TestAdminAPI_Setup_MissingFields(t *testing.T) {
|
||||
// TestAdminAPI_Setup_WeakPassword verifies that a weak password is rejected.
|
||||
func TestAdminAPI_Setup_WeakPassword(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
body := map[string]string{
|
||||
"username": "owner",
|
||||
@@ -632,7 +632,7 @@ func TestAdminAPI_Setup_WeakPassword(t *testing.T) {
|
||||
// TestAdminAPI_Setup_InvalidBody verifies that a non-JSON body returns 400.
|
||||
func TestAdminAPI_Setup_InvalidBody(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/setup", bytes.NewReader([]byte("not-json")))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
+46
-46
@@ -198,7 +198,7 @@ func doRequest(t *testing.T, handler http.Handler, method, path, token string, b
|
||||
|
||||
func TestAdminAPI_Stats_OK(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/stats", token, nil)
|
||||
@@ -221,7 +221,7 @@ func TestAdminAPI_Stats_OK(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_Stats_Unauthenticated(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/stats", "", nil)
|
||||
|
||||
@@ -232,7 +232,7 @@ func TestAdminAPI_Stats_Unauthenticated(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_Stats_Forbidden(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createMemberUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/stats", token, nil)
|
||||
@@ -246,7 +246,7 @@ func TestAdminAPI_Stats_Forbidden(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_ListUsers_OK(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/users?limit=50&offset=0", token, nil)
|
||||
@@ -267,7 +267,7 @@ func TestAdminAPI_ListUsers_OK(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_ListUsers_DefaultPagination(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// No query params — should use defaults
|
||||
@@ -280,7 +280,7 @@ func TestAdminAPI_ListUsers_DefaultPagination(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_ListUsers_Unauthenticated(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/users", "", nil)
|
||||
|
||||
@@ -293,7 +293,7 @@ func TestAdminAPI_ListUsers_Unauthenticated(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_PatchUser_BanUser(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// Create a target user
|
||||
@@ -321,7 +321,7 @@ func TestAdminAPI_PatchUser_BanUser(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_PatchUser_ChangeRole(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
targetUID, _ := database.CreateUser("rolechange", "hash", 3)
|
||||
@@ -343,7 +343,7 @@ func TestAdminAPI_PatchUser_ChangeRole(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_PatchUser_NotFound(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]any{"banned": true}
|
||||
@@ -356,7 +356,7 @@ func TestAdminAPI_PatchUser_NotFound(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_PatchUser_InvalidID(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodPatch, "/users/abc", token, nil)
|
||||
@@ -370,7 +370,7 @@ func TestAdminAPI_PatchUser_InvalidID(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_ForceLogout_OK(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
targetUID, _ := database.CreateUser("logoutme", "hash", 3)
|
||||
@@ -390,7 +390,7 @@ func TestAdminAPI_ForceLogout_OK(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_ForceLogout_Unauthenticated(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
w := doRequest(t, handler, http.MethodDelete, "/users/1/sessions", "", nil)
|
||||
|
||||
@@ -403,7 +403,7 @@ func TestAdminAPI_ForceLogout_Unauthenticated(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_ListChannels_OK(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
_, _ = database.AdminCreateChannel("general", "text", "", "", 0)
|
||||
@@ -427,7 +427,7 @@ func TestAdminAPI_ListChannels_OK(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_CreateChannel_OK(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]any{
|
||||
@@ -454,7 +454,7 @@ func TestAdminAPI_CreateChannel_OK(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_CreateChannel_MissingName(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]any{
|
||||
@@ -471,7 +471,7 @@ func TestAdminAPI_CreateChannel_MissingName(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_UpdateChannel_OK(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
chID, _ := database.AdminCreateChannel("old", "text", "", "", 0)
|
||||
@@ -492,7 +492,7 @@ func TestAdminAPI_UpdateChannel_OK(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_UpdateChannel_NotFound(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]any{"name": "x"}
|
||||
@@ -507,7 +507,7 @@ func TestAdminAPI_UpdateChannel_NotFound(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_DeleteChannel_OK(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
chID, _ := database.AdminCreateChannel("del-me", "text", "", "", 0)
|
||||
@@ -521,7 +521,7 @@ func TestAdminAPI_DeleteChannel_OK(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_DeleteChannel_NotFound(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodDelete, "/channels/99999", token, nil)
|
||||
@@ -535,7 +535,7 @@ func TestAdminAPI_DeleteChannel_NotFound(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_AuditLog_OK(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
uid, _ := database.CreateUser("actor", "hash", 1)
|
||||
@@ -558,7 +558,7 @@ func TestAdminAPI_AuditLog_OK(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_AuditLog_Empty(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/audit-log", token, nil)
|
||||
@@ -578,7 +578,7 @@ func TestAdminAPI_AuditLog_Empty(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_GetSettings_OK(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/settings", token, nil)
|
||||
@@ -600,7 +600,7 @@ func TestAdminAPI_GetSettings_OK(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_PatchSettings_OK(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]string{
|
||||
@@ -625,7 +625,7 @@ func TestAdminAPI_PatchSettings_OK(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_PatchSettings_InvalidBody(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPatch, "/settings", bytes.NewReader([]byte("not-json")))
|
||||
@@ -642,7 +642,7 @@ func TestAdminAPI_PatchSettings_InvalidBody(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_Backup_RequiresOwner(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
// Admin (role 2) can authenticate but is not Owner (role 1, position 100)
|
||||
adminUID, _ := database.CreateUser("adminonly", "hash", 2)
|
||||
@@ -659,7 +659,7 @@ func TestAdminAPI_Backup_RequiresOwner(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_Backup_Unauthenticated(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
w := doRequest(t, handler, http.MethodPost, "/backup", "", nil)
|
||||
|
||||
@@ -676,7 +676,7 @@ func TestAdminAPI_Backup_Unauthenticated(t *testing.T) {
|
||||
// which logs an audit entry containing the actor_id.
|
||||
func TestAdminAPI_ActorFromContext_AuditEntry(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// Create a target user to act on.
|
||||
@@ -710,7 +710,7 @@ func TestAdminAPI_ActorFromContext_AuditEntry(t *testing.T) {
|
||||
// DELETE /users/{id}/sessions path.
|
||||
func TestAdminAPI_ActorFromContext_ForceLogout(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
targetUID, _ := database.CreateUser("logoutctx", "hash", 3)
|
||||
@@ -742,7 +742,7 @@ func TestAdminAPI_ActorFromContext_ForceLogout(t *testing.T) {
|
||||
// returns 400 without writing anything to the database.
|
||||
func TestAdminAPI_PatchSettings_RejectsUnknownKey(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]string{
|
||||
@@ -768,7 +768,7 @@ func TestAdminAPI_PatchSettings_RejectsUnknownKey(t *testing.T) {
|
||||
// containing both valid and invalid keys is rejected entirely (no partial write).
|
||||
func TestAdminAPI_PatchSettings_RejectsMixedKeys(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]string{
|
||||
@@ -812,7 +812,7 @@ func TestAdminAPI_PatchSettings_AcceptsAllWhitelistedKeys(t *testing.T) {
|
||||
for _, key := range whitelistedKeys {
|
||||
t.Run(key, func(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
value := "testvalue"
|
||||
@@ -833,7 +833,7 @@ func TestAdminAPI_PatchSettings_AcceptsAllWhitelistedKeys(t *testing.T) {
|
||||
// (no-op update) is accepted and returns the current settings.
|
||||
func TestAdminAPI_PatchSettings_EmptyPayloadIsOK(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]string{}
|
||||
@@ -846,7 +846,7 @@ func TestAdminAPI_PatchSettings_EmptyPayloadIsOK(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_PatchSettings_RejectsRequire2FAWhenUsersNotEnrolled(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]string{
|
||||
@@ -862,7 +862,7 @@ func TestAdminAPI_PatchSettings_RejectsRequire2FAWhenUsersNotEnrolled(t *testing
|
||||
|
||||
func TestAdminAPI_PatchSettings_AllowsRequire2FAWhenAllUsersEnrolledAndRegistrationClosed(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
if _, err := database.Exec(`UPDATE users SET totp_secret = ? WHERE id = 1`, "JBSWY3DPEHPK3PXP"); err != nil {
|
||||
@@ -882,7 +882,7 @@ func TestAdminAPI_PatchSettings_AllowsRequire2FAWhenAllUsersEnrolledAndRegistrat
|
||||
|
||||
func TestAdminAPI_PatchSettings_RejectsInvalidBooleanValue(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]string{
|
||||
@@ -901,7 +901,7 @@ func TestAdminAPI_PatchSettings_RejectsInvalidBooleanValue(t *testing.T) {
|
||||
// expose the PasswordHash field in any returned user object.
|
||||
func TestAdminAPI_ListUsers_NoPasswordHash(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// Create a second user so the list is non-trivial.
|
||||
@@ -928,7 +928,7 @@ func TestAdminAPI_ListUsers_NoPasswordHash(t *testing.T) {
|
||||
// expose the TOTPSecret field.
|
||||
func TestAdminAPI_ListUsers_NoTOTPSecret(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/users", token, nil)
|
||||
@@ -947,7 +947,7 @@ func TestAdminAPI_ListUsers_NoTOTPSecret(t *testing.T) {
|
||||
// are still present after the sensitive-field removal.
|
||||
func TestAdminAPI_ListUsers_PublicFieldsPresent(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/users", token, nil)
|
||||
@@ -976,7 +976,7 @@ func TestAdminAPI_ListUsers_PublicFieldsPresent(t *testing.T) {
|
||||
// not expose PasswordHash in the returned user object.
|
||||
func TestAdminAPI_PatchUser_NoPasswordHash(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
targetUID, _ := database.CreateUser("patchvictim", "topsecretbcrypt", 3)
|
||||
@@ -1004,7 +1004,7 @@ func TestAdminAPI_PatchUser_NoPasswordHash(t *testing.T) {
|
||||
// not expose TOTPSecret in the returned user object.
|
||||
func TestAdminAPI_PatchUser_NoTOTPSecret(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
targetUID, _ := database.CreateUser("patchtotp", "hash", 3)
|
||||
@@ -1077,7 +1077,7 @@ func (m *mockHub) ClientCount() int {
|
||||
func TestAdminAPI_CreateChannel_BroadcastsChannelCreate(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
hub := &mockHub{}
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", hub, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", hub, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]any{
|
||||
@@ -1100,7 +1100,7 @@ func TestAdminAPI_CreateChannel_BroadcastsChannelCreate(t *testing.T) {
|
||||
func TestAdminAPI_CreateChannel_NilHubDoesNotPanic(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
// nil hub: handler must not panic
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]any{"name": "safe-channel", "type": "text"}
|
||||
@@ -1114,7 +1114,7 @@ func TestAdminAPI_CreateChannel_NilHubDoesNotPanic(t *testing.T) {
|
||||
func TestAdminAPI_UpdateChannel_BroadcastsChannelUpdate(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
hub := &mockHub{}
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", hub, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", hub, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
chID, _ := database.AdminCreateChannel("before", "text", "", "", 0)
|
||||
@@ -1135,7 +1135,7 @@ func TestAdminAPI_UpdateChannel_BroadcastsChannelUpdate(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_UpdateChannel_NilHubDoesNotPanic(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
chID, _ := database.AdminCreateChannel("patchme", "text", "", "", 0)
|
||||
@@ -1150,7 +1150,7 @@ func TestAdminAPI_UpdateChannel_NilHubDoesNotPanic(t *testing.T) {
|
||||
func TestAdminAPI_DeleteChannel_BroadcastsChannelDelete(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
hub := &mockHub{}
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", hub, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", hub, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
chID, _ := database.AdminCreateChannel("delete-me", "text", "", "", 0)
|
||||
@@ -1170,7 +1170,7 @@ func TestAdminAPI_DeleteChannel_BroadcastsChannelDelete(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_DeleteChannel_NilHubDoesNotPanic(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
chID, _ := database.AdminCreateChannel("del-no-hub", "text", "", "", 0)
|
||||
|
||||
@@ -40,7 +40,7 @@ func chdirTemp(t *testing.T) string {
|
||||
func TestHandleBackup_Success(t *testing.T) {
|
||||
tmpDir := chdirTemp(t)
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodPost, "/backup", token, nil)
|
||||
@@ -75,7 +75,7 @@ func TestHandleBackup_Success(t *testing.T) {
|
||||
func TestHandleBackup_RequiresOwner(t *testing.T) {
|
||||
_ = chdirTemp(t)
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
adminUID, _ := database.CreateUser("backupadmin", "hash", 2)
|
||||
token := "backup-admin-token"
|
||||
@@ -95,7 +95,7 @@ func TestHandleBackup_RequiresOwner(t *testing.T) {
|
||||
func TestHandleListBackups_EmptyWhenNoDirExists(t *testing.T) {
|
||||
_ = chdirTemp(t)
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/backups", token, nil)
|
||||
@@ -118,7 +118,7 @@ func TestHandleListBackups_EmptyWhenNoDirExists(t *testing.T) {
|
||||
func TestHandleListBackups_ReturnsCreatedBackup(t *testing.T) {
|
||||
_ = chdirTemp(t)
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// Create a backup first.
|
||||
@@ -160,7 +160,7 @@ func TestHandleListBackups_ReturnsCreatedBackup(t *testing.T) {
|
||||
func TestHandleDeleteBackup_Success(t *testing.T) {
|
||||
tmpDir := chdirTemp(t)
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// Create a real backup file to delete.
|
||||
@@ -191,7 +191,7 @@ func TestHandleDeleteBackup_Success(t *testing.T) {
|
||||
func TestHandleDeleteBackup_NotFound(t *testing.T) {
|
||||
_ = chdirTemp(t)
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodDelete, "/backups/nonexistent.db", token, nil)
|
||||
@@ -206,7 +206,7 @@ func TestHandleDeleteBackup_NotFound(t *testing.T) {
|
||||
func TestHandleDeleteBackup_InvalidNameTraversal(t *testing.T) {
|
||||
_ = chdirTemp(t)
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// The chi router URL-decodes the path parameter, so ".." arrives decoded.
|
||||
@@ -224,7 +224,7 @@ func TestHandleDeleteBackup_InvalidNameTraversal(t *testing.T) {
|
||||
func TestHandleDeleteBackup_RequiresOwner(t *testing.T) {
|
||||
tmpDir := chdirTemp(t)
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
adminUID, _ := database.CreateUser("deladmin", "hash", 2)
|
||||
token := "del-admin-token"
|
||||
@@ -249,7 +249,7 @@ func TestHandleDeleteBackup_RequiresOwner(t *testing.T) {
|
||||
func TestHandleRestoreBackup_Success(t *testing.T) {
|
||||
tmpDir := chdirTemp(t)
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// Set up backup and data directories.
|
||||
@@ -293,7 +293,7 @@ func TestHandleRestoreBackup_Success(t *testing.T) {
|
||||
func TestHandleRestoreBackup_NotFound(t *testing.T) {
|
||||
_ = chdirTemp(t)
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodPost, "/backups/missing.db/restore", token, nil)
|
||||
@@ -308,7 +308,7 @@ func TestHandleRestoreBackup_NotFound(t *testing.T) {
|
||||
func TestHandleRestoreBackup_InvalidName(t *testing.T) {
|
||||
_ = chdirTemp(t)
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodPost, "/backups/..evil.db/restore", token, nil)
|
||||
@@ -324,7 +324,7 @@ func TestHandleRestoreBackup_InvalidName(t *testing.T) {
|
||||
func TestHandleListBackups_ErrorReadingDir(t *testing.T) {
|
||||
tmpDir := chdirTemp(t)
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// Create data/ directory but make "backups" a file instead of a directory.
|
||||
@@ -352,7 +352,7 @@ func TestHandleListBackups_ErrorReadingDir(t *testing.T) {
|
||||
func TestHandleRestoreBackup_RequiresOwner(t *testing.T) {
|
||||
tmpDir := chdirTemp(t)
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
|
||||
adminUID, _ := database.CreateUser("restoreadmin", "hash", 2)
|
||||
token := "restore-admin-token"
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
func TestCreateChannel_TextUnderTextCategory_OK(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]any{
|
||||
@@ -28,7 +28,7 @@ func TestCreateChannel_TextUnderTextCategory_OK(t *testing.T) {
|
||||
|
||||
func TestCreateChannel_AnnouncementUnderTextCategory_OK(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]any{
|
||||
@@ -44,7 +44,7 @@ func TestCreateChannel_AnnouncementUnderTextCategory_OK(t *testing.T) {
|
||||
|
||||
func TestCreateChannel_VoiceUnderVoiceCategory_OK(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]any{
|
||||
@@ -60,7 +60,7 @@ func TestCreateChannel_VoiceUnderVoiceCategory_OK(t *testing.T) {
|
||||
|
||||
func TestCreateChannel_VoiceUnderTextCategory_Rejected(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]any{
|
||||
@@ -83,7 +83,7 @@ func TestCreateChannel_VoiceUnderTextCategory_Rejected(t *testing.T) {
|
||||
|
||||
func TestCreateChannel_TextUnderVoiceCategory_Rejected(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]any{
|
||||
@@ -99,7 +99,7 @@ func TestCreateChannel_TextUnderVoiceCategory_Rejected(t *testing.T) {
|
||||
|
||||
func TestCreateChannel_EmptyCategory_Allowed(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
body := map[string]any{
|
||||
@@ -115,7 +115,7 @@ func TestCreateChannel_EmptyCategory_Allowed(t *testing.T) {
|
||||
|
||||
func TestCreateChannel_CaseInsensitiveVoiceCategory(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", &mockHub{}, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
// "VOICE" in uppercase should still be treated as a voice category
|
||||
|
||||
@@ -51,7 +51,7 @@ type patchUserRequest struct {
|
||||
BanReason *string `json:"ban_reason"`
|
||||
}
|
||||
|
||||
func handlePatchUser(database *db.DB, hub HubBroadcaster) http.HandlerFunc {
|
||||
func handlePatchUser(database *db.DB, hub HubBroadcaster, permInvalidator PermissionInvalidator) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathInt64(r, "id")
|
||||
if err != nil {
|
||||
@@ -103,6 +103,9 @@ func handlePatchUser(database *db.DB, hub HubBroadcaster) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
slog.Info("role changed", "actor_id", actor, "target_user", user.Username, "new_role_id", *req.RoleID)
|
||||
if permInvalidator != nil {
|
||||
permInvalidator.InvalidateUser(id)
|
||||
}
|
||||
}
|
||||
|
||||
banReason := ""
|
||||
|
||||
@@ -249,7 +249,7 @@ func TestOwnerOnlyMiddleware_OwnerPassesThrough(t *testing.T) {
|
||||
// role_id has been set to a nonexistent value returns 401.
|
||||
func TestAdminAuthMiddleware_RoleNotFound(t *testing.T) {
|
||||
database := openWhiteboxTestDB(t)
|
||||
handler := NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
uid, err := database.CreateUser("noroleuser", "$2a$12$x", 1)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
// session has expired is rejected with 401.
|
||||
func TestAdminAuthMiddleware_ExpiredSession(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
// Create a user and session, then manually expire the session by setting
|
||||
// expires_at to a past timestamp via the exported Exec helper.
|
||||
@@ -52,7 +52,7 @@ func TestAdminAuthMiddleware_ExpiredSession(t *testing.T) {
|
||||
// Authorization header returns 401.
|
||||
func TestAdminAuthMiddleware_MissingBearer(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/stats", "", nil)
|
||||
|
||||
@@ -65,7 +65,7 @@ func TestAdminAuthMiddleware_MissingBearer(t *testing.T) {
|
||||
// sessions table returns 401.
|
||||
func TestAdminAuthMiddleware_InvalidToken(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/stats", "completely-invalid-token", nil)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func TestSetupStatus_NeedsSetup(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
rr := doRequest(t, handler, "GET", "/setup/status", "", nil)
|
||||
if rr.Code != http.StatusOK {
|
||||
@@ -32,7 +32,7 @@ func TestSetupStatus_NeedsSetup(t *testing.T) {
|
||||
func TestSetupStatus_NoSetupNeeded(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
createAdminUser(t, database) // Create a user first
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
rr := doRequest(t, handler, "GET", "/setup/status", "", nil)
|
||||
if rr.Code != http.StatusOK {
|
||||
@@ -52,7 +52,7 @@ func TestSetupStatus_NoSetupNeeded(t *testing.T) {
|
||||
|
||||
func TestSetup_CreatesOwner(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
rr := doRequest(t, handler, "POST", "/setup", "", map[string]string{
|
||||
"username": "myadmin",
|
||||
@@ -97,7 +97,7 @@ func TestSetup_CreatesOwner(t *testing.T) {
|
||||
|
||||
func TestSetup_BlockedAfterFirstUser(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
// First setup succeeds.
|
||||
rr := doRequest(t, handler, "POST", "/setup", "", map[string]string{
|
||||
@@ -120,7 +120,7 @@ func TestSetup_BlockedAfterFirstUser(t *testing.T) {
|
||||
|
||||
func TestSetup_WeakPassword(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
rr := doRequest(t, handler, "POST", "/setup", "", map[string]string{
|
||||
"username": "admin",
|
||||
@@ -133,7 +133,7 @@ func TestSetup_WeakPassword(t *testing.T) {
|
||||
|
||||
func TestSetup_MissingFields(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
rr := doRequest(t, handler, "POST", "/setup", "", map[string]string{
|
||||
"username": "",
|
||||
@@ -148,7 +148,7 @@ func TestSetup_MissingFields(t *testing.T) {
|
||||
// server and asserts that exactly one owner is created (BUG-119).
|
||||
func TestSetup_ConcurrentRace(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
const goroutines = 20
|
||||
results := make(chan int, goroutines)
|
||||
|
||||
@@ -43,6 +43,15 @@ type HubBroadcaster interface {
|
||||
ClientCount() int
|
||||
}
|
||||
|
||||
// ─── PermissionInvalidator ───────────────────────────────────────────────────
|
||||
|
||||
// PermissionInvalidator allows admin handlers to invalidate the permission
|
||||
// cache when roles or permissions change. Satisfied by *service.PermissionService.
|
||||
type PermissionInvalidator interface {
|
||||
InvalidateUser(userID int64)
|
||||
InvalidateAll()
|
||||
}
|
||||
|
||||
// ─── adminUserResponse ──────────────────────────────────────────────────────
|
||||
|
||||
// adminUserResponse is the safe public shape returned by user-listing and
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestAdminAPI_CheckUpdate_OK(t *testing.T) {
|
||||
u.SetBaseURL(mockGH.URL)
|
||||
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, u, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, u, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/updates", token, nil)
|
||||
@@ -73,7 +73,7 @@ func TestAdminAPI_CheckUpdate_IncompleteReleaseNotInstallable(t *testing.T) {
|
||||
u.SetBaseURL(mockGH.URL)
|
||||
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, u, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, u, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/updates", token, nil)
|
||||
@@ -106,7 +106,7 @@ func TestAdminAPI_CheckUpdate_UpToDate(t *testing.T) {
|
||||
u.SetBaseURL(mockGH.URL)
|
||||
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, u, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, u, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/updates", token, nil)
|
||||
@@ -123,7 +123,7 @@ func TestAdminAPI_CheckUpdate_UpToDate(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_CheckUpdate_Unauthenticated(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
w := doRequest(t, handler, http.MethodGet, "/updates", "", nil)
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
@@ -133,7 +133,7 @@ func TestAdminAPI_CheckUpdate_Unauthenticated(t *testing.T) {
|
||||
|
||||
func TestAdminAPI_ApplyUpdate_RequiresOwner(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
// Create admin user (not owner - role 2)
|
||||
adminUID, _ := database.CreateUser("adminonly2", "hash", 2)
|
||||
@@ -153,7 +153,7 @@ func TestAdminAPI_ApplyUpdate_RequiresOwner(t *testing.T) {
|
||||
func TestAdminAPI_ApplyUpdate_NilUpdater(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
// nil updater — the endpoint should return 503
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodPost, "/updates/apply", token, nil)
|
||||
@@ -166,7 +166,7 @@ func TestAdminAPI_ApplyUpdate_NilUpdater(t *testing.T) {
|
||||
// in the 503 response.
|
||||
func TestAdminAPI_ApplyUpdate_NilUpdater_ErrorCode(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodPost, "/updates/apply", token, nil)
|
||||
@@ -198,7 +198,7 @@ func TestAdminAPI_ApplyUpdate_NoUpdateAvailable(t *testing.T) {
|
||||
u.SetBaseURL(mockGH.URL)
|
||||
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, u, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, u, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodPost, "/updates/apply", token, nil)
|
||||
@@ -229,7 +229,7 @@ func TestAdminAPI_ApplyUpdate_CheckFails(t *testing.T) {
|
||||
u.SetBaseURL(mockGH.URL)
|
||||
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, u, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, u, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodPost, "/updates/apply", token, nil)
|
||||
@@ -257,7 +257,7 @@ func TestAdminAPI_ApplyUpdate_MissingAssets(t *testing.T) {
|
||||
u.SetBaseURL(mockGH.URL)
|
||||
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, u, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, u, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodPost, "/updates/apply", token, nil)
|
||||
@@ -278,7 +278,7 @@ func TestAdminAPI_ApplyUpdate_MissingAssets(t *testing.T) {
|
||||
// unauthenticated requests to POST /updates/apply.
|
||||
func TestAdminAPI_ApplyUpdate_Unauthenticated(t *testing.T) {
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, nil, nil, nil, nil)
|
||||
|
||||
w := doRequest(t, handler, http.MethodPost, "/updates/apply", "", nil)
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
@@ -345,7 +345,7 @@ func TestAdminAPI_ApplyUpdate_DownloadFails(t *testing.T) {
|
||||
// the important thing is that the code path is executed.
|
||||
|
||||
database := openAdminTestDB(t)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, u, nil, nil)
|
||||
handler := admin.NewAdminAPI(database, "1.0.0", nil, u, nil, nil, nil)
|
||||
token := createAdminUser(t, database)
|
||||
|
||||
w := doRequest(t, handler, http.MethodPost, "/updates/apply", token, nil)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/owncord/server/auth"
|
||||
"github.com/owncord/server/db"
|
||||
"github.com/owncord/server/permissions"
|
||||
"github.com/owncord/server/service"
|
||||
)
|
||||
|
||||
@@ -65,6 +66,24 @@ func MountChannelRoutes(r chi.Router, database *db.DB, svc *service.Services, li
|
||||
).Get("/api/v1/search", handleSearch(svc))
|
||||
}
|
||||
|
||||
// hasChannelPermREST checks whether the role has the given permission on the channel,
|
||||
// accounting for Administrator bypass and channel overrides.
|
||||
// Used by non-migrated handlers (e.g., upload_handler.go).
|
||||
func hasChannelPermREST(database *db.DB, role *db.Role, channelID, perm int64) bool {
|
||||
if role == nil {
|
||||
return false
|
||||
}
|
||||
if permissions.HasAdmin(role.Permissions) {
|
||||
return true
|
||||
}
|
||||
allow, deny, err := database.GetChannelPermissions(channelID, role.ID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
effective := permissions.EffectivePerms(role.Permissions, allow, deny)
|
||||
return effective&perm == perm
|
||||
}
|
||||
|
||||
// handleListChannels returns all channels the authenticated user can see.
|
||||
func handleListChannels(svc *service.Services) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -84,6 +84,10 @@ func NewRouter(cfg *config.Config, database *db.DB, ver string, logBuf *admin.Ri
|
||||
// auto-generates a key when none exists.
|
||||
}
|
||||
|
||||
// Service layer — centralizes business logic for REST and WS handlers.
|
||||
st := dbstore.NewSQLiteStore(database)
|
||||
svc := service.New(st, limiter)
|
||||
|
||||
// Auth routes: register, login, logout, me.
|
||||
MountAuthRoutes(r, database, limiter, cfg.Server.TrustedProxies, totpKey)
|
||||
|
||||
@@ -113,10 +117,6 @@ func NewRouter(cfg *config.Config, database *db.DB, ver string, logBuf *admin.Ri
|
||||
MountUploadRoutes(r, database, store, limiter, cfg.Server.AllowedOrigins)
|
||||
}
|
||||
|
||||
// Service layer — centralizes business logic for REST and WS handlers.
|
||||
st := dbstore.NewSQLiteStore(database)
|
||||
svc := service.New(st, limiter)
|
||||
|
||||
// WebSocket hub — WS does its own in-band auth, so no AuthMiddleware here.
|
||||
hub := ws.NewHub(database, limiter, svc)
|
||||
getOnlineUsers = func() int { return hub.ClientCount() }
|
||||
@@ -206,7 +206,7 @@ func NewRouter(cfg *config.Config, database *db.DB, ver string, logBuf *admin.Ri
|
||||
// Admin panel: static files + REST API (Phase 6).
|
||||
// Restrict /admin to configured CIDRs (default: private networks only).
|
||||
u := updater.NewUpdater(ver, cfg.GitHub.Token, "J3vb", "OwnCord")
|
||||
adminHandler := admin.NewHandler(database, ver, hub, u, logBuf, cfg.Server.AllowedOrigins)
|
||||
adminHandler := admin.NewHandler(database, ver, hub, u, logBuf, cfg.Server.AllowedOrigins, svc.Permissions)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(AdminIPRestrict(cfg.Server.AdminAllowedCIDRs, cfg.Server.TrustedProxies))
|
||||
r.Mount("/admin", adminHandler)
|
||||
|
||||
+10
-3
@@ -37,14 +37,21 @@ func (s *SQLiteStore) Close() error { return s.db.Close() }
|
||||
// SQLDb returns the underlying *sql.DB.
|
||||
func (s *SQLiteStore) SQLDb() *sql.DB { return s.db.SQLDb() }
|
||||
|
||||
// WithTx executes fn within a transaction.
|
||||
// WithTx executes fn within a transaction. For SQLite, all writes are
|
||||
// serialized through a single connection (MaxOpenConns=1), so the transaction
|
||||
// is started and committed on the same underlying connection that fn's
|
||||
// store calls use.
|
||||
//
|
||||
// TODO: implement properly with a transaction-scoped Store wrapper when
|
||||
// services need multi-statement transactions.
|
||||
func (s *SQLiteStore) WithTx(ctx context.Context, fn func(Store) error) error {
|
||||
// SQLite serializes all writes through one connection, so starting a
|
||||
// transaction and calling fn(s) effectively wraps fn's DB calls in
|
||||
// that transaction — provided MaxOpenConns remains 1.
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// For SQLite with single writer, the transaction operates on the same
|
||||
// *db.DB — we pass the same store since SQLite serializes writes.
|
||||
if txErr := fn(s); txErr != nil {
|
||||
_ = tx.Rollback()
|
||||
return txErr
|
||||
|
||||
Reference in New Issue
Block a user