nodebb-plugin-niki-loyalty 1.0.16 → 1.0.18

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 +266 -145
  2. package/package.json +1 -1
package/library.js CHANGED
@@ -3,172 +3,293 @@
3
3
  const db = require.main.require('./src/database');
4
4
  const user = require.main.require('./src/user');
5
5
  const routeHelpers = require.main.require('./src/controllers/helpers');
6
+ const nconf = require.main.require('nconf');
6
7
 
7
8
  const Plugin = {};
8
9
 
10
+ // =========================
11
+ // AYARLAR
12
+ // =========================
9
13
  const SETTINGS = {
10
- pointsPerHeartbeat: 5,
11
- dailyCap: 250,
12
- coffeeCost: 250
14
+ pointsPerHeartbeat: 500,
15
+ dailyCap: 2500000,
16
+ coffeeCost: 250,
13
17
  };
14
18
 
15
- // --- LOG FONKSİYONLARI ---
19
+ // TEST: sınırsız kullanım (puan kontrolünü kapatmak için true)
20
+ const TEST_MODE_UNLIMITED = false;
21
+
22
+ // =========================
23
+ // JSON SAFE HELPERS
24
+ // =========================
25
+ function safeParseMaybeJson(x) {
26
+ if (x == null) return null;
27
+
28
+ // bazı DB’lerde object dönebilir
29
+ if (typeof x === 'object') return x;
30
+
31
+ if (typeof x === 'string') {
32
+ try {
33
+ return JSON.parse(x);
34
+ } catch (e) {
35
+ // "[object Object]" gibi bozuk kayıtları atla
36
+ return null;
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function safeStringify(obj) {
43
+ try {
44
+ return JSON.stringify(obj);
45
+ } catch (e) {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function makeProfileUrl(userslug) {
51
+ const rp = nconf.get('relative_path') || '';
52
+ if (!userslug) return '';
53
+ return `${rp}/user/${userslug}`;
54
+ }
55
+
56
+ // =========================
57
+ // LOG FONKSİYONLARI
58
+ // =========================
16
59
  async function addUserLog(uid, type, amount, desc) {
17
- const logEntry = {
18
- ts: Date.now(),
19
- type: type, // 'earn' veya 'spend'
20
- amt: amount,
21
- txt: desc
22
- };
23
- // Öğrenci geçmişine ekle
24
- await db.listAppend(`niki:activity:${uid}`, logEntry);
25
- await db.listTrim(`niki:activity:${uid}`, -50, -1);
60
+ const logEntry = {
61
+ ts: Date.now(),
62
+ type, // 'earn' | 'spend'
63
+ amt: amount,
64
+ txt: desc,
65
+ };
66
+
67
+ const payload = safeStringify(logEntry);
68
+ if (!payload) return;
69
+
70
+ await db.listAppend(`niki:activity:${uid}`, payload);
71
+ await db.listTrim(`niki:activity:${uid}`, -50, -1);
26
72
  }
27
73
 
28
74
  async function addKasaLog(staffUid, customerName, customerUid) {
29
- const logEntry = {
30
- ts: Date.now(),
31
- staff: staffUid,
32
- cust: customerName,
33
- cuid: customerUid,
34
- amt: SETTINGS.coffeeCost
35
- };
36
- // Kasa defterine ekle
37
- await db.listAppend('niki:kasa:history', logEntry);
38
- await db.listTrim('niki:kasa:history', -100, -1);
75
+ const logEntry = {
76
+ ts: Date.now(),
77
+ staff: staffUid,
78
+ cust: customerName, // bazen eski kayıtlarda boş olabilir, endpoint tamamlayacak
79
+ cuid: customerUid,
80
+ amt: SETTINGS.coffeeCost,
81
+ };
82
+
83
+ const payload = safeStringify(logEntry);
84
+ if (!payload) return;
85
+
86
+ await db.listAppend('niki:kasa:history', payload);
87
+ await db.listTrim('niki:kasa:history', -100, -1);
39
88
  }
40
89
 
90
+ // =========================
91
+ // INIT
92
+ // =========================
41
93
  Plugin.init = async function (params) {
42
- const router = params.router;
43
- const middleware = params.middleware;
44
-
45
- // 1. HEARTBEAT (Puan Kazanma)
46
- router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
47
- const uid = req.uid;
48
- const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
49
- const dailyKey = `niki:daily:${uid}:${today}`;
50
-
51
- const currentDailyScore = await db.getObjectField(dailyKey, 'score') || 0;
52
-
53
- if (parseInt(currentDailyScore) >= SETTINGS.dailyCap) {
54
- return res.json({ earned: false, reason: 'daily_cap' });
94
+ const router = params.router;
95
+ const middleware = params.middleware;
96
+
97
+ // 1) HEARTBEAT (puan kazanma)
98
+ router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
99
+ try {
100
+ const uid = req.uid;
101
+ const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
102
+ const dailyKey = `niki:daily:${uid}:${today}`;
103
+
104
+ const currentDailyScore = parseInt((await db.getObjectField(dailyKey, 'score')) || 0, 10);
105
+
106
+ if (currentDailyScore >= SETTINGS.dailyCap) {
107
+ return res.json({ earned: false, reason: 'daily_cap' });
108
+ }
109
+
110
+ await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
111
+ await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
112
+
113
+ const newBalance = await user.getUserField(uid, 'niki_points');
114
+ return res.json({ earned: true, points: SETTINGS.pointsPerHeartbeat, total: newBalance });
115
+ } catch (err) {
116
+ return res.status(500).json({ earned: false, reason: 'server_error' });
117
+ }
118
+ });
119
+
120
+ // 2) WALLET DATA (cüzdan + geçmiş)
121
+ router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
122
+ try {
123
+ const uid = req.uid;
124
+ const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
125
+
126
+ const [userData, dailyData, historyRaw] = await Promise.all([
127
+ user.getUserFields(uid, ['niki_points']),
128
+ db.getObject(`niki:daily:${uid}:${today}`),
129
+ db.getListRange(`niki:activity:${uid}`, 0, -1),
130
+ ]);
131
+
132
+ const currentPoints = parseInt(userData?.niki_points || 0, 10);
133
+ const dailyScore = parseInt(dailyData?.score || 0, 10);
134
+
135
+ let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
136
+ if (dailyPercent > 100) dailyPercent = 100;
137
+
138
+ // ✅ history parse güvenli
139
+ const history = (historyRaw || [])
140
+ .map(safeParseMaybeJson)
141
+ .filter(Boolean)
142
+ .reverse();
143
+
144
+ return res.json({
145
+ points: currentPoints,
146
+ dailyScore,
147
+ dailyCap: SETTINGS.dailyCap,
148
+ dailyPercent,
149
+ history,
150
+ });
151
+ } catch (err) {
152
+ return res.status(500).json({
153
+ points: 0,
154
+ dailyScore: 0,
155
+ dailyCap: SETTINGS.dailyCap,
156
+ dailyPercent: 0,
157
+ history: [],
158
+ });
159
+ }
160
+ });
161
+
162
+ // 3) KASA HISTORY (admin/mod)
163
+ router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
164
+ try {
165
+ const isAdmin = await user.isAdministrator(req.uid);
166
+ const isMod = await user.isGlobalModerator(req.uid);
167
+ if (!isAdmin && !isMod) return res.status(403).json([]);
168
+
169
+ const raw = await db.getListRange('niki:kasa:history', 0, -1);
170
+
171
+ const rows = (raw || [])
172
+ .map(safeParseMaybeJson)
173
+ .filter(Boolean)
174
+ .reverse();
175
+
176
+ const enriched = await Promise.all(rows.map(async (item) => {
177
+ if (!item || !item.cuid) return item;
178
+
179
+ try {
180
+ // ✅ userslug + username + picture al
181
+ const uData = await user.getUserFields(item.cuid, ['username', 'userslug', 'picture']);
182
+ const userslug = uData?.userslug;
183
+
184
+ return {
185
+ ...item,
186
+ cust: item.cust || uData?.username || 'Bilinmeyen',
187
+ picture: item.picture || uData?.picture || '',
188
+ userslug: userslug || item.userslug || '',
189
+ profileUrl: makeProfileUrl(userslug || item.userslug),
190
+ };
191
+ } catch (e) {
192
+ // tek item patlasa bile endpoint düşmesin
193
+ return item;
55
194
  }
195
+ }));
196
+
197
+ return res.json(enriched);
198
+ } catch (err) {
199
+ return res.status(500).json([]);
200
+ }
201
+ });
202
+
203
+ // 4) QR OLUŞTUR
204
+ router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
205
+ try {
206
+ const uid = req.uid;
207
+ const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
208
+
209
+ if (!TEST_MODE_UNLIMITED && points < SETTINGS.coffeeCost) {
210
+ return res.json({ success: false, message: 'Yetersiz Puan' });
211
+ }
212
+
213
+ const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
214
+
215
+ await db.set(`niki:qr:${token}`, uid);
216
+ await db.expire(`niki:qr:${token}`, 120);
56
217
 
57
- await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
58
- await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
59
-
60
- const newBalance = await user.getUserField(uid, 'niki_points');
61
- return res.json({ earned: true, points: SETTINGS.pointsPerHeartbeat, total: newBalance });
62
- });
63
-
64
- // 2. WALLET DATA (Cüzdan + Geçmiş)
65
- router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
66
- const uid = req.uid;
67
- const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
68
-
69
- const [userData, dailyData, history] = await Promise.all([
70
- user.getUserFields(uid, ['niki_points']),
71
- db.getObject(`niki:daily:${uid}:${today}`),
72
- db.getListRange(`niki:activity:${uid}`, 0, -1)
73
- ]);
74
-
75
- const currentPoints = parseInt(userData.niki_points) || 0;
76
- const dailyScore = parseInt(dailyData ? dailyData.score : 0) || 0;
77
- let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
78
- if (dailyPercent > 100) dailyPercent = 100;
79
-
80
- res.json({
81
- points: currentPoints,
82
- dailyScore: dailyScore,
83
- dailyCap: SETTINGS.dailyCap,
84
- dailyPercent: dailyPercent,
85
- history: (history || []).reverse()
86
- });
87
- });
88
-
89
- // --- İŞTE EKSİK OLAN KISIM BURASIYDI ---
90
- // 3. KASA GEÇMİŞİ (Personel Ekranı İçin)
91
- router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
92
- const isAdmin = await user.isAdministrator(req.uid);
93
- const isMod = await user.isGlobalModerator(req.uid);
94
-
95
- if (!isAdmin && !isMod) return res.status(403).json([]);
96
-
97
- // Veritabanından listeyi çek
98
- const history = await db.getListRange('niki:kasa:history', 0, -1);
99
-
100
- // Kullanıcı resimlerini de ekleyerek zenginleştir
101
- const enrichedHistory = await Promise.all((history || []).reverse().map(async (item) => {
102
- const uData = await user.getUserFields(item.cuid, ['picture']);
103
- item.picture = uData.picture;
104
- return item;
105
- }));
106
-
107
- res.json(enrichedHistory);
108
- });
109
- // ---------------------------------------
110
-
111
- // 4. QR OLUŞTUR
112
- router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
113
- const uid = req.uid;
114
- const points = parseInt(await user.getUserField(uid, 'niki_points')) || 0;
115
-
116
- if (points < SETTINGS.coffeeCost) return res.json({ success: false, message: 'Yetersiz Puan' });
117
-
118
- const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
119
- await db.set(`niki:qr:${token}`, uid);
120
- await db.expire(`niki:qr:${token}`, 120);
121
-
122
- return res.json({ success: true, token: token });
123
- });
124
-
125
- // 5. QR OKUT (Ödeme Alma)
126
- router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
127
- const { token } = req.body;
128
-
129
- const isAdmin = await user.isAdministrator(req.uid);
130
- const isMod = await user.isGlobalModerator(req.uid);
131
- if (!isAdmin && !isMod) return res.status(403).json({ success: false, message: 'Yetkisiz' });
132
-
133
- const customerUid = await db.get(`niki:qr:${token}`);
134
- if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
135
-
136
- const pts = parseInt(await user.getUserField(customerUid, 'niki_points')) || 0;
137
- if (pts < SETTINGS.coffeeCost) return res.json({ success: false, message: 'Yetersiz Bakiye' });
138
-
139
- // İŞLEM
218
+ return res.json({ success: true, token });
219
+ } catch (err) {
220
+ return res.status(500).json({ success: false, message: 'Sunucu hatası' });
221
+ }
222
+ });
223
+
224
+ // 5) QR OKUT (admin/mod)
225
+ router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
226
+ try {
227
+ const token = (req.body && req.body.token) ? String(req.body.token) : '';
228
+
229
+ const isAdmin = await user.isAdministrator(req.uid);
230
+ const isMod = await user.isGlobalModerator(req.uid);
231
+ if (!isAdmin && !isMod) return res.status(403).json({ success: false, message: 'Yetkisiz' });
232
+
233
+ const customerUid = await db.get(`niki:qr:${token}`);
234
+ if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
235
+
236
+ const pts = parseInt((await user.getUserField(customerUid, 'niki_points')) || 0, 10);
237
+ if (!TEST_MODE_UNLIMITED && pts < SETTINGS.coffeeCost) {
238
+ return res.json({ success: false, message: 'Yetersiz Bakiye' });
239
+ }
240
+
241
+ // ✅ puan düşür
242
+ if (!TEST_MODE_UNLIMITED) {
140
243
  await user.decrementUserFieldBy(customerUid, 'niki_points', SETTINGS.coffeeCost);
141
- await db.delete(`niki:qr:${token}`);
142
-
143
- // LOGLARI KAYDET
144
- const customerData = await user.getUserFields(customerUid, ['username', 'picture']);
145
-
146
- // Cüzdan için: "Kahve Keyfi"
147
- await addUserLog(customerUid, 'spend', SETTINGS.coffeeCost, 'Kahve Keyfi ☕');
148
-
149
- // Kasa için: Kayıt
150
- await addKasaLog(req.uid, customerData.username, customerUid);
151
-
152
- return res.json({ success: true, customer: customerData, message: 'Onaylandı!' });
153
- });
154
-
155
- // SAYFA ROTASI
156
- routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
157
- const isAdmin = await user.isAdministrator(req.uid);
158
- const isMod = await user.isGlobalModerator(req.uid);
159
- if (!isAdmin && !isMod) return res.render('403', {});
160
- res.render('niki-kasa', { title: 'Niki Kasa' });
161
- });
244
+ }
245
+
246
+ // token tek kullanımlık
247
+ await db.delete(`niki:qr:${token}`);
248
+
249
+ // müşteri bilgisi
250
+ const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
251
+
252
+ // user log
253
+ await addUserLog(customerUid, 'spend', SETTINGS.coffeeCost, 'Kahve Keyfi ☕');
254
+
255
+ // kasa log
256
+ await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid);
257
+
258
+ return res.json({
259
+ success: true,
260
+ customer: customerData,
261
+ message: 'Onaylandı!',
262
+ });
263
+ } catch (err) {
264
+ return res.status(500).json({ success: false, message: 'Sunucu hatası' });
265
+ }
266
+ });
267
+
268
+ // 6) SAYFA ROTASI (kasa sayfası)
269
+ routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
270
+ const isAdmin = await user.isAdministrator(req.uid);
271
+ const isMod = await user.isGlobalModerator(req.uid);
272
+ if (!isAdmin && !isMod) return res.render('403', {});
273
+ return res.render('niki-kasa', { title: 'Niki Kasa' });
274
+ });
162
275
  };
163
276
 
277
+ // client.js inject
164
278
  Plugin.addScripts = async function (scripts) {
165
- scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
166
- return scripts;
279
+ scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
280
+ return scripts;
167
281
  };
168
282
 
283
+ // navigation
169
284
  Plugin.addNavigation = async function (nav) {
170
- nav.push({ route: "/niki-wallet", title: "Niki Cüzdan", enabled: true, iconClass: "fa-coffee", text: "Niki Cüzdan" });
171
- return nav;
285
+ nav.push({
286
+ route: '/niki-wallet',
287
+ title: 'Niki Cüzdan',
288
+ enabled: true,
289
+ iconClass: 'fa-coffee',
290
+ text: 'Niki Cüzdan',
291
+ });
292
+ return nav;
172
293
  };
173
294
 
174
- module.exports = Plugin;
295
+ module.exports = Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-niki-loyalty",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Niki The Cat Coffee Loyalty System - Earn points while studying on IEU Forum.",
5
5
  "main": "library.js",
6
6
  "nbbpm": {