base commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "phone" TEXT;
|
||||
@@ -0,0 +1,22 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "ShippingAddress" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"label" TEXT,
|
||||
"recipientName" TEXT NOT NULL,
|
||||
"recipientPhone" TEXT NOT NULL,
|
||||
"addressLine" TEXT NOT NULL,
|
||||
"comment" TEXT,
|
||||
"lat" REAL NOT NULL,
|
||||
"lng" REAL NOT NULL,
|
||||
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
CONSTRAINT "ShippingAddress_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ShippingAddress_userId_isDefault_idx" ON "ShippingAddress"("userId", "isDefault");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ShippingAddress_userId_updatedAt_idx" ON "ShippingAddress"("userId", "updatedAt");
|
||||
@@ -56,11 +56,33 @@ model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String?
|
||||
phone String?
|
||||
passwordHash String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
codes AuthCode[]
|
||||
addresses ShippingAddress[]
|
||||
}
|
||||
|
||||
model ShippingAddress {
|
||||
id String @id @default(cuid())
|
||||
label String?
|
||||
recipientName String
|
||||
recipientPhone String
|
||||
addressLine String
|
||||
comment String?
|
||||
lat Float
|
||||
lng Float
|
||||
isDefault Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
|
||||
@@index([userId, isDefault])
|
||||
@@index([userId, updatedAt])
|
||||
}
|
||||
|
||||
model AuthCode {
|
||||
|
||||
+209
-8
@@ -27,7 +27,7 @@ export async function registerAuthRoutes(fastify) {
|
||||
})
|
||||
|
||||
const token = fastify.jwt.sign({ sub: user.id, email: user.email })
|
||||
return { token, user: { id: user.id, email: user.email, name: user.name } }
|
||||
return { token, user: { id: user.id, email: user.email, name: user.name, phone: user.phone } }
|
||||
})
|
||||
|
||||
fastify.post('/api/auth/register', async (request, reply) => {
|
||||
@@ -42,7 +42,7 @@ export async function registerAuthRoutes(fastify) {
|
||||
const passwordHash = await hashPassword(password)
|
||||
const user = await prisma.user.create({ data: { email, passwordHash } })
|
||||
const token = fastify.jwt.sign({ sub: user.id, email: user.email })
|
||||
return reply.code(201).send({ token, user: { id: user.id, email: user.email, name: user.name } })
|
||||
return reply.code(201).send({ token, user: { id: user.id, email: user.email, name: user.name, phone: user.phone } })
|
||||
})
|
||||
|
||||
fastify.post('/api/auth/login', async (request, reply) => {
|
||||
@@ -58,7 +58,7 @@ export async function registerAuthRoutes(fastify) {
|
||||
if (!ok) return reply.code(401).send({ error: 'Неверные данные' })
|
||||
|
||||
const token = fastify.jwt.sign({ sub: user.id, email: user.email })
|
||||
return { token, user: { id: user.id, email: user.email, name: user.name } }
|
||||
return { token, user: { id: user.id, email: user.email, name: user.name, phone: user.phone } }
|
||||
})
|
||||
|
||||
fastify.get(
|
||||
@@ -68,7 +68,7 @@ export async function registerAuthRoutes(fastify) {
|
||||
const userId = request.user.sub
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } })
|
||||
if (!user) return { user: null }
|
||||
return { user: { id: user.id, email: user.email, name: user.name } }
|
||||
return { user: { id: user.id, email: user.email, name: user.name, phone: user.phone } }
|
||||
},
|
||||
)
|
||||
|
||||
@@ -108,7 +108,7 @@ export async function registerAuthRoutes(fastify) {
|
||||
where: { id: userId },
|
||||
data: { email: newEmail },
|
||||
})
|
||||
return { user: { id: user.id, email: user.email, name: user.name } }
|
||||
return { user: { id: user.id, email: user.email, name: user.name, phone: user.phone } }
|
||||
},
|
||||
)
|
||||
|
||||
@@ -133,7 +133,7 @@ export async function registerAuthRoutes(fastify) {
|
||||
|
||||
const passwordHash = await hashPassword(newPassword)
|
||||
const updated = await prisma.user.update({ where: { id: userId }, data: { passwordHash } })
|
||||
return { user: { id: updated.id, email: updated.email, name: updated.name } }
|
||||
return { user: { id: updated.id, email: updated.email, name: updated.name, phone: updated.phone } }
|
||||
},
|
||||
)
|
||||
|
||||
@@ -144,14 +144,215 @@ export async function registerAuthRoutes(fastify) {
|
||||
const userId = request.user.sub
|
||||
const nameRaw = request.body?.name
|
||||
const name = nameRaw === null || nameRaw === undefined ? null : String(nameRaw).trim()
|
||||
const phoneRaw = request.body?.phone
|
||||
const phone = phoneRaw === null || phoneRaw === undefined ? null : String(phoneRaw).trim()
|
||||
|
||||
if (name !== null && name.length > 40) return reply.code(400).send({ error: 'Имя/ник максимум 40 символов' })
|
||||
if (phone !== null) {
|
||||
const compact = phone.replace(/[\s()-]/g, '')
|
||||
if (compact.length > 20) return reply.code(400).send({ error: 'Телефон слишком длинный' })
|
||||
if (compact.length && !/^\+?\d{7,20}$/.test(compact)) {
|
||||
return reply.code(400).send({ error: 'Некорректный телефон' })
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { name: name && name.length ? name : null },
|
||||
data: { name: name && name.length ? name : null, phone: phone && phone.length ? phone : null },
|
||||
})
|
||||
return { user: { id: updated.id, email: updated.email, name: updated.name } }
|
||||
return { user: { id: updated.id, email: updated.email, name: updated.name, phone: updated.phone } }
|
||||
},
|
||||
)
|
||||
|
||||
// ---- Адреса доставки ----
|
||||
|
||||
function normalizePhoneLite(input) {
|
||||
const s = String(input || '').trim()
|
||||
if (!s) return ''
|
||||
return s.replace(/[\s()-]/g, '')
|
||||
}
|
||||
|
||||
function validateAddressPayload(body, reply) {
|
||||
const labelRaw = body?.label
|
||||
const label = labelRaw === null || labelRaw === undefined ? null : String(labelRaw).trim()
|
||||
if (label !== null && label.length > 40) return reply.code(400).send({ error: 'Метка адреса максимум 40 символов' })
|
||||
|
||||
const recipientName = String(body?.recipientName || '').trim()
|
||||
if (!recipientName) return reply.code(400).send({ error: 'Укажите ФИО получателя' })
|
||||
if (recipientName.length > 80) return reply.code(400).send({ error: 'ФИО получателя максимум 80 символов' })
|
||||
|
||||
const recipientPhone = normalizePhoneLite(body?.recipientPhone)
|
||||
if (!recipientPhone) return reply.code(400).send({ error: 'Укажите телефон получателя' })
|
||||
if (!/^\+?\d{7,20}$/.test(recipientPhone)) return reply.code(400).send({ error: 'Некорректный телефон получателя' })
|
||||
|
||||
const addressLine = String(body?.addressLine || '').trim()
|
||||
if (!addressLine) return reply.code(400).send({ error: 'Укажите адрес' })
|
||||
if (addressLine.length > 200) return reply.code(400).send({ error: 'Адрес максимум 200 символов' })
|
||||
|
||||
const commentRaw = body?.comment
|
||||
const comment = commentRaw === null || commentRaw === undefined ? null : String(commentRaw).trim()
|
||||
if (comment !== null && comment.length > 200) return reply.code(400).send({ error: 'Комментарий максимум 200 символов' })
|
||||
|
||||
const lat = Number(body?.lat)
|
||||
const lng = Number(body?.lng)
|
||||
if (!Number.isFinite(lat) || lat < -90 || lat > 90) return reply.code(400).send({ error: 'Некорректная широта' })
|
||||
if (!Number.isFinite(lng) || lng < -180 || lng > 180) return reply.code(400).send({ error: 'Некорректная долгота' })
|
||||
|
||||
return {
|
||||
label,
|
||||
recipientName,
|
||||
recipientPhone,
|
||||
addressLine,
|
||||
comment,
|
||||
lat,
|
||||
lng,
|
||||
}
|
||||
}
|
||||
|
||||
fastify.get(
|
||||
'/api/me/addresses',
|
||||
{ preHandler: [fastify.authenticate] },
|
||||
async (request) => {
|
||||
const userId = request.user.sub
|
||||
const items = await prisma.shippingAddress.findMany({
|
||||
where: { userId },
|
||||
orderBy: [{ isDefault: 'desc' }, { updatedAt: 'desc' }],
|
||||
})
|
||||
return { items }
|
||||
},
|
||||
)
|
||||
|
||||
fastify.post(
|
||||
'/api/me/addresses',
|
||||
{ preHandler: [fastify.authenticate] },
|
||||
async (request, reply) => {
|
||||
const userId = request.user.sub
|
||||
const validated = validateAddressPayload(request.body, reply)
|
||||
if (!validated) return
|
||||
|
||||
const isDefault = Boolean(request.body?.isDefault)
|
||||
const created = await prisma.$transaction(async (tx) => {
|
||||
if (isDefault) {
|
||||
await tx.shippingAddress.updateMany({ where: { userId, isDefault: true }, data: { isDefault: false } })
|
||||
}
|
||||
return tx.shippingAddress.create({
|
||||
data: {
|
||||
userId,
|
||||
...validated,
|
||||
isDefault,
|
||||
},
|
||||
})
|
||||
})
|
||||
return reply.code(201).send({ item: created })
|
||||
},
|
||||
)
|
||||
|
||||
fastify.patch(
|
||||
'/api/me/addresses/:id',
|
||||
{ preHandler: [fastify.authenticate] },
|
||||
async (request, reply) => {
|
||||
const userId = request.user.sub
|
||||
const { id } = request.params
|
||||
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
||||
if (!existing) return reply.code(404).send({ error: 'Адрес не найден' })
|
||||
|
||||
const body = request.body ?? {}
|
||||
const data = {}
|
||||
|
||||
if (body.label !== undefined) {
|
||||
const labelRaw = body.label
|
||||
const label = labelRaw === null || labelRaw === undefined ? null : String(labelRaw).trim()
|
||||
if (label !== null && label.length > 40) return reply.code(400).send({ error: 'Метка адреса максимум 40 символов' })
|
||||
data.label = label && label.length ? label : null
|
||||
}
|
||||
|
||||
if (body.recipientName !== undefined) {
|
||||
const v = String(body.recipientName || '').trim()
|
||||
if (!v) return reply.code(400).send({ error: 'Укажите ФИО получателя' })
|
||||
if (v.length > 80) return reply.code(400).send({ error: 'ФИО получателя максимум 80 символов' })
|
||||
data.recipientName = v
|
||||
}
|
||||
|
||||
if (body.recipientPhone !== undefined) {
|
||||
const v = normalizePhoneLite(body.recipientPhone)
|
||||
if (!v) return reply.code(400).send({ error: 'Укажите телефон получателя' })
|
||||
if (!/^\+?\d{7,20}$/.test(v)) return reply.code(400).send({ error: 'Некорректный телефон получателя' })
|
||||
data.recipientPhone = v
|
||||
}
|
||||
|
||||
if (body.addressLine !== undefined) {
|
||||
const v = String(body.addressLine || '').trim()
|
||||
if (!v) return reply.code(400).send({ error: 'Укажите адрес' })
|
||||
if (v.length > 200) return reply.code(400).send({ error: 'Адрес максимум 200 символов' })
|
||||
data.addressLine = v
|
||||
}
|
||||
|
||||
if (body.comment !== undefined) {
|
||||
const commentRaw = body.comment
|
||||
const comment = commentRaw === null || commentRaw === undefined ? null : String(commentRaw).trim()
|
||||
if (comment !== null && comment.length > 200) return reply.code(400).send({ error: 'Комментарий максимум 200 символов' })
|
||||
data.comment = comment && comment.length ? comment : null
|
||||
}
|
||||
|
||||
if (body.lat !== undefined) {
|
||||
const lat = Number(body.lat)
|
||||
if (!Number.isFinite(lat) || lat < -90 || lat > 90) return reply.code(400).send({ error: 'Некорректная широта' })
|
||||
data.lat = lat
|
||||
}
|
||||
|
||||
if (body.lng !== undefined) {
|
||||
const lng = Number(body.lng)
|
||||
if (!Number.isFinite(lng) || lng < -180 || lng > 180) return reply.code(400).send({ error: 'Некорректная долгота' })
|
||||
data.lng = lng
|
||||
}
|
||||
|
||||
const setDefault = body.isDefault === true
|
||||
const updated = await prisma.$transaction(async (tx) => {
|
||||
if (setDefault) {
|
||||
await tx.shippingAddress.updateMany({ where: { userId, isDefault: true }, data: { isDefault: false } })
|
||||
}
|
||||
return tx.shippingAddress.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...data,
|
||||
...(setDefault ? { isDefault: true } : {}),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return { item: updated }
|
||||
},
|
||||
)
|
||||
|
||||
fastify.delete(
|
||||
'/api/me/addresses/:id',
|
||||
{ preHandler: [fastify.authenticate] },
|
||||
async (request, reply) => {
|
||||
const userId = request.user.sub
|
||||
const { id } = request.params
|
||||
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
||||
if (!existing) return reply.code(404).send({ error: 'Адрес не найден' })
|
||||
|
||||
await prisma.shippingAddress.delete({ where: { id } })
|
||||
return reply.code(204).send()
|
||||
},
|
||||
)
|
||||
|
||||
fastify.post(
|
||||
'/api/me/addresses/:id/default',
|
||||
{ preHandler: [fastify.authenticate] },
|
||||
async (request, reply) => {
|
||||
const userId = request.user.sub
|
||||
const { id } = request.params
|
||||
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
||||
if (!existing) return reply.code(404).send({ error: 'Адрес не найден' })
|
||||
|
||||
const updated = await prisma.$transaction(async (tx) => {
|
||||
await tx.shippingAddress.updateMany({ where: { userId, isDefault: true }, data: { isDefault: false } })
|
||||
return tx.shippingAddress.update({ where: { id }, data: { isDefault: true } })
|
||||
})
|
||||
|
||||
return { item: updated }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user