From 02b26503e523a93f92923a22e71d6ac68326be4c Mon Sep 17 00:00:00 2001 From: Pascal Huber Date: Tue, 7 Jul 2026 13:57:34 +0200 Subject: [PATCH 1/6] replace basic auth with openid connect --- .env.development | 3 + .env.production | 4 +- README.md | 30 ++++-- dev/webdav/Dockerfile | 7 -- dev/webdav/README.md | 21 +--- dev/webdav/apache/Dockerfile | 16 +++ dev/webdav/apache/apache2.conf | 52 ++++++++++ dev/webdav/apache/test-oidc.py | 14 +++ dev/webdav/dex/certs/README.md | 7 ++ dev/webdav/dex/certs/dex.crt | 19 ++++ dev/webdav/dex/certs/dex.key | 28 +++++ dev/webdav/dex/config.yaml | 54 ++++++++++ dev/webdav/docker-compose.yml | 27 ++++- dev/webdav/nginx.conf | 35 ------- dev/webdav/test.sh | 182 +++++++++++++++++++++++++++++++++ src/components/EditSubject.vue | 2 +- src/components/NavBar.vue | 5 +- src/main.js | 1 + src/router.js | 20 ++-- src/store/actions.js | 117 +++++++++++++-------- src/store/getters.js | 13 ++- src/store/index.js | 3 +- src/store/mutations.js | 22 ++-- src/store/token.js | 54 ++++++++++ 24 files changed, 600 insertions(+), 136 deletions(-) create mode 100644 .env.development delete mode 100644 dev/webdav/Dockerfile create mode 100644 dev/webdav/apache/Dockerfile create mode 100644 dev/webdav/apache/apache2.conf create mode 100644 dev/webdav/apache/test-oidc.py create mode 100644 dev/webdav/dex/certs/README.md create mode 100644 dev/webdav/dex/certs/dex.crt create mode 100644 dev/webdav/dex/certs/dex.key create mode 100644 dev/webdav/dex/config.yaml delete mode 100644 dev/webdav/nginx.conf create mode 100755 dev/webdav/test.sh create mode 100644 src/store/token.js diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..9077f57 --- /dev/null +++ b/.env.development @@ -0,0 +1,3 @@ +VUE_APP_WEBDAV_URI="http://localhost:8080/dav/" +VUE_APP_OIDC_TOKEN_URL="http://localhost:8080/dex/token" +VUE_APP_OIDC_CLIENT_ID=votelog-webapp diff --git a/.env.production b/.env.production index a57a39f..7e015e9 100644 --- a/.env.production +++ b/.env.production @@ -1 +1,3 @@ -VUE_APP_WEBDAV_URI="https://dav.resolved.ch/" +VUE_APP_WEBDAV_URI="https://dav.resolved.ch/dav/" +VUE_APP_OIDC_TOKEN_URL="https://dav.resolved.ch/dex/token" +VUE_APP_OIDC_CLIENT_ID=votelog-webapp \ No newline at end of file diff --git a/README.md b/README.md index 541d8ac..3f6cece 100644 --- a/README.md +++ b/README.md @@ -3,27 +3,43 @@ A small Webapp I use to keep track of my votes in Switzerland and see which parties agree with me. +![VoteLog Screenshot](screenshot.png) +(those are not my votes, at least not all of them...) + ## Known Issues -- Doesn't handle Gegenentwürfe very well (multiple entries, preferences and links) +Functional: +- Doesn't handle *Gegenentwürfe* very well. + +Technical: +- Error handling for the WebDAV connection (fetching and storing data) could be + better. ## Features +Functional: - Show and compare results of Swiss referendums and initiatives - Provide reasoning for decision - Vote Category Overview -- Load and store data to WebDAV (no backend) -![VoteLog Screenshot](screenshot.png) -(those are not my votes, at least not all of them...) +Technical: +- Built with Vue.js, Vuex, Vue Router +- There is no (real) backend. Data is stored in WebDAV in + "bring-your-own-WebDAV manner. Note that if WebDAV and VoteLog are running on + different (sub)domains, CORS must be configured accordingly. Otherwise the + browser will block the connections. This disqualifies many managed solutions. + All the data is stored in a file called `votelog_.json` (multiple + users can share a WebDAV server). +- OpenID Connect is used for authentication. VoteLog stores the Token in + SessionStorage (which survives a refresh but is removed when the browser tab + is closed). ## Setup -### Build and run Development WebDAV server +### Build and run Development Apache2 (DAV, Proxy) and Dex (OIDC) ``` -docker build -t webdav-dev dev/webdav/ -docker-compose -f dev/webdav/docker-compose.yml up -d +docker compose -f dev/webdav/docker-compose.yml up -d --build ``` ### Install dependencies diff --git a/dev/webdav/Dockerfile b/dev/webdav/Dockerfile deleted file mode 100644 index ceac61d..0000000 --- a/dev/webdav/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM nginx:1.22 -RUN apt-get update && apt-get install -y nginx-extras apache2-utils -COPY ./nginx.conf /etc/nginx/conf.d/default.conf -RUN htpasswd -b -c /etc/nginx/dav_passwdfile cat dog -RUN rm /etc/nginx/sites-enabled/* -RUN mkdir /srv/webdav -RUN chown -R www-data: /srv/webdav diff --git a/dev/webdav/README.md b/dev/webdav/README.md index 1008e32..af43807 100644 --- a/dev/webdav/README.md +++ b/dev/webdav/README.md @@ -3,25 +3,12 @@ Build and start it: ```sh -docker build -t webdav-dev . -docker-compose -f docker-compose.yml up -d +docker compose up -d --build ``` Test it: -```sh -❯ cadaver http://localhost:8080/ -Authentication required for Restricted on server `localhost': -Username: cat -Password: dog -dav:/> put justafile.txt -Uploading justafile.txt to `/justafile.txt': -Progress: [=============================>] 100.0% of 3 bytes succeeded. -dav:/> ls -Listing collection `/': succeeded. - justafile.txt 3 Jan 17 11:14 -dav:/> rm justafile.txt -Deleting `justafile.txt': succeeded. -dav:/> exit -Connection to `localhost' closed. +```bash +./test.sh ``` + diff --git a/dev/webdav/apache/Dockerfile b/dev/webdav/apache/Dockerfile new file mode 100644 index 0000000..376344c --- /dev/null +++ b/dev/webdav/apache/Dockerfile @@ -0,0 +1,16 @@ +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + apache2 \ + libapache2-mod-auth-openidc \ + && rm -rf /var/lib/apt/lists/* + +RUN a2enmod dav dav_fs auth_openidc headers proxy proxy_http rewrite + +RUN mkdir -p /var/www/dav /var/lib/dav && \ + chown -R www-data:www-data /var/www/dav /var/lib/dav + +COPY apache2.conf /etc/apache2/sites-available/000-default.conf + +EXPOSE 80 +CMD ["apachectl", "-D", "FOREGROUND"] diff --git a/dev/webdav/apache/apache2.conf b/dev/webdav/apache/apache2.conf new file mode 100644 index 0000000..083db1e --- /dev/null +++ b/dev/webdav/apache/apache2.conf @@ -0,0 +1,52 @@ + + ServerName localhost + + # Fetch Dex's signing keys over the internal HTTPS listener (self-signed, + # dev-only cert). SSL validation is disabled because this cert isn't + # issued by a real CA — never do this for a real deployment. + OIDCOAuthVerifyJwksUri https://dex:5557/dex/keys + OIDCOAuthSSLValidateServer Off + OIDCOAuthRemoteUserClaim email + OIDCCryptoPassphrase "dev-only-passphrase-not-for-prod" + + # Browser-facing token endpoint stays on the PLAIN HTTP listener + ProxyPreserveHost On + ProxyPass /dex http://dex:5556/dex + ProxyPassReverse /dex http://dex:5556/dex + + + Header always set Access-Control-Allow-Origin "http://localhost:8001" + Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS" + Header always set Access-Control-Allow-Headers "Authorization, Content-Type" + RewriteEngine On + RewriteCond %{REQUEST_METHOD} OPTIONS + RewriteRule ^ - [R=200,L] + + + DavLockDB /var/lib/dav/DavLock + Alias /dav/ /var/www/dav/ + + DAV On + Options None + AllowOverride None + + Header always set Access-Control-Allow-Origin "http://localhost:8001" + Header always set Access-Control-Allow-Methods "GET, PUT, DELETE, OPTIONS" + Header always set Access-Control-Allow-Headers "Authorization, Content-Type, Depth" + + AuthType oauth20 + + Require method OPTIONS + + Require valid-user + Require expr "%{REQUEST_URI} == '/dav/votelog_%{REMOTE_USER}.json'" + Require method GET PUT DELETE + + + + + RedirectMatch 404 ^/$ + + ErrorLog /proc/self/fd/2 + CustomLog /proc/self/fd/1 combined + \ No newline at end of file diff --git a/dev/webdav/apache/test-oidc.py b/dev/webdav/apache/test-oidc.py new file mode 100644 index 0000000..fb3f00c --- /dev/null +++ b/dev/webdav/apache/test-oidc.py @@ -0,0 +1,14 @@ +import base64 +import json +import os + +token = os.environ["TOKEN"] +parts = token.split(".") + +print("parts:", len(parts)) + +if len(parts) >= 2: + payload = parts[1] + payload += "=" * (-len(payload) % 4) + print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2)) + diff --git a/dev/webdav/dex/certs/README.md b/dev/webdav/dex/certs/README.md new file mode 100644 index 0000000..1d4203b --- /dev/null +++ b/dev/webdav/dex/certs/README.md @@ -0,0 +1,7 @@ +# Certs generated with + + mkdir -p dex/certs + openssl req -x509 -newkey rsa:2048 -nodes \ + -keyout dex/certs/dex.key -out dex/certs/dex.crt \ + -days 3650 -subj "/CN=dex" + chmod o+r dex/certs/* \ No newline at end of file diff --git a/dev/webdav/dex/certs/dex.crt b/dev/webdav/dex/certs/dex.crt new file mode 100644 index 0000000..a7a6220 --- /dev/null +++ b/dev/webdav/dex/certs/dex.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC/TCCAeWgAwIBAgIUH+ri5OEoNkrHzCzFpXyS25dfViUwDQYJKoZIhvcNAQEL +BQAwDjEMMAoGA1UEAwwDZGV4MB4XDTI2MDcwNzE3MTQ1N1oXDTM2MDcwNDE3MTQ1 +N1owDjEMMAoGA1UEAwwDZGV4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAn02Hfl6yPPL4OQQwHqALYXsVt55S+dIZS1XKNS39PflKGlDIdcjE0JGdPUcc +d78gewM9jPGrJjl13UUkBgDSYekMz/js3UvAXn3VJV9JzVYpIBB+0BE/Za72u+77 +PCGci7m7qIrWcBxJnInmyxr6PL3H8kOw79RVDuI+AhaFPhy0LGDWtDwiYNXkz0qC +d0zwbfEvH+cpRVwYDEhVxl6JSmRJEQBYaxuxPkx2d2jiBW+MjppYrN3VMwYzfKTl +0paCw/br6sQesEABQhHclnLwAkO+JkK4XeIHTqWf9AMLeifcaJymLRmdneI5kt8V +zy9xAn1h/JsJU0YSsvwCnTgRFwIDAQABo1MwUTAdBgNVHQ4EFgQUS2gHTUVgpf20 +qglMpet1nqhIFf0wHwYDVR0jBBgwFoAUS2gHTUVgpf20qglMpet1nqhIFf0wDwYD +VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAVT5+Vu5oDiebFC7gxYIR +XmZapSBRyoTckn70RLSbV0RmKT5NtI+MY7MnISaVKo2LHLa6Kxm7nddqww26L3rp +OvglRQul42eh9q+cO0+fwIsSYMPwb8CCpvSFHdhw3Jl/FaLFSJVSRf6DrXT8blhd +SzRKr0bnQbBkojirFPliCFKR7wMrryntQmnxmob5xbQxWXM6q6UMC9HMgZ8F832o +YWK/xf6UyvIh3ur/yhf3b8k1IImbIvNxbxI9xAnwIVdbeuJPBisr/CyKEX6arymz +OxrK/wIfdoM9FJVfS96zhpfQ0jWT6NNPsaHN2k8AV0Eq1ZQqWqtzxYGydh/opwl9 +cw== +-----END CERTIFICATE----- diff --git a/dev/webdav/dex/certs/dex.key b/dev/webdav/dex/certs/dex.key new file mode 100644 index 0000000..e57f563 --- /dev/null +++ b/dev/webdav/dex/certs/dex.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCfTYd+XrI88vg5 +BDAeoAthexW3nlL50hlLVco1Lf09+UoaUMh1yMTQkZ09Rxx3vyB7Az2M8asmOXXd +RSQGANJh6QzP+OzdS8BefdUlX0nNVikgEH7QET9lrva77vs8IZyLubuoitZwHEmc +iebLGvo8vcfyQ7Dv1FUO4j4CFoU+HLQsYNa0PCJg1eTPSoJ3TPBt8S8f5ylFXBgM +SFXGXolKZEkRAFhrG7E+THZ3aOIFb4yOmlis3dUzBjN8pOXSloLD9uvqxB6wQAFC +EdyWcvACQ74mQrhd4gdOpZ/0Awt6J9xonKYtGZ2d4jmS3xXPL3ECfWH8mwlTRhKy +/AKdOBEXAgMBAAECggEAFBzzTwLAyU46BeTf+HXnifLC7rCING+Q7wCRa753O2Rm +tUTe0nQ7WUQRUMPSNdJFVRK3Kjz3CXf2yC2rGzDuXxtQGVAKA/TFAIktqK7LwCex +QJGkmTucQAfjsh29zb1GqSOVrHV/Fp+TsbZ9k8Y/svyZKp7eT6GE7cCl6JiUUR+W +p2xYKa0Cp2SzitNokMyMaz+svhz2jySqBRE0jCTSzOOS96txymQUwlEkvsIrt80d +Rfax/jwkqPZfnLg42LlVadIuDylmBBMMwbB79MHRrJ2680tRyXAY46OZ6Zv73ssT +sFoIPxjkjjOt4R9QOPb+i6rOAezUOMlEalwKDRypSQKBgQDJ8oBf27l+m5BK+yuZ +KM4dAKXB6d2JugO9XdIdj825J2VPdSNBns1mj40IiskdW0W0M5uJBaqumMFrESIC +4+qlAaWc92vG2c6X3TNb6n902wZkDQtR7tYr/6aqtMrQHPRHHMYsxZcsGM5A6RCM +yHaNu7DwNEC1sSfEiFt4FhAwmQKBgQDJ8QRRuBDmXlgemZosY+LEBp9g159kC+Mr +fkSrC7hOwtffkQ2p4mpROq9uq3xgGae/K04DzU/NkvNhr6CZy6iw2N5sGXZo5qsO +IjxdhKaZ6HfygBv4gGgHEEyseSeyKLdRzmgVOMHcy7/kxqqW8KAfC2q8SmxMIIxw +nM6Iqr5tLwKBgQCFQyhhmU5D9QX35N01WY8B5n57gwc1LnHvUW0pIs8fwpaBI4xb +bh6e8lWf8G2cmLFbo76cYgfq5VSlgY3PGHWr43VVKpSNiQdigY61tf3br/j8pvr0 +W0YrjK1/8oPvZ1vvXVaNDuqeJ9RbUzEfpd1N1DCDogednkTe1Rh/Gxgd6QKBgBM4 +q66TlJg41q5i9HfRMh5yeu0e2P8O2pqjNCoLvYlRpnaTOfe3o2MD8PrZu8bx6jOa +DZzzB+uLVzsvGlxJNE9Q9SrY6ZpmilWYEKLeI3V22SPt6bunjT9O391y0sJ0ea6B +tQqAEoPPqP4/orEnSjZqQciWVOSSHIwDgO0lQ2blAoGAEDF7z9pHndzDEbwo4Hqa +b3oGyB080T1CMkTvWf+7uRYuL2aGVwfVbfNK+3XsCbGlgsbqbMxZ/Q2a9o4tdqUV +AuCMFVRiM1+ff5YhD3xT3A4k4DsiyQ0xlEHj6Rt0nvjuilnAZ4N1idfKiSZyZDDD +WDrJhQrx65TS9GEHRlJIfRQ= +-----END PRIVATE KEY----- diff --git a/dev/webdav/dex/config.yaml b/dev/webdav/dex/config.yaml new file mode 100644 index 0000000..59e5176 --- /dev/null +++ b/dev/webdav/dex/config.yaml @@ -0,0 +1,54 @@ +issuer: http://localhost:8080/dex + +storage: + type: sqlite3 + config: + file: /var/dex/dex.db + +web: + http: 0.0.0.0:5556 # plain HTTP — used by the browser/SPA via Apache proxy + https: 0.0.0.0:5557 # HTTPS — INTERNAL ONLY, just for Apache's JWKS fetch + tlsCert: /etc/dex/certs/dex.crt + tlsKey: /etc/dex/certs/dex.key + + +# Allow password grants against the built-in local user DB +oauth2: + passwordConnector: local + +# Enable the local ("virtual") identity provider +enablePasswordDB: true + +staticPasswords: +# - email: "alice@example.com" +# # bcrypt hash of "password123" — see step 5 to generate your own +# hash: "$2a$10$L4nMuLuTIzHTQaVUnzNy7uY7C4rD26jXNQV9BF1cVn/dTFpk9UzS6" +# username: "alice" +# userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" + + - email: dog@example.com + username: dog + userID: "b42f2334-79e4-11f1-8b8f-10ffe0360572" + hash: "$2y$10$Pg3c9XaIAHBFCsWRaQAiuuwQoVxwk1bbjGVFmND8lYhJFxCt/gUfa" + + - email: cat@example.com + username: cat + userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" + # NOTE: generate hash: + # docker run --rm httpd:2.4 htpasswd -nbBC 10 "" "cat" | cut -d: -f2 + hash: "$2y$10$jgn7adGyEWAlAYUcp0v1zOn.E6AXpxA7gHZtzpVqvf8gy2GaEnlv6" + +staticClients: + # - id: webdav-votelog + # name: WebDAV VoteLog + # public: true + # redirectURIs: + # - https://localhost:8001/callback + # - https://localhost:8001/ + - id: votelog-webapp + name: WebDAV VoteLog + # secret: dev-only-secret-change-me + # public: false + public: true + redirectURIs: + - "http://localhost:8001/callback" \ No newline at end of file diff --git a/dev/webdav/docker-compose.yml b/dev/webdav/docker-compose.yml index c1422d4..5586f67 100644 --- a/dev/webdav/docker-compose.yml +++ b/dev/webdav/docker-compose.yml @@ -1,7 +1,24 @@ -version: '3' services: - webdav: - image: webdav-dev - restart: always + dex: + image: dexidp/dex:v2.39.1 + command: ["dex", "serve", "/etc/dex/config.yaml"] + volumes: + - ./dex/config.yaml:/etc/dex/config.yaml:ro + - ./dex/certs:/etc/dex/certs:ro + networks: [devnet] + + apache-dav: + build: ./apache ports: - - '8080:80' + - "8080:80" + volumes: + - dav-data:/var/www/dav + depends_on: [dex] + networks: [devnet] + +networks: + devnet: {} + +volumes: + dex-data: {} + dav-data: {} diff --git a/dev/webdav/nginx.conf b/dev/webdav/nginx.conf deleted file mode 100644 index 427a635..0000000 --- a/dev/webdav/nginx.conf +++ /dev/null @@ -1,35 +0,0 @@ -server { - listen 80; - root /srv/webdav/; - - location / { - - if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Origin' '*'; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT, PROPFIND'; - add_header 'Access-Control-Allow-Headers' 'Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization,X-CSRF-Token,Depth' always; - add_header 'Access-Control-Allow-Credentials' 'true'; - add_header 'Access-Control-Max-Age' 1728000; - add_header 'Content-Type' 'text/plain; charset=utf-8'; - add_header 'Content-Length' 0; - return 204; - } - - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT, PROPFIND' always; - add_header 'Access-Control-Allow-Credentials' 'true'; - add_header 'Access-Control-Allow-Headers' 'Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization,X-CSRF-Token,Depth' always; - add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; - - dav_methods PUT DELETE MKCOL COPY MOVE; - dav_ext_methods PROPFIND OPTIONS; - dav_access user:rw group:rw all:rw; - - client_max_body_size 0; - create_full_put_path on; - client_body_temp_path /tmp/; - - auth_basic "Restricted"; - auth_basic_user_file /etc/nginx/dav_passwdfile; - } -} diff --git a/dev/webdav/test.sh b/dev/webdav/test.sh new file mode 100755 index 0000000..f459f93 --- /dev/null +++ b/dev/webdav/test.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE="http://localhost:8080" +CLIENT_ID="votelog-webapp" +EMAIL="cat@example.com" +PASSWORD="cat" +FILE="votelog_${EMAIL}.json" +URL="${BASE}/dav/${FILE}" + +PASS=0 +FAIL=0 +STATUS="" +BODY="" + +separator() { + printf '%s\n' "------------------------------------------------------------" +} + +header() { + echo + separator + echo "TEST: $1" + separator +} + +# Performs a curl request. Captures the HTTP status into $STATUS +# and the response body into $BODY for the following assertions. +request() { + local body_file + body_file=$(mktemp) + STATUS=$(curl -s -o "$body_file" -w '%{http_code}' "$@") + BODY=$(cat "$body_file") + rm -f "$body_file" +} + +# Asserts $STATUS matches one of the given acceptable codes. +expect_status() { + local label="$1"; shift + local code + for code in "$@"; do + if [[ "$STATUS" == "$code" ]]; then + echo "[PASS] ${label}: status ${STATUS}" + PASS=$((PASS + 1)) + return + fi + done + echo "[FAIL] ${label}: got status ${STATUS}, expected one of: $*" + echo " body: ${BODY}" + FAIL=$((FAIL + 1)) +} + +# Asserts the full JSON body equals the expected JSON (key order +# and whitespace insensitive). +expect_json_body() { + local label="$1" expected="$2" + local actual_canon expected_canon + actual_canon=$(echo "$BODY" | jq -Sc . 2>/dev/null || echo "INVALID_JSON") + expected_canon=$(echo "$expected" | jq -Sc .) + + if [[ "$actual_canon" == "$expected_canon" ]]; then + echo "[PASS] ${label}: body matches expected content" + PASS=$((PASS + 1)) + else + echo "[FAIL] ${label}: body content mismatch" + echo " expected: ${expected_canon}" + echo " actual: ${actual_canon}" + FAIL=$((FAIL + 1)) + fi +} + +# Asserts a single JSON field equals an expected value. +expect_json_field() { + local label="$1" field="$2" expected="$3" + local actual + actual=$(echo "$BODY" | jq -r "$field" 2>/dev/null || echo "") + if [[ "$actual" == "$expected" ]]; then + echo "[PASS] ${label}: ${field} == '${expected}'" + PASS=$((PASS + 1)) + else + echo "[FAIL] ${label}: ${field} == '${actual}', expected '${expected}'" + FAIL=$((FAIL + 1)) + fi +} + +header "1. Obtain id_token via password grant" +request -X POST "${BASE}/dex/token" \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'grant_type=password' \ + --data-urlencode "client_id=${CLIENT_ID}" \ + --data-urlencode 'scope=openid email' \ + --data-urlencode "username=${EMAIL}" \ + --data-urlencode "password=${PASSWORD}" +expect_status "Token request" 200 + +TOKEN=$(echo "$BODY" | jq -r '.id_token // empty' 2>/dev/null || echo "") +if [[ -z "$TOKEN" ]]; then + echo "[FAIL] Token request: no id_token in response (body: ${BODY})" + FAIL=$((FAIL + 1)) + echo "Aborting: cannot continue without a token." + exit 1 +fi +echo "[PASS] Token request: id_token present (${TOKEN:0:20}...)" +PASS=$((PASS + 1)) + +header "2. Cleanup: remove file from a previous run, if present" +request -X DELETE "${URL}" -H "Authorization: Bearer ${TOKEN}" +expect_status "Cleanup DELETE" 204 404 + +header "3. PUT file (create)" +request -X PUT "${URL}" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H 'Content-Type: application/json' \ + --data '{}' +expect_status "PUT create" 201 + +header "4. GET file and verify initial content" +request -H "Authorization: Bearer ${TOKEN}" "${URL}" +expect_status "GET after create" 200 +expect_json_body "GET after create" '{}' + +header "5. PUT file (overwrite with votes A)" +VOTES_A='{"votes":["yes","no","yes"]}' +request -X PUT "${URL}" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H 'Content-Type: application/json' \ + --data "${VOTES_A}" +expect_status "PUT overwrite" 204 + +header "6. GET file and verify overwritten content" +request -H "Authorization: Bearer ${TOKEN}" "${URL}" +expect_status "GET after PUT overwrite" 200 +expect_json_body "GET after PUT overwrite" "${VOTES_A}" + +header "7. Negative: POST is not supported for writes (expect 403)" +request -X POST "${URL}" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H 'Content-Type: application/json' \ + --data '{"votes":["yes","yes","yes"]}' +expect_status "POST rejected" 403 + +header "8. GET file and verify content is unchanged after rejected POST" +request -H "Authorization: Bearer ${TOKEN}" "${URL}" +expect_status "GET after rejected POST" 200 +expect_json_body "GET after rejected POST" "${VOTES_A}" + +header "9. Negative: request with no token" +request "${URL}" +expect_status "GET without token" 401 + +header "10. Negative: PUT someone else's file" +request -X PUT "${BASE}/dav/votelog_dog@example.com.json" \ + -H "Authorization: Bearer ${TOKEN}" \ + --data '{"hack":"attempt"}' +expect_status "PUT other user's file" 401 + +header "11. Negative: wrong password" +request -X POST "${BASE}/dex/token" \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'grant_type=password' \ + --data-urlencode "client_id=${CLIENT_ID}" \ + --data-urlencode 'scope=openid email' \ + --data-urlencode "username=${EMAIL}" \ + --data-urlencode 'password=wrongpassword' +expect_status "Token request with wrong password" 401 +expect_json_field "Token request with wrong password" '.error' 'access_denied' + +header "12. CORS preflight check (simulating browser from localhost:8001)" +request -X OPTIONS "${URL}" \ + -H "Origin: http://localhost:8001" \ + -H "Access-Control-Request-Method: PUT" \ + -H "Access-Control-Request-Headers: authorization,content-type" +expect_status "CORS preflight" 200 + +echo +separator +echo "SUMMARY: ${PASS} passed, ${FAIL} failed" +separator + +if [[ "$FAIL" -gt 0 ]]; then + exit 1 +fi \ No newline at end of file diff --git a/src/components/EditSubject.vue b/src/components/EditSubject.vue index 8723786..6a4bcc6 100644 --- a/src/components/EditSubject.vue +++ b/src/components/EditSubject.vue @@ -91,7 +91,7 @@ class="btn btn-primary" @click="changeVote" > - Fertig + Speichern diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index c6ad0e6..4c732d5 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -50,7 +50,7 @@ @@ -101,4 +101,7 @@ export default { .swiss-logo { height: 30px; } +.unsaved-changes { + color: #dd2222; +} diff --git a/src/main.js b/src/main.js index 1eb3958..e7f8fa8 100644 --- a/src/main.js +++ b/src/main.js @@ -7,6 +7,7 @@ import store from '@/store/'; import router from '@/router.js'; import '@/icons.js'; +await store.dispatch('restoreSession'); const app = createApp(Index); app.use(store); app.use(router); diff --git a/src/router.js b/src/router.js index ae03872..f2f77a9 100644 --- a/src/router.js +++ b/src/router.js @@ -76,24 +76,16 @@ let defaultTerm = () => { }; router.beforeEach((to, from, next) => { - if (to.path == '/') { + if ( + to.matched.some((record) => record.meta.requiresAuth) && + !store.getters.isLoggedIn() + ) { + next({ name: 'login' }); + } else if (to.path == '/') { next('/' + defaultTerm()); } else { next(); } }); -router.beforeEach((to, from, next) => { - store.dispatch('init').then(() => { - if ( - to.matched.some((record) => record.meta.requiresAuth) && - !store.getters.isLoggedIn() - ) { - next({ name: 'login' }); - } else { - next(); - } - }); -}); - export default router; diff --git a/src/store/actions.js b/src/store/actions.js index f856faf..df931f2 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -1,71 +1,106 @@ -import { createClient } from 'webdav'; +import { createClient, AuthType } from 'webdav'; import { Answer } from '@/Answer.js'; import router from '@/router.js'; +import { fetchToken, computeExpiresAt, persistSession } from './token.js'; const actions = { - async init(context) { - if (!this.getters.isLoggedIn()) { - let webDav = sessionStorage.getItem('webDav'); - let userName = sessionStorage.getItem('userName'); - let password = sessionStorage.getItem('password'); - if (webDav != 'null' && webDav != undefined) { - try { - await context.dispatch('login', { - webDav: webDav, - userName: userName, - password: password, - }); - context.dispatch('getData'); - } catch (e) { - sessionStorage.clear(); - } - } - } - if (this.getters.isLoggedIn() && !this.getters.hasFetchedData()) { - await context.dispatch('getData', this.getters.getConnection()); + restoreSession(context) { + const userName = sessionStorage.getItem('userName'); + const webDav = sessionStorage.getItem('webDav'); + const idToken = sessionStorage.getItem('idToken'); + const expiresAt = Number(sessionStorage.getItem('tokenExpiresAt')); + + if (!userName || !webDav || !idToken || !expiresAt) return; + + console.log('Restoring Session (unless expired)'); + if (Date.now() >= expiresAt) { + sessionStorage.clear(); + return; } - }, - async login(context, connection) { - const client = createClient(connection.webDav, { - username: connection.userName, - password: connection.password, + + const client = createClient(webDav, { + authType: AuthType.Token, + token: { token_type: 'Bearer', access_token: idToken }, }); - await client.getDirectoryContents('/'); + context.commit('SET_CLIENT', client); - context.commit('SET_CONNECTION', connection); - sessionStorage.setItem('webDav', connection.webDav); - if (connection.userName) { - sessionStorage.setItem('userName', connection.userName); - } + context.commit('SET_USER_NAME', userName); + context.commit('SET_WEBDAV', webDav); + context.commit('SET_TOKEN_EXPIRES_AT', expiresAt); - if (connection.password) { - sessionStorage.setItem('password', connection.password); - } context.dispatch('getData'); }, + async login(context, credentials) { + const tokenData = await fetchToken( + credentials.userName, + credentials.password, + ); + const expiresAt = computeExpiresAt(tokenData.expires_in); + + const client = createClient(credentials.webDav, { + authType: AuthType.Token, + token: { + token_type: 'Bearer', + access_token: tokenData.id_token, // JWT — locally verifiable by Apache + }, + }); + + context.commit('SET_CLIENT', client); + context.commit('SET_USER_NAME', credentials.userName); + context.commit('SET_WEBDAV', credentials.webDav); + context.commit('SET_TOKEN_EXPIRES_AT', expiresAt); + + persistSession( + credentials.userName, + credentials.webDav, + tokenData.id_token, + expiresAt, + ); + }, async getData(context) { if (context.getters.isLoggedIn()) { try { const client = context.getters.getClient(); - const content = await client.getFileContents('/votelog.json', { - format: 'text', - }); + const userName = context.getters.getUserName(); + const content = await client.getFileContents( + '/votelog_' + userName + '.json', + { + format: 'text', + }, + ); context.commit('SET_DATA', { votes: JSON.parse(content) }); } catch (error) { - context.commit('SET_DATA', { votes: [] }); + if (error.status == '404') { + console.log('Status 404, must be a new user! *excited*'); + console.log('hi <3'); + console.log( + 'Let me create an empty file for you, just to ensure your WebDAV works.', + ); + context.commit('SET_DATA', { votes: [] }); + context.dispatch('sendData'); + } else { + console.log('Failed to fetch data.'); + console.log(error); + throw new Error( + 'Failed to fetch your data, sorry:' + error, + ); + } } } }, async sendData(context) { const client = context.getters.getClient(); let data = JSON.stringify(context.getters.getUserVotes()); - await client.putFileContents('/votelog.json', data, { + const userName = context.getters.getUserName(); + // FIXME: storing data can go wrong, handle different types of errors + await client.putFileContents('/votelog_' + userName + '.json', data, { contentLength: false, overwrite: true, }); context.commit('UNSET_UNSAVEDCHANGES'); }, setVote(context, vote) { + console.log('set vote: ' + JSON.stringify(vote)); const index = this.state.userVotes.findIndex((e) => e.id == vote.id); if (vote.answer == Answer.Novote && vote.reasoning == undefined) { if (index !== -1) { @@ -77,8 +112,10 @@ const actions = { } else if (index !== -1) { context.commit('UPDATE_VOTE', { index: index, vote: vote }); } else { + console.log('ADD_VOTE'); context.commit('ADD_VOTE', vote); } + context.dispatch('sendData'); }, logout(context) { sessionStorage.clear(); diff --git a/src/store/getters.js b/src/store/getters.js index b631f39..9f618f4 100644 --- a/src/store/getters.js +++ b/src/store/getters.js @@ -14,12 +14,23 @@ const getters = { } return state.terms.find((term) => term.hash == thash); }, + // FIXME: check expired tokens + // isTokenExpired(state) { + // if (!state.tokenExpiresAt) return true; + // return Date.now() >= state.tokenExpiresAt; + // }, + // tokenExpiresSoon: + // (state) => + // (marginMs = 30_000) => { + // if (!state.tokenExpiresAt) return true; + // return Date.now() >= state.tokenExpiresAt - marginMs; + // }, getNextTermHash: (state) => (term_id) => state.terms.find((term) => term.id == term_id + 1)?.hash, getPrevTermHash: (state) => (term_id) => state.terms.find((term) => term.id == term_id - 1)?.hash, - getConnection: (state) => () => state.connection, getTermHash: (state) => () => state.votes[2].hash, + getUserName: (state) => () => state.userName, getUserVotes: (state) => () => state.userVotes, getUserVote: (state) => (subjectId) => state.userVotes?.find((vote) => vote.id == subjectId), diff --git a/src/store/index.js b/src/store/index.js index 20267b1..c722fe9 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -10,8 +10,9 @@ import mutations from './mutations.js'; const store = createStore({ state() { return { - connection: undefined, client: undefined, + webDav: undefined, + userName: undefined, error: undefined, fetchedData: false, userVotes: undefined, diff --git a/src/store/mutations.js b/src/store/mutations.js index 2694ea2..a29b645 100644 --- a/src/store/mutations.js +++ b/src/store/mutations.js @@ -1,10 +1,18 @@ +import { terms } from '@/data.js'; + const mutations = { SET_DATA(state, payload) { state.userVotes = payload.votes; state.fetchedData = true; }, - SET_CONNECTION(state, payload) { - state.connection = payload; + SET_TOKEN_EXPIRES_AT(state, expiresAt) { + state.tokenExpiresAt = expiresAt; + }, + SET_USER_NAME(state, payload) { + state.userName = payload; + }, + SET_WEBDAV(state, payload) { + state.webDav = payload; }, SET_CLIENT(state, client) { state.client = client; @@ -23,17 +31,19 @@ const mutations = { state.userVotes = [...state.userVotes, vote]; state.unsavedChanges = true; }, - // SET_PERIOD(state, i){ - // state.period.setFullYear(state.period.getFullYear() + i); - // }, DELETE_VOTE(state, index) { state.userVotes.splice(index, 1); state.unsavedChanges = true; }, LOGOUT(state) { + console.log('LOGOUT'); state.client = undefined; + state.webDav = undefined; + state.userName = undefined; + state.error = undefined; + state.fetchedData = false; state.userVotes = undefined; - state.connection = undefined; + state.unsavedChanges = false; }, }; diff --git a/src/store/token.js b/src/store/token.js new file mode 100644 index 0000000..c98dda8 --- /dev/null +++ b/src/store/token.js @@ -0,0 +1,54 @@ +/** + * Requests an id_token from the OIDC provider using the Resource Owner + * Password Credentials grant. + * + * @param {string} username + * @param {string} password + * @returns {Promise<{id_token: string, access_token: string, expires_in: number}>} + */ +export async function fetchToken(username, password) { + const response = await fetch(process.env.VUE_APP_OIDC_TOKEN_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'password', + client_id: process.env.VUE_APP_OIDC_CLIENT_ID, + scope: 'openid profile email', + username, + password, + }), + }); + + if (!response.ok) { + const message = await response.text(); + throw new Error(`Login failed: ${message}`); + } + + return response.json(); +} + +/** + * Converts the OIDC provider's relative "expires_in" (seconds) into an + * absolute epoch-millisecond timestamp, which is what's actually useful + * to compare against later — "expires_in" alone is only meaningful at + * the instant the token was issued. + * + * @param {number} expiresInSeconds + * @returns {number} epoch ms when the token expires + */ +export function computeExpiresAt(expiresInSeconds) { + return Date.now() + expiresInSeconds * 1000; +} + +/** + * Persists just enough state in sessionStorage to survive a page refresh. + * Note: sessionStorage is cleared when the tab closes, unlike localStorage. + */ +export function persistSession(userName, webDav, idToken, expiresAt) { + sessionStorage.setItem('userName', userName); + sessionStorage.setItem('idToken', idToken); + sessionStorage.setItem('webDav', webDav); + sessionStorage.setItem('tokenExpiresAt', String(expiresAt)); +} From 95b6eceb86622fe7fa36d779f3d318c43f01c201 Mon Sep 17 00:00:00 2001 From: Pascal Huber Date: Wed, 8 Jul 2026 16:25:21 +0200 Subject: [PATCH 2/6] update packages --- .editorconfig | 9 + .env.development | 6 +- .env.production | 6 +- .eslintcache | 1 + .eslintrc.js | 98 - .oxlintrc.json | 10 + .prettierrc.json | 7 + README.md | 3 +- babel.config.js | 15 - eslint.config.js | 31 + index.html | 13 + jsconfig.json | 9 + package-lock.json | 24112 ++++------------------ package.json | 71 +- src/Answer.js | 102 +- src/components/App.vue | 10 +- src/components/EditSubject.vue | 67 +- src/components/HeaderRow.vue | 83 +- src/components/ImportanceSymbol.vue | 14 +- src/components/Index.vue | 6 +- src/components/LoginForm.vue | 40 +- src/components/NavBar.vue | 23 +- src/components/PercentageValue.vue | 20 +- src/components/ShowSubject.vue | 61 +- src/components/StatsRow.vue | 24 +- src/components/SvgIcon.vue | 4 +- src/components/VotesTable.vue | 120 +- src/components/VotesTableCategories.vue | 115 +- src/components/VotesTableCategory.vue | 56 +- src/components/VotesTableSubject.vue | 104 +- src/data.js | 157 +- src/icons.js | 52 +- src/main.js | 26 +- src/router.js | 54 +- src/store/actions.js | 133 +- src/store/getters.js | 17 +- src/store/index.js | 16 +- src/store/mutations.js | 52 +- src/store/token.js | 22 +- vite.config.js | 19 + 40 files changed, 4716 insertions(+), 21072 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintcache delete mode 100644 .eslintrc.js create mode 100644 .oxlintrc.json create mode 100644 .prettierrc.json delete mode 100644 babel.config.js create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 jsconfig.json create mode 100644 vite.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e7c022b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +max_line_length = 100 + diff --git a/.env.development b/.env.development index 9077f57..8e815f7 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,3 @@ -VUE_APP_WEBDAV_URI="http://localhost:8080/dav/" -VUE_APP_OIDC_TOKEN_URL="http://localhost:8080/dex/token" -VUE_APP_OIDC_CLIENT_ID=votelog-webapp +VITE_WEBDAV_URI="http://localhost:8080/dav/" +VITE_OIDC_TOKEN_URL="http://localhost:8080/dex/token" +VITE_OIDC_CLIENT_ID=votelog-webapp diff --git a/.env.production b/.env.production index 7e015e9..f13b21a 100644 --- a/.env.production +++ b/.env.production @@ -1,3 +1,3 @@ -VUE_APP_WEBDAV_URI="https://dav.resolved.ch/dav/" -VUE_APP_OIDC_TOKEN_URL="https://dav.resolved.ch/dex/token" -VUE_APP_OIDC_CLIENT_ID=votelog-webapp \ No newline at end of file +VITE_WEBDAV_URI="https://dav.resolved.ch/dav/" +VITE_OIDC_TOKEN_URL="https://dav.resolved.ch/dex/token" +VITE_OIDC_CLIENT_ID=votelog-webapp \ No newline at end of file diff --git a/.eslintcache b/.eslintcache new file mode 100644 index 0000000..a6a7b6e --- /dev/null +++ b/.eslintcache @@ -0,0 +1 @@ +[{"/home/pascal/git/votelog/eslint.config.js":"1","/home/pascal/git/votelog/src/Answer.js":"2","/home/pascal/git/votelog/src/components/App.vue":"3","/home/pascal/git/votelog/src/components/EditSubject.vue":"4","/home/pascal/git/votelog/src/components/HeaderRow.vue":"5","/home/pascal/git/votelog/src/components/ImportanceSymbol.vue":"6","/home/pascal/git/votelog/src/components/Index.vue":"7","/home/pascal/git/votelog/src/components/LoginForm.vue":"8","/home/pascal/git/votelog/src/components/NavBar.vue":"9","/home/pascal/git/votelog/src/components/NotFound.vue":"10","/home/pascal/git/votelog/src/components/PercentageValue.vue":"11","/home/pascal/git/votelog/src/components/ShowSubject.vue":"12","/home/pascal/git/votelog/src/components/StatsRow.vue":"13","/home/pascal/git/votelog/src/components/SvgIcon.vue":"14","/home/pascal/git/votelog/src/components/VotesTable.vue":"15","/home/pascal/git/votelog/src/components/VotesTableCategories.vue":"16","/home/pascal/git/votelog/src/components/VotesTableCategory.vue":"17","/home/pascal/git/votelog/src/components/VotesTableSubject.vue":"18","/home/pascal/git/votelog/src/data.js":"19","/home/pascal/git/votelog/src/icons.js":"20","/home/pascal/git/votelog/src/main.js":"21","/home/pascal/git/votelog/src/router.js":"22","/home/pascal/git/votelog/src/store/actions.js":"23","/home/pascal/git/votelog/src/store/getters.js":"24","/home/pascal/git/votelog/src/store/index.js":"25","/home/pascal/git/votelog/src/store/mutations.js":"26","/home/pascal/git/votelog/src/store/token.js":"27","/home/pascal/git/votelog/vite.config.js":"28"},{"size":694,"mtime":1783515231629,"results":"29","hashOfConfig":"30"},{"size":3122,"mtime":1783516914708,"results":"31","hashOfConfig":"30"},{"size":775,"mtime":1783516914735,"results":"32","hashOfConfig":"33"},{"size":4695,"mtime":1783516914779,"results":"34","hashOfConfig":"33"},{"size":2957,"mtime":1783516914763,"results":"35","hashOfConfig":"33"},{"size":627,"mtime":1783516914719,"results":"36","hashOfConfig":"33"},{"size":653,"mtime":1783516914727,"results":"37","hashOfConfig":"33"},{"size":2470,"mtime":1783516914739,"results":"38","hashOfConfig":"33"},{"size":2380,"mtime":1783516914754,"results":"39","hashOfConfig":"33"},{"size":110,"mtime":1783516914689,"results":"40","hashOfConfig":"33"},{"size":1044,"mtime":1783516914734,"results":"41","hashOfConfig":"33"},{"size":3890,"mtime":1783516914761,"results":"42","hashOfConfig":"33"},{"size":1478,"mtime":1783516914749,"results":"43","hashOfConfig":"33"},{"size":228,"mtime":1783516914732,"results":"44","hashOfConfig":"33"},{"size":6369,"mtime":1783516914799,"results":"45","hashOfConfig":"33"},{"size":5903,"mtime":1783516914800,"results":"46","hashOfConfig":"33"},{"size":3198,"mtime":1783516914790,"results":"47","hashOfConfig":"33"},{"size":4878,"mtime":1783516914797,"results":"48","hashOfConfig":"33"},{"size":53851,"mtime":1783516914802,"results":"49","hashOfConfig":"30"},{"size":1005,"mtime":1783516914697,"results":"50","hashOfConfig":"30"},{"size":351,"mtime":1783516914681,"results":"51","hashOfConfig":"30"},{"size":2022,"mtime":1783516914697,"results":"52","hashOfConfig":"30"},{"size":3886,"mtime":1783516914714,"results":"53","hashOfConfig":"30"},{"size":1640,"mtime":1783516914714,"results":"54","hashOfConfig":"30"},{"size":615,"mtime":1783516914717,"results":"55","hashOfConfig":"30"},{"size":1166,"mtime":1783516914708,"results":"56","hashOfConfig":"30"},{"size":1711,"mtime":1783516914735,"results":"57","hashOfConfig":"30"},{"size":381,"mtime":1783515150183,"results":"58","hashOfConfig":"30"},{"filePath":"59","messages":"60","suppressedMessages":"61","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1apwypj",{"filePath":"62","messages":"63","suppressedMessages":"64","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"65","messages":"66","suppressedMessages":"67","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"13dtlfd",{"filePath":"68","messages":"69","suppressedMessages":"70","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"71","messages":"72","suppressedMessages":"73","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"74","messages":"75","suppressedMessages":"76","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"77","messages":"78","suppressedMessages":"79","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"80","messages":"81","suppressedMessages":"82","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"83","messages":"84","suppressedMessages":"85","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"86","messages":"87","suppressedMessages":"88","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"89","messages":"90","suppressedMessages":"91","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"92","messages":"93","suppressedMessages":"94","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"95","messages":"96","suppressedMessages":"97","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"98","messages":"99","suppressedMessages":"100","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"101","messages":"102","suppressedMessages":"103","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"104","messages":"105","suppressedMessages":"106","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"107","messages":"108","suppressedMessages":"109","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"110","messages":"111","suppressedMessages":"112","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"113","messages":"114","suppressedMessages":"115","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"116","messages":"117","suppressedMessages":"118","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"119","messages":"120","suppressedMessages":"121","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"122","messages":"123","suppressedMessages":"124","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"125","messages":"126","suppressedMessages":"127","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"128","messages":"129","suppressedMessages":"130","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"131","messages":"132","suppressedMessages":"133","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"134","messages":"135","suppressedMessages":"136","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"137","messages":"138","suppressedMessages":"139","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"140","messages":"141","suppressedMessages":"142","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/pascal/git/votelog/eslint.config.js",[],[],"/home/pascal/git/votelog/src/Answer.js",[],[],"/home/pascal/git/votelog/src/components/App.vue",[],[],"/home/pascal/git/votelog/src/components/EditSubject.vue",[],[],"/home/pascal/git/votelog/src/components/HeaderRow.vue",[],[],"/home/pascal/git/votelog/src/components/ImportanceSymbol.vue",[],[],"/home/pascal/git/votelog/src/components/Index.vue",[],[],"/home/pascal/git/votelog/src/components/LoginForm.vue",[],[],"/home/pascal/git/votelog/src/components/NavBar.vue",[],[],"/home/pascal/git/votelog/src/components/NotFound.vue",[],[],"/home/pascal/git/votelog/src/components/PercentageValue.vue",[],[],"/home/pascal/git/votelog/src/components/ShowSubject.vue",[],[],"/home/pascal/git/votelog/src/components/StatsRow.vue",[],[],"/home/pascal/git/votelog/src/components/SvgIcon.vue",[],[],"/home/pascal/git/votelog/src/components/VotesTable.vue",[],[],"/home/pascal/git/votelog/src/components/VotesTableCategories.vue",[],[],"/home/pascal/git/votelog/src/components/VotesTableCategory.vue",[],[],"/home/pascal/git/votelog/src/components/VotesTableSubject.vue",[],[],"/home/pascal/git/votelog/src/data.js",[],[],"/home/pascal/git/votelog/src/icons.js",[],[],"/home/pascal/git/votelog/src/main.js",[],[],"/home/pascal/git/votelog/src/router.js",[],[],"/home/pascal/git/votelog/src/store/actions.js",[],[],"/home/pascal/git/votelog/src/store/getters.js",[],[],"/home/pascal/git/votelog/src/store/index.js",[],[],"/home/pascal/git/votelog/src/store/mutations.js",[],[],"/home/pascal/git/votelog/src/store/token.js",[],[],"/home/pascal/git/votelog/vite.config.js",[],[]] \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 0666e3a..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,98 +0,0 @@ -module.exports = { - extends: [ - 'plugin:vue/vue3-essential', - 'plugin:vue/recommended', - 'plugin:prettier-vue/recommended', - ], - - plugins: ['prettier'], - - env: { - node: true, - }, - - parserOptions: { - parser: '@babel/eslint-parser', - }, - - settings: { - 'prettier-vue': { - // Settings for how to process Vue SFC Blocks - SFCBlocks: { - /** - * Use prettier to process `