feat: add organization onboarding
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
using FastEndpoints;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Organizations.Data;
|
||||
using Socialize.Api.Modules.Organizations.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.Organizations.Handlers;
|
||||
|
||||
internal record CreateOrganizationRequest(
|
||||
string Name);
|
||||
|
||||
internal class CreateOrganizationRequestValidator
|
||||
: Validator<CreateOrganizationRequest>
|
||||
{
|
||||
public CreateOrganizationRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
|
||||
}
|
||||
}
|
||||
|
||||
internal class CreateOrganizationHandler(
|
||||
AppDbContext dbContext)
|
||||
: Endpoint<CreateOrganizationRequest, OrganizationDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/organizations");
|
||||
Options(o => o.WithTags("Organizations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CreateOrganizationRequest request, CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
Guid userId = User.GetUserId();
|
||||
Organization organization = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = request.Name.Trim(),
|
||||
OwnerUserId = userId,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
OrganizationMembership ownerMembership = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = organization.Id,
|
||||
UserId = userId,
|
||||
Role = OrganizationRoles.Owner,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
dbContext.Organizations.Add(organization);
|
||||
dbContext.OrganizationMemberships.Add(ownerMembership);
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendAsync(
|
||||
OrganizationDto.FromOrganization(
|
||||
organization,
|
||||
OrganizationPermissionRules.GetPermissionsForRole(OrganizationRoles.Owner)),
|
||||
StatusCodes.Status201Created,
|
||||
ct);
|
||||
}
|
||||
}
|
||||
35
docs/TASKS/organizations/007-organization-onboarding.md
Normal file
35
docs/TASKS/organizations/007-organization-onboarding.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Task: Organization onboarding for users without access
|
||||
|
||||
## Feature
|
||||
|
||||
`docs/FEATURES/organizations.md`
|
||||
|
||||
## Goal
|
||||
|
||||
When an authenticated user has no accessible organizations and no accessible workspaces, redirect them to an onboarding screen where they can create a new organization as its owner or request access from an existing organization/workspace administrator.
|
||||
|
||||
## Scope
|
||||
|
||||
- Add a protected onboarding route.
|
||||
- Add a backend endpoint for creating an organization owned by the current user.
|
||||
- Add frontend store support for organization creation.
|
||||
- Redirect authenticated users with no organization/workspace access to onboarding.
|
||||
- Keep users with direct workspace access out of onboarding even if they are not organization members.
|
||||
- Add English and French UI strings.
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
dotnet build backend/Socialize.slnx
|
||||
cd frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Done
|
||||
|
||||
- [x] Users without organization or workspace access are redirected to onboarding.
|
||||
- [x] Users can create an organization as owner.
|
||||
- [x] Users can prepare an organization/workspace access request email.
|
||||
- [x] OpenAPI snapshot and frontend schema are updated.
|
||||
- [x] Backend build and tests pass.
|
||||
- [x] Frontend build passes.
|
||||
129
frontend/src/api/schema.d.ts
vendored
129
frontend/src/api/schema.d.ts
vendored
@@ -132,22 +132,6 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/organizations/{organizationId}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["SocializeApiModulesOrganizationsHandlersGetOrganizationHandler"];
|
||||
put: operations["SocializeApiModulesOrganizationsHandlersUpdateOrganizationHandler"];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/organizations": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -157,6 +141,22 @@ export interface paths {
|
||||
};
|
||||
get: operations["SocializeApiModulesOrganizationsHandlersGetOrganizationsHandler"];
|
||||
put?: never;
|
||||
post: operations["SocializeApiModulesOrganizationsHandlersCreateOrganizationHandler"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/organizations/{organizationId}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["SocializeApiModulesOrganizationsHandlersGetOrganizationHandler"];
|
||||
put: operations["SocializeApiModulesOrganizationsHandlersUpdateOrganizationHandler"];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
@@ -1243,6 +1243,9 @@ export interface components {
|
||||
/** Format: int32 */
|
||||
limit?: number | null;
|
||||
};
|
||||
SocializeApiModulesOrganizationsHandlersCreateOrganizationRequest: {
|
||||
name: string;
|
||||
};
|
||||
SocializeApiModulesOrganizationsHandlersUpdateOrganizationRequest: {
|
||||
name: string;
|
||||
};
|
||||
@@ -2297,6 +2300,73 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesOrganizationsHandlersGetOrganizationsHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesOrganizationsHandlersOrganizationDto"][];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesOrganizationsHandlersCreateOrganizationHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesOrganizationsHandlersCreateOrganizationRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesOrganizationsHandlersOrganizationDto"];
|
||||
};
|
||||
};
|
||||
/** @description Bad Request */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["FastEndpointsErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesOrganizationsHandlersGetOrganizationHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -2368,33 +2438,6 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesOrganizationsHandlersGetOrganizationsHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesOrganizationsHandlersOrganizationDto"][];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesNotificationsHandlersGetNotificationsHandler: {
|
||||
parameters: {
|
||||
query?: {
|
||||
|
||||
@@ -22,6 +22,7 @@ export const useOrganizationStore = defineStore('organization', () => {
|
||||
const detailsById = ref({});
|
||||
const isLoading = ref(false);
|
||||
const isLoadingDetails = ref(false);
|
||||
const isCreating = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const isAddingMember = ref(false);
|
||||
const isUploadingLogo = ref(false);
|
||||
@@ -117,6 +118,38 @@ export const useOrganizationStore = defineStore('organization', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function createOrganization(payload) {
|
||||
if (!authStore.isAuthenticated) {
|
||||
throw new Error('You must be authenticated to create an organization.');
|
||||
}
|
||||
|
||||
if (isCreating.value) {
|
||||
throw new Error('An organization creation request is already in progress.');
|
||||
}
|
||||
|
||||
isCreating.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.post('/api/organizations', payload);
|
||||
const organization = response.data;
|
||||
|
||||
if (organization) {
|
||||
organizations.value = [...organizations.value, organization]
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
selectedOrganizationId.value = organization.id;
|
||||
}
|
||||
|
||||
return organization;
|
||||
} catch (createError) {
|
||||
console.error('Failed to create organization:', createError);
|
||||
error.value = 'Failed to create organization.';
|
||||
throw createError;
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateOrganization(organizationId, payload) {
|
||||
if (!authStore.isAuthenticated || !organizationId) {
|
||||
throw new Error('You must be authenticated to update an organization.');
|
||||
@@ -263,6 +296,7 @@ export const useOrganizationStore = defineStore('organization', () => {
|
||||
detailsById,
|
||||
isLoading,
|
||||
isLoadingDetails,
|
||||
isCreating,
|
||||
isSaving,
|
||||
isAddingMember,
|
||||
isUploadingLogo,
|
||||
@@ -272,6 +306,7 @@ export const useOrganizationStore = defineStore('organization', () => {
|
||||
setSelectedOrganizationFromWorkspace,
|
||||
fetchOrganizations,
|
||||
fetchOrganization,
|
||||
createOrganization,
|
||||
updateOrganization,
|
||||
addMember,
|
||||
uploadLogo,
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { mdiAccountArrowRightOutline, mdiDomainPlus, mdiEmailOutline } from '@mdi/js';
|
||||
import { useOrganizationStore } from '@/features/organizations/stores/organizationStore.js';
|
||||
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const organizationStore = useOrganizationStore();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
|
||||
const createForm = reactive({
|
||||
name: '',
|
||||
});
|
||||
const accessForm = reactive({
|
||||
targetType: 'organization',
|
||||
targetName: '',
|
||||
adminEmail: '',
|
||||
note: '',
|
||||
});
|
||||
const createError = ref(null);
|
||||
const requestStatus = ref(null);
|
||||
|
||||
const accessTypeOptions = computed(() => [
|
||||
{ title: t('organizationOnboarding.request.types.organization'), value: 'organization' },
|
||||
{ title: t('organizationOnboarding.request.types.workspace'), value: 'workspace' },
|
||||
]);
|
||||
|
||||
async function createOrganization() {
|
||||
if (organizationStore.isCreating) {
|
||||
return;
|
||||
}
|
||||
|
||||
createError.value = null;
|
||||
const name = createForm.name.trim();
|
||||
|
||||
if (!name) {
|
||||
createError.value = t('organizationOnboarding.create.errors.required');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await organizationStore.createOrganization({ name });
|
||||
await Promise.all([
|
||||
organizationStore.fetchOrganizations(),
|
||||
workspaceStore.fetchWorkspaces(),
|
||||
]);
|
||||
await router.push({ name: 'workspace-create' });
|
||||
} catch (error) {
|
||||
createError.value = t('organizationOnboarding.create.errors.failed');
|
||||
}
|
||||
}
|
||||
|
||||
function requestAccess() {
|
||||
requestStatus.value = null;
|
||||
|
||||
if (!accessForm.adminEmail.trim() || !accessForm.targetName.trim()) {
|
||||
requestStatus.value = t('organizationOnboarding.request.errors.required');
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = t('organizationOnboarding.request.emailSubject', {
|
||||
target: accessForm.targetName.trim(),
|
||||
});
|
||||
const body = t('organizationOnboarding.request.emailBody', {
|
||||
targetType: t(`organizationOnboarding.request.types.${accessForm.targetType}`),
|
||||
target: accessForm.targetName.trim(),
|
||||
note: accessForm.note.trim() || t('organizationOnboarding.request.noNote'),
|
||||
});
|
||||
|
||||
window.location.href = `mailto:${encodeURIComponent(accessForm.adminEmail.trim())}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
||||
requestStatus.value = t('organizationOnboarding.request.sent');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="onboarding-page">
|
||||
<header class="hero">
|
||||
<div>
|
||||
<div class="eyebrow">{{ t('organizationOnboarding.eyebrow') }}</div>
|
||||
<h1>{{ t('organizationOnboarding.title') }}</h1>
|
||||
<p>{{ t('organizationOnboarding.description') }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="onboarding-grid">
|
||||
<article class="panel">
|
||||
<div class="panel-header">
|
||||
<v-icon :icon="mdiDomainPlus" />
|
||||
<div>
|
||||
<strong>{{ t('organizationOnboarding.create.title') }}</strong>
|
||||
<span>{{ t('organizationOnboarding.create.description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="createError"
|
||||
class="page-message error"
|
||||
>
|
||||
{{ createError }}
|
||||
</div>
|
||||
|
||||
<v-form
|
||||
class="form-stack"
|
||||
@submit.prevent="createOrganization"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="createForm.name"
|
||||
:label="t('organizationOnboarding.create.fields.name')"
|
||||
:placeholder="t('organizationOnboarding.create.fields.namePlaceholder')"
|
||||
:disabled="organizationStore.isCreating"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
:loading="organizationStore.isCreating"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
{{ t('organizationOnboarding.create.action') }}
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="panel-header">
|
||||
<v-icon :icon="mdiAccountArrowRightOutline" />
|
||||
<div>
|
||||
<strong>{{ t('organizationOnboarding.request.title') }}</strong>
|
||||
<span>{{ t('organizationOnboarding.request.description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="requestStatus"
|
||||
class="page-message"
|
||||
>
|
||||
{{ requestStatus }}
|
||||
</div>
|
||||
|
||||
<v-form
|
||||
class="form-stack"
|
||||
@submit.prevent="requestAccess"
|
||||
>
|
||||
<v-select
|
||||
v-model="accessForm.targetType"
|
||||
:items="accessTypeOptions"
|
||||
:label="t('organizationOnboarding.request.fields.type')"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="accessForm.targetName"
|
||||
:label="t('organizationOnboarding.request.fields.name')"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="accessForm.adminEmail"
|
||||
:label="t('organizationOnboarding.request.fields.adminEmail')"
|
||||
type="email"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
<v-textarea
|
||||
v-model="accessForm.note"
|
||||
:label="t('organizationOnboarding.request.fields.note')"
|
||||
rows="3"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
color="secondary"
|
||||
type="submit"
|
||||
:prepend-icon="mdiEmailOutline"
|
||||
>
|
||||
{{ t('organizationOnboarding.request.action') }}
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
.onboarding-page {
|
||||
@apply mx-auto flex w-full max-w-6xl flex-col gap-6 px-5 py-8 md:px-8;
|
||||
}
|
||||
|
||||
.hero,
|
||||
.panel {
|
||||
@apply rounded-lg border;
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.hero {
|
||||
@apply p-6 md:p-8;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
@apply text-xs font-bold uppercase tracking-[0.22em];
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
@apply mt-2 text-3xl font-black md:text-4xl;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.hero p,
|
||||
.panel-header span {
|
||||
@apply mt-2 text-sm leading-6;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.onboarding-grid {
|
||||
@apply grid gap-4 lg:grid-cols-2;
|
||||
}
|
||||
|
||||
.panel {
|
||||
@apply flex flex-col gap-4 p-5;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
@apply flex gap-3;
|
||||
}
|
||||
|
||||
.panel-header .v-icon {
|
||||
@apply mt-1;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.panel-header strong {
|
||||
@apply block text-xl font-black;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.form-stack {
|
||||
@apply flex flex-col gap-3;
|
||||
}
|
||||
|
||||
.page-message {
|
||||
@apply rounded-lg border p-3 text-sm font-semibold;
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
background: rgba(23, 32, 51, 0.04);
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.page-message.error {
|
||||
border-color: rgba(220, 38, 38, 0.24);
|
||||
color: #b91c1c;
|
||||
}
|
||||
</style>
|
||||
@@ -389,6 +389,46 @@
|
||||
"createFailed": "The workspace could not be created."
|
||||
}
|
||||
},
|
||||
"organizationOnboarding": {
|
||||
"eyebrow": "Account setup",
|
||||
"title": "Create an organization or request access",
|
||||
"description": "Your account does not have access to an organization or workspace yet. Start a new organization if you own the account, or ask an administrator to invite you.",
|
||||
"create": {
|
||||
"title": "Create a new organization",
|
||||
"description": "You will become the owner and can create the first workspace next.",
|
||||
"action": "Create organization",
|
||||
"fields": {
|
||||
"name": "Organization name",
|
||||
"namePlaceholder": "Northstar Agency"
|
||||
},
|
||||
"errors": {
|
||||
"required": "Organization name is required.",
|
||||
"failed": "The organization could not be created."
|
||||
}
|
||||
},
|
||||
"request": {
|
||||
"title": "Request access",
|
||||
"description": "Send an access request to the administrator who owns the organization or workspace.",
|
||||
"action": "Prepare request email",
|
||||
"sent": "Your email app should open with the request prepared.",
|
||||
"noNote": "No additional note provided.",
|
||||
"emailSubject": "Access request for {target}",
|
||||
"emailBody": "Hello,\n\nPlease invite me to this {targetType}: {target}.\n\nNote: {note}\n\nThank you.",
|
||||
"types": {
|
||||
"organization": "organization",
|
||||
"workspace": "workspace"
|
||||
},
|
||||
"fields": {
|
||||
"type": "Access type",
|
||||
"name": "Organization or workspace name",
|
||||
"adminEmail": "Administrator email",
|
||||
"note": "Note"
|
||||
},
|
||||
"errors": {
|
||||
"required": "Administrator email and organization or workspace name are required."
|
||||
}
|
||||
}
|
||||
},
|
||||
"organizationSettings": {
|
||||
"eyebrow": "Organization",
|
||||
"title": "Organization settings",
|
||||
|
||||
@@ -389,6 +389,46 @@
|
||||
"createFailed": "L'espace n'a pas pu etre cree."
|
||||
}
|
||||
},
|
||||
"organizationOnboarding": {
|
||||
"eyebrow": "Configuration du compte",
|
||||
"title": "Creer une organisation ou demander un acces",
|
||||
"description": "Votre compte n'a pas encore acces a une organisation ou a un espace. Creez une organisation si vous possedez le compte, ou demandez a un administrateur de vous inviter.",
|
||||
"create": {
|
||||
"title": "Creer une nouvelle organisation",
|
||||
"description": "Vous en deviendrez le proprietaire et pourrez creer le premier espace ensuite.",
|
||||
"action": "Creer l'organisation",
|
||||
"fields": {
|
||||
"name": "Nom de l'organisation",
|
||||
"namePlaceholder": "Agence Northstar"
|
||||
},
|
||||
"errors": {
|
||||
"required": "Le nom de l'organisation est requis.",
|
||||
"failed": "L'organisation n'a pas pu etre creee."
|
||||
}
|
||||
},
|
||||
"request": {
|
||||
"title": "Demander un acces",
|
||||
"description": "Envoyez une demande a l'administrateur qui gere l'organisation ou l'espace.",
|
||||
"action": "Preparer l'email",
|
||||
"sent": "Votre application email devrait s'ouvrir avec la demande preparee.",
|
||||
"noNote": "Aucune note supplementaire.",
|
||||
"emailSubject": "Demande d'acces pour {target}",
|
||||
"emailBody": "Bonjour,\n\nVeuillez m'inviter a ce/cette {targetType} : {target}.\n\nNote : {note}\n\nMerci.",
|
||||
"types": {
|
||||
"organization": "organisation",
|
||||
"workspace": "espace"
|
||||
},
|
||||
"fields": {
|
||||
"type": "Type d'acces",
|
||||
"name": "Nom de l'organisation ou de l'espace",
|
||||
"adminEmail": "Email de l'administrateur",
|
||||
"note": "Note"
|
||||
},
|
||||
"errors": {
|
||||
"required": "L'email de l'administrateur et le nom de l'organisation ou de l'espace sont requis."
|
||||
}
|
||||
}
|
||||
},
|
||||
"organizationSettings": {
|
||||
"eyebrow": "Organisation",
|
||||
"title": "Parametres de l'organisation",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
||||
import { useOrganizationStore } from '@/features/organizations/stores/organizationStore.js';
|
||||
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
const LoginView = () => import('@/features/auth/views/LoginView.vue');
|
||||
@@ -18,6 +20,7 @@ const CampaignsView = () => import('@/features/campaigns/views/CampaignsView.vue
|
||||
const CampaignDetailView = () => import('@/features/campaigns/views/CampaignDetailView.vue');
|
||||
const MediaLibraryView = () => import('@/features/content/views/MediaLibraryView.vue');
|
||||
const WorkspaceCreateView = () => import('@/features/workspaces/views/WorkspaceCreateView.vue');
|
||||
const OrganizationOnboardingView = () => import('@/features/organizations/views/OrganizationOnboardingView.vue');
|
||||
const OrganizationSettingsView = () => import('@/features/organizations/views/OrganizationSettingsView.vue');
|
||||
const SettingsLayoutView = () => import('@/features/settings/views/SettingsLayoutView.vue');
|
||||
const UserSettingsView = () => import('@/features/user-profile/views/UserSettingsView.vue');
|
||||
@@ -132,6 +135,12 @@ const routes = [
|
||||
component: DeveloperFeedbackDetailView,
|
||||
meta: { requiresAuth: true, roles: ['developer'] },
|
||||
},
|
||||
{
|
||||
path: '/app/onboarding',
|
||||
name: 'organization-onboarding',
|
||||
component: OrganizationOnboardingView,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/app/organizations/:organizationId/settings',
|
||||
name: 'organization-settings',
|
||||
@@ -148,7 +157,7 @@ const routes = [
|
||||
path: '/app/workspaces/new',
|
||||
name: 'workspace-create',
|
||||
component: WorkspaceCreateView,
|
||||
meta: { requiresAuth: true, roles: ['administrator', 'manager'] },
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/app/settings',
|
||||
@@ -243,8 +252,33 @@ const router = createRouter({
|
||||
routes,
|
||||
});
|
||||
|
||||
async function loadAccessScope() {
|
||||
const organizationStore = useOrganizationStore();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
|
||||
await Promise.all([
|
||||
organizationStore.fetchOrganizations(),
|
||||
workspaceStore.fetchWorkspaces(),
|
||||
]);
|
||||
}
|
||||
|
||||
function hasNoOrganizationOrWorkspaceAccess() {
|
||||
const organizationStore = useOrganizationStore();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
|
||||
return organizationStore.organizations.length === 0 && workspaceStore.workspaces.length === 0;
|
||||
}
|
||||
|
||||
async function getAuthenticatedHomeRoute() {
|
||||
await loadAccessScope();
|
||||
|
||||
return hasNoOrganizationOrWorkspaceAccess()
|
||||
? { name: 'organization-onboarding' }
|
||||
: { name: 'dashboard' };
|
||||
}
|
||||
|
||||
// Navigation guards
|
||||
router.beforeEach((to) => {
|
||||
router.beforeEach(async (to) => {
|
||||
const authStore = useAuthStore();
|
||||
|
||||
if (to.matched.some(record => record.meta.requiresAuth)) {
|
||||
@@ -255,6 +289,12 @@ router.beforeEach((to) => {
|
||||
};
|
||||
}
|
||||
|
||||
await loadAccessScope();
|
||||
|
||||
if (hasNoOrganizationOrWorkspaceAccess() && to.name !== 'organization-onboarding') {
|
||||
return { name: 'organization-onboarding' };
|
||||
}
|
||||
|
||||
const requiredRoles = to.matched.flatMap(record => record.meta.roles ?? []);
|
||||
if (requiredRoles.length > 0 && !authStore.hasAnyRole(requiredRoles)) {
|
||||
return { name: 'dashboard' };
|
||||
@@ -264,9 +304,7 @@ router.beforeEach((to) => {
|
||||
}
|
||||
|
||||
if (to.matched.some(record => record.meta.notAuthenticated)) {
|
||||
return authStore.isAuthenticated
|
||||
? { name: 'dashboard' }
|
||||
: true;
|
||||
return authStore.isAuthenticated ? await getAuthenticatedHomeRoute() : true;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -511,6 +511,88 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/organizations": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Organizations",
|
||||
"Api"
|
||||
],
|
||||
"operationId": "SocializeApiModulesOrganizationsHandlersCreateOrganizationHandler",
|
||||
"requestBody": {
|
||||
"x-name": "CreateOrganizationRequest",
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SocializeApiModulesOrganizationsHandlersCreateOrganizationRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true,
|
||||
"x-position": 1
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SocializeApiModulesOrganizationsHandlersOrganizationDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/FastEndpointsErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"JWTBearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"tags": [
|
||||
"Organizations",
|
||||
"Api"
|
||||
],
|
||||
"operationId": "SocializeApiModulesOrganizationsHandlersGetOrganizationsHandler",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SocializeApiModulesOrganizationsHandlersOrganizationDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"JWTBearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/organizations/{organizationId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -612,38 +694,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/organizations": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Organizations",
|
||||
"Api"
|
||||
],
|
||||
"operationId": "SocializeApiModulesOrganizationsHandlersGetOrganizationsHandler",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SocializeApiModulesOrganizationsHandlersOrganizationDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"JWTBearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/notifications": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -4220,6 +4270,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SocializeApiModulesOrganizationsHandlersCreateOrganizationRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"maxLength": 256,
|
||||
"minLength": 0,
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"SocializeApiModulesOrganizationsHandlersUpdateOrganizationRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
Reference in New Issue
Block a user