@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.
- package/README.md +41 -0
- package/app.config.ts +10 -0
- package/app.vue +13 -0
- package/assets/css/main.css +13 -0
- package/components/data/display/row/data-avatar.vue +23 -0
- package/components/data/display/row/data-badge.vue +34 -0
- package/components/data/display/row/data-color.vue +17 -0
- package/components/data/display/row/data-display-badge-publish-status.vue +29 -0
- package/components/data/display/row/data-label.vue +37 -0
- package/components/data/display/row/data-last-update.vue +45 -0
- package/components/data/display/row/data-spacer.vue +4 -0
- package/components/data/display/row/data-summary.vue +44 -0
- package/components/data/display/row/data-text.vue +14 -0
- package/components/data/display/row/data-toggle.vue +16 -0
- package/components/data/display/row.vue +111 -0
- package/components/data/lists/data-list.vue +85 -0
- package/components/data/lists/data-lists-table.vue +155 -0
- package/components/elements/avatars/avatar-circular.vue +68 -0
- package/components/elements/avatars/avatar-with-text.vue +47 -0
- package/components/elements/badges/badge.vue +85 -0
- package/components/elements/buttons/primary.vue +32 -0
- package/components/elements/buttons/secondary.vue +35 -0
- package/components/elements/buttons/tertiary.vue +25 -0
- package/components/elements/dropdowns/dots.vue +75 -0
- package/components/elements/media/image.vue +82 -0
- package/components/elements/notifications/default.vue +90 -0
- package/components/forms/handlers/handler-collections.vue +189 -0
- package/components/forms/handlers/handler-publication-status.vue +193 -0
- package/components/forms/handlers/handler-records.vue +138 -0
- package/components/forms/handlers/handler-roles.vue +176 -0
- package/components/forms/handlers/handler-schedules.vue +218 -0
- package/components/forms/handlers/handler-steps.vue +147 -0
- package/components/forms/handlers/handler-tokens.vue +79 -0
- package/components/forms/inputs/input-color-picker.vue +159 -0
- package/components/forms/inputs/input-editor.vue +363 -0
- package/components/forms/inputs/input-files-media.vue +150 -0
- package/components/forms/inputs/input-files.vue +98 -0
- package/components/forms/inputs/input-json.vue +65 -0
- package/components/forms/inputs/input-location.vue +179 -0
- package/components/forms/inputs/input-text-area.vue +65 -0
- package/components/forms/inputs/input-text.vue +122 -0
- package/components/forms/inputs/input-toggle.vue +125 -0
- package/components/forms/inputs/media-section/media-editor.vue +95 -0
- package/components/forms/layouts/form.vue +203 -0
- package/components/forms/layouts/layout.vue +47 -0
- package/components/forms/layouts/modal.vue +96 -0
- package/components/forms/layouts/navigation.vue +48 -0
- package/components/forms/layouts/section.vue +118 -0
- package/components/forms/selects/select-collection-enum.vue +195 -0
- package/components/forms/selects/select-date.vue +245 -0
- package/components/forms/selects/select-enum.vue +186 -0
- package/components/forms/selects/select-record.vue +274 -0
- package/components/layout/headings/headings-collection.vue +29 -0
- package/components/layout/headings/headings-record.vue +33 -0
- package/components/layout/navigations/primary/dropdown-publish.vue +147 -0
- package/components/layout/navigations/primary.vue +132 -0
- package/components/layout/notifications/notifications-list.vue +19 -0
- package/composables/core/domains/use-definitions.ts +57 -0
- package/composables/core/domains/use-entries.ts +62 -0
- package/composables/core/domains/use-media.ts +82 -0
- package/composables/core/use-core.ts +12 -0
- package/composables/use-ability.js +38 -0
- package/composables/use-models.js +27 -0
- package/composables/use-session.ts +80 -0
- package/composables/use-timer.js +62 -0
- package/composables/use-trpc.js +17 -0
- package/editor/index.ts +44 -0
- package/editor/views/CalloutView.vue +22 -0
- package/editor/views/DetailsContentView.vue +11 -0
- package/editor/views/DetailsSummaryView.vue +11 -0
- package/editor/views/DetailsView.vue +72 -0
- package/editor/views/EmbedView.vue +53 -0
- package/editor/views/FileView.vue +79 -0
- package/editor/views/ImageView.vue +56 -0
- package/editor/views/UploadView.vue +71 -0
- package/layouts/default.vue +21 -0
- package/layouts/entrance.vue +10 -0
- package/layouts/record.vue +21 -0
- package/middleware/00.auth.global.js +16 -0
- package/middleware/01.instance.global.js +21 -0
- package/middleware/03.verify-permissions.global.js +11 -0
- package/middleware/04.prefetch-page.global.js +18 -0
- package/middleware/05.headings-page.global.js +25 -0
- package/models/_commun.js +243 -0
- package/models/_factory.js +34 -0
- package/models/collection.js +87 -0
- package/models/entrance.js +42 -0
- package/models/media.js +82 -0
- package/models/organization.js +92 -0
- package/models/record.js +165 -0
- package/models/role.js +66 -0
- package/models/token.js +56 -0
- package/models/user.js +97 -0
- package/nuxt.config.ts +151 -0
- package/package.json +71 -0
- package/pages/[collection]/[record]/index.vue +88 -0
- package/pages/[collection]/index.vue +55 -0
- package/pages/_lab/editor.vue +149 -0
- package/pages/auth/callback.vue +31 -0
- package/pages/index/index.vue +85 -0
- package/pages/index/password/define/[token].vue +79 -0
- package/pages/index/password/recover/index.vue +72 -0
- package/pages/index/welcome/[token].vue +90 -0
- package/pages/index.vue +44 -0
- package/pages/management/index.vue +15 -0
- package/pages/management/members.vue +176 -0
- package/pages/management/organizations.vue +192 -0
- package/pages/management/roles.vue +183 -0
- package/pages/overview.vue +15 -0
- package/pages/settings.vue +197 -0
- package/plugins/01.session.client.ts +5 -0
- package/plugins/02.vue-query.ts +25 -0
- package/public/favicon.ico +0 -0
- package/public/logo-black.svg +1 -0
- package/public/logo-white.svg +1 -0
- package/server/api/trpc/[...trpc].ts +28 -0
- package/server/api/v1/content/deployment.get.ts +45 -0
- package/server/api/v1/content/records.get.ts +12 -0
- package/server/middleware/0.security.ts +19 -0
- package/server/middleware/1.cors.ts +31 -0
- package/server/middleware/2.session.ts +20 -0
- package/server/middleware/3.api-token.ts +20 -0
- package/server/plugins/core.ts +8 -0
- package/server/routes/_tasks/[name].post.ts +20 -0
- package/server/routes/health.get.ts +6 -0
- package/server/tasks/deploy.ts +26 -0
- package/server/types.d.ts +8 -0
- package/server/utils/core.ts +18 -0
- package/store/layout/headings.js +21 -0
- package/store/layout/modal.js +80 -0
- package/store/layout/notifications.js +52 -0
- package/store/layout/workspace.js +80 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Collection Model
|
|
2
|
+
import { Model } from 'pinia-orm'
|
|
3
|
+
import ModelAPI from '~/models/_factory'
|
|
4
|
+
import {
|
|
5
|
+
attributeToField,
|
|
6
|
+
definitionToCollection,
|
|
7
|
+
invalidateDefinitions
|
|
8
|
+
} from '~/models/_commun'
|
|
9
|
+
|
|
10
|
+
const _collectionModel = class Collection extends Model {
|
|
11
|
+
// This is the name used as module name of the Vuex Store.
|
|
12
|
+
static entity = 'Collection'
|
|
13
|
+
static primaryKey = '_id'
|
|
14
|
+
|
|
15
|
+
// List of all fields (schema) of the post model. `this.attr` is used
|
|
16
|
+
// for the generic field type. The argument is the default value.
|
|
17
|
+
static fields () {
|
|
18
|
+
return {
|
|
19
|
+
_id: this.attr(null),
|
|
20
|
+
name: this.attr(''),
|
|
21
|
+
slug: this.attr(''),
|
|
22
|
+
description: this.attr(''),
|
|
23
|
+
headings: this.attr({}),
|
|
24
|
+
display: this.attr({}),
|
|
25
|
+
editor: this.attr({}),
|
|
26
|
+
attributes: this.attr([]),
|
|
27
|
+
organization: this.attr(''),
|
|
28
|
+
createdBy: this.attr({}),
|
|
29
|
+
updatedBy: this.attr({}),
|
|
30
|
+
createdAt: this.attr(0),
|
|
31
|
+
updatedAt: this.attr(0)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** collection legacy (attributes[]) → payload collections.create/update. */
|
|
37
|
+
function toDefinitionPayload(record) {
|
|
38
|
+
const payload = {}
|
|
39
|
+
for (const key of ['name', 'slug', 'description', 'editor', 'display', 'headings']) {
|
|
40
|
+
if (record[key] !== undefined) payload[key] = record[key]
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(record.attributes)) {
|
|
43
|
+
payload.fields = record.attributes.map(attributeToField).filter(Boolean)
|
|
44
|
+
}
|
|
45
|
+
return payload
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const _collectionAPI = class CollectionAPI extends ModelAPI {
|
|
49
|
+
async list() {
|
|
50
|
+
const definitions = await this.trpc.collections.list.query()
|
|
51
|
+
this.repo.save(definitions.map(definitionToCollection))
|
|
52
|
+
return this.repo.query().all()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async read(_id) {
|
|
56
|
+
const definition = await this.trpc.collections.get.query({ idOrSlug: _id })
|
|
57
|
+
const collection = definitionToCollection(definition)
|
|
58
|
+
this.repo.save(collection)
|
|
59
|
+
return this.repo.find(collection._id)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async create(record) {
|
|
63
|
+
const definition = await this.trpc.collections.create.mutate(toDefinitionPayload(record))
|
|
64
|
+
invalidateDefinitions()
|
|
65
|
+
const collection = definitionToCollection(definition)
|
|
66
|
+
this.repo.save(collection)
|
|
67
|
+
return this.repo.find(collection._id)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async update(_id, record) {
|
|
71
|
+
const definition = await this.trpc.collections.update.mutate({
|
|
72
|
+
id: _id,
|
|
73
|
+
data: toDefinitionPayload(record)
|
|
74
|
+
})
|
|
75
|
+
invalidateDefinitions()
|
|
76
|
+
this.repo.save(definitionToCollection(definition))
|
|
77
|
+
return this.repo.find(_id)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async remove(_id) {
|
|
81
|
+
await this.trpc.collections.remove.mutate({ id: _id })
|
|
82
|
+
invalidateDefinitions()
|
|
83
|
+
return this.repo.destroy(_id)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default { model: _collectionModel, api: _collectionAPI }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Entrance Model
|
|
2
|
+
import { Model } from 'pinia-orm'
|
|
3
|
+
import ModelAPI from '~/models/_factory'
|
|
4
|
+
|
|
5
|
+
const _entranceModel = class Entrance extends Model {
|
|
6
|
+
// This is the name used as module name of the Vuex Store.
|
|
7
|
+
static entity = 'Entrance'
|
|
8
|
+
static primaryKey = '_id'
|
|
9
|
+
|
|
10
|
+
// List of all fields (schema) of the post model. `this.attr` is used
|
|
11
|
+
// for the generic field type. The argument is the default value.
|
|
12
|
+
static fields () {
|
|
13
|
+
return {}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const _entranceAPI = class EntranceAPI extends ModelAPI {
|
|
18
|
+
/**
|
|
19
|
+
* Le flux legacy confirm-email → define-password est remplacé par
|
|
20
|
+
* l'acceptation d'invitation Commun (une seule étape, publique).
|
|
21
|
+
*/
|
|
22
|
+
async acceptInvitation({ token, name, password }) {
|
|
23
|
+
return this.trpc.auth.acceptInvitation.mutate({ token, name, password })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Mot de passe oublié (9.9) : réponse toujours ok (pas d'énumération). */
|
|
27
|
+
async recoverPassword(emailAddress) {
|
|
28
|
+
return this.trpc.auth.requestPasswordReset.mutate({ email: emailAddress })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Le lien du mail de reset pointe sur /password/define/<token> — même
|
|
32
|
+
* acceptation d'invitation, sans nom (conservé côté serveur). */
|
|
33
|
+
async updatePassword({ token, password }) {
|
|
34
|
+
return this.trpc.auth.acceptInvitation.mutate({ token, password })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async confirmEmail() {
|
|
38
|
+
throw new Error('E_NOT_AVAILABLE_YET')
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default { model: _entranceModel, api: _entranceAPI }
|
package/models/media.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Media Model
|
|
2
|
+
import { Model } from 'pinia-orm'
|
|
3
|
+
import ModelAPI from '~/models/_factory'
|
|
4
|
+
import { mediaToLegacy } from '~/models/_commun'
|
|
5
|
+
|
|
6
|
+
const _mediaModel = class Media extends Model {
|
|
7
|
+
// This is the name used as module name of the Vuex Store.
|
|
8
|
+
static entity = 'Media'
|
|
9
|
+
static primaryKey = '_id'
|
|
10
|
+
|
|
11
|
+
// List of all fields (schema) of the post model. `this.attr` is used
|
|
12
|
+
// for the generic field type. The argument is the default value.
|
|
13
|
+
static fields () {
|
|
14
|
+
return {
|
|
15
|
+
_id: this.attr(null),
|
|
16
|
+
originalName: this.attr(''),
|
|
17
|
+
mime: this.attr(''),
|
|
18
|
+
objects: this.attr({}),
|
|
19
|
+
organization: this.attr(''),
|
|
20
|
+
createdBy: this.attr(''),
|
|
21
|
+
updatedBy: this.attr(''),
|
|
22
|
+
createdAt: this.attr(0),
|
|
23
|
+
updatedAt: this.attr(0)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const _mediaAPI = class MediaAPI extends ModelAPI {
|
|
29
|
+
async list() {
|
|
30
|
+
throw new Error('E_UNKNOWN_ENDPOINT')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Flux presigned iso legacy en 2 temps : requestUpload rend { key, url } —
|
|
35
|
+
* la clé S3 joue le rôle du `_id` transitoire que create() (finalize)
|
|
36
|
+
* transmet ensuite.
|
|
37
|
+
*/
|
|
38
|
+
async getS3PreSignedUrl(media) {
|
|
39
|
+
const { key, url } = await this.trpc.media.requestUpload.mutate({
|
|
40
|
+
filename: media.name || 'fichier',
|
|
41
|
+
mime: media.mime
|
|
42
|
+
})
|
|
43
|
+
return { _id: key, url }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async create(record) {
|
|
47
|
+
const media = await this.trpc.media.finalize.mutate({
|
|
48
|
+
key: record._id,
|
|
49
|
+
filename: record.name || 'fichier',
|
|
50
|
+
mime: record.mime
|
|
51
|
+
})
|
|
52
|
+
// Relecture résolue : objects doit contenir les URLs signées, pas les clés.
|
|
53
|
+
const resolved = await this.trpc.media.get.query({ id: media.id })
|
|
54
|
+
const legacy = mediaToLegacy(resolved)
|
|
55
|
+
this.repo.save(legacy)
|
|
56
|
+
return this.repo.find(legacy._id)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async read(_id) {
|
|
60
|
+
const resolved = await this.trpc.media.get.query({ id: _id })
|
|
61
|
+
const legacy = mediaToLegacy(resolved)
|
|
62
|
+
this.repo.save(legacy)
|
|
63
|
+
return this.repo.find(legacy._id)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async update(_id, record) {
|
|
67
|
+
const data = {}
|
|
68
|
+
if (record.alt !== undefined) data.alt = record.alt
|
|
69
|
+
if (record.caption !== undefined) data.caption = record.caption
|
|
70
|
+
if (record.originalName !== undefined) data.filename = record.originalName
|
|
71
|
+
const media = await this.trpc.media.update.mutate({ id: _id, data })
|
|
72
|
+
this.repo.save(mediaToLegacy(media))
|
|
73
|
+
return this.repo.find(_id)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async remove(_id) {
|
|
77
|
+
await this.trpc.media.remove.mutate({ id: _id })
|
|
78
|
+
return this.repo.destroy(_id)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default { model: _mediaModel, api: _mediaAPI }
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Organization Model
|
|
2
|
+
import { Model } from 'pinia-orm'
|
|
3
|
+
import ModelAPI from '~/models/_factory'
|
|
4
|
+
|
|
5
|
+
const _organizationModel = class Organization extends Model {
|
|
6
|
+
// This is the name used as module name of the Vuex Store.
|
|
7
|
+
static entity = 'Organization'
|
|
8
|
+
static primaryKey = '_id'
|
|
9
|
+
|
|
10
|
+
// List of all fields (schema) of the post model. `this.attr` is used
|
|
11
|
+
// for the generic field type. The argument is the default value.
|
|
12
|
+
static fields () {
|
|
13
|
+
return {
|
|
14
|
+
_id: this.attr(''),
|
|
15
|
+
logo: this.attr(null),
|
|
16
|
+
name: this.attr(''),
|
|
17
|
+
slug: this.attr(''),
|
|
18
|
+
type: this.attr(''),
|
|
19
|
+
path: this.attr(''),
|
|
20
|
+
location: this.attr({}),
|
|
21
|
+
settings: this.attr({}),
|
|
22
|
+
injector: this.attr({}),
|
|
23
|
+
deployment: this.attr({}),
|
|
24
|
+
collections: this.attr([]),
|
|
25
|
+
directParent: this.attr({}),
|
|
26
|
+
createdBy: this.attr({}),
|
|
27
|
+
updatedBy: this.attr({}),
|
|
28
|
+
createdAt: this.attr(0),
|
|
29
|
+
updatedAt: this.attr(0)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// Clés du modèle Commun (revue 28/07 : address JSON, colonnes retirées).
|
|
36
|
+
const UPDATABLE = ['name', 'type', 'slug', 'description', 'address', 'deployment', 'settings']
|
|
37
|
+
|
|
38
|
+
const _organizationAPI = class OrganizationAPI extends ModelAPI {
|
|
39
|
+
/** Organization Commun (singleton) → organisation legacy, _id = slug (routes). */
|
|
40
|
+
async _toLegacy(organization) {
|
|
41
|
+
const definitions = await this.trpc.collections.list.query()
|
|
42
|
+
return {
|
|
43
|
+
_id: organization.slug,
|
|
44
|
+
name: organization.name,
|
|
45
|
+
slug: organization.slug,
|
|
46
|
+
type: organization.type,
|
|
47
|
+
path: `/${organization.slug}`,
|
|
48
|
+
logo: organization.settings?.logo ?? null,
|
|
49
|
+
location: {},
|
|
50
|
+
settings: organization.settings ?? {},
|
|
51
|
+
injector: {},
|
|
52
|
+
deployment: organization.deployment ?? {},
|
|
53
|
+
// Single-tenant : toutes les collections sont « actives ».
|
|
54
|
+
collections: definitions.map((definition) => definition.id),
|
|
55
|
+
directParent: {},
|
|
56
|
+
createdAt: organization.createdAt,
|
|
57
|
+
updatedAt: organization.updatedAt
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async list() {
|
|
62
|
+
const organization = await this.trpc.organization.get.query()
|
|
63
|
+
if (!organization) return []
|
|
64
|
+
this.repo.save(await this._toLegacy(organization))
|
|
65
|
+
return this.repo.query().all()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async update(_id, record) {
|
|
69
|
+
const payload = {}
|
|
70
|
+
for (const key of UPDATABLE) {
|
|
71
|
+
if (record[key] !== undefined) payload[key] = record[key]
|
|
72
|
+
}
|
|
73
|
+
// Ex : toggle d'usage des collections (multi-tenant) → sans objet ici.
|
|
74
|
+
if (!Object.keys(payload).length) return this.repo.find(_id)
|
|
75
|
+
const organization = await this.trpc.organization.update.mutate(payload)
|
|
76
|
+
this.repo.save(await this._toLegacy(organization))
|
|
77
|
+
return this.repo.find(_id)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Bouton Publier : déclenche le build Vercel via l'API (tâche deploy). */
|
|
81
|
+
async createDeployment() {
|
|
82
|
+
return this.trpc.organization.deploy.mutate()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// L'état du déploiement Vercel n'est plus interrogé (le legacy sondait
|
|
86
|
+
// l'API Vercel) : le hook déclenché, le build part — polling neutre.
|
|
87
|
+
async getDeployment() {
|
|
88
|
+
return { state: 'READY' }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default { model: _organizationModel, api: _organizationAPI }
|
package/models/record.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Record Model
|
|
2
|
+
import { Model } from "pinia-orm";
|
|
3
|
+
import ModelAPI from "~/models/_factory";
|
|
4
|
+
import {
|
|
5
|
+
entryToRecord,
|
|
6
|
+
getDefinition,
|
|
7
|
+
getUsersById,
|
|
8
|
+
toServerValue,
|
|
9
|
+
} from "~/models/_commun";
|
|
10
|
+
|
|
11
|
+
const _recordModel = class Record extends Model {
|
|
12
|
+
// This is the name used as module name of the Vuex Store.
|
|
13
|
+
static entity = "Record";
|
|
14
|
+
static primaryKey = "_id";
|
|
15
|
+
|
|
16
|
+
// List of all fields (schema) of the post model. `this.attr` is used
|
|
17
|
+
// for the generic field type. The argument is the default value.
|
|
18
|
+
static fields() {
|
|
19
|
+
return {
|
|
20
|
+
_id: this.attr(null),
|
|
21
|
+
title: this.attr(""),
|
|
22
|
+
slug: this.attr(""),
|
|
23
|
+
path: this.attr(""),
|
|
24
|
+
status: this.attr(""),
|
|
25
|
+
relatedCollection: this.attr(""),
|
|
26
|
+
records: this.attr([]),
|
|
27
|
+
attributes: this.attr([]),
|
|
28
|
+
createdBy: this.attr({}),
|
|
29
|
+
updatedBy: this.attr({}),
|
|
30
|
+
publishedAt: this.attr(""),
|
|
31
|
+
createdAt: this.attr(""),
|
|
32
|
+
updatedAt: this.attr(""),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static onRetrieve(data) {
|
|
37
|
+
const { attributes = [], ...rest } = data || {};
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...attributes.reduce(
|
|
41
|
+
(acc = {}, { name, value }) => Object.assign(acc, { [name]: value }),
|
|
42
|
+
{},
|
|
43
|
+
),
|
|
44
|
+
...rest,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static onSave(data) {
|
|
49
|
+
const { title, status, records, ...attributes } = data || {};
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
title,
|
|
53
|
+
status,
|
|
54
|
+
records,
|
|
55
|
+
attributes: Object.keys(attributes).length
|
|
56
|
+
? Object.entries(attributes).map(([name, value]) => ({ name, value }))
|
|
57
|
+
: undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Le serveur borne entries.list à 100 par page — les appels legacy montaient
|
|
63
|
+
// à 500 (select-record) : on pagine pour honorer la limite demandée.
|
|
64
|
+
const PAGE_MAX = 100;
|
|
65
|
+
|
|
66
|
+
const _recordAPI = class RecordAPI extends ModelAPI {
|
|
67
|
+
/**
|
|
68
|
+
* record aplati → payload entries.* : seuls les champs DÉFINIS par la
|
|
69
|
+
* collection alimentent `data` (les colonnes document _id/slug/path/…
|
|
70
|
+
* renvoyées par onSave sont ignorées, iso legacy). `records[]` (liens
|
|
71
|
+
* libres de l'onglet Relations) part en `related` — le serveur entretient
|
|
72
|
+
* la symétrie des liens inverses (upgrade-admin-nuxt4).
|
|
73
|
+
*/
|
|
74
|
+
async _payload(record, collection) {
|
|
75
|
+
const definition = await getDefinition(this.trpc, collection);
|
|
76
|
+
const fieldsByName = new Map(
|
|
77
|
+
definition.fields.map((field) => [field.name, field]),
|
|
78
|
+
);
|
|
79
|
+
const { title, status, records, attributes = [] } = this.onSave(record);
|
|
80
|
+
|
|
81
|
+
const data = {};
|
|
82
|
+
for (const { name, value } of attributes ?? []) {
|
|
83
|
+
const field = fieldsByName.get(name);
|
|
84
|
+
if (!field) continue;
|
|
85
|
+
data[name] = toServerValue(field, value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const payload = {};
|
|
89
|
+
if (title !== undefined) payload.title = title;
|
|
90
|
+
if (status) payload.status = status;
|
|
91
|
+
if (Object.keys(data).length) payload.data = data;
|
|
92
|
+
if (Array.isArray(records)) payload.related = records;
|
|
93
|
+
return { definition, payload };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async read(_id, collection) {
|
|
97
|
+
const definition = await getDefinition(this.trpc, collection);
|
|
98
|
+
const [entry, usersById] = await Promise.all([
|
|
99
|
+
this.trpc.collections.entries.get.query({ id: _id }),
|
|
100
|
+
getUsersById(this.trpc),
|
|
101
|
+
]);
|
|
102
|
+
this.repo.save(entryToRecord(definition, entry, usersById));
|
|
103
|
+
return this.repo.find(_id);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async list(collection, organizationId, queryParams = {}) {
|
|
107
|
+
// Une collection absente ne casse pas la page (iso legacy : liste vide).
|
|
108
|
+
const definition = await getDefinition(this.trpc, collection).catch(
|
|
109
|
+
() => null,
|
|
110
|
+
);
|
|
111
|
+
if (!definition) return this.repo.query().all();
|
|
112
|
+
|
|
113
|
+
const skip = Number(queryParams.skip ?? 0);
|
|
114
|
+
const wanted = Number(queryParams.limit ?? 20);
|
|
115
|
+
const entries = [];
|
|
116
|
+
while (entries.length < wanted) {
|
|
117
|
+
const size = Math.min(wanted - entries.length, PAGE_MAX);
|
|
118
|
+
const page = await this.trpc.collections.entries.list.query({
|
|
119
|
+
collectionId: definition.id,
|
|
120
|
+
skip: skip + entries.length,
|
|
121
|
+
limit: size,
|
|
122
|
+
});
|
|
123
|
+
entries.push(...page);
|
|
124
|
+
if (page.length < size) break;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const usersById = await getUsersById(this.trpc);
|
|
128
|
+
const records = entries.map((entry) => entryToRecord(definition, entry, usersById));
|
|
129
|
+
if (queryParams?.skip !== 0) {
|
|
130
|
+
this.repo.save(records);
|
|
131
|
+
} else {
|
|
132
|
+
this.repo.fresh(records);
|
|
133
|
+
}
|
|
134
|
+
return this.repo.query().all();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async create(record, collection) {
|
|
138
|
+
const { definition, payload } = await this._payload(record, collection);
|
|
139
|
+
const entry = await this.trpc.collections.entries.create.mutate({
|
|
140
|
+
collectionId: definition.id,
|
|
141
|
+
// `data` est requis à la création (le slug est généré serveur, iso legacy).
|
|
142
|
+
data: { status: "draft", ...payload, data: payload.data ?? {} },
|
|
143
|
+
});
|
|
144
|
+
const saved = entryToRecord(definition, entry, await getUsersById(this.trpc));
|
|
145
|
+
this.repo.save(saved);
|
|
146
|
+
return this.repo.find(saved._id);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async update(_id, record, collection) {
|
|
150
|
+
const { definition, payload } = await this._payload(record, collection);
|
|
151
|
+
const entry = await this.trpc.collections.entries.update.mutate({
|
|
152
|
+
id: _id,
|
|
153
|
+
data: payload,
|
|
154
|
+
});
|
|
155
|
+
this.repo.save(entryToRecord(definition, entry, await getUsersById(this.trpc)));
|
|
156
|
+
return this.repo.find(_id);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async remove(_id) {
|
|
160
|
+
await this.trpc.collections.entries.remove.mutate({ id: _id });
|
|
161
|
+
return this.repo.destroy(_id);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export default { model: _recordModel, api: _recordAPI };
|
package/models/role.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Role Model
|
|
2
|
+
import { Model } from 'pinia-orm'
|
|
3
|
+
import ModelAPI from '~/models/_factory'
|
|
4
|
+
|
|
5
|
+
const _roleModel = class Role extends Model {
|
|
6
|
+
// This is the name used as module name of the Vuex Store.
|
|
7
|
+
static entity = 'Role'
|
|
8
|
+
static primaryKey = '_id'
|
|
9
|
+
|
|
10
|
+
// List of all fields (schema) of the post model. `this.attr` is used
|
|
11
|
+
// for the generic field type. The argument is the default value.
|
|
12
|
+
static fields () {
|
|
13
|
+
return {
|
|
14
|
+
_id: this.attr(null),
|
|
15
|
+
name: this.attr(''),
|
|
16
|
+
description: this.attr(''),
|
|
17
|
+
permissions: this.attr([]),
|
|
18
|
+
onlyOwn: this.attr(false),
|
|
19
|
+
mfa: this.attr(false),
|
|
20
|
+
createdBy: this.attr({}),
|
|
21
|
+
updatedBy: this.attr({}),
|
|
22
|
+
createdAt: this.attr(0),
|
|
23
|
+
updatedAt: this.attr(0)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Les rôles paramétrables du legacy sont remplacés par le modèle fixe Commun
|
|
30
|
+
* (admin / redacteur). La liste est statique ; le CRUD est inerte.
|
|
31
|
+
*/
|
|
32
|
+
const ROLES = [
|
|
33
|
+
{
|
|
34
|
+
_id: 'admin',
|
|
35
|
+
name: 'Administrateur',
|
|
36
|
+
description: 'Accès complet : contenus, médias, membres et réglages.',
|
|
37
|
+
permissions: ['manage:all']
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
_id: 'redacteur',
|
|
41
|
+
name: 'Rédacteur',
|
|
42
|
+
description: 'Gestion des contenus et des médias.',
|
|
43
|
+
permissions: ['entrance:self', 'read:records', 'create:records', 'update:records', 'delete:records']
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
const _roleAPI = class RoleAPI extends ModelAPI {
|
|
48
|
+
async list() {
|
|
49
|
+
this.repo.save(ROLES)
|
|
50
|
+
return this.repo.query().all()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async create() {
|
|
54
|
+
throw new Error('E_ROLES_FIXED')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async update() {
|
|
58
|
+
throw new Error('E_ROLES_FIXED')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async remove() {
|
|
62
|
+
throw new Error('E_ROLES_FIXED')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default { model: _roleModel, api: _roleAPI }
|
package/models/token.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Token Model
|
|
2
|
+
import { Model } from 'pinia-orm'
|
|
3
|
+
import ModelAPI from '~/models/_factory'
|
|
4
|
+
|
|
5
|
+
const _tokenModel = class Token extends Model {
|
|
6
|
+
// This is the name used as module name of the Vuex Store.
|
|
7
|
+
static entity = 'Token'
|
|
8
|
+
static primaryKey = '_id'
|
|
9
|
+
|
|
10
|
+
// List of all fields (schema) of the post model. `this.attr` is used
|
|
11
|
+
// for the generic field type. The argument is the default value.
|
|
12
|
+
static fields () {
|
|
13
|
+
return {
|
|
14
|
+
_id: this.attr(null),
|
|
15
|
+
name: this.attr(''),
|
|
16
|
+
token: this.attr(''),
|
|
17
|
+
lastEntrance: this.attr(0),
|
|
18
|
+
organization: this.attr({}),
|
|
19
|
+
createdBy: this.attr({}),
|
|
20
|
+
createdAt: this.attr(0),
|
|
21
|
+
updatedAt: this.attr(0)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const _tokenAPI = class TokenAPI extends ModelAPI {
|
|
27
|
+
async list() {
|
|
28
|
+
const tokens = await this.trpc.apiTokens.list.query()
|
|
29
|
+
// Les tokens sont désormais stockés hachés : la valeur en clair n'est
|
|
30
|
+
// visible qu'à la création (différence assumée vs legacy plaintext).
|
|
31
|
+
this.repo.save(
|
|
32
|
+
tokens
|
|
33
|
+
.filter((token) => !token.revokedAt)
|
|
34
|
+
.map((token) => ({
|
|
35
|
+
_id: token.id,
|
|
36
|
+
name: token.name,
|
|
37
|
+
lastEntrance: token.lastUsedAt ?? 0,
|
|
38
|
+
createdAt: token.createdAt
|
|
39
|
+
}))
|
|
40
|
+
)
|
|
41
|
+
return this.repo.query().all()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async create(record) {
|
|
45
|
+
const created = await this.trpc.apiTokens.create.mutate({ name: record.name })
|
|
46
|
+
this.repo.save({ _id: created.id, name: created.name, token: created.token })
|
|
47
|
+
return this.repo.find(created.id)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async remove(_id) {
|
|
51
|
+
await this.trpc.apiTokens.revoke.mutate({ id: _id })
|
|
52
|
+
return this.repo.destroy(_id)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default { model: _tokenModel, api: _tokenAPI }
|
package/models/user.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// User Model
|
|
2
|
+
import { Model } from 'pinia-orm'
|
|
3
|
+
import ModelAPI from '~/models/_factory'
|
|
4
|
+
|
|
5
|
+
const _userModel = class User extends Model {
|
|
6
|
+
// This is the name used as module name of the Vuex Store.
|
|
7
|
+
static entity = 'User'
|
|
8
|
+
static primaryKey = '_id'
|
|
9
|
+
|
|
10
|
+
// List of all fields (schema) of the post model. `this.attr` is used
|
|
11
|
+
// for the generic field type. The argument is the default value.
|
|
12
|
+
static fields () {
|
|
13
|
+
return {
|
|
14
|
+
_id: this.attr(null),
|
|
15
|
+
firstName: this.attr(''),
|
|
16
|
+
lastName: this.attr(''),
|
|
17
|
+
phone: this.attr(''),
|
|
18
|
+
emailAddress: this.attr(''),
|
|
19
|
+
emailStatus: this.attr(''),
|
|
20
|
+
emailChangeCandidate: this.attr(''),
|
|
21
|
+
avatar: this.attr({}),
|
|
22
|
+
role: this.attr(''),
|
|
23
|
+
organizations: this.attr([]),
|
|
24
|
+
permissions: this.attr([]),
|
|
25
|
+
createdBy: this.attr({}),
|
|
26
|
+
updatedBy: this.attr({}),
|
|
27
|
+
createdAt: this.attr(0),
|
|
28
|
+
updatedAt: this.attr(0)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get full name of the user.
|
|
34
|
+
*/
|
|
35
|
+
get fullName () {
|
|
36
|
+
return [this.firstName, this.lastName].join(' ')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** PublicUser Commun { id, email, name, role } → user legacy. */
|
|
41
|
+
function userToLegacy(user) {
|
|
42
|
+
return {
|
|
43
|
+
_id: user.id,
|
|
44
|
+
firstName: user.name ?? '',
|
|
45
|
+
lastName: '',
|
|
46
|
+
emailAddress: user.email,
|
|
47
|
+
emailStatus: 'confirmed',
|
|
48
|
+
role: user.role,
|
|
49
|
+
organizations: [],
|
|
50
|
+
permissions: []
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const _userAPI = class UserAPI extends ModelAPI {
|
|
55
|
+
async list() {
|
|
56
|
+
const users = await this.trpc.users.list.query()
|
|
57
|
+
this.repo.save(users.map(userToLegacy))
|
|
58
|
+
return this.repo.query().all()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async read(_id) {
|
|
62
|
+
const user = await this.trpc.users.get.query({ id: _id })
|
|
63
|
+
this.repo.save(userToLegacy(user))
|
|
64
|
+
return this.repo.find(_id)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* La création directe n'existe plus : on émet une INVITATION (le lien est
|
|
69
|
+
* affiché à l'admin tant que l'envoi de mails — tâche 9.9 — n'est pas porté).
|
|
70
|
+
* Retourne { token, expiresAt } en plus de rafraîchir la liste.
|
|
71
|
+
*/
|
|
72
|
+
async create(record) {
|
|
73
|
+
const invitation = await this.trpc.users.invite.mutate({
|
|
74
|
+
email: record.emailAddress,
|
|
75
|
+
role: record.role || 'redacteur'
|
|
76
|
+
})
|
|
77
|
+
return invitation
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async update(_id, record) {
|
|
81
|
+
const data = {}
|
|
82
|
+
if (record.firstName !== undefined || record.lastName !== undefined) {
|
|
83
|
+
data.name = [record.firstName, record.lastName].filter(Boolean).join(' ')
|
|
84
|
+
}
|
|
85
|
+
if (record.role !== undefined) data.role = record.role
|
|
86
|
+
const user = await this.trpc.users.update.mutate({ id: _id, data })
|
|
87
|
+
this.repo.save(userToLegacy(user))
|
|
88
|
+
return this.repo.find(_id)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async remove(_id) {
|
|
92
|
+
await this.trpc.users.remove.mutate({ id: _id })
|
|
93
|
+
return this.repo.destroy(_id)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default { model: _userModel, api: _userAPI }
|