Skip to content

Commit ef180b6

Browse files
committed
Merge origin/main: Integrate JWT auth, security headers, and template improvements
2 parents 591cad0 + cdaac68 commit ef180b6

File tree

22 files changed

+643
-174
lines changed

22 files changed

+643
-174
lines changed

.env.example

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@
77
ADMIN_USER=admin
88
ADMIN_PASS=supersecret
99
JWT_SECRET=change-me-in-production
10+
# Lifetime (seconds) for issued JWT sessions (min 300, max 43200)
11+
SESSION_TTL_SECONDS=3600
12+
# Set to false in local dev if you need to test without Secure cookies
13+
COOKIE_SECURE=true
14+
15+
# Hardened security headers (set to true to disable during local troubleshooting)
16+
SECURE_HEADERS_DISABLED=false
17+
# Emit Strict-Transport-Security when requests arrive via HTTPS
18+
ENABLE_HSTS=false
19+
# Override default CSP for the backend API if custom hosts are needed
20+
# CONTENT_SECURITY_POLICY="default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; script-src 'none'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'; connect-src 'self'"
1021

1122
# SQLite database path
1223
# - For Docker (recommended):
@@ -40,4 +51,14 @@ FRONTEND_PORT_INTERNAL=8000
4051
# For Docker Compose (service name resolves on the network):
4152
BACKEND_URL=http://backend:3000
4253
# For local dev outside Docker, you might use:
43-
# BACKEND_URL=http://localhost:3000
54+
# BACKEND_URL=http://localhost:3000
55+
56+
# =====================
57+
# Frontend security headers
58+
# =====================
59+
# Set to true to disable hardened headers (handy for local development)
60+
FRONTEND_SECURE_HEADERS_DISABLED=false
61+
# Emit Strict-Transport-Security when served via HTTPS
62+
ENABLE_HSTS=false
63+
# Override CSP if you host assets elsewhere (defaults admit Tailwind/DaisyUI CDNs)
64+
# FRONTEND_CONTENT_SECURITY_POLICY="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' http://localhost:3000 https: ws: wss:; frame-ancestors 'none'; form-action 'self'; object-src 'none'; base-uri 'none'"

.github/workflows/docker.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ jobs:
5656
images: |
5757
name=ghcr.io/${{ steps.repo.outputs.name }}-${{ matrix.image }},enable=true
5858
59+
- name: Copy VERSION into frontend build context
60+
if: ${{ matrix.image == 'frontend' }}
61+
run: cp VERSION frontend/VERSION
62+
5963
- name: Build and push image
6064
uses: docker/[email protected]
6165
with:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
.env
1414

1515
/frontend/static/dev
16+
/frontend/VERSION
1617

1718
/Invio.wiki

README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,33 @@
2525
- Client‑friendly — share a secure public link—no accounts or passwords required to view invoices.
2626

2727
## 🖼️ Screenshots
28-
<p align="center">
29-
<img src="https://raw.githubusercontent.com/kittendevv/Invio/refs/heads/main/assets/inviodashboard.webp" alt="Invio Dashboard" width="100%" />
30-
</p>
28+
<details>
29+
<summary>Dashboard</summary>
30+
31+
<img src="https://hc-cdn.hel1.your-objectstorage.com/s/v3/a6f7621a1f74b0de42507743c78da94c83e82c8a_screenshot_2025-10-26_092947.png" alt="Invio Dashboard" width="100%" />
32+
33+
</details>
34+
35+
<details>
36+
<summary>Invoice Creation</summary>
37+
38+
<img src="https://hc-cdn.hel1.your-objectstorage.com/s/v3/e3ec9ac920db9f4606f9a46096b93acfa59de569_screenshot_2025-10-26_093746.png" alt="Invio Dashboard" width="100%" />
39+
40+
</details>
41+
42+
<details>
43+
<summary>Settings</summary>
44+
45+
<img src="https://hc-cdn.hel1.your-objectstorage.com/s/v3/3e0acb92d7b807c3ca472d5d8f13907d12bee50e_screenshot_2025-10-26_094056.png" alt="Invio Dashboard" width="100%" />
46+
47+
</details>
48+
49+
<details>
50+
<summary>Invoices</summary>
51+
52+
<img src="https://hc-cdn.hel1.your-objectstorage.com/s/v3/5ac9f89da2a86332583027f70630bb772f652836_invoices.png" alt="Invio Dashboard" width="100%" />
53+
54+
</details>
3155

3256
## 🤝 Contributing
3357

backend/README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ download a good‑looking PDF. That’s it.
66
## Highlights
77

88
- Simple JSON API (Deno + Hono) at `/api/v1`
9-
- Admin endpoints behind Basic Auth (ADMIN_USER/ADMIN_PASS)
9+
- Admin endpoints behind JWT bearer auth (ADMIN_USER/ADMIN_PASS bootstrap)
1010
- Public share links per invoice (no login)
1111
- HTML and PDF renderers share the same templates
1212
- UBL 2.1 (PEPPOL BIS Billing 3.0) XML export for each invoice
@@ -21,6 +21,11 @@ download a good‑looking PDF. That’s it.
2121
```
2222
invio-backend
2323
├── src
24+
JWT_SECRET=...
25+
# Optional security header toggles
26+
# SECURE_HEADERS_DISABLED=false
27+
# ENABLE_HSTS=true
28+
# CONTENT_SECURITY_POLICY="default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; script-src 'none'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'; connect-src 'self'"
2429
│ ├── app.ts
2530
│ ├── routes
2631
│ │ ├── admin.ts
@@ -101,7 +106,7 @@ Principles
101106

102107
### Auth
103108

104-
- Admin routes use Basic Auth (from ENV).
109+
- Admin routes require a JWT obtained via `/api/v1/auth/login` using the admin credentials from env.
105110
- Public routes use a share token (no auth).
106111

107112
### Settings
@@ -119,7 +124,11 @@ Principles
119124
Example:
120125

121126
```bash
122-
curl -u admin:supersecret -H "Content-Type: application/json" \
127+
TOKEN=$(curl -s -X POST http://localhost:3000/api/v1/auth/login \
128+
-H "Content-Type: application/json" \
129+
-d '{"username":"admin","password":"supersecret"}' | jq -r '.token')
130+
131+
curl -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" \
123132
-X PATCH http://localhost:3000/api/v1/settings \
124133
-d '{"companyName":"Your Company","logo":"https://example.com/logo.png","templateId":"professional-modern","highlight":"#6B4EFF"}'
125134
```

backend/deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"puppeteer-core": "npm:[email protected]",
4040
"crypto": "https://deno.land/[email protected]/crypto/mod.ts",
4141
"dotenv": "https://deno.land/[email protected]/dotenv/mod.ts",
42-
"yaml": "https://deno.land/[email protected]/yaml/mod.ts"
42+
"yaml": "https://deno.land/[email protected]/yaml/mod.ts",
43+
"std/path": "https://deno.land/[email protected]/path/mod.ts"
4344
}
4445
}

backend/src/app.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import { authRoutes } from "./routes/auth.ts";
88
import { logChromiumAvailability } from "./utils/chromium.ts";
99
import { ensureEnv } from "./utils/env.ts";
1010

11+
const SECURE_HEADERS_DISABLED = (Deno.env.get("SECURE_HEADERS_DISABLED") || "").toLowerCase() === "true";
12+
const HSTS_ENABLED = (Deno.env.get("ENABLE_HSTS") || "").toLowerCase() === "true";
13+
const CONTENT_SECURITY_POLICY = Deno.env.get("CONTENT_SECURITY_POLICY") ||
14+
"default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; script-src 'none'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'; connect-src 'self'";
15+
1116
const app = new Hono();
1217

1318
// Check for required credentials in environment
@@ -59,6 +64,39 @@ app.use(
5964
}),
6065
);
6166

67+
app.use("*", async (c, next) => {
68+
await next();
69+
if (SECURE_HEADERS_DISABLED) return;
70+
const headers = c.res.headers;
71+
if (!headers.has("X-Content-Type-Options")) {
72+
headers.set("X-Content-Type-Options", "nosniff");
73+
}
74+
if (!headers.has("X-Frame-Options")) {
75+
headers.set("X-Frame-Options", "DENY");
76+
}
77+
if (!headers.has("Referrer-Policy")) {
78+
headers.set("Referrer-Policy", "no-referrer");
79+
}
80+
if (!headers.has("Permissions-Policy")) {
81+
headers.set("Permissions-Policy", "accelerometer=(), autoplay=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
82+
}
83+
if (!headers.has("Cross-Origin-Opener-Policy")) {
84+
headers.set("Cross-Origin-Opener-Policy", "same-origin");
85+
}
86+
if (!headers.has("Cross-Origin-Resource-Policy")) {
87+
headers.set("Cross-Origin-Resource-Policy", "same-site");
88+
}
89+
if (!headers.has("Content-Security-Policy")) {
90+
headers.set("Content-Security-Policy", CONTENT_SECURITY_POLICY);
91+
}
92+
if (HSTS_ENABLED && !headers.has("Strict-Transport-Security")) {
93+
const proto = c.req.header("x-forwarded-proto")?.toLowerCase() || (c.req.url.startsWith("https://") ? "https" : "http");
94+
if (proto === "https") {
95+
headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
96+
}
97+
}
98+
});
99+
62100
// Routes
63101
app.route("/api/v1", adminRoutes);
64102
app.route("/api/v1", publicRoutes);

0 commit comments

Comments
 (0)