Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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

3 changes: 3 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
VITE_WEBDAV_URI="http://localhost:8080/dav/"
VITE_OIDC_TOKEN_URL="http://localhost:8080/dex/token"
VITE_OIDC_CLIENT_ID=votelog-webapp
4 changes: 3 additions & 1 deletion .env.production
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
VUE_APP_WEBDAV_URI="https://dav.resolved.ch/"
VITE_WEBDAV_URI="https://dav.resolved.ch/dav/"
VITE_OIDC_TOKEN_URL="https://dav.resolved.ch/dex/token"
VITE_OIDC_CLIENT_ID=votelog-webapp
1 change: 1 addition & 0 deletions .eslintcache
Original file line number Diff line number Diff line change
@@ -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",[],[]]
98 changes: 0 additions & 98 deletions .eslintrc.js

This file was deleted.

10 changes: 10 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["eslint", "unicorn", "oxc", "vue"],
"env": {
"browser": true
},
"categories": {
"correctness": "error"
}
}
6 changes: 6 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

# build stage
FROM node:lts-alpine as build-stage
FROM node:lts-alpine AS build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install -g npm@latest
Expand All @@ -9,7 +9,7 @@ COPY . .
RUN npm run build

# production stage
FROM nginx:stable-alpine as production-stage
FROM nginx:stable-alpine AS production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
37 changes: 27 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,43 @@
A small Webapp I use to keep track of my votes in Switzerland and see which
parties agree with me.

## Known Issues

- Doesn't handle Gegenentwürfe very well (multiple entries, preferences and links)
![VoteLog Screenshot](screenshot.png)
(those are not my votes, at least not all of them...)

## Features

- 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_<user-email>.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).

## Known Issues

Functional:
- Doesn't handle *Gegenentwürfe* very well.

Technical:
- Error handling for the WebDAV connection (fetching and storing data) could be
better.

## 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
Expand All @@ -35,7 +51,7 @@ npm install
### Compiles and hot-reloads for development

```
npm run serve
npm run dev
```

### Compiles and minifies for production
Expand All @@ -48,4 +64,5 @@ npm run build

```
npm run lint
npm run format
```
15 changes: 0 additions & 15 deletions babel.config.js

This file was deleted.

7 changes: 0 additions & 7 deletions dev/webdav/Dockerfile

This file was deleted.

21 changes: 4 additions & 17 deletions dev/webdav/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

16 changes: 16 additions & 0 deletions dev/webdav/apache/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
52 changes: 52 additions & 0 deletions dev/webdav/apache/apache2.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<VirtualHost *:80>
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

<Location /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]
</Location>

DavLockDB /var/lib/dav/DavLock
Alias /dav/ /var/www/dav/
<Directory /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
<RequireAny>
Require method OPTIONS
<RequireAll>
Require valid-user
Require expr "%{REQUEST_URI} == '/dav/votelog_%{REMOTE_USER}.json'"
Require method GET PUT DELETE
</RequireAll>
</RequireAny>
</Directory>

RedirectMatch 404 ^/$

ErrorLog /proc/self/fd/2
CustomLog /proc/self/fd/1 combined
</VirtualHost>
Loading