Initial commit
commit
8a47730a48
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"phabricator.uri": "https://hub.sealcode.org/",
|
||||||
|
"arc.land.onto.default": "master",
|
||||||
|
"load": ["arcanist-linters", "arc-unit-mocha/src"],
|
||||||
|
"unit.engine": "MochaEngine",
|
||||||
|
"unit.mocha.include": ["./lib/**/*.test.js"],
|
||||||
|
"unit.mocha.dockerRoot": "/opt/sealious-app"
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"linters": {
|
||||||
|
"prettier": {
|
||||||
|
"type": "prettier",
|
||||||
|
"bin": "./node_modules/.bin/prettier",
|
||||||
|
"include": ["(\\.[tj]s$)", "(\\.s?css$)"]
|
||||||
|
},
|
||||||
|
"eslint": {
|
||||||
|
"type": "eslint",
|
||||||
|
"include": ["(src/.*\\.ts$)", "(src/.*\\.js$)"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: { node: true },
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["@typescript-eslint", "prettier", "with-tsc-error"],
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: "module",
|
||||||
|
ecmaFeatures: {
|
||||||
|
modules: true,
|
||||||
|
},
|
||||||
|
project: [
|
||||||
|
"./src/back/tsconfig.json",
|
||||||
|
"./src/front/tsconfig.json",
|
||||||
|
"./src/scripts/tsconfig.json",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/require-await": 0,
|
||||||
|
/* "jsdoc/require-description": 2, */
|
||||||
|
"no-await-in-loop": 2,
|
||||||
|
"@typescript-eslint/consistent-type-assertions": [1, { assertionStyle: "never" }],
|
||||||
|
"no-console": 1,
|
||||||
|
},
|
||||||
|
settings: { jsdoc: { mode: "typescript" } },
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["*.subtest.ts", "*.test.ts"],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": 0,
|
||||||
|
"prefer-const": 0,
|
||||||
|
"@typescript-eslint/no-unsafe-call": 0,
|
||||||
|
"@typescript-eslint/no-unsafe-return": 0,
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": 0,
|
||||||
|
"no-await-in-loop": 1, // sometimes it's easier to debug when requests run sequentially
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
tmp/
|
||||||
|
*~
|
||||||
|
*.sublime-workspace
|
||||||
|
npm-debug.log
|
||||||
|
node_modules
|
||||||
|
db
|
||||||
|
coverage.html
|
||||||
|
\#*
|
||||||
|
.\#*
|
||||||
|
.npm
|
||||||
|
.config
|
||||||
|
.ash_history
|
||||||
|
cosealious
|
||||||
|
node_modules*
|
||||||
|
.cache
|
||||||
|
lib
|
||||||
|
@types
|
||||||
|
.xunit
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
/dist/
|
||||||
|
public/dist
|
||||||
|
/log.html
|
||||||
|
/hint-report/
|
||||||
|
.vscode
|
||||||
|
.env
|
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"connector": {
|
||||||
|
"name": "jsdom"
|
||||||
|
},
|
||||||
|
"formatters": ["codeframe"],
|
||||||
|
"hintsTimeout": 20000,
|
||||||
|
"extends": ["web-recommended", "accessibility"],
|
||||||
|
"hints": {
|
||||||
|
"no-friendly-error-pages": "off",
|
||||||
|
"no-broken-links": "warning",
|
||||||
|
"doctype": "error",
|
||||||
|
"apple-touch-icons": "error",
|
||||||
|
"button-type": "error",
|
||||||
|
"compat-api/css": "error",
|
||||||
|
"compat-api/html": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"ignore": ["img[loading]"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"create-element-svg": "error",
|
||||||
|
"css-prefix-order": "error",
|
||||||
|
"disown-opener": "error",
|
||||||
|
"highest-available-document-mode": "error",
|
||||||
|
"leading-dot-classlist": "error",
|
||||||
|
"manifest-exists": "error",
|
||||||
|
"meta-charset-utf-8": "error",
|
||||||
|
"meta-viewport": "error",
|
||||||
|
"no-bom": "error",
|
||||||
|
"no-inline-styles": "error",
|
||||||
|
"no-protocol-relative-urls": "error",
|
||||||
|
"html-checker": "error",
|
||||||
|
"scoped-svg-styles": "error",
|
||||||
|
"sri": "error",
|
||||||
|
"axe/aria": "error",
|
||||||
|
"axe/color": "error",
|
||||||
|
"axe/forms": "error",
|
||||||
|
"axe/keyboard": "error",
|
||||||
|
"axe/language": "error",
|
||||||
|
"axe/name-role-value": "error",
|
||||||
|
"axe/parsing": "error",
|
||||||
|
"axe/semantics": "error",
|
||||||
|
"axe/sensory-and-visual-cues": "error",
|
||||||
|
"axe/structure": "error",
|
||||||
|
"axe/tables": "error",
|
||||||
|
"axe/text-alternatives": "error",
|
||||||
|
"axe/time-and-media": "error"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"connector": "local",
|
||||||
|
"extends": ["web-recommended", "accessibility"],
|
||||||
|
"formatters": ["codeframe"],
|
||||||
|
"hints": {
|
||||||
|
"apple-touch-icons": "error",
|
||||||
|
"button-type": "error",
|
||||||
|
"compat-api/css": "error",
|
||||||
|
"compat-api/html": "error",
|
||||||
|
"create-element-svg": "error",
|
||||||
|
"css-prefix-order": "error",
|
||||||
|
"disown-opener": "error",
|
||||||
|
"highest-available-document-mode": "error",
|
||||||
|
"leading-dot-classlist": "error",
|
||||||
|
"manifest-exists": "error",
|
||||||
|
"meta-charset-utf-8": "off",
|
||||||
|
"meta-viewport": "error",
|
||||||
|
"no-bom": "error",
|
||||||
|
"no-inline-styles": "error",
|
||||||
|
"no-protocol-relative-urls": "error",
|
||||||
|
"scoped-svg-styles": "error",
|
||||||
|
"sri": "error",
|
||||||
|
"axe/aria": "error",
|
||||||
|
"axe/color": "error",
|
||||||
|
"axe/forms": "error",
|
||||||
|
"axe/keyboard": "error",
|
||||||
|
"axe/language": "error",
|
||||||
|
"axe/name-role-value": "error",
|
||||||
|
"axe/parsing": "error",
|
||||||
|
"axe/semantics": "error",
|
||||||
|
"axe/sensory-and-visual-cues": "error",
|
||||||
|
"axe/structure": "error",
|
||||||
|
"axe/tables": "error",
|
||||||
|
"axe/text-alternatives": "error",
|
||||||
|
"axe/time-and-media": "error",
|
||||||
|
"no-friendly-error-pages": "off",
|
||||||
|
"content-type": "off",
|
||||||
|
"http-cache": "off",
|
||||||
|
"http-compression": "off",
|
||||||
|
"no-disallowed-headers": "off",
|
||||||
|
"no-html-only-headers": "off",
|
||||||
|
"no-http-redirects": "off",
|
||||||
|
"no-vulnerable-javascript-libraries": "off",
|
||||||
|
"ssllabs": "off",
|
||||||
|
"strict-transport-security": "off",
|
||||||
|
"stylesheet-limits": "off",
|
||||||
|
"validate-set-cookie-header": "off",
|
||||||
|
"x-content-type-options": "off",
|
||||||
|
"no-broken-links": "off",
|
||||||
|
"typescript-config/consistent-casing": "off",
|
||||||
|
"typescript-config/is-valid": "off",
|
||||||
|
"typescript-config/strict": "off",
|
||||||
|
"typescript-config/target": "off"
|
||||||
|
},
|
||||||
|
"hintsTimeout": 10000,
|
||||||
|
"parsers": [
|
||||||
|
"babel-config",
|
||||||
|
"css",
|
||||||
|
"html",
|
||||||
|
"javascript",
|
||||||
|
"jsx",
|
||||||
|
"less",
|
||||||
|
"sass",
|
||||||
|
"typescript",
|
||||||
|
"typescript-config",
|
||||||
|
"webpack-config"
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 90,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.yml",
|
||||||
|
"options": {
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
# Sealious app
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- docker
|
||||||
|
- docker-compose (version 2.6 or up)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
./npm.sh install
|
||||||
|
```
|
||||||
|
|
||||||
|
Always use ./npm.sh when installing dependencies.
|
||||||
|
|
||||||
|
## Running the app in development mode
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build && SEALIOUS_DB_DELAY=1500 NETWORK_DELAY=1500 npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```
|
||||||
|
./npm.sh run test
|
||||||
|
```
|
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
@ -0,0 +1,20 @@
|
|||||||
|
version: "3.2"
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mongo:4.4-bionic
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${PORT:-2074}7:27017"
|
||||||
|
test:
|
||||||
|
image: sealious-app:latest
|
||||||
|
build:
|
||||||
|
context: ./docker
|
||||||
|
dockerfile: ./test.Dockerfile
|
||||||
|
volumes:
|
||||||
|
- ./:/opt/sealious-app/
|
||||||
|
- ~/.npm_cacache:/opt/sealious-app/.npm_cacache
|
||||||
|
user: ${UID:-1000}:${GID:-1000}
|
||||||
|
mailcatcher:
|
||||||
|
image: schickling/mailcatcher:latest
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${PORT:-108}2:1080"
|
||||||
|
- "127.0.0.1:${PORT:-102}6:1025"
|
@ -0,0 +1,24 @@
|
|||||||
|
FROM node:18-bullseye-slim
|
||||||
|
|
||||||
|
ENV HOME=/opt/sealious-app
|
||||||
|
|
||||||
|
# Tini will ensure that any orphaned processes get reaped properly.
|
||||||
|
ENV TINI_VERSION v0.19.0
|
||||||
|
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
|
||||||
|
|
||||||
|
RUN apt update
|
||||||
|
RUN apt install -y git
|
||||||
|
|
||||||
|
RUN chmod +x /tini
|
||||||
|
ENTRYPOINT ["/tini", "--"]
|
||||||
|
|
||||||
|
VOLUME $HOME
|
||||||
|
WORKDIR $HOME
|
||||||
|
|
||||||
|
RUN npm install -g npm@latest
|
||||||
|
|
||||||
|
USER $UID:$GID
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["/usr/local/bin/node", "."]
|
@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash -xe
|
||||||
|
set -e
|
||||||
|
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
npx sealgen make-env
|
||||||
|
cp secrets.example.json secrets.json
|
||||||
|
|
||||||
|
export SEALIOUS_PORT="${PORT}0"
|
||||||
|
SEALIOUS_BASE_URL=$(cat .base_url)
|
||||||
|
export SEALIOUS_BASE_URL
|
||||||
|
echo "PORT=$PORT" >> .env
|
||||||
|
|
||||||
|
# Create the npm cache directory if it isn't present yet. If it is not present, it will be created
|
||||||
|
# when the docker image is being built with root:root as the owner.
|
||||||
|
mkdir -p ~/.npm_cacache
|
||||||
|
|
||||||
|
# https://github.com/docker/compose/issues/4725
|
||||||
|
docker-compose build
|
||||||
|
# Create .npm directory in the container, since it is not yet present and we need it for next step.
|
||||||
|
docker-compose run --user="$UID" --rm --service-ports test mkdir -p /opt/sealious-app/.npm
|
||||||
|
# Link the host-bound npm cache directory into the container's npm cache directory.
|
||||||
|
docker-compose run --user="$UID" --rm --service-ports test ln -s /opt/sealious-app/.npm_cacache/ /opt/sealious-app/.npm/_cacache
|
||||||
|
docker-compose up -d db
|
||||||
|
./npm.sh --no-TTY --user="$UID" ci && ./npm.sh --no-TTY --user="$UID" run build
|
||||||
|
|
||||||
|
rm -f log.html
|
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash -xe
|
||||||
|
|
||||||
|
export SEALIOUS_PORT=$PORT
|
||||||
|
SEALIOUS_BASE_URL=$(cat .base_url)
|
||||||
|
export SEALIOUS_BASE_URL
|
||||||
|
|
||||||
|
./npm.sh --no-TTY --user="$UID" run typecheck:front
|
||||||
|
./npm.sh --no-TTY --user="$UID" run typecheck:back
|
||||||
|
|
||||||
|
docker-compose run --user="$UID" \
|
||||||
|
-e "SEALIOUS_MONGO_PORT=27017" \
|
||||||
|
-e "SEALIOUS_MONGO_HOST=db" \
|
||||||
|
-e "SEALIOUS_PORT=$SEALIOUS_PORT" \
|
||||||
|
-e "SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL" \
|
||||||
|
-e "SEALIOUS_SANITY=true" \
|
||||||
|
test
|
||||||
|
|
@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SEALIOUS_PORT="${PORT}0"
|
||||||
|
SEALIOUS_BASE_URL=$(cat .base_url)
|
||||||
|
export SEALIOUS_BASE_URL
|
||||||
|
|
||||||
|
./npm.sh --no-TTY --user="$UID" run build:front
|
||||||
|
|
||||||
|
docker-compose up -d mailcatcher
|
||||||
|
|
||||||
|
docker-compose run --user="$UID" \
|
||||||
|
-e "SEALIOUS_MONGO_PORT=27017" \
|
||||||
|
-e "SEALIOUS_MONGO_HOST=db" \
|
||||||
|
-e "SEALIOUS_PORT=$SEALIOUS_PORT" \
|
||||||
|
-e "SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL" \
|
||||||
|
-e "SEALIOUS_MAILER=mailcatcher" \
|
||||||
|
-p "${SEALIOUS_PORT}:${SEALIOUS_PORT}" \
|
||||||
|
-d \
|
||||||
|
test \
|
||||||
|
/bin/sh -c "{ node . --color 2>&1; } | ./node_modules/.bin/ansi-html-stream > log.html" &&
|
||||||
|
echo "App started on $SEALIOUS_PORT"
|
||||||
|
|
||||||
|
echo "Deployed app to https://${SEALIOUS_PORT}.dep.sealco.de"
|
||||||
|
echo "Mailcatcher available at https://${PORT}2.dep.sealco.de"
|
||||||
|
echo "Application logs should be available at https://jenkins.sealcode.org/job/Deploy%20to%20dep.sealco.de/ws%20v2/$PORT/log.html"
|
@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export SEALIOUS_PORT="${PORT}0"
|
||||||
|
SEALIOUS_BASE_URL=$(cat .base_url)
|
||||||
|
export SEALIOUS_BASE_URL
|
||||||
|
|
||||||
|
docker-compose down --volumes
|
||||||
|
rm -rf node_modules
|
||||||
|
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"delay": "100"
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env -S bash -x
|
||||||
|
|
||||||
|
|
||||||
|
# the "--no-TTY" option is crucial - without it the output is not captured in Jenkins
|
||||||
|
|
||||||
|
docker compose run \
|
||||||
|
--rm \
|
||||||
|
--service-ports \
|
||||||
|
test \
|
||||||
|
npm "$@"
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"name": "sealious-app",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "./dist/back/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node .",
|
||||||
|
"typecheck:back": "tsc --noEmit -p src/back",
|
||||||
|
"typecheck:front": "tsc --noEmit -p src/front",
|
||||||
|
"build": "esbuild --outfile='./public/dist/react.js' src/back/routes/react.tsx && sealgen build",
|
||||||
|
"watch": "multiple-scripts-tmux \"npm run typecheck:back -- --watch\" \"SEALIOUS_PORT=$SEALIOUS_PORT SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL nodemon --enable-source-maps .\" \"npm run build -- --watch\" \"npm run typecheck:front -- --watch\" ",
|
||||||
|
"test": "TS_NODE_PROJECT='./src/back/tsconfig.json' mocha --recursive --require ts-node/register src/back/app.ts src/back/**/*.test.ts src/back/**/**/*.test.ts src/back/**/**/**/*.test.ts src/back/**/**/**/**/*.test.ts",
|
||||||
|
"coverage": "nyc npm run test --",
|
||||||
|
"test-reports": "docker-compose up -d && ./npm.sh run coverage -- --reporter xunit --reporter-option output=.xunit",
|
||||||
|
"show-coverage": "npm run test-reports; xdg-open coverage/index.html"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.12.10",
|
||||||
|
"@hotwired/turbo": "^7.1.0",
|
||||||
|
"@koa/router": "^10.1.1",
|
||||||
|
"@sealcode/sealgen": "^0.8.39",
|
||||||
|
"@sealcode/ts-predicates": "^0.4.0",
|
||||||
|
"@types/kill-port": "^2.0.0",
|
||||||
|
"hint": "^7.0.1",
|
||||||
|
"ipsum": "^1.0.0",
|
||||||
|
"locreq": "^2.0.2",
|
||||||
|
"multiple-scripts-tmux": "^1.0.4",
|
||||||
|
"nodemon": "^2.0.7",
|
||||||
|
"sealious": "^0.17.29",
|
||||||
|
"stimulus": "^2.0.0",
|
||||||
|
"tempstream": "^0.0.21"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@hint/connector-jsdom": "^4.1.20",
|
||||||
|
"@hint/formatter-codeframe": "^3.1.29",
|
||||||
|
"@hint/hint-doctype": "^3.3.19",
|
||||||
|
"@hint/hint-no-broken-links": "^4.2.19",
|
||||||
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||||
|
"@sealcode/ansi-html-stream": "^1.0.1",
|
||||||
|
"@types/koa__router": "^8.0.4",
|
||||||
|
"@types/mocha": "^9.1.0",
|
||||||
|
"@types/tedious": "^4.0.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||||
|
"@typescript-eslint/parser": "^5.10.2",
|
||||||
|
"axios": "^0.24.0",
|
||||||
|
"eslint": "^7.19.0",
|
||||||
|
"eslint-config-prettier": "^7.2.0",
|
||||||
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
|
"eslint-plugin-with-tsc-error": "^0.0.7",
|
||||||
|
"kill-port": "^1.6.1",
|
||||||
|
"mocha": "^8.4.0",
|
||||||
|
"mri": "^1.1.6",
|
||||||
|
"nyc": "^15.1.0",
|
||||||
|
"prettier": "^2.2.1",
|
||||||
|
"ts-loader": "^8.0.14",
|
||||||
|
"ts-node": "^10.4.0",
|
||||||
|
"typescript": "^4.1.3"
|
||||||
|
},
|
||||||
|
"nyc": {
|
||||||
|
"extends": "@istanbuljs/nyc-config-typescript",
|
||||||
|
"check-coverage": false,
|
||||||
|
"all": true,
|
||||||
|
"include": [
|
||||||
|
"src/**/!(*.test.*).[tj]s?(x)"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/_tests_/**/*.*"
|
||||||
|
],
|
||||||
|
"reporter": [
|
||||||
|
"html",
|
||||||
|
"lcov",
|
||||||
|
"clover",
|
||||||
|
"text",
|
||||||
|
"text-summary"
|
||||||
|
],
|
||||||
|
"report-dir": "coverage"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=17.0.0"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,87 @@
|
|||||||
|
import _locreq from "locreq";
|
||||||
|
import { default as Sealious, App, LoggerMailer, SMTPMailer } from "sealious";
|
||||||
|
import { LoggerLevel } from "sealious/@types/src/app/logger";
|
||||||
|
import { collections } from "./collections/collections";
|
||||||
|
import { sleep } from "./util";
|
||||||
|
const locreq = _locreq(__dirname);
|
||||||
|
|
||||||
|
const PORT = process.env.SEALIOUS_PORT ? parseInt(process.env.SEALIOUS_PORT) : 8080;
|
||||||
|
const base_url = process.env.SEALIOUS_BASE_URL || `http://localhost:${PORT}`;
|
||||||
|
const MONGO_PORT = process.env.SEALIOUS_MONGO_PORT
|
||||||
|
? parseInt(process.env.SEALIOUS_MONGO_PORT)
|
||||||
|
: 20747;
|
||||||
|
const MONGO_HOST = process.env.SEALIOUS_MONGO_HOST || "127.0.0.1";
|
||||||
|
|
||||||
|
declare module "koa" {
|
||||||
|
interface BaseContext {
|
||||||
|
$context: Sealious.Context;
|
||||||
|
$app: TheApp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TheApp extends App {
|
||||||
|
config = {
|
||||||
|
upload_path: locreq.resolve("uploaded_files"),
|
||||||
|
datastore_mongo: {
|
||||||
|
host: MONGO_HOST,
|
||||||
|
port: MONGO_PORT,
|
||||||
|
db_name: "sealious-app",
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
from_address: "sealious-app@example.com",
|
||||||
|
from_name: "sealious-app app",
|
||||||
|
},
|
||||||
|
logger: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
level: "info" as LoggerLevel,
|
||||||
|
},
|
||||||
|
"www-server": {
|
||||||
|
port: PORT,
|
||||||
|
},
|
||||||
|
core: {
|
||||||
|
environment: <const>"production", // to send the full html emails
|
||||||
|
},
|
||||||
|
};
|
||||||
|
manifest = {
|
||||||
|
name: "sealious-app",
|
||||||
|
logo: locreq.resolve("assets/logo.png"),
|
||||||
|
version: "0.0.1",
|
||||||
|
default_language: "en",
|
||||||
|
base_url,
|
||||||
|
admin_email: "admin@example.com",
|
||||||
|
colors: {
|
||||||
|
primary: "#5294a1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
collections = collections;
|
||||||
|
mailer =
|
||||||
|
process.env.SEALIOUS_MAILER === "mailcatcher"
|
||||||
|
? new SMTPMailer({
|
||||||
|
host: "mailcatcher",
|
||||||
|
port: 1025,
|
||||||
|
user: "any",
|
||||||
|
password: "any",
|
||||||
|
})
|
||||||
|
: new LoggerMailer();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
await super.start();
|
||||||
|
await this.collections.posts.populate();
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
await super.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
initRouter() {
|
||||||
|
this.HTTPServer.router.use("/", async (_, next) => {
|
||||||
|
const ms = parseInt(process.env.NETWORK_DELAY || "0");
|
||||||
|
console.log("NETWORK DELAY!", ms);
|
||||||
|
await sleep(ms);
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
super.initRouter();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
// DO NOT EDIT! This file is generated automaticaly with 'npm run generate-collections'
|
||||||
|
import { App } from "sealious";
|
||||||
|
|
||||||
|
import _GroupsToUsers from "./groups-to-users";
|
||||||
|
import _Groups from "./groups";
|
||||||
|
import _PasswordResetIntents from "./password-reset-intents";
|
||||||
|
import _Posts from "./posts";
|
||||||
|
import _Secrets from "./secrets";
|
||||||
|
import _UserRoles from "./user-roles";
|
||||||
|
import _Users from "./users";
|
||||||
|
|
||||||
|
export const GroupsToUsers = new _GroupsToUsers();
|
||||||
|
export const Groups = new _Groups();
|
||||||
|
export const PasswordResetIntents = new _PasswordResetIntents();
|
||||||
|
export const Posts = new _Posts();
|
||||||
|
export const Secrets = new _Secrets();
|
||||||
|
export const UserRoles = new _UserRoles();
|
||||||
|
export const Users = new _Users();
|
||||||
|
|
||||||
|
export const collections = {
|
||||||
|
...App.BaseCollections,
|
||||||
|
"groups-to-users": GroupsToUsers,
|
||||||
|
groups: Groups,
|
||||||
|
"password-reset-intents": PasswordResetIntents,
|
||||||
|
posts: Posts,
|
||||||
|
secrets: Secrets,
|
||||||
|
"user-roles": UserRoles,
|
||||||
|
users: Users,
|
||||||
|
};
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Collection, FieldTypes, Policies } from "sealious";
|
||||||
|
import { Roles } from "../policy-types/roles";
|
||||||
|
|
||||||
|
export default class GroupsToUsers extends Collection {
|
||||||
|
fields = {
|
||||||
|
user: new FieldTypes.SingleReference("users"),
|
||||||
|
group: new FieldTypes.SingleReference("groups"),
|
||||||
|
};
|
||||||
|
defaultPolicy = new Roles(["admin"]);
|
||||||
|
policies = {
|
||||||
|
show: new Policies.Or([
|
||||||
|
new Roles(["admin"]),
|
||||||
|
new Policies.UserReferencedInField("user"),
|
||||||
|
]),
|
||||||
|
list: new Policies.Or([
|
||||||
|
new Roles(["admin"]),
|
||||||
|
new Policies.UserReferencedInField("user"),
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { Collection, FieldTypes, Policies } from "sealious";
|
||||||
|
import { Roles } from "../policy-types/roles";
|
||||||
|
|
||||||
|
export default class Groups extends Collection {
|
||||||
|
fields = {
|
||||||
|
name: new FieldTypes.Text(),
|
||||||
|
};
|
||||||
|
defaultPolicy = new Policies.LoggedIn();
|
||||||
|
policies = {
|
||||||
|
create: new Roles(["admin"]),
|
||||||
|
edit: new Roles(["admin"]),
|
||||||
|
};
|
||||||
|
|
||||||
|
async populate(): Promise<void> {
|
||||||
|
if (await this.app.Metadata.get("groups_populated")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("### Populating groups");
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
await this.app.Metadata.set("groups_populated", "true");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import assert from "assert";
|
||||||
|
import TheApp from "../app";
|
||||||
|
import { withProdApp } from "../test_utils/with-prod-app";
|
||||||
|
|
||||||
|
describe("password-reset-intents", () => {
|
||||||
|
async function createAUser(app: TheApp) {
|
||||||
|
await app.collections.users.suCreate({
|
||||||
|
username: "user",
|
||||||
|
email: "user@example.com",
|
||||||
|
password: "password",
|
||||||
|
roles: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("tells you if the email address doesn't exist", async () =>
|
||||||
|
withProdApp(async ({ app, base_url }) => {
|
||||||
|
const email = "fake@example.com";
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`${base_url}/api/v1/collections/password-reset-intents`,
|
||||||
|
{
|
||||||
|
email: email,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
assert.equal(
|
||||||
|
e.response.data.data.field_messages.email.message,
|
||||||
|
app.i18n("invalid_existing_value", ["users", "email", email])
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error("it didn't throw");
|
||||||
|
}));
|
||||||
|
|
||||||
|
it("allows anyone to create an intent, if the email exists", async () =>
|
||||||
|
withProdApp(async ({ app, base_url }) => {
|
||||||
|
await createAUser(app);
|
||||||
|
const { email, token } = (
|
||||||
|
await axios.post(
|
||||||
|
`${base_url}/api/v1/collections/password-reset-intents`,
|
||||||
|
{
|
||||||
|
email: "user@example.com",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
assert.deepEqual(
|
||||||
|
{ email, token },
|
||||||
|
{
|
||||||
|
email: "user@example.com",
|
||||||
|
token: "it's a secret to everybody",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it("tells you if the email address is malformed", async () =>
|
||||||
|
withProdApp(async ({ app, base_url }) => {
|
||||||
|
const email = "incorrect-address";
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`${base_url}/api/v1/collections/password-reset-intents`,
|
||||||
|
{
|
||||||
|
email: email,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
assert.equal(
|
||||||
|
e.response.data.data.field_messages.email.message,
|
||||||
|
app.i18n("invalid_email", [email])
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error("it didn't throw");
|
||||||
|
}));
|
||||||
|
|
||||||
|
it("sends an email with the reset password link", async () =>
|
||||||
|
withProdApp(async ({ app, base_url, mail_api }) => {
|
||||||
|
await createAUser(app);
|
||||||
|
await axios.post(`${base_url}/api/v1/collections/password-reset-intents`, {
|
||||||
|
email: "user@example.com",
|
||||||
|
});
|
||||||
|
const messages = (await mail_api.getMessages()).filter(
|
||||||
|
(message) => message.recipients[0] == "<user@example.com>"
|
||||||
|
);
|
||||||
|
assert.equal(messages.length, 1);
|
||||||
|
assert.equal(messages[0].recipients.length, 1);
|
||||||
|
assert.equal(messages[0].recipients[0], "<user@example.com>");
|
||||||
|
}));
|
||||||
|
});
|
@ -0,0 +1,49 @@
|
|||||||
|
import { App, Collection, CollectionItem, Context, FieldTypes, Policies } from "sealious";
|
||||||
|
import assert from "assert";
|
||||||
|
import PasswordResetTemplate from "../email-templates/password-reset";
|
||||||
|
import TheApp from "../app";
|
||||||
|
import { assertType, predicates } from "@sealcode/ts-predicates";
|
||||||
|
|
||||||
|
export default class PasswordResetIntents extends Collection {
|
||||||
|
name = "password-reset-intents";
|
||||||
|
fields = {
|
||||||
|
email: new FieldTypes.ValueExistingInCollection({
|
||||||
|
field: "email",
|
||||||
|
collection: "users",
|
||||||
|
include_forbidden: true,
|
||||||
|
}),
|
||||||
|
token: new FieldTypes.SecretToken(),
|
||||||
|
};
|
||||||
|
policies = {
|
||||||
|
create: new Policies.Public(),
|
||||||
|
edit: new Policies.Noone(),
|
||||||
|
};
|
||||||
|
defaultPolicy = new Policies.Super();
|
||||||
|
async init(app: App, name: string) {
|
||||||
|
assert(app instanceof TheApp);
|
||||||
|
await super.init(app, name);
|
||||||
|
app.collections["password-reset-intents"].on(
|
||||||
|
"after:create",
|
||||||
|
async ([, intent]: [
|
||||||
|
Context,
|
||||||
|
CollectionItem<PasswordResetIntents>,
|
||||||
|
unknown
|
||||||
|
]) => {
|
||||||
|
const intent_as_super = await intent.fetchAs(new app.SuperContext());
|
||||||
|
const message = await PasswordResetTemplate(app, {
|
||||||
|
email_address: assertType(
|
||||||
|
intent.get("email"),
|
||||||
|
predicates.string,
|
||||||
|
"email_address isn't a string"
|
||||||
|
),
|
||||||
|
token: assertType(
|
||||||
|
intent_as_super.get("token"),
|
||||||
|
predicates.string,
|
||||||
|
"token isn't a string"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
await message.send(app);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { Collection, FieldTypes, Policies } from "sealious";
|
||||||
|
import TheApp from "../app";
|
||||||
|
const { Ipsum } = require("ipsum");
|
||||||
|
|
||||||
|
export default class Posts extends Collection {
|
||||||
|
fields = {
|
||||||
|
title: new FieldTypes.Text(),
|
||||||
|
description: new FieldTypes.Text(),
|
||||||
|
};
|
||||||
|
defaultPolicy = new Policies.Public();
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import { Collection, FieldTypes } from "sealious";
|
||||||
|
import { Roles } from "../policy-types/roles";
|
||||||
|
|
||||||
|
/* For testing the Roles policy */
|
||||||
|
export default class Secrets extends Collection {
|
||||||
|
fields = {
|
||||||
|
content: new FieldTypes.Text(),
|
||||||
|
};
|
||||||
|
defaultPolicy = new Roles(["admin"]);
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
import assert from "assert";
|
||||||
|
import axios from "axios";
|
||||||
|
import { Context, TestUtils } from "sealious";
|
||||||
|
import { withProdApp } from "../test_utils/with-prod-app";
|
||||||
|
import { createAdmin, createAUser } from "../test_utils/users";
|
||||||
|
import Users from "./users";
|
||||||
|
|
||||||
|
describe("user-roles", () => {
|
||||||
|
it("rejects when given an empty role", async () =>
|
||||||
|
withProdApp(async ({ app, rest_api }) => {
|
||||||
|
const [user, session] = await createAdmin(app, rest_api);
|
||||||
|
await TestUtils.assertThrowsAsync(
|
||||||
|
async () => {
|
||||||
|
return rest_api.post(
|
||||||
|
`/api/v1/collections/user-roles`,
|
||||||
|
{
|
||||||
|
user: user.id,
|
||||||
|
},
|
||||||
|
session
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(e: any) => {
|
||||||
|
assert.equal(
|
||||||
|
e?.response.data.data.field_messages.role?.message,
|
||||||
|
"Missing value for field 'role'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it("accepts correct dataset", async () =>
|
||||||
|
withProdApp(async ({ app, base_url, rest_api }) => {
|
||||||
|
const [user, session] = await createAdmin(app, rest_api);
|
||||||
|
const response = await axios.post(
|
||||||
|
`${base_url}/api/v1/collections/user-roles`,
|
||||||
|
{
|
||||||
|
user: user.id,
|
||||||
|
role: "admin",
|
||||||
|
},
|
||||||
|
session
|
||||||
|
);
|
||||||
|
assert.equal(response.status, 201);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it("get user roles with admin", async () =>
|
||||||
|
withProdApp(async ({ app, rest_api }) => {
|
||||||
|
const [user] = await createAdmin(app, rest_api);
|
||||||
|
const roles = await Users.getRoles(
|
||||||
|
new Context(app, new Date().getTime(), user.id)
|
||||||
|
);
|
||||||
|
assert.ok(roles.includes("admin"));
|
||||||
|
}));
|
||||||
|
|
||||||
|
it("get user with no roles", async () =>
|
||||||
|
withProdApp(async ({ app }) => {
|
||||||
|
const user = await createAUser(app, "normal");
|
||||||
|
const roles = await Users.getRoles(
|
||||||
|
new Context(app, new Date().getTime(), user.id)
|
||||||
|
);
|
||||||
|
assert.ok(roles.length === 0);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it("get no roles for no logged user", async () =>
|
||||||
|
withProdApp(async ({ app }) => {
|
||||||
|
const roles = await Users.getRoles(
|
||||||
|
new Context(app, new Date().getTime(), null)
|
||||||
|
);
|
||||||
|
assert.ok(roles.length === 0);
|
||||||
|
}));
|
||||||
|
});
|
@ -0,0 +1,36 @@
|
|||||||
|
import { App, Collection, FieldTypes, Policies, Policy } from "sealious";
|
||||||
|
import { Roles } from "../policy-types/roles";
|
||||||
|
|
||||||
|
export default class UserRoles extends Collection {
|
||||||
|
name = "user-roles";
|
||||||
|
fields = {
|
||||||
|
role: new FieldTypes.Enum((app: App) =>
|
||||||
|
app.ConfigManager.get("roles")
|
||||||
|
).setRequired(true),
|
||||||
|
user: new FieldTypes.SingleReference("users"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
policies = {
|
||||||
|
create: new Roles(["admin"]),
|
||||||
|
delete: new Policies.Public(),
|
||||||
|
show: new Policies.UserReferencedInField("user"),
|
||||||
|
edit: new Policies.Noone(),
|
||||||
|
} as { [policy: string]: Policy }; // this `as` statement allows the policies to be overwritten;
|
||||||
|
|
||||||
|
async init(app: App, collection_name: string) {
|
||||||
|
await super.init(app, collection_name);
|
||||||
|
app.on("started", async () => {
|
||||||
|
const roles = app.collections["user-roles"];
|
||||||
|
for (const action of <const>["create", "delete"]) {
|
||||||
|
const policy = roles.getPolicy(action);
|
||||||
|
if (policy instanceof Policies.Public) {
|
||||||
|
app.Logger.warn(
|
||||||
|
"USER POLICY",
|
||||||
|
`<user-roles> collection is using <public> access strategy for ${action} action. Anyone can change anyone elses role. This is the default behavior and you should overwrite it with <set_policy>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
import { App, Collections, Context, FieldTypes, Policies } from "sealious";
|
||||||
|
import assert from "assert";
|
||||||
|
import TheApp from "../app";
|
||||||
|
|
||||||
|
export default class Users extends Collections.users {
|
||||||
|
fields = {
|
||||||
|
...App.BaseCollections.users.fields,
|
||||||
|
email: new FieldTypes.Email().setRequired(true),
|
||||||
|
roles: new FieldTypes.ReverseSingleReference({
|
||||||
|
referencing_collection: "user-roles",
|
||||||
|
referencing_field: "user",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
defaultPolicy = new Policies.Themselves();
|
||||||
|
|
||||||
|
async init(app: App, name: string) {
|
||||||
|
assert(app instanceof TheApp);
|
||||||
|
await super.init(app, name);
|
||||||
|
app.on("started", async () => {
|
||||||
|
const username = "admin";
|
||||||
|
const users = await app.collections.users
|
||||||
|
.suList()
|
||||||
|
.filter({ username })
|
||||||
|
.fetch();
|
||||||
|
if (users.empty) {
|
||||||
|
app.Logger.warn(
|
||||||
|
"ADMIN",
|
||||||
|
`Creating an admin account for ${app.manifest.admin_email}`
|
||||||
|
);
|
||||||
|
await app.collections.users.suCreate({
|
||||||
|
username,
|
||||||
|
password: "adminadmin",
|
||||||
|
email: "admin@example.com",
|
||||||
|
roles: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getRoles(ctx: Context) {
|
||||||
|
const rolesEntries = await ctx.app.collections["user-roles"]
|
||||||
|
.list(ctx)
|
||||||
|
.filter({ user: ctx.user_id || "" })
|
||||||
|
.fetch();
|
||||||
|
|
||||||
|
return rolesEntries.items.map((item) => item.get("role"));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import { App, EmailTemplates, Errors } from "sealious";
|
||||||
|
import TheApp from "../app";
|
||||||
|
|
||||||
|
export default async function PasswordResetTemplate(
|
||||||
|
app: TheApp,
|
||||||
|
{ email_address, token }: { email_address: string; token: string }
|
||||||
|
) {
|
||||||
|
const matching_users = await app.collections["users"]
|
||||||
|
.suList()
|
||||||
|
.filter({ email: email_address })
|
||||||
|
.fetch();
|
||||||
|
|
||||||
|
if (!matching_users.items.length) {
|
||||||
|
throw new Errors.NotFound("No user with that email");
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = matching_users.items[0].get("username");
|
||||||
|
|
||||||
|
return EmailTemplates.Simple(app, {
|
||||||
|
subject: app.i18n("password_reset_email_subject", [app.manifest.name]),
|
||||||
|
to: `${username}<${email_address}>`,
|
||||||
|
text: `
|
||||||
|
${app.i18n("password_reset_email_text", [app.manifest.name, username])}`,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: app.i18n("password_reset_cta"),
|
||||||
|
href: `${app.manifest.base_url}/confirm-password-reset?token=${token}&email=${email_address}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import { App, EmailTemplates } from "sealious";
|
||||||
|
|
||||||
|
export default async function RegistrationIntentTemplate(
|
||||||
|
app: App,
|
||||||
|
{ email_address, token }: { email_address: string; token: string }
|
||||||
|
) {
|
||||||
|
return EmailTemplates.Simple(app, {
|
||||||
|
subject: app.i18n("registration_intent_email_subject", [app.manifest.name]),
|
||||||
|
to: email_address,
|
||||||
|
text: `
|
||||||
|
${app.i18n("registration_intent_email_text", [app.manifest.name])}`,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: app.i18n("registration_intent_cta"),
|
||||||
|
href: `${app.manifest.base_url}/account/confirm-registration-email?token=${token}&email=${email_address}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
export default function frame(id: string, body: string): string {
|
||||||
|
return /* HTML */ `<turbo-frame id="${id}"> ${body} </turbo-frame>`;
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Templatable, tempstream } from "tempstream";
|
||||||
|
import { Readable } from "stream";
|
||||||
|
import { BaseContext } from "koa";
|
||||||
|
import navbar from "./routes/common/navbar";
|
||||||
|
|
||||||
|
export const defaultHead = (ctx: BaseContext, title: string) => /* HTML */ `<title>
|
||||||
|
${title} · ${ctx.$app.manifest.name}
|
||||||
|
</title>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<script async src="/dist/bundle.js"></script>
|
||||||
|
<link href="/dist/style.css" rel="stylesheet" type="text/css" />`;
|
||||||
|
|
||||||
|
export default function html(
|
||||||
|
ctx: BaseContext,
|
||||||
|
title: string,
|
||||||
|
body: Templatable,
|
||||||
|
makeHead: (ctx: BaseContext, title: string) => Templatable = defaultHead
|
||||||
|
): Readable {
|
||||||
|
ctx.set("content-type", "text/html;charset=utf-8");
|
||||||
|
return tempstream/* HTML */ ` <!DOCTYPE html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
${makeHead(ctx, title)}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${navbar(ctx)} ${body}
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import kill from "kill-port";
|
||||||
|
import _locreq from "locreq";
|
||||||
|
import TheApp from "./app";
|
||||||
|
import { mainRouter } from "./routes";
|
||||||
|
const locreq = _locreq(__dirname);
|
||||||
|
|
||||||
|
const app = new TheApp();
|
||||||
|
|
||||||
|
kill(app.config["www-server"].port)
|
||||||
|
.then(() => app.start())
|
||||||
|
.then(async () => {
|
||||||
|
if (process.env.SEALIOUS_SANITY === "true") {
|
||||||
|
console.log("Exiting with error code 0");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
mainRouter(app.HTTPServer.router);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
if (process.env.SEALIOUS_SANITY === "true") {
|
||||||
|
console.log("EXITING WITH STATUS 1");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.HTTPServer.addStaticRoute("/", locreq.resolve("public"));
|
@ -0,0 +1,24 @@
|
|||||||
|
import { withProdApp } from "../test_utils/with-prod-app";
|
||||||
|
|
||||||
|
describe("roles", () => {
|
||||||
|
it("allows access to users with designated role and denies access to users without it", async () =>
|
||||||
|
withProdApp(async ({ app }) => {
|
||||||
|
await app.collections.users.suCreate({
|
||||||
|
username: "regular-user",
|
||||||
|
password: "password",
|
||||||
|
email: "regular@example.com",
|
||||||
|
roles: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const admin = await app.collections.users.suCreate({
|
||||||
|
username: "someadmin",
|
||||||
|
password: "admin-password",
|
||||||
|
email: "admin@example.com",
|
||||||
|
roles: [],
|
||||||
|
});
|
||||||
|
await app.collections["user-roles"].suCreate({
|
||||||
|
user: admin.id,
|
||||||
|
role: "admin",
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
@ -0,0 +1,55 @@
|
|||||||
|
import { Context, Policy, QueryTypes } from "sealious";
|
||||||
|
|
||||||
|
export class Roles extends Policy {
|
||||||
|
static type_name = "roles";
|
||||||
|
allowed_roles: string[];
|
||||||
|
constructor(allowed_roles: string[]) {
|
||||||
|
super(allowed_roles);
|
||||||
|
this.allowed_roles = allowed_roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
async countMatchingRoles(context: Context) {
|
||||||
|
const user_id = context.user_id;
|
||||||
|
context.app.Logger.debug2("ROLES", "Checking the roles for user", user_id);
|
||||||
|
const user_roles = await context.app.collections["user-roles"]
|
||||||
|
.list(context)
|
||||||
|
.filter({ user: user_id })
|
||||||
|
.fetch();
|
||||||
|
const roles = user_roles.items.map((user_role) => user_role.get("role"));
|
||||||
|
|
||||||
|
return this.allowed_roles.filter((allowed_role) => roles.includes(allowed_role))
|
||||||
|
.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getRestrictingQuery(context: Context) {
|
||||||
|
if (context.is_super) {
|
||||||
|
return new QueryTypes.AllowAll();
|
||||||
|
}
|
||||||
|
if (context.user_id === null) {
|
||||||
|
return new QueryTypes.DenyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
const matching_roles_count = await this.countMatchingRoles(context);
|
||||||
|
|
||||||
|
return matching_roles_count > 0
|
||||||
|
? new QueryTypes.AllowAll()
|
||||||
|
: new QueryTypes.DenyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkerFunction(context: Context) {
|
||||||
|
if (context.user_id === null) {
|
||||||
|
return Policy.deny(context.app.i18n("policy_logged_in_deny"));
|
||||||
|
}
|
||||||
|
const matching_roles_count = await this.countMatchingRoles(context);
|
||||||
|
|
||||||
|
return matching_roles_count > 0
|
||||||
|
? Policy.allow(
|
||||||
|
context.app.i18n("policy_roles_allow", [
|
||||||
|
this.allowed_roles.join(", "),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
: Policy.deny(
|
||||||
|
context.app.i18n("policy_roles_deny", [this.allowed_roles.join(", ")])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
import { BaseContext } from "koa";
|
||||||
|
import { hasShape, is, predicates } from "@sealcode/ts-predicates";
|
||||||
|
import { Collection, Errors } from "sealious";
|
||||||
|
import { ItemFields } from "sealious/@types/src/chip-types/collection-item-body";
|
||||||
|
|
||||||
|
export interface CollectionTiedFormData<C extends Collection> {
|
||||||
|
values: Partial<{ [field in keyof ItemFields<C>]: string }>;
|
||||||
|
errors?: Errors.FieldsError<C>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormFields<Fields extends string> {
|
||||||
|
values: Partial<{ [field in Fields]: string }>;
|
||||||
|
errors?: Partial<{ [field in Fields]: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formHasAllFields<Fields extends readonly string[]>(
|
||||||
|
ctx: BaseContext,
|
||||||
|
fields: Fields,
|
||||||
|
obj: unknown
|
||||||
|
): obj is { [field in Fields[number]]: string } {
|
||||||
|
const valid =
|
||||||
|
is(obj, predicates.object) &&
|
||||||
|
hasShape(
|
||||||
|
Object.fromEntries(fields.map((field) => [field, predicates.string])),
|
||||||
|
obj
|
||||||
|
);
|
||||||
|
if (!valid) {
|
||||||
|
ctx.status = 422;
|
||||||
|
if (is(obj, predicates.object)) {
|
||||||
|
ctx.body = `Missing params: ${fields
|
||||||
|
.filter((field) => !Object.keys(obj).includes(field))
|
||||||
|
.join(", ")}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formHasSomeFields<Fields extends readonly string[]>(
|
||||||
|
ctx: BaseContext,
|
||||||
|
fields: Fields,
|
||||||
|
obj: unknown
|
||||||
|
): obj is Partial<{ [field in Fields[number]]: string }> {
|
||||||
|
const valid =
|
||||||
|
is(obj, predicates.object) &&
|
||||||
|
hasShape(
|
||||||
|
Object.fromEntries(fields.map((field) => [field, predicates.string])),
|
||||||
|
obj
|
||||||
|
);
|
||||||
|
if (!valid) {
|
||||||
|
ctx.status = 422;
|
||||||
|
ctx.body = "Wrong type of params, expected string or undefined";
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import html from "../../html";
|
||||||
|
import { BaseContext } from "koa";
|
||||||
|
import { Readable } from "stream";
|
||||||
|
import { tempstream } from "tempstream";
|
||||||
|
|
||||||
|
export function MainView(ctx: BaseContext): Readable {
|
||||||
|
return html(
|
||||||
|
ctx,
|
||||||
|
"",
|
||||||
|
tempstream/* HTML */ `
|
||||||
|
<title>My Own ToDo App</title>
|
||||||
|
|
||||||
|
<h1>Sealious App</h1>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import { BaseContext } from "koa";
|
||||||
|
|
||||||
|
export default async function navbar(ctx: BaseContext) {
|
||||||
|
return /* HTML */ ` <nav>
|
||||||
|
<a href="/" class="nav-logo">
|
||||||
|
<img
|
||||||
|
src="/assets/logo"
|
||||||
|
alt="${ctx.$app.manifest.name} - logo"
|
||||||
|
width="50"
|
||||||
|
height="50"
|
||||||
|
/>
|
||||||
|
Sealious App
|
||||||
|
</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/logowanie">Logowanie</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>`;
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
.input {
|
||||||
|
&__error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
export default function input({
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
type,
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
error,
|
||||||
|
readonly,
|
||||||
|
required,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
id?: string;
|
||||||
|
label?: string;
|
||||||
|
type?: string;
|
||||||
|
value?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
readonly?: boolean;
|
||||||
|
error: string;
|
||||||
|
required?: boolean;
|
||||||
|
}) {
|
||||||
|
id = id || name;
|
||||||
|
label = label || name;
|
||||||
|
type = type || "text";
|
||||||
|
value = value || "";
|
||||||
|
placeholder = placeholder || type;
|
||||||
|
readonly = readonly || false;
|
||||||
|
required = required || false;
|
||||||
|
return /* HTML */ `<div class="input">
|
||||||
|
<label for="${id}">${label}</label>
|
||||||
|
<input
|
||||||
|
id="${id}"
|
||||||
|
type="${type}"
|
||||||
|
name="${name}"
|
||||||
|
value="${value}"
|
||||||
|
placeholder="${placeholder}"
|
||||||
|
${readonly ? "readonly" : ""}
|
||||||
|
${required ? "required" : ""}
|
||||||
|
/>
|
||||||
|
${error ? `<div class="input__error">${error}</div>` : ""}
|
||||||
|
</div>`;
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { Context } from "koa";
|
||||||
|
import { tempstream } from "tempstream";
|
||||||
|
import { Page } from "@sealcode/sealgen";
|
||||||
|
import html from "../html";
|
||||||
|
|
||||||
|
export const actionName = "Hello";
|
||||||
|
|
||||||
|
export default new (class HelloPage extends Page {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async canAccess(_: Context) {
|
||||||
|
return { canAccess: true, message: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async render(ctx: Context) {
|
||||||
|
return html(ctx, "Hello", tempstream/* HTML */ `<div></div>`);
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,15 @@
|
|||||||
|
import Router from "@koa/router";
|
||||||
|
import { sleep } from "@sealcode/sealgen";
|
||||||
|
import { Middlewares } from "sealious";
|
||||||
|
import { MainView } from "./common/main-view";
|
||||||
|
import mountAutoRoutes from "./routes";
|
||||||
|
|
||||||
|
export const mainRouter = (router: Router): void => {
|
||||||
|
router.get("/", Middlewares.extractContext(), async (ctx) => {
|
||||||
|
ctx.body = MainView(ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use(Middlewares.extractContext());
|
||||||
|
|
||||||
|
mountAutoRoutes(router);
|
||||||
|
};
|
@ -0,0 +1,34 @@
|
|||||||
|
import { Context } from "koa";
|
||||||
|
import { Mountable } from "@sealcode/sealgen";
|
||||||
|
import Router from "@koa/router";
|
||||||
|
import { sleep } from "../util";
|
||||||
|
|
||||||
|
export const actionName = "motd";
|
||||||
|
export function pick<T>(options: readonly T[]): T {
|
||||||
|
const random = Math.random();
|
||||||
|
const ret = options[Math.floor(random * options.length)];
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMOTD() {
|
||||||
|
await sleep(parseInt(process.env.SEALIOUS_DB_DELAY || "0") / 2);
|
||||||
|
return pick([
|
||||||
|
"Never don't give up",
|
||||||
|
"Say no to yes. Say pizza to drugs",
|
||||||
|
"Bądź kreatwyny",
|
||||||
|
"Inside you there are two wolves",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new (class motdRedirect extends Mountable {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async canAccess(_: Context) {
|
||||||
|
return { canAccess: true, message: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(router: Router, path: string) {
|
||||||
|
router.get(path, async (ctx) => {
|
||||||
|
ctx.body = { motd: await getMOTD() };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,48 @@
|
|||||||
|
import { Context } from "koa";
|
||||||
|
import { CollectionItem } from "sealious";
|
||||||
|
import { tempstream } from "tempstream";
|
||||||
|
import { Posts } from "../collections/collections";
|
||||||
|
import html from "../html";
|
||||||
|
import { SealiousItemListPage, BaseListPageFields } from "@sealcode/sealgen";
|
||||||
|
|
||||||
|
export const actionName = "ListPosts";
|
||||||
|
|
||||||
|
const filterFields = {};
|
||||||
|
|
||||||
|
export default new (class ListPostsPage extends SealiousItemListPage<
|
||||||
|
typeof Posts,
|
||||||
|
typeof BaseListPageFields
|
||||||
|
> {
|
||||||
|
fields = BaseListPageFields;
|
||||||
|
|
||||||
|
filterFields = filterFields;
|
||||||
|
filterControls = [];
|
||||||
|
|
||||||
|
async render(ctx: Context) {
|
||||||
|
return html(
|
||||||
|
ctx,
|
||||||
|
"Posts",
|
||||||
|
tempstream/* HTML */ `<div>
|
||||||
|
<h2>Posts List</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th>id</th>
|
||||||
|
${Object.keys(Posts.fields).map(
|
||||||
|
(fieldname) => `<th>${fieldname}</th>`
|
||||||
|
)}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${super.render(ctx)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderItem(_: Context, item: CollectionItem<typeof Posts>) {
|
||||||
|
return tempstream`<tr><td>${item.id}</td>${Object.keys(Posts.fields).map(
|
||||||
|
(fieldname: keyof typeof Posts["fields"]) =>
|
||||||
|
tempstream`<td>${item.get(fieldname)}</td>`
|
||||||
|
)}</tr>`;
|
||||||
|
}
|
||||||
|
})(Posts);
|
@ -0,0 +1,17 @@
|
|||||||
|
import { withProdApp } from "../test_utils/with-prod-app";
|
||||||
|
import { LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint";
|
||||||
|
import { ListPostsURL } from "./urls";
|
||||||
|
|
||||||
|
describe("ListPosts", () => {
|
||||||
|
it("doesn't crash", async function () {
|
||||||
|
this.timeout(LONG_TEST_TIMEOUT);
|
||||||
|
return withProdApp(async ({ base_url, rest_api }) => {
|
||||||
|
await rest_api.get(ListPostsURL);
|
||||||
|
await webhintURL(base_url + ListPostsURL);
|
||||||
|
// alternatively you can use webhintHTML for faster but less precise scans
|
||||||
|
// or for scanning responses of requests that use some form of authorization:
|
||||||
|
// const response = await rest_api.get(ListPostsURL);
|
||||||
|
// await webhintHTML(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,11 @@
|
|||||||
|
"use strict";
|
||||||
|
exports.__esModule = true;
|
||||||
|
var jsx_runtime_1 = require("react/jsx-runtime");
|
||||||
|
var react_1 = require("react");
|
||||||
|
var e = react_1["default"].createElement;
|
||||||
|
var domContainer = document.querySelector("#app");
|
||||||
|
var root = ReactDOM.createRoot(domContainer);
|
||||||
|
function app() {
|
||||||
|
return (0, jsx_runtime_1.jsx)("div", { children: "Hello!" });
|
||||||
|
}
|
||||||
|
root.render(e(app));
|
@ -0,0 +1,33 @@
|
|||||||
|
import { Context } from "koa";
|
||||||
|
import { Page } from "@sealcode/sealgen";
|
||||||
|
|
||||||
|
export const actionName = "React";
|
||||||
|
|
||||||
|
export default new (class ReactPage extends Page {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async canAccess(_: Context) {
|
||||||
|
return { canAccess: true, message: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async render(ctx: Context) {
|
||||||
|
return /* HTML */ `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>React version</title>
|
||||||
|
<link href="/dist/style.css" rel="stylesheet" type="text/css" />
|
||||||
|
<script
|
||||||
|
src="https://unpkg.com/react@18/umd/react.production.min.js"
|
||||||
|
crossorigin
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"
|
||||||
|
crossorigin
|
||||||
|
></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
<script src="/dist/react.js"></script>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,95 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const e = React.createElement;
|
||||||
|
|
||||||
|
const domContainer = document.querySelector("#app");
|
||||||
|
const root = ReactDOM.createRoot(domContainer);
|
||||||
|
|
||||||
|
function footer() {
|
||||||
|
const [motd, setMotd] = React.useState("");
|
||||||
|
React.useEffect(async () => {
|
||||||
|
const response = await fetch("/motd/");
|
||||||
|
const { motd } = await response.json();
|
||||||
|
setMotd(motd);
|
||||||
|
}, []);
|
||||||
|
return <div className="footer">Copyright 2022. {motd}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navbar() {
|
||||||
|
return (
|
||||||
|
<div className="navbar">
|
||||||
|
<div className="logo">My app</div>
|
||||||
|
<div>
|
||||||
|
<a href="/react/">react</a> <a href="/ssr1/">ssr1</a>{" "}
|
||||||
|
<a href="/ssr2/">ssr2</a> <a href="/ssr25/">ssr2.5</a>{" "}
|
||||||
|
<a href="/ssr3/">ssr3</a> <a href="/ssr4/">ssr4</a>{" "}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sidebar() {
|
||||||
|
return (
|
||||||
|
<div className="sidebar">
|
||||||
|
<ul className="filters">
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" /> Filtr1
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" /> Filtr2
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" /> Filtr3
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" /> Filtr4
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItem({ item }) {
|
||||||
|
return (
|
||||||
|
<li className="item">
|
||||||
|
<h3>{item.title}</h3>
|
||||||
|
<p>{item.description}</p>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function content() {
|
||||||
|
const [isLoading, setIsLoading] = React.useState(true);
|
||||||
|
const [items, setItems] = React.useState([]);
|
||||||
|
React.useEffect(async function () {
|
||||||
|
const response = await fetch("/api/v1/collections/posts");
|
||||||
|
const { items } = await response.json();
|
||||||
|
setItems(items);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, []);
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="content">
|
||||||
|
<div className="loading">Loading.....</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="content">
|
||||||
|
<ul className="items">{items.map((item) => e(renderItem, { item }))}</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function app() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{e(navbar)}
|
||||||
|
{e(sidebar)}
|
||||||
|
{e(content)}
|
||||||
|
{e(footer)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
root.render(e(app));
|
@ -0,0 +1,27 @@
|
|||||||
|
// DO NOT EDIT! This file is generated automaticaly with npm run generate-routes
|
||||||
|
|
||||||
|
import Router from "@koa/router";
|
||||||
|
import { mount } from "@sealcode/sealgen";
|
||||||
|
import * as URLs from "./urls";
|
||||||
|
|
||||||
|
import { default as Hello } from "./hello.page";
|
||||||
|
import { default as motd } from "./motd.post";
|
||||||
|
import { default as ListPosts } from "./posts.list";
|
||||||
|
import { default as React } from "./react.page";
|
||||||
|
import { default as ssr1 } from "./ssr1.page";
|
||||||
|
import { default as ssr2 } from "./ssr2.post";
|
||||||
|
import { default as ssr25 } from "./ssr25.page";
|
||||||
|
import { default as ssr3 } from "./ssr3.page";
|
||||||
|
import { default as ssr4 } from "./ssr4.page";
|
||||||
|
|
||||||
|
export default function mountAutoRoutes(router: Router) {
|
||||||
|
mount(router, URLs.HelloURL, Hello);
|
||||||
|
mount(router, URLs.motdURL, motd);
|
||||||
|
mount(router, URLs.ListPostsURL, ListPosts);
|
||||||
|
mount(router, URLs.ReactURL, React);
|
||||||
|
mount(router, URLs.ssr1URL, ssr1);
|
||||||
|
mount(router, URLs.ssr2URL, ssr2);
|
||||||
|
mount(router, URLs.ssr25URL, ssr25);
|
||||||
|
mount(router, URLs.ssr3URL, ssr3);
|
||||||
|
mount(router, URLs.ssr4URL, ssr4);
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
import { Context } from "koa";
|
||||||
|
import { tempstream } from "tempstream";
|
||||||
|
import { Page } from "@sealcode/sealgen";
|
||||||
|
import html from "../html";
|
||||||
|
import { Posts } from "../collections/collections";
|
||||||
|
import { CollectionItem } from "sealious";
|
||||||
|
import { getMOTD } from "./motd.post";
|
||||||
|
|
||||||
|
export const actionName = "ssr1";
|
||||||
|
|
||||||
|
async function footer() {
|
||||||
|
return /* HTML */ `<div class="footer">Copyright 2022. ${await getMOTD()}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navbar() {
|
||||||
|
return /* HTML */ ` <div class="navbar">
|
||||||
|
<div class="logo">My app</div>
|
||||||
|
<div>
|
||||||
|
<a href="/react/">react</a>
|
||||||
|
<a href="/ssr1/">ssr1</a>
|
||||||
|
<a href="/ssr2/">ssr2</a>
|
||||||
|
<a href="/ssr25/">ssr2.5</a>
|
||||||
|
<a href="/ssr3/">ssr3</a>
|
||||||
|
<a href="/ssr4/">ssr4</a>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sidebar() {
|
||||||
|
return /* HTML */ `
|
||||||
|
<div class="sidebar">
|
||||||
|
<ul class="filters">
|
||||||
|
<li><input type="checkbox" /> Filtr1</li>
|
||||||
|
<li><input type="checkbox" /> Filtr2</li>
|
||||||
|
<li><input type="checkbox" /> Filtr3</li>
|
||||||
|
<li><input type="checkbox" /> Filtr4</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItem(item: CollectionItem<typeof Posts>) {
|
||||||
|
return /* HTML */ `
|
||||||
|
<li class="item">
|
||||||
|
<h3>${item.get("title")}</h3>
|
||||||
|
<p>${item.get("description")}</p>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function content() {
|
||||||
|
const { items } = await Posts.suList().fetch();
|
||||||
|
return /* HTML */ `<div class="content">
|
||||||
|
<ul class="items">
|
||||||
|
${items.map((item) => renderItem(item)).join("")}
|
||||||
|
</ul>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new (class ssr1Page extends Page {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async canAccess(_: Context) {
|
||||||
|
return { canAccess: true, message: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async render(ctx: Context) {
|
||||||
|
return /* HTML */ `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SSR1 version</title>
|
||||||
|
<link href="/dist/style.css" rel="stylesheet" type="text/css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
${navbar()}${sidebar()}${await content()}${await footer()}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,86 @@
|
|||||||
|
import { Context } from "koa";
|
||||||
|
import { tempstream } from "tempstream";
|
||||||
|
import { Page, sleep } from "@sealcode/sealgen";
|
||||||
|
import html from "../html";
|
||||||
|
import { Posts } from "../collections/collections";
|
||||||
|
import { CollectionItem } from "sealious";
|
||||||
|
import { Readable, Transform } from "stream";
|
||||||
|
|
||||||
|
export const actionName = "ssr2";
|
||||||
|
|
||||||
|
function footer() {
|
||||||
|
return /* HTML */ `<div class="footer">Copyright 2022</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navbar() {
|
||||||
|
return /* HTML */ ` <div class="navbar">
|
||||||
|
<div class="logo">My app</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sidebar() {
|
||||||
|
return /* HTML */ `
|
||||||
|
<div class="sidebar">
|
||||||
|
<ul class="filters">
|
||||||
|
<li><input type="checkbox" /> Filtr1</li>
|
||||||
|
<li><input type="checkbox" /> Filtr2</li>
|
||||||
|
<li><input type="checkbox" /> Filtr3</li>
|
||||||
|
<li><input type="checkbox" /> Filtr4</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItem(item: CollectionItem<typeof Posts>) {
|
||||||
|
return /* HTML */ `
|
||||||
|
<li class="item">
|
||||||
|
<h3>${item.get("title")}</h3>
|
||||||
|
<p>${item.get("description")}</p>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function content() {
|
||||||
|
const { items } = await Posts.suList().fetch();
|
||||||
|
console.log("Returning content!");
|
||||||
|
return /* HTML */ `<div class="content">
|
||||||
|
<ul class="items">
|
||||||
|
${items.map((item) => renderItem(item)).join("")}
|
||||||
|
</ul>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new (class ssr1Page extends Page {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async canAccess(_: Context) {
|
||||||
|
return { canAccess: true, message: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async render(ctx: Context) {
|
||||||
|
const stream = new Readable();
|
||||||
|
stream.pause();
|
||||||
|
ctx.respond = false;
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
await sleep(1);
|
||||||
|
resolve(stream);
|
||||||
|
stream.resume();
|
||||||
|
stream.push(`<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SSR2 version</title>
|
||||||
|
<link href="/dist/style.css" rel="stylesheet" type="text/css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">`);
|
||||||
|
stream.push(navbar());
|
||||||
|
stream.push(sidebar());
|
||||||
|
stream.pause();
|
||||||
|
await sleep(1);
|
||||||
|
console.log(stream);
|
||||||
|
stream.push(await content());
|
||||||
|
stream.resume();
|
||||||
|
stream.push(footer());
|
||||||
|
stream.push(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,84 @@
|
|||||||
|
import { Context } from "koa";
|
||||||
|
import { Mountable } from "@sealcode/sealgen";
|
||||||
|
import Router from "@koa/router";
|
||||||
|
import { sleep } from "../util";
|
||||||
|
import { CollectionItem } from "sealious";
|
||||||
|
import { Posts } from "../collections/collections";
|
||||||
|
import { getMOTD } from "./motd.post";
|
||||||
|
|
||||||
|
export const actionName = "ssr2";
|
||||||
|
|
||||||
|
async function footer() {
|
||||||
|
return /* HTML */ `<div class="footer">Copyright 2022. ${await getMOTD()}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navbar() {
|
||||||
|
return /* HTML */ ` <div class="navbar">
|
||||||
|
<div class="logo">My app</div>
|
||||||
|
<div>
|
||||||
|
<a href="/react/">react</a>
|
||||||
|
<a href="/ssr1/">ssr1</a>
|
||||||
|
<a href="/ssr2/">ssr2</a>
|
||||||
|
<a href="/ssr25/">ssr2.5</a>
|
||||||
|
<a href="/ssr3/">ssr3</a>
|
||||||
|
<a href="/ssr4/">ssr4</a>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sidebar() {
|
||||||
|
return /* HTML */ `
|
||||||
|
<div class="sidebar">
|
||||||
|
<ul class="filters">
|
||||||
|
<li><input type="checkbox" /> Filtr1</li>
|
||||||
|
<li><input type="checkbox" /> Filtr2</li>
|
||||||
|
<li><input type="checkbox" /> Filtr3</li>
|
||||||
|
<li><input type="checkbox" /> Filtr4</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItem(item: CollectionItem<typeof Posts>) {
|
||||||
|
return /* HTML */ `
|
||||||
|
<li class="item">
|
||||||
|
<h3>${item.get("title")}</h3>
|
||||||
|
<p>${item.get("description")}</p>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function content() {}
|
||||||
|
|
||||||
|
export default new (class ssr2Redirect extends Mountable {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async canAccess(_: Context) {
|
||||||
|
return { canAccess: true, message: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(router: Router, path: string) {
|
||||||
|
router.get(path, async (ctx) => {
|
||||||
|
ctx.res.write(`<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SSR2 version</title>
|
||||||
|
<link href="/dist/style.css" rel="stylesheet" type="text/css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">`);
|
||||||
|
ctx.res.write(navbar());
|
||||||
|
ctx.res.write(sidebar());
|
||||||
|
ctx.res.write(`<div class="content">`);
|
||||||
|
// ctx.res.write(`<style>.loading:not(:last-child){display: none}</style>`);
|
||||||
|
ctx.res.write(`<div class="loading">Loading.....</div>`);
|
||||||
|
const { items } = await Posts.suList().fetch();
|
||||||
|
ctx.res.write(/* HTML */ ` <ul class="items">
|
||||||
|
${items.map((item) => renderItem(item)).join("")}
|
||||||
|
</ul>`);
|
||||||
|
ctx.res.write("</div>");
|
||||||
|
ctx.res.write(await footer());
|
||||||
|
ctx.res.end();
|
||||||
|
ctx.body = ctx.res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,84 @@
|
|||||||
|
import { Context } from "koa";
|
||||||
|
import { Mountable } from "@sealcode/sealgen";
|
||||||
|
import Router from "@koa/router";
|
||||||
|
|
||||||
|
import { CollectionItem } from "sealious";
|
||||||
|
import { Posts } from "../collections/collections";
|
||||||
|
import { getMOTD } from "./motd.post";
|
||||||
|
|
||||||
|
export const actionName = "ssr25";
|
||||||
|
|
||||||
|
async function footer() {
|
||||||
|
return /* HTML */ `<div class="footer">Copyright 2022. ${await getMOTD()}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navbar() {
|
||||||
|
return /* HTML */ ` <div class="navbar">
|
||||||
|
<div class="logo">My app</div>
|
||||||
|
<div>
|
||||||
|
<a href="/react/">react</a>
|
||||||
|
<a href="/ssr1/">ssr1</a>
|
||||||
|
<a href="/ssr2/">ssr2</a>
|
||||||
|
<a href="/ssr25/">ssr2.5</a>
|
||||||
|
<a href="/ssr3/">ssr3</a>
|
||||||
|
<a href="/ssr4/">ssr4</a>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sidebar() {
|
||||||
|
return /* HTML */ `
|
||||||
|
<div class="sidebar">
|
||||||
|
<ul class="filters">
|
||||||
|
<li><input type="checkbox" /> Filtr1</li>
|
||||||
|
<li><input type="checkbox" /> Filtr2</li>
|
||||||
|
<li><input type="checkbox" /> Filtr3</li>
|
||||||
|
<li><input type="checkbox" /> Filtr4</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItem(item: CollectionItem<typeof Posts>) {
|
||||||
|
return /* HTML */ `
|
||||||
|
<li class="item">
|
||||||
|
<h3>${item.get("title")}</h3>
|
||||||
|
<p>${item.get("description")}</p>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function content() {}
|
||||||
|
|
||||||
|
export default new (class ssr2Redirect extends Mountable {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async canAccess(_: Context) {
|
||||||
|
return { canAccess: true, message: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(router: Router, path: string) {
|
||||||
|
router.get(path, async (ctx) => {
|
||||||
|
ctx.res.write(`<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SSR2.5 version</title>
|
||||||
|
<link href="/dist/style.css" rel="stylesheet" type="text/css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">`);
|
||||||
|
ctx.res.write(navbar());
|
||||||
|
ctx.res.write(sidebar());
|
||||||
|
ctx.res.write(`<div class="content">`);
|
||||||
|
ctx.res.write(`<style>.loading:not(:last-child){display: none}</style>`);
|
||||||
|
ctx.res.write(`<div class="loading">Loading.....</div>`);
|
||||||
|
const { items } = await Posts.suList().fetch();
|
||||||
|
ctx.res.write(/* HTML */ ` <ul class="items">
|
||||||
|
${items.map((item) => renderItem(item)).join("")}
|
||||||
|
</ul>`);
|
||||||
|
ctx.res.write("</div>");
|
||||||
|
ctx.res.write(await footer());
|
||||||
|
ctx.res.end();
|
||||||
|
ctx.body = ctx.res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,83 @@
|
|||||||
|
import { Context } from "koa";
|
||||||
|
import { Mountable } from "@sealcode/sealgen";
|
||||||
|
import Router from "@koa/router";
|
||||||
|
import { CollectionItem } from "sealious";
|
||||||
|
import { Posts } from "../collections/collections";
|
||||||
|
import { getMOTD } from "./motd.post";
|
||||||
|
|
||||||
|
export const actionName = "ssr3";
|
||||||
|
|
||||||
|
async function footer() {
|
||||||
|
return /* HTML */ `<div class="footer">Copyright 2022. ${await getMOTD()}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navbar() {
|
||||||
|
return /* HTML */ ` <div class="navbar">
|
||||||
|
<div class="logo">My app</div>
|
||||||
|
<div>
|
||||||
|
<a href="/react/">react</a>
|
||||||
|
<a href="/ssr1/">ssr1</a>
|
||||||
|
<a href="/ssr2/">ssr2</a>
|
||||||
|
<a href="/ssr25/">ssr2.5</a>
|
||||||
|
<a href="/ssr3/">ssr3</a>
|
||||||
|
<a href="/ssr4/">ssr4</a>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sidebar() {
|
||||||
|
return /* HTML */ `
|
||||||
|
<div class="sidebar">
|
||||||
|
<ul class="filters">
|
||||||
|
<li><input type="checkbox" /> Filtr1</li>
|
||||||
|
<li><input type="checkbox" /> Filtr2</li>
|
||||||
|
<li><input type="checkbox" /> Filtr3</li>
|
||||||
|
<li><input type="checkbox" /> Filtr4</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItem(item: CollectionItem<typeof Posts>) {
|
||||||
|
return /* HTML */ `
|
||||||
|
<li class="item">
|
||||||
|
<h3>${item.get("title")}</h3>
|
||||||
|
<p>${item.get("description")}</p>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function content() {}
|
||||||
|
|
||||||
|
export default new (class ssr2Redirect extends Mountable {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async canAccess(_: Context) {
|
||||||
|
return { canAccess: true, message: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(router: Router, path: string) {
|
||||||
|
router.get(path, async (ctx) => {
|
||||||
|
ctx.res.write(`<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SSR3 version</title>
|
||||||
|
<link href="/dist/style.css" rel="stylesheet" type="text/css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">`);
|
||||||
|
ctx.res.write(navbar());
|
||||||
|
ctx.res.write(sidebar());
|
||||||
|
ctx.res.write(await footer());
|
||||||
|
ctx.res.write(`<div class="content">`);
|
||||||
|
ctx.res.write(`<style>.loading:not(:last-child){display: none}</style>`);
|
||||||
|
ctx.res.write(`<div class="loading">Loading.....</div>`);
|
||||||
|
const { items } = await Posts.suList().fetch();
|
||||||
|
ctx.res.write(`<ul class="items">
|
||||||
|
${items.map((item) => renderItem(item)).join("")}
|
||||||
|
</ul>`);
|
||||||
|
ctx.res.write("</div>");
|
||||||
|
ctx.res.end();
|
||||||
|
ctx.body = ctx.res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,90 @@
|
|||||||
|
import { Context } from "koa";
|
||||||
|
import { tempstream } from "tempstream";
|
||||||
|
import { Page } from "@sealcode/sealgen";
|
||||||
|
import html from "../html";
|
||||||
|
import { Posts } from "../collections/collections";
|
||||||
|
import { CollectionItem } from "sealious";
|
||||||
|
import { getMOTD } from "./motd.post";
|
||||||
|
|
||||||
|
export const actionName = "ssr4";
|
||||||
|
|
||||||
|
function footer(message: string | Promise<string> = getMOTD()) {
|
||||||
|
return tempstream/* HTML */ `<div class="footer">Copyright 2022. ${message}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navbar() {
|
||||||
|
return /* HTML */ ` <div class="navbar">
|
||||||
|
<div class="logo">My app</div>
|
||||||
|
<div>
|
||||||
|
<a href="/react/">react</a>
|
||||||
|
<a href="/ssr1/">ssr1</a>
|
||||||
|
<a href="/ssr2/">ssr2</a>
|
||||||
|
<a href="/ssr25/">ssr2.5</a>
|
||||||
|
<a href="/ssr3/">ssr3</a>
|
||||||
|
<a href="/ssr4/">ssr4</a>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sidebar() {
|
||||||
|
return /* HTML */ `
|
||||||
|
<div class="sidebar">
|
||||||
|
<ul class="filters">
|
||||||
|
<li><input type="checkbox" /> Filter1</li>
|
||||||
|
<li><input type="checkbox" /> Filter2</li>
|
||||||
|
<li><input type="checkbox" /> Filter3</li>
|
||||||
|
<li><input type="checkbox" /> Filter4</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItem(item: CollectionItem<typeof Posts>) {
|
||||||
|
return /* HTML */ `
|
||||||
|
<li class="item">
|
||||||
|
<h3>${item.get("title")}</h3>
|
||||||
|
<p>${item.get("description")}</p>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function content() {
|
||||||
|
const items_promise = Posts.suList().fetch();
|
||||||
|
return tempstream/* HTML */ `<div class="content">
|
||||||
|
<style>
|
||||||
|
.loading:not(:last-child) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="loading">Loading.....</div>
|
||||||
|
|
||||||
|
${items_promise.then(
|
||||||
|
({ items }) =>
|
||||||
|
tempstream/* HTML */ `<ul class="items">
|
||||||
|
${items.map((item) => renderItem(item))}
|
||||||
|
</ul>`
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new (class ssr1Page extends Page {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async canAccess(_: Context) {
|
||||||
|
return { canAccess: true, message: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async render(ctx: Context) {
|
||||||
|
return tempstream/* HTML */ `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SSR4 version</title>
|
||||||
|
<link href="/dist/style.css" rel="stylesheet" type="text/css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
${navbar()}${sidebar()}${footer("")}${content()}${footer()}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,9 @@
|
|||||||
|
export const HelloURL = "/hello/";
|
||||||
|
export const motdURL = "/motd/";
|
||||||
|
export const ListPostsURL = "/posts/";
|
||||||
|
export const ReactURL = "/react/";
|
||||||
|
export const ssr1URL = "/ssr1/";
|
||||||
|
export const ssr2URL = "/ssr2/";
|
||||||
|
export const ssr25URL = "/ssr25/";
|
||||||
|
export const ssr3URL = "/ssr3/";
|
||||||
|
export const ssr4URL = "/ssr4/";
|
@ -0,0 +1,5 @@
|
|||||||
|
describe("sample test", () => {
|
||||||
|
it("always passes", () => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,32 @@
|
|||||||
|
import { Users } from "../collections/collections";
|
||||||
|
import { CollectionItem, TestUtils } from "sealious";
|
||||||
|
import TheApp from "../app";
|
||||||
|
|
||||||
|
type Unpromisify<T> = T extends Promise<infer R> ? R : T;
|
||||||
|
|
||||||
|
export function createAUser(app: TheApp, username: string) {
|
||||||
|
return app.collections.users.suCreate({
|
||||||
|
username,
|
||||||
|
email: `${username}@example.com`,
|
||||||
|
password: "password",
|
||||||
|
roles: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAdmin(
|
||||||
|
app: TheApp,
|
||||||
|
rest_api: TestUtils.MockRestApi
|
||||||
|
): Promise<
|
||||||
|
[CollectionItem<typeof Users>, Unpromisify<ReturnType<typeof rest_api.login>>]
|
||||||
|
> {
|
||||||
|
const user = await createAUser(app, "super_user");
|
||||||
|
await app.collections["user-roles"].suCreate({
|
||||||
|
user: user.id,
|
||||||
|
role: "admin",
|
||||||
|
});
|
||||||
|
const session = await rest_api.login({
|
||||||
|
username: "super_user",
|
||||||
|
password: "password",
|
||||||
|
});
|
||||||
|
return [user, session];
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
import _locreq from "locreq";
|
||||||
|
const locreq = _locreq(__dirname);
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
import { hasShape, is, predicates } from "@sealcode/ts-predicates";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
|
||||||
|
export const LONG_TEST_TIMEOUT = 30 * 1000;
|
||||||
|
|
||||||
|
export async function webhintURL(url: string, config = locreq.resolve(".hintrc")) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("scanning with webhint....", url);
|
||||||
|
try {
|
||||||
|
const subprocess = spawn(
|
||||||
|
"node",
|
||||||
|
[locreq.resolve("node_modules/.bin/hint"), "--config", config, url],
|
||||||
|
{
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
subprocess.on("close", (code) =>
|
||||||
|
code === 0 ? resolve() : reject(new Error("Webhint tests failed"))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (is(e, predicates.object) && hasShape({ stdout: predicates.string }, e)) {
|
||||||
|
throw new Error(e.stdout);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function webhintHTML(html: string) {
|
||||||
|
await fs.writeFile("/tmp/index.html", html);
|
||||||
|
await webhintURL("/tmp/index.html", locreq.resolve(".hintrc.local.json"));
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
import TheApp from "../app";
|
||||||
|
import { mainRouter } from "../routes";
|
||||||
|
import _locreq from "locreq";
|
||||||
|
|
||||||
|
const locreq = _locreq(__dirname);
|
||||||
|
import Sealious, { SMTPMailer } from "sealious";
|
||||||
|
import { TestUtils } from "sealious";
|
||||||
|
|
||||||
|
export async function withProdApp(
|
||||||
|
callback: (args: {
|
||||||
|
app: TheApp;
|
||||||
|
base_url: string;
|
||||||
|
rest_api: TestUtils.MockRestApi;
|
||||||
|
mail_api: TestUtils.MailcatcherAPI;
|
||||||
|
}) => Promise<void>
|
||||||
|
) {
|
||||||
|
const app = new TheApp();
|
||||||
|
const port = 9999;
|
||||||
|
|
||||||
|
app.config["www-server"].port = port;
|
||||||
|
app.config.datastore_mongo = {
|
||||||
|
host: "db",
|
||||||
|
port: 27017,
|
||||||
|
db_name: "sealious-app-test",
|
||||||
|
};
|
||||||
|
app.config.logger.level = <const>"none";
|
||||||
|
app.mailer = new SMTPMailer({
|
||||||
|
host: "mailcatcher",
|
||||||
|
port: 1025,
|
||||||
|
user: "any",
|
||||||
|
password: "any",
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.start();
|
||||||
|
mainRouter(app.HTTPServer.router);
|
||||||
|
|
||||||
|
app.HTTPServer.addStaticRoute("/", locreq.resolve("public"));
|
||||||
|
|
||||||
|
const base_url = `http://127.0.0.1:${port}`;
|
||||||
|
const mail_api = new TestUtils.MailcatcherAPI("http://mailcatcher:1080", app);
|
||||||
|
await mail_api.deleteAllInstanceEmails();
|
||||||
|
|
||||||
|
async function stop() {
|
||||||
|
await app.removeAllData();
|
||||||
|
await app.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await callback({
|
||||||
|
app,
|
||||||
|
base_url,
|
||||||
|
rest_api: new TestUtils.MockRestApi(base_url),
|
||||||
|
mail_api,
|
||||||
|
});
|
||||||
|
await stop();
|
||||||
|
} catch (e) {
|
||||||
|
await stop();
|
||||||
|
console.error(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"target": "ES2019",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["es6", "esnext"],
|
||||||
|
"outDir": "../../dist/back",
|
||||||
|
"keyofStringsOnly": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["./**/*", "./*"]
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { BaseContext } from "koa";
|
||||||
|
import qs from "qs";
|
||||||
|
|
||||||
|
export async function sleep(time: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, time));
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Awaited<T> = T extends Promise<infer U> ? U : T;
|
||||||
|
export type UnwrapArray<T> = T extends Array<infer U> ? U : T;
|
||||||
|
|
||||||
|
export function* naturalNumbers(min: number, max: number) {
|
||||||
|
for (let i = min; i <= max; i++) {
|
||||||
|
yield i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UrlWithNewParams(
|
||||||
|
ctx: BaseContext,
|
||||||
|
query_params: Record<string, unknown>
|
||||||
|
): string {
|
||||||
|
return `${ctx.path}?${qs.stringify(query_params)}`;
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import * as Turbo from "@hotwired/turbo";
|
||||||
|
import { Application } from "stimulus";
|
||||||
|
|
||||||
|
export { Turbo };
|
||||||
|
|
||||||
|
const application = Application.start();
|
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"target": "ES6",
|
||||||
|
"lib": ["dom"]
|
||||||
|
},
|
||||||
|
"include": ["./**/*", "./index.ts"]
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
// DO NOT EDIT! This file is generated automaticaly with npx sealgen generate-scss-includes
|
||||||
|
|
||||||
|
@import "../node_modules/@sealcode/sealgen/src/forms/forms.scss";
|
||||||
|
@import "back/routes/common/ui/input.scss";
|
@ -0,0 +1,78 @@
|
|||||||
|
html {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
max-width: 1024px;
|
||||||
|
margin: 1rem auto;
|
||||||
|
background: white;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
height: 1rem;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@import "includes.scss";
|
||||||
|
|
||||||
|
#app {
|
||||||
|
grid-template-areas:
|
||||||
|
"navbar navbar"
|
||||||
|
"sidebar content"
|
||||||
|
"footer footer";
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 200px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
grid-area: footer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
grid-area: navbar;
|
||||||
|
.logo {
|
||||||
|
font-size: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
grid-area: sidebar;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes enter {
|
||||||
|
from {
|
||||||
|
opacity: 0%;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 20px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(161px, 241px));
|
||||||
|
|
||||||
|
li {
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 1px 1px 5px 0px #00000059;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
// animation: enter 200ms;
|
||||||
|
// animation-fill-mode: both;
|
||||||
|
// @for $i from 1 through 20 {
|
||||||
|
// &:nth-child(#{$i}) {
|
||||||
|
// animation-delay: #{$i * 50}ms;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
name: "front-end-components",
|
||||||
|
entry: {
|
||||||
|
bundle: "./src/front/index.ts",
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
filename: "[name].js",
|
||||||
|
path: path.resolve(__dirname, "public/dist"),
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: "production",
|
||||||
|
devtool: "source-map",
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
extensions: [".ts", ".js"],
|
||||||
|
},
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: [/node_modules/],
|
||||||
|
use: [{ loader: "babel-loader" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
exclude: [/node_modules/],
|
||||||
|
use: [{ loader: "ts-loader" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
Loading…
Reference in New Issue