feat: roles SPA foundations

This commit is contained in:
Florian Sylvain committed 2026-10-27 22:21:12 +01:00
1 parent 80f7037d3a
commit 907949aa12
180 files changed
+4308 -835

No files matched your search

-1
View File
@@ -23,7 +23,6 @@ const SOURCES = [
'components/PublishingLinksCard.vue',
'components/ReassignDialog.vue',
'components/SiteFormPanel.vue',
'components/SitesForbidden.vue',
'components/SlugField.vue',
'components/SocialLinksRepeater.vue',
'components/UserSelect.vue',
+86
View File
@@ -2831,6 +2831,92 @@ export const messages = {
'Value in an entry': 'Valeur dans une entrée',
'1 or higher. Default 1.': '1 ou plus. 1 par défaut.',
'One of 5, 25, 50 or 100. Default 25.': '5, 25, 50 ou 100. 25 par défaut.',
'Read entries': 'Lire les entrées',
'Create entries': 'Créer des entrées',
'Update entries': 'Modifier des entrées',
'Delete entries': 'Supprimer des entrées',
'Publish entries': 'Publier des entrées',
'View content types': 'Voir les types de contenu',
'Create and edit content types': 'Créer et modifier des types de contenu',
'Delete content types': 'Supprimer des types de contenu',
'View the media library': 'Voir la médiathèque',
'Upload files': 'Téléverser des fichiers',
'Delete files': 'Supprimer des fichiers',
'View authors': 'Voir les auteurs',
'Create authors': 'Créer des auteurs',
'Edit authors': 'Modifier des auteurs',
'Delete authors': 'Supprimer des auteurs',
'See activity': 'Voir l’activité',
'Export activity': 'Exporter l’activité',
'View users': 'Voir les utilisateurs',
'Invite users': 'Inviter des utilisateurs',
"Change a user's role": 'Changer le rôle d’un utilisateur',
'Remove users': 'Retirer des utilisateurs',
'View settings': 'Voir les paramètres',
'Edit general settings': 'Modifier les paramètres généraux',
'Delete a site': 'Supprimer un site',
'See API keys': 'Voir les clés d’API',
'Create API keys': 'Créer des clés d’API',
'Revoke API keys': 'Révoquer des clés d’API',
'View roles': 'Voir les rôles',
'Manage roles': 'Gérer les rôles',
'Edit roles with owner-only power': 'Modifier les rôles à pouvoir réservé au propriétaire',
'Apply software updates': 'Appliquer les mises à jour du logiciel',
'Edit activity retention': 'Modifier la durée de conservation de l’activité',
'Transfer ownership': 'Transférer la propriété',
"You don't have permission to do that": 'Vous n’avez pas la permission de faire cela',
"Your role doesn't include {0}. Ask an owner or admin.":
'Votre rôle n’inclut pas « {0} ». Demandez à un propriétaire ou à un administrateur.',
"Your role doesn't include this action. Ask an owner or admin.":
'Votre rôle n’inclut pas cette action. Demandez à un propriétaire ou à un administrateur.',
"You can't grant that": 'Vous ne pouvez pas accorder cela',
'You can only grant what your role has.':
'Vous ne pouvez accorder que ce que votre rôle possède.',
'This role is locked': 'Ce rôle est verrouillé',
"The Owner role can't be changed or deleted. Transfer ownership from Users instead.":
'Le rôle Propriétaire ne peut être ni modifié ni supprimé. Transférez plutôt la propriété depuis Utilisateurs.',
"You can't change your own role": 'Vous ne pouvez pas changer votre propre rôle',
'Ask another owner or admin.': 'Demandez à un autre propriétaire ou administrateur.',
"Built-in roles can't be deleted": 'Les rôles intégrés ne peuvent pas être supprimés',
'Duplicate it to make a custom role.': 'Dupliquez-le pour créer un rôle personnalisé.',
'Built-in roles keep their name.': 'Les rôles intégrés gardent leur nom.',
'This changed somewhere else. Reload and try again.':
'Cela a changé ailleurs. Rechargez la page et réessayez.',
'View only': 'Lecture seule',
'You can only edit content you created.':
'Vous ne pouvez modifier que le contenu que vous avez créé.',
"You don't have access to this page": 'Vous n’avez pas accès à cette page',
"Your role, {0}, doesn't include it. Ask an owner or admin if you need it.":
'Votre rôle, {0}, ne l’inclut pas. Demandez à un propriétaire ou à un administrateur si vous en avez besoin.',
'Go to Articles': 'Aller aux articles',
writer: 'rédacteur',
'Only the owner can delete a site.': 'Seul le propriétaire peut supprimer un site.',
'Only the owner can apply updates.': 'Seul le propriétaire peut appliquer les mises à jour.',
'Apply update': 'Appliquer la mise à jour',
'Admins can read the log. Only the owner can change how long it is kept.':
'Les administrateurs peuvent lire le journal. Seul le propriétaire peut changer sa durée de conservation.',
'How long activity is kept, and how much log data to keep. Whichever limit is reached first removes the oldest events.':
'Durée de conservation de l’activité et volume de journal à garder. La première limite atteinte supprime les événements les plus anciens.',
'You can read these settings but not change them':
'Vous pouvez lire ces paramètres mais pas les modifier',
'Ask the owner to change them, or ask for a role that can edit settings.':
'Demandez au propriétaire de les modifier, ou demandez un rôle qui peut modifier les paramètres.',
'You can create entries and edit your own.':
'Vous pouvez créer des entrées et modifier les vôtres.',
'Entries by others are view only': 'Les entrées des autres sont en lecture seule',
'Your role lets you edit entries you created.':
'Votre rôle vous permet de modifier les entrées que vous avez créées.',
"Your role can't edit entries.": 'Votre rôle ne permet pas de modifier des entrées.',
"Your role can't delete entries.": 'Votre rôle ne permet pas de supprimer des entrées.',
"Your role can't edit articles.": 'Votre rôle ne permet pas de modifier des articles.',
"Your role can't delete articles.": 'Votre rôle ne permet pas de supprimer des articles.',
'Ask an editor to publish.': 'Demandez à un éditeur de publier.',
'You can write and edit all articles. Publishing is done by an editor.':
'Vous pouvez rédiger et modifier tous les articles. La publication est faite par un éditeur.',
"Your role can't delete files.": 'Votre rôle ne permet pas de supprimer des fichiers.',
'Save draft': 'Enregistrer le brouillon',
'The id of the role to give. Absent or 0 means the Writer role. The Owner role is refused.':
'L’identifiant du rôle à donner. Absent ou 0, c’est le rôle Rédacteur. Le rôle Propriétaire est refusé.',
} as const
export type MessageKey = keyof typeof messages
+7 -23
View File
@@ -1,4 +1,4 @@
import { readFileSync } from 'node:fs'
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { API_GROUPS, SESSION_GROUPS } from '@/data/apiCatalogue'
@@ -108,32 +108,16 @@ describe('Catalogue groups of Sites and Authors in French', () => {
})
})
describe('The owner_only copy of the design board', () => {
it.each([
"You don't have access to Sites",
'Only the owner can manage sites. Ask the owner of this site if you need a change.',
'Only the owner can manage sites',
'Ask the owner of this site.',
])('"%s" has a French entry', (text) => {
expect(frenchOf(text)).toMatch(/\S/)
expect(frenchOf(text)).not.toBe(text)
})
it('is used by the 403 page and by the action toast', () => {
const page = readFileSync(
join(import.meta.dirname, '..', 'components/SitesForbidden.vue'),
'utf8',
)
const toast = readFileSync(
describe('The retired owner-only sites copy', () => {
it('shows nowhere any more: the page-level state and the generic toast replace it', () => {
const toasts = readFileSync(
join(import.meta.dirname, '..', 'composables/useDirectoryToasts.ts'),
'utf8',
)
expect(page).toContain("You don\\'t have access to Sites")
expect(page).toContain(
'Only the owner can manage sites. Ask the owner of this site if you need a change.',
expect(toasts).not.toContain('Only the owner can manage sites')
expect(existsSync(join(import.meta.dirname, '..', 'components/SitesForbidden.vue'))).toBe(
false,
)
expect(toast).toContain('Only the owner can manage sites')
expect(toast).toContain('Ask the owner of this site.')
})
})
+36
View File
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest'
import { messages } from './messages'
const KEYS = [
"You don't have permission to do that",
"Your role doesn't include {0}. Ask an owner or admin.",
"Your role doesn't include this action. Ask an owner or admin.",
"You can't grant that",
'This role is locked',
"You can't change your own role",
"Built-in roles can't be deleted",
'Built-in roles keep their name.',
'This changed somewhere else. Reload and try again.',
"You don't have access to this page",
"Your role, {0}, doesn't include it. Ask an owner or admin if you need it.",
'Go to Articles',
'View only',
'Owner only',
'Only the owner can apply updates.',
'Admins can read the log. Only the owner can change how long it is kept.',
'You can read these settings but not change them',
'Ask the owner to change them, or ask for a role that can edit settings.',
'You can create entries and edit your own.',
'Entries by others are view only',
"Your role can't delete articles.",
'Ask an editor to publish.',
'You can write and edit all articles. Publishing is done by an editor.',
'Save draft',
] as const
describe('Permission copy', () => {
it.each(KEYS)('"%s" has a French entry', (key) => {
const french = (messages as Record<string, string>)[key]
expect(french).toMatch(/\S/)
})
})
+4 -2
View File
@@ -14,6 +14,7 @@ import { messages } from './messages'
import { formatAbsoluteDateFromIso } from '@/lib/relativeDate'
import { formatSize, pluralArticles } from '@/lib/mediaFormat'
import { articleEnvelope, articleListUrl } from '@/views/__tests__/articleEnvelope'
import { accessFor, ownerAccess } from '@/test/access'
vi.mock('@/composables/useApi', async () => {
const { ref } = await import('vue')
@@ -70,7 +71,7 @@ async function mountRoute(path: string) {
username: 'marta',
displayName: 'Marta',
email: 'marta@example.com',
role: 'owner',
access: accessFor('owner'),
}
session.status = 'ready'
await router.push(path)
@@ -315,7 +316,7 @@ describe('Hardener localization outcomes', () => {
async (authenticated) => {
const { default: NotFoundView } = await import('@/views/NotFoundView.vue')
useSessionStore().session = authenticated
? { verified: true, username: 'marta', displayName: '' }
? { verified: true, access: ownerAccess(), username: 'marta', displayName: '' }
: null
wrapper = mount(NotFoundView, {
attachTo: document.body,
@@ -389,6 +390,7 @@ describe('Hardener localization outcomes', () => {
setLocale('fr')
useSessionStore().session = {
verified: true,
access: ownerAccess(),
username: 'marta',
displayName: '',
}