@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 +2 -2
- package/cli.md +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +1 -1
- package/scripts/rocky-pwa.sh +200 -0
- package/src/api/document/document.model.js +1 -1
- package/src/api/document/document.service.js +89 -99
- package/src/client/components/core/Auth.js +2 -0
- package/src/client/components/core/Content.js +2 -2
- package/src/client/components/core/Css.js +30 -0
- package/src/client/components/core/FileExplorer.js +699 -42
- package/src/client/components/core/Input.js +3 -1
- package/src/client/components/core/Panel.js +2 -1
- package/src/client/components/core/Responsive.js +15 -7
- package/src/client/components/core/SearchBox.js +0 -110
- package/src/client/components/core/Translate.js +50 -0
- package/src/client/services/default/default.management.js +3 -12
- package/src/client/sw/default.sw.js +107 -184
- package/src/index.js +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
<!-- badges -->
|
|
20
20
|
|
|
21
|
-
[](https://github.com/underpostnet/engine/actions/workflows/docker-image.yml) [](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml) [](https://www.npmjs.com/package/underpost) [](https://github.com/underpostnet/engine/actions/workflows/docker-image.yml) [](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml) [](https://www.npmjs.com/package/underpost) [](https://socket.dev/npm/package/underpost/overview/2.98.0) [](https://coveralls.io/github/underpostnet/engine?branch=master) [](https://www.npmjs.org/package/underpost) [](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.
|
|
69
|
+
## underpost ci/cd cli v2.98.0
|
|
70
70
|
|
|
71
71
|
### Usage: `underpost [options] [command]`
|
|
72
72
|
```
|
package/cli.md
CHANGED
|
@@ -17,7 +17,7 @@ spec:
|
|
|
17
17
|
spec:
|
|
18
18
|
containers:
|
|
19
19
|
- name: dd-default-development-blue
|
|
20
|
-
image: localhost/rockylinux9-underpost:v2.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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(
|