diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8d0d009 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,47 @@ +--- +name: Bug Report +about: Report a bug to help us improve +title: '[BUG] ' +labels: bug +assignees: '' +--- + +## Describe the Bug + +A clear and concise description of what the bug is. + +## Steps to Reproduce + +1. Go to '...' +2. Click on '...' +3. Scroll down to '...' +4. See error + +## Expected Behavior + +A clear description of what you expected to happen. + +## Actual Behavior + +What actually happened instead. + +## Screenshots + +If applicable, add screenshots to help explain your problem. + +## Environment + +- **Browser**: [e.g., Chrome 120, Firefox 121] +- **OS**: [e.g., Windows 11, macOS Sonoma] +- **Device**: [e.g., Desktop, iPhone 15] +- **Logged in**: [Yes/No] + +## Console Errors + +``` +Paste any browser console errors here +``` + +## Additional Context + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..f229d79 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,30 @@ +--- +name: Feature Request +about: Suggest an idea for this project +title: '[FEATURE] ' +labels: enhancement +assignees: '' +--- + +## Is your feature request related to a problem? + +A clear and concise description of what the problem is. +Ex. I'm always frustrated when [...] + +## Describe the solution you'd like + +A clear and concise description of what you want to happen. + +## Describe alternatives you've considered + +A clear and concise description of any alternative solutions or features you've considered. + +## Additional context + +Add any other context, mockups, or screenshots about the feature request here. + +## Would you be willing to contribute this feature? + +- [ ] Yes, I'd like to implement this feature +- [ ] I can help test this feature +- [ ] No, just suggesting diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..221e658 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,40 @@ +## Description + +Brief description of what this PR does. + +## Type of Change + +- [ ] ๐Ÿ› Bug fix (non-breaking change which fixes an issue) +- [ ] โœจ New feature (non-breaking change which adds functionality) +- [ ] ๐Ÿ’ฅ Breaking change (fix or feature that would cause existing functionality to change) +- [ ] ๐Ÿ“ Documentation update +- [ ] ๐Ÿ”ง Refactoring (no functional changes) +- [ ] ๐Ÿงช Tests + +## Related Issue + +Fixes #(issue number) + +## How Has This Been Tested? + +Describe the tests you ran to verify your changes. + +- [ ] Manual testing in browser +- [ ] Tested on mobile +- [ ] Tested as logged-in user +- [ ] Tested as guest + +## Screenshots (if applicable) + +| Before | After | +|--------|-------| +| image | image | + +## Checklist + +- [ ] My code follows the project's code style +- [ ] I have performed a self-review of my code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] My changes generate no new warnings +- [ ] I have tested my changes locally +- [ ] Any dependent changes have been merged and published diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml new file mode 100644 index 0000000..b0b12ef --- /dev/null +++ b/.github/workflows/backend-ci.yml @@ -0,0 +1,43 @@ +name: Backend CI + +on: + pull_request: + branches: [master, main] + paths: + - 'backend/**' + - '.github/workflows/backend-ci.yml' + +jobs: + build: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: backend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'yarn' + cache-dependency-path: backend/yarn.lock + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Generate Prisma Client + run: npx prisma generate + + - name: TypeScript compile check + run: npx tsc --noEmit + + - name: Check for console.log (optional) + run: | + if grep -r "console.log" src/ --include="*.ts" | grep -v "// allowed"; then + echo "โš ๏ธ Found console.log statements (consider removing)" + fi + continue-on-error: true diff --git a/.github/workflows/backend-deploy.yml b/.github/workflows/backend-deploy.yml new file mode 100644 index 0000000..ad2e833 --- /dev/null +++ b/.github/workflows/backend-deploy.yml @@ -0,0 +1,26 @@ +name: Deploy Backend to Railway + +on: + push: + branches: [master, main] + paths: + - 'backend/**' + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Trigger Railway Deployment + run: | + curl -X POST "${{ secrets.RAILWAY_WEBHOOK_URL }}" \ + -H "Content-Type: application/json" \ + -d '{"branch": "master"}' + env: + RAILWAY_WEBHOOK_URL: ${{ secrets.RAILWAY_WEBHOOK_URL }} + + - name: Deployment triggered + run: echo "๐Ÿš€ Backend deployment triggered on Railway" diff --git a/README.md b/README.md index 313b340..b745f0c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Threads App -A modern social threads application built with GraphQL, Next.js, and PostgreSQL featuring secure JWT authentication with refresh tokens. +A full-featured social threads application built with GraphQL, Next.js, and PostgreSQL - a Threads/Twitter clone with modern features. ![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue) ![Next.js](https://img.shields.io/badge/Next.js-16-black) @@ -33,8 +33,8 @@ threads-app/ ### 1. Clone & Install ```bash -git clone https://github.com/shahadot786/threads-backend-graphql.git -cd threads-backend-graphql +git clone https://github.com/shahadot786/threads-clone.git +cd threads-clone ``` ### 2. Start PostgreSQL @@ -43,7 +43,15 @@ cd threads-backend-graphql docker compose up -d ``` -### 3. Start Backend +### 3. Set Up Supabase + +1. Create a project at [supabase.com](https://supabase.com) +2. Go to **Settings โ†’ API** and copy: + - Project URL โ†’ `SUPABASE_URL` + - `anon` public key โ†’ `SUPABASE_ANON_KEY` + - `service_role` key โ†’ `SUPABASE_SERVICE_ROLE_KEY` + +### 4. Start Backend ```bash cd backend @@ -54,10 +62,11 @@ yarn dev ``` โ†’ GraphQL API: http://localhost:8000/graphql -### 4. Start Frontend +### 5. Start Frontend ```bash cd frontend +cp .env.example .env.local # Configure environment yarn install yarn dev ``` @@ -65,20 +74,39 @@ yarn dev ## โœจ Features -### Implemented โœ… -- **JWT Authentication** with secure httpOnly cookies -- **Refresh Token Rotation** for enhanced security -- **User Registration & Login** with password hashing (HMAC-SHA256) -- **Protected Routes** on both frontend and backend -- **Modular GraphQL Architecture** with type-safe resolvers -- **PostgreSQL** with Prisma ORM - -### Coming Soon ๐Ÿšง -- Thread creation and feed -- Likes and comments -- Follow system -- Real-time notifications -- Profile editing with image upload +### Core Features โœ… +- **Posts/Threads** - Create, edit, delete posts with rich text +- **Media Upload** - Images, videos, and GIF support +- **Replies & Threads** - Nested conversation threads +- **Likes** - Like/unlike posts with counts +- **Reposts** - Share posts to your profile +- **Bookmarks** - Save posts for later (accessible via sidebar Pin) + +### Social Features โœ… +- **Follow System** - Follow/unfollow users +- **Activity Feed** - Notifications for likes, follows, mentions, replies +- **User Profiles** - Bio, stats, profile images +- **User Tooltips** - Hover to see user info with follow button + +### Discovery โœ… +- **Search** - Find users, posts, and hashtags +- **Hashtags** - #tag support with trending tags +- **@Mentions** - Mention users with autocomplete suggestions +- **Trending Posts** - Discover popular content + +### Authentication โœ… (Supabase) +- **Supabase Auth** - Email/password with magic links +- **Email Verification** - Confirm email before login +- **Password Reset** - Secure reset via email +- **Protected Routes** - Frontend and backend guards +- **Session Management** - Secure cookie-based sessions + +### UI/UX โœ… +- **Dark Mode** - Beautiful dark theme by default +- **Responsive Design** - Works on mobile and desktop +- **Media Lightbox** - Full-screen media viewing +- **Quote Generator** - AI-powered quote suggestions +- **Emoji Picker** - Rich emoji support ## ๐Ÿ›  Tech Stack @@ -86,9 +114,11 @@ yarn dev |-------|------------| | **Frontend** | Next.js 16, React 19, Tailwind CSS 4, Apollo Client | | **Backend** | Apollo Server 5, Express 5, TypeScript 5.9 | -| **Database** | PostgreSQL 16, Prisma 7 ORM | -| **Auth** | JWT, httpOnly Cookies, Refresh Tokens | -| **DevOps** | Docker, Docker Compose | +| **Database** | PostgreSQL 16 (Supabase), Prisma 7 ORM | +| **Auth** | Supabase Auth (email + password) | +| **Real-time** | Socket.io for live updates | +| **Storage** | Local file storage / CDN-ready | +| **DevOps** | Docker, Railway, Vercel | ## ๐Ÿ“š Documentation diff --git a/backend/.env.example b/backend/.env.example index a2c1c8c..40535fc 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,7 +1,21 @@ -# Required +# Database Configuration +# For local development (Docker): DATABASE_URL=postgresql://postgres:threads@localhost:5432/threads +DIRECT_URL=postgresql://postgres:threads@localhost:5432/threads + +# For production (Supabase): +# DATABASE_URL=postgresql://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres?pgbouncer=true +# DIRECT_URL=postgresql://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres + +# JWT Secret (min 32 characters) JWT_SECRET=your-super-secret-jwt-key-at-least-32-characters -# Optional +# Server Configuration PORT=8000 FRONTEND_URL=http://localhost:3000 + +# Supabase Storage (optional - when not set, files stored locally) +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_ANON_KEY=your-anon-key +SUPABASE_SERVICE_KEY=your-service-role-key +SUPABASE_BUCKET=threads-clone diff --git a/backend/README.md b/backend/README.md index dd92de4..ad36f41 100644 --- a/backend/README.md +++ b/backend/README.md @@ -8,11 +8,12 @@ Apollo Server 5 GraphQL API with Express 5, Prisma ORM, and JWT authentication. backend/ โ”œโ”€โ”€ src/ โ”‚ โ”œโ”€โ”€ graphql/ -โ”‚ โ”‚ โ”œโ”€โ”€ user/ # User module -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ user.typeDefs.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ user.resolvers.ts -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ user.service.ts +โ”‚ โ”‚ โ”œโ”€โ”€ user/ # User authentication & profiles +โ”‚ โ”‚ โ”œโ”€โ”€ post/ # Posts, likes, bookmarks, reposts +โ”‚ โ”‚ โ”œโ”€โ”€ search/ # Search users, posts, hashtags +โ”‚ โ”‚ โ”œโ”€โ”€ report/ # Report system โ”‚ โ”‚ โ”œโ”€โ”€ context.ts # Auth context & cookies +โ”‚ โ”‚ โ”œโ”€โ”€ errors.ts # Custom error handling โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # Schema aggregation โ”‚ โ”‚ โ””โ”€โ”€ server.ts # Apollo Server setup โ”‚ โ”œโ”€โ”€ lib/ @@ -21,49 +22,69 @@ backend/ โ”œโ”€โ”€ prisma/ โ”‚ โ”œโ”€โ”€ schema.prisma # Database schema โ”‚ โ””โ”€โ”€ migrations/ # Database migrations -โ””โ”€โ”€ generated/ # Generated Prisma client +โ””โ”€โ”€ uploads/ # Media file storage ``` ## ๐Ÿ” Authentication Flow -1. **Login** (`login` mutation) - - Validates credentials - - Generates access token (15min) + refresh token (7 days) - - Sets httpOnly cookies - -2. **API Requests** - - Browser sends cookies automatically - - Context extracts and verifies access token - - User attached to GraphQL context - -3. **Token Refresh** (`refreshToken` mutation) - - Old refresh token deleted (rotation) - - New token pair generated - -4. **Logout** (`logout` mutation) - - Deletes refresh token from database - - Clears cookies +1. **Login** (`login` mutation) โ†’ Sets httpOnly cookies with JWT tokens +2. **API Requests** โ†’ Context extracts and verifies access token +3. **Token Refresh** (`refreshToken`) โ†’ Rotates refresh token +4. **Logout** โ†’ Clears cookies and invalidates tokens ## ๐Ÿ“‹ GraphQL Schema -### Queries (Protected) +### User Queries ```graphql getUsers: [User!]! -getUserById(id: String!): User -getUserByEmail(email: String): User +getUserById(id: ID!): User +getUserByUsername(username: String!): User getCurrentLoggedInUser: User +getSuggestedUsers(first: Int): [User!]! +``` + +### Post Queries +```graphql +getPost(id: ID!): Post +getUserPosts(userId: ID!, first: Int, after: String, filter: PostFilter): PostConnection +getHomeFeed(first: Int, after: String): PostConnection +getTrendingPosts(first: Int, after: String): PostConnection +getMyBookmarks(first: Int, after: String): PostConnection +getPostsByHashtag(tag: String!, first: Int, after: String): PostConnection ``` -### Mutations +### Search Queries ```graphql -# Public -createUser(firstName: String!, lastName: String, email: String!, password: String!): User! +searchUsers(query: String!, first: Int, after: String): UserConnection +searchPosts(query: String!, first: Int, after: String): PostConnection +searchHashtags(query: String!, first: Int): [Hashtag!]! +getTrendingHashtags(first: Int): [Hashtag!]! +``` + +### User Mutations +```graphql +createUser(input: CreateUserInput!): User! login(email: String!, password: String!): AuthResponse! +logout: Boolean! refreshToken: AuthResponse! +updateUser(input: UpdateUserInput!): User! +followUser(userId: ID!): Boolean! +unfollowUser(userId: ID!): Boolean! +blockUser(userId: ID!): Boolean! +unblockUser(userId: ID!): Boolean! +``` -# Protected -logout: Boolean! -logoutAll: Boolean! +### Post Mutations +```graphql +createPost(input: CreatePostInput!): Post! +updatePost(postId: ID!, input: UpdatePostInput!): Post! +deletePost(postId: ID!): Boolean! +likePost(postId: ID!): Boolean! +unlikePost(postId: ID!): Boolean! +bookmarkPost(postId: ID!): Boolean! +unbookmarkPost(postId: ID!): Boolean! +repostPost(postId: ID!): Boolean! +unrepostPost(postId: ID!): Boolean! ``` ## ๐Ÿ”ง Environment Variables @@ -81,20 +102,11 @@ FRONTEND_URL=http://localhost:3000 ## ๐Ÿ›  Development ```bash -# Install dependencies -yarn install - -# Generate Prisma client -npx prisma generate - -# Run migrations -npx prisma migrate dev - -# Start dev server -yarn dev - -# Open Prisma Studio -npx prisma studio +yarn install # Install dependencies +npx prisma generate # Generate Prisma client +npx prisma migrate dev # Run migrations +yarn dev # Start dev server +npx prisma studio # Open Prisma Studio ``` ## ๐Ÿ“ก API Endpoints @@ -104,19 +116,22 @@ npx prisma studio | `GET /` | Health check | | `POST /graphql` | GraphQL API | | `GET /graphql` | Apollo Sandbox | - -## ๐Ÿงช Testing with cURL - -```bash -# Login -curl -X POST http://localhost:8000/graphql \ - -H "Content-Type: application/json" \ - -c cookies.txt \ - -d '{"query": "mutation { login(email: \"test@example.com\", password: \"test123\") { accessToken user { id firstName } } }"}' - -# Protected query (with cookies) -curl -X POST http://localhost:8000/graphql \ - -H "Content-Type: application/json" \ - -b cookies.txt \ - -d '{"query": "query { getCurrentLoggedInUser { id firstName email } }"}' -``` +| `POST /upload` | Media file upload | +| `GET /uploads/:file` | Serve uploaded files | + +## ๐Ÿ“ฆ Database Models + +- **User** - User accounts and profiles +- **Post** - Threads/posts with content and metadata +- **PostMedia** - Images, videos, GIFs attached to posts +- **PostLike** - User likes on posts +- **Bookmark** - Saved posts +- **Repost** - Reposts/shares +- **Follow** - User follow relationships +- **Hashtag** - Post hashtags with usage counts +- **PostHashtag** - Post-hashtag associations +- **PostMention** - User mentions in posts +- **Notification** - Activity notifications +- **RefreshToken** - Auth refresh tokens +- **Block** - Blocked users +- **Report** - User/post reports diff --git a/backend/package.json b/backend/package.json index 323b677..fb78250 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,6 +5,7 @@ "license": "MIT", "type": "module", "scripts": { + "build": "tsc", "start": "node dist/src/index.js", "dev": "tsc-watch --onSuccess \"npm start\"" }, @@ -24,12 +25,19 @@ "@as-integrations/express5": "^1.1.2", "@prisma/adapter-pg": "^7.2.0", "@prisma/client": "^7.2.0", + "@supabase/supabase-js": "^2.90.0", + "@types/multer": "^2.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.2.1", "graphql": "^16.12.0", "jsonwebtoken": "^9.0.3", - "pg": "^8.16.3" + "multer": "^2.0.2", + "pg": "^8.16.3", + "socket.io": "^4.8.3" + }, + "prisma": { + "seed": "npx tsx prisma/seed.ts" } } diff --git a/backend/prisma.config.ts b/backend/prisma.config.ts index 952ca5c..4d43273 100644 --- a/backend/prisma.config.ts +++ b/backend/prisma.config.ts @@ -5,8 +5,11 @@ export default defineConfig({ schema: 'prisma/schema.prisma', migrations: { path: 'prisma/migrations', + seed: 'npx tsx prisma/seed.ts', }, datasource: { - url: env('DATABASE_URL'), + // Use DIRECT_URL (Session mode pooler) for Prisma CLI operations + // Falls back to DATABASE_URL if DIRECT_URL not set + url: env('DIRECT_URL') || env('DATABASE_URL'), }, }) diff --git a/backend/prisma/migrations/20260107053405_add_like_comment_search_features/migration.sql b/backend/prisma/migrations/20260107053405_add_like_comment_search_features/migration.sql new file mode 100644 index 0000000..b7d63e0 --- /dev/null +++ b/backend/prisma/migrations/20260107053405_add_like_comment_search_features/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "password_reset_tokens" ( + "id" TEXT NOT NULL, + "token" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "expires_at" TIMESTAMP(3) NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "password_reset_tokens_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "password_reset_tokens_token_key" ON "password_reset_tokens"("token"); + +-- AddForeignKey +ALTER TABLE "password_reset_tokens" ADD CONSTRAINT "password_reset_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20260108045847_create_report_table/migration.sql b/backend/prisma/migrations/20260108045847_create_report_table/migration.sql new file mode 100644 index 0000000..ef6f82c --- /dev/null +++ b/backend/prisma/migrations/20260108045847_create_report_table/migration.sql @@ -0,0 +1,42 @@ +-- CreateTable +CREATE TABLE "reposts" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "post_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "reposts_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "reports" ( + "id" TEXT NOT NULL, + "category" TEXT NOT NULL, + "description" TEXT NOT NULL, + "attachment_url" TEXT, + "device_info" JSONB, + "status" TEXT NOT NULL DEFAULT 'PENDING', + "user_id" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "reports_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "reposts_post_id_idx" ON "reposts"("post_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "reposts_user_id_post_id_key" ON "reposts"("user_id", "post_id"); + +-- CreateIndex +CREATE INDEX "reports_user_id_idx" ON "reports"("user_id"); + +-- AddForeignKey +ALTER TABLE "reposts" ADD CONSTRAINT "reposts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "reposts" ADD CONSTRAINT "reposts_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "posts"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "reports" ADD CONSTRAINT "reports_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 4625da9..866318e 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -40,14 +40,14 @@ enum MediaType { // Users table model User { - id String @id @default(uuid()) + id String @id // Set manually from Supabase Auth ID firstName String @map("first_name") lastName String? @map("last_name") username String @unique profileImageUrl String? @map("profile_image_url") email String @unique - password String - salt String + // Removed password/salt - managed by Supabase Auth + is_verified Boolean @default(false) is_private Boolean @default(false) status String? @@ -58,10 +58,6 @@ model User { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - // Auth Relations - refreshTokens RefreshToken[] - passwordResetTokens PasswordResetToken[] - // Social Relations followers Follow[] @relation("following") following Follow[] @relation("follower") @@ -77,33 +73,13 @@ model User { likes PostLike[] bookmarks Bookmark[] mentionedIn PostMention[] @relation("mentionedUser") + reposts Repost[] + reports Report[] @@map("users") } -// Refresh tokens table -model RefreshToken { - id String @id @default(uuid()) - token String @unique - userId String @map("user_id") - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - expiresAt DateTime @map("expires_at") - createdAt DateTime @default(now()) @map("created_at") - - @@map("refresh_tokens") -} - -// Password reset tokens table -model PasswordResetToken { - id String @id @default(uuid()) - token String @unique - userId String @map("user_id") - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - expiresAt DateTime @map("expires_at") - createdAt DateTime @default(now()) @map("created_at") - - @@map("password_reset_tokens") -} +// Removed RefreshToken and PasswordResetToken tables // ===================== // SOCIAL RELATIONSHIPS @@ -173,6 +149,7 @@ model Post { media PostMedia[] hashtags PostHashtag[] mentions PostMention[] + reposts Repost[] @@index([authorId]) @@index([parentPostId]) @@ -268,3 +245,41 @@ model PostMention { @@unique([postId, mentionedUserId]) @@map("post_mentions") } + +// ===================== +// REPOSTS +// ===================== + +// Reposts (sharing other's posts) +model Repost { + id String @id @default(uuid()) + userId String @map("user_id") + postId String @map("post_id") + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) @map("created_at") + + @@unique([userId, postId]) + @@index([postId]) + @@map("reposts") +} + +// ===================== +// REPORTS +// ===================== + +model Report { + id String @id @default(uuid()) + category String + description String @db.Text + attachmentUrl String? @map("attachment_url") + deviceInfo Json? @map("device_info") + status String @default("PENDING") // PENDING, RESOLVED, DISMISSED + userId String? @map("user_id") + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([userId]) + @@map("reports") +} diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts new file mode 100644 index 0000000..bde4ce7 --- /dev/null +++ b/backend/prisma/seed.ts @@ -0,0 +1,78 @@ +import { prisma } from "../src/lib/prisma.js"; +import { userService } from "../src/graphql/user/user.service.js"; +import { randomUUID } from "crypto"; + +async function main() { + console.log("๐ŸŒฑ Starting seed..."); + + // Clear existing data + await prisma.notification.deleteMany(); + await prisma.postLike.deleteMany(); + await prisma.postMention.deleteMany(); + await prisma.postHashtag.deleteMany(); + await prisma.repost.deleteMany(); + await prisma.bookmark.deleteMany(); + await prisma.postMedia.deleteMany(); + await prisma.post.deleteMany(); + await prisma.block.deleteMany(); + await prisma.follow.deleteMany(); + await prisma.user.deleteMany(); + + // Create Users + console.log("Creating 20 mock users..."); + const users = []; + for (let i = 0; i < 20; i++) { + const firstName = `User${i + 1}`; + const email = `user${i + 1}@example.com`; + const id = randomUUID(); // Generate a fake Supabase ID + + // We strictly use userService.createUser which now requires just profile data + const user = await userService.createUser({ + id, + firstName, + email, + username: `user${i + 1}`, + profileImageUrl: `https://api.dicebear.com/7.x/avataaars/svg?seed=${i}`, + }); + users.push(user); + } + + // Follows + console.log("Creating follow relationships..."); + for (const user of users) { + const randomUsers = users.filter((u) => u.id !== user.id); + const followCount = Math.floor(Math.random() * 5) + 1; + + for (let i = 0; i < followCount; i++) { + const target = randomUsers[Math.floor(Math.random() * randomUsers.length)]; + try { + await userService.followUser(user.id, target.id); + } catch (e) { + // ignore duplicate follows + } + } + } + + // Posts + console.log("Creating 100 mock posts..."); + for (let i = 0; i < 100; i++) { + const author = users[Math.floor(Math.random() * users.length)]; + await prisma.post.create({ + data: { + authorId: author.id, + content: `This is post #${i + 1} from ${author.username}. #threads #clone`, + } + }); + } + + console.log("โœ… Seed completed!"); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/backend/src/graphql/context.ts b/backend/src/graphql/context.ts index 10bb651..294a8fb 100644 --- a/backend/src/graphql/context.ts +++ b/backend/src/graphql/context.ts @@ -1,60 +1,49 @@ -import JWT from "jsonwebtoken"; import { userService } from "./user/user.service.js"; import { Errors } from "./errors.js"; import type { User } from "../../generated/prisma/client.js"; import type { Request, Response } from "express"; +import { supabase } from "../lib/supabase.js"; +import type { User as SupabaseUser } from "@supabase/supabase-js"; export interface GraphQLContext { - user: User | null; + user: User | null; // Database Profile + supabaseUser: SupabaseUser | null; // Supabase Auth User req: Request; res: Response; } -export interface JWTPayload { - id: string; - email: string; -} - -const JWT_SECRET = process.env.JWT_SECRET; - -// Cookie options for security -export const COOKIE_OPTIONS = { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "lax" as const, - path: "/", -}; - -export const ACCESS_TOKEN_COOKIE = "__threads_graphql_demo_access_token"; -export const REFRESH_TOKEN_COOKIE = "__threads_graphql_demo_refresh_token"; - export async function createContext( req: Request, res: Response ): Promise { - // Get access token from cookie - const token = req.cookies?.[ACCESS_TOKEN_COOKIE]; - - // If no token or JWT_SECRET, return unauthenticated context - if (!token || !JWT_SECRET) { - return { user: null, req, res }; + const authHeader = req.headers.authorization; + let user: User | null = null; + let supabaseUser: SupabaseUser | null = null; + + if (authHeader && authHeader.startsWith("Bearer ")) { + const token = authHeader.replace("Bearer ", ""); + // Verify token with Supabase + const { data: { user: authUser }, error } = await supabase.auth.getUser(token); + + if (error) { + console.error("[AUTH] Supabase verification error:", error.message); + } + + if (authUser && !error) { + supabaseUser = authUser; + // Try to find corresponding profile in our DB + user = await userService.getUserById(authUser.id); + } else if (!authUser) { + console.warn("[AUTH] No user found for provided token"); + } + } else if (authHeader) { + console.warn("[AUTH] Invalid Authorization header format"); } - try { - // Verify and decode the JWT - const decoded = JWT.verify(token, JWT_SECRET) as JWTPayload; - - // Fetch user from database - const user = await userService.getUserById(decoded.id); - - return { user, req, res }; - } catch { - // Token invalid or expired - return { user: null, req, res }; - } + return { user, supabaseUser, req, res }; } -// Helper to check if user is authenticated +// Helper to check if user is authenticated (has profile) export function requireAuth(context: GraphQLContext): User { if (!context.user) { throw Errors.unauthenticated(); @@ -62,27 +51,10 @@ export function requireAuth(context: GraphQLContext): User { return context.user; } -// Helper to set auth cookies -export function setAuthCookies( - res: Response, - accessToken: string, - refreshToken: string -): void { - // Access token - shorter expiry - res.cookie(ACCESS_TOKEN_COOKIE, accessToken, { - ...COOKIE_OPTIONS, - maxAge: 15 * 60 * 1000, // 15 minutes - }); - - // Refresh token - longer expiry - res.cookie(REFRESH_TOKEN_COOKIE, refreshToken, { - ...COOKIE_OPTIONS, - maxAge: 120 * 24 * 60 * 60 * 1000, // 120 days - }); -} - -// Helper to clear auth cookies -export function clearAuthCookies(res: Response): void { - res.clearCookie(ACCESS_TOKEN_COOKIE, COOKIE_OPTIONS); - res.clearCookie(REFRESH_TOKEN_COOKIE, COOKIE_OPTIONS); +// Helper to check if user has valid Supabase session (even if no profile) +export function requireSupabaseAuth(context: GraphQLContext): SupabaseUser { + if (!context.supabaseUser) { + throw Errors.unauthenticated("Invalid or missing session"); + } + return context.supabaseUser; } diff --git a/backend/src/graphql/index.ts b/backend/src/graphql/index.ts index a677058..24e203f 100644 --- a/backend/src/graphql/index.ts +++ b/backend/src/graphql/index.ts @@ -4,6 +4,8 @@ import { postTypeDefs } from "./post/post.typeDefs.js"; import { postResolvers } from "./post/post.resolvers.js"; import { searchTypeDefs } from "./search/search.typeDefs.js"; import { searchResolvers } from "./search/search.resolvers.js"; +import { reportTypeDefs } from "./report/report.typeDefs.js"; +import { reportResolvers } from "./report/report.resolvers.js"; // Base types that can be extended const baseTypeDefs = /* GraphQL */ ` @@ -17,7 +19,7 @@ const baseTypeDefs = /* GraphQL */ ` `; // Merge all typeDefs -export const typeDefs = [baseTypeDefs, userTypeDefs, postTypeDefs, searchTypeDefs]; +export const typeDefs = [baseTypeDefs, userTypeDefs, postTypeDefs, searchTypeDefs, reportTypeDefs]; // Deep merge resolvers function mergeResolvers(...resolverArrays: Record[]) { @@ -36,4 +38,4 @@ function mergeResolvers(...resolverArrays: Record[]) { } // Merge all resolvers -export const resolvers = mergeResolvers(userResolvers, postResolvers, searchResolvers); +export const resolvers = mergeResolvers(userResolvers, postResolvers, searchResolvers, reportResolvers); diff --git a/backend/src/graphql/post/post.resolvers.ts b/backend/src/graphql/post/post.resolvers.ts index 6b238c8..f811edf 100644 --- a/backend/src/graphql/post/post.resolvers.ts +++ b/backend/src/graphql/post/post.resolvers.ts @@ -47,6 +47,16 @@ export const postResolvers = { return postService.getIsBookmarked(parent.id, context.user?.id ?? null); }, + // Is current user reposting this post + isReposted: async (parent: PostParent, _: unknown, context: GraphQLContext) => { + return postService.getIsReposted(parent.id, context.user?.id ?? null); + }, + + // Reposts count + repostsCount: async (parent: PostParent) => { + return postService.getRepostsCount(parent.id); + }, + // Get post hashtags hashtags: async (parent: PostParent) => { return postService.getPostHashtags(parent.id); @@ -97,9 +107,10 @@ export const postResolvers = { // Get post replies (PUBLIC - guests can view) getPostReplies: async ( _: unknown, - args: { postId: string; first?: number; after?: string } + args: { postId: string; first?: number; after?: string }, + context: GraphQLContext ) => { - return postService.getPostReplies(args.postId, { + return postService.getPostReplies(args.postId, context.user?.id ?? null, { first: args.first ?? 20, after: args.after, }); @@ -108,13 +119,13 @@ export const postResolvers = { // Get user posts (PUBLIC - but respects privacy settings) getUserPosts: async ( _: unknown, - args: { userId: string; first?: number; after?: string }, + args: { userId: string; filter?: "THREADS" | "REPLIES" | "REPOSTS" | "MEDIA" | "BOOKMARKS"; first?: number; after?: string }, context: GraphQLContext ) => { return postService.getUserPosts(args.userId, context.user?.id ?? null, { first: args.first ?? 20, after: args.after, - }); + }, args.filter); }, // Get home feed (PROTECTED - authenticated users only) @@ -141,6 +152,18 @@ export const postResolvers = { }); }, + // Get public feed (PUBLIC - chronological order for guests) + getPublicFeed: async ( + _: unknown, + args: { first?: number; after?: string }, + context: GraphQLContext + ) => { + return postService.getPublicFeed(context.user?.id || null, { + first: args.first ?? 20, + after: args.after, + }); + }, + // Get posts by hashtag (PUBLIC - guests can view) getPostsByHashtag: async ( _: unknown, @@ -249,5 +272,25 @@ export const postResolvers = { const user = requireAuth(context); return postService.unbookmarkPost(user.id, args.postId); }, + + // Repost post (PROTECTED) + repostPost: async ( + _: unknown, + args: { postId: string }, + context: GraphQLContext + ) => { + const user = requireAuth(context); + return postService.repostPost(user.id, args.postId); + }, + + // Unrepost post (PROTECTED) + unrepostPost: async ( + _: unknown, + args: { postId: string }, + context: GraphQLContext + ) => { + const user = requireAuth(context); + return postService.unrepostPost(user.id, args.postId); + }, }, }; diff --git a/backend/src/graphql/post/post.service.ts b/backend/src/graphql/post/post.service.ts index 96b5482..6693184 100644 --- a/backend/src/graphql/post/post.service.ts +++ b/backend/src/graphql/post/post.service.ts @@ -1,5 +1,6 @@ import { prisma } from "../../lib/prisma.js"; import { Errors } from "../errors.js"; +import { emitToAll, emitToUser } from "../../lib/socket.js"; import type { PostVisibility, MediaType, Prisma } from "../../../generated/prisma/client.js"; // ===================== @@ -57,9 +58,10 @@ function extractMentions(content: string): string[] { */ function decodeCursor(cursor: string): { createdAt: Date; id: string } { const decoded = Buffer.from(cursor, "base64").toString("utf-8"); - const parts = decoded.split(":"); - const createdAt = parts[0] ?? ""; - const id = parts[1] ?? ""; + // Split by the last colon, as ISO dates contain colons + const lastColonIndex = decoded.lastIndexOf(":"); + const createdAt = decoded.substring(0, lastColonIndex); + const id = decoded.substring(lastColonIndex + 1); return { createdAt: new Date(createdAt), id }; } @@ -84,6 +86,24 @@ function getEndCursor(edges: T[]): string | null { // ===================== export const postService = { + // ===================== + // HELPER: Get blocked user IDs (blocked by me OR blocked me) + // ===================== + async getBlockedUserIds(userId: string) { + const blocks = await prisma.block.findMany({ + where: { + OR: [{ blockerId: userId }, { blockedId: userId }], + }, + select: { blockerId: true, blockedId: true }, + }); + + const blockedIds = new Set(); + for (const block of blocks) { + if (block.blockerId === userId) blockedIds.add(block.blockedId); + if (block.blockedId === userId) blockedIds.add(block.blockerId); + } + return Array.from(blockedIds); + }, // ===================== // CREATE POST // ===================== @@ -113,14 +133,14 @@ export const postService = { ...postData, ...(media && media.length > 0 ? { - media: { - create: media.map((m, index) => ({ - mediaType: m.mediaType, - mediaUrl: m.mediaUrl, - position: m.position ?? index, - })), - }, - } + media: { + create: media.map((m, index) => ({ + mediaType: m.mediaType, + mediaUrl: m.mediaUrl, + position: m.position ?? index, + })), + }, + } : {}), }; @@ -176,6 +196,9 @@ export const postService = { } } + // Emit real-time event for new post + emitToAll("post:created", { post }); + return post; }); }, @@ -218,10 +241,22 @@ export const postService = { }); } - return prisma.post.findUnique({ + const fullReply = await prisma.post.findUnique({ where: { id: reply.id }, include: { author: true, parentPost: true, media: true }, }); + + // Emit real-time event for reply + emitToAll("post:replied", { parentPostId, reply: fullReply }); + + // Notify parent author + if (parentPost.authorId !== authorId) { + emitToUser(parentPost.authorId, "notification:new", { + notification: { type: "REPLY", entityId: reply.id, actorId: authorId }, + }); + } + + return fullReply; }, // ===================== @@ -292,6 +327,21 @@ export const postService = { throw Errors.notFound("Post"); } + // Check if blocked + if (currentUserId) { + const blocks = await prisma.block.findFirst({ + where: { + OR: [ + { blockerId: post.authorId, blockedId: currentUserId }, + { blockerId: currentUserId, blockedId: post.authorId } + ] + } + }); + if (blocks) { + throw Errors.forbidden("Post not available"); + } + } + // Check visibility for private posts if (post.visibility === "PRIVATE" && post.authorId !== currentUserId) { throw Errors.forbidden("This post is private"); @@ -301,13 +351,13 @@ export const postService = { if (post.author.is_private && post.authorId !== currentUserId) { const isFollowing = currentUserId ? await prisma.follow.findUnique({ - where: { - followerId_followingId: { - followerId: currentUserId, - followingId: post.authorId, - }, + where: { + followerId_followingId: { + followerId: currentUserId, + followingId: post.authorId, }, - }) + }, + }) : null; if (!isFollowing) { @@ -321,11 +371,14 @@ export const postService = { // ===================== // GET POST REPLIES (with pagination) // ===================== - async getPostReplies(postId: string, pagination: PaginationInput = {}) { + async getPostReplies(postId: string, currentUserId: string | null, pagination: PaginationInput = {}) { const { first = 20, after } = pagination; + const blockedIds = currentUserId ? await this.getBlockedUserIds(currentUserId) : []; + const whereClause: Prisma.PostWhereInput = { parentPostId: postId, + authorId: { notIn: blockedIds }, // Filter blocked }; if (after) { @@ -362,12 +415,13 @@ export const postService = { }, // ===================== - // GET USER POSTS (with pagination) + // GET USER POSTS (with pagination and optional filter) // ===================== async getUserPosts( userId: string, currentUserId: string | null, - pagination: PaginationInput = {} + pagination: PaginationInput = {}, + filter: "THREADS" | "REPLIES" | "REPOSTS" | "MEDIA" | "BOOKMARKS" = "THREADS" ) { const { first = 20, after } = pagination; @@ -385,13 +439,13 @@ export const postService = { if (targetUser.is_private && userId !== currentUserId) { const isFollowing = currentUserId ? await prisma.follow.findUnique({ - where: { - followerId_followingId: { - followerId: currentUserId, - followingId: userId, - }, + where: { + followerId_followingId: { + followerId: currentUserId, + followingId: userId, }, - }) + }, + }) : null; if (!isFollowing) { @@ -399,21 +453,332 @@ export const postService = { } } - const whereClause: Prisma.PostWhereInput = { - authorId: userId, - parentPostId: null, // Only top-level posts, not replies - }; + // Security check: If viewing another user's profile, check if blocked + if (currentUserId && userId !== currentUserId) { + const blocks = await prisma.block.findFirst({ + where: { + OR: [ + { blockerId: userId, blockedId: currentUserId }, // They blocked me + { blockerId: currentUserId, blockedId: userId } // I blocked them + ] + } + }); + if (blocks) { + return { edges: [], pageInfo: { hasNextPage: false, endCursor: null } }; + } + } + let cursorDate: Date | undefined; if (after) { - const { createdAt, id } = decodeCursor(after); - whereClause.OR = [ - { createdAt: { lt: createdAt } }, - { createdAt, id: { lt: id } }, + const { createdAt } = decodeCursor(after); + cursorDate = createdAt; + } + + // --------------------------------------------------------- + // CASE 1: THREADS (Original parent posts + Reposts) - DEFAULT behavior + // --------------------------------------------------------- + if (filter === "THREADS") { + // Fetch user's original posts (no replies) + const postsWhereClause: Prisma.PostWhereInput = { + authorId: userId, + parentPostId: null, + ...(cursorDate && { createdAt: { lt: cursorDate } }), + }; + + const originalPosts = await prisma.post.findMany({ + where: postsWhereClause, + take: first + 1, + orderBy: [{ createdAt: "desc" }, { id: "desc" }], + include: { + author: true, + media: { orderBy: { position: "asc" } }, + }, + }); + + // Fetch user's reposts + const repostsWhereClause: Prisma.RepostWhereInput = { + userId: userId, + post: { parentPostId: null }, + ...(cursorDate && { createdAt: { lt: cursorDate } }), + }; + + const reposts = await prisma.repost.findMany({ + where: repostsWhereClause, + take: first + 1, + orderBy: [{ createdAt: "desc" }], + include: { + user: true, + post: { + include: { + author: true, + media: { orderBy: { position: "asc" } }, + }, + }, + }, + }); + + // Merge posts and reposts + const feedItems = [ + ...originalPosts.map(post => ({ + post: { ...post, repostedBy: null as typeof reposts[0]["user"] | null }, + sortDate: post.createdAt, + })), + ...reposts.map(repost => ({ + post: { ...repost.post, repostedBy: repost.user }, + sortDate: repost.createdAt, + })), ]; + + // Sort by date descending + feedItems.sort((a, b) => b.sortDate.getTime() - a.sortDate.getTime()); + + // Slice and paginate + const hasNextPage = feedItems.length > first; + const edges = feedItems.slice(0, first).map((item) => ({ + cursor: encodeCursor(item.sortDate, item.post.id), + node: item.post, + })); + + return { + edges, + pageInfo: { + hasNextPage, + endCursor: getEndCursor(edges), + }, + }; } - const posts = await prisma.post.findMany({ - where: whereClause, + // --------------------------------------------------------- + // CASE 2: REPLIES (Posts that are responses to others) + // --------------------------------------------------------- + if (filter === "REPLIES") { + const repliesWhereClause: Prisma.PostWhereInput = { + authorId: userId, + parentPostId: { not: null }, // MUST be a reply + ...(cursorDate && { createdAt: { lt: cursorDate } }), + }; + + const replies = await prisma.post.findMany({ + where: repliesWhereClause, + take: first + 1, + orderBy: [{ createdAt: "desc" }, { id: "desc" }], + include: { + author: true, + media: { orderBy: { position: "asc" } }, + parentPost: { include: { author: true } } // Include parent context + }, + }); + + const hasNextPage = replies.length > first; + const edges = replies.slice(0, first).map((post) => ({ + cursor: encodeCursor(post.createdAt, post.id), + node: post, + })); + + return { + edges, + pageInfo: { + hasNextPage, + endCursor: getEndCursor(edges), + }, + }; + } + + // --------------------------------------------------------- + // CASE 3: REPOSTS (Only Reposts) + // --------------------------------------------------------- + if (filter === "REPOSTS") { + const repostsWhereClause: Prisma.RepostWhereInput = { + userId: userId, + ...(cursorDate && { createdAt: { lt: cursorDate } }), + }; + + const reposts = await prisma.repost.findMany({ + where: repostsWhereClause, + take: first + 1, + orderBy: [{ createdAt: "desc" }], + include: { + user: true, + post: { + include: { + author: true, + media: { orderBy: { position: "asc" } }, + }, + }, + }, + }); + + const hasNextPage = reposts.length > first; + const edges = reposts.slice(0, first).map((repost) => ({ + cursor: encodeCursor(repost.createdAt, repost.post.id), + node: { ...repost.post, repostedBy: repost.user }, + })); + + return { + edges, + pageInfo: { + hasNextPage, + endCursor: getEndCursor(edges), + }, + }; + } + + // --------------------------------------------------------- + // CASE 4: MEDIA (Posts with images/videos) + // --------------------------------------------------------- + if (filter === "MEDIA") { + const mediaWhereClause: Prisma.PostWhereInput = { + authorId: userId, + media: { some: {} }, // Has at least one media item + ...(cursorDate && { createdAt: { lt: cursorDate } }), + }; + + const mediaPosts = await prisma.post.findMany({ + where: mediaWhereClause, + take: first + 1, + orderBy: [{ createdAt: "desc" }, { id: "desc" }], + include: { + author: true, + media: { orderBy: { position: "asc" } }, + }, + }); + + const hasNextPage = mediaPosts.length > first; + const edges = mediaPosts.slice(0, first).map((post) => ({ + cursor: encodeCursor(post.createdAt, post.id), + node: post, + })); + + return { + edges, + pageInfo: { + hasNextPage, + endCursor: getEndCursor(edges), + }, + }; + } + + // --------------------------------------------------------- + // CASE 5: BOOKMARKS (Saved posts - PRIVATE to user) + // --------------------------------------------------------- + if (filter === "BOOKMARKS") { + // Security check: Only allow viewing own bookmarks + if (userId !== currentUserId) { + // Return empty if trying to view someone else's bookmarks + return { edges: [], pageInfo: { hasNextPage: false, endCursor: null } }; + } + + const bookmarksWhereClause: Prisma.BookmarkWhereInput = { + userId: userId, + ...(cursorDate && { createdAt: { lt: cursorDate } }), + }; + + const bookmarks = await prisma.bookmark.findMany({ + where: bookmarksWhereClause, + take: first + 1, + orderBy: [{ createdAt: "desc" }], + include: { + post: { + include: { + author: true, + media: { orderBy: { position: "asc" } }, + }, + }, + }, + }); + + const hasNextPage = bookmarks.length > first; + const edges = bookmarks.slice(0, first).map((bookmark) => ({ + cursor: encodeCursor(bookmark.createdAt, bookmark.post.id), // Use bookmark creation time for sorting + node: bookmark.post, + })); + + return { + edges, + pageInfo: { + hasNextPage, + endCursor: getEndCursor(edges), + }, + }; + } + + // Fallback empty return if unknown filter + return { edges: [], pageInfo: { hasNextPage: false, endCursor: null } }; + }, + + // ===================== + // GET HOME FEED (posts from followed users + their reposts) + // ===================== + async getHomeFeed(_userId: string, pagination: PaginationInput = {}) { + const { first = 20, after } = pagination; + + // Get blocked users + const blockedIds = await this.getBlockedUserIds(_userId); + + // Get the user's following list + const following = await prisma.follow.findMany({ + where: { followerId: _userId }, + select: { followingId: true }, + }); + + // Decode cursor if present + let cursorDate: Date | undefined; + if (after) { + const { createdAt } = decodeCursor(after); + cursorDate = createdAt; + } + + // If user follows no one, return top posts (excluding blocked) + if (following.length === 0) { + const whereClause: Prisma.PostWhereInput = { + visibility: "PUBLIC", + parentPostId: null, + authorId: { notIn: blockedIds }, + // Last 7 days to keep it fresh + createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }, + // Apply cursor filter if present + ...(cursorDate && { createdAt: { lt: cursorDate, gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) } }), + }; + + const topPosts = await prisma.post.findMany({ + where: whereClause, + include: { + author: true, + media: { orderBy: { position: "asc" } }, + _count: { select: { likes: true, replies: true, reposts: true } }, + }, + take: first + 1, + orderBy: [{ createdAt: "desc" }, { id: "desc" }], + }); + + const hasNextPage = topPosts.length > first; + const edges = topPosts.slice(0, first).map((post) => ({ + cursor: encodeCursor(post.createdAt, post.id), + node: post, + })); + + return { + edges, + pageInfo: { + hasNextPage, + endCursor: getEndCursor(edges), + }, + }; + } + + const followingIds = following.map(f => f.followingId).filter(id => !blockedIds.includes(id)); + followingIds.push(_userId); // Include own posts + + // Fetch original posts + const postsWhereClause: Prisma.PostWhereInput = { + parentPostId: null, + visibility: { in: ["PUBLIC", "FOLLOWERS"] }, + authorId: { in: followingIds }, + ...(cursorDate && { createdAt: { lt: cursorDate } }), + }; + + const originalPosts = await prisma.post.findMany({ + where: postsWhereClause, take: first + 1, orderBy: [{ createdAt: "desc" }, { id: "desc" }], include: { @@ -422,10 +787,58 @@ export const postService = { }, }); - const hasNextPage = posts.length > first; - const edges = posts.slice(0, first).map((post) => ({ - cursor: encodeCursor(post.createdAt, post.id), - node: post, + // Fetch reposts from followed users + const repostsWhereClause: Prisma.RepostWhereInput = { + userId: { in: followingIds }, + post: { + parentPostId: null, + visibility: "PUBLIC", + }, + ...(cursorDate && { createdAt: { lt: cursorDate } }), + }; + + const reposts = await prisma.repost.findMany({ + where: repostsWhereClause, + take: first + 1, + orderBy: [{ createdAt: "desc" }], + include: { + user: true, + post: { + include: { + author: true, + media: { orderBy: { position: "asc" } }, + }, + }, + }, + }); + + // Merge posts and reposts, adding repostedBy info + const feedItems = [ + ...originalPosts.map(post => ({ + post: { ...post, repostedBy: null as typeof reposts[0]["user"] | null }, + sortDate: post.createdAt, + })), + ...reposts.map(repost => ({ + post: { ...repost.post, repostedBy: repost.user }, + sortDate: repost.createdAt, + })), + ]; + + // Sort by date descending + feedItems.sort((a, b) => b.sortDate.getTime() - a.sortDate.getTime()); + + // Deduplicate (same post might appear as original and as repost) + const seenPostIds = new Set(); + const uniqueFeedItems = feedItems.filter(item => { + if (seenPostIds.has(item.post.id)) return false; + seenPostIds.add(item.post.id); + return true; + }); + + const hasNextPage = uniqueFeedItems.length > first; + const edges = uniqueFeedItems.slice(0, first).map((item) => ({ + cursor: encodeCursor(item.sortDate, item.post.id), + node: item.post, })); return { @@ -438,37 +851,29 @@ export const postService = { }, // ===================== - // GET HOME FEED (posts from followed users) + // GET PUBLIC FEED (public posts + reposts, sorted by createdAt for guests) // ===================== - async getHomeFeed(userId: string, pagination: PaginationInput = {}) { + async getPublicFeed(currentUserId: string | null, pagination: PaginationInput = {}) { const { first = 20, after } = pagination; - // Get IDs of users the current user follows - const following = await prisma.follow.findMany({ - where: { followerId: userId }, - select: { followingId: true }, - }); - - const followingIds = following.map((f) => f.followingId); - // Include own posts in feed - followingIds.push(userId); - - const whereClause: Prisma.PostWhereInput = { - authorId: { in: followingIds }, - parentPostId: null, // Only top-level posts - visibility: { in: ["PUBLIC", "FOLLOWERS"] }, - }; + const blockedIds = currentUserId ? await this.getBlockedUserIds(currentUserId) : []; + let cursorDate: Date | undefined; if (after) { - const { createdAt, id } = decodeCursor(after); - whereClause.OR = [ - { createdAt: { lt: createdAt } }, - { createdAt, id: { lt: id } }, - ]; + const { createdAt } = decodeCursor(after); + cursorDate = createdAt; } - const posts = await prisma.post.findMany({ - where: whereClause, + // Fetch original public posts + const postsWhereClause: Prisma.PostWhereInput = { + parentPostId: null, + visibility: "PUBLIC", + authorId: { notIn: blockedIds }, // Filter blocked + ...(cursorDate && { createdAt: { lt: cursorDate } }), + }; + + const originalPosts = await prisma.post.findMany({ + where: postsWhereClause, take: first + 1, orderBy: [{ createdAt: "desc" }, { id: "desc" }], include: { @@ -477,10 +882,57 @@ export const postService = { }, }); - const hasNextPage = posts.length > first; - const edges = posts.slice(0, first).map((post) => ({ - cursor: encodeCursor(post.createdAt, post.id), - node: post, + // Fetch recent reposts + const repostsWhereClause: Prisma.RepostWhereInput = { + post: { + parentPostId: null, + visibility: "PUBLIC", + }, + ...(cursorDate && { createdAt: { lt: cursorDate } }), + }; + + const reposts = await prisma.repost.findMany({ + where: repostsWhereClause, + take: first + 1, + orderBy: [{ createdAt: "desc" }], + include: { + user: true, + post: { + include: { + author: true, + media: { orderBy: { position: "asc" } }, + }, + }, + }, + }); + + // Merge posts and reposts + const feedItems = [ + ...originalPosts.map(post => ({ + post: { ...post, repostedBy: null as typeof reposts[0]["user"] | null }, + sortDate: post.createdAt, + })), + ...reposts.map(repost => ({ + post: { ...repost.post, repostedBy: repost.user }, + sortDate: repost.createdAt, + })), + ]; + + // Sort by date descending + feedItems.sort((a, b) => b.sortDate.getTime() - a.sortDate.getTime()); + + // Deduplicate + const seenPostIds = new Set(); + const uniqueFeedItems = feedItems.filter(item => { + if (seenPostIds.has(item.post.id)) return false; + seenPostIds.add(item.post.id); + return true; + }); + + const hasNextPage = uniqueFeedItems.length > first; + const edges = uniqueFeedItems.slice(0, first).map((item) => ({ + cursor: encodeCursor(item.sortDate, item.post.id), + node: item.post, })); return { @@ -528,11 +980,28 @@ export const postService = { }, }); - // Sort by engagement (likes + replies) - posts.sort( - (a, b) => - b._count.likes + b._count.replies - (a._count.likes + a._count.replies) - ); + // Sort by engagement (likes + replies), fallback to recency + // Primary: engagement score (higher first) + // Secondary: createdAt (newer first - higher timestamp first) + // Tertiary: id (for consistent ordering) + posts.sort((a, b) => { + const scoreA = (a._count?.likes || 0) + (a._count?.replies || 0); + const scoreB = (b._count?.likes || 0) + (b._count?.replies || 0); + + // Sort by engagement score (higher first) + if (scoreB !== scoreA) { + return scoreB - scoreA; + } + + // If scores are equal, sort by createdAt DESC (newer posts first) + const timeDiff = b.createdAt.getTime() - a.createdAt.getTime(); + if (timeDiff !== 0) { + return timeDiff; + } + + // Tertiary sort by id for consistent ordering + return b.id.localeCompare(a.id); + }); const hasNextPage = posts.length > first; const edges = posts.slice(0, first).map((post) => ({ @@ -635,6 +1104,17 @@ export const postService = { }); } + // Emit real-time event for like + const likesCount = await prisma.postLike.count({ where: { postId } }); + emitToAll("post:liked", { postId, likesCount, userId }); + + // Notify post author via socket + if (post.authorId !== userId) { + emitToUser(post.authorId, "notification:new", { + notification: { type: "LIKE", entityId: postId, actorId: userId }, + }); + } + return true; }, @@ -654,6 +1134,10 @@ export const postService = { where: { userId_postId: { userId, postId } }, }); + // Emit real-time event for unlike + const likesCount = await prisma.postLike.count({ where: { postId } }); + emitToAll("post:unliked", { postId, likesCount, userId }); + return true; }, @@ -785,4 +1269,78 @@ export const postService = { }); return mentions.map((m) => m.mentionedUser); }, + + // ===================== + // REPOST OPERATIONS + // ===================== + async repostPost(userId: string, postId: string) { + const post = await prisma.post.findUnique({ + where: { id: postId }, + select: { id: true, authorId: true }, + }); + + if (!post) { + throw Errors.notFound("Post"); + } + + // Prevent users from reposting their own posts + if (post.authorId === userId) { + throw Errors.badRequest("You cannot repost your own post"); + } + + // Check if already reposted + const existingRepost = await prisma.repost.findUnique({ + where: { userId_postId: { userId, postId } }, + }); + + if (existingRepost) { + throw Errors.conflict("You have already reposted this post"); + } + + await prisma.repost.create({ + data: { userId, postId }, + }); + + // Create notification for post author (if not self-repost) + if (post.authorId !== userId) { + await prisma.notification.create({ + data: { + userId: post.authorId, + actorId: userId, + type: "REPOST", + entityId: postId, + }, + }); + } + + return true; + }, + + async unrepostPost(userId: string, postId: string) { + const repost = await prisma.repost.findUnique({ + where: { userId_postId: { userId, postId } }, + }); + + if (!repost) { + throw Errors.notFound("Repost"); + } + + await prisma.repost.delete({ + where: { userId_postId: { userId, postId } }, + }); + + return true; + }, + + async getRepostsCount(postId: string) { + return prisma.repost.count({ where: { postId } }); + }, + + async getIsReposted(postId: string, userId: string | null) { + if (!userId) return false; + const repost = await prisma.repost.findUnique({ + where: { userId_postId: { userId, postId } }, + }); + return !!repost; + }, }; diff --git a/backend/src/graphql/post/post.typeDefs.ts b/backend/src/graphql/post/post.typeDefs.ts index 226e639..79de2ca 100644 --- a/backend/src/graphql/post/post.typeDefs.ts +++ b/backend/src/graphql/post/post.typeDefs.ts @@ -15,6 +15,24 @@ export const postTypeDefs = /* GraphQL */ ` GIF } + enum PostFilter { + THREADS + REPLIES + REPOSTS + MEDIA + BOOKMARKS + } + + # ========================= + # Core Post Types + # ========================= + + # ... (leaving this part out of replacement to minimize size, wait, I need to match context) + # Actually better to just do the enum block and the query block separately or carefully. + + # Let's do the enum first. + + # ========================= # Core Post Types # ========================= @@ -27,8 +45,11 @@ export const postTypeDefs = /* GraphQL */ ` parentPost: Post repliesCount: Int! likesCount: Int! + repostsCount: Int! isLiked: Boolean! isBookmarked: Boolean! + isReposted: Boolean! + repostedBy: User media: [PostMedia!] hashtags: [Hashtag!] mentions: [User!] @@ -129,7 +150,7 @@ export const postTypeDefs = /* GraphQL */ ` getPostReplies(postId: ID!, first: Int = 20, after: String): PostConnection! # User posts (PUBLIC - respects privacy settings) - getUserPosts(userId: ID!, first: Int = 20, after: String): PostConnection! + getUserPosts(userId: ID!, filter: PostFilter = THREADS, first: Int = 20, after: String): PostConnection! # Home feed (PROTECTED - authenticated users only) getHomeFeed(first: Int = 20, after: String): PostConnection! @@ -137,6 +158,9 @@ export const postTypeDefs = /* GraphQL */ ` # Explore / trending (PUBLIC - guests can view) getTrendingPosts(first: Int = 20, after: String): PostConnection! + # Public feed (PUBLIC - chronological order for guests) + getPublicFeed(first: Int = 20, after: String): PostConnection! + # Hashtag search (PUBLIC - guests can view) getPostsByHashtag(tag: String!, first: Int = 20, after: String): PostConnection! @@ -164,5 +188,9 @@ export const postTypeDefs = /* GraphQL */ ` # Bookmarks (PROTECTED) bookmarkPost(postId: ID!): Boolean! unbookmarkPost(postId: ID!): Boolean! + + # Reposts (PROTECTED) + repostPost(postId: ID!): Boolean! + unrepostPost(postId: ID!): Boolean! } `; diff --git a/backend/src/graphql/report/report.resolvers.ts b/backend/src/graphql/report/report.resolvers.ts new file mode 100644 index 0000000..e2e2bcb --- /dev/null +++ b/backend/src/graphql/report/report.resolvers.ts @@ -0,0 +1,11 @@ +import * as ReportService from "./report.service.js"; + +export const reportResolvers = { + Mutation: { + submitReport: async (_: any, { input }: { input: any }, context: any) => { + // User might be null if unauthenticated, which is allowed for reports + const userId = context.user?.id || null; + return await ReportService.submitReport(userId, input); + }, + }, +}; diff --git a/backend/src/graphql/report/report.service.ts b/backend/src/graphql/report/report.service.ts new file mode 100644 index 0000000..f14bcbb --- /dev/null +++ b/backend/src/graphql/report/report.service.ts @@ -0,0 +1,26 @@ +import { prisma } from "../../lib/prisma.js"; + +interface CreateReportInput { + category: String; + description: String; + attachmentUrl?: String; + deviceInfo?: String; +} + +export const submitReport = async (userId: string | null, input: any) => { + try { + await prisma.report.create({ + data: { + category: input.category, + description: input.description, + attachmentUrl: input.attachmentUrl, + deviceInfo: input.deviceInfo ? JSON.parse(input.deviceInfo) : undefined, + userId: userId, + }, + }); + return true; + } catch (error) { + console.error("Error submitting report:", error); + throw new Error("Failed to submit report"); + } +}; diff --git a/backend/src/graphql/report/report.typeDefs.ts b/backend/src/graphql/report/report.typeDefs.ts new file mode 100644 index 0000000..d2c96bd --- /dev/null +++ b/backend/src/graphql/report/report.typeDefs.ts @@ -0,0 +1,24 @@ +export const reportTypeDefs = /* GraphQL */ ` + type Report { + id: ID! + category: String! + description: String! + attachmentUrl: String + deviceInfo: String # JSON string + status: String! + userId: ID + user: User + createdAt: String! + } + + input CreateReportInput { + category: String! + description: String! + attachmentUrl: String + deviceInfo: String # JSON string + } + + extend type Mutation { + submitReport(input: CreateReportInput!): Boolean! + } +`; diff --git a/backend/src/graphql/user/user.resolvers.ts b/backend/src/graphql/user/user.resolvers.ts index c9df667..5690338 100644 --- a/backend/src/graphql/user/user.resolvers.ts +++ b/backend/src/graphql/user/user.resolvers.ts @@ -1,262 +1,133 @@ -import { - userService, - type CreateUserData, - type LoginPayload, - type UpdateUserProfileInput, -} from "./user.service.js"; -import { - requireAuth, - type GraphQLContext, - setAuthCookies, - clearAuthCookies, - REFRESH_TOKEN_COOKIE, -} from "../context.js"; -import { Errors } from "../errors.js"; +import { userService } from "./user.service.js"; +import { requireAuth, requireSupabaseAuth } from "../context.js"; export const userResolvers = { - // Field resolvers for User type - User: { - stats: async (parent: { id: string }) => { - return userService.getUserStats(parent.id); - }, - }, - Query: { - // Protected: Get all users - getUsers: async (_: unknown, __: unknown, context: GraphQLContext) => { - requireAuth(context); + // Get all users + getUsers: async (_: any, __: any, context: any) => { + // requireAuth(context); return userService.getAllUsers(); }, - // Protected: Get user by ID - getUserById: async ( - _: unknown, - args: { id: string }, - context: GraphQLContext - ) => { - requireAuth(context); - return userService.getUserById(args.id); + // Get user by ID + getUserById: async (_: any, { id }: { id: string }, context: any) => { + // requireAuth(context); + return userService.getUserById(id); }, - // Public: Get user by username (guests can view profiles) - getUserByUsername: async ( - _: unknown, - args: { username: string }, - _context: GraphQLContext - ) => { - return userService.getUserByUsername(args.username); + // Get user by username + getUserByUsername: async (_: any, { username }: { username: string }) => { + return userService.getUserByUsername(username); }, - // Protected: Get user by email - getUserByEmail: async ( - _: unknown, - args: { email: string }, - context: GraphQLContext - ) => { + // Get user by email + getUserByEmail: async (_: any, { email }: { email: string }, context: any) => { requireAuth(context); - return userService.getUserByEmail(args.email); + return userService.getUserByEmail(email); }, - // Protected: Get current logged-in user - getCurrentLoggedInUser: async ( - _: unknown, - __: unknown, - context: GraphQLContext - ) => { - return requireAuth(context); + // Get current logged in user + getCurrentLoggedInUser: async (_: any, __: any, context: any) => { + return context.user; }, - // Protected: Get followers of a user - getFollowers: async ( - _: unknown, - args: { userId: string }, - context: GraphQLContext - ) => { + // Get suggested users + getSuggestedUsers: async (_: any, { first }: { first: number }, context: any) => { + // requireAuth(context); + return userService.getSuggestedUsers(first); + }, + + // Get followers + getFollowers: async (_: any, { userId }: { userId: string }, context: any) => { requireAuth(context); - return userService.getFollowers(args.userId); + return userService.getFollowers(userId); }, - // Protected: Get users that a user is following - getFollowing: async ( - _: unknown, - args: { userId: string }, - context: GraphQLContext - ) => { + // Get following + getFollowing: async (_: any, { userId }: { userId: string }, context: any) => { requireAuth(context); - return userService.getFollowing(args.userId); + return userService.getFollowing(userId); }, - // Protected: Get blocked users - getBlockedUsers: async ( - _: unknown, - __: unknown, - context: GraphQLContext - ) => { + // Get blocked users + getBlockedUsers: async (_: any, __: any, context: any) => { const user = requireAuth(context); return userService.getBlockedUsers(user.id); }, - // Protected: Get my notifications - getMyNotifications: async ( - _: unknown, - __: unknown, - context: GraphQLContext - ) => { + // Get notifications + getMyNotifications: async (_: any, __: any, context: any) => { const user = requireAuth(context); return userService.getMyNotifications(user.id); }, }, Mutation: { - // Public: Create user (registration) - createUser: async (_: unknown, args: CreateUserData) => { - return userService.createUser(args); - }, - - // Protected: Update user profile - updateUserProfile: async ( - _: unknown, - args: { input: UpdateUserProfileInput }, - context: GraphQLContext - ) => { - const user = requireAuth(context); - return userService.updateUserProfile(user.id, args.input); - }, - - // Public: Login - login: async ( - _: unknown, - args: LoginPayload, - context: GraphQLContext - ) => { - const { accessToken, refreshToken, user } = await userService.login(args); - - // Set httpOnly cookies - setAuthCookies(context.res, accessToken, refreshToken); + // Create User Profile (after Supabase Signup) + createUser: async (_: any, args: any, context: any) => { + // Ensure user has a valid Supabase session + const supabaseUser = requireSupabaseAuth(context); - return { - accessToken, - user: await userService.getUserById(user.id), - }; + // Use the ID from Supabase Auth to create the profile + // This links the Auth User to the Public Profile + return userService.createUser({ + ...args, + id: supabaseUser.id, + }); }, - // Public: Refresh access token - refreshToken: async ( - _: unknown, - __: unknown, - context: GraphQLContext - ) => { - const refreshTokenValue = context.req.cookies?.[REFRESH_TOKEN_COOKIE]; - - if (!refreshTokenValue) { - throw Errors.unauthenticated("No refresh token provided"); - } - - const { accessToken, refreshToken: newRefreshToken } = - await userService.refreshAccessToken(refreshTokenValue); - - // Set new cookies - setAuthCookies(context.res, accessToken, newRefreshToken); - - // Get user from new access token - const user = context.user; - - return { - accessToken, - user, - }; - }, - - // Protected: Logout - logout: async (_: unknown, __: unknown, context: GraphQLContext) => { - const refreshTokenValue = context.req.cookies?.[REFRESH_TOKEN_COOKIE]; - - if (refreshTokenValue) { - await userService.logout(refreshTokenValue); - } - - clearAuthCookies(context.res); - return true; - }, - - // Protected: Logout from all devices - logoutAll: async (_: unknown, __: unknown, context: GraphQLContext) => { + // Update user profile + updateUserProfile: async (_: any, { input }: { input: any }, context: any) => { const user = requireAuth(context); - await userService.logoutAll(user.id); - clearAuthCookies(context.res); - return true; + return userService.updateUserProfile(user.id, input); }, - // Protected: Follow a user - followUser: async ( - _: unknown, - args: { userId: string }, - context: GraphQLContext - ) => { + // Follow user + followUser: async (_: any, { userId }: { userId: string }, context: any) => { const user = requireAuth(context); - return userService.followUser(user.id, args.userId); + return userService.followUser(user.id, userId); }, - // Protected: Unfollow a user - unfollowUser: async ( - _: unknown, - args: { userId: string }, - context: GraphQLContext - ) => { + // Unfollow user + unfollowUser: async (_: any, { userId }: { userId: string }, context: any) => { const user = requireAuth(context); - return userService.unfollowUser(user.id, args.userId); + return userService.unfollowUser(user.id, userId); }, - // Protected: Block a user - blockUser: async ( - _: unknown, - args: { userId: string }, - context: GraphQLContext - ) => { + // Block user + blockUser: async (_: any, { userId }: { userId: string }, context: any) => { const user = requireAuth(context); - return userService.blockUser(user.id, args.userId); + return userService.blockUser(user.id, userId); }, - // Protected: Unblock a user - unblockUser: async ( - _: unknown, - args: { userId: string }, - context: GraphQLContext - ) => { + // Unblock user + unblockUser: async (_: any, { userId }: { userId: string }, context: any) => { const user = requireAuth(context); - return userService.unblockUser(user.id, args.userId); + return userService.unblockUser(user.id, userId); }, - // Protected: Mark notification as read - markNotificationAsRead: async ( - _: unknown, - args: { notificationId: string }, - context: GraphQLContext - ) => { + // Mark notification as read + markNotificationAsRead: async (_: any, { notificationId }: { notificationId: string }, context: any) => { const user = requireAuth(context); - return userService.markNotificationAsRead(user.id, args.notificationId); + return userService.markNotificationAsRead(user.id, notificationId); }, - // Protected: Mark all notifications as read - markAllNotificationsAsRead: async ( - _: unknown, - __: unknown, - context: GraphQLContext - ) => { + // Mark all notifications as read + markAllNotificationsAsRead: async (_: any, __: any, context: any) => { const user = requireAuth(context); return userService.markAllNotificationsAsRead(user.id); }, + }, - // Password Recovery - forgotPassword: async (_: unknown, args: { email: string }) => { - return userService.forgotPassword(args.email); + User: { + // Resolve computed fields + stats: async (parent: any) => { + return userService.getUserStats(parent.id); }, - resetPassword: async ( - _: unknown, - args: { token: string; newPassword: string } - ) => { - return userService.resetPassword(args.token, args.newPassword); - }, + isFollowing: async (parent: any, _: any, context: any) => { + if (!context.user) return false; + return userService.isFollowing(context.user.id, parent.id); + } }, }; diff --git a/backend/src/graphql/user/user.service.ts b/backend/src/graphql/user/user.service.ts index 6c95a41..62a86eb 100644 --- a/backend/src/graphql/user/user.service.ts +++ b/backend/src/graphql/user/user.service.ts @@ -1,25 +1,14 @@ -import { randomBytes, createHmac } from "crypto"; import { prisma } from "../../lib/prisma.js"; -import JWT from "jsonwebtoken"; import { Errors } from "../errors.js"; +import type { User } from "../../../generated/prisma/client.js"; export interface CreateUserData { + id?: string; // Optional because we might auto-generate or pass from Supabase firstName: string; lastName?: string; username?: string; profileImageUrl?: string; email: string; - password: string; -} - -export interface LoginPayload { - email: string; - password: string; -} - -export interface TokenPair { - accessToken: string; - refreshToken: string; } export interface UpdateUserProfileInput { @@ -34,24 +23,15 @@ export interface UpdateUserProfileInput { is_private?: boolean; } -const JWT_SECRET = process.env.JWT_SECRET; -const ACCESS_TOKEN_EXPIRES = "15m"; -const REFRESH_TOKEN_EXPIRES_DAYS = 7; - -// Generate password hash with salt -function generateHash(password: string, salt: string): string { - return createHmac("sha256", salt).update(password).digest("hex"); -} - -// Generate random token -function generateRandomToken(): string { - return randomBytes(40).toString("hex"); -} - // Generate a unique username based on firstName and lastName -async function generateUniqueUsername(firstName: string, lastName?: string): Promise { +async function generateUniqueUsername( + firstName: string, + lastName?: string +): Promise { // Create base username from firstName and lastName (lowercase, no spaces) - const baseName = `${firstName}${lastName || ""}`.toLowerCase().replace(/\s+/g, ""); + const baseName = `${firstName}${lastName || ""}` + .toLowerCase() + .replace(/\s+/g, ""); let username = baseName; let isUnique = false; @@ -83,7 +63,10 @@ async function generateUniqueUsername(firstName: string, lastName?: string): Pro } // Check if username is unique (for updates) -async function isUsernameUnique(username: string, excludeUserId?: string): Promise { +async function isUsernameUnique( + username: string, + excludeUserId?: string +): Promise { const existingUser = await prisma.user.findUnique({ where: { username }, }); @@ -94,10 +77,11 @@ async function isUsernameUnique(username: string, excludeUserId?: string): Promi } export const userService = { - // Create new user + // Create new user (Profile) - ID should come from Supabase Auth async createUser(data: CreateUserData) { - const salt = randomBytes(32).toString("hex"); - const hashedPassword = generateHash(data.password, salt); + if (!data.id) { + throw Errors.badRequest("User ID is required"); + } // Generate username if not provided let username = data.username; @@ -113,13 +97,12 @@ export const userService = { return prisma.user.create({ data: { + id: data.id, // Use ID from Supabase Auth if provided firstName: data.firstName, lastName: data.lastName ?? null, username, profileImageUrl: data.profileImageUrl ?? null, email: data.email, - password: hashedPassword, - salt, }, }); }, @@ -129,6 +112,27 @@ export const userService = { return prisma.user.findMany(); }, + // Get suggested users (ordered by follower count) + async getSuggestedUsers(first: number = 10) { + const users = await prisma.user.findMany({ + take: 50, // Fetch a pool + include: { + _count: { + select: { followers: true } + } + } + }); + + // Sort by follower count descending with safety check + const sorted = users.sort((a, b) => { + const countA = (a as any)._count?.followers ?? 0; + const countB = (b as any)._count?.followers ?? 0; + return countB - countA; + }); + + return sorted.slice(0, first); + }, + // Get user by ID async getUserById(id: string) { return prisma.user.findUnique({ where: { id } }); @@ -163,8 +167,12 @@ export const userService = { ...(input.bio !== undefined && { bio: input.bio }), ...(input.website !== undefined && { website: input.website }), ...(input.location !== undefined && { location: input.location }), - ...(input.dob !== undefined && { dob: input.dob ? new Date(input.dob) : null }), - ...(input.profileImageUrl !== undefined && { profileImageUrl: input.profileImageUrl }), + ...(input.dob !== undefined && { + dob: input.dob ? new Date(input.dob) : null, + }), + ...(input.profileImageUrl !== undefined && { + profileImageUrl: input.profileImageUrl, + }), ...(input.is_private !== undefined && { is_private: input.is_private }), }, }); @@ -237,6 +245,19 @@ export const userService = { return true; }, + // Check if user follows target user + async isFollowing(followerId: string, followingId: string) { + const follow = await prisma.follow.findUnique({ + where: { + followerId_followingId: { + followerId, + followingId, + }, + }, + }); + return !!follow; + }, + // Get followers of a user async getFollowers(userId: string) { const follows = await prisma.follow.findMany({ @@ -358,185 +379,5 @@ export const userService = { }); return true; - }, - - // Generate access token - generateAccessToken(userId: string, email: string): string { - if (!JWT_SECRET) { - throw Errors.internalError("JWT_SECRET is not configured"); - } - return JWT.sign({ id: userId, email }, JWT_SECRET, { - expiresIn: ACCESS_TOKEN_EXPIRES, - } as JWT.SignOptions); - }, - - // Generate refresh token and save to DB - async generateRefreshToken(userId: string): Promise { - const token = generateRandomToken(); - const expiresAt = new Date(); - expiresAt.setDate(expiresAt.getDate() + REFRESH_TOKEN_EXPIRES_DAYS); - - await prisma.refreshToken.create({ - data: { - token, - userId, - expiresAt, - }, - }); - - return token; - }, - - // Login and generate tokens - async login( - payload: LoginPayload - ): Promise< - TokenPair & { user: { id: string; email: string; firstName: string } } - > { - const { email, password } = payload; - const user = await this.getUserByEmail(email); - - if (!user) { - throw Errors.unauthenticated("Invalid email or password"); - } - - const hashedPassword = generateHash(password, user.salt); - - if (hashedPassword !== user.password) { - throw Errors.unauthenticated("Invalid email or password"); - } - - const accessToken = this.generateAccessToken(user.id, user.email); - const refreshToken = await this.generateRefreshToken(user.id); - - return { - accessToken, - refreshToken, - user: { - id: user.id, - email: user.email, - firstName: user.firstName, - }, - }; - }, - - // Refresh access token - async refreshAccessToken(refreshToken: string): Promise { - // Find refresh token in DB - const storedToken = await prisma.refreshToken.findUnique({ - where: { token: refreshToken }, - include: { user: true }, - }); - - if (!storedToken) { - throw Errors.invalidToken("Invalid refresh token"); - } - - // Check if expired - if (storedToken.expiresAt < new Date()) { - await prisma.refreshToken.delete({ where: { id: storedToken.id } }); - throw Errors.tokenExpired("Refresh token expired"); - } - - // Delete old refresh token (rotation) - await prisma.refreshToken.delete({ where: { id: storedToken.id } }); - - // Generate new tokens - const accessToken = this.generateAccessToken( - storedToken.user.id, - storedToken.user.email - ); - const newRefreshToken = await this.generateRefreshToken( - storedToken.user.id - ); - - return { - accessToken, - refreshToken: newRefreshToken, - }; - }, - - // Logout - delete refresh token - async logout(refreshToken: string): Promise { - try { - await prisma.refreshToken.delete({ - where: { token: refreshToken }, - }); - return true; - } catch { - return false; - } - }, - - // Delete all refresh tokens for user (logout all devices) - async logoutAll(userId: string): Promise { - await prisma.refreshToken.deleteMany({ - where: { userId }, - }); - return true; - }, - - // Forgot Password - Generate reset token - async forgotPassword(email: string): Promise { - const user = await this.getUserByEmail(email); - if (!user) return true; // Return true to prevent email enumeration - - // Delete any existing reset tokens for this user - await prisma.passwordResetToken.deleteMany({ - where: { userId: user.id }, - }); - - const token = generateRandomToken(); - const expiresAt = new Date(); - expiresAt.setHours(expiresAt.getHours() + 1); // Token expires in 1 hour - - await prisma.passwordResetToken.create({ - data: { - token, - userId: user.id, - expiresAt, - }, - }); - - // In a real app, you would send this token via email. - console.log(`[PASS_RESET] Token for ${email}: ${token}`); - - return true; - }, - - // Reset Password - Verify token and update password - async resetPassword(token: string, newPassword: string): Promise { - const storedToken = await prisma.passwordResetToken.findUnique({ - where: { token }, - include: { user: true }, - }); - - if (!storedToken) { - throw Errors.invalidToken("Invalid or expired reset token"); - } - - if (storedToken.expiresAt < new Date()) { - await prisma.passwordResetToken.delete({ where: { id: storedToken.id } }); - throw Errors.tokenExpired("Reset token expired"); - } - - const salt = randomBytes(32).toString("hex"); - const hashedPassword = generateHash(newPassword, salt); - - await prisma.user.update({ - where: { id: storedToken.userId }, - data: { - password: hashedPassword, - salt, - }, - }); - - // Delete the used token - await prisma.passwordResetToken.delete({ where: { id: storedToken.id } }); - - // Also invalidate all refresh tokens (logout from all devices after password change) - await this.logoutAll(storedToken.userId); - - return true; - }, + } }; diff --git a/backend/src/graphql/user/user.typeDefs.ts b/backend/src/graphql/user/user.typeDefs.ts index ae564f0..33d82ba 100644 --- a/backend/src/graphql/user/user.typeDefs.ts +++ b/backend/src/graphql/user/user.typeDefs.ts @@ -20,6 +20,7 @@ export const userTypeDefs = /* GraphQL */ ` stats: UserStats createdAt: String! updatedAt: String! + isFollowing: Boolean! } type UserStats { @@ -28,15 +29,6 @@ export const userTypeDefs = /* GraphQL */ ` postsCount: Int! } - # ========================= - # Auth Types - # ========================= - - type AuthResponse { - accessToken: String! - user: User! - } - # ========================= # Follow System # ========================= @@ -58,7 +50,7 @@ export const userTypeDefs = /* GraphQL */ ` } # ========================= - # Notifications (User-facing) + # Notifications # ========================= enum NotificationType { @@ -71,8 +63,8 @@ export const userTypeDefs = /* GraphQL */ ` type Notification { id: ID! - user: User! # receiver - actor: User! # who triggered it + user: User! + actor: User! type: NotificationType! entityId: ID isRead: Boolean! @@ -114,21 +106,22 @@ export const userTypeDefs = /* GraphQL */ ` # ========================= extend type Query { - # User queries (PROTECTED except getUserByUsername) + # User queries getUsers: [User!]! getUserById(id: ID!): User getUserByUsername(username: String!): User getUserByEmail(email: String): User getCurrentLoggedInUser: User + getSuggestedUsers(first: Int): [User!]! - # Follow queries (PROTECTED) + # Follow queries getFollowers(userId: ID!): [User!]! getFollowing(userId: ID!): [User!]! - # Block queries (PROTECTED) + # Block queries getBlockedUsers: [User!]! - # Notifications (PROTECTED) + # Notifications getMyNotifications: [Notification!]! } @@ -137,37 +130,26 @@ export const userTypeDefs = /* GraphQL */ ` # ========================= extend type Mutation { - # User (PUBLIC for createUser) + # User Profile Creation (After Supabase Auth SignUp) createUser( firstName: String! lastName: String username: String profileImageUrl: String email: String! - password: String! ): User! updateUserProfile(input: UpdateUserProfileInput!): User! - # Auth - login(email: String!, password: String!): AuthResponse! - refreshToken: AuthResponse! - logout: Boolean! - logoutAll: Boolean! - - # Password Recovery - forgotPassword(email: String!): Boolean! - resetPassword(token: String!, newPassword: String!): Boolean! - - # Follow (PROTECTED) + # Follow followUser(userId: ID!): Boolean! unfollowUser(userId: ID!): Boolean! - # Block (PROTECTED) + # Block blockUser(userId: ID!): Boolean! unblockUser(userId: ID!): Boolean! - # Notifications (PROTECTED) + # Notifications markNotificationAsRead(notificationId: ID!): Boolean! markAllNotificationsAsRead: Boolean! } diff --git a/backend/src/index.ts b/backend/src/index.ts index b449137..fc3b511 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -2,14 +2,87 @@ import "dotenv/config"; import express from "express"; import cors from "cors"; import cookieParser from "cookie-parser"; +import { createServer } from "http"; import { expressMiddleware } from "@as-integrations/express5"; import { createGraphqlServer } from "./graphql/server.js"; import { createContext } from "./graphql/context.js"; +import { initializeSocket } from "./lib/socket.js"; +import { uploadToSupabase } from "./lib/supabase.js"; + +import multer from "multer"; +import path from "path"; +import fs from "fs"; +import { fileURLToPath } from "url"; + +// Helper for __dirname in ES modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); async function startServer() { const app = express(); const PORT = Number(process.env.PORT) || 8000; + // Create HTTP server for Socket.IO integration + const httpServer = createServer(app); + + // Initialize Socket.IO + initializeSocket(httpServer); + + // Check if Supabase is configured + const useSupabase = !!(process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_KEY); + + // Configure Multer - use memory storage for Supabase, disk for local + let upload: multer.Multer; + let uploadDir: string | undefined; + + if (useSupabase) { + // Memory storage for Supabase uploads + upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit + fileFilter: (req, file, cb) => { + if (file.mimetype.startsWith("image/") || file.mimetype.startsWith("video/")) { + cb(null, true); + } else { + cb(new Error("Only images and videos are allowed")); + } + }, + }); + console.log("๐Ÿ“ฆ Using Supabase Storage for file uploads"); + } else { + // Disk storage for local development + uploadDir = path.join(__dirname, "../uploads"); + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + + const diskStorage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, uploadDir!); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9); + cb(null, uniqueSuffix + path.extname(file.originalname)); + }, + }); + + upload = multer({ + storage: diskStorage, + limits: { fileSize: 10 * 1024 * 1024 }, + fileFilter: (req, file, cb) => { + if (file.mimetype.startsWith("image/") || file.mimetype.startsWith("video/")) { + cb(null, true); + } else { + cb(new Error("Only images and videos are allowed")); + } + }, + }); + + // Serve static files from uploads directory (only for local) + app.use("/uploads", express.static(uploadDir)); + console.log("๐Ÿ’พ Using local disk storage for file uploads"); + } + // Cookie parser app.use(cookieParser()); // JSON body parser @@ -22,7 +95,53 @@ async function startServer() { }) ); - //health check endpoint + // File Upload Endpoint + app.post("/api/upload", upload.array("files", 10), async (req, res) => { + try { + if (!req.files || (Array.isArray(req.files) && req.files.length === 0)) { + return res.status(400).json({ error: "No files uploaded" }); + } + + const files = req.files as Express.Multer.File[]; + const fileUrls: string[] = []; + + // Validate individual file sizes + for (const file of files) { + if (file.mimetype.startsWith("image/") && !file.mimetype.includes("gif") && file.size > 2 * 1024 * 1024) { + return res.status(400).json({ error: `Image ${file.originalname} exceeds 2MB limit` }); + } + if (file.mimetype.startsWith("video/") && file.size > 10 * 1024 * 1024) { + return res.status(400).json({ error: `Video ${file.originalname} exceeds 10MB limit` }); + } + } + + if (useSupabase) { + // Upload to Supabase Storage + for (const file of files) { + const result = await uploadToSupabase(file); + fileUrls.push(result.url); + } + } else { + // Use local URLs + for (const file of files) { + const fileUrl = `${req.protocol}://${req.get("host")}/uploads/${file.filename}`; + fileUrls.push(fileUrl); + } + } + + // Return response + if (files.length === 1 && files[0]) { + return res.json({ url: fileUrls[0], urls: fileUrls, type: files[0].mimetype }); + } + + res.json({ urls: fileUrls }); + } catch (error: any) { + console.error("Upload error:", error); + res.status(500).json({ error: error.message || "Failed to upload file" }); + } + }); + + // Health check endpoint app.get("/health", (req, res) => { res.json({ message: "Server is running!" }); }); @@ -36,9 +155,11 @@ async function startServer() { }) ); - app.listen(PORT, () => { + // Use httpServer.listen instead of app.listen for Socket.IO + httpServer.listen(PORT, () => { if (process.env.NODE_ENV !== "production") { console.log(`Server is running on http://localhost:${PORT}`); + console.log(`Socket.IO enabled`); } }); } diff --git a/backend/src/lib/socket.ts b/backend/src/lib/socket.ts new file mode 100644 index 0000000..30b9848 --- /dev/null +++ b/backend/src/lib/socket.ts @@ -0,0 +1,117 @@ +import { Server as HttpServer } from "http"; +import { Server, Socket } from "socket.io"; +import { supabase } from "./supabase.js"; + +// Socket.IO server instance +let io: Server | null = null; + +// ===================== +// SOCKET EVENT TYPES +// ===================== +export interface SocketEvents { + // Post events + "post:created": { post: unknown }; + "post:deleted": { postId: string }; + "post:liked": { postId: string; likesCount: number; userId: string }; + "post:unliked": { postId: string; likesCount: number; userId: string }; + "post:replied": { parentPostId: string; reply: unknown }; + "post:reposted": { postId: string; repostsCount: number; userId: string }; + "post:unreposted": { postId: string; repostsCount: number; userId: string }; + + // Notification events + "notification:new": { notification: unknown }; + + // User events + "user:followed": { + followerId: string; + followingId: string; + followerUsername: string; + }; + "user:unfollowed": { followerId: string; followingId: string }; +} + +// ===================== +// INITIALIZE SOCKET.IO +// ===================== +export function initializeSocket(httpServer: HttpServer): Server { + io = new Server(httpServer, { + cors: { + origin: process.env.FRONTEND_URL || "http://localhost:3000", + credentials: true, + }, + path: "/socket.io", + }); + + // Authentication middleware using Supabase + io.use(async (socket, next) => { + const token = socket.handshake.auth.token; + + if (!token) { + // Allow unauthenticated connections for public events + socket.data.userId = null; + return next(); + } + + try { + // Verify token with Supabase + const { data: { user }, error } = await supabase.auth.getUser(token); + + if (error || !user) { + socket.data.userId = null; + } else { + socket.data.userId = user.id; + } + next(); + } catch (err) { + console.error("[Socket] Auth error:", err); + socket.data.userId = null; + next(); + } + }); + + // Connection handler + io.on("connection", (socket: Socket) => { + const userId = socket.data.userId; + + // Join user-specific room for targeted notifications + if (userId) { + socket.join(`user:${userId}`); + } + + // Join global feed room + socket.join("feed:global"); + + socket.on("disconnect", () => { + // Cleanup if needed + }); + }); + + console.log("Socket.IO server initialized with Supabase Auth"); + return io; +} + +// ===================== +// EMIT HELPERS +// ===================== +export function getIO(): Server | null { + return io; +} + +export function emitToAll( + event: K, + data: SocketEvents[K] +): void { + if (io) { + io.to("feed:global").emit(event, data); + } +} + +export function emitToUser( + userId: string, + event: K, + data: SocketEvents[K] +): void { + if (io) { + io.to(`user:${userId}`).emit(event, data); + } +} diff --git a/backend/src/lib/supabase.ts b/backend/src/lib/supabase.ts new file mode 100644 index 0000000..4de8aaf --- /dev/null +++ b/backend/src/lib/supabase.ts @@ -0,0 +1,77 @@ +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.SUPABASE_URL!; +const supabaseServiceKey = process.env.SUPABASE_SERVICE_KEY!; +const supabaseBucket = process.env.SUPABASE_BUCKET || "threads-clone"; + +// Use service key for server-side operations (uploads) +export const supabase = createClient(supabaseUrl, supabaseServiceKey); + +// Upload file to Supabase Storage +export async function uploadToSupabase( + file: Express.Multer.File +): Promise<{ url: string; type: string }> { + const timestamp = Date.now(); + const randomSuffix = Math.round(Math.random() * 1e9); + const ext = file.originalname.split('.').pop() || 'jpg'; + const fileName = `${timestamp}-${randomSuffix}.${ext}`; + + // Determine folder based on file type + let folder = "images"; + if (file.mimetype.startsWith("video/")) { + folder = "videos"; + } else if (file.mimetype === "image/gif") { + folder = "gifs"; + } + + const filePath = `${folder}/${fileName}`; + + const { data, error } = await supabase.storage + .from(supabaseBucket) + .upload(filePath, file.buffer, { + contentType: file.mimetype, + cacheControl: "3600", + upsert: false, + }); + + if (error) { + console.error("Supabase upload error:", error); + throw new Error(`Failed to upload file: ${error.message}`); + } + + // Get public URL + const { data: publicUrlData } = supabase.storage + .from(supabaseBucket) + .getPublicUrl(filePath); + + return { + url: publicUrlData.publicUrl, + type: file.mimetype, + }; +} + +// Delete file from Supabase Storage +export async function deleteFromSupabase(fileUrl: string): Promise { + try { + // Extract path from URL + const url = new URL(fileUrl); + const pathParts = url.pathname.split(`/storage/v1/object/public/${supabaseBucket}/`); + if (pathParts.length < 2) return false; + + const filePath = pathParts[1] || ""; + + const { error } = await supabase.storage + .from(supabaseBucket) + .remove([filePath]); + + if (error) { + console.error("Supabase delete error:", error); + return false; + } + + return true; + } catch (err) { + console.error("Delete from Supabase failed:", err); + return false; + } +} diff --git a/backend/yarn.lock b/backend/yarn.lock index 4588146..34c9b2c 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -4,12 +4,12 @@ "@apollo/cache-control-types@^1.0.3": version "1.0.3" - resolved "https://registry.yarnpkg.com/@apollo/cache-control-types/-/cache-control-types-1.0.3.tgz#5da62cf64c3b4419dabfef4536b57a40c8ff0b47" + resolved "https://registry.npmjs.org/@apollo/cache-control-types/-/cache-control-types-1.0.3.tgz" integrity sha512-F17/vCp7QVwom9eG7ToauIKdAxpSoadsJnqIfyryLFSkLSOEqu+eC5Z3N8OXcUVStuOMcNHlyraRsA6rRICu4g== "@apollo/protobufjs@1.2.7": version "1.2.7" - resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.7.tgz#3a8675512817e4a046a897e5f4f16415f16a7d8a" + resolved "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.7.tgz" integrity sha512-Lahx5zntHPZia35myYDBRuF58tlwPskwHc5CWBZC/4bMKB6siTBWwtMrkqXcsNwQiFSzSx5hKdRPUmemrEp3Gg== dependencies: "@protobufjs/aspromise" "^1.1.2" @@ -27,7 +27,7 @@ "@apollo/server-gateway-interface@^2.0.0": version "2.0.0" - resolved "https://registry.yarnpkg.com/@apollo/server-gateway-interface/-/server-gateway-interface-2.0.0.tgz#16e375f8edd302641ac60815f6c448e93a2a48a0" + resolved "https://registry.npmjs.org/@apollo/server-gateway-interface/-/server-gateway-interface-2.0.0.tgz" integrity sha512-3HEMD6fSantG2My3jWkb9dvfkF9vJ4BDLRjMgsnD790VINtuPaEp+h3Hg9HOHiWkML6QsOhnaRqZ+gvhp3y8Nw== dependencies: "@apollo/usage-reporting-protobuf" "^4.1.1" @@ -37,7 +37,7 @@ "@apollo/server@^5.2.0": version "5.2.0" - resolved "https://registry.yarnpkg.com/@apollo/server/-/server-5.2.0.tgz#0be9e77e523da58b7ffb4ad21ddf9c5277112d5a" + resolved "https://registry.npmjs.org/@apollo/server/-/server-5.2.0.tgz" integrity sha512-OEAl5bwVitkvVkmZlgWksSnQ10FUr6q2qJMdkexs83lsvOGmd/y81X5LoETmKZux8UiQsy/A/xzP00b8hTHH/w== dependencies: "@apollo/cache-control-types" "^1.0.3" @@ -63,14 +63,14 @@ "@apollo/usage-reporting-protobuf@^4.1.0", "@apollo/usage-reporting-protobuf@^4.1.1": version "4.1.1" - resolved "https://registry.yarnpkg.com/@apollo/usage-reporting-protobuf/-/usage-reporting-protobuf-4.1.1.tgz#407c3d18c7fbed7a264f3b9a3812620b93499de1" + resolved "https://registry.npmjs.org/@apollo/usage-reporting-protobuf/-/usage-reporting-protobuf-4.1.1.tgz" integrity sha512-u40dIUePHaSKVshcedO7Wp+mPiZsaU6xjv9J+VyxpoU/zL6Jle+9zWeG98tr/+SZ0nZ4OXhrbb8SNr0rAPpIDA== dependencies: "@apollo/protobufjs" "1.2.7" "@apollo/utils.createhash@^3.0.0": version "3.0.1" - resolved "https://registry.yarnpkg.com/@apollo/utils.createhash/-/utils.createhash-3.0.1.tgz#19fc6507e71d44303c2fffa51052305d689d288f" + resolved "https://registry.npmjs.org/@apollo/utils.createhash/-/utils.createhash-3.0.1.tgz" integrity sha512-CKrlySj4eQYftBE5MJ8IzKwIibQnftDT7yGfsJy5KSEEnLlPASX0UTpbKqkjlVEwPPd4mEwI7WOM7XNxEuO05A== dependencies: "@apollo/utils.isnodelike" "^3.0.0" @@ -78,22 +78,22 @@ "@apollo/utils.dropunuseddefinitions@^2.0.1": version "2.0.1" - resolved "https://registry.yarnpkg.com/@apollo/utils.dropunuseddefinitions/-/utils.dropunuseddefinitions-2.0.1.tgz#916cd912cbd88769d3b0eab2d24f4674eeda8124" + resolved "https://registry.npmjs.org/@apollo/utils.dropunuseddefinitions/-/utils.dropunuseddefinitions-2.0.1.tgz" integrity sha512-EsPIBqsSt2BwDsv8Wu76LK5R1KtsVkNoO4b0M5aK0hx+dGg9xJXuqlr7Fo34Dl+y83jmzn+UvEW+t1/GP2melA== "@apollo/utils.fetcher@^3.0.0": version "3.1.0" - resolved "https://registry.yarnpkg.com/@apollo/utils.fetcher/-/utils.fetcher-3.1.0.tgz#0639c30a5ac57e3e62784b180495023f5e886fe0" + resolved "https://registry.npmjs.org/@apollo/utils.fetcher/-/utils.fetcher-3.1.0.tgz" integrity sha512-Z3QAyrsQkvrdTuHAFwWDNd+0l50guwoQUoaDQssLOjkmnmVuvXlJykqlEJolio+4rFwBnWdoY1ByFdKaQEcm7A== "@apollo/utils.isnodelike@^3.0.0": version "3.0.0" - resolved "https://registry.yarnpkg.com/@apollo/utils.isnodelike/-/utils.isnodelike-3.0.0.tgz#0b51cb20e24850e49851f8b038fae24fd4a8beaf" + resolved "https://registry.npmjs.org/@apollo/utils.isnodelike/-/utils.isnodelike-3.0.0.tgz" integrity sha512-xrjyjfkzunZ0DeF6xkHaK5IKR8F1FBq6qV+uZ+h9worIF/2YSzA0uoBxGv6tbTeo9QoIQnRW4PVFzGix5E7n/g== "@apollo/utils.keyvaluecache@^4.0.0": version "4.0.0" - resolved "https://registry.yarnpkg.com/@apollo/utils.keyvaluecache/-/utils.keyvaluecache-4.0.0.tgz#c183fcc87263589e9e402651b2bb692e9f2353e0" + resolved "https://registry.npmjs.org/@apollo/utils.keyvaluecache/-/utils.keyvaluecache-4.0.0.tgz" integrity sha512-mKw1myRUkQsGPNB+9bglAuhviodJ2L2MRYLTafCMw5BIo7nbvCPNCkLnIHjZ1NOzH7SnMAr5c9LmXiqsgYqLZw== dependencies: "@apollo/utils.logger" "^3.0.0" @@ -101,34 +101,34 @@ "@apollo/utils.logger@^3.0.0": version "3.0.0" - resolved "https://registry.yarnpkg.com/@apollo/utils.logger/-/utils.logger-3.0.0.tgz#5b67de1e476bdfa8be70bfaa05c7e65a6a427960" + resolved "https://registry.npmjs.org/@apollo/utils.logger/-/utils.logger-3.0.0.tgz" integrity sha512-M8V8JOTH0F2qEi+ktPfw4RL7MvUycDfKp7aEap2eWXfL5SqWHN6jTLbj5f5fj1cceHpyaUSOZlvlaaryaxZAmg== "@apollo/utils.printwithreducedwhitespace@^2.0.1": version "2.0.1" - resolved "https://registry.yarnpkg.com/@apollo/utils.printwithreducedwhitespace/-/utils.printwithreducedwhitespace-2.0.1.tgz#f4fadea0ae849af2c19c339cc5420d1ddfaa905e" + resolved "https://registry.npmjs.org/@apollo/utils.printwithreducedwhitespace/-/utils.printwithreducedwhitespace-2.0.1.tgz" integrity sha512-9M4LUXV/fQBh8vZWlLvb/HyyhjJ77/I5ZKu+NBWV/BmYGyRmoEP9EVAy7LCVoY3t8BDcyCAGfxJaLFCSuQkPUg== "@apollo/utils.removealiases@2.0.1": version "2.0.1" - resolved "https://registry.yarnpkg.com/@apollo/utils.removealiases/-/utils.removealiases-2.0.1.tgz#2873c93d72d086c60fc0d77e23d0f75e66a2598f" + resolved "https://registry.npmjs.org/@apollo/utils.removealiases/-/utils.removealiases-2.0.1.tgz" integrity sha512-0joRc2HBO4u594Op1nev+mUF6yRnxoUH64xw8x3bX7n8QBDYdeYgY4tF0vJReTy+zdn2xv6fMsquATSgC722FA== "@apollo/utils.sortast@^2.0.1": version "2.0.1" - resolved "https://registry.yarnpkg.com/@apollo/utils.sortast/-/utils.sortast-2.0.1.tgz#58c90bb8bd24726346b61fa51ba7fcf06e922ef7" + resolved "https://registry.npmjs.org/@apollo/utils.sortast/-/utils.sortast-2.0.1.tgz" integrity sha512-eciIavsWpJ09za1pn37wpsCGrQNXUhM0TktnZmHwO+Zy9O4fu/WdB4+5BvVhFiZYOXvfjzJUcc+hsIV8RUOtMw== dependencies: lodash.sortby "^4.7.0" "@apollo/utils.stripsensitiveliterals@^2.0.1": version "2.0.1" - resolved "https://registry.yarnpkg.com/@apollo/utils.stripsensitiveliterals/-/utils.stripsensitiveliterals-2.0.1.tgz#2f3350483be376a98229f90185eaf19888323132" + resolved "https://registry.npmjs.org/@apollo/utils.stripsensitiveliterals/-/utils.stripsensitiveliterals-2.0.1.tgz" integrity sha512-QJs7HtzXS/JIPMKWimFnUMK7VjkGQTzqD9bKD1h3iuPAqLsxd0mUNVbkYOPTsDhUKgcvUOfOqOJWYohAKMvcSA== "@apollo/utils.usagereporting@^2.1.0": version "2.1.0" - resolved "https://registry.yarnpkg.com/@apollo/utils.usagereporting/-/utils.usagereporting-2.1.0.tgz#11bca6a61fcbc6e6d812004503b38916e74313f4" + resolved "https://registry.npmjs.org/@apollo/utils.usagereporting/-/utils.usagereporting-2.1.0.tgz" integrity sha512-LPSlBrn+S17oBy5eWkrRSGb98sWmnEzo3DPTZgp8IQc8sJe0prDgDuppGq4NeQlpoqEHz0hQeYHAOA0Z3aQsxQ== dependencies: "@apollo/usage-reporting-protobuf" "^4.1.0" @@ -140,17 +140,17 @@ "@apollo/utils.withrequired@^3.0.0": version "3.0.0" - resolved "https://registry.yarnpkg.com/@apollo/utils.withrequired/-/utils.withrequired-3.0.0.tgz#1342bab438528d09c41e43ce33cb976fa9c99e52" + resolved "https://registry.npmjs.org/@apollo/utils.withrequired/-/utils.withrequired-3.0.0.tgz" integrity sha512-aaxeavfJ+RHboh7c2ofO5HHtQobGX4AgUujXP4CXpREHp9fQ9jPi6K9T1jrAKe7HIipoP0OJ1gd6JamSkFIpvA== "@as-integrations/express5@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@as-integrations/express5/-/express5-1.1.2.tgz#5c5b207b8e457bbd546e9b20898d0b1f4a478480" + resolved "https://registry.npmjs.org/@as-integrations/express5/-/express5-1.1.2.tgz" integrity sha512-BxfwtcWNf2CELDkuPQxi5Zl3WqY/dQVJYafeCBOGoFQjv5M0fjhxmAFZ9vKx/5YKKNeok4UY6PkFbHzmQrdxIA== "@chevrotain/cst-dts-gen@10.5.0": version "10.5.0" - resolved "https://registry.yarnpkg.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz#922ebd8cc59d97241bb01b1b17561a5c1ae0124e" + resolved "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz" integrity sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw== dependencies: "@chevrotain/gast" "10.5.0" @@ -159,7 +159,7 @@ "@chevrotain/gast@10.5.0": version "10.5.0" - resolved "https://registry.yarnpkg.com/@chevrotain/gast/-/gast-10.5.0.tgz#e4e614bc46d17a8892742f38e56cd33f1f3ad162" + resolved "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz" integrity sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A== dependencies: "@chevrotain/types" "10.5.0" @@ -167,32 +167,32 @@ "@chevrotain/types@10.5.0": version "10.5.0" - resolved "https://registry.yarnpkg.com/@chevrotain/types/-/types-10.5.0.tgz#52a97d74a8cfbc197f054636d93ecd8912d33d21" + resolved "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz" integrity sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A== "@chevrotain/utils@10.5.0": version "10.5.0" - resolved "https://registry.yarnpkg.com/@chevrotain/utils/-/utils-10.5.0.tgz#0ee36f65b49b447fbac71b9e5af5c5c6c98ac057" + resolved "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz" integrity sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ== "@electric-sql/pglite-socket@0.0.6": version "0.0.6" - resolved "https://registry.yarnpkg.com/@electric-sql/pglite-socket/-/pglite-socket-0.0.6.tgz#2808e1651a6980ca6b55c6950d06702e00b59815" + resolved "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.6.tgz" integrity sha512-6RjmgzphIHIBA4NrMGJsjNWK4pu+bCWJlEWlwcxFTVY3WT86dFpKwbZaGWZV6C5Rd7sCk1Z0CI76QEfukLAUXw== "@electric-sql/pglite-tools@0.2.7": version "0.2.7" - resolved "https://registry.yarnpkg.com/@electric-sql/pglite-tools/-/pglite-tools-0.2.7.tgz#f8a639a60e1fc5a8cd307aae1711db02a7781875" + resolved "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.7.tgz" integrity sha512-9dAccClqxx4cZB+Ar9B+FZ5WgxDc/Xvl9DPrTWv+dYTf0YNubLzi4wHHRGRGhrJv15XwnyKcGOZAP1VXSneSUg== "@electric-sql/pglite@0.3.2": version "0.3.2" - resolved "https://registry.yarnpkg.com/@electric-sql/pglite/-/pglite-0.3.2.tgz#1962142ac261aab3c9d54b95c1fe1edc492d6fa0" + resolved "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.2.tgz" integrity sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w== "@graphql-tools/merge@^9.1.6": version "9.1.6" - resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-9.1.6.tgz#95eb8bb5ae314bc07a62938b7a1b8c2622e1dc32" + resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.6.tgz" integrity sha512-bTnP+4oom4nDjmkS3Ykbe+ljAp/RIiWP3R35COMmuucS24iQxGLa9Hn8VMkLIoaoPxgz6xk+dbC43jtkNsFoBw== dependencies: "@graphql-tools/utils" "^10.11.0" @@ -200,7 +200,7 @@ "@graphql-tools/schema@^10.0.0": version "10.0.30" - resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-10.0.30.tgz#cdc9583e70a297d53ff0b344935bd81e3bf7d7ac" + resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.30.tgz" integrity sha512-yPXU17uM/LR90t92yYQqn9mAJNOVZJc0nQtYeZyZeQZeQjwIGlTubvvoDL0fFVk+wZzs4YQOgds2NwSA4npodA== dependencies: "@graphql-tools/merge" "^9.1.6" @@ -209,7 +209,7 @@ "@graphql-tools/utils@^10.11.0": version "10.11.0" - resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-10.11.0.tgz#2513dca9ca25bab5a2651da2d66c37b1e2616bdf" + resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.11.0.tgz" integrity sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q== dependencies: "@graphql-typed-document-node/core" "^3.1.1" @@ -219,17 +219,17 @@ "@graphql-typed-document-node/core@^3.1.1": version "3.2.0" - resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" + resolved "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== "@hono/node-server@1.19.6": version "1.19.6" - resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.6.tgz#44ec78607f37d3deb9476937049c721c7dc79390" + resolved "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.6.tgz" integrity sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw== "@mrleebo/prisma-ast@0.12.1": version "0.12.1" - resolved "https://registry.yarnpkg.com/@mrleebo/prisma-ast/-/prisma-ast-0.12.1.tgz#cb1cac178d98e351b6f300981abc1ffeb28f7cdd" + resolved "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.12.1.tgz" integrity sha512-JwqeCQ1U3fvccttHZq7Tk0m/TMC6WcFAQZdukypW3AzlJYKYTGNVd1ANU2GuhKnv4UQuOFj3oAl0LLG/gxFN1w== dependencies: chevrotain "^10.5.0" @@ -237,7 +237,7 @@ "@prisma/adapter-pg@^7.2.0": version "7.2.0" - resolved "https://registry.yarnpkg.com/@prisma/adapter-pg/-/adapter-pg-7.2.0.tgz#a956ae4b6b168ed2e1c687a0b45e0d1633959bf0" + resolved "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.2.0.tgz" integrity sha512-euIdQ13cRB2wZ3jPsnDnFhINquo1PYFPCg6yVL8b2rp3EdinQHsX9EDdCtRr489D5uhphcRk463OdQAFlsCr0w== dependencies: "@prisma/driver-adapter-utils" "7.2.0" @@ -246,19 +246,19 @@ "@prisma/client-runtime-utils@7.2.0": version "7.2.0" - resolved "https://registry.yarnpkg.com/@prisma/client-runtime-utils/-/client-runtime-utils-7.2.0.tgz#b17c9667d0d857988f6dadf3d6fbacf787ee3109" + resolved "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.2.0.tgz" integrity sha512-dn7oB53v0tqkB0wBdMuTNFNPdEbfICEUe82Tn9FoKAhJCUkDH+fmyEp0ClciGh+9Hp2Tuu2K52kth2MTLstvmA== "@prisma/client@^7.2.0": version "7.2.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-7.2.0.tgz#75681fe23843093d3b07730aaddc654ae12fbc0e" + resolved "https://registry.npmjs.org/@prisma/client/-/client-7.2.0.tgz" integrity sha512-JdLF8lWZ+LjKGKpBqyAlenxd/kXjd1Abf/xK+6vUA7R7L2Suo6AFTHFRpPSdAKCan9wzdFApsUpSa/F6+t1AtA== dependencies: "@prisma/client-runtime-utils" "7.2.0" "@prisma/config@7.2.0": version "7.2.0" - resolved "https://registry.yarnpkg.com/@prisma/config/-/config-7.2.0.tgz#cc7b81d445df9ca75d67ea7e870844597b38eef6" + resolved "https://registry.npmjs.org/@prisma/config/-/config-7.2.0.tgz" integrity sha512-qmvSnfQ6l/srBW1S7RZGfjTQhc44Yl3ldvU6y3pgmuLM+83SBDs6UQVgMtQuMRe9J3gGqB0RF8wER6RlXEr6jQ== dependencies: c12 "3.1.0" @@ -268,17 +268,17 @@ "@prisma/debug@6.8.2": version "6.8.2" - resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-6.8.2.tgz#59fb9e0ccb0f431fe7011c36c95f9bfcbe051749" + resolved "https://registry.npmjs.org/@prisma/debug/-/debug-6.8.2.tgz" integrity sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg== "@prisma/debug@7.2.0": version "7.2.0" - resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-7.2.0.tgz#569b1cbc10eb3e8cae798b40075fd11d21f6b533" + resolved "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz" integrity sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw== "@prisma/dev@0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@prisma/dev/-/dev-0.17.0.tgz#0423bc4e749d080140d316d90e08353adf52ee7a" + resolved "https://registry.npmjs.org/@prisma/dev/-/dev-0.17.0.tgz" integrity sha512-6sGebe5jxX+FEsQTpjHLzvOGPn6ypFQprcs3jcuIWv1Xp/5v6P/rjfdvAwTkP2iF6pDx2tCd8vGLNWcsWzImTA== dependencies: "@electric-sql/pglite" "0.3.2" @@ -301,19 +301,19 @@ "@prisma/driver-adapter-utils@7.2.0": version "7.2.0" - resolved "https://registry.yarnpkg.com/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.2.0.tgz#53d6686cdbb695338826e1c8ab5ac97c92efb676" + resolved "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.2.0.tgz" integrity sha512-gzrUcbI9VmHS24Uf+0+7DNzdIw7keglJsD5m/MHxQOU68OhGVzlphQRobLiDMn8CHNA2XN8uugwKjudVtnfMVQ== dependencies: "@prisma/debug" "7.2.0" "@prisma/engines-version@7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3": version "7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3.tgz#37104679cfa7cc5f5434bf02fa22b4b0cf90b94f" + resolved "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3.tgz" integrity sha512-KezsjCZDsbjNR7SzIiVlUsn9PnLePI7r5uxABlwL+xoerurZTfgQVbIjvjF2sVr3Uc0ZcsnREw3F84HvbggGdA== "@prisma/engines@7.2.0": version "7.2.0" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-7.2.0.tgz#7451f22ad7526c8f84b7dce889d796aeffd332be" + resolved "https://registry.npmjs.org/@prisma/engines/-/engines-7.2.0.tgz" integrity sha512-HUeOI/SvCDsHrR9QZn24cxxZcujOjcS3w1oW/XVhnSATAli5SRMOfp/WkG3TtT5rCxDA4xOnlJkW7xkho4nURA== dependencies: "@prisma/debug" "7.2.0" @@ -323,7 +323,7 @@ "@prisma/fetch-engine@7.2.0": version "7.2.0" - resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-7.2.0.tgz#956d8aff4420534dffc9c7e034243f711803e966" + resolved "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.2.0.tgz" integrity sha512-Z5XZztJ8Ap+wovpjPD2lQKnB8nWFGNouCrglaNFjxIWAGWz0oeHXwUJRiclIoSSXN/ptcs9/behptSk8d0Yy6w== dependencies: "@prisma/debug" "7.2.0" @@ -332,51 +332,51 @@ "@prisma/get-platform@6.8.2": version "6.8.2" - resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-6.8.2.tgz#a6509de61ceab4fca80616b7e8d73705b2705a72" + resolved "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.8.2.tgz" integrity sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow== dependencies: "@prisma/debug" "6.8.2" "@prisma/get-platform@7.2.0": version "7.2.0" - resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-7.2.0.tgz#b3a92db68de6a76e840e61d2f26659aa9f915e3e" + resolved "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz" integrity sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA== dependencies: "@prisma/debug" "7.2.0" "@prisma/query-plan-executor@6.18.0": version "6.18.0" - resolved "https://registry.yarnpkg.com/@prisma/query-plan-executor/-/query-plan-executor-6.18.0.tgz#231304e8b51959cb671ce8a6038f7e0e5c24bd9d" + resolved "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-6.18.0.tgz" integrity sha512-jZ8cfzFgL0jReE1R10gT8JLHtQxjWYLiQ//wHmVYZ2rVkFHoh0DT8IXsxcKcFlfKN7ak7k6j0XMNn2xVNyr5cA== "@prisma/studio-core@0.9.0": version "0.9.0" - resolved "https://registry.yarnpkg.com/@prisma/studio-core/-/studio-core-0.9.0.tgz#aad8b49467adeb4100d1070a0940dc3ffce8d0ef" + resolved "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.9.0.tgz" integrity sha512-xA2zoR/ADu/NCSQuriBKTh6Ps4XjU0bErkEcgMfnSGh346K1VI7iWKnoq1l2DoxUqiddPHIEWwtxJ6xCHG6W7g== "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz" integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== "@protobufjs/base64@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + resolved "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz" integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== "@protobufjs/codegen@^2.0.4": version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + resolved "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz" integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== "@protobufjs/eventemitter@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + resolved "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz" integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== "@protobufjs/fetch@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + resolved "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz" integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== dependencies: "@protobufjs/aspromise" "^1.1.1" @@ -384,37 +384,92 @@ "@protobufjs/float@^1.0.2": version "1.0.2" - resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + resolved "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz" integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== "@protobufjs/inquire@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + resolved "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz" integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== "@protobufjs/path@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + resolved "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz" integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== "@protobufjs/pool@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + resolved "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz" integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== "@protobufjs/utf8@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@socket.io/component-emitter@~3.1.0": + version "3.1.2" + resolved "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz" + integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== + "@standard-schema/spec@^1.0.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" + resolved "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz" integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== +"@supabase/auth-js@2.90.0": + version "2.90.0" + resolved "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.90.0.tgz" + integrity sha512-J5Jgr1XE86NpK0LX8yLdjv3NzzsUnGky72QIGR1AcdVXHQIhwjb3JFEjNutODNKyOrknx00lyClOvqooHFHIiw== + dependencies: + tslib "2.8.1" + +"@supabase/functions-js@2.90.0": + version "2.90.0" + resolved "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.90.0.tgz" + integrity sha512-YEk4AVtdaUzjxhil2GemOjq0VQVQfg8LmPyooJt80lNscQHfDyCGmHeQ6pDI48dkICTE87V7KVrlXaVGCZa1Rw== + dependencies: + tslib "2.8.1" + +"@supabase/postgrest-js@2.90.0": + version "2.90.0" + resolved "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.90.0.tgz" + integrity sha512-XCgrXuGtBPgxQWKfS8tZ4/deK5+cgUW+XLl4zq4v7JRUfCHlc3ZewZid5hjcpAIDYwGWq9ihrfPUNDAfl5jDdg== + dependencies: + tslib "2.8.1" + +"@supabase/realtime-js@2.90.0": + version "2.90.0" + resolved "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.90.0.tgz" + integrity sha512-Hncs2Obk2Wd4Mcj51XDtLqOuiDT6onSmpHkwjsaLDZylgLi4uN81W4LYhj+rdPm3A9nQIRB4/5SZ3Hab2Adlsg== + dependencies: + "@types/phoenix" "^1.6.6" + "@types/ws" "^8.18.1" + tslib "2.8.1" + ws "^8.18.2" + +"@supabase/storage-js@2.90.0": + version "2.90.0" + resolved "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.90.0.tgz" + integrity sha512-50XiDACfHXhibm20J94TEdOYMLXm4fZCHTdAAs9xBDJTBA4RXVN52Bx1MVW34pu3S72rAgObQxVyAFaZ+7+7Tg== + dependencies: + iceberg-js "^0.8.1" + tslib "2.8.1" + +"@supabase/supabase-js@^2.90.0": + version "2.90.0" + resolved "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.90.0.tgz" + integrity sha512-foWQl5x1+LuqT7qdnAoCDcKHTNBSaED+eSEOEmmpqXiQuCfJU2K0lDkfGm7PUBx/BDaw3tFObzuJyZtHDbrehA== + dependencies: + "@supabase/auth-js" "2.90.0" + "@supabase/functions-js" "2.90.0" + "@supabase/postgrest-js" "2.90.0" + "@supabase/realtime-js" "2.90.0" + "@supabase/storage-js" "2.90.0" + "@types/body-parser@*": version "1.19.6" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz" integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== dependencies: "@types/connect" "*" @@ -422,26 +477,26 @@ "@types/connect@*": version "3.4.38" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== dependencies: "@types/node" "*" "@types/cookie-parser@^1.4.10": version "1.4.10" - resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.10.tgz#a045272a383a30597a01955d4f9c790018f214e4" + resolved "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz" integrity sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg== -"@types/cors@^2.8.19": +"@types/cors@^2.8.12", "@types/cors@^2.8.19": version "2.8.19" - resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" + resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz" integrity sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg== dependencies: "@types/node" "*" "@types/express-serve-static-core@^5.0.0": version "5.1.0" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz#74f47555b3d804b54cb7030e6f9aa0c7485cfc5b" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz" integrity sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA== dependencies: "@types/node" "*" @@ -449,9 +504,9 @@ "@types/range-parser" "*" "@types/send" "*" -"@types/express@^5.0.6": +"@types/express@*", "@types/express@^5.0.6": version "5.0.6" - resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.6.tgz#2d724b2c990dcb8c8444063f3580a903f6d500cc" + resolved "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz" integrity sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA== dependencies: "@types/body-parser" "*" @@ -460,12 +515,12 @@ "@types/http-errors@*": version "2.0.5" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz" integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== "@types/jsonwebtoken@^9.0.10": version "9.0.10" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz#a7932a47177dcd4283b6146f3bd5c26d82647f09" + resolved "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz" integrity sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA== dependencies: "@types/ms" "*" @@ -473,92 +528,129 @@ "@types/long@^4.0.0": version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + resolved "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== "@types/ms@*": version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz" integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== -"@types/node@*", "@types/node@^25.0.3": +"@types/multer@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz" + integrity sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw== + dependencies: + "@types/express" "*" + +"@types/node@*", "@types/node@>=10.0.0", "@types/node@^25.0.3": version "25.0.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.3.tgz#79b9ac8318f373fbfaaf6e2784893efa9701f269" + resolved "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz" integrity sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA== dependencies: undici-types "~7.16.0" "@types/pg@^8.16.0": version "8.16.0" - resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.16.0.tgz#b7af0d642752340b7c9de1c33afd9bc5c5f0ebeb" + resolved "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz" integrity sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ== dependencies: "@types/node" "*" pg-protocol "*" pg-types "^2.2.0" +"@types/phoenix@^1.6.6": + version "1.6.7" + resolved "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz" + integrity sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q== + "@types/qs@*": version "6.14.0" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz" integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ== "@types/range-parser@*": version "1.2.7" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== "@types/send@*": version "1.2.1" - resolved "https://registry.yarnpkg.com/@types/send/-/send-1.2.1.tgz#6a784e45543c18c774c049bff6d3dbaf045c9c74" + resolved "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz" integrity sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ== dependencies: "@types/node" "*" "@types/serve-static@^2": version "2.2.0" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-2.2.0.tgz#d4a447503ead0d1671132d1ab6bd58b805d8de6a" + resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz" integrity sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ== dependencies: "@types/http-errors" "*" "@types/node" "*" +"@types/ws@^8.18.1": + version "8.18.1" + resolved "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== + dependencies: + "@types/node" "*" + "@whatwg-node/promise-helpers@^1.0.0": version "1.3.2" - resolved "https://registry.yarnpkg.com/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz#3b54987ad6517ef6db5920c66a6f0dada606587d" + resolved "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz" integrity sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA== dependencies: tslib "^2.6.3" accepts@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + resolved "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz" integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== dependencies: mime-types "^3.0.0" negotiator "^1.0.0" +accepts@~1.3.4: + version "1.3.8" + resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + async-retry@^1.2.1: version "1.3.3" - resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + resolved "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz" integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== dependencies: retry "0.13.1" available-typed-arrays@^1.0.7: version "1.0.7" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz" integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== dependencies: possible-typed-array-names "^1.0.0" aws-ssl-profiles@^1.1.1: version "1.1.2" - resolved "https://registry.yarnpkg.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641" + resolved "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz" integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g== +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + body-parser@^2.2.0, body-parser@^2.2.1: version "2.2.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.1.tgz#6df606b0eb0a6e3f783dde91dde182c24c82438c" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz" integrity sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw== dependencies: bytes "^3.1.2" @@ -573,17 +665,29 @@ body-parser@^2.2.0, body-parser@^2.2.1: buffer-equal-constant-time@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +busboy@^1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + bytes@^3.1.2, bytes@~3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== c12@3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/c12/-/c12-3.1.0.tgz#9e237970e1d3b74ebae51d25945cb59664c12c89" + resolved "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz" integrity sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw== dependencies: chokidar "^4.0.3" @@ -601,7 +705,7 @@ c12@3.1.0: call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: es-errors "^1.3.0" @@ -609,7 +713,7 @@ call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply- call-bind@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz" integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== dependencies: call-bind-apply-helpers "^1.0.0" @@ -619,7 +723,7 @@ call-bind@^1.0.8: call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + resolved "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz" integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== dependencies: call-bind-apply-helpers "^1.0.2" @@ -627,7 +731,7 @@ call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: chevrotain@^10.5.0: version "10.5.0" - resolved "https://registry.yarnpkg.com/chevrotain/-/chevrotain-10.5.0.tgz#9c1dc62ef0753bb562dbe521b5f72d041bad624e" + resolved "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz" integrity sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A== dependencies: "@chevrotain/cst-dts-gen" "10.5.0" @@ -639,41 +743,51 @@ chevrotain@^10.5.0: chokidar@^4.0.3: version "4.0.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz" integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== dependencies: readdirp "^4.0.1" citty@^0.1.6: version "0.1.6" - resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.6.tgz#0f7904da1ed4625e1a9ea7e0fa780981aab7c5e4" + resolved "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz" integrity sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ== dependencies: consola "^3.2.3" +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + confbox@^0.2.2: version "0.2.2" - resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.2.tgz#8652f53961c74d9e081784beed78555974a9c110" + resolved "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz" integrity sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ== consola@^3.2.3, consola@^3.4.0, consola@^3.4.2: version "3.4.2" - resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7" + resolved "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz" integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== content-disposition@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.1.tgz#a8b7bbeb2904befdfb6787e5c0c086959f605f9b" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz" integrity sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q== content-type@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== cookie-parser@^1.4.7: version "1.4.7" - resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.7.tgz#e2125635dfd766888ffe90d60c286404fa0e7b26" + resolved "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz" integrity sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw== dependencies: cookie "0.7.2" @@ -681,22 +795,22 @@ cookie-parser@^1.4.7: cookie-signature@1.0.6: version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== cookie-signature@^1.2.1: version "1.2.2" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz" integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== -cookie@0.7.2, cookie@^0.7.1: +cookie@0.7.2, cookie@^0.7.1, cookie@~0.7.2: version "0.7.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== -cors@^2.8.5: +cors@^2.8.5, cors@~2.8.5: version "2.8.5" - resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== dependencies: object-assign "^4" @@ -704,35 +818,35 @@ cors@^2.8.5: cross-inspect@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/cross-inspect/-/cross-inspect-1.0.1.tgz#15f6f65e4ca963cf4cc1a2b5fef18f6ca328712b" + resolved "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz" integrity sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A== dependencies: tslib "^2.4.0" cross-spawn@^7.0.6: version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" which "^2.0.1" -debug@^4.4.0, debug@^4.4.3: +debug@^4.4.0, debug@^4.4.3, debug@~4.4.1: version "4.4.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" deepmerge-ts@7.1.5: version "7.1.5" - resolved "https://registry.yarnpkg.com/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz#ff818564007f5c150808d2b7b732cac83aa415ab" + resolved "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz" integrity sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw== define-data-property@^1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: es-define-property "^1.0.0" @@ -741,37 +855,37 @@ define-data-property@^1.1.4: defu@^6.1.4: version "6.1.4" - resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" + resolved "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz" integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== denque@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + resolved "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz" integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== depd@^2.0.0, depd@~2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== destr@^2.0.3: version "2.0.5" - resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.5.tgz#7d112ff1b925fb8d2079fac5bdb4a90973b51fdb" + resolved "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz" integrity sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA== dotenv@^16.6.1: version "16.6.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz" integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== dotenv@^17.2.3: version "17.2.3" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.2.3.tgz#ad995d6997f639b11065f419a22fabf567cdb9a2" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz" integrity sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w== dunder-proto@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== dependencies: call-bind-apply-helpers "^1.0.1" @@ -780,24 +894,24 @@ dunder-proto@^1.0.1: duplexer@~0.1.1: version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== ecdsa-sig-formatter@1.0.11: version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== dependencies: safe-buffer "^5.0.1" ee-first@1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== effect@3.18.4: version "3.18.4" - resolved "https://registry.yarnpkg.com/effect/-/effect-3.18.4.tgz#e241fde5cc090608c51c5e29e2da357bb5d0e815" + resolved "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz" integrity sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA== dependencies: "@standard-schema/spec" "^1.0.0" @@ -805,44 +919,64 @@ effect@3.18.4: empathic@2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/empathic/-/empathic-2.0.0.tgz#71d3c2b94fad49532ef98a6c34be0386659f6131" + resolved "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz" integrity sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA== encodeurl@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== + +engine.io@~6.6.0: + version "6.6.5" + resolved "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz" + integrity sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A== + dependencies: + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.7.2" + cors "~2.8.5" + debug "~4.4.1" + engine.io-parser "~5.2.1" + ws "~8.18.3" + es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz" integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== dependencies: es-errors "^1.3.0" escape-html@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== etag@^1.8.1: version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== event-stream@=3.3.4: version "3.3.4" - resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" + resolved "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz" integrity sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g== dependencies: duplexer "~0.1.1" @@ -855,7 +989,7 @@ event-stream@=3.3.4: express@^5.2.1: version "5.2.1" - resolved "https://registry.yarnpkg.com/express/-/express-5.2.1.tgz#8f21d15b6d327f92b4794ecf8cb08a72f956ac04" + resolved "https://registry.npmjs.org/express/-/express-5.2.1.tgz" integrity sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw== dependencies: accepts "^2.0.0" @@ -889,19 +1023,19 @@ express@^5.2.1: exsolve@^1.0.7: version "1.0.8" - resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.8.tgz#7f5e34da61cd1116deda5136e62292c096f50613" + resolved "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz" integrity sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA== fast-check@^3.23.1: version "3.23.2" - resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-3.23.2.tgz#0129f1eb7e4f500f58e8290edc83c670e4a574a2" + resolved "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz" integrity sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A== dependencies: pure-rand "^6.1.0" finalhandler@^2.1.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.1.tgz#a2c517a6559852bcdb06d1f8bd7f51b68fad8099" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz" integrity sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA== dependencies: debug "^4.4.0" @@ -913,14 +1047,14 @@ finalhandler@^2.1.0: for-each@^0.3.5: version "0.3.5" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz" integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== dependencies: is-callable "^1.2.7" foreground-child@3.3.1: version "3.3.1" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz" integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== dependencies: cross-spawn "^7.0.6" @@ -928,34 +1062,34 @@ foreground-child@3.3.1: forwarded@0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== fresh@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + resolved "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz" integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== from@~0: version "0.1.7" - resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" + resolved "https://registry.npmjs.org/from/-/from-0.1.7.tgz" integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== function-bind@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== generate-function@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + resolved "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz" integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== dependencies: is-property "^1.0.2" get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: call-bind-apply-helpers "^1.0.2" @@ -971,12 +1105,12 @@ get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: get-port-please@3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/get-port-please/-/get-port-please-3.1.2.tgz#502795e56217128e4183025c89a48c71652f4e49" + resolved "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz" integrity sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ== get-proto@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== dependencies: dunder-proto "^1.0.1" @@ -984,7 +1118,7 @@ get-proto@^1.0.1: giget@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/giget/-/giget-2.0.0.tgz#395fc934a43f9a7a29a29d55b99f23e30c14f195" + resolved "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz" integrity sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA== dependencies: citty "^0.1.6" @@ -996,58 +1130,58 @@ giget@^2.0.0: gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== graceful-fs@^4.2.4: version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== grammex@^3.1.10: version "3.1.12" - resolved "https://registry.yarnpkg.com/grammex/-/grammex-3.1.12.tgz#08f021dd2cad009e64248fb53247cc6108788404" + resolved "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz" integrity sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ== graphql@^16.12.0: version "16.12.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.12.0.tgz#28cc2462435b1ac3fdc6976d030cef83a0c13ac7" + resolved "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz" integrity sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ== has-property-descriptors@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: es-define-property "^1.0.0" has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has-tostringtag@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz" integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: has-symbols "^1.0.3" hasown@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" hono@4.10.6: version "4.10.6" - resolved "https://registry.yarnpkg.com/hono/-/hono-4.10.6.tgz#b80cf3903b3d12b02c472dadf28d046874e89e5a" + resolved "https://registry.npmjs.org/hono/-/hono-4.10.6.tgz" integrity sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g== http-errors@^2.0.0, http-errors@^2.0.1, http-errors@~2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz" integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== dependencies: depd "~2.0.0" @@ -1058,66 +1192,71 @@ http-errors@^2.0.0, http-errors@^2.0.1, http-errors@~2.0.1: http-status-codes@2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.3.0.tgz#987fefb28c69f92a43aecc77feec2866349a8bfc" + resolved "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz" integrity sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA== +iceberg-js@^0.8.1: + version "0.8.1" + resolved "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz" + integrity sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA== + iconv-lite@^0.7.0, iconv-lite@~0.7.0: version "0.7.1" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.1.tgz#d4af1d2092f2bb05aab6296e5e7cd286d2f15432" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz" integrity sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw== dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -inherits@^2.0.4, inherits@~2.0.4: +inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== ipaddr.js@1.9.1: version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== is-callable@^1.2.7: version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== is-promise@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + resolved "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz" integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== is-property@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + resolved "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g== is-typed-array@^1.1.14: version "1.1.15" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz" integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== dependencies: which-typed-array "^1.1.16" isarray@^2.0.5: version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== isexe@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== jiti@^2.4.2: version "2.6.1" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92" + resolved "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz" integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ== jsonwebtoken@^9.0.3: version "9.0.3" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2" + resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz" integrity sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g== dependencies: jws "^4.0.1" @@ -1133,7 +1272,7 @@ jsonwebtoken@^9.0.3: jwa@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" + resolved "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz" integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== dependencies: buffer-equal-constant-time "^1.0.1" @@ -1142,7 +1281,7 @@ jwa@^2.0.1: jws@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690" + resolved "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz" integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA== dependencies: jwa "^2.0.1" @@ -1150,119 +1289,161 @@ jws@^4.0.1: lilconfig@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== lodash.includes@^4.3.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz" integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== lodash.isboolean@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== lodash.isinteger@^4.0.4: version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz" integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== lodash.isnumber@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz" integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== lodash.isplainobject@^4.0.6: version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz" integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== lodash.isstring@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz" integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== lodash.once@^4.0.0: version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== lodash.sortby@^4.7.0: version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== lodash@4.17.21: version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== loglevel@^1.6.8: version "1.9.2" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.2.tgz#c2e028d6c757720107df4e64508530db6621ba08" + resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz" integrity sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg== long@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + resolved "https://registry.npmjs.org/long/-/long-4.0.0.tgz" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== long@^5.2.1: version "5.3.2" - resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + resolved "https://registry.npmjs.org/long/-/long-5.3.2.tgz" integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== lru-cache@^11.0.0, lru-cache@^11.1.0: version "11.2.4" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.4.tgz#ecb523ebb0e6f4d837c807ad1abaea8e0619770d" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz" integrity sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg== lru.min@^1.0.0, lru.min@^1.1.0: version "1.1.3" - resolved "https://registry.yarnpkg.com/lru.min/-/lru.min-1.1.3.tgz#c8c3d001dfb4cbe5b8d1f4bea207d4a320e5d76f" + resolved "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz" integrity sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q== map-stream@~0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" + resolved "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz" integrity sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g== math-intrinsics@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + media-typer@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz" integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== merge-descriptors@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz" integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + mime-db@^1.54.0: version "1.54.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz" integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== mime-types@^3.0.0, mime-types@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz" integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== dependencies: mime-db "^1.54.0" +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.6: + version "0.5.6" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + ms@^2.1.1, ms@^2.1.3: version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +multer@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz" + integrity sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw== + dependencies: + append-field "^1.0.0" + busboy "^1.6.0" + concat-stream "^2.0.0" + mkdirp "^0.5.6" + object-assign "^4.1.1" + type-is "^1.6.18" + xtend "^4.0.2" + mysql2@3.15.3: version "3.15.3" - resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.15.3.tgz#f0348d9c7401bb98cb1f45ffc5a773b109f70808" + resolved "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz" integrity sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg== dependencies: aws-ssl-profiles "^1.1.1" @@ -1277,29 +1458,34 @@ mysql2@3.15.3: named-placeholders@^1.1.3: version "1.1.6" - resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.6.tgz#c50c6920b43f258f59c16add1e56654f5cc02bb5" + resolved "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz" integrity sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w== dependencies: lru.min "^1.1.0" +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + negotiator@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz" integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== node-cleanup@^2.1.2: version "2.1.2" - resolved "https://registry.yarnpkg.com/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c" + resolved "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz" integrity sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw== node-fetch-native@^1.6.6: version "1.6.7" - resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz#9d09ca63066cc48423211ed4caf5d70075d76a71" + resolved "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz" integrity sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q== nypm@^0.6.0: version "0.6.2" - resolved "https://registry.yarnpkg.com/nypm/-/nypm-0.6.2.tgz#467512024948398fafa73cea30a3ed9efc5af071" + resolved "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz" integrity sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g== dependencies: citty "^0.1.6" @@ -1308,95 +1494,95 @@ nypm@^0.6.0: pkg-types "^2.3.0" tinyexec "^1.0.1" -object-assign@^4: +object-assign@^4, object-assign@^4.1.1: version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== object-inspect@^1.13.3: version "1.13.4" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz" integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== ohash@^2.0.11: version "2.0.11" - resolved "https://registry.yarnpkg.com/ohash/-/ohash-2.0.11.tgz#60b11e8cff62ca9dee88d13747a5baa145f5900b" + resolved "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz" integrity sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ== on-finished@^2.4.1: version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== dependencies: ee-first "1.1.1" once@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" parseurl@^1.3.3: version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== path-key@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-to-regexp@^8.0.0: version "8.3.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz" integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA== pathe@2.0.3, pathe@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz" integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== pause-stream@0.0.11: version "0.0.11" - resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + resolved "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz" integrity sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A== dependencies: through "~2.3" perfect-debounce@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" + resolved "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz" integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== pg-cloudflare@^1.2.7: version "1.2.7" - resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz#a1f3d226bab2c45ae75ea54d65ec05ac6cfafbef" + resolved "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz" integrity sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg== pg-connection-string@^2.9.1: version "2.9.1" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.9.1.tgz#bb1fd0011e2eb76ac17360dc8fa183b2d3465238" + resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz" integrity sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w== pg-int8@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + resolved "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz" integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== pg-pool@^3.10.1: version "3.10.1" - resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.10.1.tgz#481047c720be2d624792100cac1816f8850d31b2" + resolved "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz" integrity sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg== pg-protocol@*, pg-protocol@^1.10.3: version "1.10.3" - resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.10.3.tgz#ac9e4778ad3f84d0c5670583bab976ea0a34f69f" + resolved "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz" integrity sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ== pg-types@2.2.0, pg-types@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + resolved "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz" integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== dependencies: pg-int8 "1.0.1" @@ -1407,7 +1593,7 @@ pg-types@2.2.0, pg-types@^2.2.0: pg@^8.16.3: version "8.16.3" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.16.3.tgz#160741d0b44fdf64680e45374b06d632e86c99fd" + resolved "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz" integrity sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw== dependencies: pg-connection-string "^2.9.1" @@ -1420,14 +1606,14 @@ pg@^8.16.3: pgpass@1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + resolved "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz" integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== dependencies: split2 "^4.1.0" pkg-types@^2.2.0, pkg-types@^2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-2.3.0.tgz#037f2c19bd5402966ff6810e32706558cb5b5726" + resolved "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz" integrity sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig== dependencies: confbox "^0.2.2" @@ -1436,44 +1622,44 @@ pkg-types@^2.2.0, pkg-types@^2.3.0: possible-typed-array-names@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" + resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz" integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== postgres-array@3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-3.0.4.tgz#4efcaf4d2c688d8bcaa8620ed13f35f299f7528c" + resolved "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz" integrity sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ== postgres-array@~2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + resolved "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz" integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== postgres-bytea@~1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.1.tgz#c40b3da0222c500ff1e51c5d7014b60b79697c7a" + resolved "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz" integrity sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ== postgres-date@~1.0.4: version "1.0.7" - resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + resolved "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz" integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== postgres-interval@^1.1.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + resolved "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz" integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== dependencies: xtend "^4.0.0" postgres@3.4.7: version "3.4.7" - resolved "https://registry.yarnpkg.com/postgres/-/postgres-3.4.7.tgz#122f460a808fe300cae53f592108b9906e625345" + resolved "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz" integrity sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw== prisma@^7.2.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-7.2.0.tgz#ae7a4da8de0cbd078d9400888cd0533abfdca648" + resolved "https://registry.npmjs.org/prisma/-/prisma-7.2.0.tgz" integrity sha512-jSdHWgWOgFF24+nRyyNRVBIgGDQEsMEF8KPHvhBBg3jWyR9fUAK0Nq9ThUmiGlNgq2FA7vSk/ZoCvefod+a8qg== dependencies: "@prisma/config" "7.2.0" @@ -1485,7 +1671,7 @@ prisma@^7.2.0: proper-lockfile@4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" + resolved "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz" integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== dependencies: graceful-fs "^4.2.4" @@ -1494,7 +1680,7 @@ proper-lockfile@4.1.2: proxy-addr@^2.0.7: version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: forwarded "0.2.0" @@ -1502,31 +1688,31 @@ proxy-addr@^2.0.7: ps-tree@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.2.0.tgz#5e7425b89508736cdd4f2224d028f7bb3f722ebd" + resolved "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz" integrity sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA== dependencies: event-stream "=3.3.4" pure-rand@^6.1.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" + resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== qs@^6.14.0: version "6.14.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159" + resolved "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz" integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ== dependencies: side-channel "^1.1.0" range-parser@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== raw-body@^3.0.1: version "3.0.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.2.tgz#3e3ada5ae5568f9095d84376fd3a49b8fb000a51" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz" integrity sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA== dependencies: bytes "~3.1.2" @@ -1536,42 +1722,51 @@ raw-body@^3.0.1: rc9@^2.1.2: version "2.1.2" - resolved "https://registry.yarnpkg.com/rc9/-/rc9-2.1.2.tgz#6282ff638a50caa0a91a31d76af4a0b9cbd1080d" + resolved "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz" integrity sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg== dependencies: defu "^6.1.4" destr "^2.0.3" +readable-stream@^3.0.2: + version "3.6.2" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readdirp@^4.0.1: version "4.1.2" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz" integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== regexp-to-ast@0.5.0: version "0.5.0" - resolved "https://registry.yarnpkg.com/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz#56c73856bee5e1fef7f73a00f1473452ab712a24" + resolved "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz" integrity sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw== remeda@2.21.3: version "2.21.3" - resolved "https://registry.yarnpkg.com/remeda/-/remeda-2.21.3.tgz#16551b02f9c7e184b7be226f576d0c6335612adf" + resolved "https://registry.npmjs.org/remeda/-/remeda-2.21.3.tgz" integrity sha512-XXrZdLA10oEOQhLLzEJEiFFSKi21REGAkHdImIb4rt/XXy8ORGXh5HCcpUOsElfPNDb+X6TA/+wkh+p2KffYmg== dependencies: type-fest "^4.39.1" retry@0.13.1: version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== retry@^0.12.0: version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== router@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + resolved "https://registry.npmjs.org/router/-/router-2.2.0.tgz" integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== dependencies: debug "^4.4.0" @@ -1580,24 +1775,24 @@ router@^2.2.0: parseurl "^1.3.3" path-to-regexp "^8.0.0" -safe-buffer@^5.0.1, safe-buffer@^5.2.1: +safe-buffer@^5.0.1, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== semver@^7.5.4: version "7.7.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== send@^1.1.0, send@^1.2.0: version "1.2.1" - resolved "https://registry.yarnpkg.com/send/-/send-1.2.1.tgz#9eab743b874f3550f40a26867bf286ad60d3f3ed" + resolved "https://registry.npmjs.org/send/-/send-1.2.1.tgz" integrity sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ== dependencies: debug "^4.4.3" @@ -1614,12 +1809,12 @@ send@^1.1.0, send@^1.2.0: seq-queue@^0.0.5: version "0.0.5" - resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" + resolved "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz" integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q== serve-static@^2.2.0: version "2.2.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.1.tgz#7f186a4a4e5f5b663ad7a4294ff1bf37cf0e98a9" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz" integrity sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw== dependencies: encodeurl "^2.0.0" @@ -1629,7 +1824,7 @@ serve-static@^2.2.0: set-function-length@^1.2.2: version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: define-data-property "^1.1.4" @@ -1641,12 +1836,12 @@ set-function-length@^1.2.2: setprototypeof@~1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== sha.js@^2.4.11: version "2.4.12" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.12.tgz#eb8b568bf383dfd1867a32c3f2b74eb52bdbf23f" + resolved "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz" integrity sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w== dependencies: inherits "^2.0.4" @@ -1655,19 +1850,19 @@ sha.js@^2.4.11: shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: shebang-regex "^3.0.0" shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== side-channel-list@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + resolved "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz" integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== dependencies: es-errors "^1.3.0" @@ -1675,7 +1870,7 @@ side-channel-list@^1.0.0: side-channel-map@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + resolved "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz" integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== dependencies: call-bound "^1.0.2" @@ -1685,7 +1880,7 @@ side-channel-map@^1.0.1: side-channel-weakmap@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + resolved "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz" integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== dependencies: call-bound "^1.0.2" @@ -1696,7 +1891,7 @@ side-channel-weakmap@^1.0.2: side-channel@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz" integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== dependencies: es-errors "^1.3.0" @@ -1707,66 +1902,107 @@ side-channel@^1.1.0: signal-exit@^3.0.2: version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== signal-exit@^4.0.1: version "4.1.0" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +socket.io-adapter@~2.5.2: + version "2.5.6" + resolved "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz" + integrity sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ== + dependencies: + debug "~4.4.1" + ws "~8.18.3" + +socket.io-parser@~4.2.4: + version "4.2.5" + resolved "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz" + integrity sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.4.1" + +socket.io@^4.8.3: + version "4.8.3" + resolved "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz" + integrity sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.4.1" + engine.io "~6.6.0" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + split2@^4.1.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + resolved "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz" integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== split@0.3: version "0.3.3" - resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" + resolved "https://registry.npmjs.org/split/-/split-0.3.3.tgz" integrity sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA== dependencies: through "2" sqlstring@^2.3.2: version "2.3.3" - resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" + resolved "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz" integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz" integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== std-env@3.9.0: version "3.9.0" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1" + resolved "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz" integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw== stream-combiner@~0.0.4: version "0.0.4" - resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" + resolved "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz" integrity sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw== dependencies: duplexer "~0.1.1" +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + string-argv@^0.3.2: version "0.3.2" - resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" + resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + through@2, through@~2.3, through@~2.3.1: version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== tinyexec@^1.0.1: version "1.0.2" - resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.0.2.tgz#bdd2737fe2ba40bd6f918ae26642f264b99ca251" + resolved "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz" integrity sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg== to-buffer@^1.2.0: version "1.2.2" - resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.2.2.tgz#ffe59ef7522ada0a2d1cb5dfe03bb8abc3cdc133" + resolved "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz" integrity sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw== dependencies: isarray "^2.0.5" @@ -1775,12 +2011,12 @@ to-buffer@^1.2.0: toidentifier@~1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== tsc-watch@^7.2.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/tsc-watch/-/tsc-watch-7.2.0.tgz#372eab1d46d107eedb2639ee53f504c51e066860" + resolved "https://registry.npmjs.org/tsc-watch/-/tsc-watch-7.2.0.tgz" integrity sha512-4gRFawQD1cVSaILvG7wl2x6NtteKbS2dGBMbL7Q6n1ldLIOKXCJUoEwUXdGuee4dp+zcnA6tukBBLz1lZrNI9w== dependencies: cross-spawn "^7.0.6" @@ -1788,19 +2024,27 @@ tsc-watch@^7.2.0: ps-tree "^1.2.0" string-argv "^0.3.2" -tslib@^2.4.0, tslib@^2.6.3: +tslib@2.8.1, tslib@^2.4.0, tslib@^2.6.3: version "2.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== type-fest@^4.39.1: version "4.41.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz" integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== +type-is@^1.6.18: + version "1.6.18" + resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + type-is@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97" + resolved "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz" integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== dependencies: content-type "^1.0.5" @@ -1809,51 +2053,61 @@ type-is@^2.0.1: typed-array-buffer@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz" integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== dependencies: call-bound "^1.0.3" es-errors "^1.3.0" is-typed-array "^1.1.14" +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + typescript@^5.9.3: version "5.9.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== undici-types@~7.16.0: version "7.16.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz" integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== unpipe@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + uuid@^11.1.0: version "11.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + resolved "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz" integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== valibot@1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/valibot/-/valibot-1.2.0.tgz#8fc720d9e4082ba16e30a914064a39619b2f1d6f" + resolved "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz" integrity sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg== vary@^1, vary@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== whatwg-mimetype@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" + resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz" integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== which-typed-array@^1.1.16: version "1.1.19" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" + resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz" integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== dependencies: available-typed-arrays "^1.0.7" @@ -1866,24 +2120,29 @@ which-typed-array@^1.1.16: which@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" wrappy@1: version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -xtend@^4.0.0: +ws@^8.18.2, ws@~8.18.3: + version "8.18.3" + resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== + +xtend@^4.0.0, xtend@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== zeptomatch@2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/zeptomatch/-/zeptomatch-2.0.2.tgz#8ef6a2288a8cde753a02327305ac912914416ab4" + resolved "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.0.2.tgz" integrity sha512-H33jtSKf8Ijtb5BW6wua3G5DhnFjbFML36eFu+VdOoVY4HD9e7ggjqdM6639B+L87rjnR6Y+XeRzBXZdy52B/g== dependencies: grammex "^3.1.10" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 49f15a6..f31c2a2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -38,72 +38,168 @@ Threads App follows a modern full-stack architecture with a clear separation bet โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ PostgreSQL โ”‚ โ”‚ -โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ users โ”‚ โ”‚ refresh_tokens โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ Users, Posts, Likes, Follows, Bookmarks, Notifications โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` -## Authentication Flow - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Frontend โ”‚ โ”‚ Backend โ”‚ โ”‚ Database โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ โ”‚ โ”‚ - โ”‚ 1. Login Request โ”‚ โ”‚ - โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ โ”‚ - โ”‚ โ”‚ 2. Validate User โ”‚ - โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ - โ”‚ โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ - โ”‚ โ”‚ โ”‚ - โ”‚ โ”‚ 3. Create Tokens โ”‚ - โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ - โ”‚ โ”‚ โ”‚ - โ”‚ 4. Set Cookies โ”‚ โ”‚ - โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”‚ - โ”‚ (httpOnly) โ”‚ โ”‚ - โ”‚ โ”‚ โ”‚ - โ”‚ 5. API Request โ”‚ โ”‚ - โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ โ”‚ - โ”‚ (with cookies) โ”‚ โ”‚ - โ”‚ โ”‚ 6. Verify Token โ”‚ - โ”‚ โ”‚ 7. Get User โ”‚ - โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ - โ”‚ โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ - โ”‚ 8. Response โ”‚ โ”‚ - โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”‚ -``` - ## Database Schema +### Core Tables + ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ users โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ id UUID (PK) โ”‚ +โ”‚ username VARCHAR (unique) โ”‚ โ”‚ first_name VARCHAR โ”‚ โ”‚ last_name VARCHAR (nullable) โ”‚ -โ”‚ profile_image_url VARCHAR (nullable) โ”‚ โ”‚ email VARCHAR (unique) โ”‚ โ”‚ password VARCHAR (hashed) โ”‚ โ”‚ salt VARCHAR โ”‚ +โ”‚ bio TEXT (nullable) โ”‚ +โ”‚ website VARCHAR (nullable) โ”‚ +โ”‚ profile_image VARCHAR (nullable) โ”‚ +โ”‚ is_private BOOLEAN โ”‚ +โ”‚ is_verified BOOLEAN โ”‚ +โ”‚ created_at TIMESTAMP โ”‚ +โ”‚ updated_at TIMESTAMP โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”‚ 1:N + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ posts โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id UUID (PK) โ”‚ +โ”‚ content TEXT โ”‚ +โ”‚ author_id UUID (FK โ†’ users) โ”‚ +โ”‚ parent_id UUID (FK โ†’ posts) โ”‚ +โ”‚ visibility ENUM โ”‚ +โ”‚ likes_count INT โ”‚ +โ”‚ replies_count INT โ”‚ +โ”‚ reposts_count INT โ”‚ โ”‚ created_at TIMESTAMP โ”‚ โ”‚ updated_at TIMESTAMP โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ”‚ 1:N - โ–ผ + โ”‚ + โ”‚ 1:N + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ post_media โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id UUID (PK) โ”‚ +โ”‚ post_id UUID (FK โ†’ posts) โ”‚ +โ”‚ media_url VARCHAR โ”‚ +โ”‚ media_type ENUM (IMAGE/VIDEO/GIF) โ”‚ +โ”‚ position INT โ”‚ +โ”‚ created_at TIMESTAMP โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Social Tables + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ follows โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id UUID (PK) โ”‚ +โ”‚ follower_id UUID (FK โ†’ users) โ”‚ +โ”‚ following_id UUID (FK โ†’ users) โ”‚ +โ”‚ created_at TIMESTAMP โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ post_likes โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id UUID (PK) โ”‚ +โ”‚ user_id UUID (FK โ†’ users) โ”‚ +โ”‚ post_id UUID (FK โ†’ posts) โ”‚ +โ”‚ created_at TIMESTAMP โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ bookmarks โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id UUID (PK) โ”‚ +โ”‚ user_id UUID (FK โ†’ users) โ”‚ +โ”‚ post_id UUID (FK โ†’ posts) โ”‚ +โ”‚ created_at TIMESTAMP โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ reposts โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id UUID (PK) โ”‚ +โ”‚ user_id UUID (FK โ†’ users) โ”‚ +โ”‚ post_id UUID (FK โ†’ posts) โ”‚ +โ”‚ created_at TIMESTAMP โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Content Discovery Tables + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ hashtags โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id UUID (PK) โ”‚ +โ”‚ tag VARCHAR (unique) โ”‚ +โ”‚ usage_count INT โ”‚ +โ”‚ created_at TIMESTAMP โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ post_hashtags โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ post_id UUID (FK โ†’ posts) โ”‚ +โ”‚ hashtag_id UUID (FK โ†’ hashtags) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ post_mentions โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id UUID (PK) โ”‚ +โ”‚ post_id UUID (FK โ†’ posts) โ”‚ +โ”‚ user_id UUID (FK โ†’ users) โ”‚ +โ”‚ created_at TIMESTAMP โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Notification & Auth Tables + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ notifications โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id UUID (PK) โ”‚ +โ”‚ user_id UUID (FK โ†’ users) โ”‚ +โ”‚ actor_id UUID (FK โ†’ users) โ”‚ +โ”‚ type ENUM โ”‚ +โ”‚ post_id UUID (FK โ†’ posts) โ”‚ +โ”‚ is_read BOOLEAN โ”‚ +โ”‚ created_at TIMESTAMP โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ refresh_tokens โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ id UUID (PK) โ”‚ โ”‚ token VARCHAR (unique) โ”‚ -โ”‚ user_id UUID (FK โ†’ users.id) โ”‚ +โ”‚ user_id UUID (FK โ†’ users) โ”‚ โ”‚ expires_at TIMESTAMP โ”‚ โ”‚ created_at TIMESTAMP โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ blocks โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id UUID (PK) โ”‚ +โ”‚ blocker_id UUID (FK โ†’ users) โ”‚ +โ”‚ blocked_id UUID (FK โ†’ users) โ”‚ +โ”‚ created_at TIMESTAMP โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` ## Technology Choices @@ -113,6 +209,7 @@ Threads App follows a modern full-stack architecture with a clear separation bet | **Frontend Framework** | Next.js 16 | App Router, SSR, modern React | | **CSS** | Tailwind CSS 4 | Utility-first, fast development | | **GraphQL Client** | Apollo Client 4 | Caching, hooks, TypeScript support | +| **State Management** | Zustand | Lightweight, intuitive API | | **Backend Framework** | Express 5 | Mature, flexible, middleware ecosystem | | **GraphQL Server** | Apollo Server 5 | Industry standard, great DX | | **ORM** | Prisma 7 | Type-safe, migrations, easy schema | @@ -126,3 +223,5 @@ Threads App follows a modern full-stack architecture with a clear separation bet 3. **Refresh Token Rotation** - Old token invalidated on refresh 4. **Short-lived Access Tokens** - 15 minute expiry 5. **CORS Configuration** - Restricted to frontend origin +6. **Input Validation** - GraphQL schema type validation +7. **Rate Limiting** - Protection against brute force attacks diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index d87d8ac..8e9d885 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -7,8 +7,8 @@ Thank you for your interest in contributing! ๐ŸŽ‰ ### 1. Fork & Clone ```bash -git clone https://github.com/shahadot786/threads-backend-graphql.git -cd threads-backend-graphql +git clone https://github.com/shahadot786/threads-clone.git +cd threads-clone ``` ### 2. Set Up Development Environment @@ -26,6 +26,7 @@ yarn dev # Frontend (new terminal) cd frontend +cp .env.example .env.local yarn install yarn dev ``` @@ -55,34 +56,57 @@ feat: add thread creation fix: resolve login redirect issue docs: update API documentation refactor: improve auth service +chore: update dependencies ``` ### Branch Naming -- `feature/` - New features -- `fix/` - Bug fixes -- `docs/` - Documentation -- `refactor/` - Code improvements +| Prefix | Purpose | +|--------|---------| +| `feature/` | New features | +| `fix/` | Bug fixes | +| `docs/` | Documentation | +| `refactor/` | Code improvements | +| `chore/` | Maintenance tasks | ## Pull Request Process -1. **Update your branch** with main +### Before Submitting + +- [ ] Branch is up to date with `master` +- [ ] Code compiles without errors (`npx tsc --noEmit`) +- [ ] No console errors in browser +- [ ] Existing features still work +- [ ] New code follows project patterns + +### Creating a PR + +1. **Push your branch** ```bash - git fetch origin - git rebase origin/main + git push origin feature/your-feature ``` -2. **Test your changes** - - Backend compiles without errors - - Frontend builds successfully - - All existing functionality works +2. **Open a Pull Request** on GitHub -3. **Create PR** with clear description +3. **Fill out the PR template** with: - What does this PR do? - How to test it? - Screenshots (if UI changes) -4. **Address review feedback** +4. **Wait for CI** - Ensure checks pass + +5. **Address review feedback** promptly + +## Code Review Checklist + +Reviewers will check: + +- [ ] Code is readable and well-structured +- [ ] TypeScript types are correct +- [ ] No security vulnerabilities +- [ ] Error handling is appropriate +- [ ] Performance considerations addressed +- [ ] Tests added (if applicable) ## Project Structure @@ -100,6 +124,29 @@ refactor: improve auth service 1. Create: `frontend/src/app/[route]/page.tsx` 2. Add auth check if needed (protected/guest-only) 3. Use Apollo hooks for data fetching +4. Add to navigation if needed + +## Issue Guidelines + +### Before Creating an Issue + +1. Search existing issues to avoid duplicates +2. Check if it's already fixed in `master` + +### Bug Reports + +Use the [bug report template](.github/ISSUE_TEMPLATE/bug_report.md) and include: +- Steps to reproduce +- Expected vs actual behavior +- Browser/OS information +- Screenshots or error logs + +### Feature Requests + +Use the [feature request template](.github/ISSUE_TEMPLATE/feature_request.md) and include: +- Problem description +- Proposed solution +- Alternative approaches considered ## Need Help? diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 7a45d57..d92c957 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,148 +1,224 @@ # Deployment Guide -## Deployment Options - -### Option 1: Docker Compose (Recommended for Small Scale) - -#### Production docker-compose.yml - -```yaml -services: - postgres: - image: postgres:16-alpine - restart: unless-stopped - environment: - POSTGRES_USER: ${DB_USER} - POSTGRES_PASSWORD: ${DB_PASSWORD} - POSTGRES_DB: ${DB_NAME} - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - backend: - build: ./backend - restart: unless-stopped - ports: - - "8000:8000" - environment: - NODE_ENV: production - DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME} - JWT_SECRET: ${JWT_SECRET} - FRONTEND_URL: ${FRONTEND_URL} - depends_on: - postgres: - condition: service_healthy - - frontend: - build: ./frontend - restart: unless-stopped - ports: - - "3000:3000" - environment: - NEXT_PUBLIC_GRAPHQL_URL: ${BACKEND_URL}/graphql - -volumes: - postgres_data: -``` +Deploy the Threads App to production using Supabase (database + auth), Railway (backend), and Vercel (frontend). + +## Prerequisites + +- [Supabase](https://supabase.com) account (free tier works) +- [Railway](https://railway.app) account +- [Vercel](https://vercel.com) account +- GitHub repository with your code + +--- + +## Step 1: Supabase Setup + +Supabase provides both PostgreSQL database and authentication. + +### 1.1 Create Project + +1. Go to [supabase.com](https://supabase.com) and create a new project +2. Choose a strong database password (save it!) +3. Select a region close to your users + +### 1.2 Get API Keys + +Navigate to **Settings โ†’ API** and copy: + +| Key | Use In | Variable Name | +|-----|--------|---------------| +| Project URL | Backend + Frontend | `SUPABASE_URL` / `NEXT_PUBLIC_SUPABASE_URL` | +| `anon` public | Frontend only | `NEXT_PUBLIC_SUPABASE_ANON_KEY` | +| `service_role` secret | Backend only | `SUPABASE_SERVICE_ROLE_KEY` | + +### 1.3 Get Database URL + +Navigate to **Settings โ†’ Database** and copy: -### Option 2: Cloud Deployment +- **Connection string** (URI format) โ†’ `DATABASE_URL` +- Make sure to replace `[YOUR-PASSWORD]` with your database password -#### Backend (Render/Railway/Fly.io) +### 1.4 Configure Auth Settings -1. Connect your GitHub repository -2. Set build command: `yarn install && npx prisma generate && yarn build` -3. Set start command: `yarn start` -4. Add environment variables: - - `DATABASE_URL` - - `JWT_SECRET` - - `FRONTEND_URL` +Navigate to **Authentication โ†’ URL Configuration**: -#### Frontend (Vercel) +1. Set **Site URL**: `https://your-frontend-domain.vercel.app` +2. Add **Redirect URLs**: + - `https://your-frontend-domain.vercel.app/auth/callback` + - `http://localhost:3000/auth/callback` (for development) -1. Import from GitHub -2. Framework preset: Next.js -3. Add environment variable: - - `NEXT_PUBLIC_GRAPHQL_URL` +### 1.5 Configure Email Templates (Optional) -#### Database (Neon/Supabase/Railway) +Navigate to **Authentication โ†’ Email Templates** to customize: +- Confirm signup email +- Reset password email -1. Create PostgreSQL database -2. Copy connection string -3. Run migrations: `npx prisma migrate deploy` +--- -## Environment Variables +## Step 2: Railway Backend Deployment -### Backend (.env) +### 2.1 Create Railway Project + +1. Go to [railway.app](https://railway.app) and create a new project +2. Select **Deploy from GitHub repo** +3. Choose your repository +4. Set **Root Directory**: `backend` + +### 2.2 Configure Build Settings + +In your Railway project settings: + +| Setting | Value | +|---------|-------| +| Build Command | `yarn install && npx prisma generate` | +| Start Command | `yarn start` | +| Watch Paths | `/backend/**` | + +### 2.3 Add Environment Variables + +Add these variables in Railway's **Variables** tab: ```env -# Database -DATABASE_URL=postgresql://user:password@host:5432/dbname +# Database (from Supabase) +DATABASE_URL=postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres -# Authentication -JWT_SECRET=your-super-secret-key-at-least-32-characters-long +# Supabase Auth +SUPABASE_URL=https://[PROJECT-REF].supabase.co +SUPABASE_SERVICE_ROLE_KEY=eyJ... # Server PORT=8000 NODE_ENV=production -FRONTEND_URL=https://your-frontend-domain.com +FRONTEND_URL=https://your-frontend.vercel.app ``` -### Frontend (.env.local) +### 2.4 Run Database Migrations -```env -NEXT_PUBLIC_GRAPHQL_URL=https://your-backend-domain.com/graphql +After deployment, run migrations via Railway CLI or shell: + +```bash +npx prisma migrate deploy ``` -## SSL/HTTPS +Or add to your start script: `"start": "npx prisma migrate deploy && node dist/src/index.js"` -For production, use HTTPS: -- **Render/Railway/Vercel**: SSL included automatically -- **Self-hosted**: Use nginx with Let's Encrypt +### 2.5 Get Backend URL -### Example nginx configuration +After deployment, copy your Railway URL (e.g., `https://your-app.up.railway.app`) -```nginx -server { - listen 443 ssl; - server_name api.threads-app.com; +--- - ssl_certificate /etc/letsencrypt/live/api.threads-app.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/api.threads-app.com/privkey.pem; +## Step 3: Vercel Frontend Deployment - location / { - proxy_pass http://localhost:8000; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } -} -``` +### 3.1 Import Project -## Database Migrations +1. Go to [vercel.com](https://vercel.com) and click **Add New โ†’ Project** +2. Import your GitHub repository +3. Set **Root Directory**: `frontend` +4. Framework Preset: **Next.js** (auto-detected) -```bash -# Development -npx prisma migrate dev +### 3.2 Add Environment Variables -# Production (apply pending migrations) -npx prisma migrate deploy +Add these in Vercel's **Environment Variables** section: -# Reset database (DANGER: deletes all data) -npx prisma migrate reset +```env +# Backend API +NEXT_PUBLIC_GRAPHQL_URL=https://your-app.up.railway.app/graphql +NEXT_PUBLIC_API_URL=https://your-app.up.railway.app + +# Supabase +NEXT_PUBLIC_SUPABASE_URL=https://[PROJECT-REF].supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... ``` -## Health Checks +### 3.3 Deploy + +Click **Deploy** and wait for the build to complete. + +### 3.4 Update Supabase Redirect URLs + +After Vercel assigns your domain, go back to **Supabase โ†’ Authentication โ†’ URL Configuration** and update: + +- Site URL: `https://your-project.vercel.app` +- Redirect URLs: Add `https://your-project.vercel.app/auth/callback` + +--- + +## Environment Variables Reference + +### Backend Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `DATABASE_URL` | โœ… | PostgreSQL connection string | +| `SUPABASE_URL` | โœ… | Supabase project URL | +| `SUPABASE_SERVICE_ROLE_KEY` | โœ… | Supabase service role secret key | +| `PORT` | โŒ | Server port (default: 8000) | +| `NODE_ENV` | โœ… | Set to `production` | +| `FRONTEND_URL` | โœ… | Frontend URL for CORS | + +### Frontend Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `NEXT_PUBLIC_GRAPHQL_URL` | โœ… | Backend GraphQL endpoint | +| `NEXT_PUBLIC_API_URL` | โœ… | Backend API for file uploads | +| `NEXT_PUBLIC_SUPABASE_URL` | โœ… | Supabase project URL | +| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | โœ… | Supabase public anon key | + +--- + +## Post-Deployment Checklist + +- [ ] Backend health check: `GET /` returns `{"message": "Hello, Threads App Backend!"}` +- [ ] GraphQL endpoint accessible: `POST /graphql` +- [ ] Frontend loads without errors +- [ ] User registration works (check email) +- [ ] Login/logout works +- [ ] Post creation works +- [ ] Image uploads work + +--- + +## Troubleshooting + +### "CORS Error" +- Ensure `FRONTEND_URL` in backend matches your Vercel domain exactly +- Check if both use HTTPS + +### "Database connection failed" +- Verify `DATABASE_URL` is correct +- Check Supabase is not paused (free tier pauses after inactivity) + +### "Auth not working" +- Verify Supabase redirect URLs match your deployed frontend +- Check `NEXT_PUBLIC_SUPABASE_*` variables are set correctly + +### "Prisma client not generated" +- Ensure build command includes `npx prisma generate` +- Check if `node_modules/.prisma` exists + +--- + +## Custom Domain (Optional) + +### Railway +1. Go to **Settings โ†’ Domains** +2. Add your custom domain +3. Configure DNS (CNAME to Railway) + +### Vercel +1. Go to **Settings โ†’ Domains** +2. Add your custom domain +3. Configure DNS (follow Vercel instructions) -- Backend: `GET /` returns `{"message": "Hello, Threads App Backend!"}` -- GraphQL: `POST /graphql` with introspection query +--- ## Monitoring Recommendations -- **Logging**: Pino or Winston -- **Metrics**: Prometheus + Grafana -- **Error Tracking**: Sentry -- **Uptime**: UptimeRobot or Pingdom +| Service | Purpose | +|---------|---------| +| [Sentry](https://sentry.io) | Error tracking | +| [Axiom](https://axiom.co) | Logging | +| [UptimeRobot](https://uptimerobot.com) | Uptime monitoring | +| Railway Metrics | Built-in CPU/Memory monitoring | diff --git a/frontend/README.md b/frontend/README.md index f626ad7..914279e 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -9,74 +9,111 @@ frontend/ โ”œโ”€โ”€ src/ โ”‚ โ”œโ”€โ”€ app/ # Next.js App Router โ”‚ โ”‚ โ”œโ”€โ”€ layout.tsx # Root layout with Apollo -โ”‚ โ”‚ โ”œโ”€โ”€ page.tsx # Home page -โ”‚ โ”‚ โ”œโ”€โ”€ login/page.tsx # Login page -โ”‚ โ”‚ โ”œโ”€โ”€ register/page.tsx # Registration page -โ”‚ โ”‚ โ””โ”€โ”€ profile/page.tsx # User profile (protected) +โ”‚ โ”‚ โ”œโ”€โ”€ page.tsx # Home feed +โ”‚ โ”‚ โ”œโ”€โ”€ [username]/ # User profile pages +โ”‚ โ”‚ โ”œโ”€โ”€ activity/ # Notifications/activity +โ”‚ โ”‚ โ”œโ”€โ”€ create/ # Create post page +โ”‚ โ”‚ โ”œโ”€โ”€ post/[id]/ # Single post view +โ”‚ โ”‚ โ”œโ”€โ”€ search/ # Search page +โ”‚ โ”‚ โ”œโ”€โ”€ tags/[tag]/ # Hashtag posts +โ”‚ โ”‚ โ”œโ”€โ”€ login/ # Login page +โ”‚ โ”‚ โ””โ”€โ”€ register/ # Registration page โ”‚ โ”œโ”€โ”€ components/ -โ”‚ โ”‚ โ””โ”€โ”€ apollo-wrapper.tsx # Apollo Provider wrapper -โ”‚ โ”œโ”€โ”€ graphql/ -โ”‚ โ”‚ โ””โ”€โ”€ user.ts # GraphQL queries & mutations -โ”‚ โ””โ”€โ”€ lib/ -โ”‚ โ””โ”€โ”€ apollo-client.tsx # Apollo Client setup +โ”‚ โ”‚ โ”œโ”€โ”€ layout/ # Sidebar, MainLayout, SettingsMenu +โ”‚ โ”‚ โ”œโ”€โ”€ post/ # PostCard, CreatePost, PostActions +โ”‚ โ”‚ โ”œโ”€โ”€ user/ # UserTooltip, BlockedUsersList +โ”‚ โ”‚ โ”œโ”€โ”€ auth/ # LoginModal, AuthCard +โ”‚ โ”‚ โ”œโ”€โ”€ profile/ # EditProfileModal +โ”‚ โ”‚ โ””โ”€โ”€ ui/ # Button, Avatar, Dialog, HoverCard +โ”‚ โ”œโ”€โ”€ graphql/ # Queries, mutations, fragments +โ”‚ โ”œโ”€โ”€ stores/ # Zustand state (auth, ui) +โ”‚ โ”œโ”€โ”€ hooks/ # Custom React hooks +โ”‚ โ””โ”€โ”€ lib/ # Apollo client, utilities โ””โ”€โ”€ public/ # Static assets ``` -## ๐Ÿ” Authentication - -- **Cookie-based auth** - httpOnly cookies set by backend -- **Automatic credential inclusion** - Apollo Client configured with `credentials: "include"` -- **Route protection**: - - `/login` & `/register` โ†’ Redirect to `/profile` if authenticated - - `/profile` โ†’ Redirect to `/login` if not authenticated - ## ๐Ÿ“„ Pages | Route | Auth | Description | |-------|------|-------------| -| `/` | Public | Home page with welcome message | +| `/` | Public | Home feed with trending posts | | `/login` | Guest only | Login form | | `/register` | Guest only | Registration form | -| `/profile` | Protected | User profile details | +| `/@username` | Public | User profile with tabs | +| `/activity` | Protected | Notifications and activity | +| `/create` | Protected | Create/edit post | +| `/post/:id` | Public | Single post with replies | +| `/search` | Public | Search users, posts, hashtags | +| `/tags/:tag` | Public | Posts with hashtag | + +## โœจ Key Features + +### Post Creation +- Rich text input with auto-growing textarea +- Image, video, and GIF upload +- @mention autocomplete with user suggestions +- Quote generator for inspiration +- Emoji picker +- Edit existing posts + +### Feed & Discovery +- Infinite scroll with pagination +- Trending posts section +- Hashtag support with clickable tags +- User search with follow suggestions + +### Social Interactions +- Like/unlike with optimistic updates +- Reply threads with nesting +- Repost to profile +- Bookmark posts (accessible via sidebar Pin) +- Follow/unfollow users + +### User Profiles +- Profile tabs: Threads, Replies, Reposts, Media, Saved (mobile), Blocked +- Edit profile modal +- Follow/follower counts +- User tooltips on hover + +### Activity/Notifications +- Real-time activity feed +- Like, follow, mention, reply notifications +- Unread indicator ## ๐Ÿ”ง Environment Variables Create `.env.local`: ```env -NEXT_PUBLIC_GRAPHQL_URL=http://localhost:8000/graphql +NEXT_PUBLIC_API_URL=http://localhost:8000 ``` ## ๐Ÿ›  Development ```bash -# Install dependencies -yarn install - -# Start dev server -yarn dev - -# Build for production -yarn build - -# Start production server -yarn start - -# Run linting -yarn lint +yarn install # Install dependencies +yarn dev # Start dev server (port 3000) +yarn build # Build for production +yarn start # Start production server +yarn lint # Run linting ``` ## ๐ŸŽจ Styling -- **Tailwind CSS 4** for utility-first styling -- **Dark theme** with gradient backgrounds -- **Glassmorphism** effects (backdrop-blur) -- **Responsive** design +- **Tailwind CSS 4** - Utility-first CSS +- **Dark theme** - Default dark mode with CSS variables +- **Glassmorphism** - Backdrop blur effects +- **Animations** - Smooth transitions and micro-interactions +- **Responsive** - Mobile-first design ## ๐Ÿ“ฆ Key Dependencies -| Package | Version | Purpose | -|---------|---------|---------| -| `next` | 16.1.1 | React framework | -| `react` | 19.2.3 | UI library | -| `@apollo/client` | 4.0.11 | GraphQL client | -| `tailwindcss` | 4.x | CSS framework | +| Package | Purpose | +|---------|---------| +| `next` | React framework | +| `react` | UI library | +| `@apollo/client` | GraphQL client | +| `tailwindcss` | CSS framework | +| `zustand` | State management | +| `lucide-react` | Icon library | +| `@radix-ui/*` | Headless UI components | +| `emoji-picker-react` | Emoji picker | diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 66e1566..a1da2be 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -3,6 +3,36 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ reactCompiler: true, + + images: { + unoptimized: true, + remotePatterns: [ + { + protocol: "http", + hostname: "localhost", + }, + { + protocol: "https", + hostname: "**.cloudinary.com", + }, + { + protocol: "https", + hostname: "**.unsplash.com", + }, + { + protocol: "https", + hostname: "**.googleusercontent.com", + }, + { + protocol: "https", + hostname: "**.githubusercontent.com", + }, + { + protocol: "https", + hostname: "**.pexels.com", + }, + ], + }, }; export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d446872..03f6eef 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,12 +11,18 @@ "@apollo/client": "^4.0.11", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", + "@supabase/auth-helpers-nextjs": "^0.15.0", + "@supabase/ssr": "^0.8.0", + "@supabase/supabase-js": "^2.90.1", + "@types/react-window": "^1.8.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "emoji-picker-react": "^4.16.1", "graphql": "^16.12.0", "lucide-react": "^0.562.0", "next": "16.1.1", @@ -24,7 +30,10 @@ "react": "19.2.3", "react-dom": "19.2.3", "react-icons": "^5.5.0", + "react-window": "^2.2.4", + "react-window-infinite-loader": "^2.0.0", "rxjs": "^7.8.2", + "socket.io-client": "^4.8.3", "tailwind-merge": "^3.4.0", "zustand": "^5.0.9" }, @@ -506,6 +515,44 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@graphql-typed-document-node/core": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", @@ -862,6 +909,29 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-avatar": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", @@ -889,6 +959,44 @@ } } }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -987,9 +1095,9 @@ } }, "node_modules/@radix-ui/react-context": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", - "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1135,47 +1243,6 @@ } } }, - "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", @@ -1216,13 +1283,21 @@ } } }, - "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -1239,24 +1314,6 @@ } } }, - "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -1275,14 +1332,22 @@ } } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", "license": "MIT", "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1299,13 +1364,14 @@ } } }, - "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1322,24 +1388,6 @@ } } }, - "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-presence": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", @@ -1365,75 +1413,6 @@ } }, "node_modules/@radix-ui/react-primitive": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", - "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", @@ -1456,7 +1435,7 @@ } } }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", @@ -1474,21 +1453,21 @@ } } }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", - "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -1505,28 +1484,21 @@ } } }, - "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1543,31 +1515,36 @@ } } }, - "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", - "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.4" + "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", @@ -1632,62 +1609,6 @@ } } }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -1791,6 +1712,48 @@ } } }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1798,6 +1761,116 @@ "dev": true, "license": "MIT" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@supabase/auth-helpers-nextjs": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-helpers-nextjs/-/auth-helpers-nextjs-0.15.0.tgz", + "integrity": "sha512-VtXz3GGnxluoxks1g3SaCoYr2OZ7PgRukDl+pLWrDfD2dPDaG8hmkp5iBZsU+lmsDYALGNO2dgbymgpAfD8eCQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.76.1" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.90.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.90.1.tgz", + "integrity": "sha512-vxb66dgo6h3yyPbR06735Ps+dK3hj0JwS8w9fdQPVZQmocSTlKUW5MfxSy99mN0XqCCuLMQ3jCEiIIUU23e9ng==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.90.1", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.90.1.tgz", + "integrity": "sha512-x9mV9dF1Lam9qL3zlpP6mSM5C9iqMPtF5B/tU1Jj/F0ufX5mjDf9ghVBaErVxmrQJRL4+iMKWKY2GnODkpS8tw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.90.1", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.90.1.tgz", + "integrity": "sha512-jh6vqzaYzoFn3raaC0hcFt9h+Bt+uxNRBSdc7PfToQeRGk7PDPoweHsbdiPWREtDVTGKfu+PyPW9e2jbK+BCgQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.90.1", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.90.1.tgz", + "integrity": "sha512-PWbnEMkcQRuor8jhObp4+Snufkq8C6fBp+MchVp2qBPY1NXk/c3Iv3YyiFYVzo0Dzuw4nAlT4+ahuPggy4r32w==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.8.0.tgz", + "integrity": "sha512-/PKk8kNFSs8QvvJ2vOww1mF5/c5W8y42duYtXvkOSe+yZKRgTTZywYG2l41pjhNomqESZCpZtXuWmYjFRMV+dw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.76.1" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.90.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.90.1.tgz", + "integrity": "sha512-GHY+Ps/K/RBfRj7kwx+iVf2HIdqOS43rM2iDOIDpapyUnGA9CCBFzFV/XvfzznGykd//z2dkGZhlZZprsVFqGg==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.90.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.90.1.tgz", + "integrity": "sha512-U8KaKGLUgTIFHtwEW1dgw1gK7XrdpvvYo7nzzqPx721GqPe8WZbAiLh/hmyKLGBYQ/mmQNr20vU9tWSDZpii3w==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.90.1", + "@supabase/functions-js": "2.90.1", + "@supabase/postgrest-js": "2.90.1", + "@supabase/realtime-js": "2.90.1", + "@supabase/storage-js": "2.90.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1932,17 +2005,21 @@ "version": "20.19.27", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", + "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1958,6 +2035,24 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.52.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.52.0.tgz", @@ -2829,6 +2924,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2848,7 +2956,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2916,7 +3023,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3024,6 +3130,21 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-picker-react": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.16.1.tgz", + "integrity": "sha512-MrPX0tOCfRL3uYI4of/2GRZ7S6qS7YlacKiF78uFH84/C62vcuHE2DZyv5b4ZJMk0e06es1jjB4e31Bb+YSM8w==", + "license": "MIT", + "dependencies": { + "flairup": "1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -3031,6 +3152,28 @@ "dev": true, "license": "MIT" }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", @@ -3778,6 +3921,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flairup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz", + "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==", + "license": "MIT" + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -4153,6 +4302,15 @@ "hermes-estree": "0.25.1" } }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4979,7 +5137,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -5552,6 +5709,26 @@ } } }, + "node_modules/react-window": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.4.tgz", + "integrity": "sha512-FiZsQHvt2qbnTz6cN+/FXvX62v2xukQ+AajUivkm/Ivdp9rnU3bp0B1eDcCNpQXNaDBdqkEVGNYHlvIUGU9yBw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-window-infinite-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-window-infinite-loader/-/react-window-infinite-loader-2.0.0.tgz", + "integrity": "sha512-dioOyvShGheEqqFHcPNKCixCOc2evwb2VEt9sitfJfTZ1hir8m6b8W0CNBvcUj+8Y8IeWu4yb88DI7k88aYTQQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5958,6 +6135,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6428,7 +6633,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -6674,6 +6878,35 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 11ce6f4..409fd5b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,12 +12,18 @@ "@apollo/client": "^4.0.11", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", + "@supabase/auth-helpers-nextjs": "^0.15.0", + "@supabase/ssr": "^0.8.0", + "@supabase/supabase-js": "^2.90.1", + "@types/react-window": "^1.8.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "emoji-picker-react": "^4.16.1", "graphql": "^16.12.0", "lucide-react": "^0.562.0", "next": "16.1.1", @@ -25,7 +31,10 @@ "react": "19.2.3", "react-dom": "19.2.3", "react-icons": "^5.5.0", + "react-window": "^2.2.4", + "react-window-infinite-loader": "^2.0.0", "rxjs": "^7.8.2", + "socket.io-client": "^4.8.3", "tailwind-merge": "^3.4.0", "zustand": "^5.0.9" }, diff --git a/frontend/src/app/[username]/page.tsx b/frontend/src/app/[username]/page.tsx index ef1fe48..0956f76 100644 --- a/frontend/src/app/[username]/page.tsx +++ b/frontend/src/app/[username]/page.tsx @@ -1,7 +1,9 @@ "use client"; -import { use } from "react"; -import { useQuery } from "@apollo/client/react"; +import { use, useState, useEffect } from "react"; +import { useQuery, useMutation } from "@apollo/client/react"; +import { FOLLOW_USER_MUTATION, UNFOLLOW_USER_MUTATION } from "@/graphql/mutations/auth"; +import { useUIStore } from "@/stores/ui"; import { MainLayout } from "@/components/layout/MainLayout"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/Avatar"; import { Button } from "@/components/ui/Button"; @@ -14,7 +16,7 @@ import { DialogContent, DialogHeader, DialogTitle, - DialogTrigger + DialogTrigger, } from "@/components/ui/dialog"; import { Settings } from "lucide-react"; import { useAuthStore } from "@/stores/auth"; @@ -23,6 +25,7 @@ import { GET_USER_POSTS } from "@/graphql/queries/post"; import { formatCount, getDisplayName } from "@/lib/utils"; import Link from "next/link"; import type { User, PostConnection } from "@/types"; +import { BlockedUsersList } from "@/components/user/BlockedUsersList"; interface ProfilePageProps { params: Promise<{ username: string }>; @@ -43,9 +46,12 @@ export default function ProfilePage({ params }: ProfilePageProps) { const decodedUsername = decodeURIComponent(rawUsername); const username = decodedUsername.replace(/^@+/, ""); - const currentUser = useAuthStore(state => state.user); + const currentUser = useAuthStore((state) => state.user); + const isAuthenticated = useAuthStore(state => state.isAuthenticated); const isOwnProfile = currentUser?.username === username; + const { openLoginModal, showToast, openEditProfileModal, openCreatePost } = useUIStore(); + const { data: userData, loading: userLoading } = useQuery( GET_USER_BY_USERNAME, { variables: { username } } @@ -53,16 +59,75 @@ export default function ProfilePage({ params }: ProfilePageProps) { const user = userData?.getUserByUsername; + const [activeTab, setActiveTab] = useState<'threads' | 'replies' | 'reposts' | 'media' | 'bookmarks' | 'blocked'>('threads'); + + const getFilterFromTab = (tab: typeof activeTab) => { + switch (tab) { + case 'replies': return 'REPLIES'; + case 'reposts': return 'REPOSTS'; + case 'media': return 'MEDIA'; + case 'bookmarks': return 'BOOKMARKS'; + default: return 'THREADS'; + } + }; + const { data: postsData, loading: postsLoading } = useQuery( GET_USER_POSTS, { - variables: { userId: user?.id, first: 20 }, - skip: !user?.id, + variables: { + userId: user?.id, + first: 20, + filter: getFilterFromTab(activeTab) + }, + skip: !user?.id || activeTab === 'blocked', + fetchPolicy: "cache-and-network", } ); const posts = postsData?.getUserPosts?.edges || []; + const [isFollowing, setIsFollowing] = useState(false); + const [followersCount, setFollowersCount] = useState(0); + + useEffect(() => { + if (user) { + setIsFollowing(user.isFollowing || false); + setFollowersCount(user.stats?.followersCount || 0); + } + }, [user]); + + const [followUser, { loading: followLoading }] = useMutation(FOLLOW_USER_MUTATION); + const [unfollowUser, { loading: unfollowLoading }] = useMutation(UNFOLLOW_USER_MUTATION); + + const handleFollowToggle = async () => { + if (!user) return; + + if (!isAuthenticated) { + openLoginModal(); + return; + } + + const previousIsFollowing = isFollowing; + const previousFollowersCount = followersCount; + + // Optimistic update + setIsFollowing(!isFollowing); + setFollowersCount(isFollowing ? followersCount - 1 : followersCount + 1); + + try { + if (isFollowing) { + await unfollowUser({ variables: { userId: user.id } }); + } else { + await followUser({ variables: { userId: user.id } }); + } + } catch (error) { + // Revert on error + setIsFollowing(previousIsFollowing); + setFollowersCount(previousFollowersCount); + showToast("Failed to update follow status"); + } + }; + if (userLoading) { return ( @@ -88,9 +153,16 @@ export default function ProfilePage({ params }: ProfilePageProps) { return (
-

User not found

-

@{username} doesn't exist

- +

+ User not found +

+

+ @{username} doesn't exist +

+ Go home
@@ -112,24 +184,38 @@ export default function ProfilePage({ params }: ProfilePageProps) { - {user.firstName[0]}{user.lastName?.[0]} + {user.firstName[0]} + {user.lastName?.[0]} {/* Bio */} {user.bio && ( -

{user.bio}

+

+ {user.bio} +

)} {/* Stats */}
- {formatCount(user.stats?.followersCount || 0)} followers + + {formatCount(followersCount)} followers + + ยท + + {formatCount(user.stats?.followingCount || 0)} following + {user.website && ( <> ยท - - {user.website.replace(/^https?:\/\//, '')} + + {user.website.replace(/^https?:\/\//, "")} )} @@ -139,18 +225,28 @@ export default function ProfilePage({ params }: ProfilePageProps) {
{isOwnProfile ? ( <> - - - + - Settings + + Settings +
@@ -160,10 +256,27 @@ export default function ProfilePage({ params }: ProfilePageProps) { ) : ( <> - - @@ -172,21 +285,79 @@ export default function ProfilePage({ params }: ProfilePageProps) {
{/* Tabs */} -
- - - + + + {/* Saved tab - only visible on mobile (desktop users use sidebar Pin) */} + {isOwnProfile && ( + + )} + + {isOwnProfile && ( + + )}
- {/* Posts */} + {/* Posts or Blocked List */}
- {postsLoading ? ( + {activeTab === 'blocked' ? ( + + ) : postsLoading ? ( <> @@ -194,12 +365,36 @@ export default function ProfilePage({ params }: ProfilePageProps) { ) : posts.length === 0 ? (
-

No threads yet

+

+ {activeTab === 'threads' && "No threads yet"} + {activeTab === 'replies' && "No replies yet"} + {activeTab === 'reposts' && "No reposts yet"} + {activeTab === 'media' && "No media yet"} + {activeTab === 'bookmarks' && "No saved posts yet"} +

) : ( - posts.map(({ node: post }) => ( - - )) +
+ {/* Guest Limit Logic */} + {!isAuthenticated && posts.length > 0 ? ( + <> + {posts.slice(0, 3).map(({ node: post }) => )} + + {/* Blur / CTA for Guests */} +
+

Log in to see more

+

+ Log in to view more posts from @{username} +

+ +
+ + ) : ( + posts.map(({ node: post }) => ) + )} +
)}
diff --git a/frontend/src/app/activity/page.tsx b/frontend/src/app/activity/page.tsx index f5b2edf..17ce096 100644 --- a/frontend/src/app/activity/page.tsx +++ b/frontend/src/app/activity/page.tsx @@ -12,6 +12,7 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; import type { Notification } from "@/types"; +import { UserTooltip } from "@/components/common/UserTooltip"; interface NotificationsData { getMyNotifications: Notification[]; @@ -76,33 +77,52 @@ export default function ActivityPage() {
) : (
- {notifications.map((notification) => ( - - - - - {notification.actor.firstName[0]}{notification.actor.lastName?.[0]} - - - -
-
- {notification.actor.username} - {notificationText[notification.type]} - - {formatRelativeTime(notification.createdAt)} - + {notifications.map((notification) => { + const mainLinkHref = notification.entityId ? `/post/${notification.entityId}` : `/@${notification.actor.username}`; + + return ( +
+ {/* Main clickable area for the row */} + + + {/* Avatar with Tooltip - z-10 to sit above main link */} +
+ + + + + + {notification.actor.firstName[0]}{notification.actor.lastName?.[0]} + + + + +
+ +
+
+ + + {notification.actor.username} + + + + {notificationText[notification.type]} + + + {formatRelativeTime(notification.createdAt)} + +
+ {!notification.isRead && ( +
+ )}
- {!notification.isRead && ( -
- )} - - ))} + ) + })}
)}
diff --git a/frontend/src/app/auth/callback/route.ts b/frontend/src/app/auth/callback/route.ts new file mode 100644 index 0000000..a1bdb90 --- /dev/null +++ b/frontend/src/app/auth/callback/route.ts @@ -0,0 +1,67 @@ +import { createServerClient } from '@supabase/ssr'; +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(request: NextRequest) { + const requestUrl = new URL(request.url); + const code = requestUrl.searchParams.get('code'); + const type = requestUrl.searchParams.get('type'); + const next = requestUrl.searchParams.get('next') || '/'; + + if (code) { + const cookieStore = await cookies(); + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll(); + }, + setAll(cookiesToSet) { + try { + cookiesToSet.forEach(({ name, value, options }) => + cookieStore.set(name, value, options) + ); + } catch { + // The `setAll` method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } + }, + }, + } + ); + + try { + // Exchange the code for a session + const { error } = await supabase.auth.exchangeCodeForSession(code); + + if (error) { + console.error('[AUTH CALLBACK] Error exchanging code:', error); + return NextResponse.redirect(new URL('/login?error=auth_failed', requestUrl.origin)); + } + + // Determine redirect based on the type of auth action + if (type === 'recovery') { + // Password reset flow - redirect to reset password page + return NextResponse.redirect(new URL('/reset-password', requestUrl.origin)); + } + + if (type === 'signup' || type === 'email') { + // Email confirmation - redirect to verify-email success page + return NextResponse.redirect(new URL('/verify-email', requestUrl.origin)); + } + + // Default redirect + return NextResponse.redirect(new URL(next, requestUrl.origin)); + } catch (err) { + console.error('[AUTH CALLBACK] Unexpected error:', err); + return NextResponse.redirect(new URL('/login?error=auth_failed', requestUrl.origin)); + } + } + + // No code provided, redirect to home + return NextResponse.redirect(new URL('/', requestUrl.origin)); +} diff --git a/frontend/src/app/create/page.tsx b/frontend/src/app/create/page.tsx new file mode 100644 index 0000000..2e2c0a8 --- /dev/null +++ b/frontend/src/app/create/page.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { CreatePost } from "@/components/post/CreatePost"; +import { useRouter } from "next/navigation"; +import { MainLayout } from "@/components/layout/MainLayout"; + +import { useUIStore } from "@/stores/ui"; +// ... + +export default function CreatePostPage() { + const router = useRouter(); + const editPostData = useUIStore(state => state.editPostData); + + return ( + +
+

{editPostData ? "Update Thread" : "New Thread"}

+
+ router.push("/")} + /> +
+
+
+ ); +} diff --git a/frontend/src/app/forgot-password/page.tsx b/frontend/src/app/forgot-password/page.tsx index 8f0bd4c..63ee75a 100644 --- a/frontend/src/app/forgot-password/page.tsx +++ b/frontend/src/app/forgot-password/page.tsx @@ -1,11 +1,19 @@ "use client"; import { ForgotPasswordForm } from "@/components/auth/ForgotPasswordForm"; +import Link from "next/link"; +import { X } from "lucide-react"; export default function ForgotPasswordPage() { return (
-
+
+ + +
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 46c0372..15691f9 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -78,7 +78,7 @@ --radius-2xl: calc(var(--radius) + 8px); --radius-3xl: calc(var(--radius) + 12px); --radius-4xl: calc(var(--radius) + 16px); - + --shadow-sm: var(--shadow); --shadow-md: var(--shadow); --shadow-lg: var(--shadow); @@ -88,6 +88,7 @@ * { @apply border-border outline-ring/50; } + body { @apply bg-background text-foreground; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; @@ -99,7 +100,7 @@ /* Custom functional styles for Threads aesthetics */ .thread-line { position: absolute; - left: 17px; + left: 17px; top: 48px; bottom: 0; width: 2px; @@ -116,16 +117,24 @@ /* Animations */ @keyframes fadeIn { - from { opacity: 0; transform: translateY(4px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(4px); + } + + to { + opacity: 1; + transform: translateY(0); + } } @keyframes scaleIn { - from { + from { opacity: 0; transform: scale(0.98) translateY(10px); } - to { + + to { opacity: 1; transform: scale(1) translateY(0); } @@ -139,6 +148,18 @@ animation: scaleIn 0.5s cubic-bezier(0.16, 1, 0.3, 1); } +/* Scrollbar Utility */ +.hide-scrollbar { + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ +} + +.hide-scrollbar::-webkit-scrollbar { + display: none; +} + /* Scrollbar */ ::-webkit-scrollbar { width: 6px; @@ -156,5 +177,4 @@ ::-webkit-scrollbar-thumb:hover { background-color: var(--foreground); -} - +} \ No newline at end of file diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 5b078ab..2c4c50b 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -2,6 +2,8 @@ import { LoginForm } from "@/components/auth/LoginForm"; import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { X } from "lucide-react"; export default function LoginPage() { const router = useRouter(); @@ -12,7 +14,13 @@ export default function LoginPage() { return (
-
+
+ + +
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 7516e63..367a826 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,12 +1,16 @@ "use client"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useQuery } from "@apollo/client/react"; import { MainLayout } from "@/components/layout/MainLayout"; import { Header } from "@/components/layout/Header"; -import { PostCard } from "@/components/post/PostCard"; +import { VirtualizedFeed } from "@/components/post/VirtualizedFeed"; import { PostSkeleton } from "@/components/ui/Loading"; import { useAuthStore } from "@/stores/auth"; -import { GET_TRENDING_POSTS } from "@/graphql/queries/post"; +import { useUIStore } from "@/stores/ui"; +import { GET_PUBLIC_FEED, GET_HOME_FEED } from "@/graphql/queries/post"; +import { useSocketEvent } from "@/hooks/useSocket"; +import { updatePostInCache } from "@/lib/apollo-client"; import type { PostConnection } from "@/types"; interface TrendingPostsData { @@ -14,21 +18,47 @@ interface TrendingPostsData { } export default function HomePage() { - const isAuthenticated = useAuthStore(state => state.isAuthenticated); - const isLoading = useAuthStore(state => state.isLoading); - - const { data, loading: postsLoading, fetchMore } = useQuery( - GET_TRENDING_POSTS, - { - variables: { first: 20 }, - notifyOnNetworkStatusChange: true, - } + const isAuthenticated = useAuthStore((state: any) => state.isAuthenticated); + const isLoading = useAuthStore((state: any) => state.isLoading); + + // Use getHomeFeed for authenticated users, getPublicFeed (chronological) for guests + const query = isAuthenticated ? GET_HOME_FEED : GET_PUBLIC_FEED; + const queryField = isAuthenticated ? "getHomeFeed" : "getPublicFeed"; + + const { + data, + loading: postsLoading, + fetchMore, + refetch, + } = useQuery(query, { + variables: { first: 20 }, + notifyOnNetworkStatusChange: true, + skip: isLoading, // Don't fetch until auth state is determined + }); + + const response = data?.[queryField]; + const posts = response?.edges || []; + const pageInfo = response?.pageInfo; + + // Handle new post event - refetch to get latest posts + const handleNewPost = useCallback(() => { + refetch(); + }, [refetch]); + + // Handle like/unlike events - update cache optimistically + const handleLikeUpdate = useCallback( + (data: { postId: string; likesCount: number; userId: string }) => { + updatePostInCache(data.postId, { likesCount: data.likesCount }); + }, + [] ); - const posts = data?.getTrendingPosts?.edges || []; - const pageInfo = data?.getTrendingPosts?.pageInfo; + // Subscribe to socket events + useSocketEvent("post:created", handleNewPost); + useSocketEvent("post:liked", handleLikeUpdate); + useSocketEvent("post:unliked", handleLikeUpdate); - const handleLoadMore = () => { + const handleLoadMore = useCallback(() => { if (!pageInfo?.hasNextPage || postsLoading) return; fetchMore({ @@ -36,49 +66,176 @@ export default function HomePage() { after: pageInfo.endCursor, }, }); + }, [pageInfo, postsLoading, fetchMore]); + + // Home Refresh Trigger Logic + const homeRefreshTrigger = useUIStore((state) => state.homeRefreshTrigger); + const homeScrollPosition = useUIStore((state) => state.homeScrollPosition); + const setHomeScrollPosition = useUIStore((state) => state.setHomeScrollPosition); + const clearHomeScrollPosition = useUIStore((state) => state.clearHomeScrollPosition); + const [isHomeRefreshing, setIsHomeRefreshing] = useState(false); + const hasRestoredScroll = useRef(false); + + useEffect(() => { + if (homeRefreshTrigger > 0) { + if (window.scrollY > 0) { + // Already scrolled down - just scroll to top + window.scrollTo({ top: 0, behavior: "smooth" }); + } else { + // At top - refresh the feed + setIsHomeRefreshing(true); + clearHomeScrollPosition(); + refetch().finally(() => { + setTimeout(() => setIsHomeRefreshing(false), 500); + }); + } + } + }, [homeRefreshTrigger, refetch, clearHomeScrollPosition]); + + // Save scroll position on unmount + useEffect(() => { + return () => { + setHomeScrollPosition(window.scrollY); + }; + }, [setHomeScrollPosition]); + + // Restore scroll position on mount when cached data is available + useEffect(() => { + if (!hasRestoredScroll.current && posts.length > 0 && homeScrollPosition > 0) { + hasRestoredScroll.current = true; + // Use requestAnimationFrame to ensure DOM is ready + requestAnimationFrame(() => { + window.scrollTo({ top: homeScrollPosition, behavior: "instant" }); + }); + } + }, [posts.length, homeScrollPosition]); + + // Pull to Refresh Logic + const [isPulling, setIsPulling] = useState(false); + const [pullDistance, setPullDistance] = useState(0); + const [isRefreshing, setIsRefreshing] = useState(false); + const startY = useRef(0); + const currentY = useRef(0); + + const PULL_THRESHOLD = 80; + const MAX_PULL = 120; + + const handleTouchStart = (e: React.TouchEvent) => { + if (window.scrollY === 0) { + startY.current = e.touches[0].clientY; + setIsPulling(true); + } + }; + + const handleTouchMove = (e: React.TouchEvent) => { + if (!isPulling || window.scrollY > 0) return; + + currentY.current = e.touches[0].clientY; + const diff = currentY.current - startY.current; + + if (diff > 0) { + // Add resistance + const newDistance = Math.min(diff * 0.5, MAX_PULL); + setPullDistance(newDistance); + } + }; + + const handleTouchEnd = async () => { + if (!isPulling) return; + + setIsPulling(false); + if (pullDistance > PULL_THRESHOLD) { + setIsRefreshing(true); + setPullDistance(50); // Keep it visible while refreshing + await refetch(); + setTimeout(() => { + setIsRefreshing(false); + setPullDistance(0); + }, 500); + } else { + setPullDistance(0); + } }; return (
+ {/* Home Refresh Loader */} + {isHomeRefreshing && ( +
+
+ {[...Array(8)].map((_, i) => ( +
+ ))} +
+ +
+ )} {/* Posts Feed */} -
- {postsLoading && posts.length === 0 ? ( - // Loading skeletons - <> - - - - - - ) : posts.length === 0 ? ( - // Empty state -
-

No posts yet. Be the first to share!

-
- ) : ( - // Posts list - <> - {posts.map(({ node: post }) => ( - - ))} +
+ {/* Pull to Refresh Spinner */} +
0 ? 1 : 0 + }} + > + {isRefreshing ? ( +
+ ) : ( +
+ โฌ‡๏ธ +
+ )} +
- {/* Load more */} - {pageInfo?.hasNextPage && ( -
- -
- )} - - )} +
+ {(isLoading || postsLoading) && posts.length === 0 ? ( + // Loading skeletons (shown while auth or posts are loading) + <> + + + + + + ) : ( + + )} +
); diff --git a/frontend/src/app/post/[id]/page.tsx b/frontend/src/app/post/[id]/page.tsx new file mode 100644 index 0000000..adc4edd --- /dev/null +++ b/frontend/src/app/post/[id]/page.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { useQuery } from "@apollo/client/react"; +import { useParams } from "next/navigation"; +import Link from "next/link"; +import { MainLayout } from "@/components/layout/MainLayout"; +import { Header } from "@/components/layout/Header"; +import { PostCard } from "@/components/post/PostCard"; +import { PostSkeleton } from "@/components/ui/Loading"; +import { GET_POST_BY_ID, GET_POST_REPLIES } from "@/graphql/queries/post"; +import type { Post, PostConnection } from "@/types"; +import { ArrowLeft } from "lucide-react"; + +interface PostData { + getPostById: Post; +} + +interface RepliesData { + getPostReplies: PostConnection; +} + +export default function PostDetailPage() { + const params = useParams(); + const postId = params?.id as string; + + const { + data: postData, + loading: postLoading, + error: postError, + } = useQuery(GET_POST_BY_ID, { + variables: { id: postId }, + skip: !postId, + }); + + const { + data: repliesData, + loading: repliesLoading, + fetchMore, + } = useQuery(GET_POST_REPLIES, { + variables: { postId, first: 20 }, + skip: !postId, + }); + + const post = postData?.getPostById; + const replies = repliesData?.getPostReplies?.edges || []; + const pageInfo = repliesData?.getPostReplies?.pageInfo; + + const handleLoadMore = () => { + if (!pageInfo?.hasNextPage || repliesLoading) return; + fetchMore({ + variables: { after: pageInfo.endCursor }, + }); + }; + + if (postLoading) { + return ( + +
+ + + + ); + } + + if (postError || !post) { + return ( + +
+
+

Post not found

+ + + Back to Home + +
+ + ); + } + + return ( + +
+ + {/* Main Post */} + + + {/* Replies Section */} +
+ {/* Replies Header */} + {replies.length > 0 && ( +
+ + {post.repliesCount} {post.repliesCount === 1 ? "Reply" : "Replies"} + +
+ )} + + {/* Replies List */} + {repliesLoading && replies.length === 0 ? ( + <> + + + + ) : replies.length === 0 ? ( +
+

+ No replies yet. Be the first to reply! +

+
+ ) : ( + <> + {replies.map(({ node: reply }) => ( + + ))} + + {/* Load More */} + {pageInfo?.hasNextPage && ( +
+ +
+ )} + + )} +
+ + ); +} diff --git a/frontend/src/app/register/page.tsx b/frontend/src/app/register/page.tsx index 6af2064..4de1efa 100644 --- a/frontend/src/app/register/page.tsx +++ b/frontend/src/app/register/page.tsx @@ -1,11 +1,19 @@ "use client"; import { RegisterForm } from "@/components/auth/RegisterForm"; +import Link from "next/link"; +import { X } from "lucide-react"; export default function RegisterPage() { return (
-
+
+ + +
diff --git a/frontend/src/app/reset-password/page.tsx b/frontend/src/app/reset-password/page.tsx index 7dcc928..dd97c93 100644 --- a/frontend/src/app/reset-password/page.tsx +++ b/frontend/src/app/reset-password/page.tsx @@ -1,43 +1,38 @@ "use client"; import React, { useState, Suspense } from "react"; -import { useMutation } from "@apollo/client/react"; -import { useSearchParams, useRouter } from "next/navigation"; +import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { ThreadsLogo } from "@/components/ui/Logo"; -import { RESET_PASSWORD_MUTATION } from "@/graphql/mutations/auth"; +import { supabase } from "@/lib/supabase"; function ResetPasswordForm() { - const searchParams = useSearchParams(); const router = useRouter(); - const token = searchParams.get("token"); - const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); const [error, setError] = useState(""); - const [resetPasswordMutation] = useMutation(RESET_PASSWORD_MUTATION); - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (password !== confirmPassword) { setError("Passwords do not match"); return; } - if (!token) { - setError("Reset token is missing"); - return; - } setStatus("loading"); setError(""); try { - await resetPasswordMutation({ - variables: { token, newPassword: password }, + // Supabase handles the recovery session automatically + // when user clicks the link and is redirected to this page. + const { error: authError } = await supabase.auth.updateUser({ + password: password, }); + + if (authError) throw authError; + setStatus("success"); } catch (err: any) { setError(err.message || "Failed to reset password"); diff --git a/frontend/src/app/search/page.tsx b/frontend/src/app/search/page.tsx index 8c64bb2..eb99875 100644 --- a/frontend/src/app/search/page.tsx +++ b/frontend/src/app/search/page.tsx @@ -1,16 +1,17 @@ "use client"; import { useState } from "react"; -import { useLazyQuery } from "@apollo/client/react"; +import { useQuery, useLazyQuery } from "@apollo/client/react"; import { MainLayout } from "@/components/layout/MainLayout"; import { Header } from "@/components/layout/Header"; import { PostCard } from "@/components/post/PostCard"; -import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/Avatar"; import { PostSkeleton } from "@/components/ui/Loading"; -import { SEARCH_USERS, SEARCH_POSTS } from "@/graphql/queries/search"; +import { SEARCH_USERS, SEARCH_POSTS, GET_SUGGESTED_USERS } from "@/graphql/queries/search"; +import { GET_TRENDING_POSTS } from "@/graphql/queries/post"; import { debounce } from "@/lib/utils"; import Link from "next/link"; import type { UserConnection, PostConnection, User } from "@/types"; +import { UserResultCard } from "@/components/search/UserResultCard"; type SearchTab = "users" | "posts"; @@ -18,11 +19,21 @@ export default function SearchPage() { const [query, setQuery] = useState(""); const [activeTab, setActiveTab] = useState("users"); + // Default data - fetch on mount + const { data: suggestedUsersData, loading: suggestedLoading } = useQuery<{ getSuggestedUsers: User[] }>(GET_SUGGESTED_USERS, { + variables: { first: 10 }, + }); + + const { data: trendingPostsData, loading: trendingLoading } = useQuery<{ getTrendingPosts: PostConnection }>(GET_TRENDING_POSTS, { + variables: { first: 10 }, + }); + + // Search queries const [searchUsers, { data: usersData, loading: usersLoading }] = useLazyQuery<{ searchUsers: UserConnection }>(SEARCH_USERS); const [searchPosts, { data: postsData, loading: postsLoading }] = useLazyQuery<{ searchPosts: PostConnection }>(SEARCH_POSTS); - const handleSearch = debounce((value: string) => { - if (value.trim().length < 2) return; + const handleSearch = debounce((value: unknown) => { + if (typeof value !== 'string' || value.trim().length < 2) return; if (activeTab === "users") { searchUsers({ variables: { query: value, first: 20 } }); @@ -48,9 +59,14 @@ export default function SearchPage() { } }; + // Determine what to show + const isSearching = query.length >= 2; const users = usersData?.searchUsers?.edges || []; const posts = postsData?.searchPosts?.edges || []; + const suggestedUsers = suggestedUsersData?.getSuggestedUsers || []; + const trendingPosts = trendingPostsData?.getTrendingPosts?.edges || []; const loading = usersLoading || postsLoading; + const defaultLoading = suggestedLoading || trendingLoading; return ( @@ -110,10 +126,51 @@ export default function SearchPage() { {/* Results */}
- {query.length === 0 ? ( -
-

Search for users or posts

-
+ {!isSearching ? ( + // Show default content (suggested users / trending posts) + activeTab === "users" ? ( + defaultLoading ? ( +
+ + + +
+ ) : suggestedUsers.length === 0 ? ( +
+

No suggested users

+
+ ) : ( +
+
+

Suggested for you

+
+ {suggestedUsers.map((user) => ( + + ))} +
+ ) + ) : ( + defaultLoading ? ( +
+ + + +
+ ) : trendingPosts.length === 0 ? ( +
+

No trending posts

+
+ ) : ( +
+
+

Trending posts

+
+ {trendingPosts.map(({ node: post }) => ( + + ))} +
+ ) + ) ) : query.length < 2 ? (

Enter at least 2 characters

@@ -132,38 +189,7 @@ export default function SearchPage() { ) : (
{users.map((edge) => ( - - - - - {edge.node.firstName[0]}{edge.node.lastName?.[0]} - - -
-
- - {edge.node.username} - -
-

- {edge.node.firstName} {edge.node.lastName} -

- {edge.node.stats && ( -

- {edge.node.stats.followersCount.toLocaleString()} followers -

- )} -
-
- -
- + ))}
) diff --git a/frontend/src/app/tags/[tag]/page.tsx b/frontend/src/app/tags/[tag]/page.tsx new file mode 100644 index 0000000..e1b1c81 --- /dev/null +++ b/frontend/src/app/tags/[tag]/page.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { use } from "react"; +import { useQuery } from "@apollo/client/react"; +import { MainLayout } from "@/components/layout/MainLayout"; +import { PostCard } from "@/components/post/PostCard"; +import { PostSkeleton } from "@/components/ui/Loading"; +import { GET_POSTS_BY_HASHTAG } from "@/graphql/queries/post"; +import type { PostConnection } from "@/types"; + +interface HashtagPageProps { + params: Promise<{ tag: string }>; +} + +interface PostsData { + getPostsByHashtag: PostConnection; +} + +export default function HashtagPage({ params }: HashtagPageProps) { + const resolvedParams = use(params); + const tag = decodeURIComponent(resolvedParams.tag); + + const { data, loading, error } = useQuery(GET_POSTS_BY_HASHTAG, { + variables: { tag, first: 20 }, + }); + + const posts = data?.getPostsByHashtag.edges || []; + + if (error) { + return ( + +
+ Error fetching posts: {error.message} +
+
+ ); + } + + return ( + + {/* Header */} +
+

#{tag}

+

{loading ? "..." : `${posts.length}+ threads`}

+
+ + {/* Posts */} +
+ {loading ? ( + <> + + + + + ) : posts.length === 0 ? ( +
+

No threads found with this hashtag.

+
+ ) : ( + posts.map(({ node: post }) => ( + + )) + )} +
+
+ ); +} diff --git a/frontend/src/app/verify-email/page.tsx b/frontend/src/app/verify-email/page.tsx new file mode 100644 index 0000000..3e17e72 --- /dev/null +++ b/frontend/src/app/verify-email/page.tsx @@ -0,0 +1,133 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useMutation } from "@apollo/client/react"; +import { Button } from "@/components/ui/Button"; +import { ThreadsLogo } from "@/components/ui/Logo"; +import { supabase } from "@/lib/supabase"; +import { REGISTER_MUTATION } from "@/graphql/mutations/auth"; +import { CheckCircle2, Loader2 } from "lucide-react"; + +export default function VerifyEmailPage() { + const router = useRouter(); + const [status, setStatus] = useState<"loading" | "success" | "creating_profile" | "error">("loading"); + const [error, setError] = useState(""); + + const [registerProfile] = useMutation(REGISTER_MUTATION); + + useEffect(() => { + async function completeVerification() { + try { + // Get the current session (should be set by the callback route) + const { data: { session }, error: sessionError } = await supabase.auth.getSession(); + + if (sessionError) { + throw sessionError; + } + + if (!session) { + // No session means verification might have failed + setError("Email verification failed. Please try again."); + setStatus("error"); + return; + } + + // Check if user profile already exists by trying to get user data + // If this is a new user (email just confirmed), we need to create their profile + setStatus("creating_profile"); + + try { + // Try to create profile - this may fail if profile already exists + await registerProfile({ + variables: { + firstName: session.user.user_metadata?.firstName || session.user.email?.split("@")[0] || "User", + lastName: session.user.user_metadata?.lastName || undefined, + username: session.user.user_metadata?.username || undefined, + email: session.user.email, + }, + context: { + headers: { + authorization: `Bearer ${session.access_token}`, + }, + }, + }); + } catch (err: any) { + // Profile might already exist, that's okay + console.log("[VERIFY] Profile creation:", err.message); + } + + setStatus("success"); + } catch (err: any) { + console.error("[VERIFY] Error:", err); + setError(err.message || "Verification failed"); + setStatus("error"); + } + } + + completeVerification(); + }, [registerProfile]); + + if (status === "loading" || status === "creating_profile") { + return ( +
+
+ +
+ + + {status === "creating_profile" ? "Setting up your account..." : "Verifying your email..."} + +
+
+
+ ); + } + + if (status === "error") { + return ( +
+
+ +

Verification Failed

+

{error}

+ +
+
+ ); + } + + return ( +
+
+
+ +
+

Email Verified!

+

+ Your email has been successfully verified. You can now access all features. +

+
+ + +
+
+
+ ); +} diff --git a/frontend/src/components/auth/ForgotPasswordForm.tsx b/frontend/src/components/auth/ForgotPasswordForm.tsx index 580569e..3c6b3aa 100644 --- a/frontend/src/components/auth/ForgotPasswordForm.tsx +++ b/frontend/src/components/auth/ForgotPasswordForm.tsx @@ -1,26 +1,26 @@ import React, { useState } from "react"; -import { useMutation } from "@apollo/client/react"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { ThreadsLogo } from "@/components/ui/Logo"; -import { FORGOT_PASSWORD_MUTATION } from "@/graphql/mutations/auth"; +import { supabase } from "@/lib/supabase"; export function ForgotPasswordForm() { const [email, setEmail] = useState(""); const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); const [error, setError] = useState(""); - const [forgotPasswordMutation] = useMutation(FORGOT_PASSWORD_MUTATION); - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setStatus("loading"); setError(""); try { - await forgotPasswordMutation({ - variables: { email }, + const { error: authError } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: `${window.location.origin}/auth/callback?type=recovery`, }); + + if (authError) throw authError; + setStatus("success"); } catch (err: any) { setError(err.message || "Something went wrong"); diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx index c8e728c..0eabec1 100644 --- a/frontend/src/components/auth/LoginForm.tsx +++ b/frontend/src/components/auth/LoginForm.tsx @@ -1,37 +1,39 @@ import React, { useState } from "react"; -import { useMutation } from "@apollo/client/react"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { ThreadsLogo } from "@/components/ui/Logo"; -import { LOGIN_MUTATION } from "@/graphql/mutations/auth"; +import { supabase } from "@/lib/supabase"; import { useAuthStore } from "@/stores/auth"; export function LoginForm({ onSuccess }: { onSuccess?: () => void }) { const router = useRouter(); - const setLogin = useAuthStore((state) => state.login); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); - - const [loginMutation, { loading }] = useMutation<{ login: { user: any; accessToken: string } }>(LOGIN_MUTATION); + const [loading, setLoading] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(""); + setLoading(true); try { - const { data } = await loginMutation({ - variables: { email, password }, + const { data, error: authError } = await supabase.auth.signInWithPassword({ + email, + password, }); - if (data?.login?.user) { - setLogin(data.login.user); + if (authError) throw authError; + + if (data?.session) { onSuccess?.(); router.push("/"); } } catch (err: any) { setError(err.message || "Invalid email or password"); + } finally { + setLoading(false); } }; @@ -49,8 +51,8 @@ export function LoginForm({ onSuccess }: { onSuccess?: () => void }) { )} setEmail(e.target.value)} required diff --git a/frontend/src/components/auth/RegisterForm.tsx b/frontend/src/components/auth/RegisterForm.tsx index f139613..bba77e7 100644 --- a/frontend/src/components/auth/RegisterForm.tsx +++ b/frontend/src/components/auth/RegisterForm.tsx @@ -6,52 +6,99 @@ import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { ThreadsLogo } from "@/components/ui/Logo"; -import { REGISTER_MUTATION, LOGIN_MUTATION } from "@/graphql/mutations/auth"; -import { useAuthStore } from "@/stores/auth"; +import { REGISTER_MUTATION } from "@/graphql/mutations/auth"; +import { supabase } from "@/lib/supabase"; export function RegisterForm({ onSuccess }: { onSuccess?: () => void }) { const router = useRouter(); - const setLogin = useAuthStore((state) => state.login); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); const [username, setUsername] = useState(""); const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const [successMessage, setSuccessMessage] = useState(""); - const [registerMutation, { loading: registerLoading }] = useMutation<{ createUser: any }>(REGISTER_MUTATION); - const [loginMutation, { loading: loginLoading }] = useMutation<{ login: { user: any; accessToken: string } }>(LOGIN_MUTATION); + // Profile creation mutation + const [registerProfile] = useMutation<{ createUser: any }>(REGISTER_MUTATION); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(""); + setSuccessMessage(""); + setLoading(true); try { - await registerMutation({ - variables: { - firstName, - lastName: lastName || undefined, - username: username || undefined, - email, - password, + // 1. SignUp with Supabase - include redirect URL and user metadata + const { data: authData, error: authError } = await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: `${window.location.origin}/auth/callback?type=signup`, + data: { + firstName, + lastName: lastName || undefined, + username: username || undefined, + }, }, }); - // Auto login after registration - const { data } = await loginMutation({ - variables: { email, password }, - }); + if (authError) throw authError; + + if (authData?.user) { + // If email confirmation is enabled, session will be null + if (!authData.session) { + setSuccessMessage("Account created! Please check your email to confirm your account before logging in."); + setLoading(false); + return; + } + + // 2. Create Profile in our DB via GraphQL + // We explicitly pass the context to ensure the new token is used + await registerProfile({ + variables: { + firstName, + lastName: lastName || undefined, + username: username || undefined, + email, + }, + context: { + headers: { + authorization: `Bearer ${authData.session.access_token}` + } + } + }); - if (data?.login?.user) { - setLogin(data.login.user); onSuccess?.(); router.push("/"); } } catch (err: any) { - setError(err.message || "Registration failed"); + console.error("[REGISTER] Error:", err); + // Handle the case where user already exists in Supabase but profile creation failed last time + if (err.message?.includes("User already registered")) { + setError("This email is already registered. Please log in."); + } else { + setError(err.message || "Registration failed"); + } + } finally { + setLoading(false); } }; + if (successMessage) { + return ( +
+ +

Check your email

+

{successMessage}

+ +
+ ); + } + return (
@@ -107,7 +154,7 @@ export function RegisterForm({ onSuccess }: { onSuccess?: () => void }) { + + {/* Navigation */} + {media.length > 1 && ( + <> + + + + )} + + {/* Content Container - Stops propagation to prevent closing when clicking content */} +
e.stopPropagation()} + > + {currentMedia.mediaType === "IMAGE" || currentMedia.mediaType === "GIF" ? ( + Full screen view + ) : ( +
+
, + document.body + ); +} diff --git a/frontend/src/components/common/ReportProblemModal.tsx b/frontend/src/components/common/ReportProblemModal.tsx new file mode 100644 index 0000000..ca3a17d --- /dev/null +++ b/frontend/src/components/common/ReportProblemModal.tsx @@ -0,0 +1,211 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/Button"; +import { Paperclip, X, Loader2 } from "lucide-react"; +import { useAuthStore } from "@/stores/auth"; +import { useUIStore } from "@/stores/ui"; +import { useMutation } from "@apollo/client/react"; +import { gql } from "@apollo/client"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/Select"; +import { Textarea } from "../ui/Textarea"; + +const SUBMIT_REPORT_MUTATION = gql` + mutation SubmitReport($input: CreateReportInput!) { + submitReport(input: $input) + } +`; + +interface ReportProblemModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function ReportProblemModal({ isOpen, onClose }: ReportProblemModalProps) { + const { showToast } = useUIStore(); + const [category, setCategory] = useState("Bug"); + const [description, setDescription] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); + const fileInputRef = useRef(null); + + const [submitReport] = useMutation(SUBMIT_REPORT_MUTATION); + + const handleFileSelect = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + const file = e.target.files[0]; + setSelectedFile(file); + setPreviewUrl(URL.createObjectURL(file)); + } + }; + + const handleRemoveFile = () => { + setSelectedFile(null); + setPreviewUrl(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + const handleSubmit = async () => { + if (!description.trim()) { + showToast("Please describe the problem"); + return; + } + + setIsSubmitting(true); + + try { + let attachmentUrl = null; + + // Upload file if exists + if (selectedFile) { + const formData = new FormData(); + formData.append("files", selectedFile); + + const uploadRes = await fetch(`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/api/upload`, { + method: "POST", + body: formData, + }); + + if (!uploadRes.ok) throw new Error("File upload failed"); + + const data = await uploadRes.json(); + attachmentUrl = data.url; + } + + // Collect device info + const deviceInfo = JSON.stringify({ + userAgent: navigator.userAgent, + screenSize: `${window.innerWidth}x${window.innerHeight}`, + language: navigator.language, + platform: navigator.platform, + url: window.location.href, + timestamp: new Date().toISOString(), + }); + + await submitReport({ + variables: { + input: { + category, + description, + attachmentUrl, + deviceInfo, + }, + }, + }); + + showToast("Report submitted successfully"); + onClose(); + // Reset form + setDescription(""); + setCategory("bug"); + handleRemoveFile(); + + } catch (error) { + console.error(error); + showToast("Failed to submit report"); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + Report a Problem + + +
+
+ + +
+ +
+
+ +
+