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 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; // Digunakan untuk passing config ke view (e.g. baseUrl)
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: total } = await this.newsService.getAllPosts(req.query.page);
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
- res.status(200).json({
14
- success: true,
15
- data: posts,
16
- total: total,
17
- page: req.query.page
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
- succses: false,
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 post = await this.newsService.getPostBySlug(req.params.slug);
32
- if (!post) {
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
- success: true,
41
- data: post
42
- })
148
+ // res.status(200).json({
149
+ // success: true,
150
+ // data: post
151
+ // })
43
152
 
44
- // Render detail view
45
- // res.render('detail', { post, baseUrl: req.baseUrl });
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
- // --- Route Admin (CRUD) ---
164
+ // Route Admin (CRUD)
56
165
  async adminList(req, res) {
57
- //tambahkan ARCHIVE
58
- const { rows: posts } = await this.newsService.getAllPosts(req.query.page, 10, ['DRAFT', 'PUBLISHED']);
166
+ const {
167
+ page = 1,
168
+ limit = 10,
169
+ title = '',
170
+ category = '',
171
+ status = ''
172
+ } = req.query;
59
173
 
60
- res.status(200).json({
61
- success: true,
62
- data: posts
63
- })
174
+ const currentPage = parseInt(page, 10);
175
+ const perPage = parseInt(limit, 10);
64
176
 
65
- //render
66
- // res.render('admin/list', { posts, baseUrl: req.baseUrl });
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 slug = title.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
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, summary, authorId, status: status || 'DRAFT' },
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
- // res.status(500).send('Gagal membuat berita.');
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 { id } = req.params;
100
- const { title, summary, authorId, status, contentBlocks } = req.body;
101
-
102
- if (!id || !title || !contentBlocks) {
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 updatedPost = await this.newsService.updatePost(
269
+ const result = await this.newsService.updatePost(
110
270
  id,
111
- {
112
- title,
113
- slug,
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 (!updatedPost) {
123
- return res.status(404).json({ success: false, message: 'News post not found for update.' });
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: 'Post updated successfully.',
129
- data: updatedPost
286
+ message: 'Berhasil update berita',
287
+ data: result.newsItem
130
288
  });
131
289
 
132
290
  } catch (error) {
133
291
  console.error(error);
134
- res.status(500).json({ success: false, message: 'Failed to update post.', error: error.message });
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
- const { slug } = req.params;
11
-
12
- // Ambil post ID (idealnya dilakukan oleh service sebelum dipanggil)
13
- // Untuk demo, kita asumsikan kita punya newsId dari service atau middleware sebelumnya.
14
- // **REAL-WORLD:** Anda harus mencari newsId berdasarkan slug di sini atau di service.
15
- // const newsId = 1; // Contoh: Asumsi newsId ditemukan
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
- const sessionId = req.sessionID || req.ip; // Gunakan sesi atau IP untuk unique ID
18
-
19
- if (newsId) {
20
- await this.statService.trackVisit(newsId, sessionId);
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
- // Pastikan Anda telah mendefinisikan dan mengimpor fungsi-fungsi ini
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'); // Gunakan fungsi mergeConfig yang sudah kita buat
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
- // 5. Setup Router Express
33
+ const services = initServices(db);
57
34
  const router = express.Router();
58
-
59
- // Middleware dasar untuk parsing body (penting untuk CRUD)
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 };