Skip to content

Commit 88d848c

Browse files
author
Kimmo Virtanen
committed
hardening the codebase
1 parent 13dfb7b commit 88d848c

5 files changed

Lines changed: 88 additions & 15 deletions

File tree

.github/CODEOWNERS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Security-sensitive server-side paths.
2+
# Replace @zache-fi with your GitHub username/team if needed.
3+
/timepoll/settings.py @zache-fi

.github/workflows/ci.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
quality:
14+
runs-on: ubuntu-latest
15+
env:
16+
TIMEPOLL_SECRET_KEY: ci-timepoll-secret-key
17+
TIMEPOLL_DEBUG: "0"
18+
TIMEPOLL_ALLOWED_HOSTS: 127.0.0.1,localhost,testserver
19+
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
24+
- name: Setup Python
25+
uses: actions/setup-python@v5
26+
with:
27+
python-version: "3.11"
28+
cache: pip
29+
30+
- name: Install runtime dependencies
31+
run: |
32+
python -m pip install --upgrade pip
33+
pip install -r requirements.txt
34+
35+
- name: Install CI tooling
36+
run: |
37+
pip install ruff mypy bandit pip-audit
38+
39+
- name: Ruff
40+
run: ruff check .
41+
42+
- name: MyPy
43+
run: mypy polls timepoll manage.py --ignore-missing-imports --exclude "polls/migrations/.*"
44+
45+
- name: Bandit
46+
run: bandit -r polls timepoll manage.py -x polls/migrations
47+
48+
- name: pip-audit
49+
run: pip-audit --requirement requirements.txt
50+
51+
- name: Django tests
52+
run: python manage.py test

README.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ source venv/bin/activate
3636
pip install -r requirements.txt
3737
```
3838

39-
3. Load local development environment variables:
39+
3. Set required environment variables:
4040

4141
```bash
42-
source ./local-dev-env.sh
42+
export TIMEPOLL_SECRET_KEY='dev-only-secret-change-me'
43+
export TIMEPOLL_DEBUG='1'
44+
export TIMEPOLL_ALLOWED_HOSTS='127.0.0.1,localhost'
4345
```
4446

4547
4. Run migrations:
@@ -61,12 +63,31 @@ python manage.py runserver
6163
## Tests
6264

6365
```bash
64-
source ./local-dev-env.sh
6566
python manage.py test
6667
```
6768

69+
## CI merge gates
70+
71+
This repository includes GitHub Actions checks in `.github/workflows/ci.yml`:
72+
73+
- `ruff check .`
74+
- `mypy polls timepoll manage.py --ignore-missing-imports --exclude "polls/migrations/.*"`
75+
- `bandit -r polls timepoll manage.py -x polls/migrations`
76+
- `pip-audit --requirement requirements.txt`
77+
- `python manage.py test`
78+
79+
To enforce these checks as merge gates in GitHub:
80+
81+
1. Enable branch protection on your default branch.
82+
2. Enable `Require a pull request before merging`.
83+
3. Enable `Require review from Code Owners`.
84+
4. Enable `Require status checks to pass before merging` and select `CI / quality`.
85+
6886
## Notes
6987

7088
- Vue app code is in Django static files.
7189
- Vue runtime is loaded from Wikimedia CDN: `https://tools-static.wmflabs.org/cdnjs/...`.
72-
- `local-dev-env.sh` creates/loads local env vars needed by settings (`TIMEPOLL_SECRET_KEY`, `TIMEPOLL_DEBUG`, `TIMEPOLL_ALLOWED_HOSTS`).
90+
- Required environment variables:
91+
- `TIMEPOLL_SECRET_KEY`: non-empty secret string
92+
- `TIMEPOLL_DEBUG`: boolean-like string (`1/0`, `true/false`, `yes/no`, `on/off`)
93+
- `TIMEPOLL_ALLOWED_HOSTS`: comma-separated hostnames (for example `127.0.0.1,localhost`)

polls/static/polls/js/app.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1908,17 +1908,13 @@
19081908
}
19091909
const yesCount = this.optionCount(option, "yes");
19101910
const noCount = this.optionCount(option, "no");
1911-
const maybeCount = this.optionCount(option, "maybe");
19121911

19131912
if (yesCount > noCount) {
19141913
return "vote-yes";
19151914
}
19161915
if (yesCount < noCount) {
19171916
return "vote-no";
19181917
}
1919-
if ((yesCount === noCount && yesCount > 0) || (yesCount === 0 && noCount === 0 && maybeCount > 0)) {
1920-
return "vote-maybe";
1921-
}
19221918
return "vote-none";
19231919
},
19241920
isSelectedVoteValue(option, status) {
@@ -2129,6 +2125,10 @@
21292125
if (!this.canVoteInPoll || this.isVoteSaving(optionId)) {
21302126
return;
21312127
}
2128+
if (!this.session.authenticated) {
2129+
this.openAuthDialog();
2130+
return;
2131+
}
21322132
this.bulkMenu = null;
21332133
this.editingVoteOptionId = this.editingVoteOptionId === optionId ? null : optionId;
21342134
},
@@ -2574,7 +2574,6 @@
25742574
async openProfile() {
25752575
if (!this.session.authenticated) {
25762576
this.openAuthDialog();
2577-
this.setError(this.t("authNeeded"));
25782577
return;
25792578
}
25802579
this.setActiveSection("profile");
@@ -2600,7 +2599,6 @@
26002599
async deleteOwnData() {
26012600
if (!this.session.authenticated) {
26022601
this.openAuthDialog();
2603-
this.setError(this.t("authNeeded"));
26042602
return;
26052603
}
26062604
if (!window.confirm(this.t("profileDeleteConfirm"))) {
@@ -2700,7 +2698,6 @@
27002698
}
27012699
this.pendingAction = action;
27022700
this.openAuthDialog();
2703-
this.setError(this.t("authNeeded"));
27042701
return Promise.resolve();
27052702
},
27062703
async fetchSession() {

polls/views.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -633,8 +633,8 @@ def polls_collection(request: HttpRequest) -> JsonResponse:
633633
.prefetch_related("options__votes")
634634
.all()
635635
)
636-
payload = [serialize_poll_summary(poll, current_identity) for poll in polls]
637-
return JsonResponse({"polls": payload})
636+
polls_payload = [serialize_poll_summary(poll, current_identity) for poll in polls]
637+
return JsonResponse({"polls": polls_payload})
638638

639639
if current_identity is None:
640640
return JsonResponse(
@@ -643,8 +643,8 @@ def polls_collection(request: HttpRequest) -> JsonResponse:
643643
)
644644

645645
try:
646-
payload = parse_json_body(request)
647-
schedule = parse_poll_payload(payload)
646+
request_payload = parse_json_body(request)
647+
schedule = parse_poll_payload(request_payload)
648648
parsed_options = generate_poll_options(schedule)
649649
tz = ZoneInfo(schedule["timezone_name"])
650650
window_starts_at = datetime.combine(schedule["start_date"], time.min, tzinfo=tz)

0 commit comments

Comments
 (0)