Compare commits

...

137 commits

Author SHA1 Message Date
Jonas Leder b1db76d7c5 set only atributes existing on the original image 2022-07-15 15:43:58 +02:00
Jonas Leder 7e56c51ca9 remove fay 2022-07-13 12:29:51 +02:00
Jonas Leder 280e67693e
update comments 2022-07-05 12:28:01 +02:00
Jonas Leder ae50943802
limit s3 request to 10 minutes 2022-07-05 12:27:31 +02:00
Jonas Leder c719db5657
show error string in german 2022-07-05 12:26:54 +02:00
Jonas Leder e00442b9da
validate email address format 2022-07-05 12:25:12 +02:00
Jonas Leder b38c66a0f0 add public ssh key 2022-06-26 15:04:44 +02:00
Jonas Leder b5d6cbfacb Revert "switch to php 8.1"
This reverts commit bab8fc435e
2022-06-13 22:20:20 +02:00
Jonas Leder 43ccbd72f7 add speedtest server 2022-06-13 22:19:48 +02:00
Jonas Leder 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
Jonas Leder 50dbff7f2c
add imgproxy settings 2022-05-01 13:13:20 +02:00
Jonas Leder 19beccff30
add variable size for images 2022-05-01 13:13:10 +02:00
Jonas Leder 3457314e87
remove old imgPreview 2022-05-01 13:02:04 +02:00
Jonas Leder 50689d2b83
apply id from jl-img to inner image element 2022-05-01 13:01:34 +02:00
Jonas Leder f0e92fe024
fix images on main page 2022-05-01 13:01:21 +02:00
Jonas Leder b2efb92a53
open preview when clicking on image 2022-05-01 12:58:57 +02:00
Jonas Leder fc75c07473
fix variable type 2022-05-01 12:56:43 +02:00
Jonas Leder 0e0ca76afb
use 512px width 2022-05-01 12:56:34 +02:00
Jonas Leder ffc750c405
use jl-image for images 2022-05-01 12:53:54 +02:00
Jonas Leder 3ab7888a2a
use jl-image for images 2022-05-01 12:53:48 +02:00
Jonas Leder f140d1e271
create ednpoint to reduce image size with imgproxy 2022-05-01 12:45:06 +02:00
Jonas Leder ccb5af6c8e add gpg key 2022-04-26 09:16:15 +02:00
Jonas Leder 9abcba556c
use webp instead of png for skills 2022-04-16 23:36:25 +02:00
Jonas Leder 8c5b68037a
render images through imgproxy 2022-04-16 23:34:35 +02:00
Jonas Leder d465f5ecb8
get skills directly from s3 server 2022-04-16 23:11:42 +02:00
Jonas Leder bfae5cc098 add open bug bounty txt file 2022-04-09 15:58:45 +02:00
Jonas Leder 0f70b811f9
fix new comment 2022-04-07 09:26:20 +02:00
Jonas Leder 7b56fbbdcd
set content type in graphql request 2022-04-07 09:24:06 +02:00
Jonas Leder a6a470f8aa use mutation query 2022-04-07 09:12:43 +02:00
Jonas Leder aa670a0512 add mutation query for new comment 2022-04-07 09:12:43 +02:00
Jonas Leder bbc1fb852f rewrite get parameter function 2022-04-07 09:12:43 +02:00
Jonas Leder 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
Jonas Leder fa119a00cd use better graphql setup, which also supports schema queries 2022-04-05 10:26:18 +02:00
Jonas Leder 8131709a88 limit post requests to 1M 2022-03-30 18:46:25 +02:00
Jonas Leder 9b6c5d0fb8
rename last stage to prduction 2022-03-25 12:17:45 +01:00
Jonas Leder 8c2252a0d4
change link label to ntp pool 2022-03-25 12:10:53 +01:00
Jonas Leder 65db25ae63
remove normalize 2022-03-25 12:06:08 +01:00
Jonas Leder fa0d3baf0b
use 100ms for zoom animation 2022-03-25 12:04:57 +01:00
Jonas Leder ac00743cb8
add comment 2022-03-25 12:01:23 +01:00
Jonas Leder b2dccc445c
remove raw html 2022-03-25 10:43:24 +01:00
Jonas Leder bae04c21cf
remove raw html 2022-03-25 10:38:01 +01:00
Jonas Leder 8e990911c1
remove raw html 2022-03-25 10:23:52 +01:00
Jonas Leder b18fe46788
remove raw html 2022-03-25 10:21:11 +01:00
Jonas Leder 04e198c6d4
add option to set language with attribute 2022-03-25 10:18:57 +01:00
Jonas Leder 031cccb475
generate footer with pure js 2022-03-25 10:13:51 +01:00
Jonas Leder e2524efc6a
use connected callback instead of constructor 2022-03-25 10:13:32 +01:00
Jonas Leder 61bc809fa8
generate social buttons from array 2022-03-25 09:38:48 +01:00
Jonas Leder f155578daa
use connected callback for loading SVGs instead of constructor 2022-03-25 09:38:20 +01:00
Jonas Leder c4685355b5
fix mobile menu has no navigation displayed 2022-03-25 09:24:28 +01:00
Jonas Leder 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
Jonas Leder 68afb9607d remove browser check 2022-03-21 11:19:47 +01:00
Jonas Leder 49ef0459cd update error message 2022-03-18 10:20:32 +01:00
Jonas Leder ffbbdae474 edit banner text 2022-03-18 08:13:51 +01:00
Jonas Leder 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
Jonas Leder 9c236bba83 fix two times sql injection possible 2022-03-16 10:18:46 +01:00
Jonas Leder 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
Jonas Leder 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
Jonas Leder 947e64d94c set higher network timeout in yarn 2022-02-25 16:08:23 +01:00
Jonas Leder b5a37776e8 add binfmt 2022-02-25 10:14:48 +01:00
Jonas Leder 4904526443 disable file uploads in PHP 2022-02-25 09:53:20 +01:00
Jonas Leder 00740f441a pull node and composer through docker proxy 2022-02-21 11:51:56 +01:00
Jonas Leder ccfb317317 Cross compile docker for arm and arm64 2022-02-21 09:51:38 +01:00
Jonas Leder bc986d7547 integrade Discord bots into main status page 2022-02-13 16:06:15 +01:00
Jonas Leder 093e63653c fix bug windows resizer only working once 2022-02-11 10:24:39 +01:00
Jonas Leder 2ad5e46827 fix typo 2022-02-09 14:42:31 +01:00
Jonas Leder 410dff42d4 remove uptime banner 2022-02-09 13:25:03 +01:00
Jonas Leder bcf5f02f36 remove newline in banner 2022-02-06 01:02:45 +01:00
Jonas Leder 9e20e5e15a fix banner 2022-02-05 23:25:37 +01:00
Jonas Leder 27dbbb1e8b add livechat link 2022-02-05 22:43:23 +01:00
Jonas Leder 2405d712fa
use class instead of id in banner div 2022-02-04 08:44:46 +01:00
Jonas Leder 9ddb43af2d
arange shields banner stacked 2022-02-04 08:43:57 +01:00
Jonas Leder dcb5262744
update about page 2022-02-04 08:40:12 +01:00
Jonas Leder 9bcc34ff43
fix typo 2022-02-04 08:34:31 +01:00
Jonas Leder 8710d5e601
add note to ebk links 2022-02-04 08:28:04 +01:00
Jonas Leder 66312e4f93
replace upper foldername with lower char for SVG folder 2022-02-04 08:24:38 +01:00
Jonas Leder bc4d5f1acb
add mor detailed error message to ebayimg 2022-02-04 08:20:55 +01:00
Jonas Leder 1e8ed63c24
return error if ebay returns other than 200 2022-02-04 08:18:03 +01:00
Jonas Leder ce30653d3b
throw error if url is not set or empty 2022-02-03 10:25:55 +01:00
Jonas Leder 0eb8e4d22d
add shipping information 2022-02-03 10:17:39 +01:00
Jonas Leder 5fbd725f6c
fix typo in filename 2022-02-03 09:53:37 +01:00
Jonas Leder fa5b9d9089 add margin right to skill images 2022-02-02 17:41:12 +01:00
Jonas Leder dbefc2ba8d
rename file 2022-02-02 14:53:54 +01:00
Jonas Leder 187ddf9683
implement red and green button on 404 page 2022-01-30 11:18:16 +01:00
Jonas Leder 8deebbb1e1
remove unneeded file 2022-01-30 10:46:35 +01:00
Jonas Leder abc8b4cacf
fix some image URL 2022-01-30 10:10:10 +01:00
Jonas Leder 8c9fa6f3b5
fix google play icon 2022-01-30 10:07:00 +01:00
Jonas Leder 7cf19bbd06
rename folder 2022-01-30 09:58:17 +01:00
Jonas Leder 40e6c049bf
update image path 2022-01-30 09:54:20 +01:00
Jonas Leder 43dcd5871c add all images to repo 2022-01-30 09:35:48 +01:00
Jonas Leder c63cc3015b add gpg package to devcontainer 2022-01-30 09:28:57 +01:00
Jonas Leder 0af4202811 add Readme 2022-01-30 09:28:13 +01:00
Jonas Leder 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
Jonas Leder fbf0932c88 set page title 2022-01-29 21:32:47 +01:00
Jonas Leder f3dfeb2424 add vaultwarden instructions 2022-01-29 21:32:04 +01:00
Jonas Leder c0ee2ec89e add inline code element 2022-01-29 21:31:48 +01:00
Jonas Leder e72b3acf0a fix traefik title 2022-01-29 20:41:33 +01:00
Jonas Leder 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
Jonas Leder 37db2cc33d add recommendations, non docker service 2022-01-29 20:27:24 +01:00
Jonas Leder 4c8fb1aae6 add http to https redirect 2022-01-29 20:01:20 +01:00
Jonas Leder 76fcf276f9 add traefik instructions 2022-01-29 19:01:53 +01:00
Jonas Leder a5707e660f add traefik to menu 2022-01-29 16:53:12 +01:00
Jonas Leder 9e04efa8c7 add new file for traefik 2022-01-29 16:51:12 +01:00
Jonas Leder d73e903044 add template html file 2022-01-29 16:50:34 +01:00
Jonas Leder 9f215ebfb2 fix image at wrong position 2022-01-29 16:49:22 +01:00
139 changed files with 1896 additions and 1212 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

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,18 +1,18 @@
# |--------------------------------------------------------------------------
# | 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 mkdir public/js
RUN mkdir public/css
RUN yarn install
RUN yarn compile
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 .
RUN composer install
@ -20,9 +20,14 @@ 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

@ -1,9 +0,0 @@
import * as basicLightbox from 'basiclightbox'
if (!'customElements' in window) {
const instance = basicLightbox.create('<h1>Nicht unterstützter WebBrowser</h1><p>Leider verwenden sie einen nicht unterstützten Webbrowser. Dies führt dazu, dass die Seite nicht dargestellt werden kann. Ich empfehle eine aktuelle Version des <a href="https://www.google.com/chrome/">Chrome</a> oder des <a href="https://www.mozilla.org/en-US/firefox/new/">Firefox</a> Browsers zu verwenden.</p>',
{
closable: false
});
instance.show();
}

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

@ -1,31 +1,31 @@
class blogFooter extends HTMLElement {
constructor(){
super();
let xhr = new XMLHttpRequest();
let ul = document.createElement("ul");
xhr.onreadystatechange = () => {
if(xhr.readyState === 4) {
if (xhr.status === 200) {
let blog = JSON.parse(xhr.responseText);
blog.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);
});
this.appendChild(ul);
} else {
let p = document.createElement("p");
p.innerText = "Leider konnte dieser Inhalt nicht geladen werden, bitte versuche die Seite neu zu laden oder komme später wieder zurück";
this.appendChild(p);
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
}
xhr.open("GET", "/API/getBlogElements.php?position=footer");
xhr.send();
})
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);
});
}
}

View file

@ -1,48 +1,49 @@
class BlogIndex extends HTMLElement {
constructor() {
super();
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let blog = JSON.parse(xhr.responseText);
blog.forEach((element) => {
const article = document.createElement("article");
article.classList.add("breakWord");
this.appendChild(article);
this.getBlogPosts();
}
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);
});
} else {
let p = document.createElement("p");
p.innerText = "Leider konnte dieser Inhalt nicht geladen werden, bitte versuche die Seite neu zu laden oder komme später wieder zurück";
this.appendChild(p);
}
}
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
}
xhr.open("GET", "/API/getBlogElements.php?position=index");
xhr.send();
})
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);
});
}
}

View file

@ -1,48 +1,47 @@
class commentsDisplay extends HTMLElement {
constructor() {
super();
let path = window.location.pathname;
let pageName = path.split("/").pop();
this.getComments()
}
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let comments = JSON.parse(xhr.responseText);
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);
});
} else {
let p = document.createElement("p");
p.innerText = "Leider konnte dieser Inhalt nicht geladen werden, bitte versuche die Seite neu zu laden oder komme später wieder zurück.";
this.appendChild(p);
}
}
async getComments() {
var graphql = JSON.stringify({
query: 'query($article: String!) { comments(article: $article) { name comment gravatarURL }}',
variables: {
"article": window.location.pathname
}
xhr.open("GET", "/API/projectComments.php?article=" + pageName);
xhr.send();
})
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);
});
}
}

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

@ -1,21 +1,26 @@
class ebkBanner extends HTMLElement {
constructor(){
super();
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if(xhr.readyState === 4 && xhr.status === 200){
if(xhr.responseText > 0) {
const h2 = document.createElement("h2");
h2.classList.add("red");
h2.innerHTML = "Ich biete aktuell wieder verschiedene Artikel zum verkauf an, eine genaue Übersucht ist <a class=\"red\" href=\"/selling.html\">hier</a> zu sehen."
this.appendChild(h2);
}
}
}
this.generateBanner();
}
xhr.open("GET", "/API/ebk.php?count");
xhr.send();
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);
customElements.define("jl-ebk-banner", ebkBanner);

View file

@ -1,42 +1,95 @@
class Footer extends HTMLElement {
constructor() {
super();
this.innerHTML = `
<footer>
<div id="blueBar"></div>
<div id="footerContent">
<div>
<a href="/datenschutzerklaerung.html">Datenschutzerkl&auml;rung</a><br>
<a href="/bildquellen.html">Bildquellen</a><br>
<a href="/impressum.html">Impressum</a><br>
<a href="//gitlab.jonasled.de/jonasled/website">Quellcode</a><br>
</div>
<div id="newestPost">
<h3>Neueste Beitr&auml;ge:</h3>
<jl-footer_blog></jl-footer_blog>
</div>
<div class="center">
<p class="center">
<a href="https://www.thingiverse.com/jonasled/designs/" aria-label="Thingiverse">
<jl-svg data-name="3d_model"></jl-svg>
</a>
<a href="https://paypal.me/jonasled/" aria-label="PayPal">
<jl-svg data-name="paypal"></jl-svg>
</a>
<a href="https://matrix.to/#/@jonasled:jonasled.de" aria-label="Matrix">
<jl-svg data-name="matrix"></jl-svg>
</a>
<a href="https://twitter.com/jonasled1" aria-label="Twitter">
<jl-svg data-name="twitter"></jl-svg>
</a>
</p>
</div>
</div>
</footer>
`;
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);
});
}
}

View file

@ -2,21 +2,42 @@ class Header extends HTMLElement {
constructor() {
super();
let pageTitle = this.getAttribute("data-title");
this.innerHTML = `
<header >
<div class="header-wrapper" >
<div class="header-homepage">
<div class="align-holder" >
<h1 onclick="document.getElementById('content').scrollIntoView();">${pageTitle}</h1>
</div >
</div >
<div class="header-separator header-separator-bottom " >
<jl-svg data-name="banner"></jl-svg>
</div >
</div >
<jl-main_menu id = "mainmenu" data-title = "${pageTitle}"></jl-main_menu>
</header>
`;
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);
}
}

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

@ -1,38 +1,137 @@
class newComment extends HTMLElement {
constructor() {
super();
this.innerHTML = `
<button class="bigButton" id="showCommentButton">Neues Kommentar verfassen</button>
`;
document.getElementById("showCommentButton").onclick = this.setupForm;
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() {
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;
let script = document.createElement('script');
script.src = "https://hCaptcha.com/1/api.js";
script.type = 'text/javascript';
script.onload = () => {
let pageName = window.location.pathname.split("/").pop();
this.parentElement.innerHTML = `
<form action="/API/newComment.php" method="post">
<label for="name">Name:</label><br>
<input type="text" id="name" name="name"><br><br>
<label for="email">E-Mail: (wird nicht ver&ouml;ffentlicht)</label><br>
<input type="text" id="email" name="email"><br><br>
<label for="comment">Kommentar:</label><br>
<textarea name="comment" id="comment"></textarea><br><br>
<div class="h-captcha" data-theme="dark" data-sitekey="${sitekey}"></div><br>
<input type="hidden" name="pagename" id="pagename" value="${pageName}">
<input type="submit" value="Kommentar ver&ouml;ffentlichen"><br>
<p>Mit dem Klick auf den obigen Button erkl&auml;ren sie sich mit der <a href="/datenschutzerklaerung.html">Datenschutzerkl&auml;rung</a> einverstanden.</p>
</form>
`;
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);
}

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();

View file

@ -1,24 +1,75 @@
class PasswordGenerator extends HTMLElement {
constructor() {
super();
this.innerHTML = `
<input type="text" disabled id="pwgen-out"><br>
<input type="range" min="8" max="128" value="32" id="pwgen-len"><span id="pwgen-len-num">32</span><br>
<input type="checkbox" id="pwgen-inc-num" checked> Zahlen<br>
<input type="checkbox" id="pwgen-inc-big-char" checked> Großbuchstaben<br>
<input type="checkbox" id="pwgen-inc-small-char" checked> Kleinbuchstaben<br>
<input type="checkbox" id="pwgen-inc-special-char" checked> Sonderzeichen<br>
<button>Generieren</button>
`;
super();
const outValue = this.querySelector("#pwgen-out");
const pwlen = this.querySelector("#pwgen-len");
const pwlenSpan = this.querySelector("#pwgen-len-num");
const includeNum = this.querySelector("#pwgen-inc-num");
const includeBigChar = this.querySelector("#pwgen-inc-big-char");
const includeSmallChar = this.querySelector("#pwgen-inc-small-char");
const includeSpecialChat = this.querySelector("#pwgen-inc-special-char");
const button = this.querySelector("button");
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;
@ -34,7 +85,7 @@ class PasswordGenerator extends HTMLElement {
includeSmallChar.onchange = () => {
button.click();
}
includeSpecialChat.onchange = () => {
includeSpecialChar.onchange = () => {
button.click();
}
@ -51,7 +102,7 @@ class PasswordGenerator extends HTMLElement {
if(includeSmallChar.checked) {
possibleChar += "abcdefghijklmnopqrstuvwxyz";
}
if(includeSpecialChat.checked) {
if(includeSpecialChar.checked) {
possibleChar += ",;.:-_#'+*?=)(/&%$§\"!°{[]}<>|~"
}

View file

@ -1,13 +1,17 @@
import * as basicLightbox from 'basiclightbox'
class sellingTable extends HTMLElement {
constructor(){
const config = [
constructor() {
super();
this.config = [
{
"title": "Bild",
"fieldName": "previewImage",
"fieldName": "preview",
"displayType": "image",
"fullImage": "image"
"fullImage": "image",
"index": 0
},
{
"title": "Titel",
@ -19,6 +23,11 @@ class sellingTable extends HTMLElement {
"fieldName": "price",
"displayType": "text"
},
{
"title": "Versand",
"fieldName": "shipping",
"displayType": "text"
},
{
"title": "Link",
"fieldName": "link",
@ -26,68 +35,71 @@ class sellingTable extends HTMLElement {
"linkText": "Anzeige ansehen",
"target": "_blank"
},
]
];
super();
this.generateTable();
}
async generateTable() {
const table = document.createElement("table");
this.appendChild(table);
const tr = document.createElement("tr");
table.appendChild(tr);
config.forEach(element => {
this.config.forEach(element => {
const th = document.createElement("th");
th.innerText = element["title"];
tr.appendChild(th);
});
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if(xhr.readyState === 4 && xhr.status === 200){
const response = JSON.parse(xhr.responseText);
response.forEach( ad => {
const tr = document.createElement("tr");
table.appendChild(tr);
config.forEach(element => {
const th = document.createElement("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[element["fieldName"]];
img.onclick = () => {
const instance = basicLightbox.create(`
<img src="${ad[element["fullImage"]]}">
`);
instance.show();
}
break;
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);
});
})
}
}
}
xhr.open("GET", "/API/ebk.php");
xhr.send();
tr.appendChild(th);
});
});
}
}

View file

@ -1,19 +1,27 @@
class Skill extends HTMLElement {
constructor() {
super();
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState == 4 && xhr.status == 200) {
JSON.parse(xhr.responseText).forEach(skill => {
const image = document.createElement("img");
image.classList.add("skills");
image.src = "/API/getFile.php?filename=" + skill;
this.appendChild(image);
});
}
}
xhr.open("GET", "/API/skills.php");
xhr.send();
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);
});
}
}

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", "/API/getFile.php?filename=svg/" + svgName + ".svg");
xhr.open("GET", "/svg/" + svgName + ".svg");
xhr.send();
}
}

View file

@ -1,14 +0,0 @@
import * as basicLightbox from 'basiclightbox'
let images = document.querySelectorAll("img, .clickSpan");
for(let i = 0; i < images.length; i++){
let element = images[i];
if(!(element.getAttribute("data-noPreview") === "true")) {
element.onclick = () => {
const instance = basicLightbox.create(`
<img src="${element.getAttribute("src")}">
`);
instance.show();
}
}
}

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");

View file

@ -2,18 +2,12 @@ if(window.location['pathname'] == "/post.html"){
loadPost();
}
function getParameter(key) {
// Address of the current window
let address = window.location.search
// Returns a URLSearchParams object instance
let parameterList = new URLSearchParams(address)
// Returning the respected value associated
// with the provided key
return parameterList.get(key)
// 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() {
@ -26,7 +20,18 @@ async function loadPost() {
if(id == null) {
content.innerHTML = "<h1>404 - Post not found</h1>";
} else {
let post = await (await fetch("/API/getPost.php?id=" + id)).json();
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";

View file

@ -6,7 +6,7 @@
"author": "jonasled <git@jonasled.de>",
"license": "GPL-3.0-or-later",
"scripts": {
"compile": "concurrently \"yarn css\" \"yarn js\"",
"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\""

View file

@ -5,14 +5,10 @@
<title>404 - Page not found</title>
<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>

View file

@ -1,12 +0,0 @@
<?php
require "./lib/config.php";
$configValue = $_GET['name'];
switch ($configValue){
case "sitekey":
echo($sitekey);
break;
default:
echo("notFound");
}

View file

@ -1,12 +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);
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);
$contentType = curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
header('Content-Type: ' . $contentType);
echo($content);
$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);
}

View file

@ -1,55 +0,0 @@
<?php
require 'vendor/autoload.php';
require "./lib/config.php";
use GuzzleHttp\Client;
$responseJSON = [];
$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" => $ad["title"]["value"],
"price" => $ad["price"]["amount"]["value"] . ""
];
foreach($ad["link"] as $link) {
if($link["rel"] == "self-public-website") {
$element["link"] = $link["href"];
}
}
if(sizeof($ad["pictures"]["picture"]) > 0) {
foreach($ad["pictures"]["picture"][0]["link"] as $picture) {
if($picture["rel"] == "teaser") {
$element["previewImage"] = str_replace("https://i.ebayimg.com", "/API/ebayimg.php?url=", $picture["href"]);
}
if($picture["rel"] == "XXL") {
$element["image"] = str_replace("https://i.ebayimg.com", "/API/ebayimg.php?url=", $picture["href"]);
}
}
}
array_push($responseJSON, $element);
}
if(isset($_GET["count"])) {
echo sizeof($responseJSON);
die();
}
echo json_encode($responseJSON);

View file

@ -1,29 +0,0 @@
<?php
include "./lib/config.php";
include "./lib/mysql.php";
$position = $_GET['position'];
if($position == "index"){
$limit = $homeMaxPost;
} else if($position == "footer"){
$limit = $footerMaxPost;
} else {
die("wrong parameter");
}
$responseJSON = [];
$result = $conn->query("SELECT * FROM posts order by id desc limit $limit");
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$blogElement = [
"title" => $row["title"],
"id" => $row["id"],
"content" => $row["content"]
];
array_push($responseJSON, $blogElement);
}
}
header('Content-Type: application/json');
echo json_encode($responseJSON);

View file

@ -1,22 +0,0 @@
<?php
include("./lib/config.php");
require("./vendor/autoload.php");
use GuzzleHttp\Client;
$data = array(
'secret' => $secretkey,
'response' => $_POST['h-captcha-response']
);
$client = new Client();
$response = $client->post("https://hcaptcha.com/siteverify", [
"form_params" => $data
]);
$responseData = json_decode($response->getBody());
if($responseData->success) {
echo("$contactmail");
} else {
echo("Failed to verify Captcha");
}

View file

@ -1,24 +0,0 @@
<?php
include "./lib/config.php";
include "./lib/mysql.php";
$id = $conn->real_escape_string($_GET["id"]);
$result = $conn->query("SELECT * FROM posts WHERE id=$id");
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
} else {
die("Post not found");
}
$title = $row["title"];
$content = $row["content"];
$date = $row["date"];
$id = $row["id"];
header('Content-Type: application/json');
echo json_encode([
"title" => $title,
"content" => $content,
"date" => $date,
"id" => $id
]);

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

@ -1,39 +0,0 @@
<?php
require './vendor/autoload.php';
include("./lib/config.php");
include("./lib/mysql.php");
use GuzzleHttp\Client;
$data = array(
'secret' => $secretkey,
'response' => $_POST['h-captcha-response']
);
$client = new Client();
$response = $client->post("https://hcaptcha.com/siteverify", [
"form_params" => $data
]);
$responseData = json_decode($response->getBody());
if($responseData->success) {
$article =$conn->escape_string($_POST["pagename"]);
$name = $conn->escape_string($_POST["name"]);
$email = $conn->escape_string($_POST["email"]);
$comment = $conn->escape_string($_POST["comment"]);
$sql = "INSERT INTO comments (name, email, comment, article) VALUES ('$name', '$email', '$comment', '$article')";
if ($conn->query($sql) === TRUE) {
header("Location: " . $_SERVER["HTTP_REFERER"]);
} else {
echo "Error: " . $sql . "<br>" . $conn->error;
}
} else {
echo "Failed to verify captcha.";
}

View file

@ -1,23 +0,0 @@
<?php
include "./lib/config.php";
include "./lib/mysql.php";
include "./lib/getGravatar.php";
$article = $conn->real_escape_string($_GET["article"]);
$responseJSON = [];
$result = $conn->query("SELECT * FROM comments WHERE article='$article'");
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$commentElement = [
"name" => $row["name"],
"comment" => $row["comment"],
"gravatarURL" => get_gravatar($row["email"])
];
array_push($responseJSON, $commentElement);
}
}
header('Content-Type: application/json');
echo json_encode($responseJSON);

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;
}

View file

@ -1,24 +0,0 @@
<?php
include("./lib/config.php");
require 'vendor/autoload.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){
array_push($response, $skill["Key"]);
}
header("Content-Type: application/json");
echo json_encode($response);

View file

@ -1,35 +1,45 @@
<!DOCTYPE html >
<!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>Daneben habe ich auch viel Erfahrung mit Betriebssystemen. So setzt ich auf meinem Server auf Ubuntu und benutze
auch viel Windows Server.</p>
<p>Meine Programme veröffentliche ich meisten auf meiner GitLab Instanz. Diese ist unter&nbsp;<a
href="https://gitlab.jonasled.de/">gitlab.jonasled.de</a>&nbsp;zu finden.</p>
</div>
<jl-footer></jl-footer>
<script src="/js/script.js"></script>
<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>
</html>

View file

@ -18,7 +18,7 @@
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="/API/getFile.php?filename=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
@ -37,8 +37,8 @@
class="language-text">yes</code>
bestätigt.
</p>
<img src="/API/getFile.php?filename=img/debian_partition_method.jpg">
<img src="/API/getFile.php?filename=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="/API/getFile.php?filename=img/debian_scan_media.jpg">
<img src="/API/getFile.php?filename=img/debian_survey.jpg">
<img src="/API/getFile.php?filename=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="/API/getFile.php?filename=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="/API/getFile.php?filename=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,7 +139,7 @@ cd mailcow-dockerized
# nicht benötigt falls noch als root angemeldet aus dem vorherigen Schritt.
docker-compose up</code>
</pre>
<img src="/API/getFile.php?filename=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
@ -161,10 +161,10 @@ docker-compose up</code>
class="language-text">Mailboxes</code> können nun
Mailboxen angelegt werden.
</p>
<img src="/API/getFile.php?filename=img/mailcow_setup_mail.jpg">
<img src="/API/getFile.php?filename=img/mailcow_domain_setup.jpg"><br>
<img src="/API/getFile.php?filename=img/mailcow_domain_new_1.jpg">
<img src="/API/getFile.php?filename=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
@ -176,7 +176,7 @@ docker-compose up</code>
sich der Nutzer ins SOGo anmelden
um das Webmail zu nutzen oder mit einem Client wie Thunderbird anmelden.
</p>
<img src="/API/getFile.php?filename=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,8 +193,9 @@ 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="/API/getFile.php?filename=img/mailcow_dkim_webui.jpg">
<img src="/API/getFile.php?filename=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"></script>

View file

@ -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="/API/getFile.php?filename=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

View file

@ -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/API/getFile.php?filename=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="/API/getFile.php?filename=img/snowboy_no_mic.png">
<img src="/API/getFile.php?filename=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>

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

@ -10,24 +10,25 @@
<jl-header data-title="Bildquellen"></jl-header>
<div id="content">
<ul>
<li><span class="clickSpan" src='/API/getFile.php?filename=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='/API/getFile.php?filename=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='/API/getFile.php?filename=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"></script>
<script>
// Set document title to "Bildquellen - Jonas Leder"
document.title = "Bildquellen - Jonas Leder";
</script>
</body>

65
public/gpg.txt Normal file
View file

@ -0,0 +1,65 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGD0Q3ABEACgcu+jcRrymIEU2uNOr1/k5RZQAeREaS+mA07SFYMDLeWMjJDl
jC5dIjhKtI1e+HARJAYUKWvNKGZI3qlg5YEfRTdcG0DUlrO8agNVVytzNaDa1e5J
9X8pbMwnH1XvyohBmC2RAPhJ6/Gm/qxKb9kg1qaOUkb8GhJ/ENLyIqeJci3sWlGx
OBtD9+kd+9kj4El6rAMWsAp+sSMOTJA1bkk9FwdzuYQC65WTvVuA8bnMQ3WLAOK8
w0oxM3xD6bm/A9fFITn325QfZzHHxZVFnoeISeqBEdwC/JofX8SneyVQTSNV0TBq
FH3apgM9UOkNCXITJ0WbDLZLjPh/EDRofHle28Y6aI8RW6n54kKDVBFt+zqwYBx2
mRZO9Yyw/rFI5HFgbvqcIhJzThDz1esHnJvjueKl7NYO4iH2zla8jArOxZbtQYaC
wvqKw11RV1ZuG7DY/QRtw3fuO2GOwhwxV2XwYm0IOKk8ZpXRoU/dJjxlWIAOlxe7
lWq3uQqN6gbu2E60DpXqVNnFMMQHgZPoqUUFYNsYasl0H3mN9UWFE29DaLn3VaPA
fu2Wc/fHfwRH2QacKTHheqGMwL5z2AZsfp8//CynHdRqrG6EIn0cMvh3pQWi7LVK
vYM4TY8tulU9ms52b0sDx0GLQ6uIiGD109d8dczDjP0nQcr/R6ew9H73QQARAQAB
tB9Kb25hcyBMZWRlciA8am9uYXNAam9uYXNsZWQuZGU+iQJOBBMBCAA4FiEE1h1W
8lMhaI6qTwtBzDxIjiff9coFAmD0Q3ACGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC
F4AACgkQzDxIjiff9cr08Q/9Fc3fOBTZvtYML73cPJvfh/QZATu2DtKfMQaO5tX4
GrBY4S6UEJDZ2cCNfT/TpJwm5JRK1kje28W2Kx458J7QyKXojU56DOPJLL7YAAF9
bbyfTh74sUC2oi0+DZPoLpa8q9Jw+nUbNtXgWhyiGBs+q2MgX9YG8e+MjoEA9oPQ
IchR6HZeiPR2UKMb45dGz1UA1+dnUrluPfwmIWYH0P5wQyNUsTPsqewGkRXOZFF7
ehbupNwmg5hGsCtH6+rWrgpq0CxGAe1fyyZ1DDUqUUP2MJtxyWuWZaIPfowtyzQD
B/Hyz6wPCGEv8EKNuT0mYUSP2gYXe2cpmn64/eZTtI1v2dUnbBmZwDPeBWGtzYx8
Lgir1gcNNVKXgAlPpyCWEPt2oSNGYCS/F1AMBUd9xtx9VNWqePSwkwc+YYEQpvBv
CxNeloTVqu/hLoshy2RUeGJhNc2OahT0YhNIDkjZc7PaguEtsnXZBy1qWANSRAtR
XXQSminSCr+oL5O2fOZChE+8GkoCugOodKJqyDTYX5km4ia4iSbKZ/Deb15SP56m
HKux3MrS29gWG2BGCbcarg2TwuGY9cq3KYNeJbLgpei0hVtXomIr6lbeMVoR4REp
VNZQuAwSWx5WNt4+NotyhgopEWxqeFf8GfLLGzBaBky/EZ00RYqw+eV+IZFkzTTm
ESG0J0pvbmFzIExlZGVyIDxqb25hcy5sZWRlckBqb2Jyb3V0ZXIuY29tPokCTgQT
AQgAOBYhBNYdVvJTIWiOqk8LQcw8SI4n3/XKBQJiZ5tgAhsDBQsJCAcCBhUKCQgL
AgQWAgMBAh4BAheAAAoJEMw8SI4n3/XKncYP/21bjKTvXQuZgzW1rbOkK5PwZ/G2
eyjN071CoH9OX5nOuURnyBvcfD3ZBgNmcsxd7B16rHZHm35PgmV85mcTuweZo1Y9
JzWhL+D6ci5prVNDfu1omflDUDtG6IuArOEvo4nlP9fH0TZ0ny9ZMvMuNhwjD9eF
7FHeVhsBza1Qfogmi/KzF0kPWV3WR5zgxCISIafwp5yoHaRN7e4dU0+cu5p0lVzZ
lPMg8pLgmcWVFK1Jk8NtErUKaCtS1+M02Mk2wWLtTLT3vhkYaVbXnp+aa77+Yoxg
kpeDyO5X/qJKrQGSNivqIw6uOELgd2W6JKxvrHz8rXdJ9ZY5M8eh6B9FZJLzzOIp
G/YzlHgMSLGOx1tQ569ZVFyI4emeXQwKh4sfyCwcWyItxLdhNo9HkukUa6HdJMg2
z2S/MjBN6Ws8ePc5zKEMMS74yx2/EDBh/weHNtaBAtRk0+ngwfF7DXcJTsSyS+O7
ufVWh4E39thUoVDWVI6x7LevKoBkDSuxPX3cp799eGL0FpHdQcjKOh3wvC8Ltl5m
TmRRPGrQTQdBkrJiCgEElgYaH+dFHM/eFiIA6vCtXnH6+9VvfSpMBTz8dI/shwzv
DX9ZOo9VsZZmuehbRFSmd7K1yovg10TO+C6ip0GygYA0ycWYc+mHF+bifE5v0UPQ
hUjC9KB4hpnpIR6OuQINBGD0Q3ABEAC7yEXWqbSpFCcCs44pPSA/zRdEkO56Ckjc
aUwZmS079n676u42RXH8EV5J4uAixhR+R/IBEY4eJnmApPkwUEyt12uofPrS/oSc
OJAh7K8qUeqH8BsOfPZsAQEb6YHErampBUZ3ov1OZAHQFsVK5cIV5UFbGhswTcll
om84YpHmqB62BGIGNbogGPJ+yxRYmVVXoIEje0JC2rlsdAr1iW/sLXsyGK7/O5cF
L7h45t+gtCqUclYG4Rmnx9RO1WBDvmWrNgHjPGagC3UeIVn3ThAof+AG75HqQF2D
KH5EBG5VE9QQZsdnCuic5bJUt5XQS8KyRRX0E4WrJgtJ7XtDE5DNxIS4iDdhBulK
JoAFtmwyEpCOMMjpSwsm29jiMJ4Rhfat06BSwbaO+tbhNctE9k4eIO4ESyxu8Lkt
162Rz5EtfEA0/TmlZc77cMQfaP578l30LOdCHA0W5uqBMw4llDJh4SGQswjRBWQY
9Ly7He3vCo391AKlNG/P3krJA3llf6BFrZsxvW6K96LU2Uy4B585PGjmqsLeTtok
7BzbhZRyySpNvisu2Yz+jOSls7q1YQ+xT8EbGpp61u4lYmu0u8VV2jQqYOES7R5T
4eK9uMAqQjRuxRRnW/XZ0kVLHhgxVLzT1tSGNp048XgdlbUz/df/Y7unJ0/VLcKX
TyB4mz14qwARAQABiQI2BBgBCAAgFiEE1h1W8lMhaI6qTwtBzDxIjiff9coFAmD0
Q3ACGwwACgkQzDxIjiff9cqFbg//dZKQ20owxoMcmjVrzF/uBTJoJKEdYDB7WMd+
Il0Y16ltA+s61jwrP5vt0GVB4BjWhysGIPXYcoUGCyZq/xYdmfDgJXmz/RZjcJuj
XrW7ILSUdAueHTsqxAFL4N4tRdVRTV4oWlmBLd9gcpQRXmS6pwlcrXSB5aFj0IVb
oE6wy5gk+CS91vuHBgnPBz1X/mbsru6MVsdFyOCVjXfcJOcvDA6ru8NvkfJCTxn4
M/6ErocpMLGeas1tqPP2R65z+oCr+4h9n7LaDSYNigZLNUd+/V3rQ2gqB7VY4TeZ
LDnkx4oQgk5Q+CFbQQUy6o8uuYEj3o26K6/Oa9+UAXKMnucQFKPfYV02Morfx7XP
zVOiHfsJbz0S2yjTpt7YX4Y/Cfy0V8wlOz5+UFrn6JcTSwRXcZqGLY8FnpvYhFOK
L4Pgf00FqNVbdoPURIgi0BLs1wiEgu2F42IZBDQ2n90jH/JJJrW/xaZ6zZGatTeq
/dYGfc+AdL1TyVDz1/LB7Pl5+07HGRe8LsUrlD6+cSIJN0UidS44HV7V9LN3xui1
7/2tTbr+Xpy3lGiUTKGEroCD8bZ8Si0zCRsy9wXpCNPYMGIR+SROLBO7qqZztMDc
flA0aHpfO51VUoZTJGXjX8py9fM7qw8xRCFftQzQU5X0ywjkC0O+7l45eZyomuWk
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

BIN
public/img/bannerHome.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
public/img/bildHome.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
public/img/systeme/PC.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 KiB

BIN
public/img/systeme/epyc.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

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