├── .prettierrc ├── src ├── utils │ ├── logging.js │ └── config.js ├── costumer.js ├── Listener.js ├── PlaylistService.js ├── MailSender.js └── templates │ └── email-template.html ├── .editorconfig ├── .env.example ├── .gitignore ├── .eslintrc.json ├── package.json └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "useTabs": false 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/logging.js: -------------------------------------------------------------------------------- 1 | import pino from 'pino'; 2 | import pretty from 'pino-pretty'; 3 | 4 | const logger = pino( 5 | { 6 | base: { 7 | pid: false, 8 | }, 9 | }, 10 | pretty(), 11 | ); 12 | 13 | export default logger; 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Aturan-aturan editor 2 | [*] 3 | indent_style = space 4 | indent_size = 2 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | # Aturan-aturan untuk file-file dengan ekstensi tertentu 9 | [*.md] 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # DATABASE CONFIGURATION 2 | PGUSER=null 3 | PGPASSWORD=null 4 | PGDATABASE=openmusicapi 5 | PGHOST=localhost 6 | PGPORT=5432 7 | 8 | # Message broker 9 | RABBITMQ_SERVER=amqp://localhost 10 | 11 | # nodemailer SMTP authentication 12 | MAIL_HOST=null 13 | MAIL_PORT=null 14 | MAIL_ADDRESS=null 15 | MAIL_PASSWORD=null 16 | MAIL_FROM_ADDRESS=openmusic@gmail.com 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js 2 | node_modules/ 3 | npm-debug.log* 4 | 5 | # IDEs and editors 6 | .vscode/ 7 | 8 | # PostgreSQL 9 | *.backup 10 | *.sql 11 | 12 | # Logs 13 | logs/ 14 | *.log 15 | 16 | # Build files 17 | dist/ 18 | build/ 19 | 20 | # Environment variables 21 | .env 22 | 23 | # Dependencies installed via npm 24 | package-lock.json 25 | 26 | # Testing 27 | coverage/ 28 | -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | dotenv.config(); 4 | 5 | const config = { 6 | rabbitMq: { 7 | server: process.env.RABBITMQ_SERVER, 8 | }, 9 | mail: { 10 | host: process.env.MAIL_HOST, 11 | port: process.env.MAIL_PORT, 12 | user: process.env.MAIL_ADDRESS, 13 | password: process.env.MAIL_PASSWORD, 14 | fromAddress: process.env.MAIL_FROM_ADDRESS, 15 | }, 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": "airbnb-base", 8 | "overrides": [], 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | "no-console": "warn", 15 | "no-underscore-dangle": "off", 16 | "import/named": "off", 17 | "object-curly-newline": "off", 18 | "camelcase": "off", 19 | "import/no-unresolved": "off", 20 | "import/extensions": "off", 21 | "no-else-return": "off", 22 | "newline-per-chained-call": "off", 23 | "no-useless-catch": "off", 24 | "no-shadow": "off", 25 | "import/no-extraneous-dependencies": "off" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/costumer.js: -------------------------------------------------------------------------------- 1 | import amqp from 'amqplib'; 2 | import PlaylistsService from './PlaylistService.js'; 3 | import MailSender from './MailSender.js'; 4 | import Listener from './Listener.js'; 5 | import config from './utils/config.js'; 6 | import logger from './utils/logging.js'; 7 | 8 | const init = async () => { 9 | const playlistsService = new PlaylistsService(); 10 | const mailSender = new MailSender(); 11 | const listener = new Listener(playlistsService, mailSender); 12 | const connection = await amqp.connect(config.rabbitMq.server); 13 | 14 | const channel = await connection.createChannel(); 15 | await channel.assertQueue('export:playlists', { 16 | durable: true, 17 | }); 18 | 19 | channel.consume('export:playlists', listener.listen, { noAck: true }); 20 | 21 | logger.info('Consumer berjalan'); 22 | }; 23 | 24 | init(); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-music-api-costumer", 3 | "version": "1.0.0", 4 | "description": "Open Music API Costumer", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node ./src/costumer.js", 9 | "dev": "nodemon ./src/costumer.js", 10 | "lint": "eslint ./src" 11 | }, 12 | "author": "Hizkia Reppi", 13 | "license": "ISC", 14 | "dependencies": { 15 | "amqplib": "^0.10.3", 16 | "dotenv": "^16.3.1", 17 | "nodemailer": "^6.9.4", 18 | "pg": "^8.11.1", 19 | "pino": "^8.14.2", 20 | "pino-pretty": "^10.2.0" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^8.40.0", 24 | "eslint-config-airbnb-base": "^15.0.0", 25 | "eslint-config-prettier": "^8.8.0", 26 | "eslint-plugin-import": "^2.27.5", 27 | "eslint-plugin-prettier": "^4.2.1", 28 | "nodemon": "^2.0.22", 29 | "prettier": "^3.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Listener.js: -------------------------------------------------------------------------------- 1 | import logger from './utils/logging.js'; 2 | 3 | class Listener { 4 | constructor(playlistsService, mailSender) { 5 | this._playlistsService = playlistsService; 6 | this._mailSender = mailSender; 7 | this.listen = this.listen.bind(this); 8 | } 9 | 10 | async listen(message) { 11 | try { 12 | const { playlistId, targetEmail } = JSON.parse( 13 | message.content.toString(), 14 | ); 15 | const playlistsData = await this._playlistsService.getPlaylists( 16 | playlistId, 17 | ); 18 | const songs = await this._playlistsService.getSongs(playlistId); 19 | const owner = await this._playlistsService.getPlaylistOwner(playlistId); 20 | await this._mailSender.sendEmail( 21 | targetEmail, 22 | owner, 23 | JSON.stringify({ 24 | playlist: { 25 | id: playlistsData.id, 26 | name: playlistsData.name, 27 | songs, 28 | }, 29 | }), 30 | ); 31 | 32 | logger.info(`Email dikirim ke ${targetEmail}`); 33 | } catch (error) { 34 | logger.error(error); 35 | } 36 | } 37 | } 38 | 39 | export default Listener; 40 | -------------------------------------------------------------------------------- /src/PlaylistService.js: -------------------------------------------------------------------------------- 1 | import pg from 'pg'; 2 | 3 | const { Pool } = pg; 4 | 5 | class PlaylistsService { 6 | constructor() { 7 | this._pool = new Pool(); 8 | } 9 | 10 | async getPlaylists(playlistId) { 11 | const query = { 12 | text: 'SELECT id, name FROM playlists WHERE id = $1', 13 | values: [playlistId], 14 | }; 15 | const { rows } = await this._pool.query(query); 16 | 17 | return rows[0]; 18 | } 19 | 20 | async getSongs(playlistId) { 21 | const query = { 22 | text: `SELECT s.id, s.title, s.performer 23 | FROM songs s 24 | INNER JOIN songs_playlist p 25 | ON p.song_id = s.id 26 | WHERE p.playlist_id = $1`, 27 | values: [playlistId], 28 | }; 29 | const { rows } = await this._pool.query(query); 30 | 31 | return rows; 32 | } 33 | 34 | async getPlaylistOwner(playlistId) { 35 | const query = { 36 | text: `SELECT users.username, users.fullname 37 | FROM users 38 | INNER JOIN playlists 39 | ON users.id = playlists.owner 40 | WHERE playlists.id = $1`, 41 | values: [playlistId], 42 | }; 43 | const { rows } = await this._pool.query(query); 44 | 45 | return rows[0]; 46 | } 47 | } 48 | 49 | export default PlaylistsService; 50 | -------------------------------------------------------------------------------- /src/MailSender.js: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | import config from './utils/config.js'; 3 | import fs from 'fs'; 4 | import util from 'util'; 5 | 6 | const readFile = util.promisify(fs.readFile); 7 | 8 | class MailSender { 9 | constructor() { 10 | this._transporter = nodemailer.createTransport({ 11 | host: config.mail.host, 12 | port: config.mail.port, 13 | auth: { 14 | user: config.mail.user, 15 | pass: config.mail.password, 16 | }, 17 | }); 18 | } 19 | 20 | async sendEmail(targetEmail, owner, content) { 21 | const emailTemplate = await readFile( 22 | 'src/templates/email-template.html', 23 | 'utf8', 24 | ); 25 | 26 | const emailContent = emailTemplate.replace( 27 | /{{name}}|{{attachmentURL}}/gi, 28 | (matched) => { 29 | switch (matched) { 30 | case '{{name}}': 31 | return owner.fullname; 32 | case '{{attachmentURL}}': 33 | return ( 34 | 'data:application/json;base64,' + 35 | Buffer.from(JSON.stringify(content, null, 2)).toString('base64') 36 | ); 37 | default: 38 | return matched; 39 | } 40 | }, 41 | ); 42 | 43 | const message = { 44 | from: config.mail.fromAddress, 45 | to: targetEmail, 46 | subject: 'Export Playlist', 47 | html: emailContent, 48 | attachments: [ 49 | { 50 | filename: 'playlist.json', 51 | content, 52 | }, 53 | ], 54 | }; 55 | 56 | return this._transporter.sendMail(message); 57 | } 58 | } 59 | 60 | export default MailSender; 61 | -------------------------------------------------------------------------------- /src/templates/email-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Export Playlist 8 | 68 | 69 | 70 |
71 |

Selamat datang di Open Music

72 |

Dear {{name}},

73 |

Anda telah berhasil mengekspor playlist musik Anda!

74 |

Berikut adalah playlist musik yang telah Anda eksport:

75 |

76 | Jangan ragu untuk menikmati musik-musik favorit Anda dan berbagi dengan 77 | teman-teman Anda! 78 |

79 |
80 | 81 | 82 | 83 |
84 |

Terima kasih telah menggunakan layanan kami!

85 |

86 | Salam hangat,
87 | Tim Open Music 88 |

89 |

Open Music - Aplikasi Musik Terbaik untuk Semua

90 |
91 | 92 | 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Belajar Fundamental Aplikasi Back-End - Dicoding 2 | 3 | Kelas Belajar Fundamental Aplikasi Back-End | Alur Belajar Back-End Developer | Dicoding 4 | 5 | Kelas ini membahas mengenai teknologi dalam membangun RESTful API yang canggih seperti Database, Storage, hingga Authentication dan Authorization. 6 | 7 | Materi yang dipelajari dikelas ini: 8 | 9 | - **Hapi Plugin dan Data Validation** : Menggunakan sistem Plugin pada Hapi untuk mengelola source code agar lebih mudah dipelihara. Selain itu, mengajarkan tentang menerapkan teknik Data Validation menggunakan Joi untuk memastikan data yang dikirim oleh client sesuai dengan yang diharapkan. 10 | 11 | - **Database dengan Amazon RDS** : Menggunakan database sebagai penyimpanan data yang persisten. Modul ini menggunakan PostgreSQL sebagai database yang dipasang baik secara lokal (development) maupun production (menggunakan Amazon RDS). 12 | 13 | - **Authentication dan Authorization** : Menerapkan teknik authentication untuk memvalidasi pengguna yang mengonsumsi RESTful API. Serta menerapkan teknik authorization untuk memvalidasi resource yang merupakan hak pengguna. 14 | 15 | - **Normalisasi Database** : Menggunakan teknik normalisasi database untuk membangun fitur kompleks yang membutuhkan join dari beberapa tabel. 16 | 17 | - **Message Broker dengan Amazon MQ** : Menggunakan teknologi Message Broker untuk menangani permintaan secara asynchronous. Modul ini menggunakan RabbitMQ sebagai Message Broker secara lokal maupun production (menggunakan Amazon MQ). 18 | 19 | - **Storage dengan Amazon S3** : Membuat storage secara lokal menggunakan core modules fs dan memanfaatkan teknologi cloud dengan menggunakan Amazon S3. 20 | 21 | - **Caching Menggunakan Amazon ElastiCache** : Menggunakan teknologi memory caching untuk memberikan respons yang cepat dalam menampilkan resource. Modul ini menggunakan Redis sebagai memory caching secara lokal maupun production (menggunakan Amazon ElastiCache). 22 | 23 | ## Submission 24 | 25 | ### Submission Pertama 26 | 27 | Pada submission pertama kelas ini, saya membuat aplikasi _Open Music API_ dengan menerapkan RESTful API menggunakan framework Hapi, data validation dengan Joi, dan PostgreSQL database. 28 | 29 | ### Submission Kedua 30 | 31 | Pada submission kedua kelas ini, saya menerapkan Authentication dan Authorization, Error Handling serta Normalisasi Database. 32 | 33 | ### Submission Ketiga / Final Submission 34 | 35 | Pada submission terakhir kelas ini, saya membuat aplikasi _Open Music API_ dengan menerapkan RESTful API menggunakan framework Hapi, data validation dengan Joi, databbase PostgreSQL, Authentication dan Authorization, Error Handling, Normalisasi Database, Upload Gambar, menerapkan RabbitMQ sebagai Message Broker dan Menggunakan Redis untuk _server-side caching_ pada Open Music API. 36 | 37 | ## Backend Repository 38 | [Backend Repository](https://github.com/HizkiaReppi/open-music-api) 39 | 40 |
41 | 42 | Create with ❤ | Hizkia Reppi 43 | --------------------------------------------------------------------------------