news-cms-module 0.1.2 → 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 +280 -60
- 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;
|
|
59
173
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
data: posts
|
|
63
|
-
})
|
|
174
|
+
const currentPage = parseInt(page, 10);
|
|
175
|
+
const perPage = parseInt(limit, 10);
|
|
64
176
|
|
|
65
|
-
|
|
66
|
-
|
|
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();
|
|
190
|
+
|
|
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
|
+
});
|
|
205
|
+
|
|
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,56 +236,106 @@ 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
|
-
return res.status(400).json({ success: false, message: 'Missing required fields or ID.' });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const slug = title.toLowerCase().trim().replace(/ /g, '-').replace(/[^\w-]+/g, '');
|
|
107
|
-
const isPublished = status === 'PUBLISHED';
|
|
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;
|
|
108
268
|
|
|
109
|
-
const
|
|
269
|
+
const result = await this.newsService.updatePost(
|
|
110
270
|
id,
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
summary,
|
|
115
|
-
authorId,
|
|
116
|
-
status: status || 'DRAFT',
|
|
117
|
-
publishedAt: isPublished ? new Date() : null
|
|
118
|
-
},
|
|
119
|
-
contentBlocks
|
|
271
|
+
{ title, slug, category, authorName, status },
|
|
272
|
+
contentBlocks,
|
|
273
|
+
files
|
|
120
274
|
);
|
|
121
275
|
|
|
122
|
-
if (
|
|
123
|
-
|
|
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
|
+
});
|
|
124
282
|
}
|
|
125
283
|
|
|
126
284
|
res.status(200).json({
|
|
127
285
|
success: true,
|
|
128
|
-
message: '
|
|
129
|
-
data:
|
|
286
|
+
message: 'Berhasil update berita',
|
|
287
|
+
data: result.newsItem
|
|
130
288
|
});
|
|
131
289
|
|
|
132
290
|
} catch (error) {
|
|
133
291
|
console.error(error);
|
|
134
|
-
|
|
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
|
+
});
|
|
135
309
|
}
|
|
136
310
|
}
|
|
137
311
|
|
|
312
|
+
async updateStatusNews(req, res) {
|
|
313
|
+
const { id } = req.params;
|
|
314
|
+
try {
|
|
315
|
+
const { status } = req.body;
|
|
316
|
+
|
|
317
|
+
const result = await this.newsService.updateStatusNews(
|
|
318
|
+
id,
|
|
319
|
+
status
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
res.status(200).json({
|
|
323
|
+
success: true,
|
|
324
|
+
message: 'Berhasil update status berita',
|
|
325
|
+
data: result
|
|
326
|
+
});
|
|
138
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
|
|
139
339
|
async deletePost(req, res) {
|
|
140
340
|
try {
|
|
141
341
|
const { id } = req.params;
|
|
@@ -156,6 +356,26 @@ class NewsController {
|
|
|
156
356
|
}
|
|
157
357
|
}
|
|
158
358
|
|
|
359
|
+
async dashboardAdmin(req, res) {
|
|
360
|
+
try {
|
|
361
|
+
const currentYear = new Date().getFullYear();
|
|
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
|
+
});
|
|
373
|
+
} catch (error) {
|
|
374
|
+
console.error(error);
|
|
375
|
+
res.status(500).json({ success: false, message: 'Failed to load data.', error: error.message });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
159
379
|
}
|
|
160
380
|
|
|
161
381
|
module.exports = NewsController;
|
|
@@ -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 };
|