@open-commune/web 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +41 -0
  2. package/app.config.ts +10 -0
  3. package/app.vue +13 -0
  4. package/assets/css/main.css +13 -0
  5. package/components/data/display/row/data-avatar.vue +23 -0
  6. package/components/data/display/row/data-badge.vue +34 -0
  7. package/components/data/display/row/data-color.vue +17 -0
  8. package/components/data/display/row/data-display-badge-publish-status.vue +29 -0
  9. package/components/data/display/row/data-label.vue +37 -0
  10. package/components/data/display/row/data-last-update.vue +45 -0
  11. package/components/data/display/row/data-spacer.vue +4 -0
  12. package/components/data/display/row/data-summary.vue +44 -0
  13. package/components/data/display/row/data-text.vue +14 -0
  14. package/components/data/display/row/data-toggle.vue +16 -0
  15. package/components/data/display/row.vue +111 -0
  16. package/components/data/lists/data-list.vue +85 -0
  17. package/components/data/lists/data-lists-table.vue +155 -0
  18. package/components/elements/avatars/avatar-circular.vue +68 -0
  19. package/components/elements/avatars/avatar-with-text.vue +47 -0
  20. package/components/elements/badges/badge.vue +85 -0
  21. package/components/elements/buttons/primary.vue +32 -0
  22. package/components/elements/buttons/secondary.vue +35 -0
  23. package/components/elements/buttons/tertiary.vue +25 -0
  24. package/components/elements/dropdowns/dots.vue +75 -0
  25. package/components/elements/media/image.vue +82 -0
  26. package/components/elements/notifications/default.vue +90 -0
  27. package/components/forms/handlers/handler-collections.vue +189 -0
  28. package/components/forms/handlers/handler-publication-status.vue +193 -0
  29. package/components/forms/handlers/handler-records.vue +138 -0
  30. package/components/forms/handlers/handler-roles.vue +176 -0
  31. package/components/forms/handlers/handler-schedules.vue +218 -0
  32. package/components/forms/handlers/handler-steps.vue +147 -0
  33. package/components/forms/handlers/handler-tokens.vue +79 -0
  34. package/components/forms/inputs/input-color-picker.vue +159 -0
  35. package/components/forms/inputs/input-editor.vue +363 -0
  36. package/components/forms/inputs/input-files-media.vue +150 -0
  37. package/components/forms/inputs/input-files.vue +98 -0
  38. package/components/forms/inputs/input-json.vue +65 -0
  39. package/components/forms/inputs/input-location.vue +179 -0
  40. package/components/forms/inputs/input-text-area.vue +65 -0
  41. package/components/forms/inputs/input-text.vue +122 -0
  42. package/components/forms/inputs/input-toggle.vue +125 -0
  43. package/components/forms/inputs/media-section/media-editor.vue +95 -0
  44. package/components/forms/layouts/form.vue +203 -0
  45. package/components/forms/layouts/layout.vue +47 -0
  46. package/components/forms/layouts/modal.vue +96 -0
  47. package/components/forms/layouts/navigation.vue +48 -0
  48. package/components/forms/layouts/section.vue +118 -0
  49. package/components/forms/selects/select-collection-enum.vue +195 -0
  50. package/components/forms/selects/select-date.vue +245 -0
  51. package/components/forms/selects/select-enum.vue +186 -0
  52. package/components/forms/selects/select-record.vue +274 -0
  53. package/components/layout/headings/headings-collection.vue +29 -0
  54. package/components/layout/headings/headings-record.vue +33 -0
  55. package/components/layout/navigations/primary/dropdown-publish.vue +147 -0
  56. package/components/layout/navigations/primary.vue +132 -0
  57. package/components/layout/notifications/notifications-list.vue +19 -0
  58. package/composables/core/domains/use-definitions.ts +57 -0
  59. package/composables/core/domains/use-entries.ts +62 -0
  60. package/composables/core/domains/use-media.ts +82 -0
  61. package/composables/core/use-core.ts +12 -0
  62. package/composables/use-ability.js +38 -0
  63. package/composables/use-models.js +27 -0
  64. package/composables/use-session.ts +80 -0
  65. package/composables/use-timer.js +62 -0
  66. package/composables/use-trpc.js +17 -0
  67. package/editor/index.ts +44 -0
  68. package/editor/views/CalloutView.vue +22 -0
  69. package/editor/views/DetailsContentView.vue +11 -0
  70. package/editor/views/DetailsSummaryView.vue +11 -0
  71. package/editor/views/DetailsView.vue +72 -0
  72. package/editor/views/EmbedView.vue +53 -0
  73. package/editor/views/FileView.vue +79 -0
  74. package/editor/views/ImageView.vue +56 -0
  75. package/editor/views/UploadView.vue +71 -0
  76. package/layouts/default.vue +21 -0
  77. package/layouts/entrance.vue +10 -0
  78. package/layouts/record.vue +21 -0
  79. package/middleware/00.auth.global.js +16 -0
  80. package/middleware/01.instance.global.js +21 -0
  81. package/middleware/03.verify-permissions.global.js +11 -0
  82. package/middleware/04.prefetch-page.global.js +18 -0
  83. package/middleware/05.headings-page.global.js +25 -0
  84. package/models/_commun.js +243 -0
  85. package/models/_factory.js +34 -0
  86. package/models/collection.js +87 -0
  87. package/models/entrance.js +42 -0
  88. package/models/media.js +82 -0
  89. package/models/organization.js +92 -0
  90. package/models/record.js +165 -0
  91. package/models/role.js +66 -0
  92. package/models/token.js +56 -0
  93. package/models/user.js +97 -0
  94. package/nuxt.config.ts +151 -0
  95. package/package.json +71 -0
  96. package/pages/[collection]/[record]/index.vue +88 -0
  97. package/pages/[collection]/index.vue +55 -0
  98. package/pages/_lab/editor.vue +149 -0
  99. package/pages/auth/callback.vue +31 -0
  100. package/pages/index/index.vue +85 -0
  101. package/pages/index/password/define/[token].vue +79 -0
  102. package/pages/index/password/recover/index.vue +72 -0
  103. package/pages/index/welcome/[token].vue +90 -0
  104. package/pages/index.vue +44 -0
  105. package/pages/management/index.vue +15 -0
  106. package/pages/management/members.vue +176 -0
  107. package/pages/management/organizations.vue +192 -0
  108. package/pages/management/roles.vue +183 -0
  109. package/pages/overview.vue +15 -0
  110. package/pages/settings.vue +197 -0
  111. package/plugins/01.session.client.ts +5 -0
  112. package/plugins/02.vue-query.ts +25 -0
  113. package/public/favicon.ico +0 -0
  114. package/public/logo-black.svg +1 -0
  115. package/public/logo-white.svg +1 -0
  116. package/server/api/trpc/[...trpc].ts +28 -0
  117. package/server/api/v1/content/deployment.get.ts +45 -0
  118. package/server/api/v1/content/records.get.ts +12 -0
  119. package/server/middleware/0.security.ts +19 -0
  120. package/server/middleware/1.cors.ts +31 -0
  121. package/server/middleware/2.session.ts +20 -0
  122. package/server/middleware/3.api-token.ts +20 -0
  123. package/server/plugins/core.ts +8 -0
  124. package/server/routes/_tasks/[name].post.ts +20 -0
  125. package/server/routes/health.get.ts +6 -0
  126. package/server/tasks/deploy.ts +26 -0
  127. package/server/types.d.ts +8 -0
  128. package/server/utils/core.ts +18 -0
  129. package/store/layout/headings.js +21 -0
  130. package/store/layout/modal.js +80 -0
  131. package/store/layout/notifications.js +52 -0
  132. package/store/layout/workspace.js +80 -0
package/nuxt.config.ts ADDED
@@ -0,0 +1,151 @@
1
+ import { globSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ // Under Bun's isolated store, the ESM build of @vue/devtools-api only exists
6
+ // inside node_modules/.bun/@vue+devtools-api@<version>, where a plain node
7
+ // resolution never looks. We point Vite at it explicitly when we can find it.
8
+ //
9
+ // Best-effort ON PURPOSE: the path assumes this app sits inside its repository.
10
+ // It does not when the app is consumed as a remote Nuxt layer, or built from a
11
+ // tarball — the lookup must then fail quietly rather than break the build.
12
+ function findDevtoolsApiEsm(): string | undefined {
13
+ try {
14
+ const [found] = globSync(
15
+ fileURLToPath(
16
+ new URL(
17
+ '../../node_modules/.bun/@vue+devtools-api@*/node_modules/@vue/devtools-api/lib/esm/index.js',
18
+ import.meta.url,
19
+ ),
20
+ ),
21
+ );
22
+ return found;
23
+ } catch {
24
+ return undefined;
25
+ }
26
+ }
27
+
28
+ const devtoolsApiEsm = findDevtoolsApiEsm();
29
+
30
+ // Resolved from THIS file, not from `~`: when this application is consumed as
31
+ // a Nuxt layer, `~` points at the consuming app, and its own assets are not
32
+ // found.
33
+ const layerDir = dirname(fileURLToPath(import.meta.url));
34
+
35
+ // Le worker de dev empaquette les sources du core, donc le dossier de
36
+ // migrations Drizzle n'y est pas resolvable relativement — on l'epingle ici,
37
+ // ou le chemin est connu. (Repris de l'ancien apps/api/nitro.config.ts.)
38
+ process.env.COMMUN_MIGRATIONS_DIR ??= resolve(
39
+ fileURLToPath(new URL('.', import.meta.url)),
40
+ '../../packages/core/drizzle',
41
+ );
42
+
43
+ // https://nuxt.com/docs/api/configuration/nuxt-config
44
+ export default defineNuxtConfig({
45
+ ssr: false,
46
+ // Reprise de l'ancien apps/api/nitro.config.ts.
47
+ compatibilityDate: '2026-05-19',
48
+ app: {
49
+ head: {
50
+ link: [
51
+ {
52
+ href: 'https://api.tiles.mapbox.com/mapbox-gl-js/v2.9.2/mapbox-gl.css',
53
+ rel: 'stylesheet',
54
+ },
55
+ ],
56
+ charset: 'utf-8',
57
+ viewport: 'width=device-width, initial-scale=1',
58
+ },
59
+ },
60
+ runtimeConfig: {
61
+ // Repertoire de donnees de l'instance (surchargeable par COMMUN_DATA_DIR).
62
+ communDataDir: process.env.COMMUN_DATA_DIR ?? '',
63
+ public: {
64
+ // Vide par défaut (image d'instance : admin et API MÊME origine, appels
65
+ // relatifs `/api/trpc`) ; NUXT_ENV_API_URL reste prioritaire (dev, harness).
66
+ apiURL: process.env.NUXT_ENV_API_URL ?? '',
67
+ baseURL: process.env.NUXT_ENV_BASE_URL || 'http://localhost:3000',
68
+ mapBoxToken: process.env.NUXT_ENV_MAPBOX_TOKEN,
69
+ },
70
+ },
71
+ // Le build du site est déclenché chaque nuit à 00:30 (heure LOCALE du
72
+ // server).
73
+ nitro: {
74
+ experimental: { tasks: true },
75
+ scheduledTasks: {
76
+ '30 0 * * *': ['deploy'],
77
+ },
78
+ },
79
+ css: [join(layerDir, 'assets/css/main.css')],
80
+ // Scan nested composables (composables/core/domains/*).
81
+ imports: { dirs: [join(layerDir, 'composables/**')] },
82
+ // Session auth lives in use-session + middleware/00.auth (no auth module).
83
+ modules: ['@nuxt/ui', '@pinia/nuxt', '@pinia-orm/nuxt', '@vueuse/nuxt'],
84
+ vite: {
85
+ resolve: {
86
+ // Sous le layout node_modules de Bun, Vite retombe sur l'entrée CJS
87
+ // (« does not provide an export named setupDevtoolsPlugin ») — on
88
+ // force l'entrée ESM via un chemin ABSOLU (un remplacement contenant
89
+ // la clé de l'alias boucle à l'infini → OOM).
90
+ alias: {
91
+ ...(devtoolsApiEsm ? { '@vue/devtools-api': devtoolsApiEsm } : {}),
92
+ },
93
+ // One ProseMirror instance is mandatory (keyed plugins): bun's store
94
+ // gives @nuxt/ui and our direct @tiptap deps distinct copies.
95
+ dedupe: [
96
+ // Même raison, côté état : les modèles de l'admin tirent pinia-orm
97
+ // tardivement. (Disparaîtra avec la sortie de pinia-orm, groupe 7.)
98
+ 'pinia',
99
+ 'pinia-orm',
100
+ '@tanstack/vue-query',
101
+ '@trpc/client',
102
+ '@tiptap/core',
103
+ '@tiptap/pm',
104
+ '@tiptap/vue-3',
105
+ 'prosemirror-state',
106
+ 'prosemirror-model',
107
+ 'prosemirror-view',
108
+ 'prosemirror-transform',
109
+ ],
110
+ },
111
+ // Prescribed by the Nuxt UI Editor docs (ProseMirror warning): share a
112
+ // single pre-bundled prosemirror across @nuxt/ui and our extensions.
113
+ optimizeDeps: {
114
+ include: [
115
+ // @open-commune/editor est un paquet de l'ESPACE DE TRAVAIL : Vite ne
116
+ // pré-empaquette pas ses dépendances au démarrage, il les découvre
117
+ // quand l'éditeur se monte — puis ré-optimise, ce qui périme les URL
118
+ // que la page tient déjà (504 « Outdated Optimize Dep », import
119
+ // dynamique en échec, page d'erreur 500). Un rechargement réparait,
120
+ // mais seulement après coup : on les déclare pour que le démarrage à
121
+ // froid soit propre.
122
+ '@tiptap/core',
123
+ '@tiptap/pm/model',
124
+ '@tiptap/pm/state',
125
+ '@tiptap/starter-kit',
126
+ '@tiptap/extension-code',
127
+ '@tiptap/extension-details',
128
+ '@tiptap/extension-file-handler',
129
+ '@tiptap/extension-highlight',
130
+ '@tiptap/extension-horizontal-rule',
131
+ '@tiptap/extension-image',
132
+ '@tiptap/extension-link',
133
+ '@tiptap/extension-list',
134
+ '@tiptap/extension-text-align',
135
+ '@tiptap/extension-text-style',
136
+ '@tiptap/extension-typography',
137
+ '@tiptap/extension-unique-id',
138
+ '@nuxt/ui > prosemirror-state',
139
+ '@nuxt/ui > prosemirror-transform',
140
+ '@nuxt/ui > prosemirror-model',
141
+ '@nuxt/ui > prosemirror-view',
142
+ '@nuxt/ui > prosemirror-gapcursor',
143
+ ],
144
+ },
145
+ // optimizeDeps.esbuildOptions supprimé : Vite 8 optimise via Rolldown
146
+ // (option dépréciée, cible moderne par défaut).
147
+ build: {
148
+ target: 'esnext',
149
+ },
150
+ },
151
+ });
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@open-commune/web",
3
+ "scripts": {
4
+ "build": "nuxt build",
5
+ "dev": "bun --bun nuxt dev",
6
+ "generate": "nuxt generate",
7
+ "preview": "nuxt preview",
8
+ "postinstall": "nuxt prepare",
9
+ "typecheck": "tsc --noEmit -p .nuxt/tsconfig.server.json"
10
+ },
11
+ "devDependencies": {
12
+ "@iconify/vue": "^5.0.1",
13
+ "@nuxt/kit": "^4.5.1",
14
+ "@nuxt/ui": "^4.3.0",
15
+ "@tiptap/pm": "^3.27.3",
16
+ "@tiptap/vue-3": "^3.27.3",
17
+ "@types/node": "^18",
18
+ "@vueuse/core": "^14.4.0",
19
+ "@vueuse/nuxt": "^14.4.0",
20
+ "nuxt": "^4.5.2",
21
+ "tailwindcss": "^4.3.2"
22
+ },
23
+ "dependencies": {
24
+ "@casl/ability": "^7.0.1",
25
+ "@open-commune/core": "workspace:*",
26
+ "@open-commune/editor": "workspace:*",
27
+ "@pinia-orm/nuxt": "^2.0.0",
28
+ "@pinia/nuxt": "^1.0.1",
29
+ "@tanstack/vue-query": "^5.101.4",
30
+ "@trpc/client": "^11.17.0",
31
+ "@trpc/server": "^11.17.0",
32
+ "axios": "^1.6.7",
33
+ "consola": "^3.4.2",
34
+ "h3": "^1.15.11",
35
+ "luxon": "^3.4.4",
36
+ "mapbox-gl": "^3.2.0",
37
+ "ofetch": "^1.3.3",
38
+ "pinia-orm": "^2.0.0",
39
+ "zod": "^4.4.3"
40
+ },
41
+ "description": "The Open Commune application: the admin interface and the server, on a single origin.",
42
+ "license": "AGPL-3.0-or-later",
43
+ "version": "0.1.3",
44
+ "type": "module",
45
+ "main": "./nuxt.config.ts",
46
+ "files": [
47
+ "app.vue",
48
+ "app.config.ts",
49
+ "nuxt.config.ts",
50
+ "assets",
51
+ "components",
52
+ "composables",
53
+ "editor",
54
+ "layouts",
55
+ "middleware",
56
+ "models",
57
+ "pages",
58
+ "plugins",
59
+ "public",
60
+ "server",
61
+ "store"
62
+ ],
63
+ "publishConfig": {
64
+ "access": "public"
65
+ },
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "git+https://github.com/open-commune/open-commune.git",
69
+ "directory": "apps/web"
70
+ }
71
+ }
@@ -0,0 +1,88 @@
1
+ <template>
2
+ <div>
3
+ <!-- <alert
4
+ v-if="alertSort"
5
+ :sort="alertSort"
6
+ :title="ALERTS_MESSAGES[alertSort].title"
7
+ :description="ALERTS_MESSAGES[alertSort].description"
8
+ /> -->
9
+ <forms-layout
10
+ v-if="componentOptions"
11
+ v-bind="{ record, ...componentOptions }"
12
+ class="mt-4"
13
+ />
14
+ </div>
15
+ </template>
16
+
17
+ <script setup>
18
+ // import alert from '@/components/template/content-section/Alert'
19
+ import formsLayout from '~/components/forms/layouts/layout'
20
+
21
+ definePageMeta({
22
+ permissions: ['read:records'],
23
+ preFetch: ['Record'],
24
+ layout: 'record'
25
+ })
26
+
27
+ // Prepare constants
28
+ // const ALERTS_MESSAGES = {
29
+ // info: {
30
+ // title: 'Cet évènement est validé',
31
+ // description: 'Nos équipes ont validé cet évènemnt, il sera prochainement publié en ligne.'
32
+ // },
33
+ // warn: {
34
+ // title: 'Cet évènement est en attente de validation',
35
+ // description: 'Avant d\'être publié, cet évènement doit être validé par nos équipes.'
36
+ // },
37
+ // success: {
38
+ // title: 'Cet évènement est publié',
39
+ // description: 'Vous n\'avez plus rien à faire, vous pouvez consulter cet évènement en ligne.'
40
+ // }
41
+ // }
42
+
43
+ const { Collection, Record } = useModels()
44
+ const $route = useRoute()
45
+
46
+ // Prepare computed
47
+ const isCreating = computed(() => $route.params.record === 'new')
48
+ const collectionRecord = computed(() => Collection.repo.where('slug', $route.params.collection).first())
49
+ const componentOptions = computed(() => {
50
+ const options = JSON.parse(JSON.stringify(collectionRecord.value?.editor || {}))
51
+ if (isCreating.value) {
52
+ options.componentOptions.layout.length = 1
53
+ options.componentOptions.layout[0].sections.length = 1
54
+ }
55
+ return options.componentOptions
56
+ })
57
+ const record = computed(() => {
58
+ // atm. we use onRetrieve directly in page, it should be better to have this
59
+ // handled directly in `pinia-orm` lib
60
+ if (!isCreating.value) {
61
+ return Record.onRetrieve(
62
+ Record.repo.where('_id', $route.params.record).first()
63
+ )
64
+ }
65
+ return {}
66
+ })
67
+
68
+ // const status = computed(() => {
69
+ // const { record: { status } } = this
70
+ // return status
71
+ // }
72
+
73
+ // const alertSort = computed(() => {
74
+ // const { status, $auth } = this
75
+ // switch (status) {
76
+ // case 'waiting':
77
+ // return $auth.hasScope('validate:events') || $auth.hasScope('manage:all') ? '' : 'warn'
78
+ // case 'ready':
79
+ // return $auth.hasScope('publish:events') || $auth.hasScope('manage:all') ? '' : 'info'
80
+ // case 'published':
81
+ // case 'scheduled':
82
+ // return 'success'
83
+ // case 'draft':
84
+ // default:
85
+ // return ''
86
+ // }
87
+ // }
88
+ </script>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <section class="w-full">
3
+ <default-table
4
+ v-if="!$route.params.event"
5
+ :headers="headers"
6
+ :filters="filters"
7
+ :items="records"
8
+ :actions="abilities"
9
+ :pagination-size="limit"
10
+ display-search-bar
11
+ paginated-content
12
+ @fetch-more="_fetchMoreRecords"
13
+ @update="_updateRecord"
14
+ @remove="_removeRecord"
15
+ />
16
+ </section>
17
+ </template>
18
+
19
+ <script setup>
20
+ import defaultTable from '~/components/data/lists/data-lists-table'
21
+
22
+ // Prepare composable
23
+ const { Collection, Record } = useModels()
24
+ const $route = useRoute()
25
+ const $router = useRouter()
26
+
27
+ // Prepare page meta
28
+ definePageMeta({
29
+ permissions: [`read:records`], // TODO handle collections
30
+ preFetch: ['Record']
31
+ })
32
+
33
+ // Prepare reactive data
34
+ const limit = ref(25)
35
+
36
+ // Prepare computed
37
+ const collectionRecord = computed(() => Collection.repo.where('slug', $route.params.collection).first())
38
+ const records = computed(() => Record.repo.where('relatedCollection', $route.params.collection).get().map(Record.onRetrieve))
39
+ const headers = computed(() => collectionRecord.value.display?.headers || [])
40
+ const filters = computed(() => [])
41
+ const abilities = computed(() => ['update', 'remove'])
42
+
43
+ // Prepare methods
44
+ const _fetchMoreRecords = async (skip = 0) => {
45
+ await Record.list($route.params.collection, undefined, { skip, limit: limit.value })
46
+ }
47
+
48
+ const _updateRecord = (item) => {
49
+ $router.push(`${$route.path}/${item._id}`)
50
+ }
51
+
52
+ const _removeRecord = async (item) => {
53
+ await Record.remove(item._id, $route.params.collection)
54
+ }
55
+ </script>
@@ -0,0 +1,149 @@
1
+ <template>
2
+ <div class="mx-auto max-w-4xl space-y-6 p-8">
3
+ <div class="flex items-center justify-between">
4
+ <h1 class="text-xl font-semibold">
5
+ Laboratoire éditeur
6
+ </h1>
7
+ <UBadge :color="roundTripOk ? 'success' : 'error'" variant="subtle">
8
+ {{ roundTripOk ? 'structure conservée' : 'STRUCTURE ALTÉRÉE' }}
9
+ </UBadge>
10
+ </div>
11
+
12
+ <!-- The real production input: same menus, same handlers. -->
13
+ <forms-inputs-input-editor
14
+ label="Contenu"
15
+ :value="doc"
16
+ @change="doc = $event"
17
+ />
18
+
19
+ <details class="text-xs">
20
+ <summary class="cursor-pointer font-medium">JSON courant (sortie de l'éditeur)</summary>
21
+ <pre class="mt-2 overflow-x-auto rounded bg-neutral-100 p-3 dark:bg-neutral-900">{{ JSON.stringify(doc, null, 2) }}</pre>
22
+ </details>
23
+ <details class="text-xs">
24
+ <summary class="cursor-pointer font-medium">JSON d'origine (échantillon des bases clients)</summary>
25
+ <pre class="mt-2 overflow-x-auto rounded bg-neutral-100 p-3 dark:bg-neutral-900">{{ JSON.stringify(SAMPLE, null, 2) }}</pre>
26
+ </details>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup>
31
+ // Editor lab: the assembled editor against a sample shaped like real client
32
+ // content, with a live conservation badge. Dev only.
33
+ definePageMeta({ auth: false })
34
+
35
+ if (!import.meta.dev) {
36
+ await navigateTo('/', { replace: true })
37
+ }
38
+
39
+ // Sample mirroring stored client structures. Any unported node must raise
40
+ // onContentError — never a silent drop.
41
+ const SAMPLE = {
42
+ type: 'doc',
43
+ content: [
44
+ {
45
+ type: 'heading',
46
+ attrs: { level: 2, textAlign: 'left', uid: 'c3d13996-0170-4b13-9a17-2b5eb84400c4' },
47
+ content: [{ type: 'text', text: 'Titre de section' }]
48
+ },
49
+ {
50
+ type: 'paragraph',
51
+ attrs: { textAlign: 'left', uid: '0d5b74a5-c4a5-4485-9d38-2c69e2d1c264' },
52
+ content: [
53
+ { type: 'text', text: 'Du texte ' },
54
+ { type: 'text', marks: [{ type: 'bold' }], text: 'gras' },
55
+ { type: 'text', text: ', de l’' },
56
+ { type: 'text', marks: [{ type: 'italic' }], text: 'italique' },
57
+ { type: 'text', text: ', un ' },
58
+ {
59
+ type: 'text',
60
+ marks: [{ type: 'link', attrs: { href: 'https://exemple.fr', target: '_blank', rel: 'noopener noreferrer nofollow', class: 'underline text-primary' } }],
61
+ text: 'lien'
62
+ },
63
+ { type: 'text', text: ' et une ' },
64
+ {
65
+ type: 'text',
66
+ marks: [{ type: 'textStyle' }],
67
+ text: 'portion neutre'
68
+ },
69
+ { type: 'text', text: '.' }
70
+ ]
71
+ },
72
+ { type: 'paragraph', attrs: { textAlign: 'left', uid: '7c9e8b1a-2f3d-4e5a-8b6c-1d2e3f4a5b6c' }, content: [{ type: 'hardBreak' }] },
73
+ {
74
+ type: 'bulletList',
75
+ attrs: { uid: '58e3399e-92e5-4a6a-b25a-039a4694e9e6' },
76
+ content: [
77
+ {
78
+ type: 'listItem',
79
+ attrs: { uid: 'f9dc0f8f-64f2-40bd-8827-d64ba9214d29' },
80
+ content: [{ type: 'paragraph', attrs: { textAlign: 'left', uid: 'a1b2c3d4-0000-4000-8000-000000000001' }, content: [{ type: 'text', text: 'Premier item' }] }]
81
+ }
82
+ ]
83
+ },
84
+ {
85
+ type: 'callout',
86
+ attrs: { icon: 'iconoir:megaphone', uid: 'a1b2c3d4-0000-4000-8000-000000000002' },
87
+ content: [{ type: 'text', text: 'Un encart d’information.' }]
88
+ },
89
+ {
90
+ type: 'file',
91
+ attrs: {
92
+ id: '64a1f2e3b4c5d6e7f8a9b0c1',
93
+ src: 'https://bucket.exemple.fr/medias/document.pdf',
94
+ title: 'document.pdf',
95
+ alt: null,
96
+ uid: 'a1b2c3d4-0000-4000-8000-000000000004'
97
+ }
98
+ },
99
+ {
100
+ type: 'image',
101
+ attrs: {
102
+ id: '64a1f2e3b4c5d6e7f8a9b0c2',
103
+ src: 'https://bucket.exemple.fr/medias/photo.jpg',
104
+ title: 'photo.jpg',
105
+ alt: 'Une photo',
106
+ uid: 'a1b2c3d4-0000-4000-8000-000000000005'
107
+ }
108
+ },
109
+ {
110
+ type: 'embed',
111
+ attrs: {
112
+ service: 'youtube',
113
+ src: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
114
+ title: null,
115
+ icon: null,
116
+ placeholder: null,
117
+ allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
118
+ allowfullscreen: true,
119
+ frameborder: '0',
120
+ height: 315
121
+ }
122
+ },
123
+ {
124
+ type: 'details',
125
+ attrs: { toggle: true },
126
+ content: [
127
+ { type: 'detailsSummary', content: [{ type: 'text', text: 'Titre de l’accordéon' }] },
128
+ {
129
+ type: 'detailsContent',
130
+ content: [{ type: 'paragraph', attrs: { textAlign: 'left', uid: 'a1b2c3d4-0000-4000-8000-000000000006' }, content: [{ type: 'text', text: 'Contenu replié.' }] }]
131
+ }
132
+ ]
133
+ },
134
+ { type: 'horizontalRule' },
135
+ {
136
+ type: 'blockquote',
137
+ attrs: { uid: 'a1b2c3d4-0000-4000-8000-000000000007' },
138
+ content: [{ type: 'paragraph', attrs: { textAlign: 'left', uid: 'a1b2c3d4-0000-4000-8000-000000000008' }, content: [{ type: 'text', text: 'Une citation.' }] }]
139
+ }
140
+ ]
141
+ }
142
+
143
+ const doc = ref(structuredClone(SAMPLE))
144
+
145
+ // Green: load → output altered nothing. Red is a regression.
146
+ const roundTripOk = computed(
147
+ () => JSON.stringify(doc.value) === JSON.stringify(SAMPLE)
148
+ )
149
+ </script>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <div class="mt-12 text-center text-sm text-gray-500">
3
+ Connexion en cours…
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ // Callback for externally issued sessions (OIDC and the like). The
9
+ // token arrives in the URL FRAGMENT so it never reaches logs or headers,
10
+ // and grants nothing extra: it was issued by this very instance. Absent or
11
+ // invalid, back to login without a technical error.
12
+ definePageMeta({ auth: false })
13
+
14
+ const session = useSession()
15
+
16
+ onMounted(async () => {
17
+ const token = new URLSearchParams(window.location.hash.slice(1)).get('token')
18
+ window.history.replaceState(null, '', '/auth/callback')
19
+ if (!token) {
20
+ return navigateTo('/', { replace: true })
21
+ }
22
+
23
+ session.setToken(token)
24
+ const user = await session.refresh()
25
+ if (user) {
26
+ await navigateTo('/overview', { replace: true })
27
+ } else {
28
+ await navigateTo('/', { replace: true })
29
+ }
30
+ })
31
+ </script>
@@ -0,0 +1,85 @@
1
+ <template>
2
+ <div class="mt-12">
3
+ <form class="space-y-6" form @submit.prevent="_signIn">
4
+ <input-text
5
+ place-holder="Adresse email"
6
+ name="email"
7
+ type="email"
8
+ autocomplete="email"
9
+ :value="emailAddress"
10
+ :read-only="isLoading"
11
+ required
12
+ @change="emailAddress = $event"
13
+ />
14
+ <input-text
15
+ place-holder="Mot de passe"
16
+ name="password"
17
+ type="password"
18
+ autocomplete="current-password"
19
+ :value="password"
20
+ :read-only="isLoading"
21
+ required
22
+ @change="password = $event"
23
+ />
24
+ <button-primary type="submit" label="S'identifier" :loading="isLoading" class="w-full" />
25
+ </form>
26
+ <!-- Le lien de réinitialisation était commenté depuis le legacy, qui
27
+ n'envoyait AUCUN email : le seul recours était le formulaire de
28
+ support. L'événement `passwordResetRequested` étant désormais relayé
29
+ vers un fournisseur d'emails, le parcours autonome reprend sa place —
30
+ et Fillout redevient ce qu'il doit être, un second recours. -->
31
+ <div class="flex mt-8 flex-col items-center justify-center">
32
+ <button-tertiary
33
+ label="Vous avez oublié votre mot de passe ?"
34
+ @click="$router.push({ path: '/password/recover' })"
35
+ />
36
+ <button-tertiary class="h-10" label="Je n'arrive pas à m'identifier" @click="_openForm()" />
37
+ </div>
38
+ </div>
39
+ </template>
40
+
41
+ <script setup>
42
+ import { useNotificationsStore } from '~/store/layout/notifications'
43
+ import InputText from '~/components/forms/inputs/input-text'
44
+ import ButtonPrimary from '~/components/elements/buttons/primary'
45
+ import ButtonTertiary from '~/components/elements/buttons/tertiary'
46
+
47
+ // Prepare composables
48
+ const notificationsStore = useNotificationsStore()
49
+ const session = useSession()
50
+
51
+ // Prepare reactive data
52
+ const isLoading = ref(false)
53
+ const emailAddress = ref('')
54
+ const password = ref('')
55
+
56
+ // Prepare emitters
57
+ defineEmits(['enter'])
58
+
59
+ // Prepare methods
60
+ const _openForm = () => {
61
+ // https://forms.fillout.com/t/vjkLG9ZXsLus
62
+ const formUrl = 'https://forms.fillout.com/t/vjkLG9ZXsLus'
63
+ const formWindow = window.open(formUrl, '_blank')
64
+ if (formWindow) {
65
+ // Browser has allowed it to be opened
66
+ formWindow.focus()
67
+ } else {
68
+ // Browser has blocked it
69
+ alert('Please allow popups for this website')
70
+ }
71
+ }
72
+ const _signIn = async () => {
73
+ isLoading.value = true
74
+ try {
75
+ await session.login(emailAddress.value, password.value)
76
+ notificationsStore.add({ icon: 'iconoir:password-pass', type: 'success', title: 'Authentification réussie.' })
77
+ await navigateTo('/overview')
78
+ } catch (err) {
79
+ console.log(err)
80
+ notificationsStore.add({ icon: 'iconoir:password-error', type: 'warn', title: 'Identifiants incorrects.' })
81
+ } finally {
82
+ isLoading.value = false
83
+ }
84
+ }
85
+ </script>