@lakutata/template-web-vue 3.0.0-beta.0
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/LICENSE +23 -0
- package/README.md +9 -0
- package/features/i18n/packages/app/package.json +6 -0
- package/features/i18n/packages/app/src/locales/en.json +5 -0
- package/features/i18n/packages/app/src/locales/zh-CN.json +5 -0
- package/features/installer/Dockerfile.installer +10 -0
- package/features/installer/package.json +9 -0
- package/features/installer/packages/app/package.json +26 -0
- package/features/installer/packages/resources/logo.png +0 -0
- package/features/installer/packages/resources/package.json +6 -0
- package/package.json +23 -0
- package/template/Dockerfile +18 -0
- package/template/README.md +73 -0
- package/template/_dockerignore +10 -0
- package/template/_gitignore +13 -0
- package/template/package.json +30 -0
- package/template/packages/app/package.json +36 -0
- package/template/packages/app/src/App.ts +7 -0
- package/template/packages/app/src/config/Config.ts +67 -0
- package/template/packages/app/src/controllers/ExampleController.ts +32 -0
- package/template/packages/app/src/controllers/HealthController.ts +13 -0
- package/template/packages/app/src/dtos/GreetDTO.ts +7 -0
- package/template/packages/app/src/middlewares/Authenticate.ts +22 -0
- package/template/packages/app/src/tests/App.spec.ts +54 -0
- package/template/packages/app/tsconfig.json +9 -0
- package/template/packages/ui/index.html +12 -0
- package/template/packages/ui/package.json +21 -0
- package/template/packages/ui/src/App.vue +30 -0
- package/template/packages/ui/src/main.ts +4 -0
- package/template/packages/ui/tsconfig.json +15 -0
- package/template/packages/ui/vite.config.ts +12 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-present Lakutata
|
|
4
|
+
|
|
5
|
+
All rights reserved
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @lakutata/template-web-vue
|
|
2
|
+
|
|
3
|
+
The Vue web template of the [lakutata](https://github.com/lakutata/lakutata) projects: the service serves its HTTP API
|
|
4
|
+
under `/api` and a Vue single-page application built with Vite (`staticUI`), from one process, one port and one Docker
|
|
5
|
+
image. The project is a workspace: `packages/app` is the service, `packages/ui` the application.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @lakutata/cli create --name my-app --template web-vue
|
|
9
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# The installers of Linux (npm run package:linux), made on Linux: the packages of the configuration (lakutata.installer
|
|
2
|
+
# of the package.json) in dist/installer. The ones of another architecture: docker buildx build --platform linux/amd64 ...
|
|
3
|
+
# The apk packages are made on Alpine (node:22-alpine), with Bun when the application has native addons.
|
|
4
|
+
FROM node:22-bookworm AS build
|
|
5
|
+
WORKDIR /project
|
|
6
|
+
COPY . .
|
|
7
|
+
RUN (npm ci --no-audit --no-fund || npm install --no-audit --no-fund) && npm run build && npm run package -w app -- --outdir /installer
|
|
8
|
+
|
|
9
|
+
FROM scratch
|
|
10
|
+
COPY --from=build /installer /
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scripts": {
|
|
3
|
+
"package:host": "npm run build && npm run package -w app -- --outdir ../../dist/installer",
|
|
4
|
+
"package:linux": "docker buildx build --file Dockerfile.installer --output type=local,dest=dist/installer ."
|
|
5
|
+
},
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@lakutata/builder": "^{$LAKUTATA_VERSION}"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bin": {
|
|
3
|
+
"{$APP_ID}": "build/App.js"
|
|
4
|
+
},
|
|
5
|
+
"scripts": {
|
|
6
|
+
"package": "lakutata package installer"
|
|
7
|
+
},
|
|
8
|
+
"lakutata": {
|
|
9
|
+
"icon": "../resources/logo.png",
|
|
10
|
+
"installer": {
|
|
11
|
+
"name": "{$APP_ID}",
|
|
12
|
+
"service": {
|
|
13
|
+
"env": {
|
|
14
|
+
"PORT": "8080"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"linux": {
|
|
18
|
+
"targets": [
|
|
19
|
+
"deb",
|
|
20
|
+
"rpm",
|
|
21
|
+
"tar.gz"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lakutata/template-web-vue",
|
|
3
|
+
"version": "3.0.0-beta.0",
|
|
4
|
+
"description": "The Vue web template of the lakutata projects: an HTTP API serving a Vue single-page application built with Vite, deployed as a Docker image.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"lakutata",
|
|
7
|
+
"lakutata-template"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"files": [
|
|
11
|
+
"template",
|
|
12
|
+
"features",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"lakutata": {
|
|
17
|
+
"template": "web-vue",
|
|
18
|
+
"features": {
|
|
19
|
+
"i18n": "The messages in several languages (lakutata/com/i18n): src/locales, the language of each request",
|
|
20
|
+
"installer": "The installers of the application (lakutata package installer): deb, rpm and tar.gz on Linux (npm run package:linux in Docker), pkg and tar.gz on macOS, nsis, msi and zip on Windows, its service registered (systemd, launchd, WinSW), its program in the PATH"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# The build: the dependencies and the sources, compiled
|
|
2
|
+
FROM node:22-alpine AS build
|
|
3
|
+
WORKDIR /project
|
|
4
|
+
COPY . .
|
|
5
|
+
RUN npm ci --no-audit --no-fund && npm run build && npm prune --omit=dev
|
|
6
|
+
|
|
7
|
+
# The image: the compiled service and its production dependencies, run by an unprivileged user
|
|
8
|
+
FROM node:22-alpine
|
|
9
|
+
ENV NODE_ENV=production PORT=8080
|
|
10
|
+
WORKDIR /project
|
|
11
|
+
COPY --from=build --chown=node:node /project /project
|
|
12
|
+
USER node
|
|
13
|
+
EXPOSE 8080
|
|
14
|
+
# Whether the process answers: the dependencies (database, message broker) are not part of it, the service stays up
|
|
15
|
+
# while they recover
|
|
16
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
|
17
|
+
CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/health').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"
|
|
18
|
+
CMD ["node", "packages/app/build/App.js"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# {$APP_NAME}
|
|
2
|
+
|
|
3
|
+
{$APP_DESC}
|
|
4
|
+
|
|
5
|
+
A [lakutata](https://github.com/lakutata/lakutata) web application. The project is a workspace: `packages/app` is the
|
|
6
|
+
service, serving its API under `/api` and the Vue application of `packages/ui` (built with Vite); the other packages
|
|
7
|
+
are added in `packages/`.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm run dev # start the service and the Vite dev server, http://localhost:5173
|
|
11
|
+
npm test # build the packages and run their tests
|
|
12
|
+
npm run build # build the packages, in the order of their dependencies (ui, then app)
|
|
13
|
+
npm start # start the built service, http://localhost:8080
|
|
14
|
+
npm run version:patch # give the packages their next version
|
|
15
|
+
npm run docker:build # build the image {$APP_ID}:<version>
|
|
16
|
+
DOCKER_REPOSITORY=registry.example.com/team/{$APP_ID} npm run docker:push # build it for linux/amd64 and push it
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The results are `{errno, errMsg, data}`, the errors too (with the status 200). The requests carry
|
|
20
|
+
`Authorization: Bearer <API_TOKEN>` when the environment has an `API_TOKEN`
|
|
21
|
+
(`packages/app/src/middlewares/Authenticate.ts`). `/health` tells whether the process answers.
|
|
22
|
+
|
|
23
|
+
`docker login <registry>` once before pushing: the credentials are kept by Docker.
|
|
24
|
+
|
|
25
|
+
<!-- @if installer -->
|
|
26
|
+
## The installers
|
|
27
|
+
|
|
28
|
+
`npm run package:host` makes the installers of the platform it runs on (`lakutata package installer`, configured by the
|
|
29
|
+
`lakutata.installer` of `packages/app/package.json`) in `dist/installer`: the application, its command
|
|
30
|
+
`{$APP_ID}` in the PATH, and its service started at the start of the system, with its environment (`PORT`, `MODE`…, kept by
|
|
31
|
+
the upgrades) and its data:
|
|
32
|
+
|
|
33
|
+
- Linux: the deb, rpm and tar.gz packages; the application in `/opt/{$APP_ID}`, its service run by systemd, its environment
|
|
34
|
+
`/etc/{$APP_ID}/{$APP_ID}.env` (`systemctl restart {$APP_ID}` after changing it), its data `/var/lib/{$APP_ID}`.
|
|
35
|
+
`npm run package:linux` makes them in Docker (`Dockerfile.installer`, in `dist/installer`) on any platform.
|
|
36
|
+
- macOS: the pkg package; the application in `/usr/local/lib/{$APP_ID}`, its service run by launchd, its environment
|
|
37
|
+
`/usr/local/etc/{$APP_ID}/{$APP_ID}.env` (`sudo /usr/local/lib/{$APP_ID}/uninstall` removes it).
|
|
38
|
+
- Windows: the installer of NSIS and the package of Windows Installer (msi); the application in
|
|
39
|
+
`C:\Program Files\{$APP_ID}`, its service run by WinSW, its environment `C:\ProgramData\{$APP_ID}\{$APP_ID}.env`
|
|
40
|
+
(`Restart-Service {$APP_ID}` after changing it).
|
|
41
|
+
|
|
42
|
+
<!-- @end -->
|
|
43
|
+
## Database migrations
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm run migration:generate # the migrations of the changes of the entities of the databases
|
|
47
|
+
npm run migration:create -- seed-users # an empty migration, written by hand
|
|
48
|
+
npm run migration:show # the migrations run [X] and pending [ ]
|
|
49
|
+
npm run migration:run # run the pending migrations
|
|
50
|
+
npm run migration:revert # revert the last migration run
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The commands (`lakutata migration <action>`) build and start the app, which connects its databases with their
|
|
54
|
+
options (without synchronizing their schemas), does the command and exits; `-- --database <name>` selects a database
|
|
55
|
+
(required to revert when there are several), `-- --fake` marks the migrations as run or reverted without executing
|
|
56
|
+
them. The migrations are written into `packages/app/src/migrations`, compiled with the app and given to the options of the
|
|
57
|
+
databases, run when they connect:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import {buildDatabaseOptions} from 'lakutata/com/database'
|
|
61
|
+
import {Migrations1788773616343} from '../migrations/1788773616343-db.js'
|
|
62
|
+
|
|
63
|
+
components: {
|
|
64
|
+
db: buildDatabaseOptions({
|
|
65
|
+
type: 'postgres',
|
|
66
|
+
url: process.env.DATABASE_URL,
|
|
67
|
+
entities: [/* the entities */],
|
|
68
|
+
synchronize: false,
|
|
69
|
+
migrationsRun: true,
|
|
70
|
+
migrations: [Migrations1788773616343]
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{$APP_ID}-workspace",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"workspaces": [
|
|
6
|
+
"packages/*"
|
|
7
|
+
],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "lakutata run rebuild",
|
|
10
|
+
"test": "lakutata run test",
|
|
11
|
+
"dev": "lakutata run dev --parallel",
|
|
12
|
+
"migration:generate": "lakutata migration generate --package app",
|
|
13
|
+
"migration:create": "lakutata migration create --package app",
|
|
14
|
+
"migration:run": "lakutata migration run --package app",
|
|
15
|
+
"migration:revert": "lakutata migration revert --package app",
|
|
16
|
+
"migration:show": "lakutata migration show --package app",
|
|
17
|
+
"start": "npm start -w app",
|
|
18
|
+
"version:patch": "lakutata version patch --yes",
|
|
19
|
+
"docker:build": "docker buildx build --tag {$APP_ID}:$npm_package_version --load .",
|
|
20
|
+
"docker:push": "docker buildx build --platform linux/amd64 --tag $DOCKER_REPOSITORY:$npm_package_version --tag $DOCKER_REPOSITORY:latest --push ."
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@lakutata/cli": "^{$LAKUTATA_VERSION}",
|
|
24
|
+
"@types/node": "^22.0.0",
|
|
25
|
+
"typescript": "^6.0.3"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "app",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "{$APP_DESC}",
|
|
5
|
+
"author": "{$APP_AUTHOR}",
|
|
6
|
+
"license": "{$APP_LICENSE}",
|
|
7
|
+
"private": true,
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "build/App.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"clean": "lakutata clean",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"rebuild": "npm run clean && npm run build",
|
|
14
|
+
"dev": "npm run rebuild && node build/App.js",
|
|
15
|
+
"start": "node build/App.js",
|
|
16
|
+
"test": "node --test \"build/tests/**/*.spec.js\""
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"compression": "^1.8.1",
|
|
20
|
+
"express": "^5.2.1",
|
|
21
|
+
"lakutata": "^{$LAKUTATA_VERSION}",
|
|
22
|
+
"ui": "^0.0.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/compression": "^1.8.1",
|
|
26
|
+
"@types/express": "^5.0.6"
|
|
27
|
+
},
|
|
28
|
+
"lakutata": {
|
|
29
|
+
"resources": [
|
|
30
|
+
{
|
|
31
|
+
"from": "../ui/dist",
|
|
32
|
+
"to": "ui"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import {Application, type ILogger} from 'lakutata'
|
|
2
|
+
import {installerConfig} from './config/Config.js'
|
|
3
|
+
|
|
4
|
+
Application
|
|
5
|
+
.run(installerConfig)
|
|
6
|
+
.onLaunched((app: Application, log: ILogger): void => log.info('%s started in %s mode', app.appName, app.mode()))
|
|
7
|
+
.onFatalException((error: Error, log: ILogger): void => log.error('The service cannot start: %s', error.message))
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {resourcesPath, type ApplicationOptions} from 'lakutata'
|
|
2
|
+
import {buildEntrypoints} from 'lakutata/com/entrypoint'
|
|
3
|
+
import {expressHTTPEntrypoint, jsonEnvelope, staticUI} from 'lakutata/adapter/express'
|
|
4
|
+
import compression from 'compression'
|
|
5
|
+
import {existsSync} from 'node:fs'
|
|
6
|
+
import {createRequire} from 'node:module'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import {authenticate} from '../middlewares/Authenticate.js'
|
|
9
|
+
import {ExampleController} from '../controllers/ExampleController.js'
|
|
10
|
+
import {HealthController} from '../controllers/HealthController.js'
|
|
11
|
+
//@if i18n
|
|
12
|
+
import {buildI18nOptions, I18nMiddleware} from 'lakutata/com/i18n'
|
|
13
|
+
import en from '../locales/en.json' with {type: 'json'}
|
|
14
|
+
import zhCN from '../locales/zh-CN.json' with {type: 'json'}
|
|
15
|
+
//@end
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The directory of the built application of the ui package, undefined before it is built (in development, Vite serves it):
|
|
19
|
+
* in the resources of the packed application (lakutata package bundle, lakutata package exe: the "lakutata.resources" of its
|
|
20
|
+
* package.json), else in the ui package
|
|
21
|
+
*/
|
|
22
|
+
function uiDirectory(): string | undefined {
|
|
23
|
+
const resources: string | undefined = resourcesPath()
|
|
24
|
+
try {
|
|
25
|
+
const directory: string = resources ? path.join(resources, 'ui') : path.join(path.dirname(createRequire(import.meta.url).resolve('ui/package.json')), 'dist')
|
|
26
|
+
return existsSync(path.join(directory, 'index.html')) ? directory : undefined
|
|
27
|
+
} catch {
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The options of the service
|
|
34
|
+
* @param port the port of the HTTP server, the PORT environment variable or 8080 by default
|
|
35
|
+
*/
|
|
36
|
+
export function installerConfig(port: number = Number(process.env.PORT ?? 8080)): ApplicationOptions {
|
|
37
|
+
return {
|
|
38
|
+
id: '{$APP_ID}',
|
|
39
|
+
name: '{$APP_NAME}',
|
|
40
|
+
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
|
41
|
+
components: {
|
|
42
|
+
//@if i18n
|
|
43
|
+
//The messages of the languages (src/locales), bundled with the code; created by the first request
|
|
44
|
+
i18n: buildI18nOptions({messages: {'en': en, 'zh-CN': zhCN}}),
|
|
45
|
+
//@end
|
|
46
|
+
entrypoint: buildEntrypoints({
|
|
47
|
+
controllers: [HealthController, ExampleController],
|
|
48
|
+
//@if i18n
|
|
49
|
+
//The messages in the language of each request: ?lang=, the cookie lang, Accept-Language
|
|
50
|
+
middlewares: [I18nMiddleware],
|
|
51
|
+
//@end
|
|
52
|
+
http: expressHTTPEntrypoint({
|
|
53
|
+
port: port,
|
|
54
|
+
//The results and the errors are {errno, errMsg, data}, with the status 200
|
|
55
|
+
respond: jsonEnvelope({errorStatus: 200}),
|
|
56
|
+
body: {limit: '100mb'},
|
|
57
|
+
server: {keepAliveTimeout: 60000, headersTimeout: 65000},
|
|
58
|
+
middlewares: [authenticate],
|
|
59
|
+
before: (app): void => void app.use(compression()),
|
|
60
|
+
//The paths outside of the actions are the pages of the application
|
|
61
|
+
fallback: ((directory: string | undefined) => directory ? staticUI(directory) : undefined)(uiDirectory())
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
bootstrap: ['entrypoint']
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {Controller} from 'lakutata/com/entrypoint'
|
|
2
|
+
import {GET, Prefix} from 'lakutata/decorator/ctrl'
|
|
3
|
+
import {GreetDTO} from '../dtos/GreetDTO.js'
|
|
4
|
+
//@if i18n
|
|
5
|
+
import {Inject} from 'lakutata/decorator/di'
|
|
6
|
+
import {Translator} from 'lakutata/com/i18n'
|
|
7
|
+
//@end
|
|
8
|
+
|
|
9
|
+
@Prefix({route: '/api/example'})
|
|
10
|
+
export class ExampleController extends Controller {
|
|
11
|
+
//@if i18n
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The messages of the language of the request
|
|
15
|
+
*/
|
|
16
|
+
@Inject(Translator)
|
|
17
|
+
protected readonly i18n: Translator
|
|
18
|
+
//@end
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* GET /api/example?name=you
|
|
22
|
+
* @param input
|
|
23
|
+
*/
|
|
24
|
+
@GET('/', GreetDTO)
|
|
25
|
+
public async greet(input: GreetDTO): Promise<{ message: string }> {
|
|
26
|
+
//@if i18n
|
|
27
|
+
return {message: this.i18n.t('greeting.hello', {name: input.name})}
|
|
28
|
+
//@else
|
|
29
|
+
return {message: `Hello, ${input.name}!`}
|
|
30
|
+
//@end
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {Controller} from 'lakutata/com/entrypoint'
|
|
2
|
+
import {GET} from 'lakutata/decorator/ctrl'
|
|
3
|
+
|
|
4
|
+
export class HealthController extends Controller {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /health: the process answers
|
|
8
|
+
*/
|
|
9
|
+
@GET('/health')
|
|
10
|
+
public async health(): Promise<{ status: string }> {
|
|
11
|
+
return {status: 'ok'}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {Exception} from 'lakutata'
|
|
2
|
+
import type {ActionInvocation, HTTPContext, Next} from 'lakutata/com/entrypoint'
|
|
3
|
+
|
|
4
|
+
export class UnauthorizedException extends Exception {
|
|
5
|
+
public errno: string = 'E_UNAUTHORIZED'
|
|
6
|
+
|
|
7
|
+
public readonly statusCode: number = 401
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The requests carry "Authorization: Bearer <API_TOKEN>" when the environment has an API_TOKEN (the health check excepted).
|
|
12
|
+
* Replace it with the authentication of the service (a session, a JWT verified with the keys of the identity provider…)
|
|
13
|
+
* @param invocation
|
|
14
|
+
* @param next
|
|
15
|
+
*/
|
|
16
|
+
export async function authenticate(invocation: ActionInvocation, next: Next): Promise<unknown> {
|
|
17
|
+
const token: string | undefined = process.env.API_TOKEN
|
|
18
|
+
const context: HTTPContext = invocation.context as HTTPContext
|
|
19
|
+
if (!token || context.route === '/health') return next()
|
|
20
|
+
if (context.request.headers.authorization !== `Bearer ${token}`) throw new UnauthorizedException('The request is not authenticated')
|
|
21
|
+
return next()
|
|
22
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {after, before, describe, it} from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import {Application} from 'lakutata'
|
|
4
|
+
import {installerConfig} from '../config/Config.js'
|
|
5
|
+
|
|
6
|
+
const PORT: number = Number(process.env.TEST_PORT ?? 18084)
|
|
7
|
+
|
|
8
|
+
describe('{$APP_NAME}', (): void => {
|
|
9
|
+
|
|
10
|
+
let app: Application
|
|
11
|
+
|
|
12
|
+
before(async (): Promise<void> => {
|
|
13
|
+
process.env.API_TOKEN = 'test-token'
|
|
14
|
+
app = await new Promise<Application>((resolve: (app: Application) => void): void => {
|
|
15
|
+
Application.run(installerConfig(PORT)).onLaunched((launched: Application): void => resolve(launched))
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
after(async (): Promise<void> => {
|
|
20
|
+
await app?.dispose()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('answers the health check without authentication', async (): Promise<void> => {
|
|
24
|
+
assert.deepStrictEqual(await (await fetch(`http://127.0.0.1:${PORT}/health`)).json(), {errno: null, errMsg: null, data: {status: 'ok'}})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('greets the authenticated requests', async (): Promise<void> => {
|
|
28
|
+
const response: Response = await fetch(`http://127.0.0.1:${PORT}/api/example?name=you`, {headers: {Authorization: 'Bearer test-token'}})
|
|
29
|
+
assert.deepStrictEqual(await response.json(), {errno: null, errMsg: null, data: {message: 'Hello, you!'}})
|
|
30
|
+
})
|
|
31
|
+
//@if i18n
|
|
32
|
+
|
|
33
|
+
it('greets in the language of the request, its validation errors too', async (): Promise<void> => {
|
|
34
|
+
const headers: Record<string, string> = {'Authorization': 'Bearer test-token', 'Accept-Language': 'zh-CN,zh;q=0.9'}
|
|
35
|
+
assert.deepStrictEqual(await (await fetch(`http://127.0.0.1:${PORT}/api/example?name=you`, {headers: headers})).json(), {errno: null, errMsg: null, data: {message: '你好,you!'}})
|
|
36
|
+
const invalid = await (await fetch(`http://127.0.0.1:${PORT}/api/example?name=`, {headers: headers})).json() as { errMsg: string }
|
|
37
|
+
assert.strictEqual(invalid.errMsg, '"name" 不能为空')
|
|
38
|
+
})
|
|
39
|
+
//@end
|
|
40
|
+
|
|
41
|
+
it('serves the pages of the application', async (): Promise<void> => {
|
|
42
|
+
for (const page of ['/', '/some/route']) {
|
|
43
|
+
const response: Response = await fetch(`http://127.0.0.1:${PORT}${page}`, {headers: {Accept: 'text/html'}})
|
|
44
|
+
assert.strictEqual(response.status, 200)
|
|
45
|
+
assert.match(await response.text(), /<div id="app"><\/div>/)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('refuses the requests without authentication', async (): Promise<void> => {
|
|
50
|
+
const response: Response = await fetch(`http://127.0.0.1:${PORT}/api/example`)
|
|
51
|
+
assert.strictEqual(response.status, 200)
|
|
52
|
+
assert.deepStrictEqual(await response.json(), {errno: 'E_UNAUTHORIZED', errMsg: 'The request is not authenticated', data: null})
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{$APP_NAME}</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.ts"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ui",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"clean": "lakutata clean dist",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"rebuild": "npm run clean && npm run build",
|
|
10
|
+
"dev": "vite",
|
|
11
|
+
"typecheck": "vue-tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"vue": "^3.5.43"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@vitejs/plugin-vue": "^6.0.9",
|
|
18
|
+
"vite": "^8.3.1",
|
|
19
|
+
"vue-tsc": "^3.3.11"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {onMounted, ref} from 'vue'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The results of the API: {errno, errMsg, data}
|
|
6
|
+
*/
|
|
7
|
+
type APIResult<T> = {
|
|
8
|
+
errno: string | null
|
|
9
|
+
errMsg: string | null
|
|
10
|
+
data: T
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const message = ref<string>('…')
|
|
14
|
+
|
|
15
|
+
onMounted(async (): Promise<void> => {
|
|
16
|
+
try {
|
|
17
|
+
const result: APIResult<{ message: string }> = await (await fetch('/api/example?name={$APP_NAME}')).json()
|
|
18
|
+
message.value = result.errno ? `${result.errno}: ${result.errMsg}` : result.data.message
|
|
19
|
+
} catch (error) {
|
|
20
|
+
message.value = (error as Error).message
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<main>
|
|
27
|
+
<h1>{$APP_NAME}</h1>
|
|
28
|
+
<p>{{ message }}</p>
|
|
29
|
+
</main>
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"jsx": "preserve",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"types": ["vite/client"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {defineConfig} from 'vite'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
|
|
4
|
+
//In development, the API is the one of the service started by "npm run dev"
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [vue()],
|
|
7
|
+
server: {
|
|
8
|
+
proxy: {
|
|
9
|
+
'/api': `http://127.0.0.1:${process.env.PORT ?? 8080}`
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
})
|