feat(backend): finalize pets logic, follow system, notifications and feed hardening
This commit is contained in:
1 parent
27174bef18
commit
7759bd0a05
5 files changed
+217
-71
No files matched your search
+19
@@ -0,0 +1,19 @@
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
|
||||
# Environment Dateien (Geheimnisse!)
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Build-Ausgaben
|
||||
dist/
|
||||
build/
|
||||
.next/
|
||||
|
||||
# Betriebssystem
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Docker
|
||||
docker-data/
|
||||
@@ -19,6 +19,8 @@ model User {
|
||||
followers Follow[] @relation("Following")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
notifications Notification[]
|
||||
}
|
||||
|
||||
|
||||
@@ -63,3 +65,14 @@ model Follow {
|
||||
|
||||
@@unique([followerId, followingId])
|
||||
}
|
||||
model Notification {
|
||||
id Int @id @default(autoincrement())
|
||||
type String // 'FOLLOW' | 'BIRTHDAY'
|
||||
payload Json
|
||||
read Boolean @default(false)
|
||||
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Controller, Post, Param, Req } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Delete,
|
||||
Param,
|
||||
Req,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
@Controller('follows')
|
||||
@@ -6,11 +13,59 @@ export class FollowsController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
@Post(':userId')
|
||||
follow(@Req() req, @Param('userId') userId: string) {
|
||||
return this.prisma.follow.create({
|
||||
data: {
|
||||
followerId: req.user.id,
|
||||
followingId: Number(userId),
|
||||
async follow(@Req() req, @Param('userId') userId: string) {
|
||||
const followerId = req.user.id;
|
||||
const followingId = Number(userId);
|
||||
|
||||
// ❌ Selbst-Follow verhindern
|
||||
if (followerId === followingId) {
|
||||
throw new BadRequestException({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Du kannst dir nicht selbst folgen.',
|
||||
field: 'userId',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// 1️⃣ Follow erstellen
|
||||
const follow = await this.prisma.follow.create({
|
||||
data: {
|
||||
followerId,
|
||||
followingId,
|
||||
},
|
||||
});
|
||||
|
||||
// 2️⃣ Notification für den Gefolgten erzeugen
|
||||
await this.prisma.notification.create({
|
||||
data: {
|
||||
type: 'FOLLOW',
|
||||
userId: followingId,
|
||||
payload: {
|
||||
followerId,
|
||||
followerName: req.user.name,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return follow;
|
||||
} catch (err) {
|
||||
// ❌ Bereits gefolgt (Unique-Constraint)
|
||||
throw new BadRequestException({
|
||||
error: 'ALREADY_FOLLOWING',
|
||||
message: 'Du folgst diesem Nutzer bereits.',
|
||||
field: 'userId',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':userId')
|
||||
async unfollow(@Req() req, @Param('userId') userId: string) {
|
||||
return this.prisma.follow.delete({
|
||||
where: {
|
||||
followerId_followingId: {
|
||||
followerId: req.user.id,
|
||||
followingId: Number(userId),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Controller, Get, Req } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
@Controller('notifications')
|
||||
export class NotificationsController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
@Get()
|
||||
getMyNotifications(@Req() req) {
|
||||
return this.prisma.notification.findMany({
|
||||
where: { userId: req.user.id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 20,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,76 +1,119 @@
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreatePetDto } from './dto/create-pet.dto';
|
||||
import { UpdatePetDto } from './dto/update-pet.dto';
|
||||
|
||||
async createPet(userId: number, dto: CreatePetDto) {
|
||||
// ❌ Beides gleichzeitig verboten
|
||||
if (dto.birthDate && dto.ageYears !== undefined) {
|
||||
throw new BadRequestException({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Entweder Geburtsdatum ODER Alter angeben.',
|
||||
field: 'birthDate',
|
||||
});
|
||||
}
|
||||
@Injectable()
|
||||
export class PetsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
if (!dto.birthDate && dto.ageYears === undefined) {
|
||||
throw new BadRequestException({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Geburtsdatum oder Alter muss angegeben werden.',
|
||||
field: 'birthDate',
|
||||
});
|
||||
}
|
||||
// -------------------------------
|
||||
// CREATE PET
|
||||
// -------------------------------
|
||||
async createPet(userId: number, dto: CreatePetDto) {
|
||||
// ❌ Beides gleichzeitig verboten
|
||||
if (dto.birthDate && dto.ageYears !== undefined) {
|
||||
throw new BadRequestException({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Entweder Geburtsdatum ODER Alter angeben, nicht beides.',
|
||||
field: 'birthDate',
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ BirthDate festlegen
|
||||
let birthDate: Date | null = null;
|
||||
// ❌ Nichts angegeben verboten
|
||||
if (!dto.birthDate && dto.ageYears === undefined) {
|
||||
throw new BadRequestException({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Geburtsdatum oder Alter muss angegeben werden.',
|
||||
field: 'birthDate',
|
||||
});
|
||||
}
|
||||
|
||||
if (dto.birthDate) {
|
||||
birthDate = new Date(dto.birthDate);
|
||||
}
|
||||
// ✅ BirthDate ableiten (Single Source of Truth)
|
||||
let birthDate: Date | null = null;
|
||||
|
||||
if (dto.ageYears !== undefined) {
|
||||
const today = new Date();
|
||||
birthDate = new Date(
|
||||
today.getFullYear() - dto.ageYears,
|
||||
today.getMonth(),
|
||||
today.getDate()
|
||||
);
|
||||
}
|
||||
if (dto.birthDate) {
|
||||
birthDate = new Date(dto.birthDate);
|
||||
}
|
||||
|
||||
return this.prisma.pet.create({
|
||||
data: {
|
||||
name: dto.name,
|
||||
type: dto.type,
|
||||
breed: dto.breed,
|
||||
birthDate,
|
||||
gender: dto.gender,
|
||||
bio: dto.bio,
|
||||
avatarUrl: dto.avatarUrl,
|
||||
visibility: dto.visibility ?? 'public',
|
||||
ownerId: userId,
|
||||
},
|
||||
});
|
||||
if (dto.ageYears !== undefined) {
|
||||
const today = new Date();
|
||||
birthDate = new Date(
|
||||
today.getFullYear() - dto.ageYears,
|
||||
today.getMonth(),
|
||||
today.getDate(),
|
||||
);
|
||||
}
|
||||
|
||||
async updatePet(
|
||||
petId: number,
|
||||
userId: number,
|
||||
dto: UpdatePetDto,
|
||||
) {
|
||||
const pet = await this.prisma.pet.findUnique({
|
||||
where: { id: petId },
|
||||
});
|
||||
// ❌ Zukunftsdatum verhindern
|
||||
if (birthDate && birthDate > new Date()) {
|
||||
throw new BadRequestException({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Geburtsdatum darf nicht in der Zukunft liegen.',
|
||||
field: 'birthDate',
|
||||
});
|
||||
}
|
||||
|
||||
if (!pet || pet.ownerId !== userId) {
|
||||
throw new NotFoundException({
|
||||
error: 'NOT_FOUND',
|
||||
message: 'Tier nicht gefunden.',
|
||||
return this.prisma.pet.create({
|
||||
data: {
|
||||
name: dto.name,
|
||||
type: dto.type,
|
||||
breed: dto.breed,
|
||||
birthDate, // ⭐ WICHTIG: berechnetes Datum
|
||||
gender: dto.gender,
|
||||
bio: dto.bio,
|
||||
avatarUrl: dto.avatarUrl,
|
||||
visibility: dto.visibility ?? 'public',
|
||||
ownerId: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return this.prisma.pet.update({
|
||||
where: { id: petId },
|
||||
data: {
|
||||
birthDate: dto.birthDate ? new Date(dto.birthDate) : pet.birthDate,
|
||||
bio: dto.bio,
|
||||
avatarUrl: dto.avatarUrl,
|
||||
visibility: dto.visibility,
|
||||
},
|
||||
});
|
||||
}
|
||||
// -------------------------------
|
||||
// UPDATE PET
|
||||
// -------------------------------
|
||||
async updatePet(
|
||||
petId: number,
|
||||
userId: number,
|
||||
dto: UpdatePetDto,
|
||||
) {
|
||||
const pet = await this.prisma.pet.findUnique({
|
||||
where: { id: petId },
|
||||
});
|
||||
|
||||
if (!pet || pet.ownerId !== userId) {
|
||||
throw new NotFoundException({
|
||||
error: 'NOT_FOUND',
|
||||
message: 'Pet nicht gefunden.',
|
||||
});
|
||||
}
|
||||
|
||||
let birthDate = pet.birthDate;
|
||||
|
||||
if (dto.birthDate) {
|
||||
birthDate = new Date(dto.birthDate);
|
||||
|
||||
if (birthDate > new Date()) {
|
||||
throw new BadRequestException({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Geburtsdatum darf nicht in der Zukunft liegen.',
|
||||
field: 'birthDate',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this.prisma.pet.update({
|
||||
where: { id: petId },
|
||||
data: {
|
||||
birthDate,
|
||||
bio: dto.bio,
|
||||
avatarUrl: dto.avatarUrl,
|
||||
visibility: dto.visibility,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user