nodebb-plugin-gemach-directory 1.0.0 → 1.0.2

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.
Files changed (2) hide show
  1. package/library.js +105 -5
  2. package/package.json +1 -1
package/library.js CHANGED
@@ -12,7 +12,13 @@
12
12
  * למנהלים (נבדק בשרת, לא רק בממשק) - אישור מעביר את הגמ"ח לרשימה
13
13
  * הציבורית באופן מיידי, דחייה מוחקת אותו לגמרי (בלי לצבור "זבל").
14
14
  * - הרשימה הציבורית (SocketPlugins.gemachDirectory.listApproved) פתוחה
15
- * לכולם, כולל גולשים לא-מחוברים.
15
+ * לכולם, כולל גולשים לא-מחוברים. כל גמ"ח מוחזר עם שם המשתמש שהעלה אותו
16
+ * (submittedByUsername/submittedByUserslug) - כדי שהלקוח יוכל להציג
17
+ * קרדיט ("מאת @שם") ולדעת אם המשתמש המחובר הוא הבעלים.
18
+ * - עריכה/מחיקה (SocketPlugins.gemachDirectory.edit/remove) פתוחים למי
19
+ * שהעלה את הגמ"ח (נבדק לפי socket.uid מול השדה submittedBy שנשמר בזמן
20
+ * ההעלאה - אי אפשר לזייף) *או* למנהל - מנהל יכול לערוך/למחוק כל גמ"ח,
21
+ * משתמש רגיל רק את שלו.
16
22
  *
17
23
  * חלק הלקוח (הצגת הרשימה, הטופס להוספה, פאנל האישור למנהל) *לא* נמצא כאן -
18
24
  * הוא קובץ נפרד (gemach-directory-client.js) שמודבק ב-Custom JS של הפורום,
@@ -32,6 +38,9 @@ const COUNTER_OBJECT = 'gemachDirectory:counters';
32
38
  const PENDING_SET = 'gemachDirectory:pending';
33
39
  const APPROVED_SET = 'gemachDirectory:approved';
34
40
  const GEMACH_KEY = id => `gemachDirectory:item:${id}`;
41
+ // מפה של uid -> '0'/'1' - האם המנהל הזה רוצה לקבל התראות על גמ"חים חדשים.
42
+ // חסר מפתח = כברירת מחדל כן (כדי שמנהלים קיימים לא "יפספסו" בלי לשים לב).
43
+ const NOTIFY_PREFS_OBJECT = 'gemachDirectory:notifyPrefs';
35
44
 
36
45
  const MAX_LENGTHS = {
37
46
  name: 120,
@@ -123,6 +132,63 @@ function registerSocketHandlers() {
123
132
 
124
133
  return { ok: true };
125
134
  };
135
+
136
+ // כל מנהל בודק/קובע בעצמו אם הוא רוצה לקבל התראות על הצעות חדשות -
137
+ // לא משפיע על שאר המנהלים.
138
+ SocketPlugins.gemachDirectory.getNotifyPreference = async function (socket) {
139
+ await requireAdmin(socket);
140
+ const value = await db.getObjectField(NOTIFY_PREFS_OBJECT, socket.uid);
141
+ return { enabled: value !== '0' };
142
+ };
143
+
144
+ SocketPlugins.gemachDirectory.setNotifyPreference = async function (socket, data) {
145
+ await requireAdmin(socket);
146
+ const enabled = !!(data && data.enabled);
147
+ await db.setObjectField(NOTIFY_PREFS_OBJECT, socket.uid, enabled ? '1' : '0');
148
+ return { ok: true };
149
+ };
150
+
151
+ // עריכת גמ"ח קיים - הבעלים (submittedBy) או מנהל בלבד. עובד גם על גמ"ח
152
+ // ממתין וגם על גמ"ח מאושר, בלי לשנות את הסטטוס שלו.
153
+ SocketPlugins.gemachDirectory.edit = async function (socket, data) {
154
+ requireLogin(socket);
155
+ const id = data && data.id;
156
+ if (!id) throw new Error('[[error:invalid-data]]');
157
+
158
+ const gemach = await db.getObject(GEMACH_KEY(id));
159
+ if (!gemach) throw new Error('[[error:no-such-gemach]]');
160
+ await requireOwnerOrAdmin(socket, gemach);
161
+
162
+ const name = sanitizeText(data && data.name, MAX_LENGTHS.name);
163
+ const city = sanitizeText(data && data.city, MAX_LENGTHS.city);
164
+ const category = sanitizeText(data && data.category, MAX_LENGTHS.category);
165
+ const contact = sanitizeText(data && data.contact, MAX_LENGTHS.contact);
166
+ const description = sanitizeText(data && data.description, MAX_LENGTHS.description);
167
+
168
+ if (!name || !city || !category || !contact) {
169
+ throw new Error('[[error:invalid-data]]');
170
+ }
171
+
172
+ await db.setObject(GEMACH_KEY(id), { name, city, category, contact, description });
173
+ return { ok: true };
174
+ };
175
+
176
+ // מחיקת גמ"ח - הבעלים או מנהל בלבד. מוחק לגמרי מכל הרשימות (ממתין/מאושר).
177
+ SocketPlugins.gemachDirectory.remove = async function (socket, data) {
178
+ requireLogin(socket);
179
+ const id = data && data.id;
180
+ if (!id) throw new Error('[[error:invalid-data]]');
181
+
182
+ const gemach = await db.getObject(GEMACH_KEY(id));
183
+ if (!gemach) return { ok: true };
184
+ await requireOwnerOrAdmin(socket, gemach);
185
+
186
+ await db.sortedSetRemove(PENDING_SET, id);
187
+ await db.sortedSetRemove(APPROVED_SET, id);
188
+ await db.delete(GEMACH_KEY(id));
189
+
190
+ return { ok: true };
191
+ };
126
192
  }
127
193
 
128
194
  async function getGemachsFromSet(setKey, newestFirst) {
@@ -130,14 +196,38 @@ async function getGemachsFromSet(setKey, newestFirst) {
130
196
  await db.getSortedSetRevRange(setKey, 0, -1) :
131
197
  await db.getSortedSetRange(setKey, 0, -1);
132
198
  if (!ids.length) return [];
133
- const gemachs = await db.getObjects(ids.map(GEMACH_KEY));
134
- return gemachs.filter(Boolean);
199
+ const gemachs = (await db.getObjects(ids.map(GEMACH_KEY))).filter(Boolean);
200
+ return attachSubmitterInfo(gemachs);
201
+ }
202
+
203
+ // מוסיף לכל גמ"ח את שם המשתמש (לקרדיט) ואת ה-userslug (לקישור לפרופיל)
204
+ // של מי שהעלה אותו - כדי שהלקוח לא יצטרך שאילתת משתמש נפרדת לכל כרטיס.
205
+ async function attachSubmitterInfo(gemachs) {
206
+ const uids = gemachs.map(g => g.submittedBy).filter(Boolean);
207
+ if (!uids.length) return gemachs;
208
+
209
+ const users = await user.getUsersFields(uids, ['uid', 'username', 'userslug']);
210
+ const byUid = {};
211
+ users.forEach((u) => { byUid[u.uid] = u; });
212
+
213
+ return gemachs.map((g) => {
214
+ const submitter = byUid[g.submittedBy];
215
+ return Object.assign({}, g, {
216
+ submittedByUsername: submitter ? submitter.username : null,
217
+ submittedByUserslug: submitter ? submitter.userslug : null,
218
+ });
219
+ });
135
220
  }
136
221
 
137
222
  async function notifyAdmins(gemach) {
138
223
  const adminUids = await groups.getMembers('administrators', 0, -1);
139
224
  if (!adminUids || !adminUids.length) return;
140
225
 
226
+ const prefs = (await db.getObject(NOTIFY_PREFS_OBJECT)) || {};
227
+ // חסר מפתח = כברירת מחדל כן (רק '0' מפורש מכבה עבור מנהל ספציפי).
228
+ const targetUids = adminUids.filter(uid => prefs[uid] !== '0');
229
+ if (!targetUids.length) return;
230
+
141
231
  const notification = await notifications.create({
142
232
  type: 'gemach-directory-pending',
143
233
  // nid כולל את מזהה הגמ"ח - כך שכל הצעה חדשה היא התראה "חדשה" נפרדת
@@ -146,7 +236,7 @@ async function notifyAdmins(gemach) {
146
236
  path: '/',
147
237
  from: gemach.submittedBy,
148
238
  });
149
- await notifications.push(notification, adminUids);
239
+ await notifications.push(notification, targetUids);
150
240
  }
151
241
 
152
242
  function requireLogin(socket) {
@@ -163,6 +253,16 @@ async function requireAdmin(socket) {
163
253
  }
164
254
  }
165
255
 
256
+ // מרשה גישה רק למי שהעלה את הגמ"ח הזה (submittedBy) או למנהל.
257
+ async function requireOwnerOrAdmin(socket, gemach) {
258
+ requireLogin(socket);
259
+ if (String(gemach.submittedBy) === String(socket.uid)) return;
260
+ const isAdmin = await user.isAdministrator(socket.uid);
261
+ if (!isAdmin) {
262
+ throw new Error('[[error:no-privileges]]');
263
+ }
264
+ }
265
+
166
266
  // חיתוך אורך + הסרת תווי בקרה - הגנת שרת בסיסית. ההגנה האמיתית מפני
167
267
  // XSS היא ב-escaping בצד הלקוח בזמן הצגה (ראו gemach-directory-client.js),
168
268
  // כי הנתונים האלה מוזרקים שם ישירות ל-innerHTML.
@@ -173,4 +273,4 @@ function sanitizeText(value, maxLength) {
173
273
  return stripped.slice(0, maxLength);
174
274
  }
175
275
 
176
- module.exports = plugin;
276
+ module.exports = plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-gemach-directory",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Server-side storage + admin approval queue for a community gemach directory, filterable by city and category.",
5
5
  "main": "library.js",
6
6
  "keywords": [