Feat/invites#227
Open
IamKirbki wants to merge 23 commits into
Open
Conversation
- Introduced ProjectInviteListResponse model for listing project invites. - Added ListProjectInvitesParams for pagination support in listing invites. - Implemented RevokeProjectInvite, ListProjectInvites, AcceptProjectInvite, and GetInviteDetails methods in the Client interface. - Created corresponding request and response parsing functions for project invite operations. - Enhanced AdminsStore with HardDeleteProjectAdmin method for direct deletion of project admins. - Expanded InvitesStore with methods to handle project invites: GetInviteByToken, AcceptProjectInvite, RevokeProjectInvite, and ListProjectInvites. - Updated database migration to enforce unique constraint on invite tokens.
- Added a new "Invites" section in the settings menu with a UserPlus icon. - Enhanced user search functionality in the ListDetail component. - Updated OrganizationEventRuleEdit to improve accessibility with better aria-labels. - Modified the InviteController to support filtering project invites by status, role, and expiration dates. - Updated OpenAPI resources to include new query parameters for invite management. - Refactored invite handling in the management store to support new filtering options. - Changed database table references from "invites" to "project_invites" for clarity. - Implemented revoke project invite functionality with proper middleware handling.
Co-authored-by: Copilot <copilot@github.com>
…rchy logic Co-authored-by: Copilot <copilot@github.com>
…rtain contexts Co-authored-by: Copilot <copilot@github.com>
…okens - Updated API paths to accept a combined token and nonce pair for invite acceptance and revocation. - Modified ProjectInvite interface to include nonce. - Implemented nonce generation and encryption in the invite creation process. - Adjusted database schema to store nonce alongside the invite token. - Enhanced invite handling logic to support nonce verification during acceptance and revocation. - Updated frontend components to handle the new token-nonce structure. - Added necessary environment configurations for invite secret key. Co-authored-by: Copilot <copilot@github.com>
…ment for project admins
…e token generation failure
- Changed API endpoint parameter from `tokenNouncePair` to `token` for clarity. - Updated the `ProjectPushProviderPlatform` enum to replace `email` with `mail`. - Refactored `AcceptInvite` component to handle new token structure and improved error handling. - Removed unused `mashTokenNonce` function and simplified token concatenation logic. - Updated database migrations to reflect changes in project provider platform naming. - Adjusted related functions and interfaces to ensure consistency with new naming conventions. - Removed email platform support from various components and validations. Co-authored-by: Copilot <copilot@github.com>
There was a problem hiding this comment.
Pull request overview
Adds an enterprise-only project invite system end-to-end (DB + API + RBAC + console UI), and refactors project scoping/auth to better reflect per-admin project membership and support dual JWT verification modes.
Changes:
- Introduces enterprise-gated project invite flow (migration, store, RBAC resource, controllers, OpenAPI, console pages).
- Updates project retrieval/listing to be scoped by
project_adminsand to return the caller’s project role instead of hardcoding"admin". - Extends auth middleware to support both RS256 (JWKS/Clerk) and HS256 (basic/HMAC) JWT verification.
Reviewed changes
Copilot reviewed 45 out of 50 changed files in this pull request and generated 19 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/store/management/store.go | Wires InvitesStore into management state. |
| internal/store/management/projects.go | Adds role field and new ListProjectsForAdmin; updates GetProject signature to accept optional admin ID. |
| internal/store/management/projects_test.go | Updates tests for new GetProject signature. |
| internal/store/management/project_push_providers.go | Renames provider mapping types and extends platform enum to include mail. |
| internal/store/management/project_push_providers_test.go | Updates tests to use renamed provider mapping types. |
| internal/store/management/migrations/1776685923398_migration.up.sql | Adds project_invites table. |
| internal/store/management/migrations/1776685923398_migration.down.sql | Drops project_invites table. |
| internal/store/management/invites.go | Implements DB layer for create/list/accept/revoke invite operations. |
| internal/store/management/admins.go | Adds hard-delete helper for project_admins. |
| internal/rbac/model.go | Adds invites resource to RBAC model with admin-only access. |
| internal/rbac/access/access.go | Adds ProjectRoleTuples helper for project-scoped role tuples. |
| internal/pubsub/consumer/campaigns.go | Updates GetProject call sites for new signature. |
| internal/http/controllers/v1/management/sender_identities.go | Updates GetProject call sites for new signature. |
| internal/http/controllers/v1/management/push_providers.go | Updates GetProject call sites and provider mapping types. |
| internal/http/controllers/v1/management/providers.go | Updates provider mapping types. |
| internal/http/controllers/v1/management/projects.go | Uses admin-scoped project listing and passes actor ID for role lookup. |
| internal/http/controllers/v1/management/oapi/resources.yml | Adds invite endpoints/schemas and extends provider platform enum. |
| internal/http/controllers/v1/management/oapi/resources_gen.go | Regenerates Go OpenAPI types/client/server scaffolding for invites and platform enum. |
| internal/http/controllers/v1/management/locales.go | Updates GetProject call sites for new signature. |
| internal/http/controllers/v1/management/lists.go | Updates GetProject call sites for new signature. |
| internal/http/controllers/v1/management/journeys.go | Updates GetProject call sites for new signature. |
| internal/http/controllers/v1/management/invites.go | OSS stub controller returning 404 for invite endpoints. |
| internal/http/controllers/v1/management/invites_enterprise.go | Enterprise invite controller with token encryption, acceptance, listing, and revocation. |
| internal/http/controllers/v1/management/controller.go | Registers invite controller in management controller. |
| internal/http/controllers/v1/management/campaigns.go | Updates GetProject call sites for new signature. |
| internal/http/console/dist/index.html | Updates console build asset references. |
| internal/http/auth/auth.go | Adds multi-keyfunc JWT verification to support RS256 + HS256 concurrently. |
| internal/config/config.go | Adds Invites config section for invite token secret key. |
| docker-compose.yml | Adds AUTH_JWKS_URL and invite secret env var wiring. |
| console/src/views/settings/Settings.tsx | Adds enterprise-only settings nav entry for invites. |
| console/src/views/settings/NewIntegration.tsx | Minor formatting change. |
| console/src/views/settings/Invites.tsx | Adds invite management UI (list/filter/search/copy/revoke/create). |
| console/src/views/settings/InviteDialog.tsx | Adds invite creation dialog with role selection and expiry. |
| console/src/views/settings/IntegrationSetup.tsx | Adjusts rate limit shape mapping. |
| console/src/views/settings/ApiKeys.tsx | Changes default API key role selection and refines debounce ref init. |
| console/src/views/settings/ApiKeyDialog.tsx | Simplifies role assignment and changes default role selection. |
| console/src/views/router.tsx | Adds routes for /invites/:token and /register, and settings invites route. |
| console/src/views/invites/AcceptInvite.tsx | Adds public invite acceptance flow with login/register handling. |
| console/src/views/broadcast/CreateBroadcastDialog.tsx | Adds lint suppression comment for datetime-local min. |
| console/src/views/auth/Register.tsx | Adds Clerk signup page wrapper for invite-driven registration flow. |
| console/src/types.ts | Adds ProjectInvite type. |
| console/src/oapi/management.generated.ts | Regenerates TS OpenAPI types for invite endpoints and platform enum changes. |
| console/src/api.ts | Adds invites API client + axios interceptor skip logic for invite pages. |
| console/public/locales/zh.json | Adds invite UI translations. |
| console/public/locales/es.json | Adds invite UI translations. |
| console/public/locales/en.json | Adds invite UI translations. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+3
to
+5
| project_id UUID REFERENCES projects(id) ON DELETE CASCADE, | ||
| inviter_admin_id UUID NOT NULL REFERENCES admins(id) ON DELETE SET NULL, | ||
| invitee_email VARCHAR(255) NOT NULL, |
Comment on lines
+9106
to
+9110
| example: "user@example.com" | ||
| role: | ||
| type: string | ||
| enum: [owner, admin, editor, support] | ||
| example: admin |
Comment on lines
+9139
to
+9142
| role: | ||
| type: string | ||
| enum: [owner, admin, editor, support] | ||
| example: admin |
Comment on lines
91
to
95
| SELECT id, organization_id, name, description, timezone, text_opt_out_message, text_help_message, locale, created_at, updated_at, | ||
| COALESCE(pr.integrations_count, 0) AS integrations_count, | ||
| COALESCE(ca.campaigns_count, 0) AS campaigns_count | ||
| COALESCE(ca.campaigns_count, 0) AS campaigns_count, | ||
| COALESCE(pa.role, 'viewer') AS role | ||
| FROM projects |
Comment on lines
+135
to
+141
| AND ($5::date IS NULL OR expires_at >= $5::date) | ||
| AND ($6::date IS NULL OR expires_at <= $6::date) | ||
| AND ($7::uuid IS NULL OR inviter_admin_id = $7::uuid)` | ||
|
|
||
| var total int | ||
| err := s.db.GetContext(ctx, &total, countStmt, projectID, search, role, status, expiresBefore, expiresAfter, inviterAdminID) | ||
| if err != nil { |
Comment on lines
+145
to
+149
| stmt := ` | ||
| SELECT pi.id AS id, pi.project_id AS project_id, pi.inviter_admin_id AS inviter_admin_id, a.email AS inviter_admin_email, pi.invitee_email AS invitee_email, pi.role AS role, pi.token AS token, pi.nonce AS nonce, pi.expires_at AS expires_at, pi.created_at AS created_at, pi.revoked_at AS revoked_at, pi.accepted_at AS accepted_at | ||
| FROM project_invites as pi | ||
| INNER JOIN admins as a ON pi.inviter_admin_id = a.id | ||
| WHERE pi.project_id = $1 |
Comment on lines
+435
to
+465
| encryptedToken, nonce, err := unpackToken(tokenNouncePair, srv.cfg.SecretKey, srv.logger) | ||
| if err != nil { | ||
| srv.logger.Error("failed to expand token and nonce", zap.String("token_nounce_pair", tokenNouncePair), zap.Error(err)) | ||
| oapi.WriteProblem(w, problem.ErrBadRequest(problem.Describe("invalid invite token"))) | ||
| return | ||
| } | ||
|
|
||
| token, _, err := encryptToken(encryptedToken, srv.cfg.SecretKey, &nonce, *srv.logger) | ||
| if err != nil { | ||
| srv.logger.Error("failed to decrypt token", zap.String("encrypted_token", encryptedToken), zap.String("nonce", string(nonce)), zap.Error(err)) | ||
| oapi.WriteProblem(w, problem.ErrBadRequest(problem.Describe("invalid invite token"))) | ||
| return | ||
| } | ||
|
|
||
| invite, err := srv.mgmt.RevokeProjectInvite(ctx, token) | ||
| if err != nil { | ||
| srv.logger.Debug("invite not found or already revoked/accepted", zap.String("token", token), zap.Error(err)) | ||
| oapi.WriteProblem(w, err) | ||
| return | ||
| } | ||
|
|
||
| response := invite.OAPI() | ||
| resToken, err := decryptToken(*response.Token, nonce, srv.cfg.SecretKey, *srv.logger) | ||
| if err != nil { | ||
| srv.logger.Error("failed to decrypt invite token", zap.String("encrypted_token", *response.Token), zap.String("nonce", string(nonce)), zap.Error(err)) | ||
| oapi.WriteProblem(w, problem.ErrBadRequest(problem.Describe("invalid invite token"))) | ||
| return | ||
| } | ||
|
|
||
| response.Token = &resToken | ||
| json.Write(w, http.StatusOK, response) |
Comment on lines
+398
to
+404
| err = access.BackfillProjectTuples(ctx, srv.logger, srv.engine, srv.db) | ||
| if err != nil { | ||
| srv.logger.Error("failed to write RBAC tuples for new project admin", zap.String("admin_id", adminId.String()), zap.String("project_id", invite.ProjectID.String()), zap.Error(err)) | ||
| oapi.WriteProblem(w, problem.ErrInternal(problem.Describe("failed to assign project role"))) | ||
| return | ||
| } | ||
|
|
Comment on lines
+328
to
+333
|
|
||
| if admin.Email != invite.InviteeEmail { | ||
| srv.logger.Debug("admin email does not match invitee email", zap.String("admin_email", admin.Email), zap.String("invitee_email", invite.InviteeEmail)) | ||
| oapi.WriteProblem(w, problem.ErrForbidden(problem.Describe("you do not have permission to accept this invite"))) | ||
| return | ||
| } |
Comment on lines
+1873
to
+1881
| parameters: | ||
| - name: token | ||
| in: path | ||
| required: true | ||
| schema: | ||
| type: string | ||
| description: The project invite token | ||
| example: "abc123def456" | ||
| responses: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: project invite system + auth and project scoping improvements
(closes: #218 #220)
This adds a full project invite flow and updates auth and project scoping.
Project invite system (enterprise)
Full invite flow behind a
//go:build enterpriseguard. OSS builds return 404 on all invite endpoints.project_invitestable with AES-256-GCM encrypted token storage, nonce column, expiry, and revoke/accept timestamps/invites/:tokenpage handles unauthenticated users, wrong account detection, and auto-accept after Clerk registration via?autoAccept=1skipAuthRedirectProject listing scoped to admin membership
ListProjectsnow queries viaproject_adminsjoin instead of org-wide scan.GetProjectaccepts an optionaladminIDto populate the real role, removing the hardcoded"admin"return.Auth: dual JWT support
WithJWTnow supports RS256 (Clerk/JWKS) and HS256 (basic auth) simultaneously via amultiKeyfuncdispatcher, configured viaAUTH_JWKS_URL.