324 lines
8.9 KiB
JavaScript
324 lines
8.9 KiB
JavaScript
import bcrypt from "bcryptjs"
|
|
import cors from "cors"
|
|
import dotenv from "dotenv"
|
|
import express from "express"
|
|
import fs from "fs"
|
|
import jwt from "jsonwebtoken"
|
|
import multer from "multer"
|
|
import path from "path"
|
|
import { fileURLToPath } from "url"
|
|
import { pool } from "./db.js"
|
|
|
|
dotenv.config()
|
|
|
|
const app = express()
|
|
const port = Number(process.env.PORT || 5000)
|
|
const jwtSecret = process.env.JWT_SECRET || "secondtech_dev_secret"
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const __dirname = path.dirname(__filename)
|
|
const uploadsDir = path.join(__dirname, "uploads")
|
|
|
|
if (!fs.existsSync(uploadsDir)) {
|
|
fs.mkdirSync(uploadsDir, { recursive: true })
|
|
}
|
|
|
|
app.use(cors({
|
|
origin: process.env.CLIENT_URL || "http://localhost:5173",
|
|
credentials: true
|
|
}))
|
|
app.use(express.json())
|
|
app.use("/uploads", express.static(uploadsDir))
|
|
|
|
const storage = multer.diskStorage({
|
|
destination: uploadsDir,
|
|
filename: (req, file, cb) => {
|
|
const ext = path.extname(file.originalname).toLowerCase()
|
|
cb(null, `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`)
|
|
}
|
|
})
|
|
|
|
const upload = multer({
|
|
storage,
|
|
limits: { fileSize: 3 * 1024 * 1024 },
|
|
fileFilter: (req, file, cb) => {
|
|
if (!file.mimetype.startsWith("image/")) {
|
|
cb(new Error("File harus berupa gambar."))
|
|
return
|
|
}
|
|
cb(null, true)
|
|
}
|
|
})
|
|
|
|
function signUser(user) {
|
|
return jwt.sign({ id: user.id, email: user.email }, jwtSecret, { expiresIn: "7d" })
|
|
}
|
|
|
|
function publicUser(user) {
|
|
return {
|
|
id: user.id,
|
|
name: user.name,
|
|
email: user.email,
|
|
whatsapp: user.whatsapp,
|
|
role: user.role || "user"
|
|
}
|
|
}
|
|
|
|
function auth(req, res, next) {
|
|
const header = req.headers.authorization || ""
|
|
const token = header.startsWith("Bearer ") ? header.slice(7) : ""
|
|
|
|
if (!token) {
|
|
res.status(401).json({ message: "Silakan login terlebih dahulu." })
|
|
return
|
|
}
|
|
|
|
try {
|
|
req.user = jwt.verify(token, jwtSecret)
|
|
next()
|
|
} catch {
|
|
res.status(401).json({ message: "Sesi login tidak valid." })
|
|
}
|
|
}
|
|
|
|
async function findProductById(id) {
|
|
const [rows] = await pool.query(
|
|
"SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE id = ?",
|
|
[id]
|
|
)
|
|
return rows[0]
|
|
}
|
|
|
|
app.get("/api/health", async (req, res, next) => {
|
|
try {
|
|
await pool.query("SELECT 1")
|
|
res.json({ status: "ok" })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
app.post("/api/auth/register", async (req, res, next) => {
|
|
try {
|
|
const { name, email, whatsapp, password } = req.body
|
|
|
|
if (!name || !email || !whatsapp || !password) {
|
|
res.status(400).json({ message: "Semua field wajib diisi." })
|
|
return
|
|
}
|
|
|
|
if (password.length < 6) {
|
|
res.status(400).json({ message: "Password minimal 6 karakter." })
|
|
return
|
|
}
|
|
|
|
const passwordHash = await bcrypt.hash(password, 10)
|
|
const [result] = await pool.query(
|
|
"INSERT INTO users (name, email, whatsapp, password_hash) VALUES (?, ?, ?, ?)",
|
|
[name, email.toLowerCase(), whatsapp, passwordHash]
|
|
)
|
|
|
|
const user = { id: result.insertId, name, email: email.toLowerCase(), whatsapp, role: "user" }
|
|
res.status(201).json({ token: signUser(user), user: publicUser(user) })
|
|
} catch (error) {
|
|
if (error.code === "ER_DUP_ENTRY") {
|
|
res.status(409).json({ message: "Email sudah terdaftar." })
|
|
return
|
|
}
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
app.post("/api/auth/login", async (req, res, next) => {
|
|
try {
|
|
const { email, password } = req.body
|
|
|
|
if (!email || !password) {
|
|
res.status(400).json({ message: "Email dan password wajib diisi." })
|
|
return
|
|
}
|
|
|
|
const [rows] = await pool.query("SELECT * FROM users WHERE email = ?", [email.toLowerCase()])
|
|
const user = rows[0]
|
|
|
|
if (!user || !(await bcrypt.compare(password, user.password_hash))) {
|
|
res.status(401).json({ message: "Email atau password salah." })
|
|
return
|
|
}
|
|
|
|
res.json({ token: signUser(user), user: publicUser(user) })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
app.get("/api/auth/me", auth, async (req, res, next) => {
|
|
try {
|
|
const [rows] = await pool.query("SELECT id, name, email, whatsapp, role FROM users WHERE id = ?", [req.user.id])
|
|
if (!rows[0]) {
|
|
res.status(404).json({ message: "User tidak ditemukan." })
|
|
return
|
|
}
|
|
res.json({ user: rows[0] })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
app.get("/api/products", async (req, res, next) => {
|
|
try {
|
|
const [rows] = await pool.query(
|
|
"SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products ORDER BY created_at DESC, id DESC"
|
|
)
|
|
res.json({ products: rows })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
app.get("/api/products/mine", auth, async (req, res, next) => {
|
|
try {
|
|
const [rows] = await pool.query(
|
|
"SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE user_id = ? ORDER BY created_at DESC, id DESC",
|
|
[req.user.id]
|
|
)
|
|
res.json({ products: rows })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
app.get("/api/products/:id", async (req, res, next) => {
|
|
try {
|
|
const product = await findProductById(req.params.id)
|
|
if (!product) {
|
|
res.status(404).json({ message: "Produk tidak ditemukan." })
|
|
return
|
|
}
|
|
res.json({ product })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
app.post("/api/products", auth, upload.single("image"), async (req, res, next) => {
|
|
try {
|
|
const { title, category, price, condition, location, seller, whatsapp, description } = req.body
|
|
|
|
if (!title || !category || !price || !condition || !location || !seller || !whatsapp || !description || !req.file) {
|
|
res.status(400).json({ message: "Semua field produk dan foto wajib diisi." })
|
|
return
|
|
}
|
|
|
|
const imagePath = `/uploads/${req.file.filename}`
|
|
const [result] = await pool.query(
|
|
"INSERT INTO products (user_id, title, category, price, `condition`, location, seller, whatsapp, image, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
[req.user.id, title, category, Number(price), condition, location, seller, whatsapp, imagePath, description]
|
|
)
|
|
|
|
const product = await findProductById(result.insertId)
|
|
res.status(201).json({ product })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
app.patch("/api/products/:id/status", auth, async (req, res, next) => {
|
|
try {
|
|
const status = req.body.status === "Terjual" ? "Terjual" : "Tersedia"
|
|
const [result] = await pool.query(
|
|
"UPDATE products SET status = ? WHERE id = ? AND user_id = ?",
|
|
[status, req.params.id, req.user.id]
|
|
)
|
|
|
|
if (!result.affectedRows) {
|
|
res.status(404).json({ message: "Produk milik kamu tidak ditemukan." })
|
|
return
|
|
}
|
|
|
|
const product = await findProductById(req.params.id)
|
|
res.json({ product })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
app.delete("/api/products/:id", auth, async (req, res, next) => {
|
|
try {
|
|
const product = await findProductById(req.params.id)
|
|
if (!product || product.user_id !== req.user.id) {
|
|
res.status(404).json({ message: "Produk milik kamu tidak ditemukan." })
|
|
return
|
|
}
|
|
|
|
await pool.query("DELETE FROM products WHERE id = ? AND user_id = ?", [req.params.id, req.user.id])
|
|
|
|
if (product.image.startsWith("/uploads/")) {
|
|
fs.rm(path.join(uploadsDir, path.basename(product.image)), { force: true }, () => {})
|
|
}
|
|
|
|
res.json({ message: "Produk berhasil dihapus." })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
app.delete("/api/admin/products/:id", auth, superadminOnly, async (req, res, next) => {
|
|
try {
|
|
const product = await findProductById(req.params.id)
|
|
|
|
if (!product) {
|
|
res.status(404).json({ message: "Produk tidak ditemukan." })
|
|
return
|
|
}
|
|
|
|
await pool.query("DELETE FROM products WHERE id = ?", [req.params.id])
|
|
|
|
if (product.image.startsWith("/uploads/")) {
|
|
fs.rm(path.join(uploadsDir, path.basename(product.image)), { force: true }, () => {})
|
|
}
|
|
|
|
res.json({ message: "Produk berhasil dihapus oleh superadmin." })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
|
|
app.use((error, req, res, next) => {
|
|
if (error instanceof multer.MulterError) {
|
|
res.status(400).json({ message: "Upload gagal. Ukuran foto maksimal 3MB." })
|
|
return
|
|
}
|
|
|
|
if (error.message === "File harus berupa gambar.") {
|
|
res.status(400).json({ message: error.message })
|
|
return
|
|
}
|
|
|
|
console.error(error)
|
|
res.status(500).json({ message: "Terjadi kesalahan pada server." })
|
|
})
|
|
|
|
app.listen(port, () => {
|
|
console.log(`SecondTech API berjalan di http://localhost:${port}`)
|
|
})
|
|
|
|
|
|
async function superadminOnly(req, res, next) {
|
|
try {
|
|
const [rows] = await pool.query(
|
|
"SELECT role FROM users WHERE id = ?",
|
|
[req.user.id]
|
|
)
|
|
|
|
if (!rows[0] || rows[0].role !== "superadmin") {
|
|
res.status(403).json({ message: "Hanya superadmin yang boleh mengakses fitur ini." })
|
|
return
|
|
}
|
|
|
|
next()
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
}
|