2 Commits

Author SHA1 Message Date
237b1a4242 docs: adds workspace-invites feature and tasks
Some checks failed
Backend CI/CD / build_and_deploy (push) Has been cancelled
Frontend CI/CD / build_and_deploy (push) Has been cancelled
2026-04-30 15:46:06 -04:00
ace0279bd0 fix(workspace-invite): inconsistence in roles names 2026-04-30 15:45:32 -04:00
12 changed files with 446 additions and 210 deletions

View File

@@ -2,10 +2,10 @@
public static class KnownRoles public static class KnownRoles
{ {
public const string Administrator = nameof(Administrator); public const string Administrator = "administrator";
public const string Manager = nameof(Manager); public const string Manager = "manager";
public const string Client = nameof(Client); public const string Client = "client";
public const string Provider = nameof(Provider); public const string Provider = "provider";
public const string WorkspaceMember = nameof(WorkspaceMember); public const string WorkspaceMember = "workspace-member";
public const string Developer = nameof(Developer); public const string Developer = "developer";
} }

View File

@@ -0,0 +1,6 @@
namespace Socialize.Api.Modules.Workspaces.Data;
public static class WorkspaceInviteStatuses
{
public const string Pending = "Pending";
}

View File

@@ -24,7 +24,7 @@ public class CreateWorkspaceInviteRequestValidator
public CreateWorkspaceInviteRequestValidator() public CreateWorkspaceInviteRequestValidator()
{ {
RuleFor(x => x.Email).NotEmpty().MaximumLength(256).EmailAddress(); RuleFor(x => x.Email).NotEmpty().MaximumLength(256).EmailAddress();
RuleFor(x => x.Role).NotEmpty().Must(role => AllowedRoles.Contains(role)); RuleFor(x => x.Role).NotEmpty().Must(role => AllowedRoles.Contains(role)).WithMessage("A valid role should be specified");
} }
} }
@@ -65,7 +65,7 @@ public class CreateWorkspaceInviteHandler(
bool duplicateInvite = await dbContext.WorkspaceInvites.AnyAsync( bool duplicateInvite = await dbContext.WorkspaceInvites.AnyAsync(
invite => invite.WorkspaceId == workspaceId && invite => invite.WorkspaceId == workspaceId &&
invite.Email == normalizedEmail && invite.Email == normalizedEmail &&
invite.Status == "Pending", invite.Status == WorkspaceInviteStatuses.Pending,
ct); ct);
if (duplicateInvite) if (duplicateInvite)
@@ -81,7 +81,7 @@ public class CreateWorkspaceInviteHandler(
WorkspaceId = workspaceId, WorkspaceId = workspaceId,
Email = normalizedEmail, Email = normalizedEmail,
Role = normalizedRole, Role = normalizedRole,
Status = "Pending", Status = WorkspaceInviteStatuses.Pending,
InvitedByUserId = User.GetUserId(), InvitedByUserId = User.GetUserId(),
CreatedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow,
}; };

View File

@@ -0,0 +1,66 @@
# Feature: Workspace Invites
## Status
Draft
## Goal
Allow workspace managers to invite teammates, clients, and providers into a workspace and allow invited people to accept access with the correct role and workspace scope.
## User Stories
- As a workspace manager, I want to invite a person by email and role so that they can access the right workspace.
- As an invited person, I want to accept an invite from a link so that I can join the workspace without administrator help.
- As an invited person without an account, I want to create my account as part of accepting the invite.
- As an invited person with an account, I want the accepted workspace to appear after sign-in.
- As a workspace manager, I want to see pending, accepted, cancelled, and expired invites so that I understand who has access or still needs follow-up.
## Domain Rules
- Workspace invites belong to exactly one workspace.
- Invite email matching should use normalized email addresses.
- Pending invite tokens must be single-use and should expire.
- Accepted invites must grant the invited role and a workspace scope claim for the invite workspace.
- Signed-in users may accept invites only when their account email matches the invite email.
- New users may create an account during invite acceptance, then receive the invited role and workspace scope.
- Accepted, cancelled, and expired invites must not be accepted again.
- Managers can create, list, cancel, and resend invites only for workspaces they can manage.
- Managers must not be able to create duplicate pending invites for the same normalized email in the same workspace.
- Invite acceptance must be auditable through stored status and timestamp changes.
## Proposed Statuses
- `Pending`
- `Accepted`
- `Cancelled`
- `Expired`
## Backend Surface
- `POST /api/workspaces/{workspaceId:guid}/invites`
- `GET /api/workspaces/{workspaceId:guid}/invites`
- `POST /api/workspace-invites/{inviteId:guid}/resend`
- `POST /api/workspace-invites/{inviteId:guid}/cancel`
- `GET /api/workspace-invites/accept/{token}`
- `POST /api/workspace-invites/accept`
## Frontend Surface
- Workspace settings members tab for invite creation and invite management.
- Public invite acceptance route.
- Authenticated invite acceptance route for signed-in users.
- Registration/sign-in handoff for invited users without a usable session.
## Done When
- [ ] Invite creation sends an email with an acceptance link.
- [ ] Acceptance link validates a pending, unexpired, single-use token.
- [ ] Signed-in users can accept matching-email invites.
- [ ] New users can register through the invite path.
- [ ] Accepted invites grant role and workspace scope.
- [ ] Accepted users see the workspace after token refresh or sign-in.
- [ ] Managers can cancel and resend pending invites.
- [ ] Invite statuses are represented without magic strings.
- [ ] Backend tests cover create, duplicate, accept, expired, cancelled, and email mismatch cases.
- [ ] OpenAPI and frontend API usage are updated after contract changes.

View File

@@ -0,0 +1,46 @@
# Task: Backend Workspace Invite Foundation
## Goal
Implement the backend data model and endpoints needed to create, list, and accept workspace invites.
## Feature Spec
- `docs/FEATURES/workspace-invites.md`
## Scope
- Add a `WorkspaceInvite` persistence model with workspace id, normalized email, role, status, inviter, token data, timestamps, and expiration.
- Add invite statuses for `Pending`, `Accepted`, `Cancelled`, and `Expired` without magic strings.
- Add a manager-only endpoint to create workspace invites.
- Add a manager-only endpoint to list workspace invites.
- Prevent duplicate pending invites for the same normalized email in the same workspace.
- Add a public endpoint to resolve an invite token for display-safe invite details.
- Add an accept endpoint that validates token, status, expiration, and email match.
- On acceptance, grant the invited role and `KnownClaims.WorkspaceScope` claim to the user.
- Mark the invite as accepted in the same transaction as access grants.
- Add backend tests for create, list, pending, accepted, expired, cancelled, duplicate, and email mismatch paths.
## Constraints
- Keep backend code under `backend/src/Socialize.Api`.
- Keep workspace feature code under `Modules/Workspaces`.
- Do not expose raw token values in manager invite lists.
- Frontend invite screens are covered by task 003 and task 004.
## Likely Files
- `backend/src/Socialize.Api/Modules/Workspaces/Data/WorkspaceInvite.cs`
- `backend/src/Socialize.Api/Modules/Workspaces/Data/WorkspaceInviteStatuses.cs`
- `backend/src/Socialize.Api/Modules/Workspaces/Data/WorkspaceModelConfiguration.cs`
- `backend/src/Socialize.Api/Modules/Workspaces/Handlers/CreateWorkspaceInvite.cs`
- `backend/src/Socialize.Api/Modules/Workspaces/Handlers/GetWorkspaceInvites.cs`
- `backend/src/Socialize.Api/Modules/Workspaces/Handlers/*Accept*Invite*.cs`
- `backend/tests/Socialize.Tests/`
## Validation
```bash
dotnet build backend/Socialize.slnx
dotnet test backend/Socialize.slnx
```

View File

@@ -0,0 +1,37 @@
# Task: Invite Email Delivery
## Goal
Send invited users an acceptance link when a workspace invite is created or resent.
## Feature Spec
- `docs/FEATURES/workspace-invites.md`
## Scope
- Generate acceptance URLs from configured website options.
- Send an invite email after successful invite creation.
- Add a manager-only resend endpoint for pending, unexpired invites.
- Avoid sending email if invite creation fails.
- Do not include sensitive token values in logs.
## Constraints
- Use the repository email infrastructure.
- Do not introduce a new email provider.
- Keep email copy concise and product-specific.
## Likely Files
- `backend/src/Socialize.Api/Modules/Workspaces/Handlers/CreateWorkspaceInvite.cs`
- `backend/src/Socialize.Api/Modules/Workspaces/Handlers/ResendWorkspaceInvite.cs`
- `backend/src/Socialize.Api/Infrastructure/Emailer/`
- `backend/src/Socialize.Api/Infrastructure/Configuration/WebsiteOptions.cs`
## Validation
```bash
dotnet build backend/Socialize.slnx
dotnet test backend/Socialize.slnx
```

View File

@@ -0,0 +1,39 @@
# Task: Frontend Invite Acceptance
## Goal
Build the invite acceptance route and connect it to registration or sign-in.
## Feature Spec
- `docs/FEATURES/workspace-invites.md`
## Scope
- Add a public route for invite acceptance links.
- Load display-safe invite details from the token.
- If the user is signed in with the invited email, allow direct acceptance.
- If the user is signed in with a different email, show a clear mismatch state.
- If the user is signed out, route them to sign in or register and resume acceptance afterward.
- Refresh the current user profile after acceptance so the new workspace appears.
## Constraints
- Frontend runtime config must flow through `frontend/src/config.js`.
- Feature-owned code belongs under `frontend/src/features/workspaces`.
- Do not add a marketing-style landing page for invite acceptance.
## Likely Files
- `frontend/src/router/router.js`
- `frontend/src/features/workspaces/`
- `frontend/src/features/workspaces/stores/workspaceStore.js`
- `frontend/src/locales/en.json`
- `frontend/src/locales/fr.json`
## Validation
```bash
cd frontend
npm run build
```

View File

@@ -0,0 +1,42 @@
# Task: Invite Management Polish
## Goal
Make workspace invite management complete for managers after acceptance exists.
## Feature Spec
- `docs/FEATURES/workspace-invites.md`
## Scope
- Show invite statuses in workspace settings.
- Add manager actions to cancel and resend pending invites.
- Hide or disable actions for accepted, cancelled, and expired invites.
- Decide whether the default list shows all invites or only active pending invites.
- Ensure accepted users appear in the active members list after acceptance.
- Update OpenAPI and frontend API usage after backend contract changes.
## Constraints
- Keep workspace settings within repository layout conventions.
- Avoid broad member-management refactors.
## Likely Files
- `backend/src/Socialize.Api/Modules/Workspaces/Handlers/GetWorkspaceInvites.cs`
- `backend/src/Socialize.Api/Modules/Workspaces/Handlers/CancelWorkspaceInvite.cs`
- `frontend/src/features/workspaces/views/WorkspaceSettingsView.vue`
- `frontend/src/features/workspaces/stores/workspaceStore.js`
- `shared/openapi/openapi.json`
- `frontend/src/api/schema.d.ts`
## Validation
```bash
dotnet build backend/Socialize.slnx
dotnet test backend/Socialize.slnx
./scripts/update-openapi.sh
cd frontend
npm run build
```

View File

@@ -29,7 +29,7 @@
const inviteForm = reactive({ const inviteForm = reactive({
email: '', email: '',
role: 'workspaceMember', role: 'workspace-member',
}); });
const pendingInvites = computed(() => const pendingInvites = computed(() =>
@@ -161,7 +161,7 @@
}); });
inviteForm.email = ''; inviteForm.email = '';
inviteForm.role = 'workspaceMember'; inviteForm.role = 'workspace-member';
} catch (error) { } catch (error) {
console.error('Failed to invite workspace member:', error); console.error('Failed to invite workspace member:', error);
} }
@@ -330,7 +330,7 @@
<label class="field"> <label class="field">
<span>{{ t('workspaceSettings.fields.memberRole') }}</span> <span>{{ t('workspaceSettings.fields.memberRole') }}</span>
<select v-model="inviteForm.role"> <select v-model="inviteForm.role">
<option value="workspaceMember">{{ t('workspaceSettings.roles.workspaceMember') }}</option> <option value="workspace-member">{{ t('workspaceSettings.roles.workspace-member') }}</option>
<option value="client">{{ t('workspaceSettings.roles.client') }}</option> <option value="client">{{ t('workspaceSettings.roles.client') }}</option>
<option value="provider">{{ t('workspaceSettings.roles.provider') }}</option> <option value="provider">{{ t('workspaceSettings.roles.provider') }}</option>
</select> </select>

View File

@@ -512,7 +512,7 @@
"manager": "Manager", "manager": "Manager",
"client": "Client reviewer", "client": "Client reviewer",
"provider": "Subcontractor", "provider": "Subcontractor",
"workspaceMember": "Workspace member" "workspace-member": "Workspace member"
}, },
"summary": { "summary": {
"name": "Name", "name": "Name",

View File

@@ -512,7 +512,7 @@
"manager": "Gestionnaire", "manager": "Gestionnaire",
"client": "Réviseur client", "client": "Réviseur client",
"provider": "Sous-traitant", "provider": "Sous-traitant",
"workspaceMember": "Membre de l'espace" "workspace-member": "Membre de l'espace"
}, },
"summary": { "summary": {
"name": "Nom", "name": "Nom",

View File

@@ -94,25 +94,25 @@ const routes = [
path: '/app/feedback', path: '/app/feedback',
name: 'developer-feedback', name: 'developer-feedback',
component: DeveloperFeedbackListView, component: DeveloperFeedbackListView,
meta: { requiresAuth: true, roles: ['Developer'] }, meta: { requiresAuth: true, roles: ['developer'] },
}, },
{ {
path: '/app/feedback/:id', path: '/app/feedback/:id',
name: 'developer-feedback-detail', name: 'developer-feedback-detail',
component: DeveloperFeedbackDetailView, component: DeveloperFeedbackDetailView,
meta: { requiresAuth: true, roles: ['Developer'] }, meta: { requiresAuth: true, roles: ['developer'] },
}, },
{ {
path: '/app/workspace-settings', path: '/app/workspace-settings',
name: 'workspace-settings', name: 'workspace-settings',
component: WorkspaceSettingsView, component: WorkspaceSettingsView,
meta: { requiresAuth: true, roles: ['Administrator', 'Manager'] }, meta: { requiresAuth: true, roles: ['administrator', 'manager'] },
}, },
{ {
path: '/app/workspaces/new', path: '/app/workspaces/new',
name: 'workspace-create', name: 'workspace-create',
component: WorkspaceCreateView, component: WorkspaceCreateView,
meta: { requiresAuth: true, roles: ['Administrator', 'Manager'] }, meta: { requiresAuth: true, roles: ['administrator', 'manager'] },
}, },
{ {
path: '/app/settings', path: '/app/settings',
@@ -132,13 +132,13 @@ const routes = [
path: 'workspaces', path: 'workspaces',
name: 'settings-workspaces', name: 'settings-workspaces',
component: WorkspaceSettingsView, component: WorkspaceSettingsView,
meta: { requiresAuth: true, roles: ['Administrator', 'Manager'] }, meta: { requiresAuth: true, roles: ['administrator', 'manager'] },
}, },
{ {
path: 'integrations', path: 'integrations',
name: 'settings-integrations', name: 'settings-integrations',
component: IntegrationsSettingsView, component: IntegrationsSettingsView,
meta: { requiresAuth: true, roles: ['Administrator', 'Manager'] }, meta: { requiresAuth: true, roles: ['administrator', 'manager'] },
}, },
], ],
}, },