Compare commits
merge into: Arfiansyah:main
Arfiansyah:main
Arfiansyah:master
pull from: Arfiansyah:master
Arfiansyah:main
Arfiansyah:master
No commits in common. 'main' and 'master' have entirely different histories.
20 changed files with 2950 additions and 2 deletions
-
1.gitignore
-
2README.md
-
8blog.rest
-
16config/database.js
-
32controllers/RefreshToken.js
-
123controllers/blogController.js
-
114controllers/userController.js
-
30index.js
-
36middleware/UploadImage.js
-
13middleware/VerifyToken.js
-
24models/blogImage.js
-
27models/blogModel.js
-
27models/userModel.js
-
2401package-lock.json
-
24package.json
-
22routes/blogRoutes.js
-
10routes/index.js
-
18routes/userRoutes.js
-
BINuploads/images-1743046783320-gambar pohon.jpg
-
24user.rest
@ -1,2 +0,0 @@ |
|||||
# ComproGSI_BE |
|
||||
|
|
@ -0,0 +1,8 @@ |
|||||
|
POST http://localhost:5000/blog/blog |
||||
|
Content-Type: application/json |
||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsIm5hbWUiOiJhZG1pbiIsImVtYWlsIjoiYWRtaW5AYWRtaW4uY29tIiwiaWF0IjoxNzQzMDQ1NjA0LCJleHAiOjE3NDMwNDU2MjR9.DnR5lQCXYOAF0NuLkLCxBRhZctnmw_jg9dfmN9R_zY0 |
||||
|
|
||||
|
{ |
||||
|
"title": "GSI JAYA", |
||||
|
"description" : "lorem uasdasdadasdasdad" |
||||
|
} |
@ -0,0 +1,16 @@ |
|||||
|
import { Sequelize } from "sequelize"; |
||||
|
import dotenv from "dotenv"; |
||||
|
|
||||
|
dotenv.config(); |
||||
|
|
||||
|
const db = new Sequelize( |
||||
|
process.env.DB_NAME, |
||||
|
process.env.DB_USER, |
||||
|
process.env.DB_PASSWORD, |
||||
|
{ |
||||
|
host: process.env.DB_HOST, |
||||
|
dialect: process.env.DB_DIALECT, |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
export default db; |
@ -0,0 +1,32 @@ |
|||||
|
import Users from "../models/userModel.js"; |
||||
|
import jwt from "jsonwebtoken"; |
||||
|
|
||||
|
export const refreshToken = async (req, res) => { |
||||
|
try { |
||||
|
const refreshToken = req.cookies.refreshToken; |
||||
|
if (!refreshToken) return res.sendStatus(401); |
||||
|
|
||||
|
const user = await Users.findOne({ |
||||
|
where: { refresh_token: refreshToken }, |
||||
|
}); |
||||
|
if (!user) return res.sendStatus(403); |
||||
|
|
||||
|
jwt.verify( |
||||
|
refreshToken, |
||||
|
process.env.REFRESH_TOKEN_SECRET, |
||||
|
(err, decoded) => { |
||||
|
if (err) return res.sendStatus(403); |
||||
|
|
||||
|
const accessToken = jwt.sign( |
||||
|
{ userId: user.id, name: user.name, email: user.email }, |
||||
|
process.env.ACCESS_TOKEN_SECRET, |
||||
|
{ expiresIn: "20s" } |
||||
|
); |
||||
|
|
||||
|
res.json({ accessToken }); |
||||
|
} |
||||
|
); |
||||
|
} catch (error) { |
||||
|
res.status(500).json({ msg: "Terjadi kesalahan pada server" }); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,123 @@ |
|||||
|
import blogModel from "../models/blogModel.js"; |
||||
|
import blogImage from "../models/blogImage.js"; |
||||
|
|
||||
|
// Get all blogs with images
|
||||
|
export const getAllBlogs = async (req, res) => { |
||||
|
try { |
||||
|
const blogs = await blogModel.findAll({ |
||||
|
include: [{ model: blogImage, as: "images" }], // Tambahkan alias "images"
|
||||
|
}); |
||||
|
res.json(blogs); |
||||
|
} catch (error) { |
||||
|
res.status(500).json({ message: error.message }); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Get a single blog by ID
|
||||
|
export const getBlogById = async (req, res) => { |
||||
|
try { |
||||
|
const blog = await blogModel.findByPk(req.params.id, { |
||||
|
include: [{ model: blogImage, as: "images" }], // Tambahkan alias "images"
|
||||
|
}); |
||||
|
if (!blog) return res.status(404).json({ message: "Blog not found" }); |
||||
|
res.json(blog); |
||||
|
} catch (error) { |
||||
|
res.status(500).json({ message: error.message }); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Get blog by slug
|
||||
|
export const getBlogBySlug = async (req, res) => { |
||||
|
try { |
||||
|
const blog = await blogModel.findOne({ |
||||
|
where: { slug: req.params.slug }, |
||||
|
include: [{ model: blogImage, as: "images" }], |
||||
|
}); |
||||
|
|
||||
|
if (!blog) return res.status(404).json({ message: "Blog not found" }); |
||||
|
|
||||
|
res.json(blog); |
||||
|
} catch (error) { |
||||
|
res.status(500).json({ message: error.message }); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Create a new blog
|
||||
|
export const createBlog = async (req, res) => { |
||||
|
try { |
||||
|
const { title, description } = req.body; |
||||
|
|
||||
|
if (!req.files || req.files.length === 0) { |
||||
|
return res.status(400).json({ message: "Minimal 1 gambar diperlukan!" }); |
||||
|
} |
||||
|
|
||||
|
const slug = generateSlug(title); // 🔥 generate slug di sini
|
||||
|
|
||||
|
const blog = await blogModel.create({ title, description, slug }); |
||||
|
|
||||
|
const imagePaths = req.files.map((file) => ({ |
||||
|
blogId: blog.id, |
||||
|
imageUrl: `/uploads/${file.filename}`, |
||||
|
})); |
||||
|
|
||||
|
await blogImage.bulkCreate(imagePaths); |
||||
|
|
||||
|
const newBlog = await blogModel.findByPk(blog.id, { |
||||
|
include: [{ model: blogImage, as: "images" }], |
||||
|
}); |
||||
|
|
||||
|
res.status(201).json({ |
||||
|
message: "Blog created successfully", |
||||
|
blog: newBlog, |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
res.status(500).json({ message: error.message }); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const generateSlug = (title) => { |
||||
|
return title |
||||
|
.toLowerCase() |
||||
|
.replace(/\s+/g, "-") |
||||
|
.replace(/[^\w-]+/g, ""); |
||||
|
}; |
||||
|
|
||||
|
// Update a blog
|
||||
|
export const updateBlog = async (req, res) => { |
||||
|
try { |
||||
|
const blog = await blogModel.findByPk(req.params.id); |
||||
|
if (!blog) return res.status(404).json({ message: "Blog not found" }); |
||||
|
|
||||
|
const { title, description, slug } = req.body; |
||||
|
|
||||
|
// Tentukan slug baru (jika ada title baru dan tidak ada slug manual)
|
||||
|
let updatedSlug = blog.slug; // default: slug lama
|
||||
|
if (title && !slug) { |
||||
|
updatedSlug = generateSlug(title); |
||||
|
} else if (slug) { |
||||
|
updatedSlug = slug; |
||||
|
} |
||||
|
|
||||
|
// Update data
|
||||
|
await blog.update({ |
||||
|
title: title ?? blog.title, |
||||
|
description: description ?? blog.description, |
||||
|
slug: updatedSlug, |
||||
|
}); |
||||
|
res.json(blog); |
||||
|
} catch (error) { |
||||
|
res.status(500).json({ message: error.message }); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Delete a blog
|
||||
|
export const deleteBlog = async (req, res) => { |
||||
|
try { |
||||
|
const blog = await blogModel.findByPk(req.params.id); |
||||
|
if (!blog) return res.status(404).json({ message: "Blog not found" }); |
||||
|
await blog.destroy(); |
||||
|
res.json({ message: "Blog deleted successfully" }); |
||||
|
} catch (error) { |
||||
|
res.status(500).json({ message: error.message }); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,114 @@ |
|||||
|
import Users from "../models/userModel.js"; |
||||
|
import bcrypt from "bcrypt"; |
||||
|
import jwt from "jsonwebtoken"; |
||||
|
|
||||
|
//Get List User from database
|
||||
|
|
||||
|
export const getUsers = async (req, res) => { |
||||
|
try { |
||||
|
const users = await Users.findAll({ |
||||
|
attributes: ["id", "name", "email"], |
||||
|
}); |
||||
|
res.json(users); |
||||
|
} catch (error) { |
||||
|
console.log(error); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export const Register = async (req, res) => { |
||||
|
const { name, email, password, confirmPassword } = req.body; |
||||
|
if (password !== confirmPassword) |
||||
|
return res.status(400).json({ msg: "Password don't match" }); |
||||
|
|
||||
|
// Check if name or email already exists
|
||||
|
const existingUser = await Users.findOne({ where: { email: email } }); |
||||
|
if (existingUser) { |
||||
|
return res.status(400).json({ msg: "Email already exists" }); |
||||
|
} |
||||
|
|
||||
|
//Hash Password with bcrypt with Register account
|
||||
|
const salt = await bcrypt.genSalt(); |
||||
|
const hashPassword = await bcrypt.hash(password, salt); |
||||
|
try { |
||||
|
await Users.create({ |
||||
|
name: name, |
||||
|
email: email, |
||||
|
password: hashPassword, |
||||
|
}); |
||||
|
res.status(201).json({ msg: "Account Created", user: { name, email } }); |
||||
|
} catch (error) { |
||||
|
res.status(400).json({ msg: "Failed to create Account" }); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Login user
|
||||
|
export const Login = async (req, res) => { |
||||
|
try { |
||||
|
const user = await Users.findOne({ |
||||
|
where: { |
||||
|
email: req.body.email, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
if (!user) { |
||||
|
return res.status(404).json({ msg: "Email tidak ditemukan" }); |
||||
|
} |
||||
|
|
||||
|
// Perbaiki akses password (user.password bukan user[0].password)
|
||||
|
const match = await bcrypt.compare(req.body.password, user.password); |
||||
|
if (!match) return res.status(400).json({ msg: "Wrong Password" }); |
||||
|
|
||||
|
const userId = user.id; |
||||
|
const name = user.name; |
||||
|
const email = user.email; |
||||
|
|
||||
|
const accessToken = jwt.sign( |
||||
|
{ userId, name, email }, |
||||
|
process.env.ACCESS_TOKEN_SECRET, |
||||
|
{ expiresIn: "1d" } |
||||
|
); |
||||
|
|
||||
|
const refreshToken = jwt.sign( |
||||
|
{ userId, name, email }, |
||||
|
process.env.REFRESH_TOKEN_SECRET, |
||||
|
{ expiresIn: "1d" } |
||||
|
); |
||||
|
|
||||
|
await Users.update( |
||||
|
{ refresh_token: refreshToken }, |
||||
|
{ where: { id: userId } } |
||||
|
); |
||||
|
|
||||
|
res.cookie("refreshToken", refreshToken, { |
||||
|
httpOnly: true, |
||||
|
maxAge: 24 * 60 * 60 * 1000, |
||||
|
}); |
||||
|
|
||||
|
res.json({ accessToken }); |
||||
|
} catch (error) { |
||||
|
console.error("Error saat login:", error); |
||||
|
res.status(500).json({ msg: "Internal Server Error" }); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export const Logout = async (req, res) => { |
||||
|
const refreshToken = req.cookies.refreshToken; |
||||
|
if (!refreshToken) return res.sendStatus(204); |
||||
|
const user = await Users.findAll({ |
||||
|
where: { |
||||
|
refresh_token: refreshToken, |
||||
|
}, |
||||
|
}); |
||||
|
if (!user[0]) return res.json.sendStatus(204); |
||||
|
const userId = user[0].id; |
||||
|
await Users.update( |
||||
|
{ refresh_token: null }, |
||||
|
{ |
||||
|
where: { |
||||
|
id: userId, |
||||
|
}, |
||||
|
} |
||||
|
); |
||||
|
res.clearCookie("refreshToken"); |
||||
|
return res.sendStatus(200); |
||||
|
}; |
@ -0,0 +1,30 @@ |
|||||
|
import express from "express"; |
||||
|
import db from "./config/database.js"; |
||||
|
import dotenv from "dotenv"; |
||||
|
import router from "./routes/index.js"; // Hanya import 1 router
|
||||
|
import cors from "cors"; |
||||
|
import cookieParser from "cookie-parser"; |
||||
|
import path from "path"; |
||||
|
|
||||
|
const app = express(); |
||||
|
dotenv.config(); |
||||
|
|
||||
|
const connectToDatabase = async () => { |
||||
|
// await db.sync({ force: true });
|
||||
|
try { |
||||
|
await db.authenticate(); |
||||
|
console.log("Database connected..."); |
||||
|
} catch (error) { |
||||
|
console.log("Connection error:", error); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
connectToDatabase(); |
||||
|
|
||||
|
app.use(cookieParser()); |
||||
|
app.use(cors()); |
||||
|
app.use(express.json()); |
||||
|
app.use(router); |
||||
|
app.use("/uploads", express.static(path.join(process.cwd(), "uploads"))); |
||||
|
|
||||
|
app.listen(5000, () => console.log("server running at port 5000")); |
@ -0,0 +1,36 @@ |
|||||
|
import multer from "multer"; |
||||
|
import path from "path"; |
||||
|
|
||||
|
// Konfigurasi storage
|
||||
|
const storage = multer.diskStorage({ |
||||
|
destination: (req, file, cb) => { |
||||
|
cb(null, "uploads/"); // Folder penyimpanan
|
||||
|
}, |
||||
|
filename: (req, file, cb) => { |
||||
|
cb(null, file.fieldname + "-" + Date.now() + "-" + file.originalname); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
// Filter jenis file
|
||||
|
const fileFilter = (req, file, cb) => { |
||||
|
const allowedTypes = /jpeg|jpg|png|gif/; |
||||
|
const extname = allowedTypes.test( |
||||
|
path.extname(file.originalname).toLowerCase() |
||||
|
); |
||||
|
const mimetype = allowedTypes.test(file.mimetype); |
||||
|
|
||||
|
if (mimetype && extname) { |
||||
|
return cb(null, true); |
||||
|
} else { |
||||
|
cb(new Error("Only images (jpeg, jpg, png, gif) are allowed")); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
//for multiple images
|
||||
|
const upload = multer({ |
||||
|
storage: storage, |
||||
|
limits: { fileSize: 5 * 1024 * 1024 }, // Maksimal 5MB per file
|
||||
|
fileFilter: fileFilter, |
||||
|
}); |
||||
|
|
||||
|
export default upload; |
@ -0,0 +1,13 @@ |
|||||
|
import jwt from "jsonwebtoken"; |
||||
|
|
||||
|
export const verifyToken = (req, res, next) => { |
||||
|
const authHeader = req.headers["authorization"]; |
||||
|
const token = authHeader && authHeader.split(" ")[1]; |
||||
|
if (token == null) return res.sendStatus(401); |
||||
|
|
||||
|
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => { |
||||
|
if (err) return res.sendStatus(403); |
||||
|
req.user = user; |
||||
|
next(); |
||||
|
}); |
||||
|
}; |
@ -0,0 +1,24 @@ |
|||||
|
import { Sequelize } from "sequelize"; |
||||
|
import db from "../config/database.js"; |
||||
|
|
||||
|
const { DataTypes } = Sequelize; |
||||
|
|
||||
|
const BlogImage = db.define("blog_images", { |
||||
|
blogId: { |
||||
|
type: DataTypes.INTEGER, |
||||
|
allowNull: false, |
||||
|
references: { |
||||
|
model: "blogs", |
||||
|
key: "id" |
||||
|
}, |
||||
|
onDelete: "CASCADE" |
||||
|
}, |
||||
|
imageUrl: { |
||||
|
type: DataTypes.STRING, |
||||
|
allowNull: false |
||||
|
} |
||||
|
}, { |
||||
|
freezeTableName: true |
||||
|
}); |
||||
|
|
||||
|
export default BlogImage; |
@ -0,0 +1,27 @@ |
|||||
|
import { Sequelize } from "sequelize"; |
||||
|
import db from "../config/database.js"; |
||||
|
import blogImage from "./blogImage.js"; |
||||
|
|
||||
|
const { DataTypes } = Sequelize; |
||||
|
|
||||
|
const Blog = db.define("blog", { |
||||
|
title: { |
||||
|
type: DataTypes.STRING, |
||||
|
allowNull: false, |
||||
|
}, |
||||
|
description: { |
||||
|
type: DataTypes.TEXT, |
||||
|
allowNull: false, |
||||
|
}, |
||||
|
slug: { |
||||
|
type: DataTypes.STRING, |
||||
|
allowNull: false, |
||||
|
unique: true, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
// Definisi relasi dengan alias 'images'
|
||||
|
Blog.hasMany(blogImage, { foreignKey: "blogId", as: "images" }); |
||||
|
blogImage.belongsTo(Blog, { foreignKey: "blogId", as: "blog" }); |
||||
|
|
||||
|
export default Blog; |
@ -0,0 +1,27 @@ |
|||||
|
import { Sequelize } from "sequelize"; |
||||
|
import db from "../config/database.js"; |
||||
|
|
||||
|
const { DataTypes } = Sequelize; |
||||
|
|
||||
|
const User = db.define( |
||||
|
"users", |
||||
|
{ |
||||
|
name: { |
||||
|
type: DataTypes.STRING, |
||||
|
}, |
||||
|
email: { |
||||
|
type: DataTypes.STRING, |
||||
|
}, |
||||
|
password: { |
||||
|
type: DataTypes.STRING, |
||||
|
}, |
||||
|
refresh_token: { |
||||
|
type: DataTypes.TEXT, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
freezeTableName: true, |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
export default User; |
2401
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,24 @@ |
|||||
|
{ |
||||
|
"name": "becompro", |
||||
|
"version": "1.0.0", |
||||
|
"description": "", |
||||
|
"license": "ISC", |
||||
|
"author": "arfan", |
||||
|
"type": "module", |
||||
|
"main": "index.js", |
||||
|
"scripts": { |
||||
|
"test": "echo \"Error: no test specified\" && exit 1" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"bcrypt": "^5.1.1", |
||||
|
"cookie-parser": "^1.4.7", |
||||
|
"cors": "^2.8.5", |
||||
|
"dotenv": "^16.4.7", |
||||
|
"express": "^4.21.2", |
||||
|
"jsonwebtoken": "^9.0.2", |
||||
|
"multer": "^1.4.5-lts.2", |
||||
|
"mysql2": "^3.12.0", |
||||
|
"nodemon": "^3.1.9", |
||||
|
"sequelize": "^6.37.5" |
||||
|
} |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
import express from "express"; |
||||
|
import { |
||||
|
getAllBlogs, |
||||
|
getBlogById, |
||||
|
getBlogBySlug, |
||||
|
createBlog, |
||||
|
updateBlog, |
||||
|
deleteBlog, |
||||
|
} from "../controllers/blogController.js"; |
||||
|
import upload from "../middleware/UploadImage.js"; |
||||
|
import { verifyToken } from "../middleware/VerifyToken.js"; |
||||
|
|
||||
|
const router = express.Router(); |
||||
|
|
||||
|
router.get("/blog", getAllBlogs); |
||||
|
router.get("/blog/:id", getBlogById); |
||||
|
router.get("/blog/slug/:slug", getBlogBySlug); |
||||
|
router.post("/blog", upload.array("images", 5), createBlog); |
||||
|
router.put("/blog/:id", upload.array("images", 5), updateBlog); |
||||
|
router.delete("/blog/:id", deleteBlog); |
||||
|
|
||||
|
export default router; |
@ -0,0 +1,10 @@ |
|||||
|
import express from "express"; |
||||
|
import blogRoutes from "./blogRoutes.js"; |
||||
|
import userRoutes from "./userRoutes.js"; |
||||
|
|
||||
|
const router = express.Router(); |
||||
|
|
||||
|
router.use("/blog", blogRoutes); |
||||
|
router.use("/users", userRoutes); |
||||
|
|
||||
|
export default router; |
@ -0,0 +1,18 @@ |
|||||
|
import express from "express"; |
||||
|
import { |
||||
|
getUsers, |
||||
|
Register, |
||||
|
Login, |
||||
|
Logout, |
||||
|
} from "../controllers/userController.js"; |
||||
|
import { refreshToken } from "../controllers/RefreshToken.js"; |
||||
|
import { verifyToken } from "../middleware/VerifyToken.js"; |
||||
|
const router = express.Router(); |
||||
|
|
||||
|
router.get("/", verifyToken, getUsers); |
||||
|
router.get("/token", refreshToken); |
||||
|
router.post("/register", Register); |
||||
|
router.post("/login", Login); |
||||
|
router.post("/logout", Logout); |
||||
|
|
||||
|
export default router; |
After ![]() Width: 1280 | Height: 720 | Size: 90 KiB |
@ -0,0 +1,24 @@ |
|||||
|
POST http://localhost:5000/users/register |
||||
|
Content-type: application/json |
||||
|
|
||||
|
{ |
||||
|
"name" : "admin", |
||||
|
"email" : "admin@admin.com", |
||||
|
"password" : "admin123", |
||||
|
"confirmPassword" : "admin123" |
||||
|
} |
||||
|
|
||||
|
### |
||||
|
|
||||
|
POST http://localhost:5000/users/login/ |
||||
|
Content-Type: application/json |
||||
|
|
||||
|
{ |
||||
|
"email": "admin@admin.com", |
||||
|
"password": "admin123" |
||||
|
} |
||||
|
|
||||
|
### |
||||
|
|
||||
|
GET http://localhost:5000/users/token |
||||
|
|
Reference in new issue
xxxxxxxxxx