MediaProject is a full-stack social platform for discovering, tracking, and discussing movies and books. It was built as a university MVP and combines media discovery, personal libraries, community features, real-time messaging, recommendations, and import tools in one application.
Project status: Release candidate. Backend, frontend, lint, and production browser validation have passed locally; public deployment and operational sign-off remain pending.
The ordered production checklist, migration sequence, smoke tests, and rollback
procedure are documented in
docs/PRODUCTION_RUNBOOK.md.
- Discover movies through TMDB and books through Google Books.
- Track watched/read progress, ratings, favorites, plans, and personal notes.
- Import Letterboxd data through a staged preview, confirmation, and rollback workflow.
- Publish reviews and media quotes with likes, replies, reports, and moderation.
- Follow or block users and explore personalized social feeds.
- Chat in real time with rich movie, book, GIF, reply, reaction, shared pinned-message, message edit/delete, typing indicators, conversation search, archive, and per-conversation notification mute support.
- Search movies, books, people, and public community lists from one keyboard-friendly global search, ranked by textual relevance and community signals.
- Invite an eligible friend to a Buddy Watch or Buddy Read session, accept or decline invitations, schedule the session, compare both participants' progress, and complete it together. A unified personal schedule combines upcoming Buddy plans and joined club events without per-club client requests, supports bulk calendar export, and gives each participant an independent 15-minute, one-hour, one-day, or disabled reminder preference.
- Create public or private movie, book, and mixed-media clubs, discover public communities, join or leave them, browse member rosters, nominate media, vote on candidates, let club leadership confirm the shared selection, and discuss that selection through spoiler-protected posts and replies. Club leaders can also schedule online, in-person, or hybrid watch/read events with RSVP states, cancellation notices, and configurable reminders. Private invitations expire after seven days and can be accepted from the club hub; owners can appoint moderators, while authorized club leadership can remove members without erasing the discussion history they contributed. Owners can update club settings and use a reversible archive mode that preserves history while revoking pending invitations, cancelling future events, and preventing new activity.
- Use recommendation, matching, quiz, badge, streak, and leaderboard features.
- Try a Stripe-hosted Test Mode Premium subscription flow with signed webhook activation, Event ID idempotency, and no card data entering MediaProject.
- Manage users, badges, reports, and moderation actions from an admin area.
- Generate selected media content through a Groq-backed AI integration.
- ASP.NET Core 8 Web API
- Entity Framework Core 8 and SQL Server
- JWT Bearer authentication and role-based authorization
- Email OTP multi-factor authentication for administrator accounts
- Rotating, server-revocable refresh sessions with hashed token storage
- SignalR for real-time communication
- Resilient typed HTTP clients for TMDB, Google Books, Groq, and Resend
- Stripe Test Mode Checkout with backend-owned Price IDs and signed webhooks
- Provider-based quiz session storage with optional Redis scale-out
- FluentValidation
- BCrypt password hashing
- Swagger / OpenAPI
- React 19 and TypeScript
- Vite 8 and Tailwind CSS 4
- TanStack Query
- React Router
- Zustand
- React Hook Form and Zod
- Axios and the SignalR JavaScript client
The backend follows a layered architecture:
MediaProject.Domain
↑
MediaProject.Application
↑
MediaProject.Infrastructure
↑
MediaProject.API
- Domain contains entities and shared domain types.
- Application contains DTOs, validation, service contracts, and repository contracts.
- Infrastructure implements persistence, external API clients, and application services.
- API exposes HTTP endpoints, authentication, rate limiting, middleware, Swagger, and SignalR hubs.
- Frontend is organized around application features and route-level code splitting.
- .NET 8 SDK
- SQL Server or SQL Server Express
- Node.js 24 and npm
- TMDB API key
- Google Books API key
- Groq API key
- Optional Resend account and verified sending domain for local email testing
- Optional GIPHY browser API key for GIF search
- Optional Redis instance for multi-instance quiz sessions
- Optional Stripe Test Mode account and Stripe CLI for Premium checkout testing
The API project uses .NET user secrets for sensitive local configuration:
dotnet user-secrets set "JwtSettings:Secret" "use-a-random-secret-with-at-least-32-bytes" --project MediaProject.API
dotnet user-secrets set "TmdbSettings:ApiKey" "your-tmdb-api-key" --project MediaProject.API
dotnet user-secrets set "GoogleBooksSettings:ApiKey" "your-google-books-api-key" --project MediaProject.API
dotnet user-secrets set "GroqSettings:ApiKey" "your-groq-api-key" --project MediaProject.APIThe repository intentionally contains no database connection string. Configure
ConnectionStrings:DefaultConnection through user secrets for local development:
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "your-connection-string" --project MediaProject.APIEmail confirmation and password recovery are disabled by default in local development. To exercise the complete account flow with Resend, verify a sending domain first and configure:
dotnet user-secrets set "Email:Enabled" "true" --project MediaProject.API
dotnet user-secrets set "Email:ApiKey" "re_your-resend-api-key" --project MediaProject.API
dotnet user-secrets set "Email:FromEmail" "auth@updates.example.com" --project MediaProject.API
dotnet user-secrets set "Email:SupportEmail" "support@example.com" --project MediaProject.API
dotnet user-secrets set "Email:FromName" "MediaProject" --project MediaProject.API
dotnet user-secrets set "Email:FrontendBaseUrl" "http://localhost:5173" --project MediaProject.API
dotnet user-secrets set "Auth:AccountSecurity:RequireConfirmedEmail" "true" --project MediaProject.API
dotnet user-secrets set "Auth:AccountSecurity:RequireAdminMfa" "true" --project MediaProject.APIResend requires SPF and DKIM DNS records for the sending domain. A dedicated
subdomain such as updates.example.com keeps application mail isolated from
the root domain. The API stores only SHA-256 hashes of verification and reset
tokens, makes every token single-use, expires verification links after 24
hours and reset links after 30 minutes, and sends requests with an idempotency
key. Forgot-password and resend-verification responses are deliberately
generic so an email address cannot be enumerated.
For a public deployment, provide the same values through the platform secret manager or environment variables:
Email__Enabled=true
Email__ApiKey=re_...
Email__FromEmail=auth@updates.example.com
Email__SupportEmail=support@example.com
Email__FromName=MediaProject
Email__FrontendBaseUrl=https://app.example.com
Auth__AccountSecurity__RequireConfirmedEmail=true
Auth__AccountSecurity__RequireAdminMfa=true
Production startup rejects disabled or incomplete email configuration, an
unconfirmed-email or administrator-MFA policy that is turned off, and loopback
or non-HTTPS frontend URLs. Administrator password login creates no session
until the single-use six-digit email code is verified. Codes expire after ten
minutes by default, are stored only as keyed hashes, and are locked after five
failed attempts. Do not place the API key in appsettings.json or a frontend
VITE_ variable.
The current release is intentionally limited to one API replica. Keep
Deployment__Mode=SingleInstance and configure the hosting platform's minimum
and maximum API replica count to 1. Production startup rejects any other mode
because rate-limit counters, authenticated-user security cache invalidation,
and SignalR connection state are currently process-local. Do not enable
horizontal auto-scaling until distributed rate limiting, a distributed
security-state cache, and a SignalR backplane have been configured together.
Quiz sessions use a bounded, thread-safe in-process provider by default, which keeps local development dependency-free. Multi-instance deployments should select the Redis provider and configure its connection string outside source control:
dotnet user-secrets set "Cache:QuizSessions:Provider" "Redis" --project MediaProject.API
dotnet user-secrets set "ConnectionStrings:Redis" "localhost:6379,abortConnect=false" --project MediaProject.APIThe Redis identity needs GET, SET, DEL, PTTL, TIME, and script
execution permissions for the configured mediaproject:quiz-session: key
prefix.
Stripe Test Mode Premium setup, Price ID configuration, webhook listener, test
card, and migration instructions are documented in
docs/STRIPE_TEST_MODE.md. The integration rejects
sk_live_ keys and never accepts card fields in MediaProject.
External HTTP clients use centrally configured total/attempt timeouts, bounded
connection pools, transient retries for idempotent TMDB and Google Books reads,
and circuit breakers. Groq requests are never automatically retried because a
POST may consume quota even when its response is lost. Defaults are stored in
ExternalHttp, while provider-specific total timeouts are stored in each
provider section of appsettings.json.
Reverse-proxy processing is disabled by default. Production deployments behind
Nginx, IIS, a cloud load balancer, or another proxy must enable ReverseProxy
and list only infrastructure-controlled proxy IP addresses or CIDR networks.
Forwarded headers are processed before HTTPS redirection, authentication, rate
limiting, logging, and session IP auditing. Never trust arbitrary forwarded
headers from the public internet.
dotnet ef database update --project MediaProject.Infrastructure --startup-project MediaProject.APIdotnet run --project MediaProject.API --launch-profile httpsSwagger is available at
https://localhost:7119/swagger in the Development environment.
The API exposes two anonymous, rate-limit-exempt health endpoints for hosting platforms and container orchestrators:
GET /health/liveconfirms that the API process is running.GET /health/readyconfirms that the API, SQL Server connection, and the configured quiz-session store are ready to serve requests.
Both endpoints return a compact JSON payload with status, UTC timestamp,
duration, trace ID, and individual check results. Internal exceptions and
connection details are never included in the response. Normal API responses
also include an X-Trace-Id header, and requests are written to structured
application logs without query strings or request bodies.
Copy-Item MediaProject.Frontend/.env.example MediaProject.Frontend/.env.local
Set-Location MediaProject.Frontend
npm ci
npm run devThe example frontend configuration targets the local HTTPS API and SignalR
hub. Add VITE_GIPHY_API_KEY only to .env.local when GIF search is needed.
Values prefixed with VITE_ are embedded into the browser bundle and are not
secrets; restrict browser keys by allowed origin and quota in the provider
dashboard. Production builds fail before bundling when API or SignalR URLs are
missing, malformed, contain credentials/query data, or do not use HTTPS.
Access tokens are kept only in frontend runtime memory and are never persisted
to Local Storage or Session Storage. A random refresh token is stored in a
HttpOnly, Secure, scoped cookie, while the database stores only its SHA-256
hash. Refresh operations rotate the token atomically; replaying a revoked token
revokes the user's remaining active sessions. Refresh and logout endpoints also
require a custom CSRF-protection header.
The refresh cookie defaults to SameSite=Lax in production. Development uses
SameSite=None because the Vite frontend and HTTPS API run on different
schemes/origins. Production deployments should keep the frontend and API on
the same site where possible and must use HTTPS.
dotnet build MediaProject.sln --configuration Release
dotnet test MediaProject.sln --configuration Release
npm.cmd --prefix MediaProject.Frontend run lint
npm.cmd --prefix MediaProject.Frontend run build
npm.cmd --prefix MediaProject.Frontend run test:e2e
npm.cmd --prefix MediaProject.Frontend run test:e2e:productionGitHub Actions first runs scripts/check-repository-hygiene.sh. The check
rejects generated output, IDE state, local environment files, logs, inline
connection strings, and non-empty secret values in tracked configuration files.
It then runs backend restore/build/tests and frontend install/lint checks. The
Chromium suite is executed against a Vite production build served through
vite preview, so route chunks, lazy imports, and production asset references
are validated on pushes and pull requests targeting main.
To clean an existing Windows working tree that previously tracked generated files, run:
powershell -ExecutionPolicy Bypass -File scripts/clean-repository.ps1Review the resulting Git deletions before committing.
The browser suite currently covers authentication and refresh sessions, social activity and moderation, movie/book detail actions, quiz hint and cooldown behavior, notification state transitions, direct messaging, private block-list management, and the complete Letterboxd preview/import/rollback workflow. Deque axe-core also checks representative anonymous and authenticated pages against automated WCAG 2 A/AA rules, while keyboard tests cover login and skip-navigation behavior. Route metadata tests verify document titles and live announcements, and a forced lazy-route failure verifies the branded recovery screen. Quiz and rollback dialogs support safe initial focus and Escape dismissal; mobile chat supports keyboard submission and exposes its message stream as an accessible live log.
- Secrets, connection strings, local environment files, IDE state, and build output are excluded from source control and Docker build contexts.
- Startup fails when required secrets or the database connection are missing.
- Production startup rejects wildcard or loopback hosts, placeholder/non-HTTPS
CORS origins, CORS values containing paths/query data/credentials, and
database connections using disabled encryption, trusted server certificates,
persisted security information, or the high-privilege
saaccount. - Runtime database migration is disabled in production; schema changes are applied as an explicit, observable release step before the new API starts.
- Passwords are hashed with BCrypt.
- The API validates JWT issuer, audience, lifetime, signature, and user status.
- Access tokens are memory-only; refresh tokens are hashed, rotated, revocable,
and delivered through
HttpOnlysecure cookies. - Admin endpoints use role-based authorization.
- Global, feature-specific, and stricter anonymous authentication rate limits are configured.
- Repeated invalid passwords trigger an account-level temporary lockout.
- Password changes and the "sign out all devices" action revoke refresh sessions and increment a server-validated security version, invalidating previously issued access tokens.
- Users can inspect active refresh sessions, identify the current device, and revoke an individual device without affecting their other sessions.
- AI-generated media content requires authentication, resolves movie/book context from canonical provider IDs, and has a configurable per-user daily quota.
- Trusted reverse-proxy handling is opt-in and restricted to configured proxy addresses or networks.
- Production responses use HSTS and restrictive browser security headers.
- CORS uses an explicit origin allowlist.
- Health responses do not expose exception or connection details.
Review SECURITY.md, configure production secrets and infrastructure outside
source control, then run the complete release gate before deployment.
Run the complete local release gate from the repository root:
powershell -ExecutionPolicy Bypass -File scripts/verify-release.ps1- Move runtime uploads to object storage and add SignalR scale-out before horizontally scaling the API.
- Periodically review framework and dependency support windows, then plan the next LTS upgrade as a dedicated compatibility project.
No public license has been selected yet. All rights are reserved until a license is added.