news-cms-module 0.1.1 → 1.0.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 +68 -0
- package/config/index.js +1 -3
- package/controllers/NewsController.js +299 -58
- package/controllers/StatController.js +22 -15
- package/index.js +18 -32
- package/middlewares/authAdminMiddleware.js +22 -0
- package/middlewares/multerMiddleware.js +45 -0
- package/middlewares/parseForm.js +15 -0
- package/models/News.js +1 -0
- package/models/index.js +3 -8
- package/package.json +4 -1
- package/public/img/Main.png +0 -0
- package/public/img/berita-terkait.png +0 -0
- package/public/img/berita.png +0 -0
- package/public/img/berita2.png +0 -0
- package/public/img/detail1.png +0 -0
- package/public/img/detail2.png +0 -0
- package/public/img/logo.png +0 -0
- package/public/js/script.js +92 -0
- package/public/style.css +1620 -0
- package/routes/index.js +25 -28
- package/services/NewsService.js +202 -34
- package/services/StatService.js +14 -5
- package/services/index.js +3 -3
- package/validations/mainValidation.js +25 -0
- package/validations/newsValidations.js +84 -0
- package/views/admin/create_news.ejs +316 -0
- package/views/admin/dashboard.ejs +104 -0
- package/views/admin/detailadmin.ejs +202 -0
- package/views/admin/list.ejs +331 -0
- package/views/admin/update_news.ejs +368 -0
- package/views/detail.ejs +235 -0
- package/views/home.ejs +220 -0
- package/views/list.ejs +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# News CMS Module
|
|
2
|
+
|
|
3
|
+
Modul Content Management System (CMS) Berita yang siap pakai untuk aplikasi Express.js. Modul ini menangani manajemen database (Sequelize), logika bisnis, hingga tampilan antarmuka (EJS) secara otomatis.
|
|
4
|
+
|
|
5
|
+
## Fitur
|
|
6
|
+
- **Auto-Sync Database**: Membuat tabel `news`, `content_news`, dan `visitor_logs` secara otomatis.
|
|
7
|
+
- **Tracking Pengunjung**: Sistem pelacakan unik pengunjung per berita dalam 24 jam.
|
|
8
|
+
- **Trending News**: Menampilkan 10 berita paling populer berdasarkan jumlah pengunjung 24 jam terakhir.
|
|
9
|
+
- **CMS Admin**: Dashboard manajemen berita dengan prefix yang dapat diatur.
|
|
10
|
+
- **Static Assets**: CSS internal yang sudah terintegrasi.
|
|
11
|
+
|
|
12
|
+
## Instalasi
|
|
13
|
+
|
|
14
|
+
Jalankan perintah berikut pada terminal proyek Anda:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install news-cms-module ejs express-session mysql2
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Panduan Penggunaan (app.js)
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
const express = require('express');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const newsModule = require('news-cms-module');
|
|
26
|
+
|
|
27
|
+
const app = express();
|
|
28
|
+
|
|
29
|
+
const dbConfig = {
|
|
30
|
+
database: 'dummy_news',// bisa disesuaikan lagi
|
|
31
|
+
username: 'root',
|
|
32
|
+
password: 'your_password',
|
|
33
|
+
host: 'localhost',};
|
|
34
|
+
|
|
35
|
+
const PORT = 3000;
|
|
36
|
+
|
|
37
|
+
async function startServer() {
|
|
38
|
+
// 1. Middleware Global & View Engine
|
|
39
|
+
app.use(express.json());
|
|
40
|
+
app.use(express.urlencoded({ extended: true }));
|
|
41
|
+
|
|
42
|
+
// Public asset untuk akses gambar berita
|
|
43
|
+
app.use(express.static('public'))
|
|
44
|
+
|
|
45
|
+
// WAJIB: Atur EJS agar tampilan modul dapat dirender
|
|
46
|
+
app.set('view engine', 'ejs');
|
|
47
|
+
|
|
48
|
+
// 2. Inisialisasi Modul News
|
|
49
|
+
// Package dijalankan secara ASYNC karena sinkronisasi database
|
|
50
|
+
const newsRouter = await newsModule(dbConfig, {
|
|
51
|
+
adminRoutePrefix: '/cms-admin',
|
|
52
|
+
sessionSecret: 'news_cms_secret_key'});
|
|
53
|
+
|
|
54
|
+
// 3. Pasang Router ke Prefix URL Host
|
|
55
|
+
app.use('/berita', newsRouter);
|
|
56
|
+
|
|
57
|
+
// 4. Jalankan Server
|
|
58
|
+
app.listen(PORT, () => {
|
|
59
|
+
console.log(`Server running on port ${PORT}`);
|
|
60
|
+
console.log(`User Interface: http://localhost:${PORT}/berita/list`);
|
|
61
|
+
console.log(`Admin Dashboard: http://localhost:${PORT}/berita/cms-admin/dashboard`);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
startServer();
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
pastikan untuk menyesuaikan bagian authAdminMiddleware pada module di bagian middlewares. Sesuaikan dengan preferensi tabel user masing-masing.
|
package/config/index.js
CHANGED
|
@@ -24,10 +24,8 @@ const defaultConfig = {
|
|
|
24
24
|
* @returns {object} Objek konfigurasi akhir yang lengkap.
|
|
25
25
|
*/
|
|
26
26
|
module.exports = (userConfig = {}) => {
|
|
27
|
-
// Menggunakan penyebaran (spread operator) untuk menggabungkan objek.
|
|
28
|
-
// userConfig akan menimpa (overwrite) defaultConfig jika ada properti yang sama.
|
|
29
27
|
return {
|
|
30
28
|
...defaultConfig,
|
|
31
29
|
...userConfig
|
|
32
30
|
};
|
|
33
|
-
};
|
|
31
|
+
};
|
|
@@ -1,50 +1,159 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const News = require('../models/News');
|
|
3
|
+
const { Op, fn, col, where } = require('sequelize');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
1
6
|
class NewsController {
|
|
2
7
|
constructor(newsService, statService, config) {
|
|
3
8
|
this.newsService = newsService;
|
|
4
9
|
this.statService = statService;
|
|
5
|
-
this.config = config;
|
|
10
|
+
this.config = config;
|
|
6
11
|
}
|
|
7
12
|
|
|
8
|
-
// --- Route Publik ---
|
|
9
13
|
async listPublic(req, res) {
|
|
14
|
+
const {
|
|
15
|
+
page = 1,
|
|
16
|
+
limit = 6,
|
|
17
|
+
title = '',
|
|
18
|
+
category = ''
|
|
19
|
+
} = req.query;
|
|
20
|
+
|
|
21
|
+
const currentPage = parseInt(page, 10);
|
|
22
|
+
const perPage = parseInt(limit, 10);
|
|
23
|
+
|
|
24
|
+
const offset = (currentPage - 1) * perPage;
|
|
25
|
+
|
|
10
26
|
try {
|
|
11
|
-
const { rows: posts, count:
|
|
27
|
+
const { rows: posts, count: totalItems } = await this.newsService.getAllPosts({
|
|
28
|
+
offset,
|
|
29
|
+
limit: perPage,
|
|
30
|
+
title,
|
|
31
|
+
category,
|
|
32
|
+
status: "PUBLISHED"
|
|
33
|
+
});
|
|
12
34
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
35
|
+
const totalPages = Math.ceil(totalItems / perPage);
|
|
36
|
+
|
|
37
|
+
const categories = await this.newsService.getUniqueCategories();
|
|
38
|
+
const trending = await this.newsService.getTrendingNews();
|
|
39
|
+
|
|
40
|
+
//ini mati klo dah ada view
|
|
41
|
+
// res.status(200).json({
|
|
42
|
+
// success: true,
|
|
43
|
+
// data: {
|
|
44
|
+
// posts,
|
|
45
|
+
// categories,
|
|
46
|
+
// trending,
|
|
47
|
+
// pagination: {
|
|
48
|
+
// totalItems,
|
|
49
|
+
// totalPages,
|
|
50
|
+
// currentPage,
|
|
51
|
+
// perPage,
|
|
52
|
+
// hasNextPage: currentPage < totalPages,
|
|
53
|
+
// hasPrevPage: currentPage > 1
|
|
54
|
+
// }
|
|
55
|
+
// }
|
|
56
|
+
// });
|
|
57
|
+
|
|
58
|
+
//render
|
|
59
|
+
res.render(path.join(__dirname, "../views/home.ejs"), {
|
|
60
|
+
posts,
|
|
61
|
+
categories,
|
|
62
|
+
trending,
|
|
63
|
+
query: { title, category },
|
|
64
|
+
pagination: {
|
|
65
|
+
totalItems,
|
|
66
|
+
totalPages,
|
|
67
|
+
currentPage,
|
|
68
|
+
perPage,
|
|
69
|
+
hasNextPage: currentPage < totalPages,
|
|
70
|
+
hasPrevPage: currentPage > 1,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
19
73
|
|
|
20
|
-
// res.render('list', { posts, total, baseUrl: req.baseUrl });
|
|
21
74
|
} catch (error) {
|
|
75
|
+
//console.error('Error loading news list:', error);
|
|
22
76
|
res.status(500).json({
|
|
23
|
-
|
|
24
|
-
error: 'Error loading news list.'
|
|
77
|
+
success: false,
|
|
78
|
+
error: 'Error loading news list.',
|
|
79
|
+
message: error.message
|
|
25
80
|
});
|
|
26
81
|
}
|
|
27
82
|
}
|
|
28
83
|
|
|
29
84
|
async getDetail(req, res) {
|
|
30
85
|
try {
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
86
|
+
const news = await this.newsService.getPostBySlug(req.params.slug);
|
|
87
|
+
if (!news) {
|
|
88
|
+
return res.status(404).json({
|
|
89
|
+
success: false,
|
|
90
|
+
error: 'news tidak ditemukan'
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const categories = await this.newsService.getUniqueCategories();
|
|
95
|
+
const recommendation = await this.newsService.getRecommendationNews(news.category);
|
|
96
|
+
const trending = await this.newsService.getTrendingNews();
|
|
97
|
+
|
|
98
|
+
res.render(path.join(__dirname, "../views/detail.ejs"), {
|
|
99
|
+
news,
|
|
100
|
+
categories,
|
|
101
|
+
recommendation,
|
|
102
|
+
trending,
|
|
103
|
+
query: {}
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Error loading news detail:', error);
|
|
107
|
+
res.status(500).json({
|
|
108
|
+
success: false,
|
|
109
|
+
error: 'gagal melihat news',
|
|
110
|
+
message: error.message
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async getDetailForAdmin(req, res) {
|
|
116
|
+
try {
|
|
117
|
+
const news = await this.newsService.getPostBySlugForAdmin(req.params.slug);
|
|
118
|
+
if (!news) {
|
|
119
|
+
return res.status(404).json({
|
|
120
|
+
success: false,
|
|
121
|
+
error: 'news tidak ditemukan'
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
res.render(path.join(__dirname, "../views/admin/detailadmin.ejs"), {
|
|
126
|
+
news
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('Error loading news detail for admin:', error);
|
|
130
|
+
res.status(500).json({
|
|
131
|
+
success: false,
|
|
132
|
+
error: 'gagal melihat news',
|
|
133
|
+
message: error.message
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async getEditForAdmin(req, res) {
|
|
139
|
+
try {
|
|
140
|
+
const posts = await this.newsService.getPostBySlugForAdmin(req.params.slug);
|
|
141
|
+
if (!posts) {
|
|
33
142
|
return res.status(404).json({
|
|
34
143
|
succses: false,
|
|
35
144
|
error: 'news tidak ditemukan'
|
|
36
145
|
});
|
|
37
146
|
}
|
|
38
147
|
|
|
39
|
-
res.status(200).json({
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
})
|
|
148
|
+
// res.status(200).json({
|
|
149
|
+
// success: true,
|
|
150
|
+
// data: post
|
|
151
|
+
// })
|
|
43
152
|
|
|
44
|
-
|
|
45
|
-
|
|
153
|
+
res.render(path.join(__dirname, '../views/admin/update_news.ejs'), {
|
|
154
|
+
data: posts
|
|
155
|
+
});
|
|
46
156
|
} catch (error) {
|
|
47
|
-
// res.status(500).send('Error loading post detail.');
|
|
48
157
|
res.status(500).json({
|
|
49
158
|
succses: false,
|
|
50
159
|
error: 'gagal melihat news'
|
|
@@ -52,29 +161,70 @@ class NewsController {
|
|
|
52
161
|
}
|
|
53
162
|
}
|
|
54
163
|
|
|
55
|
-
//
|
|
164
|
+
// Route Admin (CRUD)
|
|
56
165
|
async adminList(req, res) {
|
|
57
|
-
|
|
58
|
-
|
|
166
|
+
const {
|
|
167
|
+
page = 1,
|
|
168
|
+
limit = 10,
|
|
169
|
+
title = '',
|
|
170
|
+
category = '',
|
|
171
|
+
status = ''
|
|
172
|
+
} = req.query;
|
|
173
|
+
|
|
174
|
+
const currentPage = parseInt(page, 10);
|
|
175
|
+
const perPage = parseInt(limit, 10);
|
|
176
|
+
|
|
177
|
+
const offset = (currentPage - 1) * perPage;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const { rows: posts, count: totalItems } = await this.newsService.getAllPosts({
|
|
181
|
+
offset,
|
|
182
|
+
limit: perPage,
|
|
183
|
+
title,
|
|
184
|
+
category,
|
|
185
|
+
status: status.toUpperCase()
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const totalPages = Math.ceil(totalItems / perPage);
|
|
189
|
+
const categories = await this.newsService.getUniqueCategories();
|
|
59
190
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
191
|
+
// Render view instead of sending JSON
|
|
192
|
+
res.render(path.join(__dirname, "../views/admin/list.ejs"), {
|
|
193
|
+
posts,
|
|
194
|
+
categories,
|
|
195
|
+
query: { title, category, status },
|
|
196
|
+
pagination: {
|
|
197
|
+
totalItems,
|
|
198
|
+
totalPages,
|
|
199
|
+
currentPage,
|
|
200
|
+
perPage,
|
|
201
|
+
hasNextPage: currentPage < totalPages,
|
|
202
|
+
hasPrevPage: currentPage > 1
|
|
203
|
+
}
|
|
204
|
+
});
|
|
64
205
|
|
|
65
|
-
|
|
66
|
-
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('Error loading admin news list:', error);
|
|
208
|
+
res.status(500).json({
|
|
209
|
+
success: false,
|
|
210
|
+
error: 'Error loading news list.',
|
|
211
|
+
message: error.message
|
|
212
|
+
});
|
|
213
|
+
}
|
|
67
214
|
}
|
|
68
215
|
|
|
69
216
|
async createPost(req, res) {
|
|
70
217
|
try {
|
|
71
|
-
const { title, summary, authorId, status, contentBlocks } = req.body;
|
|
72
218
|
|
|
73
|
-
const
|
|
219
|
+
const { title, authorName, status, contentBlocks } = req.body;
|
|
220
|
+
const category = req.body.category.toLowerCase();
|
|
221
|
+
const files = req.files;
|
|
222
|
+
const slug = title.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
|
|
74
223
|
|
|
75
224
|
const newNews = await this.newsService.createPost(
|
|
76
|
-
{ title, slug,
|
|
77
|
-
contentBlocks
|
|
225
|
+
{ title, slug, category, authorName, status: status || 'DRAFT' },
|
|
226
|
+
contentBlocks,
|
|
227
|
+
files
|
|
78
228
|
);
|
|
79
229
|
|
|
80
230
|
res.status(201).json({
|
|
@@ -86,52 +236,143 @@ class NewsController {
|
|
|
86
236
|
// res.redirect(this.config.adminRoutePrefix + '/');
|
|
87
237
|
} catch (error) {
|
|
88
238
|
console.error(error);
|
|
89
|
-
|
|
239
|
+
|
|
240
|
+
if (req.files) {
|
|
241
|
+
const allFiles = [
|
|
242
|
+
...(req.files['thumbnailImage'] || []),
|
|
243
|
+
...(req.files['contentImages'] || [])
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
allFiles.forEach(file => {
|
|
247
|
+
fs.unlink(file.path, (err) => {
|
|
248
|
+
if (err) console.error(`Gagal menghapus file: ${file.path}`, err);
|
|
249
|
+
else console.log(`Berhasil menghapus sampah file: ${file.path}`);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
90
254
|
res.status(500).json({
|
|
91
255
|
succses: false,
|
|
92
256
|
error: 'gagal membuat news'
|
|
93
257
|
});
|
|
94
258
|
}
|
|
95
259
|
}
|
|
96
|
-
|
|
260
|
+
|
|
97
261
|
async updatePost(req, res) {
|
|
262
|
+
const { id } = req.params;
|
|
98
263
|
try {
|
|
99
|
-
const {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
264
|
+
const { title, authorName, status, contentBlocks } = req.body;
|
|
265
|
+
const category = req.body.category.toLowerCase();
|
|
266
|
+
const files = req.files;
|
|
267
|
+
const slug = title ? title.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '') : undefined;
|
|
268
|
+
|
|
269
|
+
const result = await this.newsService.updatePost(
|
|
270
|
+
id,
|
|
271
|
+
{ title, slug, category, authorName, status },
|
|
272
|
+
contentBlocks,
|
|
273
|
+
files
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (result.filesToDelete && result.filesToDelete.length > 0) {
|
|
277
|
+
result.filesToDelete.forEach(filePath => {
|
|
278
|
+
fs.unlink(filePath, (err) => {
|
|
279
|
+
if (err) console.error(`Gagal hapus file lama: ${filePath}`, err);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
104
282
|
}
|
|
105
283
|
|
|
106
|
-
|
|
107
|
-
|
|
284
|
+
res.status(200).json({
|
|
285
|
+
success: true,
|
|
286
|
+
message: 'Berhasil update berita',
|
|
287
|
+
data: result.newsItem
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error(error);
|
|
292
|
+
|
|
293
|
+
if (req.files) {
|
|
294
|
+
const uploadedFiles = [
|
|
295
|
+
...(req.files['thumbnailImage'] || []),
|
|
296
|
+
...(req.files['contentImages'] || [])
|
|
297
|
+
];
|
|
298
|
+
uploadedFiles.forEach(file => {
|
|
299
|
+
fs.unlink(file.path, (err) => {
|
|
300
|
+
if (err) console.error(`Cleanup error file gagal: ${file.path}`, err);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
res.status(500).json({
|
|
306
|
+
success: false,
|
|
307
|
+
error: error.message || 'Gagal update news'
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async updateStatusNews(req, res) {
|
|
313
|
+
const { id } = req.params;
|
|
314
|
+
try {
|
|
315
|
+
const { status } = req.body;
|
|
108
316
|
|
|
109
|
-
const
|
|
317
|
+
const result = await this.newsService.updateStatusNews(
|
|
110
318
|
id,
|
|
111
|
-
|
|
112
|
-
title,
|
|
113
|
-
slug,
|
|
114
|
-
summary,
|
|
115
|
-
authorId,
|
|
116
|
-
status: status || 'DRAFT',
|
|
117
|
-
publishedAt: isPublished ? new Date() : null
|
|
118
|
-
},
|
|
119
|
-
contentBlocks
|
|
319
|
+
status
|
|
120
320
|
);
|
|
121
321
|
|
|
122
|
-
|
|
123
|
-
|
|
322
|
+
res.status(200).json({
|
|
323
|
+
success: true,
|
|
324
|
+
message: 'Berhasil update status berita',
|
|
325
|
+
data: result
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.error(error);
|
|
330
|
+
|
|
331
|
+
res.status(500).json({
|
|
332
|
+
success: false,
|
|
333
|
+
error: error.message || 'Gagal update news'
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
//ini blum selesai
|
|
339
|
+
async deletePost(req, res) {
|
|
340
|
+
try {
|
|
341
|
+
const { id } = req.params;
|
|
342
|
+
|
|
343
|
+
const result = await this.newsService.deletePost(id);
|
|
344
|
+
|
|
345
|
+
if (result === 0) { // Sequelize destroy mengembalikan 0 jika tidak ada baris yang terpengaruh
|
|
346
|
+
return res.status(404).json({ success: false, message: 'News post not found for deletion.' });
|
|
124
347
|
}
|
|
125
348
|
|
|
126
349
|
res.status(200).json({
|
|
127
350
|
success: true,
|
|
128
|
-
message:
|
|
129
|
-
data: updatedPost
|
|
351
|
+
message: `Post with ID ${id} deleted successfully.`
|
|
130
352
|
});
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error(error);
|
|
355
|
+
res.status(500).json({ success: false, message: 'Failed to delete post.', error: error.message });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async dashboardAdmin(req, res) {
|
|
360
|
+
try {
|
|
361
|
+
const currentYear = new Date().getFullYear();
|
|
131
362
|
|
|
363
|
+
const data = await this.newsService.dashboardAdmin(currentYear);
|
|
364
|
+
|
|
365
|
+
// res.status(200).json({
|
|
366
|
+
// success: true,
|
|
367
|
+
// data
|
|
368
|
+
// });
|
|
369
|
+
|
|
370
|
+
res.render(path.join(__dirname, '../views/admin/dashboard.ejs'), {
|
|
371
|
+
data: data
|
|
372
|
+
});
|
|
132
373
|
} catch (error) {
|
|
133
374
|
console.error(error);
|
|
134
|
-
res.status(500).json({ success: false, message: 'Failed to
|
|
375
|
+
res.status(500).json({ success: false, message: 'Failed to load data.', error: error.message });
|
|
135
376
|
}
|
|
136
377
|
}
|
|
137
378
|
|
|
@@ -1,25 +1,32 @@
|
|
|
1
|
-
// controllers/StatController.js
|
|
2
|
-
|
|
3
1
|
class StatController {
|
|
4
|
-
constructor(statService) {
|
|
2
|
+
constructor(newsService, statService) {
|
|
3
|
+
this.newsService = newsService;
|
|
5
4
|
this.statService = statService;
|
|
6
5
|
}
|
|
7
6
|
|
|
8
|
-
// Middleware untuk pelacakan kunjungan blum selesai
|
|
9
7
|
async trackVisitMiddleware(req, res, next) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
try {
|
|
9
|
+
const slug = req.params.slug;
|
|
10
|
+
const post = await this.newsService.getPostBySlug(slug);
|
|
11
|
+
|
|
12
|
+
if (post) {
|
|
13
|
+
if (!req.session.viewedPosts) {
|
|
14
|
+
req.session.viewedPosts = [];
|
|
15
|
+
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
if (!req.session.viewedPosts.includes(post.id)) {
|
|
18
|
+
const sessionId = req.sessionID || req.ip;
|
|
19
|
+
await this.statService.trackVisit(post.id, sessionId);
|
|
20
|
+
|
|
21
|
+
req.session.viewedPosts.push(post.id);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
req.postData = post;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("News tracking failed but request continued:", error);
|
|
21
29
|
}
|
|
22
|
-
|
|
23
30
|
next();
|
|
24
31
|
}
|
|
25
32
|
|
package/index.js
CHANGED
|
@@ -1,67 +1,53 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fungsi utama package yang di-export.
|
|
3
|
-
* @param {object} dbConfig - Konfigurasi koneksi database dari aplikasi pengguna.
|
|
4
|
-
* @param {object} options - Opsi tambahan (misalnya, nama folder views pengguna).
|
|
5
|
-
* @returns {express.Router} Router Express yang sudah terkonfigurasi.
|
|
6
|
-
*/
|
|
7
|
-
// index.js (Revisi Akhir)
|
|
8
|
-
|
|
9
1
|
const express = require('express');
|
|
10
|
-
|
|
2
|
+
const session = require('express-session');
|
|
3
|
+
const path = require('path');
|
|
11
4
|
const initModels = require('./models');
|
|
12
5
|
const initServices = require('./services');
|
|
13
6
|
const setupRoutes = require('./routes');
|
|
14
|
-
const mergeConfig = require('./config');
|
|
7
|
+
const mergeConfig = require('./config');
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
* Fungsi utama package yang di-export.
|
|
18
|
-
* @param {object} dbConfig - Konfigurasi koneksi database dari aplikasi pengguna.
|
|
19
|
-
* @param {object} userOptions - Opsi tambahan (misalnya, autoMigrate) dari pengguna.
|
|
20
|
-
* @returns {express.Router} Router Express yang sudah terkonfigurasi.
|
|
21
|
-
*/
|
|
22
|
-
module.exports = async (dbConfig, userOptions = {}) => { // <<< KUNCI 1: Jadikan ASYNC
|
|
9
|
+
module.exports = async (dbConfig, userOptions = {}) => {
|
|
23
10
|
|
|
24
|
-
// 1. Validasi dan Konfigurasi
|
|
25
11
|
if (!dbConfig || !dbConfig.database) {
|
|
26
12
|
throw new Error('News module requires database configuration (dbConfig).');
|
|
27
13
|
}
|
|
28
14
|
|
|
29
|
-
// Gabungkan konfigurasi database dan opsi pengguna (walaupun di sini kita hanya menggunakan dbConfig)
|
|
30
15
|
const finalConfig = mergeConfig({
|
|
31
16
|
...userOptions,
|
|
32
17
|
db: dbConfig
|
|
33
18
|
});
|
|
34
19
|
|
|
35
|
-
// 2. Inisialisasi Koneksi Database dan Model
|
|
36
20
|
const db = initModels(finalConfig.db);
|
|
37
21
|
|
|
38
|
-
// 3. Sinkronisasi Tabel (Pembuatan Tabel Otomatis)
|
|
39
|
-
// Secara default, kita asumsikan sinkronisasi ON. Pengguna bisa menonaktifkannya dengan { autoMigrate: false }
|
|
40
22
|
if (finalConfig.autoMigrate !== false) {
|
|
41
23
|
try {
|
|
42
24
|
console.log('News module: Synchronizing database tables...');
|
|
43
|
-
// Memanggil syncTables yang didefinisikan di models/index.js
|
|
44
25
|
await db.syncTables({ alter: true });
|
|
45
26
|
console.log('News module: Synchronization complete.');
|
|
46
27
|
} catch (error) {
|
|
47
28
|
console.error('ERROR: News module failed to synchronize database tables.', error);
|
|
48
|
-
// Anda bisa memilih untuk melempar error agar server tidak berjalan tanpa tabel
|
|
49
29
|
throw error;
|
|
50
30
|
}
|
|
51
31
|
}
|
|
52
|
-
|
|
53
|
-
// 4. Inisialisasi Services (Injeksi Dependencies)
|
|
54
|
-
const services = initServices(db);
|
|
55
32
|
|
|
56
|
-
|
|
33
|
+
const services = initServices(db);
|
|
57
34
|
const router = express.Router();
|
|
58
|
-
|
|
59
|
-
//
|
|
35
|
+
|
|
36
|
+
// 2. DAFTARKAN FOLDER PUBLIC MILIK MODUL
|
|
37
|
+
router.use(express.static(path.join(__dirname, 'public')));
|
|
38
|
+
|
|
60
39
|
router.use(express.json());
|
|
61
40
|
router.use(express.urlencoded({ extended: true }));
|
|
41
|
+
router.use(session({
|
|
42
|
+
secret: userOptions.sessionSecret || 'news_module_default_secret',
|
|
43
|
+
resave: false,
|
|
44
|
+
saveUninitialized: true,
|
|
45
|
+
cookie: {
|
|
46
|
+
maxAge: 24 * 60 * 60 * 1000,
|
|
47
|
+
secure: false
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
62
50
|
|
|
63
|
-
// 6. Setup Router dan Controllers
|
|
64
|
-
// Route diatur, Controllers menggunakan Services dan finalConfig
|
|
65
51
|
setupRoutes(router, services, finalConfig);
|
|
66
52
|
|
|
67
53
|
return router;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const isAdmin = (req, res, next) => {
|
|
2
|
+
//buat bagian admin autentikasi ini sesuai dengan tabel dan kebutuhan anda
|
|
3
|
+
|
|
4
|
+
// 1. Cek apakah user sudah login (session ada)
|
|
5
|
+
// if (!req.session || !req.session.user) {
|
|
6
|
+
// return res.redirect('/login'); // Redirect ke halaman login jika belum auth
|
|
7
|
+
// }
|
|
8
|
+
|
|
9
|
+
// 2. Cek apakah role user adalah ADMIN
|
|
10
|
+
// Diasumsikan di tabel User Anda memiliki kolom 'role'
|
|
11
|
+
// if (req.session.user.role !== 'ADMIN') {
|
|
12
|
+
// // Jika login tapi bukan admin, arahkan ke home atau beri error 403
|
|
13
|
+
// return res.status(403).render('error/403', {
|
|
14
|
+
// message: 'Akses Ditolak: Anda bukan Administrator'
|
|
15
|
+
// });
|
|
16
|
+
// }
|
|
17
|
+
|
|
18
|
+
// 3. Jika semua terpenuhi, lanjut ke controller
|
|
19
|
+
next();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
module.exports = { isAdmin };
|