@vevedh/create-feathers-app 0.1.0-rc.1 → 0.1.0-rc.5
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 +55 -18
- package/bin/create-feathers-app.mjs +13 -2
- package/lib/scaffold.mjs +196 -14
- package/lib/secure-runtime/connect.mjs +28 -0
- package/lib/secure-runtime/policy.mjs +85 -0
- package/lib/secure-runtime/prepare-storage.mjs +14 -0
- package/lib/secure-runtime/providers.mjs +74 -0
- package/lib/secure-runtime/register.mjs +80 -0
- package/lib/secure-runtime/storage.mjs +31 -0
- package/lib/services.mjs +227 -0
- package/lib/typescript.mjs +148 -0
- package/package.json +2 -2
- package/templates/common/dev-all-quasar-ssr.mjs +37 -0
- package/templates/daisyui-ssg/gitignore.template +20 -0
- package/templates/daisyui-ssg/src/style.css +3 -1
- package/templates/daisyui-ssr/gitignore.template +20 -0
- package/templates/daisyui-ssr/src/style.css +3 -1
- package/templates/quasar-ssr/.env.example +11 -0
- package/templates/quasar-ssr/api/server.mjs +45 -0
- package/templates/quasar-ssr/api/services/health/health.class.mjs +3 -0
- package/templates/quasar-ssr/api/services/health/health.service.mjs +4 -0
- package/templates/quasar-ssr/api/services/notes/notes.class.mjs +25 -0
- package/templates/quasar-ssr/api/services/notes/notes.service.mjs +10 -0
- package/templates/quasar-ssr/gitignore.template +36 -0
- package/templates/quasar-ssr/index.html +12 -0
- package/templates/quasar-ssr/quasar.config.ts +27 -0
- package/templates/quasar-ssr/scripts/edge-proxy.mjs +30 -0
- package/templates/quasar-ssr/scripts/edge-routing.mjs +14 -0
- package/templates/quasar-ssr/src/App.vue +1 -0
- package/templates/quasar-ssr/src/css/app.css +20 -0
- package/templates/quasar-ssr/src/pages/IndexPage.vue +146 -0
- package/templates/quasar-ssr/src/pages/NotFoundPage.vue +11 -0
- package/templates/quasar-ssr/src/router/index.ts +8 -0
- package/templates/quasar-ssr/src/router/routes.ts +6 -0
- package/templates/quasar-ssr/src/stores/index.ts +4 -0
- package/templates/quasar-ssr/src/stores/notes.ts +73 -0
- package/templates/quasar-ssr/src-ssr/middlewares/render.ts +15 -0
- package/templates/quasar-ssr/src-ssr/server.ts +42 -0
- package/templates/quasar-ssr/tsconfig.json +4 -0
- package/templates/spa/common/.env.example +1 -0
- package/templates/spa/common/api/server.mjs +2 -1
- package/templates/spa/common/gitignore.template +20 -0
- package/templates/spa/common/src/stores/notes.ts +71 -12
- package/templates/spa/daisyui/src/components/NotesPanel.vue +24 -7
- package/templates/spa/daisyui/src/style.css +3 -1
- package/templates/spa/quasar2/src/components/NotesPanel.vue +24 -6
package/README.md
CHANGED
|
@@ -1,43 +1,80 @@
|
|
|
1
1
|
# create-feathers-app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Générez des applications FeathersJS v5 avec Vue 3, Pinia, Vite, Quasar 2 ou Tailwind CSS + daisyUI.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Le CLI est indépendant du runtime [`@vevedh/vite-plugin-feathersjs`](https://www.npmjs.com/package/@vevedh/vite-plugin-feathersjs) (version 0.2.6). La version candidate `0.1.0-rc.4` **n'est pas encore publiée**. Elle ajoute la PWA optionnelle au rendu SPA.
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
9
9
|
```powershell
|
|
10
|
-
|
|
10
|
+
# Préversion disponible sur npm : 0.1.0-rc.1
|
|
11
|
+
npm exec --yes --package=@vevedh/create-feathers-app@0.1.0-rc.1 -- create-feathers-app portail --ui daisyui --render spa
|
|
12
|
+
|
|
13
|
+
# R17-r3 candidat, depuis les sources mises en stage :
|
|
14
|
+
node .release/create-feathers-app/bin/create-feathers-app.mjs portail-ssr --ui quasar2 --render ssr
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
| Interface | SPA | SSR | SSG |
|
|
18
|
+
| --- | --- | --- | --- |
|
|
19
|
+
| Quasar 2 | Générable | Générable, validation Windows en cours | Bloqué |
|
|
20
|
+
| daisyUI 5 | Générable | Générable | Générable |
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
## PWA optionnelle (SPA uniquement)
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
Le paramètre `--pwa on` ajoute `vite-plugin-pwa` v2 en dépendance de développement, une icône SVG locale de démonstration et un manifeste Web App. Le service worker, désactivé en développement, est généré au build et conserve uniquement des ressources statiques, jamais les réponses `/api` ou les échanges Socket.IO. Par défaut, `--pwa off` ne change pas les templates existants.
|
|
25
|
+
|
|
26
|
+
```powershell
|
|
27
|
+
node .release/create-feathers-app/bin/create-feathers-app.mjs portail-spa --ui quasar2 --render spa --pwa on --dry-run
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
SSR/SSG avec PWA est refusé tant que leur cycle de vie n'est pas certifié. Pour une installation sur les navigateurs ciblés, remplacer l'icône SVG de démonstration par des icônes PNG 192 et 512 pixels, puis contrôler HTTPS, les mises à jour et le mode hors-ligne.
|
|
31
|
+
|
|
32
|
+
## Ajouter un service Feathers
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
```powershell
|
|
35
|
+
cd portail
|
|
36
|
+
node ../chemin/vers/create-feathers-app/bin/create-feathers-app.mjs service add tickets --schema zod --db main
|
|
37
|
+
```
|
|
23
38
|
|
|
24
|
-
|
|
39
|
+
Le générateur ajoute le module `api/services/tickets` et son inscription dans le registre Feathers. Les schémas disponibles sont `typebox`, `json` et `zod`. Le service mémoire est destiné au laboratoire local, avec CRUD REST et validation des données.
|
|
25
40
|
|
|
26
|
-
|
|
41
|
+
## Profils d'identité et de bases
|
|
27
42
|
|
|
28
|
-
|
|
43
|
+
La création de projet accepte `--auth none|jwt|oauth2|oidc|keycloak`, `--schema typebox|json|zod` et `--db main:memory,audit:mongodb` avec alias nommés. **Les authentifications externes et les connexions persistantes sont des profils de configuration, pas des intégrations opérationnelles.** L'API échoue volontairement au démarrage jusqu'au raccordement et à la certification du fournisseur ou du SGBD.
|
|
44
|
+
|
|
45
|
+
Les projets sans authentification sont exclusivement des démonstrations locales. Ils ne doivent jamais être exposés tels quels sur Internet. Aucun secret, mot de passe ou jeton n'est inclus dans les templates.
|
|
46
|
+
|
|
47
|
+
## Windows
|
|
29
48
|
|
|
30
49
|
```powershell
|
|
31
50
|
corepack pnpm install
|
|
32
|
-
corepack pnpm run dev:all
|
|
33
51
|
corepack pnpm run typecheck
|
|
34
52
|
corepack pnpm run build
|
|
53
|
+
corepack pnpm run dev:all
|
|
35
54
|
```
|
|
36
55
|
|
|
37
|
-
|
|
56
|
+
SPA et daisyUI SSR/SSG utilisent `http://127.0.0.1:5173`. Quasar SSR utilise `http://127.0.0.1:8080` via un proxy local ; le SSR brut écoute sur 9100.
|
|
57
|
+
|
|
58
|
+
La documentation publique VitePress figure dans `docs/cli` du dépôt et sera déployée sur GitHub Pages après activation du workflow de publication.
|
|
38
59
|
|
|
39
|
-
|
|
60
|
+
## API Feathers intégrée à Vite en développement (RC4)
|
|
40
61
|
|
|
41
|
-
|
|
62
|
+
Pour les applications SPA, `--dev-api vite` est désormais la valeur par défaut.
|
|
63
|
+
Le plugin public `@vevedh/vite-plugin-feathersjs@0.2.6` est chargé depuis
|
|
64
|
+
`vite.config.ts` et monte les services sur `/api` pendant `vite dev`.
|
|
65
|
+
Il n'y a pas de second serveur REST ni de proxy REST à démarrer.
|
|
66
|
+
|
|
67
|
+
L'option `--dev-api separate` conserve l'ancien comportement à deux processus.
|
|
68
|
+
Pour SSR/SSG, le mode séparé reste obligatoire. Avec `--transport both`,
|
|
69
|
+
Socket.IO reste sur le port privé `127.0.0.1:3031`, accessible depuis la SPA
|
|
70
|
+
via le proxy WebSocket Vite. La configuration de Feathers échoue au chargement
|
|
71
|
+
si les profils d'identité ou bases externes ne sont pas opérationnels.
|
|
72
|
+
|
|
73
|
+
```powershell
|
|
74
|
+
node .release/create-feathers-app/bin/create-feathers-app.mjs mon-spa --ui quasar2 --render spa --dev-api vite --transport both
|
|
75
|
+
```
|
|
42
76
|
|
|
43
|
-
|
|
77
|
+
**Production** : le plugin Vite s'exécute uniquement en développement.
|
|
78
|
+
Le serveur `api/server.mjs` autonome reste disponible et obligatoire pour
|
|
79
|
+
le déploiement ; `vite preview` n'héberge pas l'API.
|
|
80
|
+
Le runtime `0.2.6` n'a pas été modifié.
|
|
@@ -3,6 +3,7 @@ import { dirname,resolve } from 'node:path'
|
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { readFile } from 'node:fs/promises'
|
|
5
5
|
import { createGenerator } from '../lib/scaffold.mjs'
|
|
6
|
+
import { runServiceCommand } from '../lib/services.mjs'
|
|
6
7
|
const packageRoot=resolve(dirname(fileURLToPath(import.meta.url)),'..')
|
|
7
8
|
const {version}=JSON.parse(await readFile(resolve(packageRoot,'package.json'),'utf8'))
|
|
8
9
|
const args=process.argv.slice(2)
|
|
@@ -16,13 +17,23 @@ Usage: create-feathers-app <name> [options]
|
|
|
16
17
|
--render <spa|ssr|ssg> Default spa
|
|
17
18
|
--adapter <koa|express> Default koa
|
|
18
19
|
--transport <rest|both> Default rest (both only for SPA)
|
|
20
|
+
--pwa <on|off> Optional offline SPA shell (default off, no API caching)
|
|
21
|
+
--dev-api <vite|separate> SPA default vite; SSR/SSG always separate
|
|
19
22
|
--dry-run Preview files without writing
|
|
20
23
|
--json Machine-readable output
|
|
24
|
+
--schema <typebox|json|zod> Service schema preset (default typebox)
|
|
25
|
+
--auth <none|jwt|oauth2|oidc|keycloak> Identity configuration blueprint
|
|
26
|
+
--db <alias:adapter[,alias:adapter]> Named connections (default main:memory)
|
|
21
27
|
--version Print CLI version
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
Service: create-feathers-app service add <name> [--project <path>] [--schema typebox|json|zod] [--db <alias>] [--auth none|jwt] [--lang js|ts] [--dry-run]
|
|
30
|
+
|
|
31
|
+
Available: Quasar 2 SPA/SSR; daisyUI SPA, SSR, SSG.
|
|
32
|
+
Quasar SSR uses the R16-certified architecture; R17-generated SSR requires its own Windows E2E certification. Quasar SSG not supported.`)
|
|
25
33
|
}else if(args.includes('--version')||args.includes('-v'))console.log(version)
|
|
34
|
+
else if(args[0]==='service') {
|
|
35
|
+
try { await runServiceCommand(args.slice(1),process.cwd()) } catch(error){console.error(`FAIL: ${error.message}`);process.exitCode=1}
|
|
36
|
+
}
|
|
26
37
|
else {
|
|
27
38
|
const {parse,generate}=createGenerator({outputRoot:process.cwd(),templatesRoot:resolve(packageRoot,'templates'),reportRoot:process.cwd()})
|
|
28
39
|
try {
|
package/lib/scaffold.mjs
CHANGED
|
@@ -3,9 +3,10 @@ import { existsSync } from 'node:fs'
|
|
|
3
3
|
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
4
4
|
import { buildProjectPlan } from './planner/plan.mjs'
|
|
5
5
|
|
|
6
|
-
const KEYS = new Set(['--name', '--dir', '--ui', '--render', '--adapter', '--transport', '--dry-run', '--json'])
|
|
6
|
+
const KEYS = new Set(['--name', '--dir', '--ui', '--render', '--adapter', '--transport', '--dry-run', '--json', '--schema', '--auth', '--db', '--pwa', '--dev-api'])
|
|
7
7
|
const CASES = Object.freeze({
|
|
8
8
|
'quasar2:spa': 'quasar-spa',
|
|
9
|
+
'quasar2:ssr': 'quasar-ssr',
|
|
9
10
|
'daisyui:spa': 'daisyui-spa',
|
|
10
11
|
'daisyui:ssr': 'daisyui-ssr',
|
|
11
12
|
'daisyui:ssg': 'daisyui-ssg',
|
|
@@ -50,6 +51,7 @@ function spaManifest(o) {
|
|
|
50
51
|
vite:'^7.3.7', '@vitejs/plugin-vue':'^6.0.0', 'vue-tsc':'^3.0.7',
|
|
51
52
|
typescript:'^5.9.3', '@types/node':'^22.0.0',
|
|
52
53
|
}
|
|
54
|
+
if (o.pwa === 'on') devDependencies['vite-plugin-pwa'] = '^2.0.0'
|
|
53
55
|
if (o.ui === 'quasar2') { dependencies.quasar = '^2.35.2'; devDependencies['@quasar/vite-plugin']='^1.12.0';devDependencies['sass-embedded']='^1.93.2' }
|
|
54
56
|
else { devDependencies.tailwindcss='^4.1.0';devDependencies['@tailwindcss/vite']='^4.1.0';devDependencies.daisyui='^5.0.0' }
|
|
55
57
|
return {name:o.name,version:'0.1.0',private:true,type:'module',packageManager:'pnpm@10.30.2',engines:{node:'>=22.12'},
|
|
@@ -60,19 +62,100 @@ function spaManifest(o) {
|
|
|
60
62
|
function generatedReadme(o) {
|
|
61
63
|
const renderInfo = o.render === 'ssg' ? 'Le build pré-rend les routes publiques / et /about. Les notes restent dynamiques via /api.'
|
|
62
64
|
: o.render === 'ssr' ? 'Le serveur Node effectue le SSR avec un état Pinia distinct par requête.'
|
|
63
|
-
:
|
|
65
|
+
: o.devApi === 'vite'
|
|
66
|
+
? 'En développement, le plugin Vite monte les services Feathers directement sur /api. En production, l’API doit être lancée et déployée séparément.'
|
|
67
|
+
: 'Le frontend est une SPA Vite. En développement, une API Feathers indépendante écoute sur 127.0.0.1:3030.'
|
|
64
68
|
return `# ${o.name}\n\nProjet ${o.ui} ${o.render.toUpperCase()} avec Vue 3, Pinia et FeathersJS v5 (${o.adapter}, ${o.transport}).\n\n`+
|
|
65
69
|
`${renderInfo}\n\n## Démarrage local (Windows / PowerShell)\n\n`+
|
|
66
70
|
'```powershell\ncorepack pnpm install\ncorepack pnpm run dev:all\n```\n\n' +
|
|
67
|
-
'Ouvrir http://127.0.0.1:5173. L’API utilise http://127.0.0.1:3030.\n\n' +
|
|
71
|
+
(o.devApi === 'vite' ? 'Ouvrir http://127.0.0.1:5173. Pendant le développement, Feathers REST est servi sur le même port via /api.\n\n' : 'Ouvrir http://127.0.0.1:5173. L’API indépendante utilise http://127.0.0.1:3030.\n\n') +
|
|
68
72
|
'```powershell\ncorepack pnpm run typecheck\ncorepack pnpm run build\n```\n\n' +
|
|
69
73
|
(o.render !== 'spa' ? 'Pour vérifier le build, lancer `corepack pnpm run serve:production` avec l’API active.\n\n' : '') +
|
|
70
|
-
'
|
|
74
|
+
(o.devApi === 'vite' ? 'Le plugin Vite est actif en développement uniquement. `pnpm run preview` sert les fichiers statiques et ne démarre pas l’API. Pour tester la SPA construite avec CRUD, lancer `corepack pnpm run dev:api` dans un premier terminal puis `corepack pnpm run preview` dans un second. Le proxy `preview.proxy` relaie /api vers 127.0.0.1:3030 (et Socket.IO vers 3031 si activé). En l’absence de backend, l’interface affiche « API indisponible » ; hors ligne, seule la coque statique reste disponible. Ne jamais exposer le serveur de démonstration sans authentification.\n\n' : '') +
|
|
75
|
+
`Profil: schema ${o.schema}; auth ${o.auth}; databases ${o.db}. Les profils d'identité et les bases externes nécessitent un raccordement explicite.\n\n`+
|
|
76
|
+
(o.ui==='quasar2' && o.render==='ssr'?'Quasar SSR : ouvrir http://127.0.0.1:8080 (reverse proxy local). Ne pas exposer le serveur SSR directement.\n\n':'')+
|
|
77
|
+
(o.pwa === 'on' ? 'PWA SPA activée : le service worker n’est produit qu’au build. Il met en cache les ressources statiques uniquement, jamais les réponses API ou Socket.IO.\nL’icône SVG d’exemple doit être remplacée par des icônes PNG 192/512 adaptées avant une distribution publique. En HTTPS, tester les mises à jour et l’installation sur le navigateur cible.\n\n' : '') +
|
|
78
|
+
'Les données de démonstration sont en mémoire ; sans authentification les services ne doivent pas être exposés publiquement.\n'
|
|
71
79
|
}
|
|
80
|
+
/** Replace the SPA proxy with the public Vite plugin. The standalone API remains for production. */
|
|
81
|
+
function enableSpaViteRuntime(files, options) {
|
|
82
|
+
const source = files.get('vite.config.ts')
|
|
83
|
+
const marker = 'plugins: [vue(),'
|
|
84
|
+
const oldProxy = ",\n proxy: { '/api': 'http://127.0.0.1:3030', '/socket.io': { target: 'ws://127.0.0.1:3031', ws: true } }"
|
|
85
|
+
if (typeof source !== 'string' || !source.includes(marker) || !source.includes(oldProxy)) {
|
|
86
|
+
throw new Error('Unsupported SPA Vite template for embedded Feathers; refusing partial generation')
|
|
87
|
+
}
|
|
88
|
+
const realtime = options.transport === 'both'
|
|
89
|
+
const socket = realtime
|
|
90
|
+
? "{ port: 3031, host: '127.0.0.1', origins: ['http://127.0.0.1:5173'] }"
|
|
91
|
+
: 'false'
|
|
92
|
+
const runtime = `feathers({
|
|
93
|
+
serviceFiles: 'api/services/*/*.service.mjs',
|
|
94
|
+
watchDirectories: ['api/services'],
|
|
95
|
+
adapter: ${JSON.stringify(options.adapter)},
|
|
96
|
+
prefix: '/api',
|
|
97
|
+
lifecycle: 'managed',
|
|
98
|
+
cors: false,
|
|
99
|
+
socketio: ${socket},
|
|
100
|
+
})`
|
|
101
|
+
// 'vite preview' serves static assets: the Feathers Vite plugin is dev-only.
|
|
102
|
+
// Proxy API traffic to the separately started, loopback-bound demo API.
|
|
103
|
+
const previewProxy = `preview: { host: '127.0.0.1', port: 4173, strictPort: true,
|
|
104
|
+
proxy: { '/api': 'http://127.0.0.1:3030',${realtime ? " '/socket.io': { target: 'ws://127.0.0.1:3031', ws: true }" : ''} } },`
|
|
105
|
+
const socketProxy = realtime
|
|
106
|
+
? ",\n proxy: { '/socket.io': { target: 'ws://127.0.0.1:3031', ws: true } }"
|
|
107
|
+
: ''
|
|
108
|
+
files.set('vite.config.ts',
|
|
109
|
+
`import feathers from '@vevedh/vite-plugin-feathersjs'\n` +
|
|
110
|
+
`import { assertCapabilities } from './api/config/capabilities.mjs'\n` +
|
|
111
|
+
source.replace(marker, `${marker} ${runtime},`).replace(oldProxy, socketProxy)
|
|
112
|
+
.replace('export default defineConfig({',
|
|
113
|
+
'// Do not silently start an unprotected or unconfigured identity/database service.\nassertCapabilities()\nexport default defineConfig({\n' +
|
|
114
|
+
` ${previewProxy}\n` +
|
|
115
|
+
` define: { 'import.meta.env.VITE_FEATHERS_REALTIME': '${JSON.stringify(realtime ? '1' : '0')}' },`))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** PWA is opt-in, SPA-only, and must never cache API or realtime responses. */
|
|
119
|
+
function enableSpaPwa(files, options) {
|
|
120
|
+
const config = files.get('vite.config.ts')
|
|
121
|
+
const marker = 'plugins: [vue(),'
|
|
122
|
+
if (typeof config !== 'string' || !config.includes(marker) || config.includes('vite-plugin-pwa')) {
|
|
123
|
+
throw new Error('Unsupported Vite SPA template for PWA; refusing partial generation')
|
|
124
|
+
}
|
|
125
|
+
const pwa = `VitePWA({
|
|
126
|
+
registerType: 'prompt',
|
|
127
|
+
injectRegister: 'auto',
|
|
128
|
+
devOptions: { enabled: false },
|
|
129
|
+
includeAssets: ['pwa-icon.svg'],
|
|
130
|
+
manifest: {
|
|
131
|
+
name: ${JSON.stringify(options.name)},
|
|
132
|
+
short_name: ${JSON.stringify(options.name)},
|
|
133
|
+
description: 'Application Feathers et Vue',
|
|
134
|
+
lang: 'fr',
|
|
135
|
+
start_url: '/',
|
|
136
|
+
scope: '/',
|
|
137
|
+
display: 'standalone',
|
|
138
|
+
background_color: '#ffffff',
|
|
139
|
+
theme_color: '#ffffff',
|
|
140
|
+
icons: [{ src: '/pwa-icon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any' }],
|
|
141
|
+
},
|
|
142
|
+
workbox: {
|
|
143
|
+
globPatterns: ['**/*.{js,css,html,svg}'],
|
|
144
|
+
globIgnores: ['**/api/**', '**/socket.io/**'],
|
|
145
|
+
navigateFallback: '/index.html',
|
|
146
|
+
navigateFallbackDenylist: [/^\\/api(?:\\/|$)/, /^\\/socket\\.io(?:\\/|$)/],
|
|
147
|
+
runtimeCaching: [],
|
|
148
|
+
},
|
|
149
|
+
})`
|
|
150
|
+
files.set('vite.config.ts', `import { VitePWA } from 'vite-plugin-pwa'\n${config.replace(marker, `${marker} ${pwa},`)}`)
|
|
151
|
+
if (files.has('public/pwa-icon.svg')) throw new Error('PWA icon conflict in source template')
|
|
152
|
+
files.set('public/pwa-icon.svg', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192" role="img" aria-label="Feathers app"><rect width="192" height="192" rx="40" fill="#1e293b"/><path d="M44 96l40 40 64-80" fill="none" stroke="#fff" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/></svg>\n')
|
|
153
|
+
}
|
|
154
|
+
|
|
72
155
|
export function createGenerator({outputRoot, templatesRoot, examplesRoot = templatesRoot, reportRoot = outputRoot}) {
|
|
73
156
|
const output = resolve(outputRoot), templates = resolve(templatesRoot), report = resolve(reportRoot)
|
|
74
157
|
function parse(args) {
|
|
75
|
-
const o={ui:'daisyui',render:'spa',adapter:'koa',transport:'rest',dryRun:false,json:false}
|
|
158
|
+
const o={ui:'daisyui',render:'spa',adapter:'koa',transport:'rest',schema:'typebox',auth:'none',db:'main:memory',pwa:'off',devApi:undefined,dryRun:false,json:false}
|
|
76
159
|
const seen=new Set()
|
|
77
160
|
if (args.length && !args[0].startsWith('-')) {o.name=args[0];args=args.slice(1);if(args.includes('--name'))throw new Error('Use either positional name or --name, not both')}
|
|
78
161
|
for(let i=0;i<args.length;i++) {
|
|
@@ -80,20 +163,30 @@ export function createGenerator({outputRoot, templatesRoot, examplesRoot = templ
|
|
|
80
163
|
if(!KEYS.has(flag)||seen.has(flag))throw new Error(`Unknown or duplicate option: ${flag}`)
|
|
81
164
|
seen.add(flag)
|
|
82
165
|
if(flag==='--dry-run'||flag==='--json') o[flag==='--dry-run'?'dryRun':'json']=true
|
|
83
|
-
else { const value=args[++i];if(!value||value.startsWith('--'))throw new Error(`Missing ${flag}`);o[flag.slice(2)]=value }
|
|
166
|
+
else { const value=args[++i];if(!value||value.startsWith('--'))throw new Error(`Missing ${flag}`);o[flag==='--dev-api'?'devApi':flag.slice(2)]=value }
|
|
84
167
|
}
|
|
85
168
|
if(!safeProjectName(o.name))throw new Error('Invalid or missing --name (lowercase kebab-case, max 48 characters)')
|
|
169
|
+
o.devApi ??= o.render === 'spa' ? 'vite' : 'separate'
|
|
170
|
+
if (!['vite','separate'].includes(o.devApi)) throw new Error('Invalid --dev-api: expected vite|separate')
|
|
171
|
+
if (o.devApi === 'vite' && o.render !== 'spa') throw new Error('--dev-api vite is supported for SPA only; SSR/SSG require separate API')
|
|
86
172
|
if(!['koa','express'].includes(o.adapter))throw new Error('Only koa|express adapters are supported')
|
|
87
173
|
if(!['rest','both'].includes(o.transport))throw new Error('Only rest|both are supported')
|
|
174
|
+
if (!['on','off'].includes(o.pwa)) throw new Error('Invalid --pwa: expected on|off')
|
|
175
|
+
if (o.pwa==='on' && o.render!=='spa') throw new Error('PWA is supported for SPA only; SSR/SSG require separate certification')
|
|
176
|
+
if (!['typebox','json','zod'].includes(o.schema)) throw new Error('Invalid --schema: expected typebox|json|zod')
|
|
177
|
+
if (!['none','jwt','oauth2','oidc','keycloak'].includes(o.auth)) throw new Error('Invalid --auth: expected none|jwt|oauth2|oidc|keycloak')
|
|
178
|
+
if (!/^[a-z][a-z0-9-]{0,31}:(?:memory|mongodb|postgresql|mariadb|mysql|sqlite)(?:,[a-z][a-z0-9-]{0,31}:(?:memory|mongodb|postgresql|mariadb|mysql|sqlite))*$/.test(o.db)) throw new Error('Invalid --db: use alias:adapter[,alias:adapter]')
|
|
179
|
+
const dbPairs=o.db.split(',').map(x=>{const [alias,adapter]=x.split(':');return {alias,adapter}})
|
|
180
|
+
if(new Set(dbPairs.map(x=>x.alias)).size!==dbPairs.length)throw new Error('Duplicate database alias')
|
|
88
181
|
// Reuse the actual R15 planner as the capability validator, not a separate feature catalogue.
|
|
89
182
|
const plan=buildProjectPlan({name:o.name,ui:o.ui,render:o.render,transport:o.transport,
|
|
90
|
-
engine:'auto',auth:'
|
|
91
|
-
if(!CASES[`${o.ui}:${o.render}`])throw new Error('Unsupported combination: Quasar2 supports SPA; daisyui supports SPA
|
|
183
|
+
engine:'auto',auth:o.auth==='oauth2'?'jwt,oauth':o.auth==='oidc'||o.auth==='keycloak'?'jwt,oidc':o.auth,db:o.db,primary:dbPairs[0].alias,pwa:o.pwa,schema:o.schema})
|
|
184
|
+
if(!CASES[`${o.ui}:${o.render}`])throw new Error('Unsupported combination: Quasar2 supports SPA/SSR; daisyui supports SPA/SSR/SSG')
|
|
92
185
|
if(o.render!=='spa' && o.transport!=='rest')throw new Error('SSR/SSG supports REST only; Socket.IO proxy not certified')
|
|
93
186
|
if(o.dir!==undefined && !safeProjectName(o.dir))throw new Error('--dir must be a single safe directory name')
|
|
94
187
|
o.destination=resolve(output,o.dir||o.name)
|
|
95
188
|
if(dirname(o.destination)!==output)throw new Error('Destination escapes output root')
|
|
96
|
-
o.case=CASES[`${o.ui}:${o.render}`];o.plan=plan
|
|
189
|
+
o.case=CASES[`${o.ui}:${o.render}`];o.plan=plan;o.databases=dbPairs
|
|
97
190
|
return o
|
|
98
191
|
}
|
|
99
192
|
async function generate(o) {
|
|
@@ -103,22 +196,83 @@ export function createGenerator({outputRoot, templatesRoot, examplesRoot = templ
|
|
|
103
196
|
const files=new Map(o.render==='spa' ? [
|
|
104
197
|
...await walk(join(templates,'spa','common')),
|
|
105
198
|
...await walk(join(templates,'spa',o.ui)),
|
|
106
|
-
] : await walk(join(resolve(examplesRoot)
|
|
199
|
+
] : await walk(join(resolve(examplesRoot),`${o.ui==='quasar2'?'quasar':'daisyui'}-${o.render}`)))
|
|
107
200
|
// npm pack excludes files literally named .gitignore, even in subdirectories.
|
|
108
201
|
// The published templates use gitignore.template, rewritten here to real .gitignore.
|
|
109
202
|
if(files.has('gitignore.template')){files.set('.gitignore',files.get('gitignore.template'));files.delete('gitignore.template')}
|
|
203
|
+
if(files.has('.gitignore')) {
|
|
204
|
+
const ignores=files.get('.gitignore')
|
|
205
|
+
const additions=['.agents/','AGENTS.md','SKILLS.md','*.prompt.md','.ai/','.codex/','.cursor/','.claude/','.gemini/','.windsurf/','.aider*','.mcp.json','.vscode/mcp.json','.github/copilot-instructions.md','docs-private/','.npmrc','.env*','!.env.example','api/.create-feathers-service.lock']
|
|
206
|
+
files.set('.gitignore',ignores.trimEnd()+'\n\n# Local editor tooling and private maintenance files\n'+additions.filter(x=>!ignores.split(/\r?\n/).includes(x)).join('\n')+'\n')
|
|
207
|
+
}
|
|
110
208
|
if(!files.has('api/server.mjs')||!files.has('.env.example')||!files.has('index.html'))throw new Error('Incomplete source template')
|
|
111
209
|
files.set('.env.example',envChange(files.get('.env.example'),o))
|
|
210
|
+
if (o.devApi === 'vite') {
|
|
211
|
+
enableSpaViteRuntime(files, o)
|
|
212
|
+
files.set('api/config/capabilities.d.mts', 'export declare function assertCapabilities(): void\n')
|
|
213
|
+
}
|
|
214
|
+
if (o.pwa === 'on') enableSpaPwa(files,o)
|
|
215
|
+
if(o.ui==='quasar2' && o.render==='ssr') {
|
|
216
|
+
// R16 HTML has no Vite SPA identity marker; add one without touching frozen R16 sources.
|
|
217
|
+
const html=files.get('index.html')
|
|
218
|
+
files.set('index.html',html.replace('<meta charset="utf-8">','<meta charset="utf-8">\n <meta name="r17-example" content="quasar-ssr">\n <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\'/%3E">'))
|
|
219
|
+
}
|
|
112
220
|
files.set('index.html',files.get('index.html').replace('__R17_CASE__',o.case).replace(/(<meta name="r17-example" content=")[^"]+("\s*\/?>)/,`$1${o.case}$2`))
|
|
113
|
-
const manifest=o.render==='spa'?spaManifest(o):JSON.parse(files.get('package.json'))
|
|
221
|
+
const manifest=o.render==='spa'?spaManifest(o):o.ui==='quasar2'?quasarSSRManifest(o):JSON.parse(files.get('package.json'))
|
|
114
222
|
manifest.name=o.name;manifest.private=true
|
|
115
|
-
manifest.scripts={...manifest.scripts,'dev:all':'node scripts/dev-all.mjs'}
|
|
223
|
+
manifest.scripts={...manifest.scripts,'dev:all':o.devApi==='vite'?'vite --host 127.0.0.1':'node scripts/dev-all.mjs'}
|
|
116
224
|
files.set('package.json',JSON.stringify(manifest,null,2)+'\n')
|
|
117
225
|
files.set('README.md',generatedReadme(o))
|
|
118
|
-
|
|
226
|
+
// Keep the visible application description consistent with the actual dev API mode.
|
|
227
|
+
// The standalone production API is intentionally unaffected.
|
|
228
|
+
if (o.render === 'spa' && o.devApi === 'vite') {
|
|
229
|
+
const app = files.get('src/App.vue')
|
|
230
|
+
const separateDescription = 'Le service Feathers est exécuté dans un processus séparé.'
|
|
231
|
+
if (typeof app !== 'string' || !app.includes(separateDescription)) {
|
|
232
|
+
throw new Error('SPA description contract changed: refusing inconsistent Vite-native scaffold')
|
|
233
|
+
}
|
|
234
|
+
files.set('src/App.vue', app.replace(separateDescription,
|
|
235
|
+
'En développement, les services Feathers sont intégrés directement à Vite.'))
|
|
236
|
+
}
|
|
237
|
+
if (o.devApi !== 'vite') files.set('scripts/dev-all.mjs',await readFile(join(templates,'common',o.ui==='quasar2'&&o.render==='ssr'?'dev-all-quasar-ssr.mjs':'dev-all.mjs'),'utf8'))
|
|
238
|
+
// A configuration contract, not a simulated authentication or persistence layer.
|
|
239
|
+
// Non-memory databases and external identity providers fail closed at startup
|
|
240
|
+
// until explicitly integrated and tested with a real provider.
|
|
241
|
+
files.set('api/config/capabilities.mjs',capabilityGuard(o))
|
|
242
|
+
const server=files.get('api/server.mjs').replace("from 'vite-plugin-feathersjs'","from '@vevedh/vite-plugin-feathersjs'")
|
|
243
|
+
files.set('api/server.mjs',"import { assertCapabilities } from './config/capabilities.mjs'\nassertCapabilities()\n" + server)
|
|
244
|
+
files.set('api/config/README.md',`# Configuration des capacités\n\nProfil sélectionné : schéma ${o.schema}, identité ${o.auth}, connexions ${o.db}.\n\nLes services en mémoire et sans authentification sont destinés au laboratoire local. Les bases externes nécessitent une instance réelle, un client Feathers/Knex ou MongoDB, ses migrations et la vérification de son cycle de vie.\n\nL'authentification JWT exige un service utilisateurs, un secret hors Git, une stratégie JWT active, des contrôles d'accès et des tests. OAuth2, OIDC et Keycloak exigent en plus un fournisseur HTTPS, la vérification de l'issuer, audience, JWKS, PKCE et le mapping des identités. Ne pas exposer l'API avant validation.\n\nLa commande \`service add tickets --schema zod --db main\` crée un service et l'enregistre dans l'API.\n`)
|
|
245
|
+
files.set('api/config/profile.json',JSON.stringify({schema:o.schema,auth:o.auth,databases:o.databases,primary:o.databases[0].alias,operational:o.auth==='none' && o.databases.every(db=>db.adapter==='memory')},null,2)+'\n')
|
|
246
|
+
const depMap={typebox:['@feathersjs/typebox','ajv'],json:['ajv'],zod:['zod']}
|
|
247
|
+
// Dependencies for the service subcommand; project options alone create no arbitrary services.
|
|
248
|
+
for(const dep of depMap[o.schema])manifest.dependencies[dep] ||= dep==='ajv'?'^8.17.1':dep==='zod'?'^4.1.0':'^5.0.50'
|
|
249
|
+
if(o.auth==='jwt')manifest.dependencies['@feathersjs/authentication']='^5.0.50'
|
|
250
|
+
if(o.auth==='oauth2')manifest.dependencies['@feathersjs/authentication-oauth']='^5.0.50'
|
|
251
|
+
for(const db of o.databases) {
|
|
252
|
+
const drivers={mongodb:['@feathersjs/mongodb','mongodb'],postgresql:['@feathersjs/knex','knex','pg'],mariadb:['@feathersjs/knex','knex','mysql2'],mysql:['@feathersjs/knex','knex','mysql2'],sqlite:['@feathersjs/knex','knex','better-sqlite3']}
|
|
253
|
+
for(const dep of drivers[db.adapter]||[])manifest.dependencies[dep] ||= dep === 'mongodb' ? '6.19.0' : dep === '@feathersjs/mongodb' ? '5.0.51' : '*'
|
|
254
|
+
}
|
|
255
|
+
files.set('package.json',JSON.stringify(manifest,null,2)+'\n')
|
|
256
|
+
if (o.auth === 'jwt' && o.databases.some(db => ['postgresql','mariadb','mongodb'].includes(db.adapter))) {
|
|
257
|
+
for (const name of ['register.mjs','policy.mjs']) files.set('api/security/' + name, await readFile(new URL('./secure-runtime/' + name, import.meta.url), 'utf8'))
|
|
258
|
+
Object.assign(manifest.dependencies, { '@feathersjs/authentication-local': '5.0.50', '@casl/ability': '6.8.1', 'feathers-casl': '3.1.0' })
|
|
259
|
+
manifest.dependencies.zod ||= '^4.1.0'
|
|
260
|
+
files.set('package.json',JSON.stringify(manifest,null,2)+'\n')
|
|
261
|
+
}
|
|
262
|
+
const persistentDatabases = o.databases.filter(db => ['postgresql','mariadb','mongodb'].includes(db.adapter))
|
|
263
|
+
if (o.auth === 'jwt' && persistentDatabases.length) {
|
|
264
|
+
const declarations = persistentDatabases.map(db => ({ alias: db.alias, adapter: db.adapter, envPrefix: 'P5_DB_' + db.alias.replaceAll('-', '_').toUpperCase() }))
|
|
265
|
+
if (new Set(declarations.map(db => db.envPrefix)).size !== declarations.length) throw new Error('Database environment prefix collision')
|
|
266
|
+
files.set('api/config/database-connections.generated.mjs', 'export const databaseDeclarations = Object.freeze(' + JSON.stringify(declarations, null, 2) + ')\n')
|
|
267
|
+
for (const [source, target] of [['providers.mjs','api/runtime/database-providers.generated.mjs'], ['storage.mjs','api/runtime/storage.mjs'], ['prepare-storage.mjs','scripts/prepare-storage.mjs'], ['connect.mjs','api/security/connect.mjs']]) {
|
|
268
|
+
files.set(target, await readFile(new URL('./secure-runtime/' + source, import.meta.url), 'utf8'))
|
|
269
|
+
}
|
|
270
|
+
const env = declarations.flatMap(db => [db.envPrefix + '_HOST=127.0.0.1', db.envPrefix + '_PORT=' + (db.adapter === 'postgresql' ? '5432' : db.adapter === 'mariadb' ? '3306' : '27017'), db.envPrefix + '_USER=', db.envPrefix + '_PASSWORD=', db.envPrefix + '_NAME='])
|
|
271
|
+
files.set('database.env.example', env.join('\n') + '\n')
|
|
272
|
+
}
|
|
119
273
|
const list=[...files.keys()].sort()
|
|
120
274
|
const result={name:o.name,ui:o.ui,render:o.render,adapter:o.adapter,transport:o.transport,
|
|
121
|
-
destination:relative(report,o.destination).split(sep).join('/'),files:list}
|
|
275
|
+
destination:relative(report,o.destination).split(sep).join('/'),schema:o.schema,auth:o.auth,pwa:o.pwa,devApi:o.devApi,databases:o.databases,files:list}
|
|
122
276
|
if(o.dryRun)return result
|
|
123
277
|
// Do not follow symlinks and refuse any existing destination, including empty ones.
|
|
124
278
|
await mkdir(output,{recursive:true})
|
|
@@ -134,3 +288,31 @@ export function createGenerator({outputRoot, templatesRoot, examplesRoot = templ
|
|
|
134
288
|
}
|
|
135
289
|
return {parse,generate}
|
|
136
290
|
}
|
|
291
|
+
|
|
292
|
+
function quasarSSRManifest(o){
|
|
293
|
+
return {name:o.name,version:'0.1.0',private:true,type:'module',packageManager:'pnpm@10.30.2',engines:{node:'>=22.12'},
|
|
294
|
+
scripts:{'dev:ssr':'quasar dev -m ssr','build:ssr':'quasar build -m ssr',build:'quasar build -m ssr',
|
|
295
|
+
typecheck:'vue-tsc --noEmit', 'dev:api':'node --env-file=.env.example api/server.mjs',
|
|
296
|
+
'serve:production':'node dist/ssr/index.js',postinstall:'quasar prepare', 'dev:edge':'node scripts/edge-proxy.mjs'},
|
|
297
|
+
dependencies:{'@vevedh/vite-plugin-feathersjs':'0.2.6', '@feathersjs/feathers':'^5.0.50', '@feathersjs/koa':'^5.0.50',
|
|
298
|
+
'@feathersjs/express':'^5.0.50', '@feathersjs/errors':'^5.0.50', '@feathersjs/socketio':'^5.0.50',
|
|
299
|
+
'@feathersjs/rest-client':'^5.0.50', '@feathersjs/socketio-client':'^5.0.50',
|
|
300
|
+
'@quasar/extras':'^1.18.0',pinia:'^3.0.4',quasar:'^2.35.2','socket.io-client':'^4.8.4',
|
|
301
|
+
vue:'^3.5.44','vue-router':'^4.6.4',express:'^4.22.3','serialize-javascript':'^7.1.4'},
|
|
302
|
+
devDependencies:{'@quasar/app-vite':'2.6.2','@types/node':'^22.0.0',typescript:'^5.9.3','vue-tsc':'^3.3.14',
|
|
303
|
+
// Quasar app-vite 2.6.2 runs Express v4. Keep its ambient types in
|
|
304
|
+
// the same major as Express runtime, including nested pnpm dependencies.
|
|
305
|
+
'@types/express':'4.17.25','@types/express-serve-static-core':'4.19.9',vite:'^7.3.7'},
|
|
306
|
+
pnpm:{overrides:{'@types/express':'4.17.25','@types/express-serve-static-core':'4.19.9'}}}
|
|
307
|
+
}
|
|
308
|
+
function capabilityGuard(o) {
|
|
309
|
+
const external=o.databases.filter(x=>x.adapter!=='memory')
|
|
310
|
+
const desc=JSON.stringify({auth:o.auth,databases:o.databases})
|
|
311
|
+
return `// Generated capability contract: never silently downgrade identity or persistence.\n`+
|
|
312
|
+
`export const profile=Object.freeze(${desc})\n`+
|
|
313
|
+
`export function assertCapabilities(){\n`+
|
|
314
|
+
` if (profile.auth !== 'none') throw new Error('Authentication '+profile.auth+' is a configuration blueprint; identity provider and JWT validation must be integrated and certified before serving requests')\n`+
|
|
315
|
+
` const missing=profile.databases.filter(item=>item.adapter!=='memory')\n`+
|
|
316
|
+
` if(missing.length)throw new Error('Database adapters not provisioned: '+missing.map(x=>x.alias+':'+x.adapter).join(', ')+'. Configure named adapters before serving requests')\n`+
|
|
317
|
+
`}\n`
|
|
318
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { openNamedDatabaseProviders } from '../runtime/database-providers.generated.mjs'
|
|
2
|
+
import { storageNames } from '../runtime/storage.mjs'
|
|
3
|
+
import { registerSecuredPersistence } from './register.mjs'
|
|
4
|
+
|
|
5
|
+
/** Attach real persistence/auth to an unstarted Feathers app; caller owns listener and shutdown.
|
|
6
|
+
* No schema creation, account seeding or socket broadcast occurs during startup.
|
|
7
|
+
*/
|
|
8
|
+
export async function connectSecuredPersistence(app, config) {
|
|
9
|
+
if (!config || Object.keys(config).sort().join(',') !== 'alias,audience,issuer,secret') throw Error('INVALID_SECURITY_CONFIG')
|
|
10
|
+
let providers
|
|
11
|
+
try {
|
|
12
|
+
providers = await openNamedDatabaseProviders()
|
|
13
|
+
const provider = providers.get(config.alias)
|
|
14
|
+
// Readiness checks the actual schema/collections before registration.
|
|
15
|
+
if (provider.adapter === 'mongodb') {
|
|
16
|
+
for (const name of Object.values(storageNames)) {
|
|
17
|
+
if (!await provider.db.listCollections({ name }).hasNext()) throw Error('STORAGE_NOT_PREPARED')
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
for (const name of Object.values(storageNames)) if (!await provider.db.schema.hasTable(name)) throw Error('STORAGE_NOT_PREPARED')
|
|
21
|
+
}
|
|
22
|
+
registerSecuredPersistence(app, providers, { ...config, ...storageNames })
|
|
23
|
+
return Object.freeze({ close: () => providers.close() })
|
|
24
|
+
} catch (error) {
|
|
25
|
+
try { await providers?.close() } catch {}
|
|
26
|
+
throw error
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** P5-R5-R4-R2: only server-validated identities determine Feathers/CASL permissions. */
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { AbilityBuilder, createMongoAbility } from '@casl/ability'
|
|
4
|
+
import { BadRequest, NotAuthenticated } from '@feathersjs/errors'
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
const id = z.string().uuid()
|
|
9
|
+
const title = z.string().trim().min(1).max(80).regex(/^[A-Za-z0-9 _.-]+$/)
|
|
10
|
+
const create = z.object({ title }).strict()
|
|
11
|
+
const patch = z.object({ title }).strict()
|
|
12
|
+
const list = z.object({
|
|
13
|
+
$limit: z.coerce.number().int().min(0).max(50).optional(),
|
|
14
|
+
$skip: z.coerce.number().int().min(0).max(100).optional(),
|
|
15
|
+
}).strict()
|
|
16
|
+
const empty = z.object({}).strict()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
export function validateIdentity(input ) {
|
|
27
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
28
|
+
throw new NotAuthenticated('Identity unavailable')
|
|
29
|
+
}
|
|
30
|
+
const value = input
|
|
31
|
+
const parsed = z.object({
|
|
32
|
+
id,
|
|
33
|
+
role: z.enum(['viewer','editor','admin']),
|
|
34
|
+
status: z.literal('active'),
|
|
35
|
+
sessionVersion: z.number().int().positive().safe(),
|
|
36
|
+
}).passthrough().safeParse(value)
|
|
37
|
+
if (!parsed.success) throw new NotAuthenticated('Identity unavailable')
|
|
38
|
+
return parsed.data
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function serverAbility(identity ) {
|
|
42
|
+
const { can, rules } = new AbilityBuilder(createMongoAbility)
|
|
43
|
+
if (identity.role === 'admin') {
|
|
44
|
+
can('manage', 'all')
|
|
45
|
+
} else {
|
|
46
|
+
can('find', 'documents', ['id','title','ownerId'], { ownerId: identity.id })
|
|
47
|
+
can('get', 'documents', ['id','title','ownerId'], { ownerId: identity.id })
|
|
48
|
+
if (identity.role === 'editor') {
|
|
49
|
+
can('create', 'documents')
|
|
50
|
+
can('patch', 'documents', ['title'], { ownerId: identity.id })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return createMongoAbility(rules)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
/** Must run after real Feathers authenticate('jwt') and before CASL authorize(). */
|
|
62
|
+
export function documentPolicy(revalidate ) {
|
|
63
|
+
return async (context ) => {
|
|
64
|
+
if (['get', 'patch', 'remove'].includes(context.method) && !id.safeParse(context.id).success) throw new BadRequest('Invalid id')
|
|
65
|
+
const params = context.params
|
|
66
|
+
if (!params.provider) throw new NotAuthenticated('Authenticated transport required')
|
|
67
|
+
const token = params.authentication?.accessToken
|
|
68
|
+
if (typeof token !== 'string' || token.length < 24 || token.length > 4096) {
|
|
69
|
+
throw new NotAuthenticated('Authentication required')
|
|
70
|
+
}
|
|
71
|
+
// Never use params.user, client JWT roles, client-supplied ability, or stale socket permissions.
|
|
72
|
+
const identity = validateIdentity(await revalidate(token))
|
|
73
|
+
params.ability = serverAbility(identity)
|
|
74
|
+
const query = (context.method === 'find' ? list : empty).safeParse(params.query ?? {})
|
|
75
|
+
if (!query.success) throw new BadRequest('Invalid query')
|
|
76
|
+
context.params.query = query.data
|
|
77
|
+
if (context.method === 'create' || context.method === 'patch') {
|
|
78
|
+
const parsed = (context.method === 'create' ? create : patch).safeParse(context.data )
|
|
79
|
+
if (!parsed.success) throw new BadRequest('Invalid data')
|
|
80
|
+
context.data = context.method === 'create'
|
|
81
|
+
? { ...parsed.data, id: randomUUID(), ownerId: identity.id, internal: null }
|
|
82
|
+
: parsed.data
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { openNamedDatabaseProviders } from '../api/runtime/database-providers.generated.mjs'
|
|
2
|
+
import { prepareIdentityStorage } from '../api/runtime/storage.mjs'
|
|
3
|
+
const [mode, flag, alias, ...extra] = process.argv.slice(2)
|
|
4
|
+
if (mode !== '--apply' || flag !== '--alias' || !/^[a-z][a-z0-9-]{0,31}$/.test(alias ?? '') || extra.length) {
|
|
5
|
+
console.error('STORAGE=REFUSED | EXPECTED=--apply --alias NAME'); process.exitCode = 2
|
|
6
|
+
} else {
|
|
7
|
+
let providers
|
|
8
|
+
try {
|
|
9
|
+
providers = await openNamedDatabaseProviders()
|
|
10
|
+
await prepareIdentityStorage(providers.get(alias))
|
|
11
|
+
console.log('STORAGE=PREPARED | ACCOUNTS_CREATED=0 | PUBLIC_RUNTIME=BLOCKED')
|
|
12
|
+
} catch { console.error('STORAGE=FAILED | PUBLIC_RUNTIME=BLOCKED'); process.exitCode = 2 }
|
|
13
|
+
finally { try { await providers?.close() } catch { console.error('STORAGE=CLEANUP_FAILED'); process.exitCode = 2 } }
|
|
14
|
+
}
|