This commit is contained in:
Lurkars 2021-01-12 19:29:00 +01:00
parent b7b4e2d032
commit 997a512e00
96 changed files with 2711 additions and 304 deletions

11
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"spellright.language": [
"German_de_DE"
],
"spellright.documentTypes": [
"html",
"markdown",
"latex",
"plaintext"
]
}

99
package-lock.json generated
View File

@ -2624,6 +2624,15 @@
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
"dev": true "dev": true
}, },
"angularx-qrcode": {
"version": "10.0.11",
"resolved": "https://registry.npmjs.org/angularx-qrcode/-/angularx-qrcode-10.0.11.tgz",
"integrity": "sha512-sbtqdqAboEFNoyxgG4FQYPZDzwX9TlICT2mLpsC/Se3OuT+HntW56q8E/i1BL1fJhx7zt0JJR7bc7LfofUeAlQ==",
"requires": {
"qrcode": "1.4.2",
"tslib": "^2.0.0"
}
},
"ansi-colors": { "ansi-colors": {
"version": "3.2.4", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
@ -2655,7 +2664,6 @@
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": { "requires": {
"color-convert": "^1.9.0" "color-convert": "^1.9.0"
} }
@ -3628,7 +3636,6 @@
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"dev": true,
"requires": { "requires": {
"string-width": "^3.1.0", "string-width": "^3.1.0",
"strip-ansi": "^5.2.0", "strip-ansi": "^5.2.0",
@ -3638,14 +3645,12 @@
"ansi-regex": { "ansi-regex": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
"dev": true
}, },
"strip-ansi": { "strip-ansi": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": { "requires": {
"ansi-regex": "^4.1.0" "ansi-regex": "^4.1.0"
} }
@ -3747,7 +3752,6 @@
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": { "requires": {
"color-name": "1.1.3" "color-name": "1.1.3"
} }
@ -3755,8 +3759,7 @@
"color-name": { "color-name": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
"dev": true
}, },
"color-string": { "color-string": {
"version": "1.5.4", "version": "1.5.4",
@ -4472,8 +4475,7 @@
"decamelize": { "decamelize": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
"dev": true
}, },
"decode-uri-component": { "decode-uri-component": {
"version": "0.2.0", "version": "0.2.0",
@ -4715,6 +4717,11 @@
} }
} }
}, },
"dijkstrajs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz",
"integrity": "sha1-082BIh4+pAdCz83lVtTpnpjdxxs="
},
"dir-glob": { "dir-glob": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -4870,8 +4877,7 @@
"emoji-regex": { "emoji-regex": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
"dev": true
}, },
"emojis-list": { "emojis-list": {
"version": "3.0.0", "version": "3.0.0",
@ -5695,7 +5701,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"requires": { "requires": {
"locate-path": "^3.0.0" "locate-path": "^3.0.0"
} }
@ -5842,8 +5847,7 @@
"get-caller-file": { "get-caller-file": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
"dev": true
}, },
"get-stream": { "get-stream": {
"version": "4.1.0", "version": "4.1.0",
@ -6825,8 +6829,7 @@
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
"dev": true
}, },
"is-glob": { "is-glob": {
"version": "4.0.1", "version": "4.0.1",
@ -7661,7 +7664,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": { "requires": {
"p-locate": "^3.0.0", "p-locate": "^3.0.0",
"path-exists": "^3.0.0" "path-exists": "^3.0.0"
@ -8966,7 +8968,6 @@
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": { "requires": {
"p-try": "^2.0.0" "p-try": "^2.0.0"
} }
@ -8975,7 +8976,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": { "requires": {
"p-limit": "^2.0.0" "p-limit": "^2.0.0"
} }
@ -9001,8 +9001,7 @@
"p-try": { "p-try": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
"dev": true
}, },
"pacote": { "pacote": {
"version": "9.5.12", "version": "9.5.12",
@ -9284,8 +9283,7 @@
"path-exists": { "path-exists": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
"dev": true
}, },
"path-is-absolute": { "path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
@ -9378,6 +9376,11 @@
"find-up": "^3.0.0" "find-up": "^3.0.0"
} }
}, },
"pngjs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
"integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="
},
"pnp-webpack-plugin": { "pnp-webpack-plugin": {
"version": "1.6.4", "version": "1.6.4",
"resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz",
@ -10542,6 +10545,24 @@
"integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==",
"dev": true "dev": true
}, },
"qrcode": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.2.tgz",
"integrity": "sha512-eR6RgxFYPDFH+zFLTJKtoNP/RlsHANQb52AUmQ2bGDPMuUw7jJb0F+DNEgx7qQGIElrbFxWYMc0/B91zLZPF9Q==",
"requires": {
"dijkstrajs": "^1.0.1",
"isarray": "^2.0.1",
"pngjs": "^3.3.0",
"yargs": "^13.2.4"
},
"dependencies": {
"isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
}
}
},
"qs": { "qs": {
"version": "6.7.0", "version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
@ -10893,14 +10914,12 @@
"require-directory": { "require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
"dev": true
}, },
"require-main-filename": { "require-main-filename": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
"dev": true
}, },
"requires-port": { "requires-port": {
"version": "1.0.0", "version": "1.0.0",
@ -11431,8 +11450,7 @@
"set-blocking": { "set-blocking": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
"dev": true
}, },
"set-immediate-shim": { "set-immediate-shim": {
"version": "1.0.1", "version": "1.0.1",
@ -12213,7 +12231,6 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": { "requires": {
"emoji-regex": "^7.0.1", "emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0", "is-fullwidth-code-point": "^2.0.0",
@ -12223,14 +12240,12 @@
"ansi-regex": { "ansi-regex": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
"dev": true
}, },
"strip-ansi": { "strip-ansi": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": { "requires": {
"ansi-regex": "^4.1.0" "ansi-regex": "^4.1.0"
} }
@ -14117,8 +14132,7 @@
"which-module": { "which-module": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
"dev": true
}, },
"worker-farm": { "worker-farm": {
"version": "1.7.0", "version": "1.7.0",
@ -14164,7 +14178,6 @@
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"requires": { "requires": {
"ansi-styles": "^3.2.0", "ansi-styles": "^3.2.0",
"string-width": "^3.0.0", "string-width": "^3.0.0",
@ -14174,14 +14187,12 @@
"ansi-regex": { "ansi-regex": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
"dev": true
}, },
"strip-ansi": { "strip-ansi": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": { "requires": {
"ansi-regex": "^4.1.0" "ansi-regex": "^4.1.0"
} }
@ -14244,8 +14255,7 @@
"y18n": { "y18n": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
"dev": true
}, },
"yallist": { "yallist": {
"version": "4.0.0", "version": "4.0.0",
@ -14257,7 +14267,6 @@
"version": "13.3.2", "version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true,
"requires": { "requires": {
"cliui": "^5.0.0", "cliui": "^5.0.0",
"find-up": "^3.0.0", "find-up": "^3.0.0",
@ -14275,7 +14284,6 @@
"version": "13.1.2", "version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": { "requires": {
"camelcase": "^5.0.0", "camelcase": "^5.0.0",
"decamelize": "^1.2.0" "decamelize": "^1.2.0"
@ -14284,8 +14292,7 @@
"camelcase": { "camelcase": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
"dev": true
} }
} }
}, },

View File

@ -21,6 +21,7 @@
"@angular/platform-browser": "~10.1.5", "@angular/platform-browser": "~10.1.5",
"@angular/platform-browser-dynamic": "~10.1.5", "@angular/platform-browser-dynamic": "~10.1.5",
"@angular/router": "~10.1.5", "@angular/router": "~10.1.5",
"angularx-qrcode": "^10.0.11",
"openpgp": "^4.10.8", "openpgp": "^4.10.8",
"rxjs": "~6.6.0", "rxjs": "~6.6.0",
"tslib": "^2.0.0", "tslib": "^2.0.0",

12
src/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"spellright.language": [
"English (British)",
"German_de_DE"
],
"spellright.documentTypes": [
"html",
"markdown",
"latex",
"plaintext"
]
}

View File

@ -2,10 +2,17 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { AuthGuard, AuthUpdateGuard, AuthenticatedGuard, AnonymousGuard } from './auth/auth.guard'; import { AuthGuard, AuthUpdateGuard, AuthenticatedGuard, AnonymousGuard } from './auth/auth.guard';
import { HomeComponent } from './pages/home/home.component'; import { HomeComponent, ImprintComponent, PrivacyPolicyComponent } from './pages/home/home.component';
import { HomeClubComponent } from './pages/home/club/home-club.component';
import { HomeGeneralComponent } from './pages/home/general/home-general.component';
import { HomePrivacyComponent } from './pages/home/privacy/home-privacy.component';
import { HomeServicesComponent } from './pages/home/services/home-services.component';
import { LoginComponent } from './pages/login/login.component'; import { LoginComponent } from './pages/login/login.component';
import { LoginTotpComponent } from './pages/login-totp/login-totp.component';
import { FormLoginComponent } from './pages/form-login/form-login.component'; import { FormLoginComponent } from './pages/form-login/form-login.component';
import { FormLoginTotpComponent } from './pages/form-login-totp/form-login-totp.component';
import { PasswordComponent } from './pages/password/password.component'; import { PasswordComponent } from './pages/password/password.component';
import { PasswordResetComponent } from './pages/password-reset/password-reset.component';
import { AccountComponent } from './pages/account/account.component'; import { AccountComponent } from './pages/account/account.component';
import { RegisterComponent } from './pages/register/register.component'; import { RegisterComponent } from './pages/register/register.component';
import { TokensComponent } from './pages/tokens/tokens.component'; import { TokensComponent } from './pages/tokens/tokens.component';
@ -17,23 +24,30 @@ import { UnavailableComponent } from './pages/unavailable/unavailable.component'
import { NotfoundComponent } from './pages/notfound/notfound.component'; import { NotfoundComponent } from './pages/notfound/notfound.component';
const routes: Routes = [ const routes: Routes = [
{ path: '', component: HomeComponent, canActivate: [AuthUpdateGuard], pathMatch: 'full', runGuardsAndResolvers: 'always' }, { path: '', redirectTo: "/general", pathMatch: 'full' },
{
path: '', component: HomeComponent, canActivate: [AuthUpdateGuard], runGuardsAndResolvers: 'always', children: [
{ path: 'general', component: HomeGeneralComponent, canActivate: [AuthUpdateGuard] },
{ path: 'privacy', component: HomePrivacyComponent, canActivate: [AuthUpdateGuard] },
{ path: 'services', component: HomeServicesComponent, canActivate: [AuthUpdateGuard] },
{ path: 'club', component: HomeClubComponent, canActivate: [AuthUpdateGuard] },
]
},
{ path: 'imprint', component: ImprintComponent, canActivate: [AuthUpdateGuard] },
{ path: 'privacy-policy', component: PrivacyPolicyComponent, canActivate: [AuthUpdateGuard] },
{ path: 'login', component: LoginComponent, canActivate: [AnonymousGuard] }, { path: 'login', component: LoginComponent, canActivate: [AnonymousGuard] },
{ path: 'login/totp', component: LoginTotpComponent, canActivate: [AnonymousGuard] },
{ path: 'external-login', component: FormLoginComponent, canActivate: [AnonymousGuard] }, { path: 'external-login', component: FormLoginComponent, canActivate: [AnonymousGuard] },
{ path: 'external-login/totp', component: FormLoginTotpComponent, canActivate: [AnonymousGuard] },
{ path: 'password', component: PasswordComponent, canActivate: [AnonymousGuard] }, { path: 'password', component: PasswordComponent, canActivate: [AnonymousGuard] },
{ path: 'password-reset', component: PasswordResetComponent, canActivate: [AnonymousGuard] },
{ path: 'apps', component: AppsComponent, canActivate: [AuthenticatedGuard] }, { path: 'apps', component: AppsComponent, canActivate: [AuthenticatedGuard] },
{ {
path: 'account', component: AccountComponent, canActivate: [AuthenticatedGuard], children: [ path: 'account', component: AccountComponent, canActivate: [AuthenticatedGuard], children: [
{ { path: 'info', component: InfoComponent, canActivate: [AuthenticatedGuard] },
path: 'info', component: InfoComponent, canActivate: [AuthenticatedGuard] { path: 'voucher', component: VoucherComponent, canActivate: [AuthenticatedGuard] },
}, { path: 'security', component: SecurityComponent, canActivate: [AuthenticatedGuard] }
{
path: 'voucher', component: VoucherComponent, canActivate: [AuthenticatedGuard]
},
{
path: 'security', component: SecurityComponent, canActivate: [AuthenticatedGuard]
}
] ]
}, },
{ path: 'register', component: RegisterComponent, canActivate: [AnonymousGuard] }, { path: 'register', component: RegisterComponent, canActivate: [AnonymousGuard] },

View File

@ -1,20 +1,21 @@
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<a href="javascript:" mat-icon-button aria-label="Menu"> <a href="javascript:" mat-icon-button>
<mat-icon (click)="sidenav.toggle()">menu</mat-icon> <mat-icon (click)="sidenav.toggle()">menu</mat-icon>
</a> </a>
<mat-icon svgIcon="logo"></mat-icon>
<span> <span>
<mat-icon svgIcon="logo"></mat-icon> we.bstly we.bstly
</span> </span>
<span class="spacer"></span> <span class="spacer"></span>
<ng-container> <ng-container>
<button mat-button [matMenuTriggerFor]="menu"> <button *ngIf="locales.length > 1" mat-button [matMenuTriggerFor]="menu">
<mat-icon>language</mat-icon> <mat-icon>language</mat-icon>
<mat-icon>arrow_drop_down</mat-icon> <mat-icon>arrow_drop_down</mat-icon>
</button> </button>
<mat-menu #menu="matMenu"> <mat-menu #menu="matMenu">
<a mat-menu-item (click)="setLocale('en')">{{'locale.en.long' | i18n}}</a> <a *ngFor="let locale of locales" mat-menu-item
<a mat-menu-item (click)="setLocale('de-informal')">{{'locale.de-informal.long' | i18n}}</a> (click)="setLocale(locale)">{{'locale.' + locale + '.long' | i18n}}</a>
</mat-menu> </mat-menu>
</ng-container> </ng-container>
</mat-toolbar> </mat-toolbar>
@ -23,7 +24,7 @@
<mat-sidenav #sidenav [mode]="isBiggerScreen() ? 'side' : 'over'" [(opened)]="opened" <mat-sidenav #sidenav [mode]="isBiggerScreen() ? 'side' : 'over'" [(opened)]="opened"
(click)="!isBiggerScreen() && opened=false"> (click)="!isBiggerScreen() && opened=false">
<mat-nav-list> <mat-nav-list>
<a routerLink="/" aria-label="Home" mat-list-item> <a routerLink="/general" mat-list-item>
<mat-icon>home</mat-icon> {{'home' | i18n}} <mat-icon>home</mat-icon> {{'home' | i18n}}
</a> </a>
<a *ngIf="!auth || auth && !auth.authenticated" routerLink="/login" routerLinkActive="active" mat-list-item> <a *ngIf="!auth || auth && !auth.authenticated" routerLink="/login" routerLinkActive="active" mat-list-item>
@ -35,10 +36,10 @@
<a *ngIf="auth && auth.authenticated" routerLink="/apps" routerLinkActive="active" mat-list-item> <a *ngIf="auth && auth.authenticated" routerLink="/apps" routerLinkActive="active" mat-list-item>
<mat-icon>widgets</mat-icon> {{'apps' | i18n}} <mat-icon>widgets</mat-icon> {{'apps' | i18n}}
</a> </a>
<a routerLink="/tokens" aria-label="Enter tokens" mat-list-item> <a routerLink="/tokens" mat-list-item>
<mat-icon>card_giftcard</mat-icon> {{'tokens.redeem' | i18n}} <mat-icon>card_giftcard</mat-icon> {{'tokens.redeem' | i18n}}
</a> </a>
<a href="https://we.bstly.de" target="_blank" aria-label="Go to we.bstly.de" mat-list-item> <a href="https://we.bstly.de" target="_blank" mat-list-item>
<mat-icon>shopping_cart</mat-icon> {{'tokens.get' | i18n}}<mat-icon style="font-size: 1em;">open_in_new <mat-icon>shopping_cart</mat-icon> {{'tokens.get' | i18n}}<mat-icon style="font-size: 1em;">open_in_new
</mat-icon> </mat-icon>
</a> </a>
@ -46,6 +47,17 @@
<mat-icon>exit_to_app</mat-icon> {{'logout' | i18n}} <mat-icon>exit_to_app</mat-icon> {{'logout' | i18n}}
</a> </a>
</mat-nav-list> </mat-nav-list>
<span class="spacer"></span>
<mat-nav-list>
<a routerLink="/imprint" mat-list-item style="font-size: 0.7em;">
{{'imprint' | i18n}}
</a>
<a routerLink="/privacy-policy" mat-list-item style="font-size: 0.7em;">
{{'privacy-policy' | i18n}}
</a>
</mat-nav-list>
</mat-sidenav> </mat-sidenav>
<!-- Main content --> <!-- Main content -->

View File

@ -1,33 +0,0 @@
.spacer {
flex: 1 1 auto;
}
mat-sidenav-container {
height: 100%;
max-height: 100%;
}
.container {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
margin-bottom: 15px;
@media screen and (min-width: 576px) {
width: 540px;
}
@media screen and (min-width: 768px) {
width: 580px;
}
@media screen and (min-width: 992px) {
width: 820px;
}
@media screen and (min-width: 1200px) {
width: 1000px;
}
}

View File

@ -16,11 +16,12 @@ export class AppComponent {
opened = true; opened = true;
title = 'we.bstly'; title = 'we.bstly';
currentLocale: String; currentLocale: String;
locales;
auth; auth;
constructor(private i18n: I18nService, private authService: AuthService, private router: Router, private iconRegistry: MatIconRegistry, private sanitizer: DomSanitizer) { constructor(private i18n: I18nService, private authService: AuthService, private router: Router, private iconRegistry: MatIconRegistry, private sanitizer: DomSanitizer) {
this.currentLocale = this.i18n.getLocale(); this.currentLocale = this.i18n.getLocale();
this.locales = this.i18n.getLocales();
this.authService.auth.subscribe(data => { this.authService.auth.subscribe(data => {
this.auth = data; this.auth = data;
}) })
@ -45,7 +46,6 @@ export class AppComponent {
logout() { logout() {
this.authService.logout().subscribe(data => { this.authService.logout().subscribe(data => {
this.router.navigate([""]); this.router.navigate([""]);
console.log("ja?");
}) })
} }

View File

@ -6,24 +6,31 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HttpInterceptor, HttpHandler, HttpRequest, HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpClientModule, HttpInterceptor, HttpHandler, HttpRequest, HTTP_INTERCEPTORS } from '@angular/common/http';
import { MaterialModule } from './material/material.module'; import { MaterialModule } from './material/material.module';
import { QRCodeModule } from 'angularx-qrcode';
import { I18nPipe } from './utils/i18n.pipe'; import { I18nPipe } from './utils/i18n.pipe';
import { HomeComponent } from './pages/home/home.component'; import { HomeComponent, ImprintComponent, PrivacyPolicyComponent } from './pages/home/home.component';
import { HomeClubComponent } from './pages/home/club/home-club.component';
import { HomeGeneralComponent } from './pages/home/general/home-general.component';
import { HomePrivacyComponent } from './pages/home/privacy/home-privacy.component';
import { HomeServicesComponent } from './pages/home/services/home-services.component';
import { AccountComponent } from './pages/account/account.component'; import { AccountComponent } from './pages/account/account.component';
import { AppsComponent } from './pages/apps/apps.component'; import { AppsComponent } from './pages/apps/apps.component';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { LoginComponent } from './pages/login/login.component'; import { LoginComponent } from './pages/login/login.component';
import { LoginTotpComponent } from './pages/login-totp/login-totp.component';
import { FormLoginComponent } from './pages/form-login/form-login.component'; import { FormLoginComponent } from './pages/form-login/form-login.component';
import { FormLoginTotpComponent } from './pages/form-login-totp/form-login-totp.component';
import { TokensComponent } from './pages/tokens/tokens.component'; import { TokensComponent } from './pages/tokens/tokens.component';
import { PermissionsComponent } from './ui/permissions/permissions.component'; import { PermissionsComponent } from './ui/permissions/permissions.component';
import { QuotasComponent } from './ui/quotas/quotas.component'; import { QuotasComponent } from './ui/quotas/quotas.component';
import { SecurityComponent } from './pages/account/security/security.component'; import { SecurityComponent, SecurityTotpDialog } from './pages/account/security/security.component';
import { VoucherComponent } from './pages/account/voucher/voucher.component'; import { VoucherComponent } from './pages/account/voucher/voucher.component';
import { VoucherDialog } from './pages/account/voucher/voucher.component'; import { VoucherDialog } from './pages/account/voucher/voucher.component';
import { InfoComponent } from './pages/account/info/info.component'; import { InfoComponent } from './pages/account/info/info.component';
import { PasswordComponent } from './pages/password/password.component'; import { PasswordComponent } from './pages/password/password.component';
import { RegisterComponent } from './pages/register/register.component'; import { PasswordResetComponent } from './pages/password-reset/password-reset.component';
import { RegisterDialog } from './pages/register/register.component'; import { RegisterComponent, RegisterDialog } from './pages/register/register.component';
import { UsernameDialog } from './pages/register/username-dialog/username.dialog'; import { UsernameDialog } from './pages/register/username-dialog/username.dialog';
import { UnavailableComponent } from './pages/unavailable/unavailable.component'; import { UnavailableComponent } from './pages/unavailable/unavailable.component';
import { NotfoundComponent } from './pages/notfound/notfound.component'; import { NotfoundComponent } from './pages/notfound/notfound.component';
@ -34,7 +41,7 @@ import { I18nService } from './services/i18n.service';
export function init_app(i18n: I18nService) { export function init_app(i18n: I18nService) {
return () => i18n.fetch(i18n.getLocale()); return () => i18n.fetch(i18n.getLocale()).then(response => { }, error => { });
} }
@Injectable() @Injectable()
@ -52,19 +59,27 @@ export class XhrInterceptor implements HttpInterceptor {
declarations: [ declarations: [
I18nPipe, I18nPipe,
AppComponent, AppComponent,
HomeComponent, HomeComponent, ImprintComponent, PrivacyPolicyComponent,
HomeClubComponent,
HomeGeneralComponent,
HomePrivacyComponent,
HomeServicesComponent,
AccountComponent, AccountComponent,
LoginComponent, LoginComponent,
LoginTotpComponent,
FormLoginComponent, FormLoginComponent,
FormLoginTotpComponent,
TokensComponent, TokensComponent,
AppsComponent, AppsComponent,
PermissionsComponent, PermissionsComponent,
QuotasComponent, QuotasComponent,
SecurityComponent, SecurityComponent,
SecurityTotpDialog,
VoucherComponent, VoucherComponent,
VoucherDialog, VoucherDialog,
InfoComponent, InfoComponent,
PasswordComponent, PasswordComponent,
PasswordResetComponent,
RegisterComponent, RegisterComponent,
RegisterDialog, RegisterDialog,
UsernameDialog, UsernameDialog,
@ -80,6 +95,7 @@ export class XhrInterceptor implements HttpInterceptor {
HttpClientModule, HttpClientModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
QRCodeModule,
], ],
exports: [MaterialModule], exports: [MaterialModule],
providers: [{ provide: APP_INITIALIZER, useFactory: init_app, deps: [I18nService], multi: true }, { provide: HTTP_INTERCEPTORS, useClass: XhrInterceptor, multi: true }], providers: [{ provide: APP_INITIALIZER, useFactory: init_app, deps: [I18nService], multi: true }, { provide: HTTP_INTERCEPTORS, useClass: XhrInterceptor, multi: true }],

View File

@ -1,4 +1,4 @@
<h2>{{'greet' | i18n:auth.name}} <mat-icon aria-hidden="false" aria-label="Smile">sentiment_satisfied_alt</mat-icon> <h2>{{'greet' | i18n:auth.name}} <mat-icon>sentiment_satisfied_alt</mat-icon>
</h2> </h2>
<nav mat-tab-nav-bar> <nav mat-tab-nav-bar>

View File

@ -0,0 +1,16 @@
<h1 mat-dialog-title>{{'security.2fa.totp.enable' | i18n}}</h1>
<div mat-dialog-content>
{{'security.2fa.totp.hint' | i18n}}
<qrcode *ngIf="data.qrData" [qrdata]="data.qrData" [width]="400" [errorCorrectionLevel]="'M'" title="{{data.qrData}}"></qrcode>
{{'security.2fa.totp.activate' | i18n}}
<mat-form-field>
<input matInput placeholder="{{'security.2fa.totp.code' | i18n}}" [formControl]="code" required>
</mat-form-field>
</div>
<div mat-dialog-actions>
<button mat-button [mat-dialog-close]="false">{{'cancel' | i18n}}</button>
<button [disabled]="code.invalid" mat-raised-button [mat-dialog-close]="code.value" cdkFocusInitial color="accent">{{'security.2fa.totp.enable' | i18n}}</button>
</div>

View File

@ -1,4 +1,4 @@
<form [formGroup]="form" (ngSubmit)="changePassword()" #formDirective="ngForm"> <form [formGroup]="passwordForm" (ngSubmit)="changePassword()" #passwordFormDirective="ngForm">
<mat-card> <mat-card>
<mat-card-content> <mat-card-content>
<h2>{{'password.change' | i18n}}</h2> <h2>{{'password.change' | i18n}}</h2>
@ -6,22 +6,22 @@
{{'password.changed' | i18n}} {{'password.changed' | i18n}}
</mat-hint> </mat-hint>
<mat-form-field> <mat-form-field>
<input matInput type="password" placeholder="{{'password.current' | i18n}}" formControlName="oldPassword" <input matInput type="password" placeholder="{{'password.current' | i18n}}"
[(ngModel)]="model.old"> formControlName="oldPassword" [(ngModel)]="model.old">
<mat-error *ngFor="let error of form.get('oldPassword').errors | keyvalue"> <mat-error *ngFor="let error of passwordForm.get('oldPassword').errors | keyvalue">
{{error.key}} {{error.key}}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<input matInput type="password" placeholder="{{'password' | i18n}}" formControlName="password" <input matInput type="password" placeholder="{{'password' | i18n}}" formControlName="password"
[(ngModel)]="model.password"> [(ngModel)]="model.password">
<mat-error *ngFor="let error of form.get('password').errors | keyvalue"> <mat-error *ngFor="let error of passwordForm.get('password').errors | keyvalue">
{{error.key}} {{error.key}}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<input matInput type="password" placeholder="{{'password.confirm' | i18n}}" formControlName="password2" <input matInput type="password" length="6" placeholder="{{'password.confirm' | i18n}}"
[(ngModel)]="model.password2"> formControlName="password2" [(ngModel)]="model.password2">
<mat-error> <mat-error>
{{'password.not-match' | i18n}} {{'password.not-match' | i18n}}
</mat-error> </mat-error>
@ -29,9 +29,20 @@
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<mat-progress-bar *ngIf="working" mode="indeterminate"></mat-progress-bar> <mat-progress-bar *ngIf="working" mode="indeterminate"></mat-progress-bar>
<button *ngIf="!working" mat-raised-button color="primary" [disabled]="form.invalid"> <button *ngIf="!working" mat-raised-button color="primary" [disabled]="passwordForm.invalid">
{{'password.change' | i18n}} {{'password.change' | i18n}}
</button> </button>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
</form> </form>
<mat-card>
<mat-card-content>
<h2>{{'security.2fa' | i18n}}</h2>
<p>{{'security.2fa.info' | i18n}}</p>
</mat-card-content>
<mat-card-actions>
<button *ngIf="!totp" (click)="createTotp()" mat-raised-button color="accent">{{'security.2fa.totp.create' | i18n}}</button>
<button *ngIf="totp" (click)="removeTotp()" mat-raised-button color="warn">{{'security.2fa.totp.remove' | i18n}}</button>
</mat-card-actions>
</mat-card>

View File

@ -1,3 +1,3 @@
mat-form-field { mat-form-field {
display: block; display: inline;
} }

View File

@ -1,6 +1,8 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild, Inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators, NgForm } from '@angular/forms'; import { FormBuilder, FormGroup, FormControl, Validators, NgForm } from '@angular/forms';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { AuthService } from './../../../services/auth.service';
import { UserService } from './../../../services/user.service'; import { UserService } from './../../../services/user.service';
import { MatchingValidator } from './../../../utils/matching.validator'; import { MatchingValidator } from './../../../utils/matching.validator';
@ -15,28 +17,39 @@ export class SecurityComponent implements OnInit {
model: any = {}; model: any = {};
public working: boolean; public working: boolean;
public success: boolean; public success: boolean;
form: FormGroup; public totp: boolean = false;
@ViewChild('formDirective') private formDirective: NgForm;
constructor(private formBuilder: FormBuilder, private userService: UserService) { } passwordForm: FormGroup;
@ViewChild('passwordFormDirective') private passwordFormDirective: NgForm;
constructor(
private formBuilder: FormBuilder,
private userService: UserService,
private authService: AuthService,
public dialog: MatDialog) { }
ngOnInit(): void { ngOnInit(): void {
this.form = this.formBuilder.group({ this.passwordForm = this.formBuilder.group({
oldPassword: ['', Validators.required], oldPassword: ['', Validators.required],
password: ['', Validators.required], password: ['', Validators.required],
password2: ['', Validators.required] password2: ['', Validators.required]
}, { }, {
validator: MatchingValidator('password', 'password2') validator: MatchingValidator('password', 'password2')
}); });
this.authService.isTotpEnabled().subscribe(response => {
this.totp = true;
}, error => {
this.totp = false;
})
} }
changePassword() { changePassword() {
if (this.form.valid && !this.working) { if (this.passwordForm.valid && !this.working) {
this.working = true; this.working = true;
this.userService.password(this.model).subscribe((result: any) => { this.userService.password(this.model).subscribe((result: any) => {
this.formDirective.resetForm(); this.passwordFormDirective.resetForm();
this.success = true; this.success = true;
this.working = false; this.working = false;
}, (error) => { }, (error) => {
@ -49,11 +62,68 @@ export class SecurityComponent implements OnInit {
} }
for (let code in errors) { for (let code in errors) {
this.form.get(code).setErrors(errors[code]); this.passwordForm.get(code).setErrors(errors[code]);
} }
} }
}) })
} }
} }
createTotp() {
this.authService.createTotp().subscribe((result: any) => {
const dialogRef = this.dialog.open(SecurityTotpDialog, {
closeOnNavigation: false,
disableClose: true,
data: result
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.authService.enableTotp(result).subscribe((result: any) => {
this.totp = true;
})
} else {
this.authService.removeTotp().subscribe((result: any) => {
this.totp = false;
})
}
});
})
}
enableTotp() {
const dialogRef = this.dialog.open(SecurityTotpDialog, {
closeOnNavigation: false,
disableClose: true,
data: {}
});
}
removeTotp() {
this.authService.removeTotp().subscribe((result: any) => {
this.totp = false;
})
}
}
@Component({
selector: 'app-security-totp-dialog',
templateUrl: 'security-totp.dialog.html',
styleUrls: ['./security.component.scss']
})
export class SecurityTotpDialog {
code: FormControl;
constructor(
public dialogRef: MatDialogRef<SecurityTotpDialog>,
@Inject(MAT_DIALOG_DATA) public data: any
) { }
ngOnInit(): void {
this.code = new FormControl('', [Validators.required, Validators.pattern("[0-9]{6}")]);
}
} }

View File

@ -57,7 +57,6 @@ export class VoucherComponent implements OnInit {
data: this.model data: this.model
}); });
}, error => { }, error => {
console.log(error);
}) })
} }

View File

@ -1,20 +1,17 @@
<h3>{{'apps' | i18n}}</h3> <h3>{{'apps' | i18n}}</h3>
<mat-grid-list cols="2">
<mat-grid-tile *ngFor="let app of apps"> <mat-card *ngFor="let app of apps">
<mat-card>
<mat-card-header> <mat-card-header>
<mat-icon>{{'app.' + app.name + '.icon' | i18n}}</mat-icon> <mat-icon>{{'apps.' + app.name + '.icon' | i18n}}</mat-icon>
<mat-card-title>{{'app.' + app.name + '.title' | i18n}}</mat-card-title> <mat-card-title>{{'apps.' + app.name + '.title' | i18n}}</mat-card-title>
<mat-card-subtitle>{{'app.' + app.name + '.subtitle' | i18n}}</mat-card-subtitle> <mat-card-subtitle>{{'apps.' + app.name + '.subtitle' | i18n}}</mat-card-subtitle>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<p> <p>
{{ 'app.' + app.name + '.text' | i18n}} {{ 'apps.' + app.name + '.text' | i18n}}
</p> </p>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<a href="{{app.url}}" target="_blank" mat-raised-button color="primary">{{'app.goto' | i18n}}</a> <a href="{{app.url}}" target="_blank" mat-raised-button color="primary">{{'apps.goto' | i18n}}</a>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
</mat-grid-tile>
</mat-grid-list>

View File

@ -1,4 +0,0 @@
mat-card {
width: 100%;
margin: 2em 2em;
}

View File

@ -0,0 +1,28 @@
<form [formGroup]="form">
<form ngNoForm action="{{apiUrl}}/auth/formlogin/totp" method="POST">
<mat-card>
<mat-card-content>
<h2>{{'security.2fa.totp.external' | i18n}}<mat-icon style="font-size: 1em;">open_in_new
</mat-icon></h2>
<mat-error *ngIf="loginInvalid">
{{'security.2fa.totp.invalid' | i18n}}
</mat-error>
<mat-form-field>
<input id="code" name="code" matInput placeholder="{{'security.2fa.totp.code' | i18n}}"
formControlName="code" required>
<mat-error>
{{'security.2fa.totp.missing' | i18n}}
</mat-error>
</mat-form-field>
<mat-slide-toggle id="keep" name="keep" formControlName="keep">
{{'security.2fa.totp.keepSession' | i18n}}
</mat-slide-toggle>
</mat-card-content>
<mat-card-actions>
<button type="submit" mat-raised-button color="primary"
[disabled]="form.invalid">{{'security.2fa.totp.login' | i18n}}<mat-icon style="font-size: 1em;">open_in_new
</mat-icon></button>
</mat-card-actions>
</mat-card>
</form>
</form>

View File

@ -0,0 +1,3 @@
mat-form-field {
display: block;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormLoginTotpComponent } from './form-login-totp.component';
describe('FormLoginTotpComponent', () => {
let component: FormLoginTotpComponent;
let fixture: ComponentFixture<FormLoginTotpComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FormLoginTotpComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FormLoginTotpComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,28 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { environment } from '../../../environments/environment';
@Component({
selector: 'app-form-login-totp',
templateUrl: './form-login-totp.component.html',
styleUrls: ['./form-login-totp.component.scss']
})
export class FormLoginTotpComponent implements OnInit {
form: FormGroup;
public loginInvalid: boolean;
public apiUrl = environment.apiUrl;
constructor(private formBuilder: FormBuilder) { }
async ngOnInit() {
this.form = this.formBuilder.group({
code: ['', Validators.required],
keep : ['']
});
}
}

View File

@ -1,9 +1,10 @@
<form [formGroup]="form"> <form [formGroup]="form">
<form ngNoForm action="{{apiUrl}}/auth/login" method="POST"> <form ngNoForm action="{{apiUrl}}/auth/formlogin" method="POST">
<mat-card> <mat-card>
<mat-card-content> <mat-card-content>
<h2>{{'login.external' | i18n}}<mat-icon style="font-size: 1em;">open_in_new <h2>{{'login.external' | i18n}}<mat-icon style="font-size: 1em;">open_in_new
</mat-icon></h2> </mat-icon>
</h2>
<mat-error *ngIf="loginInvalid"> <mat-error *ngIf="loginInvalid">
{{'login.invalid' | i18n}} {{'login.invalid' | i18n}}
</mat-error> </mat-error>
@ -21,13 +22,15 @@
{{'password.invalid.hint' | i18n}} {{'password.invalid.hint' | i18n}}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-slide-toggle id="keep" name="keep" formControlName="keep">
{{'login.keepSession' | i18n}}
</mat-slide-toggle>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<button type="submit" mat-raised-button color="primary" <button type="submit" mat-raised-button color="primary"
[disabled]="form.invalid">{{'login.external' | i18n}}<mat-icon style="font-size: 1em;">open_in_new [disabled]="form.invalid">{{'login.external' | i18n}}<mat-icon style="font-size: 1em;">open_in_new
</mat-icon></button> </mat-icon></button>
<a routerLink="/password" aria-label="Enter tokens" mat-raised-button <a routerLink="/password" mat-raised-button color="warn">{{'password.forgot' | i18n}}</a>
color="warn">{{'password.forgot' | i18n}}</a>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
</form> </form>

View File

@ -19,7 +19,8 @@ export class FormLoginComponent implements OnInit {
async ngOnInit() { async ngOnInit() {
this.form = this.formBuilder.group({ this.form = this.formBuilder.group({
username: ['', Validators.required], username: ['', Validators.required],
password: ['', Validators.required] password: ['', Validators.required],
keep : ['']
}); });
} }

View File

@ -0,0 +1,29 @@
<mat-accordion>
<mat-expansion-panel expanded>
<mat-expansion-panel-header>
<mat-panel-title>
{{'home.club.membership' | i18n}}
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'club/membership'"></app-html>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{'home.club.about' | i18n}}
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'club/about'"></app-html>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{'home.club.charter' | i18n}}
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'club/charter'"></app-html>
</mat-expansion-panel>
</mat-accordion>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeClubComponent } from './home-club.component';
describe('HomeClubComponent', () => {
let component: HomeClubComponent;
let fixture: ComponentFixture<HomeClubComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ HomeClubComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HomeClubComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-home-club',
templateUrl: './home-club.component.html'
})
export class HomeClubComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}

View File

@ -0,0 +1,28 @@
<mat-accordion>
<mat-expansion-panel expanded>
<mat-expansion-panel-header>
<mat-panel-title>
{{'home.general.what' | i18n}}
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'general/what'"></app-html>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{'home.general.you' | i18n}}
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'general/you'"></app-html>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{'home.general.we' | i18n}}
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'general/we'"></app-html>
</mat-expansion-panel>
</mat-accordion>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeGeneralComponent } from './home-general.component';
describe('HomeGeneralComponent', () => {
let component: HomeGeneralComponent;
let fixture: ComponentFixture<HomeGeneralComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ HomeGeneralComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HomeGeneralComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-home-general',
templateUrl: './home-general.component.html'
})
export class HomeGeneralComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}

View File

@ -1,28 +1,15 @@
<app-html [template]="'about'"></app-html> <app-html [template]="'about'"></app-html>
<h3>F.A.Q. - Frequently Asked Questions</h3> <nav mat-tab-nav-bar>
<mat-accordion> <a mat-tab-link routerLink="general" routerLinkActive #rlag="routerLinkActive"
<mat-expansion-panel> [active]="rlag.isActive">{{'home.general' | i18n}}</a>
<mat-expansion-panel-header> <a mat-tab-link routerLink="services" routerLinkActive #rlas="routerLinkActive"
<mat-panel-title> [active]="rlas.isActive">{{'home.services' | i18n}}</a>
Question 1 <a mat-tab-link routerLink="privacy" routerLinkActive #rlap="routerLinkActive"
</mat-panel-title> [active]="rlap.isActive">{{'home.privacy' | i18n}}</a>
<mat-panel-description> <a mat-tab-link routerLink="club" routerLinkActive #rlac="routerLinkActive"
Answers 1 summaray [active]="rlac.isActive">{{'home.club' | i18n}}</a>
</mat-panel-description> </nav>
</mat-expansion-panel-header>
<p>TAnswer 1 detail</p>
</mat-expansion-panel>
<mat-expansion-panel> <p></p>
<mat-expansion-panel-header> <router-outlet></router-outlet>
<mat-panel-title>
Question 2
</mat-panel-title>
<mat-panel-description>
Answers 2 summaray
</mat-panel-description>
</mat-expansion-panel-header>
<p>TAnswer 2 detail</p>
</mat-expansion-panel>
</mat-accordion>

View File

@ -1,6 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
templateUrl: './home.component.html', templateUrl: './home.component.html',
@ -15,3 +14,31 @@ export class HomeComponent implements OnInit {
} }
} }
@Component({
selector: 'app-imprint',
templateUrl: './home.imprint.html'
})
export class ImprintComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}
@Component({
selector: 'app-privacy-policy',
templateUrl: './home.privacy-policy.html'
})
export class PrivacyPolicyComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}

View File

@ -0,0 +1 @@
<app-html [template]="'imprint'"></app-html>

View File

@ -0,0 +1 @@
<app-html [template]="'privacy-policy'"></app-html>

View File

@ -0,0 +1,57 @@
<h3> {{'home.privacy.design' | i18n}}</h3>
<mat-accordion>
<mat-expansion-panel expanded hideToggle disabled>
<app-html [template]="'privacy/design'"></app-html>
</mat-expansion-panel>
</mat-accordion>
<h3>{{'home.privacy.services' | i18n}}</h3>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
Webserver
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'privacy/webserver'"></app-html>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
Pretix
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'privacy/pretix'"></app-html>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
we.bstly
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'privacy/we-bstly'"></app-html>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
Nextcloud
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'privacy/nextcloud'"></app-html>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{'home.services.email' | i18n}}
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'privacy/email'"></app-html>
</mat-expansion-panel>
</mat-accordion>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomePrivacyComponent } from './home-privacy.component';
describe('HomePrivacyComponent', () => {
let component: HomePrivacyComponent;
let fixture: ComponentFixture<HomePrivacyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ HomePrivacyComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HomePrivacyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-home-privacy',
templateUrl: './home-privacy.component.html'
})
export class HomePrivacyComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}

View File

@ -0,0 +1,523 @@
<h3>{{'home.services.active' | i18n}}</h3>
<mat-accordion>
<mat-expansion-panel expanded>
<mat-expansion-panel-header>
<mat-panel-title>
we.bstly
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'services/active/we-bstly'"></app-html>
<span>{{'software' | i18n}}:</span>
<mat-chip-list>
<mat-chip color="accent" selected [matMenuTriggerFor]="webstly">we.bstly</mat-chip>
<mat-menu #webstly="matMenu">
<a href="https://git.bstly.de/_Bastler/we.bstly" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="https://www.bstly.de" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
</mat-chip-list>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
Nextcloud
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'services/active/nextcloud'"></app-html>
<span>{{'software' | i18n}}:</span>
<mat-chip-list>
<mat-chip color="accent" selected [matMenuTriggerFor]="nextcloud">Nextcloud</mat-chip>
<mat-menu #nextcloud="matMenu">
<a href="https://github.com/nextcloud/server" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="https://nextcloud.com" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
<mat-chip color="accent" selected [matMenuTriggerFor]="nextcloud_collabora">Collabora Online</mat-chip>
<mat-menu #nextcloud_collabora="matMenu">
<a href="https://github.com/CollaboraOnline/online" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="https://www.collaboraoffice.com/code/" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
</mat-chip-list>
<br>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
Apps
</mat-panel-title>
</mat-expansion-panel-header>
<mat-panel-description>
<mat-chip-list>
<mat-chip>Accessibility</mat-chip>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_announcementcenter">Announcement center
</mat-chip>
<mat-menu #nextcloud_app_announcementcenter="matMenu">
<a href="https://github.com/nextcloud/announcementcenter" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_apporder">AppOrder</mat-chip>
<mat-menu #nextcloud_app_apporder="matMenu">
<a href="https://github.com/juliushaertl/apporder" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_calendar">Calendar</mat-chip>
<mat-menu #nextcloud_app_calendar="matMenu">
<a href="https://apps.nextcloud.com/apps/calendar" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_checksum">Checksum</mat-chip>
<mat-menu #nextcloud_app_checksum="matMenu">
<a href="https://github.com/westberliner/owncloud-checksum/" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_collabora">Collabora Online</mat-chip>
<mat-menu #nextcloud_app_collabora="matMenu">
<a href="https://github.com/nextcloud/richdocuments" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip>Collaborative tags</mat-chip>
<mat-chip>Comments</mat-chip>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_contacts">Contacts</mat-chip>
<mat-menu #nextcloud_app_contacts="matMenu">
<a href="https://github.com/nextcloud/contacts" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_contacts">Contacts</mat-chip>
<mat-menu #nextcloud_app_contacts="matMenu">
<a href="https://github.com/nextcloud/contacts" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_custom_menu">Custom menu</mat-chip>
<mat-menu #nextcloud_app_custom_menu="matMenu">
<a href="https://gitnet.fr/deblan/side_menu" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_data_request">Data Request</mat-chip>
<mat-menu #nextcloud_app_data_request="matMenu">
<a href="https://github.com/nextcloud/data_request" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_deck">Deck</mat-chip>
<mat-menu #nextcloud_app_deck="matMenu">
<a href="https://github.com/nextcloud/deck" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip>Default encryption module</mat-chip>
<mat-chip>Deleted files</mat-chip>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_external_sites">External sites</mat-chip>
<mat-menu #nextcloud_app_external_sites="matMenu">
<a href="https://github.com/nextcloud/external" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip>Federation</mat-chip>
<mat-chip>File sharing</mat-chip>
<mat-chip>First run wizard</mat-chip>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_notifications">Notifications</mat-chip>
<mat-menu #nextcloud_app_notifications="matMenu">
<a href="https://github.com/nextcloud/notifications" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_oidc">OpenID Connect Login</mat-chip>
<mat-menu #nextcloud_app_oidc="matMenu">
<a href="https://github.com/pulsejet/nextcloud-single-openid-connect" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_password_policy">Password policy
</mat-chip>
<mat-menu #nextcloud_app_password_policy="matMenu">
<a href="https://github.com/nextcloud/password_policy" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_pdf">PDF viewer</mat-chip>
<mat-menu #nextcloud_app_pdf="matMenu">
<a href="https://github.com/nextcloud/files_pdfviewer" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_photos">Photos</mat-chip>
<mat-menu #nextcloud_app_photos="matMenu">
<a href="https://github.com/nextcloud/photos" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_polls">Polls</mat-chip>
<mat-menu #nextcloud_app_polls="matMenu">
<a href="https://github.com/nextcloud/polls" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_privacy">Privacy</mat-chip>
<mat-menu #nextcloud_app_privacy="matMenu">
<a href="https://github.com/nextcloud/privacy" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_quota">Quota warning</mat-chip>
<mat-menu #nextcloud_app_quota="matMenu">
<a href="https://github.com/nextcloud/quota_warning" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_rightclick">Right click</mat-chip>
<mat-menu #nextcloud_app_rightclick="matMenu">
<a href="https://github.com/nextcloud/files_rightclick" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip>Share by mail</mat-chip>
<mat-chip>Support</mat-chip>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_talk">Talk</mat-chip>
<mat-menu #nextcloud_app_talk="matMenu">
<a href="https://github.com/nextcloud/spreed" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_tasks">Tasks</mat-chip>
<mat-menu #nextcloud_app_tasks="matMenu">
<a href="https://github.com/nextcloud/tasks/" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_terms">Terms of services</mat-chip>
<mat-menu #nextcloud_app_terms="matMenu">
<a href="https://github.com/nextcloud/terms_of_service/" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_text">Text</mat-chip>
<mat-menu #nextcloud_app_text="matMenu">
<a href="https://github.com/nextcloud/text" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip>Theming</mat-chip>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_2fagateway">Two-Factor Gateway</mat-chip>
<mat-menu #nextcloud_app_2fagateway="matMenu">
<a href="https://github.com/nextcloud/twofactor_gateway" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_2fatotp">Two-Factor TOTP Provider
</mat-chip>
<mat-menu #nextcloud_app_2fatotp="matMenu">
<a href="https://github.com/nextcloud/twofactor_totp" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
<mat-chip>Versions</mat-chip>
<mat-chip selected [matMenuTriggerFor]="nextcloud_app_videoplayer">Video player</mat-chip>
<mat-menu #nextcloud_app_videoplayer="matMenu">
<a href="https://github.com/nextcloud/files_videoplayer" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
</mat-menu>
</mat-chip-list>
</mat-panel-description>
</mat-expansion-panel>
</mat-accordion>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{'home.services.email' | i18n}}
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'services/active/email'"></app-html>
<span>{{'software' | i18n}}:</span>
<mat-chip-list>
<mat-chip color="accent" selected [matMenuTriggerFor]="dovecot">Dovecot</mat-chip>
<mat-menu #dovecot="matMenu">
<a href="https://github.com/dovecot/core" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="https://www.dovecot.org" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
<mat-chip color="accent" selected [matMenuTriggerFor]="postfix">Postfix</mat-chip>
<mat-menu #postfix="matMenu">
<a href="http://www.postfix.org/download.html" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="http://www.postfix.org" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
<mat-chip color="accent" selected [matMenuTriggerFor]="rspamd">Rspamd</mat-chip>
<mat-menu #rspamd="matMenu">
<a href="https://github.com/rspamd/rspamd" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="https://www.rspamd.com" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
<mat-chip color="accent" selected [matMenuTriggerFor]="rainloop">RainLoop Webmail</mat-chip>
<mat-menu #rainloop="matMenu">
<a href="https://github.com/RainLoop/rainloop-webmail" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="https://www.rainloop.net" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
</mat-chip-list>
</mat-expansion-panel>
</mat-accordion>
<h3>{{'home.services.planned' | i18n}}</h3>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
Matrix
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'services/planned/matrix'"></app-html>
<span>{{'software' | i18n}}:</span>
<mat-chip-list>
<mat-chip color="accent" selected [matMenuTriggerFor]="synapse">Synapse</mat-chip>
<mat-menu #synapse="matMenu">
<a href="https://github.com/matrix-org/synapse/" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="https://matrix.org/docs/projects/server/synapse" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
<mat-chip color="accent" selected [matMenuTriggerFor]="element-web">Element Web</mat-chip>
<mat-menu #element-web="matMenu">
<a href="https://github.com/vector-im/element-web" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="https://element.io/" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
</mat-chip-list>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
Gitea
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'services/planned/gitea'"></app-html>
<span>{{'software' | i18n}}:</span>
<mat-chip-list>
<mat-chip color="accent" selected [matMenuTriggerFor]="gitea">Gitea</mat-chip>
<mat-menu #gitea="matMenu">
<a href="https://github.com/go-gitea/gitea" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="https://gitea.io/" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
</mat-chip-list>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
Jitsi Meet
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'services/planned/jitsi-meet'"></app-html>
<span>{{'software' | i18n}}:</span>
<mat-chip-list>
<mat-chip color="accent" selected [matMenuTriggerFor]="jitsi">Jitsi Meet</mat-chip>
<mat-menu #jitsi="matMenu">
<a href="https://github.com/jitsi/jitsi-meet" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="https://jitsi.org/jitsi-meet/" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
</mat-chip-list>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
Wireguard
⚠️
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'services/planned/wireguard'"></app-html>
<span>{{'software' | i18n}}:</span>
<mat-chip-list>
<mat-chip color="accent" selected [matMenuTriggerFor]="wireguard">Wireguard</mat-chip>
<mat-menu #wireguard="matMenu">
<a href="https://www.wireguard.com/repositories/" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="https://www.wireguard.com" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
</mat-chip-list>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
PiHole
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'services/planned/pihole'"></app-html>
<span>{{'software' | i18n}}:</span>
<mat-chip-list>
<mat-chip color="accent" selected [matMenuTriggerFor]="pihole">Pi-Hole</mat-chip>
<mat-menu #pihole="matMenu">
<a href="https://github.com/pi-hole/pi-hole" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="https://pi-hole.net" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
</mat-chip-list>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
BigBlueButton
⚠️
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'services/planned/bigbluebutton'"></app-html>
<span>{{'software' | i18n}}:</span>
<mat-chip-list>
<mat-chip color="accent" selected [matMenuTriggerFor]="bigbluebutton">BigBlueButton</mat-chip>
<mat-menu #bigbluebutton="matMenu">
<a href="https://github.com/bigbluebutton/bigbluebutton" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="https://bigbluebutton.org" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
</mat-chip-list>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
Bitwarden
⚠️
</mat-panel-title>
</mat-expansion-panel-header>
<app-html [template]="'services/planned/bitwarden'"></app-html>
<span>{{'software' | i18n}}:</span>
<mat-chip-list>
<mat-chip color="accent" selected [matMenuTriggerFor]="bitwarden">Bitwarden</mat-chip>
<mat-menu #bitwarden="matMenu">
<a href="https://github.com/bitwarden/server" target="_blank" mat-menu-item>
<mat-icon>code</mat-icon> {{'sourcecode' | i18n}}
</a>
<a href="https://bitwarden.com/" target="_blank" mat-menu-item>
<mat-icon>public</mat-icon> {{'homepage' | i18n}}
</a>
</mat-menu>
</mat-chip-list>
</mat-expansion-panel>
</mat-accordion>
<p>{{'home.services.legend' | i18n}}
<mat-list>
<mat-list-item>{{'home.services.legend.ready' | i18n}}</mat-list-item>
<mat-list-item>{{'home.services.legend.not-ready' | i18n}}</mat-list-item>
<mat-list-item>{{'home.services.legend.not-available' | i18n}}</mat-list-item>
</mat-list>
</p>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeServicesComponent } from './home-services.component';
describe('HomeServicesComponent', () => {
let component: HomeServicesComponent;
let fixture: ComponentFixture<HomeServicesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ HomeServicesComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HomeServicesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-home-services',
templateUrl: './home-services.component.html'
})
export class HomeServicesComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}

View File

@ -0,0 +1,24 @@
<form [formGroup]="form" (ngSubmit)="loginTotp()">
<mat-card>
<mat-card-content>
<h2>{{'security.2fa.totp' | i18n}}</h2>
<mat-error *ngIf="loginInvalid">
{{'security.2fa.totp.invalid' | i18n}}
</mat-error>
<mat-form-field>
<input id="code" name="code" matInput placeholder="{{'security.2fa.totp.code' | i18n}}" formControlName="code"
required>
<mat-error>
{{'security.2fa.totp.missing' | i18n}}
</mat-error>
</mat-form-field>
<mat-slide-toggle id="keep" name="keep" formControlName="keep">
{{'security.2fa.totp.keepSession' | i18n}}
</mat-slide-toggle>
</mat-card-content>
<mat-card-actions>
<button type="submit" mat-raised-button color="primary"
[disabled]="form.invalid">{{'security.2fa.totp.login' | i18n}}</button>
</mat-card-actions>
</mat-card>
</form>

View File

@ -0,0 +1,3 @@
mat-form-field {
display: block;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginTotpComponent } from './login-totp.component';
describe('LoginTotpComponent', () => {
let component: LoginTotpComponent;
let fixture: ComponentFixture<LoginTotpComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LoginTotpComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LoginTotpComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,54 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService } from '../../services/auth.service';
import { Router, ActivatedRoute } from '@angular/router';
import { environment } from '../../../environments/environment';
@Component({
selector: 'app-login',
templateUrl: './login-totp.component.html',
styleUrls: ['./login-totp.component.scss']
})
export class LoginTotpComponent implements OnInit {
form: FormGroup;
public loginInvalid: boolean;
public apiUrl = environment.apiUrl;
targetRoute = '/account/info';
constructor(private formBuilder: FormBuilder, private authService: AuthService, private router: Router, private route: ActivatedRoute) { }
ngOnInit() {
this.form = this.formBuilder.group({
code: ['', Validators.required],
keep: ['']
});
this.route.queryParams.subscribe(params => {
if (params['target']) {
this.targetRoute = params['target'];
}
});
}
async loginTotp() {
this.loginInvalid = false;
if (this.form.valid) {
const totpModel = {
code: this.form.get('code').value,
keep: this.form.get('keep').value
};
this.authService.loginTotp(totpModel).subscribe((response: any) => {
this.router.navigate([this.targetRoute]);
}, error => {
this.loginInvalid = true;
});
}
}
}

View File

@ -19,12 +19,14 @@
{{'password.invalid.hint' | i18n}} {{'password.invalid.hint' | i18n}}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-slide-toggle id="keep" name="keep" formControlName="keep">
{{'login.keepSession' | i18n}}
</mat-slide-toggle>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<button type="submit" mat-raised-button color="primary" <button type="submit" mat-raised-button color="primary"
[disabled]="form.invalid">{{'login' | i18n}}</button> [disabled]="form.invalid">{{'login' | i18n}}</button>
<a routerLink="/password" aria-label="Enter tokens" mat-raised-button <a routerLink="/password" mat-raised-button color="warn">{{'password.forgot' | i18n}}</a>
color="warn">{{'password.forgot' | i18n}}</a>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
</form> </form>

View File

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService } from './../../services/auth.service'; import { AuthService } from './../../services/auth.service';
import { Router } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { environment } from './../../../environments/environment'; import { environment } from './../../../environments/environment';
@ -15,27 +15,40 @@ export class LoginComponent implements OnInit {
form: FormGroup; form: FormGroup;
public loginInvalid: boolean; public loginInvalid: boolean;
public apiUrl = environment.apiUrl; public apiUrl = environment.apiUrl;
targetRoute = '/account/info';
loginModel = {}; loginModel = {};
constructor(private formBuilder: FormBuilder, private authService: AuthService, private router: Router) { } constructor(private formBuilder: FormBuilder, private authService: AuthService, private router: Router, private route: ActivatedRoute) { }
async ngOnInit() { async ngOnInit() {
this.form = this.formBuilder.group({ this.form = this.formBuilder.group({
username: ['', Validators.required], username: ['', Validators.required],
password: ['', Validators.required] password: ['', Validators.required],
keep: ['']
});
this.route.queryParams.subscribe(params => {
if (params['target']) {
this.targetRoute = params['target'];
}
}); });
} }
async login() { async login() {
this.loginInvalid = false; this.loginInvalid = false;
if (this.form.valid) { if (this.form.valid) {
const username = this.form.get('username').value;
const password = this.form.get('password').value; const loginModel = {
this.authService.login(username, password).subscribe((response: any) => { username: this.form.get('username').value,
this.router.navigate(["/account/info"]); password: this.form.get('password').value,
keep: this.form.get('keep').value
};
this.authService.login(loginModel).subscribe((response: any) => {
this.router.navigate([this.targetRoute]);
}, error => { }, error => {
if (error.status == 302) { if (error.status == 428) {
console.log(error); this.router.navigate(["/login/totp"], { queryParams: { target: this.targetRoute } });
} else { } else {
this.loginInvalid = true; this.loginInvalid = true;
} }

View File

@ -0,0 +1,43 @@
<form [formGroup]="form" (ngSubmit)="passwordReset()" *ngIf="!success">
<mat-card>
<mat-card-content>
<h2>{{'password.reset' | i18n}}</h2>
<mat-error *ngIf="tokenInvalid">
{{'password.reset.tokenInvalid' | i18n}}
</mat-error>
<mat-form-field>
<input matInput type="password" placeholder="{{'password' | i18n}}" formControlName="password"
[(ngModel)]="model.password">
<mat-error *ngFor="let error of form.get('password').errors | keyvalue">
{{error.key}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput type="password" length="6" placeholder="{{'password.confirm' | i18n}}"
formControlName="password2" [(ngModel)]="model.password2">
<mat-error>
{{'password.not-match' | i18n}}
</mat-error>
</mat-form-field>
</mat-card-content>
<mat-card-actions>
<button *ngIf="!working" mat-raised-button color="primary" [disabled]="form.invalid">
{{'password.reset' | i18n}}
</button>
<mat-progress-bar *ngIf="working" mode="indeterminate"></mat-progress-bar>
</mat-card-actions>
</mat-card>
</form>
<mat-card *ngIf="success">
<mat-card-content>
<h2>{{'password.reset.success.title' | i18n}}</h2>
<p>{{'password.reset.success.text' | i18n}}</p>
</mat-card-content>
<mat-card-actions>
<a routerLink="/login" mat-raised-button color="primary">
{{'password.reset.login' | i18n}}
</a>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,3 @@
mat-form-field {
display: block;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PasswordResetComponent } from './password-reset.component';
describe('PasswordResetComponent', () => {
let component: PasswordResetComponent;
let fixture: ComponentFixture<PasswordResetComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ PasswordResetComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PasswordResetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,65 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService } from '../../services/auth.service';
import { MatchingValidator } from '../../utils/matching.validator';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-password-reset',
templateUrl: './password-reset.component.html',
styleUrls: ['./password-reset.component.scss']
})
export class PasswordResetComponent implements OnInit {
form: FormGroup;
model: any = {};
public working: boolean;
public success: boolean;
public tokenInvalid: boolean = false;
constructor(
private formBuilder: FormBuilder,
private authService: AuthService,
private router: Router,
private route: ActivatedRoute) { }
ngOnInit(): void {
this.form = this.formBuilder.group({
password: ['', Validators.required],
password2: ['', Validators.required]
}, {
validator: MatchingValidator('password', 'password2')
});
this.route.queryParams.subscribe(params => {
if (params.token) {
this.model.token = params.token;
}
});
}
passwordReset() {
this.working = true;
this.authService.passwordReset(this.model).subscribe(response => {
this.success = true;
}, (error) => {
this.working = false;
if (error.status == 409) {
let errors = {};
for (let code of error.error) {
errors[code.field] = errors[code.field] || {};
errors[code.field][code.code] = true;
}
for (let code in errors) {
this.form.get(code).setErrors(errors[code]);
}
} else {
this.tokenInvalid = true;
}
})
}
}

View File

@ -1,4 +1,4 @@
<form [formGroup]="form" (ngSubmit)="passwordReset()"> <form [formGroup]="form" (ngSubmit)="passwordRequest()">
<mat-card> <mat-card>
<mat-card-content> <mat-card-content>
<h2>{{'password.request' | i18n}}</h2> <h2>{{'password.request' | i18n}}</h2>
@ -10,15 +10,17 @@
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>{{'pgp-key.private' | i18n}}</mat-label> <mat-label>{{'pgp.privateKey' | i18n}}</mat-label>
<textarea matInput formControlName="privateKey" placeholder="Private Key" <textarea matInput formControlName="privateKey" placeholder="Private Key"
[(ngModel)]="model.privateKey"></textarea> [(ngModel)]="model.privateKey"></textarea>
</mat-form-field> </mat-form-field>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<button *ngIf="!working" mat-raised-button color="primary" [disabled]="form.invalid"> <button *ngIf="!working" mat-raised-button color="primary" [disabled]="form.invalid">
{{'password.reset' | i18n}} {{'password.request' | i18n}}
</button> </button>
<mat-progress-bar *ngIf="working" mode="indeterminate"></mat-progress-bar>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
</form> </form>

View File

@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService } from './../../services/auth.service'; import { AuthService } from './../../services/auth.service';
import { MatchingValidator } from './../../utils/matching.validator'; import { Router } from '@angular/router';
var openpgp = require('openpgp'); var openpgp = require('openpgp');
@Component({ @Component({
@ -16,37 +16,39 @@ export class PasswordComponent implements OnInit {
public working: boolean; public working: boolean;
form: FormGroup; form: FormGroup;
constructor(private formBuilder: FormBuilder, private authService: AuthService) { } constructor(private formBuilder: FormBuilder, private authService: AuthService, private router: Router) { }
ngOnInit(): void { ngOnInit(): void {
this.form = this.formBuilder.group({ this.form = this.formBuilder.group({
username: ['', Validators.required], username: ['', Validators.required],
privateKey: ['', Validators.required] privateKey: ['']
}); });
} }
async passwordReset() { async passwordRequest() {
this.working = true;
const { keys: [privateKey] } = await openpgp.key.readArmored(this.model.privateKey); const { keys: [privateKey] } = await openpgp.key.readArmored(this.model.privateKey);
console.log(privateKey.isPrivate());
const model = { const model = {
username: this.model.username username: this.model.username
} }
/* this.authService.passwordRequest(this.model.username).subscribe(async response => {
const message = await openpgp.message.readArmored(encrypted); if (privateKey) {
const message = await openpgp.message.readArmored(response);
const decrypted = await openpgp.decrypt({ const decrypted = await openpgp.decrypt({
message: message, message: message,
privateKeys: [privateKey] privateKeys: [privateKey]
}); });
this.working = false;
this.router.navigate(['/password-reset'], { queryParams: { token: decrypted.data.trim() } });
} else {
this.working = false;
}
})
console.log(decrypted);
// this.authService.passwordReset(model).subscribe(async response => { })
*/
} }
} }

View File

@ -1,9 +1,9 @@
<form [formGroup]="form" (ngSubmit)="register()"> <form [formGroup]="form" (ngSubmit)="register()" *ngIf="!success">
<mat-card> <mat-card>
<mat-card-content> <mat-card-content>
<h2>{{'register' | i18n}}</h2> <h2>{{'register' | i18n}}</h2>
<mat-error *ngIf="missingToken"> <mat-error *ngIf="missingToken">
<a routerLink="/tokens" aria-label="Enter tokens">{{'register.token.missing' | i18n}}</a> <a routerLink="/tokens">{{'register.token.missing' | i18n}}</a>
</mat-error> </mat-error>
<mat-form-field> <mat-form-field>
<input matInput placeholder="{{'username' | i18n}}" formControlName="username" <input matInput placeholder="{{'username' | i18n}}" formControlName="username"
@ -60,3 +60,15 @@
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
</form> </form>
<mat-card *ngIf="success">
<mat-card-content>
<h2>{{'register.success.title' | i18n}}</h2>
<p>{{'register.success.text' | i18n}}</p>
</mat-card-content>
<mat-card-actions>
<a routerLink="/login" mat-raised-button color="primary">
{{'register.login' | i18n}}
</a>
</mat-card-actions>
</mat-card>

View File

@ -22,6 +22,7 @@ export class RegisterComponent implements OnInit {
form: FormGroup; form: FormGroup;
public missingToken: boolean; public missingToken: boolean;
public success: boolean;
public working: boolean; public working: boolean;
items = []; items = [];
currentLocale: String; currentLocale: String;
@ -73,22 +74,38 @@ export class RegisterComponent implements OnInit {
if (this.form.valid && !this.working) { if (this.form.valid && !this.working) {
this.working = true; this.working = true;
let pgpOption = { let pgpOption = {
userIds: [{ name: this.model.username, email: this.model.email }], userIds: [{ name: this.model.username, email: this.model.username + "@we.bstly.de" }],
numBits: 4096, curve: "ed25519",
} }
var pubKey, privKey var pubKey, privKey
openpgp.generateKey(pgpOption).then((key) => { openpgp.generateKey(pgpOption).then((key) => {
privKey = key.privateKeyArmored privKey = key.privateKeyArmored;
pubKey = key.publicKeyArmored pubKey = key.publicKeyArmored;
this.model.publicKey = pubKey; this.model.publicKey = pubKey;
this.userService.register(this.model).subscribe((result: any) => { this.userService.register(this.model).subscribe((result: any) => {
result.privateKey = privKey; result.privateKey = privKey;
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(privKey));
element.setAttribute('download', result.username + ".private.key");
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
const dialogRef = this.dialog.open(RegisterDialog, { const dialogRef = this.dialog.open(RegisterDialog, {
closeOnNavigation: false, closeOnNavigation: false,
disableClose: true, disableClose: true,
data: result data: result
}); });
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.success = true;
}
});
this.working = false; this.working = false;
}, (error) => { }, (error) => {
this.working = false; this.working = false;
@ -106,7 +123,6 @@ export class RegisterComponent implements OnInit {
} }
} }
}) })
}) })
} }
} }
@ -124,9 +140,4 @@ export class RegisterDialog {
public dialogRef: MatDialogRef<RegisterDialog>, public dialogRef: MatDialogRef<RegisterDialog>,
@Inject(MAT_DIALOG_DATA) public data: any) { } @Inject(MAT_DIALOG_DATA) public data: any) { }
onOkClick(): void {
this.dialogRef.close();
this.router.navigate(["/login"]);
}
} }

View File

@ -1,18 +1,20 @@
<h1 mat-dialog-title>{{data.username}}</h1> <h1 mat-dialog-title>{{data.username}}</h1>
<div mat-dialog-content> <div mat-dialog-content>
<h3>Permissions</h3> <h3>{{'permissions' | i18n}}</h3>
<app-permissions [permissions]="data.permissions"></app-permissions> <app-permissions [permissions]="data.permissions"></app-permissions>
<h3>Quotas</h3> <h3>{{'quotas' | i18n}}</h3>
<app-quotas [quotas]="data.quotas"></app-quotas> <app-quotas [quotas]="data.quotas"></app-quotas>
<h3>{{'pgp.privateKey' | i18n}}</h3>
<mat-form-field> <mat-form-field>
<mat-label>Private PGP key</mat-label> <qrcode [qrdata]="data.privateKey" [width]="400" [errorCorrectionLevel]="'M'"></qrcode>
<mat-label>{{'pgp.privateKey' | i18n}}</mat-label>
<textarea matInput readonly [(ngModel)]="data.privateKey"></textarea> <textarea matInput readonly [(ngModel)]="data.privateKey"></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
<div mat-dialog-actions> <div mat-dialog-actions>
<mat-slide-toggle [(ngModel)]="data.confirmClose"> <mat-slide-toggle [(ngModel)]="data.confirmClose">
I have saved my private key securely! {{'pgp.privateKey.confirmStore' | i18n}}
</mat-slide-toggle> </mat-slide-toggle>
<button mat-button (click)="onOkClick()" [disabled]="!data.confirmClose">Ok</button> <button mat-button [disabled]="!data.confirmClose" [mat-dialog-close]="true">{{'ok' | i18n}}</button>
</div> </div>

View File

@ -49,8 +49,6 @@ export class UsernameDialog {
}; };
this.username = uniqueNamesGenerator(config); this.username = uniqueNamesGenerator(config);
console.log(this.username);
} }
toggle(dict) { toggle(dict) {

View File

@ -46,7 +46,8 @@
<a *ngIf="!auth.authenticated" routerLink="/register" mat-raised-button color="accent"> <a *ngIf="!auth.authenticated" routerLink="/register" mat-raised-button color="accent">
<mat-icon>how_to_reg</mat-icon> {{'register' | i18n}} <mat-icon>how_to_reg</mat-icon> {{'register' | i18n}}
</a> </a>
<a *ngIf="!auth.authenticated" routerLink="/login" mat-raised-button color="primary"> <a *ngIf="!auth.authenticated" routerLink="/login" [queryParams]="{ target:'tokens' }" mat-raised-button
color="primary">
<mat-icon>login</mat-icon> {{'login' | i18n}} <mat-icon>login</mat-icon> {{'login' | i18n}}
</a> </a>
</mat-card-actions> </mat-card-actions>

View File

@ -97,7 +97,7 @@ export class TokensComponent implements OnInit {
redeem() { redeem() {
if (this.auth.authenticated) { if (this.auth.authenticated) {
this.itemService.redeem().subscribe((data: any) => { this.itemService.redeem().subscribe((data: any) => {
this.router.navigate(["/account/info"]);
}) })
} }
} }

View File

@ -27,22 +27,41 @@ export class AuthService {
return this.http.get(environment.apiUrl + "/auth/me"); return this.http.get(environment.apiUrl + "/auth/me");
} }
login(username, password) { login(loginModel) {
return this.http.post(environment.apiUrl + "/auth/login", { username: username, password: password }); return this.http.post(environment.apiUrl + "/auth/login", loginModel);
} }
logout() { logout() {
return this.http.post(environment.apiUrl + "/auth/logout", {}); return this.http.post(environment.apiUrl + "/auth/logout", {});
} }
passwordRequest() { passwordRequest(username) {
return this.http.post(environment.apiUrl + "/auth/password/request", {}); const headers = new HttpHeaders().set('Content-Type', 'text/plain; charset=utf-8');
return this.http.post(environment.apiUrl + "/auth/password/request", username, { headers, responseType: 'text' });
} }
passwordReset(model) { passwordReset(model) {
const headers = new HttpHeaders().set('Content-Type', 'text/plain; charset=utf-8'); const headers = new HttpHeaders().set('Content-Type', 'text/plain; charset=utf-8');
return this.http.post(environment.apiUrl + "/auth/password/reset", model, return this.http.post(environment.apiUrl + "/auth/password/reset", model);
{ headers, responseType: 'text' });
} }
isTotpEnabled() {
return this.http.get(environment.apiUrl + "/auth/totp");
}
createTotp() {
return this.http.put(environment.apiUrl + "/auth/totp", {});
}
enableTotp(code) {
return this.http.patch(environment.apiUrl + "/auth/totp", code);
}
removeTotp() {
return this.http.delete(environment.apiUrl + "/auth/totp");
}
loginTotp(totpModel) {
return this.http.post(environment.apiUrl + "/auth/login/totp", totpModel);
}
} }

View File

@ -8,6 +8,7 @@ import { isEmpty } from 'rxjs/operators';
export class I18nService { export class I18nService {
locale: String; locale: String;
locales = ["de-informal"];
i18n: any; i18n: any;
constructor(private http: HttpClient) { constructor(private http: HttpClient) {
@ -17,15 +18,24 @@ export class I18nService {
browserLocale = browserLocale.split("-")[0]; browserLocale = browserLocale.split("-")[0];
} }
let locale = localStorage.getItem("bstly.locale") || browserLocale || 'en'; let locale = localStorage.getItem("bstly.locale") || browserLocale || this.locales[0];
if (locale == 'de') { if (locale == 'de') {
locale = 'de-informal'; locale = 'de-informal';
} }
if (this.locales.indexOf(locale) == -1) {
locale = this.locales[0];
}
this.setLocale(locale); this.setLocale(locale);
} }
getLocales() {
return this.locales;
}
getLocale() { getLocale() {
return this.locale; return this.locale;
} }

View File

@ -15,6 +15,10 @@ export class UserService {
return this.http.post(environment.apiUrl + "/users", userModel); return this.http.post(environment.apiUrl + "/users", userModel);
} }
checkModel(userModel) {
return this.http.post(environment.apiUrl + "/users/model", userModel);
}
password(passwordModel) { password(passwordModel) {
return this.http.patch(environment.apiUrl + "/users/password", passwordModel); return this.http.patch(environment.apiUrl + "/users/password", passwordModel);
} }

View File

@ -1,11 +1,13 @@
<table mat-table [dataSource]="permissions"> <table mat-table matSort [dataSource]="permissions" (matSortChange)="sortData($event)">
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{'permissions.name' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'permissions.name' | i18n}} </th>
<td mat-cell *matCellDef="let permission"> {{permission.name}} <mat-icon *ngIf="permission.addon" aria-hidden="false" aria-label="Add-on">add_circle</mat-icon></td> <td mat-cell *matCellDef="let permission"> {{'permissions.' + permission.name | i18n}}
<mat-icon *ngIf="permission.addon">add_circle</mat-icon>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="expires"> <ng-container matColumnDef="expires">
<th mat-header-cell *matHeaderCellDef> {{'permissions.expires' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="expires"> {{'permissions.expires' | i18n}} </th>
<td mat-cell *matCellDef="let permission">{{permission.expires | date}}</td> <td mat-cell *matCellDef="let permission">{{permission.expires | date}}</td>
</ng-container> </ng-container>

View File

@ -1,4 +1,6 @@
import { Component, OnInit, Input } from '@angular/core'; import { Component, OnInit, Input } from '@angular/core';
import { Sort } from '@angular/material/sort';
import { I18nService } from './../../services/i18n.service';
@Component({ @Component({
selector: 'app-permissions', selector: 'app-permissions',
@ -7,12 +9,33 @@ import { Component, OnInit, Input } from '@angular/core';
}) })
export class PermissionsComponent implements OnInit { export class PermissionsComponent implements OnInit {
@Input() permissions; @Input() permissions;
permissionColumns = ["name", "expires"]; permissionColumns = ["name", "expires"];
constructor() { } constructor(private i18n: I18nService) { }
ngOnInit(): void { ngOnInit(): void {
} }
sortData(sort: Sort) {
const data = this.permissions.slice();
if (!sort.active || sort.direction === '') {
this.permissions = data;
return;
}
this.permissions = data.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'name': return this.compare(this.i18n.get('permissions.' + a.name,[]), this.i18n.get('permissions.' + b.name,[]), isAsc);
case 'expires': return this.compare(a.expires, b.expires, isAsc);
default: return 0;
}
});
}
compare(a: number | string | String, b: number | string | String, isAsc: boolean) {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
} }

View File

@ -1,13 +1,21 @@
<table mat-table [dataSource]="quotas"> <table mat-table matSort [dataSource]="quotas" (matSortChange)="sortData($event)">
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{'quotas.name' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'quotas.name' | i18n}} </th>
<td mat-cell *matCellDef="let quota"> {{quota.name}} </td> <td mat-cell *matCellDef="let quota"> {{'quotas.' + quota.name | i18n}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="quota"> <ng-container matColumnDef="quota">
<th mat-header-cell *matHeaderCellDef> {{'quotas.value' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="value"> {{'quotas.value' | i18n}} </th>
<td mat-cell *matCellDef="let quota">{{quota.value}} {{quota.unit}}</td> <td mat-cell *matCellDef="let quota"> {{quota.value}} </td>
</ng-container>
<ng-container matColumnDef="quotaUnit">
<th mat-header-cell *matHeaderCellDef> {{'quotas.unit' | i18n}} </th>
<td mat-cell *matCellDef="let quota">
<span *ngIf="quota.unit">{{'quotas.unit.' + quota.unit | i18n}}</span>
<span *ngIf="!quota.unit">#</span>
</td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="quotaColumns"></tr> <tr mat-header-row *matHeaderRowDef="quotaColumns"></tr>

View File

@ -1,4 +1,6 @@
import { Component, OnInit, Input } from '@angular/core'; import { Component, OnInit, Input } from '@angular/core';
import { Sort } from '@angular/material/sort';
import { I18nService } from './../../services/i18n.service';
@Component({ @Component({
selector: 'app-quotas', selector: 'app-quotas',
@ -7,11 +9,34 @@ import { Component, OnInit, Input } from '@angular/core';
}) })
export class QuotasComponent implements OnInit { export class QuotasComponent implements OnInit {
@Input() quotas; @Input() quotas;
quotaColumns = ["name", "quota"]; quotaColumns = ["name", "quota", "quotaUnit"];
constructor() { }
constructor(private i18n: I18nService) { }
ngOnInit(): void { ngOnInit(): void {
} }
sortData(sort: Sort) {
const data = this.quotas.slice();
if (!sort.active || sort.direction === '') {
this.quotas = data;
return;
}
this.quotas = data.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'name': return this.compare(this.i18n.get('quotas.' + a.name, []), this.i18n.get('quotas.' + b.name,[]), isAsc);
case 'value': return this.compare(a.value, b.value, isAsc);
default: return 0;
}
});
}
compare(a: number | string | String, b: number | string | String, isAsc: boolean) {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
} }

View File

@ -22,7 +22,7 @@ export class HtmlComponent implements OnInit {
const headers = new HttpHeaders() const headers = new HttpHeaders()
.set('content-type', 'text/html'); .set('content-type', 'text/html');
this.httpClient.get( this.httpClient.get(
'./assets/templates/' + this.template + (this.locale ? "." + this.locale : "") + ".html", './assets/templates/' + (this.locale ? this.locale + "/" : "") + this.template + ".html",
{ {
headers: headers, headers: headers,
responseType: 'text' responseType: 'text'

View File

@ -1,11 +1,47 @@
{ {
"i18n.test.replace": "Wat!?! {0} {1} {2}", "i18n.test.replace": "Wat!?! {0} {1} {2}",
"greet": "Hallo {0}", "greet": "Hallo {0}",
"home": "Start", "home": {
".": "Über we.bstly",
"general": {
".": "Über we.bstly",
"what": "Was wir machen",
"you": "Was du machen kannst",
"we": "Was unser Ziel ist"
},
"privacy": {
".": "Datenschutz",
"design": "Privacy By Design",
"pretix": "Shop System (Pretix)",
"we-bstly": "we.bstly",
"services": "Aktuelle Services"
},
"services": {
".": "Services",
"active": "Aktive Services",
"planned": "Geplante Services",
"email": "E-Mail Postfach",
"legend": {
".": "Legende",
"ready": "✅ fertig, benötigt nur Finanzierung",
"not-ready": "❔ noch nicht fertig",
"not-available": "⚠️ noch nicht konkret/technische Hürden"
}
},
"club": {
".": "Verein",
"membership": "Mitgliedschaft",
"charter": "Satzung (Entwurf)",
"about": "Über den Verein"
}
},
"services": {},
"cancel": "Abbrechen",
"login": { "login": {
".": "Login", ".": "Login",
"external": "Login", "external": "Login",
"invalid": "Falscher Username oder Passwort." "invalid": "Falscher Username oder Passwort.",
"keepSession": "Eingelogged bleiben"
}, },
"not-found": { "not-found": {
".": "Nicht gefunden", ".": "Nicht gefunden",
@ -23,7 +59,7 @@
".": "Tokens", ".": "Tokens",
"redeem": "Tokens einlösen", "redeem": "Tokens einlösen",
"redeemed": "Das Token wurde bereits eingelöst.", "redeemed": "Das Token wurde bereits eingelöst.",
"get": "Tokens holen", "get": "Mitmachen",
"enter": "Token eingeben", "enter": "Token eingeben",
"validate": "Prüfen", "validate": "Prüfen",
"invalid": "Das Token ist leider nicht gültig.", "invalid": "Das Token ist leider nicht gültig.",
@ -38,7 +74,14 @@
".": "Passwort", ".": "Passwort",
"forgot": "Passwort vergessen", "forgot": "Passwort vergessen",
"request": "Neues Passwort anfordern", "request": "Neues Passwort anfordern",
"reset": "Passwort setzen", "reset": {
".": "Passwort setzen",
"login": "Zum Login",
"success": {
"title": "Passwort erfolgreich geändert",
"text": "Dein neues Passwort wurde übernommen. Du kannst dich nun mit deinem neuen Passwort einloggen."
}
},
"change": "Passwort ändern", "change": "Passwort ändern",
"changed": "Passwort erfolgreich geändert", "changed": "Passwort erfolgreich geändert",
"current": "Akutelles Passwort", "current": "Akutelles Passwort",
@ -58,25 +101,42 @@
}, },
"register": { "register": {
".": "Registrierung", ".": "Registrierung",
"token.missing": "Du benötigst leider ein gültiges Token!" "token.missing": "Du benötigst leider ein gültiges Token!",
"login": "Zum Login",
"success": {
"title": "Registrierung abgeschlossen",
"text": "Deine Registrierung war erfolgreich. Du kannst dich nun einloggen!"
}
}, },
"email": { "email": {
".": "E-Mail Adresse", ".": "E-Mail Adresse",
"primary": "primäre E-Mail Adresse", "primary": "primäre E-Mail Adresse",
"invalid": "ungültige E-Mail Adresse" "invalid": "ungültige E-Mail Adresse"
}, },
"apps": "Apps", "apps": {
"app": { ".": "Apps",
"goto": "Gehe zur App" "goto": "Gehe zur App",
"nextcloud": {
"icon": "cloud",
"title": "Nextcloud",
"subtitle": "wolkige bstly",
"text": "Nextcloud bietet dir Dateiverwaltung, Kalendar, Aufgabenmanagement, Kontaktmanagement, Kommunikationskanäle und Abstimmungen."
},
"mail": {
"icon": "email",
"title": "E-Mail Postfach",
"subtitle": "E-Mail wie es sein sollte",
"text": "Catch-All an @{username}.we.bstly.de, lernender Spam-Filter und PGP Verschlüsselung."
}
}, },
"locale": { "locale": {
"en": { "en": {
"short": "EN", "short": "EN",
"long": "Englisch" "long": "English"
}, },
"de-informal": { "de-informal": {
"short": "DE", "short": "DE",
"long": "German" "long": "Deutsch"
} }
}, },
"info": { "info": {
@ -85,12 +145,23 @@
"permissions": { "permissions": {
".": "Berechtigungen", ".": "Berechtigungen",
"name": "Name", "name": "Name",
"expires": "Gültig bis" "expires": "Gültig bis",
"nextcloud": "Nextcloud",
"mail": "E-Mail Postfach",
"ROLE_MEMBER": "Vereinsmitgliedschaft"
}, },
"quotas": { "quotas": {
".": "Quotas", ".": "Quotas",
"name": "Name", "name": "Name",
"value": "Quota" "value": "Quota",
"unit": {
".": "Einheit",
"G": "GB (Gigabyte)",
"#": "# (Anzahl)"
},
"nextcloud": "Nextcloud",
"mail": "E-Mail Postfach",
"registration_vouchers": "Registrierungs-Gutscheincodes"
}, },
"voucher": { "voucher": {
".": "Gutscheincode", ".": "Gutscheincode",
@ -99,7 +170,7 @@
}, },
"vouchers": { "vouchers": {
".": "Gutscheincodes", ".": "Gutscheincodes",
"info": "Gutscheincodes für Add-Ons und Registrierung", "info": "Hier kannst du Gutscheincodes für Add-Ons und Registrierung generieren.",
"registration": "Registrierung", "registration": "Registrierung",
"add-on": "Add-On", "add-on": "Add-On",
"temp": { "temp": {
@ -112,6 +183,36 @@
} }
}, },
"security": { "security": {
".": "Sicherheit" ".": "Sicherheit",
"2fa": {
".": "Zwei-Faktor-Authentifierung (2FA)",
"info": "Du kannst hier einen zweiten Faktor zusätzlich zu deinem Passwort hinzufügen. Beachte, dass dies nur den Login in deinen we.bstly-Account betrifft. 2FA gilt nicht für deinen E-Mail Account. Aktuell wird nur TOTP (bekannt als Google Authenticator) unterstützt.",
"totp": {
".": "2FA (TOTP)",
"hint": "Um TOTP als zweiten Faktor beim Login zu verwenden, scanne den QRCode mit deiner TOTP App.",
"enable": "Aktiviere 2FA (TOTP)",
"code": "TOTP Code",
"login": "Code verfizieren",
"create": "2FA (TOTP) einrichten",
"remove": "2FA (TOTP) deaktivieren",
"external": "2FA (TOTP)",
"invalid": "TOTP Code ist ungültig",
"missing": "Bitte TOTP Code eingeben",
"activate": "Um TOTP als 2FA zu aktivieren, gebe bitte deinen aktuellen Code ein.",
"keepSession": "2FA (TOTP) für dieses Gerät merken"
} }
} }
},
"pgp": {
".": "PGP",
"privateKey": {
".": "Privater PGP Schlüssel",
"confirmStore": "Ich habe meinen privaten Schlüssel sicher gespeichert!"
}
},
"software": "Software",
"sourcecode": "Quellcode",
"homepage": "Homepage",
"imprint": "Impressum",
"privacy-policy": "Datenschutzerklärung"
}

View File

@ -2,11 +2,24 @@
"i18n.test.replace": "yes no it's clear! {0} {1} {2}", "i18n.test.replace": "yes no it's clear! {0} {1} {2}",
"greet": "Hello {0}", "greet": "Hello {0}",
"home": "Home", "home": "Home",
"cancel" : "Cancel",
"login": { "login": {
".": "Login", ".": "Login",
"external": "Login", "external": "Login",
"invalid": "The username and password were not recognised." "invalid": "The username and password were not recognised."
}, },
"totp": {
".": "2FA (TOTP)",
"hint": "For using TOTP (known as Google Authenticator) as second factor, please scan the QRCode with your TOTP App.",
"enable": "Enable 2FA (TOTP)",
"code": "TOTP code",
"login" : "Login",
"create": "Setup 2FA (TOTP)",
"remove": "Disable 2FA (TOTP)",
"external": "2FA (TOTP)",
"invalid": "TOTP code is invalid",
"missing": "TOTP code is missing"
},
"not-found": { "not-found": {
".": "Not Found", ".": "Not Found",
"text": "What's Up!?" "text": "What's Up!?"
@ -113,5 +126,8 @@
}, },
"security": { "security": {
".": "Security" ".": "Security"
} },
"software" : "Software",
"sourcecode": "Sourcecode",
"homepage": "Homepage"
} }

View File

@ -1,3 +0,0 @@
<h2>we.bstly</h2>
<p>Willkommen zur digitalen Bastelei.</p>
<p></p>

View File

@ -1,3 +0,0 @@
<h2>we.bstly</h2>
<p>Welcome to the digital tinkering.</p>

View File

@ -0,0 +1,2 @@
<h2>we.bstly</h2>
<p>Willkommen zu 'Bastelei (bald e.V.)'.</p>

View File

@ -0,0 +1 @@
<p>Der eingetragene Verein bietet sich für unsere Zwecke als Rechtsform an, da wir nicht wirtschaftlich orientiert arbeiten wollen, sondern ideelle Ziele haben. Wir haben uns allerdings bewusst gegen eine Gemeinnützigkeit im Sinne des Vereinsgesetzes entschieden. Dies hat rechtliche Gründe, da wir uns selbst sehr wohl als Gemeinnützig betrachten würden. Die Erfahrung mit der Arbeit in gemeinnützigen Vereinen sowie die aktuellen Rechtsprechungen, die nicht mit unserem Verständnis von Gemeinnützigkeit übereinstimmen, haben uns jedoch zu dem Schluss kommen lassen, dass wir von Anfang an auf den juristischen Anspruch der Gemeinnützigkeit verzichten werden. Der offensichtliche Nachteil besteht vor allem im Verzicht auf Steuerbegünstigungen, dies gilt auch z.B. für die Steuererstattung von Mitgliedsbeiträgen, die hier entfällt. Auf der anderen Seite können wir jetzt einfacher Rücklagen bilden, Mitglieder für Arbeit entlohnen und freier Entscheidungen treffen, so dass wir unsere Ziele einfacher und schneller erreichen können.</p>

View File

@ -0,0 +1,202 @@
<h1>Satzung</h1>
<p>des “Bastelei e.V.”</p>
<h3>Präambel</h3>
<p>Die ökologischen, ökonomischen und kulturellen Probleme unserer Zeit stellen uns vor die Aufgabe, unsere gesamte
Kultur und Technologie auf nachhaltigere, ethische und zivilisiertere Grundlagen zu stellen. Diese Probleme durch
globale Institutionen anzugehen zeitigt dabei nur bedingt Erfolge, Dezentralisierung ist Teil des Kulturbegriffs,
eine zentrale Kultur gibt es nicht.</p>
<p>Die Zukunft ist keine Errungenschaft einer Elite oder eine technische Ingenieursleistung sondern eine Reihe von
Möglichkeiten die jeder einzelne Mensch durch sein Handeln gestalten kann indem er seine Kreativität und Solidarität
entwickelt und die friedliche Gemeinschaft mit anderen Lebewesen unabhängig von Alter, Geschlecht und Abstammung
sowie gesellschaftlicher Stellung pflegt.</p>
<p>Die Kommende Bastelei möchte diesen Bestrebungen zur Gestaltung einer lebenswerten und kulturreichen Zukunft in ihrem
Rahmen und mit den Mitteln der Bastelei eine organisatorische Grundlage bieten. Der Verein stellt seinen Mitgliedern
deswegen eine organisatorische und institutionelle Infrastruktur zur Verfügung um ihre Projekte und Ideen
selbstständig verfolgen zu können.</p>
<h2>§1 Name, Sitz, Geschäftsjahr</h2>
<p>Der Verein führt den Namen "Bastelei". Der Verein wird in das Vereinsregister eingetragen und dann um den Zusatz „e.
V.“ ergänzt. </p>
<p>Der Verein hat seinen Sitz in Gevelsberg.</p>
<p>Das Geschäftsjahr ist das Kalenderjahr.</p>
<h2>§2 Zweck</h2>
<p>Zweck des Vereins ist die gemeinsame Arbeit an der Organisation, Entwicklung, Bereitstellung und dem Betrieb offener,
kollaborativ genutzter technischer Infrastruktur sowie der Ausrichtung von Veranstaltungen zur Förderung des
nachhaltigen Umgangs mit Technologien und der informationellen Selbstbestimmung aus ideellem Interesse. Hierbei
werden alle rassistischen, faschistischen und sexistischen Strömungen ausgeschlossen.</p>
<h2>§3 Mitgliedschaft</h2>
<p>Ordentliche Vereinsmitglieder können ausschließlich natürliche Personen werden.</p>
<p>Die Beitrittserklärung erfolgt in Textform gegenüber dem Vorstand. Über die Annahme der Beitrittserklärung
entscheidet der Vorstand. Die Mitgliedschaft beginnt mit der vorläufigen Annahme der Beitrittserklärung und der
Zahlung des ersten Beitrages im Voraus. Die Vorläufigkeit endet mit der Bestätigung des Mitglieds durch den
Vorstand.</p>
<p>Die Mitgliedschaft endet durch Austrittserklärung, durch Tod von natürlichen Personen oder durch Auflösung und
Erlöschen von juristischen Personen, Handelsgesellschaften, nicht rechtsfähigen Vereinen sowie Anstalten und
Körperschaften des öffentlichen Rechts oder durch Ausschluss; die Beitragspflicht für das laufende Beitragsjahr
bleibt hiervon unberührt.</p>
<p>Der Austritt wird durch Willenserklärung in Textform gegenüber dem Vorstand vollzogen.</p>
<p>Die Mitgliederversammlung kann solche Personen, die sich besondere Verdienste um den Verein oder um die von ihm
verfolgten satzungsgemäßen Zwecke erworben haben, zu Ehrenmitgliedern ernennen. Ehrenmitglieder haben alle Rechte
eines ordentlichen Mitglieds. Sie sind von Beitragsleistungen befreit.</p>
<p>Fördermitglieder sind passive Mitglieder ohne Stimmrecht in der Mitgliederversammlung. Fördermitglieder können
ausschließlich natürliche Personen werden. Bei Minderjährigen ist die Zustimmung des gesetzlichen Vertreters
erforderlich.</p>
<h2>§4 Rechte und Pflichten der Mitglieder</h2>
<p>Die Mitglieder sind berechtigt, die Leistungen des Vereins in Anspruch zu nehmen.</p>
<p>Die Mitglieder sind verpflichtet, die satzungsgemäßen Zwecke des Vereins zu unterstützen und zu fördern. Sie sind
verpflichtet, die festgesetzten Beiträge zu zahlen.</p>
<h2>§5 Ausschluss eines Mitglieds</h2>
<p>Ein Mitglied kann durch Beschluss des Vorstandes ausgeschlossen werden, wenn es das Ansehen des Vereins schädigt,
seinen Beitragsverpflichtungen nicht nachkommt oder wenn ein sonstiger wichtiger Grund vorliegt. Der Vorstand muss
dem auszuschließenden Mitglied den Beschluss in Textform unter Angabe von Gründen, an die letzte bekannte Anschrift
oder an die zuletzt bekannte E-Mail-Adresse, mitteilen und ihm auf Verlangen eine Anhörung gewähren.</p>
<p>Gegen den Beschluss des Vorstandes kann das auszuschließende Mitglied die Mitgliederversammlung anrufen. Bis zum
Beschluss der Mitgliederversammlung ruht die Mitgliedschaft. Die Anrufung muss innerhalb einer Frist von vier Wochen
ab Zugang des Ausschließungsbeschlusses in Textform beim Vorstand eingelegt werden. Erfolgt keine Anrufung oder
verstreicht die Frist, gilt die Mitgliedschaft ab dem Zeitpunkt des Ausschlusses als beendet.</p>
<h2>§6 Beitrag</h2>
<p>Der Verein erhebt Mitgliedsbeiträge. Das Nähere regelt eine Beitragsordnung, die von der Mitgliederversammlung
beschlossen wird. Im Falle nicht fristgerechter Entrichtung der Beiträge ruht die Mitgliedschaft.</p>
<p>Im begründeten Einzelfall kann für ein Mitglied durch Vorstandsbeschluss ein von der Beitragsordnung abweichender
Beitrag festgesetzt werden.
<h2>§7 Organe des Vereins</h2>
<p>Die Organe des Vereins sind:
<ul>
<li>die Mitgliederversammlung</li>
<li>der Vorstand</li>
</ul>
</p>
<h2>§8 Mitgliederversammlung</h2>
<p>Oberstes Beschlussorgan ist die Mitgliederversammlung. Ihrer Beschlussfassung unterliegen:
<ul>
<li>die Genehmigung des Finanzberichtes</li>
<li>die Entlastung des Vorstandes</li>
<li>die Wahl der einzelnen Vorstandsmitglieder</li>
<li>die Bestellung von FinanzprüferInnen</li>
<li>die Satzungsänderungen</li>
<li>die Genehmigung der Beitragsordnung</li>
<li>die Richtlinie über die Erstattung von Reisekosten und Auslagen</li>
<li>die Anträge des Vorstandes und der Mitglieder</li>
<li>die Ernennung von Ehrenmitgliedern</li>
<li>die Auflösung des Vereins</li>
</ul>
</p>
<p>Die ordentliche Mitgliederversammlung findet jedes Jahr beim Kongress der Kommenden Bastelei statt. </p>
<p>Mitgliederversammlungen können digital oder auch als Hybridveranstaltungen abgehalten werden. Technische
Hürden können durch Bildung von Kleingruppen gelöst werden.</p>
<p>Außerordentliche Mitgliederversammlungen werden auf Beschluss des Vorstandes abgehalten, wenn die Interessen des
Vereins dies erfordern, oder wenn mindestens fünf Prozent, bei weniger als 60 Mitgliedern mindestens drei
Mitglieder, aller stimmberechtigten Mitglieder dies unter Angabe des Zwecks in Textform beantragen. Die Einberufung
der Mitgliederversammlung erfolgt in Textform durch den Vorstand mit einer Frist von mindestens zwei Wochen. Zur
Wahrung der Frist reicht die Versendung an die zuletzt bekannte E-Mail-Adresse oder die Aufgabe der Einladung zur
Post an die letzte bekannte Anschrift. Hierbei sind die Tagesordnung bekannt zugeben und ihr die nötigen
Informationen zugänglich zu machen. Anträge zur Tagesordnung sind mindestens sieben Tage vor der
Mitgliederversammlung beim Vorstand in Textform einzureichen. Über die Behandlung von Initiativanträgen entscheidet
die Mitgliederversammlung.
</p>
<p>Die Mitgliederversammlung ist beschlussfähig, wenn mindestens fünf Prozent, bei weniger als 60 Mitgliedern mindestens
drei Mitglieder, aller stimmberechtigten Mitglieder anwesend sind. Beschlüsse sind jedoch gültig, wenn die
Beschlussfähigkeit vor der
Beschlussfassung nicht angezweifelt worden ist. Ist die Mitgliederversammlung aufgrund mangelnder Teilnehmerzahl
nicht
beschlussfähig, ist die darauf folgende ordentlich einberufene Mitgliederversammlung ungeachtet der Teilnehmerzahl
beschlussfähig.</p>
<p>Beschlüsse über Satzungsänderungen und über die Auflösung des Vereins bedürfen zu ihrer Rechtswirksamkeit
der Dreiviertelmehrheit der anwesenden stimmberechtigten Mitglieder. In allen anderen Fällen genügt die
einfache Mehrheit.</p>
<p>Jedes stimmberechtigte Mitglied, welches mit den Beiträgen nicht im Rückstand ist, hat eine Stimme.
Stimmen können übertragen werden.</p>
<p>Über die Beschlüsse der Mitgliederversammlung ist ein Protokoll anzufertigen, das von der
VersammlungsleiterIn und der ProtokollführerIn zu unterzeichnen ist. Das Protokoll ist allen Mitgliedern
zugänglich zu machen und auf der nächsten Mitgliederversammlung genehmigen zu lassen.</p>
<p>Die Mitgliederversammlung wählt den Vorstand und die FinanzprüferInnen. Die Wahlen finden offen in Form
der „Wahl durch Zustimmung” statt.</p>
<p>Entsprechend sichere, elektronische Wahlformen sind zulässig, dadurch können jedoch keine geheimen Wahlen
durchgeführt werden. Abwesende Mitglieder können so jedoch auch an Wahlen teilnehmen. Technische Hürden
können durch Bevollmächtigungen gelöst werden.</p>
<p>Jede WählerIn kann beliebig vielen KandidatInnen jeweils eine Stimme geben. Jeder zu besetzende Posten
wird einzeln gewählt, wobei gleichrangige Posten (die zwei FinanzprüferInnen) jeweils gemeinsam gewählt
werden können. Bei der Wahl des Vorstandes ist gewählt, wer die meisten abgegebenen Stimmen erhält. Bei
Stimmengleichheit findet eine Stichwahl statt. Bei erneuter Stimmengleichheit entscheidet das Los. Bei
der Wahl der FinanzprüferInnen sind diejenigen beiden KandidatInnen gewählt, die die meisten Stimmen erhalten.
Bei Stimmengleichheit findet eine Stichwahl statt. Bei erneuter Stimmengleichheit entscheidet das Los.
</p>
<h2>§9 Vorstand</h2>
<p>Der Vorstand besteht aus zwei oder mehr gleichberechtigten Mitgliedern.</p>
<p>Vorstand im Sinne des § 26, Abs. 2 BGB ist jedes Vorstandsmitglied. Ausgenommen sind Einstellung und
Entlassung von Angestellten, gerichtliche Vertretung sowie Anzeigen, Aufnahme von Krediten, Gründung,
Erwerb und Veräußerung von Gesellschaften und Geschäftsanteilen von Gesellschaften zur Verwirklichung
der satzungsgemäßen Ziele; bei denen der Verein durch mindestens zwei Vorstandsmitglieder vertreten
wird.</p>
<p>Scheidet ein Vorstandsmitglied vorzeitig aus, kann der Vorstand ein neues Vorstandsmitglied aus dem Kreis der
Mitglieder bis zur nächsten Vollversammlung berufen.</p>
<p>Die Amtsdauer der Vorstandsmitglieder beträgt zwei Jahre. Wiederwahl ist zulässig. Damit auch nach Ablauf
der Amtsdauer eine ordnungsgemäße gesetzliche Vertretung gesichert ist, bleibt der Vorstand bis zur
Neuwahl im Amt.</p>
<p>Der Vorstand ist Dienstvorgesetzter aller vom Verein angestellten MitarbeiterInnen.</p>
<p>Die Vorstandsmitglieder nehmen eine interne Aufgabenverteilung vor. Mit dem Ablauf des Geschäftsjahres
stellt der Vorstand unverzüglich die Abrechnung sowie die Vermögensübersicht und sonstige Unterlagen von
wirtschaftlichen Belang den FinanzprüferInnen des Vereins zur Prüfung zur Verfügung.</p>
<p>Der Vorstand führt die laufenden Geschäfte des Vereins. Bei der Geschäftsführung sind die
Vorstandsmitglieder an die Beschlüsse der Mitgliederversammlung gebunden. Der Vorstand soll seine
gesamte Tätigkeit so durchschaubar wie möglich erledigen und andere Vereinsmitglieder kooperativ
beteiligen. Der Vorstand kann haupt- oder ehrenamtlich Tätige mit der Führung der Geschäfte beauftragen.</p>
<p>Der Vorstand verwaltet das Vereinsvermögen.</p>
<p>Der Verein wird gerichtlich und außergerichtlich durch zwei Vorstandsmitglieder gemeinsam vertreten.</p>
<h2>§10 FinanzprüferInnen</h2>
<p>Zur Kontrolle der Haushaltsführung bestellt die Mitgliederversammlung zwei FinanzprüferInnen. Nach
Durchführung ihrer Prüfung informieren sie den Vorstand von ihrem Prüfungsergebnis und erstatten der
Mitgliederversammlung Bericht.</p>
<p>Die FinanzprüferInnen dürfen dem Vorstand nicht angehören.</p>
<p>Die FinanzprüferInnen sind grundsätzlich ehrenamtlich tätig; sie haben Anspruch auf Erstattung
notwendiger Auslagen im Rahmen einer von der Mitgliederversammlung zu beschließenden Richtlinie über die
Erstattung von Reisekosten und Auslagen.</p>
<h2>§11 Auflösung des Vereins</h2>
<p>Bei der Auflösung des Vereins oder bei Wegfall seines Zweckes fällt das Vereinsvermögen an eine von der
Mitgliederversammlung zu bestimmende Körperschaft des öffentlichen Rechts oder eine andere
steuerbegünstigte Körperschaft zwecks Verwendung für die bürgerschaftliche Bildung.</p>

View File

@ -0,0 +1,7 @@
<p>Die Vereinsmitgliedschaft richtet sich an alle, die den Verein aktiv unterstützen und mitgestalten möchten. Als Vereinsmitglied kannst du durch Teilnahme an Mitgliederversammlungen und Abstimmungen mitbestimmen.</p>
<p>Vor allem mit Blick auf zukünftige Projekte auch außerhalb des digitalen Raumes ist deine aktive Beteiligung nötig, damit wir größere Vielfältigkeit und Kreativität in unsere Projekte bekommen. Und da wir große Freude an demokratischen Prozessen haben, ist jedes Mitglied auch eine Bereicherung um Vereinsprozesse offener gestalten zu können. Bei der Bastelei soll es um möglichst kreative, zwanglose und gute Zusammenarbeit zur Bereicherung aller Beteiligten gehen.</p>
<p>Wenn du also nicht nur ein Interesse an unseren digitalen Services hast, sondern Ideen und Ziele mitverfolgen möchtest, kannst du dir deine Vereinsmitgliedschaft ganz einfach unter <a href="https://we.bstly.de/" target="_blank">we.bstly.de</a> klicken. Du musst nur noch auf Genehmigung deines Antrages warten und anschließend deinen ersten Mitgliedsbeitrag überweisen. Anschließend bekommst du jährlich automatisch eine Erinnerung an deine hinterlegte E-Mail Adresse, deine Mitgliedschaft zu verlängern.</p>
<p class="hint">Falls du nur an unseren digitalen Angeboten interessiert bist, bieten wir diese auch Nicht-Vereinsmitgliedern an. Du solltest allerdings grundsätzlich mit den Zielen und Idealen des Vereins übereinstimmen. Wir nutzen die Beiträge aus deiner Nutzung, um unsere Vereinsarbeit weiter zu finanzieren.</p>

View File

@ -0,0 +1,4 @@
<p>Jeden Monat überweisen wir zwielichtigen Konzernen zweistellige Beträge, damit sie uns unsere Daten abnehmen. Dafür
bekommen wir intransparente Software mit denen wir auf Geräten, die wir nicht kontrollieren irgendwie unseren
digitalen Alltag bestreiten müssen. Als erste Initiative dem ein Ende zu setzen bringen _Bastler
und Louis Fabu, der Sekretär der Kommenden Bastelei, euch DIE DIGITALE BASTELEI.</p>

View File

@ -0,0 +1,11 @@
<p>Wir betreiben freie Software auf selbst-verwaltenden Servern und bieten euch so Alternativen zu den sog.
"Cloud-Diensten", die euch sonst Google, Apple oder wer sonst andrehen. Wir verteilen die Kosten gleichmäßig auf
alle Nutzenden und sammeln noch etwas mehr Geld ein, um das Ganze weiter zu entwickeln und weitere Projekte und
Initiativen im Rahmen der Kommenden Bastelei zu finanzieren. Wir verwahren nur <a href="/privacy">minimale Daten</a>
von euch und
verknüpfen eure Zahlungsinformationen nicht mit euren User-Konten.</p>
<p>
Zum Start gibt es aktuell für 3€ im Monat zunächst ein E-Mail-Konto auf @bstly.de mit eigenem Adressbereich, 5GB
Speicher und catch-all-Funktion und einen Account für unsere Nextcloud mit 15GB
Speicherplatz. Dazu gehören so praktische Funktionen wie synchronisierbare Kalender, Online Office zum
kollaborativen Erstellen von Texten und einiges mehr. Mehr Details zu den aktuellen und geplanten Services findest du <a href="/services">hier</a>.</p>

View File

@ -0,0 +1,7 @@
<p>Du kannst Teil der Digitalen Bastelei werden: Entweder wirst du Vereinsmitglied oder bezahlst deine Teilnahme direkt
über unser
Einkaufs-System und überweist uns im halbjährlichen oder
jährlichen
Abstand. Einen Invite bekommst du bei den Beauftragten der Kommenden Bastelei in eurer Nähe. Je mehr Leute die Services
nutzen, desto effizienter wird es natürlich, weswegen wir später eventuell weitere Dienste, Upgrades oder
Wunschfeatures zur Verfügung stellen können.</p>

View File

@ -0,0 +1,51 @@
<h2>Impressum</h2>
<p>Kontakt<br>
Name: Bastelei (bald e. V.)<br>
E-Mail: impressum@bstly.de</p>
<h3>Ausschluss der Haftung</h3>
<h4>1. Haftung für Inhalte</h4>
<p>Der Inhalt unserer Internetseiten wurde mit größtmöglicher Sorgfalt erstellt. Wir übernehmen jedoch keine Gewähr
dafür,
dass dieser Inhalt richtig, vollständig, und aktuell ist und zudem noch gefällt. Gemäß § 7 Abs. 1 TMG sind wir für
den Inhalt verantwortlich, selbst wenn dieser bestellt wurde.</p>
<p>Gemäß den §§ 8, 9 und 10 TMG ist für uns keine Verpflichtung gegeben, dass wir Informationen von Dritten, die
übermittelt oder gespeichert wurden, überwachen oder Umstände erheben, die Hinweise auf nicht rechtmäßige
Tätigkeiten
ergeben.</p>
<p>Davon nicht berührt, ist unsere Verpflichtung zur Sperrung oder Entfernung von Informationen, welche von den
allgemeinen Gesetzen herrührt.</p>
<p>Wir haften allerdings erst in dem Moment, in dem wir von einer konkreten Verletzung von Rechten Kenntnis bekommen.
Dann
wird eine unverzügliche Entfernung des entsprechenden Inhalts vorgenommen.</p>
<h4>2. Haftung für Links</h4>
<p>Unsere Internetseiten enthält Links, die zu externen Internetseiten von Dritten führen, auf deren Inhalte wir jedoch
keinen Einfluss haben. Es ist uns daher nicht möglich, eine Gewähr für diese Inhalte zu tragen.</p>
<p>Die Verantwortung dafür hat immer der jeweilige Anbieter/Betreiber der entsprechenden Internetseite. Wir überprüfen
die von uns verlinkten Internetseiten zum Zeitpunkt der Verlinkung auf einen möglichen Rechtsverstoß in voller
Breite.</p>
<p>Es kann uns jedoch, ohne einen konkreten Anhaltspunkt, nicht zugemutet werden, ständig die verlinkten Internetseiten
inhaltlich zu überwachen. Wenn wir jedoch von einer Rechtsverletzung Kenntnis erlangen, werden wir den
entsprechenden
Link unverzüglich entfernen.</p>
<h4>3. Urheberrecht</h4>
<p>Wir weisen darauf hin, dass wir hinsichtlich der Inhalte auf unserer Internetseiten, soweit sie nicht von uns
erstellt
worden sind, das Urheberrecht von Dritten jederzeit beachtet haben.</p>
<p>Wenn du uns mitteilen würdest, dass du trotzdem eine Urheberrechtsverletzung gefunden hast, würden wir das sehr
schätzen. Dann können wir den entsprechenden Inhalt sofort entfernen und würde damit das Urheberrecht nicht mehr
verletzen.</p>

View File

@ -0,0 +1,384 @@
<p>Die folgende Erklärung gilt für die Domain bstly.de sowie deren Subdomains.</p>
<h2>Datenschutzerklärung</h2>
<p>Personenbezogene Daten (nachfolgend zumeist nur „Daten“ genannt) werden von uns nur im Rahmen der Erforderlichkeit
sowie zum Zwecke der Bereitstellung eines funktionsfähigen und nutzerfreundlichen Internetauftritts, inklusive
seiner Inhalte und der dort angebotenen Leistungen, verarbeitet.</p>
<p>Gemäß Art. 4 Ziffer 1. der Verordnung (EU) 2016/679, also der Datenschutz-Grundverordnung (nachfolgend nur „DSGVO“
genannt), gilt als „Verarbeitung“ jeder mit oder ohne Hilfe automatisierter Verfahren ausgeführter Vorgang oder jede
solche Vorgangsreihe im Zusammenhang mit personenbezogenen Daten, wie das Erheben, das Erfassen, die Organisation,
das Ordnen, die Speicherung, die Anpassung oder Veränderung, das Auslesen, das Abfragen, die Verwendung, die
Offenlegung durch Übermittlung, Verbreitung oder eine andere Form der Bereitstellung, den Abgleich oder die
Verknüpfung, die Einschränkung, das Löschen oder die Vernichtung.</p>
<p>Mit der nachfolgenden Datenschutzerklärung informieren wir dich insbesondere über Art, Umfang, Zweck, Dauer und
Rechtsgrundlage der Verarbeitung personenbezogener Daten, soweit wir entweder allein oder gemeinsam mit anderen über
die Zwecke und Mittel der Verarbeitung entscheiden.</p>
<p>Die Datenschutzerklärung ist wie folgt gegliedert:<br>
I. Informationen über uns als Verantwortliche<br>
II. Rechte der Nutzer und Betroffenen<br>
III. Informationen zur Datenverarbeitung</p>
<h3>I. Informationen über uns als Verantwortlicher</h3>
<p>Verantwortliche Anbieter dieses Internetauftritts im datenschutzrechtlichen Sinne:<br>
Name: Bastelei (bald e. V.)<br>
E-Mail: datenschutz@bstly.de</p>
<h3>II. Rechte der Nutzer und Betroffenen</h3>
<p>Mit Blick auf die nachfolgend noch näher beschriebene Datenverarbeitung hast du als Nutzer und Betroffenen das Recht
auf Bestätigung, ob dich betreffende Daten verarbeitet werden, auf Auskunft über die verarbeiteten Daten, auf
weitere
Informationen über die Datenverarbeitung sowie auf Kopien der Daten (vgl. auch Art. 15 DSGVO);
auf Berichtigung oder Vervollständigung unrichtiger bzw. unvollständiger Daten (vgl. auch Art. 16 DSGVO);
auf unverzügliche Löschung der dich betreffenden Daten (vgl. auch Art. 17 DSGVO), oder, alternativ, soweit eine
weitere Verarbeitung gemäß Art. 17 Abs. 3 DSGVO erforderlich ist, auf Einschränkung der Verarbeitung nach Maßgabe
von Art. 18 DSGVO;
auf Erhalt der dich betreffenden und von dir bereitgestellten Daten und auf Übermittlung dieser Daten an andere
Anbieter/Verantwortliche (vgl. auch Art. 20 DSGVO);<br>
auf Beschwerde gegenüber der Aufsichtsbehörde, sofern du der Ansicht bist, dass die dich betreffenden Daten durch
den Anbieter unter Verstoß gegen datenschutzrechtliche Bestimmungen verarbeitet werden (vgl. auch Art. 77 DSGVO).
</p>
<p>Darüber hinaus ist der Anbieter dazu verpflichtet, alle Empfänger, denen gegenüber Daten durch den Anbieter
offengelegt worden sind, über jedwede Berichtigung oder Löschung von Daten oder die Einschränkung der Verarbeitung,
die aufgrund der Artikel 16, 17 Abs. 1, 18 DSGVO erfolgt, zu unterrichten. Diese Verpflichtung besteht jedoch nicht,
soweit diese Mitteilung unmöglich oder mit einem unverhältnismäßigen Aufwand verbunden ist. Unbeschadet dessen hat
der Nutzer ein Recht auf Auskunft über diese Empfänger.</p>
<p>Ebenfalls hast du als Nutzer und Betroffenen nach Art. 21 DSGVO das Recht auf Widerspruch gegen die künftige
Verarbeitung der dich betreffenden Daten, sofern die Daten durch den Anbieter nach Maßgabe von Art. 6 Abs. 1 lit. f)
DSGVO verarbeitet werden. Insbesondere ist ein Widerspruch gegen die Datenverarbeitung zum Zwecke der Direktwerbung
statthaft.</p>
<p>Du hast gemäß denVorschriften der Datenschutzgrundverordnung (DSGVO) ein Auskunftsrecht über die zu deiner Person
gespeicherten Daten, einen Berichtigungsanspruch sowie bei Vorliegen der rechtlichen Voraussetzungen einen
Anspruch auf Einschränkung der Verarbeitung und Löschung.</p>
<p>Eine Auskunft / Löschung kann entweder in den entsprechenden Diensten über die persönlichen Einstellungen angefordert
werden oder per E-Mail Kontakt erfragt werden.</p>
<h3>III. Informationen zur Datenverarbeitung</h3>
<p>Deine bei Nutzung des Internetauftritts verarbeiteten Daten werden gelöscht oder gesperrt, sobald der Zweck der
Speicherung entfällt, der Löschung der Daten keine gesetzlichen Aufbewahrungspflichten entgegenstehen und
nachfolgend keine anderslautenden Angaben zu einzelnen Verarbeitungsverfahren gemacht werden.</p>
<h4>Serverdaten</h4>
<p>Aus technischen Gründen, werden Daten durch deinen Internet-Browser an den Server übermittelt. Soweit technisch
möglich, werden Daten wie u.a. Typ und Version deines Internetbrowsers, das Betriebssystem, die Website, von der aus
du auf unseren Internetauftritt gewechselt hast (Referrer URL), die Website(s) des Internetauftritts, die du
besuchst, Datum und Uhrzeit des jeweiligen Zugriffs sowie die IP-Adresse des Internetanschlusses, von dem aus die
Nutzung unseres Internetauftritts erfolgt, nicht(!) erhoben.</p>
<p>Da unser Interesse im Schutz dieser personenbezogenen Daten liegt, werden diese Daten generell nicht erhoben. Zur
Verbesserung, Stabilität, Funktionalität und Sicherheit unseres Internetauftritts ist jedoch eine temporäre
Erhebung möglich. Diese Speicherung erfolgt auf der Rechtsgrundlage von Art. 6 Abs. 1 lit. f) DSGVO.</p>
<p>Sollten diese so erhobenen Daten vorübergehend in sog. Server-Log-Files gespeichert werden, geschieht dies jedoch
nicht gemeinsam mit anderen Daten von dir.</p>
<p>Die Daten werden spätestens nach 14 Tagen wieder gelöscht.</p>
<h4>Kontaktanfragen / Kontaktmöglichkeit</h4>
<p>Sofern Du per Kontaktformular oder E-Mail mit uns in Kontakt trittst, werden die dabei von dir angegebenen Daten
zur Bearbeitung deiner Anfrage genutzt. Die Angabe der Daten ist zur Bearbeitung und Beantwortung deiner Anfrage
erforderlich - ohne deren Bereitstellung können wir deine Anfrage nicht oder allenfalls eingeschränkt beantworten.
</p>
<p>Rechtsgrundlage für die Verarbeitung dieser Daten ist Art. 6 Abs. 1 lit. b) DSGVO.</p>
<p>Deine Daten werden gelöscht, sofern deine Anfrage abschließend beantwortet worden ist und der Löschung keine
gesetzlichen Aufbewahrungspflichten entgegenstehen, wie bspw. bei einer sich etwaig anschließenden
Vertragsabwicklung.</p>
<h4>Nutzung des Dienstes Pretix</h4>
<h5>Gespeicherte Daten</h5>
<p>Die folgenden Daten werden durch den Dienst Pretix erfasst und gespeichert:
<p>
<table border="1">
<thead>
<tr>
<th>Bezeichnung</th>
<th>Betroffene Benutzer / Speicherfrist</th>
<th>Verwendungszweck / Weitergabe an Dritte</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<strong>Cookies</strong>
<p>Zufällig generierte IDs, technisch bedingte Parameter</p>
</td>
<td>
<p>alle Besucher der Seite / Sitzungsende (Beenden des Browsers)</p>
</td>
<td>
<p>Wiedererkennung des Benutzers während der Nutzung der Anwendung</p>
<p>keine Weitergabe an Dritte</p>
</td>
</tr>
<tr>
<td>
<strong>Rechnungsdaten / Mitgliedsdaten</strong>
<p>E-Mail Adresse, Name, Anschrift (freiwillig: Kommentar, Referenz)</p>
</td>
<td>
<p>Benutzer mit getätigter Bestellung / bis zur Löschung</p>
</td>
<td>
<p>Durchführung der Bestellung, interne Auflistung der Vereinsmitglieder, Archivierung für Steuerprüfung
</p>
<p>keine Weitergabe an Dritte</p>
</td>
</tr>
<tr>
<td>
<strong>Bestelldaten</strong>
<p>Datum, Status, Menge sowie Art der Bestellung</p>
</td>
<td>
<p>Benutzer mit getätigter Bestellung / bis zur Löschung</p>
</td>
<td>
<p>Durchführung der Bestellung, automatische Erinnerung für Mitglieder</p>
<p>Keine Weitergabe an Dritte</p>
</td>
</tr>
</tbody>
</table>
<h4>Nutzung des Dienstes we.bstly</h4>
<h5>Gespeicherte Daten</h5>
<p>Die folgenden Daten werden durch den Dienst we.bstly erfasst und gespeichert:
<p>
<table border="1">
<thead>
<tr>
<th>Bezeichnung</th>
<th>Betroffene Benutzer / Speicherfrist</th>
<th>Verwendungszweck / Weitergabe an Dritte</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<strong>Cookies</strong>
<p>Zufällig generierte IDs, technisch bedingte Parameter</p>
</td>
<td>
<p>alle Besucher der Seite / Sitzungsende (Beenden des Browsers)</p>
<p>Benutzer der „automatischen Anmeldung“ / bis zur aktiven Beendigung der Sitzung</p>
</td>
<td>
<p>Wiedererkennung des Benutzers während der Nutzung der Anwendung</p>
<p>Wiedererkennung des Benutzers bei „automatischer Anmeldung“</p>
<p>keine Weitergabe an Dritte</p>
</td>
</tr>
<tr>
<td>
<strong>Account-Daten</strong>
<p>Benutzername, öffentlicher PGP Schlüssel (freiwillig: E-Mail Adresse)</p>
</td>
<td>
<p>Benutzer mit Account / bis zur Löschung</p>
</td>
<td>
<p>Identifizierung für Login, Nutzung weiterer Diensten</p>
<p>keine Weitergabe an Dritte</p>
</td>
</tr>
<tr>
<td>
<p>Passwort</p>
</td>
<td>
<p>Keine permanente Speicherung, direkte Weitergabe an Authentifizierungsserver</p>
</td>
<td>
<p>Authentifizierung (Login)</p>
<p>Keine Weitergabe an Dritte</p>
</td>
</tr>
<tr>
<td>
<strong>Einstellungen / Eigenschaften</strong>
<p>Berechtigungen und Quotas (freiwillig: Parameter Zweifaktor Authentifizierung)</p>
</td>
<td>
<p>Benutzer mit Account / siehe Account-Daten</p>
</td>
<td>
<p>Nutzung weiterer Diensten, Steuerung des Login Prozesses</p>
<p>Keine Weitergabe an Dritte</p>
</td>
</tr>
</tbody>
</table>
<h4>Nutzung des Dienstes Nextcloud</h4>
<h5>Gespeicherte Daten</h5>
<p>Die folgenden Daten werden durch den Dienst Nextcloud erfasst und gespeichert:
<p>
<table border="1">
<thead>
<tr>
<th>Bezeichnung</th>
<th>Betroffene Benutzer / Speicherfrist</th>
<th>Verwendungszweck / Weitergabe an Dritte</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<strong>Cookies</strong>
<p>Zufällig generierte IDs, technisch bedingte Parameter</p>
</td>
<td>
<p>alle Besucher der Seite / Sitzungsende (Beenden des Browsers)</p>
<p>Benutzer der „automatischen Anmeldung“ / 16 Tage nach letzter Nutzung</p>
</td>
<td>
<p>Wiedererkennung des Benutzers während der Nutzung der Anwendung</p>
<p>Wiedererkennung des Benutzers bei „automatischer Anmeldung“</p>
<p>keine Weitergabe an Dritte</p>
</td>
</tr>
<tr>
<td>
<strong>Account-Daten</strong>
<p>Benutzername (freiwillig: E-Mail Adresse)</p>
</td>
<td>
<p>Benutzer mit Account / bis zur Löschung</p>
</td>
<td>
<p>Suche nach Benutzern beim Teilen von Inhalten, Senden von Benachrichtigungen</p>
<p>Weitergabe an alle Nutzer</p>
</td>
</tr>
<tr>
<td>
<strong>Einstellungen/ Eigenschaften</strong>
<p>Zeitstempel letztes Login, Speicherplatzkontingent, Speicherplatzkauf / Laufzeit, Sprache,
vorgenommene persönliche Einstellungen</p>
</td>
<td>
<p>Benutzer mit Account / siehe Account-Daten</p>
</td>
<td>
<p>Erkennung inaktiver Benutzer, Speicherplatzzuweisung, persönliche Anpassung der Oberfläche,
Benachrichtigungen, etc.</p>
<p>Keine Weitergabe an Dritte</p>
</td>
</tr>
<tr>
<td>
<strong>Aktivitäten</strong>
<p>Auflistung der im System durchgeführten Aktionen, z.B. Upload von Dateien</p>
</td>
<td>
<p>Benutzer mit Account / 14 Tage</p>
</td>
<td>
<p>Nachvollziehbarkeit von Änderungen</p>
<p>Weitergabe individuell vom Nutzer einstellbar</p>
</td>
</tr>
<tr>
<td>
<strong>strukturierte Daten</strong>
<p>Daten, welche von Apps in der Datenbank abgelegt werden</p>
</td>
<td>
<p>Benutzer mit Account / maximale Speicherfrist siehe Account-Daten, ansonsten abhängig von der
jeweiligen App</p>
</td>
<td>
<p>Nutzung der jeweiligen Apps</p>
<p>Weitergabe von in Apps erfassten Daten individuell vom Nutzer einstellbar</p>
</td>
</tr>
<tr>
<td>
<strong>Dateien</strong>
<p>Dateien, welche mit der „Dateien“-Anwendung oder externen Clients abgelegt werden</p>
</td>
<td>
<p>Benutzer mit Account / siehe Account-Daten</p>
</td>
<td>
<p>Nutzung der „Dateien“-App oder Clients für verschiedene Plattformen zur Dateisynchronisierung, Teilen
von Dateien mit Dritten</p>
<p>Weitergabe von Dateien individuell vom Nutzer einstellbar</p>
</td>
</tr>
</tbody>
</table>
<h4>Nutzung des Dienstes E-Mail Postfach</h4>
<h5>Gespeicherte Daten</h5>
<p>Die folgenden Daten werden durch den Dienst E-Mail Postfach erfasst und gespeichert:
<p>
<table border="1">
<thead>
<tr>
<th>Bezeichnung</th>
<th>Betroffene Benutzer / Speicherfrist</th>
<th>Verwendungszweck / Weitergabe an Dritte</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<strong>E-Mails</strong>
<p>Empfangene und gesendete E-Mails</p>
</td>
<td>
<p>Benutzer mit E-Mail Postfach / bis zur Löschung</p>
</td>
<td>
<p>Speicherung der E-Mails zur Abfrage</p>
<p>Jeweilige Sender & Empfänger der E-Mail</p>
</td>
</tr>
</tbody>
</table>
<h4>Sicherheit</h4>
<p>Sämtliche Daten werden verschlüsselt übertragen. Die Nutzung einer unverschlüsselten Verbindung zum Server ist
technisch ausgeschlossen.</p>
<p>Zusätzliche Sicherheitsfunktionen, wie 2-Faktor-Authentifizierung und anwendungsspezifische Logins werden unterstützt
und können in den Einstellungen aktiviert werden.</p>
<h4>Rechenzentrum</h4>
<p>Die Daten werden im Rechenzentrum der <a href="https://www.netcup.de/ueber-netcup/rechenzentrum.php"
target="_blank">netcup GmbH</a> gespeichert. Eine regelmäßige, automatisierte Datensicherung der Bestandsdaten wird durchgeführt.
</p>

View File

@ -0,0 +1,25 @@
<p>Hier findest du eine kleine Übersicht über Datenschutz bei uns und unser Verständnis davon. Die genaue Auflistung der
Verwendung deiner Nutzungsdaten findest du in unserer <a href="/privacy-policy">Datenschutzerklärung</a>.</p>
<p>Privacy by Design bedeutet, dass die Systeme grundlegend auf Datensparsamkeit ausgelegt sind. Wir versuchen generell immer so
wenig Daten wie möglich zu erheben. Frei nach dem Motto 'was man nicht hat, kann man auch nicht verlieren'. Das gilt
zuerst einmal generell für Daten, die wir erheben, aber auch für die Verknüpfung verschiedener Daten zueinander.</p>
<p>Um Account- und Zahlungsdaten zu trennen, benutzen wir das Ticket-System Pretix mit seinem Check-In System. Für jedes
Item wird ein geheimes Token generiert. In unserer selbst entwickelten Serverkomponente, we.bstly, können dann diese
Tokens eingelöst werden und die entsprechende Leistung gespeichert werden. Das Token wird dann in Pretix als
eingelöst markiert.</p>
<p>Die Verknüpfung von Account/Leistung und Token ist also nur temporär für die bestehende Browser Session gültig und
wird sonst nicht dauerhaft gespeichert. Sprich: es gibt zwei Datenbanken, eine für Zahlungsdaten und Tokens
(Pretix-System) und eine mit Account- und Berechtigungsdaten (we.bstly-System). Die Verknüpfung findet nur in einer
aktiven Browser-Session statt und wird nach Einlösen des Tokens auch dort vergessen. Es gibt also keinerlei
Verknüpfung von Zahlungsdaten und Account.</p>
<p>Abstriche machen wir aktuell bei der Verknüpfung von Accounts bei den einzelnen Services. Wie immer haben Komfort und
Einfachheit ihren Preis. Durch Single-Sign-On (SSO) über OIDC heißt das: Ein Account für Alles. Sprich, die Nutzung
der einzelnen Services ist immer auf deinen we.bstly-Account zurückzuführen. Zum Einen bietet das den Komfort von
SSO, dass du nur auf diesen einen Account gut aufpassen musst. Zum Anderen bekommt man dafür natürlich auch die
Einfachheit, dass alle die Sicherheit haben, dass es sich bei den verschiedenen Services immer um den gleichen
we.bstly-Account handelt. So weißt du z.B., dass du auch genau dem Menschen eine E-Mail schreibst, mit dem du gerade
gechattest hast.</p>

View File

@ -0,0 +1,8 @@
<p>Aktuell werden die E-Mails so wie sie ankommen auf dem Server gespeichert. Da dies einige Nachteile und unnötiges
Vertrauen benötigt, arbeitet _Bastler an einer Lösung, dass alle E-Mails automatisch mit deinem Public Key
verschlüsselt werden. Das gibt dir die Sicherheit, dass auch nur du die E-Mails entschlüsseln kannst. Allerdings
bedeutet dies auch, dass du all deine E-Mail-Clients für die Entschlüsselung einrichten musst. Wir werden
selbstverständlich detaillierte Anleitungen dazu veröffentlichen wenn es soweit ist und vermutlich auch ein Opt-Out
anbieten, wenn du auf diese Funktion verzichten möchtest.</p>
<p>Zur Authentifizierung werden deine we.bstly Account-Daten verwendet, d.h. hier gilt immer dieselbe Sicherheit.</p>

View File

@ -0,0 +1,14 @@
<p>Wie sensibel die Daten in der Nextcloud sind, hängt natürlich nur von deiner Nutzung ab. Vom Dienst selber fallen
keine Daten außer deine Account-Daten (nur Username und freiwillige Profildaten) an. Da hier ausschließlich von
Usern
selbst erstellte Inhalte gespeichert werden, kommt es darauf an, was du hochlädst, veröffentlichst und schreibst.
Die Daten werden automatisch verschlüsselt gespeichert, es handelt sich
dabei allerdings nur um eine serverseitige Verschlüsselung, sodass du die Daten weiterhin mit Anderen teilen kannst.
Nextcloud bietet allerdings in den aktuellen Versionen auch eine eigene Ende-Zu-Ende-Verschlüsselung (E2EE) an. Es
steht dir natürlich frei diese für sensible Daten zu nutzen, sodass auch niemand anderes an diese Dateien kommt.
Beachte aber, dass diese Dateien dann nicht mehr im Browser zugänglich sind und nicht geteilt werden können.
Außerdem gilt die E2EE nur für Dateien und nicht für andere Daten wie Nachrichten, Kalender o.Ä., so dass die
Empfehlung ist, immer darüber nachzudenken, welche Daten man gerade erzeugt und wie sensibel diese sind.</p>
<p>Da die Nextcloud auch als Basis für unsere Community dient, werden deine Account-Daten mit allen anderen Usern
geteilt. Es steht dir aber auch frei, weitere Daten wie Dateien, Kalender etc. mit anderen Usern zu teilen.</p>

View File

@ -0,0 +1,3 @@
<p>Im Pretix System müssen wir natürlich persönliche Daten zum Zahlungsverkehr sowie eine E-Mail-Adresse speichern zum
Versenden von E-Mails mit Bestätigungen, Zahlungsdaten sowie zum Verschicken der Tokens. Als Vereinsmitglied werden
hier deine zu erhebenden Mitgliedsdaten sowie dein Mitgliedsbeitragskonto gespeichert.</p>

View File

@ -0,0 +1,12 @@
<p>Im we.bstly-System brauchst du lediglich einen Usernamen und ein Passwort (gut gesalzen, Argon2 gehashed!).
Zusätzlich wird noch ein Private-Public-Schlüsselpaar erstellt. Mehr nicht. Optional ist noch die Angabe einer
E-Mail Adresse. Diese ist erforderlich beim Verlust der Login-Daten bzw. deines Private-Keys. Wir halten diese
Option allerdings offen, so dass eine völlig anonyme Nutzung aller Dienste möglich ist, wenn du dein Passwort bzw.
deinen Private-Key nicht verlierst!</p>
<p>Zusätzlich zu deinen zentralen Account-Daten werden hier auch deine Berechtigungen und das Ablaufdatum deiner Services
gespeichert. Aktuell ist es auch vorgesehen, die Information zu speichern, ob du ein reguläres Vereinsmitglied bist.
Dies hat den Vorteil, dass wir über unsere Services auch alle Vereinsmitglieder direkt erreichen können oder
erweiterte Services anbieten können. Die Daten werden selbstverständlich nicht mit deinen Mitgliedsdaten und dem
Mitgliedsbeitragskonto verknüpft, sprich: wir wissen lediglich, <strong>dass</strong> du Vereinsmitglied bist,
<strong>nicht welches</strong>!</p>

View File

@ -0,0 +1,9 @@
<p>Alle Dienste laufen über einen Webserver, wie auch z.B. diese Seite. Generell fallen bei einem Webserver automatisch
ein Haufen Daten an, wie z.B. deine IP-Adresse, deine Webbrowser-Version und einiges mehr. Dass diese Daten
übertragen werden ist technisch bedingt und lässt sich nur mit Aufwand von deiner Seite aus verhindern. Wichtig ist
also, was wir mit diesen Daten machen.</p>
<p>Und was machen wir mit diesen Daten? Die Antwort ist einfach: Nichts! Im Allgemeinen werden diese Daten gar nicht
gespeichert. Im Zuge von Wartungsarbeiten o.Ä. kann es dazu kommen, dass diese Daten in Log-Files geschrieben werden,
damit wir Problemen auf den Grund gehen können. Diese werden dann allerdings mit keinen anderen Daten verknüpft und
direkt nach Beendigung der Arbeiten gelöscht!</p>

View File

@ -0,0 +1,8 @@
<p>Für deinen Usernamen wird automatisch ein E-Mail Konto mit dem Schema {username}@we.bstly.de (Beispiel für
den User foobar: foobar@we.bstly.de) erstellt. Außerdem wird ein sogenanntes @Catch-All für @{username}.we.bstly.de eingerichtet. Das
bedeutet, dass automatisch alle E-Mails an eine Adresse an @{username}.we.bstly.de in deinem Postfach landen. So hast
du eigentlich endlos viele E-Mail-Adressen zur Verfügung. Ein automatischer Spam-Filter ist natürlich auch dabei.
Ein Webmail Zugang steht ebenfalls zur Verfügung.</p>
<p>Für E-Mail Protokolle gibt es keinen OIDC-Login, dies bedeutet technisch, dass zum Login einfach die
Account-Datenbank von we.bstly genommen werden und somit dein we.bstly-Account Passwort.</p>

View File

@ -0,0 +1,7 @@
<p>Herzstück der digitalen Bastelei ist die Nextcloud. Nextcloud vereint einige Cloud-Dienste in einem. Ein einfaches
Dateisystem wie z.B. von Dropbox, einen Kalender wie z.B. von Google und und und. Die
Daten in der Nextcloud sind generell verschlüsselt, für volle Sicherheit deiner Dateien kannst du allerdings auch
die Ende-zu-Ende-Verschlüsselung der Nextcloud nutzen.</p>
<p>Über Nextcloud werden wir euch auch mit allen wichtigen Informationen über "Bastelei (bald e.V.)" informieren.
Des weiteren bietet Nextcloud auch einige Community-Funktionen, die wir gerne mit euch nutzen möchten.</p>

View File

@ -0,0 +1,8 @@
<p> Das we.bstly-System ist die zentrale Verwaltung für Account-Daten und Berechtigungen. Hier werden also Login-Daten
(Username + Password-Hash) und dein Public Key gespeichert, sowie verknüpfte Berechtigungen mit ihrer
Gültigkeitsdauer (Beispiel: Mitgliedschaft noch bis 24.03.2049, Git-Zugang bis 01.02.2027).</p>
<p>Implementiert ist dort ein <a href="https://openid.net/connect/" target="_blank">OpenID Connect (OIDC) Provider</a>
der den Zugriff auf die anderen Dienste ermöglicht.
Sprich, für alle weiteren Dienste (Ausnahme E-Mail, siehe unten) läuft der Login direkt über we.bstly (SSO - Single
Sign On).</p>

View File

@ -0,0 +1,5 @@
<p>Online Konferenzsoftware.</p>
<p>BigBlueButton ist ein mächtiges Konferenz-Tool, dass für den Einsatz an Schulen konzipiert wurde. Dadurch bietet es einen großen Umfang an Funktionen um viele denkbare Konferenz-Situationen abzubilden. Gerade aus Sicht der Bastelei und des Vereins bietet ein solches Tool Vorteile um größere Online-Konferenzen zu organisieren oder z.B. eine Mitgliederversammlung abzuhalten.</p>
<p class="hint">⚠️ Größerer Umfang an Funktionen bringt große Komplexität mit sich. Aktuell wird für BBB empfohlen einen eigenständigen Server zu verwenden. Hinzu kommen einige Sicherheitsbedenken, da die empfohlenen Systemvoraussetzungen veraltet sind. Außerdem bietet BBB derzeit auch keine OIDC-Unterstützung, was zusätzlich das Thema der Authentifizierung mit sich bringt.</p>

View File

@ -0,0 +1,7 @@
<p>Passwort Manager.</p>
<p>Ein Passwort Manager verwaltet sicher all deine Passwörter. Außerdem kannst du dir komplizierte Passwörter generieren lassen, so dass du für all deine Konten ein anderes, schwer zu erratendes Passwort hast und die Sicherheit deiner Konten erhöhst.</p>
<p>Bitwarden empfiehlt sich vor allem durch seine regelmäßigen unabhängigen Audits mit positiven Ergebnissen. Es gibt kaum vergleichbare Alternativen in selbst betriebenen Open-Source Password Managern.</p>
<p class="hint">⚠️ Aktuell bietet Bitwarden leider OIDC Unterstützung nur für eine bezahlte Premium Lizenz. Es gibt einen offenen <a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank">Rust-Klon</a> der Server Komponente, diese legt aber aktuell wenig Wert auf die Unterstützung des Organisationen-Feature und damit auf das damit verbundene OIDC. Somit bleibt abzuwarten, ob sich hier in nächster Zeit etwas entwickelt oder ob etwas für den Einsatz einer Alternative spricht.</p>

View File

@ -0,0 +1,10 @@
<p>Offene Source-Code Verwaltung auf Basis von git-Repositories (vergleichbar mit GitHub).</p>
<p>Gegenüber der Konkurrenz bietet Gitea vor allem den Vorteil, dass es selbst betrieben werden kann und man so die
Hoheit über seine Daten und seinen Code behält. Nennenswerte zusätzliche Features gegenüber vergleichbarer Software
gibt es nicht, der Funktionsumfang steht allerdings auch in nichts nach.</p>
<p class="hint">✅ Da Gitea auch OIDC Unterstützung mitbringt, stellt sich vor allem die Frage, ob für eine solche
Plattform Bedarf besteht. Da durch die Repositories natürlich einiges an Speicher hinzukommt, empfiehlt sich auch
hier eventuell ein separater Server, der dann von den Usern, die diesen Service nutzen wollen, finanziert werden
würde.</p>

View File

@ -0,0 +1,13 @@
<p>Online Konferenzsoftware.</p>
<p>Vor allem die Einfachheit in der Bedienung und aufs Nützlichste beschränkte Funktionen sind die Vorteile von Jitsi
Meet. Da es in jedem modernen Browser läuft, fällt auch die lästige Installation von Anwendungen auf Endgeräten weg.
Lediglich auf Smartphones wird die kostenlose Jitsi-App benötigt. Hier muss als Server dann der Bastelei-Server
angegeben werden.</p>
<p class="hint">❔ Bei Jitsi Meet ist eigentlich nur die Frage offen, ob es von den Serverkapazitäten möglich ist eine
offene Instanz zu betreiben bzw. ein eigener Server nötig ist, da die Software doch recht ressourcenhungrig ist.
Ansonsten muss sich _Bastler noch um eine Authentifizierungsmethode kümmern. OIDC wird nicht direkt unterstützt,
eine Beschränkung über VPN (Wireguard) wäre auch denkbar. Zu erwähnen ist hier, dass die Authentifizierung lediglich
für das Erstellen von Räumen nötig wäre. Jitsi Meet bietet eine Gästekonfiguration, so dass du so oder so beliebige
Personen zu deiner Konferenz einladen kannst.</p>

View File

@ -0,0 +1,10 @@
<p>Dezentrales Ende-zu-Ende-verschlüsseltes Messaging Protokoll.</p>
<p>Mit einem Matrix-Server können wir Teil eines dezentralen Messaging Netzwerk werden, welches mit einer E2EE die
größtmögliche Datensicherheit bietet. Es gibt verschiedene Clients, alles im allem ähnelt die Handhabung aber den
gängigen, bekannten Messenger wie Signal, WhatsApp, Threema oder Telegram.</p>
<p>Damit hätten wir auch ein verschlüsseltes Kommunikationssystem für unsere internen Nachrichten.</p>
<p class="hint">✅ Da der Synapse Server direkte Unterstützung für OIDC bietet, braucht es lediglich Serverkapazitäten
um diesen Service einzurichten.</p>

View File

@ -0,0 +1,11 @@
<p>Ad-Blocking via DNS (benötigt dann Wireguard ⚠️).</p>
<p>Werbeblocking über DNS bietet einige Vorteile gegenüber klassischem Ad-Blocking über Browser-Plugins. Da das Request
als solches blockiert wird, bekommt der Werbeserver nicht einmal mit, dass er gerade blockiert wird. Außerdem
funktioniert so ein Blocking dann für alle Geräte, Applikation usw. die auf das Internet zugreifen. So wird auch
Werbung und Tracking in mobilen Anwendungen blockiert.</p>
<p class="hint">❔ Pi-Hole selber einzurichten ist kein Problem. ⚠️ Allerdings würde eine offene Konfiguration für jeden
zugänglich sein, so dass Serverlasten nicht kontrollierbar wären. Deshalb ist die Idee von _Bastler, den Service
über ein VPN (Wireguard) zur Verfügung zu stellen. So ist Pi-Hole immer aktiv, sobald du eine gültige VPN Verbindung
zu unserem Server hast.</p>

View File

@ -0,0 +1,14 @@
<p>VPN Server.</p>
<p>Ein VPN Server bietet zum einen den Vorteil, dass deine echte IP-Adresse verschleiert wird, zum anderen würde ein
VPN uns ein paar technische Möglichkeiten bieten, da du dich als User dann in einem internen Netzwerk befindest,
worüber wir weitere Dienste oder Zugriffe auf bestimmte Dienste ermöglichen können.</p>
<p>Da natürlich sehr viel Traffic über den Server läuft wenn viele User im VPN sind, wird aktuell ein Hybrid-Betrieb
bevorzugt. Dies bedeutet dass du zwar weiterhin mit deiner eigenen IP direkt auf das Internet zugreifst, wir aber
eben durch das interne Netzwerk weitere Services zur Verfügung stellen können.</p>
<p class="hint">⚠️ In der Theorie ist das Aufsetzen des Servers kein Problem. Da die Authentifizierung über ein
Public-Private-Key Verfahren läuft, bräuchte es eigentlich nur ein kleines Script um den Public-Key eines Users
zur Konfiguration hinzuzufügen (oder zu entfernen). Das Szenario muss allerdings noch von _Bastler verifiziert und
getestet werden.</p>

View File

@ -1 +0,0 @@
<h1>Hello you lalala</h1>

View File

@ -123,3 +123,57 @@ mat-form-field {
} }
} }
} }
qrcode {
margin: 0 auto;
text-align: center;
}
qrcode canvas {
width: 100% !important;
height: auto !important;
max-width: 400px !important;
}
.spacer {
flex: 1 1 auto;
}
.hint {
opacity: 0.7;
}
.mat-drawer-inner-container {
display: flex;
flex-direction: column;
}
mat-sidenav-container {
height: 100%;
max-height: 100%;
}
.container {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
margin-bottom: 15px;
@media screen and (min-width: 576px) {
width: 540px;
}
@media screen and (min-width: 768px) {
width: 580px;
}
@media screen and (min-width: 992px) {
width: 820px;
}
@media screen and (min-width: 1200px) {
width: 1000px;
}
}