Merge branch 'develop'
This commit is contained in:
commit
8e134fd353
135
.eslintrc.cjs
Normal file
135
.eslintrc.cjs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: [
|
||||||
|
'svelte3',
|
||||||
|
'@typescript-eslint',
|
||||||
|
'import',
|
||||||
|
'simple-import-sort',
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: [
|
||||||
|
'./tsconfig.test.json',
|
||||||
|
'./src/tsconfig.json',
|
||||||
|
'./src/common/tsconfig.json',
|
||||||
|
'./src/assets/ts/tsconfig.json',
|
||||||
|
'./src/assets/views/tsconfig.json',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
indent: [
|
||||||
|
'error',
|
||||||
|
4,
|
||||||
|
{
|
||||||
|
SwitchCase: 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'no-trailing-spaces': 'error',
|
||||||
|
'max-len': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
code: 120,
|
||||||
|
ignoreTemplateLiterals: true,
|
||||||
|
ignoreRegExpLiterals: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
semi: 'off',
|
||||||
|
'@typescript-eslint/semi': [
|
||||||
|
'error'
|
||||||
|
],
|
||||||
|
'no-extra-semi': 'error',
|
||||||
|
'eol-last': 'error',
|
||||||
|
'comma-dangle': 'off',
|
||||||
|
'simple-import-sort/imports': 'error',
|
||||||
|
'no-extra-parens': 'off',
|
||||||
|
'no-nested-ternary': 'error',
|
||||||
|
'no-return-await': 'off',
|
||||||
|
'no-useless-return': 'error',
|
||||||
|
'no-useless-constructor': 'off',
|
||||||
|
'import/extensions': ['error', 'ignorePackages'],
|
||||||
|
'@typescript-eslint/comma-dangle': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
arrays: 'always-multiline',
|
||||||
|
objects: 'always-multiline',
|
||||||
|
imports: 'always-multiline',
|
||||||
|
exports: 'always-multiline',
|
||||||
|
functions: 'always-multiline',
|
||||||
|
enums: 'always-multiline',
|
||||||
|
generics: 'always-multiline',
|
||||||
|
tuples: 'always-multiline'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-extra-parens': [
|
||||||
|
'error'
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-inferrable-types': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'error',
|
||||||
|
'@typescript-eslint/no-unnecessary-condition': 'error',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'error',
|
||||||
|
'@typescript-eslint/no-useless-constructor': [
|
||||||
|
'error'
|
||||||
|
],
|
||||||
|
'@typescript-eslint/return-await': [
|
||||||
|
'error',
|
||||||
|
'always'
|
||||||
|
],
|
||||||
|
'@typescript-eslint/explicit-member-accessibility': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
accessibility: 'explicit'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
|
},
|
||||||
|
ignorePatterns: [
|
||||||
|
'.eslintrc.js',
|
||||||
|
'rollup.config.js',
|
||||||
|
'jest.config.js',
|
||||||
|
'dist/**/*',
|
||||||
|
'config/**/*',
|
||||||
|
'intermediates/**/*',
|
||||||
|
'public/**/*',
|
||||||
|
'scripts/**/*',
|
||||||
|
'src/frontend/register_svelte/register_svelte.js',
|
||||||
|
],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'test/**/*'
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'max-len': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
code: 120,
|
||||||
|
ignoreTemplateLiterals: true,
|
||||||
|
ignoreRegExpLiterals: true,
|
||||||
|
ignoreStrings: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['*.svelte'],
|
||||||
|
processor: 'svelte3/svelte3'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
'svelte3/typescript': require('typescript'),
|
||||||
|
'svelte3/ignore-styles': function (attributes) {
|
||||||
|
return !!(attributes['lang'] && attributes['lang'] !== 'css');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
111
.eslintrc.json
111
.eslintrc.json
@ -1,111 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"plugins": [
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"project": [
|
|
||||||
"./tsconfig.json",
|
|
||||||
"./tsconfig.test.json"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"indent": [
|
|
||||||
"error",
|
|
||||||
4,
|
|
||||||
{
|
|
||||||
"SwitchCase": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-trailing-spaces": "error",
|
|
||||||
"max-len": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"code": 120,
|
|
||||||
"ignoreTemplateLiterals": true,
|
|
||||||
"ignoreRegExpLiterals": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"semi": "off",
|
|
||||||
"@typescript-eslint/semi": [
|
|
||||||
"error"
|
|
||||||
],
|
|
||||||
"no-extra-semi": "error",
|
|
||||||
"eol-last": "error",
|
|
||||||
"comma-dangle": "off",
|
|
||||||
"@typescript-eslint/comma-dangle": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"arrays": "always-multiline",
|
|
||||||
"objects": "always-multiline",
|
|
||||||
"imports": "always-multiline",
|
|
||||||
"exports": "always-multiline",
|
|
||||||
"functions": "always-multiline",
|
|
||||||
"enums": "always-multiline",
|
|
||||||
"generics": "always-multiline",
|
|
||||||
"tuples": "always-multiline"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-extra-parens": "off",
|
|
||||||
"@typescript-eslint/no-extra-parens": [
|
|
||||||
"error"
|
|
||||||
],
|
|
||||||
"no-nested-ternary": "error",
|
|
||||||
"@typescript-eslint/no-inferrable-types": "off",
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "error",
|
|
||||||
"@typescript-eslint/no-unnecessary-condition": "error",
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"argsIgnorePattern": "^_"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "error",
|
|
||||||
"no-useless-return": "error",
|
|
||||||
"no-useless-constructor": "off",
|
|
||||||
"@typescript-eslint/no-useless-constructor": [
|
|
||||||
"error"
|
|
||||||
],
|
|
||||||
"no-return-await": "off",
|
|
||||||
"@typescript-eslint/return-await": [
|
|
||||||
"error",
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"@typescript-eslint/explicit-member-accessibility": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"accessibility": "explicit"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-floating-promises": "error"
|
|
||||||
},
|
|
||||||
"ignorePatterns": [
|
|
||||||
"jest.config.js",
|
|
||||||
"scripts/**/*",
|
|
||||||
"dist/**/*",
|
|
||||||
"config/**/*"
|
|
||||||
],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
"test/**/*"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"max-len": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"code": 120,
|
|
||||||
"ignoreTemplateLiterals": true,
|
|
||||||
"ignoreRegExpLiterals": true,
|
|
||||||
"ignoreStrings": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,7 +1,10 @@
|
|||||||
.idea
|
.idea
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
intermediates
|
||||||
|
public
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
||||||
src/package.json
|
|
||||||
config/local.*
|
config/local.*
|
||||||
|
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
@ -1,19 +1,39 @@
|
|||||||
{
|
{
|
||||||
|
asset_cache: false,
|
||||||
|
gitlab_webhook_token: 'default',
|
||||||
app: {
|
app: {
|
||||||
|
listen_addr: '127.0.0.1',
|
||||||
|
port: 4899,
|
||||||
|
public_url: "http://localhost:4899",
|
||||||
|
public_websocket_url: "ws://localhost:4899",
|
||||||
name: 'Example App',
|
name: 'Example App',
|
||||||
contact_email: 'contact@example.net',
|
contact_email: 'contact@example.net',
|
||||||
display_email_warning: true,
|
display_email_warning: true,
|
||||||
},
|
},
|
||||||
|
auth: {
|
||||||
|
// Registered accounts need to be approved by an administrator
|
||||||
|
approval_mode: false,
|
||||||
|
// 30 days
|
||||||
|
name_change_wait_period: 2592000000,
|
||||||
|
},
|
||||||
log: {
|
log: {
|
||||||
level: "DEBUG",
|
level: "DEBUG",
|
||||||
verbose: true,
|
verbose: true,
|
||||||
db_level: "ERROR",
|
db_level: "ERROR",
|
||||||
},
|
},
|
||||||
public_url: "http://localhost:4899",
|
magic_link: {
|
||||||
public_websocket_url: "ws://localhost:4899",
|
validity_period: 20,
|
||||||
listen_addr: '127.0.0.1',
|
},
|
||||||
port: 4899,
|
mail: {
|
||||||
gitlab_webhook_token: 'default',
|
host: "127.0.0.1",
|
||||||
|
port: "1025",
|
||||||
|
secure: false,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
allow_invalid_tls: true,
|
||||||
|
from: 'contact@example.net',
|
||||||
|
from_name: 'Example App',
|
||||||
|
},
|
||||||
mysql: {
|
mysql: {
|
||||||
connectionLimit: 10,
|
connectionLimit: 10,
|
||||||
host: "localhost",
|
host: "localhost",
|
||||||
@ -31,27 +51,12 @@
|
|||||||
secret: 'default',
|
secret: 'default',
|
||||||
cookie: {
|
cookie: {
|
||||||
secure: false,
|
secure: false,
|
||||||
maxAge: 31557600000, // 1 year
|
// 1 year
|
||||||
|
maxAge: 31557600000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mail: {
|
|
||||||
host: "127.0.0.1",
|
|
||||||
port: "1025",
|
|
||||||
secure: false,
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
allow_invalid_tls: true,
|
|
||||||
from: 'contact@example.net',
|
|
||||||
from_name: 'Example App',
|
|
||||||
},
|
|
||||||
view: {
|
view: {
|
||||||
cache: false,
|
cache: false,
|
||||||
},
|
dev: true,
|
||||||
magic_link: {
|
|
||||||
validity_period: 20,
|
|
||||||
},
|
|
||||||
auth: {
|
|
||||||
approval_mode: false, // Registered accounts need to be approved by an administrator
|
|
||||||
name_change_wait_period: 2592000000, // 30 days
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,28 @@
|
|||||||
{
|
{
|
||||||
|
asset_cache: true,
|
||||||
|
app: {
|
||||||
|
public_url: "https://swaf.example",
|
||||||
|
public_websocket_url: "wss://swaf.example",
|
||||||
|
},
|
||||||
log: {
|
log: {
|
||||||
level: "DEV",
|
level: "DEV",
|
||||||
verbose: false,
|
verbose: false,
|
||||||
db_level: "ERROR",
|
db_level: "ERROR",
|
||||||
},
|
},
|
||||||
public_url: "https://swaf.example",
|
magic_link: {
|
||||||
public_websocket_url: "wss://swaf.example",
|
validity_period: 900,
|
||||||
session: {
|
|
||||||
cookie: {
|
|
||||||
secure: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
mail: {
|
mail: {
|
||||||
secure: true,
|
secure: true,
|
||||||
allow_invalid_tls: false,
|
allow_invalid_tls: false,
|
||||||
},
|
},
|
||||||
magic_link: {
|
session: {
|
||||||
validity_period: 900,
|
cookie: {
|
||||||
|
secure: true,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
cache: true,
|
||||||
|
dev: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
auth: {
|
||||||
|
approval_mode: true,
|
||||||
|
},
|
||||||
mysql: {
|
mysql: {
|
||||||
host: "localhost",
|
host: "localhost",
|
||||||
user: "root",
|
user: "root",
|
||||||
@ -12,7 +15,4 @@
|
|||||||
maxAge: 1000,
|
maxAge: 1000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
|
||||||
approval_mode: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
module.exports = {
|
export default {
|
||||||
globals: {
|
globals: {
|
||||||
'ts-jest': {
|
'ts-jest': {
|
||||||
tsconfig: 'tsconfig.test.json',
|
tsconfig: 'tsconfig.test.json',
|
||||||
@ -17,4 +17,5 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
testTimeout: 60000,
|
testTimeout: 60000,
|
||||||
|
resolver: "jest-ts-webcompat-resolver",
|
||||||
};
|
};
|
||||||
|
69
package.json
69
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "swaf",
|
"name": "swaf",
|
||||||
"version": "0.23.10",
|
"version": "0.24.0",
|
||||||
"description": "Structure Web Application Framework.",
|
"description": "Structure Web Application Framework.",
|
||||||
"repository": "https://eternae.ink/ashpie/swaf",
|
"repository": "https://eternae.ink/ashpie/swaf",
|
||||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
"author": "Alice Gaudon <alice@gaudon.pro>",
|
||||||
@ -11,53 +11,80 @@
|
|||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
|
"type": "module",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest --verbose --runInBand",
|
"test": "jest --verbose --runInBand",
|
||||||
"clean": "node scripts/clean.js",
|
"clean": "node scripts/clean.js",
|
||||||
"prepare-sources": "node scripts/prepare-sources.js",
|
"prepare-sources": "node scripts/prepare-sources.js",
|
||||||
"compile": "yarn clean && tsc",
|
"compile": "yarn clean && yarn prepare-sources && tsc --build",
|
||||||
"build": "yarn prepare-sources && yarn compile && node scripts/dist.js",
|
"build": "yarn compile && node . pre-compile-views && node scripts/dist.js",
|
||||||
"dev": "yarn prepare-sources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"maildev\"",
|
"build-production": "NODE_ENV=production yarn build",
|
||||||
|
"dev": "yarn compile && concurrently -k -n \"Maildev,Typescript,ViewPreCompile,Node\" -p \"[{name}]\" -c \"yellow,blue,red,green\" \"maildev\" \"tsc --build --watch --preserveWatchOutput\" \"nodemon -i public -i intermediates -- pre-compile-views --watch\" \"nodemon -i public -i intermediates\"",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"release": "yarn build && yarn lint && yarn test && cd dist && yarn publish"
|
"release": "yarn build && yarn lint && yarn test && cd dist && yarn publish"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-commonjs": "^21.0.1",
|
||||||
|
"@rollup/plugin-node-resolve": "^13.0.6",
|
||||||
|
"@sveltejs/eslint-config": "sveltejs/eslint-config",
|
||||||
|
"@tsconfig/svelte": "^2.0.1",
|
||||||
"@types/compression": "^1.7.0",
|
"@types/compression": "^1.7.0",
|
||||||
"@types/config": "^0.0.38",
|
"@types/config": "^0.0.40",
|
||||||
"@types/connect-flash": "^0.0.36",
|
"@types/connect-flash": "^0.0.37",
|
||||||
"@types/cookie": "^0.4.0",
|
"@types/cookie": "^0.4.0",
|
||||||
"@types/cookie-parser": "^1.4.2",
|
"@types/cookie-parser": "^1.4.2",
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.6",
|
||||||
"@types/express-session": "^1.17.0",
|
"@types/express-session": "^1.17.0",
|
||||||
"@types/formidable": "^1.0.31",
|
"@types/feather-icons": "^4.7.0",
|
||||||
|
"@types/formidable": "^2.0.0",
|
||||||
"@types/geoip-lite": "^1.1.31",
|
"@types/geoip-lite": "^1.1.31",
|
||||||
"@types/jest": "^26.0.4",
|
"@types/jest": "^27.0.2",
|
||||||
"@types/mjml": "^4.0.4",
|
"@types/mjml": "^4.0.4",
|
||||||
"@types/mysql": "^2.15.10",
|
"@types/mysql": "^2.15.10",
|
||||||
"@types/nanoid": "^2.1.0",
|
|
||||||
"@types/node-fetch": "^2.5.7",
|
|
||||||
"@types/nodemailer": "^6.4.0",
|
"@types/nodemailer": "^6.4.0",
|
||||||
"@types/nunjucks": "^3.1.3",
|
"@types/nunjucks": "^3.1.3",
|
||||||
"@types/on-finished": "^2.3.1",
|
"@types/on-finished": "^2.3.1",
|
||||||
"@types/redis": "^2.8.18",
|
"@types/redis": "^2.8.18",
|
||||||
|
"@types/require-from-string": "^1.2.0",
|
||||||
"@types/supertest": "^2.0.10",
|
"@types/supertest": "^2.0.10",
|
||||||
"@types/uuid": "^8.0.0",
|
"@types/uuid": "^8.0.0",
|
||||||
"@types/ws": "^7.2.4",
|
"@types/ws": "^8.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.2.0",
|
"@typescript-eslint/eslint-plugin": "^5.3.0",
|
||||||
"@typescript-eslint/parser": "^4.2.0",
|
"@typescript-eslint/parser": "^5.3.0",
|
||||||
|
"chokidar": "^3.5.1",
|
||||||
|
"clear-module": "^4.1.1",
|
||||||
"concurrently": "^6.0.0",
|
"concurrently": "^6.0.0",
|
||||||
"eslint": "^7.9.0",
|
"eslint": "^8.2.0",
|
||||||
"jest": "^26.1.0",
|
"eslint-plugin-import": "^2.22.1",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||||
|
"eslint-plugin-svelte3": "^3.1.2",
|
||||||
|
"feather-icons": "^4.28.0",
|
||||||
|
"jest": "^27.3.1",
|
||||||
|
"jest-resolve": "^27.3.1",
|
||||||
|
"jest-ts-webcompat-resolver": "^1.0.0",
|
||||||
"maildev": "^1.1.0",
|
"maildev": "^1.1.0",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^3.0.0",
|
||||||
"nodemon": "^2.0.6",
|
"nodemon": "^2.0.6",
|
||||||
|
"normalize.css": "^8.0.1",
|
||||||
|
"require-from-string": "^2.0.2",
|
||||||
|
"rollup": "^2.42.3",
|
||||||
|
"rollup-plugin-css-only": "^3.1.0",
|
||||||
|
"rollup-plugin-imagemin": "^0.4.1",
|
||||||
|
"rollup-plugin-livereload": "^2.0.0",
|
||||||
|
"rollup-plugin-svelte": "^7.1.0",
|
||||||
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
|
"sass": "^1.32.12",
|
||||||
"supertest": "^6.0.0",
|
"supertest": "^6.0.0",
|
||||||
"ts-jest": "^26.1.1",
|
"svelte": "^3.35.0",
|
||||||
|
"svelte-check": "^2.2.8",
|
||||||
|
"svelte-preprocess": "4.6.9",
|
||||||
|
"ts-jest": "^27.0.7",
|
||||||
"typescript": "^4.0.2"
|
"typescript": "^4.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argon2": "^0.27.0",
|
"argon2": "^0.28.2",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"config": "^3.3.1",
|
"config": "^3.3.1",
|
||||||
"connect-flash": "^0.1.1",
|
"connect-flash": "^0.1.1",
|
||||||
@ -65,7 +92,7 @@
|
|||||||
"cookie-parser": "^1.4.5",
|
"cookie-parser": "^1.4.5",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-session": "^1.17.1",
|
"express-session": "^1.17.1",
|
||||||
"formidable": "^1.2.2",
|
"formidable": "^2.0.1",
|
||||||
"geoip-lite": "^1.4.2",
|
"geoip-lite": "^1.4.2",
|
||||||
"mjml": "^4.6.2",
|
"mjml": "^4.6.2",
|
||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
@ -74,9 +101,9 @@
|
|||||||
"nunjucks": "^3.2.1",
|
"nunjucks": "^3.2.1",
|
||||||
"on-finished": "^2.3.0",
|
"on-finished": "^2.3.0",
|
||||||
"redis": "^3.0.2",
|
"redis": "^3.0.2",
|
||||||
"ts-node": "^9.0.0",
|
"ts-node": "^10.4.0",
|
||||||
"tslog": "^3.0.1",
|
"tslog": "^3.0.1",
|
||||||
"uuid": "^8.0.0",
|
"uuid": "^8.0.0",
|
||||||
"ws": "^7.2.3"
|
"ws": "^8.2.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
64
rollup.config.js
Normal file
64
rollup.config.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import path from "path";
|
||||||
|
import svelte from "rollup-plugin-svelte";
|
||||||
|
import cssOnlyRollupPlugin from "rollup-plugin-css-only";
|
||||||
|
import resolve from "@rollup/plugin-node-resolve";
|
||||||
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
|
import {terser} from "rollup-plugin-terser";
|
||||||
|
import livereloadRollupPlugin from "rollup-plugin-livereload";
|
||||||
|
import imageminPlugin from "rollup-plugin-imagemin";
|
||||||
|
|
||||||
|
const production = process.env.ENV === 'production';
|
||||||
|
const buildDir = process.env.BUILD_DIR;
|
||||||
|
const publicDir = process.env.PUBLIC_DIR;
|
||||||
|
const input = process.env.INPUT.split(':');
|
||||||
|
|
||||||
|
export default commandLineArgs => ({
|
||||||
|
input: input,
|
||||||
|
output: {
|
||||||
|
format: 'es',
|
||||||
|
sourcemap: true,
|
||||||
|
dir: path.join(publicDir, 'js'),
|
||||||
|
entryFileNames: (chunkInfo) => {
|
||||||
|
const name = chunkInfo.facadeModuleId ?
|
||||||
|
path.relative(buildDir, chunkInfo.facadeModuleId) :
|
||||||
|
chunkInfo.name;
|
||||||
|
return name + '.js';
|
||||||
|
},
|
||||||
|
chunkFileNames: '[name].js',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
imageminPlugin({
|
||||||
|
fileName: '../img/[name][extname]'
|
||||||
|
}),
|
||||||
|
|
||||||
|
svelte({
|
||||||
|
compilerOptions: {
|
||||||
|
dev: !production,
|
||||||
|
hydratable: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Extract css into separate files
|
||||||
|
cssOnlyRollupPlugin({output: 'bundle.css'}),
|
||||||
|
|
||||||
|
// If you have external dependencies installed from
|
||||||
|
// npm, you'll most likely need these plugins. In
|
||||||
|
// some cases you'll need additional configuration -
|
||||||
|
// consult the documentation for details:
|
||||||
|
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||||
|
resolve({
|
||||||
|
browser: true,
|
||||||
|
dedupe: ['svelte'],
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
|
||||||
|
// Live reload in dev
|
||||||
|
!production && !!commandLineArgs.watch && livereloadRollupPlugin(publicDir),
|
||||||
|
|
||||||
|
// Minify in production
|
||||||
|
production && terser(),
|
||||||
|
],
|
||||||
|
watch: {
|
||||||
|
clearScreen: false,
|
||||||
|
},
|
||||||
|
});
|
18
scripts/_functions.js
Normal file
18
scripts/_functions.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export function copyRecursively(file, destination) {
|
||||||
|
const target = path.join(destination, path.basename(file));
|
||||||
|
if (fs.statSync(file).isDirectory()) {
|
||||||
|
console.log('mkdir', target);
|
||||||
|
|
||||||
|
fs.mkdirSync(target, {recursive: true});
|
||||||
|
fs.readdirSync(file).forEach(f => {
|
||||||
|
copyRecursively(path.join(file, f), target);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('> cp ', target);
|
||||||
|
|
||||||
|
fs.copyFileSync(file, target);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
const fs = require('fs');
|
import fs from "fs";
|
||||||
|
|
||||||
[
|
[
|
||||||
|
'intermediates',
|
||||||
'dist',
|
'dist',
|
||||||
|
'public',
|
||||||
].forEach(file => {
|
].forEach(file => {
|
||||||
if (fs.existsSync(file)) {
|
if (fs.existsSync(file)) {
|
||||||
console.log('Cleaning', file, '...');
|
console.log('Cleaning', file, '...');
|
||||||
|
@ -1,28 +1,12 @@
|
|||||||
const fs = require('fs');
|
import fs from "fs";
|
||||||
const path = require('path');
|
import path from "path";
|
||||||
|
import {copyRecursively} from "./_functions.js";
|
||||||
function copyRecursively(file, destination) {
|
|
||||||
const target = path.join(destination, path.basename(file));
|
|
||||||
if (fs.statSync(file).isDirectory()) {
|
|
||||||
console.log('mkdir', target);
|
|
||||||
|
|
||||||
fs.mkdirSync(target, {recursive: true});
|
|
||||||
fs.readdirSync(file).forEach(f => {
|
|
||||||
copyRecursively(path.join(file, f), target);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('> cp ', target);
|
|
||||||
|
|
||||||
fs.copyFileSync(file, target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[
|
[
|
||||||
'yarn.lock',
|
'yarn.lock',
|
||||||
'README.md',
|
'README.md',
|
||||||
'config/',
|
'config/',
|
||||||
'views/',
|
|
||||||
].forEach(file => {
|
].forEach(file => {
|
||||||
copyRecursively(file, 'dist');
|
copyRecursively(file, 'dist');
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,24 @@
|
|||||||
const fs = require('fs');
|
import fs from "fs";
|
||||||
const path = require('path');
|
import path from "path";
|
||||||
|
import {copyRecursively} from "./_functions.js";
|
||||||
|
|
||||||
fs.copyFileSync('package.json', path.join('src', 'package.json'));
|
// These folders must exist for nodemon not to loop indefinitely.
|
||||||
|
[
|
||||||
|
'public',
|
||||||
|
'dist',
|
||||||
|
'intermediates',
|
||||||
|
'intermediates/assets',
|
||||||
|
].forEach(dir => {
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Symlink to build/common
|
||||||
|
const symlink = path.resolve('intermediates/common');
|
||||||
|
if (!fs.existsSync(symlink)) {
|
||||||
|
fs.symlinkSync(path.resolve('dist/common'), symlink);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all source files
|
||||||
|
fs.readdirSync('src').forEach(file => {
|
||||||
|
copyRecursively(path.join('src', file), 'dist');
|
||||||
|
});
|
||||||
|
@ -1,23 +1,26 @@
|
|||||||
import express, {NextFunction, Request, Response, Router} from 'express';
|
|
||||||
import {BadRequestError, HttpError, NotFoundHttpError, ServerError, ServiceUnavailableHttpError} from "./HttpError";
|
|
||||||
import {lib} from "nunjucks";
|
|
||||||
import WebSocketListener from "./WebSocketListener";
|
|
||||||
import ApplicationComponent from "./ApplicationComponent";
|
|
||||||
import Controller from "./Controller";
|
|
||||||
import MysqlConnectionManager from "./db/MysqlConnectionManager";
|
|
||||||
import Migration, {MigrationType} from "./db/Migration";
|
|
||||||
import {Type} from "./Utils";
|
|
||||||
import LogRequestsComponent from "./components/LogRequestsComponent";
|
|
||||||
import {ValidationBag, ValidationError} from "./db/Validator";
|
|
||||||
import config from "config";
|
import config from "config";
|
||||||
|
import express, {NextFunction, Request, Response, Router} from 'express';
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import SecurityError from "./SecurityError";
|
import nunjucks from "nunjucks";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import CacheProvider from "./CacheProvider";
|
|
||||||
import RedisComponent from "./components/RedisComponent";
|
import ApplicationComponent from "./ApplicationComponent.js";
|
||||||
import Extendable from "./Extendable";
|
import CacheProvider from "./CacheProvider.js";
|
||||||
import {logger, loggingContextMiddleware} from "./Logger";
|
import {route, setPublicUrl} from "./common/Routing.js";
|
||||||
import TemplateError = lib.TemplateError;
|
import FrontendToolsComponent from "./components/FrontendToolsComponent.js";
|
||||||
|
import LogRequestsComponent from "./components/LogRequestsComponent.js";
|
||||||
|
import RedisComponent from "./components/RedisComponent.js";
|
||||||
|
import Controller from "./Controller.js";
|
||||||
|
import Migration, {MigrationType} from "./db/Migration.js";
|
||||||
|
import MysqlConnectionManager from "./db/MysqlConnectionManager.js";
|
||||||
|
import {ValidationBag, ValidationError} from "./db/Validator.js";
|
||||||
|
import Extendable, {MissingComponentError} from "./Extendable.js";
|
||||||
|
import {BadRequestError, HttpError, NotFoundHttpError, ServerError, ServiceUnavailableHttpError} from "./HttpError.js";
|
||||||
|
import {logger, loggingContextMiddleware} from "./Logger.js";
|
||||||
|
import SecurityError from "./SecurityError.js";
|
||||||
|
import {doesFileExist, Type} from "./Utils.js";
|
||||||
|
import WebSocketListener from "./WebSocketListener.js";
|
||||||
|
import TemplateError = nunjucks.lib.TemplateError;
|
||||||
|
|
||||||
export default abstract class Application implements Extendable<ApplicationComponent | WebSocketListener<Application>> {
|
export default abstract class Application implements Extendable<ApplicationComponent | WebSocketListener<Application>> {
|
||||||
private readonly version: string;
|
private readonly version: string;
|
||||||
@ -35,6 +38,8 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
protected constructor(version: string, ignoreCommandLine: boolean = false) {
|
protected constructor(version: string, ignoreCommandLine: boolean = false) {
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.ignoreCommandLine = ignoreCommandLine;
|
this.ignoreCommandLine = ignoreCommandLine;
|
||||||
|
|
||||||
|
setPublicUrl(config.get<string>('app.public_url'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract getMigrations(): MigrationType<Migration>[];
|
protected abstract getMigrations(): MigrationType<Migration>[];
|
||||||
@ -66,9 +71,9 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
this.busy = true;
|
this.busy = true;
|
||||||
|
|
||||||
// Load core version
|
// Load core version
|
||||||
const file = this.isInNodeModules() ?
|
const file = await this.isInNodeModules() ?
|
||||||
path.join(__dirname, '../../package.json') :
|
'node_modules/swaf/package.json' :
|
||||||
path.join(__dirname, '../package.json');
|
'package.json';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.coreVersion = JSON.parse(fs.readFileSync(file).toString()).version;
|
this.coreVersion = JSON.parse(fs.readFileSync(file).toString()).version;
|
||||||
@ -92,15 +97,28 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
// Register migrations
|
// Register migrations
|
||||||
MysqlConnectionManager.registerMigrations(this.getMigrations());
|
MysqlConnectionManager.registerMigrations(this.getMigrations());
|
||||||
|
|
||||||
// Register all components and alike
|
// Register and initialize all components and alike
|
||||||
await this.init();
|
await this.init();
|
||||||
|
for (const component of this.components) {
|
||||||
|
await component.init?.();
|
||||||
|
}
|
||||||
|
|
||||||
// Process command line
|
// Process command line
|
||||||
if (!this.ignoreCommandLine && await this.processCommandLine()) {
|
if (!this.ignoreCommandLine) {
|
||||||
|
let result: boolean;
|
||||||
|
try {
|
||||||
|
result = await this.processCommandLine();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
this.started = true;
|
this.started = true;
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Security
|
// Security
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
@ -148,7 +166,7 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
},
|
},
|
||||||
html: () => {
|
html: () => {
|
||||||
req.flash('validation', bag.getMessages());
|
req.flash('validation', bag.getMessages());
|
||||||
res.redirect(req.getPreviousUrl() || Controller.route('home'));
|
res.redirect(req.getPreviousUrl() || route('home'));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -170,11 +188,18 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
res.status(httpError.errorCode);
|
res.status(httpError.errorCode);
|
||||||
res.format({
|
res.format({
|
||||||
html: () => {
|
html: () => {
|
||||||
res.render('errors/' + httpError.errorCode + '.njk', {
|
const locals = {
|
||||||
error_code: httpError.errorCode,
|
error_code: httpError.errorCode,
|
||||||
error_message: httpError.message,
|
error_message: httpError.message,
|
||||||
error_instructions: httpError.instructions,
|
error_instructions: httpError.instructions,
|
||||||
error_id: errorId,
|
error_id: errorId,
|
||||||
|
};
|
||||||
|
res.render('errors/' + httpError.errorCode, locals, (err: Error | undefined, html) => {
|
||||||
|
if (err) {
|
||||||
|
res.render('errors/Error', locals);
|
||||||
|
} else {
|
||||||
|
res.send(html);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
json: () => {
|
json: () => {
|
||||||
@ -192,26 +217,26 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start components
|
|
||||||
for (const component of this.components) {
|
|
||||||
await component.start?.(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Components routes
|
// Components routes
|
||||||
for (const component of this.components) {
|
for (const component of this.components) {
|
||||||
if (component.init) {
|
if (component.initRoutes) {
|
||||||
component.setCurrentRouter(initRouter);
|
component.setCurrentRouter(initRouter);
|
||||||
await component.init(initRouter);
|
await component.initRoutes(initRouter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (component.handle) {
|
if (component.handleRoutes) {
|
||||||
component.setCurrentRouter(handleRouter);
|
component.setCurrentRouter(handleRouter);
|
||||||
await component.handle(handleRouter);
|
await component.handleRoutes(handleRouter);
|
||||||
}
|
}
|
||||||
|
|
||||||
component.setCurrentRouter(null);
|
component.setCurrentRouter(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start components
|
||||||
|
for (const component of this.components) {
|
||||||
|
await component.start?.(app);
|
||||||
|
}
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
this.routes(initRouter, handleRouter);
|
this.routes(initRouter, handleRouter);
|
||||||
|
|
||||||
@ -226,6 +251,7 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
const flags = {
|
const flags = {
|
||||||
verbose: false,
|
verbose: false,
|
||||||
fullHttpRequests: false,
|
fullHttpRequests: false,
|
||||||
|
watch: false,
|
||||||
};
|
};
|
||||||
let mainCommand: string | null = null;
|
let mainCommand: string | null = null;
|
||||||
const mainCommandArgs: string[] = [];
|
const mainCommandArgs: string[] = [];
|
||||||
@ -237,7 +263,11 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
case '--full-http-requests':
|
case '--full-http-requests':
|
||||||
flags.fullHttpRequests = true;
|
flags.fullHttpRequests = true;
|
||||||
break;
|
break;
|
||||||
|
case '--watch':
|
||||||
|
flags.watch = true;
|
||||||
|
break;
|
||||||
case 'migration':
|
case 'migration':
|
||||||
|
case 'pre-compile-views':
|
||||||
if (mainCommand === null) mainCommand = args[i];
|
if (mainCommand === null) mainCommand = args[i];
|
||||||
else throw new Error(`Only one main command can be used at once (${mainCommand},${args[i]})`);
|
else throw new Error(`Only one main command can be used at once (${mainCommand},${args[i]})`);
|
||||||
break;
|
break;
|
||||||
@ -257,6 +287,20 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
await MysqlConnectionManager.migrationCommand(mainCommandArgs);
|
await MysqlConnectionManager.migrationCommand(mainCommandArgs);
|
||||||
await this.stop();
|
await this.stop();
|
||||||
break;
|
break;
|
||||||
|
case 'pre-compile-views': {
|
||||||
|
// Prepare migrations
|
||||||
|
for (const migration of this.getMigrations()) {
|
||||||
|
new migration().registerModels?.();
|
||||||
|
}
|
||||||
|
// Prepare routes
|
||||||
|
for (const controller of this.controllers) {
|
||||||
|
controller.setupRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontendToolsComponent = this.as(FrontendToolsComponent);
|
||||||
|
await frontendToolsComponent.preCompileViews(flags.watch);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
logger.fatal('Unimplemented main command', mainCommand);
|
logger.fatal('Unimplemented main command', mainCommand);
|
||||||
break;
|
break;
|
||||||
@ -332,6 +376,10 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
return this.cacheProvider || null;
|
return this.cacheProvider || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getComponents(): ApplicationComponent[] {
|
||||||
|
return [...this.components];
|
||||||
|
}
|
||||||
|
|
||||||
public as<C extends ApplicationComponent | WebSocketListener<Application>>(type: Type<C>): C {
|
public as<C extends ApplicationComponent | WebSocketListener<Application>>(type: Type<C>): C {
|
||||||
const module = this.components.find(component => component.constructor === type) ||
|
const module = this.components.find(component => component.constructor === type) ||
|
||||||
Object.values(this.webSocketListeners).find(listener => listener.constructor === type);
|
Object.values(this.webSocketListeners).find(listener => listener.constructor === type);
|
||||||
@ -345,8 +393,18 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
return module ? module as C : null;
|
return module ? module as C : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isInNodeModules(): boolean {
|
public has<C extends ApplicationComponent | WebSocketListener<Application>>(type: Type<C>): boolean {
|
||||||
return fs.existsSync(path.join(__dirname, '../../package.json'));
|
return !!this.asOptional(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public require<C extends ApplicationComponent | WebSocketListener<Application>>(type: Type<C>): void {
|
||||||
|
if (!this.has(type)) {
|
||||||
|
throw new MissingComponentError(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async isInNodeModules(): Promise<boolean> {
|
||||||
|
return await doesFileExist('node_modules/swaf');
|
||||||
}
|
}
|
||||||
|
|
||||||
public isReady(): boolean {
|
public isReady(): boolean {
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import {Express, Router} from "express";
|
|
||||||
import {logger} from "./Logger";
|
|
||||||
import {sleep} from "./Utils";
|
|
||||||
import Application from "./Application";
|
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import SecurityError from "./SecurityError";
|
import {Express, Router} from "express";
|
||||||
import Middleware, {MiddlewareType} from "./Middleware";
|
|
||||||
|
import Application from "./Application.js";
|
||||||
|
import {logger} from "./Logger.js";
|
||||||
|
import Middleware, {MiddlewareType} from "./Middleware.js";
|
||||||
|
import SecurityError from "./SecurityError.js";
|
||||||
|
import {sleep} from "./Utils.js";
|
||||||
|
|
||||||
export default abstract class ApplicationComponent {
|
export default abstract class ApplicationComponent {
|
||||||
private currentRouter?: Router;
|
private currentRouter?: Router;
|
||||||
@ -12,14 +13,20 @@ export default abstract class ApplicationComponent {
|
|||||||
|
|
||||||
public async checkSecuritySettings?(): Promise<void>;
|
public async checkSecuritySettings?(): Promise<void>;
|
||||||
|
|
||||||
|
public async init?(): Promise<void>;
|
||||||
|
|
||||||
|
public async initRoutes?(router: Router): Promise<void>;
|
||||||
|
|
||||||
|
public async handleRoutes?(router: Router): Promise<void>;
|
||||||
|
|
||||||
public async start?(expressApp: Express): Promise<void>;
|
public async start?(expressApp: Express): Promise<void>;
|
||||||
|
|
||||||
public async init?(router: Router): Promise<void>;
|
|
||||||
|
|
||||||
public async handle?(router: Router): Promise<void>;
|
|
||||||
|
|
||||||
public async stop?(): Promise<void>;
|
public async stop?(): Promise<void>;
|
||||||
|
|
||||||
|
public isReady(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected async prepare(name: string, prepare: () => Promise<void>): Promise<void> {
|
protected async prepare(name: string, prepare: () => Promise<void>): Promise<void> {
|
||||||
let err;
|
let err;
|
||||||
do {
|
do {
|
||||||
|
@ -1,54 +1,13 @@
|
|||||||
import express, {IRouter, RequestHandler, Router} from "express";
|
import express, {IRouter, RequestHandler, Router} from "express";
|
||||||
import {PathParams} from "express-serve-static-core";
|
import {PathParams} from "express-serve-static-core";
|
||||||
import config from "config";
|
|
||||||
import {logger} from "./Logger";
|
import Application from "./Application.js";
|
||||||
import FileUploadMiddleware from "./FileUploadMiddleware";
|
import {registerRoute} from "./common/Routing.js";
|
||||||
import * as querystring from "querystring";
|
import FileUploadMiddleware from "./FileUploadMiddleware.js";
|
||||||
import {ParsedUrlQueryInput} from "querystring";
|
import {logger} from "./Logger.js";
|
||||||
import Middleware, {MiddlewareType} from "./Middleware";
|
import Middleware, {MiddlewareType} from "./Middleware.js";
|
||||||
import Application from "./Application";
|
|
||||||
|
|
||||||
export default abstract class Controller {
|
export default abstract class Controller {
|
||||||
/**
|
|
||||||
* TODO: this should not be static, it should actually be bound to an app instance.
|
|
||||||
*/
|
|
||||||
private static readonly routes: { [p: string]: string | undefined } = {};
|
|
||||||
|
|
||||||
public static route(
|
|
||||||
route: string,
|
|
||||||
params: RouteParams = [],
|
|
||||||
query: ParsedUrlQueryInput = {},
|
|
||||||
absolute: boolean = false,
|
|
||||||
): string {
|
|
||||||
let path = this.routes[route];
|
|
||||||
if (path === undefined) throw new Error(`Unknown route for name ${route}.`);
|
|
||||||
|
|
||||||
const regExp = this.getRouteParamRegExp('[a-zA-Z0-9_-]+', 'g');
|
|
||||||
if (typeof params === 'string' || typeof params === 'number') {
|
|
||||||
path = path.replace(regExp, '' + params);
|
|
||||||
} else if (Array.isArray(params)) {
|
|
||||||
let i = 0;
|
|
||||||
for (const match of path.matchAll(regExp)) {
|
|
||||||
if (match.length > 0) {
|
|
||||||
path = path.replace(match[0], typeof params[i] !== 'undefined' ? params[i] : '');
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
path = path.replace(/\/+/g, '/');
|
|
||||||
} else {
|
|
||||||
for (const key of Object.keys(params)) {
|
|
||||||
path = path.replace(this.getRouteParamRegExp(key), params[key].toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryStr = querystring.stringify(query);
|
|
||||||
return `${absolute ? config.get<string>('public_url') : ''}${path}` + (queryStr.length > 0 ? '?' + queryStr : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
private static getRouteParamRegExp(key: string, flags?: string): RegExp {
|
|
||||||
return new RegExp(`:${key}(\\(.+?\\))?\\??`, flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly router: Router = express.Router();
|
private readonly router: Router = express.Router();
|
||||||
private readonly fileUploadFormRouter: Router = express.Router();
|
private readonly fileUploadFormRouter: Router = express.Router();
|
||||||
private app?: Application;
|
private app?: Application;
|
||||||
@ -182,13 +141,15 @@ export default abstract class Controller {
|
|||||||
routePath = (prefix !== '/' ? prefix : '') + path;
|
routePath = (prefix !== '/' ? prefix : '') + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Controller.routes[routeName]) {
|
if (typeof routePath !== 'string') {
|
||||||
if (typeof routePath === 'string') {
|
|
||||||
logger.info(`Route ${routeName} has path ${routePath}`);
|
|
||||||
Controller.routes[routeName] = routePath;
|
|
||||||
} else {
|
|
||||||
logger.warn(`Cannot assign path to route ${routeName}.`);
|
logger.warn(`Cannot assign path to route ${routeName}.`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (registerRoute(routeName, routePath)) {
|
||||||
|
logger.info(`Route ${routeName} has path ${routePath}`);
|
||||||
|
} else {
|
||||||
|
logger.warn(`Couldn't register ${routeName} for path ${routePath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,5 +162,3 @@ export default abstract class Controller {
|
|||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RouteParams = { [p: string]: string | number } | string[] | string | number;
|
|
||||||
|
@ -1,7 +1,20 @@
|
|||||||
import {Type} from "./Utils";
|
import {Type} from "./Utils.js";
|
||||||
|
|
||||||
export default interface Extendable<ComponentClass> {
|
export default interface Extendable<ComponentClass> {
|
||||||
as<C extends ComponentClass>(type: Type<C>): C;
|
as<C extends ComponentClass>(type: Type<C>): C;
|
||||||
|
|
||||||
asOptional<C extends ComponentClass>(type: Type<C>): C | null;
|
asOptional<C extends ComponentClass>(type: Type<C>): C | null;
|
||||||
|
|
||||||
|
has<C extends ComponentClass>(type: Type<C>): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws MissingComponentError
|
||||||
|
*/
|
||||||
|
require<C extends ComponentClass>(type: Type<C>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MissingComponentError<ComponentClass> extends Error {
|
||||||
|
public constructor(type: Type<ComponentClass>) {
|
||||||
|
super(`Required component ${type.name} was not found.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import Formidable from "formidable";
|
|
||||||
import Middleware from "./Middleware";
|
|
||||||
import {NextFunction, Request, Response} from "express";
|
import {NextFunction, Request, Response} from "express";
|
||||||
import {FileError, ValidationBag} from "./db/Validator";
|
import IncomingForm from "formidable/Formidable.js";
|
||||||
|
|
||||||
|
import {FileError, ValidationBag} from "./db/Validator.js";
|
||||||
|
import Middleware from "./Middleware.js";
|
||||||
|
|
||||||
export default abstract class FileUploadMiddleware extends Middleware {
|
export default abstract class FileUploadMiddleware extends Middleware {
|
||||||
protected abstract makeForm(): Formidable;
|
protected abstract makeForm(): IncomingForm;
|
||||||
|
|
||||||
protected abstract getDefaultField(): string;
|
protected abstract getDefaultField(): string;
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ export default abstract class FileUploadMiddleware extends Middleware {
|
|||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
const bag = new ValidationBag();
|
const bag = new ValidationBag();
|
||||||
const fileError = new FileError(e);
|
const fileError = new FileError(e);
|
||||||
fileError.thingName = this.getDefaultField();
|
fileError.thingName = this.getDefaultField();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {WrappingError} from "./Utils";
|
import {WrappingError} from "./Utils.js";
|
||||||
|
|
||||||
export abstract class HttpError extends WrappingError {
|
export abstract class HttpError extends WrappingError {
|
||||||
public readonly instructions: string;
|
public readonly instructions: string;
|
||||||
@ -12,7 +12,7 @@ export abstract class HttpError extends WrappingError {
|
|||||||
return this.constructor.name;
|
return this.constructor.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract get errorCode(): number;
|
public abstract get errorCode(): number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BadRequestError extends HttpError {
|
export class BadRequestError extends HttpError {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {Logger as TsLogger} from "tslog";
|
|
||||||
import {AsyncLocalStorage} from "async_hooks";
|
import {AsyncLocalStorage} from "async_hooks";
|
||||||
import {RequestHandler} from "express";
|
import {RequestHandler} from "express";
|
||||||
import {nanoid} from "nanoid";
|
import {nanoid} from "nanoid";
|
||||||
|
import {Logger as TsLogger} from "tslog";
|
||||||
|
|
||||||
const requestIdStorage: AsyncLocalStorage<string> = new AsyncLocalStorage();
|
const requestIdStorage: AsyncLocalStorage<string> = new AsyncLocalStorage();
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import config from "config";
|
import config from "config";
|
||||||
import {MailTemplate} from "./mail/Mail";
|
|
||||||
|
import MailTemplate from "./mail/MailTemplate.js";
|
||||||
|
|
||||||
export const MAGIC_LINK_MAIL = new MailTemplate(
|
export const MAGIC_LINK_MAIL = new MailTemplate(
|
||||||
'magic_link',
|
'magic_link',
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import {RequestHandler} from "express";
|
import {RequestHandler} from "express";
|
||||||
import {NextFunction, Request, Response} from "express-serve-static-core";
|
import {NextFunction, Request, Response} from "express-serve-static-core";
|
||||||
import Application from "./Application";
|
|
||||||
import {Type} from "./Utils";
|
import Application from "./Application.js";
|
||||||
|
import {Type} from "./Utils.js";
|
||||||
|
|
||||||
export default abstract class Middleware {
|
export default abstract class Middleware {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
125
src/TestApp.ts
125
src/TestApp.ts
@ -1,38 +1,47 @@
|
|||||||
import Application from "./Application";
|
|
||||||
import Migration, {MigrationType} from "./db/Migration";
|
|
||||||
import ExpressAppComponent from "./components/ExpressAppComponent";
|
|
||||||
import RedisComponent from "./components/RedisComponent";
|
|
||||||
import MysqlComponent from "./components/MysqlComponent";
|
|
||||||
import NunjucksComponent from "./components/NunjucksComponent";
|
|
||||||
import LogRequestsComponent from "./components/LogRequestsComponent";
|
|
||||||
import MailComponent from "./components/MailComponent";
|
|
||||||
import SessionComponent from "./components/SessionComponent";
|
|
||||||
import AuthComponent from "./auth/AuthComponent";
|
|
||||||
import FormHelperComponent from "./components/FormHelperComponent";
|
|
||||||
import ServeStaticDirectoryComponent from "./components/ServeStaticDirectoryComponent";
|
|
||||||
import {Express} from "express";
|
import {Express} from "express";
|
||||||
import MagicLinkAuthMethod from "./auth/magic_link/MagicLinkAuthMethod";
|
|
||||||
import PasswordAuthMethod from "./auth/password/PasswordAuthMethod";
|
import Application from "./Application.js";
|
||||||
import {MAGIC_LINK_MAIL} from "./Mails";
|
import AccountController from "./auth/AccountController.js";
|
||||||
import CreateMigrationsTable from "./migrations/CreateMigrationsTable";
|
import AuthComponent from "./auth/AuthComponent.js";
|
||||||
import CreateUsersAndUserEmailsTableMigration from "./auth/migrations/CreateUsersAndUserEmailsTableMigration";
|
import AuthController from "./auth/AuthController.js";
|
||||||
import CreateMagicLinksTableMigration from "./auth/magic_link/CreateMagicLinksTableMigration";
|
import AddUsedToMagicLinksMigration from "./auth/magic_link/AddUsedToMagicLinksMigration.js";
|
||||||
import AuthController from "./auth/AuthController";
|
import CreateMagicLinksTableMigration from "./auth/magic_link/CreateMagicLinksTableMigration.js";
|
||||||
import MagicLinkWebSocketListener from "./auth/magic_link/MagicLinkWebSocketListener";
|
import MagicLinkAuthMethod from "./auth/magic_link/MagicLinkAuthMethod.js";
|
||||||
import MagicLinkController from "./auth/magic_link/MagicLinkController";
|
import MagicLinkController from "./auth/magic_link/MagicLinkController.js";
|
||||||
import AddPasswordToUsersMigration from "./auth/password/AddPasswordToUsersMigration";
|
import MagicLinkWebSocketListener from "./auth/magic_link/MagicLinkWebSocketListener.js";
|
||||||
import AddNameToUsersMigration from "./auth/migrations/AddNameToUsersMigration";
|
import MakeMagicLinksSessionNotUniqueMigration from "./auth/magic_link/MakeMagicLinksSessionNotUniqueMigration.js";
|
||||||
import CsrfProtectionComponent from "./components/CsrfProtectionComponent";
|
import AddApprovedFieldToUsersTableMigration from "./auth/migrations/AddApprovedFieldToUsersTableMigration.js";
|
||||||
import MailController from "./mail/MailController";
|
import AddNameChangedAtToUsersMigration from "./auth/migrations/AddNameChangedAtToUsersMigration.js";
|
||||||
import WebSocketServerComponent from "./components/WebSocketServerComponent";
|
import AddNameToUsersMigration from "./auth/migrations/AddNameToUsersMigration.js";
|
||||||
import Controller from "./Controller";
|
import CreateUsersAndUserEmailsTableMigration from "./auth/migrations/CreateUsersAndUserEmailsTableMigration.js";
|
||||||
import AccountController from "./auth/AccountController";
|
import AddPasswordToUsersMigration from "./auth/password/AddPasswordToUsersMigration.js";
|
||||||
import MakeMagicLinksSessionNotUniqueMigration from "./auth/magic_link/MakeMagicLinksSessionNotUniqueMigration";
|
import PasswordAuthMethod from "./auth/password/PasswordAuthMethod.js";
|
||||||
import AddUsedToMagicLinksMigration from "./auth/magic_link/AddUsedToMagicLinksMigration";
|
import CsrfProtectionComponent from "./components/CsrfProtectionComponent.js";
|
||||||
import PreviousUrlComponent from "./components/PreviousUrlComponent";
|
import ExpressAppComponent from "./components/ExpressAppComponent.js";
|
||||||
import AddNameChangedAtToUsersMigration from "./auth/migrations/AddNameChangedAtToUsersMigration";
|
import FormHelperComponent from "./components/FormHelperComponent.js";
|
||||||
import BackendController from "./helpers/BackendController";
|
import FrontendToolsComponent from "./components/FrontendToolsComponent.js";
|
||||||
import packageJson = require('./package.json');
|
import LogRequestsComponent from "./components/LogRequestsComponent.js";
|
||||||
|
import MailComponent from "./components/MailComponent.js";
|
||||||
|
import MaintenanceComponent from "./components/MaintenanceComponent.js";
|
||||||
|
import MysqlComponent from "./components/MysqlComponent.js";
|
||||||
|
import PreviousUrlComponent from "./components/PreviousUrlComponent.js";
|
||||||
|
import RedisComponent from "./components/RedisComponent.js";
|
||||||
|
import ServeStaticDirectoryComponent from "./components/ServeStaticDirectoryComponent.js";
|
||||||
|
import SessionComponent from "./components/SessionComponent.js";
|
||||||
|
import WebSocketServerComponent from "./components/WebSocketServerComponent.js";
|
||||||
|
import Controller from "./Controller.js";
|
||||||
|
import Migration, {MigrationType} from "./db/Migration.js";
|
||||||
|
import AssetCompiler from "./frontend/AssetCompiler.js";
|
||||||
|
import CopyAssetPreCompiler from "./frontend/CopyAssetPreCompiler.js";
|
||||||
|
import MailViewEngine from "./frontend/MailViewEngine.js";
|
||||||
|
import NunjucksViewEngine from "./frontend/NunjucksViewEngine.js";
|
||||||
|
import ScssAssetPreCompiler from "./frontend/ScssAssetPreCompiler.js";
|
||||||
|
import SvelteViewEngine from "./frontend/SvelteViewEngine.js";
|
||||||
|
import TypeScriptPreCompiler from "./frontend/TypeScriptPreCompiler.js";
|
||||||
|
import BackendController from "./helpers/BackendController.js";
|
||||||
|
import MailController from "./mail/MailController.js";
|
||||||
|
import {MAGIC_LINK_MAIL} from "./Mails.js";
|
||||||
|
import CreateMigrationsTable from "./migrations/CreateMigrationsTable.js";
|
||||||
|
|
||||||
export const MIGRATIONS = [
|
export const MIGRATIONS = [
|
||||||
CreateMigrationsTable,
|
CreateMigrationsTable,
|
||||||
@ -49,14 +58,26 @@ export default class TestApp extends Application {
|
|||||||
private readonly addr: string;
|
private readonly addr: string;
|
||||||
private readonly port: number;
|
private readonly port: number;
|
||||||
|
|
||||||
public constructor(addr: string, port: number, ignoreCommandLine: boolean = false) {
|
public constructor(
|
||||||
super(packageJson.version, ignoreCommandLine);
|
version: string,
|
||||||
|
addr: string,
|
||||||
|
port: number,
|
||||||
|
ignoreCommandLine: boolean = false,
|
||||||
|
private readonly approvalMode: boolean,
|
||||||
|
) {
|
||||||
|
super(version, ignoreCommandLine);
|
||||||
this.addr = addr;
|
this.addr = addr;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getMigrations(): MigrationType<Migration>[] {
|
protected getMigrations(): MigrationType<Migration>[] {
|
||||||
return MIGRATIONS;
|
const migrations = [...MIGRATIONS];
|
||||||
|
|
||||||
|
if (this.approvalMode) {
|
||||||
|
migrations.push(AddApprovedFieldToUsersTableMigration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrations;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async init(): Promise<void> {
|
protected async init(): Promise<void> {
|
||||||
@ -73,13 +94,26 @@ export default class TestApp extends Application {
|
|||||||
// Static files
|
// Static files
|
||||||
this.use(new ServeStaticDirectoryComponent('public'));
|
this.use(new ServeStaticDirectoryComponent('public'));
|
||||||
|
|
||||||
|
// Maintenance
|
||||||
|
this.use(new MaintenanceComponent());
|
||||||
|
|
||||||
// Dynamic views and routes
|
// Dynamic views and routes
|
||||||
this.use(new NunjucksComponent(['test/views', 'views']));
|
const intermediateDirectory = 'intermediates/assets';
|
||||||
|
const assetCompiler = new AssetCompiler(intermediateDirectory, 'public');
|
||||||
|
this.use(new FrontendToolsComponent(
|
||||||
|
assetCompiler,
|
||||||
|
new CopyAssetPreCompiler(intermediateDirectory, '', 'json', ['test/assets'], false),
|
||||||
|
new ScssAssetPreCompiler(intermediateDirectory, assetCompiler.targetDir, 'scss', ['test/assets']),
|
||||||
|
new CopyAssetPreCompiler(intermediateDirectory, 'img', 'svg', ['test/assets'], true),
|
||||||
|
new TypeScriptPreCompiler(intermediateDirectory, ['test/assets']),
|
||||||
|
new SvelteViewEngine(intermediateDirectory, 'test/assets'),
|
||||||
|
new NunjucksViewEngine(intermediateDirectory, 'test/assets'),
|
||||||
|
));
|
||||||
this.use(new PreviousUrlComponent());
|
this.use(new PreviousUrlComponent());
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
this.use(new MysqlComponent());
|
this.use(new MysqlComponent());
|
||||||
this.use(new MailComponent());
|
this.use(new MailComponent(new MailViewEngine('intermediates/assets', 'test/assets')));
|
||||||
|
|
||||||
// Session
|
// Session
|
||||||
this.use(new RedisComponent());
|
this.use(new RedisComponent());
|
||||||
@ -95,7 +129,7 @@ export default class TestApp extends Application {
|
|||||||
this.use(new AuthComponent(this, new MagicLinkAuthMethod(this, MAGIC_LINK_MAIL), new PasswordAuthMethod(this)));
|
this.use(new AuthComponent(this, new MagicLinkAuthMethod(this, MAGIC_LINK_MAIL), new PasswordAuthMethod(this)));
|
||||||
|
|
||||||
// WebSocket server
|
// WebSocket server
|
||||||
this.use(new WebSocketServerComponent(this, this.as(ExpressAppComponent), this.as(RedisComponent)));
|
this.use(new WebSocketServerComponent());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected registerWebSocketListeners(): void {
|
protected registerWebSocketListeners(): void {
|
||||||
@ -116,6 +150,17 @@ export default class TestApp extends Application {
|
|||||||
this.get('/', (req, res) => {
|
this.get('/', (req, res) => {
|
||||||
res.render('home');
|
res.render('home');
|
||||||
}, 'home');
|
}, 'home');
|
||||||
|
this.get('/tests', (req, res) => {
|
||||||
|
res.render('tests');
|
||||||
|
}, 'tests');
|
||||||
|
this.get('/design', (req, res) => {
|
||||||
|
req.flash('success', 'Success.');
|
||||||
|
req.flash('info', 'Info.');
|
||||||
|
req.flash('warning', 'Warning.');
|
||||||
|
req.flash('error', 'Error.');
|
||||||
|
req.flash('error-alert', 'Error alert.');
|
||||||
|
res.render('design');
|
||||||
|
}, 'design');
|
||||||
}
|
}
|
||||||
}());
|
}());
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {TooManyRequestsHttpError} from "./HttpError";
|
import {TooManyRequestsHttpError} from "./HttpError.js";
|
||||||
import {logger} from "./Logger";
|
import {logger} from "./Logger.js";
|
||||||
|
|
||||||
export default class Throttler {
|
export default class Throttler {
|
||||||
private static readonly throttles: Record<string, Throttle | undefined> = {};
|
private static readonly throttles: Record<string, Throttle | undefined> = {};
|
||||||
|
32
src/Utils.ts
32
src/Utils.ts
@ -1,3 +1,6 @@
|
|||||||
|
import {promises as fs} from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
export async function sleep(ms: number): Promise<void> {
|
export async function sleep(ms: number): Promise<void> {
|
||||||
return await new Promise(resolve => {
|
return await new Promise(resolve => {
|
||||||
setTimeout(() => resolve(), ms);
|
setTimeout(() => resolve(), ms);
|
||||||
@ -44,3 +47,32 @@ export function getMethods<T extends { [p: string]: unknown }>(obj: T): string[]
|
|||||||
} while (currentObj);
|
} while (currentObj);
|
||||||
return [...properties.keys()].filter(item => typeof obj[item] === 'function');
|
return [...properties.keys()].filter(item => typeof obj[item] === 'function');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listFilesRecursively(dir: string): Promise<string[]> {
|
||||||
|
const localFiles = await fs.readdir(dir);
|
||||||
|
const files: string[] = [];
|
||||||
|
for (const file of localFiles.map(file => path.join(dir, file))) {
|
||||||
|
const stat = await fs.stat(file);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
files.push(...await listFilesRecursively(file));
|
||||||
|
} else {
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function doesFileExist(file: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.stat(file);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err?.code === 'ENOENT') {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import WebSocket from "ws";
|
|
||||||
import {IncomingMessage} from "http";
|
|
||||||
import Application from "./Application";
|
|
||||||
import {Session} from "express-session";
|
import {Session} from "express-session";
|
||||||
|
import {IncomingMessage} from "http";
|
||||||
|
import WebSocket from "ws";
|
||||||
|
|
||||||
|
import Application from "./Application.js";
|
||||||
|
|
||||||
export default abstract class WebSocketListener<T extends Application> {
|
export default abstract class WebSocketListener<T extends Application> {
|
||||||
private app!: T;
|
private app!: T;
|
||||||
|
239
src/assets/img/logo.svg
Normal file
239
src/assets/img/logo.svg
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="256"
|
||||||
|
height="256"
|
||||||
|
viewBox="0 0 67.733332 67.733335"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
inkscape:version="1.1 (c4e8f9ed74, 2021-05-24)"
|
||||||
|
sodipodi:docname="logo.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#ffffff"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:pageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
showgrid="true"
|
||||||
|
units="px"
|
||||||
|
width="256px"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:zoom="3.1108586"
|
||||||
|
inkscape:cx="167.96006"
|
||||||
|
inkscape:cy="124.88514"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1408"
|
||||||
|
inkscape:window-x="1920"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="g2910">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid9"
|
||||||
|
empspacing="8"
|
||||||
|
color="#808080"
|
||||||
|
opacity="0.25098039"
|
||||||
|
empcolor="#3b3bc4"
|
||||||
|
empopacity="0.25098039" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath4993">
|
||||||
|
<path
|
||||||
|
style="font-variation-settings:normal;opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1"
|
||||||
|
sodipodi:type="inkscape:offset"
|
||||||
|
inkscape:radius="0"
|
||||||
|
inkscape:original="M 38.130859 9.1289062 C 38.130859 9.1289062 36.869291 14.550899 33.769531 22.080078 C 31.015624 28.769197 25.443612 29.282491 25.431641 38.761719 C 25.421841 46.502055 31.116842 51.460937 38.130859 51.460938 C 45.144876 51.460938 50.795057 46.501973 50.832031 38.761719 C 50.880267 28.66385 49.355438 30.099351 44.685547 20.40625 C 41.206475 13.184883 38.130859 9.1289062 38.130859 9.1289062 z "
|
||||||
|
xlink:href="#path63"
|
||||||
|
id="path4995"
|
||||||
|
inkscape:href="#path63"
|
||||||
|
d="m 38.130859,9.4160156 c 0,0 -12.699218,21.1661454 -12.699218,29.6328124 0,7.014017 5.685201,12.701172 12.699218,12.701172 7.014017,0 12.701172,-5.687155 12.701172,-12.701172 0,-8.466667 -12.701172,-29.6328124 -12.701172,-29.6328124 z"
|
||||||
|
transform="translate(-4.2646454,3.5720328)" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath5199">
|
||||||
|
<path
|
||||||
|
style="font-variation-settings:normal;opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1"
|
||||||
|
sodipodi:type="inkscape:offset"
|
||||||
|
inkscape:radius="0"
|
||||||
|
inkscape:original="M 36.546875 7.2324219 C 36.546875 7.2324219 23.845703 28.40052 23.845703 36.867188 C 23.845703 43.881204 29.532858 49.566406 36.546875 49.566406 C 43.560892 49.566406 49.246094 43.881204 49.246094 36.867188 C 49.246094 28.40052 36.546875 7.2324219 36.546875 7.2324219 z "
|
||||||
|
xlink:href="#path4972"
|
||||||
|
id="path5201"
|
||||||
|
inkscape:href="#path4972"
|
||||||
|
d="m 36.242188,6.9941406 c 0,0 -12.701172,21.1680984 -12.701172,29.6347654 0,7.014017 5.687155,12.699219 12.701172,12.699219 7.014017,0 12.699218,-5.685202 12.699218,-12.699219 0,-8.466667 -12.699219,-29.6347654 -12.699218,-29.6347654 z"
|
||||||
|
transform="translate(5.7867919,-2.9996614)" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g2910"
|
||||||
|
style="display:inline">
|
||||||
|
<g
|
||||||
|
id="g4378"
|
||||||
|
style="display:inline"
|
||||||
|
transform="translate(-1.0583342)">
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#ff900d;fill-opacity:1;stroke:#ff900d;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;stop-color:#000000;stop-opacity:1"
|
||||||
|
id="rect21528"
|
||||||
|
width="46.566666"
|
||||||
|
height="46.566666"
|
||||||
|
x="20.108334"
|
||||||
|
y="2.1166666" />
|
||||||
|
<rect
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:#fffdff;stroke-width:1.05833;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;stop-color:#000000"
|
||||||
|
id="rect52610-8"
|
||||||
|
width="38.099995"
|
||||||
|
height="2.1166658"
|
||||||
|
x="28.575006"
|
||||||
|
y="21.166666" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
aria-label="express"
|
||||||
|
id="text15483-1"
|
||||||
|
style="font-size:10.0542px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:Rubik;text-align:end;text-anchor:end;display:none;fill:#fefefe;stroke-width:0.529167"
|
||||||
|
transform="translate(3.9913951,0.37437841)">
|
||||||
|
<path
|
||||||
|
d="m 27.144137,18.146446 q -1.035579,0 -1.648883,-0.633413 -0.613305,-0.643466 -0.67363,-1.749425 -0.01005,-0.130704 -0.01005,-0.331787 0,-0.211138 0.01005,-0.341842 0.04022,-0.713846 0.331788,-1.246717 0.291571,-0.542925 0.794279,-0.834495 0.512763,-0.291571 1.196446,-0.291571 0.764117,0 1.276879,0.321733 0.522817,0.321733 0.794279,0.914929 0.271463,0.593196 0.271463,1.387475 v 0.170921 q 0,0.110596 -0.07038,0.170921 -0.06032,0.06032 -0.160867,0.06032 H 25.77677 q 0,0.01005 0,0.04022 0,0.03016 0,0.05027 0.02011,0.412221 0.180975,0.774171 0.160867,0.351896 0.462492,0.573088 0.301625,0.221191 0.7239,0.221191 0.36195,0 0.60325,-0.110595 0.2413,-0.110596 0.392112,-0.2413 0.150813,-0.140759 0.201084,-0.211138 0.09049,-0.130704 0.140758,-0.150812 0.05027,-0.03016 0.160867,-0.03016 h 0.4826 q 0.100541,0 0.160866,0.06032 0.07038,0.05027 0.06032,0.150813 -0.01005,0.150812 -0.160866,0.372004 -0.150813,0.211137 -0.432329,0.422275 -0.281517,0.211137 -0.683684,0.351896 -0.402166,0.130704 -0.924983,0.130704 z M 25.77677,15.049762 h 2.754842 V 15.0196 q 0,-0.452438 -0.170921,-0.804333 -0.160867,-0.351896 -0.472546,-0.55298 -0.311679,-0.211137 -0.744008,-0.211137 -0.432329,0 -0.744008,0.211137 -0.301625,0.201084 -0.462492,0.55298 -0.160867,0.351895 -0.160867,0.804333 z"
|
||||||
|
id="path2866"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 30.34322,18.045904 q -0.08043,0 -0.150813,-0.06032 -0.06032,-0.06033 -0.06032,-0.150812 0,-0.03016 0.01006,-0.07038 0.02011,-0.05027 0.06032,-0.110595 l 1.749425,-2.282296 -1.63883,-2.161646 q -0.04022,-0.06032 -0.06032,-0.100542 -0.01005,-0.04022 -0.01005,-0.08043 0,-0.09049 0.06032,-0.150813 0.06032,-0.06033 0.150813,-0.06033 h 0.512762 q 0.110596,0 0.160867,0.06033 0.06032,0.05027 0.100542,0.100542 l 1.337204,1.739371 1.337204,-1.729317 q 0.04022,-0.05027 0.09049,-0.110596 0.06032,-0.06033 0.170921,-0.06033 h 0.492654 q 0.09049,0 0.150813,0.06033 0.06032,0.06033 0.06032,0.150813 0,0.04022 -0.02011,0.08043 -0.01006,0.04022 -0.05027,0.100542 l -1.658938,2.181754 1.749425,2.262188 q 0.04022,0.06033 0.05027,0.100541 0.02011,0.04022 0.02011,0.08043 0,0.09049 -0.06032,0.150812 -0.06032,0.06032 -0.150812,0.06032 h -0.532871 q -0.100542,0 -0.160867,-0.05027 -0.06032,-0.05027 -0.100541,-0.100541 l -1.417638,-1.839913 -1.417637,1.839913 q -0.04022,0.04022 -0.100542,0.100541 -0.05027,0.05027 -0.160867,0.05027 z"
|
||||||
|
id="path2868"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 36.245011,19.956196 q -0.100542,0 -0.170921,-0.07038 -0.06032,-0.06032 -0.06032,-0.160867 v -6.675967 q 0,-0.100541 0.06032,-0.160866 0.07038,-0.07038 0.170921,-0.07038 h 0.462491 q 0.100542,0 0.160867,0.07038 0.07038,0.06033 0.07038,0.160866 v 0.442384 q 0.2413,-0.331788 0.643467,-0.55298 0.402167,-0.221191 1.005417,-0.221191 0.563033,0 0.955146,0.191029 0.402166,0.191029 0.65352,0.522817 0.261409,0.331787 0.392113,0.754062 0.130704,0.422275 0.140758,0.904875 0.01005,0.160867 0.01005,0.341842 0,0.180975 -0.01005,0.341841 -0.01005,0.472546 -0.140758,0.904875 -0.130704,0.422275 -0.392113,0.754063 -0.251354,0.321733 -0.65352,0.522817 -0.392113,0.191029 -0.955146,0.191029 -0.583142,0 -0.985309,-0.211138 -0.392112,-0.221191 -0.643466,-0.542925 v 2.332567 q 0,0.100542 -0.06032,0.160867 -0.06033,0.07038 -0.170921,0.07038 z m 2.131483,-2.624138 q 0.522817,0 0.814388,-0.221191 0.301625,-0.231246 0.432329,-0.593196 0.130704,-0.372004 0.150812,-0.794279 0.01006,-0.291571 0,-0.583142 -0.02011,-0.422275 -0.150812,-0.784225 -0.130704,-0.372004 -0.432329,-0.593196 -0.291571,-0.231246 -0.814388,-0.231246 -0.492654,0 -0.804333,0.231246 -0.301625,0.231246 -0.452438,0.593196 -0.140758,0.351896 -0.160866,0.7239 -0.01006,0.160867 -0.01006,0.382058 0,0.221192 0.01006,0.392113 0.01005,0.351896 0.160866,0.693737 0.160867,0.341842 0.472546,0.563034 0.311679,0.221191 0.784225,0.221191 z"
|
||||||
|
id="path2870"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 42.227228,18.045904 q -0.100542,0 -0.170921,-0.06032 -0.06032,-0.07038 -0.06032,-0.170921 v -4.755621 q 0,-0.100541 0.06032,-0.17092 0.07038,-0.07038 0.170921,-0.07038 h 0.462492 q 0.100541,0 0.170921,0.07038 0.07038,0.07038 0.07038,0.17092 v 0.442384 q 0.201083,-0.341842 0.552979,-0.512763 0.351896,-0.170921 0.84455,-0.170921 h 0.402167 q 0.100541,0 0.160866,0.07038 0.06032,0.06033 0.06032,0.160866 v 0.412221 q 0,0.100542 -0.06032,0.160867 -0.06032,0.06033 -0.160866,0.06033 h -0.60325 q -0.542925,0 -0.854605,0.321733 -0.311679,0.311679 -0.311679,0.854604 v 2.955925 q 0,0.100542 -0.07038,0.170921 -0.07038,0.06032 -0.170921,0.06032 z"
|
||||||
|
id="path2872"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 47.799738,18.146446 q -1.035579,0 -1.648883,-0.633413 -0.613304,-0.643466 -0.673629,-1.749425 -0.01005,-0.130704 -0.01005,-0.331787 0,-0.211138 0.01005,-0.341842 0.04022,-0.713846 0.331787,-1.246717 0.291571,-0.542925 0.79428,-0.834495 0.512762,-0.291571 1.196445,-0.291571 0.764117,0 1.27688,0.321733 0.522816,0.321733 0.794279,0.914929 0.271462,0.593196 0.271462,1.387475 v 0.170921 q 0,0.110596 -0.07038,0.170921 -0.06033,0.06032 -0.160867,0.06032 h -3.478741 q 0,0.01005 0,0.04022 0,0.03016 0,0.05027 0.02011,0.412221 0.180975,0.774171 0.160866,0.351896 0.462491,0.573088 0.301625,0.221191 0.7239,0.221191 0.36195,0 0.60325,-0.110595 0.2413,-0.110596 0.392113,-0.2413 0.150812,-0.140759 0.201083,-0.211138 0.09049,-0.130704 0.140759,-0.150812 0.05027,-0.03016 0.160866,-0.03016 h 0.4826 q 0.100542,0 0.160867,0.06032 0.07038,0.05027 0.06032,0.150813 -0.01005,0.150812 -0.160867,0.372004 -0.150812,0.211137 -0.432329,0.422275 -0.281517,0.211137 -0.683683,0.351896 -0.402167,0.130704 -0.924984,0.130704 z m -1.367366,-3.096684 h 2.754841 V 15.0196 q 0,-0.452438 -0.17092,-0.804333 -0.160867,-0.351896 -0.472546,-0.55298 -0.311679,-0.211137 -0.744009,-0.211137 -0.432329,0 -0.744008,0.211137 -0.301625,0.201084 -0.462492,0.55298 -0.160866,0.351895 -0.160866,0.804333 z"
|
||||||
|
id="path2874"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 53.158603,18.146446 q -0.563033,0 -0.9652,-0.140759 -0.402167,-0.140758 -0.653521,-0.341841 -0.251354,-0.201084 -0.382058,-0.402167 -0.12065,-0.201083 -0.130704,-0.321733 -0.01005,-0.110596 0.07038,-0.170921 0.08043,-0.06032 0.160867,-0.06032 h 0.442383 q 0.06032,0 0.09049,0.02011 0.04022,0.01005 0.100542,0.08043 0.130704,0.140758 0.291571,0.281516 0.160867,0.140759 0.392113,0.231246 0.2413,0.09049 0.593195,0.09049 0.512763,0 0.84455,-0.19103 0.331788,-0.201083 0.331788,-0.583141 0,-0.251354 -0.140758,-0.402167 -0.130705,-0.150812 -0.4826,-0.271462 -0.341842,-0.12065 -0.945092,-0.251355 -0.60325,-0.140758 -0.955146,-0.341841 -0.351896,-0.211138 -0.502708,-0.492654 -0.150813,-0.291571 -0.150813,-0.653521 0,-0.372004 0.221192,-0.713846 0.221191,-0.351896 0.643467,-0.573088 0.432329,-0.221191 1.075795,-0.221191 0.522817,0 0.894821,0.130704 0.372004,0.130704 0.613304,0.331787 0.2413,0.19103 0.36195,0.382059 0.12065,0.191029 0.130705,0.321733 0.01005,0.100542 -0.06033,0.170921 -0.07038,0.06033 -0.160867,0.06033 h -0.422275 q -0.07038,0 -0.12065,-0.03016 -0.04022,-0.03016 -0.08043,-0.07038 -0.100542,-0.130704 -0.2413,-0.261408 -0.130705,-0.130704 -0.351896,-0.211138 -0.211138,-0.09049 -0.563034,-0.09049 -0.502708,0 -0.754062,0.211137 -0.251354,0.211138 -0.251354,0.532871 0,0.191029 0.110596,0.341842 0.110595,0.150812 0.422275,0.271462 0.311679,0.12065 0.924983,0.261409 0.663575,0.130704 1.045633,0.351896 0.382059,0.221191 0.542925,0.512762 0.160867,0.291571 0.160867,0.673629 0,0.422275 -0.251354,0.774171 -0.2413,0.351896 -0.7239,0.563033 -0.472546,0.201084 -1.176338,0.201084 z"
|
||||||
|
id="path2876"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 58.286212,18.146446 q -0.563033,0 -0.9652,-0.140759 -0.402166,-0.140758 -0.65352,-0.341841 -0.251355,-0.201084 -0.382059,-0.402167 -0.12065,-0.201083 -0.130704,-0.321733 -0.01005,-0.110596 0.07038,-0.170921 0.08043,-0.06032 0.160867,-0.06032 h 0.442383 q 0.06032,0 0.09049,0.02011 0.04022,0.01005 0.100541,0.08043 0.130705,0.140758 0.291571,0.281516 0.160867,0.140759 0.392113,0.231246 0.2413,0.09049 0.593196,0.09049 0.512762,0 0.84455,-0.19103 0.331787,-0.201083 0.331787,-0.583141 0,-0.251354 -0.140758,-0.402167 -0.130704,-0.150812 -0.4826,-0.271462 -0.341842,-0.12065 -0.945092,-0.251355 -0.60325,-0.140758 -0.955146,-0.341841 -0.351896,-0.211138 -0.502708,-0.492654 -0.150813,-0.291571 -0.150813,-0.653521 0,-0.372004 0.221192,-0.713846 0.221192,-0.351896 0.643467,-0.573088 0.432329,-0.221191 1.075796,-0.221191 0.522816,0 0.89482,0.130704 0.372005,0.130704 0.613305,0.331787 0.2413,0.19103 0.36195,0.382059 0.12065,0.191029 0.130704,0.321733 0.01005,0.100542 -0.06033,0.170921 -0.07038,0.06033 -0.160867,0.06033 h -0.422275 q -0.07038,0 -0.12065,-0.03016 -0.04022,-0.03016 -0.08043,-0.07038 -0.100542,-0.130704 -0.2413,-0.261408 -0.130704,-0.130704 -0.351896,-0.211138 -0.211138,-0.09049 -0.563033,-0.09049 -0.502709,0 -0.754063,0.211137 -0.251354,0.211138 -0.251354,0.532871 0,0.191029 0.110596,0.341842 0.110596,0.150812 0.422275,0.271462 0.311679,0.12065 0.924983,0.261409 0.663575,0.130704 1.045633,0.351896 0.382059,0.221191 0.542925,0.512762 0.160867,0.291571 0.160867,0.673629 0,0.422275 -0.251354,0.774171 -0.2413,0.351896 -0.7239,0.563033 -0.472546,0.201084 -1.176338,0.201084 z"
|
||||||
|
id="path2878"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g1018"
|
||||||
|
style="display:inline">
|
||||||
|
<g
|
||||||
|
id="g4374"
|
||||||
|
style="display:inline"
|
||||||
|
transform="translate(-1.0583342)">
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#ff3e00;fill-opacity:1;stroke:#ff3e00;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;stop-color:#000000;stop-opacity:1"
|
||||||
|
id="rect21526"
|
||||||
|
width="46.566666"
|
||||||
|
height="46.566666"
|
||||||
|
x="11.641667"
|
||||||
|
y="10.583333" />
|
||||||
|
<rect
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:#fffdff;stroke-width:1.05833;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;stop-color:#000000"
|
||||||
|
id="rect52610-6"
|
||||||
|
width="38.099995"
|
||||||
|
height="2.1166663"
|
||||||
|
x="20.108339"
|
||||||
|
y="29.633333" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
aria-label="svelte"
|
||||||
|
id="text24132"
|
||||||
|
style="font-size:13.7583px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:Rubik;text-align:end;text-anchor:end;display:none;fill:#fefefe;stroke-width:0.529167"
|
||||||
|
transform="translate(-0.11704721,3.6255731)">
|
||||||
|
<path
|
||||||
|
d="m 22.379335,23.475161 q -0.770465,0 -1.320797,-0.192616 -0.550332,-0.192617 -0.894289,-0.467783 -0.343958,-0.275166 -0.522815,-0.550332 -0.1651,-0.275166 -0.178858,-0.440265 -0.01376,-0.151341 0.09631,-0.233891 0.110066,-0.08255 0.220133,-0.08255 h 0.605365 q 0.08255,0 0.123824,0.02752 0.05503,0.01376 0.137583,0.110066 0.178858,0.192616 0.398991,0.385232 0.220133,0.192617 0.536574,0.316441 0.330199,0.123825 0.81174,0.123825 0.701673,0 1.155697,-0.261408 0.454024,-0.275166 0.454024,-0.797981 0,-0.343958 -0.192617,-0.550332 -0.178858,-0.206375 -0.660398,-0.371474 -0.467782,-0.1651 -1.29328,-0.343958 -0.825498,-0.192616 -1.307039,-0.467782 -0.48154,-0.288924 -0.687915,-0.674157 -0.206374,-0.39899 -0.206374,-0.894289 0,-0.509057 0.302682,-0.976839 0.302683,-0.481541 0.880532,-0.784223 0.591607,-0.302683 1.472138,-0.302683 0.715431,0 1.224488,0.178858 0.509057,0.178858 0.839257,0.454024 0.330199,0.261407 0.495298,0.522815 0.1651,0.261408 0.178858,0.440266 0.01376,0.137583 -0.08255,0.233891 -0.09631,0.08255 -0.220133,0.08255 h -0.577849 q -0.09631,0 -0.165099,-0.04127 -0.05503,-0.04127 -0.110067,-0.09631 -0.137583,-0.178858 -0.330199,-0.357716 -0.178858,-0.178858 -0.481541,-0.288925 -0.288924,-0.123824 -0.770464,-0.123824 -0.687915,0 -1.031873,0.288924 -0.343957,0.288924 -0.343957,0.72919 0,0.261408 0.151341,0.467782 0.151341,0.206375 0.577849,0.371474 0.426507,0.1651 1.265763,0.357716 0.908048,0.178858 1.430863,0.481541 0.522816,0.302682 0.742949,0.701673 0.220132,0.398991 0.220132,0.921806 0,0.577849 -0.343957,1.059389 -0.330199,0.481541 -0.990598,0.770465 -0.64664,0.275166 -1.609721,0.275166 z"
|
||||||
|
id="path977"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 29.165596,23.337578 q -0.206374,0 -0.330199,-0.09631 -0.110066,-0.09631 -0.178858,-0.24765 l -2.545285,-6.383851 q -0.02752,-0.06879 -0.02752,-0.137583 0,-0.123824 0.08255,-0.206374 0.08255,-0.08255 0.206375,-0.08255 h 0.660398 q 0.151341,0 0.233891,0.08255 0.08255,0.08255 0.09631,0.151341 l 2.10502,5.42077 2.091262,-5.42077 q 0.02752,-0.06879 0.09631,-0.151341 0.08255,-0.08255 0.233891,-0.08255 h 0.674156 q 0.110067,0 0.192617,0.08255 0.09631,0.08255 0.09631,0.206374 0,0.06879 -0.02752,0.137583 L 30.26626,22.99362 q -0.05503,0.151342 -0.178858,0.24765 -0.110066,0.09631 -0.330199,0.09631 z"
|
||||||
|
id="path979"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 36.889984,23.475161 q -1.417105,0 -2.256361,-0.866773 -0.839257,-0.880531 -0.921807,-2.393944 -0.01376,-0.178858 -0.01376,-0.454024 0,-0.288924 0.01376,-0.467782 0.05503,-0.97684 0.454024,-1.70603 0.398991,-0.742948 1.086906,-1.141938 0.701673,-0.398991 1.637238,-0.398991 1.045631,0 1.747304,0.440266 0.715431,0.440265 1.086906,1.252005 0.371474,0.811739 0.371474,1.898645 v 0.233891 q 0,0.151342 -0.09631,0.233891 -0.08255,0.08255 -0.220133,0.08255 h -4.760372 q 0,0.01376 0,0.05503 0,0.04128 0,0.06879 0.02752,0.56409 0.247649,1.059389 0.220133,0.48154 0.632882,0.784223 0.412749,0.302683 0.990598,0.302683 0.495299,0 0.825498,-0.151342 0.330199,-0.151341 0.536573,-0.330199 0.206375,-0.192616 0.275166,-0.288924 0.123825,-0.178858 0.192617,-0.206375 0.06879,-0.04127 0.220132,-0.04127 h 0.660399 q 0.137583,0 0.220133,0.08255 0.09631,0.06879 0.08255,0.206375 -0.01376,0.206374 -0.220132,0.509057 -0.206375,0.288924 -0.591607,0.577848 -0.385233,0.288925 -0.935565,0.481541 -0.550332,0.178858 -1.265763,0.178858 z m -1.871129,-4.237557 h 3.769774 v -0.04127 q 0,-0.619124 -0.233891,-1.100664 -0.220133,-0.481541 -0.64664,-0.756707 -0.426507,-0.288924 -1.018114,-0.288924 -0.591607,0 -1.018114,0.288924 -0.412749,0.275166 -0.632882,0.756707 -0.220133,0.48154 -0.220133,1.100664 z"
|
||||||
|
id="path981"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 42.076855,23.337578 q -0.137583,0 -0.233891,-0.08255 -0.08255,-0.09631 -0.08255,-0.233891 v -9.135511 q 0,-0.137583 0.08255,-0.220133 0.09631,-0.09631 0.233891,-0.09631 h 0.64664 q 0.151342,0 0.233891,0.09631 0.08255,0.08255 0.08255,0.220133 v 9.135511 q 0,0.137583 -0.08255,0.233891 -0.08255,0.08255 -0.233891,0.08255 z"
|
||||||
|
id="path983"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 47.882832,23.337578 q -0.770465,0 -1.252006,-0.288924 -0.48154,-0.302683 -0.701673,-0.839257 -0.220133,-0.550332 -0.220133,-1.29328 v -3.632191 h -1.073147 q -0.137583,0 -0.233891,-0.08255 -0.08255,-0.09631 -0.08255,-0.233891 v -0.467782 q 0,-0.137583 0.08255,-0.220133 0.09631,-0.09631 0.233891,-0.09631 h 1.073147 v -2.297636 q 0,-0.137583 0.08255,-0.220133 0.09631,-0.09631 0.233891,-0.09631 h 0.64664 q 0.137583,0 0.220133,0.09631 0.09631,0.08255 0.09631,0.220133 v 2.297636 h 1.706029 q 0.137583,0 0.220133,0.09631 0.09631,0.08255 0.09631,0.220133 v 0.467782 q 0,0.137583 -0.09631,0.233891 -0.08255,0.08255 -0.220133,0.08255 h -1.706029 v 3.535883 q 0,0.64664 0.220133,1.018114 0.220133,0.371474 0.784223,0.371474 h 0.839256 q 0.137583,0 0.220133,0.09631 0.09631,0.08255 0.09631,0.220133 v 0.495299 q 0,0.137583 -0.09631,0.233891 -0.08255,0.08255 -0.220133,0.08255 z"
|
||||||
|
id="path985"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 53.317345,23.475161 q -1.417105,0 -2.256361,-0.866773 -0.839256,-0.880531 -0.921806,-2.393944 -0.01376,-0.178858 -0.01376,-0.454024 0,-0.288924 0.01376,-0.467782 0.05503,-0.97684 0.454024,-1.70603 0.398991,-0.742948 1.086906,-1.141938 0.701673,-0.398991 1.637237,-0.398991 1.045631,0 1.747304,0.440266 0.715432,0.440265 1.086906,1.252005 0.371474,0.811739 0.371474,1.898645 v 0.233891 q 0,0.151342 -0.09631,0.233891 -0.08255,0.08255 -0.220133,0.08255 h -4.760372 q 0,0.01376 0,0.05503 0,0.04128 0,0.06879 0.02752,0.56409 0.24765,1.059389 0.220133,0.48154 0.632882,0.784223 0.412749,0.302683 0.990597,0.302683 0.495299,0 0.825498,-0.151342 0.330199,-0.151341 0.536574,-0.330199 0.206374,-0.192616 0.275166,-0.288924 0.123825,-0.178858 0.192616,-0.206375 0.06879,-0.04127 0.220133,-0.04127 h 0.660398 q 0.137583,0 0.220133,0.08255 0.09631,0.06879 0.08255,0.206375 -0.01376,0.206374 -0.220133,0.509057 -0.206374,0.288924 -0.591607,0.577848 -0.385232,0.288925 -0.935564,0.481541 -0.550332,0.178858 -1.265764,0.178858 z m -1.871129,-4.237557 h 3.769775 v -0.04127 q 0,-0.619124 -0.233891,-1.100664 -0.220133,-0.481541 -0.646641,-0.756707 -0.426507,-0.288924 -1.018114,-0.288924 -0.591607,0 -1.018114,0.288924 -0.412749,0.275166 -0.632882,0.756707 -0.220133,0.48154 -0.220133,1.100664 z"
|
||||||
|
id="path987"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g975"
|
||||||
|
style="display:inline">
|
||||||
|
<g
|
||||||
|
id="g4370"
|
||||||
|
style="display:inline"
|
||||||
|
transform="translate(-1.0583342)">
|
||||||
|
<rect
|
||||||
|
style="display:inline;opacity:1;fill:#ff0d2f;fill-opacity:1;stroke:#ff0d2f;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;stop-color:#000000;stop-opacity:1"
|
||||||
|
id="rect26826"
|
||||||
|
width="46.566666"
|
||||||
|
height="46.566669"
|
||||||
|
x="3.1750009"
|
||||||
|
y="19.049999" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#fffdff;stroke-width:1.05833;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;stop-color:#000000;stop-opacity:1"
|
||||||
|
id="rect52610"
|
||||||
|
width="38.100002"
|
||||||
|
height="2.116667"
|
||||||
|
x="11.641667"
|
||||||
|
y="38.099998" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
aria-label="swaf"
|
||||||
|
id="text15483"
|
||||||
|
style="font-size:16.9333px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:Rubik;text-align:end;text-anchor:end;display:none;fill:#fefefe;stroke-width:0.529167"
|
||||||
|
transform="translate(-5.0785949,7.0535064)">
|
||||||
|
<path
|
||||||
|
d="m 19.182091,28.269415 q -0.948264,0 -1.625596,-0.237066 -0.677332,-0.237066 -1.100665,-0.575732 -0.423332,-0.338666 -0.643465,-0.677332 -0.2032,-0.338666 -0.220133,-0.541866 -0.01693,-0.186266 0.118533,-0.287866 0.135466,-0.1016 0.270933,-0.1016 h 0.745065 q 0.1016,0 0.1524,0.03387 0.06773,0.01693 0.169333,0.135466 0.220132,0.237067 0.491065,0.474133 0.270933,0.237066 0.660399,0.389466 0.406399,0.152399 0.999065,0.152399 0.863598,0 1.422397,-0.321732 0.558799,-0.338666 0.558799,-0.982132 0,-0.423332 -0.237066,-0.677332 -0.220133,-0.253999 -0.812799,-0.457199 -0.575732,-0.203199 -1.59173,-0.423332 -1.015998,-0.237066 -1.608664,-0.575732 -0.592665,-0.3556 -0.846665,-0.829732 -0.253999,-0.491066 -0.253999,-1.100665 0,-0.626532 0.372533,-1.202264 0.372532,-0.592665 1.083731,-0.965198 0.728132,-0.372533 1.811863,-0.372533 0.880531,0 1.507064,0.220133 0.626532,0.220133 1.032931,0.558799 0.406399,0.321733 0.609599,0.643466 0.203199,0.321732 0.220133,0.541865 0.01693,0.169333 -0.1016,0.287866 -0.118533,0.1016 -0.270933,0.1016 H 21.38342 q -0.118533,0 -0.203199,-0.0508 -0.06773,-0.0508 -0.135467,-0.118533 -0.169333,-0.220133 -0.406399,-0.440266 -0.220133,-0.220133 -0.592665,-0.355599 -0.3556,-0.1524 -0.948265,-0.1524 -0.846665,0 -1.269998,0.3556 -0.423332,0.355599 -0.423332,0.897464 0,0.321733 0.186266,0.575733 0.186266,0.253999 0.711199,0.457199 0.524932,0.203199 1.557863,0.440266 1.117598,0.220132 1.761064,0.592665 0.643465,0.372533 0.914398,0.863598 0.270933,0.491066 0.270933,1.134531 0,0.711199 -0.423333,1.303865 -0.406399,0.592665 -1.219198,0.948264 -0.795865,0.338666 -1.981196,0.338666 z"
|
||||||
|
id="path841"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 26.874013,28.100082 q -0.220133,0 -0.338666,-0.101599 -0.118533,-0.118534 -0.2032,-0.321733 l -2.404528,-7.857051 q -0.03387,-0.08467 -0.03387,-0.169333 0,-0.1524 0.1016,-0.254 0.118533,-0.1016 0.253999,-0.1016 h 0.745065 q 0.186267,0 0.287867,0.1016 0.101599,0.1016 0.135466,0.186266 l 1.879596,6.383855 2.015063,-6.316121 q 0.03387,-0.118533 0.135466,-0.237067 0.118533,-0.118533 0.338666,-0.118533 h 0.575733 q 0.220132,0 0.338666,0.118533 0.118533,0.118534 0.135466,0.237067 l 2.015063,6.316121 1.879596,-6.383855 q 0.01693,-0.08467 0.118533,-0.186266 0.1016,-0.1016 0.287866,-0.1016 h 0.745065 q 0.1524,0 0.254,0.1016 0.1016,0.1016 0.1016,0.254 0,0.08467 -0.03387,0.169333 l -2.404529,7.857051 q -0.0508,0.203199 -0.186266,0.321733 -0.118533,0.101599 -0.355599,0.101599 h -0.660399 q -0.220133,0 -0.372532,-0.101599 -0.135467,-0.118534 -0.186267,-0.321733 L 30.0744,21.614628 28.110138,27.67675 q -0.06773,0.203199 -0.2032,0.321733 -0.135466,0.101599 -0.372533,0.101599 z"
|
||||||
|
id="path843"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 40.316868,28.269415 q -0.846665,0 -1.540931,-0.338666 -0.694265,-0.338666 -1.117597,-0.914398 -0.423333,-0.575732 -0.423333,-1.303864 0,-1.168398 0.948265,-1.862663 0.948265,-0.694265 2.472262,-0.914398 l 2.523061,-0.355599 v -0.491066 q 0,-0.812799 -0.474132,-1.269998 -0.457199,-0.457199 -1.507064,-0.457199 -0.761998,0 -1.236131,0.3048 -0.457199,0.304799 -0.643465,0.778931 -0.1016,0.254 -0.355599,0.254 h -0.761999 q -0.186266,0 -0.287866,-0.1016 -0.08467,-0.118533 -0.08467,-0.270933 0,-0.253999 0.186266,-0.626532 0.203199,-0.372532 0.609599,-0.728132 0.406399,-0.355599 1.032931,-0.592665 0.643465,-0.254 1.557864,-0.254 1.015998,0 1.710263,0.270933 0.694265,0.254 1.083731,0.694265 0.406399,0.440266 0.575732,0.999065 0.186267,0.558799 0.186267,1.134531 v 5.486389 q 0,0.169333 -0.118534,0.287867 -0.101599,0.101599 -0.270932,0.101599 h -0.778932 q -0.186266,0 -0.287866,-0.101599 -0.1016,-0.118534 -0.1016,-0.287867 v -0.728131 q -0.220133,0.304799 -0.592666,0.609598 -0.372532,0.287866 -0.931331,0.491066 -0.558799,0.186266 -1.371597,0.186266 z m 0.355599,-1.269997 q 0.694265,0 1.269997,-0.287866 0.575733,-0.3048 0.897465,-0.931332 0.338666,-0.626532 0.338666,-1.574797 v -0.474132 l -1.964262,0.287866 q -1.202265,0.169333 -1.811864,0.575732 -0.609598,0.389466 -0.609598,0.999065 0,0.474132 0.270932,0.795865 0.287867,0.304799 0.711199,0.457199 0.440266,0.1524 0.897465,0.1524 z"
|
||||||
|
id="path845"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 48.397167,28.100082 q -0.169333,0 -0.287866,-0.101599 -0.1016,-0.118534 -0.1016,-0.287867 V 20.64943 h -1.43933 q -0.169333,0 -0.287866,-0.101599 -0.1016,-0.118534 -0.1016,-0.287867 v -0.575732 q 0,-0.169333 0.1016,-0.270933 0.118533,-0.118533 0.287866,-0.118533 h 1.43933 v -0.846665 q 0,-0.863598 0.287867,-1.507063 0.287866,-0.660399 0.914398,-1.015998 0.643465,-0.3556 1.710263,-0.3556 h 1.015998 q 0.169333,0 0.270933,0.118533 0.1016,0.1016 0.1016,0.270933 v 0.575732 q 0,0.169333 -0.1016,0.287867 -0.1016,0.101599 -0.270933,0.101599 h -0.982131 q -0.795865,0 -1.083732,0.423333 -0.287866,0.406399 -0.287866,1.185331 v 0.761998 h 2.184396 q 0.169333,0 0.270933,0.118533 0.1016,0.1016 0.1016,0.270933 v 0.575732 q 0,0.169333 -0.1016,0.287867 -0.1016,0.101599 -0.270933,0.101599 h -2.184396 v 7.061186 q 0,0.169333 -0.118533,0.287867 -0.1016,0.101599 -0.270933,0.101599 z"
|
||||||
|
id="path847"
|
||||||
|
style="fill:#ffffff;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 29 KiB |
3
src/assets/package.json
Normal file
3
src/assets/package.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
1
src/assets/scss/_components.scss
Normal file
1
src/assets/scss/_components.scss
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import "data-table";
|
81
src/assets/scss/_fonts.scss
Normal file
81
src/assets/scss/_fonts.scss
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5iU1EQVg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5jU1EQVg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5tU1E.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8cceyI9tScg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8ccezI9tScg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8cce9I9s.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5iU1EQVg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5jU1EQVg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5tU1E.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
136
src/assets/scss/_helpers.scss
Normal file
136
src/assets/scss/_helpers.scss
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
@import "vars";
|
||||||
|
|
||||||
|
@mixin darkMode() {
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin surface($shadowStrength: 0) {
|
||||||
|
background-color: var(--surface);
|
||||||
|
color: var(--on-surface);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-on-surface);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-light-on-surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(a) {
|
||||||
|
color: var(--primary-on-surface);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-light-on-surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// States modifiers
|
||||||
|
.primary:not(.bold) {
|
||||||
|
--color: var(--primary-on-surface);
|
||||||
|
--background-color: var(--surface);
|
||||||
|
}
|
||||||
|
.info:not(.bold) {
|
||||||
|
--color: var(--info-on-surface);
|
||||||
|
--background-color: var(--surface);
|
||||||
|
}
|
||||||
|
.success:not(.bold) {
|
||||||
|
--color: var(--success-on-surface);
|
||||||
|
--background-color: var(--surface);
|
||||||
|
}
|
||||||
|
.warning:not(.bold) {
|
||||||
|
--color: var(--warning-on-surface);
|
||||||
|
--background-color: var(--surface);
|
||||||
|
}
|
||||||
|
.error:not(.bold), .danger:not(.bold) {
|
||||||
|
--color: var(--error-on-surface);
|
||||||
|
--background-color: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
button {
|
||||||
|
--background-color: var(--surface);
|
||||||
|
}
|
||||||
|
button:hover::after {
|
||||||
|
background-color: var(--on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(button:hover::after) {
|
||||||
|
background-color: var(--on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
@if ($shadowStrength > 0) {
|
||||||
|
box-shadow: 0 #{$shadowStrength}px #{$shadowStrength}px #00000045;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin subsurface($shadowStrength: 0) {
|
||||||
|
@include surface($shadowStrength);
|
||||||
|
|
||||||
|
background-color: var(--subsurface);
|
||||||
|
color: var(--on-subsurface);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Responsivity ---
|
||||||
|
@mixin mobile-le {
|
||||||
|
@media (max-width: $mobileThreshold - 1px) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin mobile-ge {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin medium-le {
|
||||||
|
@media (max-width: $desktopThreshold - 1px) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin medium-ge {
|
||||||
|
@media (min-width: $mobileThreshold) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin large-le {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin large-ge {
|
||||||
|
@media (min-width: $desktopThreshold) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
|
||||||
|
@include medium-ge {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include large-ge {
|
||||||
|
width: $desktopThreshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin fake-hide {
|
||||||
|
width: 0.1px;
|
||||||
|
height: 0.1px;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
116
src/assets/scss/_vars.scss
Normal file
116
src/assets/scss/_vars.scss
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
//
|
||||||
|
// --- Color palette ---
|
||||||
|
//
|
||||||
|
$onLight: #222;
|
||||||
|
$onDark: #eee;
|
||||||
|
|
||||||
|
|
||||||
|
// Primary
|
||||||
|
$primary: #0046af;
|
||||||
|
$primaryLight: lighten($primary, 10%);
|
||||||
|
$onPrimary: $onDark;
|
||||||
|
$primaryOnBackground: $primary;
|
||||||
|
$primaryLightOnBackground: $primaryLight;
|
||||||
|
$primaryOnSurface: $primary;
|
||||||
|
$primaryLightOnSurface: $primaryLight;
|
||||||
|
|
||||||
|
$primaryDarkMode: #0054c9;
|
||||||
|
$primaryLightDarkMode: lighten($primaryDarkMode, 23%);
|
||||||
|
$onPrimaryDarkMode: $onDark;
|
||||||
|
$primaryOnBackgroundDarkMode: lighten($primaryDarkMode, 20%);
|
||||||
|
$primaryLightOnBackgroundDarkMode: $primaryLightDarkMode;
|
||||||
|
$primaryOnSurfaceDarkMode: lighten($primaryDarkMode, 29%);
|
||||||
|
$primaryLightOnSurfaceDarkMode: lighten($primaryOnSurfaceDarkMode, 15%);
|
||||||
|
|
||||||
|
|
||||||
|
// Secondary
|
||||||
|
$secondary: #f21170;
|
||||||
|
$onSecondary: $onLight;
|
||||||
|
$secondaryDarkMode: $secondary;
|
||||||
|
$onSecondaryDarkMode: $onSecondary;
|
||||||
|
|
||||||
|
|
||||||
|
// Background
|
||||||
|
$backgroundBase: #eee;
|
||||||
|
$background: mix($backgroundBase, $primary, 98%);
|
||||||
|
$onBackground: $onLight;
|
||||||
|
$backgroundBaseDarkMode: #222;
|
||||||
|
$backgroundDarkMode: mix($backgroundBaseDarkMode, $primaryDarkMode, 98%);
|
||||||
|
$onBackgroundDarkMode: $onDark;
|
||||||
|
|
||||||
|
|
||||||
|
// Surface
|
||||||
|
$surface: lighten($background, 6%);
|
||||||
|
$onSurface: $onLight;
|
||||||
|
$surfaceDarkMode: darken($backgroundDarkMode, 4.5%);
|
||||||
|
$onSurfaceDarkMode: $onDark;
|
||||||
|
|
||||||
|
// Subsurface
|
||||||
|
$subsurface: darken($surface, 3%);
|
||||||
|
$onSubsurface: $onLight;
|
||||||
|
$subsurfaceDarkMode: darken($surfaceDarkMode, 3%);
|
||||||
|
$onSubsurfaceDarkMode: $onDark;
|
||||||
|
|
||||||
|
|
||||||
|
// Input
|
||||||
|
$input: darken($surface, 5%);
|
||||||
|
$onInput: $onLight;
|
||||||
|
$inputDarkMode: darken($surfaceDarkMode, 5%);
|
||||||
|
$onInputDarkMode: $onDark;
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// --- Layout ---
|
||||||
|
//
|
||||||
|
$header: $surface;
|
||||||
|
$headerDarkMode: $surfaceDarkMode;
|
||||||
|
$headerContainer: true;
|
||||||
|
$headerHeight: 72px;
|
||||||
|
$footer: transparent;
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// --- State palette ---
|
||||||
|
//
|
||||||
|
$info: #4499ff;
|
||||||
|
$onInfo: darken($info, 50%);
|
||||||
|
$infoOnBackground: darken($info, 20%);
|
||||||
|
$infoOnSurface: darken($info, 20%);
|
||||||
|
$infoDarkMode: darken($info, 40%);
|
||||||
|
$onInfoDarkMode: lighten($info, 20%);
|
||||||
|
$infoOnBackgroundDarkMode: $info;
|
||||||
|
$infoOnSurfaceDarkMode: $info;
|
||||||
|
|
||||||
|
$success: #55ff55;
|
||||||
|
$onSuccess: darken($success, 45%);
|
||||||
|
$successOnBackground: darken($success, 45%);
|
||||||
|
$successOnSurface: darken($success, 45%);
|
||||||
|
$successDarkMode: darken($success, 45%);
|
||||||
|
$onSuccessDarkMode: lighten($success, 20%);
|
||||||
|
$successOnBackgroundDarkMode: $success;
|
||||||
|
$successOnSurfaceDarkMode: $success;
|
||||||
|
|
||||||
|
$warning: #ffcc00;
|
||||||
|
$onWarning: darken($warning, 30%);
|
||||||
|
$warningOnBackground: darken($warning, 25%);
|
||||||
|
$warningOnSurface: darken($warning, 25%);
|
||||||
|
$warningDarkMode: darken($warning, 30%);
|
||||||
|
$onWarningDarkMode: lighten($warning, 20%);
|
||||||
|
$warningOnBackgroundDarkMode: $warning;
|
||||||
|
$warningOnSurfaceDarkMode: $warning;
|
||||||
|
|
||||||
|
$error: #ff0000;
|
||||||
|
$onError: darken($error, 40%);
|
||||||
|
$errorOnBackground: darken($error, 10%);
|
||||||
|
$errorOnSurface: darken($error, 10%);
|
||||||
|
$errorDarkMode: darken($error, 30%);
|
||||||
|
$onErrorDarkMode: lighten($error, 20%);
|
||||||
|
$errorOnBackgroundDarkMode: lighten($error, 15%);
|
||||||
|
$errorOnSurfaceDarkMode: lighten($error, 3%);
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// --- Responsivity ---
|
||||||
|
//
|
||||||
|
$mobileThreshold: 632px;
|
||||||
|
$desktopThreshold: 940px;
|
263
src/assets/scss/base.scss
Normal file
263
src/assets/scss/base.scss
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
@import "vars";
|
||||||
|
@import "helpers";
|
||||||
|
|
||||||
|
@import "fonts";
|
||||||
|
@import "../../../node_modules/normalize.css/normalize";
|
||||||
|
|
||||||
|
|
||||||
|
// --- Css variables, dark mode ---
|
||||||
|
:root {
|
||||||
|
// Primary
|
||||||
|
--primary: #{$primary};
|
||||||
|
--primary-light: #{$primaryLight};
|
||||||
|
--on-primary: #{$onPrimary};
|
||||||
|
--primary-on-background: #{$primaryOnBackground};
|
||||||
|
--primary-light-on-background: #{$primaryLightOnBackground};
|
||||||
|
--primary-on-surface: #{$primaryOnSurface};
|
||||||
|
--primary-light-on-surface: #{$primaryLightOnSurface};
|
||||||
|
|
||||||
|
// Secondary
|
||||||
|
--secondary: #{$secondary};
|
||||||
|
--on-secondary: #{$onSecondary};
|
||||||
|
|
||||||
|
// Background
|
||||||
|
--background: #{$background};
|
||||||
|
--on-background: #{$onBackground};
|
||||||
|
|
||||||
|
// Surface
|
||||||
|
--surface: #{$surface};
|
||||||
|
--on-surface: #{$onSurface};
|
||||||
|
|
||||||
|
// Subsurface
|
||||||
|
--subsurface: #{$subsurface};
|
||||||
|
--on-subsurface: #{$onSubsurface};
|
||||||
|
|
||||||
|
// Input
|
||||||
|
--input: #{$input};
|
||||||
|
--on-input: #{$onInput};
|
||||||
|
|
||||||
|
|
||||||
|
// States
|
||||||
|
--info: #{$info};
|
||||||
|
--success: #{$success};
|
||||||
|
--warning: #{$warning};
|
||||||
|
--error: #{$error};
|
||||||
|
|
||||||
|
// States text
|
||||||
|
--on-info: #{$onInfo};
|
||||||
|
--on-success: #{$onSuccess};
|
||||||
|
--on-warning: #{$onWarning};
|
||||||
|
--on-error: #{$onError};
|
||||||
|
|
||||||
|
// States text on background
|
||||||
|
--info-on-background: #{$infoOnBackground};
|
||||||
|
--success-on-background: #{$successOnBackground};
|
||||||
|
--warning-on-background: #{$warningOnBackground};
|
||||||
|
--error-on-background: #{$errorOnBackground};
|
||||||
|
|
||||||
|
// States text on surface
|
||||||
|
--info-on-surface: #{$infoOnSurface};
|
||||||
|
--success-on-surface: #{$successOnSurface};
|
||||||
|
--warning-on-surface: #{$warningOnSurface};
|
||||||
|
--error-on-surface: #{$errorOnSurface};
|
||||||
|
|
||||||
|
@include darkMode {
|
||||||
|
// Primary
|
||||||
|
--primary: #{$primaryDarkMode};
|
||||||
|
--primary-light: #{$primaryLightDarkMode};
|
||||||
|
--on-primary: #{$onPrimaryDarkMode};
|
||||||
|
--primary-on-background: #{$primaryOnBackgroundDarkMode};
|
||||||
|
--primary-light-on-background: #{$primaryLightOnBackgroundDarkMode};
|
||||||
|
--primary-on-surface: #{$primaryOnSurfaceDarkMode};
|
||||||
|
--primary-light-on-surface: #{$primaryLightOnSurfaceDarkMode};
|
||||||
|
|
||||||
|
// Secondary
|
||||||
|
--secondary: #{$secondaryDarkMode};
|
||||||
|
--on-secondary: #{$onSecondaryDarkMode};
|
||||||
|
|
||||||
|
// Background
|
||||||
|
--background: #{$backgroundDarkMode};
|
||||||
|
--on-background: #{$onBackgroundDarkMode};
|
||||||
|
|
||||||
|
// Surface
|
||||||
|
--surface: #{$surfaceDarkMode};
|
||||||
|
--on-surface: #{$onSurfaceDarkMode};
|
||||||
|
|
||||||
|
// Subsurface
|
||||||
|
--subsurface: #{$subsurfaceDarkMode};
|
||||||
|
--on-subsurface: #{$onSubsurfaceDarkMode};
|
||||||
|
|
||||||
|
// Input
|
||||||
|
--input: #{$inputDarkMode};
|
||||||
|
--on-input: #{$onInputDarkMode};
|
||||||
|
|
||||||
|
|
||||||
|
// States
|
||||||
|
--info: #{$infoDarkMode};
|
||||||
|
--success: #{$successDarkMode};
|
||||||
|
--warning: #{$warningDarkMode};
|
||||||
|
--error: #{$errorDarkMode};
|
||||||
|
|
||||||
|
// States text
|
||||||
|
--on-info: #{$onInfoDarkMode};
|
||||||
|
--on-success: #{$onSuccessDarkMode};
|
||||||
|
--on-warning: #{$onWarningDarkMode};
|
||||||
|
--on-error: #{$onErrorDarkMode};
|
||||||
|
|
||||||
|
// States text on background
|
||||||
|
--info-on-background: #{$infoOnBackgroundDarkMode};
|
||||||
|
--success-on-background: #{$successOnBackgroundDarkMode};
|
||||||
|
--warning-on-background: #{$warningOnBackgroundDarkMode};
|
||||||
|
--error-on-background: #{$errorOnBackgroundDarkMode};
|
||||||
|
|
||||||
|
// States text on surface
|
||||||
|
--info-on-surface: #{$infoOnSurfaceDarkMode};
|
||||||
|
--success-on-surface: #{$successOnSurfaceDarkMode};
|
||||||
|
--warning-on-surface: #{$warningOnSurfaceDarkMode};
|
||||||
|
--error-on-surface: #{$errorOnSurfaceDarkMode};
|
||||||
|
}
|
||||||
|
|
||||||
|
--color: var(--on-background);
|
||||||
|
--background-color: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-visible,
|
||||||
|
button:focus-visible,
|
||||||
|
[type="button"]:focus-visible,
|
||||||
|
[type="reset"]:focus-visible,
|
||||||
|
[type="submit"]:focus-visible {
|
||||||
|
outline: 3px solid var(--primary-light);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
color: var(--color);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-on-background);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-light-on-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
.feather.feather-external-link { //todo add js
|
||||||
|
--icon-size: 16px;
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-top: -3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: '- ';
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary, .bold {
|
||||||
|
--color: var(--primary-on-background);
|
||||||
|
--background-color: var(--background);
|
||||||
|
|
||||||
|
&.bold {
|
||||||
|
--color: var(--on-primary);
|
||||||
|
--background-color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
--color: var(--info-on-background);
|
||||||
|
--background-color: var(--background);
|
||||||
|
|
||||||
|
&.bold {
|
||||||
|
--color: var(--on-info);
|
||||||
|
--background-color: var(--info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
--color: var(--success-on-background);
|
||||||
|
--background-color: var(--background);
|
||||||
|
|
||||||
|
&.bold {
|
||||||
|
--color: var(--on-success);
|
||||||
|
--background-color: var(--success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
--color: var(--warning-on-background);
|
||||||
|
--background-color: var(--background);
|
||||||
|
|
||||||
|
&.bold {
|
||||||
|
--color: var(--on-warning);
|
||||||
|
--background-color: var(--warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error, .danger {
|
||||||
|
--color: var(--error-on-background);
|
||||||
|
--background-color: var(--background);
|
||||||
|
|
||||||
|
&.bold {
|
||||||
|
--color: var(--on-error);
|
||||||
|
--background-color: var(--error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
button, .button {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
margin: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--color);
|
||||||
|
|
||||||
|
color: var(--color);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
font-weight: bolder;
|
||||||
|
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.bold {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feather {
|
||||||
|
--icon-size: 16px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feather.last {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::after {
|
||||||
|
content: "";
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--on-background);
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
31
src/assets/scss/data-table.scss
Normal file
31
src/assets/scss/data-table.scss
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
border-bottom: 1px solid #39434a;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
thead tr:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
26
src/assets/scss/layout.scss
Normal file
26
src/assets/scss/layout.scss
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
@import "vars";
|
||||||
|
@import "helpers";
|
||||||
|
@import "base";
|
||||||
|
@import "panel";
|
||||||
|
@import "components";
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Nunito Sans", sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--on-background);
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
53
src/assets/scss/panel.scss
Normal file
53
src/assets/scss/panel.scss
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
@import "vars";
|
||||||
|
@import "helpers";
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
position: relative;
|
||||||
|
margin: 16px 0 48px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
@include surface;
|
||||||
|
.panel {
|
||||||
|
@include subsurface;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 16px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .feather:first-child {
|
||||||
|
--icon-size: 24px;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.2;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> h1, > h2, > h3, > h4, > h5, > h6 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
.feather {
|
||||||
|
--icon-size: 24px;
|
||||||
|
margin: 0 16px 0 0;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 16px;
|
||||||
|
height: 0;
|
||||||
|
border-bottom: 1px solid var(--on-surface);
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
src/assets/scss/tip.scss
Normal file
46
src/assets/scss/tip.scss
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
@import "vars";
|
||||||
|
|
||||||
|
@mixin tip {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10000;
|
||||||
|
pointer-events: none;
|
||||||
|
display: block;
|
||||||
|
width: max-content;
|
||||||
|
height: 30px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
line-height: 22px;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--on-surface);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity ease-out 100ms, visibility step-end 150ms;
|
||||||
|
transition-delay: 0ms;
|
||||||
|
background-color: var(--surface);
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
text-transform: initial;
|
||||||
|
font-weight: initial;
|
||||||
|
|
||||||
|
&.top {
|
||||||
|
top: auto;
|
||||||
|
bottom: calc(100% + 8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:active {
|
||||||
|
.tip {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity ease-out 100ms;
|
||||||
|
transition-delay: 150ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
src/assets/ts/WebsocketClient.ts
Normal file
37
src/assets/ts/WebsocketClient.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export default class WebsocketClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param websocketUrl
|
||||||
|
* @param listener
|
||||||
|
* @param reconnectOnCloseAfter time to reconnect after connection fail in ms. -1 to not reconnect automatically.
|
||||||
|
* @param checkFunction
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private readonly websocketUrl: string,
|
||||||
|
private readonly listener: (websocket: WebSocket, e: MessageEvent) => void,
|
||||||
|
private readonly reconnectOnCloseAfter: number = 1000,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public run(): void {
|
||||||
|
const websocket = new WebSocket(this.websocketUrl);
|
||||||
|
websocket.onopen = () => {
|
||||||
|
console.debug('Websocket connected');
|
||||||
|
};
|
||||||
|
websocket.onmessage = (e) => {
|
||||||
|
this.listener(websocket, e);
|
||||||
|
};
|
||||||
|
websocket.onerror = (e) => {
|
||||||
|
console.error('Websocket error', e);
|
||||||
|
};
|
||||||
|
websocket.onclose = (e) => {
|
||||||
|
console.debug('Websocket closed', e.code, e.reason);
|
||||||
|
|
||||||
|
if (this.reconnectOnCloseAfter >= 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.run();
|
||||||
|
}, this.reconnectOnCloseAfter);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
10
src/assets/ts/featherIcons.ts
Normal file
10
src/assets/ts/featherIcons.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import feather from "feather-icons";
|
||||||
|
|
||||||
|
let alreadyReplaced = false;
|
||||||
|
|
||||||
|
export function replaceIcons(once: boolean = true) {
|
||||||
|
if (!once || !alreadyReplaced) {
|
||||||
|
alreadyReplaced = true;
|
||||||
|
feather.replace();
|
||||||
|
}
|
||||||
|
}
|
3
src/assets/ts/stores.ts
Normal file
3
src/assets/ts/stores.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import {writable} from "svelte/store";
|
||||||
|
|
||||||
|
export const locals = writable<Record<string, unknown>>({});
|
27
src/assets/ts/tsconfig.json
Normal file
27
src/assets/ts/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"baseUrl": "../../../intermediates/assets",
|
||||||
|
"rootDir": "../../../intermediates/assets/ts-source",
|
||||||
|
"sourceRoot": "../../../intermediates/assets/ts-source",
|
||||||
|
"outDir": "../../../intermediates/assets/ts",
|
||||||
|
"declaration": false,
|
||||||
|
|
||||||
|
"typeRoots": [],
|
||||||
|
"resolveJsonModule": false,
|
||||||
|
"lib": [
|
||||||
|
"es2020",
|
||||||
|
"DOM"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"../../../intermediates/assets/ts-source/**/*"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../common"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
49
src/assets/views/DesignButtons.svelte
Normal file
49
src/assets/views/DesignButtons.svelte
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "./utils/Icon.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<p style="display: flex; flex-direction: column; align-items: start">
|
||||||
|
<button>Default button</button>
|
||||||
|
<button class="bold">Default bold button</button>
|
||||||
|
<button class="primary">
|
||||||
|
<Icon name="square"/>
|
||||||
|
Primary button
|
||||||
|
</button>
|
||||||
|
<button class="primary bold">
|
||||||
|
<Icon name="square"/>
|
||||||
|
Primary bold button
|
||||||
|
</button>
|
||||||
|
<button class="success">
|
||||||
|
<Icon name="check"/>
|
||||||
|
Success button
|
||||||
|
</button>
|
||||||
|
<button class="success bold">
|
||||||
|
<Icon name="check"/>
|
||||||
|
Success bold button
|
||||||
|
</button>
|
||||||
|
<button class="info">
|
||||||
|
<Icon name="info"/>
|
||||||
|
Info button
|
||||||
|
</button>
|
||||||
|
<button class="info bold">
|
||||||
|
<Icon name="info"/>
|
||||||
|
Info bold button
|
||||||
|
</button>
|
||||||
|
<button class="warning">
|
||||||
|
<Icon name="alert-triangle"/>
|
||||||
|
Warning button
|
||||||
|
</button>
|
||||||
|
<button class="warning bold">
|
||||||
|
<Icon name="alert-triangle"/>
|
||||||
|
Warning bold button
|
||||||
|
</button>
|
||||||
|
<button class="error">
|
||||||
|
<Icon name="x-circle"/>
|
||||||
|
Error button
|
||||||
|
</button>
|
||||||
|
<button class="error bold">
|
||||||
|
<Icon name="x-circle"/>
|
||||||
|
Error bold button
|
||||||
|
</button>
|
||||||
|
</p>
|
31
src/assets/views/auth/account/NamePanel.svelte
Normal file
31
src/assets/views/auth/account/NamePanel.svelte
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<script>
|
||||||
|
import {locals} from "../../../ts/stores";
|
||||||
|
import Message from "../../components/Message.svelte";
|
||||||
|
import Form from "../../utils/Form.svelte";
|
||||||
|
import Field from "../../utils/Field.svelte";
|
||||||
|
|
||||||
|
let newName = '';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2><i data-feather="key"></i> Change name</h2>
|
||||||
|
|
||||||
|
|
||||||
|
{#if $locals.can_change_name}
|
||||||
|
<Form action={$locals.route('change-name')}
|
||||||
|
submitIcon="save" submitText="Change my name {newName.length > 0 ? 'to ' + newName : ''}"
|
||||||
|
confirm="Are you sure you want to change your name to {newName}?">
|
||||||
|
<Field type="text" name="name" icon="user" placeholder="New name" required bind:value={newName}/>
|
||||||
|
|
||||||
|
<Field type="checkbox" name="terms"
|
||||||
|
placeholder="I understand that I can only change my name once every {$locals.name_change_wait_period}"
|
||||||
|
required/>
|
||||||
|
|
||||||
|
<Field type="checkbox" name="terms2"
|
||||||
|
placeholder="I understand that my old name {$locals.user.name} will become available for anyone to take"
|
||||||
|
required/>
|
||||||
|
</Form>
|
||||||
|
{:else}
|
||||||
|
<Message type="info" content="You will be able to change your name in {$locals.can_change_name_in}" sticky discreet/>
|
||||||
|
{/if}
|
||||||
|
</section>
|
30
src/assets/views/auth/account/PasswordPanel.svelte
Normal file
30
src/assets/views/auth/account/PasswordPanel.svelte
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<script>
|
||||||
|
import {locals} from "../../../ts/stores";
|
||||||
|
import Form from "../../utils/Form.svelte";
|
||||||
|
import Field from "../../utils/Field.svelte";
|
||||||
|
|
||||||
|
let removePasswordMode = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2><i data-feather="key"></i> {$locals.has_password ? 'Change' : 'Set'} password</h2>
|
||||||
|
|
||||||
|
{#if removePasswordMode}
|
||||||
|
<Form action={$locals.route('remove-password')}
|
||||||
|
submitIcon="trash" submitText="Remove password" submitClass="danger"
|
||||||
|
confirm="Are you sure you want to remove your password?">
|
||||||
|
<button type="button" on:click={() => removePasswordMode = false}>Go back</button>
|
||||||
|
</Form>
|
||||||
|
{:else}
|
||||||
|
<Form action={$locals.route('change-password')}
|
||||||
|
submitIcon="save" submitText="Set password">
|
||||||
|
{#if $locals.has_password}
|
||||||
|
<Field type="password" name="current_password" icon="key" placeholder="Current password"/>
|
||||||
|
<button type="button" on:click={() => removePasswordMode = true}>Forgot your password?</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Field type="password" name="new_password" icon="key" placeholder="New password" required/>
|
||||||
|
<Field type="password" name="new_password_confirmation" icon="key" placeholder="New password confirmation" required/>
|
||||||
|
</Form>
|
||||||
|
{/if}
|
||||||
|
</section>
|
100
src/assets/views/auth/account/account.svelte
Normal file
100
src/assets/views/auth/account/account.svelte
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<script>
|
||||||
|
import {locals} from "../../../ts/stores";
|
||||||
|
import BaseLayout from "../../layouts/BaseLayout.svelte";
|
||||||
|
import Message from "../../components/Message.svelte";
|
||||||
|
import NamePanel from "./NamePanel.svelte";
|
||||||
|
import PasswordPanel from "./PasswordPanel.svelte";
|
||||||
|
import Form from "../../utils/Form.svelte";
|
||||||
|
import Field from "../../utils/Field.svelte";
|
||||||
|
|
||||||
|
const mainEmail = $locals.main_email?.email;
|
||||||
|
const personalInfoFields = $locals.user_personal_info_fields || [];
|
||||||
|
const emails = $locals.emails || [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BaseLayout title="Account" description="Manage your account settings and data.">
|
||||||
|
<div class="container">
|
||||||
|
<div class="panel">
|
||||||
|
<h2><i data-feather="user"></i> Personal information</h2>
|
||||||
|
|
||||||
|
{#if $locals.display_email_warning && $locals.emails.length <= 0}
|
||||||
|
<Message type="warning" content="To avoid losing access to your account, please add an email address."/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each personalInfoFields as field}
|
||||||
|
<p>{field.name}: {field.value}</p>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if mainEmail}
|
||||||
|
<p>Contact email: {mainEmail} <a href="#emails">More...</a></p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $locals.has_name_component}
|
||||||
|
<NamePanel/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $locals.has_password_component}
|
||||||
|
<PasswordPanel/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2 id="emails"><i data-feather="shield"></i> Email addresses</h2>
|
||||||
|
|
||||||
|
<div class="data-table-container">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Address</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{#each emails as email}
|
||||||
|
{#if email.id === $locals.user.main_email_id}
|
||||||
|
<tr>
|
||||||
|
<td>Main</td>
|
||||||
|
<td>{email.email}</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#each emails as email}
|
||||||
|
{#if email.id !== $locals.user.main_email_id}
|
||||||
|
<tr>
|
||||||
|
<td>Secondary</td>
|
||||||
|
<td>{email.email}</td>
|
||||||
|
<td class="actions">
|
||||||
|
<Form action={$locals.route('set-main-email')} button
|
||||||
|
submitIcon="refresh-ccw" submitText="Set as main address" submitClass="warning"
|
||||||
|
confirm="Are you sure you want to set {email.email} as your main address?">
|
||||||
|
<Field type="hidden" name="id" value={email.id}/>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<Form action={$locals.route('remove-email')} button
|
||||||
|
submitIcon="trash" submitText="Remove" submitClass="danger"
|
||||||
|
confirm="Are you sure you want to delete {email.email}?">
|
||||||
|
<Field type="hidden" name="id" value={email.id}/>
|
||||||
|
</Form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form action={$locals.route('add-email')} class="sub-panel"
|
||||||
|
submitIcon="plus" submitText="Add email address">
|
||||||
|
<h3>Add an email address:</h3>
|
||||||
|
|
||||||
|
<Field type="email" name="email" icon="at-sign" placeholder="Choose a safe email address"
|
||||||
|
hint="An email address we can use to identify you in case you lose access to your account"
|
||||||
|
required/>
|
||||||
|
</Form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
72
src/assets/views/auth/auth.svelte
Normal file
72
src/assets/views/auth/auth.svelte
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {locals} from "../../ts/stores.js";
|
||||||
|
import BaseLayout from "../layouts/BaseLayout.svelte";
|
||||||
|
import Form from "../utils/Form.svelte";
|
||||||
|
import Field from "../utils/Field.svelte";
|
||||||
|
import Icon from "../utils/Icon.svelte";
|
||||||
|
|
||||||
|
let registerUsingMagicLink = $locals.previousFormData()?.['auth_method'] !== 'password';
|
||||||
|
let loginUsingMagicLink = true;
|
||||||
|
|
||||||
|
let queryStr = '';
|
||||||
|
let previousUrl = $locals.getPreviousUrl();
|
||||||
|
if ($locals.query?.redirect_uri) {
|
||||||
|
queryStr = '?' + new URLSearchParams({redirect_uri: $locals.query?.redirect_uri}).toString();
|
||||||
|
} else if (previousUrl) {
|
||||||
|
queryStr = '?' + new URLSearchParams({redirect_uri: previousUrl}).toString();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BaseLayout title="Authentication / Registration"
|
||||||
|
description="Join {$locals.app.name} and share your files!"
|
||||||
|
h1="Authentication and registration">
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<section class="panel">
|
||||||
|
<h2><i data-feather="log-in"></i> Log in</h2>
|
||||||
|
|
||||||
|
<Form action={$locals.route('login') + queryStr} submitText="Authenticate" submitIcon="log-in">
|
||||||
|
<Field type="text" name="identifier" value={$locals.query?.identifier} icon="at-sign"
|
||||||
|
hint={loginUsingMagicLink ? 'You will receive a magic link in your mailbox. Click on the link from any device to authenticate here.' : ''}
|
||||||
|
placeholder="Your email address or username" required/>
|
||||||
|
|
||||||
|
{#if !loginUsingMagicLink}
|
||||||
|
<Field type="password" name="password" placeholder="Your password" icon="key" required/>
|
||||||
|
<button on:click={() => loginUsingMagicLink=true} type="button"><Icon name="mail"/> Use magic link</button>
|
||||||
|
{:else}
|
||||||
|
<button on:click={() => loginUsingMagicLink=false} type="button"><Icon name="key"/> Use password</button>
|
||||||
|
{/if}
|
||||||
|
<Field type="checkbox" name="persist_session" icon="clock" placeholder="Stay logged in on this computer."/>
|
||||||
|
</Form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2><i data-feather="user-plus"></i> Register</h2>
|
||||||
|
|
||||||
|
<Form action={$locals.route('register') + queryStr} submitText="Register" submitIcon="check">
|
||||||
|
<Field type="hidden" name="auth_method" value={registerUsingMagicLink ? 'magic_link': 'password'}/>
|
||||||
|
|
||||||
|
{#if $locals.has_username}
|
||||||
|
<Field type="text" name={registerUsingMagicLink ? 'name' : 'identifier'} icon="user"
|
||||||
|
placeholder="Choose your username"
|
||||||
|
pattern="[0-9a-z_-]+" required/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if registerUsingMagicLink}
|
||||||
|
<Field type="email" name="identifier" icon="at-sign" placeholder="Your email address"
|
||||||
|
hint="You will receive a magic link in your mailbox. Click on the link from any device to register here."
|
||||||
|
required/>
|
||||||
|
<button on:click={() => registerUsingMagicLink=false} type="button"><Icon name="key"/>Use password</button>
|
||||||
|
{:else}
|
||||||
|
<Field type="password" name="password" icon="key" placeholder="Choose a password" required/>
|
||||||
|
<Field type="password" name="password_confirmation" icon="key" placeholder="Confirm your password" required/>
|
||||||
|
<button on:click={() => registerUsingMagicLink=true} type="button"><Icon name="at-sign"/>Use email address instead</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Field type="checkbox" name="terms" icon="file-text" required>
|
||||||
|
I accept the <a href="/terms-of-services" target="_blank">Terms Of Services</a>.
|
||||||
|
</Field>
|
||||||
|
</Form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
75
src/assets/views/backend/accounts_approval.svelte
Normal file
75
src/assets/views/backend/accounts_approval.svelte
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {locals} from "../../ts/stores";
|
||||||
|
import BaseLayout from "../layouts/BaseLayout.svelte";
|
||||||
|
import Pagination from "../components/Pagination.svelte";
|
||||||
|
import Form from "../utils/Form.svelte";
|
||||||
|
import Field from "../utils/Field.svelte";
|
||||||
|
import Breadcrumb from "../components/Breadcrumb.svelte";
|
||||||
|
import {route} from "../../../common/Routing";
|
||||||
|
|
||||||
|
const accounts = $locals.accounts || [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
td.empty {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<BaseLayout title="{$locals.app.name} - Review accounts" h1={false}>
|
||||||
|
<Breadcrumb currentPageTitle="Accounts pending review" pages={[
|
||||||
|
{link: route('backend'), title:'Backend'},
|
||||||
|
]}/>
|
||||||
|
|
||||||
|
<h1>Accounts pending review</h1>
|
||||||
|
|
||||||
|
<Pagination pagination={$locals.pagination} routeName="accounts-approval" contextSize="3" />
|
||||||
|
|
||||||
|
<div class="panel data-table-container">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="shrink-col">#</th>
|
||||||
|
{#if $locals.has_user_name_component}
|
||||||
|
<th>Name</th>
|
||||||
|
{/if}
|
||||||
|
<th>Main email</th>
|
||||||
|
<th>Registered at</th>
|
||||||
|
<th class="shrink-col">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each accounts as user}
|
||||||
|
<tr>
|
||||||
|
<td>{user.id}</td>
|
||||||
|
{#if $locals.has_user_name_component}
|
||||||
|
<td>{user.name}</td>
|
||||||
|
{/if}
|
||||||
|
<td>{user.mainEmailStr || 'No email'}</td>
|
||||||
|
<td><time datetime={user.created_at_iso}>{user.created_at_human} ago</time></td>
|
||||||
|
<td>
|
||||||
|
<div class="max-content">
|
||||||
|
<Form action={$locals.route('approve-account')}
|
||||||
|
submitIcon="check" submitText="Approve" submitClass="success">
|
||||||
|
<Field type="hidden" name="user_id" value={user.id}/>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<Form action={$locals.route('reject-account')}
|
||||||
|
submitIcon="trash" submitText="Reject" submitClass="danger"
|
||||||
|
confirm="This will irrevocably delete the {user.mainEmailStr || user.name || user.id} account.">
|
||||||
|
<Field type="hidden" name="user_id" value={user.id}/>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{:else}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="empty">No account to review.</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Pagination pagination={$locals.pagination} routeName="accounts-approval" contextSize="3" />
|
||||||
|
</BaseLayout>
|
32
src/assets/views/backend/index.svelte
Normal file
32
src/assets/views/backend/index.svelte
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {locals} from "../../ts/stores.js";
|
||||||
|
import BaseLayout from "../layouts/BaseLayout.svelte";
|
||||||
|
import Breadcrumb from "../components/Breadcrumb.svelte";
|
||||||
|
|
||||||
|
const menu = $locals.menu || [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BaseLayout title="{$locals.app.name} backend" h1={false}>
|
||||||
|
<div class="container">
|
||||||
|
<Breadcrumb currentPageTitle="Backend"/>
|
||||||
|
|
||||||
|
<h1>App administration</h1>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
{#each menu as element}
|
||||||
|
<li>
|
||||||
|
<a href={element.link}>
|
||||||
|
{#if element.display_icon !== null}
|
||||||
|
<i data-feather={element.display_icon}></i>
|
||||||
|
{/if}
|
||||||
|
{element.display_string}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
41
src/assets/views/components/Breadcrumb.svelte
Normal file
41
src/assets/views/components/Breadcrumb.svelte
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from "../utils/Icon.svelte";
|
||||||
|
|
||||||
|
export let currentPageTitle: string;
|
||||||
|
export let pages: { link: string, title: string }[] = [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
ol {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 0 8px;
|
||||||
|
|
||||||
|
list-style: none;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
white-space: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
a, span {
|
||||||
|
display: block;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb panel">
|
||||||
|
{#each pages as page}
|
||||||
|
<li><a href={page.link}>{page.title}</a> <Icon name="chevron-right"/></li>
|
||||||
|
{/each}
|
||||||
|
<li class="active" aria-current="page"><span>{currentPageTitle}</span></li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
22
src/assets/views/components/FlashMessages.svelte
Normal file
22
src/assets/views/components/FlashMessages.svelte
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {locals} from "../../ts/stores.js";
|
||||||
|
import Message from "./Message.svelte";
|
||||||
|
|
||||||
|
export let flashed = $locals.flash();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.messages :global(.message:not(:last-child)) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="messages">
|
||||||
|
{#if flashed}
|
||||||
|
{#each Object.entries(flashed) as [key, bag], i}
|
||||||
|
{#each bag as content}
|
||||||
|
<Message type={key} content={content}/>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
123
src/assets/views/components/Message.svelte
Normal file
123
src/assets/views/components/Message.svelte
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "../utils/Icon.svelte";
|
||||||
|
|
||||||
|
export let type;
|
||||||
|
export let content;
|
||||||
|
export let raw = false;
|
||||||
|
export let discreet = false;
|
||||||
|
export let sticky = false;
|
||||||
|
|
||||||
|
let icon = undefined;
|
||||||
|
switch (type) {
|
||||||
|
case 'success':
|
||||||
|
icon = 'check';
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
icon = 'info';
|
||||||
|
break;
|
||||||
|
case 'warning':
|
||||||
|
icon = 'alert-triangle';
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
icon = 'x-circle';
|
||||||
|
break;
|
||||||
|
case 'error-alert':
|
||||||
|
icon = 'alert-circle';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message;
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
message.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../scss/vars";
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 8px 16px;
|
||||||
|
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
:global(.feather) {
|
||||||
|
--icon-size: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(&-discreet) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.33);
|
||||||
|
|
||||||
|
&[data-type=info], &[data-type=question] {
|
||||||
|
color: var(--on-info);
|
||||||
|
background-color: var(--info);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type=success] {
|
||||||
|
color: var(--on-success);
|
||||||
|
background-color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type=warning] {
|
||||||
|
color: var(--on-warning);
|
||||||
|
background-color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type=error], &[data-type=error-alert] {
|
||||||
|
color: var(--on-error);
|
||||||
|
background-color: var(--error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-discreet {
|
||||||
|
color: var(--on-surface);
|
||||||
|
|
||||||
|
.feather {
|
||||||
|
--icon-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
:global(.feather) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="message" class:message-discreet={discreet} data-type="{type}" bind:this={message}>
|
||||||
|
{#if icon}
|
||||||
|
<Icon name={icon}/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<span class="content">
|
||||||
|
{#if raw}
|
||||||
|
{@html content}
|
||||||
|
{:else}
|
||||||
|
{content}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{#if !sticky}
|
||||||
|
<button type="button" on:click={hide}>
|
||||||
|
<Icon name="x"/>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
123
src/assets/views/components/NavMenu.svelte
Normal file
123
src/assets/views/components/NavMenu.svelte
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<script>
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
import Icon from "../utils/Icon.svelte";
|
||||||
|
|
||||||
|
let open = false;
|
||||||
|
let locked = false;
|
||||||
|
|
||||||
|
function stopPropagation(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMenu() {
|
||||||
|
if (locked) return;
|
||||||
|
open = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMenu() {
|
||||||
|
if (locked) return;
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lock() {
|
||||||
|
locked = true;
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
locked = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let nav;
|
||||||
|
onMount(() => {
|
||||||
|
nav.querySelectorAll('ul li > a, ul li > form > button')
|
||||||
|
.forEach(el => {
|
||||||
|
el.addEventListener('focus', () => {
|
||||||
|
openMenu();
|
||||||
|
});
|
||||||
|
el.addEventListener('blur', () => {
|
||||||
|
closeMenu();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../scss/vars";
|
||||||
|
@import "../../scss/helpers";
|
||||||
|
|
||||||
|
nav {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
@include medium-le {
|
||||||
|
z-index: 1;
|
||||||
|
position: fixed;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
@include surface(3);
|
||||||
|
|
||||||
|
transition: transform ease-out 150ms;
|
||||||
|
|
||||||
|
&:not(.open) {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@include large-ge {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: #{$headerHeight - 16px};
|
||||||
|
height: #{$headerHeight - 16px};
|
||||||
|
margin: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--on-surface);
|
||||||
|
|
||||||
|
border-radius: $headerHeight;
|
||||||
|
|
||||||
|
:global(.feather) {
|
||||||
|
--icon-size: 28px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include large-ge {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<svelte:window on:click={closeMenu}/>
|
||||||
|
|
||||||
|
<button on:click={openMenu} on:click={stopPropagation}
|
||||||
|
on:focus={openMenu} on:blur={closeMenu}
|
||||||
|
tabindex="0" aria-label="Toggle menu">
|
||||||
|
<Icon name="menu"/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<nav class:open on:click={openMenu} on:click={stopPropagation} on:mousedown={lock} bind:this={nav}
|
||||||
|
aria-hidden={open ? 'false' : 'true'}>
|
||||||
|
<ul>
|
||||||
|
<slot/>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
76
src/assets/views/components/NavMenuItem.svelte
Normal file
76
src/assets/views/components/NavMenuItem.svelte
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "../utils/Icon.svelte";
|
||||||
|
import Form from "../utils/Form.svelte";
|
||||||
|
|
||||||
|
export let href;
|
||||||
|
export let icon;
|
||||||
|
export let text;
|
||||||
|
export let action = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../scss/helpers";
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
@include medium-le {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include large-ge {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.07);
|
||||||
|
|
||||||
|
@include darkMode {
|
||||||
|
background-color: rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(form) {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
:global(button) {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.feather) {
|
||||||
|
--icon-size: 16px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
{#if action}
|
||||||
|
<Form action={href} submitIcon={icon} submitText={text}/>
|
||||||
|
{:else}
|
||||||
|
<a href={href}>
|
||||||
|
<Icon name={icon}/>
|
||||||
|
<span class="tip">{text}</span></a>
|
||||||
|
{/if}
|
||||||
|
</li>
|
88
src/assets/views/components/Pagination.svelte
Normal file
88
src/assets/views/components/Pagination.svelte
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {route} from "../../../common/Routing.js";
|
||||||
|
import {Pagination} from "../../../common/Pagination.js";
|
||||||
|
import Icon from "../utils/Icon.svelte";
|
||||||
|
|
||||||
|
export let pagination: string;
|
||||||
|
export let routeName: string;
|
||||||
|
export let contextSize: number;
|
||||||
|
|
||||||
|
if (typeof contextSize !== 'number') contextSize = parseInt(contextSize);
|
||||||
|
|
||||||
|
$: paginationObj = pagination ? Pagination.deserialize(pagination) : null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../scss/helpers";
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 8px auto;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
list-style: none;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
a, span {
|
||||||
|
display: block;
|
||||||
|
padding: 8px 12px;
|
||||||
|
|
||||||
|
&:not(span):hover {
|
||||||
|
background: #0002;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background: #fff2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{#if paginationObj && (paginationObj.hasPrevious() || paginationObj.hasNext())}
|
||||||
|
<nav class="pagination">
|
||||||
|
<ul class="panel">
|
||||||
|
{#if paginationObj.hasPrevious()}
|
||||||
|
<li><a href={route(routeName, {page: paginationObj.page - 1})}>
|
||||||
|
<Icon name="chevron-left"/>
|
||||||
|
Previous
|
||||||
|
</a></li>
|
||||||
|
|
||||||
|
{#each paginationObj.previousPages(contextSize) as i}
|
||||||
|
{#if i === -1}
|
||||||
|
<li class="ellipsis"><span>...</span></li>
|
||||||
|
{:else}
|
||||||
|
<li><a href={route(routeName, {page: i})}>{i}</a></li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<li class="active"><span>{paginationObj.page}</span></li>
|
||||||
|
|
||||||
|
{#if paginationObj.hasNext()}
|
||||||
|
{#each paginationObj.nextPages(contextSize) as i}
|
||||||
|
{#if i === -1}
|
||||||
|
<li class="ellipsis"><span>...</span></li>
|
||||||
|
{:else}
|
||||||
|
<li><a href={route(routeName, {page: i})}>{i}</a></li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<li><a href={route(routeName, {page: paginationObj.page + 1})}>
|
||||||
|
Next
|
||||||
|
<Icon name="chevron-right"/>
|
||||||
|
</a></li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{/if}
|
119
src/assets/views/design.svelte
Normal file
119
src/assets/views/design.svelte
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<script>
|
||||||
|
import {route} from "../../common/Routing";
|
||||||
|
import BaseLayout from "./layouts/BaseLayout.svelte";
|
||||||
|
import Message from "./components/Message.svelte";
|
||||||
|
import Form from "./utils/Form.svelte";
|
||||||
|
import Field from "./utils/Field.svelte";
|
||||||
|
import Pagination from "./components/Pagination.svelte";
|
||||||
|
import {Pagination as PaginationData} from "../../common/Pagination.js";
|
||||||
|
import Breadcrumb from "./components/Breadcrumb.svelte";
|
||||||
|
import Icon from "./utils/Icon.svelte";
|
||||||
|
import DesignButtons from "./DesignButtons.svelte";
|
||||||
|
|
||||||
|
const paginationData = new PaginationData(20, 20, 1000).serialize();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BaseLayout title="Design test">
|
||||||
|
<DesignButtons/>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2>
|
||||||
|
<Icon name="git-commit"/>
|
||||||
|
Common elements
|
||||||
|
</h2>
|
||||||
|
<p>Paragraph</p>
|
||||||
|
|
||||||
|
<p>Paragraph, <a href={route('home')}>Link</a></p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Unordered</li>
|
||||||
|
<li>List</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Ordered</li>
|
||||||
|
<li>List</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<DesignButtons/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2>
|
||||||
|
<Icon name="eye-off"/>
|
||||||
|
Discreet messages
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Message type="success" content="Discreet success." discreet/>
|
||||||
|
<Message type="info" content="Discreet info." discreet/>
|
||||||
|
<Message type="warning" content="Discreet warning." discreet/>
|
||||||
|
<Message type="error" content="Discreet sticky error." discreet sticky/>
|
||||||
|
<Message type="error-alert" content="Discreet error-alert." discreet/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2>
|
||||||
|
<Icon name="file-text"/>
|
||||||
|
Forms
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Form action="javascript: void(0)"
|
||||||
|
submitText="Submit" submitIcon="check"
|
||||||
|
confirm="Are you sure?">
|
||||||
|
|
||||||
|
<Field type="text" name="text" placeholder="Text field" icon="type"
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
<Field type="password" name="password" placeholder="Password field" icon="key"
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
<Field type="duration" name="duration" placeholder="Duration field" icon="clock" extraData={{
|
||||||
|
h: 'Hours',
|
||||||
|
m: 'Minutes',
|
||||||
|
s: 'Seconds',
|
||||||
|
}} hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
<Field type="select" name="select" placeholder="Select field" icon="list" extraData={[
|
||||||
|
'Option 1',
|
||||||
|
'Option 2',
|
||||||
|
'Option 3',
|
||||||
|
]} hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
<Field type="textarea" name="textarea" placeholder="Textarea field" icon="file-text"
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
<Field type="checkbox" name="checkbox" placeholder="Checkbox field" icon="check-square"
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
|
||||||
|
<Field type="color" name="color" placeholder="Color field" icon="aperture"
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
|
||||||
|
<Field type="file" name="file" placeholder="Choose a file" icon="upload"
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
<Field type="file" name="files" placeholder="Choose files" icon="upload" multiple
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
|
||||||
|
<Field type="text" name="disabled_text" placeholder="Disabled text field" icon="type" disabled
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2>
|
||||||
|
<Icon name="file"/>
|
||||||
|
Pagination
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Pagination routeName="home" contextSize="3" pagination={paginationData}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2>
|
||||||
|
<Icon name="map-pin"/>
|
||||||
|
Breadcrumb
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Breadcrumb currentPageTitle="Design test" pages={[
|
||||||
|
{link: route('home'), title: 'Home'},
|
||||||
|
{link: route('home'), title: 'Home again'},
|
||||||
|
{link: route('design'), title: 'self'},
|
||||||
|
]}/>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
|
5
src/assets/views/errors/400.svelte
Normal file
5
src/assets/views/errors/400.svelte
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
import Error from "./Error.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Error code={400} message="Bad request." />
|
5
src/assets/views/errors/401.svelte
Normal file
5
src/assets/views/errors/401.svelte
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
import Error from "./Error.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Error code={401} message="Unauthorized." />
|
5
src/assets/views/errors/403.svelte
Normal file
5
src/assets/views/errors/403.svelte
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
import Error from "./Error.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Error code={403} message="Forbidden" />
|
5
src/assets/views/errors/404.svelte
Normal file
5
src/assets/views/errors/404.svelte
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
import Error from "./Error.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Error code={404} message="Page not found." />
|
5
src/assets/views/errors/429.svelte
Normal file
5
src/assets/views/errors/429.svelte
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
import Error from "./Error.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Error code={429} message="Too many requests." />
|
5
src/assets/views/errors/500.svelte
Normal file
5
src/assets/views/errors/500.svelte
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
import Error from "./Error.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Error code={500} message="Internal server error." />
|
5
src/assets/views/errors/503.svelte
Normal file
5
src/assets/views/errors/503.svelte
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
import Error from "./Error.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Error code={503} message="Service unavailable." />
|
47
src/assets/views/errors/Error.svelte
Normal file
47
src/assets/views/errors/Error.svelte
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {locals} from "../../ts/stores.js";
|
||||||
|
|
||||||
|
const previousUrl = $locals.getPreviousUrl();
|
||||||
|
|
||||||
|
export let code;
|
||||||
|
code = $locals.error_code || code;
|
||||||
|
|
||||||
|
export let message;
|
||||||
|
message = $locals.error_message || message;
|
||||||
|
|
||||||
|
export let instructions;
|
||||||
|
instructions = $locals.error_instructions || instructions;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{code + ' - ' + message}</title>
|
||||||
|
<link rel="stylesheet" href="/css/error.css">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="logo"><a href="/">{$locals.app.name}</a></div>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<!--TODO: flash messages-->
|
||||||
|
|
||||||
|
<div class="error-code">{code}</div>
|
||||||
|
<div class="error-message">{message}</div>
|
||||||
|
{#if instructions}
|
||||||
|
<div class="error-instructions">{@html instructions}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
{#if previousUrl && previousUrl !== '/' && previousUrl !== $locals.url}
|
||||||
|
<a href={previousUrl} class="button"><i data-feather="arrow-left"></i> Go back</a>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<a href="/" class="button"><i data-feather="home"></i> Go to homepage</a>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div class="contact">
|
||||||
|
<p>Error ID: {$locals.error_id || 'Request has no indentifier.'}</p>
|
||||||
|
<p>
|
||||||
|
If you think this isn't right, please contact us with the above error ID at
|
||||||
|
<a href="mailto:{$locals.app.contact_email}">{$locals.app.contact_email}</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
19
src/assets/views/home.svelte
Normal file
19
src/assets/views/home.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {locals} from "../ts/stores";
|
||||||
|
import {route} from "../../common/Routing";
|
||||||
|
import BaseLayout from "./layouts/BaseLayout.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BaseLayout title="{$locals.app.name}" h1={false}>
|
||||||
|
<div class="panel">
|
||||||
|
<h1>swaf - Svelte Web Application Framework</h1>
|
||||||
|
<p>Welcome to {$locals.app.name}!</p>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a href={route('tests')}>Frontend tests</a></li>
|
||||||
|
<li><a href={route('design')}>Design test</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
126
src/assets/views/layouts/BaseLayout.svelte
Normal file
126
src/assets/views/layouts/BaseLayout.svelte
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {locals} from "../../ts/stores.js";
|
||||||
|
import {route} from "../../../common/Routing.js";
|
||||||
|
import FlashMessages from "../components/FlashMessages.svelte";
|
||||||
|
import NavMenuItem from "../components/NavMenuItem.svelte";
|
||||||
|
import NavMenu from "../components/NavMenu.svelte";
|
||||||
|
|
||||||
|
export let title: string;
|
||||||
|
export let h1: string = title;
|
||||||
|
export let description: string;
|
||||||
|
export let refresh_after: number | undefined = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../scss/vars";
|
||||||
|
@import "../../scss/helpers";
|
||||||
|
|
||||||
|
header {
|
||||||
|
@if $headerContainer {
|
||||||
|
@include container;
|
||||||
|
}
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
height: $headerHeight;
|
||||||
|
|
||||||
|
@include medium-le {
|
||||||
|
z-index: 1;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
|
@include surface(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: initial;
|
||||||
|
height: calc(#{$headerHeight} - 16px);
|
||||||
|
margin-right: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
@include container;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-messages {
|
||||||
|
@include container;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
|
||||||
|
<title>{title || 'Undefined title'}</title>
|
||||||
|
{#if description}
|
||||||
|
<meta name="description" content={description}>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<link rel="shortcut icon" type="image/png" href="/img/logox1024.png">
|
||||||
|
<link rel="shortcut icon" type="image/png" href="/img/logox128.png">
|
||||||
|
<link rel="shortcut icon" type="image/svg" href="/img/logo.svg">
|
||||||
|
|
||||||
|
{#if refresh_after}
|
||||||
|
<meta http-equiv="refresh" content={refresh_after}>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/css/layout.css">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<a href="/" class="logo"><img src="/img/logo.svg" alt="{$locals.app.name} logo"> {$locals.app.name}</a>
|
||||||
|
<NavMenu>
|
||||||
|
{#if $locals.user}
|
||||||
|
{#if $locals.user.is_admin}
|
||||||
|
<NavMenuItem href={route('backend')} icon="settings" text="Backend"/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<NavMenuItem href={route('account')} icon="user" text={$locals.user.name || 'Account'}/>
|
||||||
|
<NavMenuItem href={route('logout')} icon="log-out" text="Logout" action/>
|
||||||
|
{:else}
|
||||||
|
<NavMenuItem href={route('auth')} icon="log-in" text="Log in / Register"/>
|
||||||
|
{/if}
|
||||||
|
</NavMenu>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flash-messages">
|
||||||
|
<FlashMessages/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{#if h1}
|
||||||
|
<h1>{h1}</h1>
|
||||||
|
{/if}
|
||||||
|
{#if $$slots.subtitle}
|
||||||
|
<p>
|
||||||
|
<slot name="subtitle"/>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<slot/>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>{$locals.app.name} v{$locals.app_version} - all rights reserved.</footer>
|
34
src/assets/views/layouts/svelte_layout.html
Normal file
34
src/assets/views/layouts/svelte_layout.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
%head%
|
||||||
|
|
||||||
|
<style>%css%</style>
|
||||||
|
<script type="module" defer>
|
||||||
|
import View from '/js/views/%canonicalViewName%.js';
|
||||||
|
import * as Routing from '/js/Routing.js';
|
||||||
|
const setRoutes = Routing.R.setRoutes;
|
||||||
|
const setPublicUrl = Routing.R.setPublicUrl;
|
||||||
|
import * as stores from '/js/stores.js';
|
||||||
|
const localStore = stores.l;
|
||||||
|
|
||||||
|
const localMap = %locals%;
|
||||||
|
localStore.set((key, args) => {
|
||||||
|
return localMap[args ?
|
||||||
|
`'${key}', \`${args}\``
|
||||||
|
: `'${key}'`];
|
||||||
|
});
|
||||||
|
|
||||||
|
setRoutes(%routes%);
|
||||||
|
setPublicUrl(%publicUrl%);
|
||||||
|
|
||||||
|
new View({
|
||||||
|
hydrate: true,
|
||||||
|
target: document.body,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
%html%
|
||||||
|
</body>
|
||||||
|
</html>
|
21
src/assets/views/magic_link.svelte
Normal file
21
src/assets/views/magic_link.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
import {locals} from "../ts/stores";
|
||||||
|
import BaseLayout from "./layouts/BaseLayout.svelte";
|
||||||
|
import Message from "./components/Message.svelte";
|
||||||
|
|
||||||
|
const actionType = $locals.magicLink?.action_type;
|
||||||
|
const h1 = 'Magic Link' + (actionType ? (' - ' + actionType) : '');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BaseLayout title="{$locals.app.name} {h1}" {h1}>
|
||||||
|
<div class="container">
|
||||||
|
<div class="panel">
|
||||||
|
{#if $locals.err}
|
||||||
|
<Message type="error" content={$locals.err}/>
|
||||||
|
{:else}
|
||||||
|
<Message type="success" content="Success!"/>
|
||||||
|
<p>You can now close this page.</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
51
src/assets/views/magic_link_lobby.svelte
Normal file
51
src/assets/views/magic_link_lobby.svelte
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {locals} from "../ts/stores.js";
|
||||||
|
import BaseLayout from "./layouts/BaseLayout.svelte";
|
||||||
|
import Message from "./components/Message.svelte";
|
||||||
|
import WebsocketClient from "../ts/WebsocketClient.js";
|
||||||
|
import {Time} from "../../common/Time.js";
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
|
||||||
|
const validUntil = parseFloat($locals.validUntil as string);
|
||||||
|
|
||||||
|
function isValid() {
|
||||||
|
return new Date().getTime() < validUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
let countdown;
|
||||||
|
let validUntilDate = new Date(validUntil);
|
||||||
|
$: countdown = $locals.isPreRender ? '...' : Time.humanizeTimeTo(validUntilDate);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
validUntilDate = new Date(validUntil);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
if (isValid()) {
|
||||||
|
const webSocket = new WebsocketClient($locals.websocketUrl as string, (websocket, e) => {
|
||||||
|
if (e.data === 'refresh') {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
webSocket.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BaseLayout h1="Authentication lobby" title="{$locals.app.name} authentication lobby">
|
||||||
|
<div class="container">
|
||||||
|
<div class="panel">
|
||||||
|
<Message type="success" sticky
|
||||||
|
content={`We sent a link to ${$locals.email}. To authenticate, open it from any device.`}/>
|
||||||
|
<Message type="info" discreet sticky raw
|
||||||
|
content={`This link will be valid for ${countdown} and can only be used once.`}/>
|
||||||
|
|
||||||
|
<p class="center">Waiting for you to open the link...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'mails/base_layout.mjml.njk' %}
|
{% extends 'mails/base_layout.mnjk' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<mj-section>
|
<mj-section>
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'mails/base_layout.mjml.njk' %}
|
{% extends 'mails/base_layout.mnjk' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<mj-section>
|
<mj-section>
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'mails/base_layout.mjml.njk' %}
|
{% extends 'mails/base_layout.mnjk' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<mj-section>
|
<mj-section>
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'mails/base_layout.mjml.njk' %}
|
{% extends 'mails/base_layout.mnjk' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<mj-section>
|
<mj-section>
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'mails/base_layout.mjml.njk' %}
|
{% extends 'mails/base_layout.mnjk' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<mj-section>
|
<mj-section>
|
8
src/assets/views/tests.svelte
Normal file
8
src/assets/views/tests.svelte
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import AllTests from "./AllTests.svelte";
|
||||||
|
import BaseLayout from "./layouts/BaseLayout.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BaseLayout title="Frontend tests">
|
||||||
|
<AllTests/>
|
||||||
|
</BaseLayout>
|
15
src/assets/views/tsconfig.json
Normal file
15
src/assets/views/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "public/js",
|
||||||
|
"rootDir": "../../../intermediates/assets",
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/assets/ts/**/*"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../common"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
6
src/assets/views/utils/CsrfTokenField.svelte
Normal file
6
src/assets/views/utils/CsrfTokenField.svelte
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {locals} from "../../ts/stores.js";
|
||||||
|
import Field from "./Field.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field type="hidden" name="csrf" value={$locals.getCsrfToken()}/>
|
459
src/assets/views/utils/Field.svelte
Normal file
459
src/assets/views/utils/Field.svelte
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {locals} from '../../ts/stores.js';
|
||||||
|
import {FileSize} from "../../../common/FileSize.js";
|
||||||
|
import Message from "../components/Message.svelte";
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
|
||||||
|
export let type: string;
|
||||||
|
export let name: string;
|
||||||
|
type FieldValue = string | number | Record<string, FieldValue>;
|
||||||
|
export let value: FieldValue | undefined = undefined;
|
||||||
|
export let placeholder: string | undefined = undefined;
|
||||||
|
export let hint: string | undefined = undefined;
|
||||||
|
export let extraData: string[] | undefined = undefined;
|
||||||
|
export let icon: string | undefined = undefined;
|
||||||
|
export let validation: { message: string, value?: string } | undefined = $locals.validation()?.[name];
|
||||||
|
|
||||||
|
const formId = getContext('formId');
|
||||||
|
const fieldId = `${formId}-${name}-field`;
|
||||||
|
|
||||||
|
const previousFormData = $locals.previousFormData() || [];
|
||||||
|
|
||||||
|
value = type !== 'hidden' && previousFormData[name] || value || validation?.value || '';
|
||||||
|
|
||||||
|
function durationValue(f: string): number {
|
||||||
|
if (previousFormData[name]) {
|
||||||
|
return value[f];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (f) {
|
||||||
|
case 's':
|
||||||
|
return value % 60;
|
||||||
|
case 'm':
|
||||||
|
return (value - value % 60) / 60 % 60;
|
||||||
|
case 'h':
|
||||||
|
return (value - value % 3600) / 3600;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusInput() {
|
||||||
|
if (input) {
|
||||||
|
if (['file', 'checkbox', 'color'].indexOf(input.type) >= 0) {
|
||||||
|
input.click();
|
||||||
|
} else {
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.querySelector('input')?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput() {
|
||||||
|
// in here, you can switch on type and implement
|
||||||
|
// whatever behaviour you need
|
||||||
|
value = type.match(/^(number|range)$/)
|
||||||
|
? +this.value
|
||||||
|
: this.value;
|
||||||
|
|
||||||
|
if (this.type === 'file') {
|
||||||
|
handleFileInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let input: HTMLInputElement;
|
||||||
|
|
||||||
|
function chooseFile() {
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
let files: FileList | undefined;
|
||||||
|
|
||||||
|
function handleFileInput() {
|
||||||
|
files = input.files;
|
||||||
|
}
|
||||||
|
|
||||||
|
let focused = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../scss/helpers";
|
||||||
|
|
||||||
|
.form-field:not(.hidden) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 16px auto;
|
||||||
|
|
||||||
|
.control {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
color: var(--on-input);
|
||||||
|
background-color: var(--input);
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
> :global(.feather.icon) {
|
||||||
|
--icon-size: 24px;
|
||||||
|
margin: 18px;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
left: 8px;
|
||||||
|
top: 22px;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
transition-property: top, font-size;
|
||||||
|
transition-duration: 150ms;
|
||||||
|
transition-timing-function: ease-out;
|
||||||
|
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-icon label {
|
||||||
|
left: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
&, * {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select, textarea {
|
||||||
|
z-index: 1;
|
||||||
|
border: 0;
|
||||||
|
color: inherit;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
outline-offset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.empty),
|
||||||
|
select ~,
|
||||||
|
[type="file"] ~,
|
||||||
|
[type="color"] ~,
|
||||||
|
[type="number"] ~,
|
||||||
|
.focused ~,
|
||||||
|
:focus ~,
|
||||||
|
fieldset {
|
||||||
|
.sections label, legend, .time-input label {
|
||||||
|
top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
select,
|
||||||
|
textarea,
|
||||||
|
.form-display,
|
||||||
|
.textarea-growing-wrapper,
|
||||||
|
.textarea-growing-wrapper:after {
|
||||||
|
display: block;
|
||||||
|
padding: 32px 8px 8px;
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
position: relative;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
|
||||||
|
&::-ms-expand {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + :global(.feather) {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
transition: transform 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Temporary
|
||||||
|
&:focus + :global(.feather) {
|
||||||
|
transform: rotateX(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-growing-wrapper {
|
||||||
|
display: grid;
|
||||||
|
flex-grow: 1;
|
||||||
|
height: revert;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: attr(data-value) " ";
|
||||||
|
color: red;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font: inherit;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
height: auto;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after, textarea {
|
||||||
|
grid-area: 1 / 1 / 2 / 2;
|
||||||
|
margin-left: revert;
|
||||||
|
min-height: 100px;
|
||||||
|
height: revert;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: anywhere;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-icon > {
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
select,
|
||||||
|
fieldset,
|
||||||
|
.form-display,
|
||||||
|
.textarea-growing-wrapper,
|
||||||
|
.textarea-growing-wrapper:after,
|
||||||
|
.textarea-growing-wrapper > textarea {
|
||||||
|
margin-left: -60px;
|
||||||
|
padding-left: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-growing-wrapper > textarea {
|
||||||
|
margin-left: -68px;
|
||||||
|
width: calc(100% + 68px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=color] {
|
||||||
|
height: calc(32px + 8px + 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.checkbox, &.color, &.file {
|
||||||
|
input, label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.checkbox {
|
||||||
|
.control {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: auto 8px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
& ~ .sections {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
label {
|
||||||
|
position: static;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.file {
|
||||||
|
input {
|
||||||
|
@include fake-hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
position: static;
|
||||||
|
display: block;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: revert !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.files {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
|
.file {
|
||||||
|
margin: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
color: var(--on-primary);
|
||||||
|
background: var(--primary);
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 20px;
|
||||||
|
|
||||||
|
:global(.feather) {
|
||||||
|
--icon-size: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: start;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding-top: 8px;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
legend {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-input {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
label {
|
||||||
|
left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+ {
|
||||||
|
.error, .hint {
|
||||||
|
margin-top: -16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field, fieldset + {
|
||||||
|
.error, .hint {
|
||||||
|
padding: 2px 2px 2px 4px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{#if type === 'hidden'}
|
||||||
|
{#if validation}
|
||||||
|
<Message type="error" content={validation.message}/>
|
||||||
|
{/if}
|
||||||
|
<input type="hidden" name={name} value={value}>
|
||||||
|
{:else}
|
||||||
|
<div class="form-field"
|
||||||
|
class:checkbox={type === 'checkbox'}
|
||||||
|
class:color={type === 'color'}
|
||||||
|
class:file={type === 'file'}
|
||||||
|
class:empty={value === ''}
|
||||||
|
class:disabled={Object.keys($$restProps).indexOf('disabled') >= 0}>
|
||||||
|
<div class="control" class:has-icon={icon} on:click={focusInput}>
|
||||||
|
{#if icon}
|
||||||
|
<Icon name={icon}/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if type === 'duration'}
|
||||||
|
<fieldset>
|
||||||
|
<legend>{placeholder}</legend>
|
||||||
|
{#each Object.keys(extraData) as f}
|
||||||
|
<div class="time-input">
|
||||||
|
<input type="number" name="{name}[{f}]" id="{fieldId}-{f}"
|
||||||
|
value={durationValue(f)}
|
||||||
|
min="0" max={(f === 's' || f === 'm') && '60' || undefined}
|
||||||
|
{...$$restProps} on:click={e => e.stopPropagation()}>
|
||||||
|
<label for="{fieldId}-{f}" on:click={e => e.stopPropagation()}>{extraData[f] || f}</label>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</fieldset>
|
||||||
|
{:else if type === 'select'}
|
||||||
|
<select name={name} id={fieldId} {...$$restProps} bind:this={input} on:input={handleInput}>
|
||||||
|
{#each extraData as option}
|
||||||
|
<option value={(option.display === undefined || option.value !== undefined) && (option.value || option)}
|
||||||
|
selected={value === (option.value || option)}>{option.display || option}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<Icon name="chevron-down"/>
|
||||||
|
{:else if type === 'textarea'}
|
||||||
|
<div class="textarea-growing-wrapper" class:focused={focused} data-value={value}>
|
||||||
|
<textarea {name} id={fieldId} {value} {...$$restProps} bind:this={input}
|
||||||
|
on:input={handleInput}
|
||||||
|
on:focusin={() => focused = true}
|
||||||
|
on:focusout={() => focused = false}></textarea>
|
||||||
|
</div>
|
||||||
|
{:else if type === 'checkbox'}
|
||||||
|
<input {type} {name} id={fieldId} checked={value === 'on'} {...$$restProps} bind:this={input}>
|
||||||
|
{:else}
|
||||||
|
<input {type} {name} id={fieldId} {value} {...$$restProps} bind:this={input} on:input={handleInput}
|
||||||
|
tabindex={type === 'file' ? '-1' : undefined}>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="sections">
|
||||||
|
{#if type !== 'duration'}
|
||||||
|
<label for={fieldId}>{@html placeholder || ''}
|
||||||
|
<slot/>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if type === 'file'}
|
||||||
|
{#if files}
|
||||||
|
<div class="files">
|
||||||
|
{#each files as file}
|
||||||
|
<div class="file">
|
||||||
|
<div class="name" title="Type: {file.type}">
|
||||||
|
<Icon name="file"/> {file.name}
|
||||||
|
</div>
|
||||||
|
<div class="size" title="{file.size} bytes">
|
||||||
|
{FileSize.humanizeFileSize(file.size, true)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<button type="button" on:click={chooseFile}>Browse...</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if validation}
|
||||||
|
<div class="error">
|
||||||
|
<Icon name="alert-circle"/> {validation.message}</div>
|
||||||
|
{/if}
|
||||||
|
{#if hint}
|
||||||
|
<div class="hint">
|
||||||
|
<Icon name="info"/> {hint}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
59
src/assets/views/utils/Form.svelte
Normal file
59
src/assets/views/utils/Form.svelte
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<script context="module">
|
||||||
|
let nextAvailableFormId = 0;
|
||||||
|
</script>
|
||||||
|
<script lang="ts">
|
||||||
|
import CsrfTokenField from "./CsrfTokenField.svelte";
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
import {setContext} from "svelte";
|
||||||
|
|
||||||
|
export let action: string;
|
||||||
|
export let button: boolean = false;
|
||||||
|
export let submitText: string;
|
||||||
|
export let submitIcon: string;
|
||||||
|
export let submitClass: string = undefined;
|
||||||
|
export let isBoldSubmit: boolean = true;
|
||||||
|
export let resetButton: boolean = false;
|
||||||
|
export let confirm: string = undefined;
|
||||||
|
|
||||||
|
const formId = nextAvailableFormId++;
|
||||||
|
setContext('formId', formId);
|
||||||
|
|
||||||
|
function handleSubmit(e) {
|
||||||
|
if (confirm && !window.confirm(confirm)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.form-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
> :last-child:not(* + :last-child) {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<form {action} method="POST" id="{formId}-form" on:submit={handleSubmit}>
|
||||||
|
<CsrfTokenField/>
|
||||||
|
<slot/>
|
||||||
|
<div class="form-controls">
|
||||||
|
{#if resetButton}
|
||||||
|
<button type="reset"><Icon name="trash"/>Reset</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button type="submit" class={submitClass} class:bold={isBoldSubmit}>
|
||||||
|
{#if submitIcon}
|
||||||
|
<Icon name={submitIcon}/>
|
||||||
|
{/if}
|
||||||
|
{#if button}
|
||||||
|
<span class="tip">{submitText}</span>
|
||||||
|
{:else}
|
||||||
|
{submitText}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
50
src/assets/views/utils/Icon.svelte
Normal file
50
src/assets/views/utils/Icon.svelte
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {replaceIcons} from "../../ts/featherIcons.js";
|
||||||
|
import {afterUpdate, onMount} from "svelte";
|
||||||
|
|
||||||
|
export let name: string;
|
||||||
|
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
replaceIcons();
|
||||||
|
});
|
||||||
|
afterUpdate(() => {
|
||||||
|
replaceIcons(false);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
// ---
|
||||||
|
// --- Feather
|
||||||
|
// ---
|
||||||
|
:global(.feather) {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: var(--icon-size);
|
||||||
|
height: var(--icon-size);
|
||||||
|
|
||||||
|
--icon-size: 16px;
|
||||||
|
font-size: var(--icon-size);
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: square;
|
||||||
|
stroke-linejoin: miter;
|
||||||
|
fill: none;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
h1 > &, h2 > &, h3 > & {
|
||||||
|
--icon-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{#if name}
|
||||||
|
{#if name.startsWith('fa') }
|
||||||
|
<i class="{name} feather icon" aria-hidden="true" {...$$restProps}></i>
|
||||||
|
{:else}
|
||||||
|
<i data-feather="{name}" class="feather icon" aria-hidden="true" {...$$restProps}></i>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
@ -1,19 +1,21 @@
|
|||||||
import Controller from "../Controller";
|
|
||||||
import {RequireAuthMiddleware} from "./AuthComponent";
|
|
||||||
import {Request, Response} from "express";
|
|
||||||
import {BadRequestError, ForbiddenHttpError, NotFoundHttpError} from "../HttpError";
|
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import Validator, {EMAIL_REGEX, InvalidFormatValidationError} from "../db/Validator";
|
import {Request, Response} from "express";
|
||||||
import UserPasswordComponent from "./password/UserPasswordComponent";
|
|
||||||
import User from "./models/User";
|
import {route} from "../common/Routing.js";
|
||||||
import ModelFactory from "../db/ModelFactory";
|
import {Time} from "../common/Time.js";
|
||||||
import UserEmail from "./models/UserEmail";
|
import Controller from "../Controller.js";
|
||||||
import MagicLinkController from "./magic_link/MagicLinkController";
|
import ModelFactory from "../db/ModelFactory.js";
|
||||||
import {MailTemplate} from "../mail/Mail";
|
import Validator, {EMAIL_REGEX, InvalidFormatValidationError} from "../db/Validator.js";
|
||||||
import {ADD_EMAIL_MAIL_TEMPLATE, REMOVE_PASSWORD_MAIL_TEMPLATE} from "../Mails";
|
import {BadRequestError, ForbiddenHttpError, NotFoundHttpError} from "../HttpError.js";
|
||||||
import AuthMagicLinkActionType from "./magic_link/AuthMagicLinkActionType";
|
import MailTemplate from "../mail/MailTemplate.js";
|
||||||
import UserNameComponent from "./models/UserNameComponent";
|
import {ADD_EMAIL_MAIL_TEMPLATE, REMOVE_PASSWORD_MAIL_TEMPLATE} from "../Mails.js";
|
||||||
import Time from "../Time";
|
import {RequireAuthMiddleware} from "./AuthComponent.js";
|
||||||
|
import AuthMagicLinkActionType from "./magic_link/AuthMagicLinkActionType.js";
|
||||||
|
import MagicLinkController from "./magic_link/MagicLinkController.js";
|
||||||
|
import User from "./models/User.js";
|
||||||
|
import UserEmail from "./models/UserEmail.js";
|
||||||
|
import UserNameComponent from "./models/UserNameComponent.js";
|
||||||
|
import UserPasswordComponent from "./password/UserPasswordComponent.js";
|
||||||
|
|
||||||
export default class AccountController extends Controller {
|
export default class AccountController extends Controller {
|
||||||
|
|
||||||
@ -56,6 +58,7 @@ export default class AccountController extends Controller {
|
|||||||
const nameChangeRemainingTime = new Date(nameChangedAt + nameChangeWaitPeriod);
|
const nameChangeRemainingTime = new Date(nameChangedAt + nameChangeWaitPeriod);
|
||||||
|
|
||||||
res.render('auth/account/account', {
|
res.render('auth/account/account', {
|
||||||
|
user_personal_info_fields: user.getPersonalInfoFields(),
|
||||||
main_email: await user.mainEmail.get(),
|
main_email: await user.mainEmail.get(),
|
||||||
emails: await user.emails.get(),
|
emails: await user.emails.get(),
|
||||||
display_email_warning: config.get('app.display_email_warning'),
|
display_email_warning: config.get('app.display_email_warning'),
|
||||||
@ -80,14 +83,14 @@ export default class AccountController extends Controller {
|
|||||||
const nameChangedAt = userNameComponent.getNameChangedAt()?.getTime() || Date.now();
|
const nameChangedAt = userNameComponent.getNameChangedAt()?.getTime() || Date.now();
|
||||||
const nameChangeWaitPeriod = config.get<number>('auth.name_change_wait_period');
|
const nameChangeWaitPeriod = config.get<number>('auth.name_change_wait_period');
|
||||||
req.flash('error', `Your can't change your name until ${new Date(nameChangedAt + nameChangeWaitPeriod)}.`);
|
req.flash('error', `Your can't change your name until ${new Date(nameChangedAt + nameChangeWaitPeriod)}.`);
|
||||||
res.redirect(Controller.route('account'));
|
res.redirect(route('account'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
req.flash('success', `Your name was successfully changed to ${req.body.name}.`);
|
req.flash('success', `Your name was successfully changed to ${req.body.name}.`);
|
||||||
res.redirect(Controller.route('account'));
|
res.redirect(route('account'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async postChangePassword(req: Request, res: Response): Promise<void> {
|
protected async postChangePassword(req: Request, res: Response): Promise<void> {
|
||||||
@ -100,7 +103,7 @@ export default class AccountController extends Controller {
|
|||||||
const passwordComponent = user.as(UserPasswordComponent);
|
const passwordComponent = user.as(UserPasswordComponent);
|
||||||
if (passwordComponent.hasPassword() && !await passwordComponent.verifyPassword(req.body.current_password)) {
|
if (passwordComponent.hasPassword() && !await passwordComponent.verifyPassword(req.body.current_password)) {
|
||||||
req.flash('error', 'Invalid current password.');
|
req.flash('error', 'Invalid current password.');
|
||||||
res.redirect(Controller.route('account'));
|
res.redirect(route('account'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +111,7 @@ export default class AccountController extends Controller {
|
|||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
req.flash('success', 'Password changed successfully.');
|
req.flash('success', 'Password changed successfully.');
|
||||||
res.redirect(Controller.route('account'));
|
res.redirect(route('account'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async postRemovePassword(req: Request, res: Response): Promise<void> {
|
protected async postRemovePassword(req: Request, res: Response): Promise<void> {
|
||||||
@ -116,7 +119,7 @@ export default class AccountController extends Controller {
|
|||||||
const mainEmail = await user.mainEmail.get();
|
const mainEmail = await user.mainEmail.get();
|
||||||
if (!mainEmail || !mainEmail.email) {
|
if (!mainEmail || !mainEmail.email) {
|
||||||
req.flash('error', 'You can\'t remove your password without adding an email address first.');
|
req.flash('error', 'You can\'t remove your password without adding an email address first.');
|
||||||
res.redirect(Controller.route('account'));
|
res.redirect(route('account'));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -125,14 +128,14 @@ export default class AccountController extends Controller {
|
|||||||
this.getApp(),
|
this.getApp(),
|
||||||
req.getSession().id,
|
req.getSession().id,
|
||||||
AuthMagicLinkActionType.REMOVE_PASSWORD,
|
AuthMagicLinkActionType.REMOVE_PASSWORD,
|
||||||
Controller.route('account'),
|
route('account'),
|
||||||
mainEmail.email,
|
mainEmail.email,
|
||||||
this.removePasswordMailTemplate,
|
this.removePasswordMailTemplate,
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
res.redirect(route('magic_link_lobby', undefined, {
|
||||||
redirect_uri: Controller.route('account'),
|
redirect_uri: route('account'),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +158,7 @@ export default class AccountController extends Controller {
|
|||||||
this.getApp(),
|
this.getApp(),
|
||||||
req.getSession().id,
|
req.getSession().id,
|
||||||
AuthMagicLinkActionType.ADD_EMAIL,
|
AuthMagicLinkActionType.ADD_EMAIL,
|
||||||
Controller.route('account'),
|
route('account'),
|
||||||
email,
|
email,
|
||||||
this.addEmailMailTemplate,
|
this.addEmailMailTemplate,
|
||||||
{
|
{
|
||||||
@ -163,8 +166,8 @@ export default class AccountController extends Controller {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
res.redirect(route('magic_link_lobby', undefined, {
|
||||||
redirect_uri: Controller.route('account'),
|
redirect_uri: route('account'),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +190,7 @@ export default class AccountController extends Controller {
|
|||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
req.flash('success', 'This email was successfully set as your main address.');
|
req.flash('success', 'This email was successfully set as your main address.');
|
||||||
res.redirect(Controller.route('account'));
|
res.redirect(route('account'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async postRemoveEmail(req: Request, res: Response): Promise<void> {
|
protected async postRemoveEmail(req: Request, res: Response): Promise<void> {
|
||||||
@ -207,6 +210,6 @@ export default class AccountController extends Controller {
|
|||||||
await userEmail.delete();
|
await userEmail.delete();
|
||||||
|
|
||||||
req.flash('success', 'This email was successfully removed from your account.');
|
req.flash('success', 'This email was successfully removed from your account.');
|
||||||
res.redirect(Controller.route('account'));
|
res.redirect(route('account'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import ApplicationComponent from "../ApplicationComponent";
|
|
||||||
import {NextFunction, Request, Response} from "express";
|
import {NextFunction, Request, Response} from "express";
|
||||||
import AuthGuard from "./AuthGuard";
|
|
||||||
import Controller from "../Controller";
|
import Application from "../Application.js";
|
||||||
import {ForbiddenHttpError} from "../HttpError";
|
import ApplicationComponent from "../ApplicationComponent.js";
|
||||||
import Middleware from "../Middleware";
|
import {route} from "../common/Routing.js";
|
||||||
import User from "./models/User";
|
import {ForbiddenHttpError} from "../HttpError.js";
|
||||||
import Application from "../Application";
|
import Middleware from "../Middleware.js";
|
||||||
import AuthMethod from "./AuthMethod";
|
import AuthGuard from "./AuthGuard.js";
|
||||||
import AuthProof from "./AuthProof";
|
import AuthMethod from "./AuthMethod.js";
|
||||||
|
import AuthProof from "./AuthProof.js";
|
||||||
|
import User from "./models/User.js";
|
||||||
|
|
||||||
export default class AuthComponent extends ApplicationComponent {
|
export default class AuthComponent extends ApplicationComponent {
|
||||||
private readonly authGuard: AuthGuard;
|
private readonly authGuard: AuthGuard;
|
||||||
@ -17,7 +18,7 @@ export default class AuthComponent extends ApplicationComponent {
|
|||||||
this.authGuard = new AuthGuard(app, ...authMethods);
|
this.authGuard = new AuthGuard(app, ...authMethods);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async init(): Promise<void> {
|
public async initRoutes(): Promise<void> {
|
||||||
this.use(AuthMiddleware);
|
this.use(AuthMiddleware);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +66,7 @@ export class RequireRequestAuthMiddleware extends Middleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.flash('error', `You must be logged in to access ${req.url}.`);
|
req.flash('error', `You must be logged in to access ${req.url}.`);
|
||||||
res.redirect(Controller.route('auth', undefined, {
|
res.redirect(route('auth', undefined, {
|
||||||
redirect_uri: req.url,
|
redirect_uri: req.url,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -101,7 +102,7 @@ export class RequireAuthMiddleware extends Middleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.flash('error', `You must be logged in to access ${req.url}.`);
|
req.flash('error', `You must be logged in to access ${req.url}.`);
|
||||||
res.redirect(Controller.route('auth', undefined, {
|
res.redirect(route('auth', undefined, {
|
||||||
redirect_uri: req.url,
|
redirect_uri: req.url,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -116,7 +117,7 @@ export class RequireGuestMiddleware extends Middleware {
|
|||||||
protected async handle(req: Request, res: Response, next: NextFunction): Promise<void> {
|
protected async handle(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
const proofs = await req.as(AuthMiddleware).getAuthGuard().getProofsForSession(req.getSession());
|
const proofs = await req.as(AuthMiddleware).getAuthGuard().getProofsForSession(req.getSession());
|
||||||
if (proofs.length > 0) {
|
if (proofs.length > 0) {
|
||||||
res.redirect(Controller.route('home'));
|
res.redirect(route('home'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import Controller from "../Controller";
|
|
||||||
import {NextFunction, Request, Response} from "express";
|
import {NextFunction, Request, Response} from "express";
|
||||||
import AuthComponent, {AuthMiddleware, RequireAuthMiddleware, RequireGuestMiddleware} from "./AuthComponent";
|
|
||||||
import {BadRequestError} from "../HttpError";
|
import Controller from "../Controller.js";
|
||||||
import ModelFactory from "../db/ModelFactory";
|
import ModelFactory from "../db/ModelFactory.js";
|
||||||
import User from "./models/User";
|
import {UnknownRelationValidationError} from "../db/Validator.js";
|
||||||
import UserPasswordComponent from "./password/UserPasswordComponent";
|
import {BadRequestError} from "../HttpError.js";
|
||||||
import UserNameComponent from "./models/UserNameComponent";
|
import AuthComponent, {AuthMiddleware, RequireAuthMiddleware, RequireGuestMiddleware} from "./AuthComponent.js";
|
||||||
import {UnknownRelationValidationError} from "../db/Validator";
|
import AuthMethod from "./AuthMethod.js";
|
||||||
import AuthMethod from "./AuthMethod";
|
import AuthProof from "./AuthProof.js";
|
||||||
import AuthProof from "./AuthProof";
|
import User from "./models/User.js";
|
||||||
|
import UserNameComponent from "./models/UserNameComponent.js";
|
||||||
|
import UserPasswordComponent from "./password/UserPasswordComponent.js";
|
||||||
|
|
||||||
export default class AuthController extends Controller {
|
export default class AuthController extends Controller {
|
||||||
public getRoutesPrefix(): string {
|
public getRoutesPrefix(): string {
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import AuthProof from "./AuthProof";
|
|
||||||
import MysqlConnectionManager from "../db/MysqlConnectionManager";
|
|
||||||
import User from "./models/User";
|
|
||||||
import {Connection} from "mysql";
|
|
||||||
import {Request, Response} from "express";
|
|
||||||
import {PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE} from "../Mails";
|
|
||||||
import Mail from "../mail/Mail";
|
|
||||||
import Controller from "../Controller";
|
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import Application from "../Application";
|
import {Request, Response} from "express";
|
||||||
import NunjucksComponent from "../components/NunjucksComponent";
|
|
||||||
import AuthMethod from "./AuthMethod";
|
|
||||||
import {Session, SessionData} from "express-session";
|
import {Session, SessionData} from "express-session";
|
||||||
import UserNameComponent from "./models/UserNameComponent";
|
import {Connection} from "mysql";
|
||||||
|
|
||||||
|
import Application from "../Application.js";
|
||||||
|
import {route} from "../common/Routing.js";
|
||||||
|
import MailComponent from "../components/MailComponent.js";
|
||||||
|
import MysqlConnectionManager from "../db/MysqlConnectionManager.js";
|
||||||
|
import Mail from "../mail/Mail.js";
|
||||||
|
import {PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE} from "../Mails.js";
|
||||||
|
import AuthMethod from "./AuthMethod.js";
|
||||||
|
import AuthProof from "./AuthProof.js";
|
||||||
|
import User from "./models/User.js";
|
||||||
|
import UserNameComponent from "./models/UserNameComponent.js";
|
||||||
|
|
||||||
export default class AuthGuard {
|
export default class AuthGuard {
|
||||||
private readonly authMethods: AuthMethod<AuthProof<User>>[];
|
private readonly authMethods: AuthMethod<AuthProof<User>>[];
|
||||||
@ -145,12 +146,12 @@ export default class AuthGuard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (User.isApprovalMode()) {
|
if (User.isApprovalMode()) {
|
||||||
await new Mail(this.app.as(NunjucksComponent).getEnvironment(), PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, {
|
await this.app.as(MailComponent).sendMail(new Mail(PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, {
|
||||||
username: user.asOptional(UserNameComponent)?.getName() ||
|
username: user.asOptional(UserNameComponent)?.getName() ||
|
||||||
(await user.mainEmail.get())?.getOrFail('email') ||
|
(await user.mainEmail.get())?.getOrFail('email') ||
|
||||||
'Could not find an identifier',
|
'Could not find an identifier',
|
||||||
link: config.get<string>('public_url') + Controller.route('accounts-approval'),
|
link: route('accounts-approval', {}, {}, true),
|
||||||
}).send(config.get<string>('app.contact_email'));
|
}), config.get<string>('app.contact_email'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import User from "./models/User";
|
|
||||||
import AuthProof from "./AuthProof";
|
|
||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
import {Session} from "express-session";
|
import {Session} from "express-session";
|
||||||
|
|
||||||
|
import AuthProof from "./AuthProof.js";
|
||||||
|
import User from "./models/User.js";
|
||||||
|
|
||||||
|
|
||||||
export default interface AuthMethod<P extends AuthProof<User>> {
|
export default interface AuthMethod<P extends AuthProof<User>> {
|
||||||
/**
|
/**
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Migration from "../../db/Migration";
|
import Migration from "../../db/Migration.js";
|
||||||
|
|
||||||
export default class AddUsedToMagicLinksMigration extends Migration {
|
export default class AddUsedToMagicLinksMigration extends Migration {
|
||||||
public async install(): Promise<void> {
|
public async install(): Promise<void> {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Migration from "../../db/Migration";
|
import Migration from "../../db/Migration.js";
|
||||||
import ModelFactory from "../../db/ModelFactory";
|
import ModelFactory from "../../db/ModelFactory.js";
|
||||||
import MagicLink from "../models/MagicLink";
|
import MagicLink from "../models/MagicLink.js";
|
||||||
|
|
||||||
export default class CreateMagicLinksTableMigration extends Migration {
|
export default class CreateMagicLinksTableMigration extends Migration {
|
||||||
public async install(): Promise<void> {
|
public async install(): Promise<void> {
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
import AuthMethod from "../AuthMethod";
|
|
||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
import User from "../models/User";
|
|
||||||
import UserEmail from "../models/UserEmail";
|
|
||||||
import MagicLink from "../models/MagicLink";
|
|
||||||
import {WhereTest} from "../../db/ModelQuery";
|
|
||||||
import Controller from "../../Controller";
|
|
||||||
import geoip from "geoip-lite";
|
|
||||||
import MagicLinkController from "./MagicLinkController";
|
|
||||||
import Application from "../../Application";
|
|
||||||
import {MailTemplate} from "../../mail/Mail";
|
|
||||||
import AuthMagicLinkActionType from "./AuthMagicLinkActionType";
|
|
||||||
import Validator, {EMAIL_REGEX} from "../../db/Validator";
|
|
||||||
import ModelFactory from "../../db/ModelFactory";
|
|
||||||
import UserNameComponent from "../models/UserNameComponent";
|
|
||||||
import {Session} from "express-session";
|
import {Session} from "express-session";
|
||||||
|
import geoip from "geoip-lite";
|
||||||
|
|
||||||
|
import Application from "../../Application.js";
|
||||||
|
import {route} from "../../common/Routing.js";
|
||||||
|
import ModelFactory from "../../db/ModelFactory.js";
|
||||||
|
import {WhereTest} from "../../db/ModelQuery.js";
|
||||||
|
import Validator, {EMAIL_REGEX} from "../../db/Validator.js";
|
||||||
|
import MailTemplate from "../../mail/MailTemplate.js";
|
||||||
|
import AuthMethod from "../AuthMethod.js";
|
||||||
|
import MagicLink from "../models/MagicLink.js";
|
||||||
|
import User from "../models/User.js";
|
||||||
|
import UserEmail from "../models/UserEmail.js";
|
||||||
|
import UserNameComponent from "../models/UserNameComponent.js";
|
||||||
|
import AuthMagicLinkActionType from "./AuthMagicLinkActionType.js";
|
||||||
|
import MagicLinkController from "./MagicLinkController.js";
|
||||||
|
|
||||||
export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -54,7 +55,7 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
|||||||
|
|
||||||
if (pendingLink) {
|
if (pendingLink) {
|
||||||
if (await pendingLink.isValid()) {
|
if (await pendingLink.isValid()) {
|
||||||
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
res.redirect(route('magic_link_lobby', undefined, {
|
||||||
redirect_uri: req.getIntendedUrl() || pendingLink.original_url || undefined,
|
redirect_uri: req.getIntendedUrl() || pendingLink.original_url || undefined,
|
||||||
}));
|
}));
|
||||||
return true;
|
return true;
|
||||||
@ -104,7 +105,7 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
|||||||
this.app,
|
this.app,
|
||||||
req.getSession().id,
|
req.getSession().id,
|
||||||
actionType,
|
actionType,
|
||||||
Controller.route('auth', undefined, {
|
route('auth', undefined, {
|
||||||
redirect_uri: req.getIntendedUrl() || undefined,
|
redirect_uri: req.getIntendedUrl() || undefined,
|
||||||
}),
|
}),
|
||||||
email,
|
email,
|
||||||
@ -119,7 +120,7 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
res.redirect(route('magic_link_lobby', undefined, {
|
||||||
redirect_uri: req.getIntendedUrl(),
|
redirect_uri: req.getIntendedUrl(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
import Controller from "../../Controller";
|
|
||||||
import {Request, Response} from "express";
|
|
||||||
import MagicLinkWebSocketListener from "./MagicLinkWebSocketListener";
|
|
||||||
import {BadRequestError, NotFoundHttpError} from "../../HttpError";
|
|
||||||
import Throttler from "../../Throttler";
|
|
||||||
import Mail, {MailTemplate} from "../../mail/Mail";
|
|
||||||
import MagicLink from "../models/MagicLink";
|
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import Application from "../../Application";
|
import {Request, Response} from "express";
|
||||||
import {ParsedUrlQueryInput} from "querystring";
|
|
||||||
import NunjucksComponent from "../../components/NunjucksComponent";
|
import Application from "../../Application.js";
|
||||||
import User from "../models/User";
|
import {QueryParamsRecord, route} from "../../common/Routing.js";
|
||||||
import AuthComponent, {AuthMiddleware} from "../AuthComponent";
|
import MailComponent from "../../components/MailComponent.js";
|
||||||
import {AuthError, PendingApprovalAuthError, RegisterCallback} from "../AuthGuard";
|
import Controller from "../../Controller.js";
|
||||||
import UserEmail from "../models/UserEmail";
|
import {QueryVariable} from "../../db/MysqlConnectionManager.js";
|
||||||
import AuthMagicLinkActionType from "./AuthMagicLinkActionType";
|
import {BadRequestError, NotFoundHttpError} from "../../HttpError.js";
|
||||||
import {QueryVariable} from "../../db/MysqlConnectionManager";
|
import {logger} from "../../Logger.js";
|
||||||
import UserNameComponent from "../models/UserNameComponent";
|
import Mail from "../../mail/Mail.js";
|
||||||
import MagicLinkUserNameComponent from "../models/MagicLinkUserNameComponent";
|
import MailTemplate from "../../mail/MailTemplate.js";
|
||||||
import {logger} from "../../Logger";
|
import Throttler from "../../Throttler.js";
|
||||||
import UserPasswordComponent from "../password/UserPasswordComponent";
|
import AuthComponent, {AuthMiddleware} from "../AuthComponent.js";
|
||||||
|
import {AuthError, PendingApprovalAuthError, RegisterCallback} from "../AuthGuard.js";
|
||||||
|
import MagicLink from "../models/MagicLink.js";
|
||||||
|
import MagicLinkUserNameComponent from "../models/MagicLinkUserNameComponent.js";
|
||||||
|
import User from "../models/User.js";
|
||||||
|
import UserEmail from "../models/UserEmail.js";
|
||||||
|
import UserNameComponent from "../models/UserNameComponent.js";
|
||||||
|
import UserPasswordComponent from "../password/UserPasswordComponent.js";
|
||||||
|
import AuthMagicLinkActionType from "./AuthMagicLinkActionType.js";
|
||||||
|
import MagicLinkWebSocketListener from "./MagicLinkWebSocketListener.js";
|
||||||
|
|
||||||
export default class MagicLinkController<A extends Application> extends Controller {
|
export default class MagicLinkController<A extends Application> extends Controller {
|
||||||
public static async sendMagicLink(
|
public static async sendMagicLink(
|
||||||
@ -28,7 +30,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
|||||||
original_url: string,
|
original_url: string,
|
||||||
email: string,
|
email: string,
|
||||||
mailTemplate: MailTemplate,
|
mailTemplate: MailTemplate,
|
||||||
data: ParsedUrlQueryInput,
|
data: QueryParamsRecord,
|
||||||
magicLinkData: Record<string, QueryVariable> = {},
|
magicLinkData: Record<string, QueryVariable> = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
Throttler.throttle('magic_link', process.env.NODE_ENV === 'test' ? 10 : 2, MagicLink.validityPeriod(),
|
Throttler.throttle('magic_link', process.env.NODE_ENV === 'test' ? 10 : 2, MagicLink.validityPeriod(),
|
||||||
@ -46,12 +48,12 @@ export default class MagicLinkController<A extends Application> extends Controll
|
|||||||
await link.save();
|
await link.save();
|
||||||
|
|
||||||
// Send email
|
// Send email
|
||||||
await new Mail(app.as(NunjucksComponent).getEnvironment(), mailTemplate, Object.assign(data, {
|
await app.as(MailComponent).sendMail(new Mail(mailTemplate, Object.assign(data, {
|
||||||
link: `${config.get<string>('public_url')}${Controller.route('magic_link', undefined, {
|
link: `${route('magic_link', undefined, {
|
||||||
id: link.id,
|
id: link.id,
|
||||||
token: token,
|
token: token,
|
||||||
})}`,
|
}, true)}`,
|
||||||
})).send(email);
|
})), email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async checkAndAuth(req: Request, res: Response, magicLink: MagicLink): Promise<User | null> {
|
public static async checkAndAuth(req: Request, res: Response, magicLink: MagicLink): Promise<User | null> {
|
||||||
@ -88,7 +90,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof PendingApprovalAuthError) {
|
if (e instanceof PendingApprovalAuthError) {
|
||||||
req.flash('warning', `Your account is pending review. You'll receive an email once you're approved.`);
|
req.flash('warning', `Your account is pending review. You'll receive an email once you're approved.`);
|
||||||
res.redirect(Controller.route('auth'));
|
res.redirect(route('auth'));
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
@ -114,33 +116,42 @@ export default class MagicLinkController<A extends Application> extends Controll
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async getLobby(req: Request, res: Response): Promise<void> {
|
protected async getLobby(req: Request, res: Response): Promise<void> {
|
||||||
const link = await MagicLink.select()
|
const links = await MagicLink.select()
|
||||||
.where('session_id', req.getSession().id)
|
.where('session_id', req.getSession().id)
|
||||||
.sortBy('authorized')
|
.sortBy('authorized')
|
||||||
.where('used', 0)
|
.where('used', 0)
|
||||||
.first();
|
.get();
|
||||||
if (!link) {
|
if (links.length === 0) {
|
||||||
throw new NotFoundHttpError('magic link', req.url);
|
throw new NotFoundHttpError('magic link', req.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await link.isValid()) {
|
let validLink;
|
||||||
|
for (const link of links) {
|
||||||
|
if (await link.isValid()) {
|
||||||
|
validLink = link;
|
||||||
|
} else {
|
||||||
req.flash('error', 'This magic link has expired. Please try again.');
|
req.flash('error', 'This magic link has expired. Please try again.');
|
||||||
res.redirect(link.getOrFail('original_url'));
|
await link.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validLink) {
|
||||||
|
res.redirect(req.getIntendedUrl() || route('home'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await link.isAuthorized()) {
|
if (await validLink.isAuthorized()) {
|
||||||
link.use();
|
validLink.use();
|
||||||
await link.save();
|
await validLink.save();
|
||||||
await this.performAction(link, req, res);
|
await this.performAction(validLink, req, res);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render('magic_link_lobby', {
|
res.render('magic_link_lobby', {
|
||||||
email: link.getOrFail('email'),
|
email: validLink.getOrFail('email'),
|
||||||
type: link.getOrFail('action_type'),
|
type: validLink.getOrFail('action_type'),
|
||||||
validUntil: link.getExpirationDate().getTime(),
|
validUntil: validLink.getExpirationDate().getTime(),
|
||||||
websocketUrl: config.get<string>('public_websocket_url') + this.magicLinkWebsocketPath,
|
websocketUrl: config.get<string>('app.public_websocket_url') + this.magicLinkWebsocketPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,7 +200,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
|||||||
// Auth success
|
// Auth success
|
||||||
const name = user.asOptional(UserNameComponent)?.getName();
|
const name = user.asOptional(UserNameComponent)?.getName();
|
||||||
req.flash('success', `Authentication success. Welcome${name ? `, ${name}` : ''}`);
|
req.flash('success', `Authentication success. Welcome${name ? `, ${name}` : ''}`);
|
||||||
res.redirect(req.getIntendedUrl() || Controller.route('home'));
|
res.redirect(req.getIntendedUrl() || route('home'));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -209,7 +220,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
|||||||
if (await UserEmail.select().with('user').where('email', email).first()) {
|
if (await UserEmail.select().with('user').where('email', email).first()) {
|
||||||
req.flash('error', 'An account already exists with this email address.' +
|
req.flash('error', 'An account already exists with this email address.' +
|
||||||
' Please first remove it there before adding it here.');
|
' Please first remove it there before adding it here.');
|
||||||
res.redirect(Controller.route('account'));
|
res.redirect(route('account'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,7 +237,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.flash('success', `Email address ${userEmail.email} successfully added.`);
|
req.flash('success', `Email address ${userEmail.email} successfully added.`);
|
||||||
res.redirect(Controller.route('account'));
|
res.redirect(route('account'));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,7 +259,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.flash('success', `Password successfully removed.`);
|
req.flash('success', `Password successfully removed.`);
|
||||||
res.redirect(Controller.route('account'));
|
res.redirect(route('account'));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import WebSocket from "ws";
|
|
||||||
import {IncomingMessage} from "http";
|
|
||||||
import WebSocketListener from "../../WebSocketListener";
|
|
||||||
import MagicLink from "../models/MagicLink";
|
|
||||||
import Application from "../../Application";
|
|
||||||
import {Session} from "express-session";
|
import {Session} from "express-session";
|
||||||
|
import {IncomingMessage} from "http";
|
||||||
|
import WebSocket from "ws";
|
||||||
|
|
||||||
|
import Application from "../../Application.js";
|
||||||
|
import WebSocketListener from "../../WebSocketListener.js";
|
||||||
|
import MagicLink from "../models/MagicLink.js";
|
||||||
|
|
||||||
export default class MagicLinkWebSocketListener<A extends Application> extends WebSocketListener<A> {
|
export default class MagicLinkWebSocketListener<A extends Application> extends WebSocketListener<A> {
|
||||||
private readonly connections: { [p: string]: (() => void)[] | undefined } = {};
|
private readonly connections: { [p: string]: (() => void)[] | undefined } = {};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Migration from "../../db/Migration";
|
import Migration from "../../db/Migration.js";
|
||||||
|
|
||||||
export default class MakeMagicLinksSessionNotUniqueMigration extends Migration {
|
export default class MakeMagicLinksSessionNotUniqueMigration extends Migration {
|
||||||
public async install(): Promise<void> {
|
public async install(): Promise<void> {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Migration from "../../db/Migration";
|
import Migration from "../../db/Migration.js";
|
||||||
import ModelFactory from "../../db/ModelFactory";
|
import ModelFactory from "../../db/ModelFactory.js";
|
||||||
import User from "../models/User";
|
import User from "../models/User.js";
|
||||||
import UserApprovedComponent from "../models/UserApprovedComponent";
|
import UserApprovedComponent from "../models/UserApprovedComponent.js";
|
||||||
|
|
||||||
export default class AddApprovedFieldToUsersTableMigration extends Migration {
|
export default class AddApprovedFieldToUsersTableMigration extends Migration {
|
||||||
public async install(): Promise<void> {
|
public async install(): Promise<void> {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Migration from "../../db/Migration";
|
import Migration from "../../db/Migration.js";
|
||||||
|
|
||||||
export default class AddNameChangedAtToUsersMigration extends Migration {
|
export default class AddNameChangedAtToUsersMigration extends Migration {
|
||||||
public async install(): Promise<void> {
|
public async install(): Promise<void> {
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import Migration from "../../db/Migration";
|
|
||||||
import ModelFactory from "../../db/ModelFactory";
|
|
||||||
import User from "../models/User";
|
|
||||||
import UserNameComponent from "../models/UserNameComponent";
|
|
||||||
import MagicLink from "../models/MagicLink";
|
|
||||||
import MagicLinkUserNameComponent from "../models/MagicLinkUserNameComponent";
|
|
||||||
import {nanoid} from "nanoid";
|
import {nanoid} from "nanoid";
|
||||||
|
|
||||||
|
import Migration from "../../db/Migration.js";
|
||||||
|
import ModelFactory from "../../db/ModelFactory.js";
|
||||||
|
import MagicLink from "../models/MagicLink.js";
|
||||||
|
import MagicLinkUserNameComponent from "../models/MagicLinkUserNameComponent.js";
|
||||||
|
import User from "../models/User.js";
|
||||||
|
import UserNameComponent from "../models/UserNameComponent.js";
|
||||||
|
|
||||||
export default class AddNameToUsersMigration extends Migration {
|
export default class AddNameToUsersMigration extends Migration {
|
||||||
public async install(): Promise<void> {
|
public async install(): Promise<void> {
|
||||||
await this.query(`ALTER TABLE users
|
await this.query(`ALTER TABLE users
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Migration from "../../db/Migration";
|
import Migration from "../../db/Migration.js";
|
||||||
import ModelFactory from "../../db/ModelFactory";
|
import ModelFactory from "../../db/ModelFactory.js";
|
||||||
import User from "../models/User";
|
import User from "../models/User.js";
|
||||||
import UserEmail from "../models/UserEmail";
|
import UserEmail from "../models/UserEmail.js";
|
||||||
|
|
||||||
export default class CreateUsersAndUserEmailsTableMigration extends Migration {
|
export default class CreateUsersAndUserEmailsTableMigration extends Migration {
|
||||||
public async install(): Promise<void> {
|
public async install(): Promise<void> {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Migration from "../../db/Migration";
|
import Migration from "../../db/Migration.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated - TODO may be remove at next major version >= 0.24, replace with DummyMigration.
|
* @deprecated - TODO may be remove at next major version >= 0.24, replace with DummyMigration.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Migration from "../../db/Migration";
|
import Migration from "../../db/Migration.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated - TODO may be remove at next major version >= 0.24, replace with DummyMigration.
|
* @deprecated - TODO may be remove at next major version >= 0.24, replace with DummyMigration.
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import crypto from "crypto";
|
|
||||||
import config from "config";
|
|
||||||
import Model from "../../db/Model";
|
|
||||||
import AuthProof from "../AuthProof";
|
|
||||||
import User from "./User";
|
|
||||||
import argon2 from "argon2";
|
import argon2 from "argon2";
|
||||||
import UserEmail from "./UserEmail";
|
import config from "config";
|
||||||
import {EMAIL_REGEX} from "../../db/Validator";
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import Model from "../../db/Model.js";
|
||||||
|
import {EMAIL_REGEX} from "../../db/Validator.js";
|
||||||
|
import AuthProof from "../AuthProof.js";
|
||||||
|
import User from "./User.js";
|
||||||
|
import UserEmail from "./UserEmail.js";
|
||||||
|
|
||||||
export default class MagicLink extends Model implements AuthProof<User> {
|
export default class MagicLink extends Model implements AuthProof<User> {
|
||||||
public static validityPeriod(): number {
|
public static validityPeriod(): number {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import ModelComponent from "../../db/ModelComponent";
|
import ModelComponent from "../../db/ModelComponent.js";
|
||||||
import MagicLink from "./MagicLink";
|
import MagicLink from "./MagicLink.js";
|
||||||
import {USERNAME_REGEXP} from "./UserNameComponent";
|
import User from "./User.js";
|
||||||
import User from "./User";
|
import {USERNAME_REGEXP} from "./UserNameComponent.js";
|
||||||
|
|
||||||
export default class MagicLinkUserNameComponent extends ModelComponent<MagicLink> {
|
export default class MagicLinkUserNameComponent extends ModelComponent<MagicLink> {
|
||||||
public readonly username?: string = undefined;
|
public readonly username?: string = undefined;
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import Model from "../../db/Model";
|
|
||||||
import MysqlConnectionManager from "../../db/MysqlConnectionManager";
|
|
||||||
import AddApprovedFieldToUsersTableMigration from "../migrations/AddApprovedFieldToUsersTableMigration";
|
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import {ManyModelRelation} from "../../db/ModelRelation";
|
|
||||||
import UserEmail from "./UserEmail";
|
import Model from "../../db/Model.js";
|
||||||
import UserApprovedComponent from "./UserApprovedComponent";
|
import {ManyModelRelation} from "../../db/ModelRelation.js";
|
||||||
import UserNameComponent from "./UserNameComponent";
|
import MysqlConnectionManager from "../../db/MysqlConnectionManager.js";
|
||||||
|
import AddApprovedFieldToUsersTableMigration from "../migrations/AddApprovedFieldToUsersTableMigration.js";
|
||||||
|
import UserApprovedComponent from "./UserApprovedComponent.js";
|
||||||
|
import UserEmail from "./UserEmail.js";
|
||||||
|
import UserNameComponent from "./UserNameComponent.js";
|
||||||
|
|
||||||
export default class User extends Model {
|
export default class User extends Model {
|
||||||
/**
|
/**
|
||||||
@ -42,7 +43,7 @@ export default class User extends Model {
|
|||||||
return !User.isApprovalMode() || this.as(UserApprovedComponent).approved;
|
return !User.isApprovalMode() || this.as(UserApprovedComponent).approved;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getPersonalInfoFields(): { name: string, value: string }[] {
|
public getPersonalInfoFields(): { name: string, value: string }[] {
|
||||||
const fields: { name: string, value: string }[] = [];
|
const fields: { name: string, value: string }[] = [];
|
||||||
const nameComponent = this.asOptional(UserNameComponent);
|
const nameComponent = this.asOptional(UserNameComponent);
|
||||||
if (nameComponent && nameComponent.hasName()) {
|
if (nameComponent && nameComponent.hasName()) {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user