@underpostnet/underpost 2.97.5 → 2.98.0

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/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  <!-- badges -->
20
20
 
21
- [![Node.js CI](https://github.com/underpostnet/engine/actions/workflows/docker-image.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/docker-image.yml) [![Test](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml) [![Downloads](https://img.shields.io/npm/dm/underpost.svg)](https://www.npmjs.com/package/underpost) [![Socket Badge](https://socket.dev/api/badge/npm/package/underpost/2.97.5)](https://socket.dev/npm/package/underpost/overview/2.97.5) [![Coverage Status](https://coveralls.io/repos/github/underpostnet/engine/badge.svg?branch=master)](https://coveralls.io/github/underpostnet/engine?branch=master) [![Version](https://img.shields.io/npm/v/underpost.svg)](https://www.npmjs.org/package/underpost) [![License](https://img.shields.io/npm/l/underpost.svg)](https://www.npmjs.com/package/underpost)
21
+ [![Node.js CI](https://github.com/underpostnet/engine/actions/workflows/docker-image.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/docker-image.yml) [![Test](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml) [![Downloads](https://img.shields.io/npm/dm/underpost.svg)](https://www.npmjs.com/package/underpost) [![Socket Badge](https://socket.dev/api/badge/npm/package/underpost/2.98.0)](https://socket.dev/npm/package/underpost/overview/2.98.0) [![Coverage Status](https://coveralls.io/repos/github/underpostnet/engine/badge.svg?branch=master)](https://coveralls.io/github/underpostnet/engine?branch=master) [![Version](https://img.shields.io/npm/v/underpost.svg)](https://www.npmjs.org/package/underpost) [![License](https://img.shields.io/npm/l/underpost.svg)](https://www.npmjs.com/package/underpost)
22
22
 
23
23
  <!-- end-badges -->
24
24
 
@@ -66,7 +66,7 @@ Run dev client server
66
66
  npm run dev
67
67
  ```
68
68
  <!-- -->
69
- ## underpost ci/cd cli v2.97.5
69
+ ## underpost ci/cd cli v2.98.0
70
70
 
71
71
  ### Usage: `underpost [options] [command]`
72
72
  ```
package/cli.md CHANGED
@@ -1,4 +1,4 @@
1
- ## underpost ci/cd cli v2.97.5
1
+ ## underpost ci/cd cli v2.98.0
2
2
 
3
3
  ### Usage: `underpost [options] [command]`
4
4
  ```
@@ -17,7 +17,7 @@ spec:
17
17
  spec:
18
18
  containers:
19
19
  - name: dd-default-development-blue
20
- image: localhost/rockylinux9-underpost:v2.97.5
20
+ image: localhost/rockylinux9-underpost:v2.98.0
21
21
  # resources:
22
22
  # requests:
23
23
  # memory: "124Ki"
@@ -100,7 +100,7 @@ spec:
100
100
  spec:
101
101
  containers:
102
102
  - name: dd-default-development-green
103
- image: localhost/rockylinux9-underpost:v2.97.5
103
+ image: localhost/rockylinux9-underpost:v2.98.0
104
104
  # resources:
105
105
  # requests:
106
106
  # memory: "124Ki"
@@ -18,7 +18,7 @@ spec:
18
18
  spec:
19
19
  containers:
20
20
  - name: dd-test-development-blue
21
- image: localhost/rockylinux9-underpost:v2.97.5
21
+ image: localhost/rockylinux9-underpost:v2.98.0
22
22
 
23
23
  command:
24
24
  - /bin/sh
@@ -103,7 +103,7 @@ spec:
103
103
  spec:
104
104
  containers:
105
105
  - name: dd-test-development-green
106
- image: localhost/rockylinux9-underpost:v2.97.5
106
+ image: localhost/rockylinux9-underpost:v2.98.0
107
107
 
108
108
  command:
109
109
  - /bin/sh
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "main": "src/index.js",
4
4
  "name": "@underpostnet/underpost",
5
- "version": "2.97.5",
5
+ "version": "2.98.0",
6
6
  "description": "pwa api rest template",
7
7
  "scripts": {
8
8
  "start": "env-cmd -f .env.production node --max-old-space-size=8192 src/server",
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # rocky-pwa.sh
5
+ # Purpose: Build and install a PWA as a native application on Rocky Linux (RHEL).
6
+ # Usage: sudo ./rocky-pwa.sh <URL> <APP_NAME>
7
+
8
+ if [[ $EUID -ne 0 ]]; then
9
+ echo "ERROR: This script must be run as root." >&2
10
+ exit 1
11
+ fi
12
+
13
+ if [[ $# -lt 2 ]]; then
14
+ echo "Usage: $0 <URL> <APP_NAME>"
15
+ echo "Example: $0 'https://underpost.net' 'Underpost'"
16
+ exit 1
17
+ fi
18
+
19
+ TARGET_URL="$1"
20
+ RAW_NAME="$2"
21
+ # Sanitize name for filesystem (My App -> My_App)
22
+ APP_NAME="${RAW_NAME// /_}"
23
+ DEST_DIR="/opt/$APP_NAME"
24
+
25
+ echo ">>> Starting PWA installation for '$APP_NAME' ($TARGET_URL)"
26
+
27
+ # ------------------------------------------------------------------------------
28
+ # 1. Install Dependencies
29
+ # ------------------------------------------------------------------------------
30
+ echo ">>> Installing system dependencies..."
31
+ # Common Electron/Nativefier requirements
32
+ dnf install -y nodejs desktop-file-utils libXScrnSaver libX11 libXrandr \
33
+ alsa-lib atk at-spi2-core cups-libs wget curl grep \
34
+ || echo "Warning: specific package install failed, proceeding in case they exist..."
35
+
36
+ # ------------------------------------------------------------------------------
37
+ # 2. Install/Update Nativefier
38
+ # ------------------------------------------------------------------------------
39
+ if ! command -v nativefier &> /dev/null; then
40
+ echo ">>> Installing nativefier..."
41
+ npm install -g nativefier || echo "Warning: Global install had issues. Attempting to run via npx..."
42
+ fi
43
+
44
+ # ------------------------------------------------------------------------------
45
+ # 3. Build Application
46
+ # ------------------------------------------------------------------------------
47
+ BUILD_TMP=$(mktemp -d)
48
+ trap 'rm -rf "$BUILD_TMP"' EXIT
49
+
50
+ echo ">>> Building application in temporary directory..."
51
+ cd "$BUILD_TMP"
52
+
53
+ # --single-instance: Only allow one window
54
+ # --internal-urls ".*": Don't open links in external browser (keep user inside app)
55
+ npx --yes nativefier --name "$APP_NAME" \
56
+ --platform linux \
57
+ --arch x64 \
58
+ --single-instance \
59
+ --internal-urls ".*" \
60
+ "$TARGET_URL"
61
+
62
+ # ------------------------------------------------------------------------------
63
+ # 4. Install to /opt
64
+ # ------------------------------------------------------------------------------
65
+ # Nativefier creates a folder named like "Underpost-linux-x64"
66
+ BUILT_FOLDER=$(find . -maxdepth 1 -type d -name "*-linux-x64" | head -n 1)
67
+
68
+ if [[ -z "$BUILT_FOLDER" ]]; then
69
+ echo "ERROR: Build failed. No output directory found."
70
+ exit 1
71
+ fi
72
+
73
+ echo ">>> Installing to $DEST_DIR..."
74
+ rm -rf "$DEST_DIR"
75
+ mv "$BUILT_FOLDER" "$DEST_DIR"
76
+
77
+ # Fix ownership
78
+ chown -R root:root "$DEST_DIR"
79
+ chmod -R 755 "$DEST_DIR"
80
+
81
+ # ------------------------------------------------------------------------------
82
+ # 5. Locate Executable & Fix Nested Structure
83
+ # ------------------------------------------------------------------------------
84
+ # Sometimes Nativefier nests: /opt/App/App-linux-x64/App
85
+ # We want: /opt/App/App
86
+ # Check if the DEST_DIR contains only one folder which is also named *-linux-x64
87
+ NESTED_DIR=$(find "$DEST_DIR" -mindepth 1 -maxdepth 1 -type d -name "*-linux-x64" | head -n 1)
88
+
89
+ if [[ -n "$NESTED_DIR" ]]; then
90
+ echo ">>> Flattening nested directory structure..."
91
+ # Move contents up
92
+ mv "$NESTED_DIR"/* "$DEST_DIR/"
93
+ rmdir "$NESTED_DIR"
94
+ fi
95
+
96
+ # Find the binary
97
+ EXECUTABLE="$DEST_DIR/$APP_NAME"
98
+ if [[ ! -f "$EXECUTABLE" ]]; then
99
+ # Try finding any executable file that isn't a library or helper
100
+ EXECUTABLE=$(find "$DEST_DIR" -maxdepth 2 -type f -executable ! -name "*.so*" ! -name "chrome_sandbox" ! -name "*.sh" | head -n 1)
101
+ fi
102
+
103
+ if [[ -z "$EXECUTABLE" || ! -f "$EXECUTABLE" ]]; then
104
+ echo "ERROR: Could not locate executable file in $DEST_DIR"
105
+ exit 1
106
+ fi
107
+
108
+ echo ">>> Found binary: $EXECUTABLE"
109
+
110
+ # ------------------------------------------------------------------------------
111
+ # 6. System Integration (Symlink, Icon, Desktop File)
112
+ # ------------------------------------------------------------------------------
113
+ BIN_LINK="/usr/local/bin/$APP_NAME"
114
+ echo ">>> Creating symlink at $BIN_LINK..."
115
+ ln -sf "$EXECUTABLE" "$BIN_LINK"
116
+
117
+ # Handle Icon
118
+ ICON_DEST="/usr/share/pixmaps/$APP_NAME.png"
119
+ FOUND_ICON=""
120
+
121
+ # 1. Look in resources
122
+ if [[ -f "$DEST_DIR/resources/app/icon.png" ]]; then
123
+ FOUND_ICON="$DEST_DIR/resources/app/icon.png"
124
+ elif [[ -f "$DEST_DIR/resources/app/icon.ico" ]]; then
125
+ FOUND_ICON="$DEST_DIR/resources/app/icon.ico"
126
+ fi
127
+
128
+ # 2. If not found, try to download from PWA manifest (Simplified)
129
+ if [[ -z "$FOUND_ICON" ]]; then
130
+ echo ">>> Icon not found in build. Attempting to fetch from website..."
131
+ # Simple heuristic: try to grab apple-touch-icon or shortcut icon
132
+ ICON_URL=$(curl -sL "$TARGET_URL" | grep -oP 'rel="(apple-touch-icon|icon|shortcut icon)" href="\K[^"]+' | head -n 1)
133
+
134
+ if [[ -n "$ICON_URL" ]]; then
135
+ # Handle relative URLs
136
+ if [[ "$ICON_URL" != http* ]]; then
137
+ # Remove trailing slash from base if present and leading slash from path
138
+ BASE_URL="${TARGET_URL%/}"
139
+ PATH_URL="${ICON_URL#/}"
140
+ ICON_URL="$BASE_URL/$PATH_URL"
141
+ fi
142
+
143
+ echo ">>> Downloading icon from $ICON_URL..."
144
+ wget -q -O "$BUILD_TMP/downloaded_icon" "$ICON_URL" || true
145
+ if [[ -s "$BUILD_TMP/downloaded_icon" ]]; then
146
+ FOUND_ICON="$BUILD_TMP/downloaded_icon"
147
+ fi
148
+ fi
149
+ fi
150
+
151
+ if [[ -n "$FOUND_ICON" ]]; then
152
+ cp "$FOUND_ICON" "$ICON_DEST"
153
+ chmod 644 "$ICON_DEST"
154
+ echo ">>> Icon installed to $ICON_DEST"
155
+ else
156
+ echo ">>> WARNING: No icon found. Desktop entry will use generic icon."
157
+ fi
158
+
159
+ # Desktop File
160
+ DESKTOP_FILE="/usr/share/applications/$APP_NAME.desktop"
161
+ echo ">>> Creating desktop entry at $DESKTOP_FILE..."
162
+
163
+ cat > "$DESKTOP_FILE" <<EOF
164
+ [Desktop Entry]
165
+ Name=$RAW_NAME
166
+ Exec=$BIN_LINK %U
167
+ Icon=$APP_NAME
168
+ Type=Application
169
+ StartupNotify=true
170
+ Categories=Network;Web;
171
+ Terminal=false
172
+ StartupWMClass=$APP_NAME
173
+ EOF
174
+
175
+ # Update cache
176
+ if command -v update-desktop-database &> /dev/null; then
177
+ update-desktop-database /usr/share/applications || true
178
+ fi
179
+
180
+ # ------------------------------------------------------------------------------
181
+ # 7. Create /bin wrapper with --no-sandbox flag
182
+ # ------------------------------------------------------------------------------
183
+ BIN_WRAPPER="/bin/$APP_NAME"
184
+ echo ">>> Creating wrapper script at $BIN_WRAPPER..."
185
+
186
+ cat > "$BIN_WRAPPER" <<EOF
187
+ #!/usr/bin/env bash
188
+ exec '$DEST_DIR/$APP_NAME' --no-sandbox "\$@"
189
+ EOF
190
+
191
+ chmod 755 "$BIN_WRAPPER"
192
+ echo ">>> Wrapper script created: $BIN_WRAPPER"
193
+
194
+ echo "------------------------------------------------------"
195
+ echo " Installation Complete!"
196
+ echo " App Name: $RAW_NAME"
197
+ echo " Command: $APP_NAME"
198
+ echo " Location: $DEST_DIR"
199
+ echo " Wrapper: $BIN_WRAPPER"
200
+ echo "------------------------------------------------------"
@@ -60,7 +60,7 @@ const DocumentDto = {
60
60
  return {
61
61
  path: 'userId',
62
62
  model: 'User',
63
- select: '_id email username profileImageId role briefDescription',
63
+ select: '_id role username profileImageId briefDescription',
64
64
  populate: {
65
65
  path: 'profileImageId',
66
66
  model: 'File',
@@ -31,6 +31,8 @@ const DocumentService = {
31
31
  const Document = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Document;
32
32
  /** @type {import('../user/user.model.js').UserModel} */
33
33
  const User = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.User;
34
+ /** @type {import('../file/file.model.js').FileModel} */
35
+ const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
34
36
 
35
37
  // High-query endpoint for typeahead search
36
38
  //
@@ -38,21 +40,8 @@ const DocumentService = {
38
40
  // - Unauthenticated users: CAN see public documents (isPublic=true) from publishers (admin/moderator)
39
41
  // - Authenticated users: CAN see public documents from publishers + ALL their own documents (public or private)
40
42
  // - No user can see private documents from other users
41
- //
42
- // Search Optimization Strategy:
43
- // 1. Case-insensitive matching ($options: 'i') - maximizes matches across case variations
44
- // 2. Multi-term search - splits "hello world" into ["hello", "world"] and matches ANY term
45
- // 3. Multi-field search - searches BOTH title AND tags array
46
- // 4. OR logic - ANY term matching ANY field counts as a match
47
- // 5. Minimum length: 1 character - allows maximum user flexibility
48
- //
49
- // Example: Query "javascript tutorial"
50
- // - Matches documents with title "JavaScript Guide" (term 1, case-insensitive)
51
- // - Matches documents with tag "tutorial" (term 2, tag match)
52
- // - Matches documents with both terms in different fields
53
- //
43
+ // - PANEL FILTER: Only documents with idPanel tag are returned (prevents out-of-panel context results)
54
44
  if (req.path.startsWith('/public/high') && req.query['q']) {
55
- // Input validation
56
45
  const rawQuery = req.query['q'];
57
46
  if (!rawQuery || typeof rawQuery !== 'string') {
58
47
  throw new Error('Invalid search query');
@@ -68,6 +57,13 @@ const DocumentService = {
68
57
  throw new Error('Search query too long (max 100 characters)');
69
58
  }
70
59
 
60
+ // Get idPanel filter to prevent out-of-panel context results
61
+ const idPanel = req.query['idPanel'];
62
+ if (!idPanel || typeof idPanel !== 'string') {
63
+ logger.warn('Missing idPanel parameter for high-query search');
64
+ return { data: [] };
65
+ }
66
+
71
67
  const publisherUsers = await User.find({ $or: [{ role: 'admin' }, { role: 'moderator' }] });
72
68
 
73
69
  const token = getBearerToken(req);
@@ -97,15 +93,9 @@ const DocumentService = {
97
93
  const queryPayload = {
98
94
  isPublic: true,
99
95
  userId: { $in: publisherUsers.map((p) => p._id) },
96
+ tags: { $in: [idPanel] }, // Filter by idPanel to prevent out-of-panel context results (tags is array)
100
97
  };
101
98
 
102
- logger.info('Special "public" search query', {
103
- authenticated: !!user,
104
- userId: user?._id?.toString(),
105
- role: user?.role,
106
- limit,
107
- });
108
-
109
99
  const data = await Document.find(queryPayload)
110
100
  .sort({ createdAt: -1 })
111
101
  .limit(limit)
@@ -139,7 +129,7 @@ const DocumentService = {
139
129
  return filteredDoc;
140
130
  });
141
131
 
142
- return { data: sanitizedData };
132
+ return { data: sanitizedData.map((d) => (d.userId.role = undefined)) };
143
133
  }
144
134
 
145
135
  // OPTIMIZATION: Split search query into individual terms for multi-term matching
@@ -151,21 +141,7 @@ const DocumentService = {
151
141
  .map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); // Escape each term for regex safety
152
142
 
153
143
  // Build query based on authentication status
154
- // ============================================
155
- // OPTIMIZED FOR MAXIMUM RESULTS:
156
- // - Multi-term search: matches ANY term
157
- // - Case-insensitive: $options: 'i' flag
158
- // - Multi-field: searches title AND tags
159
- // - Minimum match: ANY term in ANY field = result
160
- //
161
- // Example Query: "javascript react"
162
- // Matches:
163
- // ✓ Document with title "JavaScript Tutorial" (term 1 in title)
164
- // ✓ Document with tag "react" (term 2 in tags)
165
- // ✓ Document with title "Learn React JS" (term 2 in title, case-insensitive)
166
- // ✓ Document with tags ["javascript", "tutorial"] (term 1 in tags)
167
-
168
- // Build search conditions for maximum permissiveness
144
+ // and search conditions for maximum permissiveness
169
145
  const buildSearchConditions = () => {
170
146
  const conditions = [];
171
147
 
@@ -185,14 +161,11 @@ const DocumentService = {
185
161
  // Authenticated user can see:
186
162
  // 1. ALL their own documents (public AND private - no tag restriction)
187
163
  // 2. Public-tagged documents from publishers (admin/moderator only)
188
- //
189
- // MAXIMUM RESULTS STRATEGY:
190
- // - Search by: ANY term matches title OR ANY tag
191
- // - Case-insensitive matching
192
- // - No minimum match threshold beyond 1 character
164
+
193
165
  const searchConditions = buildSearchConditions();
194
166
 
195
167
  queryPayload = {
168
+ tags: { $in: [idPanel] }, // Filter by idPanel to prevent out-of-panel context results (tags is array)
196
169
  $or: [
197
170
  {
198
171
  // Public documents from publishers (admin/moderator)
@@ -210,14 +183,11 @@ const DocumentService = {
210
183
  };
211
184
  } else {
212
185
  // Public/unauthenticated user: ONLY public-tagged documents from publishers (admin/moderator)
213
- //
214
- // MAXIMUM RESULTS STRATEGY for public users:
215
- // - Search by: ANY term matches title OR ANY tag
216
- // - Case-insensitive matching
217
- // - Still respects security: only public docs from trusted publishers
186
+
218
187
  const searchConditions = buildSearchConditions();
219
188
 
220
189
  queryPayload = {
190
+ tags: { $in: [idPanel] }, // Filter by idPanel to prevent out-of-panel context results (tags is array)
221
191
  userId: { $in: publisherUsers.map((p) => p._id) },
222
192
  isPublic: true,
223
193
  $or: searchConditions, // ANY term in title OR tags
@@ -230,19 +200,6 @@ const DocumentService = {
230
200
  return { data: [] };
231
201
  }
232
202
 
233
- // Security audit logging
234
- logger.info('High-query search (OPTIMIZED FOR MAX RESULTS)', {
235
- query: searchQuery.substring(0, 50), // Log only first 50 chars for privacy
236
- terms: searchTerms.length, // Number of search terms
237
- searchStrategy: 'multi-term OR matching, case-insensitive, title+tags',
238
- authenticated: !!user,
239
- userId: user?._id?.toString(),
240
- role: user?.role,
241
- limit,
242
- publishersCount: publisherUsers.length,
243
- timestamp: new Date().toISOString(),
244
- });
245
-
246
203
  const data = await Document.find(queryPayload)
247
204
  .sort({ createdAt: -1 })
248
205
  .limit(limit)
@@ -265,10 +222,7 @@ const DocumentService = {
265
222
  }
266
223
  }
267
224
  // Remove role field from userId before sending to client (all users)
268
- if (filteredDoc.userId && filteredDoc.userId.role) {
269
- const { role, ...userWithoutRole } = filteredDoc.userId;
270
- filteredDoc.userId = userWithoutRole;
271
- }
225
+ delete filteredDoc.userId.role;
272
226
  return filteredDoc;
273
227
  });
274
228
 
@@ -372,18 +326,7 @@ const DocumentService = {
372
326
  };
373
327
  }
374
328
  }
375
- // Security audit logging
376
- logger.info('Public tag search', {
377
- authenticated: !!user,
378
- userId: user?._id?.toString(),
379
- role: user?.role,
380
- requestedTags,
381
- hasPublicTag,
382
- hasCidFilter: !!req.query.cid,
383
- limit,
384
- skip,
385
- publishersCount: publisherUsers.length,
386
- });
329
+
387
330
  // sort in descending (-1) order by length
388
331
  const sort = { createdAt: -1 };
389
332
 
@@ -417,9 +360,11 @@ const DocumentService = {
417
360
  if ((!docObj.isPublic || !isPublisher) && !isOwnDoc) userInfo = undefined;
418
361
  return {
419
362
  ...docObj,
420
- role: undefined,
421
- email: undefined,
422
- userId: userInfo,
363
+ userId: {
364
+ ...userInfo,
365
+ role: undefined,
366
+ email: undefined,
367
+ },
423
368
  tags: DocumentDto.filterPublicTag(docObj.tags),
424
369
  totalCopyShareLinkCount: DocumentDto.getTotalCopyShareLinkCount(doc),
425
370
  };
@@ -430,32 +375,65 @@ const DocumentService = {
430
375
 
431
376
  switch (req.params.id) {
432
377
  default: {
433
- const data = await Document.find({
378
+ // Simple pagination support for FileExplorer
379
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : 50;
380
+ const skip = req.query.skip ? parseInt(req.query.skip, 10) : 0;
381
+
382
+ // Search filter parameters
383
+ const searchTitle = req.query.searchTitle ? req.query.searchTitle.trim() : '';
384
+ const searchMdFile = req.query.searchMdFile ? req.query.searchMdFile.trim() : '';
385
+ const searchFile = req.query.searchFile ? req.query.searchFile.trim() : '';
386
+
387
+ const query = {
434
388
  userId: req.auth.user._id,
435
389
  ...(req.params.id ? { _id: req.params.id } : undefined),
436
- })
390
+ };
391
+
392
+ // Filter by title
393
+ if (searchTitle) {
394
+ const searchRegex = searchTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
395
+ query.title = { $regex: searchRegex, $options: 'i' };
396
+ }
397
+
398
+ // Filter by markdown file name
399
+ if (searchMdFile) {
400
+ const searchRegex = searchMdFile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
401
+ const files = await File.find({ name: { $regex: searchRegex, $options: 'i' } }).select('_id');
402
+ query.mdFileId = { $in: files.map((f) => f._id) };
403
+ }
404
+
405
+ // Filter by generic file name
406
+ if (searchFile) {
407
+ const searchRegex = searchFile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
408
+ const files = await File.find({ name: { $regex: searchRegex, $options: 'i' } }).select('_id');
409
+ query.fileId = { $in: files.map((f) => f._id) };
410
+ }
411
+
412
+ // Get total count for pagination
413
+ const totalCount = await Document.countDocuments(query);
414
+
415
+ const data = await Document.find(query)
416
+ .sort({ createdAt: -1 })
417
+ .limit(limit)
418
+ .skip(skip)
437
419
  .populate(DocumentDto.populate.file())
438
420
  .populate(DocumentDto.populate.mdFile())
439
421
  .populate(DocumentDto.populate.user());
440
422
 
441
- // Add totalCopyShareLinkCount to each document and filter 'public' from tags
442
- return data.map((doc) => {
443
- const docObj = doc.toObject ? doc.toObject() : doc;
444
-
445
- // Remove role field from userId before sending to client
446
- let userInfo = docObj.userId;
447
- if (userInfo && userInfo.role) {
448
- const { role, ...userWithoutRole } = userInfo;
449
- userInfo = userWithoutRole;
450
- }
451
-
452
- return {
453
- ...docObj,
454
- userId: userInfo,
455
- tags: DocumentDto.filterPublicTag(docObj.tags),
456
- totalCopyShareLinkCount: DocumentDto.getTotalCopyShareLinkCount(doc),
457
- };
458
- });
423
+ return {
424
+ data,
425
+ pagination: {
426
+ totalCount,
427
+ limit,
428
+ skip,
429
+ hasMore: skip + data.length < totalCount,
430
+ search: {
431
+ title: searchTitle,
432
+ mdFile: searchMdFile,
433
+ file: searchFile,
434
+ },
435
+ },
436
+ };
459
437
  }
460
438
  }
461
439
  },
@@ -502,6 +480,18 @@ const DocumentService = {
502
480
  File,
503
481
  });
504
482
 
483
+ // Update file names if provided
484
+ if (req.body.mdFileName && document.mdFileId) {
485
+ await File.findByIdAndUpdate(document.mdFileId, { name: req.body.mdFileName });
486
+ }
487
+ if (req.body.fileName && document.fileId) {
488
+ await File.findByIdAndUpdate(document.fileId, { name: req.body.fileName });
489
+ }
490
+
491
+ // Remove file name fields from body as they are not part of Document schema
492
+ delete req.body.mdFileName;
493
+ delete req.body.fileName;
494
+
505
495
  // Extract 'public' from tags and set isPublic field on update
506
496
  const { isPublic, tags } = DocumentDto.extractPublicFromTags(req.body.tags);
507
497
  req.body.isPublic = isPublic;
@@ -11,6 +11,7 @@ import { loggerFactory } from './Logger.js';
11
11
  import { LogIn } from './LogIn.js';
12
12
  import { LogOut } from './LogOut.js';
13
13
  import { NotificationManager } from './NotificationManager.js';
14
+ import { SearchBox } from './SearchBox.js';
14
15
  import { Translate } from './Translate.js';
15
16
  import { s } from './VanillaJs.js';
16
17
 
@@ -267,6 +268,7 @@ class Auth {
267
268
  try {
268
269
  const result = await UserService.delete({ id: 'logout' });
269
270
  localStorage.removeItem('jwt');
271
+ SearchBox.RecentResults.clear();
270
272
  this.deleteToken();
271
273
  if (this.#refreshTimeout) {
272
274
  clearTimeout(this.#refreshTimeout);
@@ -32,7 +32,8 @@ const Content = {
32
32
  if (!queryParams.cid) throw new Error(`no-result-found`);
33
33
 
34
34
  {
35
- const { data, status, message } = await DocumentService.get({ id: queryParams.cid });
35
+ const { data: responseData, status, message } = await DocumentService.get({ id: queryParams.cid });
36
+ const data = Array.isArray(responseData) ? responseData : responseData?.data || [];
36
37
  if (status !== 'success' || !data || !data[0]) {
37
38
  logger.error(message);
38
39
  throw new Error(`no-result-found`);
@@ -81,7 +82,6 @@ const Content = {
81
82
  if (file) await this.RenderFile({ idModal, file, id: file._id });
82
83
  Modal.Data[idModal].onObserverListener[`main-content-observer`]();
83
84
  } catch (error) {
84
- logger.error(error);
85
85
  htmls(`.content-render-${idModal}`, '');
86
86
  htmls(`.error-${idModal}`, html`<i class="fas fa-exclamation-circle"></i> ${Translate.Render(error.message)}`);
87
87
  s(`.error-${idModal}`).classList.remove('hide');
@@ -340,6 +340,36 @@ const dynamicCol = (options = { containerSelector: '', id: '', type: '', limit:
340
340
  );
341
341
  break;
342
342
 
343
+ case 'search-inputs':
344
+ if (s(`.${containerSelector}`).offsetWidth < limitCol)
345
+ htmls(
346
+ `.style-${id}-col`,
347
+ css`
348
+ .${id}-col-a, .${id}-col-b, .${id}-col-c, .${id}-col-d {
349
+ width: 100%;
350
+ }
351
+ `,
352
+ );
353
+ else
354
+ htmls(
355
+ `.style-${id}-col`,
356
+ css`
357
+ .${id}-col-a {
358
+ width: 30%;
359
+ }
360
+ .${id}-col-b {
361
+ width: 30%;
362
+ }
363
+ .${id}-col-c {
364
+ width: 30%;
365
+ }
366
+ .${id}-col-d {
367
+ width: 10%;
368
+ }
369
+ `,
370
+ );
371
+ break;
372
+
343
373
  default:
344
374
  if (s(`.${containerSelector}`).offsetWidth < 900)
345
375
  htmls(