10 Commits
Author SHA1 Message Date
admin baadfe618c feat(feed): handle followers-only posts with locked UI (issue #246) 2026-01-02 15:35:52 +01:00
admin cc5db2b37e feat: enable CORS and add frontend API test page 2026-01-02 14:55:56 +01:00
admin 06e9240909 feat(backend): feed, posts & pets APIs frontend-ready with visibility enforcement 2026-01-02 14:45:40 +01:00
admin e1e1b7ab21 Fundament: Zentrale Auth-Leitlinie & RequestUser vereinheitlicht (#241)
- Guard-first Auth-Architektur dokumentiert
- RequestUser als einzige User-Wahrheit etabliert
- Keine Auth-Logik in Services
- Public-Routen explizit via @Public()
- Vorbereitung für JWT ohne Service-Änderungen
2026-01-01 14:12:49 +01:00
admin 831e6b0cbd Phase 2.1: Feed angebunden & Visibility serverseitig enforced (#245 #246)
- Echter /feed Endpoint angebunden (Mock entfernt)
- Visibility-Regeln serverseitig umgesetzt (public / followers)
- RequestUser als zentraler Auth-Typ vereinheitlicht
- Visibility-Leak behoben
2026-01-01 14:08:14 +01:00
admin b492ecdaa3 feat(frontend): stabilize feed mock, align PostCard types and polish UX 2025-12-27 12:57:05 +01:00
admin 09599ff7e9 chore(frontend): before ux polish 2025-12-27 12:35:58 +01:00
admin 5b66e57d2e feat(backend): add /feed endpoint mirrored from /posts
- Introduced FeedModule with controller and service
- /feed mirrors /posts 1:1 (baseline, deterministic)
- No discovery or random logic yet
- Prisma + DI fully wired and stable
- Backend now frontend-ready for feed consumption
2025-12-27 10:46:05 +01:00
admin 703470e501 feat(backend): activate posts feed with prisma + strict DI 2025-12-27 10:42:10 +01:00
admin 1081fc8f7f feat(backend): activate posts feed with prisma + strict DI 2025-12-27 10:42:06 +01:00
31 changed files with 3703 additions and 174 deletions
+129
View File
@@ -0,0 +1,129 @@
# 🧊 Backend Freeze Frontend Ready
**Projekt:** OnlyPets
**Stand:** Backend eingefroren (Frontend-ready)
**Datum:** 27.12.2025
Ziel dieses Dokuments ist es, klar festzuhalten:
> Das Backend ist technisch und strukturell abgeschlossen,
> sodass die weitere Entwicklung primär im Frontend stattfinden kann.
Spezial-Features werden bewusst **getrennt** und **nachgelagert** umgesetzt.
---
## 🧱 1. Architektur & Infrastruktur
- NestJS stabil gebootstrapped
- Module sauber getrennt:
- Posts
- Feed
- Prisma
- Auth
- Dependency Injection stabil (keine offenen DI-Fehler)
- Prisma korrekt als Service eingebunden
- Strict TypeScript aktiv (keine `any`-Leaks)
- Ordnerstruktur final (kein späteres Refactoring notwendig)
**Status:** ✅ abgeschlossen
---
## 🔐 2. Auth-Grundlage (Phase 1)
- Globaler `DummyAuthGuard` aktiv
- `req.user` immer definiert (`null | FakeUser`)
- Rollenmodell vorbereitet (`USER | MOD | ADMIN`)
- `@Public()` Decorator vorhanden
- Guard-first-Architektur (keine Auth-Logik in Services)
> Echte Auth (JWT, Login) ist **bewusst nicht Teil** des Freeze-Zustands.
**Status:** ✅ abgeschlossen (Phase-1-Scope)
---
## 🗃️ 3. Datenmodell & Prisma
- Prisma Schema stabil
- Keine redundanten oder abgeleiteten Felder
- Alter wird **nicht gespeichert**
- `birthDate` ist Single Source of Truth
- Beziehungen konsistent:
- User ↔ Pet ↔ Post
- Seed reproduzierbar & idempotent
- DB-Zugriff ausschließlich über Services
**Status:** ✅ abgeschlossen
---
## 📰 4. Feed & Content (Frontend-relevant)
Aktive Endpoints:
- `GET /posts`
- `GET /feed`
Eigenschaften:
- `/feed` ist 1:1 aus `/posts` gespiegelt
- Deterministischer Feed
- Keine Random- oder Discovery-Logik
- Sortierung: `createdAt DESC`
- Inkludierte Relationen:
- `pet`
- `author`
Smoke-Tests erfolgreich (Invoke-RestMethod).
**Status:** ✅ abgeschlossen / frontend-ready
---
## 🩺 5. Betrieb & Stabilität
- `GET /health` Endpoint vorhanden
- Server startet ohne Errors
- Keine offenen TypeScript- oder DI-Probleme
- Lokales DEV-Setup reproduzierbar
**Status:** ✅ abgeschlossen
---
## 🧩 6. Bewusst ausgeklammerte Spezial-Features
Diese Themen sind **nicht Teil des Backend-Freeze** und werden separat umgesetzt:
- Discovery Feed (siehe Issue #243)
- Likes
- Kommentare
- Notifications
- Admin / Moderation
- Echte Auth (JWT, Login)
> Diese Features sind **entkoppelt geplant**,
> sodass sie nach Live-Frontend **ohne Rewrites** ergänzt werden können.
---
## 🧊 Freeze-Regel
Ab diesem Punkt gilt für das Backend:
- ❄️ Keine neuen Features
- ❄️ Nur Bugfixes
- ❄️ Keine API-Breaking-Changes
- ❄️ Fokus liegt auf Frontend-Entwicklung
---
## ✅ Fazit
Das Backend befindet sich im **frontend-ready Freeze-Zustand**.
- Die API ist stabil
- Die Architektur ist vorbereitet
- Erweiterungen sind planbar, aber nicht blockierend
➡️ **Empfohlener nächster Schritt:** Frontend entwickeln.
+26
View File
@@ -0,0 +1,26 @@
## Lokales Dev-Setup
Frontend:
- URL: http://localhost:3000
Backend:
- URL: http://localhost:3001
ENV:
- NEXT_PUBLIC_API_URL=http://localhost:3001
Hinweis:
Backend ist frontend-ready eingefroren.
Neue Features nur über separate Issues.
# 🛠 DEV SETUP OnlyPets
## Ports
- Frontend: http://localhost:3000
- Backend: http://localhost:3001
## Frontend (Next.js)
```bash
cd frontend
npm install
npm run dev
+542
View File
@@ -0,0 +1,542 @@
PROJECT: OnlyPets (ehemals OnlyCats)
STACK:
- Backend: NestJS + Prisma
- DB: PostgreSQL (Docker, DEV)
- Frontend: Next.js (separat, noch nicht Thema dieses Blocks)
CORE IDEA:
- Tiere stehen IMMER im Mittelpunkt
- Kein Post ohne Pet
- User sind sekundär, Pets sind primäre Entität
DATA PRINCIPLES:
- Single Source of Truth
- Pet.age wird NIE gespeichert
- Stattdessen: Pet.birthDate (DateTime)
- Alter & Geburtstag werden IMMER berechnet (Utils / Frontend)
PET RULES:
- birthDate optional, aber bevorzugt
- fallbackAge nur temporär (kein Persist)
- Pet Edit erlaubt birthDate-Korrektur
- type & breed sind immutable
- visibility: public | followers (serverseitig enforced)
BACKEND STATUS:
- Prisma Schema vorhanden
- Migration erfolgreich ausgeführt
- Tabellen existieren
- Follow-System implementiert
- Visibility-Logik vorbereitet
- Notifications vorbereitet (FOLLOW, BIRTHDAY später)
DATABASE (DEV):
- PostgreSQL läuft im Docker
- Eigener Port: 55432
- Credentials:
user: onlypets_user
pass: onlypets_pass
db: onlypets
- DATABASE_URL:
postgresql://onlypets_user:onlypets_pass@localhost:55432/onlypets
SEED STATUS:
- seed.ts vollständig funktionsfähig
- Seed OHNE Passwort (Auth kommt später)
- User werden per upsert erstellt (idempotent)
- Enthält:
- 3 Alpha-User
- Pets mit birthDate (DateTime via new Date())
- Posts
- Follow-Beziehungen
- Seed läuft nur NACH Migration
- Seed mehrfach ausführbar ohne Fehler
AUTH STATUS:
- Noch kein Passwortfeld im User-Model
- Auth bewusst verschoben
- Schema & Seed auth-ready gedacht (passwordHash später)
ISSUES:
- Issue #1: Seed / DB → erledigt
- Issue #242: Teilweise vorbereitet (Seed & Datenbasis), noch offen
LAST ACTIONS:
- Docker-Port-Konflikte gelöst
- ENV-Probleme identifiziert & behoben
- Migration → Seed Reihenfolge geklärt
- Seed finalisiert & stabil gemacht
NEXT INTENDED STEPS:
- Frontend anbinden & Feed testen
- Oder Auth vorbereiten (passwordHash + Migration)
- Oder Seed erweitern (Likes, Comments, Discovery)
-----------------------------------------------------
Gitea - Issue Liste zum abarbeiten !
Projekt: OnlyPets (Social Network für Haustiere)
Tech-Stack:
- Backend: NestJS + Prisma
- DB: PostgreSQL (lokal, aktuell instabil / Multi-Versionen 12,16,17)
- Frontend: Next.js (läuft getrennt unter C:/Projekte/OnlyPets/)
- Repo: Gitea (selfhosted), Issues aktiv genutzt
- OS: Windows (PowerShell)
Status Backend (wichtig):
- Prisma Schema vorhanden (User, Pet, Post, Follow etc.)
- birthDate wird als DateTime gespeichert (nicht Alter)
- Follow/Unfollow Controller existiert
- Feed + Discovery Feed Logik umgesetzt (Visibility public / followers)
- Fake-Auth / Dev-User Konzept beschlossen (kein echter Login yet)
- Seed läuft prinzipiell, aber:
- PostgreSQL Auth & Rollen waren fehlerhaft
- Mehrere PG-Versionen gleichzeitig aktiv
- Tabellen fehlten → Migration nötig
- Seed jetzt OHNE Passwort (Auth nur vorbereitet)
- Seed-Fehler zuletzt: Unique constraint (email) → Seed muss idempotent werden
Prisma / DB Probleme:
- DATABASE_URL mehrfach getestet (5432 / 55432)
- Auth-Fehler → User/Rolle existierte nicht
- Lösungspfad:
- Entweder PostgreSQL sauber neu (1 Version)
- ODER Übergang auf Docker-Postgres
- Migration (`prisma migrate deploy`) vor Seed ist Pflicht
Seed-Ziel:
- Dev-User (ohne Passwort)
- Pets mit birthDate als Date (ISO / new Date())
- Posts, Follows
- Kein Auth-Zwang
Auth-Strategie (entscheidend):
- Guard-first, KEINE Auth-Logik in Services
- Globaler Dummy Auth Guard
- @Public() Decorator für offene Endpunkte
- req.user immer definiert (null oder Fake-User)
- Rollen vorbereitet (USER / ADMIN / MOD)
Aktive Issues (Auszug, merken!):
- #242 Fake-User für Dev/Admin
- #241 Zentrale Auth-Regeln
- #240 Public Decorator
- #239 Globaler Auth-Guard (Dummy)
- #238 Request-User Typisierung
- Phase 1 Fundament ist aktuell aktiv
- Phase 2 Tier & Social danach
Arbeitsstil:
- Klare Pfade nennen (vollständig)
- Kurz & gebündelt, kein Datei-Hopping
- Backend zuerst stabil, Frontend später anbinden
---
# ✅ PROJEKT-BACKUP · STAND **26.12.2025 (TAGESABSCHLUSS)**
**Projekt:** OnlyPets (ehemals OnlyCats)
---
## STACK
* **Backend:** NestJS + Prisma
* **DB (DEV):** PostgreSQL (Docker)
* **Frontend:** Next.js (separat, nicht Teil dieses Blocks)
---
## CORE IDEA
* Tiere sind **primäre Entität**
* Kein Post ohne Pet
* User sind sekundär
---
## DATA PRINCIPLES
* Single Source of Truth
* `Pet.age` wird **niemals gespeichert**
* Stattdessen: `Pet.birthDate: DateTime`
* Alter & Geburtstag werden **immer berechnet**
* Keine redundanten Altersfelder
---
## PET RULES
* `birthDate` optional, aber bevorzugt
* `fallbackAge` nur temporär (nicht persistent)
* Pet-Edit erlaubt birthDate-Korrektur
* `type` & `breed` immutable
* `visibility`: `public | followers` (serverseitig enforced)
---
## DATABASE (DEV AKTUELL STABIL)
* PostgreSQL läuft **im Docker**
* Port: **55432**
* Credentials:
* user: `onlypets_user`
* pass: `onlypets_pass`
* db: `onlypets`
* `DATABASE_URL`:
```
postgresql://onlypets_user:onlypets_pass@localhost:55432/onlypets
```
---
## SEED STATUS
* `seed.ts` vollständig funktionsfähig
* Seed **ohne Passwort** (Auth kommt später)
* User via **upsert** (idempotent)
* Enthält:
* Alpha-User
* Pets mit `birthDate`
* Posts
* Follow-Beziehungen
* Reihenfolge fix:
* Migration → Seed
* Seed mehrfach ausführbar **ohne Fehler**
---
## AUTH STATUS **PHASE 1 ABGESCHLOSSEN (HEUTE)**
* ❌ Noch kein Passwortfeld im User-Model
* ❌ Kein echter Login (bewusst verschoben)
* ✅ Guard-first Architektur umgesetzt
* ✅ Globaler DummyAuthGuard aktiv
* ✅ `@Public()` Decorator implementiert
* ✅ `req.user` ist **immer definiert** (`null` oder Fake-User)
* ✅ Rollen vorbereitet (`USER | MOD | ADMIN`)
* ✅ Fake-User steuerbar über ENV
* ✅ Keine Auth-Logik in Services
---
## BACKEND STATUS **HEUTE ERREICHT**
* NestJS **korrekt gebootstrapped**
* HTTP-Driver (`@nestjs/platform-express`) installiert
* TypeScript **strict & stabil**
* Decorators korrekt konfiguriert:
* `experimentalDecorators`
* `emitDecoratorMetadata`
* Globaler Auth-Guard registriert
* **HealthController aktiv**
* `GET /health`
* Antwort: `{ status: "ok", timestamp }`
* **Smoke Test erfolgreich**
* Server antwortet
* Routing funktioniert
* Auth blockiert Public-Routen nicht
---
## ISSUE STATUS (GITEA)
**HEUTE ERLEDIGT & GESCHLOSSEN:**
* #238 Request-User Typisierung
* #239 Globaler Auth-Guard (Dummy)
* #240 Public Decorator
* #241 Zentrale Auth-Regeln
* #242 Fake-User für Dev/Admin
➡️ **Phase 1 Fundament offiziell abgeschlossen**
---
## LAST ACTIONS (HEUTE)
* NestJS Backend startfähig gemacht
* ts-node / TS / Decorator-Fehler sauber gelöst
* DummyAuth produktiv aktiviert
* Health-Endpoint als Smoke Test eingebaut
* Vollständiger **Backup-Commit** erstellt
* Push zu Gitea durchgeführt
---
## AKTUELLER PROJEKTSTATUS
🟢 **Backend Fundament stabil**
🟢 **Auth-Architektur vorbereitet**
🟢 **DB & Seed zuverlässig**
🟢 **Bereit für Phase 2**
---
## NEXT INTENDED STEPS (NICHT HEUTE)
* Phase 2:
* Pets / Posts Controller aktivieren
* Feed & Discovery real testen
* Oder:
* Auth Phase 2 (passwordHash + JWT)
* Oder:
* Frontend anbinden
---
## FAZIT (intern & ehrlich)
> Das heutige Ziel war **nicht Features**,
> sondern **Stabilität, Architektur und Reproduzierbarkeit**.
>
> **Ziel erreicht.**
---
Wenn du willst, mache ich dir das beim nächsten Start:
* als **CHANGELOG.md**
* oder als **`PROJECT_STATUS.md`**
* oder direkt als **README-Abschnitt**
---
Mega gut gepflegtes Tagebuch 👍
Ich habe deinen Text **inhaltlich geprüft, konsolidiert und auf den HEUTIGEN Stand erweitert**
ohne deinen Stil kaputtzumachen.
Unten bekommst du **eine neue, saubere TXT-Version**, **1:1 Copy-Paste**, die du:
* als neues Tagebuch ablegen kannst **ODER**
* als Ersatz für die bisherige Datei nutzen kannst
Ich habe **nur ergänzt & präzisiert**, nichts „schöngefärbt“.
---
# 📘 PROJECT ONLYPETS ENTWICKLERTAGEBUCH
**(ehemals OnlyCats)**
**Stand:** **27.12.2025 FEED & FRONTEND STABILISIERT**
---
## STACK
* **Backend:** NestJS + Prisma
* **DB (DEV):** PostgreSQL (Docker)
* **Frontend:** Next.js (App Router, getrenntes Projekt)
* **Repo:** Gitea (selfhosted, Issues aktiv)
* **OS:** Windows (PowerShell)
---
## CORE IDEA
* Tiere sind **primäre Entität**
* Kein Post ohne Pet
* User sind sekundär (Owner / Kontext)
---
## DATA PRINCIPLES
* Single Source of Truth
* `Pet.age` wird **niemals gespeichert**
* Stattdessen: `Pet.birthDate: DateTime`
* Alter & Geburtstag werden **immer berechnet**
* Keine redundanten Altersfelder
---
## PET RULES
* `birthDate` optional, aber bevorzugt
* `fallbackAge` nur temporär (nicht persistent)
* Pet-Edit erlaubt birthDate-Korrektur
* `type` & `breed` immutable
* `visibility`: `public | followers` (serverseitig enforced)
---
## DATABASE (DEV STABIL)
* PostgreSQL läuft **im Docker**
* Port: **55432**
* Credentials:
* user: `onlypets_user`
* pass: `onlypets_pass`
* db: `onlypets`
* `DATABASE_URL`:
```
postgresql://onlypets_user:onlypets_pass@localhost:55432/onlypets
```
---
## BACKEND STATUS (AKTUELL)
* Prisma Schema vollständig vorhanden
* Migration erfolgreich ausgeführt
* Tabellen existieren
* Follow / Unfollow implementiert
* Visibility-Logik (`public | followers`) vorbereitet
* Notifications vorbereitet (FOLLOW, BIRTHDAY später)
* Feed-Endpoint `/feed` funktionsfähig
* Backend läuft stabil auf **Port 3001**
---
## SEED STATUS
* `seed.ts` vollständig funktionsfähig
* Seed **ohne Passwort** (Auth kommt später)
* User via **upsert** (idempotent)
* Enthält:
* Alpha-User
* Pets mit `birthDate`
* Posts
* Follow-Beziehungen
* Reihenfolge:
* Migration → Seed
* Seed mehrfach ausführbar **ohne Fehler**
---
## AUTH STATUS PHASE 1 ABGESCHLOSSEN
* ❌ Kein Passwortfeld im User-Model
* ❌ Kein echter Login (bewusst verschoben)
* ✅ Guard-first Architektur
* ✅ Globaler `DummyAuthGuard`
* ✅ `@Public()` Decorator
* ✅ `req.user` immer definiert (Fake-User oder `null`)
* ✅ Rollen vorbereitet (`USER | MOD | ADMIN`)
* ✅ Fake-User steuerbar per ENV
* ✅ Keine Auth-Logik in Services
---
## FRONTEND STATUS **HEUTE ERREICHT**
* Frontend läuft stabil auf **Port 3000**
* API-Anbindung an Backend (`3001`) erfolgreich getestet
* `/feed` Route existiert & funktioniert
* App Router korrekt genutzt
* **Mock-Feed aktiv**, kein Backend-Zwang
### Feed-Logik (Frontend)
* Follow-Feed + Discovery-Feed kombiniert
* Discovery-Ratio: **15 %**
* Relevance-Score:
* Pet-Typ
* Rasse
* Altersnähe
* Likes & Comments
* `isDiscovery` sauber an `PostCard` durchgereicht
### PostCard Status
* Typen konsolidiert (`petBirthDate?` ergänzt)
* Birthday-Logik vorbereitet
* Relative Zeit via `timeAgo`
* UX-Polish:
* klare Hierarchie (Titel / Meta / Text)
* Action-Zone (Like / Comment)
* Preview-Modal mit Touch-Dismiss
➡️ **Mock → Backend-Feed später nahezu 1:1 möglich**
---
## ISSUE STATUS (GITEA)
### Heute neu abgeschlossen:
* **#244 Frontend: Feed-Route stabilisieren & PostCard UX-Polish** ✅
### Bereits erledigt (Phase 1 Fundament):
* #238 Request-User Typisierung
* #239 Globaler Auth-Guard (Dummy)
* #240 Public Decorator
* #241 Zentrale Auth-Regeln
* #242 Fake-User für Dev/Admin
➡️ **Phase 1 offiziell abgeschlossen**
➡️ **Frontend-Feed vorbereitet**
---
## LAST ACTIONS (HEUTE)
* Backend-Ports bereinigt (3001)
* Frontend / Backend parallel stabilisiert
* Feed-Route verifiziert
* Mock-Feed & Discovery-Logik geprüft
* Typ-Inkonsistenzen behoben
* UX-Polish umgesetzt
* Commit & Push durchgeführt
* Issue #244 dokumentiert & geschlossen
---
## AKTUELLER PROJEKTSTATUS
🟢 Backend Fundament stabil
🟢 Auth-Architektur vorbereitet
🟢 DB & Seed zuverlässig
🟢 Frontend-Feed stabil (Mock)
🟢 Klarer Übergang zu Phase 2
---
## NEXT INTENDED STEPS (NACH BREAK)
* Phase 2:
* Mock-Feed → echtes `/feed` Backend
* Pets / Posts Controller vertiefen
* Oder:
* Auth Phase 2 (passwordHash + JWT)
* Oder:
* Frontend weiter UX-polishen
---
## FAZIT (INTERN)
> Heute ging es **nicht um neue Features**,
> sondern um **Stabilität, Konsistenz und Zukunftssicherheit**.
>
> **Ziel erreicht. Sehr guter Stand für eine Pause.**
+13 -2
View File
@@ -1,12 +1,23 @@
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { PostsModule } from './posts/posts.module';
import { FeedModule } from './feed/feed.module';
import { PrismaModule } from './prisma/prisma.module';
import { PetsModule } from './pets/pets.module';
import { DummyAuthGuard } from './auth/guards/dummy-auth.guard';
import { HealthController } from './health.controller';
@Module({
imports: [],
controllers: [HealthController],
imports: [
PrismaModule, // Basis zuerst
PostsModule, // /posts
FeedModule, // /feed (1:1 Spiegel)
PetsModule, // /pets
],
controllers: [
HealthController,
],
providers: [
{
provide: APP_GUARD,
+6
View File
@@ -0,0 +1,6 @@
import { Request } from 'express';
import { RequestUser } from '../../types/request-user.type';
export interface AuthRequest extends Request {
user: RequestUser;
}
+13
View File
@@ -0,0 +1,13 @@
import { Controller, Get, Req } from '@nestjs/common';
import { FeedService } from './feed.service';
import { AuthRequest } from '../auth/types/auth-request';
@Controller('feed')
export class FeedController {
constructor(private readonly feedService: FeedService) {}
@Get()
async getFeed(@Req() req: AuthRequest) {
return this.feedService.getFeed(req.user);
}
}
+10
View File
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { FeedController } from './feed.controller';
import { FeedService } from './feed.service';
import { PrismaService } from '../prisma/prisma.service';
@Module({
controllers: [FeedController],
providers: [FeedService, PrismaService],
})
export class FeedModule {}
+60
View File
@@ -0,0 +1,60 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { RequestUser } from '../types/request-user.type';
@Injectable()
export class FeedService {
constructor(private readonly prisma: PrismaService) {}
async getFeed(user: RequestUser | null) {
const userId = user?.id;
const posts = await this.prisma.post.findMany({
where: {
OR: [
{
pet: {
visibility: 'public',
},
},
...(userId
? [
{
pet: {
ownerId: userId,
},
},
{
pet: {
visibility: 'followers',
owner: {
followers: {
some: {
followerId: userId,
},
},
},
},
},
]
: []),
],
},
include: {
pet: true,
author: true,
},
orderBy: {
createdAt: 'desc',
},
});
return posts.map((post) => ({
...post,
isDiscovery: false,
}));
}
}
+5 -2
View File
@@ -3,8 +3,11 @@ import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const port = process.env.PORT || 3000;
app.enableCors({
origin: 'http://localhost:3000',
credentials: true,
});
const port = process.env.PORT || 3001;
await app.listen(port);
console.log(`🚀 Backend running on http://localhost:${port}`);
+29 -3
View File
@@ -1,14 +1,40 @@
import { Patch, Param, Body, Req } from '@nestjs/common';
import {
Controller,
Get,
Patch,
Param,
Body,
Req,
} from '@nestjs/common';
import { PetsService } from './pets.service';
import { UpdatePetDto } from './dto/update-pet.dto';
import { AuthRequest } from '../auth/types/auth-request';
@Controller('pets')
export class PetsController {
constructor(private readonly petsService: PetsService) {}
// ✅ READ-ONLY: Pet-Profil anzeigen
@Get(':id')
getPet(
@Param('id') id: string,
@Req() req: AuthRequest,
): Promise<{ owner: { id: number; name: string; createdAt: Date; email: string; }; } & { id: number; name: string; type: string; breed: string; birthDate: Date | null; gender: string | null; bio: string | null; avatarUrl: string | null; visibility: string; ownerId: number; }> {
return this.petsService.getPetById(Number(id), req.user);
}
@Patch(':id')
updatePet(
@Param('id') id: string,
@Body() dto: UpdatePetDto,
@Req() req,
@Req() req: AuthRequest,
) {
return this.petsService.updatePet(
Number(id),
req.user.id,
req.user!.id,
dto,
);
}
}
+11
View File
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { PetsService } from './pets.service';
import { PetsController } from './pets.controller';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [PetsController],
providers: [PetsService],
})
export class PetsModule {}
+49
View File
@@ -2,15 +2,64 @@ import {
Injectable,
BadRequestException,
NotFoundException,
ForbiddenException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePetDto } from './dto/create-pet.dto';
import { UpdatePetDto } from './dto/update-pet.dto';
import { RequestUser } from '../types/request-user.type';
@Injectable()
export class PetsService {
constructor(private readonly prisma: PrismaService) {}
/**
* Read-only: einzelnes Pet für Profilansicht
* Visibility-Regeln:
* - public: immer sichtbar
* - owner: immer sichtbar
* - followers: nur für Follower
*/
async getPetById(id: number, user: RequestUser) {
const pet = await this.prisma.pet.findUnique({
where: { id },
include: {
owner: true,
},
});
if (!pet) {
throw new NotFoundException('Pet not found');
}
// 1️⃣ Public Pets → immer sichtbar
if (pet.visibility === 'public') {
return pet;
}
// 2️⃣ Owner darf sein Pet immer sehen
if (user && pet.ownerId === user.id) {
return pet;
}
// 3️⃣ Followers-Visibility
if (user && pet.visibility === 'followers') {
const isFollower = await this.prisma.follow.findFirst({
where: {
followerId: user.id,
followingId: pet.ownerId,
},
});
if (isFollower) {
return pet;
}
}
// 4️⃣ Alles andere → verboten
throw new ForbiddenException('You are not allowed to view this pet');
}
// -------------------------------
// CREATE PET
// -------------------------------
+7 -22
View File
@@ -1,33 +1,18 @@
import { Controller, Get, Query, Req } from '@nestjs/common';
import { Controller, Get, Req } from '@nestjs/common';
import { PostsService } from './posts.service';
import { AuthRequest } from '../auth/types/auth-request';
@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Get()
getFeed(
@Req() req,
@Query('take') take?: string,
@Query('cursorId') cursorId?: string,
) {
return this.postsService.getFeed(req.user.id, {
mode: 'mixed',
take: take ? Number(take) : undefined,
cursorId: cursorId ? Number(cursorId) : undefined,
});
async getPosts(@Req() req: AuthRequest) {
return this.postsService.getPosts(req.user);
}
@Get('discovery')
getDiscovery(
@Req() req,
@Query('take') take?: string,
@Query('cursorId') cursorId?: string,
) {
return this.postsService.getFeed(req.user.id, {
mode: 'discovery',
take: take ? Number(take) : undefined,
cursorId: cursorId ? Number(cursorId) : undefined,
});
@Get('my')
async getMyPosts(@Req() req: AuthRequest) {
return this.postsService.getMyPosts(req.user);
}
}
+14
View File
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
import { PrismaModule } from '../prisma/prisma.module';
import { PrismaService } from '../prisma/prisma.service';
@Module({
imports: [
PrismaModule, // 👈 DAS ist der Schlüssel
],
controllers: [PostsController],
providers: [PostsService, PrismaService,],
})
export class PostsModule {}
+50 -136
View File
@@ -1,156 +1,70 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { RequestUser } from '../types/request-user.type';
type FeedMode = 'mixed' | 'discovery';
type FeedOptions = {
mode?: FeedMode;
take?: number;
cursorId?: number;
};
@Injectable()
export class PostsService {
constructor(private readonly prisma: PrismaService) {}
async getFeed(userId: number, options?: FeedOptions) {
const mode: FeedMode = options?.mode ?? 'mixed';
const take = Math.min(Math.max(options?.take ?? 20, 1), 50);
const cursorId = options?.cursorId;
async getPosts(user: RequestUser) {
const userId = user?.id;
const include = {
pet: {
select: {
id: true,
name: true,
type: true,
birthDate: true,
return this.prisma.post.findMany({
where: {
OR: [
{
pet: {
visibility: 'public',
},
},
},
author: {
select: {
name: true,
_count: {
select: {
followers: true,
following: true,
},
},
},
},
} as const;
// Sichtbarkeit
const baseWhere =
mode === 'discovery'
? {
pet: { visibility: 'public' },
}
: {
pet: {
OR: [
{ visibility: 'public' },
{
visibility: 'followers',
owner: {
followers: {
some: {
followerId: userId,
...(userId
? [
{
pet: {
ownerId: userId,
},
},
{
pet: {
visibility: 'followers',
owner: {
followers: {
some: {
followerId: userId,
},
},
},
},
},
},
],
},
};
const commonQuery = {
orderBy: [
{ createdAt: 'desc' as const },
{ id: 'desc' as const },
]
: []),
],
include,
...(cursorId ? { cursor: { id: cursorId }, skip: 1 } : {}),
};
},
include: {
pet: true,
author: true,
},
orderBy: {
createdAt: 'desc',
},
});
}
// -------- Discovery only --------
if (mode === 'discovery') {
const items = await this.prisma.post.findMany({
where: baseWhere,
take,
...commonQuery,
});
return {
items,
nextCursorId:
items.length === take ? items[items.length - 1].id : null,
meta: { mode, take },
};
}
// -------- Mixed Feed (85 / 15) --------
const discoveryCount = Math.max(1, Math.round(take * 0.15));
const normalCount = take - discoveryCount;
const [normalItems, discoveryItems] = await Promise.all([
this.prisma.post.findMany({
where: baseWhere,
take: normalCount,
...commonQuery,
}),
this.prisma.post.findMany({
where: { pet: { visibility: 'public' } },
take: discoveryCount,
...commonQuery,
}),
]);
// Ruhige Mischung + Duplikat-Schutz
const seen = new Set<number>();
const merged = [];
const n = [...normalItems];
const d = [...discoveryItems];
while (merged.length < take && (n.length || d.length)) {
for (let i = 0; i < 6 && n.length && merged.length < take; i++) {
const item = n.shift();
if (item && !seen.has(item.id)) {
seen.add(item.id);
merged.push(item);
}
}
if (d.length && merged.length < take) {
const item = d.shift();
if (item && !seen.has(item.id)) {
seen.add(item.id);
merged.push(item);
}
}
}
while (merged.length < take && n.length) {
const item = n.shift();
if (item && !seen.has(item.id)) {
seen.add(item.id);
merged.push(item);
}
}
return {
items: merged,
nextCursorId:
merged.length === take ? merged[merged.length - 1].id : null,
meta: {
mode,
take,
normalCount,
discoveryCount,
async getMyPosts(user: RequestUser) {
return this.prisma.post.findMany({
where: {
authorId: user!.id, // 👈 explizit casten
},
};
include: {
pet: true,
author: true,
},
orderBy: {
createdAt: 'desc',
},
});
}
}
+9
View File
@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
+16
View File
@@ -0,0 +1,16 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
+2 -2
View File
@@ -3,9 +3,9 @@ import { RequestUser } from '../auth/types/request-user.type';
declare global {
namespace Express {
interface Request {
user?: RequestUser | null;
user: RequestUser;
}
}
}
export {};
+7
View File
@@ -0,0 +1,7 @@
export type UserRole = 'USER' | 'ADMIN' | 'MOD';
export type RequestUser = {
id: number;
role: UserRole;
isFake?: boolean;
} | null;
+22
View File
@@ -34,3 +34,25 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
## Auth-Architektur (Grundlagen)
### Prinzipien
- Guard-first Architektur
- Keine Auth-Logik in Services
- Controller bleiben dünn
- Zentrale User-Wahrheit über Request-Kontext
### RequestUser
- `req.user` ist der einzige User-Kontext im Request
- Typ: `RequestUser`
- ID ist **number** (DB-kompatibel)
- Kann `null` sein (Public Routes)
```ts
export type UserRole = 'USER' | 'ADMIN' | 'MOD';
export type RequestUser = {
id: number;
role: UserRole;
isFake?: boolean;
} | null;
@@ -2,6 +2,7 @@
import { useRef } from 'react';
import { formatPetAge, isBirthdayToday } from '@/app/lib/petAge';
import { timeAgo } from "@/lib/time";
export type PostCardData = {
id: number;
@@ -12,6 +13,7 @@ export type PostCardData = {
text: string;
likesCount: number;
commentsCount: number;
petbirthDate?: string;
};
@@ -83,7 +85,7 @@ export default function PostCard({
· {post.petType}
{post.petBirthDate && ` · ${formatPetAge(post.petBirthDate)}`}
· {post.authorName}
· {post.createdAt}
· {timeAgo(post.createdAt)}
</span>
{isBirthday && (
+4 -4
View File
@@ -40,7 +40,7 @@ const allPosts: FeedPost[] = [
breed: 'Bengal',
ageMonths: 26,
authorName: 'Anna',
createdAt: 'vor 10 Minuten',
createdAt: new Date().toISOString(),
text: 'Luna liebt den neuen Kratzbaum.',
likesCount: 8,
commentsCount: 2,
@@ -53,7 +53,7 @@ const allPosts: FeedPost[] = [
breed: 'Labrador',
ageMonths: 30,
authorName: 'Tim',
createdAt: 'vor 1 Stunde',
createdAt: new Date().toISOString(),
text: 'Rocky war schwimmen 🐕‍🦺',
likesCount: 15,
commentsCount: 4,
@@ -66,7 +66,7 @@ const allPosts: FeedPost[] = [
breed: 'Bengal',
ageMonths: 22,
authorName: 'Sophie',
createdAt: 'heute',
createdAt: new Date().toISOString(),
text: 'Milo entdeckt den Balkon.',
likesCount: 11,
commentsCount: 3,
@@ -79,7 +79,7 @@ const allPosts: FeedPost[] = [
breed: 'Maine Coon',
ageMonths: 40,
authorName: 'Laura',
createdAt: 'gestern',
createdAt: new Date().toISOString(),
text: 'Nala beobachtet alles ganz ruhig.',
likesCount: 6,
commentsCount: 1,
+41
View File
@@ -748,3 +748,44 @@ select:disabled {
0 0 0 2px rgba(46, 125, 50, 0.18),
var(--shadow-sm);
}
/* ===========================
UX Polish PostCard hierarchy & actions
(safe additions, no refactor)
=========================== */
.postcard-title {
font-size: 1.05rem;
font-weight: 600;
color: var(--color-text-main, #111);
}
.postcard-meta {
font-size: 0.75rem;
color: var(--color-text-muted, #6b7280);
white-space: nowrap;
}
.postcard-text {
margin-top: var(--space-sm, 10px);
font-size: 0.95rem;
line-height: 1.55;
color: var(--color-text-secondary, #1f2937);
max-width: 65ch;
}
/* Click affordance: main area opens preview, actions do not */
.postcard-main {
cursor: pointer;
}
.postcard-actions {
cursor: default;
margin-top: var(--space-sm, 10px);
padding-top: var(--space-sm, 10px);
border-top: 1px solid var(--color-border, rgba(0,0,0,0.08));
}
.post-locked-hint {
margin-top: 0.5rem;
font-size: 0.85rem;
opacity: 0.7;
}
+63
View File
@@ -0,0 +1,63 @@
'use client';
import { useEffect } from 'react';
export default function TestPage() {
useEffect(() => {
console.log('--- FE TEST START ---');
// Feed testen
fetch('http://localhost:3001/feed')
.then(res => res.json())
.then(data => {
console.log('FEED RESPONSE:', data);
})
.catch(err => {
console.error('FEED ERROR:', err);
});
// Öffentliches Pet testen
fetch('http://localhost:3001/pets/2')
.then(async res => {
if (res.status === 403) {
console.log('PET 2: FORBIDDEN');
return;
}
const data = await res.json();
console.log('PET 2 RESPONSE:', data);
})
.catch(err => {
console.error('PET 2 ERROR:', err);
});
// Followers-Pet testen (soll 403 geben)
fetch('http://localhost:3001/pets/5')
.then(async res => {
if (res.status === 403) {
console.log('PET 5: FORBIDDEN (expected)');
return;
}
const data = await res.json();
console.log('PET 5 RESPONSE:', data);
})
.catch(err => {
console.error('PET 5 ERROR:', err);
});
}, []);
return (
<section style={{ padding: '2rem' }}>
<h1>🧪 Frontend API Test</h1>
<p>
Diese Seite dient nur zum Testen der Backend-APIs.
<br />
👉 Öffne die <strong>Browser-Konsole</strong>.
</p>
<ul>
<li>Feed wird geladen</li>
<li>Pet 2 (public) wird geladen</li>
<li>Pet 5 (followers) sollte 403 sein</li>
</ul>
</section>
);
}
+3 -1
View File
@@ -1,7 +1,9 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
turbopack: {
root: __dirname,
},
};
export default nextConfig;
+1
View File
@@ -4,6 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev",
"dev": "set NODE_OPTIONS=--max-old-space-size=4096 && next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
+20
View File
@@ -0,0 +1,20 @@
export function timeAgo(date: string | Date) {
const ts = new Date(date).getTime();
const diffMs = Date.now() - ts;
const minutes = Math.floor(diffMs / 60000);
if (minutes < 1) return "gerade eben";
if (minutes < 60) return `vor ${minutes} Minute${minutes === 1 ? "" : "n"}`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `vor ${hours} Stunde${hours === 1 ? "" : "n"}`;
const days = Math.floor(hours / 24);
if (days < 7) return `vor ${days} Tag${days === 1 ? "" : "en"}`;
const weeks = Math.floor(days / 7);
if (weeks < 4) return `vor ${weeks} Woche${weeks === 1 ? "" : "n"}`;
// Fallback: Datum kurz
return new Date(date).toLocaleDateString("de-DE");
}
+2505 -1
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -1,5 +1,24 @@
{
"name": "onlypets-frontend",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"export": "next export"
},
"dependencies": {
"next": "^16.1.1",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"autoprefixer": "^10.4.23",
"eslint": "^8.40.0",
"next-auth": "^4.20.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
}
+24
View File
@@ -0,0 +1,24 @@
@echo off
title OnlyPets Dev Starter
echo ===============================
echo Starting OnlyPets (DEV)
echo ===============================
REM --- Backend ---
echo Starting Backend on port 3001...
start "OnlyPets Backend" cmd /k ^
cd /d C:\Projekte\OnlyPets\backend ^& ^
npm run dev
REM --- Frontend ---
echo Starting Frontend on port 3000...
start "OnlyPets Frontend" cmd /k ^
cd /d C:\Projekte\OnlyPets\frontend ^& ^
npm run dev
echo.
echo All services started.
echo Backend -> http://localhost:3001
echo Frontend -> http://localhost:3000
echo.
BIN
View File
Binary file not shown.