Conversation
…tices Replaces bare <p>/<a> string concatenation with a proper table-based, inline-CSS HTML layout (Outlook-safe), a hidden preheader, an accessible CTA button with a raw-link fallback, and a real multipart/alternative plain-text body generated from the same structured EmailContent — so HTML and plain text can't drift apart. Also fixes an HTML-injection gap: display/organization/project names were interpolated raw into the email body; they're now escaped via EmailTemplateRenderer before insertion.
…plates feat: redesign transactional email templates with 2026 best practices
… invitation sendProjectInvitationEmail reused sendInvitationEmail's subject, heading, and preheader verbatim — a recipient scanning their inbox list couldn't tell a project invite apart from a plain org invite (the only difference was buried in a body paragraph). Both now name the project explicitly. Found via real end-to-end Mailpit verification, not just unit tests.
…plates fix: distinguish project-invitation email subject from plain org invitation
Introduce a GitHub-style RBAC floor applied to every project member on top of their explicit project role. A new BasePermission enum (NONE, READ) maps READ to the workspace *_READ permissions (integration read excluded — integrations are org-admin config); the Organization aggregate carries memberBasePermission (default READ) with a domain mutator. A public-schema Flyway migration adds the member_base_permission column.
hasPermission now grants a permission when the organization's base floor carries it for an active member, in addition to owner/admin bypass and the project role. Add effectivePermissions(org, projectId, userId) returning the caller's full permission set: all permissions for owners/admins, else the union of the base floor and their project role. This flows through @authz.projectPermission and WorkspaceModuleApi.callerHasProjectPermission, so every gated endpoint honors it.
…n endpoints
Add three org-scoped endpoints (header Api-Version: 1):
- GET /organizations/{orgId}/base-permission (owner/admin) -> {basePermission}
- PUT /organizations/{orgId}/base-permission (owner/admin) sets the floor
- GET /organizations/{orgId}/me/authorization (member) -> {orgRole, memberBasePermission}
Backed by a CQRS command/handler for the change and query/handlers for the reads;
me/authorization resolves the caller's org role via OrganizationAdminAccessService
(owner -> OWNER, else the member's ADMIN/MEMBER role).
…dpoint
Add GET /api/projects/{projectId}/me/permissions (header Api-Version: 1) ->
{permissions: [...]}, the caller's effective project permissions (base floor
union project role, or the full catalog for owners/admins). The route carries no
orgId, so a new tenant-context projectAccess gate (WorkspaceAuthorization plus
WorkspaceModuleApi.callerCanAccessProject) lets any member who can access the
project — or an org owner/admin — read their own permissions.
… permissions Unit-test BasePermission.grantedPermissions (NONE empty, READ = the 7 *_READ baseline), the Organization default (READ) and mutator, and ProjectPermissionService: base READ grants read-only to a role-less member, base NONE denies even read, non-members get no floor, owners bypass, and effectivePermissions returns the full catalog for owners and the base-plus-role union for members. Add a base-permission override to the Organization test builder.
…oject access
The base-permission floor is organization-wide, so a role-less member legitimately
has read access to a project without an explicit assignment. Gate
GET /projects/{projectId}/me/permissions on active org membership of the bound
tenant (WorkspaceAuthorization.tenantMember + WorkspaceModuleApi.callerIsActiveMember)
so any member can read their own effective set — empty when they have neither a
floor nor a project role — instead of requiring a project assignment.
…endpoints Testcontainers coverage: PUT base-permission (owner ok, plain member 403), a role-less member gets 200 on a CONSTRAINT_READ endpoint when base=READ and 403 when base=NONE, me/permissions returns the full catalog for the owner, the READ floor for a role-less member and floor-plus-role for a member with a custom role, and me/authorization returns the caller's org role and the floor.
…sion floor The member base permission floor (default READ) now grants an unassigned org member implicit read, so this suite's 'unassigned member denied on reads' assertions no longer held. Pin the test org's floor to NONE so it keeps verifying project-role gating in isolation; the READ floor is covered by BasePermissionIntegrationTest.
canAccessProject/accessibleProjectIds required an explicit ProjectMember assignment for every member, ignoring the organization's base permission floor. With the default READ floor a member could load a project's stories and sessions yet be 403'd by @authz.projectAccess on the project itself. Grant all-project access when the floor is non-NONE; fall back to assignment-based access only when the floor is NONE. Tests that exercise assignment-based access now pin the floor to NONE.
Listing project members required only MEMBER_READ, but the frontend also fetched the project roles (ROLE_READ) just to resolve each member's role name — so a viewer whose role grants only MEMBER_READ hit a 403 and the members page failed to load. Resolve role names in the list query handler and embed roleName in each ProjectMemberResponse, so the roster is self-contained: MEMBER_READ alone now shows members with their roles. Single-assignment responses pass null and the client falls back to its own lookup.
Editing or assigning a member's role needs the roles list for its options, but listing roles required ROLE_READ — so a member whose role grants MEMBER_UPDATE_ROLE/MEMBER_INVITE (but not ROLE_READ) got an empty role editor. Add @authz.projectAnyPermission (holds ANY of several permissions, resolving the org once) and gate the roles list on ROLE_READ OR MEMBER_UPDATE_ROLE OR MEMBER_INVITE.
feat(workspace): member base permission floor + permission endpoints
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.
Description
Release PR that promotes the current
developtomain. This rolls up everything merged intodevelopsince the last release, most recently the member base permission floor (PR #75), on top of the transactional-email redesign and the billing/Stripe work already ondevelop.Bounded context / area: workspace | iam | billing | discovery | gateway
Related issue / US: Release — promote
develop→mainNotable changes since
main:memberBasePermission(NONE/READ, defaultREAD) applied to all project members on top of their explicit project role; owners/admins bypass it. Wired throughProjectPermissionService.hasPermission,@authz.projectPermissionandWorkspaceModuleApi.callerHasProjectPermission. New endpoints:GET/PUT .../base-permission,GET .../me/authorization, and the caller's effective project permissions; project members list now embeds the role name and member-managers can read the project roles list.multipart/alternativefallback, HTML-escaping of user-controlled values, accurate token expiry, and a newEmailContent/EmailTemplateRenderertemplate package; distinct subject for project vs org invitations.fakegateway as default; cross-module plan-change relay to Workspace; Flyway migrationV13__billing_processed_events.sql;docs/BILLING.md.[Unreleased].Type of Change
feat— new featurefix— bug fixrefactor— code change without behavior changetest— tests onlydocs— documentation onlybuild/ci— build, dependencies, or CI/CDchore— maintenanceChecklist
develop(notmain) — (release PR: develop → main, intentional)feature/*,bugfix/*, orhotfix/*— n/a (release fromdevelop)./gradlew buildpasses locally (compile + tests +verifyModularity) — verified ondevelopvia CI.pemkeys are committedCHANGELOG.mdupdated under[Unreleased]common/ortenant/)How to Test
developis green (./gradlew build— compile + tests +verifyModularity).NONEvsREAD, effective-permissions endpoint), the redesigned transactional emails, and the billing subscription/Stripe webhook flow.Notes / Screenshots (optional)
Release / promotion PR: base is
main, head isdevelop.mainhas a few commits not ondevelop; if this PR reports conflicts they must be resolved before merge.