Compare commits

..

137 commits

Author SHA1 Message Date
b1db76d7c5 set only atributes existing on the original image 2022-07-15 15:43:58 +02:00
7e56c51ca9 remove fay 2022-07-13 12:29:51 +02:00
280e67693e
update comments 2022-07-05 12:28:01 +02:00
ae50943802
limit s3 request to 10 minutes 2022-07-05 12:27:31 +02:00
c719db5657
show error string in german 2022-07-05 12:26:54 +02:00
e00442b9da
validate email address format 2022-07-05 12:25:12 +02:00
b38c66a0f0 add public ssh key 2022-06-26 15:04:44 +02:00
b5d6cbfacb Revert "switch to php 8.1"
This reverts commit bab8fc435e
2022-06-13 22:20:20 +02:00
43ccbd72f7 add speedtest server 2022-06-13 22:19:48 +02:00
bab8fc435e
switch to php 8.1 2022-05-31 14:18:47 +02:00
Jonas Leder
71ce6ee81b
add new readme file 2022-05-20 16:44:43 +02:00
Jonas Leder
5df5d7412e
enable comment function on instruction pages 2022-05-20 16:32:48 +02:00
Jonas Leder
ff731ce18e
add RAM 2022-05-20 16:29:28 +02:00
Jonas Leder
3ca2112351
add hdd 2022-05-20 16:28:32 +02:00
Jonas Leder
fabd83f1f4
add AIO cooler 2022-05-20 16:27:18 +02:00
Jonas Leder
2613032cd0
replace nvme 2022-05-20 16:23:31 +02:00
Jonas Leder
cb475c3746
add 10G NIC, remove GPU and replace mainboard 2022-05-20 16:20:52 +02:00
Jonas Leder
2c3bc4b4e7
add new 10G NIC 2022-05-20 16:20:35 +02:00
Jonas Leder
0348d52123
remove old non existing systems 2022-05-20 16:20:24 +02:00
Jonas Leder
ae0dac4437
update hardware 2022-05-20 16:16:53 +02:00
Jonas Leder
3f214a4cac
update text above ntp status 2022-05-20 16:08:41 +02:00
50dbff7f2c
add imgproxy settings 2022-05-01 13:13:20 +02:00
19beccff30
add variable size for images 2022-05-01 13:13:10 +02:00
3457314e87
remove old imgPreview 2022-05-01 13:02:04 +02:00
50689d2b83
apply id from jl-img to inner image element 2022-05-01 13:01:34 +02:00
f0e92fe024
fix images on main page 2022-05-01 13:01:21 +02:00
b2efb92a53
open preview when clicking on image 2022-05-01 12:58:57 +02:00
fc75c07473
fix variable type 2022-05-01 12:56:43 +02:00
0e0ca76afb
use 512px width 2022-05-01 12:56:34 +02:00
ffc750c405
use jl-image for images 2022-05-01 12:53:54 +02:00
3ab7888a2a
use jl-image for images 2022-05-01 12:53:48 +02:00
f140d1e271
create ednpoint to reduce image size with imgproxy 2022-05-01 12:45:06 +02:00
ccb5af6c8e add gpg key 2022-04-26 09:16:15 +02:00
9abcba556c
use webp instead of png for skills 2022-04-16 23:36:25 +02:00
8c5b68037a
render images through imgproxy 2022-04-16 23:34:35 +02:00
d465f5ecb8
get skills directly from s3 server 2022-04-16 23:11:42 +02:00
bfae5cc098 add open bug bounty txt file 2022-04-09 15:58:45 +02:00
0f70b811f9
fix new comment 2022-04-07 09:26:20 +02:00
7b56fbbdcd
set content type in graphql request 2022-04-07 09:24:06 +02:00
a6a470f8aa use mutation query 2022-04-07 09:12:43 +02:00
aa670a0512 add mutation query for new comment 2022-04-07 09:12:43 +02:00
bbc1fb852f rewrite get parameter function 2022-04-07 09:12:43 +02:00
b8605da2b3 Merge branch 'jonasled-master-patch-52063' into 'master'
use better graphql setup, which also supports schema queries

See merge request jonasled/website!12
2022-04-07 09:11:54 +02:00
fa119a00cd use better graphql setup, which also supports schema queries 2022-04-05 10:26:18 +02:00
8131709a88 limit post requests to 1M 2022-03-30 18:46:25 +02:00
9b6c5d0fb8
rename last stage to prduction 2022-03-25 12:17:45 +01:00
8c2252a0d4
change link label to ntp pool 2022-03-25 12:10:53 +01:00
65db25ae63
remove normalize 2022-03-25 12:06:08 +01:00
fa0d3baf0b
use 100ms for zoom animation 2022-03-25 12:04:57 +01:00
ac00743cb8
add comment 2022-03-25 12:01:23 +01:00
b2dccc445c
remove raw html 2022-03-25 10:43:24 +01:00
bae04c21cf
remove raw html 2022-03-25 10:38:01 +01:00
8e990911c1
remove raw html 2022-03-25 10:23:52 +01:00
b18fe46788
remove raw html 2022-03-25 10:21:11 +01:00
04e198c6d4
add option to set language with attribute 2022-03-25 10:18:57 +01:00
031cccb475
generate footer with pure js 2022-03-25 10:13:51 +01:00
e2524efc6a
use connected callback instead of constructor 2022-03-25 10:13:32 +01:00
61bc809fa8
generate social buttons from array 2022-03-25 09:38:48 +01:00
f155578daa
use connected callback for loading SVGs instead of constructor 2022-03-25 09:38:20 +01:00
c4685355b5
fix mobile menu has no navigation displayed 2022-03-25 09:24:28 +01:00
1781d611f3 Merge branch 'jonasled-master-patch-76500' into 'master'
remove browser check

See merge request jonasled/website!11
2022-03-21 11:28:28 +01:00
68afb9607d remove browser check 2022-03-21 11:19:47 +01:00
49ef0459cd update error message 2022-03-18 10:20:32 +01:00
ffbbdae474 edit banner text 2022-03-18 08:13:51 +01:00
281a7c3ff4 Merge branch 'jonasled-master-patch-15565' into 'master'
fix two times sql injection possible

See merge request jonasled/website!10
2022-03-16 10:27:11 +01:00
9c236bba83 fix two times sql injection possible 2022-03-16 10:18:46 +01:00
b4371f8db4 Merge branch 'graphql' into 'master'
fix graphql using localhost for api

See merge request jonasled/website!9
2022-03-15 08:58:37 +01:00
Jonas Leder
a150b0c744
fix graphql using localhost for api 2022-03-15 08:49:36 +01:00
b183bf90c1 Merge branch 'graphql' into 'master'
Rewrite API endpoint to graphql

See merge request jonasled/website!8
2022-03-14 15:48:50 +01:00
Jonas Leder
15b36397ae
remove old endpoint for creating comments 2022-03-14 15:39:38 +01:00
Jonas Leder
b818a962d5
reformat file and fix new comment function 2022-03-14 15:38:13 +01:00
Jonas Leder
fa7109260e
use graphql api for creating comments 2022-03-14 15:37:52 +01:00
Jonas Leder
7fe912b172
add option to regenerate comments 2022-03-14 15:37:35 +01:00
Jonas Leder
97158b5f0c
use require instead of import 2022-03-08 15:11:10 +01:00
Jonas Leder
e305c030e6
add new comment endpoint 2022-03-08 15:10:52 +01:00
Jonas Leder
92ce267baf
get ebay kleinanzeigen from graphql 2022-03-08 13:42:03 +01:00
Jonas Leder
abe8db6e8b
get impressum mail from graphQL 2022-03-08 12:46:06 +01:00
Jonas Leder
9b12fe2c94
load comments from graphQL 2022-03-08 12:23:05 +01:00
Jonas Leder
f132ba2c55
format document 2022-03-08 12:19:39 +01:00
Jonas Leder
63c2fde0de
add graphql endpoint to get comments 2022-03-08 12:18:31 +01:00
Jonas Leder
356f839f9a
load blog on index and footer from graphQL 2022-03-08 12:06:56 +01:00
Jonas Leder
49fa8a89b8
use graphql variables for postid 2022-03-08 11:23:37 +01:00
Jonas Leder
4fb15ec6d5
remove newlines in graphql query 2022-03-08 11:15:41 +01:00
Jonas Leder
5c001a992b
update error message 2022-03-08 11:14:31 +01:00
Jonas Leder
7e371dac06
load blog posts from graphql 2022-03-08 11:14:23 +01:00
Jonas Leder
bd9be3b0b5
add blog posts to graphql 2022-03-08 11:05:52 +01:00
Jonas Leder
d1cda0ba2f
load sitekey from graphql 2022-03-08 10:05:28 +01:00
Jonas Leder
7509544b00
load skills from graphql 2022-03-08 10:04:11 +01:00
Jonas Leder
3eb89d763c
get hcaptcha sitekey using graphql 2022-03-08 09:54:38 +01:00
Jonas Leder
0349c0533e
renme compile job to build 2022-03-08 08:57:52 +01:00
947e64d94c set higher network timeout in yarn 2022-02-25 16:08:23 +01:00
b5a37776e8 add binfmt 2022-02-25 10:14:48 +01:00
4904526443 disable file uploads in PHP 2022-02-25 09:53:20 +01:00
00740f441a pull node and composer through docker proxy 2022-02-21 11:51:56 +01:00
ccfb317317 Cross compile docker for arm and arm64 2022-02-21 09:51:38 +01:00
bc986d7547 integrade Discord bots into main status page 2022-02-13 16:06:15 +01:00
093e63653c fix bug windows resizer only working once 2022-02-11 10:24:39 +01:00
2ad5e46827 fix typo 2022-02-09 14:42:31 +01:00
410dff42d4 remove uptime banner 2022-02-09 13:25:03 +01:00
bcf5f02f36 remove newline in banner 2022-02-06 01:02:45 +01:00
9e20e5e15a fix banner 2022-02-05 23:25:37 +01:00
27dbbb1e8b add livechat link 2022-02-05 22:43:23 +01:00
2405d712fa
use class instead of id in banner div 2022-02-04 08:44:46 +01:00
9ddb43af2d
arange shields banner stacked 2022-02-04 08:43:57 +01:00
dcb5262744
update about page 2022-02-04 08:40:12 +01:00
9bcc34ff43
fix typo 2022-02-04 08:34:31 +01:00
8710d5e601
add note to ebk links 2022-02-04 08:28:04 +01:00
66312e4f93
replace upper foldername with lower char for SVG folder 2022-02-04 08:24:38 +01:00
bc4d5f1acb
add mor detailed error message to ebayimg 2022-02-04 08:20:55 +01:00
1e8ed63c24
return error if ebay returns other than 200 2022-02-04 08:18:03 +01:00
ce30653d3b
throw error if url is not set or empty 2022-02-03 10:25:55 +01:00
0eb8e4d22d
add shipping information 2022-02-03 10:17:39 +01:00
5fbd725f6c
fix typo in filename 2022-02-03 09:53:37 +01:00
fa5b9d9089 add margin right to skill images 2022-02-02 17:41:12 +01:00
dbefc2ba8d
rename file 2022-02-02 14:53:54 +01:00
187ddf9683
implement red and green button on 404 page 2022-01-30 11:18:16 +01:00
8deebbb1e1
remove unneeded file 2022-01-30 10:46:35 +01:00
abc8b4cacf
fix some image URL 2022-01-30 10:10:10 +01:00
8c9fa6f3b5
fix google play icon 2022-01-30 10:07:00 +01:00
7cf19bbd06
rename folder 2022-01-30 09:58:17 +01:00
40e6c049bf
update image path 2022-01-30 09:54:20 +01:00
43dcd5871c add all images to repo 2022-01-30 09:35:48 +01:00
c63cc3015b add gpg package to devcontainer 2022-01-30 09:28:57 +01:00
0af4202811 add Readme 2022-01-30 09:28:13 +01:00
a108682032 Merge branch 'vaultwarden-instructions' into 'master'
Add setup instructions for vaultwarden

See merge request jonasled/website!7
2022-01-29 21:34:36 +01:00
fbf0932c88 set page title 2022-01-29 21:32:47 +01:00
f3dfeb2424 add vaultwarden instructions 2022-01-29 21:32:04 +01:00
c0ee2ec89e add inline code element 2022-01-29 21:31:48 +01:00
e72b3acf0a fix traefik title 2022-01-29 20:41:33 +01:00
23664a55bd Merge branch 'traefik-instructions' into 'master'
add instructions how to setup traefik

See merge request jonasled/website!6
2022-01-29 20:28:49 +01:00
37db2cc33d add recommendations, non docker service 2022-01-29 20:27:24 +01:00
4c8fb1aae6 add http to https redirect 2022-01-29 20:01:20 +01:00
76fcf276f9 add traefik instructions 2022-01-29 19:01:53 +01:00
a5707e660f add traefik to menu 2022-01-29 16:53:12 +01:00
9e04efa8c7 add new file for traefik 2022-01-29 16:51:12 +01:00
d73e903044 add template html file 2022-01-29 16:50:34 +01:00
9f215ebfb2 fix image at wrong position 2022-01-29 16:49:22 +01:00
222 changed files with 3159 additions and 4886 deletions

View file

@ -1,7 +1,7 @@
FROM alpine:edge
RUN apk update && \
apk upgrade && \
apk add nodejs yarn php8 php8-mysqli php8-mbstring php8-curl php8-simplexml git nano composer openssh-client curl && \
apk add nodejs yarn php8 php8-mysqli php8-mbstring php8-curl php8-simplexml git nano composer openssh-client curl gpg && \
ln -s /usr/bin/php8 /usr/bin/php && \
curl -L https://unpkg.com/@pnpm/self-installer | node

11
.gitignore vendored
View file

@ -1,5 +1,5 @@
#config file
src/API/lib/config.php
public/API/lib/config.php
#phpstorm
.idea/
@ -7,14 +7,13 @@ src/API/lib/config.php
# vscode
.vscode/
public/css/
public/js/
.sass-cache/
#node cache
node_modules/
pnpm-lock.yaml
#composer
src/API/vendor
# parcel
dist/
.parcel-cache/
public/API/vendor

View file

@ -1,13 +1,13 @@
docker-build:
# Use the official docker image.
image: docker:latest
image: gitlab.jonasled.de/jonasled/buildx-docker:latest
stage: build
services:
- docker:dind
before_script:
- docker context create build
- docker buildx create build --use
- docker run --rm --privileged docker/binfmt:66f9012c56a8316f9244ffd7622d7c21c1f6f28d
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
# Default branch leaves tag empty (= latest tag)
# All other branches are tagged with the escaped branch name (commit ref slug)
script:
- |
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
@ -17,10 +17,9 @@ docker-build:
tag=":$CI_COMMIT_REF_SLUG"
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
fi
- docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" .
- docker push "$CI_REGISTRY_IMAGE${tag}"
- docker buildx build --platform linux/amd64,linux/arm,linux/arm64 --push --tag "$CI_REGISTRY_IMAGE${tag}" .
# Run this job in a branch where a Dockerfile exists
rules:
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile
- Dockerfile

View file

@ -1,26 +1,33 @@
# |--------------------------------------------------------------------------
# | Build SCSS and JS
# |--------------------------------------------------------------------------
FROM node:lts-alpine AS buildJS
FROM docker-proxy.jonasled.de/library/node:lts-alpine AS buildJS
WORKDIR /build
COPY . .
RUN yarn install
RUN yarn compile
RUN mkdir public/js
RUN mkdir public/css
RUN yarn install --network-timeout 1000000
RUN yarn build
# |--------------------------------------------------------------------------
# | Install PHP dependencies
# |--------------------------------------------------------------------------
FROM composer:2 AS composer
FROM docker-proxy.jonasled.de/library/composer:2 AS composer
WORKDIR /build
COPY --from=buildJS /build/dist .
COPY --from=buildJS /build .
RUN composer install
# |--------------------------------------------------------------------------
# | Install Webserver
# |--------------------------------------------------------------------------
FROM gitlab.jonasled.de/jonasled/nginx-php-minimal:8-latest
FROM gitlab.jonasled.de/jonasled/nginx-php-minimal:8-latest as production
ENV PHP_FILE_UPLOADS=Off \
PHP_MAX_POST=1M
RUN apk update && \
apk add php8-mysqli php8-mbstring php8-curl php8-simplexml --no-cache && \
rm /etc/nginx/http.d/default.conf
COPY --from=composer /build/public .
COPY ./nginx.conf /etc/nginx/http.d/default.conf

34
Readme.md Normal file
View file

@ -0,0 +1,34 @@
# Website
This is the repo containing my personal website. It is based on simple HTML pages, with custom HTML componetes, for things like header and footer. The components are written in pure CS and compiled with webpackt to one big file. In this step also the dependencies are getting injected. Stylesheets are written in stylus and then compiled to CSS in the build process. For the backend there is a GraphQL endpoint at `/API/graphql.php` which holds most of the resources, for files (ebay images and S3) there are seperate endpoints in the API directory. Comments and the blog ist stored in a MySQL Database.
## Dev-Setup
### Requirements
* NodeJS
* Yarn
* PHP 8 with the following extensions:
* mysqli
* mbstring
* curl
* simplexml
* Composer
### Setup
1. Install the node dependencies: `yarn install`
2. Install the PHP depdendencies: `composer install`
3. Start the dev environment: `yarn watch`
This will start the compile service for the JavaScript and the stylus files and also start a PHP dev server on port 1234
## Production-Setup
For production there is a docker images with the following name available: `gitlab.jonasled.de/jonasled/website:latest`. The configuration is stored in the config PHP, a example file is available in this repo in the `/public/API/lib` folder. Later this file has to be mounted at `/var/www/html/API/lib/config.php` A example compose could look like this:
```yaml
version: '3.2'
services:
website:
image: gitlab.jonasled.de/jonasled/website:latest
restart: always
volumes:
./config.php:/var/www/html/API/lib/config.php
ports:
- "80:80"
```

View file

@ -1,7 +1,8 @@
{
"require": {
"aws/aws-sdk-php": "^3.181",
"guzzlehttp/guzzle": "^7.0"
"guzzlehttp/guzzle": "^7.0",
"webonyx/graphql-php": "^14.11"
},
"config": {
"vendor-dir": "public/API/vendor"

70
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f47a9b8d286ca72493a7fe02f263976e",
"content-hash": "31a3a0321659f9c8afff63a68a9fafb6",
"packages": [
{
"name": "aws/aws-crt-php",
@ -669,6 +669,72 @@
}
],
"time": "2021-05-27T12:26:48+00:00"
},
{
"name": "webonyx/graphql-php",
"version": "v14.11.5",
"source": {
"type": "git",
"url": "https://github.com/webonyx/graphql-php.git",
"reference": "ffa431c0821821839370a68dab3c2597c06bf7f0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/ffa431c0821821839370a68dab3c2597c06bf7f0",
"reference": "ffa431c0821821839370a68dab3c2597c06bf7f0",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"php": "^7.1 || ^8"
},
"require-dev": {
"amphp/amp": "^2.3",
"doctrine/coding-standard": "^6.0",
"nyholm/psr7": "^1.2",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "0.12.82",
"phpstan/phpstan-phpunit": "0.12.18",
"phpstan/phpstan-strict-rules": "0.12.9",
"phpunit/phpunit": "^7.2 || ^8.5",
"psr/http-message": "^1.0",
"react/promise": "2.*",
"simpod/php-coveralls-mirror": "^3.0",
"squizlabs/php_codesniffer": "3.5.4"
},
"suggest": {
"psr/http-message": "To use standard GraphQL server",
"react/promise": "To leverage async resolving on React PHP platform"
},
"type": "library",
"autoload": {
"psr-4": {
"GraphQL\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A PHP port of GraphQL reference implementation",
"homepage": "https://github.com/webonyx/graphql-php",
"keywords": [
"api",
"graphql"
],
"support": {
"issues": "https://github.com/webonyx/graphql-php/issues",
"source": "https://github.com/webonyx/graphql-php/tree/v14.11.5"
},
"funding": [
{
"url": "https://opencollective.com/webonyx-graphql-php",
"type": "open_collective"
}
],
"time": "2022-01-24T11:13:31+00:00"
}
],
"packages-dev": [],
@ -679,5 +745,5 @@
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.1.0"
"plugin-api-version": "2.0.0"
}

View file

@ -0,0 +1,67 @@
class notFoundButtons extends HTMLElement {
constructor() {
super();
this.windowResized = false;
const redButton = document.createElement("div");
redButton.classList.add("button");
redButton.classList.add("red");
this.appendChild(redButton);
const yellowButton = document.createElement("div");
yellowButton.classList.add("button");
yellowButton.classList.add("yellow");
this.appendChild(yellowButton);
const greenButton = document.createElement("div");
greenButton.classList.add("button");
greenButton.classList.add("green");
this.appendChild(greenButton);
greenButton.onclick
greenButton.onclick = () => {
const terminal = document.querySelector(".terminal-window");
if (!this.windowResized) {
terminal.style.width = "95%";
terminal.style.height = "95%";
terminal.style.top = "2%";
this.windowResized = true;
} else {
terminal.style.width = "37.5rem";
terminal.style.height = "22.5rem";
terminal.style.top = "10.5rem";
this.windowResized = false;
}
}
redButton.onclick = () => {
location.href = "https://jonasled.de";
}
}
getWidth() {
return Math.max(
document.body.scrollWidth,
document.documentElement.scrollWidth,
document.body.offsetWidth,
document.documentElement.offsetWidth,
document.documentElement.clientWidth
);
}
getHeight() {
return Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight,
document.documentElement.clientHeight
);
}
}
customElements.define("jl-404_buttons", notFoundButtons);

View file

@ -0,0 +1,32 @@
class blogFooter extends HTMLElement {
connectedCallback(){
this.getBlogEntries();
}
async getBlogEntries() {
let ul = document.createElement("ul");
this.appendChild(ul);
var graphql = JSON.stringify({
query: 'query($count: Int!) { blogPosts(count: $count) { title id }}',
variables: {
"count": 5
}
})
var requestOptions = {
method: 'POST',
body: graphql,
headers: { 'Content-Type': 'application/json' }
};
let posts = (await (await fetch("/API/graphql.php", requestOptions)).json()).data.blogPosts;
posts.forEach((element) => {
let li = document.createElement("li");
let a = document.createElement("a");
a.href = "/post.html?id=" + element["id"];
a.innerText = element["title"];
li.appendChild(a);
ul.appendChild(li);
});
}
}
customElements.define("jl-footer_blog", blogFooter);

View file

@ -0,0 +1,50 @@
class BlogIndex extends HTMLElement {
constructor() {
super();
this.getBlogPosts();
}
async getBlogPosts() {
var graphql = JSON.stringify({
query: 'query($count: Int! $contentLength: Int!) { blogPosts(count: $count contentLength: $contentLength) { content title id }}',
variables: {
"count": 3,
"contentLength": 300
}
})
var requestOptions = {
method: 'POST',
body: graphql,
headers: { 'Content-Type': 'application/json' }
};
let posts = (await (await fetch("/API/graphql.php", requestOptions)).json()).data.blogPosts;
posts.forEach((element) => {
const article = document.createElement("article");
article.classList.add("breakWord");
this.appendChild(article);
const h2 = document.createElement("h2");
h2.innerText = element["title"];
article.appendChild(h2);
const content = document.createElement("p");
content.classList.add("breakWord");
content.innerHTML = element["content"];
article.appendChild(content);
const moreP = document.createElement("p");
moreP.classList.add("center");
article.appendChild(moreP);
const moreLink = document.createElement("a");
moreLink.href = "/post.html?id=" + element["id"];
moreP.appendChild(moreLink);
const moreButton = document.createElement("button");
moreButton.innerText = "Mehr lesen";
moreLink.appendChild(moreButton);
});
}
}
customElements.define("jl-blog_index", BlogIndex);

View file

@ -0,0 +1,48 @@
class commentsDisplay extends HTMLElement {
constructor() {
super();
this.getComments()
}
async getComments() {
var graphql = JSON.stringify({
query: 'query($article: String!) { comments(article: $article) { name comment gravatarURL }}',
variables: {
"article": window.location.pathname
}
})
var requestOptions = {
method: 'POST',
body: graphql,
headers: { 'Content-Type': 'application/json' }
};
let comments = (await (await fetch("/API/graphql.php", requestOptions)).json()).data.comments;
this.innerHTML = "";
comments.forEach((element) => {
const h3 = document.createElement("h3");
h3.classList.add("commentTitle");
h3.innerText = element["name"];
this.appendChild(h3);
const commentDiv = document.createElement("div");
commentDiv.classList.add("comment");
this.appendChild(commentDiv);
const image = document.createElement("img");
image.src = element["gravatarURL"];
commentDiv.appendChild(image);
const article = document.createElement("article");
article.classList.add("commentArticle");
commentDiv.appendChild(article);
const commentText = document.createElement("p");
commentText.classList.add("commentText");
commentText.innerText = element["comment"];
article.appendChild(commentText);
});
}
}
customElements.define("jl-comments_display", commentsDisplay);

View file

@ -6,7 +6,16 @@ class contactMailButton extends HTMLElement {
}
async addButton() {
let sitekey = await (await fetch("/API/config.php?name=sitekey")).text();
var graphql = JSON.stringify({
query: "query {sitekey}"
})
var requestOptions = {
method: 'POST',
body: graphql,
headers: { 'Content-Type': 'application/json' }
};
let sitekey = (await (await fetch("/API/graphql.php", requestOptions)).json()).data.sitekey;
console.log(sitekey);
this.innerHTML = `E-Mail: <button id="emailButton" class="h-captcha" data-sitekey="${sitekey}" data-callback="onSubmit">laden</button><br>`;
const script = document.createElement("script");

View file

@ -0,0 +1,26 @@
class ebkBanner extends HTMLElement {
constructor(){
super();
this.generateBanner();
}
async generateBanner() {
var graphql = JSON.stringify({
query: 'query { ebayKleinanzeigen{ count }}',
})
var requestOptions = {
method: 'POST',
body: graphql,
headers: { 'Content-Type': 'application/json' }
};
let elementCount = (await (await fetch("/API/graphql.php", requestOptions)).json()).data.ebayKleinanzeigen.count;
if(elementCount > 0) {
const h2 = document.createElement("h2");
h2.classList.add("red");
h2.innerHTML = "Ich biete aktuell verschiedene Artikel zum verkauf an, eine genaue Übersicht ist <a class=\"red\" href=\"/selling.html\">hier</a> zu sehen."
this.appendChild(h2);
}
}
}
customElements.define("jl-ebk-banner", ebkBanner);

View file

@ -0,0 +1,96 @@
class Footer extends HTMLElement {
constructor() {
super();
this.socialButtons = [
{
"link": "//www.thingiverse.com/jonasled/designs/",
"icon": "3d_model",
},
{
"link": "//paypal.me/jonasled",
"icon": "paypal",
},
{
"link": "//matrix.to/#/@jonasled:jonasled.de",
"icon": "matrix"
},
{
"link": "//twitter.com/jonasled1",
"icon": "twitter"
}
];
this.links = [
{
"name": "Datenschutzerklärung",
"link": "/datenschutzerklaerung.html"
},
{
"name": "Bildquellen",
"link": "/bildquellen.html"
},
{
"name": "Impressum",
"link": "/impressum.html"
},
{
"name": "Quellcode",
"link": "//gitlab.jonasled.de/jonasled/website"
}
]
const footer = document.createElement("footer");
this.appendChild(footer);
const blueBar = document.createElement("div");
blueBar.id = "blueBar";
footer.appendChild(blueBar);
const footerContent = document.createElement("div");
footerContent.id = "footerContent";
footer.appendChild(footerContent);
const footerLinks = document.createElement("div");
footerContent.appendChild(footerLinks);
this.links.forEach(link => {
const linkElement = document.createElement("a");
linkElement.href = link["link"];
linkElement.innerText = link["name"];
footerLinks.appendChild(linkElement);
const linebreak = document.createElement("br");
footerLinks.appendChild(linebreak);
});
const footerPostDiv = document.createElement("div");
footerPostDiv.id = "newestPost";
footerContent.appendChild(footerPostDiv);
const postHeadline = document.createElement("h3");
postHeadline.innerText = "Neueste Beiträge";
footerPostDiv.appendChild(postHeadline);
const footerPost = document.createElement("jl-footer_blog");
footerPostDiv.appendChild(footerPost);
const footerIconDiv = document.createElement("div");
footerIconDiv.className = "center";
footerContent.appendChild(footerIconDiv);
const socialButtonsContainer = document.createElement("p");
socialButtonsContainer.className = "center";
footerIconDiv.appendChild(socialButtonsContainer);
this.socialButtons.forEach(button => {
const link = document.createElement("a");
link.href = button["link"];
socialButtonsContainer.appendChild(link)
const icon = document.createElement("jl-svg");
icon.setAttribute("data-name", button["icon"]);
link.appendChild(icon);
});
}
}
customElements.define("jl-footer", Footer);

View file

@ -0,0 +1,44 @@
class Header extends HTMLElement {
constructor() {
super();
let pageTitle = this.getAttribute("data-title");
const header = document.createElement("header");
this.appendChild(header);
const headerWrapper = document.createElement("div");
headerWrapper.classList.add("header-wrapper");
header.appendChild(headerWrapper);
const headerHomepage = document.createElement("div");
headerHomepage.classList.add("header-homepage");
headerWrapper.appendChild(headerHomepage);
const alignHolder = document.createElement("div");
alignHolder.classList.add("align-holder");
headerHomepage.appendChild(alignHolder);
const h1 = document.createElement("h1");
h1.innerText = pageTitle;
h1.onclick = () => {
document.getElementById("content").scrollIntoView();
}
alignHolder.appendChild(h1);
const headerSeparator = document.createElement("div");
headerSeparator.classList.add("header-separator");
headerSeparator.classList.add("header-separator-bottom");
headerWrapper.appendChild(headerSeparator);
const svg = document.createElement("jl-svg");
svg.setAttribute("data-name", "banner");
headerSeparator.appendChild(svg);
const mainMenu = document.createElement("jl-main_menu");
mainMenu.setAttribute("data-title", pageTitle);
mainMenu.id = "mainmenu";
header.appendChild(mainMenu);
}
}
customElements.define("jl-header", Header);

View file

@ -0,0 +1,48 @@
import * as basicLightbox from 'basiclightbox'
class CustomImage extends HTMLElement {
async connectedCallback(){
const originalURL = new URL(this.getAttribute("src"), document.baseURI).href;
var graphql = JSON.stringify({
query: "query($url: String!) {imgproxy(url: $url)}",
variables: {
"url": originalURL
}
})
var requestOptions = {
method: 'POST',
body: graphql,
headers: { 'Content-Type': 'application/json' }
};
let imgproxy = (await (await fetch("/API/graphql.php", requestOptions)).json()).data.imgproxy;
let image = document.createElement("img");
image.src = imgproxy;
if(this.getAttribute("alt") != null) image.setAttribute("alt", this.getAttribute("alt"));
if(this.getAttribute("title") != null) image.setAttribute("title", this.getAttribute("title"));
if(this.getAttribute("class") != null) image.setAttribute("class", this.getAttribute("class"));
if(this.getAttribute("style") != null) image.setAttribute("style", this.getAttribute("style"));
if(this.getAttribute("width") != null) image.setAttribute("width", this.getAttribute("width"));
if(this.getAttribute("height") != null) image.setAttribute("height", this.getAttribute("height"));
if(this.getAttribute("id") != null) image.setAttribute("id", this.getAttribute("id"));
image.setAttribute("loading", "lazy");
image.setAttribute("original-src", originalURL);
this.appendChild(image);
this.setAttribute("id", "");
if(!(this.getAttribute("data-noPreview") === "true")) {
image.onclick = () => {
const instance = basicLightbox.create(`
<img src="${originalURL}">
`);
instance.show();
}
}
}
}
customElements.define("jl-img", CustomImage)

View file

@ -0,0 +1,15 @@
class InlineCode extends HTMLElement {
constructor() {
super();
const codeElement = document.createElement("code");
if (this.hasAttribute("language")) {
codeElement.classList.add(this.getAttribute("language"));
} else {
codeElement.classList.add("language-text");
}
codeElement.innerHTML = this.innerHTML;
this.appendChild(codeElement);
}
}
customElements.define("jl-code", InlineCode);

View file

@ -10,10 +10,10 @@ class MainMenu extends HTMLElement {
menuContainer.className = "mainmenuContainer";
let burgerMenu = document.createElement("div");
burgerMenu.id = "burgerMenu";
burgerMenu.id = "burgermenu";
burgerMenu.onclick = () => {
document.getElementById("burgerMenu").classList.toggle("change");
document.getElementById("burgermenu").classList.toggle("change");
document.querySelector(".mainmenuContainer").classList.toggle("visible");
}

View file

@ -0,0 +1,140 @@
class newComment extends HTMLElement {
connectedCallback() {
const buttonElement = document.createElement("button");
buttonElement.classList.add("bigButton");
buttonElement.id = "showCommentButton";
buttonElement.innerText = "Neues Kommentar verfassen";
this.appendChild(buttonElement);
buttonElement.onclick = this.setupForm;
}
async setupForm() {
var graphql = JSON.stringify({
query: "query {sitekey}"
})
var requestOptions = {
method: 'POST',
body: graphql,
headers: { 'Content-Type': 'application/json' }
};
let sitekey = (await (await fetch("/API/graphql.php", requestOptions)).json()).data.sitekey;
let script = document.createElement('script');
script.src = "https://hCaptcha.com/1/api.js";
script.type = 'text/javascript';
script.onload = () => {
let pageName = window.location.pathname
const parent = this.parentElement;
parent.innerHTML = "";
const form = document.createElement("form");
parent.appendChild(form);
const labelName = document.createElement("label")
labelName.setAttribute("for", "name");
labelName.innerText = "Name:";
form.appendChild(labelName);
const nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.name = "name";
nameInput.id = "name";
form.appendChild(nameInput);
let linebreak = document.createElement("br");
form.appendChild(linebreak);
const labelMail = document.createElement("label")
labelMail.setAttribute("for", "email");
labelMail.innerText = "E-Mail: (wird nicht veröffentlicht)";
form.appendChild(labelMail);
const mailInput = document.createElement("input");
mailInput.type = "email";
mailInput.name = "email";
mailInput.id = "email";
form.appendChild(mailInput);
linebreak = document.createElement("br");
form.appendChild(linebreak);
const labelComment = document.createElement("label")
labelComment.setAttribute("for", "comment");
labelComment.innerText = "Kommentar:";
form.appendChild(labelComment);
const commentInput = document.createElement("textarea");
commentInput.name = "comment";
commentInput.id = "comment";
form.appendChild(commentInput);
linebreak = document.createElement("br");
form.appendChild(linebreak);
const hcaptcha = document.createElement("div");
hcaptcha.classList.add("h-captcha");
hcaptcha.setAttribute("data-theme", "dark");
hcaptcha.setAttribute("data-sitekey", sitekey);
form.appendChild(hcaptcha);
linebreak = document.createElement("br");
form.appendChild(linebreak);
const submitButton = document.createElement("input");
submitButton.value = "Kommentar veröffentlichen";
submitButton.type = "submit";
form.appendChild(submitButton);
const labelDatenschutz = document.createElement("p");
labelDatenschutz.innerText = "Mit dem Klick auf den obigen Button erklären sie sich mit der ";
form.appendChild(labelDatenschutz);
const datenschutzLink = document.createElement("a");
datenschutzLink.innerText = "Datenschutzerklärung";
datenschutzLink.href = "/datenschutzerklaerung.html";
labelDatenschutz.appendChild(datenschutzLink);
const datenschutzTextNode = document.createTextNode(" einverstanden");
labelDatenschutz.appendChild(datenschutzTextNode);
submitButton.onclick = async () => {
if (nameInput.value == "" || commentInput.value == "") {
alert("Name oder Kommentar nicht ausgefüllt.");
return;
}
var graphql = JSON.stringify({
query: 'mutation($article: String!, $name: String!, $hCaptchaResponse: String!, $email: String!, $comment: String!) { comment(article: $article, name: $name, email: $email, comment: $comment, hCaptchaResponse: $hCaptchaResponse)}',
variables: {
"article": pageName,
"name": nameInput.value,
"email": mailInput.value,
"comment": commentInput.value,
"hCaptchaResponse": form.querySelector(".h-captcha iframe").getAttribute("data-hcaptcha-response")
}
})
var requestOptions = {
method: 'POST',
body: graphql,
headers: { 'Content-Type': 'application/json' }
};
let data = (await (await fetch("/API/graphql.php", requestOptions)).json()).data;
if (data.comment == "OK") {
document.querySelector("jl-comments_display").getComments();
parent.innerHTML = "<jl-new_comment></jl-new_comment>"
} else {
alert("Fehler: " + data.newComment);
}
}
form.onsubmit = () => {
return false;
}
}
document.body.append(script);
}
}
customElements.define("jl-new_comment", newComment);

View file

@ -6,22 +6,37 @@ class NtpGraph extends HTMLElement {
let ip = this.getAttribute("data-server-ip");
this.innerHTML = `
<span class="ntpBanner">${ip}</span>
<span class="ntpContent">
<a target="_blank" href="https://www.ntppool.org/scores/${ip}" class="linkToNtpPool">Server auf dem NTP Pool anzeigen</a>
<canvas class="graphDelay"></canvas>
<canvas class="graphScore"></canvas>
</span>
`;
const ntpBanner = document.createElement("span");
ntpBanner.classList.add("ntpBanner");
ntpBanner.innerText = ip;
this.appendChild(ntpBanner);
this.querySelector(".ntpBanner").onclick = () => {
let contentElement = this.querySelector(".ntpContent");
const ntpContent = document.createElement("span");
ntpContent.classList.add("ntpContent");
this.appendChild(ntpContent);
if (contentElement.classList.contains("visible")) {
contentElement.classList.remove("visible");
const ntpLink = document.createElement("a");
ntpLink.target = "_blank";
ntpLink.href = `https://www.ntppool.org/scores/${ip}`;
ntpLink.classList.add("linkToNtpPool");
ntpLink.innerText = "Server auf der Seite des NTP Pools anzeigen";
ntpContent.appendChild(ntpLink);
const ntpDelayCanvas = document.createElement("canvas");
ntpDelayCanvas.classList.add("graphDelay");
ntpContent.appendChild(ntpDelayCanvas);
const ntpScoreCanvas = document.createElement("canvas");
ntpScoreCanvas.classList.add("graphScore");
ntpContent.appendChild(ntpScoreCanvas);
ntpBanner.onclick = () => {
if (ntpContent.classList.contains("visible")) {
ntpContent.classList.remove("visible");
} else {
contentElement.classList.add("visible");
ntpContent.classList.add("visible");
}
let xhr = new XMLHttpRequest();

120
js/customElements/pwgen.js Normal file
View file

@ -0,0 +1,120 @@
class PasswordGenerator extends HTMLElement {
constructor() {
super();
const outValue = document.createElement("input");
outValue.type = "text";
this.appendChild(outValue);
const lineBreak = document.createElement("br");
this.appendChild(lineBreak);
const pwlen = document.createElement("input");
pwlen.type = "range";
pwlen.min = "8";
pwlen.max = "128";
pwlen.value = "32";
this.appendChild(pwlen);
const pwlenSpan = document.createElement("span");
pwlenSpan.innerText = "32";
this.appendChild(pwlenSpan);
const lineBreak2 = document.createElement("br");
this.appendChild(lineBreak2);
const includeNum = document.createElement("input");
includeNum.type = "checkbox";
includeNum.checked = true;
this.appendChild(includeNum);
const includeNumText = document.createTextNode(" Zahlen");
this.appendChild(includeNumText);
const lineBreak3 = document.createElement("br");
this.appendChild(lineBreak3);
const includeBigChar = document.createElement("input");
includeBigChar.type = "checkbox";
includeBigChar.checked = true;
this.appendChild(includeBigChar);
const bigCharText = document.createTextNode(" Großbuchstaben");
this.appendChild(bigCharText);
const lineBreak4 = document.createElement("br");
this.appendChild(lineBreak4);
const includeSmallChar = document.createElement("input");
includeSmallChar.type = "checkbox";
includeSmallChar.checked = true;
this.appendChild(includeSmallChar);
const includeSmallCharText = document.createTextNode(" Kleinbuchstaben");
this.appendChild(includeSmallCharText);
const lineBreak5 = document.createElement("br");
this.appendChild(lineBreak5);
const includeSpecialChar = document.createElement("input");
includeSpecialChar.type = "checkbox";
includeSpecialChar.checked = true;
this.appendChild(includeSpecialChar);
const inclideSpecialCharText = document.createTextNode(" Sonderzeichen");
this.appendChild(inclideSpecialCharText);
const lineBreak6 = document.createElement("br");
this.appendChild(lineBreak6);
const button = document.createElement("button");
button.innerText = "Generieren";
this.appendChild(button);
pwlen.oninput = () => {
pwlenSpan.innerText = pwlen.value;
button.click();
}
includeNum.onchange = () => {
button.click();
}
includeBigChar.onchange = () => {
button.click();
}
includeSmallChar.onchange = () => {
button.click();
}
includeSpecialChar.onchange = () => {
button.click();
}
button.onclick = () => {
let possibleChar = "";
let password = "";
if(includeNum.checked) {
possibleChar += "1234567890";
}
if(includeBigChar.checked) {
possibleChar += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
}
if(includeSmallChar.checked) {
possibleChar += "abcdefghijklmnopqrstuvwxyz";
}
if(includeSpecialChar.checked) {
possibleChar += ",;.:-_#'+*?=)(/&%$§\"!°{[]}<>|~"
}
for(let i = 0; i < pwlen.value; i++) {
password += possibleChar[Math.floor(Math.random() * possibleChar.length)];
}
outValue.value = password;
}
button.click();
}
}
customElements.define("jl-pwgen", PasswordGenerator)

View file

@ -0,0 +1,106 @@
import * as basicLightbox from 'basiclightbox'
class sellingTable extends HTMLElement {
constructor() {
super();
this.config = [
{
"title": "Bild",
"fieldName": "preview",
"displayType": "image",
"fullImage": "image",
"index": 0
},
{
"title": "Titel",
"fieldName": "title",
"displayType": "text"
},
{
"title": "Preis",
"fieldName": "price",
"displayType": "text"
},
{
"title": "Versand",
"fieldName": "shipping",
"displayType": "text"
},
{
"title": "Link",
"fieldName": "link",
"displayType": "link",
"linkText": "Anzeige ansehen",
"target": "_blank"
},
];
this.generateTable();
}
async generateTable() {
const table = document.createElement("table");
this.appendChild(table);
const tr = document.createElement("tr");
table.appendChild(tr);
this.config.forEach(element => {
const th = document.createElement("th");
th.innerText = element["title"];
tr.appendChild(th);
});
var graphql = JSON.stringify({
query: 'query { ebayKleinanzeigen(imageCount: 1) { elements { images { preview image } title price shipping link }}}',
})
var requestOptions = {
method: 'POST',
body: graphql,
headers: { 'Content-Type': 'application/json' }
};
let elements = (await (await fetch("/API/graphql.php", requestOptions)).json()).data.ebayKleinanzeigen.elements;
elements.forEach(ad => {
const tr = document.createElement("tr");
table.appendChild(tr);
this.config.forEach(element => {
const th = document.createElement("th");
switch (element["displayType"]) {
case "text":
th.innerText = ad[element["fieldName"]];
break;
case "link":
const link = document.createElement("a");
th.appendChild(link);
link.href = ad[element["fieldName"]];
link.innerText = element["linkText"];
if ("target" in element) {
link.target = element["target"];
}
break;
case "image":
const img = document.createElement("img");
th.appendChild(img);
img.src = ad["images"][element["index"]][element["fieldName"]];
img.onclick = () => {
const instance = basicLightbox.create(`
<img src="${ad["images"][element["index"]][element["fullImage"]]}">
`);
instance.show();
}
break;
}
tr.appendChild(th);
});
});
}
}
customElements.define("jl-selling-table", sellingTable);

View file

@ -0,0 +1,28 @@
class Skill extends HTMLElement {
constructor() {
super();
this.getSkills();
}
async getSkills(){
var graphql = JSON.stringify({
query: "query {skills}"
})
var requestOptions = {
method: 'POST',
body: graphql,
headers: { 'Content-Type': 'application/json' }
};
let skills = (await (await fetch("/API/graphql.php", requestOptions)).json()).data.skills;
skills.forEach(skill => {
const image = document.createElement("img");
image.classList.add("skills");
image.src = skill;
this.appendChild(image);
});
}
}
customElements.define("jl-skills", Skill);

View file

@ -1,6 +1,5 @@
class svgLoad extends HTMLElement {
constructor(){
super();
connectedCallback() {
let svgName = this.getAttribute("data-name");
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
@ -9,7 +8,7 @@ class svgLoad extends HTMLElement {
}
}
xhr.open("GET", "../assets/svg/" + svgName + ".svg");
xhr.open("GET", "/svg/" + svgName + ".svg");
xhr.send();
}
}

View file

@ -1,7 +1,4 @@
require("./browserCheck");
require("./error");
require("./imgPreview");
require("./viewPost");
require("./externalLinkHandler");
require("./prism");
@ -19,4 +16,7 @@ require("./customElements/footer");
require("./customElements/ebkBanner");
require("./customElements/sellingTable");
require("./customElements/skills");
require("./customElements/pwgen");
require("./customElements/pwgen");
require("./customElements/inline-code");
require("./customElements/404Buttons");
require("./customElements/image");

45
js/viewPost.js Normal file
View file

@ -0,0 +1,45 @@
if(window.location['pathname'] == "/post.html"){
loadPost();
}
// return the value of the get parameter with the given name
function getParameter(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
var results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
async function loadPost() {
let id = getParameter("id");
let header = document.createElement("jl-header");
let footer = document.createElement("jl-footer");
let content = document.createElement("div");
if(id == null) {
content.innerHTML = "<h1>404 - Post not found</h1>";
} else {
var graphql = JSON.stringify({
query: 'query($postID: String!) {blogPost(id: $postID) {content title}}',
variables: {
"postID": id
}
})
var requestOptions = {
method: 'POST',
body: graphql,
headers: { 'Content-Type': 'application/json' }
};
let post = (await (await fetch("/API/graphql.php", requestOptions)).json()).data.blogPost;
content.innerHTML = post["content"];
document.title = post["title"] + " - Jonas Leder";
header.setAttribute("data-title", post["title"]);
}
content.id = "content";
document.body.appendChild(header);
document.body.appendChild(content);
document.body.appendChild(footer);
}

View file

@ -6,15 +6,19 @@
"author": "jonasled <git@jonasled.de>",
"license": "GPL-3.0-or-later",
"scripts": {
"watch": "parcel serve src/*.html src/*/*.html",
"build": "parcel build src/*.html src/*/*.html"
"build": "concurrently \"yarn css\" \"yarn js\"",
"css": "stylus styl/ -o public/css/ ",
"js": "webpack --config ./webpack.conf.js",
"watch": "concurrently \"stylus -w styl/ -o public/css/\" \"cd public && php -S 0.0.0.0:1234\" \"webpack --config ./webpack.conf.js --mode development --watch\""
},
"devDependencies": {
"@parcel/transformer-sass": "^2.2.1"
"concurrently": "^6.0.0",
"webpack": "^5.28.0",
"webpack-cli": "^4.5.0",
"stylus": "^0.56.0"
},
"dependencies": {
"basiclightbox": "^5.0.4",
"chart.js": "^2.9.4",
"parcel": "^2.2.1"
"chart.js": "^2.9.4"
}
}

View file

@ -3,16 +3,12 @@
<head>
<meta charset="UTF-8">
<title>404 - Page not found</title>
<link href="scss/error.scss" rel="stylesheet">
<link href="/css/error.css" rel="stylesheet">
</head>
<body>
<jl-matomo></jl-matomo>
<div id="particles-js"></div>
<body style="height: 100vh;">
<div class="terminal-window">
<header>
<div class="button green"></div>
<div class="button yellow"></div>
<div class="button red"></div>
<jl-404_buttons></jl-404_buttons>
</header>
<section class="terminal">
<div class="history"></div>
@ -43,6 +39,6 @@
];
</script>
<script src="js/script.js" type="module"></script>
<script src="/js/script.js"></script>
</body>
</html>

22
public/API/ebayimg.php Normal file
View file

@ -0,0 +1,22 @@
<?php
if (!array_key_exists("url", $_GET) || $_GET["url"] == "") {
die("URL not set or empty");
}
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, "https://i.ebayimg.com/" . $_GET["url"]);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 20);
curl_setopt($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
$content = curl_exec($curl);
$httpcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($httpcode == 200) {
$contentType = curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
header('Content-Type: ' . $contentType);
echo ($content);
} else {
die("Failed to fetch image, server responded with " . $httpcode);
}

29
public/API/graphql.php Normal file
View file

@ -0,0 +1,29 @@
<?php
use GraphQL\Server\StandardServer;
use GraphQL\Type\Schema;
use GraphQL\Error\DebugFlag;
require 'vendor/autoload.php';
require "./lib/config.php";
require "./lib/mysql.php";
require "./queries/queries.php";
$schema = new Schema([
'query' => $queryType,
'mutation' => $mutationType,
]);
try {
$serverConfig = [
'schema' => $schema,
'rootValue' => [
'db' =>$conn,
],
'debugFlag' => DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE,
];
$server = new StandardServer($serverConfig);
$server->handleRequest();
} catch (Throwable $error) {
StandardServer::send500Error($error);
}

View file

@ -19,4 +19,10 @@ $S3SecretKey = "";
$S3BucketName = "";
$ebayKleinanzeigenUserId = "";
$ebayKleinanzeigenToken = ""; # To get this token you have to sniff the HTTPS traffic from the app or decompile the app and find it.
$ebayKleinanzeigenToken = ""; # To get this token you have to sniff the HTTPS traffic from the app or decompile the app and find it.
$imgProxyUrl = "";
$imgProxyKey = "";
$imgProxySalt = "";
$defaultImageWidth = 512; # width is in px
$defaultSkillsWidth = 80; # width is in px

View file

@ -97,6 +97,16 @@
"name": "Mailcow E-Mail Server",
"url": "/anleitungen/mailcow.html",
"type": "link"
},
{
"name": "Traefik 2 Reverse Proxy",
"url": "/anleitungen/traefik.html",
"type": "link"
},
{
"name": "Vaultwarden Passwort Manager",
"url": "/anleitungen/vaultwarden.html",
"type": "link"
}
]
},
@ -114,11 +124,6 @@
"url": "/systeme/laptop.html",
"type": "link"
},
{
"name": "NAS",
"url": "/systeme/nas.html",
"type": "link"
},
{
"name": "Epyc Server",
"url": "/systeme/epycServer.html",
@ -128,11 +133,6 @@
"name": "i7 Server",
"url": "/systeme/i7Server.html",
"type": "link"
},
{
"name": "Backup Server",
"url": "/systeme/backupServer.html",
"type": "link"
}
]
},
@ -179,6 +179,11 @@
"name": "Passwort Generator",
"url": "/passwordgen.html",
"type": "link"
},
{
"name": "Speedtest Server",
"url": "//jonasled.speedtestcustom.com",
"type": "link"
}
]
},
@ -191,11 +196,6 @@
"url": "//status.jonasled.de",
"type": "link"
},
{
"name": "Discord Bots",
"url": "//discordstatus.jonasled.de",
"type": "link"
},
{
"name": "NTP Server",
"url": "/ntpstatus.html",

View file

@ -0,0 +1,66 @@
<?php
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ObjectType;
$blogPostFields = new ObjectType([
"name" => "Blog",
"fields" => [
"title" => Type::string(),
"content" => Type::string(),
"date" => Type::string(),
"id" => Type::string()
],
]);
function blogPost($id, $conn)
{
$id = $conn->real_escape_string($id);
$result = $conn->query("SELECT * FROM posts WHERE id=$id");
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
} else {
return [
"title" => "Nicht Gefunden",
"content" => "Post wurde nicht gefunden",
"date" => "2000-01-01 00:00:00",
"id" => "-1"
];
}
return [
"title" => $row["title"],
"content" => $row["content"],
"date" => $row["date"],
"id" => $row["id"],
];
}
function blogPosts($count, $contentLength, $conn)
{
$response = [];
$count = $conn->real_escape_string($count);
$result = $conn->query("SELECT * FROM posts order by id desc limit $count");
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$content = $row["content"];
if($contentLength != null && strlen($content) > $contentLength) {
$contentNew = substr($content, 0, $contentLength);
$contentRest = substr($content, $contentLength);
$content = $contentNew . explode(" ", $contentRest)[0] . " ...";
}
$blogElement = [
"title" => $row["title"],
"content" => $content,
"date" => $row["date"],
"id" => $row["id"],
];
array_push($response, $blogElement);
}
}
return $response;
}

View file

@ -0,0 +1,70 @@
<?php
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ObjectType;
use GuzzleHttp\Client;
include "lib/getGravatar.php";
$commentField = new ObjectType([
"name" => "Comment",
"fields" => [
"name" => Type::string(),
"comment" => Type::string(),
"gravatarURL" => Type::string(),
"id" => Type::int()
],
]);
function comments($article, $conn)
{
$response = [];
$article = $conn->real_escape_string($article);
$result = $conn->query("SELECT * FROM comments WHERE article='$article'");
while ($row = $result->fetch_assoc()) {
$commentElement = [
"name" => $row["name"],
"comment" => $row["comment"],
"gravatarURL" => get_gravatar($row["email"]),
"id" => $row["id"]
];
array_push($response, $commentElement);
}
return $response;
}
function newComment($conn, $article, $name, $email, $comment, $hCaptchaResponse)
{
require "./lib/config.php";
$data = array(
'secret' => $secretkey,
'response' => $hCaptchaResponse
);
$client = new Client();
$response = $client->post("https://hcaptcha.com/siteverify", [
"form_params" => $data
]);
$responseData = json_decode($response->getBody());
if (!$responseData->success) {
return "Failed to verify Captcha";
}
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return "Invalid email address.";
}
$article = $conn->escape_string($article);
$name = $conn->escape_string($name);
$email = $conn->escape_string($email);
$comment = $conn->escape_string($comment);
$sql = "INSERT INTO comments (name, email, comment, article) VALUES ('$name', '$email', '$comment', '$article')";
if ($conn->query($sql) === TRUE) {
return "OK";
} else {
return "Error: " . $sql . "<br>" . $conn->error;
}
}

View file

@ -0,0 +1,103 @@
<?php
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ObjectType;
use GuzzleHttp\Client;
$ebayKleinanzeigenImages = new ObjectType([
"name" => "EBK Image",
"fields" => [
"preview" => Type::string(),
"image" => Type::string()
]
]);
$ebayKleinanzeigenElements = new ObjectType([
"name" => "EBK Elements",
"fields" => [
"title" => Type::string(),
"price" => Type::string(),
"shipping" => Type::string(),
"link" => Type::string(),
"images" => [
"type" => Type::listOf($ebayKleinanzeigenImages),
"args" => [
"count" => Type::int()
]
],
"id" => Type::string()
]
]);
$ebayKleinanzeigenFields = new ObjectType([
"name" => "Ebay Kleinanzeigen",
"fields" => [
"count" => Type::int(),
"elements" => Type::listOf($ebayKleinanzeigenElements)
],
]);
function ebayKleinanzeigen($imageCount) {
require "./lib/config.php";
$elements = [];
$client = new Client();
$headers = [
'authorization' => 'Basic ' . $ebayKleinanzeigenToken,
'user-agent' => 'okhttp/4.9.1',
'x-ebayk-app' => '4e10d7fd-6fef-4f87-afb0-b8ede2f494071636475109828',
'Host' => 'api.ebay-kleinanzeigen.de',
'Accept' => '*/*',
'Accept-Encoding' => 'gzip, deflate, br'
];
$response = $client->request('GET', "https://api.ebay-kleinanzeigen.de/api/ads.json?_in=title,price,pictures,link,features-active,search-distance,negotiation-enabled,attributes,medias,medias.media,medias.media.title,medias.media.media-link,store-id,store-title&page=0&size=31&userIds=$ebayKleinanzeigenUserId&pictureRequired=false&includeTopAds=false&limitTotalResultCount=true", [
'headers' => $headers ]);
$response = json_decode($response->getBody(), true);
$ads = $response["{http://www.ebayclassifiedsgroup.com/schema/ad/v1}ads"]["value"]["ad"];
foreach($ads as $ad) {
$element = [
"title" => html_entity_decode($ad["title"]["value"]),
"id" => $ad["id"],
"price" => $ad["price"]["amount"]["value"] . "",
"shipping" => "nein"
];
foreach($ad["attributes"]["attribute"] as $attribute) {
if(str_contains($attribute["name"], "versand")) {
$element["shipping"] = $attribute["value"][0]["value"];
}
}
foreach($ad["link"] as $link) {
if($link["rel"] == "self-public-website") {
$element["link"] = $link["href"];
}
}
$images = [];
foreach(array_slice($ad["pictures"]["picture"], 0, $imageCount) as $picture) {
$image = [];
foreach($picture["link"] as $pictureSize) {
if($pictureSize["rel"] == "teaser") {
$image["preview"] = str_replace("https://i.ebayimg.com", "/API/ebayimg.php?url=", $pictureSize["href"]);
}
if($pictureSize["rel"] == "XXL") {
$image["image"] = str_replace("https://i.ebayimg.com", "/API/ebayimg.php?url=", $pictureSize["href"]);
}
}
array_push($images, $image);
}
$element["images"] = $images;
array_push($elements, $element);
}
return [
"count" => sizeof($elements),
"elements" => $elements
];
}

View file

@ -0,0 +1,10 @@
<?php
function imgproxy($imageURL) {
require "./lib/config.php";
$encodedUrl = rtrim(strtr(base64_encode($imageURL), '+/', '-_'), '=');
$path = "/rs:fit:0:$defaultImageWidth:1/g:no/{$encodedUrl}.webp";
$signature = rtrim(strtr(base64_encode(hash_hmac('sha256', $imgProxySalt.$path, $imgProxyKey, true)), '+/', '-_'), '=');
return $imgProxyUrl . "/" . $signature . $path;
}

View file

@ -0,0 +1,22 @@
<?php
use GuzzleHttp\Client;
function mailAddress($hCaptchaResponse) {
require "./lib/config.php";
$data = array(
'secret' => $secretkey,
'response' => $hCaptchaResponse
);
$client = new Client();
$response = $client->post("https://hcaptcha.com/siteverify", [
"form_params" => $data
]);
$responseData = json_decode($response->getBody());
if($responseData->success) {
return "$contactmail";
} else {
return "Fehler beim Validieren des Captchas.";
}
}

View file

@ -0,0 +1,93 @@
<?php
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
require "./queries/skills.php";
require "./queries/blogPost.php";
require "./queries/comments.php";
require "./queries/mailAddress.php";
require "./queries/ebayKleinanzeigen.php";
require "./queries/imgproxy.php";
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'sitekey' => [
'type' => Type::string(),
'resolve' => fn ($rootValue, $args) => $sitekey,
],
'mailAddress' => [
'type' => Type::string(),
"args" => [
"hCaptchaResponse" => Type::string()
],
'resolve' => fn ($rootValue, $args) => mailAddress($args["hCaptchaResponse"]),
],
'skills' => [
'type' => Type::listOf(Type::string()),
'resolve' => fn ($rootValue, $args) => getSkills(),
],
'blogPost' => [
"type" => $blogPostFields,
'args' => [
'id' => Type::nonNull(Type::string()),
],
'resolve' => fn ($rootValue, $args) => blogPost($args["id"], $rootValue["db"]),
],
'blogPosts' => [
"type" => Type::listOf($blogPostFields),
"args" => [
"count" => Type::nonNull(Type::int()),
"contentLength" => [
"type" => Type::int(),
"defaultValue" => null
]
],
'resolve' => fn ($rootValue, $args) => blogPosts($args["count"], $args["contentLength"], $rootValue["db"]),
],
'comments' => [
"type" => Type::listOf($commentField),
"args" => [
"article" => Type::nonNull(Type::string()),
],
'resolve' => fn ($rootValue, $args) => comments($args["article"], $rootValue["db"]),
],
'ebayKleinanzeigen' => [
"type" => $ebayKleinanzeigenFields,
"args" => [
"imageCount" => [
"type" => Type::int(),
"defaultValue" => 0
]
],
'resolve' => fn ($rootValue, $args) => ebayKleinanzeigen($args["imageCount"]),
],
'imgproxy' => [
"type" => Type::string(),
"args" => [
"url" => Type::nonNull(Type::string()),
],
'resolve' => fn ($rootValue, $args) => imgproxy($args["url"]),
]
]
]);
$mutationType = new ObjectType([
'name' => 'Mutation',
'fields' => [
"comment" => [
"type" => Type::string(),
"args" => [
"article" => Type::string(),
"name" => Type::string(),
"email" => Type::string(),
"comment" => Type::string(),
"hCaptchaResponse" => Type::string()
],
'resolve' => fn ($rootValue, $args) => newComment($rootValue["db"], $args["article"], $args["name"], $args["email"], $args["comment"], $args["hCaptchaResponse"]),
],
]
]);

View file

@ -0,0 +1,40 @@
<?php
function getSkills() {
require "./lib/config.php";
$s3Client = new Aws\S3\S3Client([
'version' => 'latest',
'region' => 'us-east-1',
'endpoint' => $S3Server,
'use_path_style_endpoint' => true,
'credentials' => [
'key' => $S3AccessKey,
'secret' => $S3SecretKey,
],
]);
$result = $s3Client->ListObjects(['Bucket' => $S3BucketName, 'Delimiter'=>'/', 'Prefix' => 'skills/']);
$response = [];
foreach ($result["Contents"] as $skill){
// Get a command object from the client
$command = $s3Client->getCommand('GetObject', [
'Bucket' => $S3BucketName,
'Key' => $skill["Key"]
]);
// Create a pre-signed URL for a request with duration of 10 miniutes
$presignedRequest = $s3Client->createPresignedRequest($command, '10 minutes');
// Get the actual presigned-url
$downloadURL = (string) $presignedRequest->getUri();
// Generate the imgproxy URL
$encodedUrl = rtrim(strtr(base64_encode($downloadURL), '+/', '-_'), '=');
$path = "/rs:fit:0:$defaultSkillsWidth:1/g:no/{$encodedUrl}.webp";
$signature = rtrim(strtr(base64_encode(hash_hmac('sha256', $imgProxySalt.$path, $imgProxyKey, true)), '+/', '-_'), '=');
array_push($response, $imgProxyUrl . "/" . $signature . $path);
}
return $response;
}

45
public/about.html Normal file
View file

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title></title>
<link href="/css/style.css" rel="stylesheet">
</head>
<body>
<jl-header data-title="Über mich"></jl-header>
<div id="content">
<p>Hallo, mein Name ist Jonas Leder. Ich bin aktuell Auszubildender bei der <a
href="https://www.jobrouter.com/de/">JobRouter AG</a> in Fachrichtung Fachinformatiker für
Anwendungsentwicklung. In meiner
Freizeit beschäftige ich mich viel mit Computern und der Softwareprogrammierung. Dabei verwende ich häufig
folgende Programmiersprachen:</p>
<ul>
<li>Python</li>
<li>C / C#</li>
<li>"HTML"</li>
<li>JavaScript / TypeScript</li>
<li>CSS / SCSS</li>
<li>PHP</li>
<li>Java</li>
<li>Bash</li>
</ul>
<p>Die oben genannten Sprachen setzt ich auf PCs und Microcontrollern ein. Dabei verwende ich die Standard
Arduino
Boards (nano, mega, …) und spezielle, wie das ESP8266, welches WLAN unterstützt.</p>
<p>Neben Microcontrollern beschäftige ich auch viel mit Servern und Computern. Auch für diese Systeme schreibe
ich verschiedenste Programme und Scripts. Mein Hauptgebiet ist aktuell die Webentwicklung. Dierfür setze ich
bei den meisten Seiten auf HTML, JS und SCSS für das Frontend. Im Backend nutze ich in den meisten Fällen
PHP.</p>
<p>
Neben der Erfahrung im Programmieren kenne ich mich auch sowohl mit Windows als auch mit Linux aus. Wobei
ich mittlerweile fast überall nurnoch Linux einsetze und Windows nur im Ausnahmefall verwendet wird.
</p>
</div>
<jl-footer></jl-footer>
<script src="/js/script.js"></script>
</body>
</html>

View file

@ -5,7 +5,7 @@
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title></title>
<link href="../scss/style.scss" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">
</head>
<body>
@ -18,16 +18,16 @@
href="https://www.debian.org/download">hier</a> heruntergeladen werden. Hierfür wird ein System mit
mindestens 2GB RAM, 2 Kernen und 10GB Storage empfohlen. Wenn die VM con der CD gebootet wird kommt als
erstes ein Auswahlmenü, in diesem die Option Install wählen und danach enter drücken.</p>
<img src="../assets/img/debian_grub.jpg">
<jl-img src="/img/anleitungen/mailcow/debian_grub.jpg"></jl-img>
<p>Im nächsten Schritt wird die Sprache, die Region und das Tastaturlayout fetgelegt. Im nächsten Schritt
konfiguriert Debian einige Einstellungen wie die Netzwerkkonfiguration. Wenn die automatische Konfiguration
abgeschlossen ist, frag der Installer nach dem Hostname, dieser kann frei gewählt werden. Ich verwende
hierbei gerne Namen, die zu dem System passen, wie zum Beispiel mailcow. Der Domain Name im darauffolgenden
Schritt kann leer gelassen werden. Wenn dieser festgelegt wurde sollte das Passwort für den root Benutzer
festgelegt werden. Hierbei sollte auf einen <a href="../passwordgen.html">Passwortgenerator</a>
festgelegt werden. Hierbei sollte auf einen <a href="/passwordgen.html/">Passwortgenerator</a>
gesetzt werden. Nachdem das Passwort für den root Benutzer festgelegt wurde fragt Debian noch nach
benötigten Daten für einen nicht root Nutzer. Hierbei muss ein Anzeigenahme, ein Nutzername und ein <a
href="../passwordgen.html">generiertes Passwort</a> festgelegt werden. Die Partitionierung
href="/passwordgen.html/">generiertes Passwort</a> festgelegt werden. Die Partitionierung
wird mit <code class="language-text">Guieded - use entire disk</code> bestätigt, danach die Festplatte
ausgewählt. Als Partitionsschema wird
<code class="language-text">All Files in one partition</code> gewählt. Wenn alle Optionen gesetzt wurden
@ -37,8 +37,8 @@
class="language-text">yes</code>
bestätigt.
</p>
<img src="../assets/img/debian_partition_method.jpg">
<img src="../assets/img/debian_partition_finish.jpg">
<jl-img src="/img/anleitungen/mailcow/debian_partition_method.jpg"></jl-img>
<jl-img src="/img/anleitungen/mailcow/debian_partition_finish.jpg"></jl-img>
<p>Nun wird Debian auf die Festplatte installiert. Jenachdem wie schnell das Bootlaufwerk und die Festplatte ist
kann dieser Schritt einige Minuten dauern. Nachdem die ersten Dateien auf die Festplatte kopiert wurden,
fragt Debian ob CDs mit Paketen eingelesen werden sollen. Dieser Schritt sollte mit <code
@ -52,13 +52,13 @@
weitergeben. Im nächsten Schritt sollte nur der SSH Server aktiviert werden. Die Optionen können deaktiviert
oder aktiviert werden, indem mit den Pfeiltasten auf die entsprechende Option navigiert wird und dann die
Leertaste gedrückt wird.</p>
<img src="../assets/img/debian_scan_media.jpg">
<img src="../assets/img/debian_survey.jpg">
<img src="../assets/img/debian_software.jpg">
<jl-img src="/img/anleitungen/mailcow/debian_scan_media.jpg"></jl-img>
<jl-img src="/img/anleitungen/mailcow/debian_survey.jpg"></jl-img>
<jl-img src="/img/anleitungen/mailcow/debian_software.jpg"></jl-img>
<p>Nachdem der SSH Server installiert wurde, muss der Bootloader installiert werden, dafür muss bei der Frage ob
Grub installiert werden soll "Yes" gedrückt werdeb und in der nächsten Seite die Systemfestplatte ausgewählt
werden.</p>
<img src="../assets/img/debian_grub_install.jpg">
<jl-img src="/img/anleitungen/mailcow/debian_grub_install.jpg"></jl-img>
<p>Nachdem das System installiert wurde wird Debian neugestartet. Nun kann sich entweder über die Oberfläche
angemeldet werden oder via SSH über die IP Adresse. Nach der Authentifizierung am System mit dem zuvor
erstellten Nutzer muss sich als root angemeldet werden und danach das System auf den neuesten Stand gebracht
@ -127,7 +127,7 @@ cd mailcow-dockerized
die IP Adresse der reverse DNS Eintrag auf den gleichen Domain wie im MX Eintrag geändert werden (also in meinem
Fall auf <code class="language-text">mail.jonasled-test.xyz</code>)
</p>
<img src="../assets/img/mailcow_dns.jpg">
<jl-img src="/img/anleitungen/mailcow/mailcow_dns.jpg"></jl-img>
<p>
Wenn nun alle DNS Einstellungen laufen kann Mailcow das erste mal mit dem nachfolgenden Befehl gestartet
werden. Beim ersten mal werden alle Programme heruntergeladen, abhängig von der Internetgeschwindigkeit kann
@ -139,14 +139,14 @@ cd mailcow-dockerized
# nicht benötigt falls noch als root angemeldet aus dem vorherigen Schritt.
docker-compose up</code>
</pre>
<img src="../assets/img/mailcow_login.jpg">
<jl-img src="/img/anleitungen/mailcow/mailcow_login.jpg"></jl-img>
<p>
Nachdem das oben abgebildete Login Fenster angezeigt wird, ist Mailcow fertig gestartet. Der default
Nutzername ist admin mit dem Passwort moohoo, dieses sollte umgehend nach dem ersten Login abgeändert
werden. Dazu in der Benutzerübersicht beim Admin Benutzer auf <code class="language-text">edit</code>
(blauer Button im Bild unten) klicken
und ein neues Passwort mit
einem <a href="../passwordgen.html">Passwortgenerator</a> erstellen und speichern. Als nächstes
einem <a href="/passwordgen.html">Passwortgenerator</a> erstellen und speichern. Als nächstes
empfehle ich dringend ein Zweifaktor Login festzulegen. Dazu kann entweder wenn ein passender <a
href="https://www.amazon.de/dp/B07HBD71HL/">Hardwareschlüssel</a> vorhanden ist WebAuthn oder Yubico
verwendet werden. Wenn kein Hardwareshlüssel vorhanden ist, können time based OTP Keys verwendet werden.
@ -161,22 +161,22 @@ docker-compose up</code>
class="language-text">Mailboxes</code> können nun
Mailboxen angelegt werden.
</p>
<img src="../assets/img/mailcow_setup_mail.jpg">
<img src="../assets/img/mailcow_domain_setup.jpg"><br>
<img src="../assets/img/mailcow_domain_new_1.jpg">
<img src="../assets/img/mailcow_domain_new_2.jpg">
<jl-img src="/img/anleitungen/mailcow/mailcow_setup_mail.jpg"></jl-img>
<jl-img src="/img/anleitungen/mailcow/mailcow_domain_setup.jpg"></jl-img><br>
<jl-img src="/img/anleitungen/mailcow/mailcow_domain_new_1.jpg"></jl-img>
<jl-img src="/img/anleitungen/mailcow/mailcow_domain_new_2.jpg"></jl-img>
<p>
In der Mailbox Konfiguration kann nun mit dem Button <code class="language-text">Add mailbox</code> eine
neue Mailbox angelegt werden. Hier
muss der Teil der Mail vor dem <code class="language-text">@</code> angegeben werden. (Beispielswiese für
die Mail <code class="language-text">info@jonasled-test.xyz</code>
muss hier info angegeben werden) Danach sollte der volle Name des Nutzers und ein Passwort aus einem
<a href="../passwordgen.html">Passwortgenerator</a> festgelegt werden. Wenn nun alle
<a href="/passwordgen.html/">Passwortgenerator</a> festgelegt werden. Wenn nun alle
Einstellungen passen, kann der Domain mit <code class="language-text">Add</code> angelegt werden. Nun kann
sich der Nutzer ins SOGo anmelden
um das Webmail zu nutzen oder mit einem Client wie Thunderbird anmelden.
</p>
<img src="../assets/img/mailcow_mailbox_new.jpg">
<jl-img src="/img/anleitungen/mailcow/mailcow_mailbox_new.jpg"></jl-img>
<p>
Nachdem wir nun die erste Mailbox erstellt haben, muss noch ein DNS Eintrag erstellt werden, damit andere
Server validieren können, dass der sendende Server wirklich authorisiert dazu ist. Dazu im Mailcow Admin
@ -184,6 +184,8 @@ docker-compose up</code>
Configuration auf ARC/DKIM Keys gehen. Danach den beim Domain angegebenen Key kopieren und in der DNS
Verwaltung als TXT Record mit der Bezeichnung <code class="language-text">dkim._domainkey</code> eintragen.
</p>
<jl-img src="/img/anleitungen/mailcow/mailcow_dkim_webui.jpg"></jl-img>
<jl-img src="/img/anleitungen/mailcow/mailcow_dkim_dns.jpg"></jl-img>
<p>
Nun ist unser Mail Server vollständig konfiguriert und kann auch eingesetzt haben. Um die Funktion zu testen
sollte zuerst mit einem anderen Anbieter eine Mail an eine Adresse auf dem neuen Server gesendet werden.
@ -191,11 +193,12 @@ docker-compose up</code>
href="https://www.mail-tester.com/">mail-tester.com</a>. Auf dieser bekommt man eine Mail Adresse, an
welche man eine Mail senden kann und danach alle fehler angezeigt bekommt.
</p>
<img src="../assets/img/mailcow_dkim_webui.jpg">
<img src="../assets/img/mailcow_dkim_dns.jpg">
<h2>Kommentare:</h2>
<jl-comments_display></jl-comments_display>
<jl-new_comment id="newComment"></jl-new_comment>
</div>
<jl-footer></jl-footer>
<script src="../js/script.js" type="module"></script>
<script src="/js/script.js"></script>
<script>
document.title = "Mailcow installieren - Jonas Leder";
</script>

View file

@ -5,7 +5,7 @@
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title></title>
<link href="../scss/style.scss" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">
</head>
<body>
@ -70,7 +70,7 @@ chown apache /var/www/localhost/htdocs/ -R</code>
installiert. Nachdem dieser Schritt erfolgreich durchlaufen ist landen wir auf der Setup-Seite, dort müssen wir
unsere Datenbank und unseren admin Nutzer wie unten im Bild zu sehen angeben. Falls gewünscht unten den Haken
bei den empfohlenden Anwendungen entfernen.<br>
<img src="../assets/img/nextcloud-setup.png"><br>
<jl-img src="/img/anleitungen/nextcloud-setup.png"></jl-img><br>
Nachdem die Datenbank angelegt und alle Apps installiert wurden, solltest du auf der Startseite der Nextcloud
gelandet sein. Optional kann nun für mehr performance noch ein Memory-Caching konfiguriert werden. Wie dieses
eingerichtet ist, kann dem <a
@ -84,4 +84,4 @@ chown apache /var/www/localhost/htdocs/ -R</code>
<jl-footer></jl-footer>
<script src="../js/script.js" type="module"></script>
<script src="/js/script.js"></script>

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title></title>
<link href="../scss/style.scss" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">
</head>
<body>
<jl-header data-title="Installation von Snowboy"></jl-header>
@ -31,7 +31,7 @@ build-essential git python3-setuptools python3-dev autotools-dev automake</code>
<pre><code class="language-bash">wget http://downloads.sourceforge.net/swig/swig-3.0.12.tar.gz
tar -xovzf swig-3.0.12.tar.gz
cd swig-3.0.12
wget https://sourceforge.net/projects/pcre../assets/files/pcre/8.42/pcre-8.42.tar.gz
wget https://sourceforge.net/projects/pcre/8.42/pcre-8.42.tar.gz
./Tools/pcre-build.sh
./autogen
./configure
@ -57,8 +57,8 @@ make</code></pre>
Rate". Das Problem der Soundkarte ist, dass sie nur 44kHz als Abtastrate unterstützt, Snowboy braucht aber eine
Abtastrate von 16kHz wie ich daraufhin nachgelesen habe. Unten sind zwei Screenshots der Fehler angefügt.</p>
<br>
<img src="../assets/img/snowboy_no_mic.png">
<img src="../assets/img/snowboy_wrong_sample_rate.png">
<jl-img src="/img/anleitungen/snowboy/snowboy_no_mic.png"></jl-img>
<jl-img src="/img/anleitungen/snowboy/snowboy_wrong_sample_rate.png"></jl-img>
<h2>Kommentare:</h2>
<jl-comments_display></jl-comments_display>
<jl-new_comment id="newComment"></jl-new_comment>
@ -66,4 +66,4 @@ make</code></pre>
<jl-footer></jl-footer>
<script src="../js/script.js" type="module"></script>
<script src="/js/script.js"></script>

View file

@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/css/style.css" rel="stylesheet">
<title></title>
</head>
<body>
<jl-header data-title="Traefik Reverse Proxy"></jl-header>
<div id="content">
<p>In der folgenden Anleitung werde ich darauf eingehen, wie man eine VM mit alpine Linux aufsetzt, darauf
Docker installiert und dann als Container einen Traefik 2 reverse Proxy.</p>
<p>Als erstes muss eine neue VM erstellt werden und als Boot Medium Alpine Linux eingelegt werden. Wenn die ISO
gebootet ist kommt ein Login Fenster, hier einfach mit dem Benutzer <code class="language-text">root</code>
anmelden. Danach sollte ein Terminal Prompt kommen. Um Alpine zu installieren muss der Befehl <code
class="language-text">setup-alpine</code> eingegeben werden. Hierbei gilt es zu beachten, dass die ISO
ein englisches Tastaturlayout benutzt. Der Bindestrich liegt hier auf dem ß. Als erstes muss nun im Setup
das Tastaturlayout angegeben werden, um dieses auf Deutsch festzulegen zwie mal <code
class="language-text">de</code> eingeben.
Nachdem die Tastatur geändert wurde muss der Hostname festgelegt werden, meine VM heißt einfach <code
class="language-text">docker</code>. In der darauffolgenden Netzwerkkonfiguration muss als erstes der
Name des zu konfigurierendes Netzwerkinterfaces angegeben werden (meist <code
class="language-text">eth0</code>)
Im darauffolgenden Schritt wird die Methode der IP konfiguration angegeben. Im Folgenden setze ich hier auf
DHCP. Nachdem das root Passwort (welches mit einem <a href="/passwordgen.html">Passwortgenerator</a>
generiert wurde) festgelegt wurde muss die Zeitzone angegeben werden. Für Deutschland ist
diese <code class="language-text">Europe/Berlin</code>. Einen Proxy brauchen wir nicht, genauso wie beim
mirror können wir einfach mit enter bestätigen. Der SSH Server kann auch bei openssh belassen werden.
Nachdem nun die Grundkonfiguration im Installer abgeschlossen ist, muss noch die Festplatte angegeben
werden. Dafür wird eine Liste an erkannten Platten angezeigt. In meinem Fall war dies <code
class="language-text">sda</code> und danach noch der Typ. Dies ist <code
class="language-text">sys</code>,
da das System auf die Platte installiert wird. Nach der Installation muss das System nochmal neugestartet
werden.
</p>
<p>
Wenn das System nun von der Festplatte gebootet hat kann man sich mit dem Benutzer <code
class="language-text">root</code> und dem zuvor gewählten Passwort anmelden. Danach muss als erstes das
System auf den neuesten Stand gebracht werden und ein paar tool installiert werden. Dazu die beiden unten
ausfgeführten Befehle eingeben.
<pre>
<code class="language-bash">apk update
apk upgrade
apk add nano htop git</code>
</pre>
Um nun Docker zu installieren muss als erstes die Community repo aktiviert werden. Dazu mit <code
class="language-bash">nano</code> die Datei <code class="language-text">/etc/apk/repositories</code> öffnen
und in der Zeile, welche mit <code class="language-text">community</code> endet das <code
class="language-text">#</code> am Anfang entfernen. (Nicht in den Zeilen mit <code
class="language-text">edge</code> im URL) Danach kann Docker installiert werden.
</p>
<pre>
<code class="language-bash">nano /etc/apk/repositories
apk update
apk add docker docker-compose
rc-update add docker
/etc/init.d/docker start</code>
</pre>
<p>
Nun ist Docker auf unserem System installiert und kann eingesetzt werden. Um Traefik 2 nun einzusetzen muss
als erstes meine Vorlage von <a href="https://gitlab.jonasled.de/jonasled/traefik-config">hier</a>
heruntergeladen werden. Danach muss in der Datei <code class="language-text">config/traefik.yml</code> unter
letsencrypt => acme => email die E-Mail Adresse festgelegt werden, welche für letsencrypt verwendet werden
soll. Danach noch die Berechtigungen von der Zertifikatsdatei einschränken. Bevor wir traefik starten könenn
müssen wir noch ein Netzwerk namens <code class="language-text">web</code> angelegt werden. Nachdem nun
alles vorbereit wurde kann dieser mit
<code class="language-bash">docker-compose up</code> gestartet werden.
</p>
<pre>
<code class="language-bash">git clone https://gitlab.jonasled.de/jonasled/traefik-config
cd traefik-config
nano config/traefik.yml
chmod 600 letsencrypt/acme.json
docker network create web
docker-compose up
# Wenn alles läuft strg und c drücken
docker-compose up -d</code>
</pre>
<p>
Traefik ist nun installiert und sollte von außen erreichbar sein. Als Antwort sollte bei nicht bekannten
Domains immer ein 404 Fehler kommen. Zum testen setzen wir als nächstes den whoami Docker Container auf,
dieser ist nur wenige kb groß und bietet einen minimalen Webserver. Dazu muss als erstes die unten
angehängte docker-compose auf dem Host in einem neuen Ordner unter dem Namen <code
class="language-text">docker-compose.yml</code> abspeichern und den Host anpassen. Danach kann der
Container mit <code class="language-bash">docker-compose up</code> gestartet werden. Nun sollte nach 1-2
Minuten auf dem zuvor angegebenen Domain die 404 Meldung durch eine Seite ersetzt werden. Falls dies nicht
der Fall ist kann im Ordner, in dem der Traefik abgelegt wurde der Befehl <code
class="language-text">docker-compose logs -f</code> ausgeführt werden um den Fehlerlog zu überprüfen.
</p>
<pre>
<code class="language-yaml">version: "3.2"
services:
whoami:
image: containous/whoami
restart: unless-stopped
networks:
- web
labels:
- traefik.http.routers.whoami-https.rule=Host(`whoami.jonasled-test.xyz`)
- traefik.http.routers.whoami-https.entrypoints=https
- traefik.http.routers.whoami-https.tls=true
- traefik.http.routers.whoami-https.tls.certresolver=letsencrypt
- traefik.http.services.whoami.loadbalancer.server.port=80
networks:
web:
external: true</code>
</pre>
<p>
Um die Konfigurationen für den Traefik Server zu erstellen verwende nutze ich ein kleines selber
geschriebenes Tool, welches <a
href="https://jonasled.pages.gitlab.jonasled.de/traefik-config-generator/">hier</a> erreichbar ist.
</p>
<h2>Verbesserungen</h2>
<h3>HTTP auf HTTPS weiterleiten</h3>
<p>Ich empfehle diesen Schritt für alle, da dadurch traefik den Nutzer automatisch von einer unverschlüsselten
HTTP Verbindung auf eine verschlüsselte HTTPS verbindung weiterleitet. Dies verhindert das mitlesen der
Daten durch dritte. Um die Weiterleitung einzurichten muss unter <code
class="language-text">config/providers</code> eine neue Datei mit der Endung <code
class="language-text">.yml</code> angelegt werden. (also z.B. <code
class="language-bash">nano config/providers/http.yml</code>) Danach muss in die Datei der unten
angeführte Snippet eingefügt werden und danach gescpeichert werden (strg + x und dann mit y und enter
bestätigen)
</p>
<pre>
<code class="language-yaml">http:
routers:
http-redirect:
rule: HostRegexp(`{any:.+}`)
middlewares: redirect
service: dummy
middlewares:
redirect:
redirectscheme:
scheme: https
services:
dummy:
loadBalancer:
servers:
- url: "http://0.0.0.0/"
passHostHeader: true</code>
</pre>
<h2>Nicht Docker Services einbinden</h2>
<p>
Das Einbinden von Diensten, die nicht auf dem Docker Host laufen ist auch ziemlich einfach. Dafür muss nur
eine Provider yaml Datei (Dateiendung .yml) im <code class="language-text">config/providers</code> Ordner
mit dem nachfolgenden Inhalt angelegt werden. Um den Router zu nutzen muss <code
class="language-text">servicename</code> und <code class="language-text">routername</code> durch Namen
ersetzt werden. Zusätzlich muss im Router der Hostname und im Service der interne URL zu dem dienst, der von
außen erreichbar sein soll gesetzt werden.
</p>
<pre>
<code class="language-yaml">http:
# Add the router
routers:
routername:
service: servicename
rule: Host(`whoami.jonasled-test.xyz`)
tls:
certresolver: letsencrypt
# Add the service
services:
servicename:
loadBalancer:
servers:
- url: http://10.0.0.1
passHostHeader: true</code>
</pre>
<h2>Kommentare:</h2>
<jl-comments_display></jl-comments_display>
<jl-new_comment id="newComment"></jl-new_comment>
</div>
<jl-footer></jl-footer>
<script src="/js/script.js"></script>
</body>
</html>

View file

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/css/style.css" rel="stylesheet">
<title></title>
</head>
<body>
<jl-header data-title="Vaultwarden Passwortmanager"></jl-header>
<div id="content">
<p>
Vaultwarden ist ein Passwort Manager, welcher das Frontend und die Anwendungen von Bitwarden unterstützt,
aber durch die Implementation in Rust schneller und Resourcensparender als der originale Server ist.
Zusätzlich bietet Vaultwarden alle Premium Features kostenlos. Für die Installation setze ich Vorraus, dass
auf dem System bereits ein Reverse Proxy wie <a href="traefik.html">Traefik 2</a> existiert und Docker mit
Docker Compose installiert ist.
</p>
<p>
Als erstes muss ein neuer Ordner auf dem Host erstellt werden. In diesem muss als erstes eine <jl-code>
docker-compose.yml</jl-code> Datei mit dem Inhalt unten erstellt werden.
<pre>
<code class="language-yaml">version: "3.2"
services:
bitwarden:
image: vaultwarden/server:latest
restart: always
ports:
- 8080:80
- 3012:3012
environment:
- ADMIN_TOKEN=
- DOMAIN=https://bitwarden.jonasled-test.xyz
- WEBSOCKET_ENABLED=true
volumes:
- ./data:/data</code>
</pre>
In der Datei muss noch der <jl-code>ADMIN_TOKEN</jl-code> auf ein sicheres Passwort aus einem <a
href="/passwordgen.html">Passwortgenerator</a> ersetzt werden. Dieses Password wird benötigt um die Admin
Oberfläsche zu erreichen. Da über die Oberfläche auch zum Beispiel Nutzerkonten gelöscht werden können sollte es
wirklich sehr sicher sein. Zusätzlich muss auch der URL angegeben werden, über den der Passwortmanager später
von außen erreichbar ist.
</p>
<p>
Als nächstes muss der Reverse Proxy eingerichtet werden. Dazu muss Port 8080 als root Pfad (also /) und auf
<jl-code>/notifications/hub</jl-code> port 3012 freigegeben werden. Traefik Nutzer können die Docker Compose
welche <a href="https://gitlab.jonasled.de/-/snippets/8">hier</a> zu finden ist anstelle der obigen
verwenden und dort die Hosts anpassen.
</p>
<p>
Nachdem nun die Konfiguration abgeschlossen ist kann der Container mit dem Befehl <jl-code>docker-compose up
</jl-code> das erste mal gestartet werden. Wenn der Container läuft und alles funktioniert kann der
Container wieder mit strg und c gestoppt werden und danach mit <jl-code>docker-compose up -d</jl-code> im
Hintergrund gestartet werden.
</p>
<p>
Als erstes sollte das Admin Interface aufgerufen werden und ein SMTP Server für den E-Mail versand
eingestellt werden. Um das Interface zu erreichen muss an den URL <jl-code>/admin</jl-code> angehängt und
dass Passwort aus der <jl-code>docker-compose.yml</jl-code> angegeben werden.
</p>
<p>
Nun sollte die Konfiguration vom Bitwarden / Vaultwarden abgeschlossen sein. Wenn man jetzt die URL aufruft
kann man einen neuen Account anlegen. Im Client muss man zuerst in die Einstellungen gehen und dann dort den
URL zum Server angeben. Ganz wichtig ist hierbei das vorgestellte HTTPS. Anschließend funktioniert der Login
auf der Startseite mit dem gleichen Benutzer, wie auch im Webclient.
</p>
<jl-img src="/img/anleitungen/bitwarden/bitwarden_browser_1.jpg"></jl-img>
<jl-img src="/img/anleitungen/bitwarden/bitwarden_browser_2.jpg"></jl-img>
<h2>Kommentare:</h2>
<jl-comments_display></jl-comments_display>
<jl-new_comment id="newComment"></jl-new_comment>
</div>
<jl-footer></jl-footer>
<script src="/js/script.js"></script>
</body>
</html>

View file

@ -4,30 +4,31 @@
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title></title>
<link href="scss/style.scss" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">
</head>
<body>
<jl-header data-title="Bildquellen"></jl-header>
<div id="content">
<ul>
<li><span class="clickSpan" src='../assets/img/bannerHome.webp'>Bild oben</span>: Photo by <a
<li><span class="clickSpan" src='/img/bannerHome.webp'>Bild oben</span>: Photo by <a
href="https://unsplash.com/@hishahadat?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Shahadat
Rahman</a>&nbsp;on&nbsp;<a
href="https://unsplash.com/s/photos/programmer?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a>
</li>
<li><span class="clickSpan" src='../assets/img/bildHome.webp'>Bild Startseite</span>: Photo by&nbsp;<a
<li><span class="clickSpan" src='/img/bildHome.webp'>Bild Startseite</span>: Photo by&nbsp;<a
href="https://unsplash.com/@grohsfabian?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Fabian
Grohs</a>&nbsp;on&nbsp;<a
href="https://unsplash.com/s/photos/programmer?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a>
</li>
<li>
<span class="clickSpan" src='../assets/img/laptop.jpg'>Bild Laptop</span>: <a href="https://sm.pcmag.com/t/pcmag_au/review/l/lenovo-thi/lenovo-thinkpad-l13-yoga_7bvb.1920.jpg">pcmag</a>
<span class="clickSpan" src='/img/systeme/laptop.jpg'>Bild Laptop</span>: <a href="https://sm.pcmag.com/t/pcmag_au/review/l/lenovo-thi/lenovo-thinkpad-l13-yoga_7bvb.1920.jpg">pcmag</a>
</li>
</ul>
</div>
<jl-footer></jl-footer>
<script src="js/script.js" type="module"></script>
<script src="/js/script.js"></script>
<script>
// Set document title to "Bildquellen - Jonas Leder"
document.title = "Bildquellen - Jonas Leder";
</script>
</body>

View file

@ -5,7 +5,7 @@
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title></title>
<link href="scss/style.scss" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">
</head>
<body>
@ -263,7 +263,7 @@
mit Datenschutz-Generator.de von RA Dr. Thomas Schwenke</a></p>
</div>
<jl-footer></jl-footer>
<script src="js/script.js" type="module"></script>
<script src="/js/script.js"></script>
<script>
document.title = "Datenschutzerklärung - Jonas Leder";
</script>

View file

Before

Width:  |  Height:  |  Size: 706 B

After

Width:  |  Height:  |  Size: 706 B

65
public/gpg.txt Normal file
View file

@ -0,0 +1,65 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
0Kfe8lA=
=cYJ8
-----END PGP PUBLIC KEY BLOCK-----

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View file

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View file

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View file

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View file

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View file

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View file

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View file

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View file

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View file

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View file

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View file

Before

Width:  |  Height:  |  Size: 612 KiB

After

Width:  |  Height:  |  Size: 612 KiB

View file

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

View file

Before

Width:  |  Height:  |  Size: 243 KiB

After

Width:  |  Height:  |  Size: 243 KiB

View file

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

View file

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

View file

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 827 KiB

After

Width:  |  Height:  |  Size: 827 KiB

View file

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 1,005 KiB

After

Width:  |  Height:  |  Size: 1,005 KiB

View file

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

Before

Width:  |  Height:  |  Size: 735 KiB

After

Width:  |  Height:  |  Size: 735 KiB

View file

Before

Width:  |  Height:  |  Size: 939 KiB

After

Width:  |  Height:  |  Size: 939 KiB

View file

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

Before

Width:  |  Height:  |  Size: 675 KiB

After

Width:  |  Height:  |  Size: 675 KiB

View file

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View file

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 253 KiB

View file

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

View file

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 187 KiB

Some files were not shown because too many files have changed in this diff Show more