Compare commits

...

2 Commits

Author SHA1 Message Date
cureb
e966158d37 chore: basic abilities added 2025-11-05 14:56:26 +00:00
cureb
d9ca95470f - Uploading Avatars - Created Image array 2025-11-03 20:56:14 +00:00
25 changed files with 1370 additions and 106 deletions

3
db/client.js Normal file
View File

@ -0,0 +1,3 @@
const { PrismaClient } = require("@prisma/client")
const prisma = new PrismaClient()
module.exports = prisma

28
db/comment.db.js Normal file
View File

@ -0,0 +1,28 @@
const prisma = require("./client")
async function findComments(where, options = {}) {
return prisma.comment.findMany({
where,
include: options.include || undefined,
select: options.select || undefined,
orderBy: options.orderBy || { createdAt: "desc" },
})
}
async function createComment(data, options = {}) {
return prisma.comment.create({
data,
include: options.include || undefined,
select: options.select || undefined,
})
}
async function deleteComment(where) {
return prisma.comment.delete({ where })
}
module.exports = {
findComments,
createComment,
deleteComment,
}

82
db/deal.db.js Normal file
View File

@ -0,0 +1,82 @@
const prisma = require("./client")
async function findDeals(where = {}, options = {}) {
return prisma.deal.findMany({
where,
include: options.include || undefined,
select: options.select || undefined,
orderBy: options.orderBy || { createdAt: "desc" },
skip: options.skip || 0,
take: options.take || undefined,
})
}
async function findDeal(where, options = {}) {
return prisma.deal.findUnique({
where,
include: options.include || undefined,
select: options.select || undefined,
})
}
async function createDeal(data, options = {}) {
return prisma.deal.create({
data,
include: options.include || undefined,
select: options.select || undefined,
})
}
async function updateDeal(where, data, options = {}) {
return prisma.deal.update({
where,
data,
include: options.include || undefined,
select: options.select || undefined,
})
}
async function countDeals(where = {}) {
return prisma.deal.count({ where })
}
async function findVotes(where = {}, options = {}) {
return prisma.dealVote.findMany({
where,
include: options.include || undefined,
select: options.select || undefined,
})
}
async function createVote(data, options = {}) {
return prisma.dealVote.create({
data,
include: options.include || undefined,
select: options.select || undefined,
})
}
async function updateVote(where, data, options = {}) {
return prisma.dealVote.update({
where,
data,
include: options.include || undefined,
select: options.select || undefined,
})
}
async function countVotes(where = {}) {
return prisma.dealVote.count({ where })
}
module.exports = {
findDeals,
findDeal,
createDeal,
updateDeal,
countDeals,
findVotes,
createVote,
updateVote,
countVotes,
}

24
db/user.db.js Normal file
View File

@ -0,0 +1,24 @@
const { PrismaClient } = require("@prisma/client")
const prisma = new PrismaClient()
async function findUser(where, options = {}) {
return prisma.user.findUnique({
where,
include: options.include || undefined,
select: options.select || undefined,
})
}
async function updateUser(where, data, options = {}) {
return prisma.user.update({
where,
data,
include: options.include || undefined,
select: options.select || undefined,
})
}
module.exports = {
findUser,
updateUser,
}

875
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,16 +12,19 @@
"type": "commonjs",
"dependencies": {
"@prisma/client": "^6.18.0",
"@supabase/supabase-js": "^2.78.0",
"bcryptjs": "^3.0.2",
"cors": "^2.8.5",
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2",
"multer": "^2.0.2",
"zod": "^4.1.12"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.5",
"@types/node": "^24.9.2",
"dependency-cruiser": "^17.2.0",
"prisma": "^6.18.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"

View File

@ -0,0 +1,21 @@
/*
Warnings:
- You are about to drop the column `imageUrl` on the `Deal` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Deal" DROP COLUMN "imageUrl";
-- CreateTable
CREATE TABLE "DealImage" (
"id" SERIAL NOT NULL,
"imageUrl" VARCHAR(512) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"dealId" INTEGER NOT NULL,
CONSTRAINT "DealImage_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "DealImage" ADD CONSTRAINT "DealImage_dealId_fkey" FOREIGN KEY ("dealId") REFERENCES "Deal"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "DealImage" ADD COLUMN "order" INTEGER NOT NULL DEFAULT 0;

View File

@ -31,18 +31,26 @@ model Deal {
title String
description String?
url String?
imageUrl String?
price Float?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId Int
score Int @default(0)
user User @relation(fields: [userId], references: [id])
votes DealVote[]
comments Comment[] // ← burası eklendi
user User @relation(fields: [userId], references: [id])
votes DealVote[]
comments Comment[]
images DealImage[] // ← yeni ilişki
}
model DealImage {
id Int @id @default(autoincrement())
imageUrl String @db.VarChar(512)
order Int @default(0)
createdAt DateTime @default(now())
dealId Int
deal Deal @relation(fields: [dealId], references: [id], onDelete: Cascade)
}
model DealVote {
id Int @id @default(autoincrement())

View File

@ -0,0 +1,40 @@
const express = require("express")
const multer = require("multer")
const fs = require("fs")
const { uploadProfileImage } = require("../../services/supabase/supabaseUploadService")
const { validateImage } = require("../../utils/validateImage")
const authMiddleware = require("../../middleware/authMiddleware")
const { updateAvatarUrl, getUserProfile } = require("../../services/profile/myProfileService")
const router = express.Router()
const upload = multer({ dest: "uploads/" })
router.post("/avatar", authMiddleware, upload.single("file"), async (req, res) => {
try {
const file = req.file
if (!file) return res.status(400).json({ error: "No file uploaded" })
validateImage({ mimetype: file.mimetype, size: file.size })
const data = fs.readFileSync(file.path)
const url = await uploadProfileImage(req.user.userId, { data, mimetype: file.mimetype })
fs.unlinkSync(file.path)
const updated = await updateAvatarUrl(req.user.userId, url)
res.json({ message: "Avatar updated", user: updated })
} catch (err) {
console.error(err)
res.status(400).json({ error: err.message })
}
})
router.get("/me", authMiddleware, async (req, res) => {
try {
const user = await getUserProfile(req.user.id)
res.json(user)
} catch (err) {
res.status(400).json({ error: err.message })
}
})
module.exports = router

View File

@ -50,7 +50,7 @@ router.post("/login", async (req, res) => {
res.json({
token,
user: { id: user.id, username: user.username, email: user.email },
user: { id: user.id, username: user.username, email: user.email,avatarUrl:user.avatarUrl },
});
} catch (err) {
console.error(err);
@ -65,7 +65,7 @@ router.get("/me", authMiddleware, async (req, res) => {
try {
const user = await prisma.user.findUnique({
where: { id: req.user.userId },
select: { id: true, username: true, email: true },
select: { id: true, username: true, email: true,avatarUrl:true },
})
if (!user) return res.status(404).json({ error: "Kullanıcı bulunamadı" })

View File

@ -1,6 +1,6 @@
const express = require("express")
const router = express.Router()
const { getAllDeals, getDealById, createDeal } = require("../../services/deal/dealService")
const { getAllDeals, getDealById, createDeal,searchDeals } = require("../../services/deal/dealService")
const authMiddleware = require("../../middleware/authMiddleware")
router.get("/", async (req, res) => {
@ -15,6 +15,25 @@ router.get("/", async (req, res) => {
}
})
router.get("/search", async (req, res) => {
try {
const query = req.query.q || ""
const page = Number(req.query.page) || 1
const limit = 10
if (!query.trim()) {
return res.json({ results: [], total: 0, totalPages: 0, page })
}
const data = await searchDeals(query, page, limit)
res.json(data)
} catch (e) {
console.error(e)
res.status(500).json({ error: "Sunucu hatası" })
}
})
router.get("/:id", async (req, res) => {
try {
const deal = await getDealById(req.params.id)

63
routes/user/userRoutes.js Normal file
View File

@ -0,0 +1,63 @@
// routes/profileRoutes.js
const express = require("express")
const { PrismaClient } = require("@prisma/client")
const prisma = new PrismaClient()
const router = express.Router()
// Belirli bir kullanıcının profil detayları
router.get("/:userName", async (req, res) => {
console.log("İstek geldi:", req.params.userName)
try {
const username = req.params.userName
const user = await prisma.user.findUnique({
where: { username: username },
select: {
id: true,
username: true,
avatarUrl: true,
createdAt: true,
},
})
if (!user) return res.status(404).json({ message: "Kullanıcı bulunamadı." })
// Kullanıcının paylaştığı fırsatlar
const deals = await prisma.deal.findMany({
where: { userId: user.id },
orderBy: { createdAt: "desc" },
select: {
id: true,
title: true,
price: true,
createdAt: true,
score: true,
images: {
orderBy: { order: "asc" }, // küçük order en önde
take: 1, // sadece ilk görsel
select: { imageUrl: true },
},
},
})
// Kullanıcının yaptığı yorumlar
const comments = await prisma.comment.findMany({
where: { userId:user.id },
orderBy: { createdAt: "desc" },
select: {
id: true,
text: true,
dealId: true,
createdAt: true,
deal: { select: { title: true } },
},
})
res.json({ user, deals, comments })
} catch (err) {
console.error(err)
res.status(500).json({ message: "Profil bilgileri alınamadı.", error: err.message })
}
})
module.exports = router

View File

@ -1,22 +1,27 @@
const express = require("express");
const cors = require("cors");
require("dotenv").config();
const express = require("express")
const cors = require("cors")
require("dotenv").config()
const userRoutes = require("./routes/userRoutes");
const dealRoutes = require("./routes/deal/dealRoutes");
const authRoutes = require("./routes/authRoutes");
const dealVoteRoutes = require("./routes/deal/voteRoutes");
const userRoutesneedRefactor = require("./routes/userRoutes")
const dealRoutes = require("./routes/deal/dealRoutes")
const authRoutes = require("./routes/authRoutes")
const dealVoteRoutes = require("./routes/deal/voteRoutes")
const commentRoutes = require("./routes/deal/commentRoutes")
const accountSettingsRoutes = require("./routes/account/accountSettingsRoutes")
const userRoutes = require("./routes/user/userRoutes")
const app = express();
app.use(cors());
app.use(express.json());
const app = express()
app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use("/api/users", userRoutes);
app.use("/api/deals", dealRoutes);
app.use("/api/auth", authRoutes);
app.use("/api/deal-votes", dealVoteRoutes);
app.use("/api/users", userRoutesneedRefactor)
app.use("/api/deals", dealRoutes)
app.use("/api/auth", authRoutes)
app.use("/api/deal-votes", dealVoteRoutes)
app.use("/api/comments", commentRoutes)
app.use("/api/account", accountSettingsRoutes)
app.use("/api/user", userRoutes)
app.listen(3000, () => console.log("Server running on http://localhost:3000"));
app.listen(3000, () => console.log("Server running on http://localhost:3000"))

View File

@ -1,6 +1,5 @@
// services/deal/commentService.js
const { PrismaClient } = require("@prisma/client")
const prisma = new PrismaClient()
const dealDB = require("../../db/deal.db")
const commentDB = require("../../db/comment.db")
function assertPositiveInt(v, name = "id") {
const n = Number(v)
@ -11,56 +10,51 @@ function assertPositiveInt(v, name = "id") {
async function getCommentsByDealId(dealId) {
const id = assertPositiveInt(dealId, "dealId")
// Deal mevcut mu kontrol et
const deal = await prisma.deal.findUnique({ where: { id } })
const deal = await dealDB.findDeal({ id })
if (!deal) throw new Error("Deal bulunamadı.")
return prisma.comment.findMany({
where: { dealId: id },
include: { user: { select: { username: true } } },
orderBy: { createdAt: "desc" },
})
const include = { user: { select: { username: true, avatarUrl: true } } }
return commentDB.findComments({ dealId: id }, { include })
}
async function createComment({ dealId, userId, text }) {
// Basit doğrulamalar
const dId = assertPositiveInt(dealId, "dealId")
const uId = assertPositiveInt(userId, "userId")
if (!text || typeof text !== "string" || !text.trim())
throw new Error("Yorum boş olamaz.")
// Deal var mı kontrol et
const deal = await prisma.deal.findUnique({ where: { id: dId } })
const deal = await dealDB.findDeal({ id: dId })
if (!deal) throw new Error("Deal bulunamadı.")
// (Opsiyonel) Kullanıcı var mı kontrolü (ek güvenlik)
const user = await prisma.user.findUnique({ where: { id: uId } })
if (!user) throw new Error("Kullanıcı bulunamadı.")
const include = { user: { select: { username: true, avatarUrl: true } } }
const data = {
text: text.trim(),
userId: uId,
dealId: dId,
}
const comment = await prisma.comment.create({
data: {
text: text.trim(),
userId: uId,
dealId: dId,
},
include: {
user: { select: { username: true } },
},
})
return comment
return commentDB.createComment(data, { include })
}
async function deleteComment(commentId, userId) {
const cId = assertPositiveInt(commentId, "commentId")
const uId = assertPositiveInt(userId, "userId")
const comment = await prisma.comment.findUnique({ where: { id: cId } })
if (!comment) throw new Error("Yorum bulunamadı.")
if (comment.userId !== uId) throw new Error("Bu yorumu silme yetkin yok.")
const comments = await commentDB.findComments(
{ id: cId },
{ select: { userId: true } }
)
await prisma.comment.delete({ where: { id: cId } })
if (!comments || comments.length === 0) throw new Error("Yorum bulunamadı.")
if (comments[0].userId !== uId) throw new Error("Bu yorumu silme yetkin yok.")
await commentDB.deleteComment({ id: cId })
return { message: "Yorum silindi." }
}
module.exports = { getCommentsByDealId, createComment, deleteComment }
module.exports = {
getCommentsByDealId,
createComment,
deleteComment,
}

View File

@ -1,18 +1,57 @@
const { PrismaClient } = require("@prisma/client")
const prisma = new PrismaClient()
const dealDB = require("../../db/deal.db")
async function searchDeals(query, page = 1, limit = 10) {
const skip = (page - 1) * limit
const where = {
OR: [
{ title: { contains: query, mode: "insensitive" } },
{ description: { contains: query, mode: "insensitive" } },
],
}
const [deals, total] = await Promise.all([
dealDB.findDeals(where, {
skip,
take: limit,
orderBy: { createdAt: "desc" },
include: {
user: { select: { username: true } },
images: {
orderBy: { order: "asc" },
take: 1,
select: { imageUrl: true },
},
},
}),
dealDB.countDeals(where),
])
return {
page,
total,
totalPages: Math.ceil(total / limit),
results: deals,
}
}
async function getAllDeals(page = 1, limit = 10) {
const skip = (page - 1) * limit
const [deals, total] = await Promise.all([
prisma.deal.findMany({
dealDB.findDeals({}, {
skip,
take: limit,
include: { user: { select: { username: true } } },
orderBy: { createdAt: "desc" },
include: {
user: { select: { username: true } },
images: {
orderBy: { order: "asc" },
take: 1,
select: { imageUrl: true },
},
},
}),
prisma.deal.count(),
dealDB.countDeals(),
])
return {
@ -24,68 +63,71 @@ async function getAllDeals(page = 1, limit = 10) {
}
async function getDealById(id) {
return prisma.deal.findUnique({
where: { id: Number(id) },
include: { user: { select: { username: true } } },
})
return dealDB.findDeal(
{ id: Number(id) },
{
include: {
user: { select: { username: true } },
images: {
orderBy: { order: "asc" },
select: { id: true, imageUrl: true, order: true },
},
},
}
)
}
async function createDeal(data, userId) {
return prisma.deal.create({
data: {
title: data.title,
description: data.description,
url: data.url,
imageUrl: data.imageUrl,
price: data.price,
user: { connect: { id: userId } }, // JWTden gelen userId burada bağlanır
},
})
const payload = {
title: data.title,
description: data.description,
url: data.url,
price: data.price,
user: { connect: { id: userId } },
images: data.images?.length
? {
create: data.images.map((imgUrl, index) => ({
imageUrl: imgUrl,
order: index,
})),
}
: undefined,
}
return dealDB.createDeal(payload, { include: { images: true } })
}
async function voteDeal(dealId, userId, voteType) {
if (!dealId || !userId || !voteType) throw new Error("Eksik veri")
const existingVote = await prisma.dealVote.findFirst({
where: { dealId, userId },
})
const existingVote = await dealDB.findVotes({ dealId, userId })
const vote = existingVote[0]
if (existingVote) {
await prisma.dealVote.update({
where: { id: existingVote.id },
data: { voteType },
})
if (vote) {
await dealDB.updateVote({ id: vote.id }, { voteType })
} else {
await prisma.dealVote.create({
data: { dealId, userId, voteType },
})
await dealDB.createVote({ dealId, userId, voteType })
}
const upvotes = await prisma.dealVote.count({
where: { dealId, voteType: "UP" },
})
const downvotes = await prisma.dealVote.count({
where: { dealId, voteType: "DOWN" },
})
const upvotes = await dealDB.countVotes({ dealId, voteType: "UP" })
const downvotes = await dealDB.countVotes({ dealId, voteType: "DOWN" })
const score = upvotes - downvotes
await prisma.deal.update({
where: { id: dealId },
data: { score },
})
await dealDB.updateDeal({ id: dealId }, { score })
return score
}
async function getVotes(dealId) {
const upvotes = await prisma.dealVote.count({
where: { dealId: Number(dealId), voteType: "UP" },
})
const downvotes = await prisma.dealVote.count({
where: { dealId: Number(dealId), voteType: "DOWN" },
})
const upvotes = await dealDB.countVotes({ dealId: Number(dealId), voteType: "UP" })
const downvotes = await dealDB.countVotes({ dealId: Number(dealId), voteType: "DOWN" })
return { upvotes, downvotes, score: upvotes - downvotes }
}
module.exports = { getAllDeals, getDealById, createDeal, voteDeal, getVotes }
module.exports = {
getAllDeals,
getDealById,
createDeal,
voteDeal,
getVotes,
searchDeals,
}

View File

@ -0,0 +1,33 @@
const userDb = require("../../db/user.db")
function assertPositiveInt(v, name = "id") {
const n = Number(v)
if (!Number.isInteger(n) || n <= 0) throw new Error(`Geçersiz ${name}.`)
return n
}
async function updateAvatarUrl(userId, url) {
const id = assertPositiveInt(userId, "userId")
if (!url || typeof url !== "string" || !url.trim())
throw new Error("Geçersiz URL.")
const select = { id: true, username: true, avatarUrl: true }
return userDb.updateUser({ id }, { avatarUrl: url.trim() }, { select })
}
async function getUserProfile(userId) {
const id = assertPositiveInt(userId, "userId")
const select = {
id: true,
username: true,
email: true,
avatarUrl: true,
createdAt: true,
}
return userDb.findUser({ id }, { select })
}
module.exports = {
updateAvatarUrl,
getUserProfile,
}

View File

@ -0,0 +1,18 @@
const { createClient } = require("@supabase/supabase-js")
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
async function uploadProfileImage(userId, file) {
const path = `avatars/${userId}_${Date.now()}.jpg`
const { data, error } = await supabase.storage
.from("deal")
.upload(path, file.data, {
contentType: "image/jpeg",
upsert: true,
})
if (error) throw new Error(error.message)
const { data: publicUrl } = supabase.storage.from("avatars").getPublicUrl(path)
return publicUrl.publicUrl
}
module.exports = { uploadProfileImage }

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

8
utils/validateImage.js Normal file
View File

@ -0,0 +1,8 @@
function validateImage(file) {
if (!file) throw new Error("No file uploaded")
if (!["image/jpeg", "image/jpg"].includes(file.mimetype))
throw new Error("Only JPEG allowed")
if (file.size > 2 * 1024 * 1024)
throw new Error("File too large")
}
module.exports = { validateImage }