CatPtain commited on
Commit
85bc0da
·
verified ·
1 Parent(s): 7a57f48

Upload 9 files

Browse files
Files changed (9) hide show
  1. README-USER-ISOLATION.md +108 -0
  2. dashboard.html +440 -0
  3. editor.html +66 -2
  4. editor.php +61 -0
  5. login.html +202 -0
  6. save.php +63 -4
  7. storage.php +147 -7
  8. upload.php +39 -12
  9. user-manager.php +149 -0
README-USER-ISOLATION.md ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # User Isolation System for VvvebJs
2
+
3
+ This implementation adds user isolation to VvvebJs, allowing multiple users to work with their own separate files and projects.
4
+
5
+ ## Features
6
+
7
+ - **User Registration & Login**: Users can create accounts and log in securely
8
+ - **File Isolation**: Each user has their own directory for HTML files
9
+ - **Session Management**: Secure session handling with automatic logout
10
+ - **Demo Files Access**: Users can view demo files but cannot edit them
11
+ - **File Management**: Load, save, and manage user-specific files
12
+
13
+ ## File Structure
14
+
15
+ ```
16
+ user-files/
17
+ ├── username1/
18
+ │ ├── project1.html
19
+ │ └── project2.html
20
+ ├── username2/
21
+ │ ├── mysite.html
22
+ │ └── portfolio.html
23
+ └── ...
24
+
25
+ user-data/
26
+ └── users.json (user credentials)
27
+ ```
28
+
29
+ ## How to Use
30
+
31
+ ### 1. First Time Setup
32
+ 1. Navigate to `login.html` in your browser
33
+ 2. Click "Register here" to create a new account
34
+ 3. Fill in username and password (email is optional)
35
+ 4. Click "Register"
36
+
37
+ ### 2. Login
38
+ 1. Go to `login.html`
39
+ 2. Enter your username and password
40
+ 3. Click "Login"
41
+ 4. You'll be redirected to the editor
42
+
43
+ ### 3. Working with Files
44
+ - **New Files**: Create new files using the editor - they'll be automatically saved to your user directory
45
+ - **Load Files**: Use the file selector dropdown in the editor to load your existing files
46
+ - **Save Files**: All saves are automatically isolated to your user directory
47
+ - **Demo Files**: View demo files for reference (marked as "Demo" and read-only)
48
+
49
+ ### 4. Logout
50
+ - Close the browser or navigate away from the editor
51
+ - Sessions automatically expire for security
52
+
53
+ ## Technical Implementation
54
+
55
+ ### Modified Files:
56
+ - `storage.php`: Added user-specific path handling
57
+ - `save.php`: Enhanced with user isolation and file management
58
+ - `user-manager.php`: Complete user authentication system
59
+ - `editor.php`: Integration with user system and file loading
60
+ - `editor.html`: Added file selector and user interface improvements
61
+ - `login.html`: New login/registration interface
62
+
63
+ ### Security Features:
64
+ - Password hashing using PHP's `password_hash()`
65
+ - Session-based authentication
66
+ - Path validation to prevent directory traversal
67
+ - User input sanitization
68
+ - Automatic session cleanup
69
+
70
+ ### File Permissions:
71
+ - Users can only access files in their own directory
72
+ - Demo files are read-only for all users
73
+ - No cross-user file access possible
74
+
75
+ ## Configuration
76
+
77
+ The system works out of the box with no additional configuration needed. User data is stored in JSON format for simplicity, but can be easily adapted to use a database.
78
+
79
+ ## Development Notes
80
+
81
+ - User directories are created automatically on first save
82
+ - The system maintains backward compatibility with existing VvvebJs functionality
83
+ - All user data is stored locally in the `user-files` and `user-data` directories
84
+ - Sessions are managed via PHP's native session handling
85
+
86
+ ## Future Enhancements
87
+
88
+ Possible improvements could include:
89
+ - Database integration for user management
90
+ - File sharing between users
91
+ - Project templates
92
+ - File versioning
93
+ - Export/import functionality
94
+ - Admin panel for user management
95
+
96
+ ## Troubleshooting
97
+
98
+ **Login Issues:**
99
+ - Ensure `user-data` directory exists and is writable
100
+ - Check that PHP sessions are working on your server
101
+
102
+ **File Save Issues:**
103
+ - Verify `user-files` directory exists and is writable
104
+ - Check file permissions on the server
105
+
106
+ **Access Denied:**
107
+ - Make sure you're logged in (visit `login.html`)
108
+ - Clear browser cookies if experiencing session issues
dashboard.html ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>VvvebJs - User Dashboard</title>
7
+ <link href="css/editor.css" rel="stylesheet">
8
+ <style>
9
+ .user-dashboard {
10
+ max-width: 1200px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ }
14
+
15
+ .user-info {
16
+ background: #f8f9fa;
17
+ padding: 15px;
18
+ border-radius: 5px;
19
+ margin-bottom: 20px;
20
+ }
21
+
22
+ .file-manager {
23
+ background: white;
24
+ border: 1px solid #ddd;
25
+ border-radius: 5px;
26
+ overflow: hidden;
27
+ }
28
+
29
+ .file-manager-header {
30
+ background: #007bff;
31
+ color: white;
32
+ padding: 15px;
33
+ display: flex;
34
+ justify-content: space-between;
35
+ align-items: center;
36
+ }
37
+
38
+ .file-list {
39
+ max-height: 400px;
40
+ overflow-y: auto;
41
+ }
42
+
43
+ .file-item {
44
+ display: flex;
45
+ justify-content: space-between;
46
+ align-items: center;
47
+ padding: 10px 15px;
48
+ border-bottom: 1px solid #eee;
49
+ transition: background-color 0.2s;
50
+ }
51
+
52
+ .file-item:hover {
53
+ background-color: #f8f9fa;
54
+ }
55
+
56
+ .file-name {
57
+ font-weight: 500;
58
+ color: #333;
59
+ }
60
+
61
+ .file-meta {
62
+ font-size: 0.85em;
63
+ color: #666;
64
+ }
65
+
66
+ .file-actions {
67
+ display: flex;
68
+ gap: 10px;
69
+ }
70
+
71
+ .btn {
72
+ padding: 5px 10px;
73
+ border: none;
74
+ border-radius: 3px;
75
+ cursor: pointer;
76
+ font-size: 0.85em;
77
+ text-decoration: none;
78
+ display: inline-block;
79
+ }
80
+
81
+ .btn-primary {
82
+ background: #007bff;
83
+ color: white;
84
+ }
85
+
86
+ .btn-warning {
87
+ background: #ffc107;
88
+ color: #333;
89
+ }
90
+
91
+ .btn-danger {
92
+ background: #dc3545;
93
+ color: white;
94
+ }
95
+
96
+ .btn-success {
97
+ background: #28a745;
98
+ color: white;
99
+ }
100
+
101
+ .empty-state {
102
+ text-align: center;
103
+ padding: 40px;
104
+ color: #666;
105
+ }
106
+
107
+ .loading {
108
+ text-align: center;
109
+ padding: 20px;
110
+ color: #666;
111
+ }
112
+
113
+ .error {
114
+ background: #f8d7da;
115
+ color: #721c24;
116
+ padding: 10px;
117
+ border-radius: 3px;
118
+ margin: 10px 0;
119
+ }
120
+
121
+ .success {
122
+ background: #d4edda;
123
+ color: #155724;
124
+ padding: 10px;
125
+ border-radius: 3px;
126
+ margin: 10px 0;
127
+ }
128
+
129
+ .modal {
130
+ display: none;
131
+ position: fixed;
132
+ z-index: 1000;
133
+ left: 0;
134
+ top: 0;
135
+ width: 100%;
136
+ height: 100%;
137
+ background-color: rgba(0,0,0,0.4);
138
+ }
139
+
140
+ .modal-content {
141
+ background-color: #fefefe;
142
+ margin: 15% auto;
143
+ padding: 20px;
144
+ border: 1px solid #888;
145
+ border-radius: 5px;
146
+ width: 300px;
147
+ }
148
+
149
+ .modal-header {
150
+ display: flex;
151
+ justify-content: space-between;
152
+ align-items: center;
153
+ margin-bottom: 15px;
154
+ }
155
+
156
+ .close {
157
+ color: #aaa;
158
+ font-size: 28px;
159
+ font-weight: bold;
160
+ cursor: pointer;
161
+ }
162
+
163
+ .close:hover {
164
+ color: black;
165
+ }
166
+
167
+ .form-group {
168
+ margin-bottom: 15px;
169
+ }
170
+
171
+ .form-group label {
172
+ display: block;
173
+ margin-bottom: 5px;
174
+ font-weight: bold;
175
+ }
176
+
177
+ .form-group input {
178
+ width: 100%;
179
+ padding: 8px;
180
+ border: 1px solid #ddd;
181
+ border-radius: 3px;
182
+ box-sizing: border-box;
183
+ }
184
+ </style>
185
+ </head>
186
+ <body>
187
+ <div class="user-dashboard">
188
+ <div class="user-info">
189
+ <h2>Welcome to VvvebJs</h2>
190
+ <p><strong>User:</strong> <span id="current-user">Loading...</span></p>
191
+ <p><strong>Storage Path:</strong> <span id="user-path">Loading...</span></p>
192
+ </div>
193
+
194
+ <div class="file-manager">
195
+ <div class="file-manager-header">
196
+ <h3>My Files</h3>
197
+ <div>
198
+ <button class="btn btn-success" onclick="createNewFile()">New Page</button>
199
+ <button class="btn btn-primary" onclick="refreshFileList()">Refresh</button>
200
+ <a href="editor.html" class="btn btn-primary">Open Editor</a>
201
+ </div>
202
+ </div>
203
+
204
+ <div id="file-list-container">
205
+ <div class="loading">Loading files...</div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+
210
+ <!-- Rename Modal -->
211
+ <div id="renameModal" class="modal">
212
+ <div class="modal-content">
213
+ <div class="modal-header">
214
+ <h4>Rename File</h4>
215
+ <span class="close" onclick="closeModal('renameModal')">&times;</span>
216
+ </div>
217
+ <div class="form-group">
218
+ <label for="newFileName">New filename:</label>
219
+ <input type="text" id="newFileName" placeholder="Enter new filename">
220
+ </div>
221
+ <div>
222
+ <button class="btn btn-primary" onclick="confirmRename()">Rename</button>
223
+ <button class="btn" onclick="closeModal('renameModal')">Cancel</button>
224
+ </div>
225
+ </div>
226
+ </div>
227
+
228
+ <!-- Delete Confirmation Modal -->
229
+ <div id="deleteModal" class="modal">
230
+ <div class="modal-content">
231
+ <div class="modal-header">
232
+ <h4>Delete File</h4>
233
+ <span class="close" onclick="closeModal('deleteModal')">&times;</span>
234
+ </div>
235
+ <p>Are you sure you want to delete <strong id="deleteFileName"></strong>?</p>
236
+ <div>
237
+ <button class="btn btn-danger" onclick="confirmDelete()">Delete</button>
238
+ <button class="btn" onclick="closeModal('deleteModal')">Cancel</button>
239
+ </div>
240
+ </div>
241
+ </div>
242
+
243
+ <script>
244
+ let currentFiles = [];
245
+ let selectedFile = '';
246
+
247
+ // Load user info and files on page load
248
+ document.addEventListener('DOMContentLoaded', function() {
249
+ loadUserInfo();
250
+ loadFileList();
251
+ });
252
+
253
+ async function loadUserInfo() {
254
+ try {
255
+ const response = await fetch('save.php?action=checkAuth');
256
+ const data = await response.json();
257
+
258
+ if (data.success) {
259
+ document.getElementById('current-user').textContent = data.user;
260
+ document.getElementById('user-path').textContent = data.userPath || 'Default';
261
+ }
262
+ } catch (error) {
263
+ console.error('Error loading user info:', error);
264
+ }
265
+ }
266
+
267
+ async function loadFileList() {
268
+ const container = document.getElementById('file-list-container');
269
+ container.innerHTML = '<div class="loading">Loading files...</div>';
270
+
271
+ try {
272
+ const response = await fetch('save.php?action=listFiles');
273
+ const data = await response.json();
274
+
275
+ if (data.success) {
276
+ currentFiles = data.files;
277
+ renderFileList(data.files);
278
+ } else {
279
+ container.innerHTML = '<div class="error">Error loading files: ' + (data.message || 'Unknown error') + '</div>';
280
+ }
281
+ } catch (error) {
282
+ console.error('Error loading files:', error);
283
+ container.innerHTML = '<div class="error">Error loading files: ' + error.message + '</div>';
284
+ }
285
+ }
286
+
287
+ function renderFileList(files) {
288
+ const container = document.getElementById('file-list-container');
289
+
290
+ if (files.length === 0) {
291
+ container.innerHTML = `
292
+ <div class="empty-state">
293
+ <p>No files found. <a href="editor.html" class="btn btn-success">Create your first page</a></p>
294
+ </div>
295
+ `;
296
+ return;
297
+ }
298
+
299
+ const fileListHTML = files.map(file => `
300
+ <div class="file-item">
301
+ <div>
302
+ <div class="file-name">${file.name}</div>
303
+ <div class="file-meta">
304
+ ${file.size ? `Size: ${formatFileSize(file.size)} • ` : ''}
305
+ Path: ${file.path || file.name}
306
+ </div>
307
+ </div>
308
+ <div class="file-actions">
309
+ <a href="editor.html?file=${encodeURIComponent(file.path || file.name)}"
310
+ class="btn btn-primary">Edit</a>
311
+ ${file.url ? `<a href="${file.url}" target="_blank" class="btn btn-success">View</a>` : ''}
312
+ <button class="btn btn-warning" onclick="renameFile('${file.name}')">Rename</button>
313
+ <button class="btn btn-danger" onclick="deleteFile('${file.name}')">Delete</button>
314
+ </div>
315
+ </div>
316
+ `).join('');
317
+
318
+ container.innerHTML = '<div class="file-list">' + fileListHTML + '</div>';
319
+ }
320
+
321
+ function formatFileSize(bytes) {
322
+ if (bytes === 0) return '0 Bytes';
323
+ const k = 1024;
324
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
325
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
326
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
327
+ }
328
+
329
+ function refreshFileList() {
330
+ loadFileList();
331
+ }
332
+
333
+ function createNewFile() {
334
+ const fileName = prompt('Enter filename (without .html extension):');
335
+ if (fileName) {
336
+ const safeFileName = fileName.replace(/[^a-zA-Z0-9_-]/g, '_') + '.html';
337
+ window.location.href = `editor.html?file=${encodeURIComponent(safeFileName)}`;
338
+ }
339
+ }
340
+
341
+ function renameFile(fileName) {
342
+ selectedFile = fileName;
343
+ document.getElementById('newFileName').value = fileName.replace('.html', '');
344
+ openModal('renameModal');
345
+ }
346
+
347
+ async function confirmRename() {
348
+ const newFileName = document.getElementById('newFileName').value.trim();
349
+ if (!newFileName) {
350
+ alert('Please enter a valid filename');
351
+ return;
352
+ }
353
+
354
+ const safeNewFileName = newFileName.replace(/[^a-zA-Z0-9_-]/g, '_') + '.html';
355
+
356
+ try {
357
+ const formData = new FormData();
358
+ formData.append('file', selectedFile);
359
+ formData.append('newfile', safeNewFileName);
360
+
361
+ const response = await fetch('save.php?action=rename', {
362
+ method: 'POST',
363
+ body: formData
364
+ });
365
+
366
+ const result = await response.text();
367
+ if (response.ok) {
368
+ showMessage('File renamed successfully', 'success');
369
+ closeModal('renameModal');
370
+ loadFileList();
371
+ } else {
372
+ showMessage('Error renaming file: ' + result, 'error');
373
+ }
374
+ } catch (error) {
375
+ showMessage('Error renaming file: ' + error.message, 'error');
376
+ }
377
+ }
378
+
379
+ function deleteFile(fileName) {
380
+ selectedFile = fileName;
381
+ document.getElementById('deleteFileName').textContent = fileName;
382
+ openModal('deleteModal');
383
+ }
384
+
385
+ async function confirmDelete() {
386
+ try {
387
+ const formData = new FormData();
388
+ formData.append('file', selectedFile);
389
+
390
+ const response = await fetch('save.php?action=delete', {
391
+ method: 'POST',
392
+ body: formData
393
+ });
394
+
395
+ const result = await response.text();
396
+ if (response.ok) {
397
+ showMessage('File deleted successfully', 'success');
398
+ closeModal('deleteModal');
399
+ loadFileList();
400
+ } else {
401
+ showMessage('Error deleting file: ' + result, 'error');
402
+ }
403
+ } catch (error) {
404
+ showMessage('Error deleting file: ' + error.message, 'error');
405
+ }
406
+ }
407
+
408
+ function openModal(modalId) {
409
+ document.getElementById(modalId).style.display = 'block';
410
+ }
411
+
412
+ function closeModal(modalId) {
413
+ document.getElementById(modalId).style.display = 'none';
414
+ }
415
+
416
+ function showMessage(message, type) {
417
+ const container = document.querySelector('.user-dashboard');
418
+ const messageDiv = document.createElement('div');
419
+ messageDiv.className = type;
420
+ messageDiv.textContent = message;
421
+
422
+ container.insertBefore(messageDiv, container.firstChild);
423
+
424
+ setTimeout(() => {
425
+ messageDiv.remove();
426
+ }, 5000);
427
+ }
428
+
429
+ // Close modals when clicking outside
430
+ window.onclick = function(event) {
431
+ const modals = document.querySelectorAll('.modal');
432
+ modals.forEach(modal => {
433
+ if (event.target === modal) {
434
+ modal.style.display = 'none';
435
+ }
436
+ });
437
+ }
438
+ </script>
439
+ </body>
440
+ </html>
editor.html CHANGED
@@ -36,6 +36,61 @@
36
  }).catch(err => {
37
  console.log('Auth check failed, continuing anyway');
38
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  });
40
  </script>
41
  </head>
@@ -68,7 +123,16 @@
68
  <i class="la la-undo la-flip-horizontal"></i>
69
  </button>
70
  </div>
71
-
 
 
 
 
 
 
 
 
 
72
 
73
  <div class="btn-group me-3" role="group">
74
  <button class="btn btn-light" title="Designer Mode (Free dragging)" id="designer-mode-btn" data-bs-toggle="button" aria-pressed="false" data-vvveb-action="setDesignerMode">
@@ -416,7 +480,7 @@
416
 
417
  <div class="tab-content" data-offset="20">
418
  <div class="tab-pane show active" id="content-left-panel-tab" data-section="content" role="tabpanel" aria-labelledby="content-tab">
419
- <div class="alert alert-dismissible fade show alert-light m-3" role="alert" style="">
420
  <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
421
  <strong>No selected element!</strong><br> Click on an element to edit.
422
  </div>
 
36
  }).catch(err => {
37
  console.log('Auth check failed, continuing anyway');
38
  });
39
+
40
+ // File loading functionality
41
+ function loadFileList() {
42
+ fetch('save.php?action=list')
43
+ .then(response => response.json())
44
+ .then(data => {
45
+ const selector = document.getElementById('file-selector');
46
+ selector.innerHTML = '<option value="">Select a file...</option>';
47
+
48
+ if (data.success && data.files) {
49
+ data.files.forEach(file => {
50
+ const option = document.createElement('option');
51
+ option.value = file.filename;
52
+ option.textContent = file.title || file.filename;
53
+ selector.appendChild(option);
54
+ });
55
+ }
56
+ })
57
+ .catch(error => console.error('Error loading file list:', error));
58
+ }
59
+
60
+ // Load file when selected
61
+ document.getElementById('load-file-btn').addEventListener('click', function() {
62
+ const selector = document.getElementById('file-selector');
63
+ const filename = selector.value;
64
+
65
+ if (!filename) {
66
+ alert('Please select a file to load');
67
+ return;
68
+ }
69
+
70
+ fetch(`save.php?action=load&filename=${encodeURIComponent(filename)}`)
71
+ .then(response => response.json())
72
+ .then(data => {
73
+ if (data.success) {
74
+ // Load the HTML content into the editor
75
+ Vvveb.Builder.loadHtml(data.html);
76
+ // Update the page title input if it exists
77
+ const titleInput = document.getElementById('page-title');
78
+ if (titleInput && data.title) {
79
+ titleInput.value = data.title;
80
+ }
81
+ console.log('File loaded successfully');
82
+ } else {
83
+ alert('Error loading file: ' + (data.message || 'Unknown error'));
84
+ }
85
+ })
86
+ .catch(error => {
87
+ console.error('Error loading file:', error);
88
+ alert('Error loading file');
89
+ });
90
+ });
91
+
92
+ // Load file list on page load
93
+ loadFileList();
94
  });
95
  </script>
96
  </head>
 
123
  <i class="la la-undo la-flip-horizontal"></i>
124
  </button>
125
  </div>
126
+
127
+ <!-- File loading dropdown -->
128
+ <div class="btn-group me-3" role="group">
129
+ <select class="form-select" id="file-selector" title="Select file to load">
130
+ <option value="">Select a file...</option>
131
+ </select>
132
+ <button class="btn btn-light" title="Load selected file" id="load-file-btn">
133
+ <i class="la la-folder-open"></i>
134
+ </button>
135
+ </div>
136
 
137
  <div class="btn-group me-3" role="group">
138
  <button class="btn btn-light" title="Designer Mode (Free dragging)" id="designer-mode-btn" data-bs-toggle="button" aria-pressed="false" data-vvveb-action="setDesignerMode">
 
480
 
481
  <div class="tab-content" data-offset="20">
482
  <div class="tab-pane show active" id="content-left-panel-tab" data-section="content" role="tabpanel" aria-labelledby="content-tab">
483
+ <div class="alert alert-dismissible fade show alert-light m-3" role="alert">
484
  <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
485
  <strong>No selected element!</strong><br> Click on an element to edit.
486
  </div>
editor.php ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ require_once 'user-manager.php';
3
+
4
+ // Initialize user manager and require login
5
+ $userManager = new UserManager();
6
+ $userManager->requireLogin();
7
+
8
+ $currentUser = $userManager->getCurrentUser();
9
+
10
+ // Get the editor HTML
11
+ $html = file_get_contents('editor.html');
12
+
13
+ // Search for HTML files in user's directory and demo folder
14
+ $userDir = "user-files/$currentUser";
15
+ $userFiles = [];
16
+ if (is_dir($userDir)) {
17
+ $userFiles = glob("$userDir/*.html");
18
+ }
19
+
20
+ // Also include demo files for reference
21
+ $demoFiles = array_merge(glob('demo/*/*.html'), glob('demo/*.html'));
22
+
23
+ $files = '';
24
+
25
+ // Add user files first
26
+ foreach ($userFiles as $file) {
27
+ if (in_array(basename($file), array('new-page-blank-template.html', 'editor.html'))) continue;
28
+ $pathInfo = pathinfo($file);
29
+ $filename = $pathInfo['filename'];
30
+ $folder = 'My Files';
31
+ $url = $file;
32
+ $name = $filename;
33
+ $title = ucfirst($name);
34
+ $files .= "{name:'$name', file:'$file', title:'$title', url: '$url', folder:'$folder', editable: true},";
35
+ }
36
+
37
+ // Add demo files (read-only)
38
+ foreach ($demoFiles as $file) {
39
+ if (in_array($file, array('new-page-blank-template.html', 'editor.html'))) continue;
40
+ $pathInfo = pathinfo($file);
41
+ $filename = $pathInfo['filename'];
42
+ $folder = preg_replace('@/.+?$@', '', $pathInfo['dirname']);
43
+ $subfolder = preg_replace('@^.+?/@', '', $pathInfo['dirname']);
44
+ if ($filename == 'index' && $subfolder) {
45
+ $filename = $subfolder;
46
+ }
47
+ $url = $pathInfo['dirname'] . '/' . $pathInfo['basename'];
48
+ $name = $filename;
49
+ $title = ucfirst($name) . ' (Demo)';
50
+ $files .= "{name:'$name', file:'$file', title:'$title', url: '$url', folder:'$folder', editable: false},";
51
+ }
52
+
53
+ // Replace files list from html with the dynamic list
54
+ $html = str_replace('= defaultPages;', " = [$files];", $html);
55
+
56
+ // Add user info to the page
57
+ $userInfo = "<script>window.currentUser = '$currentUser';</script>";
58
+ $html = str_replace('</head>', "$userInfo</head>", $html);
59
+
60
+ echo $html;
61
+ ?>
login.html ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>VvvebJs - Login</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <style>
9
+ body {
10
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
11
+ min-height: 100vh;
12
+ display: flex;
13
+ align-items: center;
14
+ }
15
+ .login-container {
16
+ background: white;
17
+ border-radius: 15px;
18
+ box-shadow: 0 15px 35px rgba(0,0,0,0.1);
19
+ overflow: hidden;
20
+ }
21
+ .login-header {
22
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
23
+ color: white;
24
+ padding: 2rem;
25
+ text-align: center;
26
+ }
27
+ .login-form {
28
+ padding: 2rem;
29
+ }
30
+ .btn-primary {
31
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
32
+ border: none;
33
+ }
34
+ .btn-primary:hover {
35
+ background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
36
+ }
37
+ </style>
38
+ </head>
39
+ <body>
40
+ <div class="container">
41
+ <div class="row justify-content-center">
42
+ <div class="col-md-6 col-lg-4">
43
+ <div class="login-container">
44
+ <div class="login-header">
45
+ <h2>VvvebJs Editor</h2>
46
+ <p class="mb-0">Please login to continue</p>
47
+ </div>
48
+ <div class="login-form">
49
+ <div id="loginForm">
50
+ <form id="loginFormElement">
51
+ <div class="mb-3">
52
+ <label for="username" class="form-label">Username</label>
53
+ <input type="text" class="form-control" id="username" name="username" required>
54
+ </div>
55
+ <div class="mb-3">
56
+ <label for="password" class="form-label">Password</label>
57
+ <input type="password" class="form-control" id="password" name="password" required>
58
+ </div>
59
+ <button type="submit" class="btn btn-primary w-100 mb-3">Login</button>
60
+ </form>
61
+ <div class="text-center">
62
+ <small>Don't have an account? <a href="#" id="showRegister">Register here</a></small>
63
+ </div>
64
+ </div>
65
+
66
+ <div id="registerForm" style="display: none;">
67
+ <form id="registerFormElement">
68
+ <div class="mb-3">
69
+ <label for="regUsername" class="form-label">Username</label>
70
+ <input type="text" class="form-control" id="regUsername" name="username" required>
71
+ <small class="form-text text-muted">At least 3 characters, letters, numbers, underscore, and dash only</small>
72
+ </div>
73
+ <div class="mb-3">
74
+ <label for="regEmail" class="form-label">Email (optional)</label>
75
+ <input type="email" class="form-control" id="regEmail" name="email">
76
+ </div>
77
+ <div class="mb-3">
78
+ <label for="regPassword" class="form-label">Password</label>
79
+ <input type="password" class="form-control" id="regPassword" name="password" required>
80
+ </div>
81
+ <div class="mb-3">
82
+ <label for="regPasswordConfirm" class="form-label">Confirm Password</label>
83
+ <input type="password" class="form-control" id="regPasswordConfirm" required>
84
+ </div>
85
+ <button type="submit" class="btn btn-primary w-100 mb-3">Register</button>
86
+ </form>
87
+ <div class="text-center">
88
+ <small>Already have an account? <a href="#" id="showLogin">Login here</a></small>
89
+ </div>
90
+ </div>
91
+
92
+ <div id="message" class="alert" style="display: none;"></div>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
100
+ <script>
101
+ document.addEventListener('DOMContentLoaded', function() {
102
+ const loginForm = document.getElementById('loginForm');
103
+ const registerForm = document.getElementById('registerForm');
104
+ const showRegister = document.getElementById('showRegister');
105
+ const showLogin = document.getElementById('showLogin');
106
+ const messageDiv = document.getElementById('message');
107
+
108
+ showRegister.addEventListener('click', function(e) {
109
+ e.preventDefault();
110
+ loginForm.style.display = 'none';
111
+ registerForm.style.display = 'block';
112
+ hideMessage();
113
+ });
114
+
115
+ showLogin.addEventListener('click', function(e) {
116
+ e.preventDefault();
117
+ registerForm.style.display = 'none';
118
+ loginForm.style.display = 'block';
119
+ hideMessage();
120
+ });
121
+
122
+ document.getElementById('loginFormElement').addEventListener('submit', function(e) {
123
+ e.preventDefault();
124
+ const formData = new FormData(this);
125
+ formData.append('action', 'login');
126
+
127
+ fetch('user-manager.php', {
128
+ method: 'POST',
129
+ body: formData
130
+ })
131
+ .then(response => response.json())
132
+ .then(data => {
133
+ showMessage(data.message, data.success ? 'success' : 'danger');
134
+ if (data.success) {
135
+ setTimeout(() => {
136
+ window.location.href = 'editor.html';
137
+ }, 1000);
138
+ }
139
+ })
140
+ .catch(error => {
141
+ showMessage('An error occurred. Please try again.', 'danger');
142
+ });
143
+ });
144
+
145
+ document.getElementById('registerFormElement').addEventListener('submit', function(e) {
146
+ e.preventDefault();
147
+ const password = document.getElementById('regPassword').value;
148
+ const confirmPassword = document.getElementById('regPasswordConfirm').value;
149
+
150
+ if (password !== confirmPassword) {
151
+ showMessage('Passwords do not match.', 'danger');
152
+ return;
153
+ }
154
+
155
+ const formData = new FormData(this);
156
+ formData.append('action', 'register');
157
+
158
+ fetch('user-manager.php', {
159
+ method: 'POST',
160
+ body: formData
161
+ })
162
+ .then(response => response.json())
163
+ .then(data => {
164
+ showMessage(data.message, data.success ? 'success' : 'danger');
165
+ if (data.success) {
166
+ setTimeout(() => {
167
+ registerForm.style.display = 'none';
168
+ loginForm.style.display = 'block';
169
+ document.getElementById('registerFormElement').reset();
170
+ }, 1500);
171
+ }
172
+ })
173
+ .catch(error => {
174
+ showMessage('An error occurred. Please try again.', 'danger');
175
+ });
176
+ });
177
+
178
+ function showMessage(message, type) {
179
+ messageDiv.textContent = message;
180
+ messageDiv.className = `alert alert-${type}`;
181
+ messageDiv.style.display = 'block';
182
+ }
183
+
184
+ function hideMessage() {
185
+ messageDiv.style.display = 'none';
186
+ }
187
+
188
+ // Check if already logged in
189
+ fetch('user-manager.php', {
190
+ method: 'POST',
191
+ body: new URLSearchParams({action: 'check_login'})
192
+ })
193
+ .then(response => response.json())
194
+ .then(data => {
195
+ if (data.logged_in) {
196
+ window.location.href = 'editor.html';
197
+ }
198
+ });
199
+ });
200
+ </script>
201
+ </body>
202
+ </html>
save.php CHANGED
@@ -126,25 +126,84 @@ if (isset($_GET['action'])) {
126
  if ($action) {
127
  //file manager actions, delete and rename
128
  switch ($action) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  case 'rename':
130
  $newfile = sanitizeFileName($_POST['newfile']);
131
  if ($file && $newfile) {
132
- if (rename($file, $newfile)) {
133
- echo "File '$file' renamed to '$newfile'";
 
 
 
 
 
 
 
134
  } else {
135
- showError("Error renaming file '$file' renamed to '$newfile'");
136
  }
137
  }
138
  break;
 
139
  case 'delete':
140
  if ($file) {
141
- if (unlink($file)) {
142
  echo "File '$file' deleted";
143
  } else {
144
  showError("Error deleting file '$file'");
145
  }
146
  }
147
  break;
 
148
  case 'saveReusable':
149
  //block or section
150
  $type = $_POST['type'] ?? false;
 
126
  if ($action) {
127
  //file manager actions, delete and rename
128
  switch ($action) {
129
+ case 'listFiles':
130
+ // List files for current user
131
+ $files = $storageManager->listFiles();
132
+ header('Content-Type: application/json');
133
+ echo json_encode([
134
+ 'success' => true,
135
+ 'files' => $files,
136
+ 'user' => $storageManager->getCurrentUser(),
137
+ 'userPath' => $storageManager->getUserPath()
138
+ ]);
139
+ exit;
140
+
141
+ case 'loadFile':
142
+ // Load a specific file for current user
143
+ $filename = sanitizeFileName($_GET['file'] ?? '');
144
+ if ($filename) {
145
+ $content = $storageManager->getFile($filename);
146
+ if ($content !== false) {
147
+ header('Content-Type: application/json');
148
+ echo json_encode([
149
+ 'success' => true,
150
+ 'content' => $content,
151
+ 'filename' => $filename
152
+ ]);
153
+ } else {
154
+ header('Content-Type: application/json');
155
+ echo json_encode([
156
+ 'success' => false,
157
+ 'message' => 'File not found or access denied'
158
+ ]);
159
+ }
160
+ } else {
161
+ header('Content-Type: application/json');
162
+ echo json_encode([
163
+ 'success' => false,
164
+ 'message' => 'Invalid filename'
165
+ ]);
166
+ }
167
+ exit;
168
+
169
+ case 'checkAuth':
170
+ // Check if user is authenticated
171
+ header('Content-Type: application/json');
172
+ echo json_encode([
173
+ 'success' => true,
174
+ 'user' => $storageManager->getCurrentUser(),
175
+ 'authenticated' => true
176
+ ]);
177
+ exit;
178
+
179
  case 'rename':
180
  $newfile = sanitizeFileName($_POST['newfile']);
181
  if ($file && $newfile) {
182
+ // For user isolation, we need to handle this through storage manager
183
+ $content = $storageManager->getFile($file);
184
+ if ($content !== false) {
185
+ if ($storageManager->saveFile($newfile, $content)) {
186
+ $storageManager->deleteFile($file);
187
+ echo "File '$file' renamed to '$newfile'";
188
+ } else {
189
+ showError("Error renaming file '$file' to '$newfile'");
190
+ }
191
  } else {
192
+ showError("File '$file' not found");
193
  }
194
  }
195
  break;
196
+
197
  case 'delete':
198
  if ($file) {
199
+ if ($storageManager->deleteFile($file)) {
200
  echo "File '$file' deleted";
201
  } else {
202
  showError("Error deleting file '$file'");
203
  }
204
  }
205
  break;
206
+
207
  case 'saveReusable':
208
  //block or section
209
  $type = $_POST['type'] ?? false;
storage.php CHANGED
@@ -42,6 +42,19 @@ class StorageConfig {
42
  ];
43
  }
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  private static function makeGitHubRequest($url, $method = 'GET', $data = null) {
46
  $config = self::getGitHubConfig();
47
 
@@ -291,21 +304,26 @@ class StorageConfig {
291
  // External storage handlers
292
  class KVStorage {
293
  private $config;
 
294
 
295
  public function __construct($config) {
296
  $this->config = $config;
 
297
  }
298
 
299
  public function save($key, $content) {
300
  if (empty($this->config['api_key'])) return false;
301
 
 
 
 
302
  $url = $this->config['endpoint'] . '/';
303
  $params = [
304
  'Action' => 'ModifyApplicationProxyRule',
305
  'Version' => '2022-09-01',
306
  'Region' => 'ap-beijing',
307
  'ZoneId' => $this->config['zone_id'],
308
- 'Key' => $key,
309
  'Value' => base64_encode($content)
310
  ];
311
 
@@ -315,19 +333,28 @@ class KVStorage {
315
  public function get($key) {
316
  if (empty($this->config['api_key'])) return false;
317
 
 
 
 
318
  $url = $this->config['endpoint'] . '/';
319
  $params = [
320
  'Action' => 'DescribeApplicationProxyDetail',
321
  'Version' => '2022-09-01',
322
  'Region' => 'ap-beijing',
323
  'ZoneId' => $this->config['zone_id'],
324
- 'Key' => $key
325
  ];
326
 
327
  $result = $this->makeRequest($url, $params);
328
  return $result ? base64_decode($result) : false;
329
  }
330
 
 
 
 
 
 
 
331
  private function makeRequest($url, $params) {
332
  $ch = curl_init();
333
  curl_setopt($ch, CURLOPT_URL, $url);
@@ -348,9 +375,11 @@ class KVStorage {
348
  class GitHubStorage {
349
  private $config;
350
  private $actualBranch = null;
 
351
 
352
  public function __construct($config) {
353
  $this->config = $config;
 
354
  // Auto-detect the actual default branch
355
  $this->detectDefaultBranch();
356
  }
@@ -402,16 +431,18 @@ class GitHubStorage {
402
  return false;
403
  }
404
 
405
- $path = $this->config['path'] . $filename;
 
 
406
  $url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$path}";
407
 
408
- error_log("GitHub Save Debug: Attempting to save to $url on branch '{$this->config['branch']}'");
409
 
410
  // Get current file SHA if exists
411
  $sha = $this->getFileSHA($path);
412
 
413
  $data = [
414
- 'message' => 'Update ' . $filename . ' via VvvebJs',
415
  'content' => base64_encode($content),
416
  'branch' => $this->config['branch']
417
  ];
@@ -437,7 +468,9 @@ class GitHubStorage {
437
  public function get($filename) {
438
  if (empty($this->config['token'])) return false;
439
 
440
- $path = $this->config['path'] . $filename;
 
 
441
  $url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$path}";
442
 
443
  $result = $this->makeRequest($url, 'GET');
@@ -447,6 +480,55 @@ class GitHubStorage {
447
  return false;
448
  }
449
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  private function getFileSHA($path) {
451
  $url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$path}";
452
  $result = $this->makeRequest($url, 'GET');
@@ -517,14 +599,16 @@ class GitHubStorage {
517
  }
518
  }
519
 
520
- // Storage manager
521
  class StorageManager {
522
  private $kvStorage;
523
  private $githubStorage;
524
  private $storageType;
 
525
 
526
  public function __construct() {
527
  $this->storageType = StorageConfig::getStorageType();
 
528
 
529
  if (in_array($this->storageType, ['kv', 'both'])) {
530
  $this->kvStorage = new KVStorage(StorageConfig::getKVConfig());
@@ -536,6 +620,9 @@ class StorageManager {
536
  }
537
 
538
  public function saveFile($filename, $content) {
 
 
 
539
  $success = false;
540
 
541
  if ($this->githubStorage) {
@@ -550,6 +637,9 @@ class StorageManager {
550
  }
551
 
552
  public function getFile($filename) {
 
 
 
553
  if ($this->githubStorage) {
554
  $content = $this->githubStorage->get($filename);
555
  if ($content !== false) return $content;
@@ -562,4 +652,54 @@ class StorageManager {
562
 
563
  return false;
564
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
  }
 
42
  ];
43
  }
44
 
45
+ public static function getCurrentUser() {
46
+ return $_SERVER['PHP_AUTH_USER'] ?? 'anonymous';
47
+ }
48
+
49
+ public static function getUserPath($username = null) {
50
+ if ($username === null) {
51
+ $username = self::getCurrentUser();
52
+ }
53
+ // Sanitize username for safe path usage
54
+ $safeUsername = preg_replace('/[^a-zA-Z0-9_-]/', '_', $username);
55
+ return "users/{$safeUsername}/";
56
+ }
57
+
58
  private static function makeGitHubRequest($url, $method = 'GET', $data = null) {
59
  $config = self::getGitHubConfig();
60
 
 
304
  // External storage handlers
305
  class KVStorage {
306
  private $config;
307
+ private $userPath;
308
 
309
  public function __construct($config) {
310
  $this->config = $config;
311
+ $this->userPath = StorageConfig::getUserPath();
312
  }
313
 
314
  public function save($key, $content) {
315
  if (empty($this->config['api_key'])) return false;
316
 
317
+ // Add user path to key
318
+ $userKey = $this->userPath . $key;
319
+
320
  $url = $this->config['endpoint'] . '/';
321
  $params = [
322
  'Action' => 'ModifyApplicationProxyRule',
323
  'Version' => '2022-09-01',
324
  'Region' => 'ap-beijing',
325
  'ZoneId' => $this->config['zone_id'],
326
+ 'Key' => $userKey,
327
  'Value' => base64_encode($content)
328
  ];
329
 
 
333
  public function get($key) {
334
  if (empty($this->config['api_key'])) return false;
335
 
336
+ // Add user path to key
337
+ $userKey = $this->userPath . $key;
338
+
339
  $url = $this->config['endpoint'] . '/';
340
  $params = [
341
  'Action' => 'DescribeApplicationProxyDetail',
342
  'Version' => '2022-09-01',
343
  'Region' => 'ap-beijing',
344
  'ZoneId' => $this->config['zone_id'],
345
+ 'Key' => $userKey
346
  ];
347
 
348
  $result = $this->makeRequest($url, $params);
349
  return $result ? base64_decode($result) : false;
350
  }
351
 
352
+ public function listUserFiles() {
353
+ // This would require additional API endpoints to list files
354
+ // Implementation depends on the KV storage provider's capabilities
355
+ return [];
356
+ }
357
+
358
  private function makeRequest($url, $params) {
359
  $ch = curl_init();
360
  curl_setopt($ch, CURLOPT_URL, $url);
 
375
  class GitHubStorage {
376
  private $config;
377
  private $actualBranch = null;
378
+ private $userPath;
379
 
380
  public function __construct($config) {
381
  $this->config = $config;
382
+ $this->userPath = StorageConfig::getUserPath();
383
  // Auto-detect the actual default branch
384
  $this->detectDefaultBranch();
385
  }
 
431
  return false;
432
  }
433
 
434
+ // Add user path to filename
435
+ $userFilename = $this->userPath . $filename;
436
+ $path = $this->config['path'] . $userFilename;
437
  $url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$path}";
438
 
439
+ error_log("GitHub Save Debug: Attempting to save to $url on branch '{$this->config['branch']}' for user '{$this->userPath}'");
440
 
441
  // Get current file SHA if exists
442
  $sha = $this->getFileSHA($path);
443
 
444
  $data = [
445
+ 'message' => "Update {$userFilename} via VvvebJs",
446
  'content' => base64_encode($content),
447
  'branch' => $this->config['branch']
448
  ];
 
468
  public function get($filename) {
469
  if (empty($this->config['token'])) return false;
470
 
471
+ // Add user path to filename
472
+ $userFilename = $this->userPath . $filename;
473
+ $path = $this->config['path'] . $userFilename;
474
  $url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$path}";
475
 
476
  $result = $this->makeRequest($url, 'GET');
 
480
  return false;
481
  }
482
 
483
+ public function listUserFiles() {
484
+ if (empty($this->config['token'])) return [];
485
+
486
+ $userDir = $this->config['path'] . $this->userPath;
487
+ $url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$userDir}";
488
+
489
+ $result = $this->makeRequest($url, 'GET');
490
+ if ($result && is_array($result)) {
491
+ $files = [];
492
+ foreach ($result as $item) {
493
+ if ($item['type'] === 'file') {
494
+ // Remove user path prefix from filename
495
+ $relativePath = str_replace($this->userPath, '', $item['path']);
496
+ $relativePath = str_replace($this->config['path'], '', $relativePath);
497
+ $files[] = [
498
+ 'name' => $item['name'],
499
+ 'path' => $relativePath,
500
+ 'size' => $item['size'],
501
+ 'url' => $item['download_url']
502
+ ];
503
+ }
504
+ }
505
+ return $files;
506
+ }
507
+ return [];
508
+ }
509
+
510
+ public function delete($filename) {
511
+ if (empty($this->config['token'])) return false;
512
+
513
+ // Add user path to filename
514
+ $userFilename = $this->userPath . $filename;
515
+ $path = $this->config['path'] . $userFilename;
516
+ $url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$path}";
517
+
518
+ // Get current file SHA
519
+ $sha = $this->getFileSHA($path);
520
+ if (!$sha) return false;
521
+
522
+ $data = [
523
+ 'message' => "Delete {$userFilename} via VvvebJs",
524
+ 'sha' => $sha,
525
+ 'branch' => $this->config['branch']
526
+ ];
527
+
528
+ $result = $this->makeRequest($url, 'DELETE', $data);
529
+ return $result !== false;
530
+ }
531
+
532
  private function getFileSHA($path) {
533
  $url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$path}";
534
  $result = $this->makeRequest($url, 'GET');
 
599
  }
600
  }
601
 
602
+ // Storage manager with user isolation
603
  class StorageManager {
604
  private $kvStorage;
605
  private $githubStorage;
606
  private $storageType;
607
+ private $currentUser;
608
 
609
  public function __construct() {
610
  $this->storageType = StorageConfig::getStorageType();
611
+ $this->currentUser = StorageConfig::getCurrentUser();
612
 
613
  if (in_array($this->storageType, ['kv', 'both'])) {
614
  $this->kvStorage = new KVStorage(StorageConfig::getKVConfig());
 
620
  }
621
 
622
  public function saveFile($filename, $content) {
623
+ // Ensure filename is safe and doesn't contain path traversal
624
+ $filename = $this->sanitizeFilename($filename);
625
+
626
  $success = false;
627
 
628
  if ($this->githubStorage) {
 
637
  }
638
 
639
  public function getFile($filename) {
640
+ // Ensure filename is safe and doesn't contain path traversal
641
+ $filename = $this->sanitizeFilename($filename);
642
+
643
  if ($this->githubStorage) {
644
  $content = $this->githubStorage->get($filename);
645
  if ($content !== false) return $content;
 
652
 
653
  return false;
654
  }
655
+
656
+ public function listFiles() {
657
+ $files = [];
658
+
659
+ if ($this->githubStorage) {
660
+ $githubFiles = $this->githubStorage->listUserFiles();
661
+ $files = array_merge($files, $githubFiles);
662
+ }
663
+
664
+ if ($this->kvStorage) {
665
+ $kvFiles = $this->kvStorage->listUserFiles();
666
+ $files = array_merge($files, $kvFiles);
667
+ }
668
+
669
+ return $files;
670
+ }
671
+
672
+ public function deleteFile($filename) {
673
+ // Ensure filename is safe and doesn't contain path traversal
674
+ $filename = $this->sanitizeFilename($filename);
675
+
676
+ $success = false;
677
+
678
+ if ($this->githubStorage) {
679
+ $success = $this->githubStorage->delete($filename) || $success;
680
+ }
681
+
682
+ // KV storage delete would need to be implemented based on provider
683
+
684
+ return $success;
685
+ }
686
+
687
+ public function getCurrentUser() {
688
+ return $this->currentUser;
689
+ }
690
+
691
+ public function getUserPath() {
692
+ return StorageConfig::getUserPath();
693
+ }
694
+
695
+ private function sanitizeFilename($filename) {
696
+ // Remove any path traversal attempts
697
+ $filename = str_replace(['../', '..\\', '../', '..\\'], '', $filename);
698
+ // Remove leading slashes
699
+ $filename = ltrim($filename, '/\\');
700
+ // Ensure safe characters only
701
+ $filename = preg_replace('/[^a-zA-Z0-9_\-\.\/]/', '_', $filename);
702
+
703
+ return $filename;
704
+ }
705
  }
upload.php CHANGED
@@ -24,42 +24,69 @@ This script is used by image upload input to save the image on the server and re
24
  // Include authentication and storage
25
  require_once __DIR__ . '/save.php';
26
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  if ($_FILES && $_FILES['file']) {
28
  $file = $_FILES['file'];
29
 
30
  // Validate file
31
- $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
32
  if (!in_array($file['type'], $allowedTypes)) {
33
  http_response_code(400);
34
- die('Invalid file type');
 
 
 
 
35
  }
36
 
37
  $maxSize = 5 * 1024 * 1024; // 5MB
38
  if ($file['size'] > $maxSize) {
39
  http_response_code(400);
40
- die('File too large');
 
 
 
 
41
  }
42
 
43
- // Generate unique filename
44
  $extension = pathinfo($file['name'], PATHINFO_EXTENSION);
45
- $filename = 'uploads/' . uniqid() . '.' . $extension;
 
46
 
47
- // Save to local uploads directory
48
- $uploadDir = __DIR__ . '/uploads/';
49
- if (!is_dir($uploadDir)) {
50
- mkdir($uploadDir, 0777, true);
51
  }
52
 
53
- $localPath = $uploadDir . basename($filename);
54
 
55
  if (move_uploaded_file($file['tmp_name'], $localPath)) {
56
- // Try to save to external storage as well
57
  $fileContent = file_get_contents($localPath);
58
  $storageManager->saveFile($filename, $fileContent);
59
 
 
 
 
60
  echo json_encode([
61
  'success' => true,
62
- 'url' => $filename,
 
 
 
63
  'message' => 'File uploaded successfully'
64
  ]);
65
  } else {
 
24
  // Include authentication and storage
25
  require_once __DIR__ . '/save.php';
26
 
27
+ // Check if user is authenticated
28
+ if (!isset($_SESSION['user_id'])) {
29
+ http_response_code(401);
30
+ echo json_encode([
31
+ 'success' => false,
32
+ 'message' => 'Authentication required'
33
+ ]);
34
+ exit;
35
+ }
36
+
37
+ $userId = $_SESSION['user_id'];
38
+
39
  if ($_FILES && $_FILES['file']) {
40
  $file = $_FILES['file'];
41
 
42
  // Validate file
43
+ $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
44
  if (!in_array($file['type'], $allowedTypes)) {
45
  http_response_code(400);
46
+ echo json_encode([
47
+ 'success' => false,
48
+ 'message' => 'Invalid file type. Allowed: JPEG, PNG, GIF, WebP, SVG'
49
+ ]);
50
+ exit;
51
  }
52
 
53
  $maxSize = 5 * 1024 * 1024; // 5MB
54
  if ($file['size'] > $maxSize) {
55
  http_response_code(400);
56
+ echo json_encode([
57
+ 'success' => false,
58
+ 'message' => 'File too large. Maximum size: 5MB'
59
+ ]);
60
+ exit;
61
  }
62
 
63
+ // Generate unique filename with user prefix
64
  $extension = pathinfo($file['name'], PATHINFO_EXTENSION);
65
+ $timestamp = date('Y-m-d_H-i-s');
66
+ $filename = 'media/' . $userId . '_' . $timestamp . '_' . uniqid() . '.' . $extension;
67
 
68
+ // Create user-specific media directory
69
+ $userMediaDir = __DIR__ . '/user-files/' . $userId . '/media/';
70
+ if (!is_dir($userMediaDir)) {
71
+ mkdir($userMediaDir, 0777, true);
72
  }
73
 
74
+ $localPath = $userMediaDir . basename($filename);
75
 
76
  if (move_uploaded_file($file['tmp_name'], $localPath)) {
77
+ // Save to user's storage as well
78
  $fileContent = file_get_contents($localPath);
79
  $storageManager->saveFile($filename, $fileContent);
80
 
81
+ // Return relative path for use in editor
82
+ $relativePath = 'user-files/' . $userId . '/media/' . basename($filename);
83
+
84
  echo json_encode([
85
  'success' => true,
86
+ 'url' => $relativePath,
87
+ 'filename' => basename($filename),
88
+ 'size' => $file['size'],
89
+ 'type' => $file['type'],
90
  'message' => 'File uploaded successfully'
91
  ]);
92
  } else {
user-manager.php ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ session_start();
3
+
4
+ class UserManager {
5
+ private $usersFile = 'users.json';
6
+
7
+ public function __construct() {
8
+ if (!file_exists($this->usersFile)) {
9
+ file_put_contents($this->usersFile, json_encode([]));
10
+ }
11
+ }
12
+
13
+ public function registerUser($username, $password, $email = '') {
14
+ $users = $this->getUsers();
15
+
16
+ // Check if user already exists
17
+ if (isset($users[$username])) {
18
+ return ['success' => false, 'message' => 'User already exists'];
19
+ }
20
+
21
+ // Validate username
22
+ if (!preg_match('/^[a-zA-Z0-9_-]+$/', $username) || strlen($username) < 3) {
23
+ return ['success' => false, 'message' => 'Username must be at least 3 characters and contain only letters, numbers, underscore, and dash'];
24
+ }
25
+
26
+ // Hash password
27
+ $hashedPassword = password_hash($password, PASSWORD_DEFAULT);
28
+
29
+ // Add user
30
+ $users[$username] = [
31
+ 'password' => $hashedPassword,
32
+ 'email' => $email,
33
+ 'created' => date('Y-m-d H:i:s'),
34
+ 'last_login' => null
35
+ ];
36
+
37
+ if ($this->saveUsers($users)) {
38
+ return ['success' => true, 'message' => 'User registered successfully'];
39
+ } else {
40
+ return ['success' => false, 'message' => 'Failed to save user data'];
41
+ }
42
+ }
43
+
44
+ public function loginUser($username, $password) {
45
+ $users = $this->getUsers();
46
+
47
+ if (!isset($users[$username])) {
48
+ return ['success' => false, 'message' => 'User not found'];
49
+ }
50
+
51
+ if (password_verify($password, $users[$username]['password'])) {
52
+ $_SESSION['username'] = $username;
53
+ $_SESSION['logged_in'] = true;
54
+
55
+ // Update last login
56
+ $users[$username]['last_login'] = date('Y-m-d H:i:s');
57
+ $this->saveUsers($users);
58
+
59
+ return ['success' => true, 'message' => 'Login successful'];
60
+ } else {
61
+ return ['success' => false, 'message' => 'Invalid password'];
62
+ }
63
+ }
64
+
65
+ public function logoutUser() {
66
+ session_destroy();
67
+ return ['success' => true, 'message' => 'Logged out successfully'];
68
+ }
69
+
70
+ public function isLoggedIn() {
71
+ return isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true;
72
+ }
73
+
74
+ public function getCurrentUser() {
75
+ if ($this->isLoggedIn()) {
76
+ return $_SESSION['username'];
77
+ }
78
+ return null;
79
+ }
80
+
81
+ public function requireLogin() {
82
+ if (!$this->isLoggedIn()) {
83
+ header('Location: login.html');
84
+ exit;
85
+ }
86
+ }
87
+
88
+ private function getUsers() {
89
+ $content = file_get_contents($this->usersFile);
90
+ return json_decode($content, true) ?: [];
91
+ }
92
+
93
+ private function saveUsers($users) {
94
+ return file_put_contents($this->usersFile, json_encode($users, JSON_PRETTY_PRINT)) !== false;
95
+ }
96
+
97
+ public function getUserList() {
98
+ $users = $this->getUsers();
99
+ $userList = [];
100
+ foreach ($users as $username => $data) {
101
+ $userList[] = [
102
+ 'username' => $username,
103
+ 'email' => $data['email'],
104
+ 'created' => $data['created'],
105
+ 'last_login' => $data['last_login']
106
+ ];
107
+ }
108
+ return $userList;
109
+ }
110
+ }
111
+
112
+ // Handle AJAX requests
113
+ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
114
+ header('Content-Type: application/json');
115
+
116
+ $userManager = new UserManager();
117
+ $action = $_POST['action'] ?? '';
118
+
119
+ switch ($action) {
120
+ case 'register':
121
+ $username = $_POST['username'] ?? '';
122
+ $password = $_POST['password'] ?? '';
123
+ $email = $_POST['email'] ?? '';
124
+ echo json_encode($userManager->registerUser($username, $password, $email));
125
+ break;
126
+
127
+ case 'login':
128
+ $username = $_POST['username'] ?? '';
129
+ $password = $_POST['password'] ?? '';
130
+ echo json_encode($userManager->loginUser($username, $password));
131
+ break;
132
+
133
+ case 'logout':
134
+ echo json_encode($userManager->logoutUser());
135
+ break;
136
+
137
+ case 'check_login':
138
+ echo json_encode([
139
+ 'logged_in' => $userManager->isLoggedIn(),
140
+ 'username' => $userManager->getCurrentUser()
141
+ ]);
142
+ break;
143
+
144
+ default:
145
+ echo json_encode(['success' => false, 'message' => 'Invalid action']);
146
+ }
147
+ exit;
148
+ }
149
+ ?>