fitur fitur darkmode dan foto foto
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
|
||||
async function getProductImages(productId, fallbackImage) {
|
||||
const [images] = await pool.query(
|
||||
"SELECT image FROM product_images WHERE product_id = ? ORDER BY sort_order ASC, id ASC",
|
||||
[productId]
|
||||
)
|
||||
|
||||
return images.length ? images.map((item) => item.image) : [fallbackImage]
|
||||
}
|
||||
|
||||
async function attachProductImages(products) {
|
||||
return Promise.all(
|
||||
products.map(async (product) => ({
|
||||
...product,
|
||||
images: await getProductImages(product.id, product.image)
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
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]
|
||||
)
|
||||
|
||||
const product = rows[0]
|
||||
if (!product) return null
|
||||
|
||||
product.images = await getProductImages(product.id, product.image)
|
||||
return product
|
||||
}
|
||||
|
||||
async function deleteUploadedImages(product) {
|
||||
const images = product.images?.length ? product.images : [product.image]
|
||||
|
||||
images.forEach((image) => {
|
||||
if (image?.startsWith("/uploads/")) {
|
||||
fs.rm(path.join(uploadsDir, path.basename(image)), { force: true }, () => {})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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: await attachProductImages(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: await attachProductImages(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.array("images", 6), 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.files?.length) {
|
||||
res.status(400).json({ message: "Semua field produk dan minimal 1 foto wajib diisi." })
|
||||
return
|
||||
}
|
||||
|
||||
const mainImagePath = `/uploads/${req.files[0].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, mainImagePath, description]
|
||||
)
|
||||
|
||||
const imageRows = req.files.map((file, index) => [result.insertId, `/uploads/${file.filename}`, index])
|
||||
await pool.query("INSERT INTO product_images (product_id, image, sort_order) VALUES ?", [imageRows])
|
||||
|
||||
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])
|
||||
await deleteUploadedImages(product)
|
||||
|
||||
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])
|
||||
await deleteUploadedImages(product)
|
||||
|
||||
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 setiap foto maksimal 3MB dan maksimal 6 foto." })
|
||||
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}`)
|
||||
})
|
||||
Reference in New Issue
Block a user