From a0ad2039931ea71ba8c53786997c65d9884fb953 Mon Sep 17 00:00:00 2001 From: shrutiiiii Date: Fri, 27 Mar 2026 20:40:42 +0530 Subject: [PATCH 01/22] Containerized api with production data seeding script enabled --- .dockerignore | 81 ++++++++++++++ .env.example | 3 + .gitignore | 2 +- Dockerfile | 80 ++++++++++++++ LOCAL_DEVELOPMENT.md | 58 ++++++++++ docker-compose.yml | 43 ++++++++ package.json | 3 +- scripts/setup-local.sh | 235 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 503 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 LOCAL_DEVELOPMENT.md create mode 100644 docker-compose.yml create mode 100755 scripts/setup-local.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..03dade1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,81 @@ +# Dependencies +node_modules +bun.lockb + +# Build outputs +dist +build +.next + +# Environment files +.env +.env.local +.env.*.local + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +bun-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# IDE files +.vscode +.idea +*.swp +*.swo + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git +.gitignore + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# Documentation and README +README.md +*.md +docs + +# Test files +tests +__tests__ +*.test.ts +*.test.js +*.spec.ts +*.spec.js + +# Development tools +.eslintrc* +.prettierrc* +jest.config* +tsconfig*.json + +# Prisma migrations +# prisma/migrations + +# Temporary files +tmp +temp \ No newline at end of file diff --git a/.env.example b/.env.example index ccfaba5..6d8cbae 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,9 @@ DIRECT_URL= # Supabase URL SUPABASE_URL= +# Session-mode pooler URL — used by setup-local.sh for pg_dump (IPv4-accessible). +SESSION_POOLER= + # Supabase service role key SUPABASE_SERVICE_ROLE_KEY= diff --git a/.gitignore b/.gitignore index 573081c..b5f1f39 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid -*.seed +seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..51e8992 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,80 @@ +FROM node:20-alpine AS base + +RUN apk add --no-cache curl bash ca-certificates + +# Install Bun +RUN curl -fsSL https://bun.sh/install | bash +ENV PATH="/root/.bun/bin:$PATH" + +RUN if [ -f /root/.bun/bin/bun ]; then \ + /root/.bun/bin/bun --version && \ + if [ ! -f /root/.bun/bin/bunx ]; then \ + ln -s /root/.bun/bin/bun /root/.bun/bin/bunx; \ + fi; \ + else \ + echo "ERROR: Bun not installed properly" && exit 1; \ + fi + +WORKDIR /app + + +# ----------------------------- +# deps stage - cache dependencies +# ----------------------------- + + +FROM base AS deps + +COPY package.json bun.lock* ./ +COPY prisma ./prisma + +RUN bun install --frozen-lockfile +RUN bunx prisma generate + + +# ----------------------------- +# development stage +# ----------------------------- + + +FROM deps AS development + +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/src/generated ./src/generated + +COPY . . + +EXPOSE 3000 + +CMD ["bun", "src/server.ts"] + + +# ----------------------------- +# production stage +# ----------------------------- + + +FROM deps AS production + +RUN addgroup -g 1001 -S nodejs && adduser -S bunjs -u 1001 +RUN cp -r /root/.bun /usr/local/bun && chown -R bunjs:nodejs /usr/local/bun + +COPY --from=deps --chown=bunjs:nodejs /app/node_modules ./node_modules +COPY --from=deps --chown=bunjs:nodejs /app/src/generated ./src/generated + +COPY --chown=bunjs:nodejs src ./src +COPY --chown=bunjs:nodejs package.json ./ +COPY --chown=bunjs:nodejs prisma ./prisma + +USER bunjs + +ENV NODE_ENV=production +ENV PORT=3000 +ENV PATH="/usr/local/bun/bin:$PATH" + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://localhost:3000/health || exit 1 + +CMD ["sh", "-c", "bun src/server.ts"] \ No newline at end of file diff --git a/LOCAL_DEVELOPMENT.md b/LOCAL_DEVELOPMENT.md new file mode 100644 index 0000000..af78426 --- /dev/null +++ b/LOCAL_DEVELOPMENT.md @@ -0,0 +1,58 @@ +# Local Development Environment Setup + +This document explains how to set up and manage the local development environment for the COC-API project. + +## Prerequisites + +- **Docker & Docker Compose**: Ensure you have Docker installed and running. +- **PostgreSQL Client (`pg_dump`)**: The setup script uses `pg_dump` to pull data from the remote database. + +## Setup Instructions + +### 1. Configure Environment Variables + +Copy the `.env.example` file to `.env` and fill in the required values: + +```bash +cp .env.example .env +``` + +Key variables for the setup script: +- `SESSION_POOLER`: The direct connection string to your Supabase project (Session Pooler / IPv4). + - **Format**: `postgresql://postgres.PROJECT_REF:PASSWORD@aws-0-us-east-1.pooler.supabase.com:5432/postgres` + +### 2. Run the Setup Script + +The `scripts/setup-local.sh` script automates the entire process: + +```bash +bun run local +``` + +#### What the script does: +1. **Starts Postgres**: Launches the `db` container. +2. **Installs Extensions**: Pre-installs `pgcrypto`, `uuid-ossp`, and `pg_stat_statements` into the `public` schema. +3. **Dumps Remote DB**: Uses `pg_dump` to create a snapshot of the production database. +4. **Cleans the Dump**: Strips Supabase-specific extensions and patches schema references to work locally. +5. **Seeds the Database**: Loads the cleaned dump into your local Postgres container. +6. **Starts the API**: Launches the `api` container and waits for it to be healthy. + +### 3. Useful Commands + +- **Start environment**: `bash scripts/setup-local.sh` +- **Skip dump (reuse existing `seed/dump.sql`)**: `bash scripts/setup-local.sh --skip-dump` +- **Skip seeding (just start containers)**: `bash scripts/setup-local.sh --skip-seed` +- **View logs**: `docker compose logs -f` +- **Stop containers**: `docker compose down` +- **Wipe local data (force re-seed)**: `docker compose down -v` + +## Troubleshooting + +### "Tenant or user not found" during dump +This usually means your `SESSION_POOLER` username is just `postgres`. Supabase requires the format `postgres.PROJECT_REF`. + +### Tables not in `public` schema +The script automatically patches the dump to ensure tables are placed in the `public` schema. If you manually imported a dump, ensure you've installed the required extensions first. + +### Re-seeding +The script skips seeding if it detects existing tables in the `public` schema. To force a re-seed, run `docker compose down -v` before running the setup script. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..411e316 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +services: + api: + image: coc-api + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" + environment: + NODE_ENV: production + PORT: 3000 + DATABASE_URL: "postgresql://postgres:example@db:5432/coc?sslmode=disable" + volumes: + - ./:/app:cached + - /app/node_modules + command: ["bun", "src/server.ts"] + depends_on: + - db + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"] + interval: 30s + timeout: 3s + retries: 3 + + db: + image: postgres:17 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: example + POSTGRES_DB: coc + volumes: + - pgdata:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres", "-d", "coc", "-h", "127.0.0.1", "-p", "5432"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + pgdata: + seed-data: diff --git a/package.json b/package.json index 86ab8eb..3781805 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "precommit": "lint-staged", "migrate:first": "bunx prisma migrate dev --name init", "migrate": "bunx prisma migrate dev", - "generate": "bunx prisma generate" + "generate": "bunx prisma generate", + "local": "bash scripts/setup-local.sh" }, "repository": { "type": "git", diff --git a/scripts/setup-local.sh b/scripts/setup-local.sh new file mode 100755 index 0000000..16cc674 --- /dev/null +++ b/scripts/setup-local.sh @@ -0,0 +1,235 @@ +# ============================================================================= +# setup-local.sh +# +# Sets up the local development environment: +# 1. Starts the local postgres container +# 2. Waits for postgres to be healthy +# 3. Installs required extensions (pgcrypto, uuid-ossp, pg_stat_statements) +# 4. Checks whether the DB already has data — skips seeding if so +# 5. Dumps the remote database into ./seed/dump.sql using pg_dump +# 6. Strips Supabase-only extensions from the dump +# 7. Loads the dump into the running postgres container +# 8. Starts the API container +# +# --skip-dump Skip the pg_dump step (reuse an existing ./seed/dump.sql) +# --skip-seed Skip seeding entirely (just start containers) +# ============================================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +ENV_FILE="$PROJECT_ROOT/.env" +SEED_DIR="$PROJECT_ROOT/seed" +DUMP_FILE="$SEED_DIR/dump.sql" +SKIP_DUMP=false +SKIP_SEED=false + +# ---- Parse args ---- +for arg in "$@"; do + case $arg in + --skip-dump) SKIP_DUMP=true ;; + --skip-seed) SKIP_SEED=true ;; + *) echo "Unknown argument: $arg" && exit 1 ;; + esac +done + +# ---- Colour helpers ---- +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; RESET='\033[0m' +info() { echo -e "${GREEN}[setup]${RESET} $*"; } +warn() { echo -e "${YELLOW}[setup]${RESET} $*"; } +error() { echo -e "${RED}[setup]${RESET} $*" >&2; exit 1; } + +# ---- Sanity checks ---- +command -v docker >/dev/null 2>&1 || error "docker is not installed or not in PATH" + +# ---- Load .env ---- +if [[ ! -f "$ENV_FILE" ]]; then + error ".env file not found at $ENV_FILE. Copy .env.example and fill in your values." +fi + +info "Loading environment from $ENV_FILE" +set -o allexport +# shellcheck disable=SC1090 +source <(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$ENV_FILE" | sed 's/\r//') +set +o allexport + +# ---- Local postgres URL ---- +LOCAL_DATABASE_URL="postgresql://postgres:example@db:5432/coc?sslmode=disable" + + +# ======================================================== +# 1. Start the DB container +# ======================================================== +info "Starting postgres container..." +cd "$PROJECT_ROOT" +docker compose up db --build -d + + +# ======================================================== +# 2. Wait for postgres to be healthy +# ======================================================== +info "Waiting for postgres to be healthy..." +RETRIES=20 +until docker compose exec -T db pg_isready -U postgres -d coc -h 127.0.0.1 -p 5432 -q 2>/dev/null; do + RETRIES=$((RETRIES - 1)) + if [[ $RETRIES -le 0 ]]; then + error "Postgres did not become healthy in time. Check logs: docker compose logs db" + fi + sleep 2 +done +info "Postgres is healthy." + + +# ======================================================== +# 3. Install required extensions +# ======================================================== +info "Installing required extensions into postgres..." +docker compose exec -T db psql -U postgres -d coc <<'EXTSQL' +CREATE SCHEMA IF NOT EXISTS extensions; +CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public; +CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public; +CREATE EXTENSION IF NOT EXISTS pg_stat_statements WITH SCHEMA public; +EXTSQL +info "Extensions installed." + + +# ======================================================== +# 4. Check if DB already has data (skip seeding if so) +# ======================================================== +if [[ "$SKIP_SEED" == true ]]; then + warn "--skip-seed passed. Skipping dump and load." +else + TABLE_COUNT=$(docker compose exec -T db psql -U postgres -d coc -tAc \ + "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE';" 2>/dev/null || echo "0") + TABLE_COUNT="${TABLE_COUNT//[[:space:]]/}" + + if [[ "$TABLE_COUNT" -gt 0 ]]; then + warn "Database already has $TABLE_COUNT table(s) in public schema. Skipping seed." + warn "To re-seed, run: docker compose down -v then re-run this script." + else + info "Database is empty — proceeding with seed." + + + # ====================================================== + # 5. pg_dump remote database + # ====================================================== + command -v pg_dump >/dev/null 2>&1 || error "pg_dump is not installed. Install postgresql-client and retry." + + if [[ -n "${SESSION_POOLER:-}" ]]; then + REMOTE_URL="$SESSION_POOLER" + info "Using SESSION_POOLER for database dump" + else + error "Set SESSION_POOLER in $ENV_FILE" + fi + + mkdir -p "$SEED_DIR" + + if [[ "$SKIP_DUMP" == true ]]; then + if [[ ! -f "$DUMP_FILE" ]]; then + error "--skip-dump was passed but $DUMP_FILE does not exist. Run without --skip-dump first." + fi + warn "Skipping pg_dump — reusing existing $DUMP_FILE" + else + info "Dumping remote database to $DUMP_FILE ..." + info "(This may take a moment depending on database size)" + + PGDUMP_ERR_LOG="/tmp/pgdump_err_$$.log" + if pg_dump \ + --no-owner \ + --no-acl \ + --if-exists \ + --clean \ + "$REMOTE_URL" \ + > "$DUMP_FILE" 2>"$PGDUMP_ERR_LOG"; then + info "Dump complete: $DUMP_FILE ($(du -sh "$DUMP_FILE" | cut -f1))" + + # ---- Strip Supabase-only extensions ---- + info "Stripping Supabase-only extensions from dump..." + SUPABASE_EXTS="pg_graphql|pg_net|pgsodium|supabase_vault|wrappers|pg_stat_monitor|hypopg" + + sed -i -E \ + "s#^(CREATE EXTENSION IF NOT EXISTS ($SUPABASE_EXTS).*)#-- [local] \1#g" \ + "$DUMP_FILE" + sed -i -E \ + "s#^(DROP EXTENSION IF EXISTS ($SUPABASE_EXTS).*)#-- [local] \1#g" \ + "$DUMP_FILE" + + # Strip COMMENT ON EXTENSION for stripped extensions + sed -i -E \ + "s#^(COMMENT ON EXTENSION ($SUPABASE_EXTS) .*)#-- [local] \1#g" \ + "$DUMP_FILE" + info "Supabase-only extension cleanup done." + + # ---- Patch extension schema: 'extensions' -> 'public' ---- + info "Patching extension schema references (extensions -> public)..." + sed -i -E \ + "s#WITH SCHEMA extensions#WITH SCHEMA public#g" \ + "$DUMP_FILE" + info "Extension schema patch done." + + else + DUMP_ERR="$(cat "$PGDUMP_ERR_LOG")" + rm -f "$DUMP_FILE" "$PGDUMP_ERR_LOG" + warn "pg_dump failed:" + warn " $DUMP_ERR" + warn "" + if echo "$DUMP_ERR" | grep -qi "tenant or user not found"; then + warn "Tip: The session pooler requires username format: postgres.PROJECT_REF" + warn " e.g. postgres.riqqtbuoaycwwiemnmri (not just: postgres)" + warn " Update SESSION_POOLER in $ENV_FILE." + elif echo "$DUMP_ERR" | grep -qi "network is unreachable\|no route to host"; then + warn "Tip: The host appears unreachable (IPv6-only?). Try a VPN or an IPv4 URL." + fi + warn "" + warn "Options:" + warn " 1. Fix SESSION_POOLER in $ENV_FILE" + warn " 2. Manually copy a dump to $DUMP_FILE and re-run with: bash scripts/setup-local.sh --skip-dump" + error "Aborting: cannot seed without a database dump." + fi + fi # end skip-dump + + + # ====================================================== + # 6. Load dump into running postgres container + # ====================================================== + info "Loading dump into postgres..." + docker compose exec -T db psql -U postgres -d coc < "$DUMP_FILE" + info "Seed complete." + + fi # end table count check +fi # end skip-seed + + +# ======================================================== +# 7. Start the API container +# ======================================================== +info "Starting API container..." +DATABASE_URL="$LOCAL_DATABASE_URL" \ + docker compose up api --build -d + +info "Waiting for api to be healthy..." +RETRIES=15 +until curl -sf http://localhost:3000/health >/dev/null 2>&1; do + RETRIES=$((RETRIES - 1)) + if [[ $RETRIES -le 0 ]]; then + warn "API health check timed out — it may still be starting. Check: docker compose logs api" + break + fi + sleep 3 +done + + +echo "" +echo -e "${GREEN}======================================================${RESET}" +echo -e "${GREEN} Local development environment is up!${RESET}" +echo -e "${GREEN}======================================================${RESET}" +echo -e " API: http://localhost:3000" +echo -e " Postgres: localhost:5432 (user=postgres, db=coc)" +echo -e "" +echo -e " Useful commands:" +echo -e " docker compose logs -f # stream all logs" +echo -e " docker compose logs -f api # api logs only" +echo -e " docker compose down # stop containers (keep data)" +echo -e " docker compose down -v # stop and wipe local DB volume" +echo -e "${GREEN}======================================================${RESET}" From 0e71d9dd423f92b5388945c8eb03f04c42136a27 Mon Sep 17 00:00:00 2001 From: shrutiiiii Date: Sat, 28 Mar 2026 17:46:48 +0530 Subject: [PATCH 02/22] implemented rabbit's recommendations --- Dockerfile | 2 +- docker-compose.yml | 4 ++-- scripts/setup-local.sh | 16 ++++++++++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 51e8992..5c718ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -75,6 +75,6 @@ ENV PATH="/usr/local/bun/bin:$PATH" EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget -qO- http://localhost:3000/health || exit 1 + CMD curl -sf http://localhost:3000/health || exit 1 CMD ["sh", "-c", "bun src/server.ts"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 411e316..58bf178 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: ports: - "3000:3000" environment: - NODE_ENV: production + NODE_ENV: development PORT: 3000 DATABASE_URL: "postgresql://postgres:example@db:5432/coc?sslmode=disable" volumes: @@ -17,7 +17,7 @@ services: depends_on: - db healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"] + test: ["CMD", "curl", "-sf", "http://localhost:3000/health"] interval: 30s timeout: 3s retries: 3 diff --git a/scripts/setup-local.sh b/scripts/setup-local.sh index 16cc674..c40a16c 100755 --- a/scripts/setup-local.sh +++ b/scripts/setup-local.sh @@ -25,6 +25,12 @@ DUMP_FILE="$SEED_DIR/dump.sql" SKIP_DUMP=false SKIP_SEED=false +if [[ "$OSTYPE" == "darwin"* ]]; then + SED_INPLACE=(-i '') +else + SED_INPLACE=(-i) +fi + # ---- Parse args ---- for arg in "$@"; do case $arg in @@ -148,22 +154,24 @@ else info "Stripping Supabase-only extensions from dump..." SUPABASE_EXTS="pg_graphql|pg_net|pgsodium|supabase_vault|wrappers|pg_stat_monitor|hypopg" - sed -i -E \ + sed "${SED_INPLACE[@]}" -E \ "s#^(CREATE EXTENSION IF NOT EXISTS ($SUPABASE_EXTS).*)#-- [local] \1#g" \ "$DUMP_FILE" - sed -i -E \ + + sed "${SED_INPLACE[@]}" -E \ "s#^(DROP EXTENSION IF EXISTS ($SUPABASE_EXTS).*)#-- [local] \1#g" \ "$DUMP_FILE" + # Strip COMMENT ON EXTENSION for stripped extensions - sed -i -E \ + sed "${SED_INPLACE[@]}" -E \ "s#^(COMMENT ON EXTENSION ($SUPABASE_EXTS) .*)#-- [local] \1#g" \ "$DUMP_FILE" info "Supabase-only extension cleanup done." # ---- Patch extension schema: 'extensions' -> 'public' ---- info "Patching extension schema references (extensions -> public)..." - sed -i -E \ + sed "${SED_INPLACE[@]}" -E \ "s#WITH SCHEMA extensions#WITH SCHEMA public#g" \ "$DUMP_FILE" info "Extension schema patch done." From 56e3d95f50663a03cc95c0a292db6456bc1bdb90 Mon Sep 17 00:00:00 2001 From: shrutiiiii Date: Sun, 29 Mar 2026 03:43:10 +0530 Subject: [PATCH 03/22] Added static seed --- .gitignore | 1 - Dockerfile | 4 +- LOCAL_DEVELOPMENT.md | 20 +- docker-compose.yml | 7 +- scripts/setup-local.sh | 105 +---- seed/dump.sql | 863 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 885 insertions(+), 115 deletions(-) create mode 100644 seed/dump.sql diff --git a/.gitignore b/.gitignore index b5f1f39..11aecae 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid -seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover diff --git a/Dockerfile b/Dockerfile index 5c718ce..d6421f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,7 +37,7 @@ RUN bunx prisma generate # ----------------------------- -FROM deps AS development +FROM base AS development COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/src/generated ./src/generated @@ -54,7 +54,7 @@ CMD ["bun", "src/server.ts"] # ----------------------------- -FROM deps AS production +FROM base AS production RUN addgroup -g 1001 -S nodejs && adduser -S bunjs -u 1001 RUN cp -r /root/.bun /usr/local/bun && chown -R bunjs:nodejs /usr/local/bun diff --git a/LOCAL_DEVELOPMENT.md b/LOCAL_DEVELOPMENT.md index af78426..706af01 100644 --- a/LOCAL_DEVELOPMENT.md +++ b/LOCAL_DEVELOPMENT.md @@ -5,7 +5,7 @@ This document explains how to set up and manage the local development environmen ## Prerequisites - **Docker & Docker Compose**: Ensure you have Docker installed and running. -- **PostgreSQL Client (`pg_dump`)**: The setup script uses `pg_dump` to pull data from the remote database. +- **PostgreSQL Client (`pg_dump`)**: Optional. The current local setup prefers loading a local `seed/dump.sql` file. `pg_dump` is only required if you want to create a fresh dump from a remote database manually. ## Setup Instructions @@ -23,7 +23,7 @@ Key variables for the setup script: ### 2. Run the Setup Script -The `scripts/setup-local.sh` script automates the entire process: +The `scripts/setup-local.sh` script automates the local environment bring-up. By default it will load the local `seed/dump.sql` into the local Postgres instance. ```bash bun run local @@ -32,10 +32,12 @@ bun run local #### What the script does: 1. **Starts Postgres**: Launches the `db` container. 2. **Installs Extensions**: Pre-installs `pgcrypto`, `uuid-ossp`, and `pg_stat_statements` into the `public` schema. -3. **Dumps Remote DB**: Uses `pg_dump` to create a snapshot of the production database. -4. **Cleans the Dump**: Strips Supabase-specific extensions and patches schema references to work locally. -5. **Seeds the Database**: Loads the cleaned dump into your local Postgres container. -6. **Starts the API**: Launches the `api` container and waits for it to be healthy. +3. **Loads Local Seed**: If `public` has no tables, the script loads `seed/dump.sql` into the DB. This is the default and recommended local flow. +4. **Starts the API**: Launches the `api` container and waits for it to be healthy. + +Flags: + - `--skip-dump`: reuse existing `seed/dump.sql` (no remote dump step) + - `--skip-seed`: skip loading the seed and just start containers ### 3. Useful Commands @@ -48,11 +50,5 @@ bun run local ## Troubleshooting -### "Tenant or user not found" during dump -This usually means your `SESSION_POOLER` username is just `postgres`. Supabase requires the format `postgres.PROJECT_REF`. - ### Tables not in `public` schema The script automatically patches the dump to ensure tables are placed in the `public` schema. If you manually imported a dump, ensure you've installed the required extensions first. - -### Re-seeding -The script skips seeding if it detects existing tables in the `public` schema. To force a re-seed, run `docker compose down -v` before running the setup script. diff --git a/docker-compose.yml b/docker-compose.yml index 58bf178..7bd9726 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,11 @@ services: build: context: . dockerfile: Dockerfile + target: development ports: - - "3000:3000" + - "127.0.0.1:3000:3000" + env_file: + - .env environment: NODE_ENV: development PORT: 3000 @@ -31,7 +34,7 @@ services: volumes: - pgdata:/var/lib/postgresql/data ports: - - "5432:5432" + - "127.0.0.1:5432:5432" healthcheck: test: ["CMD", "pg_isready", "-U", "postgres", "-d", "coc", "-h", "127.0.0.1", "-p", "5432"] interval: 5s diff --git a/scripts/setup-local.sh b/scripts/setup-local.sh index c40a16c..87ebf79 100755 --- a/scripts/setup-local.sh +++ b/scripts/setup-local.sh @@ -15,6 +15,7 @@ # --skip-seed Skip seeding entirely (just start containers) # ============================================================================= +!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -101,112 +102,20 @@ info "Extensions installed." # ======================================================== -# 4. Check if DB already has data (skip seeding if so) +# 4. Check if seeding is required # ======================================================== if [[ "$SKIP_SEED" == true ]]; then warn "--skip-seed passed. Skipping dump and load." else - TABLE_COUNT=$(docker compose exec -T db psql -U postgres -d coc -tAc \ - "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE';" 2>/dev/null || echo "0") - TABLE_COUNT="${TABLE_COUNT//[[:space:]]/}" - - if [[ "$TABLE_COUNT" -gt 0 ]]; then - warn "Database already has $TABLE_COUNT table(s) in public schema. Skipping seed." - warn "To re-seed, run: docker compose down -v then re-run this script." - else - info "Database is empty — proceeding with seed." - - - # ====================================================== - # 5. pg_dump remote database - # ====================================================== - command -v pg_dump >/dev/null 2>&1 || error "pg_dump is not installed. Install postgresql-client and retry." - - if [[ -n "${SESSION_POOLER:-}" ]]; then - REMOTE_URL="$SESSION_POOLER" - info "Using SESSION_POOLER for database dump" - else - error "Set SESSION_POOLER in $ENV_FILE" - fi - mkdir -p "$SEED_DIR" + if [[ ! -f "$DUMP_FILE" ]]; then + error "$DUMP_FILE not found. Create the seed SQL at $DUMP_FILE and re-run this script." + fi - if [[ "$SKIP_DUMP" == true ]]; then - if [[ ! -f "$DUMP_FILE" ]]; then - error "--skip-dump was passed but $DUMP_FILE does not exist. Run without --skip-dump first." - fi - warn "Skipping pg_dump — reusing existing $DUMP_FILE" - else - info "Dumping remote database to $DUMP_FILE ..." - info "(This may take a moment depending on database size)" - - PGDUMP_ERR_LOG="/tmp/pgdump_err_$$.log" - if pg_dump \ - --no-owner \ - --no-acl \ - --if-exists \ - --clean \ - "$REMOTE_URL" \ - > "$DUMP_FILE" 2>"$PGDUMP_ERR_LOG"; then - info "Dump complete: $DUMP_FILE ($(du -sh "$DUMP_FILE" | cut -f1))" - - # ---- Strip Supabase-only extensions ---- - info "Stripping Supabase-only extensions from dump..." - SUPABASE_EXTS="pg_graphql|pg_net|pgsodium|supabase_vault|wrappers|pg_stat_monitor|hypopg" - - sed "${SED_INPLACE[@]}" -E \ - "s#^(CREATE EXTENSION IF NOT EXISTS ($SUPABASE_EXTS).*)#-- [local] \1#g" \ - "$DUMP_FILE" - - sed "${SED_INPLACE[@]}" -E \ - "s#^(DROP EXTENSION IF EXISTS ($SUPABASE_EXTS).*)#-- [local] \1#g" \ - "$DUMP_FILE" - - - # Strip COMMENT ON EXTENSION for stripped extensions - sed "${SED_INPLACE[@]}" -E \ - "s#^(COMMENT ON EXTENSION ($SUPABASE_EXTS) .*)#-- [local] \1#g" \ - "$DUMP_FILE" - info "Supabase-only extension cleanup done." - - # ---- Patch extension schema: 'extensions' -> 'public' ---- - info "Patching extension schema references (extensions -> public)..." - sed "${SED_INPLACE[@]}" -E \ - "s#WITH SCHEMA extensions#WITH SCHEMA public#g" \ - "$DUMP_FILE" - info "Extension schema patch done." - - else - DUMP_ERR="$(cat "$PGDUMP_ERR_LOG")" - rm -f "$DUMP_FILE" "$PGDUMP_ERR_LOG" - warn "pg_dump failed:" - warn " $DUMP_ERR" - warn "" - if echo "$DUMP_ERR" | grep -qi "tenant or user not found"; then - warn "Tip: The session pooler requires username format: postgres.PROJECT_REF" - warn " e.g. postgres.riqqtbuoaycwwiemnmri (not just: postgres)" - warn " Update SESSION_POOLER in $ENV_FILE." - elif echo "$DUMP_ERR" | grep -qi "network is unreachable\|no route to host"; then - warn "Tip: The host appears unreachable (IPv6-only?). Try a VPN or an IPv4 URL." - fi - warn "" - warn "Options:" - warn " 1. Fix SESSION_POOLER in $ENV_FILE" - warn " 2. Manually copy a dump to $DUMP_FILE and re-run with: bash scripts/setup-local.sh --skip-dump" - error "Aborting: cannot seed without a database dump." - fi - fi # end skip-dump - - - # ====================================================== - # 6. Load dump into running postgres container - # ====================================================== - info "Loading dump into postgres..." + info "Loading local seed SQL from $DUMP_FILE ..." docker compose exec -T db psql -U postgres -d coc < "$DUMP_FILE" info "Seed complete." - - fi # end table count check -fi # end skip-seed +fi # ======================================================== diff --git a/seed/dump.sql b/seed/dump.sql new file mode 100644 index 0000000..1e21c39 --- /dev/null +++ b/seed/dump.sql @@ -0,0 +1,863 @@ +-- +-- PostgreSQL database dump +-- + + +-- Dumped from database version 17.4 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- Supabase internal commands removed to ensure compatibility with local seeding + +-- Simplified seed SQL for local development +-- Creates core tables and inserts up to 30 members and some relations. + +CREATE SCHEMA IF NOT EXISTS realtime; +CREATE SCHEMA IF NOT EXISTS storage; +CREATE SCHEMA IF NOT EXISTS extensions; + +BEGIN; + +-- Drop existing tables and types (simple ordering) +DROP TABLE IF EXISTS public."_prisma_migrations" CASCADE; +DROP TABLE IF EXISTS public."MemberProject" CASCADE; +DROP TABLE IF EXISTS public."MemberAchievement" CASCADE; +DROP TABLE IF EXISTS public."CompletedQuestion" CASCADE; +DROP TABLE IF EXISTS public."InterviewExperience" CASCADE; +DROP TABLE IF EXISTS public."Question" CASCADE; +DROP TABLE IF EXISTS public."Topic" CASCADE; +DROP TABLE IF EXISTS public."Project" CASCADE; +DROP TABLE IF EXISTS public."Achievement" CASCADE; +DROP TABLE IF EXISTS public."Account" CASCADE; +DROP TABLE IF EXISTS public."Member" CASCADE; +DROP TABLE IF EXISTS realtime.schema_migrations CASCADE; +DROP TABLE IF EXISTS auth.users CASCADE; + +DROP TYPE IF EXISTS public."Difficulty" CASCADE; +DROP TYPE IF EXISTS public."Verdict" CASCADE; + +-- Enum types +CREATE TYPE public."Difficulty" AS ENUM ( + 'Easy', + 'Medium', + 'Hard' +); + +CREATE TYPE public."Verdict" AS ENUM ( + 'Selected', + 'Rejected', + 'Pending' +); + +CREATE TABLE IF NOT EXISTS realtime.schema_migrations ( + version TEXT, + inserted_at TIMESTAMP WITH TIME ZONE +); + + +-- Member table +CREATE TABLE public."Member" ( + id UUID, + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + birth_date DATE, + phone TEXT, + bio TEXT, + "profilePhoto" TEXT, + github TEXT, + linkedin TEXT, + twitter TEXT, + geeksforgeeks TEXT, + leetcode TEXT, + codechef TEXT, + codeforces TEXT, + "passoutYear" DATE, + "isApproved" BOOLEAN NOT NULL DEFAULT false, + "isManager" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "approvedById" UUID +); + +-- Account +CREATE TABLE public."Account" ( + id UUID, + provider TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + password TEXT, + "accessToken" TEXT, + "refreshToken" TEXT, + "expiresAt" TIMESTAMP WITH TIME ZONE, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "memberId" UUID +); + +-- Achievement +CREATE TABLE public."Achievement" ( + id SERIAL, + title TEXT NOT NULL, + description TEXT NOT NULL, + "achievedAt" DATE NOT NULL, + "imageUrl" TEXT NOT NULL, + "createdById" UUID, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "updatedById" UUID, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- MemberAchievement (join) +CREATE TABLE public."MemberAchievement" ( + "memberId" UUID, + "achievementId" INTEGER +); + +-- Project +CREATE TABLE public."Project" ( + id SERIAL, + name TEXT NOT NULL, + "imageUrl" TEXT NOT NULL, + "githubUrl" TEXT NOT NULL, + "deployUrl" TEXT, + "createdById" UUID, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "updatedById" UUID, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- MemberProject (join) +CREATE TABLE public."MemberProject" ( + "memberId" UUID, + "projectId" INTEGER +); + +-- Topic +CREATE TABLE public."Topic" ( + id SERIAL, + title TEXT NOT NULL, + description TEXT NOT NULL, + "createdById" UUID, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "updatedById" UUID, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- Question +CREATE TABLE public."Question" ( + id SERIAL, + "questionName" TEXT NOT NULL, + difficulty public."Difficulty" NOT NULL, + link TEXT NOT NULL, + "topicId" INTEGER, + "createdById" UUID, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "updatedById" UUID, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- InterviewExperience +CREATE TABLE public."InterviewExperience" ( + id SERIAL, + company TEXT NOT NULL, + role TEXT NOT NULL, + verdict public."Verdict" NOT NULL, + content TEXT NOT NULL, + "isAnonymous" BOOLEAN NOT NULL DEFAULT false, + "memberId" UUID +); + +-- CompletedQuestion (join) +CREATE TABLE public."CompletedQuestion" ( + "memberId" UUID, + "questionId" INTEGER +); + +-- _prisma_migrations +CREATE TABLE public."_prisma_migrations" ( + id TEXT, + checksum TEXT NOT NULL, + finished_at TIMESTAMP WITH TIME ZONE, + migration_name TEXT NOT NULL, + logs TEXT, + rolled_back_at TIMESTAMP WITH TIME ZONE, + started_at TIMESTAMP WITH TIME ZONE DEFAULT now(), + applied_steps_count INTEGER DEFAULT 0 +); + +-- (Enum types moved to top of file, before CREATE TABLE statements) + +INSERT INTO public."Account" (id, provider, "providerAccountId", password, "accessToken", "refreshToken", "expiresAt", "createdAt", "updatedAt", "memberId") VALUES +('e882157d-fc39-49e0-9df9-99ca0adb8a7c', 'vedant@gmail.com', '$2b$10$eH5Xpk0e88tWULMgIMJdqeAPE3ND9qych/bfGeh7MM3tFbXX90sF.', NULL, NULL, NULL, NULL, '2025-07-27 12:36:07.398', '2025-07-27 12:36:07.398', '738b73b4-0725-4f7c-95bf-cbdb72eb4e84'), +('0b121686-c462-4fae-8324-b88316013243', 'bhaven@gmail.com', '$2b$10$irh3xCai75iDsLKfp8TJPeEMA2pfFHpIqIqkCAezNjvs25HV13vSy', NULL, NULL, NULL, NULL, '2025-07-27 12:42:51.984', '2025-07-27 12:42:51.984', '86237b63-1339-49d6-ad57-1d4b93bd5092'), +('8b8d3535-1592-4673-ab98-5c41e7899932', 'shaheen@gmail.com', '$2b$10$psuckT2vhmGq3ngd9L5PMOg/ACk/qg7wH.060RqBhs9YcP0j23w4S', NULL, NULL, NULL, NULL, '2025-07-27 12:49:37.408', '2025-07-27 12:49:37.408', '3e6666f5-8ad9-4686-8656-a6904360d4ba'), +('0d95b051-0a15-4811-84ed-e0b67a57dcbb', 'sanskar@gmail.com', '$2b$10$ajN/.3Jdvb78aGo/HKY9suTqrwozl4VFbrpz9BTFRarVnKBkb3QBO', NULL, NULL, NULL, NULL, '2025-07-27 12:53:26.146', '2025-07-27 12:53:26.146', '855fb554-02f7-4b6a-a17e-d55b7976babf'), +('36fc123d-0f51-4373-aa9c-a2db94cbaf6c', 'eshwar@gmail.com', '$2b$10$boqMJ//X86GNTKG4uG4yV.ibMMi1U43iz2MwYiwUgPwYcBCDPNjf6', NULL, NULL, NULL, NULL, '2025-07-27 13:00:38.612', '2025-07-27 13:00:38.612', '03a1a0f8-c1b9-4ac1-99d9-f10f26a82f7c'), +('a895aacf-aabe-4d0d-9d42-5a463061d45c', 'yash@gmail.com', '$2b$10$zonROajObqXYsiDX1UuYou6Pms.qvS4hxiR4Ehm5bTHT6MT.n8xmW', NULL, NULL, NULL, NULL, '2025-07-27 13:03:03.733', '2025-07-27 13:03:03.733', '0cc29a18-180c-408d-9cbe-0fc06109a5c1'), +('0ece4d7c-6a17-4d5f-8e57-2dddb8f1cc12', 'prathamesh@gmail.com', '$2b$10$KaF.GZydyvWUTb.Pk85APuVCh66sZ/uY3/3heTYUAhnQdoHThKVpG', NULL, NULL, NULL, NULL, '2025-07-27 13:04:35.928', '2025-07-27 13:04:35.928', 'bf152df5-3c23-4c1c-85aa-212e0487b420'), +('89c4d268-131f-439d-9ebd-02082af93c2f', 'pratik@gmail.com', '$2b$10$0PDGCKCKSuC5d0aJ4SyoReJjrMuRjUdNPRjSUVxJcYRACcKWu0TT6', NULL, NULL, NULL, NULL, '2025-07-27 13:05:30.243', '2025-07-27 13:05:30.243', 'ac713749-77c1-46b6-ae82-f16d616b1c7c'), +('94023539-b795-4504-85de-f4dd89362884', 'swaraj@gmail.com', '$2b$10$pH/8h6.Ic.jFy0vusM/au.1VCe.Sl37GBtrAR99eNxl7GxPS7Asxa', NULL, NULL, NULL, NULL, '2025-07-27 13:11:28.79', '2025-07-27 13:11:28.79', '1404e81c-d567-4103-941b-0abeea7fc049'), +('b95dc5ec-e7a2-4689-b0c3-2247db3b3c23', 'vansh@gmail.com', '$2b$10$eq6f05tX/ZMwWm2gkxNJnOhfr3m.zYW23eY4TTiIVDvAOE03RZS.6', NULL, NULL, NULL, NULL, '2025-07-27 13:12:10.092', '2025-07-27 13:12:10.092', '22ba7f7a-14e7-45fa-bf5f-51d5f015496f'), +('0a667c9f-e611-4d58-bd2d-1a65860fcc97', 'shivaji@gmail.com', '$2b$10$LHj6GmVwPH0.YHU3wSVLf.PK2cQ23swfr6MoI3hFxp4tInDDLnwu2', NULL, NULL, NULL, NULL, '2025-07-27 13:13:07.284', '2025-07-27 13:13:07.284', '8eeacf82-18e5-48f8-a11e-fdbbe2eb81ce'), +('edd6063c-a60f-4653-8512-f21973ab5879', 'sanica@gmail.com', '$2b$10$QlXDLQeKvVeRyA9r2.MC8umgGR.GF343BUrFtX2KSg8gUo92bFGZW', NULL, NULL, NULL, NULL, '2025-07-27 13:20:15.287', '2025-07-27 13:20:15.287', '644b5e8f-910d-450e-a855-a88f31d02b7b'), +('7e78693b-d788-4e17-aa87-94e631cee02e', 'aditya@gmail.com', '$2b$10$W4aJemPA5H/Ws1rUuC.Kge8/fhLXJE2eTTKh6x.wksQ4Z35waZmkW', NULL, NULL, NULL, NULL, '2025-07-27 13:21:05.854', '2025-07-27 13:21:05.854', '1ff4b36d-5671-4855-8476-d0a8993f9873'), +('a524c15b-3a24-484d-947d-b440aa5fa4f3', 'sarvesh@gmail.com', '$2b$10$6F4VAW1PAOwRYvXbM14Cd.XMvI61neTdp.54qXVze/r.GwF/bWseS', NULL, NULL, NULL, NULL, '2025-07-27 13:29:39.074', '2025-07-27 13:29:39.074', 'ad525dce-67dd-4878-ab95-068943923b81'), +('d754025d-f04a-489f-9cd3-a863e3a2083b', 'Mukul@gmail.com', '$2b$10$gsbswEwuP3LEY6mUg3WHUu3qfeDe7S2KLHvv.ibMTEIlwNqLJPl9u', NULL, NULL, NULL, NULL, '2025-07-27 13:36:08.586', '2025-07-27 13:36:08.586', '1b482f80-f649-45f9-a90b-7538a7a6e66e'), +('1fc11447-d4b2-44ac-9bc8-76f841f11d15', 'anushka@gmail.com', '$2b$10$lAn7dozXDFFKzmD2SovmF.MXRKChjy7EqzV/REdPokjNvbfBoJdHa', NULL, NULL, NULL, NULL, '2025-07-27 13:37:18.144', '2025-07-27 13:37:18.144', 'b6f44922-fff3-48eb-a0c9-15d41e786e38'), +('c577b0c8-ad80-46b9-bb57-03a39f740157', 'samarth@gmail.com', '$2b$10$NbmkATGYbJckgOs.rIkVIetZXCAv0jdyNbo075Kp.ybgDR2MySHIq', NULL, NULL, NULL, NULL, '2025-07-27 13:41:04.035', '2025-07-27 13:41:04.035', '1b5933a9-5d50-4246-861a-ca0d30bd581f'), +('102d22ea-192b-400f-bdce-dac29abeb49b', 'vaishnavi@gmail.com', '$2b$10$wTra4lS7IrfMWPJiMc/V9u8YDZJg5.2mIWmD8ZNyvbt07JPmako/S', NULL, NULL, NULL, NULL, '2025-07-27 13:42:05.112', '2025-07-27 13:42:05.112', 'd46a667d-5b68-4b82-9de7-fcfdf0ab0181'), +('1f24974e-5ffe-4655-94e8-282f3266bb7d', 'vaishnaviadhav@gmail.com', '$2b$10$CvBVhnqFPq3s5f6q2VYCuOVZtuZloaBduLuloZwERvS3CGJOo3nnG', NULL, NULL, NULL, NULL, '2025-07-27 13:43:26.945', '2025-07-27 13:43:26.945', '7dd07cc1-08da-48cc-a162-f546356fe291'), +('78e6f26b-dd53-440e-96c1-f4d2205fab87', 'sakshi@gmail.com', '$2b$10$TdCn5HveTLvSoIdFMR1n/eJGKLofoFXx5lQCsEEQU0GnyZrdp9qkC', NULL, NULL, NULL, NULL, '2025-07-27 13:44:46.419', '2025-07-27 13:44:46.419', '259a1e70-c093-43d4-8aa1-bba058a896b8'), +('c0c097c3-c2dc-4ded-8a82-2e718eb46eff', 'piyushaa@gmail.com', '$2b$10$GkiaYm.5cG73HgDAANN6xedU/zqzvmz1JVsoC1C6/jmCXM67shOSG', NULL, NULL, NULL, NULL, '2025-07-27 13:46:17.354', '2025-07-27 13:46:17.354', '46cfa3aa-1efe-4cc6-a624-340808ef7cb8'), +('bea77f0d-37f6-4303-b549-93846e36d774', 'siddhesh@gmail.com', '$2b$10$TcfZ9HVPsTNARAk31CwbxeAIn7gADgfQ1E2cF/8AgeCh7dRSY/xfi', NULL, NULL, NULL, NULL, '2025-07-27 13:47:45.957', '2025-07-27 13:47:45.957', 'a8443783-dd59-446a-93f5-19f5e590e88b'), +('e43283f2-13d9-4a1b-a505-deb2d4f8b967', 'aarya@gmail.com', '$2b$10$C3yPZHUkWOpgXDNFNYuRf.OSSz.RhpQI03T1IwdgiH/bqyl6rvCw2', NULL, NULL, NULL, NULL, '2025-07-27 13:56:12.936', '2025-07-27 13:56:12.936', 'db2bd9ec-25e5-4134-ae53-fea1734ca161'), +('07c9e6e0-7f65-4494-9764-c7d1c258fd75', 'shashwati@gmail.com', '$2b$10$Xb45AKKbCU8ma95Me6Yc7u5nmmX.OGnkShA4CKgInaw9On6XhFLEy', NULL, NULL, NULL, NULL, '2025-07-27 13:57:15.237', '2025-07-27 13:57:15.237', 'c5b2470d-7fb4-4c93-bfe2-fedc00415dc2'), +('2cf94aa7-3790-4758-b83d-66e762b2505d', 'suhani@gmail.com', '$2b$10$a8QFdX9ws02jnsUz4O1DdehSDJjhRvX96fuUxJIjZHoFlhfzMRkYq', NULL, NULL, NULL, NULL, '2025-07-27 13:58:46.809', '2025-07-27 13:58:46.809', '329d6d7a-9787-452e-9c0c-506481c5462a'), +('f48da39b-98fd-481a-bbd6-68c10be660d0', 'sarveshshiralkar@gmail.com', '$2b$10$7V9FuR7rpABeRvbMuU4bUeoMQKQXrehykMmvzrXarIsLuJKNa1tl.', NULL, NULL, NULL, NULL, '2025-07-27 14:02:16.734', '2025-07-27 14:02:16.734', 'd7c96d3c-d45d-4bde-8c2b-39f0451a389f'), +('2074b920-4329-4e6f-9798-07b372a6679c', 'sahillakare@gmail.com', '$2b$10$XJ.r8SaroRak1GUyVIoXj.oBCYMW1regaZVHBlP1lWLc50WyYSovW', NULL, NULL, NULL, NULL, '2025-07-27 14:07:23.722', '2025-07-27 14:07:23.722', '516af252-e8dc-48a4-80c4-5af1e0758e58'), +('7e4be315-6cd3-410f-8083-fe49f9c2305c', 'sachin@gmail.com', '$2b$10$4EiXQeSIWLAsaY.4ThRyR.Dmpt3Yo5ezPrV9re7wcES8KG7QHTmB2', NULL, NULL, NULL, NULL, '2025-07-27 14:28:15.082', '2025-07-27 14:28:15.082', '6c968bfa-ebf8-4b2b-a349-36bbc9cc2870'), +('3891d5cf-1c51-4245-9a78-81b28ce13266', 'sherin@gmail.com', '$2b$10$NvZJXjKUK2IOMzC3raJvi.9bIYUgEWH69ST9pHbbcrJyMmAJm4HIS', NULL, NULL, NULL, NULL, '2025-07-27 16:57:39.729', '2025-07-27 16:57:39.729', 'd7d54e46-8db2-449c-87f9-8e89e8537c42'), +('101d1b9b-743e-4930-830e-9a33c0429199', 'shruti@gmail.com', '$2b$10$ZuuiwH/L3Aal9jBCDO9qsuZLd6lRZz6rPCMQWIr2bAnM33oMUfsVa', NULL, NULL, NULL, NULL, '2025-07-27 17:18:51.245', '2025-07-27 17:18:51.245', 'c494d747-5123-457f-b9cf-f3359f5a0fe8'), +('f121b746-6942-4013-a11b-178571ed988e', 'shivam@gmail.com', '$2b$10$L5c9yXrvfgLjEQ4y7yLSseLhafRXbYPiOkoWt//rwj1h6f80PdnnC', NULL, NULL, NULL, NULL, '2025-07-27 17:32:16.949', '2025-07-27 17:32:16.949', '75ef229a-3770-46aa-adc8-f4d250c6ac81'), +('ab673cb2-cbb2-4129-a2af-1d911cd981d1', 'veda@gmail.com', '$2b$10$oHbUZdsrcq6tk9lUyKsFeuCLDEzpbdrPzN9/nL8BcUSFB3FbTJvPe', NULL, NULL, NULL, NULL, '2025-07-27 18:06:53.921', '2025-07-27 18:06:53.921', '64464cc4-4dfc-4522-a256-6aca2371df7f'), +('3195836d-c300-4406-bb40-7e6e665ac9a9', 'sheryash@gmail.com', '$2b$10$wdVUVGhmuykY.tdcEYNHU.8jjskz/U0JXw/XHh8OOgir4c2qux/3q', NULL, NULL, NULL, NULL, '2025-07-27 18:28:45.633', '2025-07-27 18:28:45.633', '046d352f-10d9-49b5-bbed-31d67bf4b583'), +('16423861-0e69-4a67-84f0-383dbfca9bd8', 'prajakta@gmail.com', '$2b$10$JXImJtLR3SblUWGxAMF.6.VHetvNMKcoNOMF4BAkAKDsXHK/TU0rC', NULL, NULL, NULL, NULL, '2025-07-27 18:32:27.558', '2025-07-27 18:32:27.558', '0b83d3e3-8685-4cfe-9f63-6cc22c1ceae4'), +('5bf224bd-cd63-41d7-950d-7c66725dd7a6', 'harsh@gmail.com', '$2b$10$ND15qcpzkY3t7lg5gZx4CuPs5XWP3OsT7.9x2/ODf7tGo6AvZuMDm', NULL, NULL, NULL, NULL, '2025-07-27 18:37:17.457', '2025-07-27 18:37:17.457', 'cffc47fb-1147-4dd5-9818-21d209dbe3f3'), +('e2e31f9d-1321-4c7e-98f6-fdb004cd0f27', 'Abhiram@gmail.com', '$2b$10$gOz6M.O48PwsBmvv4AsdLuHkArwIKNSspySKaAmEdkBy5by.s1Clm', NULL, NULL, NULL, NULL, '2025-07-27 18:41:32.941', '2025-07-27 18:41:32.941', 'e68ca856-978b-4bb5-a2f1-6497278624bb'), +('55badea7-0c98-4d18-a2a6-f54d15a12afc', 'aryan@gmail.com', '$2b$10$atDdFN1RzGh0ixzo8AAqjuLoIScI1DFhc1/y620fDg9.iKBL/K0AS', NULL, NULL, NULL, NULL, '2025-07-27 18:47:08.087', '2025-07-27 18:47:08.087', '48724979-f9c8-46de-b9ab-9ca0186596d0'), +('4e428487-f19d-476f-ab27-10e9577e98fd', 'shubham@gmail.com', '$2b$10$4pmoJTrSIDrfYS4t0sqql..MZXu0K5f1FA1hSJ7cf4VGsBkYDWqxi', NULL, NULL, NULL, NULL, '2025-07-27 18:49:15.105', '2025-07-27 18:49:15.105', '69394246-3e41-4eb5-812a-48801b0b5f3e'), +('5aca50b8-f251-4632-834d-3f4e92ef6c9c', 'komal@gmail.com', '$2b$10$hf21ih62PzTbX6ba4VaZAeBLrBLwwdbkXsvDfT5swq0CPd/EsIP.a', NULL, NULL, NULL, NULL, '2025-07-27 19:29:30.114', '2025-07-27 19:29:30.114', 'ef59db8b-2ad5-4e0a-b741-f58521bf61ec'), +('ab59bbd5-d03f-41fe-8267-5697d2d7774a', 'sahil@gmail.com', '$2b$10$lMayDbHFuV3p.xSULly1zOoLhWOlgPaQoVDykm3TG12RcXTSoZfua', NULL, NULL, NULL, NULL, '2025-07-27 19:42:25.794', '2025-07-27 19:42:25.794', 'f032f524-c153-496f-9eea-e2ff8622f3d1'), +('ceb3abbe-ec03-4531-9434-3265e5d1f141', 'dillip@gmail.com', '$2b$10$3o.97fG5vAcS3WJAmy9MbOMyU9yUDXkZMOHtMhQ4vbV5P757F5Z2G', NULL, NULL, NULL, NULL, '2025-07-27 19:47:56.176', '2025-07-27 19:47:56.176', '20ee0910-36b3-48d6-ad96-2112d02fd9b6'), +('229a9b54-94ec-4164-87a8-abe852079016', 'harish@gmail.com', '$2b$10$C2V0fELssTODLmt6AjO53eLTqT51C8ga.JyT4bTgFrajpB.37OvT.', '$2b$10$C2V0fELssTODLmt6AjO53eLTqT51C8ga.JyT4bTgFrajpB.37OvT.', NULL, NULL, NULL, '2025-07-27 20:43:23.073', '2025-07-27 20:43:23.073', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('50317f35-cd52-4a05-8641-52abf9736c2a', 'credentials', 'yourmom@gmail.com', '$2b$08$M63.zOUte/5o2DLUAMxgJOK/VOyVy2CQF61XucPcuTZWd80hZBLG.', NULL, NULL, NULL, '2025-10-27 20:12:59.287', '2025-10-27 20:12:59.287', 'a6bc0b3a-71bf-4e0d-8879-ddedbbc0a766'), +('bc3b8b6c-b292-46b6-96bf-48168d6a7c21', 'credentials', 'hello123@gmail.com', '$2b$08$tY.I//asON4Xxci0ANDuLeEVzysjnPDoBynffnYrsVIfUbzksWXNS', NULL, NULL, NULL, '2025-11-20 09:01:14.333', '2025-11-20 09:01:14.333', '92f0e65d-f306-4cdd-baad-059f645cf148'), +('d696a095-96fc-44af-8daa-f9afb01049ba', 'credentials', 'syswraith@gmail.com', '$2b$08$tFFZNuza5BopfhggwSR7zuedbc9O9egCZ/NGwEXLAEr.iEi/nIMAK', NULL, NULL, NULL, '2025-10-27 14:57:14.684', '2025-10-27 14:57:14.684', '207bb8bd-3e48-40c8-83ce-a825cb9fe474'); + + +-- +-- Data for Name: Achievement; Type: TABLE DATA; Schema: public; Owner: - +-- + +INSERT INTO public."Achievement" (id, title, "achievedAt", "imageUrl", "createdAt", "createdById", description, "updatedAt", "updatedById") VALUES +('14', 'Winner At BMCC''s Troika 2025 Coding Event', '2025-02-04 00:00:00', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/achievements/2dd9eb5c-66c6-4c8d-b978-0e671a243c76.jpeg', '2025-07-27 19:39:03.77', 'd7d54e46-8db2-449c-87f9-8e89e8537c42', 'On 4th February 2025, the team secured the winner position at BMCC''s Troika 2025 Coding Event, demonstrating outstanding innovation and technical excellence.', '2025-07-27 19:39:03.77', NULL), +('3', 'Runner-Up At Jigyasa Coding Competition', '2024-02-10 00:00:00', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/achievements/67235c1c-6573-40af-994c-f38138d0e175.jpeg', '2025-07-27 17:27:07.721', 'd7d54e46-8db2-449c-87f9-8e89e8537c42', 'On 10th February 2024, Vansh Waldeo secured the runner-up position in a prestigious coding competition held at IMCC, Pune.', '2025-07-27 18:10:15.82', 'd7d54e46-8db2-449c-87f9-8e89e8537c42'), +('4', 'Runner Up BITS PILANI POSTMAN API Hackathon 3.0', '2023-01-08 00:00:00', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/achievements/97d19654-7002-4f76-ba6c-e38bfa958d8e.png', '2025-07-27 18:36:06.816', '1b5933a9-5d50-4246-861a-ca0d30bd581f', 'Secured Runner-Up position in the BITS Pilani Postman API Hackathon 3.0, an online event centered around solving real-world problems through effective API design and collaboration. Showcased strong skills in building, testing, and integrating APIs using Postman.', '2025-07-27 18:36:06.816', NULL), +('5', 'Runner Up IXPLORER WEB DESIGN & DEVELOPMENT HACKATHON', '2023-11-27 00:00:00', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/achievements/1bcc40eb-3916-466a-baf4-7e4ed69ca6e8.jpeg', '2025-07-27 18:49:57.866', '1b5933a9-5d50-4246-861a-ca0d30bd581f', 'Secured Runner-Up position in the IXplorer Web Design & Development Hackathon, an online hackathon organized by IIT Patna. Built a creative and functional web application, showcasing strong skills in full-stack development and user-centered design.', '2025-07-27 18:49:57.866', NULL), +('6', 'Runner Up At Zeal Institute''s Web Development Project Competition', '2025-04-12 00:00:00', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/achievements/57de0ddc-322e-4c1f-a91b-b0c4a1acc8db.jpeg', '2025-07-27 18:51:38.653', 'd7d54e46-8db2-449c-87f9-8e89e8537c42', 'On 12th April 2025, the team secured the runner-up position at Zeal Institute''s Web Development Project Competition, showcasing innovation and strong technical skills.', '2025-07-27 18:51:38.653', NULL), +('9', 'Winner At MKSSS Cummins College of Engineering CodeBid Event', '2025-04-05 00:00:00', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/achievements/f39c95ca-ca7e-4138-9ac4-0d9cdfdb0cb5.jpeg', '2025-07-27 19:12:45.321', 'd7d54e46-8db2-449c-87f9-8e89e8537c42', 'On 5th April 2025, the team secured the winner position at MKSSS Cummins College of Engineering''s CodeBid event, demonstrating outstanding innovation and technical excellence.', '2025-07-27 20:02:49.514', 'd7d54e46-8db2-449c-87f9-8e89e8537c42'), +('11', 'Runner Up Smart India Hackathon 2023', '2023-12-26 00:00:00', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/achievements/26bce2c2-9603-4b99-8868-abfeb15ed07f.jpeg', '2025-07-27 19:24:19.865', '1b5933a9-5d50-4246-861a-ca0d30bd581f', 'Crowned Winner at Smart India Hackathon 2023 for developing an AI-powered women safety solution. Our model could detect distress situations in real time and send instant alerts, showcasing innovation, social impact, and strong technical execution.', '2025-07-27 19:24:19.865', NULL), +('12', 'Winner At Hunar Intern Web Development Hackathon', '2024-08-26 00:00:00', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/achievements/d982a872-e4c9-4b1d-8af9-91cbc0e2f2d1.jpeg', '2025-07-27 19:26:23.925', 'd7d54e46-8db2-449c-87f9-8e89e8537c42', 'On 26th August 2024, the team secured the winner position at Hunar Intern''s Web Development Hackathon, demonstrating outstanding innovation and technical excellence.', '2025-07-27 19:42:06.922', 'd7d54e46-8db2-449c-87f9-8e89e8537c42'), +('15', 'Consecutive COEP MindSpark Finalists 2023 & 2024 ', '2024-09-23 00:00:00', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/achievements/03379229-271c-4e7a-b09e-9dbbaedc894c.jpeg', '2025-07-27 19:50:25.906', '1b5933a9-5d50-4246-861a-ca0d30bd581f', 'Achieved finalist positions two years in a row at COEP MindSpark, Pune’s premier techfest, competing among hundreds of participants. Selected in the top 10 for WebScape (2023) and Retracer (2024), showcasing standout skills in web development, problem-solving, and technical creativity.', '2025-07-27 19:50:25.906', NULL), +('7', 'Winner At Zeal Institute''s Web Development Project Competition', '2025-04-12 00:00:00', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/achievements/04c4131b-56a7-46d1-a6e0-797e095a33b6.jpeg', '2025-07-27 18:58:46.02', 'd7d54e46-8db2-449c-87f9-8e89e8537c42', 'On 12th April 2025, the team secured the winner position at Zeal Institute''s Web Development Project Competition, demonstrating outstanding innovation and technical excellence.', '2025-07-27 18:58:46.02', NULL), +('10', 'Finalist Meher Baba Drone Competition', '2022-02-13 00:00:00', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/achievements/ac141a2e-07f5-42f2-a5a2-6195d22f15a7.jpeg', '2025-07-27 19:16:06.848', '1b5933a9-5d50-4246-861a-ca0d30bd581f', 'Selected as a Finalist in the Meher Baba Drone Competition, showcasing innovative solutions in drone technology and aerial system design. Recognized for technical creativity, problem-solving, and practical implementation in a competitive national setting.', '2025-07-29 11:57:51.485', 'd7d54e46-8db2-449c-87f9-8e89e8537c42'), +('13', 'Runner Up Smart India Hackathon 2024', '2024-12-12 00:00:00', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/achievements/d99c3eae-831d-4043-90a8-4f15d9970d65.jpeg', '2025-07-27 19:27:03.366', '1b5933a9-5d50-4246-861a-ca0d30bd581f', 'Secured Runner-Up position at Smart India Hackathon 2024, a prestigious national-level innovation challenge. Built an AI-powered virtual therapy platform for individuals with speech impairments, promoting accessible and inclusive mental health support.', '2025-07-27 19:27:03.366', NULL), +('8', 'Runner Up At Clash of CSS NIT Kurukshetra', '2023-11-30 00:00:00', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/achievements/a6a92603-a4a9-4ea8-9afe-f1473f0b21ab.jpeg', '2025-07-27 19:09:12.806', '1b5933a9-5d50-4246-861a-ca0d30bd581f', 'Runner Up at the Clash of CSS Hackathon organized online by NIT Kurukshetra.\nWorked in a team of three to design a visually appealing, responsive frontend interface under time constraints, demonstrating strong UI/UX skills and effective collaboration.', '2025-07-27 19:09:12.806', NULL); + + +-- +-- Data for Name: CompletedQuestion; Type: TABLE DATA; Schema: public; Owner: - +-- + +-- Empty COPY for public."CompletedQuestion" ("memberId", "questionId") removed + + +-- +-- Data for Name: InterviewExperience; Type: TABLE DATA; Schema: public; Owner: - +-- + +INSERT INTO public."InterviewExperience" (id, company, role, verdict, content, "isAnonymous", "memberId") VALUES +('33', 'Grinder', 'Sword Department', 'Pending', 'I got selected because of my skills with my sword ( i am revealing my identity)', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('6', 'Google', 'Software Engineering', 'Rejected', '"
  1. I SUCK

"', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('7', 'Amazon', 'Engineer', 'Selected', '

I ROCK
i SHINE

I AM THE BEST

I AM HUGE

FABULOUS IS MY MIDDLE NAME

', 'true', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('8', 'TCS', 'Prime', 'Pending', '

IF YOU BECAME PREGNANT ON THE DAY OF YOUR TCS INTERIVEW THEN BY THE TIME YOU GET YOUR INTERVIEW RESULT YOUR FETUS WOULD HAVE DEVLEOPED THE ABILITY TO OPEN ITS EYES, LITERAL EYES DEVELOP IN LESS TIME THAN THESE PEOPLE NEED TO TELL YOU WHETHER YOU HAVE A JOB OR NOT

', 'true', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('10', 'Meta', 'Floor sweeper', 'Rejected', '# how tf did I mess this up man\n\n*It was literally a floor sweeping job*\n\n### I even showed my floor coding skills by creating a program that does `print("I am floor sweeper!!!")`\n\n> Honestly I am quite impressed that someone can fail an interveiw for this job\n\n -my interviewer\n\nThe job requirements:\n\n\n1. Know how to walk\n2. Know how to hold a mop\n3. Know how to use it on the floor \n\nWhen I asked them why I failed they gave the following evaluation:\n\n\n- The candidate came with a hatsune miku themed mop that had "my waifu helps me clean" on it \n- He was constantly staring at the breasts of female as well as male interviewers (while licking lips)\n- When asked for an explaination for the "I love pedophilia" tshirt that the candidate was sporting, he went on a 30 minute tirade on the topic of **"Old enough to swear, then my children she can bear"**\n\n**idk man just sounds very unreasonable if you ask me :(**\n\n', 'true', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('12', 'call of code', 'President', 'Rejected', 'How tf did I get rejected for a job I already had 😭', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('13', 'xyz', 'asdf', 'Pending', '> asdfasdfasdf', 'true', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('14', 'asdf', 'asdf', 'Selected', '` -dsfasdf`', 'true', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('15', 'asdf', 'qwer', 'Pending', ' sarvesh', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('16', 'xvbasdf', 'l;sjdfl;jasdf', 'Pending', 'asdfasdfasdfasdfasdfasdf', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('17', 'Deolitte', 'SDE1', 'Selected', 'Guys i got selected the techical rounds were easy** **', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('18', 'Human Inc', 'Ex-Kitten', 'Selected', 'Moo. I mean, Mew.', 'true', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('19', 'Company', 'SDE', 'Selected', 'Yeahhhhhhhhh i got selected', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('11', 'Netflix', 'floor sweeper', 'Rejected', '# how tf did I mess this up man\n\n*It was literally a floor sweeping job*\n\n### I even showed my floor coding skills by creating a program that does `print("I am floor sweeper!!!")`\n\n> Honestly I am quite impressed that someone can fail an interveiw for this job\n\n```\n -my interviewer\n\n```\n\nThe job requirements:\n\n1. Know how to walk\n2. Know how to hold a mop\n3. Know how to use it on the floor\n\nWhen I asked them why I failed they gave the following evaluation:\n\n- The candidate came with a hatsune miku themed mop that had "my waifu helps me clean" on it\n', 'true', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('20', 'Google', 'Software Engineer', 'Selected', 'Yeah i got selected at Google', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('23', 'aaaaaaaaaaaa', 'aaaaaaaaaaaaaaaa', 'Selected', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('24', 'aabaasaxax', 'aaaa', 'Selected', '1234567cdcs', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('25', 'qwert', 'qwert', 'Selected', 'qwertyuiokjhgfdszxcv', 'true', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('26', 'asdfghjk', 'asdfghj', 'Selected', 'qwerdfghbnss', 'true', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('27', 'qqqqqqqqqq', 'qqqqqqqqqq', 'Selected', 'qqqqqqqqqqqqqqqqqqqqqq', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('28', 'qqqqqqqqqqqqqqq', 'qqqqqqqqqqqqqqqqqqq', 'Selected', 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('29', 'qqqqqqqqqqqqqqq', 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq', 'Pending', 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('34', 'qwed', 'ssss', 'Selected', 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('31', 'OnlyFans', 'Content Head', 'Selected', 'Yeahh i got selected because of my content ideas.', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('32', 'Tinder', 'Match Maker', 'Rejected', 'I got rejected because of my dogshit match making skillssss brooooooo', 'false', '77165f92-1a09-407c-987f-0fc9be16fad8'); + + +-- +-- Data for Name: Member; Type: TABLE DATA; Schema: public; Owner: - +-- + +INSERT INTO public."Member" (id, name, email, phone, bio, "profilePhoto", github, linkedin, twitter, geeksforgeeks, leetcode, codechef, codeforces, "passoutYear", "isApproved", "isManager", "createdAt", "updatedAt", "approvedById", birth_date) VALUES +('a6bc0b3a-71bf-4e0d-8879-ddedbbc0a766', 'your mom', 'yourmom@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2026-01-01 00:00:00', 'false', 'false', '2025-10-27 20:12:58.862', '2025-10-27 20:12:58.862', NULL, NULL), +('259a1e70-c093-43d4-8aa1-bba058a896b8', 'Sakshi Chaudhari', 'sakshi@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/27b32a1e-2edd-4663-8635-e0cf9c159967.jpeg', 'https://github.com/sakshiatul30', 'https://www.linkedin.com/in/sakshi-chaudhari-536a81324', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'false', 'false', '2025-07-27 13:44:45.59', '2025-09-15 18:34:02.698', '77165f92-1a09-407c-987f-0fc9be16fad8', NULL), +('92f0e65d-f306-4cdd-baad-059f645cf148', 'hello', 'hello123@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'false', 'false', '2025-11-20 09:01:13.485', '2025-11-20 09:01:13.485', NULL, NULL), +('db2bd9ec-25e5-4134-ae53-fea1734ca161', 'Aarya Godbole', 'aaryagodbole550@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/b7d68ce2-87c9-4ea6-9add-4e809b7ae37c.jpeg', 'https://github.com/aaryagodbole', 'https://www.linkedin.com/in/aarya-godbole/', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'true', 'false', '2025-07-27 13:56:11.408', '2025-07-27 19:37:09.064', NULL, NULL), +('22ba7f7a-14e7-45fa-bf5f-51d5f015496f', 'Vansh Waldeo', 'vansh@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/b856d400-78ea-4eb4-8f3c-1ffb1eb1b0fa.jpeg', 'https://github.com/VanshKing30', 'https://www.linkedin.com/in/vansh-waldeo-81ab31285/', NULL, NULL, NULL, NULL, NULL, '2025-01-01 00:00:00', 'true', 'false', '2025-07-27 13:12:09.267', '2025-07-27 13:12:09.267', NULL, NULL), +('6c968bfa-ebf8-4b2b-a349-36bbc9cc2870', 'Sachin Barvekar', 'sachin@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/d84017f1-8859-4444-a378-f7591f2c2456.jpeg', NULL, 'https://www.linkedin.com/in/sachin-barvekar-2874481a2/', NULL, NULL, NULL, NULL, NULL, '2025-01-01 00:00:00', 'true', 'false', '2025-07-27 14:28:13.538', '2025-07-27 14:28:13.538', NULL, NULL), +('7dd07cc1-08da-48cc-a162-f546356fe291', 'Vaishnavi Adhav', 'vaishnaviadhav@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/2ed0e256-7b52-4c90-abf1-2c759b4aeaa8.jpeg', 'https://github.com/vaishnavi4049', 'https://www.linkedin.com/in/vaishnavi-adhav-b5b346362', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'true', 'false', '2025-07-27 13:43:26.12', '2025-07-27 19:35:31.505', NULL, NULL), +('86237b63-1339-49d6-ad57-1d4b93bd5092', 'Bhaven Rathod', 'bhaven@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/df2e0e37-50e8-435e-92e7-fa2e507ca424.jpeg', NULL, 'https://www.linkedin.com/in/bhaven-rathod/', NULL, NULL, NULL, NULL, NULL, '2025-01-01 00:00:00', 'true', 'false', '2025-07-27 12:42:50.416', '2025-07-27 12:42:50.416', NULL, NULL), +('46cfa3aa-1efe-4cc6-a624-340808ef7cb8', 'Piyushaa Mahajan', 'piyushaa@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/396f4028-0423-45d0-94b4-5222e239d875.jpeg', 'https://github.com/piyushaa20', 'https://www.linkedin.com/in/piyushaa-mahajan-826594323', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'true', 'false', '2025-07-27 13:46:16.531', '2025-07-27 19:51:48.667', NULL, NULL), +('a8443783-dd59-446a-93f5-19f5e590e88b', 'Siddhesh Thorat', 'siddhesh@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/1ed6dc74-090b-4cca-9c9a-687b12a5086f.jpeg', 'https://github.com/siddhu9993', 'https://www.linkedin.com/in/d2d-siddhesh/', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'true', 'false', '2025-07-27 13:47:44.371', '2025-07-27 13:47:44.371', NULL, NULL), +('329d6d7a-9787-452e-9c0c-506481c5462a', 'Suhani Bhati', 'suhani@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/57bb90b4-817f-4aaf-ab83-6f202e50a1e7.jpeg', 'https://github.com/SuhaniBhati', 'https://www.linkedin.com/in/suhani-bhati-aa528828b', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'true', 'false', '2025-07-27 13:58:45.984', '2025-07-27 19:39:11.221', NULL, NULL), +('855fb554-02f7-4b6a-a17e-d55b7976babf', 'Sanskar Darekar', 'sanskar@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/3d832d1b-3d81-481e-a6a3-e9e206c61c65.jpeg', 'https://github.com/sanskar-sd', 'https://www.linkedin.com/in/sanskar-darekar/', NULL, NULL, NULL, NULL, NULL, '2026-01-01 00:00:00', 'true', 'false', '2025-07-27 12:53:25.333', '2025-07-27 19:40:07.915', NULL, NULL), +('738b73b4-0725-4f7c-95bf-cbdb72eb4e84', 'Vedant Bulbule', 'vedant@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/6b61cb98-edc9-4e17-9ef7-6784850b77c7.jpeg', 'https://github.com/Vedantbulbule1223', 'https://www.linkedin.com/in/vedant-bulbule-aiml/', NULL, NULL, NULL, NULL, NULL, '2026-01-01 00:00:00', 'true', 'false', '2025-07-27 12:36:06.689', '2025-07-27 12:36:06.689', NULL, NULL), +('b6f44922-fff3-48eb-a0c9-15d41e786e38', 'Anushka Bendle', 'anushka@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/f71382bc-a6d5-467e-ac77-81e6667ddbd1.jpeg', 'https://github.com/anushkabendle', 'https://www.linkedin.com/in/anushkabendle445', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'true', 'false', '2025-07-27 13:37:17.319', '2025-07-27 19:52:42.056', NULL, NULL), +('1b482f80-f649-45f9-a90b-7538a7a6e66e', 'Mukul Dhobale', 'Mukul@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/0c6b2d21-32cd-4cce-96ee-58d1e265dac5.jpeg', 'https://github.com/Mukul306', 'https://www.linkedin.com/in/mukul-dhobale-53b547333', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'true', 'false', '2025-07-27 13:36:07.064', '2025-07-27 19:55:31.61', NULL, NULL), +('1b5933a9-5d50-4246-861a-ca0d30bd581f', 'Samarth Lad', 'samarth@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/1f07275c-e0e5-4c66-b068-25c593c263cb.jpeg', 'https://github.com/samrth07', 'https://www.linkedin.com/in/samarth-lad-675b99322/', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'true', 'true', '2025-07-27 13:41:03.21', '2025-07-27 19:58:32.541', NULL, NULL), +('3e6666f5-8ad9-4686-8656-a6904360d4ba', 'Shaheen Khan', 'shaheen@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/71310f7a-2dba-4d68-9aef-eb18dc60b1e6.jpeg', 'https://github.com/Shaheen-25', 'https://www.linkedin.com/in/shaheenkhan25', NULL, NULL, NULL, NULL, NULL, '2026-01-01 00:00:00', 'true', 'false', '2025-07-27 12:49:35.89', '2025-07-27 19:34:16.322', NULL, NULL), +('8eeacf82-18e5-48f8-a11e-fdbbe2eb81ce', 'Shivaji Raut', 'shivaji@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/cb24b95d-7ff8-444e-993e-c4dae169a5b2.jpeg', 'https://github.com/shivaji43', 'https://www.linkedin.com/in/shivajiraut/', NULL, NULL, NULL, NULL, NULL, '2025-01-01 00:00:00', 'true', 'false', '2025-07-27 13:13:06.46', '2025-07-27 13:13:06.46', NULL, NULL), +('1404e81c-d567-4103-941b-0abeea7fc049', 'Swaraj Pawar', 'swaraj@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/1d6c896c-7889-4140-81df-6d1d11021b71.jpeg', 'https://github.com/Swaraj-23', 'https://www.linkedin.com/in/swaraj-pawar-webdev/', NULL, NULL, NULL, NULL, NULL, '2025-01-01 00:00:00', 'true', 'false', '2025-07-27 13:11:27.264', '2025-07-27 13:11:27.264', NULL, NULL), +('644b5e8f-910d-450e-a855-a88f31d02b7b', 'Sanica chorey', 'sanica@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/5cfd39ec-9bd1-4414-91f7-c25b2f4b96b7.jpeg', NULL, 'https://www.linkedin.com/in/sanica-chorey-0876b024b/', NULL, NULL, NULL, NULL, NULL, '2025-01-01 00:00:00', 'true', 'false', '2025-07-27 13:20:13.746', '2025-07-27 13:20:13.746', NULL, NULL), +('03a1a0f8-c1b9-4ac1-99d9-f10f26a82f7c', 'Eshwar Varpe', 'eshwar@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/23b3a8c4-5821-43fe-9f91-9e48e43ed1be.jpeg', NULL, 'https://www.linkedin.com/in/eshwar-varpe-37a309246/', NULL, NULL, NULL, NULL, NULL, '2024-01-01 00:00:00', 'true', 'false', '2025-07-27 13:00:37.038', '2025-07-27 13:00:37.038', NULL, NULL), +('0cc29a18-180c-408d-9cbe-0fc06109a5c1', 'Yash Kathane', 'yash@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/5dd11423-a067-4ccb-b24b-1a4c0eb0aed2.jpeg', NULL, 'https://www.linkedin.com/in/yash-kathane-31b3b1215/', NULL, NULL, NULL, NULL, NULL, '2024-01-01 00:00:00', 'true', 'false', '2025-07-27 13:03:02.901', '2025-07-27 13:03:02.901', NULL, NULL), +('ac713749-77c1-46b6-ae82-f16d616b1c7c', 'Pratik Bhoite', 'pratik@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/3dc40d0d-5101-4550-9999-1dc4a4f649c3.jpeg', NULL, 'https://www.linkedin.com/in/pratik-bhoite-839770232/', NULL, NULL, NULL, NULL, NULL, '2024-01-01 00:00:00', 'true', 'false', '2025-07-27 13:05:29.412', '2025-07-27 19:19:08.055', NULL, NULL), +('48724979-f9c8-46de-b9ab-9ca0186596d0', 'Aryan Jadlie', 'aryan@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/a7e96fa8-21af-481d-ad80-dcc6c55b3be2.png', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2026-01-01 00:00:00', 'false', 'false', '2025-07-27 18:47:06.554', '2025-10-15 21:42:34.758', '77165f92-1a09-407c-987f-0fc9be16fad8', NULL), +('69394246-3e41-4eb5-812a-48801b0b5f3e', 'Shubham Tohake', 'shubham@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/22dc88c0-57a4-42ca-913f-7ab9ffd92080.png', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'false', 'false', '2025-07-27 18:49:14.273', '2025-09-15 18:35:19.025', '77165f92-1a09-407c-987f-0fc9be16fad8', NULL), +('f032f524-c153-496f-9eea-e2ff8622f3d1', 'Sahil Mulani', 'sahil@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/c286bed1-186d-4c3d-aa3c-3fcfc2e33752.jpeg', 'https://github.com/MulaniSahil', 'https://www.linkedin.com/in/sahil-mulani-5b7bba2a8/', NULL, NULL, 'https://leetcode.com/u/mulanisahil/', NULL, NULL, '2026-01-01 00:00:00', 'true', 'false', '2025-07-27 19:42:24.97', '2025-07-28 10:07:00.727', NULL, NULL), +('d46a667d-5b68-4b82-9de7-fcfdf0ab0181', 'Vaishnavi Ambhore', 'vaishnavi@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/c13561c7-cebf-4bad-994e-2370c841720d.jpeg', 'https://github.com/vaish12345678', 'https://www.linkedin.com/in/vaishnavi-ambhore-157131335/', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'true', 'false', '2025-07-27 13:42:03.576', '2025-07-27 19:36:06.959', NULL, NULL), +('d7c96d3c-d45d-4bde-8c2b-39f0451a389f', 'Sarvesh Shiralkar', 'sarveshshiralkar@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/84ee8088-de1e-49ab-8bae-e044cf8d7830.jpeg', 'https://github.com/SarveshMS7', 'https://www.linkedin.com/in/sarvesh-shiralkar-69559a326/', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'true', 'false', '2025-07-27 14:02:15.223', '2025-07-27 19:41:33.591', NULL, NULL), +('d7d54e46-8db2-449c-87f9-8e89e8537c42', 'Sherin Thomas', 'sherin@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/de491aeb-c096-4995-8428-26b94607d45b.jpeg', 'https://github.com/Sherin-2711', 'https://www.linkedin.com/in/sherin-thomas-644242333', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'true', 'true', '2025-07-27 16:57:39.035', '2025-07-27 19:57:11.54', NULL, NULL), +('77165f92-1a09-407c-987f-0fc9be16fad8', 'Harish Narote', 'harish@gmail.com', '8668673365', 'DSA Head of Call Of Code.....', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/bc133101-04d9-457f-8c57-a0b3f7fd683a.jpeg', 'https://github.com/Harish-Naruto', 'https://www.linkedin.com/in/harish-narote-600717339/', NULL, NULL, NULL, NULL, NULL, '2028-01-01 18:30:00', 'true', 'true', '2025-07-27 20:43:21.525', '2025-11-21 14:20:00.495', NULL, '2025-11-28'), +('20ee0910-36b3-48d6-ad96-2112d02fd9b6', 'Dilip Choudhary', 'dillip@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/1a814f4f-cdf4-43a8-980e-f1062695f730.png', 'https://github.com/dilip7654', 'https://www.linkedin.com/in/dilip-choudhary-39966421a/', NULL, NULL, NULL, NULL, NULL, '2026-01-01 00:00:00', 'true', 'false', '2025-07-27 19:47:54.658', '2025-07-27 20:00:57.185', NULL, NULL), +('1ff4b36d-5671-4855-8476-d0a8993f9873', ' Aditya Modak', 'aditya@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/01800613-ca50-43ae-b0a4-a076c9da427d.jpeg', 'https://github.com/Aditya2002M', 'https://www.linkedin.com/in/aditya-modak-42a684250/', NULL, NULL, NULL, NULL, NULL, '2025-01-01 00:00:00', 'true', 'false', '2025-07-27 13:21:05.022', '2025-07-27 13:21:05.022', NULL, NULL), +('207bb8bd-3e48-40c8-83ce-a825cb9fe474', 'syswraith', 'syswraith@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'false', 'false', '2025-10-27 14:57:13.93', '2025-10-27 14:57:13.93', NULL, NULL), +('c5b2470d-7fb4-4c93-bfe2-fedc00415dc2', 'Shashwati Meshram ', 'shashwati@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/WhatsApp%20Image%202025-07-28%20at%205.00.43%20PM.jpeg', 'https://github.com/Shashwati12', 'https://www.linkedin.com/in/shashwati-meshram-785316325/', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'true', 'false', '2025-07-27 13:57:14.412', '2025-07-27 13:57:14.412', NULL, NULL), +('75ef229a-3770-46aa-adc8-f4d250c6ac81', 'Shivam Korade', 'shivam@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/be5d5c49-da35-4979-8e06-0b8bc56edea6.jpeg', NULL, 'https://www.linkedin.com/in/shivam-korade/', NULL, NULL, NULL, NULL, NULL, '2025-01-01 00:00:00', 'true', 'false', '2025-07-27 17:32:15.43', '2025-07-27 17:32:15.43', NULL, NULL), +('516af252-e8dc-48a4-80c4-5af1e0758e58', 'Sahil Lakare', 'sahillakare@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/ec07d6cd-d1b5-43b1-8749-11baca35abba.jpeg', NULL, 'https://www.linkedin.com/in/sahil-lakare-842304298/', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'false', 'false', '2025-07-27 14:07:22.15', '2025-10-06 06:38:03.943', '77165f92-1a09-407c-987f-0fc9be16fad8', NULL), +('046d352f-10d9-49b5-bbed-31d67bf4b583', 'Shreyash Kapse', 'sheryash@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/6360ee38-249d-4d40-83c2-de1bd2b03d22.png', NULL, NULL, NULL, NULL, 'https://leetcode.com', 'https://codechef.com', NULL, '2026-01-01 00:00:00', 'false', 'false', '2025-07-27 18:28:44.084', '2025-10-06 06:38:11.206', '77165f92-1a09-407c-987f-0fc9be16fad8', NULL), +('e68ca856-978b-4bb5-a2f1-6497278624bb', 'Abhiram Suradkar', 'Abhiram@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/6cec207d-5cac-466a-96cf-00faaae9141f.jpeg', 'https://github.com/abhi32GBram', 'https://www.linkedin.com/in/abhiram-suradkar-a6728622b/', NULL, NULL, NULL, NULL, NULL, '2025-01-01 00:00:00', 'true', 'false', '2025-07-27 18:41:32.116', '2025-07-27 18:41:32.116', NULL, NULL), +('ef59db8b-2ad5-4e0a-b741-f58521bf61ec', 'Komal Warake', 'komal@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/8aa5e9df-6bf1-458f-b65f-03d1a3389398.png', 'https://github.com/komalwarke1', 'https://www.linkedin.com/in/komal-warake01', NULL, NULL, NULL, NULL, NULL, '2026-01-01 00:00:00', 'true', 'false', '2025-07-27 19:29:28.585', '2025-07-27 19:32:17.955', NULL, NULL), +('bf152df5-3c23-4c1c-85aa-212e0487b420', 'Prathamesh Shinde', 'prathamesh@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/a0915ccc-c8f3-498a-96b1-1731e0f01258.jpeg', 'https://github.com/prathameshshinde555', 'https://www.linkedin.com/in/prathameshshinde555/', NULL, NULL, NULL, NULL, NULL, '2024-01-01 00:00:00', 'true', 'false', '2025-07-27 13:04:35.098', '2025-07-27 13:04:35.098', NULL, NULL), +('0b83d3e3-8685-4cfe-9f63-6cc22c1ceae4', 'Prajkta Patil', 'prajakta@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/ac073ec8-59b2-4f71-8fb0-1be1af7044b5.png', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2026-01-01 00:00:00', 'false', 'false', '2025-07-27 18:32:26.724', '2025-07-31 18:13:22.227', '77165f92-1a09-407c-987f-0fc9be16fad8', NULL), +('64464cc4-4dfc-4522-a256-6aca2371df7f', 'Veda Bhadane', 'veda@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/0b34ae85-da4a-480f-adc5-f27e83e47c32.jpeg', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2026-01-01 00:00:00', 'false', 'false', '2025-07-27 18:06:52.395', '2025-10-27 06:54:48.588', '77165f92-1a09-407c-987f-0fc9be16fad8', NULL), +('ad525dce-67dd-4878-ab95-068943923b81', 'Sarvesh Shahane', 'sarvesh@gmail.com', '9421957635', 'I am that guy.', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/2e341e00-8b4b-4276-b7c6-e0a68c45f73c.jpeg', 'https://github.com/i-am-that-guy', 'https://www.linkedin.com/in/sarveshshahane', 'https://x.com/_That_Guy_Here_', 'https://www.geeksforgeeks.org/profile/sarveshshaqram/', 'https://leetcode.com/u/This-Is-My-GitHub-Account/', 'https://www.codechef.com/users/i_am_that_guy', 'https://codeforces.com/profile/i_am_that_guy', '2025-01-01 00:00:00', 'true', 'false', '2025-07-27 13:29:37.542', '2025-07-27 21:14:03.77', NULL, '2026-01-07'), +('c494d747-5123-457f-b9cf-f3359f5a0fe8', 'Shruti Jadhav ', 'shruti@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/b01eec07-de6c-49eb-b4cd-31dbb17d1d71.jpeg', 'https://github.com/shrutiiiyet', 'https://www.linkedin.com/in/shruti-jadhav-892164277/', NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'true', 'true', '2025-07-27 17:18:50.419', '2025-07-27 19:59:53.048', NULL, NULL), +('cffc47fb-1147-4dd5-9818-21d209dbe3f3', 'Harsh Bhavsar', 'harsh@gmail.com', NULL, NULL, 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/members/66a493c8-5de1-4be8-b64a-d2639ec000e3.png', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2027-01-01 00:00:00', 'false', 'false', '2025-07-27 18:37:15.882', '2025-09-15 14:45:06.273', 'c494d747-5123-457f-b9cf-f3359f5a0fe8', NULL); + + +-- +-- Data for Name: MemberAchievement; Type: TABLE DATA; Schema: public; Owner: - +-- + +INSERT INTO public."MemberAchievement" ("memberId", "achievementId") VALUES +('22ba7f7a-14e7-45fa-bf5f-51d5f015496f', '3'), +('644b5e8f-910d-450e-a855-a88f31d02b7b', '4'), +('6c968bfa-ebf8-4b2b-a349-36bbc9cc2870', '4'), +('1ff4b36d-5671-4855-8476-d0a8993f9873', '4'), +('046d352f-10d9-49b5-bbed-31d67bf4b583', '4'), +('bf152df5-3c23-4c1c-85aa-212e0487b420', '5'), +('75ef229a-3770-46aa-adc8-f4d250c6ac81', '5'), +('48724979-f9c8-46de-b9ab-9ca0186596d0', '5'), +('86237b63-1339-49d6-ad57-1d4b93bd5092', '5'), +('1b5933a9-5d50-4246-861a-ca0d30bd581f', '6'), +('c494d747-5123-457f-b9cf-f3359f5a0fe8', '6'), +('db2bd9ec-25e5-4134-ae53-fea1734ca161', '6'), +('d46a667d-5b68-4b82-9de7-fcfdf0ab0181', '6'), +('69394246-3e41-4eb5-812a-48801b0b5f3e', '7'), +('7dd07cc1-08da-48cc-a162-f546356fe291', '7'), +('b6f44922-fff3-48eb-a0c9-15d41e786e38', '7'), +('259a1e70-c093-43d4-8aa1-bba058a896b8', '7'), +('644b5e8f-910d-450e-a855-a88f31d02b7b', '8'), +('86237b63-1339-49d6-ad57-1d4b93bd5092', '8'), +('ad525dce-67dd-4878-ab95-068943923b81', '8'), +('69394246-3e41-4eb5-812a-48801b0b5f3e', '9'), +('d7d54e46-8db2-449c-87f9-8e89e8537c42', '9'), +('46cfa3aa-1efe-4cc6-a624-340808ef7cb8', '9'), +('b6f44922-fff3-48eb-a0c9-15d41e786e38', '9'), +('bf152df5-3c23-4c1c-85aa-212e0487b420', '10'), +('0cc29a18-180c-408d-9cbe-0fc06109a5c1', '10'), +('03a1a0f8-c1b9-4ac1-99d9-f10f26a82f7c', '11'), +('64464cc4-4dfc-4522-a256-6aca2371df7f', '11'), +('046d352f-10d9-49b5-bbed-31d67bf4b583', '11'), +('69394246-3e41-4eb5-812a-48801b0b5f3e', '12'), +('c5b2470d-7fb4-4c93-bfe2-fedc00415dc2', '12'), +('0b83d3e3-8685-4cfe-9f63-6cc22c1ceae4', '13'), +('1b5933a9-5d50-4246-861a-ca0d30bd581f', '13'), +('22ba7f7a-14e7-45fa-bf5f-51d5f015496f', '13'), +('329d6d7a-9787-452e-9c0c-506481c5462a', '13'), +('64464cc4-4dfc-4522-a256-6aca2371df7f', '13'), +('cffc47fb-1147-4dd5-9818-21d209dbe3f3', '13'), +('69394246-3e41-4eb5-812a-48801b0b5f3e', '14'), +('ad525dce-67dd-4878-ab95-068943923b81', '15'), +('20ee0910-36b3-48d6-ad96-2112d02fd9b6', '15'), +('738b73b4-0725-4f7c-95bf-cbdb72eb4e84', '15'), +('f032f524-c153-496f-9eea-e2ff8622f3d1', '15'); + + +-- +-- Data for Name: MemberProject; Type: TABLE DATA; Schema: public; Owner: - +-- + +INSERT INTO public."MemberProject" ("memberId", "projectId") VALUES +('22ba7f7a-14e7-45fa-bf5f-51d5f015496f', '3'), +('22ba7f7a-14e7-45fa-bf5f-51d5f015496f', '4'), +('ad525dce-67dd-4878-ab95-068943923b81', '4'), +('1404e81c-d567-4103-941b-0abeea7fc049', '4'), +('1b5933a9-5d50-4246-861a-ca0d30bd581f', '6'), +('c494d747-5123-457f-b9cf-f3359f5a0fe8', '6'), +('db2bd9ec-25e5-4134-ae53-fea1734ca161', '6'), +('d46a667d-5b68-4b82-9de7-fcfdf0ab0181', '6'), +('d7d54e46-8db2-449c-87f9-8e89e8537c42', '8'), +('c5b2470d-7fb4-4c93-bfe2-fedc00415dc2', '8'), +('1b482f80-f649-45f9-a90b-7538a7a6e66e', '8'), +('329d6d7a-9787-452e-9c0c-506481c5462a', '8'), +('69394246-3e41-4eb5-812a-48801b0b5f3e', '10'), +('d7d54e46-8db2-449c-87f9-8e89e8537c42', '10'), +('46cfa3aa-1efe-4cc6-a624-340808ef7cb8', '10'), +('b6f44922-fff3-48eb-a0c9-15d41e786e38', '10'), +('ad525dce-67dd-4878-ab95-068943923b81', '11'), +('8eeacf82-18e5-48f8-a11e-fdbbe2eb81ce', '11'), +('1ff4b36d-5671-4855-8476-d0a8993f9873', '11'), +('22ba7f7a-14e7-45fa-bf5f-51d5f015496f', '11'), +('77165f92-1a09-407c-987f-0fc9be16fad8', '11'), +('d7d54e46-8db2-449c-87f9-8e89e8537c42', '11'), +('c5b2470d-7fb4-4c93-bfe2-fedc00415dc2', '11'), +('1b5933a9-5d50-4246-861a-ca0d30bd581f', '11'), +('c494d747-5123-457f-b9cf-f3359f5a0fe8', '11'), +('69394246-3e41-4eb5-812a-48801b0b5f3e', '13'), +('c5b2470d-7fb4-4c93-bfe2-fedc00415dc2', '13'), +('ad525dce-67dd-4878-ab95-068943923b81', '14'), +('77165f92-1a09-407c-987f-0fc9be16fad8', '14'), +('bf152df5-3c23-4c1c-85aa-212e0487b420', '15'), +('69394246-3e41-4eb5-812a-48801b0b5f3e', '16'), +('c5b2470d-7fb4-4c93-bfe2-fedc00415dc2', '16'), +('22ba7f7a-14e7-45fa-bf5f-51d5f015496f', '16'), +('db2bd9ec-25e5-4134-ae53-fea1734ca161', '16'); + + +-- +-- Data for Name: Project; Type: TABLE DATA; Schema: public; Owner: - +-- + +INSERT INTO public."Project" (id, name, "imageUrl", "githubUrl", "deployUrl", "createdAt", "createdById", "updatedAt", "updatedById") VALUES +('3', 'Codenest', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/projects/ea048f75-12ca-426f-a0f7-da5bd8d2a6ab.jpeg', 'https://github.com/VanshKing30/codenest', NULL, '2025-07-27 20:24:12.073', 'c494d747-5123-457f-b9cf-f3359f5a0fe8', '2025-07-27 20:24:12.073', NULL), +('4', 'FoodiesWeb', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/projects/5b2ea1df-4bbb-40f5-896a-add8dd6573c7.png', 'https://github.com/VanshKing30/FoodiesWeb', 'https://foodies-web-app.vercel.app/', '2025-07-27 20:25:05.349', 'c494d747-5123-457f-b9cf-f3359f5a0fe8', '2025-07-27 20:25:05.349', NULL), +('6', 'Sportify', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/projects/2cfe891d-f838-4118-9a89-bae85a5adab6.png', 'https://github.com/call-0f-code/Sportify', NULL, '2025-07-27 20:40:13.289', 'c494d747-5123-457f-b9cf-f3359f5a0fe8', '2025-07-27 20:40:13.289', NULL), +('8', 'EventHub', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/projects/259f8cf5-d265-4948-bbc7-987710af6b7e.jpeg', 'https://github.com/Shashwati12/Event-Hub', NULL, '2025-07-27 20:51:06.857', 'c494d747-5123-457f-b9cf-f3359f5a0fe8', '2025-07-27 20:51:06.857', NULL), +('10', 'Wanderlust', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/projects/0da621d2-a076-4043-ac6a-04d2aabbcebf.jpeg', 'https://github.com/Sherin-2711/Wanderlust', NULL, '2025-07-27 20:58:40.253', 'c494d747-5123-457f-b9cf-f3359f5a0fe8', '2025-07-27 20:58:40.253', NULL), +('11', 'CallOfCode', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/projects/fb99db7f-4262-4093-af16-c45fdadea1e8.png', 'https://github.com/call-0f-code/call-of-code', 'https://callofcode.in/', '2025-07-27 21:07:20.413', 'c494d747-5123-457f-b9cf-f3359f5a0fe8', '2025-07-27 21:07:20.413', NULL), +('13', 'WellnessWave', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/projects/54f25849-1484-4746-aa77-aa623b52bf54.png', 'https://github.com/SHUBHAMTOHAKE0203/WellnessWave-Hospital-Hunar-Intern-Hackathon', 'https://shubhamtohake0203.github.io/WellnessWave-Hospital-Hunar-Intern-Hackathon/', '2025-07-27 21:10:42.857', 'c494d747-5123-457f-b9cf-f3359f5a0fe8', '2025-07-27 21:10:42.857', NULL), +('14', 'EventHub', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/projects/79e2f1c2-4fa6-4267-9847-b8b6ae516a76.png', 'https://github.com/i-am-that-guy/EventHub', 'https://eventhub-1ukr.onrender.com/', '2025-07-27 21:28:03.777', 'c494d747-5123-457f-b9cf-f3359f5a0fe8', '2025-07-27 21:28:03.777', NULL), +('15', 'Technothon', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/projects/a4fae4ae-3829-47c6-a088-b5e3f2d1d51a.png', 'https://github.com/call-0f-code/technothon', 'https://call-0f-code.github.io/technothon/', '2025-07-27 21:29:52.401', 'c494d747-5123-457f-b9cf-f3359f5a0fe8', '2025-07-27 21:29:52.401', NULL), +('16', 'CureWave', 'https://riqqtbuoaycwwiemnmri.supabase.co/storage/v1/object/public/images/projects/bedabfee-a51e-49e4-86e7-f9d263002f1f.png', 'https://github.com/SHUBHAMTOHAKE0203/CureWave', 'https://cure-wave-one.vercel.app/', '2025-07-27 21:33:33.669', 'c494d747-5123-457f-b9cf-f3359f5a0fe8', '2025-07-27 21:33:33.669', NULL); + + +-- +-- Data for Name: Question; Type: TABLE DATA; Schema: public; Owner: - +-- + +INSERT INTO public."Question" (id, "questionName", difficulty, link, "topicId", "createdById", "createdAt", "updatedAt", "updatedById") VALUES +('19', 'Remove element', 'Medium', 'https://leetcode.com/problems/remove-element/description/?envType=study-plan-v2&envId=top-interview-150', '22', '77165f92-1a09-407c-987f-0fc9be16fad8', '2025-10-14 19:05:11.882', '2025-10-15 03:49:46.988', '77165f92-1a09-407c-987f-0fc9be16fad8'); + + +-- +-- Data for Name: Topic; Type: TABLE DATA; Schema: public; Owner: - +-- + +INSERT INTO public."Topic" (id, title, description, "createdById", "createdAt", "updatedAt", "updatedById") VALUES +('23', 'TOPIC NAME ', 'JSBDNREMC DKVNROEJFPWMC REGFIRHGR VSM CLSMJFORJO HA HEE ITJAOJJA', '77165f92-1a09-407c-987f-0fc9be16fad8', '2025-10-14 18:47:47.527', '2025-10-14 18:47:47.527', '77165f92-1a09-407c-987f-0fc9be16fad8'), +('22', 'demoooooo', 'who the fuck writes demo like this ', '77165f92-1a09-407c-987f-0fc9be16fad8', '2025-10-14 18:31:21.048', '2025-10-15 03:43:53.083', '77165f92-1a09-407c-987f-0fc9be16fad8'); + + +-- Supabase compatibility fixes (appended for local setup) +-- Ensure enum types used by Prisma exist +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'Difficulty') THEN + CREATE TYPE public."Difficulty" AS ENUM ('Easy','Medium','Hard'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'Verdict') THEN + CREATE TYPE public."Verdict" AS ENUM ('Selected','Rejected','Pending'); + END IF; +END$$; + +-- Create auth schema and minimal users table if missing +CREATE SCHEMA IF NOT EXISTS auth; + +CREATE TABLE IF NOT EXISTS auth.users ( + id uuid PRIMARY KEY, + aud text, + role text, + email text, + encrypted_password text, + email_confirmed boolean DEFAULT false, + raw_user_meta_data jsonb, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now() +); + +-- Insert a sample auth user mapped to an existing Member (no-op if already exists) +INSERT INTO auth.users (id, aud, role, email, email_confirmed) +VALUES ('77165f92-1a09-407c-987f-0fc9be16fad8','authenticated','authenticated','harish@gmail.com', true) +ON CONFLICT (id) DO NOTHING; + +-- Create simple DB roles for local testing and grant access +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN + CREATE ROLE anon; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN + CREATE ROLE service_role; + END IF; +END$$; + +-- Grant read access to anon and full access to service_role for local development only +GRANT USAGE ON SCHEMA public TO anon, service_role; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO anon; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO service_role; + +-- Ensure future tables inherit privileges +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO anon; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO service_role; + +-- End of appended Supabase compatibility SQL + + +-- +-- Data for Name: _prisma_migrations; Type: TABLE DATA; Schema: public; Owner: - +-- + +INSERT INTO public._prisma_migrations (id, checksum, finished_at, migration_name, logs, rolled_back_at, started_at, applied_steps_count) VALUES +('2b698eba-1b33-44fd-b82a-c5c6043ef533', '89625be867ca784d446e9010c858e973cdac24ce7375c77db48bf56c294ec822', '2025-07-27 11:16:59.055498+00', '20250713111533_first_prisma_migration', NULL, NULL, '2025-07-27 11:16:58.7832+00', '1'), +('62ee3fac-53d8-4a55-84a1-3b92da855594', 'a13448142741adec639e1f8a1c28c39c767427e580ead7fac23fe3a6a3f394ae', '2025-07-27 11:16:59.4006+00', '20250718201903_init', NULL, NULL, '2025-07-27 11:16:59.145301+00', '1'), +('f8043ab8-18cb-4d4e-a20c-acb3ef6cdf68', '14515e15ca68369491dabe78a7f059e4d5157e51eab6a28d38483afe49afcb39', '2025-07-27 11:16:59.703971+00', '20250719123800_image_url_githuburk_make_complusory', NULL, NULL, '2025-07-27 11:16:59.501527+00', '1'), +('f0a81f30-46ba-4561-a537-60ce243a191e', 'e312f3719ce8eb87764107f8467b419001c25510c4302f444e7743fa20b0c30d', '2025-07-27 11:18:21.610297+00', '20250727111821_passoutyear_optional', NULL, NULL, '2025-07-27 11:18:21.388193+00', '1'), +('6446026f-c163-4250-a5f1-e1e89e8269a0', '247a03b37d029c1af7ade67e7c91eced3e5adeec02cd9b8c7e67e08e148cbfd6', '2025-10-16 19:11:32.783002+00', '20251016191132_birth_date_added_to_member', NULL, NULL, '2025-10-16 19:11:32.620529+00', '1'), +('e87cdcbd-1fbb-4285-8738-e7072925decb', 'f8c188dd8523234c3b95f96c4a8a78a585828817f07fdbf1c294502072ace662', '2025-10-16 19:38:12.06791+00', '20251016193811_birth_date_type_fix', NULL, NULL, '2025-10-16 19:38:11.846426+00', '1'); + + +-- +-- Data for Name: schema_migrations; Type: TABLE DATA; Schema: realtime; Owner: - +-- + +INSERT INTO realtime.schema_migrations (version, inserted_at) VALUES +('20211116024918', '2025-07-13 10:28:51'), +('20211116045059', '2025-07-13 10:28:52'), +('20211116050929', '2025-07-13 10:28:53'), +('20211116051442', '2025-07-13 10:28:53'), +('20211116212300', '2025-07-13 10:28:54'), +('20211116213355', '2025-07-13 10:28:55'), +('20211116213934', '2025-07-13 10:28:55'), +('20211116214523', '2025-07-13 10:28:56'), +('20211122062447', '2025-07-13 10:28:57'), +('20211124070109', '2025-07-13 10:28:57'), +('20211202204204', '2025-07-13 10:28:58'), +('20211202204605', '2025-07-13 10:28:59'), +('20211210212804', '2025-07-13 10:29:01'), +('20211228014915', '2025-07-13 10:29:01'), +('20220107221237', '2025-07-13 10:29:02'), +('20220228202821', '2025-07-13 10:29:02'), +('20220312004840', '2025-07-13 10:29:03'), +('20220603231003', '2025-07-13 10:29:04'), +('20220603232444', '2025-07-13 10:29:05'), +('20220615214548', '2025-07-13 10:29:05'), +('20220712093339', '2025-07-13 10:29:06'), +('20220908172859', '2025-07-13 10:29:07'), +('20220916233421', '2025-07-13 10:29:07'), +('20230119133233', '2025-07-13 10:29:08'), +('20230128025114', '2025-07-13 10:29:09'), +('20230128025212', '2025-07-13 10:29:09'), +('20230227211149', '2025-07-13 10:29:10'), +('20230228184745', '2025-07-13 10:29:11'), +('20230308225145', '2025-07-13 10:29:11'), +('20230328144023', '2025-07-13 10:29:12'), +('20231018144023', '2025-07-13 10:29:12'), +('20231204144023', '2025-07-13 10:29:13'), +('20231204144024', '2025-07-13 10:29:14'), +('20231204144025', '2025-07-13 10:29:15'), +('20240108234812', '2025-07-13 10:29:15'), +('20240109165339', '2025-07-13 10:29:16'), +('20240227174441', '2025-07-13 10:29:17'), +('20240311171622', '2025-07-13 10:29:18'), +('20240321100241', '2025-07-13 10:29:19'), +('20240401105812', '2025-07-13 10:29:21'), +('20240418121054', '2025-07-13 10:29:22'), +('20240523004032', '2025-07-13 10:29:24'), +('20240618124746', '2025-07-13 10:29:25'), +('20240801235015', '2025-07-13 10:29:25'), +('20240805133720', '2025-07-13 10:29:26'), +('20240827160934', '2025-07-13 10:29:26'), +('20240919163303', '2025-07-13 10:29:27'), +('20240919163305', '2025-07-13 10:29:28'), +('20241019105805', '2025-07-13 10:29:29'), +('20241030150047', '2025-07-13 10:29:31'), +('20241108114728', '2025-07-13 10:29:32'), +('20241121104152', '2025-07-13 10:29:32'), +('20241130184212', '2025-07-13 10:29:33'), +('20241220035512', '2025-07-13 10:29:34'), +('20241220123912', '2025-07-13 10:29:34'), +('20241224161212', '2025-07-13 10:29:35'), +('20250107150512', '2025-07-13 10:29:35'), +('20250110162412', '2025-07-13 10:29:36'), +('20250123174212', '2025-07-13 10:29:37'), +('20250128220012', '2025-07-13 10:29:37'), +('20250506224012', '2025-07-13 10:29:38'), +('20250523164012', '2025-07-13 10:29:38'), +('20250714121412', '2025-07-18 09:59:25'), +('20250905041441', '2025-10-03 22:35:03'), +('20251103001201', '2025-11-13 08:50:05'), +('20251120212548', '2026-03-23 05:56:26'), +('20251120215549', '2026-03-23 05:56:26'), +('20260218120000', '2026-03-23 05:56:27'); + +-- +-- Name: Account Account_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Account" + ADD CONSTRAINT "Account_pkey" PRIMARY KEY (id); + + +-- +-- Name: Achievement Achievement_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Achievement" + ADD CONSTRAINT "Achievement_pkey" PRIMARY KEY (id); + + +-- +-- Name: CompletedQuestion CompletedQuestion_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."CompletedQuestion" + ADD CONSTRAINT "CompletedQuestion_pkey" PRIMARY KEY ("memberId", "questionId"); + + +-- +-- Name: InterviewExperience InterviewExperience_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."InterviewExperience" + ADD CONSTRAINT "InterviewExperience_pkey" PRIMARY KEY (id); + + +-- +-- Name: MemberAchievement MemberAchievement_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."MemberAchievement" + ADD CONSTRAINT "MemberAchievement_pkey" PRIMARY KEY ("memberId", "achievementId"); + + +-- +-- Name: MemberProject MemberProject_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."MemberProject" + ADD CONSTRAINT "MemberProject_pkey" PRIMARY KEY ("memberId", "projectId"); + + +-- +-- Name: Member Member_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Member" + ADD CONSTRAINT "Member_pkey" PRIMARY KEY (id); + + +-- +-- Name: Project Project_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Project" + ADD CONSTRAINT "Project_pkey" PRIMARY KEY (id); + + +-- +-- Name: Question Question_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Question" + ADD CONSTRAINT "Question_pkey" PRIMARY KEY (id); + + +-- +-- Name: Topic Topic_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Topic" + ADD CONSTRAINT "Topic_pkey" PRIMARY KEY (id); + + +-- +-- Name: _prisma_migrations _prisma_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public._prisma_migrations + ADD CONSTRAINT _prisma_migrations_pkey PRIMARY KEY (id); + + +-- +-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: realtime; Owner: - +-- + +ALTER TABLE ONLY realtime.schema_migrations + ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); + +-- +-- Name: Account Account_memberId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Account" + ADD CONSTRAINT "Account_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES public."Member"(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: Achievement Achievement_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Achievement" + ADD CONSTRAINT "Achievement_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public."Member"(id) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- Name: Achievement Achievement_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Achievement" + ADD CONSTRAINT "Achievement_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public."Member"(id) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- Name: CompletedQuestion CompletedQuestion_memberId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."CompletedQuestion" + ADD CONSTRAINT "CompletedQuestion_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES public."Member"(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: CompletedQuestion CompletedQuestion_questionId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."CompletedQuestion" + ADD CONSTRAINT "CompletedQuestion_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES public."Question"(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: InterviewExperience InterviewExperience_memberId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."InterviewExperience" + ADD CONSTRAINT "InterviewExperience_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES public."Member"(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: MemberAchievement MemberAchievement_achievementId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."MemberAchievement" + ADD CONSTRAINT "MemberAchievement_achievementId_fkey" FOREIGN KEY ("achievementId") REFERENCES public."Achievement"(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: MemberAchievement MemberAchievement_memberId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."MemberAchievement" + ADD CONSTRAINT "MemberAchievement_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES public."Member"(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: MemberProject MemberProject_memberId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."MemberProject" + ADD CONSTRAINT "MemberProject_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES public."Member"(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: MemberProject MemberProject_projectId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."MemberProject" + ADD CONSTRAINT "MemberProject_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES public."Project"(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: Member Member_approvedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Member" + ADD CONSTRAINT "Member_approvedById_fkey" FOREIGN KEY ("approvedById") REFERENCES public."Member"(id) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- Name: Project Project_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Project" + ADD CONSTRAINT "Project_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public."Member"(id) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- Name: Project Project_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Project" + ADD CONSTRAINT "Project_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public."Member"(id) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- Name: Question Question_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Question" + ADD CONSTRAINT "Question_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public."Member"(id) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- Name: Question Question_topicId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Question" + ADD CONSTRAINT "Question_topicId_fkey" FOREIGN KEY ("topicId") REFERENCES public."Topic"(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: Question Question_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Question" + ADD CONSTRAINT "Question_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public."Member"(id) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- Name: Topic Topic_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Topic" + ADD CONSTRAINT "Topic_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public."Member"(id) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- Name: Topic Topic_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."Topic" + ADD CONSTRAINT "Topic_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public."Member"(id) ON UPDATE CASCADE ON DELETE SET NULL; + + +COMMIT; From 2556b39ba60e1b2f3d9b2fc83404f3c2f4fa947b Mon Sep 17 00:00:00 2001 From: shrutiiiii Date: Sun, 29 Mar 2026 14:35:08 +0530 Subject: [PATCH 04/22] optimized image --- Dockerfile | 58 ++++++++++++++---------------------------- LOCAL_DEVELOPMENT.md | 2 +- scripts/setup-local.sh | 30 ++++++++++++++++------ 3 files changed, 42 insertions(+), 48 deletions(-) diff --git a/Dockerfile b/Dockerfile index d6421f4..723f1e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,80 +1,60 @@ -FROM node:20-alpine AS base - -RUN apk add --no-cache curl bash ca-certificates - -# Install Bun -RUN curl -fsSL https://bun.sh/install | bash -ENV PATH="/root/.bun/bin:$PATH" - -RUN if [ -f /root/.bun/bin/bun ]; then \ - /root/.bun/bin/bun --version && \ - if [ ! -f /root/.bun/bin/bunx ]; then \ - ln -s /root/.bun/bin/bun /root/.bun/bin/bunx; \ - fi; \ - else \ - echo "ERROR: Bun not installed properly" && exit 1; \ - fi - +FROM oven/bun:1.3 AS base WORKDIR /app - # ----------------------------- # deps stage - cache dependencies # ----------------------------- - - FROM base AS deps COPY package.json bun.lock* ./ COPY prisma ./prisma - RUN bun install --frozen-lockfile RUN bunx prisma generate - # ----------------------------- -# development stage +# build stage - compile TypeScript to JavaScript # ----------------------------- +FROM deps AS build +COPY . . +RUN bun build src/server.ts --outdir dist -FROM base AS development +# ----------------------------- +# prod deps stage - install only production dependencies +# ----------------------------- +FROM base AS prod-deps -COPY --from=deps /app/node_modules ./node_modules -COPY --from=deps /app/src/generated ./src/generated +COPY package.json bun.lock* ./ +RUN bun install --production --frozen-lockfile +# ----------------------------- +# development stage +# ----------------------------- +FROM deps AS development COPY . . - EXPOSE 3000 - CMD ["bun", "src/server.ts"] - # ----------------------------- # production stage # ----------------------------- -FROM base AS production +FROM prod-deps AS production RUN addgroup -g 1001 -S nodejs && adduser -S bunjs -u 1001 -RUN cp -r /root/.bun /usr/local/bun && chown -R bunjs:nodejs /usr/local/bun -COPY --from=deps --chown=bunjs:nodejs /app/node_modules ./node_modules COPY --from=deps --chown=bunjs:nodejs /app/src/generated ./src/generated - -COPY --chown=bunjs:nodejs src ./src -COPY --chown=bunjs:nodejs package.json ./ -COPY --chown=bunjs:nodejs prisma ./prisma +COPY --from=build --chown=bunjs:nodejs /app/dist ./dist USER bunjs ENV NODE_ENV=production ENV PORT=3000 -ENV PATH="/usr/local/bun/bin:$PATH" EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -sf http://localhost:3000/health || exit 1 + CMD wget -qO- http://localhost:3000/health || exit 1 -CMD ["sh", "-c", "bun src/server.ts"] \ No newline at end of file +CMD ["bun", "dist/server.js"] \ No newline at end of file diff --git a/LOCAL_DEVELOPMENT.md b/LOCAL_DEVELOPMENT.md index 706af01..1a7af3a 100644 --- a/LOCAL_DEVELOPMENT.md +++ b/LOCAL_DEVELOPMENT.md @@ -32,7 +32,7 @@ bun run local #### What the script does: 1. **Starts Postgres**: Launches the `db` container. 2. **Installs Extensions**: Pre-installs `pgcrypto`, `uuid-ossp`, and `pg_stat_statements` into the `public` schema. -3. **Loads Local Seed**: If `public` has no tables, the script loads `seed/dump.sql` into the DB. This is the default and recommended local flow. +3. **Loads Local Seed**: The script will attempt to load `seed/dump.sql` into the DB only when there are no existing user tables present in the database. If the database already contains tables (non-system tables), the seed step will be skipped to avoid accidentally overwriting existing data. This is the default and recommended local flow. 4. **Starts the API**: Launches the `api` container and waits for it to be healthy. Flags: diff --git a/scripts/setup-local.sh b/scripts/setup-local.sh index 87ebf79..3bf9e5d 100755 --- a/scripts/setup-local.sh +++ b/scripts/setup-local.sh @@ -1,3 +1,4 @@ +!/usr/bin/env bash # ============================================================================= # setup-local.sh # @@ -15,7 +16,6 @@ # --skip-seed Skip seeding entirely (just start containers) # ============================================================================= -!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -57,7 +57,7 @@ fi info "Loading environment from $ENV_FILE" set -o allexport -# shellcheck disable=SC1090 +# shellcheck disable: SC1090 source <(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$ENV_FILE" | sed 's/\r//') set +o allexport @@ -77,7 +77,7 @@ docker compose up db --build -d # 2. Wait for postgres to be healthy # ======================================================== info "Waiting for postgres to be healthy..." -RETRIES=20 +RETRIES=10 until docker compose exec -T db pg_isready -U postgres -d coc -h 127.0.0.1 -p 5432 -q 2>/dev/null; do RETRIES=$((RETRIES - 1)) if [[ $RETRIES -le 0 ]]; then @@ -92,7 +92,7 @@ info "Postgres is healthy." # 3. Install required extensions # ======================================================== info "Installing required extensions into postgres..." -docker compose exec -T db psql -U postgres -d coc <<'EXTSQL' +docker compose exec -T db psql -v ON_ERROR_STOP=1 -U postgres -d coc <<'EXTSQL' CREATE SCHEMA IF NOT EXISTS extensions; CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public; CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public; @@ -103,6 +103,8 @@ info "Extensions installed." # ======================================================== # 4. Check if seeding is required +# - If user passed --skip-seed, we skip +# - Otherwise, if the DB already contains user tables (non-system), skip seeding # ======================================================== if [[ "$SKIP_SEED" == true ]]; then warn "--skip-seed passed. Skipping dump and load." @@ -112,9 +114,21 @@ else error "$DUMP_FILE not found. Create the seed SQL at $DUMP_FILE and re-run this script." fi - info "Loading local seed SQL from $DUMP_FILE ..." - docker compose exec -T db psql -U postgres -d coc < "$DUMP_FILE" - info "Seed complete." + info "Checking whether database already has user tables..." + TABLE_COUNT=$(docker compose exec -T db psql -U postgres -d coc -t -A -c "SELECT count(*) FROM pg_catalog.pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema');" | tr -d '[:space:]' || true) + + if [[ -z "$TABLE_COUNT" ]]; then + warn "Could not determine table count; proceeding with seed." + info "Loading local seed SQL from $DUMP_FILE ..." + docker compose exec -T db psql -v ON_ERROR_STOP=1 -U postgres -d coc < "$DUMP_FILE" + info "Seed complete." + elif [[ "$TABLE_COUNT" -gt 0 ]]; then + warn "Database already has ${TABLE_COUNT} user tables; skipping seed." + else + info "No existing user tables found. Loading local seed SQL from $DUMP_FILE ..." + docker compose exec -T db psql -v ON_ERROR_STOP=1 -U postgres -d coc < "$DUMP_FILE" + info "Seed complete." + fi fi @@ -126,7 +140,7 @@ DATABASE_URL="$LOCAL_DATABASE_URL" \ docker compose up api --build -d info "Waiting for api to be healthy..." -RETRIES=15 +RETRIES=10 until curl -sf http://localhost:3000/health >/dev/null 2>&1; do RETRIES=$((RETRIES - 1)) if [[ $RETRIES -le 0 ]]; then From 780a51aff87295e691d23d3ac99c494bdec488b1 Mon Sep 17 00:00:00 2001 From: shrutiiiii Date: Tue, 31 Mar 2026 17:25:55 +0530 Subject: [PATCH 05/22] made api file production optimized --- Dockerfile | 25 ++++++++++--------------- docker-compose.yml | 1 - 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/Dockerfile b/Dockerfile index 723f1e2..7e4436f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM oven/bun:1.3 AS base -WORKDIR /app +WORKDIR /var/www/api # ----------------------------- # deps stage - cache dependencies @@ -17,15 +17,7 @@ RUN bunx prisma generate FROM deps AS build COPY . . -RUN bun build src/server.ts --outdir dist - -# ----------------------------- -# prod deps stage - install only production dependencies -# ----------------------------- -FROM base AS prod-deps - -COPY package.json bun.lock* ./ -RUN bun install --production --frozen-lockfile +RUN bun build src/server.ts --target=bun --production --outdir dist # ----------------------------- # development stage @@ -40,13 +32,16 @@ CMD ["bun", "src/server.ts"] # ----------------------------- -FROM prod-deps AS production +FROM oven/bun:1-slim AS production + +WORKDIR /var/www/api +RUN groupadd -g 1001 nodejs && useradd -u 1001 -g nodejs -m bunjs -RUN addgroup -g 1001 -S nodejs && adduser -S bunjs -u 1001 +COPY --from=build --chown=bunjs:nodejs /var/www/api/dist ./dist +COPY --from=deps --chown=bunjs:nodejs /var/www/api/src/generated ./dist/generated -COPY --from=deps --chown=bunjs:nodejs /app/src/generated ./src/generated -COPY --from=build --chown=bunjs:nodejs /app/dist ./dist +RUN chown -R bunjs:nodejs /var/www/api USER bunjs ENV NODE_ENV=production @@ -57,4 +52,4 @@ EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget -qO- http://localhost:3000/health || exit 1 -CMD ["bun", "dist/server.js"] \ No newline at end of file +CMD ["bun", "./dist/server.js"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7bd9726..46a1305 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,6 @@ services: volumes: - ./:/app:cached - /app/node_modules - command: ["bun", "src/server.ts"] depends_on: - db healthcheck: From 52b940c901f4a6c3f23263d7175cb599b0ed65a2 Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Tue, 16 Jun 2026 14:02:29 +0530 Subject: [PATCH 06/22] update schema - add SiteAction and SitePageContent - Support dynamic callofCode content --- .../migration.sql | 34 +++++++++++++++++++ prisma/schema.prisma | 27 +++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 prisma/migrations/20260614203131_add_dynamic_site_feature/migration.sql diff --git a/prisma/migrations/20260614203131_add_dynamic_site_feature/migration.sql b/prisma/migrations/20260614203131_add_dynamic_site_feature/migration.sql new file mode 100644 index 0000000..6070d35 --- /dev/null +++ b/prisma/migrations/20260614203131_add_dynamic_site_feature/migration.sql @@ -0,0 +1,34 @@ +-- CreateTable +CREATE TABLE "SitePageContent" ( + "id" INTEGER NOT NULL DEFAULT 1, + "heroImageUrl" TEXT, + "heroCaption" TEXT, + "heroAltText" TEXT, + "galleryPhotos" JSONB NOT NULL DEFAULT '[]', + "updatedById" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "SitePageContent_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SiteAction" ( + "id" SERIAL NOT NULL, + "key" TEXT NOT NULL, + "label" TEXT, + "url" TEXT, + "isVisible" BOOLEAN NOT NULL DEFAULT false, + "updatedById" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "SiteAction_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "SiteAction_key_key" ON "SiteAction"("key"); + +-- AddForeignKey +ALTER TABLE "SitePageContent" ADD CONSTRAINT "SitePageContent_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES "Member"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SiteAction" ADD CONSTRAINT "SiteAction_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES "Member"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 538e9a5..7e1cf95 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -52,6 +52,8 @@ model Member { updatedAchievements Achievement[] @relation("AchievementUpdatedBy") createdProjects Project[] @relation("ProjectCreatedBy") updatedProjects Project[] @relation("ProjectUpdatedBy") + updatedSitePageContent SitePageContent[] @relation("SitePageContentUpdatedBy") + updatedSiteActions SiteAction[] @relation("SiteActionUpdatedBy") } model Account { @@ -192,3 +194,28 @@ model CompletedQuestion { @@id([memberId, questionId]) } + +model SitePageContent { + id Int @id @default(1) + heroImageUrl String? + heroCaption String? + heroAltText String? + galleryPhotos Json @default("[]") + + updatedBy Member? @relation("SitePageContentUpdatedBy", fields: [updatedById], references: [id], onDelete: SetNull) + updatedById String? + updatedAt DateTime @updatedAt +} + +model SiteAction { + id Int @id @default(autoincrement()) + key String @unique + label String? + url String? + isVisible Boolean @default(false) + + updatedBy Member? @relation("SiteActionUpdatedBy", fields: [updatedById], references: [id], onDelete: SetNull) + updatedById String? + updatedAt DateTime @updatedAt +} + From 29ffade3ca8c02aa84061a94231a2dbe4f4413bc Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Tue, 16 Jun 2026 14:05:26 +0530 Subject: [PATCH 07/22] add Client util to avoid Parameter Tunneling --- src/utils/supabaseClient.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/utils/supabaseClient.ts diff --git a/src/utils/supabaseClient.ts b/src/utils/supabaseClient.ts new file mode 100644 index 0000000..0d9bbc4 --- /dev/null +++ b/src/utils/supabaseClient.ts @@ -0,0 +1,7 @@ +import { createClient } from "@supabase/supabase-js"; +import config from "../config"; + +export const supabase = createClient( + config.SUPABASE_URL, + config.SUPABASE_SERVICE_ROLE_KEY, +); From 6955f6bb0f7f13d285847d5625a27bb587230740 Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Tue, 16 Jun 2026 14:06:49 +0530 Subject: [PATCH 08/22] add service to support dynamic site content --- src/controllers/site-content.controller.ts | 223 ++++++++++++++++++ src/routes/index.ts | 3 + src/routes/site-content.ts | 173 ++++++++++++++ src/services/site-content.service.ts | 254 +++++++++++++++++++++ src/types/site-content.d.ts | 58 +++++ 5 files changed, 711 insertions(+) create mode 100644 src/controllers/site-content.controller.ts create mode 100644 src/routes/site-content.ts create mode 100644 src/services/site-content.service.ts create mode 100644 src/types/site-content.d.ts diff --git a/src/controllers/site-content.controller.ts b/src/controllers/site-content.controller.ts new file mode 100644 index 0000000..adab6c9 --- /dev/null +++ b/src/controllers/site-content.controller.ts @@ -0,0 +1,223 @@ +import { Request, Response } from "express"; +import * as siteContentService from "../services/site-content.service"; +import { uploadImage, deleteImage } from "../utils/imageUtils"; +import { supabase } from "../utils/supabaseClient"; +import { ApiError } from "../utils/apiError"; + +function parseSiteContentData(body: Record) { + let siteContentData = body.siteContentData ?? body; + + if (typeof siteContentData === "string") { + try { + siteContentData = JSON.parse(siteContentData); + } catch { + throw new ApiError("Invalid JSON in siteContentData field", 400); + } + } + + return siteContentData as Record; +} + +function parseActionData(body: Record) { + let actionData = body.actionData ?? body; + + if (typeof actionData === "string") { + try { + actionData = JSON.parse(actionData); + } catch { + throw new ApiError("Invalid JSON in actionData field", 400); + } + } + + return actionData as Record; +} + +function parsePhotoData(body: Record) { + let photoData = body.photoData ?? body; + + if (typeof photoData === "string") { + try { + photoData = JSON.parse(photoData); + } catch { + throw new ApiError("Invalid JSON in photoData field", 400); + } + } + + return photoData as Record; +} + +export const getSiteContent = async (_req: Request, res: Response) => { + const content = await siteContentService.getSiteContent(); + + res.status(200).json({ + success: true, + data: content, + }); +}; + +export const updateSiteContent = async (req: Request, res: Response) => { + const siteContentData = parseSiteContentData(req.body); + const adminId = siteContentData.adminId as string | undefined; + + if (!adminId) { + throw new ApiError("adminId is required", 400); + } + + const file = req.file; + let heroImageUrl: string | undefined; + + if (file) { + const current = await siteContentService.getSiteContent(); + heroImageUrl = await uploadImage( + supabase, + file, + "group-photos", + current.hero.imageUrl ?? undefined, + ); + } + + const content = await siteContentService.updateSitePageContent(adminId, { + heroCaption: siteContentData.heroCaption as string | null | undefined, + heroAltText: siteContentData.heroAltText as string | null | undefined, + heroImageUrl, + }); + + res.status(200).json({ + success: true, + data: content, + }); +}; + +export const updateSiteAction = async (req: Request, res: Response) => { + const key = req.params.key; + if (!key) { + throw new ApiError("Action key is required", 400); + } + + const actionData = parseActionData(req.body); + const adminId = actionData.adminId as string | undefined; + + if (!adminId) { + throw new ApiError("adminId is required", 400); + } + + const content = await siteContentService.updateSiteAction(adminId, key, { + label: actionData.label as string | null | undefined, + url: actionData.url as string | null | undefined, + isVisible: actionData.isVisible as boolean | undefined, + }); + + res.status(200).json({ + success: true, + data: content, + }); +}; + +export const addGalleryPhoto = async (req: Request, res: Response) => { + const file = req.file; + if (!file) { + throw new ApiError("Image file is required", 400); + } + + const photoData = parsePhotoData(req.body); + const adminId = photoData.adminId as string | undefined; + + if (!adminId) { + throw new ApiError("adminId is required", 400); + } + + const imageUrl = await uploadImage(supabase, file, "group-photos"); + if (!imageUrl) { + throw new ApiError("Image URL is missing", 400); + } + + const content = await siteContentService.addGalleryPhoto(adminId, { + imageUrl, + caption: photoData.caption as string | undefined, + altText: photoData.altText as string | undefined, + sortOrder: photoData.sortOrder as number | undefined, + }); + + res.status(201).json({ + success: true, + data: content, + }); +}; + +export const updateGalleryPhoto = async (req: Request, res: Response) => { + const photoId = req.params.photoId; + if (!photoId) { + throw new ApiError("Photo ID is required", 400); + } + + const photoData = parsePhotoData(req.body); + const adminId = photoData.adminId as string | undefined; + + if (!adminId) { + throw new ApiError("adminId is required", 400); + } + + const file = req.file; + let imageUrl: string | undefined; + + if (file) { + const existing = await siteContentService.getGalleryPhoto(adminId, photoId); + imageUrl = await uploadImage( + supabase, + file, + "group-photos", + existing.imageUrl, + ); + } + + const hasUpdate = + imageUrl !== undefined || + photoData.caption !== undefined || + photoData.altText !== undefined || + photoData.sortOrder !== undefined; + + if (!hasUpdate) { + throw new ApiError( + "At least one field (image, caption, altText, or sortOrder) must be provided", + 400, + ); + } + + const { content, previousImageUrl } = + await siteContentService.updateGalleryPhoto(adminId, photoId, { + imageUrl, + caption: photoData.caption as string | null | undefined, + altText: photoData.altText as string | null | undefined, + sortOrder: photoData.sortOrder as number | undefined, + }); + + if (previousImageUrl) { + await deleteImage(supabase, previousImageUrl); + } + + res.status(200).json({ + success: true, + data: content, + }); +}; + +export const deleteGalleryPhoto = async (req: Request, res: Response) => { + const photoId = req.params.photoId; + const adminId = req.body.adminId as string | undefined; + + if (!photoId) { + throw new ApiError("Photo ID is required", 400); + } + + if (!adminId) { + throw new ApiError("adminId is required", 400); + } + + const imageUrl = await siteContentService.deleteGalleryPhoto(adminId, photoId); + await deleteImage(supabase, imageUrl); + + res.status(200).json({ + success: true, + message: "Gallery photo deleted successfully", + }); +}; diff --git a/src/routes/index.ts b/src/routes/index.ts index 74757ee..1089a3f 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -8,6 +8,7 @@ import topicRouter from './topics' import quetionsRouter from './questions' import progressRouter from './progress' import membersRouter from './members' +import siteContentRouter from './site-content' export default function routes(upload: Multer, supabase: SupabaseClient) { const router = Router(); @@ -25,6 +26,8 @@ export default function routes(upload: Multer, supabase: SupabaseClient) { router.use("/progress", progressRouter()); + router.use("/site-content", siteContentRouter(upload)); + return router; } diff --git a/src/routes/site-content.ts b/src/routes/site-content.ts new file mode 100644 index 0000000..4d7e5d6 --- /dev/null +++ b/src/routes/site-content.ts @@ -0,0 +1,173 @@ +import express from "express"; +import { Multer } from "multer"; +import { Request, Response, NextFunction } from "express"; +import * as siteContentCtrl from "../controllers/site-content.controller"; + +function parseSiteContentData(req: Request, res: Response, next: NextFunction) { + if (req.body.siteContentData) { + try { + req.body.siteContentData = JSON.parse(req.body.siteContentData); + } catch { + return res + .status(400) + .json({ message: "Invalid JSON in siteContentData field" }); + } + } + next(); +} + +function parsePhotoData(req: Request, res: Response, next: NextFunction) { + if (req.body.photoData) { + try { + req.body.photoData = JSON.parse(req.body.photoData); + } catch { + return res.status(400).json({ message: "Invalid JSON in photoData field" }); + } + } + next(); +} + +function parseActionData(req: Request, res: Response, next: NextFunction) { + if (req.body.actionData) { + try { + req.body.actionData = JSON.parse(req.body.actionData); + } catch { + return res.status(400).json({ message: "Invalid JSON in actionData field" }); + } + } + next(); +} + +export default function siteContentRouter(upload: Multer) { + const router = express.Router(); + + /** + * @api {get} /site-content Get published site content + * @apiName getSiteContent + * @apiGroup SiteContent + * + * @apiSuccess {Boolean} success Request status + * @apiSuccess {Object} data Site content including actions, hero, and gallery + * @apiError (500) InternalServerError Failed to fetch site content + */ + router.get("/", siteContentCtrl.getSiteContent); + + /** + * @api {patch} /site-content Update site content + * @apiName updateSiteContent + * @apiGroup SiteContent + * + * @apiBody (FormData) {File} [image] Optional hero image file + * @apiBody (FormData) {String} siteContentData JSON string of fields: + * - adminId: string (required) + * - heroCaption?: string + * - heroAltText?: string + * + * @apiSuccess {Boolean} success Request status + * @apiSuccess {Object} data Updated site content + * @apiError (400) BadRequest Missing or invalid data + * @apiError (403) Forbidden Manager access required + * @apiError (500) InternalServerError Server error + */ + router.patch( + "/", + upload.single("image"), + parseSiteContentData, + siteContentCtrl.updateSiteContent, + ); + + /** + * @api {patch} /site-content/actions/:key Update a site action + * @apiName updateSiteAction + * @apiGroup SiteContent + * + * @apiParam (Path Params) {String} key Action key (e.g. recruitment) + * @apiBody {String} adminId ID of the manager (required) + * @apiBody {String} [label] Button label + * @apiBody {String} [url] Action URL + * @apiBody {Boolean} [isVisible] Whether the action is visible + * + * @apiSuccess {Boolean} success Request status + * @apiSuccess {Object} data Updated site content + * @apiError (400) BadRequest Missing or invalid data + * @apiError (403) Forbidden Manager access required + * @apiError (404) NotFound Site action not found + * @apiError (500) InternalServerError Server error + */ + router.patch( + "/actions/:key", + parseActionData, + siteContentCtrl.updateSiteAction, + ); + + /** + * @api {post} /site-content/gallery Add a gallery photo + * @apiName addGalleryPhoto + * @apiGroup SiteContent + * + * @apiBody (FormData) {File} image Image file + * @apiBody (FormData) {String} photoData JSON string of fields: + * - adminId: string (required) + * - caption?: string + * - altText?: string + * - sortOrder?: number + * + * @apiSuccess {Boolean} success Request status + * @apiSuccess {Object} data Updated site content + * @apiError (400) BadRequest Missing or invalid data + * @apiError (403) Forbidden Manager access required + * @apiError (500) InternalServerError Server error + */ + router.post( + "/gallery", + upload.single("image"), + parsePhotoData, + siteContentCtrl.addGalleryPhoto, + ); + + /** + * @api {patch} /site-content/gallery/:photoId Update a gallery photo + * @apiName updateGalleryPhoto + * @apiGroup SiteContent + * + * @apiParam (Path Params) {String} photoId Gallery photo ID + * @apiBody (FormData) {File} [image] Optional new image + * @apiBody (FormData) {String} photoData JSON string of fields: + * - adminId: string (required) + * - caption?: string + * - altText?: string + * - sortOrder?: number + * + * @apiSuccess {Boolean} success Request status + * @apiSuccess {Object} data Updated site content + * @apiError (400) BadRequest Missing or invalid data + * @apiError (403) Forbidden Manager access required + * @apiError (404) NotFound Gallery photo not found + * @apiError (500) InternalServerError Server error + */ + router.patch( + "/gallery/:photoId", + upload.single("image"), + parsePhotoData, + siteContentCtrl.updateGalleryPhoto, + ); + + /** + * @api {delete} /site-content/gallery/:photoId Delete a gallery photo + * @apiName deleteGalleryPhoto + * @apiGroup SiteContent + * + * @apiParam (Path Params) {String} photoId Gallery photo ID + * @apiBody {String} adminId ID of the manager performing the delete + * + * @apiSuccess {Boolean} success Request status + * @apiSuccess {String} message Deletion confirmation + * @apiError (400) BadRequest Missing or invalid data + * @apiError (403) Forbidden Manager access required + * @apiError (404) NotFound Gallery photo not found + * @apiError (500) InternalServerError Server error + */ + router.delete("/gallery/:photoId", siteContentCtrl.deleteGalleryPhoto); + + return router; +} diff --git a/src/services/site-content.service.ts b/src/services/site-content.service.ts new file mode 100644 index 0000000..f704525 --- /dev/null +++ b/src/services/site-content.service.ts @@ -0,0 +1,254 @@ +import { v4 as uuidv4 } from "uuid"; +import prisma from "../db/client"; +import { ApiError } from "../utils/apiError"; +import { Prisma } from "../generated/prisma/client"; + +const PAGE_CONTENT_ID = 1; + +function toGalleryJson(gallery: GalleryPhoto[]): Prisma.InputJsonValue { + return gallery as unknown as Prisma.InputJsonValue; +} + +async function assertManager(adminId: string) { + const member = await prisma.member.findUnique({ + where: { id: adminId }, + select: { isManager: true }, + }); + + if (!member?.isManager) { + throw new ApiError("Forbidden: manager access required", 403); + } +} + +function parseGalleryPhotos(value: unknown): GalleryPhoto[] { + if (!Array.isArray(value)) { + return []; + } + + return value + .filter( + (item): item is GalleryPhoto => + typeof item === "object" && + item !== null && + typeof (item as GalleryPhoto).id === "string" && + typeof (item as GalleryPhoto).imageUrl === "string" && + typeof (item as GalleryPhoto).sortOrder === "number", + ) + .sort((a, b) => a.sortOrder - b.sortOrder); +} + +function toSiteContentResponse( + page: { + heroImageUrl: string | null; + heroCaption: string | null; + heroAltText: string | null; + galleryPhotos: unknown; + }, + actions: { + key: string; + label: string | null; + url: string | null; + isVisible: boolean; + }[], +): SiteContentResponse { + return { + actions: actions.map(({ key, label, url, isVisible }) => ({ + key, + label, + url, + isVisible, + })), + hero: { + imageUrl: page.heroImageUrl, + caption: page.heroCaption, + altText: page.heroAltText, + }, + gallery: parseGalleryPhotos(page.galleryPhotos), + }; +} + +async function getPageContent() { + return prisma.sitePageContent.findUniqueOrThrow({ + where: { id: PAGE_CONTENT_ID }, + }); +} + +async function getActions() { + return prisma.siteAction.findMany({ + orderBy: { key: "asc" }, + }); +} + +async function buildSiteContentResponse(): Promise { + const [page, actions] = await Promise.all([getPageContent(), getActions()]); + return toSiteContentResponse(page, actions); +} + +export async function getSiteContent(): Promise { + return buildSiteContentResponse(); +} + +export async function updateSitePageContent( + adminId: string, + data: Omit, +) { + await assertManager(adminId); + + const page = await prisma.sitePageContent.update({ + where: { id: PAGE_CONTENT_ID }, + data: { + heroCaption: data.heroCaption, + heroAltText: data.heroAltText, + heroImageUrl: data.heroImageUrl, + updatedById: adminId, + }, + }); + + const actions = await getActions(); + return toSiteContentResponse(page, actions); +} + +export async function updateSiteAction( + adminId: string, + key: string, + data: Omit, +) { + await assertManager(adminId); + + const current = await prisma.siteAction.findUnique({ where: { key } }); + if (!current) { + throw new ApiError("Site action not found", 404); + } + + const isVisible = data.isVisible ?? current.isVisible; + const url = data.url !== undefined ? data.url : current.url; + + if (isVisible && !url) { + throw new ApiError("url is required when action is visible", 400); + } + console.log(key) + await prisma.siteAction.update({ + where: { key }, + data: { + label: data.label, + url: data.url, + isVisible: data.isVisible, + updatedById: adminId, + }, + }); + + return buildSiteContentResponse(); +} + +export async function addGalleryPhoto( + adminId: string, + data: Omit, +) { + await assertManager(adminId); + + const current = await getPageContent(); + const gallery = parseGalleryPhotos(current.galleryPhotos); + + const photo: GalleryPhoto = { + id: uuidv4(), + imageUrl: data.imageUrl, + caption: data.caption, + altText: data.altText, + sortOrder: + data.sortOrder ?? + (gallery.length > 0 + ? Math.max(...gallery.map((p) => p.sortOrder)) + 1 + : 0), + }; + + await prisma.sitePageContent.update({ + where: { id: PAGE_CONTENT_ID }, + data: { + galleryPhotos: toGalleryJson([...gallery, photo]), + updatedById: adminId, + }, + }); + + return buildSiteContentResponse(); +} + +export async function updateGalleryPhoto( + adminId: string, + photoId: string, + data: Omit, +) { + await assertManager(adminId); + + const current = await getPageContent(); + const gallery = parseGalleryPhotos(current.galleryPhotos); + const index = gallery.findIndex((photo) => photo.id === photoId); + + if (index === -1) { + throw new ApiError("Gallery photo not found", 404); + } + + const existing = gallery[index]; + gallery[index] = { + ...existing, + imageUrl: data.imageUrl ?? existing.imageUrl, + caption: + data.caption !== undefined ? (data.caption ?? undefined) : existing.caption, + altText: + data.altText !== undefined ? (data.altText ?? undefined) : existing.altText, + sortOrder: data.sortOrder ?? existing.sortOrder, + }; + + await prisma.sitePageContent.update({ + where: { id: PAGE_CONTENT_ID }, + data: { + galleryPhotos: toGalleryJson( + gallery.sort((a, b) => a.sortOrder - b.sortOrder), + ), + updatedById: adminId, + }, + }); + + return { + content: await buildSiteContentResponse(), + previousImageUrl: + data.imageUrl && data.imageUrl !== existing.imageUrl + ? existing.imageUrl + : undefined, + }; +} + +export async function deleteGalleryPhoto(adminId: string, photoId: string) { + await assertManager(adminId); + + const current = await getPageContent(); + const gallery = parseGalleryPhotos(current.galleryPhotos); + const photo = gallery.find((item) => item.id === photoId); + + if (!photo) { + throw new ApiError("Gallery photo not found", 404); + } + + await prisma.sitePageContent.update({ + where: { id: PAGE_CONTENT_ID }, + data: { + galleryPhotos: toGalleryJson(gallery.filter((item) => item.id !== photoId)), + updatedById: adminId, + }, + }); + + return photo.imageUrl; +} + +export async function getGalleryPhoto(adminId: string, photoId: string) { + await assertManager(adminId); + + const current = await getPageContent(); + const gallery = parseGalleryPhotos(current.galleryPhotos); + const photo = gallery.find((item) => item.id === photoId); + + if (!photo) { + throw new ApiError("Gallery photo not found", 404); + } + + return photo; +} diff --git a/src/types/site-content.d.ts b/src/types/site-content.d.ts new file mode 100644 index 0000000..99754bb --- /dev/null +++ b/src/types/site-content.d.ts @@ -0,0 +1,58 @@ +export {}; + +declare global { + interface GalleryPhoto { + id: string; + imageUrl: string; + caption?: string; + altText?: string; + sortOrder: number; + } + + interface SiteActionItem { + key: string; + label: string | null; + url: string | null; + isVisible: boolean; + } + + interface SiteContentResponse { + actions: SiteActionItem[]; + hero: { + imageUrl: string | null; + caption: string | null; + altText: string | null; + }; + gallery: GalleryPhoto[]; + } + + interface UpdateSitePageContentInput { + heroCaption?: string | null; + heroAltText?: string | null; + heroImageUrl?: string | null; + updatedById: string; + } + + interface UpdateSiteActionInput { + label?: string | null; + url?: string | null; + isVisible?: boolean; + updatedById: string; + } + + interface CreateGalleryPhotoInput { + imageUrl: string; + caption?: string; + altText?: string; + sortOrder?: number; + createdById: string; + } + + interface UpdateGalleryPhotoInput { + imageUrl?: string; + caption?: string | null; + altText?: string | null; + sortOrder?: number; + updatedById: string; + } +} From 6206abe500ebe9a2e8592f1c4a7129253c911a7c Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Tue, 16 Jun 2026 14:07:36 +0530 Subject: [PATCH 09/22] fix server Api doc rendering and path --- src/app.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/app.ts b/src/app.ts index 2a4470d..ca51aef 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,16 +4,11 @@ import multer from "multer"; import { json, urlencoded } from "body-parser"; import routes from "./routes"; import { errorHandler } from "./utils/apiError"; -import { createClient } from "@supabase/supabase-js"; -import config from "./config"; import path from "path"; import { logger } from "./utils/logger"; import morgan from "morgan"; -// Initialize Supabase client for storage operations -export const supabase = createClient( - config.SUPABASE_URL, - config.SUPABASE_SERVICE_ROLE_KEY, -); +import { supabase } from "./utils/supabaseClient"; +import config from "./config"; const app = express(); class LoggerStream { @@ -46,6 +41,9 @@ app.use("/health",(req,res)=>{ app.use("/api/v1", routes(upload, supabase)); +// Serve API documentation +app.use("/docs", express.static(path.join(__dirname, "..", "doc"))); + // 404 handler app.use((req, res) => { res.status(404).json({ message: "Not Found" }); @@ -55,6 +53,4 @@ app.use((req, res) => { // Global error handler app.use(errorHandler); -// Serve API documentation -app.use("/docs", express.static(path.join(__dirname, "..", "docs/apidoc"))); export default app; From 26ea8a88415103dcfcedff732499f06221d4ccdd Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Tue, 16 Jun 2026 14:07:57 +0530 Subject: [PATCH 10/22] Site content test --- tests/SiteContent.test.ts | 301 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 tests/SiteContent.test.ts diff --git a/tests/SiteContent.test.ts b/tests/SiteContent.test.ts new file mode 100644 index 0000000..fc26ebe --- /dev/null +++ b/tests/SiteContent.test.ts @@ -0,0 +1,301 @@ +import { + getSiteContent, + updateSiteContent, + updateSiteAction, + addGalleryPhoto, + deleteGalleryPhoto, +} from "../src/controllers/site-content.controller"; +import * as siteContentService from "../src/services/site-content.service"; +import { uploadImage, deleteImage } from "../src/utils/imageUtils"; +import { ApiError } from "../src/utils/apiError"; + +jest.mock("../src/utils/supabaseClient", () => ({ + supabase: { + storage: { + from: jest.fn(() => ({ + upload: jest + .fn() + .mockResolvedValue({ data: { path: "fake-path" }, error: null }), + remove: jest.fn().mockResolvedValue({ data: null, error: null }), + })), + }, + }, +})); + +jest.mock("../src/db/client", () => ({ + __esModule: true, + default: { + member: { + findUnique: jest.fn(), + }, + sitePageContent: { + findUniqueOrThrow: jest.fn(), + update: jest.fn(), + }, + siteAction: { + findMany: jest.fn(), + findUnique: jest.fn(), + update: jest.fn(), + }, + }, +})); + +jest.mock("../src/utils/imageUtils", () => ({ + uploadImage: jest.fn(), + deleteImage: jest.fn(), +})); + +const mockedUploadImage = uploadImage as jest.Mock; +const mockedDeleteImage = deleteImage as jest.Mock; + +afterEach(() => { + jest.restoreAllMocks(); +}); + +const mockContent: SiteContentResponse = { + actions: [ + { + key: "recruitment", + isVisible: true, + url: "https://forms.example.com/recruit", + label: "Join Us", + }, + ], + hero: { + imageUrl: "https://example.com/hero.jpg", + caption: "Our team", + altText: "Group photo", + }, + gallery: [ + { + id: "photo-1", + imageUrl: "https://example.com/gallery-1.jpg", + caption: "Event 2025", + sortOrder: 0, + }, + ], +}; + +describe("getSiteContent", () => { + it("should return site content", async () => { + const req: any = {}; + const res: any = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + + jest + .spyOn(siteContentService, "getSiteContent") + .mockResolvedValue(mockContent); + + await getSiteContent(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + success: true, + data: mockContent, + }); + }); +}); + +describe("updateSiteContent", () => { + it("should update hero content for a manager", async () => { + const req: any = { + body: { + siteContentData: { + adminId: "manager-1", + heroCaption: "Our team", + heroAltText: "Group photo", + }, + }, + }; + const res: any = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + + jest + .spyOn(siteContentService, "updateSitePageContent") + .mockResolvedValue(mockContent); + + await updateSiteContent(req, res); + + expect(siteContentService.updateSitePageContent).toHaveBeenCalledWith( + "manager-1", + { + heroCaption: "Our team", + heroAltText: "Group photo", + heroImageUrl: undefined, + }, + ); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it("should throw when adminId is missing", async () => { + const req: any = { + body: { + siteContentData: { + heroCaption: "Our team", + }, + }, + }; + const res: any = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + + await expect(updateSiteContent(req, res)).rejects.toThrow( + new ApiError("adminId is required", 400), + ); + }); +}); + +describe("updateSiteAction", () => { + it("should update a site action", async () => { + const req: any = { + params: { key: "recruitment" }, + body: { + actionData: { + adminId: "manager-1", + isVisible: true, + url: "https://forms.example.com/recruit", + label: "Join Us", + }, + }, + }; + const res: any = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + + jest + .spyOn(siteContentService, "updateSiteAction") + .mockResolvedValue(mockContent); + + await updateSiteAction(req, res); + + expect(siteContentService.updateSiteAction).toHaveBeenCalledWith( + "manager-1", + "recruitment", + { + isVisible: true, + url: "https://forms.example.com/recruit", + label: "Join Us", + }, + ); + expect(res.status).toHaveBeenCalledWith(200); + }); +}); + +describe("addGalleryPhoto", () => { + it("should add a gallery photo and return updated content", async () => { + const req: any = { + file: { + originalname: "event.png", + buffer: Buffer.from("test"), + }, + body: { + photoData: { + adminId: "manager-1", + caption: "Hackathon", + sortOrder: 1, + }, + }, + }; + const res: any = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + + mockedUploadImage.mockResolvedValue("https://example.com/event.jpg"); + jest + .spyOn(siteContentService, "addGalleryPhoto") + .mockResolvedValue(mockContent); + + await addGalleryPhoto(req, res); + + expect(siteContentService.addGalleryPhoto).toHaveBeenCalledWith( + "manager-1", + { + imageUrl: "https://example.com/event.jpg", + caption: "Hackathon", + altText: undefined, + sortOrder: 1, + }, + ); + expect(res.status).toHaveBeenCalledWith(201); + }); +}); + +describe("deleteGalleryPhoto", () => { + it("should delete a gallery photo and its image", async () => { + const req: any = { + params: { photoId: "photo-1" }, + body: { adminId: "manager-1" }, + }; + const res: any = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + + jest + .spyOn(siteContentService, "deleteGalleryPhoto") + .mockResolvedValue("https://example.com/gallery-1.jpg"); + mockedDeleteImage.mockResolvedValue(undefined); + + await deleteGalleryPhoto(req, res); + + expect(siteContentService.deleteGalleryPhoto).toHaveBeenCalledWith( + "manager-1", + "photo-1", + ); + expect(mockedDeleteImage).toHaveBeenCalledWith( + expect.anything(), + "https://example.com/gallery-1.jpg", + ); + expect(res.status).toHaveBeenCalledWith(200); + }); +}); + +describe("site-content service guards", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should reject non-manager adminId", async () => { + const prisma = (await import("../src/db/client")).default; + + (prisma.member.findUnique as jest.Mock).mockResolvedValue({ + isManager: false, + }); + + await expect( + siteContentService.updateSiteAction("user-1", "recruitment", { + isVisible: true, + url: "https://example.com", + }), + ).rejects.toThrow(new ApiError("Forbidden: manager access required", 403)); + }); + + it("should reject visible action without URL", async () => { + const prisma = (await import("../src/db/client")).default; + + (prisma.member.findUnique as jest.Mock).mockResolvedValue({ + isManager: true, + }); + (prisma.siteAction.findUnique as jest.Mock).mockResolvedValue({ + key: "recruitment", + label: null, + url: null, + isVisible: false, + }); + + await expect( + siteContentService.updateSiteAction("manager-1", "recruitment", { + isVisible: true, + }), + ).rejects.toThrow( + new ApiError("url is required when action is visible", 400), + ); + }); +}); From 0a123f4d51f91ea2865be39279557ab9c35118ff Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Tue, 16 Jun 2026 14:22:05 +0530 Subject: [PATCH 11/22] change supabase client --- src/app.ts | 2 +- src/controllers/achievement.controller.ts | 2 +- src/controllers/project.controller.ts | 2 +- src/routes/achievements.ts | 3 +-- src/routes/index.ts | 8 ++++---- src/routes/members.ts | 4 ++-- src/routes/projects.ts | 4 +--- 7 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/app.ts b/src/app.ts index ca51aef..8f535ad 100644 --- a/src/app.ts +++ b/src/app.ts @@ -39,7 +39,7 @@ app.use("/health",(req,res)=>{ res.status(200).json({ message: "OK" }); }) -app.use("/api/v1", routes(upload, supabase)); +app.use("/api/v1", routes(upload)); // Serve API documentation app.use("/docs", express.static(path.join(__dirname, "..", "doc"))); diff --git a/src/controllers/achievement.controller.ts b/src/controllers/achievement.controller.ts index b7e176d..6edc1b4 100644 --- a/src/controllers/achievement.controller.ts +++ b/src/controllers/achievement.controller.ts @@ -1,8 +1,8 @@ import { Request, Response } from "express"; import * as achievementService from "../services/achievement.service"; import { uploadImage, deleteImage } from "../utils/imageUtils"; -import { supabase } from "../app"; import { ApiError } from "../utils/apiError"; +import { supabase } from "../utils/supabaseClient"; export const getAchievements = async (req: Request, res: Response) => { const achievements = await achievementService.getAchievements(); diff --git a/src/controllers/project.controller.ts b/src/controllers/project.controller.ts index 1821604..c8be4b9 100644 --- a/src/controllers/project.controller.ts +++ b/src/controllers/project.controller.ts @@ -2,7 +2,7 @@ import * as projectService from "../services/project.service"; import { Request, Response } from "express"; import { ApiError } from "../utils/apiError"; import { deleteImage, uploadImage } from "../utils/imageUtils"; -import { supabase } from "../app"; +import { supabase } from "../utils/supabaseClient"; export const getProjects = async (req: Request, res: Response) => { diff --git a/src/routes/achievements.ts b/src/routes/achievements.ts index 6e892e5..0f76d8a 100644 --- a/src/routes/achievements.ts +++ b/src/routes/achievements.ts @@ -1,7 +1,6 @@ import express from 'express'; import * as acheivementsCtrl from '../controllers/achievement.controller'; import { Multer } from 'multer'; -import { SupabaseClient } from '@supabase/supabase-js'; import { Request, Response,NextFunction } from 'express'; @@ -18,7 +17,7 @@ export function parseCreateAchievementData(req: Request, res: Response, next: Ne -export default function acheivementsRouter(upload: Multer, supabase: SupabaseClient) { +export default function acheivementsRouter(upload: Multer) { const router = express.Router(); diff --git a/src/routes/index.ts b/src/routes/index.ts index 1089a3f..80a3e14 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -10,13 +10,13 @@ import progressRouter from './progress' import membersRouter from './members' import siteContentRouter from './site-content' -export default function routes(upload: Multer, supabase: SupabaseClient) { +export default function routes(upload: Multer) { const router = Router(); - router.use('/members', membersRouter(upload, supabase)) + router.use('/members', membersRouter(upload)) - router.use('/projects', projectsRouter(upload, supabase)) + router.use('/projects', projectsRouter(upload)) - router.use('/achievements' ,acheivementsRouter(upload, supabase)); + router.use('/achievements' ,acheivementsRouter(upload)); router.use('/interviews', interviewRouter()); diff --git a/src/routes/members.ts b/src/routes/members.ts index 1a7addc..0b4b331 100644 --- a/src/routes/members.ts +++ b/src/routes/members.ts @@ -1,11 +1,11 @@ import express from "express"; import * as memberCtrl from "../controllers/member.controller"; import { Multer } from "multer"; -import { SupabaseClient } from "@supabase/supabase-js"; +import { supabase } from "../utils/supabaseClient"; + export default function membersRouter( upload: Multer, - supabase: SupabaseClient, ) { const router = express.Router(); diff --git a/src/routes/projects.ts b/src/routes/projects.ts index 8463e62..002086b 100644 --- a/src/routes/projects.ts +++ b/src/routes/projects.ts @@ -1,6 +1,5 @@ import { NextFunction, Request, Response, Router } from 'express' import { Multer } from 'multer' -import { SupabaseClient } from '@supabase/supabase-js' import { addMembers, createProject, @@ -26,8 +25,7 @@ function parseProjectData(req: Request, res: Response, next: NextFunction) { } export default function projectsRouter( - upload: Multer, - supabase: SupabaseClient, + upload: Multer, ) { const router = Router(); From 5130843345f2e9944f8cd13a2d78b9b6a2d441f4 Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Tue, 16 Jun 2026 14:41:58 +0530 Subject: [PATCH 12/22] change test to support supabase client --- tests/Achievement.test.ts | 2 +- tests/Project.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Achievement.test.ts b/tests/Achievement.test.ts index 620a32e..8c796be 100644 --- a/tests/Achievement.test.ts +++ b/tests/Achievement.test.ts @@ -10,7 +10,7 @@ import * as achievementService from "../src/services/achievement.service"; import { uploadImage, deleteImage } from "../src/utils/imageUtils"; import { ApiError } from "../src/utils/apiError"; -jest.mock("../src/app", () => ({ +jest.mock("../src/utils/supabaseClient", () => ({ supabase: { storage: { from: jest.fn(() => ({ diff --git a/tests/Project.test.ts b/tests/Project.test.ts index 6678b06..2570f4c 100644 --- a/tests/Project.test.ts +++ b/tests/Project.test.ts @@ -7,7 +7,7 @@ import { Response , Request} from 'express'; import * as imageUtils from '../src/utils/imageUtils'; -jest.mock('../src/app', () => ({ +jest.mock('../src/utils/supabaseClient', () => ({ supabase: { storage: { from: jest.fn(() => ({ From ed5acdbc8aae239bb1cca4804d0df800650936c3 Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Tue, 16 Jun 2026 15:23:24 +0530 Subject: [PATCH 13/22] fix double delete --- src/controllers/site-content.controller.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/controllers/site-content.controller.ts b/src/controllers/site-content.controller.ts index adab6c9..184768d 100644 --- a/src/controllers/site-content.controller.ts +++ b/src/controllers/site-content.controller.ts @@ -183,7 +183,7 @@ export const updateGalleryPhoto = async (req: Request, res: Response) => { ); } - const { content, previousImageUrl } = + const { content} = await siteContentService.updateGalleryPhoto(adminId, photoId, { imageUrl, caption: photoData.caption as string | null | undefined, @@ -191,9 +191,6 @@ export const updateGalleryPhoto = async (req: Request, res: Response) => { sortOrder: photoData.sortOrder as number | undefined, }); - if (previousImageUrl) { - await deleteImage(supabase, previousImageUrl); - } res.status(200).json({ success: true, From 0fac96672b858e54688f748ffda2c9f09c44dfac Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Tue, 16 Jun 2026 19:39:04 +0530 Subject: [PATCH 14/22] feat: add EmailTemplate model and CRUD endpoints for managing email templates --- .../migration.sql | 23 +++++ prisma/schema.prisma | 17 ++++ src/controllers/emailTemplate.controller.ts | 66 +++++++++++++ src/routes/email.ts | 94 +++++++++++++++++++ src/routes/index.ts | 2 + src/services/emailTemplate.service.ts | 47 ++++++++++ 6 files changed, 249 insertions(+) create mode 100644 prisma/migrations/20260616135723_email_format_support/migration.sql create mode 100644 src/controllers/emailTemplate.controller.ts create mode 100644 src/routes/email.ts create mode 100644 src/services/emailTemplate.service.ts diff --git a/prisma/migrations/20260616135723_email_format_support/migration.sql b/prisma/migrations/20260616135723_email_format_support/migration.sql new file mode 100644 index 0000000..7826244 --- /dev/null +++ b/prisma/migrations/20260616135723_email_format_support/migration.sql @@ -0,0 +1,23 @@ +-- CreateTable +CREATE TABLE "EmailTemplate" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "subject" TEXT NOT NULL, + "htmlBody" TEXT NOT NULL, + "textBody" TEXT, + "createdById" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedById" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "EmailTemplate_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "EmailTemplate_name_key" ON "EmailTemplate"("name"); + +-- AddForeignKey +ALTER TABLE "EmailTemplate" ADD CONSTRAINT "EmailTemplate_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "Member"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "EmailTemplate" ADD CONSTRAINT "EmailTemplate_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES "Member"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7e1cf95..577e789 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -54,6 +54,8 @@ model Member { updatedProjects Project[] @relation("ProjectUpdatedBy") updatedSitePageContent SitePageContent[] @relation("SitePageContentUpdatedBy") updatedSiteActions SiteAction[] @relation("SiteActionUpdatedBy") + createdEmailTemplates EmailTemplate[] @relation("EmailTemplateCreatedBy") + updatedEmailTemplates EmailTemplate[] @relation("EmailTemplateUpdatedBy") } model Account { @@ -219,3 +221,18 @@ model SiteAction { updatedAt DateTime @updatedAt } + +model EmailTemplate { + id Int @id @default(autoincrement()) + name String @unique + subject String + htmlBody String + textBody String? + + createdBy Member? @relation("EmailTemplateCreatedBy", fields: [createdById], references: [id], onDelete: SetNull) + createdById String? + createdAt DateTime @default(now()) + updatedBy Member? @relation("EmailTemplateUpdatedBy", fields: [updatedById], references: [id], onDelete: SetNull) + updatedById String? + updatedAt DateTime @updatedAt +} diff --git a/src/controllers/emailTemplate.controller.ts b/src/controllers/emailTemplate.controller.ts new file mode 100644 index 0000000..ad27a33 --- /dev/null +++ b/src/controllers/emailTemplate.controller.ts @@ -0,0 +1,66 @@ +import { Request, Response } from "express"; +import * as emailTemplateService from "../services/emailTemplate.service"; +import { ApiError } from "../utils/apiError"; + +// GET /email/templates +export const listTemplates = async (_req: Request, res: Response) => { + const templates = await emailTemplateService.listTemplates(); + res.status(200).json({ success: true, templates }); +}; + +// GET /email/templates/:id +export const getTemplate = async (req: Request, res: Response) => { + const id = Number(req.params.id); + if (isNaN(id)) throw new ApiError("Invalid template id", 400); + + const template = await emailTemplateService.getTemplateById(id); + if (!template) throw new ApiError("Template not found", 404); + + res.status(200).json({ success: true, template }); +}; + +// POST /email/templates +export const createTemplate = async (req: Request, res: Response) => { + const { name, subject, htmlBody, textBody, createdById } = req.body; + + if (!name || !subject || !htmlBody) { + throw new ApiError("name, subject and htmlBody are required", 400); + } + + const template = await emailTemplateService.createTemplate( + name, + subject, + htmlBody, + textBody, + createdById, + ); + + res.status(201).json({ success: true, template }); +}; + +// PATCH /email/templates/:id +export const updateTemplate = async (req: Request, res: Response) => { + const id = Number(req.params.id); + if (isNaN(id)) throw new ApiError("Invalid template id", 400); + + const { name, subject, htmlBody, textBody, updatedById } = req.body; + + const template = await emailTemplateService.updateTemplate(id, { + name, + subject, + htmlBody, + textBody, + updatedById, + }); + + res.status(200).json({ success: true, template }); +}; + +// DELETE /email/templates/:id +export const deleteTemplate = async (req: Request, res: Response) => { + const id = Number(req.params.id); + if (isNaN(id)) throw new ApiError("Invalid template id", 400); + + await emailTemplateService.deleteTemplate(id); + res.status(200).json({ success: true, message: "Template deleted" }); +}; diff --git a/src/routes/email.ts b/src/routes/email.ts new file mode 100644 index 0000000..ec8f06b --- /dev/null +++ b/src/routes/email.ts @@ -0,0 +1,94 @@ +import express from "express"; +import * as emailTemplateCtrl from "../controllers/emailTemplate.controller"; + +export default function emailRouter() { + const router = express.Router(); + + /** + * @api {get} /email/templates List all email templates + * @apiName ListEmailTemplates + * @apiGroup EmailTemplate + * + * @apiSuccess {Object[]} templates Array of email templates. + * + * @apiExample {curl} Example usage: + * curl -X GET http://localhost:3000/api/v1/email/templates + */ + router.get("/templates", emailTemplateCtrl.listTemplates); + + /** + * @api {get} /email/templates/:id Get a single email template + * @apiName GetEmailTemplate + * @apiGroup EmailTemplate + * + * @apiParam (URL Params) {Number} id Template ID. + * + * @apiSuccess {Object} template Email template object. + * + * @apiExample {curl} Example usage: + * curl -X GET http://localhost:3000/api/v1/email/templates/1 + */ + router.get("/templates/:id", emailTemplateCtrl.getTemplate); + + /** + * @api {post} /email/templates Save a new email template + * @apiName CreateEmailTemplate + * @apiGroup EmailTemplate + * + * @apiBody {String} name Unique template name (e.g. "welcome"). (Required) + * @apiBody {String} subject Email subject line. (Required) + * @apiBody {String} htmlBody HTML email body. Supports {{name}}, {{email}}, + * {{whatsapp_link}}, {{discord_link}}, {{year}} + * placeholders. (Required) + * @apiBody {String} [textBody] Plain-text fallback body. + * + * @apiSuccess {Object} template Saved template object. + * @apiError (400) BadRequest Missing required fields. + * + * @apiExample {curl} Example usage: + * curl -X POST http://localhost:3000/api/v1/email/templates \ + * -H "Content-Type: application/json" \ + * -d '{ + * "name": "welcome", + * "subject": "Welcome to Call of Code!", + * "htmlBody": "

Hi {{name}}

Join WhatsApp: {{whatsapp_link}}

" + * }' + */ + router.post("/templates", emailTemplateCtrl.createTemplate); + + /** + * @api {patch} /email/templates/:id Update an email template + * @apiName UpdateEmailTemplate + * @apiGroup EmailTemplate + * + * @apiParam (URL Params) {Number} id Template ID. + * @apiBody {String} [name] New unique name. + * @apiBody {String} [subject] New subject. + * @apiBody {String} [htmlBody] New HTML body. + * @apiBody {String} [textBody] New plain-text body. + * + * @apiSuccess {Object} template Updated template object. + * + * @apiExample {curl} Example usage: + * curl -X PATCH http://localhost:3000/api/v1/email/templates/1 \ + * -H "Content-Type: application/json" \ + * -d '{"subject": "Welcome aboard!"}' + */ + router.patch("/templates/:id", emailTemplateCtrl.updateTemplate); + + /** + * @api {delete} /email/templates/:id Delete an email template + * @apiName DeleteEmailTemplate + * @apiGroup EmailTemplate + * + * @apiParam (URL Params) {Number} id Template ID. + * + * @apiSuccess {String} message Confirmation message. + * + * @apiExample {curl} Example usage: + * curl -X DELETE http://localhost:3000/api/v1/email/templates/1 + */ + router.delete("/templates/:id", emailTemplateCtrl.deleteTemplate); + + return router; +} diff --git a/src/routes/index.ts b/src/routes/index.ts index 80a3e14..74ddb5b 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -9,6 +9,7 @@ import quetionsRouter from './questions' import progressRouter from './progress' import membersRouter from './members' import siteContentRouter from './site-content' +import emailRouter from './email' export default function routes(upload: Multer) { const router = Router(); @@ -27,6 +28,7 @@ export default function routes(upload: Multer) { router.use("/progress", progressRouter()); router.use("/site-content", siteContentRouter(upload)); + router.use("/email", emailRouter()); return router; } diff --git a/src/services/emailTemplate.service.ts b/src/services/emailTemplate.service.ts new file mode 100644 index 0000000..dcd4cbe --- /dev/null +++ b/src/services/emailTemplate.service.ts @@ -0,0 +1,47 @@ + import prisma from "../db/client"; +import { ApiError } from "../utils/apiError"; + +export const createTemplate = async ( + name: string, + subject: string, + htmlBody: string, + textBody?: string, + createdById?: string, +) => { + return await prisma.emailTemplate.create({ + data: { name, subject, htmlBody, textBody, createdById }, + }); +}; + +export const listTemplates = async () => { + return await prisma.emailTemplate.findMany({ + orderBy: { createdAt: "desc" }, + }); +}; + +export const getTemplateById = async (id: number) => { + return await prisma.emailTemplate.findUnique({ where: { id } }); +}; + +export const updateTemplate = async ( + id: number, + payload: Partial<{ + name: string; + subject: string; + htmlBody: string; + textBody: string; + updatedById: string; + }>, +) => { + const exists = await prisma.emailTemplate.findUnique({ where: { id } }); + if (!exists) throw new ApiError("Template not found", 404); + + return await prisma.emailTemplate.update({ where: { id }, data: payload }); +}; + +export const deleteTemplate = async (id: number) => { + const exists = await prisma.emailTemplate.findUnique({ where: { id } }); + if (!exists) throw new ApiError("Template not found", 404); + + return await prisma.emailTemplate.delete({ where: { id } }); +}; From 95e700bee322a165675778c2d1cc042ed872fb11 Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Wed, 17 Jun 2026 00:06:43 +0530 Subject: [PATCH 15/22] change updateRequest to support isApproved == false --- src/controllers/member.controller.ts | 29 ++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/controllers/member.controller.ts b/src/controllers/member.controller.ts index 525854f..74e8cf6 100644 --- a/src/controllers/member.controller.ts +++ b/src/controllers/member.controller.ts @@ -3,6 +3,7 @@ import * as memberService from "../services/member.service"; import { ApiError } from "../utils/apiError"; import { uploadImage } from "../utils/imageUtils"; import { SupabaseClient } from "@supabase/supabase-js"; +import { Role } from "../generated/prisma/client"; // List all approved members export const listAllApprovedMembers = async (req: Request, res: Response) => { @@ -114,8 +115,6 @@ export const updateRequest = async (req: Request, res: Response) => { throw new ApiError("No essential creds provided", 400); } - if(!isApproved) throw new ApiError("Someone interrupting the backend flow", 400); - const update = await memberService.approveRequest( isApproved, adminId, @@ -156,3 +155,29 @@ export const getUserInterviews = async (req: Request, res: Response) => { const interviews = await memberService.getInterviews(memberId); res.status(200).json({ success: true, interviews }); }; + +// Update a member's role (SUPER_ADMIN only) +export const updateMemberRole = async (req: Request, res: Response) => { + const { memberId } = req.params; + const { adminId, role } = req.body; + + if (!memberId || !adminId || !role) { + throw new ApiError("memberId, adminId, and role are required", 400); + } + + const validRoles = Object.values(Role); + if (!validRoles.includes(role)) { + throw new ApiError( + `Invalid role. Must be one of: ${validRoles.join(", ")}`, + 400, + ); + } + + const updated = await memberService.updateMemberRole(adminId, memberId, role as Role); + + res.status(200).json({ + success: true, + user: updated, + message: `Role updated to ${role}`, + }); +}; From b617d6386008d137a55c6c746a743ee286788e3d Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Wed, 17 Jun 2026 00:07:01 +0530 Subject: [PATCH 16/22] feat: replace isManager boolean with Role enum and implement restricted role management functionality --- .../migration.sql | 24 +++++++++++++++ prisma/schema.prisma | 9 +++++- src/routes/members.ts | 23 +++++++++++++++ src/services/member.service.ts | 29 ++++++++++++++++++- src/services/site-content.service.ts | 24 ++++++++------- tests/Member.test.ts | 12 ++++++-- tests/SiteContent.test.ts | 6 ++-- tsconfig.json | 2 +- 8 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 prisma/migrations/20260616181835_new_role_added/migration.sql diff --git a/prisma/migrations/20260616181835_new_role_added/migration.sql b/prisma/migrations/20260616181835_new_role_added/migration.sql new file mode 100644 index 0000000..1e2772a --- /dev/null +++ b/prisma/migrations/20260616181835_new_role_added/migration.sql @@ -0,0 +1,24 @@ +/* + Warnings: + + - You are about to drop the column `isManager` on the `Member` table. All the data in the column will be lost. + +*/ +-- 1. Create the Role enum +CREATE TYPE "Role" AS ENUM ('SUPER_ADMIN', 'ADMIN', 'FOUNDER', 'MEMBER'); + +-- 2. Add the new role column (nullable initially, no default yet) +ALTER TABLE "Member" ADD COLUMN "role" "Role"; + +-- 3. Backfill: convert isManager → role +UPDATE "Member" SET "role" = CASE + WHEN "isManager" = true THEN 'ADMIN'::"Role" + ELSE 'MEMBER'::"Role" +END; + +-- 4. Now make it NOT NULL with a default +ALTER TABLE "Member" ALTER COLUMN "role" SET NOT NULL; +ALTER TABLE "Member" ALTER COLUMN "role" SET DEFAULT 'MEMBER'::"Role"; + +-- 5. Drop the old column +ALTER TABLE "Member" DROP COLUMN "isManager"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 577e789..934f560 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -27,7 +27,7 @@ model Member { codeforces String? passoutYear DateTime? isApproved Boolean @default(false) - isManager Boolean @default(false) + role Role @default(MEMBER) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -171,6 +171,13 @@ enum Difficulty { Hard } +enum Role { + SUPER_ADMIN + ADMIN + FOUNDER + MEMBER +} + model InterviewExperience { id Int @id @default(autoincrement()) company String diff --git a/src/routes/members.ts b/src/routes/members.ts index 0b4b331..559e526 100644 --- a/src/routes/members.ts +++ b/src/routes/members.ts @@ -195,5 +195,28 @@ export default function membersRouter( */ router.get("/:memberId/interviews", memberCtrl.getUserInterviews); + /** + * @api {patch} /members/:memberId/role Update a member's role + * @apiName UpdateMemberRole + * @apiGroup Member + * + * @apiParam (URL Params) {String} memberId Target member's ID. + * @apiBody {String} adminId ID of the Super Admin performing the change. + * @apiBody {String="SUPER_ADMIN","ADMIN","FOUNDER","MEMBER"} role New role to assign. + * + * @apiSuccess {Boolean} success Request status. + * @apiSuccess {Object} user Updated member object. + * @apiSuccess {String} message Confirmation message. + * + * @apiError (Error 400) BadRequest Missing required fields or invalid role. + * @apiError (Error 403) Forbidden Only Super Admins can assign roles. + * + * @apiExample {curl} Example usage: + * curl -X PATCH http://localhost:3000/members/123/role \ + * -H "Content-Type: application/json" \ + * -d '{"adminId": "superadmin-id", "role": "ADMIN"}' + */ + router.patch("/:memberId/role", memberCtrl.updateMemberRole); + return router; } diff --git a/src/services/member.service.ts b/src/services/member.service.ts index 804a175..4553988 100644 --- a/src/services/member.service.ts +++ b/src/services/member.service.ts @@ -1,5 +1,6 @@ import prisma from "../db/client"; import { ApiError } from "../utils/apiError"; +import { Role } from "../generated/prisma/client"; export const getUserByEmail = async(email: string) => { return await prisma.member.findUnique({ @@ -9,7 +10,7 @@ export const getUserByEmail = async(email: string) => { select: { id: true, isApproved: true, - isManager: true, + role: true, accounts: { select: { password: true @@ -143,4 +144,30 @@ export const getInterviews = async (id: string) => { return await prisma.interviewExperience.findMany({ where: { memberId: id }, }); +}; + +export const updateMemberRole = async ( + superAdminId: string, + memberId: string, + newRole: Role, +) => { + // Verify the requester is a SUPER_ADMIN + const requester = await prisma.member.findUnique({ + where: { id: superAdminId }, + select: { role: true }, + }); + + if (!requester || requester.role !== Role.SUPER_ADMIN) { + throw new ApiError("Forbidden: only Super Admins can assign roles", 403); + } + + // Prevent modifying own role + if (superAdminId === memberId) { + throw new ApiError("Cannot modify your own role", 400); + } + + return await prisma.member.update({ + where: { id: memberId }, + data: { role: newRole }, + }); }; \ No newline at end of file diff --git a/src/services/site-content.service.ts b/src/services/site-content.service.ts index f704525..37c2119 100644 --- a/src/services/site-content.service.ts +++ b/src/services/site-content.service.ts @@ -1,7 +1,7 @@ import { v4 as uuidv4 } from "uuid"; import prisma from "../db/client"; import { ApiError } from "../utils/apiError"; -import { Prisma } from "../generated/prisma/client"; +import { Prisma, Role } from "../generated/prisma/client"; const PAGE_CONTENT_ID = 1; @@ -9,14 +9,16 @@ function toGalleryJson(gallery: GalleryPhoto[]): Prisma.InputJsonValue { return gallery as unknown as Prisma.InputJsonValue; } -async function assertManager(adminId: string) { +const ADMIN_ROLES: Role[] = [Role.SUPER_ADMIN, Role.ADMIN]; + +async function assertAdmin(adminId: string) { const member = await prisma.member.findUnique({ where: { id: adminId }, - select: { isManager: true }, + select: { role: true }, }); - if (!member?.isManager) { - throw new ApiError("Forbidden: manager access required", 403); + if (!member || !ADMIN_ROLES.includes(member.role)) { + throw new ApiError("Forbidden: admin access required", 403); } } @@ -92,7 +94,7 @@ export async function updateSitePageContent( adminId: string, data: Omit, ) { - await assertManager(adminId); + await assertAdmin(adminId); const page = await prisma.sitePageContent.update({ where: { id: PAGE_CONTENT_ID }, @@ -113,7 +115,7 @@ export async function updateSiteAction( key: string, data: Omit, ) { - await assertManager(adminId); + await assertAdmin(adminId); const current = await prisma.siteAction.findUnique({ where: { key } }); if (!current) { @@ -144,7 +146,7 @@ export async function addGalleryPhoto( adminId: string, data: Omit, ) { - await assertManager(adminId); + await assertAdmin(adminId); const current = await getPageContent(); const gallery = parseGalleryPhotos(current.galleryPhotos); @@ -177,7 +179,7 @@ export async function updateGalleryPhoto( photoId: string, data: Omit, ) { - await assertManager(adminId); + await assertAdmin(adminId); const current = await getPageContent(); const gallery = parseGalleryPhotos(current.galleryPhotos); @@ -218,7 +220,7 @@ export async function updateGalleryPhoto( } export async function deleteGalleryPhoto(adminId: string, photoId: string) { - await assertManager(adminId); + await assertAdmin(adminId); const current = await getPageContent(); const gallery = parseGalleryPhotos(current.galleryPhotos); @@ -240,7 +242,7 @@ export async function deleteGalleryPhoto(adminId: string, photoId: string) { } export async function getGalleryPhoto(adminId: string, photoId: string) { - await assertManager(adminId); + await assertAdmin(adminId); const current = await getPageContent(); const gallery = parseGalleryPhotos(current.galleryPhotos); diff --git a/tests/Member.test.ts b/tests/Member.test.ts index d80f545..f244af6 100644 --- a/tests/Member.test.ts +++ b/tests/Member.test.ts @@ -4,8 +4,11 @@ import * as memberService from '../src/services/member.service'; import { ApiError } from '../src/utils/apiError'; import { SupabaseClient } from '@supabase/supabase-js'; import { uploadImage } from '../src/utils/imageUtils'; +import { Role } from '../src/db/client'; + jest.mock('../src/db/client', () => ({ + ...jest.requireActual('../src/db/client'), prisma: { member: { findUnique: jest.fn(), @@ -83,6 +86,7 @@ describe('Member Controller - updateAMember', () => { const res = mockResponse(); + const role = Role.MEMBER const updatedMember = { id: '123', name: 'Test User', @@ -100,7 +104,7 @@ describe('Member Controller - updateAMember', () => { gfg: null, geeksforgeeks: null, passoutYear: new Date('2025-05-31'), - isManager: false, + role: role, isApproved: false, approvedById: null, createdAt: new Date(), @@ -131,6 +135,7 @@ describe('Member Controller - updateAMember', () => { const res = mockResponse(); + const role = Role.MEMBER const oldMember = { id: '123', name: 'Old User', @@ -148,7 +153,7 @@ describe('Member Controller - updateAMember', () => { gfg: null, geeksforgeeks: null, passoutYear: new Date('2025-05-31'), - isManager: false, + role: role, isApproved: false, approvedById: null, createdAt: new Date(), @@ -200,6 +205,7 @@ describe('Member Controller - updateAMember', () => { const res = mockResponse(); + const role:Role = Role.MEMBER const updatedMember = { id: '123', name: 'Updated User', @@ -217,7 +223,7 @@ describe('Member Controller - updateAMember', () => { gfg: null, geeksforgeeks: null, passoutYear: new Date('2025-05-31'), - isManager: false, + role: role, isApproved: false, approvedById: null, createdAt: new Date(), diff --git a/tests/SiteContent.test.ts b/tests/SiteContent.test.ts index fc26ebe..bb48fe0 100644 --- a/tests/SiteContent.test.ts +++ b/tests/SiteContent.test.ts @@ -266,7 +266,7 @@ describe("site-content service guards", () => { const prisma = (await import("../src/db/client")).default; (prisma.member.findUnique as jest.Mock).mockResolvedValue({ - isManager: false, + role: 'MEMBER', }); await expect( @@ -274,14 +274,14 @@ describe("site-content service guards", () => { isVisible: true, url: "https://example.com", }), - ).rejects.toThrow(new ApiError("Forbidden: manager access required", 403)); + ).rejects.toThrow(new ApiError("Forbidden: admin access required", 403)); }); it("should reject visible action without URL", async () => { const prisma = (await import("../src/db/client")).default; (prisma.member.findUnique as jest.Mock).mockResolvedValue({ - isManager: true, + role: 'ADMIN', }); (prisma.siteAction.findUnique as jest.Mock).mockResolvedValue({ key: "recruitment", diff --git a/tsconfig.json b/tsconfig.json index 39fca51..45713f1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -112,7 +112,7 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src/**/*" ], // Include your source files + "include": ["src/**/*", "tests/**/*"], // Include your source files "exclude": ["node_modules"] // Exclude test files from compilation } From de1689dd01b591354cdf0bce3c8218e2c10484e7 Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Wed, 17 Jun 2026 01:03:58 +0530 Subject: [PATCH 17/22] feat: add ghosting functionality to hide members from public lists and unapprove list without deletion --- .../migration.sql | 6 + prisma/schema.prisma | 6 + src/controllers/member.controller.ts | 29 ++++ src/routes/members.ts | 49 ++++++ src/services/member.service.ts | 52 +++++- tests/Member.test.ts | 148 +++++++++++++++++- 6 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20260616190854_add_is_ghosted/migration.sql diff --git a/prisma/migrations/20260616190854_add_is_ghosted/migration.sql b/prisma/migrations/20260616190854_add_is_ghosted/migration.sql new file mode 100644 index 0000000..f919c4a --- /dev/null +++ b/prisma/migrations/20260616190854_add_is_ghosted/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "Member" ADD COLUMN "ghostedById" TEXT, +ADD COLUMN "isGhosted" BOOLEAN NOT NULL DEFAULT false; + +-- AddForeignKey +ALTER TABLE "Member" ADD CONSTRAINT "Member_ghostedById_fkey" FOREIGN KEY ("ghostedById") REFERENCES "Member"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 934f560..0d277c4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -27,6 +27,7 @@ model Member { codeforces String? passoutYear DateTime? isApproved Boolean @default(false) + isGhosted Boolean @default(false) role Role @default(MEMBER) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -36,6 +37,11 @@ model Member { approvedById String? approvedMembers Member[] @relation("MemberApprovals") + // Ghost / Dead Zone audit (self-reference) + ghostedBy Member? @relation("MemberGhosts", fields: [ghostedById], references: [id]) + ghostedById String? + ghostedMembers Member[] @relation("MemberGhosts") + // Authentication & Relations accounts Account[] achievements MemberAchievement[] diff --git a/src/controllers/member.controller.ts b/src/controllers/member.controller.ts index 74e8cf6..070a3e2 100644 --- a/src/controllers/member.controller.ts +++ b/src/controllers/member.controller.ts @@ -181,3 +181,32 @@ export const updateMemberRole = async (req: Request, res: Response) => { message: `Role updated to ${role}`, }); }; + +// Ghost or unghost a member (Dead Zone) — ADMIN & SUPER_ADMIN only +export const ghostMember = async (req: Request, res: Response) => { + const { memberId } = req.params; + const { adminId, ghost = true } = req.body; + + if (!memberId || !adminId) { + throw new ApiError("memberId and adminId are required", 400); + } + + if (typeof ghost !== "boolean") { + throw new ApiError('"ghost" must be a boolean', 400); + } + + const updated = await memberService.ghostMember(adminId, memberId, ghost); + + const action = ghost ? "ghosted" : "unghosted"; + res.status(200).json({ + success: true, + user: updated, + message: `Member ${action} and moved to Dead Zone`, + }); +}; + +// Get all ghosted members (Dead Zone audit list) — ADMIN & SUPER_ADMIN only +export const getDeadZoneMembers = async (req: Request, res: Response) => { + const members = await memberService.deadZoneMembers(); + res.status(200).json({ success: true, members }); +}; diff --git a/src/routes/members.ts b/src/routes/members.ts index 559e526..3084d72 100644 --- a/src/routes/members.ts +++ b/src/routes/members.ts @@ -20,6 +20,22 @@ export default function membersRouter( * curl -X GET http://localhost:3000/members/unapproved */ router.get("/unapproved", memberCtrl.getUnapprovedMembers); + + /** + * @api {get} /members/dead-zone List all ghosted members (Dead Zone) + * @apiName GetDeadZoneMembers + * @apiGroup Member + * + * @apiDescription Returns all member records that have been ghosted by an admin. + * These members are hidden from the public approved list and the pending + * approval queue. Data is preserved for audit purposes. + * + * @apiSuccess {Object[]} members List of ghosted member objects (includes ghostedBy admin info). + * + * @apiExample {curl} Example usage: + * curl -X GET http://localhost:3000/members/dead-zone + */ + router.get("/dead-zone", memberCtrl.getDeadZoneMembers); /** * @api {get} /members/:memberId Get a member's details @@ -152,6 +168,39 @@ export default function membersRouter( */ router.patch("/approve/:memberId", memberCtrl.updateRequest); + /** + * @api {patch} /members/ghost/:memberId Ghost or unghost a member (Dead Zone) + * @apiName GhostMember + * @apiGroup Member + * + * @apiDescription Moves a member into the Dead Zone by setting isGhosted=true. + * Ghosted members are silently removed from the pending approval queue and + * the public member listing without hard-deleting their data. + * Setting ghost=false restores the member back to the active queue. + * + * @apiParam (URL Params) {String} memberId Target member's ID. + * @apiBody {String} adminId ID of the Admin or Super Admin performing the action. + * @apiBody {Boolean} [ghost=true] true to ghost, false to unghost. + * + * @apiSuccess {Boolean} success Request status. + * @apiSuccess {Object} user Updated member object. + * @apiSuccess {String} message Confirmation message. + * + * @apiError (Error 400) BadRequest Missing required fields or invalid ghost value. + * @apiError (Error 403) Forbidden Only Admins and Super Admins can ghost members. + * + * @apiExample {curl} Ghost a member: + * curl -X PATCH http://localhost:3000/members/ghost/123 \ + * -H "Content-Type: application/json" \ + * -d '{"adminId": "admin-id", "ghost": true}' + * + * @apiExample {curl} Unghost a member: + * curl -X PATCH http://localhost:3000/members/ghost/123 \ + * -H "Content-Type: application/json" \ + * -d '{"adminId": "admin-id", "ghost": false}' + */ + router.patch("/ghost/:memberId", memberCtrl.ghostMember); + /** * @api {get} /members/:memberId/achievements Get member's achievements diff --git a/src/services/member.service.ts b/src/services/member.service.ts index 4553988..7a0d98c 100644 --- a/src/services/member.service.ts +++ b/src/services/member.service.ts @@ -2,6 +2,8 @@ import prisma from "../db/client"; import { ApiError } from "../utils/apiError"; import { Role } from "../generated/prisma/client"; +const GHOST_ALLOWED_ROLES: Role[] = [Role.ADMIN, Role.SUPER_ADMIN]; + export const getUserByEmail = async(email: string) => { return await prisma.member.findUnique({ where: { @@ -24,6 +26,7 @@ export const approvedMembers = async () => { return await prisma.member.findMany({ where: { isApproved: true, + isGhosted: false, }, }); }; @@ -98,7 +101,7 @@ export const updatePassword = async(id: string, password: string) => { export const unapprovedMembers = async () => { return await prisma.member.findMany({ - where: { isApproved: false }, + where: { isApproved: false, isGhosted: false }, }); }; @@ -170,4 +173,51 @@ export const updateMemberRole = async ( where: { id: memberId }, data: { role: newRole }, }); +}; + +/** + * Ghost or unghost a member request (Dead Zone). + * Only ADMIN and SUPER_ADMIN are allowed to perform this action. + */ +export const ghostMember = async ( + adminId: string, + memberId: string, + ghost: boolean, +) => { + // Verify requester exists and has the right role + const requester = await prisma.member.findUnique({ + where: { id: adminId }, + select: { role: true }, + }); + + if (!requester || !GHOST_ALLOWED_ROLES.includes(requester.role)) { + throw new ApiError("Forbidden: only Admins and Super Admins can ghost members", 403); + } + + // Prevent self-ghosting + if (adminId === memberId) { + throw new ApiError("Cannot ghost yourself", 400); + } + + return await prisma.member.update({ + where: { id: memberId }, + data: { + isGhosted: ghost, + ghostedBy: ghost ? { connect: { id: adminId } } : { disconnect: true }, + }, + }); +}; + +/** + * Retrieve all members currently in the Dead Zone (ghosted). + */ +export const deadZoneMembers = async () => { + return await prisma.member.findMany({ + where: { isGhosted: true }, + include: { + ghostedBy: { + select: { id: true, name: true, email: true, role: true }, + }, + }, + }); }; \ No newline at end of file diff --git a/tests/Member.test.ts b/tests/Member.test.ts index f244af6..0a257a2 100644 --- a/tests/Member.test.ts +++ b/tests/Member.test.ts @@ -1,5 +1,5 @@ import { Request, Response } from 'express'; -import { createAMember, updateAMember } from '../src/controllers/member.controller'; +import { createAMember, updateAMember, ghostMember, getDeadZoneMembers } from '../src/controllers/member.controller'; import * as memberService from '../src/services/member.service'; import { ApiError } from '../src/utils/apiError'; import { SupabaseClient } from '@supabase/supabase-js'; @@ -109,6 +109,9 @@ describe('Member Controller - updateAMember', () => { approvedById: null, createdAt: new Date(), updatedAt: new Date(), + isGhosted: false, + ghostedById: null, + ghostedAt: null, }; jest.spyOn(memberService, 'updateMember').mockResolvedValue(updatedMember); @@ -158,6 +161,9 @@ describe('Member Controller - updateAMember', () => { approvedById: null, createdAt: new Date(), updatedAt: new Date(), + isGhosted: false, + ghostedById: null, + ghostedAt: null, }; const updatedMember = { @@ -228,6 +234,9 @@ describe('Member Controller - updateAMember', () => { approvedById: null, createdAt: new Date(), updatedAt: new Date(), + isGhosted: false, + ghostedById: null, + ghostedAt: null, }; // Mock updatePassword and getDetails @@ -246,3 +255,140 @@ describe('Member Controller - updateAMember', () => { }); }); }); + +// Dead Zone (Ghost) feature +const makeBaseMember = (overrides: Record = {}) => ({ + id: 'member-123', + name: 'Test User', + email: 'test@example.com', + birth_date: null, + phone: null, + bio: null, + profilePhoto: null, + github: null, + linkedin: null, + twitter: null, + leetcode: null, + codeforces: null, + codechef: null, + gfg: null, + geeksforgeeks: null, + passoutYear: new Date('2025-05-31'), + role: Role.MEMBER, + isApproved: false, + isGhosted: false, + approvedById: null, + ghostedById: null, + createdAt: new Date(), + updatedAt: new Date(), + ...overrides, +}); + +describe('Member Controller - ghostMember', () => { + beforeEach(() => jest.clearAllMocks()); + + it('should ghost a member and return 200', async () => { + const req = { + params: { memberId: 'member-123' }, + body: { adminId: 'admin-456', ghost: true }, + } as unknown as Request; + const res = mockResponse(); + + const ghosted = makeBaseMember({ isGhosted: true, ghostedById: 'admin-456' }); + jest.spyOn(memberService, 'ghostMember').mockResolvedValue(ghosted); + + await ghostMember(req, res); + + expect(memberService.ghostMember).toHaveBeenCalledWith('admin-456', 'member-123', true); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + success: true, + user: ghosted, + message: 'Member ghosted and moved to Dead Zone', + }); + }); + + it('should unghost a member when ghost=false', async () => { + const req = { + params: { memberId: 'member-123' }, + body: { adminId: 'admin-456', ghost: false }, + } as unknown as Request; + const res = mockResponse(); + + const restored = makeBaseMember({ isGhosted: false, ghostedById: null }); + jest.spyOn(memberService, 'ghostMember').mockResolvedValue(restored); + + await ghostMember(req, res); + + expect(memberService.ghostMember).toHaveBeenCalledWith('admin-456', 'member-123', false); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + success: true, + user: restored, + message: 'Member unghosted and moved to Dead Zone', + }); + }); + + it('should throw 400 if memberId or adminId is missing', async () => { + const req = { + params: { memberId: 'member-123' }, + body: {}, + } as unknown as Request; + const res = mockResponse(); + + await expect(ghostMember(req, res)).rejects.toThrow( + new ApiError('memberId and adminId are required', 400), + ); + }); + + it('should throw 400 if ghost is not a boolean', async () => { + const req = { + params: { memberId: 'member-123' }, + body: { adminId: 'admin-456', ghost: 'yes' }, + } as unknown as Request; + const res = mockResponse(); + + await expect(ghostMember(req, res)).rejects.toThrow( + new ApiError('"ghost" must be a boolean', 400), + ); + }); + + it('should propagate 403 when service rejects non-admin requester', async () => { + const req = { + params: { memberId: 'member-123' }, + body: { adminId: 'non-admin-id', ghost: true }, + } as unknown as Request; + const res = mockResponse(); + + jest.spyOn(memberService, 'ghostMember').mockRejectedValue( + new ApiError('Forbidden: only Admins and Super Admins can ghost members', 403), + ); + + await expect(ghostMember(req, res)).rejects.toThrow( + new ApiError('Forbidden: only Admins and Super Admins can ghost members', 403), + ); + }); +}); + +describe('Member Controller - getDeadZoneMembers', () => { + beforeEach(() => jest.clearAllMocks()); + + it('should return all ghosted members with 200', async () => { + const req = {} as Request; + const res = mockResponse(); + + const ghostedList = [ + makeBaseMember({ isGhosted: true, ghostedById: 'admin-456' }), + makeBaseMember({ id: 'member-789', isGhosted: true, ghostedById: 'admin-456' }), + ]; + + jest.spyOn(memberService, 'deadZoneMembers').mockResolvedValue(ghostedList as any); + + await getDeadZoneMembers(req, res); + + expect(memberService.deadZoneMembers).toHaveBeenCalledTimes(1); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ success: true, members: ghostedList }); + }); +}); + From 06e9c2acdcdad66309d5f3543e1d67d59a6743db Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Wed, 17 Jun 2026 01:54:30 +0530 Subject: [PATCH 18/22] removed unused import --- src/app.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 8f535ad..c9dab81 100644 --- a/src/app.ts +++ b/src/app.ts @@ -7,7 +7,6 @@ import { errorHandler } from "./utils/apiError"; import path from "path"; import { logger } from "./utils/logger"; import morgan from "morgan"; -import { supabase } from "./utils/supabaseClient"; import config from "./config"; const app = express(); From f35d8e9d8fc803fe06dc90150f90b48ad7bb3210 Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Thu, 18 Jun 2026 21:41:34 +0530 Subject: [PATCH 19/22] Update src/services/member.service.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/services/member.service.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/services/member.service.ts b/src/services/member.service.ts index 7a0d98c..6bd1466 100644 --- a/src/services/member.service.ts +++ b/src/services/member.service.ts @@ -199,6 +199,14 @@ export const ghostMember = async ( throw new ApiError("Cannot ghost yourself", 400); } + const target = await prisma.member.findUnique({ + where: { id: memberId }, + select: { id: true }, + }); + if (!target) { + throw new ApiError("Member not found", 404); + } + return await prisma.member.update({ where: { id: memberId }, data: { From b796396a6cd09354842edb589c27ffcde0e94877 Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Thu, 18 Jun 2026 21:43:26 +0530 Subject: [PATCH 20/22] res updated --- src/controllers/member.controller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/member.controller.ts b/src/controllers/member.controller.ts index 070a3e2..19ee9e9 100644 --- a/src/controllers/member.controller.ts +++ b/src/controllers/member.controller.ts @@ -198,10 +198,11 @@ export const ghostMember = async (req: Request, res: Response) => { const updated = await memberService.ghostMember(adminId, memberId, ghost); const action = ghost ? "ghosted" : "unghosted"; + const movement = ghost ? "moved to Dead Zone" : "restored from Dead Zone"; res.status(200).json({ success: true, user: updated, - message: `Member ${action} and moved to Dead Zone`, + message: `Member ${action} and ${movement}`, }); }; From 7c2ac0b587c9c7572dcdf4b303fa74b64f448a0e Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Thu, 18 Jun 2026 21:50:02 +0530 Subject: [PATCH 21/22] fix member test --- tests/Member.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Member.test.ts b/tests/Member.test.ts index 0a257a2..9efedbc 100644 --- a/tests/Member.test.ts +++ b/tests/Member.test.ts @@ -325,7 +325,7 @@ describe('Member Controller - ghostMember', () => { expect(res.json).toHaveBeenCalledWith({ success: true, user: restored, - message: 'Member unghosted and moved to Dead Zone', + message: 'Member unghosted and restored from Dead Zone', }); }); From 3189dd9a4d74f9928475a48e483154d43663f756 Mon Sep 17 00:00:00 2001 From: Harish-Naruto Date: Thu, 18 Jun 2026 22:09:01 +0530 Subject: [PATCH 22/22] docs: update README with project structure, setup instructions, and standardize API v1 route prefixing --- README.md | 146 +++++++++++++++++++++++++++++++----------- src/routes/members.ts | 28 ++++---- 2 files changed, 121 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 6232c9c..defde45 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # COC-API -This repository contains the common Express.js API for the backends of our Coding Club's websites , backed by PostgreSQL (via Prisma) and Supabase storage. We’re using **Bun** as our runtime. +This repository contains the shared Express.js API for the backends of our Coding Club's websites, backed by PostgreSQL (via Prisma) and Supabase storage. We use **Bun** as our runtime. ## 📂 Folder Structure @@ -8,70 +8,90 @@ This repository contains the common Express.js API for the backends of our Codin / ├── prisma/ # Prisma schema and migration files │ ├── schema.prisma -│ ├── .env # your DATABASE_URL, etc. -│ └── migrations/ # auto‑generated by `bun prisma migrate` +│ └── migrations/ # auto-generated by `bun run migrate` +│ +├── seed/ +│ └── dump.sql # local seed file loaded by the setup script +│ +├── scripts/ +│ └── setup-local.sh # automates local environment bring-up │ ├── src/ │ ├── config/ # environment/configuration loaders -│ │ └── index.ts # loads process.env and exports typed config │ │ │ ├── db/ # database client initialization -│ │ └── client.ts # `export const prisma = new PrismaClient()` │ │ │ ├── routes/ # Express route definitions -│ │ ├── index.ts # main router that mounts sub‑routers +│ │ ├── index.ts # main router — mounts all sub-routers under /api/v1 │ │ ├── members.ts │ │ ├── projects.ts │ │ ├── achievements.ts +│ │ ├── interviews.ts │ │ ├── topics.ts │ │ ├── questions.ts -│ │ ├── interviews.ts -│ │ └── progress.ts +│ │ ├── progress.ts +│ │ ├── site-content.ts +│ │ └── email.ts │ │ │ ├── controllers/ # controllers: take req → call services → send res │ │ ├── member.controller.ts │ │ ├── project.controller.ts │ │ ├── achievement.controller.ts +│ │ ├── interview.controller.ts │ │ ├── topic.controller.ts │ │ ├── question.controller.ts -│ │ ├── interview.controller.ts -│ │ └── progress.controller.ts +│ │ ├── progress.controller.ts +│ │ ├── site-content.controller.ts +│ │ └── emailTemplate.controller.ts │ │ │ ├── services/ # business logic / Prisma queries │ │ ├── member.service.ts │ │ ├── project.service.ts │ │ ├── achievement.service.ts +│ │ ├── interview.service.ts │ │ ├── topic.service.ts │ │ ├── question.service.ts -│ │ ├── interview.service.ts -│ │ └── progress.service.ts +│ │ ├── progress.service.ts +│ │ ├── site-content.service.ts +│ │ └── emailTemplate.service.ts │ │ -│ ├── utils/ # shared helpers (e.g. error wrappers, validators) -│ │ └── apiError.ts +│ ├── utils/ # shared helpers +│ │ ├── apiError.ts # custom error class and global error handler +│ │ ├── imageUtils.ts # image upload / Supabase storage helpers +│ │ ├── logger.ts # Winston logger instance +│ │ └── supabaseClient.ts │ │ │ ├── app.ts # configure Express app, mount routes, error handler -│ └── server.ts # start HTTP server (calls `app.listen`) +│ └── server.ts # start HTTP server │ -├── tests/ # integration and unit tests (Jest or Mocha) -│ ├── members.test.ts -│ └── ... +├── tests/ # unit tests (Jest + ts-jest) +│ ├── Member.test.ts +│ ├── Project.test.ts +│ ├── Achievement.test.ts +│ ├── Interview.test.ts +│ ├── Topics.test.ts +│ ├── Question.test.ts +│ ├── Progress.test.ts +│ ├── SiteContent.test.ts +│ └── imageUtils.test.ts │ ├── .env.example # template for environment variables ├── package.json -└── tsconfig.json # TypeScript configuration +└── tsconfig.json ``` ## 🚀 Getting Started -### Prerequisite +### Prerequisites - Install [Bun](https://bun.sh/) on your machine. +- Install [Docker & Docker Compose](https://docs.docker.com/get-docker/) for the local database. ### 1. Clone the repo ```bash -git clone https://github.com/your-org/coding-club-api.git -cd coding-club-api +git clone https://github.com/call-0f-code/COC-API.git +cd COC-API ``` ### 2. Install dependencies @@ -82,39 +102,87 @@ bun install ### 3. Configure environment -- Copy `.env.example` to `.env` -- Update `.env` with your Supabase/PostgreSQL connection URL and any other variables: +Copy `.env.example` to `.env` and fill in the required values: + +```bash +cp .env.example .env +``` + +Key variables: + +| Variable | Description | +| ------------------------- | --------------------------------------------------------------------------- | +| `DATABASE_URL` | Supabase connection-pooling URL (used by Prisma at runtime) | +| `DIRECT_URL` | Direct DB connection URL (used by Prisma Migrate) | +| `SUPABASE_URL` | Your Supabase project URL | +| `SUPABASE_SERVICE_ROLE_KEY` | Supabase service-role secret key | +| `SESSION_POOLER` | Session-mode pooler URL — used by `setup-local.sh` for `pg_dump` (IPv4) | +| `NODE_ENV` | `development` \| `production` | + +### 4. Local development (Docker) + +The setup script starts a local Postgres container and seeds it automatically: -### 4. Initialize Prisma & Database +```bash +bun run local +``` + +> See [LOCAL_DEVELOPMENT.md](LOCAL_DEVELOPMENT.md) for full details, flags, and troubleshooting. + +### 5. Initialize / run migrations + +For a brand-new database: ```bash -bun prisma migrate dev --name init -bun prisma generate +bun run migrate:first # runs: bunx prisma migrate dev --name init +bun run generate # runs: bunx prisma generate ``` -### 5. Run in development +For subsequent schema changes: + +```bash +bun run migrate # runs: bunx prisma migrate dev +``` + +### 6. Start the server + +```bash +bun run start +``` + +The server listens on `http://localhost:3000` by default. +All API routes are prefixed with `/api/v1`, e.g. `http://localhost:3000/api/v1/members`. + +### 7. API Documentation + +Generate and serve the API docs: ```bash -bun run dev +bun run apidoc # generates static docs into /doc ``` -- By default, the server listens on `http://localhost:3000` -- `app.ts` sets up your Express instance; `server.ts` starts the HTTP listener +Then visit `http://localhost:3000/docs` while the server is running. -### 6. Run tests +### 8. Run tests ```bash -bun test +bun run test # runs: jest ``` ## 📦 Scripts -| Command | Description | -| ------------------------------- | --------------------------------------- | -| `bun run dev` | Start the dev server with hot reloading | -| `bun prisma migrate dev --name` | Apply migrations in development | -| `bun prisma generate` | Generate Prisma client | -| `bun test` | Run tests (Jest or Mocha) | +| Command | Description | +| -------------------- | -------------------------------------------------- | +| `bun run start` | Start the server (`bun src/server.ts`) | +| `bun run local` | Spin up local Docker environment and seed the DB | +| `bun run migrate` | Run pending Prisma migrations in development | +| `bun run migrate:first` | Apply initial migration (`--name init`) | +| `bun run generate` | Regenerate Prisma client | +| `bun run test` | Run all tests with Jest | +| `bun run apidoc` | Generate API documentation into `/doc` | +| `bun run lint` | Lint `src/` with ESLint | +| `bun run lint:fix` | Lint and auto-fix `src/` | +| `bun run format` | Format `src/` with Prettier | --- diff --git a/src/routes/members.ts b/src/routes/members.ts index 3084d72..c0a97bb 100644 --- a/src/routes/members.ts +++ b/src/routes/members.ts @@ -17,7 +17,7 @@ export default function membersRouter( * @apiSuccess {Object[]} unapprovedMembers List of unapproved members. * * @apiExample {curl} Example usage: - * curl -X GET http://localhost:3000/members/unapproved + * curl -X GET http://localhost:3000/api/v1/members/unapproved */ router.get("/unapproved", memberCtrl.getUnapprovedMembers); @@ -33,7 +33,7 @@ export default function membersRouter( * @apiSuccess {Object[]} members List of ghosted member objects (includes ghostedBy admin info). * * @apiExample {curl} Example usage: - * curl -X GET http://localhost:3000/members/dead-zone + * curl -X GET http://localhost:3000/api/v1/members/dead-zone */ router.get("/dead-zone", memberCtrl.getDeadZoneMembers); @@ -48,7 +48,7 @@ export default function membersRouter( * @apiError (Error 400) BadRequest No memberId provided. * * @apiExample {curl} Example usage: - * curl -X GET http://localhost:3000/members/123 + * curl -X GET http://localhost:3000/api/v1/members/123 */ router.get("/:memberId", memberCtrl.getUserDetails); @@ -70,10 +70,10 @@ export default function membersRouter( * @apiError (400) IncorrectEmail The provided email does not match any user. * * @apiExample {curl} Example usage (list all): - * curl -X GET http://localhost:3000/members + * curl -X GET http://localhost:3000/api/v1/members * * @apiExample {curl} Example usage (get by email): - * curl -X GET "http://localhost:3000/members?email=john@example.com" + * curl -X GET "http://localhost:3000/api/v1/members?email=john@example.com" */ router.get("/", memberCtrl.listAllApprovedMembers); @@ -102,7 +102,7 @@ export default function membersRouter( * -F "password=securePass123" \ * -F "passoutYear=2026" \ * -F "provider=credentials" \ - * http://localhost:3000/members + * http://localhost:3000/api/v1/members */ router.post("/", upload.single("file"), memberCtrl.createAMember(supabase)); @@ -140,7 +140,7 @@ export default function membersRouter( * @apiExample {curl} Example usage: * curl -X PATCH -F "file=@profile.jpg" \ * -F 'memberData={"name":"John Doe","email":"john@example.com"}' \ - * http://localhost:3000/members/123 + * http://localhost:3000/api/v1/members/123 */ router.patch( "/:memberId", @@ -162,7 +162,7 @@ export default function membersRouter( * @apiError (Error 400) BadRequest Missing required fields. * * @apiExample {curl} Example usage: - * curl -X PATCH http://localhost:3000/members/approve/123 \ + * curl -X PATCH http://localhost:3000/api/v1/members/approve/123 \ * -H "Content-Type: application/json" \ * -d '{"isApproved": true, "adminId": "admin123"}' */ @@ -190,12 +190,12 @@ export default function membersRouter( * @apiError (Error 403) Forbidden Only Admins and Super Admins can ghost members. * * @apiExample {curl} Ghost a member: - * curl -X PATCH http://localhost:3000/members/ghost/123 \ + * curl -X PATCH http://localhost:3000/api/v1/members/ghost/123 \ * -H "Content-Type: application/json" \ * -d '{"adminId": "admin-id", "ghost": true}' * * @apiExample {curl} Unghost a member: - * curl -X PATCH http://localhost:3000/members/ghost/123 \ + * curl -X PATCH http://localhost:3000/api/v1/members/ghost/123 \ * -H "Content-Type: application/json" \ * -d '{"adminId": "admin-id", "ghost": false}' */ @@ -212,7 +212,7 @@ export default function membersRouter( * @apiSuccess {Object[]} achievements List of achievements. * * @apiExample {curl} Example usage: - * curl -X GET http://localhost:3000/members/123/achievements + * curl -X GET http://localhost:3000/api/v1/members/123/achievements */ router.get("/:memberId/achievements", memberCtrl.getUserAchievements); @@ -226,7 +226,7 @@ export default function membersRouter( * @apiSuccess {Object[]} projects List of projects. * * @apiExample {curl} Example usage: - * curl -X GET http://localhost:3000/members/123/projects + * curl -X GET http://localhost:3000/api/v1/members/123/projects */ router.get("/:memberId/projects", memberCtrl.getUserProjects); @@ -240,7 +240,7 @@ export default function membersRouter( * @apiSuccess {Object[]} interviews List of interviews. * * @apiExample {curl} Example usage: - * curl -X GET http://localhost:3000/members/123/interviews + * curl -X GET http://localhost:3000/api/v1/members/123/interviews */ router.get("/:memberId/interviews", memberCtrl.getUserInterviews); @@ -261,7 +261,7 @@ export default function membersRouter( * @apiError (Error 403) Forbidden Only Super Admins can assign roles. * * @apiExample {curl} Example usage: - * curl -X PATCH http://localhost:3000/members/123/role \ + * curl -X PATCH http://localhost:3000/api/v1/members/123/role \ * -H "Content-Type: application/json" \ * -d '{"adminId": "superadmin-id", "role": "ADMIN"}' */