nodebb-plugin-pdf-secure2 1.2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +22 -0
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/commitlint.config.js +26 -0
- package/docs/plans/2026-02-24-premium-gate-design.md +52 -0
- package/docs/plans/2026-02-24-premium-gate.md +323 -0
- package/eslint.config.mjs +10 -0
- package/image.png +0 -0
- package/languages/de/pdf-secure.json +7 -0
- package/languages/en-GB/pdf-secure.json +7 -0
- package/languages/en-US/pdf-secure.json +7 -0
- package/lib/controllers.js +65 -0
- package/lib/nonce-store.js +70 -0
- package/lib/pdf-handler.js +122 -0
- package/library.js +249 -0
- package/package.json +53 -0
- package/plugin.json +37 -0
- package/renovate.json +5 -0
- package/static/image.png +0 -0
- package/static/lib/main.js +477 -0
- package/static/lib/pdf-secure (1).pdf +0 -0
- package/static/lib/pdf.min.mjs +21 -0
- package/static/lib/pdf.worker.min.mjs +21 -0
- package/static/lib/viewer.js +184 -0
- package/static/style.less +142 -0
- package/static/templates/admin/plugins/pdf-secure.tpl +43 -0
- package/static/viewer-app.js +2906 -0
- package/static/viewer-yedek.html +4548 -0
- package/static/viewer.css +1500 -0
- package/static/viewer.html +5632 -0
- package/test/.eslintrc +9 -0
- package/test/index.js +41 -0
package/.gitattributes
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Auto detect text files and perform LF normalization
|
|
2
|
+
* text=auto
|
|
3
|
+
|
|
4
|
+
# Custom for Visual Studio
|
|
5
|
+
*.cs diff=csharp
|
|
6
|
+
*.sln merge=union
|
|
7
|
+
*.csproj merge=union
|
|
8
|
+
*.vbproj merge=union
|
|
9
|
+
*.fsproj merge=union
|
|
10
|
+
*.dbproj merge=union
|
|
11
|
+
|
|
12
|
+
# Standard to msysgit
|
|
13
|
+
*.doc diff=astextplain
|
|
14
|
+
*.DOC diff=astextplain
|
|
15
|
+
*.docx diff=astextplain
|
|
16
|
+
*.DOCX diff=astextplain
|
|
17
|
+
*.dot diff=astextplain
|
|
18
|
+
*.DOT diff=astextplain
|
|
19
|
+
*.pdf diff=astextplain
|
|
20
|
+
*.PDF diff=astextplain
|
|
21
|
+
*.rtf diff=astextplain
|
|
22
|
+
*.RTF diff=astextplain
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 NodeBB Inc. <sales@nodebb.org>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Quickstart Plugin for NodeBB
|
|
2
|
+
|
|
3
|
+
A starter kit for quickly creating NodeBB plugins. Comes with a pre-setup SCSS file, server side JS script with an `static:app.load` hook, and a client-side script. Most plugins need at least one of the above, so this ought to save you some time. For a full list of hooks have a look at our [wiki page](https://github.com/NodeBB/NodeBB/wiki/Hooks), and for more information about creating plugins please visit our [documentation portal](https://docs.nodebb.org/).
|
|
4
|
+
|
|
5
|
+
Fork this or copy it, and using your favourite text editor find and replace all instances of `nodebb-plugin-quickstart` with `nodebb-plugin-your-plugins-name`. Change the author's name in the LICENSE and package.json files.
|
|
6
|
+
|
|
7
|
+
## Hello World
|
|
8
|
+
|
|
9
|
+
Really simple, just edit `public/lib/main.js` and paste in `console.log('hello world');`, and that's it!
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
npm install nodebb-plugin-quickstart
|
|
14
|
+
|
|
15
|
+
## Screenshots
|
|
16
|
+
|
|
17
|
+
Don't forget to add screenshots!
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
extends: ['@commitlint/config-angular'],
|
|
5
|
+
rules: {
|
|
6
|
+
'header-max-length': [1, 'always', 72],
|
|
7
|
+
'type-enum': [
|
|
8
|
+
2,
|
|
9
|
+
'always',
|
|
10
|
+
[
|
|
11
|
+
'breaking',
|
|
12
|
+
'build',
|
|
13
|
+
'chore',
|
|
14
|
+
'ci',
|
|
15
|
+
'docs',
|
|
16
|
+
'feat',
|
|
17
|
+
'fix',
|
|
18
|
+
'perf',
|
|
19
|
+
'refactor',
|
|
20
|
+
'revert',
|
|
21
|
+
'style',
|
|
22
|
+
'test',
|
|
23
|
+
],
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Premium Gate Design
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
All PDFs are currently fully viewable by all users. We need to restrict non-Premium users to viewing only the first page, with a lock overlay prompting them to upgrade.
|
|
5
|
+
|
|
6
|
+
## Requirements
|
|
7
|
+
- Users in the "Premium" NodeBB group see all PDF pages
|
|
8
|
+
- Users NOT in "Premium" see only the first page
|
|
9
|
+
- Locked pages show a warning overlay with:
|
|
10
|
+
- Lock icon + "Premium required" message
|
|
11
|
+
- Link to forumtest.ieu.app/premium
|
|
12
|
+
- Secondary message about uploading materials to become Premium
|
|
13
|
+
- Admins and Global Moderators always get full access
|
|
14
|
+
|
|
15
|
+
## Approach: Server-Side Control
|
|
16
|
+
|
|
17
|
+
### Server Changes
|
|
18
|
+
|
|
19
|
+
**library.js (viewer route, line ~83):**
|
|
20
|
+
- Replace `const isPremium = true;` with actual group membership check
|
|
21
|
+
- Use `groups.isMember(req.uid, 'Premium')`
|
|
22
|
+
- Admin + Global Mod bypass (already exists for direct access, reuse pattern)
|
|
23
|
+
- Inject `isPremium` and `totalPages` into `window.PDF_SECURE_CONFIG`
|
|
24
|
+
|
|
25
|
+
**lib/pdf-handler.js:**
|
|
26
|
+
- Add `getTotalPages(filename)` function
|
|
27
|
+
- Loads PDF, returns `srcDoc.getPageCount()`
|
|
28
|
+
- Used by viewer route to tell client how many pages exist
|
|
29
|
+
|
|
30
|
+
**lib/controllers.js:**
|
|
31
|
+
- No changes needed - already handles `isPremium` flag from nonce data
|
|
32
|
+
|
|
33
|
+
### Client Changes
|
|
34
|
+
|
|
35
|
+
**static/viewer.html / viewer-app.js:**
|
|
36
|
+
- After PDF loads, if `isPremium === false`:
|
|
37
|
+
- Show only 1 page (server already sends only 1 page)
|
|
38
|
+
- Below the first page, render a lock overlay
|
|
39
|
+
- Overlay content:
|
|
40
|
+
- Lock icon (SVG)
|
|
41
|
+
- "Bu icerigi goruntulemek icin Premium uyelik gereklidir"
|
|
42
|
+
- "Premium Satin Al" button → forumtest.ieu.app/premium
|
|
43
|
+
- "Materyal yukleyerek de Premium olabilirsiniz!" secondary text
|
|
44
|
+
|
|
45
|
+
### Flow
|
|
46
|
+
1. User clicks PDF → viewer route called
|
|
47
|
+
2. Server checks Premium group membership
|
|
48
|
+
3. isPremium flag set in nonce + injected to viewer HTML
|
|
49
|
+
4. If not premium: server sends single-page PDF via getSinglePagePdf()
|
|
50
|
+
5. Client shows the page + lock overlay with upgrade prompt
|
|
51
|
+
6. If premium: server sends full PDF via getFullPdf()
|
|
52
|
+
7. Client shows all pages normally
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# Premium Gate Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Restrict non-Premium users to viewing only the first PDF page, with a lock overlay prompting upgrade.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Server checks user's "Premium" group membership, sends single-page PDF for non-premium users. Client renders a lock overlay below the first page showing upgrade prompts. Admin/Global Moderators always get full access.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** NodeBB plugin (Node.js), pdf-lib, PDF.js viewer, LESS/CSS
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: Add getTotalPages to pdf-handler
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
- Modify: `lib/pdf-handler.js`
|
|
17
|
+
|
|
18
|
+
**Step 1: Add getTotalPages function**
|
|
19
|
+
|
|
20
|
+
Add after `getSinglePagePdf` (after line 89):
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
PdfHandler.getTotalPages = async function (filename) {
|
|
24
|
+
const filePath = PdfHandler.resolveFilePath(filename);
|
|
25
|
+
if (!filePath) {
|
|
26
|
+
throw new Error('Invalid filename');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(filePath)) {
|
|
30
|
+
throw new Error('File not found');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const pdfBytes = await fs.promises.readFile(filePath);
|
|
34
|
+
const srcDoc = await PDFDocument.load(pdfBytes);
|
|
35
|
+
return srcDoc.getPageCount();
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Step 2: Commit**
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git add lib/pdf-handler.js
|
|
43
|
+
git commit -m "feat: add getTotalPages to pdf-handler"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### Task 2: Add Premium group check to viewer route
|
|
49
|
+
|
|
50
|
+
**Files:**
|
|
51
|
+
- Modify: `library.js` (lines 63-119, the viewer route)
|
|
52
|
+
|
|
53
|
+
**Step 1: Replace hardcoded isPremium with group check**
|
|
54
|
+
|
|
55
|
+
In `library.js`, replace line 83:
|
|
56
|
+
```javascript
|
|
57
|
+
const isPremium = true;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
With:
|
|
61
|
+
```javascript
|
|
62
|
+
// Check if user is Premium (admins/global mods always premium)
|
|
63
|
+
let isPremium = false;
|
|
64
|
+
if (req.uid) {
|
|
65
|
+
const [isAdmin, isGlobalMod, isPremiumMember] = await Promise.all([
|
|
66
|
+
groups.isMember(req.uid, 'administrators'),
|
|
67
|
+
groups.isMember(req.uid, 'Global Moderators'),
|
|
68
|
+
groups.isMember(req.uid, 'Premium'),
|
|
69
|
+
]);
|
|
70
|
+
isPremium = isAdmin || isGlobalMod || isPremiumMember;
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Step 2: Add totalPages to config injection**
|
|
75
|
+
|
|
76
|
+
In `library.js`, inside the viewer route handler, before the nonce generation, add:
|
|
77
|
+
```javascript
|
|
78
|
+
// Get total page count for non-premium lock overlay
|
|
79
|
+
const pdfHandler = require('./lib/pdf-handler');
|
|
80
|
+
let totalPages = 1;
|
|
81
|
+
try {
|
|
82
|
+
totalPages = await pdfHandler.getTotalPages(safeName);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.error('[PDF-Secure] Failed to get page count:', e.message);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Note: Move the `pdfHandler` require to the top of the file (line 10 area) instead of inline.
|
|
89
|
+
|
|
90
|
+
**Step 3: Inject isPremium and totalPages into viewer config**
|
|
91
|
+
|
|
92
|
+
In the `window.PDF_SECURE_CONFIG` object (around line 108-114), add two new fields:
|
|
93
|
+
```javascript
|
|
94
|
+
window.PDF_SECURE_CONFIG = {
|
|
95
|
+
filename: ${JSON.stringify(safeName)},
|
|
96
|
+
relativePath: ${JSON.stringify(req.app.get('relative_path') || '')},
|
|
97
|
+
csrfToken: ${JSON.stringify(req.csrfToken ? req.csrfToken() : '')},
|
|
98
|
+
nonce: ${JSON.stringify(nonceData.nonce)},
|
|
99
|
+
dk: ${JSON.stringify(nonceData.xorKey)},
|
|
100
|
+
isPremium: ${JSON.stringify(isPremium)},
|
|
101
|
+
totalPages: ${JSON.stringify(totalPages)}
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Step 4: Make the viewer route handler async**
|
|
106
|
+
|
|
107
|
+
The current route handler at line 64 is a sync callback: `router.get('/plugins/pdf-secure/viewer', (req, res) => {`
|
|
108
|
+
|
|
109
|
+
Change it to async: `router.get('/plugins/pdf-secure/viewer', async (req, res) => {`
|
|
110
|
+
|
|
111
|
+
**Step 5: Commit**
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
git add library.js
|
|
115
|
+
git commit -m "feat: check Premium group membership in viewer route"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### Task 3: Add lock overlay to viewer-app.js
|
|
121
|
+
|
|
122
|
+
**Files:**
|
|
123
|
+
- Modify: `static/viewer-app.js`
|
|
124
|
+
|
|
125
|
+
**Step 1: Add premium lock overlay after PDF loads**
|
|
126
|
+
|
|
127
|
+
In `autoLoadSecurePDF()`, after `await loadPDFFromBuffer(pdfBuffer);` (line 262), add the lock overlay logic. The config is still available at this point (it gets deleted on line 270).
|
|
128
|
+
|
|
129
|
+
Insert before `pdfBuffer = null;` (line 267):
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
// Premium Gate: Show lock overlay for non-premium users
|
|
133
|
+
if (config.isPremium === false && config.totalPages > 1) {
|
|
134
|
+
showPremiumLockOverlay(config.totalPages);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Step 2: Add showPremiumLockOverlay function**
|
|
139
|
+
|
|
140
|
+
Add this function inside the IIFE, before `autoLoadSecurePDF()`:
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
function showPremiumLockOverlay(totalPages) {
|
|
144
|
+
const viewerEl = document.getElementById('viewer');
|
|
145
|
+
if (!viewerEl) return;
|
|
146
|
+
|
|
147
|
+
const overlay = document.createElement('div');
|
|
148
|
+
overlay.id = 'premiumLockOverlay';
|
|
149
|
+
overlay.innerHTML = `
|
|
150
|
+
<div class="premium-lock-icon">
|
|
151
|
+
<svg viewBox="0 0 24 24" width="64" height="64" fill="#ffd700">
|
|
152
|
+
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1s3.1 1.39 3.1 3.1v2z"/>
|
|
153
|
+
</svg>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="premium-lock-pages">
|
|
156
|
+
${totalPages - 1} sayfa daha kilitli
|
|
157
|
+
</div>
|
|
158
|
+
<div class="premium-lock-message">
|
|
159
|
+
Bu icerigi goruntuleye devam etmek icin Premium uyelik gereklidir.
|
|
160
|
+
</div>
|
|
161
|
+
<a href="https://forumtest.ieu.app/premium" target="_blank" class="premium-lock-button">
|
|
162
|
+
Premium Satin Al
|
|
163
|
+
</a>
|
|
164
|
+
<div class="premium-lock-secondary">
|
|
165
|
+
Materyal yukleyerek de Premium olabilirsiniz!
|
|
166
|
+
</div>
|
|
167
|
+
`;
|
|
168
|
+
|
|
169
|
+
viewerEl.appendChild(overlay);
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Step 3: Commit**
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
git add static/viewer-app.js
|
|
177
|
+
git commit -m "feat: add premium lock overlay for non-premium users"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
### Task 4: Add lock overlay CSS styles
|
|
183
|
+
|
|
184
|
+
**Files:**
|
|
185
|
+
- Modify: `static/viewer.html` (inline styles section)
|
|
186
|
+
|
|
187
|
+
**Step 1: Add premium lock overlay styles**
|
|
188
|
+
|
|
189
|
+
Add these styles inside the `<style>` tag in viewer.html, before the closing `</style>`:
|
|
190
|
+
|
|
191
|
+
```css
|
|
192
|
+
/* Premium Lock Overlay */
|
|
193
|
+
#premiumLockOverlay {
|
|
194
|
+
display: flex;
|
|
195
|
+
flex-direction: column;
|
|
196
|
+
align-items: center;
|
|
197
|
+
justify-content: center;
|
|
198
|
+
padding: 60px 20px;
|
|
199
|
+
text-align: center;
|
|
200
|
+
background: linear-gradient(180deg, rgba(31,31,31,0.95) 0%, rgba(20,20,20,0.98) 100%);
|
|
201
|
+
border-top: 2px solid rgba(255, 215, 0, 0.3);
|
|
202
|
+
min-height: 400px;
|
|
203
|
+
margin: 0 auto;
|
|
204
|
+
max-width: 100%;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.premium-lock-icon {
|
|
208
|
+
margin-bottom: 20px;
|
|
209
|
+
opacity: 0.9;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.premium-lock-pages {
|
|
213
|
+
font-size: 22px;
|
|
214
|
+
font-weight: 600;
|
|
215
|
+
color: #ffd700;
|
|
216
|
+
margin-bottom: 12px;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.premium-lock-message {
|
|
220
|
+
font-size: 16px;
|
|
221
|
+
color: #a0a0a0;
|
|
222
|
+
margin-bottom: 28px;
|
|
223
|
+
max-width: 400px;
|
|
224
|
+
line-height: 1.5;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.premium-lock-button {
|
|
228
|
+
display: inline-block;
|
|
229
|
+
padding: 14px 40px;
|
|
230
|
+
background: linear-gradient(135deg, #ffd700 0%, #ffaa00 100%);
|
|
231
|
+
color: #1a1a1a;
|
|
232
|
+
font-size: 16px;
|
|
233
|
+
font-weight: 700;
|
|
234
|
+
border-radius: 8px;
|
|
235
|
+
text-decoration: none;
|
|
236
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
237
|
+
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.premium-lock-button:hover {
|
|
241
|
+
transform: translateY(-2px);
|
|
242
|
+
box-shadow: 0 6px 20px rgba(255, 215, 0, 0.4);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.premium-lock-secondary {
|
|
246
|
+
margin-top: 20px;
|
|
247
|
+
font-size: 14px;
|
|
248
|
+
color: #888;
|
|
249
|
+
font-style: italic;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@media (max-width: 768px) {
|
|
253
|
+
#premiumLockOverlay {
|
|
254
|
+
padding: 40px 16px;
|
|
255
|
+
min-height: 300px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.premium-lock-pages {
|
|
259
|
+
font-size: 18px;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.premium-lock-message {
|
|
263
|
+
font-size: 14px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.premium-lock-button {
|
|
267
|
+
padding: 12px 32px;
|
|
268
|
+
font-size: 14px;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Step 2: Commit**
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
git add static/viewer.html
|
|
277
|
+
git commit -m "feat: add premium lock overlay CSS styles"
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
### Task 5: Hide sidebar and page count for non-premium users
|
|
283
|
+
|
|
284
|
+
**Files:**
|
|
285
|
+
- Modify: `static/viewer-app.js`
|
|
286
|
+
|
|
287
|
+
**Step 1: Update page count display for non-premium**
|
|
288
|
+
|
|
289
|
+
In the `pagesinit` event handler (around line 360-362), update the page count to show total pages from config instead of the actual loaded pages:
|
|
290
|
+
|
|
291
|
+
Find the `eventBus.on('pagesinit', ...)` handler and wrap the pageCount update:
|
|
292
|
+
|
|
293
|
+
```javascript
|
|
294
|
+
eventBus.on('pagesinit', () => {
|
|
295
|
+
pdfViewer.currentScaleValue = 'page-width';
|
|
296
|
+
// For non-premium, show "1 / totalPages" instead of "1 / 1"
|
|
297
|
+
const savedConfig = window._pdfSecurePremiumInfo;
|
|
298
|
+
if (savedConfig && !savedConfig.isPremium && savedConfig.totalPages > 1) {
|
|
299
|
+
document.getElementById('pageCount').textContent = `/ ${savedConfig.totalPages}`;
|
|
300
|
+
} else {
|
|
301
|
+
document.getElementById('pageCount').textContent = `/ ${pdfViewer.pagesCount}`;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Step 2: Save premium info before config is deleted**
|
|
307
|
+
|
|
308
|
+
In `autoLoadSecurePDF()`, right before the config delete line (`delete window.PDF_SECURE_CONFIG`), save the premium info:
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
// Save premium info for UI (page count display)
|
|
312
|
+
window._pdfSecurePremiumInfo = {
|
|
313
|
+
isPremium: config.isPremium,
|
|
314
|
+
totalPages: config.totalPages
|
|
315
|
+
};
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Step 3: Commit**
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
git add static/viewer-app.js
|
|
322
|
+
git commit -m "feat: show real total page count for non-premium users"
|
|
323
|
+
```
|
package/image.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"view-pdf": "PDF anzeigen",
|
|
3
|
+
"premium-upgrade": "Upgrade auf Premium, um das vollständige Dokument anzuzeigen.",
|
|
4
|
+
"loading": "PDF wird geladen...",
|
|
5
|
+
"access-denied": "Zugriff verweigert",
|
|
6
|
+
"login-required": "Sie müssen angemeldet sein, um diese PDF anzuzeigen."
|
|
7
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const nonceStore = require('./nonce-store');
|
|
5
|
+
const pdfHandler = require('./pdf-handler');
|
|
6
|
+
|
|
7
|
+
const Controllers = module.exports;
|
|
8
|
+
|
|
9
|
+
// AES-256-GCM encryption - replaces weak XOR obfuscation
|
|
10
|
+
function aesGcmEncrypt(buffer, key, iv) {
|
|
11
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
12
|
+
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
|
13
|
+
const authTag = cipher.getAuthTag(); // 16 bytes
|
|
14
|
+
// Format: [authTag (16 bytes)] [ciphertext]
|
|
15
|
+
return Buffer.concat([authTag, encrypted]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Controllers.renderAdminPage = function (req, res) {
|
|
19
|
+
res.render('admin/plugins/pdf-secure', {
|
|
20
|
+
title: 'PDF Secure Viewer',
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
Controllers.servePdfBinary = async function (req, res) {
|
|
25
|
+
// Authentication gate - require logged-in user
|
|
26
|
+
if (!req.uid) {
|
|
27
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { nonce } = req.query;
|
|
31
|
+
if (!nonce) {
|
|
32
|
+
return res.status(400).json({ error: 'Missing nonce' });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const uid = req.uid;
|
|
36
|
+
|
|
37
|
+
const data = nonceStore.validate(nonce, uid);
|
|
38
|
+
if (!data) {
|
|
39
|
+
return res.status(403).json({ error: 'Invalid or expired nonce' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Server-side premium gate: non-premium users only get first page
|
|
44
|
+
const pdfBuffer = data.isPremium
|
|
45
|
+
? await pdfHandler.getFullPdf(data.file)
|
|
46
|
+
: await pdfHandler.getSinglePagePdf(data.file);
|
|
47
|
+
|
|
48
|
+
// Apply AES-256-GCM encryption
|
|
49
|
+
const encodedBuffer = aesGcmEncrypt(pdfBuffer, data.encKey, data.encIv);
|
|
50
|
+
|
|
51
|
+
res.set({
|
|
52
|
+
'Content-Type': 'application/octet-stream',
|
|
53
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate, private',
|
|
54
|
+
'X-Content-Type-Options': 'nosniff',
|
|
55
|
+
'Content-Disposition': 'inline',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return res.send(encodedBuffer);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err.message === 'File not found') {
|
|
61
|
+
return res.status(404).json({ error: 'PDF not found' });
|
|
62
|
+
}
|
|
63
|
+
return res.status(500).json({ error: 'Internal error' });
|
|
64
|
+
}
|
|
65
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { v4: uuidv4 } = require('uuid');
|
|
5
|
+
|
|
6
|
+
const store = new Map();
|
|
7
|
+
const NONCE_TTL = 30 * 1000; // 30 seconds
|
|
8
|
+
const CLEANUP_INTERVAL = 60 * 1000; // 60 seconds
|
|
9
|
+
|
|
10
|
+
// Periodic cleanup of expired nonces
|
|
11
|
+
setInterval(() => {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
for (const [nonce, data] of store.entries()) {
|
|
14
|
+
if (now - data.createdAt > NONCE_TTL) {
|
|
15
|
+
store.delete(nonce);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}, CLEANUP_INTERVAL).unref();
|
|
19
|
+
|
|
20
|
+
// Generate AES-256-GCM key (32 bytes) and IV (12 bytes)
|
|
21
|
+
function generateEncryptionParams() {
|
|
22
|
+
return {
|
|
23
|
+
key: crypto.randomBytes(32),
|
|
24
|
+
iv: crypto.randomBytes(12),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const NonceStore = module.exports;
|
|
29
|
+
|
|
30
|
+
NonceStore.generate = function (uid, file, isPremium) {
|
|
31
|
+
const nonce = uuidv4();
|
|
32
|
+
const { key, iv } = generateEncryptionParams();
|
|
33
|
+
|
|
34
|
+
store.set(nonce, {
|
|
35
|
+
uid: uid,
|
|
36
|
+
file: file,
|
|
37
|
+
isPremium: isPremium,
|
|
38
|
+
encKey: key, // AES-256-GCM key
|
|
39
|
+
encIv: iv, // GCM initialization vector
|
|
40
|
+
createdAt: Date.now(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
nonce: nonce,
|
|
45
|
+
dk: key.toString('base64'), // decryption key for viewer
|
|
46
|
+
iv: iv.toString('base64'), // IV for viewer
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
NonceStore.validate = function (nonce, uid) {
|
|
51
|
+
const data = store.get(nonce);
|
|
52
|
+
if (!data) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Delete immediately (single-use)
|
|
57
|
+
store.delete(nonce);
|
|
58
|
+
|
|
59
|
+
// Check UID match
|
|
60
|
+
if (data.uid !== uid) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check TTL
|
|
65
|
+
if (Date.now() - data.createdAt > NONCE_TTL) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return data; // Includes encKey and encIv for AES-256-GCM
|
|
70
|
+
};
|