Skip to content

Feat/invites#227

Open
IamKirbki wants to merge 23 commits into
mainfrom
feat/invites
Open

Feat/invites#227
IamKirbki wants to merge 23 commits into
mainfrom
feat/invites

Conversation

@IamKirbki
Copy link
Copy Markdown
Contributor

@IamKirbki IamKirbki commented May 1, 2026

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 enterprise guard. OSS builds return 404 on all invite endpoints.

  • New project_invites table with AES-256-GCM encrypted token storage, nonce column, expiry, and revoke/accept timestamps
  • Tokens are encrypted at rest and decrypted before sending to the frontend. Re-encrypting with the same nonce reproduces the DB ciphertext for querying
  • Auto-revokes any pending invite for the same email + project on new invite creation
  • New endpoints: create, list (with filters), accept, revoke, and a public get-details endpoint requiring no auth
  • Accept invite handles existing project admins, upgrading role if the invite role is higher
  • Frontend mashes nonce + token into a single URL-safe base64 string for invite links
  • Invite management page under project settings with search, filters, pagination, copy link, and revoke
  • Public /invites/:token page handles unauthenticated users, wrong account detection, and auto-accept after Clerk registration via ?autoAccept=1
  • Axios interceptor updated to skip 401 redirect on invite pages and requests flagged with skipAuthRedirect

Project listing scoped to admin membership

ListProjects now queries via project_admins join instead of org-wide scan. GetProject accepts an optional adminID to populate the real role, removing the hardcoded "admin" return.

Auth: dual JWT support

WithJWT now supports RS256 (Clerk/JWKS) and HS256 (basic auth) simultaneously via a multiKeyfunc dispatcher, configured via AUTH_JWKS_URL.

IamKirbki and others added 13 commits April 20, 2026 15:05
- 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>
IamKirbki and others added 8 commits May 5, 2026 11:09
- 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>
@IamKirbki IamKirbki requested a review from jeroenrinzema May 6, 2026 12:57
@IamKirbki IamKirbki marked this pull request as ready for review May 6, 2026 12:57
@jeroenrinzema jeroenrinzema requested a review from Copilot May 12, 2026 19:08
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_admins and 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:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create invite system (backend)

2 participants