news-cms-module 0.1.2 → 1.1.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 +92 -0
- package/config/index.js +3 -3
- package/controllers/NewsController.js +266 -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 +7 -9
- 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 +41 -28
- package/services/NewsService.js +199 -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 +312 -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 +360 -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,92 @@
|
|
|
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
|
+
require('dotenv').config();
|
|
25
|
+
const newsModule = require('news-cms-module');
|
|
26
|
+
|
|
27
|
+
const app = express();
|
|
28
|
+
|
|
29
|
+
const dbConfig = {
|
|
30
|
+
host: process.env.DB_HOST,
|
|
31
|
+
port: process.env.DB_PORT || 3036, // Sesuaikan dengan port db anda
|
|
32
|
+
username: process.env.DB_USER_NAME,
|
|
33
|
+
password: process.env.DB_PASSWORD,
|
|
34
|
+
database: process.env.DB_NAME,
|
|
35
|
+
dialect: 'mysql',
|
|
36
|
+
dialectOptions: {
|
|
37
|
+
ssl: {
|
|
38
|
+
require: true,
|
|
39
|
+
rejectUnauthorized: false // Set ke false jika tidak pakai file sertifikat .pem
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
// Opsional: tambahkan ini untuk mencegah timeout di koneksi lambat
|
|
43
|
+
pool: {
|
|
44
|
+
max: 5,
|
|
45
|
+
min: 0,
|
|
46
|
+
acquire: 30000,
|
|
47
|
+
idle: 10000
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Konfigurasi URL Dinamis
|
|
52
|
+
const PORT = process.env.PORT || 3000;
|
|
53
|
+
const APP_URL = process.env.APP_URL || `http://localhost:${PORT}`;
|
|
54
|
+
const NEWS_PREFIX = '/berita'; // Anda bisa mengganti ini sesuka hati (misal: /news)
|
|
55
|
+
|
|
56
|
+
async function startServer() {
|
|
57
|
+
// 1. Inisialisasi Middleware Global
|
|
58
|
+
app.use(express.json());
|
|
59
|
+
app.use(express.urlencoded({ extended: true }));
|
|
60
|
+
app.use(express.static('public'));
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// 2. Inisialisasi Package (Dijalankan secara ASYNC)
|
|
64
|
+
const newsRouter = await newsModule(dbConfig, {
|
|
65
|
+
adminRoutePrefix: '/cms-admin',
|
|
66
|
+
newsPrefix: NEWS_PREFIX,
|
|
67
|
+
baseUrl: APP_URL + NEWS_PREFIX,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 3. Pasang Router ke Prefix URL
|
|
71
|
+
// Ini akan membuat rute: /berita/list, /berita/cms-admin, dll.
|
|
72
|
+
app.use(NEWS_PREFIX, newsRouter);
|
|
73
|
+
|
|
74
|
+
// 4. Jalankan Server
|
|
75
|
+
app.listen(PORT, () => {
|
|
76
|
+
console.log(`Server Berhasil Dijalankan!`);
|
|
77
|
+
console.log(`Base URL : ${APP_URL}`);
|
|
78
|
+
console.log(`API Berita : ${APP_URL}${NEWS_PREFIX}/list`);
|
|
79
|
+
console.log(`Admin CMS : ${APP_URL}${NEWS_PREFIX}/cms-admin/dashboard`);
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error("Gagal menjalankan server:", error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
startServer();
|
|
87
|
+
```
|
|
88
|
+
pastikan untuk menyesuaikan bagian authAdminMiddleware pada module di bagian middlewares. Sesuaikan dengan preferensi tabel user masing-masing.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
github news-cms-module: https://github.com/DewaAry-D/news-cms-module.git
|
|
92
|
+
github contoh implementasi: https://github.com/DewaAry-D/dummy-news.git
|
package/config/index.js
CHANGED
|
@@ -4,6 +4,8 @@ const defaultConfig = {
|
|
|
4
4
|
// Pengaturan Router
|
|
5
5
|
adminRoutePrefix: '/admin', // Prefix default untuk route CRUD admin
|
|
6
6
|
publicRoutePrefix: '/', // Prefix default untuk route publik
|
|
7
|
+
newsPrefix: '/berita', // Prefix defaul umum contohnya /berita/{..}
|
|
8
|
+
baseURL: '', // Prefix defaul untuk fetch API
|
|
7
9
|
|
|
8
10
|
// Pengaturan Views
|
|
9
11
|
viewEngine: 'ejs', // Template engine default
|
|
@@ -24,10 +26,8 @@ const defaultConfig = {
|
|
|
24
26
|
* @returns {object} Objek konfigurasi akhir yang lengkap.
|
|
25
27
|
*/
|
|
26
28
|
module.exports = (userConfig = {}) => {
|
|
27
|
-
// Menggunakan penyebaran (spread operator) untuk menggabungkan objek.
|
|
28
|
-
// userConfig akan menimpa (overwrite) defaultConfig jika ada properti yang sama.
|
|
29
29
|
return {
|
|
30
30
|
...defaultConfig,
|
|
31
31
|
...userConfig
|
|
32
32
|
};
|
|
33
|
-
};
|
|
33
|
+
};
|
|
@@ -1,50 +1,145 @@
|
|
|
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
|
+
//render
|
|
41
|
+
res.render(path.join(__dirname, "../views/home.ejs"), {
|
|
42
|
+
posts,
|
|
43
|
+
categories,
|
|
44
|
+
trending,
|
|
45
|
+
query: { title, category },
|
|
46
|
+
pagination: {
|
|
47
|
+
totalItems,
|
|
48
|
+
totalPages,
|
|
49
|
+
currentPage,
|
|
50
|
+
perPage,
|
|
51
|
+
hasNextPage: currentPage < totalPages,
|
|
52
|
+
hasPrevPage: currentPage > 1,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
19
55
|
|
|
20
|
-
// res.render('list', { posts, total, baseUrl: req.baseUrl });
|
|
21
56
|
} catch (error) {
|
|
57
|
+
//console.error('Error loading news list:', error);
|
|
22
58
|
res.status(500).json({
|
|
23
|
-
|
|
24
|
-
error: 'Error loading news list.'
|
|
59
|
+
success: false,
|
|
60
|
+
error: 'Error loading news list.',
|
|
61
|
+
message: error.message
|
|
25
62
|
});
|
|
26
63
|
}
|
|
27
64
|
}
|
|
28
65
|
|
|
29
66
|
async getDetail(req, res) {
|
|
30
67
|
try {
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
68
|
+
const news = await this.newsService.getPostBySlug(req.params.slug);
|
|
69
|
+
if (!news) {
|
|
70
|
+
return res.status(404).json({
|
|
71
|
+
success: false,
|
|
72
|
+
error: 'news tidak ditemukan'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const categories = await this.newsService.getUniqueCategories();
|
|
77
|
+
const recommendation = await this.newsService.getRecommendationNews(news.category);
|
|
78
|
+
const trending = await this.newsService.getTrendingNews();
|
|
79
|
+
|
|
80
|
+
res.render(path.join(__dirname, "../views/detail.ejs"), {
|
|
81
|
+
news,
|
|
82
|
+
categories,
|
|
83
|
+
recommendation,
|
|
84
|
+
trending,
|
|
85
|
+
query: {}
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Error loading news detail:', error);
|
|
89
|
+
res.status(500).json({
|
|
90
|
+
success: false,
|
|
91
|
+
error: 'gagal melihat news',
|
|
92
|
+
message: error.message
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async getDetailForAdmin(req, res) {
|
|
98
|
+
try {
|
|
99
|
+
const news = await this.newsService.getPostBySlugForAdmin(req.params.slug);
|
|
100
|
+
if (!news) {
|
|
101
|
+
return res.status(404).json({
|
|
102
|
+
success: false,
|
|
103
|
+
error: 'news tidak ditemukan'
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
res.render(path.join(__dirname, "../views/admin/detailadmin.ejs"), {
|
|
108
|
+
news
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('Error loading news detail for admin:', error);
|
|
112
|
+
res.status(500).json({
|
|
113
|
+
success: false,
|
|
114
|
+
error: 'gagal melihat news',
|
|
115
|
+
message: error.message
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async getEditForAdmin(req, res) {
|
|
121
|
+
try {
|
|
122
|
+
const posts = await this.newsService.getPostBySlugForAdmin(req.params.slug);
|
|
123
|
+
if (!posts) {
|
|
33
124
|
return res.status(404).json({
|
|
34
125
|
succses: false,
|
|
35
126
|
error: 'news tidak ditemukan'
|
|
36
127
|
});
|
|
37
128
|
}
|
|
38
129
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
130
|
+
const appBaseUrl = config.baseUrl;
|
|
131
|
+
const newsPrefix = config.newsPrefix;
|
|
132
|
+
const adminPrefix = config.adminRoutePrefix;
|
|
133
|
+
|
|
134
|
+
const fullApiUrl = `${appBaseUrl}${adminPrefix}/create`;
|
|
135
|
+
const nextUrl = `${newsPrefix}${adminPrefix}/list`
|
|
43
136
|
|
|
44
|
-
|
|
45
|
-
|
|
137
|
+
res.render(path.join(__dirname, '../views/admin/update_news.ejs'), {
|
|
138
|
+
data: posts,
|
|
139
|
+
apiBaseUrl: fullApiUrl,
|
|
140
|
+
nextUrl
|
|
141
|
+
});
|
|
46
142
|
} catch (error) {
|
|
47
|
-
// res.status(500).send('Error loading post detail.');
|
|
48
143
|
res.status(500).json({
|
|
49
144
|
succses: false,
|
|
50
145
|
error: 'gagal melihat news'
|
|
@@ -52,29 +147,70 @@ class NewsController {
|
|
|
52
147
|
}
|
|
53
148
|
}
|
|
54
149
|
|
|
55
|
-
//
|
|
150
|
+
// Route Admin (CRUD)
|
|
56
151
|
async adminList(req, res) {
|
|
57
|
-
|
|
58
|
-
|
|
152
|
+
const {
|
|
153
|
+
page = 1,
|
|
154
|
+
limit = 10,
|
|
155
|
+
title = '',
|
|
156
|
+
category = '',
|
|
157
|
+
status = ''
|
|
158
|
+
} = req.query;
|
|
159
|
+
|
|
160
|
+
const currentPage = parseInt(page, 10);
|
|
161
|
+
const perPage = parseInt(limit, 10);
|
|
162
|
+
|
|
163
|
+
const offset = (currentPage - 1) * perPage;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const { rows: posts, count: totalItems } = await this.newsService.getAllPosts({
|
|
167
|
+
offset,
|
|
168
|
+
limit: perPage,
|
|
169
|
+
title,
|
|
170
|
+
category,
|
|
171
|
+
status: status.toUpperCase()
|
|
172
|
+
});
|
|
59
173
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
data: posts
|
|
63
|
-
})
|
|
174
|
+
const totalPages = Math.ceil(totalItems / perPage);
|
|
175
|
+
const categories = await this.newsService.getUniqueCategories();
|
|
64
176
|
|
|
65
|
-
|
|
66
|
-
|
|
177
|
+
// Render view instead of sending JSON
|
|
178
|
+
res.render(path.join(__dirname, "../views/admin/list.ejs"), {
|
|
179
|
+
posts,
|
|
180
|
+
categories,
|
|
181
|
+
query: { title, category, status },
|
|
182
|
+
pagination: {
|
|
183
|
+
totalItems,
|
|
184
|
+
totalPages,
|
|
185
|
+
currentPage,
|
|
186
|
+
perPage,
|
|
187
|
+
hasNextPage: currentPage < totalPages,
|
|
188
|
+
hasPrevPage: currentPage > 1
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error('Error loading admin news list:', error);
|
|
194
|
+
res.status(500).json({
|
|
195
|
+
success: false,
|
|
196
|
+
error: 'Error loading news list.',
|
|
197
|
+
message: error.message
|
|
198
|
+
});
|
|
199
|
+
}
|
|
67
200
|
}
|
|
68
201
|
|
|
69
202
|
async createPost(req, res) {
|
|
70
203
|
try {
|
|
71
|
-
const { title, summary, authorId, status, contentBlocks } = req.body;
|
|
72
204
|
|
|
73
|
-
const
|
|
205
|
+
const { title, authorName, status, contentBlocks } = req.body;
|
|
206
|
+
const category = req.body.category.toLowerCase();
|
|
207
|
+
const files = req.files;
|
|
208
|
+
const slug = title.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
|
|
74
209
|
|
|
75
210
|
const newNews = await this.newsService.createPost(
|
|
76
|
-
{ title, slug,
|
|
77
|
-
contentBlocks
|
|
211
|
+
{ title, slug, category, authorName, status: status || 'DRAFT' },
|
|
212
|
+
contentBlocks,
|
|
213
|
+
files
|
|
78
214
|
);
|
|
79
215
|
|
|
80
216
|
res.status(201).json({
|
|
@@ -86,56 +222,106 @@ class NewsController {
|
|
|
86
222
|
// res.redirect(this.config.adminRoutePrefix + '/');
|
|
87
223
|
} catch (error) {
|
|
88
224
|
console.error(error);
|
|
89
|
-
|
|
225
|
+
|
|
226
|
+
if (req.files) {
|
|
227
|
+
const allFiles = [
|
|
228
|
+
...(req.files['thumbnailImage'] || []),
|
|
229
|
+
...(req.files['contentImages'] || [])
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
allFiles.forEach(file => {
|
|
233
|
+
fs.unlink(file.path, (err) => {
|
|
234
|
+
if (err) console.error(`Gagal menghapus file: ${file.path}`, err);
|
|
235
|
+
else console.log(`Berhasil menghapus sampah file: ${file.path}`);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
90
240
|
res.status(500).json({
|
|
91
241
|
succses: false,
|
|
92
242
|
error: 'gagal membuat news'
|
|
93
243
|
});
|
|
94
244
|
}
|
|
95
245
|
}
|
|
96
|
-
|
|
246
|
+
|
|
97
247
|
async updatePost(req, res) {
|
|
248
|
+
const { id } = req.params;
|
|
98
249
|
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';
|
|
250
|
+
const { title, authorName, status, contentBlocks } = req.body;
|
|
251
|
+
const category = req.body.category.toLowerCase();
|
|
252
|
+
const files = req.files;
|
|
253
|
+
const slug = title ? title.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '') : undefined;
|
|
108
254
|
|
|
109
|
-
const
|
|
255
|
+
const result = await this.newsService.updatePost(
|
|
110
256
|
id,
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
summary,
|
|
115
|
-
authorId,
|
|
116
|
-
status: status || 'DRAFT',
|
|
117
|
-
publishedAt: isPublished ? new Date() : null
|
|
118
|
-
},
|
|
119
|
-
contentBlocks
|
|
257
|
+
{ title, slug, category, authorName, status },
|
|
258
|
+
contentBlocks,
|
|
259
|
+
files
|
|
120
260
|
);
|
|
121
261
|
|
|
122
|
-
if (
|
|
123
|
-
|
|
262
|
+
if (result.filesToDelete && result.filesToDelete.length > 0) {
|
|
263
|
+
result.filesToDelete.forEach(filePath => {
|
|
264
|
+
fs.unlink(filePath, (err) => {
|
|
265
|
+
if (err) console.error(`Gagal hapus file lama: ${filePath}`, err);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
124
268
|
}
|
|
125
269
|
|
|
126
270
|
res.status(200).json({
|
|
127
271
|
success: true,
|
|
128
|
-
message: '
|
|
129
|
-
data:
|
|
272
|
+
message: 'Berhasil update berita',
|
|
273
|
+
data: result.newsItem
|
|
130
274
|
});
|
|
131
275
|
|
|
132
276
|
} catch (error) {
|
|
133
277
|
console.error(error);
|
|
134
|
-
|
|
278
|
+
|
|
279
|
+
if (req.files) {
|
|
280
|
+
const uploadedFiles = [
|
|
281
|
+
...(req.files['thumbnailImage'] || []),
|
|
282
|
+
...(req.files['contentImages'] || [])
|
|
283
|
+
];
|
|
284
|
+
uploadedFiles.forEach(file => {
|
|
285
|
+
fs.unlink(file.path, (err) => {
|
|
286
|
+
if (err) console.error(`Cleanup error file gagal: ${file.path}`, err);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
res.status(500).json({
|
|
292
|
+
success: false,
|
|
293
|
+
error: error.message || 'Gagal update news'
|
|
294
|
+
});
|
|
135
295
|
}
|
|
136
296
|
}
|
|
137
297
|
|
|
298
|
+
async updateStatusNews(req, res) {
|
|
299
|
+
const { id } = req.params;
|
|
300
|
+
try {
|
|
301
|
+
const { status } = req.body;
|
|
138
302
|
|
|
303
|
+
const result = await this.newsService.updateStatusNews(
|
|
304
|
+
id,
|
|
305
|
+
status
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
res.status(200).json({
|
|
309
|
+
success: true,
|
|
310
|
+
message: 'Berhasil update status berita',
|
|
311
|
+
data: result
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error(error);
|
|
316
|
+
|
|
317
|
+
res.status(500).json({
|
|
318
|
+
success: false,
|
|
319
|
+
error: error.message || 'Gagal update news'
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
//ini blum selesai
|
|
139
325
|
async deletePost(req, res) {
|
|
140
326
|
try {
|
|
141
327
|
const { id } = req.params;
|
|
@@ -156,6 +342,26 @@ class NewsController {
|
|
|
156
342
|
}
|
|
157
343
|
}
|
|
158
344
|
|
|
345
|
+
async dashboardAdmin(req, res) {
|
|
346
|
+
try {
|
|
347
|
+
const currentYear = new Date().getFullYear();
|
|
348
|
+
|
|
349
|
+
const data = await this.newsService.dashboardAdmin(currentYear);
|
|
350
|
+
|
|
351
|
+
// res.status(200).json({
|
|
352
|
+
// success: true,
|
|
353
|
+
// data
|
|
354
|
+
// });
|
|
355
|
+
|
|
356
|
+
res.render(path.join(__dirname, '../views/admin/dashboard.ejs'), {
|
|
357
|
+
data: data
|
|
358
|
+
});
|
|
359
|
+
} catch (error) {
|
|
360
|
+
console.error(error);
|
|
361
|
+
res.status(500).json({ success: false, message: 'Failed to load data.', error: error.message });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
159
365
|
}
|
|
160
366
|
|
|
161
367
|
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 };
|