From f0a6aeb17bddf619953e7c6f7d3fdf9a86c06d08 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Thu, 16 Apr 2026 13:11:51 -0700 Subject: [PATCH 1/6] refactor(db): remove user/team provisioning triggers --- .../internal/handlers/team_handlers_test.go | 68 +++---- ...00_remove_user_team_provision_triggers.sql | 183 ++++++++++++++++++ packages/db/scripts/seed/postgres/seed-db.go | 31 +-- packages/local-dev/seed-local-database.go | 8 + spec/openapi-dashboard.yml | 2 +- tests/integration/internal/utils/user.go | 7 + tests/integration/seed.go | 8 + 7 files changed, 253 insertions(+), 54 deletions(-) create mode 100644 packages/db/migrations/20260416120000_remove_user_team_provision_triggers.sql diff --git a/packages/dashboard-api/internal/handlers/team_handlers_test.go b/packages/dashboard-api/internal/handlers/team_handlers_test.go index 7fd9e7709d..378eda2a96 100644 --- a/packages/dashboard-api/internal/handlers/team_handlers_test.go +++ b/packages/dashboard-api/internal/handlers/team_handlers_test.go @@ -284,9 +284,31 @@ VALUES ($1, $2, $3) t.Fatalf("failed to create test user: %v", err) } + if err := db.AuthDB.Write.UpsertPublicUser(t.Context(), authqueries.UpsertPublicUserParams{ + ID: userID, + Email: email, + }); err != nil { + t.Fatalf("failed to create public user: %v", err) + } + return userID } +func bootstrapHandlerTestUser(t *testing.T, db *testutils.Database, userID uuid.UUID) { + t.Helper() + + store := &APIStore{ + db: db.SqlcClient, + authDB: db.AuthDB, + supabaseDB: db.SupabaseDB, + teamProvisionSink: &fakeTeamProvisionSink{}, + } + + if _, err := store.bootstrapUser(t.Context(), userID); err != nil { + t.Fatalf("failed to bootstrap test user: %v", err) + } +} + func handlerTestUserEmail(userID uuid.UUID) string { return "user-" + userID.String() + "@example.com" } @@ -311,17 +333,6 @@ func TestPostUsersBootstrap_CreatesDefaultTeamAndCallsSink(t *testing.T) { userID := createHandlerTestUser(t, testDB) sink := &fakeTeamProvisionSink{} - existingTeam, err := testDB.AuthDB.Write.GetDefaultTeamByUserID(ctx, userID) - if err != nil { - t.Fatalf("expected trigger-created default team: %v", err) - } - if err := testDB.AuthDB.Write.DeleteTeamByID(ctx, existingTeam.ID); err != nil { - t.Fatalf("failed to remove trigger-created default team: %v", err) - } - if err := testDB.AuthDB.Write.DeletePublicUser(ctx, userID); err != nil { - t.Fatalf("failed to remove trigger-created public user: %v", err) - } - recorder := httptest.NewRecorder() ginCtx, _ := gin.CreateTestContext(recorder) ginCtx.Request = httptest.NewRequestWithContext(ctx, http.MethodPost, "/", nil) @@ -378,17 +389,6 @@ func TestPostUsersBootstrap_ProvisioningFailureKeepsCreatedDefaultTeam(t *testin }, } - existingTeam, err := testDB.AuthDB.Write.GetDefaultTeamByUserID(ctx, userID) - if err != nil { - t.Fatalf("expected trigger-created default team: %v", err) - } - if err := testDB.AuthDB.Write.DeleteTeamByID(ctx, existingTeam.ID); err != nil { - t.Fatalf("failed to remove trigger-created default team: %v", err) - } - if err := testDB.AuthDB.Write.DeletePublicUser(ctx, userID); err != nil { - t.Fatalf("failed to remove trigger-created public user: %v", err) - } - recorder := httptest.NewRecorder() ginCtx, _ := gin.CreateTestContext(recorder) ginCtx.Request = httptest.NewRequestWithContext(ctx, http.MethodPost, "/", nil) @@ -437,14 +437,6 @@ func TestBootstrapUser_ConcurrentRequestsCreateSingleDefaultTeam(t *testing.T) { userID := createHandlerTestUser(t, testDB) sink := &fakeTeamProvisionSink{} - existingTeam, err := testDB.AuthDB.Write.GetDefaultTeamByUserID(ctx, userID) - if err != nil { - t.Fatalf("expected trigger-created default team: %v", err) - } - if err := testDB.AuthDB.Write.DeleteTeamByID(ctx, existingTeam.ID); err != nil { - t.Fatalf("failed to remove trigger-created default team: %v", err) - } - store := &APIStore{ db: testDB.SqlcClient, authDB: testDB.AuthDB, @@ -491,7 +483,7 @@ func TestBootstrapUser_ConcurrentRequestsCreateSingleDefaultTeam(t *testing.T) { } var defaultTeamCount int - err = testDB.AuthDB.TestsRawSQLQuery(ctx, + err := testDB.AuthDB.TestsRawSQLQuery(ctx, `SELECT count(*) FROM public.users_teams WHERE user_id = $1 AND is_default = true`, @@ -570,6 +562,7 @@ func TestPostTeams_LocalPolicyDeniedReturnsBadRequestWithoutCreatingTeam(t *test testDB := testutils.SetupDatabase(t) ctx := t.Context() userID := createHandlerTestUser(t, testDB) + bootstrapHandlerTestUser(t, testDB, userID) sink := &fakeTeamProvisionSink{} for range 2 { @@ -627,6 +620,7 @@ func TestPostTeams_InvalidNameReturnsBadRequest(t *testing.T) { testDB := testutils.SetupDatabase(t) ctx := t.Context() userID := createHandlerTestUser(t, testDB) + bootstrapHandlerTestUser(t, testDB, userID) for _, body := range []string{`{}`, `{"name":""}`, `{"name":" "}`} { recorder := httptest.NewRecorder() @@ -659,6 +653,7 @@ func TestPostTeams_InvalidRequestBodyReturnsBadRequest(t *testing.T) { testDB := testutils.SetupDatabase(t) ctx := t.Context() userID := createHandlerTestUser(t, testDB) + bootstrapHandlerTestUser(t, testDB, userID) sink := &fakeTeamProvisionSink{} recorder := httptest.NewRecorder() @@ -692,6 +687,7 @@ func TestPostTeams_TrimsNameBeforeCreate(t *testing.T) { testDB := testutils.SetupDatabase(t) ctx := t.Context() userID := createHandlerTestUser(t, testDB) + bootstrapHandlerTestUser(t, testDB, userID) sink := &fakeTeamProvisionSink{} recorder := httptest.NewRecorder() @@ -739,6 +735,7 @@ func TestPostTeams_ProvisioningFailureRollsBackCreatedTeam(t *testing.T) { testDB := testutils.SetupDatabase(t) ctx := t.Context() userID := createHandlerTestUser(t, testDB) + bootstrapHandlerTestUser(t, testDB, userID) sink := &fakeTeamProvisionSink{ err: &internalteamprovision.ProvisionError{ StatusCode: http.StatusBadRequest, @@ -798,6 +795,7 @@ func TestPostTeams_ProvisioningFailurePreservesProvisionErrorStatus(t *testing.T testDB := testutils.SetupDatabase(t) ctx := t.Context() userID := createHandlerTestUser(t, testDB) + bootstrapHandlerTestUser(t, testDB, userID) sink := &fakeTeamProvisionSink{ err: &internalteamprovision.ProvisionError{ StatusCode: tt.status, @@ -868,14 +866,6 @@ func TestCreateTeam_ConcurrentRequestsRespectLocalPolicyWithZeroMemberships(t *t ctx := t.Context() userID := createHandlerTestUser(t, testDB) - existingTeam, err := testDB.AuthDB.Write.GetDefaultTeamByUserID(ctx, userID) - if err != nil { - t.Fatalf("expected trigger-created default team: %v", err) - } - if err := testDB.AuthDB.Write.DeleteTeamByID(ctx, existingTeam.ID); err != nil { - t.Fatalf("failed to remove default team: %v", err) - } - store := &APIStore{ db: testDB.SqlcClient, authDB: testDB.AuthDB, diff --git a/packages/db/migrations/20260416120000_remove_user_team_provision_triggers.sql b/packages/db/migrations/20260416120000_remove_user_team_provision_triggers.sql new file mode 100644 index 0000000000..795b3f5193 --- /dev/null +++ b/packages/db/migrations/20260416120000_remove_user_team_provision_triggers.sql @@ -0,0 +1,183 @@ +-- +goose Up + +-- The application now owns auth user projection and default team bootstrap. +-- Remove the legacy database triggers/functions that used to keep public.users +-- in sync and auto-create default teams on signup. + +DROP TRIGGER IF EXISTS sync_inserts_to_public_users ON auth.users; +DROP TRIGGER IF EXISTS sync_updates_to_public_users ON auth.users; +DROP TRIGGER IF EXISTS sync_deletes_to_public_users ON auth.users; +DROP TRIGGER IF EXISTS post_user_signup ON public.users; + +DROP FUNCTION IF EXISTS public.sync_insert_auth_users_to_public_users_trigger(); +DROP FUNCTION IF EXISTS public.sync_update_auth_users_to_public_users_trigger(); +DROP FUNCTION IF EXISTS public.sync_delete_auth_users_to_public_users_trigger(); +DROP FUNCTION IF EXISTS public.post_user_signup(); +DROP FUNCTION IF EXISTS public.extra_for_post_user_signup(uuid, uuid); + +DROP POLICY IF EXISTS "Allow to create a new user" ON public.users; +DROP POLICY IF EXISTS "Allow to select a user" ON public.users; +DROP POLICY IF EXISTS "Allow to update a user" ON public.users; +DROP POLICY IF EXISTS "Allow to delete a user" ON public.users; + +DROP POLICY IF EXISTS "Allow to create a team to new user" ON public.teams; +DROP POLICY IF EXISTS "Allow to create a user team connection to new user" ON public.users_teams; +DROP POLICY IF EXISTS "Allow to select a team for supabase auth admin" ON public.teams; + +REVOKE INSERT ON public.users FROM trigger_user; +REVOKE SELECT (id) ON public.users FROM trigger_user; +REVOKE UPDATE ON public.users FROM trigger_user; +REVOKE DELETE ON public.users FROM trigger_user; + +REVOKE SELECT, INSERT, TRIGGER ON public.teams FROM trigger_user; +REVOKE INSERT ON public.users_teams FROM trigger_user; + +-- +goose Down +-- +goose StatementBegin + +GRANT SELECT, INSERT, TRIGGER ON public.teams TO trigger_user; +GRANT INSERT ON public.users_teams TO trigger_user; +GRANT INSERT ON public.users TO trigger_user; +GRANT SELECT (id) ON public.users TO trigger_user; +GRANT UPDATE ON public.users TO trigger_user; +GRANT DELETE ON public.users TO trigger_user; + +CREATE POLICY "Allow to create a new user" + ON public.users + AS PERMISSIVE + FOR INSERT + TO trigger_user + WITH CHECK (TRUE); + +CREATE POLICY "Allow to select a user" + ON public.users + AS PERMISSIVE + FOR SELECT + TO trigger_user + USING (true); + +CREATE POLICY "Allow to update a user" + ON public.users + AS PERMISSIVE + FOR UPDATE + TO trigger_user + USING (true) + WITH CHECK (true); + +CREATE POLICY "Allow to delete a user" + ON public.users + AS PERMISSIVE + FOR DELETE + TO trigger_user + USING (true); + +CREATE POLICY "Allow to create a team to new user" + ON public.teams + AS PERMISSIVE + FOR INSERT + TO trigger_user + WITH CHECK (TRUE); + +CREATE POLICY "Allow to create a user team connection to new user" + ON public.users_teams + AS PERMISSIVE + FOR INSERT + TO trigger_user + WITH CHECK (TRUE); + +CREATE POLICY "Allow to select a team for supabase auth admin" + ON public.teams + AS PERMISSIVE + FOR SELECT + TO trigger_user + USING (TRUE); + +CREATE OR REPLACE FUNCTION public.extra_for_post_user_signup(user_id uuid, team_id uuid) + RETURNS void + LANGUAGE plpgsql +AS $extra_for_post_user_signup$ +DECLARE +BEGIN +END +$extra_for_post_user_signup$ SECURITY DEFINER SET search_path = public; + +ALTER FUNCTION public.extra_for_post_user_signup(uuid, uuid) OWNER TO trigger_user; + +CREATE OR REPLACE FUNCTION public.post_user_signup() + RETURNS TRIGGER + LANGUAGE plpgsql +AS $post_user_signup$ +DECLARE + team_id uuid; +BEGIN + RAISE NOTICE 'Creating default team for user %', NEW.id; + INSERT INTO public.teams(name, tier, email) VALUES (NEW.email, 'base_v1', NEW.email) RETURNING id INTO team_id; + INSERT INTO public.users_teams(user_id, team_id, is_default) VALUES (NEW.id, team_id, true); + RAISE NOTICE 'Created default team for user % and team %', NEW.id, team_id; + + PERFORM public.extra_for_post_user_signup(NEW.id, team_id); + + RETURN NEW; +END +$post_user_signup$ SECURITY DEFINER SET search_path = public; + +ALTER FUNCTION public.post_user_signup() OWNER TO trigger_user; + +CREATE OR REPLACE FUNCTION public.sync_insert_auth_users_to_public_users_trigger() RETURNS TRIGGER +LANGUAGE plpgsql +AS $func$ +BEGIN + INSERT INTO public.users (id, email) + VALUES (NEW.id, NEW.email); + + RETURN NEW; +END; +$func$ SECURITY DEFINER SET search_path = public; + +CREATE OR REPLACE FUNCTION public.sync_update_auth_users_to_public_users_trigger() RETURNS TRIGGER +LANGUAGE plpgsql +AS $func$ +BEGIN + UPDATE public.users + SET email = NEW.email, + updated_at = now() + WHERE id = NEW.id; + + IF NOT FOUND THEN + RAISE EXCEPTION 'User with id % does not exist in public.users', NEW.id; + END IF; + + RETURN NEW; +END; +$func$ SECURITY DEFINER SET search_path = public; + +CREATE OR REPLACE FUNCTION public.sync_delete_auth_users_to_public_users_trigger() RETURNS TRIGGER +LANGUAGE plpgsql +AS $func$ +BEGIN + DELETE FROM public.users WHERE id = OLD.id; + RETURN OLD; +END; +$func$ SECURITY DEFINER SET search_path = public; + +ALTER FUNCTION public.sync_insert_auth_users_to_public_users_trigger() OWNER TO trigger_user; +ALTER FUNCTION public.sync_update_auth_users_to_public_users_trigger() OWNER TO trigger_user; +ALTER FUNCTION public.sync_delete_auth_users_to_public_users_trigger() OWNER TO trigger_user; + +CREATE TRIGGER sync_inserts_to_public_users + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.sync_insert_auth_users_to_public_users_trigger(); + +CREATE TRIGGER sync_updates_to_public_users + AFTER UPDATE ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.sync_update_auth_users_to_public_users_trigger(); + +CREATE TRIGGER sync_deletes_to_public_users + AFTER DELETE ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.sync_delete_auth_users_to_public_users_trigger(); + +CREATE TRIGGER post_user_signup + AFTER INSERT ON public.users + FOR EACH ROW EXECUTE FUNCTION public.post_user_signup(); + +-- +goose StatementEnd diff --git a/packages/db/scripts/seed/postgres/seed-db.go b/packages/db/scripts/seed/postgres/seed-db.go index fc2b524f41..50a868e2a8 100644 --- a/packages/db/scripts/seed/postgres/seed-db.go +++ b/packages/db/scripts/seed/postgres/seed-db.go @@ -79,7 +79,7 @@ func main() { // Clean up existing data for idempotent re-seeding. // Delete child rows that have ON DELETE NO ACTION constraints - // before deleting the team and user. + // before deleting the team and user rows. err = authDb.TestsRawSQL(ctx, ` DELETE FROM envs WHERE team_id IN (SELECT id FROM teams WHERE email = $1) `, email) @@ -102,13 +102,13 @@ DELETE FROM volumes WHERE team_id IN (SELECT id FROM teams WHERE email = $1) } err = authDb.TestsRawSQL(ctx, ` -DELETE FROM addons WHERE added_by IN (SELECT id FROM auth.users WHERE email = $1) +DELETE FROM addons WHERE added_by IN (SELECT id FROM public.users WHERE email = $1) `, email) if err != nil { panic(err) } - // Now safe to delete team (team_api_keys, users_teams cascade automatically) + // Now safe to delete team (team_api_keys cascade automatically). err = authDb.TestsRawSQL(ctx, ` DELETE FROM teams WHERE email = $1 `, email) @@ -116,7 +116,15 @@ DELETE FROM teams WHERE email = $1 panic(err) } - // Now safe to delete user (access_tokens cascade automatically) + // Delete the projected user row so access_tokens and users_teams cascade. + err = authDb.TestsRawSQL(ctx, ` +DELETE FROM public.users WHERE email = $1 +`, email) + if err != nil { + panic(err) + } + + // Delete the auth user separately. err = authDb.TestsRawSQL(ctx, ` DELETE FROM auth.users WHERE email = $1 `, email) @@ -124,11 +132,7 @@ DELETE FROM auth.users WHERE email = $1 panic(err) } - // Create the user - // NOTE: Inserting into auth.users fires the sync_inserts_to_public_users trigger, - // which inserts into public.users, which fires the post_user_signup trigger, - // which auto-creates a default team + users_teams row. We delete that - // trigger-created team below so we can create our own with a known ID/name. + // Create the auth user and its projected public user row explicitly. userID := uuid.New() err = authDb.TestsRawSQL(ctx, ` INSERT INTO auth.users (id, email) @@ -138,11 +142,10 @@ VALUES ($1, $2) panic(err) } - // Remove the team auto-created by the post_user_signup trigger so we can - // create our own seed team with a deterministic ID and custom name/slug. - err = authDb.TestsRawSQL(ctx, ` -DELETE FROM teams WHERE email = $1 -`, email) + err = authDb.Write.UpsertPublicUser(ctx, authqueries.UpsertPublicUserParams{ + ID: userID, + Email: email, + }) if err != nil { panic(err) } diff --git a/packages/local-dev/seed-local-database.go b/packages/local-dev/seed-local-database.go index fa7fd62770..7b79288df0 100644 --- a/packages/local-dev/seed-local-database.go +++ b/packages/local-dev/seed-local-database.go @@ -181,6 +181,14 @@ ON CONFLICT (id) DO UPDATE SET return fmt.Errorf("failed to upsert user: %w", err) } + err = db.Write.UpsertPublicUser(ctx, authqueries.UpsertPublicUserParams{ + ID: userID, + Email: "user@e2b-dev.local", + }) + if err != nil { + return fmt.Errorf("failed to upsert public user: %w", err) + } + return nil } diff --git a/spec/openapi-dashboard.yml b/spec/openapi-dashboard.yml index f848845da2..1de7279cdb 100644 --- a/spec/openapi-dashboard.yml +++ b/spec/openapi-dashboard.yml @@ -214,7 +214,7 @@ components: nextCursor: type: string nullable: true - description: Cursor to pass to the next list request, or `null` if there is no next page. + description: Cursor to pass to the next list request, or `null` if there is no next page. BuildStatusItem: type: object diff --git a/tests/integration/internal/utils/user.go b/tests/integration/internal/utils/user.go index d766adceb9..ac4584e5fa 100644 --- a/tests/integration/internal/utils/user.go +++ b/tests/integration/internal/utils/user.go @@ -25,7 +25,14 @@ VALUES ($1, $2) `, userID, fmt.Sprintf("user-test-integration-%s@e2b.dev", userID)) require.NoError(t, err) + err = db.AuthDb.Write.UpsertPublicUser(t.Context(), authqueries.UpsertPublicUserParams{ + ID: userID, + Email: fmt.Sprintf("user-test-integration-%s@e2b.dev", userID), + }) + require.NoError(t, err) + t.Cleanup(func() { + _ = db.AuthDb.Write.DeletePublicUser(t.Context(), userID) db.AuthDb.TestsRawSQL(t.Context(), ` DELETE FROM auth.users WHERE id = $1 `, userID) diff --git a/tests/integration/seed.go b/tests/integration/seed.go index f550ca6c3f..be18bfddcc 100644 --- a/tests/integration/seed.go +++ b/tests/integration/seed.go @@ -97,6 +97,14 @@ VALUES ($1, $2) return fmt.Errorf("failed to create user: %w", err) } + err = authdb.Write.UpsertPublicUser(ctx, authqueries.UpsertPublicUserParams{ + ID: data.UserID, + Email: "user-test-integration@e2b.dev", + }) + if err != nil { + return fmt.Errorf("failed to create public user: %w", err) + } + // Access token tokenWithoutPrefix := strings.TrimPrefix(data.AccessToken, keys.AccessTokenPrefix) accessTokenBytes, err := hex.DecodeString(tokenWithoutPrefix) From 0add9142aa67a9886d2c67e5aab190dd24e9d934 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Thu, 16 Apr 2026 13:11:55 -0700 Subject: [PATCH 2/6] fix(dashboard-api): bootstrap users with admin token only --- .../dashboard-api/internal/api/api.gen.go | 125 ++++++++------- .../internal/api/api_auth_test.go | 143 ++++++++++++++++++ .../internal/handlers/team_handlers_test.go | 7 +- .../internal/handlers/team_provisioning.go | 5 +- spec/openapi-dashboard.yml | 5 +- 5 files changed, 218 insertions(+), 67 deletions(-) create mode 100644 packages/dashboard-api/internal/api/api_auth_test.go diff --git a/packages/dashboard-api/internal/api/api.gen.go b/packages/dashboard-api/internal/api/api.gen.go index 00ac397e5c..f4170c453a 100644 --- a/packages/dashboard-api/internal/api/api.gen.go +++ b/packages/dashboard-api/internal/api/api.gen.go @@ -345,8 +345,8 @@ type PostTeamsTeamIDMembersJSONRequestBody = AddTeamMemberRequest // ServerInterface represents all server handlers. type ServerInterface interface { // Bootstrap user - // (POST /admin/users/bootstrap) - PostAdminUsersBootstrap(c *gin.Context) + // (POST /admin/users/{userId}/bootstrap) + PostAdminUsersUserIdBootstrap(c *gin.Context, userId UserId) // List team builds // (GET /builds) GetBuilds(c *gin.Context, params GetBuildsParams) @@ -397,12 +397,21 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(c *gin.Context) -// PostAdminUsersBootstrap operation middleware -func (siw *ServerInterfaceWrapper) PostAdminUsersBootstrap(c *gin.Context) { +// PostAdminUsersUserIdBootstrap operation middleware +func (siw *ServerInterfaceWrapper) PostAdminUsersUserIdBootstrap(c *gin.Context) { - c.Set(AdminTokenAuthScopes, []string{}) + var err error - c.Set(Supabase1TokenAuthScopes, []string{}) + // ------------- Path parameter "userId" ------------- + var userId UserId + + err = runtime.BindStyledParameterWithOptions("simple", "userId", c.Param("userId"), &userId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter userId: %w", err), http.StatusBadRequest) + return + } + + c.Set(AdminTokenAuthScopes, []string{}) for _, middleware := range siw.HandlerMiddlewares { middleware(c) @@ -411,7 +420,7 @@ func (siw *ServerInterfaceWrapper) PostAdminUsersBootstrap(c *gin.Context) { } } - siw.Handler.PostAdminUsersBootstrap(c) + siw.Handler.PostAdminUsersUserIdBootstrap(c, userId) } // GetBuilds operation middleware @@ -802,7 +811,7 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options ErrorHandler: errorHandler, } - router.POST(options.BaseURL+"/admin/users/bootstrap", wrapper.PostAdminUsersBootstrap) + router.POST(options.BaseURL+"/admin/users/:userId/bootstrap", wrapper.PostAdminUsersUserIdBootstrap) router.GET(options.BaseURL+"/builds", wrapper.GetBuilds) router.GET(options.BaseURL+"/builds/statuses", wrapper.GetBuildsStatuses) router.GET(options.BaseURL+"/builds/:build_id", wrapper.GetBuildsBuildId) @@ -821,56 +830,56 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "OLVzw/9GAAA=", + "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", + "AAAA///Y9cxiL0cAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/packages/dashboard-api/internal/api/api_auth_test.go b/packages/dashboard-api/internal/api/api_auth_test.go new file mode 100644 index 0000000000..060a71e249 --- /dev/null +++ b/packages/dashboard-api/internal/api/api_auth_test.go @@ -0,0 +1,143 @@ +package api + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + ginmiddleware "github.com/oapi-codegen/gin-middleware" + + sharedauth "github.com/e2b-dev/infra/packages/auth/pkg/auth" +) + +type authTestServer struct { + receivedUserID uuid.UUID + hitBootstrap bool +} + +func (s *authTestServer) PostAdminUsersUserIdBootstrap(c *gin.Context, userId UserId) { + s.hitBootstrap = true + s.receivedUserID = uuid.UUID(userId) + c.Status(http.StatusNoContent) +} + +func (s *authTestServer) GetBuilds(c *gin.Context, params GetBuildsParams) { + panic("unexpected call to GetBuilds") +} + +func (s *authTestServer) GetBuildsStatuses(c *gin.Context, params GetBuildsStatusesParams) { + panic("unexpected call to GetBuildsStatuses") +} + +func (s *authTestServer) GetBuildsBuildId(c *gin.Context, buildId BuildId) { + panic("unexpected call to GetBuildsBuildId") +} + +func (s *authTestServer) GetHealth(c *gin.Context) { + panic("unexpected call to GetHealth") +} + +func (s *authTestServer) GetSandboxesSandboxIDRecord(c *gin.Context, sandboxID SandboxID) { + panic("unexpected call to GetSandboxesSandboxIDRecord") +} + +func (s *authTestServer) GetTeams(c *gin.Context) { + panic("unexpected call to GetTeams") +} + +func (s *authTestServer) PostTeams(c *gin.Context) { + panic("unexpected call to PostTeams") +} + +func (s *authTestServer) GetTeamsResolve(c *gin.Context, params GetTeamsResolveParams) { + panic("unexpected call to GetTeamsResolve") +} + +func (s *authTestServer) PatchTeamsTeamID(c *gin.Context, teamID TeamID) { + panic("unexpected call to PatchTeamsTeamID") +} + +func (s *authTestServer) GetTeamsTeamIDMembers(c *gin.Context, teamID TeamID) { + panic("unexpected call to GetTeamsTeamIDMembers") +} + +func (s *authTestServer) PostTeamsTeamIDMembers(c *gin.Context, teamID TeamID) { + panic("unexpected call to PostTeamsTeamIDMembers") +} + +func (s *authTestServer) DeleteTeamsTeamIDMembersUserId(c *gin.Context, teamID TeamID, userId UserId) { + panic("unexpected call to DeleteTeamsTeamIDMembersUserId") +} + +func (s *authTestServer) GetTemplatesDefaults(c *gin.Context) { + panic("unexpected call to GetTemplatesDefaults") +} + +func TestAdminBootstrapRoute_AcceptsAdminTokenOnly(t *testing.T) { + t.Parallel() + + gin.SetMode(gin.TestMode) + + server := &authTestServer{} + swagger, err := GetSwagger() + if err != nil { + t.Fatalf("failed to load swagger: %v", err) + } + swagger.Servers = nil + + supabaseCalled := false + authenticationFunc := sharedauth.CreateAuthenticationFunc( + []sharedauth.Authenticator{ + sharedauth.NewAdminTokenAuthenticator("super-secret-token"), + sharedauth.NewSupabaseTokenAuthenticator(func(_ context.Context, _ *gin.Context, _ string) (uuid.UUID, *sharedauth.APIError) { + supabaseCalled = true + return uuid.Nil, &sharedauth.APIError{Code: http.StatusUnauthorized, ClientMsg: "unexpected", Err: fmt.Errorf("unexpected supabase auth call")} + }), + }, + nil, + ) + + r := gin.New() + r.Use(ginmiddleware.OapiRequestValidatorWithOptions(swagger, &ginmiddleware.Options{ + ErrorHandler: func(c *gin.Context, message string, statusCode int) { + c.AbortWithStatusJSON(statusCode, gin.H{"code": statusCode, "message": message}) + }, + MultiErrorHandler: func(me openapi3.MultiError) error { + msgs := make([]string, 0, len(me)) + for _, e := range me { + msgs = append(msgs, e.Error()) + } + + return fmt.Errorf("%s", strings.Join(msgs, "; ")) + }, + Options: openapi3filter.Options{AuthenticationFunc: authenticationFunc}, + })) + RegisterHandlers(r, server) + + targetUserID := uuid.New() + req := httptest.NewRequest(http.MethodPost, "/admin/users/"+targetUserID.String()+"/bootstrap", nil) + req.Header.Set(sharedauth.HeaderAdminToken, "super-secret-token") + recorder := httptest.NewRecorder() + + r.ServeHTTP(recorder, req) + + if recorder.Code != http.StatusNoContent { + t.Fatalf("expected status 204, got %d with body %s", recorder.Code, recorder.Body.String()) + } + if !server.hitBootstrap { + t.Fatal("expected bootstrap handler to be called") + } + if server.receivedUserID != targetUserID { + t.Fatalf("expected user id %s, got %s", targetUserID, server.receivedUserID) + } + if supabaseCalled { + t.Fatal("expected route to authenticate without calling Supabase auth") + } +} diff --git a/packages/dashboard-api/internal/handlers/team_handlers_test.go b/packages/dashboard-api/internal/handlers/team_handlers_test.go index 378eda2a96..4a1f38021a 100644 --- a/packages/dashboard-api/internal/handlers/team_handlers_test.go +++ b/packages/dashboard-api/internal/handlers/team_handlers_test.go @@ -17,6 +17,7 @@ import ( "github.com/e2b-dev/infra/packages/auth/pkg/auth" authtypes "github.com/e2b-dev/infra/packages/auth/pkg/types" + "github.com/e2b-dev/infra/packages/dashboard-api/internal/api" internalteamprovision "github.com/e2b-dev/infra/packages/dashboard-api/internal/teamprovision" authqueries "github.com/e2b-dev/infra/packages/db/pkg/auth/queries" "github.com/e2b-dev/infra/packages/db/pkg/testutils" @@ -336,7 +337,6 @@ func TestPostUsersBootstrap_CreatesDefaultTeamAndCallsSink(t *testing.T) { recorder := httptest.NewRecorder() ginCtx, _ := gin.CreateTestContext(recorder) ginCtx.Request = httptest.NewRequestWithContext(ctx, http.MethodPost, "/", nil) - auth.SetUserID(ginCtx, userID) store := &APIStore{ db: testDB.SqlcClient, @@ -344,7 +344,7 @@ func TestPostUsersBootstrap_CreatesDefaultTeamAndCallsSink(t *testing.T) { supabaseDB: testDB.SupabaseDB, teamProvisionSink: sink, } - store.PostAdminUsersBootstrap(ginCtx) + store.PostAdminUsersUserIdBootstrap(ginCtx, api.UserId(userID)) if recorder.Code != http.StatusOK { t.Fatalf("expected status 200, got %d", recorder.Code) @@ -392,7 +392,6 @@ func TestPostUsersBootstrap_ProvisioningFailureKeepsCreatedDefaultTeam(t *testin recorder := httptest.NewRecorder() ginCtx, _ := gin.CreateTestContext(recorder) ginCtx.Request = httptest.NewRequestWithContext(ctx, http.MethodPost, "/", nil) - auth.SetUserID(ginCtx, userID) store := &APIStore{ db: testDB.SqlcClient, @@ -400,7 +399,7 @@ func TestPostUsersBootstrap_ProvisioningFailureKeepsCreatedDefaultTeam(t *testin supabaseDB: testDB.SupabaseDB, teamProvisionSink: sink, } - store.PostAdminUsersBootstrap(ginCtx) + store.PostAdminUsersUserIdBootstrap(ginCtx, api.UserId(userID)) if recorder.Code != http.StatusOK { t.Fatalf("expected status 200, got %d", recorder.Code) diff --git a/packages/dashboard-api/internal/handlers/team_provisioning.go b/packages/dashboard-api/internal/handlers/team_provisioning.go index 70e1f51e45..c02cddf264 100644 --- a/packages/dashboard-api/internal/handlers/team_provisioning.go +++ b/packages/dashboard-api/internal/handlers/team_provisioning.go @@ -40,12 +40,11 @@ type provisionedTeam struct { BlockedReason *string } -func (s *APIStore) PostAdminUsersBootstrap(c *gin.Context) { +func (s *APIStore) PostAdminUsersUserIdBootstrap(c *gin.Context, userId api.UserId) { ctx := c.Request.Context() telemetry.ReportEvent(ctx, "bootstrap user") - userID := auth.MustGetUserID(c) - team, err := s.bootstrapUser(ctx, userID) + team, err := s.bootstrapUser(ctx, userId) if err != nil { s.handleProvisioningError(ctx, c, "bootstrap user", err) diff --git a/spec/openapi-dashboard.yml b/spec/openapi-dashboard.yml index 1de7279cdb..fecc23b0ff 100644 --- a/spec/openapi-dashboard.yml +++ b/spec/openapi-dashboard.yml @@ -757,13 +757,14 @@ paths: "500": $ref: "#/components/responses/500" - /admin/users/bootstrap: + /admin/users/{userId}/bootstrap: post: summary: Bootstrap user tags: [teams] security: - AdminTokenAuth: [] - Supabase1TokenAuth: [] + parameters: + - $ref: "#/components/parameters/userId" responses: "200": description: Successfully bootstrapped user. From e6a5f2e35ad0b139fcff522e33dea2813be96cd9 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Thu, 16 Apr 2026 13:34:59 -0700 Subject: [PATCH 3/6] Revert "fix(dashboard-api): bootstrap users with admin token only" This reverts commit 0add9142aa67a9886d2c67e5aab190dd24e9d934. --- .../dashboard-api/internal/api/api.gen.go | 125 +++++++-------- .../internal/api/api_auth_test.go | 143 ------------------ .../internal/handlers/team_handlers_test.go | 7 +- .../internal/handlers/team_provisioning.go | 5 +- spec/openapi-dashboard.yml | 5 +- 5 files changed, 67 insertions(+), 218 deletions(-) delete mode 100644 packages/dashboard-api/internal/api/api_auth_test.go diff --git a/packages/dashboard-api/internal/api/api.gen.go b/packages/dashboard-api/internal/api/api.gen.go index f4170c453a..00ac397e5c 100644 --- a/packages/dashboard-api/internal/api/api.gen.go +++ b/packages/dashboard-api/internal/api/api.gen.go @@ -345,8 +345,8 @@ type PostTeamsTeamIDMembersJSONRequestBody = AddTeamMemberRequest // ServerInterface represents all server handlers. type ServerInterface interface { // Bootstrap user - // (POST /admin/users/{userId}/bootstrap) - PostAdminUsersUserIdBootstrap(c *gin.Context, userId UserId) + // (POST /admin/users/bootstrap) + PostAdminUsersBootstrap(c *gin.Context) // List team builds // (GET /builds) GetBuilds(c *gin.Context, params GetBuildsParams) @@ -397,22 +397,13 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(c *gin.Context) -// PostAdminUsersUserIdBootstrap operation middleware -func (siw *ServerInterfaceWrapper) PostAdminUsersUserIdBootstrap(c *gin.Context) { - - var err error - - // ------------- Path parameter "userId" ------------- - var userId UserId - - err = runtime.BindStyledParameterWithOptions("simple", "userId", c.Param("userId"), &userId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter userId: %w", err), http.StatusBadRequest) - return - } +// PostAdminUsersBootstrap operation middleware +func (siw *ServerInterfaceWrapper) PostAdminUsersBootstrap(c *gin.Context) { c.Set(AdminTokenAuthScopes, []string{}) + c.Set(Supabase1TokenAuthScopes, []string{}) + for _, middleware := range siw.HandlerMiddlewares { middleware(c) if c.IsAborted() { @@ -420,7 +411,7 @@ func (siw *ServerInterfaceWrapper) PostAdminUsersUserIdBootstrap(c *gin.Context) } } - siw.Handler.PostAdminUsersUserIdBootstrap(c, userId) + siw.Handler.PostAdminUsersBootstrap(c) } // GetBuilds operation middleware @@ -811,7 +802,7 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options ErrorHandler: errorHandler, } - router.POST(options.BaseURL+"/admin/users/:userId/bootstrap", wrapper.PostAdminUsersUserIdBootstrap) + router.POST(options.BaseURL+"/admin/users/bootstrap", wrapper.PostAdminUsersBootstrap) router.GET(options.BaseURL+"/builds", wrapper.GetBuilds) router.GET(options.BaseURL+"/builds/statuses", wrapper.GetBuildsStatuses) router.GET(options.BaseURL+"/builds/:build_id", wrapper.GetBuildsBuildId) @@ -830,56 +821,56 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "AAAA///Y9cxiL0cAAA==", + "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", + "OLVzw/9GAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/packages/dashboard-api/internal/api/api_auth_test.go b/packages/dashboard-api/internal/api/api_auth_test.go deleted file mode 100644 index 060a71e249..0000000000 --- a/packages/dashboard-api/internal/api/api_auth_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package api - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/openapi3filter" - "github.com/gin-gonic/gin" - "github.com/google/uuid" - ginmiddleware "github.com/oapi-codegen/gin-middleware" - - sharedauth "github.com/e2b-dev/infra/packages/auth/pkg/auth" -) - -type authTestServer struct { - receivedUserID uuid.UUID - hitBootstrap bool -} - -func (s *authTestServer) PostAdminUsersUserIdBootstrap(c *gin.Context, userId UserId) { - s.hitBootstrap = true - s.receivedUserID = uuid.UUID(userId) - c.Status(http.StatusNoContent) -} - -func (s *authTestServer) GetBuilds(c *gin.Context, params GetBuildsParams) { - panic("unexpected call to GetBuilds") -} - -func (s *authTestServer) GetBuildsStatuses(c *gin.Context, params GetBuildsStatusesParams) { - panic("unexpected call to GetBuildsStatuses") -} - -func (s *authTestServer) GetBuildsBuildId(c *gin.Context, buildId BuildId) { - panic("unexpected call to GetBuildsBuildId") -} - -func (s *authTestServer) GetHealth(c *gin.Context) { - panic("unexpected call to GetHealth") -} - -func (s *authTestServer) GetSandboxesSandboxIDRecord(c *gin.Context, sandboxID SandboxID) { - panic("unexpected call to GetSandboxesSandboxIDRecord") -} - -func (s *authTestServer) GetTeams(c *gin.Context) { - panic("unexpected call to GetTeams") -} - -func (s *authTestServer) PostTeams(c *gin.Context) { - panic("unexpected call to PostTeams") -} - -func (s *authTestServer) GetTeamsResolve(c *gin.Context, params GetTeamsResolveParams) { - panic("unexpected call to GetTeamsResolve") -} - -func (s *authTestServer) PatchTeamsTeamID(c *gin.Context, teamID TeamID) { - panic("unexpected call to PatchTeamsTeamID") -} - -func (s *authTestServer) GetTeamsTeamIDMembers(c *gin.Context, teamID TeamID) { - panic("unexpected call to GetTeamsTeamIDMembers") -} - -func (s *authTestServer) PostTeamsTeamIDMembers(c *gin.Context, teamID TeamID) { - panic("unexpected call to PostTeamsTeamIDMembers") -} - -func (s *authTestServer) DeleteTeamsTeamIDMembersUserId(c *gin.Context, teamID TeamID, userId UserId) { - panic("unexpected call to DeleteTeamsTeamIDMembersUserId") -} - -func (s *authTestServer) GetTemplatesDefaults(c *gin.Context) { - panic("unexpected call to GetTemplatesDefaults") -} - -func TestAdminBootstrapRoute_AcceptsAdminTokenOnly(t *testing.T) { - t.Parallel() - - gin.SetMode(gin.TestMode) - - server := &authTestServer{} - swagger, err := GetSwagger() - if err != nil { - t.Fatalf("failed to load swagger: %v", err) - } - swagger.Servers = nil - - supabaseCalled := false - authenticationFunc := sharedauth.CreateAuthenticationFunc( - []sharedauth.Authenticator{ - sharedauth.NewAdminTokenAuthenticator("super-secret-token"), - sharedauth.NewSupabaseTokenAuthenticator(func(_ context.Context, _ *gin.Context, _ string) (uuid.UUID, *sharedauth.APIError) { - supabaseCalled = true - return uuid.Nil, &sharedauth.APIError{Code: http.StatusUnauthorized, ClientMsg: "unexpected", Err: fmt.Errorf("unexpected supabase auth call")} - }), - }, - nil, - ) - - r := gin.New() - r.Use(ginmiddleware.OapiRequestValidatorWithOptions(swagger, &ginmiddleware.Options{ - ErrorHandler: func(c *gin.Context, message string, statusCode int) { - c.AbortWithStatusJSON(statusCode, gin.H{"code": statusCode, "message": message}) - }, - MultiErrorHandler: func(me openapi3.MultiError) error { - msgs := make([]string, 0, len(me)) - for _, e := range me { - msgs = append(msgs, e.Error()) - } - - return fmt.Errorf("%s", strings.Join(msgs, "; ")) - }, - Options: openapi3filter.Options{AuthenticationFunc: authenticationFunc}, - })) - RegisterHandlers(r, server) - - targetUserID := uuid.New() - req := httptest.NewRequest(http.MethodPost, "/admin/users/"+targetUserID.String()+"/bootstrap", nil) - req.Header.Set(sharedauth.HeaderAdminToken, "super-secret-token") - recorder := httptest.NewRecorder() - - r.ServeHTTP(recorder, req) - - if recorder.Code != http.StatusNoContent { - t.Fatalf("expected status 204, got %d with body %s", recorder.Code, recorder.Body.String()) - } - if !server.hitBootstrap { - t.Fatal("expected bootstrap handler to be called") - } - if server.receivedUserID != targetUserID { - t.Fatalf("expected user id %s, got %s", targetUserID, server.receivedUserID) - } - if supabaseCalled { - t.Fatal("expected route to authenticate without calling Supabase auth") - } -} diff --git a/packages/dashboard-api/internal/handlers/team_handlers_test.go b/packages/dashboard-api/internal/handlers/team_handlers_test.go index 4a1f38021a..378eda2a96 100644 --- a/packages/dashboard-api/internal/handlers/team_handlers_test.go +++ b/packages/dashboard-api/internal/handlers/team_handlers_test.go @@ -17,7 +17,6 @@ import ( "github.com/e2b-dev/infra/packages/auth/pkg/auth" authtypes "github.com/e2b-dev/infra/packages/auth/pkg/types" - "github.com/e2b-dev/infra/packages/dashboard-api/internal/api" internalteamprovision "github.com/e2b-dev/infra/packages/dashboard-api/internal/teamprovision" authqueries "github.com/e2b-dev/infra/packages/db/pkg/auth/queries" "github.com/e2b-dev/infra/packages/db/pkg/testutils" @@ -337,6 +336,7 @@ func TestPostUsersBootstrap_CreatesDefaultTeamAndCallsSink(t *testing.T) { recorder := httptest.NewRecorder() ginCtx, _ := gin.CreateTestContext(recorder) ginCtx.Request = httptest.NewRequestWithContext(ctx, http.MethodPost, "/", nil) + auth.SetUserID(ginCtx, userID) store := &APIStore{ db: testDB.SqlcClient, @@ -344,7 +344,7 @@ func TestPostUsersBootstrap_CreatesDefaultTeamAndCallsSink(t *testing.T) { supabaseDB: testDB.SupabaseDB, teamProvisionSink: sink, } - store.PostAdminUsersUserIdBootstrap(ginCtx, api.UserId(userID)) + store.PostAdminUsersBootstrap(ginCtx) if recorder.Code != http.StatusOK { t.Fatalf("expected status 200, got %d", recorder.Code) @@ -392,6 +392,7 @@ func TestPostUsersBootstrap_ProvisioningFailureKeepsCreatedDefaultTeam(t *testin recorder := httptest.NewRecorder() ginCtx, _ := gin.CreateTestContext(recorder) ginCtx.Request = httptest.NewRequestWithContext(ctx, http.MethodPost, "/", nil) + auth.SetUserID(ginCtx, userID) store := &APIStore{ db: testDB.SqlcClient, @@ -399,7 +400,7 @@ func TestPostUsersBootstrap_ProvisioningFailureKeepsCreatedDefaultTeam(t *testin supabaseDB: testDB.SupabaseDB, teamProvisionSink: sink, } - store.PostAdminUsersUserIdBootstrap(ginCtx, api.UserId(userID)) + store.PostAdminUsersBootstrap(ginCtx) if recorder.Code != http.StatusOK { t.Fatalf("expected status 200, got %d", recorder.Code) diff --git a/packages/dashboard-api/internal/handlers/team_provisioning.go b/packages/dashboard-api/internal/handlers/team_provisioning.go index c02cddf264..70e1f51e45 100644 --- a/packages/dashboard-api/internal/handlers/team_provisioning.go +++ b/packages/dashboard-api/internal/handlers/team_provisioning.go @@ -40,11 +40,12 @@ type provisionedTeam struct { BlockedReason *string } -func (s *APIStore) PostAdminUsersUserIdBootstrap(c *gin.Context, userId api.UserId) { +func (s *APIStore) PostAdminUsersBootstrap(c *gin.Context) { ctx := c.Request.Context() telemetry.ReportEvent(ctx, "bootstrap user") - team, err := s.bootstrapUser(ctx, userId) + userID := auth.MustGetUserID(c) + team, err := s.bootstrapUser(ctx, userID) if err != nil { s.handleProvisioningError(ctx, c, "bootstrap user", err) diff --git a/spec/openapi-dashboard.yml b/spec/openapi-dashboard.yml index fecc23b0ff..1de7279cdb 100644 --- a/spec/openapi-dashboard.yml +++ b/spec/openapi-dashboard.yml @@ -757,14 +757,13 @@ paths: "500": $ref: "#/components/responses/500" - /admin/users/{userId}/bootstrap: + /admin/users/bootstrap: post: summary: Bootstrap user tags: [teams] security: - AdminTokenAuth: [] - parameters: - - $ref: "#/components/parameters/userId" + Supabase1TokenAuth: [] responses: "200": description: Successfully bootstrapped user. From 32b35d0eab921cb0e7308ae58b9e4e3ef43acc8f Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Thu, 16 Apr 2026 15:53:58 -0700 Subject: [PATCH 4/6] chore(seed): narrow branch to seeding updates --- ...00_remove_user_team_provision_triggers.sql | 183 ------------------ 1 file changed, 183 deletions(-) delete mode 100644 packages/db/migrations/20260416120000_remove_user_team_provision_triggers.sql diff --git a/packages/db/migrations/20260416120000_remove_user_team_provision_triggers.sql b/packages/db/migrations/20260416120000_remove_user_team_provision_triggers.sql deleted file mode 100644 index 795b3f5193..0000000000 --- a/packages/db/migrations/20260416120000_remove_user_team_provision_triggers.sql +++ /dev/null @@ -1,183 +0,0 @@ --- +goose Up - --- The application now owns auth user projection and default team bootstrap. --- Remove the legacy database triggers/functions that used to keep public.users --- in sync and auto-create default teams on signup. - -DROP TRIGGER IF EXISTS sync_inserts_to_public_users ON auth.users; -DROP TRIGGER IF EXISTS sync_updates_to_public_users ON auth.users; -DROP TRIGGER IF EXISTS sync_deletes_to_public_users ON auth.users; -DROP TRIGGER IF EXISTS post_user_signup ON public.users; - -DROP FUNCTION IF EXISTS public.sync_insert_auth_users_to_public_users_trigger(); -DROP FUNCTION IF EXISTS public.sync_update_auth_users_to_public_users_trigger(); -DROP FUNCTION IF EXISTS public.sync_delete_auth_users_to_public_users_trigger(); -DROP FUNCTION IF EXISTS public.post_user_signup(); -DROP FUNCTION IF EXISTS public.extra_for_post_user_signup(uuid, uuid); - -DROP POLICY IF EXISTS "Allow to create a new user" ON public.users; -DROP POLICY IF EXISTS "Allow to select a user" ON public.users; -DROP POLICY IF EXISTS "Allow to update a user" ON public.users; -DROP POLICY IF EXISTS "Allow to delete a user" ON public.users; - -DROP POLICY IF EXISTS "Allow to create a team to new user" ON public.teams; -DROP POLICY IF EXISTS "Allow to create a user team connection to new user" ON public.users_teams; -DROP POLICY IF EXISTS "Allow to select a team for supabase auth admin" ON public.teams; - -REVOKE INSERT ON public.users FROM trigger_user; -REVOKE SELECT (id) ON public.users FROM trigger_user; -REVOKE UPDATE ON public.users FROM trigger_user; -REVOKE DELETE ON public.users FROM trigger_user; - -REVOKE SELECT, INSERT, TRIGGER ON public.teams FROM trigger_user; -REVOKE INSERT ON public.users_teams FROM trigger_user; - --- +goose Down --- +goose StatementBegin - -GRANT SELECT, INSERT, TRIGGER ON public.teams TO trigger_user; -GRANT INSERT ON public.users_teams TO trigger_user; -GRANT INSERT ON public.users TO trigger_user; -GRANT SELECT (id) ON public.users TO trigger_user; -GRANT UPDATE ON public.users TO trigger_user; -GRANT DELETE ON public.users TO trigger_user; - -CREATE POLICY "Allow to create a new user" - ON public.users - AS PERMISSIVE - FOR INSERT - TO trigger_user - WITH CHECK (TRUE); - -CREATE POLICY "Allow to select a user" - ON public.users - AS PERMISSIVE - FOR SELECT - TO trigger_user - USING (true); - -CREATE POLICY "Allow to update a user" - ON public.users - AS PERMISSIVE - FOR UPDATE - TO trigger_user - USING (true) - WITH CHECK (true); - -CREATE POLICY "Allow to delete a user" - ON public.users - AS PERMISSIVE - FOR DELETE - TO trigger_user - USING (true); - -CREATE POLICY "Allow to create a team to new user" - ON public.teams - AS PERMISSIVE - FOR INSERT - TO trigger_user - WITH CHECK (TRUE); - -CREATE POLICY "Allow to create a user team connection to new user" - ON public.users_teams - AS PERMISSIVE - FOR INSERT - TO trigger_user - WITH CHECK (TRUE); - -CREATE POLICY "Allow to select a team for supabase auth admin" - ON public.teams - AS PERMISSIVE - FOR SELECT - TO trigger_user - USING (TRUE); - -CREATE OR REPLACE FUNCTION public.extra_for_post_user_signup(user_id uuid, team_id uuid) - RETURNS void - LANGUAGE plpgsql -AS $extra_for_post_user_signup$ -DECLARE -BEGIN -END -$extra_for_post_user_signup$ SECURITY DEFINER SET search_path = public; - -ALTER FUNCTION public.extra_for_post_user_signup(uuid, uuid) OWNER TO trigger_user; - -CREATE OR REPLACE FUNCTION public.post_user_signup() - RETURNS TRIGGER - LANGUAGE plpgsql -AS $post_user_signup$ -DECLARE - team_id uuid; -BEGIN - RAISE NOTICE 'Creating default team for user %', NEW.id; - INSERT INTO public.teams(name, tier, email) VALUES (NEW.email, 'base_v1', NEW.email) RETURNING id INTO team_id; - INSERT INTO public.users_teams(user_id, team_id, is_default) VALUES (NEW.id, team_id, true); - RAISE NOTICE 'Created default team for user % and team %', NEW.id, team_id; - - PERFORM public.extra_for_post_user_signup(NEW.id, team_id); - - RETURN NEW; -END -$post_user_signup$ SECURITY DEFINER SET search_path = public; - -ALTER FUNCTION public.post_user_signup() OWNER TO trigger_user; - -CREATE OR REPLACE FUNCTION public.sync_insert_auth_users_to_public_users_trigger() RETURNS TRIGGER -LANGUAGE plpgsql -AS $func$ -BEGIN - INSERT INTO public.users (id, email) - VALUES (NEW.id, NEW.email); - - RETURN NEW; -END; -$func$ SECURITY DEFINER SET search_path = public; - -CREATE OR REPLACE FUNCTION public.sync_update_auth_users_to_public_users_trigger() RETURNS TRIGGER -LANGUAGE plpgsql -AS $func$ -BEGIN - UPDATE public.users - SET email = NEW.email, - updated_at = now() - WHERE id = NEW.id; - - IF NOT FOUND THEN - RAISE EXCEPTION 'User with id % does not exist in public.users', NEW.id; - END IF; - - RETURN NEW; -END; -$func$ SECURITY DEFINER SET search_path = public; - -CREATE OR REPLACE FUNCTION public.sync_delete_auth_users_to_public_users_trigger() RETURNS TRIGGER -LANGUAGE plpgsql -AS $func$ -BEGIN - DELETE FROM public.users WHERE id = OLD.id; - RETURN OLD; -END; -$func$ SECURITY DEFINER SET search_path = public; - -ALTER FUNCTION public.sync_insert_auth_users_to_public_users_trigger() OWNER TO trigger_user; -ALTER FUNCTION public.sync_update_auth_users_to_public_users_trigger() OWNER TO trigger_user; -ALTER FUNCTION public.sync_delete_auth_users_to_public_users_trigger() OWNER TO trigger_user; - -CREATE TRIGGER sync_inserts_to_public_users - AFTER INSERT ON auth.users - FOR EACH ROW EXECUTE FUNCTION public.sync_insert_auth_users_to_public_users_trigger(); - -CREATE TRIGGER sync_updates_to_public_users - AFTER UPDATE ON auth.users - FOR EACH ROW EXECUTE FUNCTION public.sync_update_auth_users_to_public_users_trigger(); - -CREATE TRIGGER sync_deletes_to_public_users - AFTER DELETE ON auth.users - FOR EACH ROW EXECUTE FUNCTION public.sync_delete_auth_users_to_public_users_trigger(); - -CREATE TRIGGER post_user_signup - AFTER INSERT ON public.users - FOR EACH ROW EXECUTE FUNCTION public.post_user_signup(); - --- +goose StatementEnd From de1e475070ec85529c05f856df615e515d949829 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Thu, 16 Apr 2026 15:55:03 -0700 Subject: [PATCH 5/6] chore(seed): drop stray dashboard spec diff --- spec/openapi-dashboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/openapi-dashboard.yml b/spec/openapi-dashboard.yml index 1de7279cdb..f848845da2 100644 --- a/spec/openapi-dashboard.yml +++ b/spec/openapi-dashboard.yml @@ -214,7 +214,7 @@ components: nextCursor: type: string nullable: true - description: Cursor to pass to the next list request, or `null` if there is no next page. + description: Cursor to pass to the next list request, or `null` if there is no next page. BuildStatusItem: type: object From ed6c0b608006e617d8f2c8550905082196688be6 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Thu, 16 Apr 2026 16:03:30 -0700 Subject: [PATCH 6/6] chore(seed): drop handler test trigger cleanup --- .../internal/handlers/team_handlers_test.go | 68 +++++++++++-------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/packages/dashboard-api/internal/handlers/team_handlers_test.go b/packages/dashboard-api/internal/handlers/team_handlers_test.go index 378eda2a96..7fd9e7709d 100644 --- a/packages/dashboard-api/internal/handlers/team_handlers_test.go +++ b/packages/dashboard-api/internal/handlers/team_handlers_test.go @@ -284,31 +284,9 @@ VALUES ($1, $2, $3) t.Fatalf("failed to create test user: %v", err) } - if err := db.AuthDB.Write.UpsertPublicUser(t.Context(), authqueries.UpsertPublicUserParams{ - ID: userID, - Email: email, - }); err != nil { - t.Fatalf("failed to create public user: %v", err) - } - return userID } -func bootstrapHandlerTestUser(t *testing.T, db *testutils.Database, userID uuid.UUID) { - t.Helper() - - store := &APIStore{ - db: db.SqlcClient, - authDB: db.AuthDB, - supabaseDB: db.SupabaseDB, - teamProvisionSink: &fakeTeamProvisionSink{}, - } - - if _, err := store.bootstrapUser(t.Context(), userID); err != nil { - t.Fatalf("failed to bootstrap test user: %v", err) - } -} - func handlerTestUserEmail(userID uuid.UUID) string { return "user-" + userID.String() + "@example.com" } @@ -333,6 +311,17 @@ func TestPostUsersBootstrap_CreatesDefaultTeamAndCallsSink(t *testing.T) { userID := createHandlerTestUser(t, testDB) sink := &fakeTeamProvisionSink{} + existingTeam, err := testDB.AuthDB.Write.GetDefaultTeamByUserID(ctx, userID) + if err != nil { + t.Fatalf("expected trigger-created default team: %v", err) + } + if err := testDB.AuthDB.Write.DeleteTeamByID(ctx, existingTeam.ID); err != nil { + t.Fatalf("failed to remove trigger-created default team: %v", err) + } + if err := testDB.AuthDB.Write.DeletePublicUser(ctx, userID); err != nil { + t.Fatalf("failed to remove trigger-created public user: %v", err) + } + recorder := httptest.NewRecorder() ginCtx, _ := gin.CreateTestContext(recorder) ginCtx.Request = httptest.NewRequestWithContext(ctx, http.MethodPost, "/", nil) @@ -389,6 +378,17 @@ func TestPostUsersBootstrap_ProvisioningFailureKeepsCreatedDefaultTeam(t *testin }, } + existingTeam, err := testDB.AuthDB.Write.GetDefaultTeamByUserID(ctx, userID) + if err != nil { + t.Fatalf("expected trigger-created default team: %v", err) + } + if err := testDB.AuthDB.Write.DeleteTeamByID(ctx, existingTeam.ID); err != nil { + t.Fatalf("failed to remove trigger-created default team: %v", err) + } + if err := testDB.AuthDB.Write.DeletePublicUser(ctx, userID); err != nil { + t.Fatalf("failed to remove trigger-created public user: %v", err) + } + recorder := httptest.NewRecorder() ginCtx, _ := gin.CreateTestContext(recorder) ginCtx.Request = httptest.NewRequestWithContext(ctx, http.MethodPost, "/", nil) @@ -437,6 +437,14 @@ func TestBootstrapUser_ConcurrentRequestsCreateSingleDefaultTeam(t *testing.T) { userID := createHandlerTestUser(t, testDB) sink := &fakeTeamProvisionSink{} + existingTeam, err := testDB.AuthDB.Write.GetDefaultTeamByUserID(ctx, userID) + if err != nil { + t.Fatalf("expected trigger-created default team: %v", err) + } + if err := testDB.AuthDB.Write.DeleteTeamByID(ctx, existingTeam.ID); err != nil { + t.Fatalf("failed to remove trigger-created default team: %v", err) + } + store := &APIStore{ db: testDB.SqlcClient, authDB: testDB.AuthDB, @@ -483,7 +491,7 @@ func TestBootstrapUser_ConcurrentRequestsCreateSingleDefaultTeam(t *testing.T) { } var defaultTeamCount int - err := testDB.AuthDB.TestsRawSQLQuery(ctx, + err = testDB.AuthDB.TestsRawSQLQuery(ctx, `SELECT count(*) FROM public.users_teams WHERE user_id = $1 AND is_default = true`, @@ -562,7 +570,6 @@ func TestPostTeams_LocalPolicyDeniedReturnsBadRequestWithoutCreatingTeam(t *test testDB := testutils.SetupDatabase(t) ctx := t.Context() userID := createHandlerTestUser(t, testDB) - bootstrapHandlerTestUser(t, testDB, userID) sink := &fakeTeamProvisionSink{} for range 2 { @@ -620,7 +627,6 @@ func TestPostTeams_InvalidNameReturnsBadRequest(t *testing.T) { testDB := testutils.SetupDatabase(t) ctx := t.Context() userID := createHandlerTestUser(t, testDB) - bootstrapHandlerTestUser(t, testDB, userID) for _, body := range []string{`{}`, `{"name":""}`, `{"name":" "}`} { recorder := httptest.NewRecorder() @@ -653,7 +659,6 @@ func TestPostTeams_InvalidRequestBodyReturnsBadRequest(t *testing.T) { testDB := testutils.SetupDatabase(t) ctx := t.Context() userID := createHandlerTestUser(t, testDB) - bootstrapHandlerTestUser(t, testDB, userID) sink := &fakeTeamProvisionSink{} recorder := httptest.NewRecorder() @@ -687,7 +692,6 @@ func TestPostTeams_TrimsNameBeforeCreate(t *testing.T) { testDB := testutils.SetupDatabase(t) ctx := t.Context() userID := createHandlerTestUser(t, testDB) - bootstrapHandlerTestUser(t, testDB, userID) sink := &fakeTeamProvisionSink{} recorder := httptest.NewRecorder() @@ -735,7 +739,6 @@ func TestPostTeams_ProvisioningFailureRollsBackCreatedTeam(t *testing.T) { testDB := testutils.SetupDatabase(t) ctx := t.Context() userID := createHandlerTestUser(t, testDB) - bootstrapHandlerTestUser(t, testDB, userID) sink := &fakeTeamProvisionSink{ err: &internalteamprovision.ProvisionError{ StatusCode: http.StatusBadRequest, @@ -795,7 +798,6 @@ func TestPostTeams_ProvisioningFailurePreservesProvisionErrorStatus(t *testing.T testDB := testutils.SetupDatabase(t) ctx := t.Context() userID := createHandlerTestUser(t, testDB) - bootstrapHandlerTestUser(t, testDB, userID) sink := &fakeTeamProvisionSink{ err: &internalteamprovision.ProvisionError{ StatusCode: tt.status, @@ -866,6 +868,14 @@ func TestCreateTeam_ConcurrentRequestsRespectLocalPolicyWithZeroMemberships(t *t ctx := t.Context() userID := createHandlerTestUser(t, testDB) + existingTeam, err := testDB.AuthDB.Write.GetDefaultTeamByUserID(ctx, userID) + if err != nil { + t.Fatalf("expected trigger-created default team: %v", err) + } + if err := testDB.AuthDB.Write.DeleteTeamByID(ctx, existingTeam.ID); err != nil { + t.Fatalf("failed to remove default team: %v", err) + } + store := &APIStore{ db: testDB.SqlcClient, authDB: testDB.AuthDB,