Skip to content

Commit 5244325

Browse files
authored
Merge pull request #24 from kittendevv/dev
feat: Enhance security headers and authentication mechanism
2 parents b3a400a + 605f7ef commit 5244325

File tree

22 files changed

+617
-147
lines changed

22 files changed

+617
-147
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

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.6.0
1+
1.7.0

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
@@ -6,6 +6,11 @@ import { adminRoutes } from "./routes/admin.ts";
66
import { publicRoutes } from "./routes/public.ts";
77
import { authRoutes } from "./routes/auth.ts";
88

9+
const SECURE_HEADERS_DISABLED = (Deno.env.get("SECURE_HEADERS_DISABLED") || "").toLowerCase() === "true";
10+
const HSTS_ENABLED = (Deno.env.get("ENABLE_HSTS") || "").toLowerCase() === "true";
11+
const CONTENT_SECURITY_POLICY = Deno.env.get("CONTENT_SECURITY_POLICY") ||
12+
"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'";
13+
914
const app = new Hono();
1015

1116
// Check for required credentials in environment
@@ -57,6 +62,39 @@ app.use(
5762
}),
5863
);
5964

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

0 commit comments

Comments
 (0)