diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..6a6c053 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,44 @@ +name: Build and Push Docker image to GHCR + +on: + push: + branches: [dev] + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set lowercase image name + id: meta + run: | + IMAGE_NAME="ghcr.io/${GITHUB_REPOSITORY,,}:latest" + echo "image=$IMAGE_NAME" >> $GITHUB_OUTPUT + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.image }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de9f486 --- /dev/null +++ b/.gitignore @@ -0,0 +1,202 @@ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +.vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore +node_modules +.env +.DS_Store + +*.txt +test \ No newline at end of file diff --git a/.hooks/commit-msg b/.hooks/commit-msg deleted file mode 100644 index e9c3524..0000000 --- a/.hooks/commit-msg +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -commit_message=$(cat "$1" | sed -e 's/^[[:space:]]*//') -matched_str=$(echo "$commit_message" | grep -E "^Merge.+|(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|types)(\(.+\))?!?: [a-zA-Z0-9 ]+$") -echo "$matched_str" -if [ "$matched_str" != "" ]; -then - exit 0 -else - echo "Commit rejected due to incorrect commit message format. See commit standards here - https://www.conventionalcommits.org/en/v1.0.0/" - exit 1 -fi diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c6651ee --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "cSpell.words": [ + "Buildx", + "gdgvit", + "kargo", + "libc", + "Mailgun", + "NEXTAUTH", + "upayan" + ], + "colorWheel.settings.workspaceColor": "hsl(176,58%,68%)" +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8fc127f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +# syntax=docker/dockerfile:1.5 + +FROM --platform=$BUILDPLATFORM node:20-slim AS base +WORKDIR /app + +RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends libc6 && rm -rf /var/lib/apt/lists/* + +FROM base AS deps +WORKDIR /app + +COPY api/package.json api/package-lock.json* ./ +RUN npm ci + +FROM base AS builder +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY api . + +RUN npm run build + +FROM node:20-slim AS runner +WORKDIR /app + +ENV NODE_ENV=production + +RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/* + +RUN apt-get update && \ + apt-get install -y --no-install-recommends python3 python3-pip && \ + ln -s /usr/bin/python3 /usr/bin/python && \ + rm -rf /var/lib/apt/lists/* + +RUN apt-get update && \ + apt-get install -y --no-install-recommends curl ca-certificates && \ + curl -LO https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl && \ + install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl && \ + rm kubectl && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +COPY --from=deps /app/node_modules ./node_modules +COPY --from=builder /app/dist ./dist +COPY api/package.json ./ + +COPY api/AI/requirements.txt ./AI/requirements.txt +RUN pip3 install --no-cache-dir --break-system-packages -r ./AI/requirements.txt + +COPY api/AI ./AI + +EXPOSE 5000 + +CMD ["node", "dist/server.js"] + diff --git a/README.md b/README.md index 8f25179..c39250a 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ GDSC VIT -

< Insert Project Title Here >

-

< Insert Project Description Here >

+

kargo

+

Kargo is a comprehensive, AI-augmented deployment platform that empowers developers to deploy Docker-based applications with maximum flexibility and ease

--- @@ -11,34 +11,42 @@ [![Discord Chat](https://img.shields.io/discord/760928671698649098.svg)](https://discord.gg/498KVdSKWR) [![DOCS](https://img.shields.io/badge/Documentation-see%20docs-green?style=flat-square&logo=appveyor)](INSERT_LINK_FOR_DOCS_HERE) - [![UI ](https://img.shields.io/badge/User%20Interface-Link%20to%20UI-orange?style=flat-square&logo=appveyor)](INSERT_UI_LINK_HERE) +[![UI ](https://img.shields.io/badge/User%20Interface-Link%20to%20UI-orange?style=flat-square&logo=appveyor)](INSERT_UI_LINK_HERE) ## Features -- [ ] < feature > -- [ ] < feature > -- [ ] < feature > -- [ ] < feature > +- [x] AI-Powered Docker Configuration - Automatic Dockerfile and Docker Compose generation +- [x] Kubernetes Native Integration - Secure cluster management and deployment +- [x] Multi-Provider Authentication - Google OAuth, GitHub OAuth, and local authentication +- [x] Real-time Application Monitoring - Live status tracking and metrics collection
## Dependencies - - < dependency > - - < dependency > + - Node.js 20 or higher with TypeScript 5.x + - MongoDB for database storage + - Python 3.8+ for AI features (LangChain, Groq) + - Kubernetes cluster access for deployment ## Running -< directions to install > +Clone the repository and install dependencies ```bash -< insert code > +git clone https://github.com/GDGVIT/kargo-backend.git +cd kargo-backend +npm install +cd api && npm install +pip install -r AI/requirements.txt ``` -< directions to execute > +Configure environment and start the development server ```bash -< insert code > +cp example.env .env +# Edit .env with your configuration +npm run dev ``` ## Contributors @@ -46,15 +54,56 @@ + + +
- John Doe + Upayan Mazumder

- Your Name Here (Insert Your Image Link In Src + Upayan Mazumder

- + GitHub - + + LinkedIn + +

+
+ Swayam +

+ Swayam +

+ + GitHub + + + LinkedIn + +

+
+ Aayush Kumar +

+ Aayush Kumar +

+

+ + GitHub + + + LinkedIn + +

+
+ Noel Alex +

+ +

+

+ + GitHub + + LinkedIn

diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 0000000..db4c6d9 --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules \ No newline at end of file diff --git a/api/AI/.gitignore b/api/AI/.gitignore new file mode 100644 index 0000000..7cfd44e --- /dev/null +++ b/api/AI/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +output \ No newline at end of file diff --git a/api/AI/docker.py b/api/AI/docker.py new file mode 100644 index 0000000..c65c69c --- /dev/null +++ b/api/AI/docker.py @@ -0,0 +1,23 @@ +from main import dockerise +from sys import argv +import warnings +import os + +warnings.filterwarnings("ignore", category=DeprecationWarning) + +def main(): + if len(argv) < 2: + print("Usage: python docker.py git-repo-url") + exit(1) + repo_url_small = argv[1] + script_dir = os.path.dirname(os.path.abspath(__file__)) + output_dir = os.path.join(script_dir, "output") + os.makedirs(output_dir, exist_ok=True) + dockerfile, docker_compose = dockerise(repo_url_small) + with open(os.path.join(output_dir, "Dockerfile"), "w") as f: + f.write(dockerfile) + with open(os.path.join(output_dir, "docker-compose.yml"), "w") as f: + f.write(docker_compose) + +if __name__ == "__main__": + main() diff --git a/api/AI/main.py b/api/AI/main.py new file mode 100644 index 0000000..76748e3 --- /dev/null +++ b/api/AI/main.py @@ -0,0 +1,753 @@ +# Loaders +from langchain.schema import Document +# Splitters +from langchain.text_splitter import RecursiveCharacterTextSplitter +#Model +from langchain_groq import ChatGroq +from langchain.chains.summarize import load_summarize_chain +from dotenv import load_dotenv +from langchain_core.prompts import PromptTemplate +import re +import requests +import tempfile +import subprocess +import shutil +import os +from urllib.parse import urlparse +import fnmatch +from pathlib import Path + +load_dotenv() +verbose = False +PRG_EXTENSIONS = [ + # General Purpose & Scripting + '.py', # Python + '.rb', # Ruby + '.pl', # Perl + '.pm', # Perl Module + '.php', # PHP + '.js', # JavaScript + '.mjs', # JavaScript ES Module + '.cjs', # JavaScript CommonJS Module + '.ts', # TypeScript + '.tsx', # TypeScript with JSX (React) + '.jsx', # JavaScript with JSX (React) + '.lua', # Lua + '.groovy', # Groovy + '.tcl', # Tcl + '.sh', # Shell Script (Bash, sh, etc.) + '.bash', # Bash Script + '.zsh', # Zsh Script + '.ps1', # PowerShell (PowerShell Core runs on Linux) + '.swift', # Swift (server-side) + '.dart', # Dart (server-side, Flutter build) + '.coffee', # CoffeeScript + + # Compiled Languages (source files) + '.c', # C + '.h', # C Header + '.cpp', # C++ + '.cc', # C++ + '.cxx', # C++ + '.hpp', # C++ Header + '.hh', # C++ Header + '.java', # Java + '.scala', # Scala + '.sc', # Scala Script + '.kt', # Kotlin + '.kts', # Kotlin Script + '.go', # Go + '.rs', # Rust + '.cs', # C# (.NET Core/5+) + '.fs', # F# (.NET Core/5+) + '.fsx', # F# Script + '.vb', # VB.NET (.NET Core/5+) + '.d', # D + '.pas', # Pascal + '.pp', # Free Pascal + '.f', # Fortran + '.for', # Fortran + '.f90', # Fortran 90 + '.f95', # Fortran 95 + '.ada', # Ada + '.adb', # Ada Body + '.ads', # Ada Spec + '.cob', # COBOL (GnuCOBOL) + '.cbl', # COBOL + '.asm', # Assembly + '.s', # Assembly (Unix-like) + '.vala', # Vala + '.nim', # Nim + '.cr', # Crystal + '.zig', # Zig + '.m', # Objective-C (compilers exist for Linux) or MATLAB/Octave + + # Functional Languages (source files) + '.hs', # Haskell + '.lhs', # Literate Haskell + '.lisp', # Lisp + '.lsp', # Lisp + '.cl', # Common Lisp + '.el', # Emacs Lisp (can be scripted) + '.scm', # Scheme + '.ss', # Scheme + '.rkt', # Racket + '.clj', # Clojure + '.cljs', # ClojureScript + '.cljc', # Clojure/ClojureScript (shared) + '.ml', # OCaml + '.mli', # OCaml Interface + '.elm', # Elm (compiles to JS) + '.erl', # Erlang + '.hrl', # Erlang Header + '.ex', # Elixir + '.exs', # Elixir Script + '.purs', # PureScript (compiles to JS) + '.idr', # Idris + '.re', # ReasonML + '.rei', # ReasonML Interface + + '.vue', # Vue.js Single File Components + '.svelte', # Svelte components + + '.R', # R + '.ipynb', # Jupyter Notebook (Python, R, Julia, etc.) + + '.md' # Markdown +] +model = ChatGroq( + groq_api_key=os.getenv("GROQ"), + model_name="llama-3.3-70b-versatile", + max_tokens=2000, + ) +def large_summariser(code_base, directory_structure): + text = code_base + text_splitter = RecursiveCharacterTextSplitter( + separators=["\n\n", "\n", "\t"], chunk_size=10000, chunk_overlap=50 + ) + + docs = text_splitter.create_documents([text]) + + num_documents = len(docs) + +# llm = ChatGroq( +# groq_api_key=os.getenv("GROQ"), +# model_name="llama-3.2-3b-preview", +# max_tokens=1000, +# ) + + map_prompt = """The following text contains snippets from a project’s codebase. Enclosed within triple backticks (```), extract only the **relevant information** needed to write a `Dockerfile` and `docker-compose.yml`, including: +- Main programming language and estimated runtime version +- Presence of a dependency manifest (e.g., `requirements.txt`, `package.json`) +- System-level dependencies (e.g., `curl`, `gcc`) +- Use of virtual environments or language-specific package managers +- Build tools required (e.g., `make`, `maven`) +- Pre-/post-build steps +- Application entry point +- Files/folders to be added to `.dockerignore` +- Required config files (`.env`, `.yaml`, etc.) +- Environment variables used at runtime +- Custom entrypoint scripts (if any) +- Ports to expose +- Inter-container communication +- Required services (DBs like Postgres, Redis, etc.) +- Folders needing persistent volumes (only if necessary) +Only include what's clearly evident or logically inferred from the codebase. Avoid assumptions or unnecessary services, volumes, or steps. +```{text}``` + """ + map_prompt_template = PromptTemplate(template=map_prompt, input_variables=["text"]) + + map_chain = load_summarize_chain( + llm=model, chain_type="stuff", prompt=map_prompt_template, verbose=verbose + ) + + selected_docs = list(docs) + + # Make an empty list to hold your summaries + summary_list = [] + + # Loop through a range of the lenght of your selected docs + for i, doc in enumerate(selected_docs): + # Go get a summary of the chunk + chunk_summary = map_chain.run([doc]) + + # If the summary is a dict (as returned by some LLM chains), extract the string + if isinstance(chunk_summary, dict): + # Try to get the main content from common keys + if 'output_text' in chunk_summary: + chunk_summary = chunk_summary['output_text'] + elif 'text' in chunk_summary: + chunk_summary = chunk_summary['text'] + else: + chunk_summary = str(chunk_summary) + print(chunk_summary) + # Append that summary to your list + summary_list.append(chunk_summary) + + summaries = "\n".join(str(s) for s in summary_list) + # Convert it back to a document + summaries = Document(page_content=summaries) + + llm2 = ChatGroq( + groq_api_key=os.getenv("GROQ"), + model_name="llama-3.3-70b-versatile", + max_tokens=2000, + ) + directory_structure = directory_structure[:7000] if len(directory_structure) > 7000 else directory_structure + combine_prompt = directory_structure+""" + You will be given a passage containing **compiled information extracted from a codebase** using earlier prompts that analyze various components relevant for building Dockerfiles and Docker Compose configurations. +Your task is to: +1. **Aggregate** all relevant insights from the passage. +2. **Organize the data** into clearly structured categories useful for containerization. +3. **Eliminate redundancy**, contradictions, or irrelevant assumptions. +4. **Summarize the final context** to be used for writing an optimized, production-ready `Dockerfile` and `docker-compose.yml`. +Your output should be well-organized, logically grouped, and include only details that are clearly evident or _strongly inferred_ from the provided content. The final structure should be clean and concise, ready to guide containerization of the application. +Organize your summary under the following structured headings: +- **Language & Runtime** +- **Dependency Management** +- **Build & Compilation Tools** +- **App Structure & Entry Point** +- **Environment & Configs** +- **Ports & Networking** +- **Services & Inter-Container Needs** +- **Volumes & Persistence** +- **Entrypoint/Startup Requirements** +- **Other Notable Observations (if any)** +Ensure no unnecessary Dockerfile steps or services are introduced unless logically inferred or explicitly mentioned. Volumes should also be mounted where needed (ie for data that needs to be written down and persisted) +the output should only consist of the dockerfile and docker-compose code block, nothing more under no circumstances. The output should be enclosed in ```dockerfile ``` for the dockerfile and ```yml ``` for the docker-compose +The input will be enclosed in triple backticks: + +```{text}``` + + + """ + combine_prompt_template = PromptTemplate( + template=combine_prompt, input_variables=["text"] + ) + reduce_chain = load_summarize_chain( + llm=llm2, chain_type="stuff", prompt=combine_prompt_template, verbose=verbose + ) # Set this to true if you want to see the inner workings + + output = reduce_chain.run([summaries]) + print(output) + return output + + + +def get_folder_structure(repo_root_path: Path, ignore_patterns: list = None, max_depth: int = 5) -> str: + """ + Generates a string representation of the folder structure, optimized for + viewing files relevant to Dockerfile/docker-compose development. + + This function aggressively ignores common files and directories that are not + typically included in a production Docker image, such as test suites, + documentation, local virtual environments, and build artifacts. + + Args: + repo_root_path (Path): The absolute path to the root of the repository. + ignore_patterns (list, optional): A list of glob patterns for files/directories + to ignore. If None, a comprehensive list + for Docker development is used. + max_depth (int, optional): Maximum depth to traverse. Defaults to 5, which is + usually sufficient for a high-level overview. + + Returns: + str: A string representing the folder structure. + """ + if not repo_root_path.is_dir(): + return f"Error: Provided path '{repo_root_path}' is not a valid directory." + + # A more comprehensive ignore list tailored for creating Docker images. + # We ignore anything that isn't part of the final application runtime. + if ignore_patterns is None: + ignore_patterns = [ + # Version control + '.git', + # Python specific + '__pycache__', '*.pyc', '*.pyo', '*.pyd', '*.egg-info', 'pip-wheel-metadata', + # Virtual environments + '.venv', 'venv', 'env', 'ENV', + # IDE / Editor config + '.vscode', '.idea', '.project', '.settings', + # Build artifacts + 'build', 'dist', 'target', 'out', 'bin', + # Dependency caches + 'node_modules', 'bower_components', + # Testing + 'tests', 'test', '*.test.js', '*.spec.js', 'pytest.ini', '.pytest_cache', + # Documentation + 'docs', 'site', 'mkdocs.yml', + # CI/CD + '.github', '.gitlab-ci.yml', '.circleci', + # OS specific + '.DS_Store', 'Thumbs.db', + # Logs and temporary files + '*.log', '*.tmp', '*.swp', + # Docker context itself + '.dockerignore', + ] + + structure_lines = [f"Repository Structure: {repo_root_path.name}"] + prefix_item = "├── " + prefix_last_item = "└── " + prefix_indent = "│ " + prefix_empty_indent = " " + + def _should_ignore(path_obj: Path) -> bool: + """Checks if a path name matches any of the ignore patterns.""" + for pattern in ignore_patterns: + if fnmatch.fnmatch(path_obj.name, pattern): + return True + return False + + def _generate_structure_recursive(current_path: Path, current_prefix: str, depth: int): + if max_depth > 0 and depth > max_depth: + structure_lines.append(f"{current_prefix}{prefix_last_item}... (max depth reached)") + return + + try: + # Filter out ignored items before processing + items = [ + item for item in sorted(current_path.iterdir(), key=lambda p: (p.is_file(), p.name.lower())) + if not _should_ignore(item) + ] + except OSError as e: + structure_lines.append(f"{current_prefix}└── [Error reading: {e.strerror}]") + return + + for i, item_path in enumerate(items): + is_last = (i == len(items) - 1) + connector = prefix_last_item if is_last else prefix_item + structure_lines.append(f"{current_prefix}{connector}{item_path.name}") + + if item_path.is_dir(): + new_prefix = current_prefix + (prefix_empty_indent if is_last else prefix_indent) + _generate_structure_recursive(item_path, new_prefix, depth + 1) + + _generate_structure_recursive(repo_root_path, "", 1) + return "\n".join(structure_lines) + +def extract_port_snippets(codebase_text, context_lines=3): + """ + Extracts snippets of code from a large text string wherever a potential + port declaration is found. + + Args: + codebase_text (str): The entire codebase as a single string. + context_lines (int): Number of lines before and after the matching line + to include in the snippet. + + Returns: + list: A list of strings, where each string is a code snippet. + Each snippet will also include a header indicating the + approximate line number and the pattern that matched. + """ + snippets = [] + # Split the codebase into lines to easily get context + # Keepends=True is important if you want to preserve original line endings in snippets + lines = codebase_text.splitlines(keepends=True) + if not lines: # Handle empty codebase text + return [] + + # Regex patterns for port declarations. + # These are illustrative; you'll want to expand and refine them. + # The 'desc' field helps identify which pattern matched. + PORT_REGEX_PATTERNS = [ + {"desc": "Dockerfile EXPOSE", "pattern": r"^\s*EXPOSE\s+([1-9]\d{2,4})\b"}, + {"desc": "docker-compose ports (target)", "pattern": r"ports:\s*-\s*[\"']?\d+:([1-9]\d{2,4})[\"']?"}, + {"desc": "docker-compose ports (simple)", "pattern": r"ports:\s*-\s*[\"']?([1-9]\d{2,4})[\"']?"}, + # e.g. - "3000" + {"desc": "Node.js/Express app.listen", "pattern": r"\.listen\(\s*([1-9]\d{2,4})\s*[,)]"}, + {"desc": "Python Flask app.run port", "pattern": r"app\.run\(.*port\s*=\s*([1-9]\d{2,4})"}, + {"desc": "Python http.server", "pattern": r"HTTPServer\(\s*\([^,]+,\s*([1-9]\d{2,4})\s*\)"}, + {"desc": "Python socket.bind port", "pattern": r"\.bind\(\s*\([^,]+,\s*([1-9]\d{2,4})\s*\)"}, + {"desc": "Java Spring server.port", "pattern": r"^\s*server\.port\s*=\s*([1-9]\d{2,4})"}, + {"desc": ".env PORT variable", "pattern": r"^\s*PORT\s*=\s*([1-9]\d{2,4})\b"}, + {"desc": "Generic 'port' keyword followed by number", "pattern": r"port[\s:=]+([1-9]\d{2,4})\b"}, + ] + + # To avoid adding highly overlapping snippets from different regexes on the same line, + # we can keep track of line numbers already processed for a snippet. + # This is a simple form of deduplication. + processed_line_indices = set() + + for i, line_content in enumerate(lines): + if i in processed_line_indices: + continue # Already part of a snippet from this line + + for config in PORT_REGEX_PATTERNS: + pattern = config["pattern"] + description = config["desc"] + try: + # We search line by line because finditer on the whole text + # makes it harder to get line-based context without more complex line mapping. + match = re.search(pattern, line_content, re.IGNORECASE) + if match: + # Determine snippet boundaries + start_line_idx = max(0, i - context_lines) + end_line_idx = min(len(lines), i + 1 + context_lines) # +1 because slice is exclusive + + # Extract the snippet lines + snippet_lines = lines[start_line_idx:end_line_idx] + + # Add a header to the snippet + header = f"--- Snippet: Match for '{description}' (approx line {i + 1}) ---\n" + snippet_text = header + "".join(snippet_lines) + snippets.append(snippet_text) + + # Mark these lines as processed to avoid too much overlap + for j in range(start_line_idx, end_line_idx): + processed_line_indices.add(j) + + break # Move to the next line in the codebase after finding a match on this line + except re.error as e: + print(f"Regex error with pattern '{pattern}': {e}") + continue + + if snippets: + unique_snippets_dict = {snip: None for snip in snippets} # Preserves order in Python 3.7+ + snippets = list(unique_snippets_dict.keys()) + + return snippets + + +def _extract_volume_snippets_from_content( + file_content_str: str, + file_relative_path: Path, # Relative path of the current file within the repo + repo_root_path: Path, # Absolute path to the root of the cloned repo on the host + context_lines: int = 2 +) -> list: + """ + Extracts snippets of code from file content wherever a potential + volume, persistent path, or database file is mentioned. + Resolves relative paths to be "normalized relative to the repository root". + Attempts to filter out URLs and non-path-like strings. + """ + snippets = [] + lines = file_content_str.splitlines(keepends=True) + if not lines: + return [] + + # Regex patterns for volumes/paths. + # Each dict: desc, pattern, path_group_idx, is_container_absolute, confidence + VOLUME_PATTERNS_CONFIG = [ + { + "desc": "Dockerfile VOLUME instruction", + # Matches "VOLUME /path" or "VOLUME ["/path1", "/path2"]" + "pattern": r"^\s*VOLUME\s+(?:\[\s*(?:\"([^\"]+)\"|\'([^\']+)\')(?:,\s*(?:\"([^\"]+)\"|\'([^\']+)\'))*\s*\]|([/\w\.-]+(?:[/\w\s\.-]*[/\w\.-]+)?))", + "path_groups": [1, 2, 3, 4, 5], # All potential quoted paths or the direct path + "is_container_absolute": True, "confidence": 10 + }, + { + "desc": "docker-compose volume (container path)", + "pattern": r"volumes:\s*-.*:(? 3 and not any(c in original_matched_path_str for c in '/\\.'): # more than 3 words, no path chars + # print(f"Skipping generic phrase: {original_matched_path_str}") + continue + # 3. Filter out if looks like env var placeholder + if original_matched_path_str.startswith("${") and original_matched_path_str.endswith("}"): + # print(f"Skipping env var placeholder: {original_matched_path_str}") + continue + # 4. Avoid non-path parts of Dockerfile VOLUME array string + if config["desc"] == "Dockerfile VOLUME instruction" and original_matched_path_str in ('[', ']', ','): + continue + + + notes = "" + effective_path_str = "" + + if config["is_container_absolute"]: + effective_path_str = Path(original_matched_path_str).as_posix() + notes = " (Path is absolute in container definition or code)" + else: + path_in_code = Path(original_matched_path_str) + if path_in_code.is_absolute(): + effective_path_str = path_in_code.as_posix() + notes = " (Path is absolute in code, refers to container filesystem)" + else: + path_from_repo_root = file_relative_path.parent / path_in_code + normalized_path_str = os.path.normpath(path_from_repo_root.as_posix()) + effective_path_str = Path(normalized_path_str).as_posix() + notes = " (Path normalized relative to repository root)" + if effective_path_str.startswith(".."): + notes += " - WARNING: Path appears to go above repository root." + + start_line_idx = max(0, i - context_lines) + end_line_idx = min(len(lines), i + 1 + context_lines) + snippet_lines = lines[start_line_idx:end_line_idx] + + # Desired output format + header = ( +# f" {effective_path_str} Effective Path Suggestion (relative to repo root or absolute): '{effective_path_str}'{notes}\n" +# f" (Original in code: '{original_matched_path_str}', File: {file_relative_path.as_posix()}:{i + 1}, Pattern: '{config['desc']}')\n" + f"--- Code Context ---\n" + ) + snippet_text = header + "".join(snippet_lines) + snippets.append(snippet_text) + + for j in range(start_line_idx, end_line_idx): + processed_line_indices.add(j) + + break # Break from iterating through VOLUME_PATTERNS_CONFIG for this line + if i in processed_line_indices: # If the inner loop added to processed_line_indices + break # Break from outer match loop to go to next line + + except re.error as e: + print(f"Regex error for '{config['desc']}' with pattern '{config['pattern']}': {e}") + continue + except IndexError: + print(f"Warning: Path group issue for pattern '{config['pattern']}'") + continue + + if snippets: + unique_snippets_dict = {snip: None for snip in snippets} + snippets = list(unique_snippets_dict.keys()) + + return snippets +def get_repo_code_as_string_and_volume_snippets( + github_url: str, + max_size_mb: float = 200.0, + target_extensions: list = PRG_EXTENSIONS, # For main codebase string + snippet_scan_extensions: list = None, # For volume snippet scanning (broader by default) + ignore_patterns: list = None, + branch: str = None +) -> tuple: # Returns (str | None, list | None) -> (codebase_string, volume_snippets) + """ + Checks GitHub repo size, downloads it, consolidates code into a single string, + and extracts snippets related to potential persistent volumes. + (Full function body from previous correct answer) + """ + if ignore_patterns is None: + ignore_patterns = ['.git', 'node_modules', '__pycache__', 'dist', 'build', 'target', 'vendor', '.venv', 'venv', '*.lock', 'package-lock.json'] + if snippet_scan_extensions is None: + snippet_scan_extensions = [] + + match = re.match(r"https?://github\.com/([^/]+)/([^/.]+)(\.git)?", github_url) + if not match: + return "Error: Invalid GitHub URL format.", None, None + owner, repo_name = match.group(1), match.group(2) + + api_url = f"https://api.github.com/repos/{owner}/{repo_name}" + try: + response = requests.get(api_url) + response.raise_for_status() + repo_data = response.json() + except requests.exceptions.RequestException as e: + return f"Error: Could not fetch repository data from GitHub API: {e}", None, None + + repo_size_kb = repo_data.get("size", 0) + repo_size_mb = repo_size_kb / 1024 + print(f"Repository: {owner}/{repo_name}, Reported size: {repo_size_mb:.2f} MB") + + if repo_size_mb > max_size_mb: + return f"Error: Repository size ({repo_size_mb:.2f}MB) > max ({max_size_mb}MB).", None, None + + temp_dir_path_obj = Path(tempfile.mkdtemp()) + print(f"Cloning into temporary directory: {temp_dir_path_obj}...") + try: + clone_command = ["git", "clone", "--depth", "1"] + if branch: + clone_command.extend(["--branch", branch]) + clone_command.extend([github_url, str(temp_dir_path_obj)]) + subprocess.run(clone_command, capture_output=True, text=True, check=True, encoding='utf-8') + print("Clone successful.") + except subprocess.CalledProcessError as e: + shutil.rmtree(temp_dir_path_obj) + error_message = f"Error: Could not clone repository.\nGit stderr: {e.stderr}\nGit stdout: {e.stdout}" + # Attempt to decode if bytes + if isinstance(e.stderr, bytes): + error_message = f"Error: Could not clone repository.\nGit stderr: {e.stderr.decode(errors='replace')}\nGit stdout: {e.stdout.decode(errors='replace')}" + return error_message, None, None + except FileNotFoundError: + shutil.rmtree(temp_dir_path_obj) + return "Error: Git command not found. Ensure Git is installed and in PATH.", None, None + + all_code_string = "" + all_volume_snippets = [] + processed_files_for_code = 0 + scanned_files_for_snippets = 0 # Total files scanned, not just those yielding snippets + + print("Processing files...") + for current_path_obj in temp_dir_path_obj.rglob('*'): + relative_path_obj = current_path_obj.relative_to(temp_dir_path_obj) + + skip = False + path_parts = relative_path_obj.parts + for pattern in ignore_patterns: + # Handle glob patterns in ignore_patterns if needed, e.g. using fnmatch + if pattern in path_parts or (pattern.startswith('.') and relative_path_obj.name == pattern) or \ + (pattern.endswith('.lock') and relative_path_obj.name.endswith('.lock')) or \ + (relative_path_obj.name == 'package-lock.json' and pattern == 'package-lock.json'): + skip = True + break + if skip: + continue + + if current_path_obj.is_file(): + file_content = None + try: + with open(current_path_obj, 'r', encoding='utf-8', errors='ignore') as f: + file_content = f.read() + except Exception as e: + print(f"Warning: Could not read file {relative_path_obj.as_posix()}: {e}") + continue + + include_in_main_code = not target_extensions or \ + current_path_obj.suffix.lower() in [ext.lower() for ext in target_extensions] + if include_in_main_code: + all_code_string += f"--- File: {relative_path_obj.as_posix()} ---\n" + all_code_string += file_content + all_code_string += "\n\n" + processed_files_for_code += 1 + + scan_for_snippets = not snippet_scan_extensions or \ + current_path_obj.suffix.lower() in [ext.lower() for ext in snippet_scan_extensions] + if scan_for_snippets and file_content: + scanned_files_for_snippets += 1 # Count every file we attempt to scan + snippets_from_file = _extract_volume_snippets_from_content( + file_content, + relative_path_obj, + temp_dir_path_obj + ) + if snippets_from_file: + all_volume_snippets.extend(snippets_from_file) + + print(f"Processed {processed_files_for_code} files for codebase string.") + print(f"Scanned {scanned_files_for_snippets} files for volume snippets.") + + try: + shutil.rmtree(temp_dir_path_obj) + print(f"Cleaned up temporary directory: {temp_dir_path_obj}") + except Exception as e: + print(f"Warning: Could not remove temporary directory {temp_dir_path_obj}: {e}") + + if not all_code_string and processed_files_for_code == 0 and target_extensions: + all_code_string = "Warning: No files matched target_extensions for codebase string." + if not all_volume_snippets and scanned_files_for_snippets > 0: # If we scanned but found nothing + all_volume_snippets.append("--- Info: No potential volume snippets found in scanned files. ---") + elif scanned_files_for_snippets == 0 and snippet_scan_extensions: + all_volume_snippets.append("--- Info: No files matched snippet_scan_extensions for volume snippets. ---") + + + return all_code_string, all_volume_snippets, get_folder_structure(temp_dir_path_obj) + + +def dockerise(repo_url_small): + code_str, vol_snippets, directory_structure = get_repo_code_as_string_and_volume_snippets(repo_url_small) + if vol_snippets is None: + vol_snippets = [] + if code_str is None: + code_str = "" + if directory_structure is None: + directory_structure = "" # Fix: avoid TypeError if None + code_str = "".join(code_str) if isinstance(code_str, (list, tuple)) else str(code_str) + vol_snippets = "".join(vol_snippets) if isinstance(vol_snippets, (list, tuple)) else str(vol_snippets) + if len(vol_snippets) < 7000: + vol_snippets = "Possible volumes\n"+("".join(vol_snippets)) + else: + vol_snippets = "Possible volumes\n"+("".join(vol_snippets)) + vol_snippets = vol_snippets[:7000] + ports = "Possible ports\n"+("".join(extract_port_snippets(code_str))) + + if ports == []: + ports = "None" + if vol_snippets == []: + vol_snippets = "None" + + result = vol_snippets+"\n"+ports + + + docker = large_summariser(result, directory_structure) + + # If the output is a dict, try to extract the main text + if isinstance(docker, dict): + if 'output_text' in docker: + docker = docker['output_text'] + elif 'text' in docker: + docker = docker['text'] + else: + docker = str(docker) + # Now docker is a string + docker = str(docker) + # Defensive: check for 'dockerfile' and 'yml' blocks before extracting + if "dockerfile" in docker and "yml" in docker: + docker = docker[docker.index("dockerfile"):] + dockerfile = docker[:docker.index("```")] + docker_compose = docker[docker.index("yml"):] + docker_compose = docker_compose[:docker_compose.index("```")] + else: + dockerfile = "" + docker_compose = "" + return "#"+dockerfile, "#"+docker_compose + +if __name__ == "__main__": + repo_url_small = "https://github.com/tiangolo/fastapi" + + + + dockerfile, docker_compose = dockerise(repo_url_small) + print(dockerfile) + print(docker_compose) + + + diff --git a/api/AI/requirements.txt b/api/AI/requirements.txt new file mode 100644 index 0000000..e3bf6df Binary files /dev/null and b/api/AI/requirements.txt differ diff --git a/api/nodemon.json b/api/nodemon.json new file mode 100644 index 0000000..7178b1a --- /dev/null +++ b/api/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": ["src"], + "ext": "ts,js,json", + "ignore": ["dist"], + "exec": "ts-node ./src/server.ts" +} diff --git a/api/package-lock.json b/api/package-lock.json new file mode 100644 index 0000000..d041179 --- /dev/null +++ b/api/package-lock.json @@ -0,0 +1,4264 @@ +{ + "name": "api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "api", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.9.0", + "bcryptjs": "^3.0.2", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.5.0", + "express": "^5.1.0", + "express-rate-limit": "^7.5.0", + "express-session": "^1.18.1", + "js-yaml": "^4.1.0", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.15.1", + "morgan": "^1.10.0", + "next-auth": "^4.24.11", + "nodemailer": "^6.10.1", + "nodemon": "^3.1.10", + "passport": "^0.7.0", + "passport-github2": "^0.1.12", + "passport-google-oauth20": "^2.0.0", + "passport-local": "^1.0.0", + "razorpay": "^2.9.6", + "rimraf": "^6.0.1", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.8.3" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cookie-parser": "^1.4.8", + "@types/cors": "^2.8.18", + "@types/express": "^5.0.2", + "@types/express-session": "^1.18.1", + "@types/js-yaml": "^4.0.9", + "@types/jsonwebtoken": "^9.0.9", + "@types/morgan": "^1.9.9", + "@types/node": "^22.15.30", + "@types/nodemailer": "^6.4.17", + "@types/passport": "^1.0.17", + "@types/passport-github2": "^1.2.9", + "@types/passport-google-oauth20": "^2.0.16", + "@types/passport-local": "^1.0.38", + "@types/rimraf": "^3.0.2" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.3.tgz", + "integrity": "sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz", + "integrity": "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@next/env": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.0.tgz", + "integrity": "sha512-sDaprBAfzCQiOgo2pO+LhnV0Wt2wBgartjrr+dpcTORYVnnXD0gwhHhiiyIih9hQbq+JnbqH4odgcFWhqCGidw==", + "license": "MIT", + "peer": true + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.0.tgz", + "integrity": "sha512-v7Jj9iqC6enxIRBIScD/o0lH7QKvSxq2LM8UTyqJi+S2w2QzhMYjven4vgu/RzgsdtdbpkyCxBTzHl/gN5rTRg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.0.tgz", + "integrity": "sha512-s2Nk6ec+pmYmAb/utawuURy7uvyYKDk+TRE5aqLRsdnj3AhwC9IKUBmhfnLmY/+P+DnwqpeXEFIKe9tlG0p6CA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.0.tgz", + "integrity": "sha512-mGlPJMZReU4yP5fSHjOxiTYvZmwPSWn/eF/dcg21pwfmiUCKS1amFvf1F1RkLHPIMPfocxLViNWFvkvDB14Isg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.0.tgz", + "integrity": "sha512-biWqIOE17OW/6S34t1X8K/3vb1+svp5ji5QQT/IKR+VfM3B7GvlCwmz5XtlEan2ukOUf9tj2vJJBffaGH4fGRw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.0.tgz", + "integrity": "sha512-zPisT+obYypM/l6EZ0yRkK3LEuoZqHaSoYKj+5jiD9ESHwdr6QhnabnNxYkdy34uCigNlWIaCbjFmQ8FY5AlxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.0.tgz", + "integrity": "sha512-+t3+7GoU9IYmk+N+FHKBNFdahaReoAktdOpXHFIPOU1ixxtdge26NgQEEkJkCw2dHT9UwwK5zw4mAsURw4E8jA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.0.tgz", + "integrity": "sha512-d8MrXKh0A+c9DLiy1BUFwtg3Hu90Lucj3k6iKTUdPOv42Ve2UiIG8HYi3UAb8kFVluXxEfdpCoPPCSODk5fDcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.0.tgz", + "integrity": "sha512-Fe1tGHxOWEyQjmygWkkXSwhFcTJuimrNu52JEuwItrKJVV4iRjbWp9I7zZjwqtiNnQmxoEvoisn8wueFLrNpvQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "license": "MIT" + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", + "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.18.tgz", + "integrity": "sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.2.tgz", + "integrity": "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/morgan": { + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", + "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.15.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", + "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/oauth": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz", + "integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-github2": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@types/passport-github2/-/passport-github2-1.2.9.tgz", + "integrity": "sha512-/nMfiPK2E6GKttwBzwj0Wjaot8eHrM57hnWxu52o6becr5/kXlH/4yE2v2rh234WGvSgEEzIII02Nc5oC5xEHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-oauth2": "*" + } + }, + "node_modules/@types/passport-google-oauth20": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.16.tgz", + "integrity": "sha512-ayXK2CJ7uVieqhYOc6k/pIr5pcQxOLB6kBev+QUGS7oEZeTgIs1odDobXRqgfBPvXzl0wXCQHftV5220czZCPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-oauth2": "*" + } + }, + "node_modules/@types/passport-local": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz", + "integrity": "sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-oauth2": { + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.17.tgz", + "integrity": "sha512-ODiAHvso6JcWJ6ZkHHroVp05EHGhqQN533PtFNBkg8Fy5mERDqsr030AX81M0D69ZcaMvhF92SRckEk2B0HYYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", + "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0", + "peer": true + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT", + "peer": true + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", + "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mongodb": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.16.0.tgz", + "integrity": "sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.3", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.15.1.tgz", + "integrity": "sha512-RhQ4DzmBi5BNGcS0w4u1vdMRIKcteXTCNzDt1j7XRcdWYBz1MjMjulBhPaeC5jBCHOD1yinuOFTTSOWLLGexWw==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.3", + "kareem": "2.6.3", + "mongodb": "~6.16.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/next": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.0.tgz", + "integrity": "sha512-N1lp9Hatw3a9XLt0307lGB4uTKsXDhyOKQo7uYMzX4i0nF/c27grcGXkLdb7VcT8QPYLBa8ouIyEoUQJ2OyeNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@next/env": "15.5.0", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.0", + "@next/swc-darwin-x64": "15.5.0", + "@next/swc-linux-arm64-gnu": "15.5.0", + "@next/swc-linux-arm64-musl": "15.5.0", + "@next/swc-linux-x64-gnu": "15.5.0", + "@next/swc-linux-x64-musl": "15.5.0", + "@next/swc-win32-arm64-msvc": "15.5.0", + "@next/swc-win32-x64-msvc": "15.5.0", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-auth": { + "version": "4.24.11", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", + "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "license": "ISC", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.7.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "@auth/core": "0.34.2", + "next": "^12.2.5 || ^13 || ^14 || ^15", + "nodemailer": "^6.6.5", + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@auth/core": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/next-auth/node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/oidc-token-hash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", + "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-github2": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz", + "integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC", + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.26.7", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.7.tgz", + "integrity": "sha512-43xS+QYc1X1IPbw03faSgY6I6OYWcLrJRv3hU0+qMOfh/XCHcP0MX2CVjNARYR2cC/guu975sta4OcjlczxD7g==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/razorpay": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/razorpay/-/razorpay-2.9.6.tgz", + "integrity": "sha512-zsHAQzd6e1Cc6BNoCNZQaf65ElL6O6yw0wulxmoG5VQDr363fZC90Mp1V5EktVzG45yPyNomNXWlf4cQ3622gQ==", + "license": "MIT", + "dependencies": { + "axios": "^1.6.8" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "peer": true, + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ts-node-dev/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "peer": true + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..738a554 --- /dev/null +++ b/api/package.json @@ -0,0 +1,58 @@ +{ + "name": "api", + "version": "1.0.0", + "description": "", + "main": "dist/server.js", + "scripts": { + "build": "tsc", + "start": "node dist/server.js", + "dev": "nodemon --watch \"src/**/*.ts\" --exec ts-node src/server.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "axios": "^1.9.0", + "bcryptjs": "^3.0.2", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.5.0", + "express": "^5.1.0", + "express-rate-limit": "^7.5.0", + "express-session": "^1.18.1", + "js-yaml": "^4.1.0", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.15.1", + "morgan": "^1.10.0", + "next-auth": "^4.24.11", + "nodemailer": "^6.10.1", + "nodemon": "^3.1.10", + "passport": "^0.7.0", + "passport-github2": "^0.1.12", + "passport-google-oauth20": "^2.0.0", + "passport-local": "^1.0.0", + "razorpay": "^2.9.6", + "rimraf": "^6.0.1", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.8.3" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cookie-parser": "^1.4.8", + "@types/cors": "^2.8.18", + "@types/express": "^5.0.2", + "@types/express-session": "^1.18.1", + "@types/js-yaml": "^4.0.9", + "@types/jsonwebtoken": "^9.0.9", + "@types/morgan": "^1.9.9", + "@types/node": "^22.15.30", + "@types/nodemailer": "^6.4.17", + "@types/passport": "^1.0.17", + "@types/passport-github2": "^1.2.9", + "@types/passport-google-oauth20": "^2.0.16", + "@types/passport-local": "^1.0.38", + "@types/rimraf": "^3.0.2" + } +} diff --git a/api/src/api.ts b/api/src/api.ts new file mode 100644 index 0000000..a64b092 --- /dev/null +++ b/api/src/api.ts @@ -0,0 +1,82 @@ +import express, { Request, Response } from "express"; +import cors from "cors"; +import session from "express-session"; +import passport from "passport"; +import cookieParser from "cookie-parser"; +import morgan from "morgan"; +import authRoutes from "./routes/auth.routes"; +import githubRoutes from "./routes/github.routes"; +import applicationRoutes from "./routes/application.routes"; +import userRoutes from "./routes/user.routes"; +import planRoutes from "./routes/plan.routes"; +import metricsRoutes from "./routes/metrics.routes"; +import log from "./utils/logging/logger"; +import env from "./config/env"; +import "./auth/passport"; +import "./auth/local.strategy"; + +const app = express(); +const frontendUrl = env.FRONTEND_URL; +const production = env.NODE_ENV === "production"; + +if (production) { + app.set("trust proxy", 1); +} + +log({ + type: "info", + message: `Server starting at ${new Date().toISOString()} | ENV: ${ + env.NODE_ENV + }`, +}); + +app.use( + cors({ + origin: frontendUrl, + credentials: true, + }) +); + +app.use(morgan("dev")); +app.use(express.json()); +app.use(cookieParser()); + +const sessionSecret = env.SESSION_SECRET; +if (!sessionSecret) { + throw new Error("SESSION_SECRET is not set in environment variables."); +} + +app.use( + session({ + name: "kargo.sid", + secret: sessionSecret, + resave: false, + saveUninitialized: false, + cookie: { + httpOnly: true, + secure: production, + sameSite: "lax", + domain: production ? ".kargo.upayan.dev" : undefined, + maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week + }, + }) +); + +app.use(passport.initialize()); +app.use(passport.session()); + +app.get("/", (_req: Request, res: Response) => { + res.json({ + message: "API is running", + uptime: Math.round(process.uptime()), + }); +}); + +app.use("/api/auth", authRoutes); +app.use("/api/github", githubRoutes); +app.use("/api/applications", applicationRoutes); +app.use("/api/users", userRoutes); +app.use("/api/plans", planRoutes); +app.use("/api/metrics", metricsRoutes); + +export default app; diff --git a/api/src/auth/local.strategy.ts b/api/src/auth/local.strategy.ts new file mode 100644 index 0000000..75992c9 --- /dev/null +++ b/api/src/auth/local.strategy.ts @@ -0,0 +1,68 @@ +import passport from "passport"; +import { Strategy as LocalStrategy } from "passport-local"; +import bcrypt from "bcryptjs"; +import User from "../models/user.model"; +import Plan from "../models/plan.model"; +import log from "../utils/logging/logger"; + +passport.use( + new LocalStrategy( + { usernameField: "email", passwordField: "password" }, + async (email, password, done) => { + try { + const user = await User.findOne({ email }); + if (!user) { + log({ + type: "error", + message: `Login failed: Incorrect email (${email})`, + }); + return done(null, false, { message: "Incorrect email" }); + } + + if (!user.password) { + log({ + type: "error", + message: `Login failed: Email/password login not enabled for ${email}`, + }); + return done(null, false, { + message: + "Email/password login is not enabled for your account. Please continue using OAuth to sign in.", + }); + } + + if (!user.isVerified) { + log({ + type: "warning", + message: `Login failed: Email not verified for ${email}`, + }); + return done(null, false, { + message: "Please verify your email before logging in.", + }); + } + + const valid = await bcrypt.compare(password, user.password || ""); + if (!valid) { + log({ + type: "error", + message: `Login failed: Incorrect password for ${email}`, + }); + return done(null, false, { message: "Incorrect password" }); + } + + if (!user.plan) { + const basePlan = await Plan.findOne({ isDefault: true }); + if (basePlan) { + user.plan = basePlan._id; + await user.save(); + } + } + + log({ type: "success", message: `User logged in: ${email}` }); + return done(null, user); + } catch (error) { + log({ type: "error", message: "LocalStrategy error", meta: error }); + return done(error); + } + } + ) +); diff --git a/api/src/auth/passport.ts b/api/src/auth/passport.ts new file mode 100644 index 0000000..0a24f25 --- /dev/null +++ b/api/src/auth/passport.ts @@ -0,0 +1,27 @@ +import passport from "passport"; +import User from "../models/user.model"; +import { setupGitHubStrategy } from "./passport/github.strategy"; +import { setupGoogleStrategy } from "./passport/google.strategy"; +import log from "../utils/logging/logger"; + +setupGitHubStrategy(); +setupGoogleStrategy(); + +passport.serializeUser((user: any, done) => { + try { + done(null, user.id); + } catch (err) { + log({ type: "error", message: "Error in serializeUser", meta: err }); + done(err); + } +}); + +passport.deserializeUser(async (id: string, done) => { + try { + const user = await User.findById(id); + done(null, user); + } catch (err) { + log({ type: "error", message: "Error in deserializeUser", meta: err }); + done(err); + } +}); diff --git a/api/src/auth/passport/github.strategy.ts b/api/src/auth/passport/github.strategy.ts new file mode 100644 index 0000000..32ee1bf --- /dev/null +++ b/api/src/auth/passport/github.strategy.ts @@ -0,0 +1,67 @@ +import passport from "passport"; +import { Strategy as GitHubStrategy } from "passport-github2"; +import User from "../../models/user.model"; +import Plan from "../../models/plan.model"; +import log from "../../utils/logging/logger"; +import env from "../../config/env"; + +export function setupGitHubStrategy() { + passport.use( + new GitHubStrategy( + { + clientID: env.GITHUB_CLIENT_ID!, + clientSecret: env.GITHUB_CLIENT_SECRET!, + callbackURL: "/api/auth/github/callback", + scope: ["user:email"], + }, + async ( + _accessToken: string, + _refreshToken: string, + profile: any, + done: (error: any, user?: any) => void + ) => { + try { + let user = await User.findOne({ "oauth.githubId": profile.id }); + if (user) { + log({ + type: "success", + message: `GitHub login: existing user ${user.email}`, + }); + return done(null, user); + } + + const email = profile.emails?.[0].value; + if (email) { + user = await User.findOne({ email }); + if (user) { + user.oauth = { ...user.oauth, githubId: profile.id }; + await user.save(); + log({ + type: "success", + message: `GitHub login: linked GitHub to existing user ${user.email}`, + }); + return done(null, user); + } + } + + const basePlan = await Plan.findOne({ isDefault: true }); + const newUser = await User.create({ + email, + name: profile.displayName, + profilePicture: profile.photos?.[0]?.value, + oauth: { githubId: profile.id }, + plan: basePlan ? basePlan._id : undefined, + }); + log({ + type: "success", + message: `GitHub login: new user created ${newUser.email}`, + }); + return done(null, newUser); + } catch (err) { + log({ type: "error", message: "GitHubStrategy error", meta: err }); + return done(err); + } + } + ) + ); +} diff --git a/api/src/auth/passport/google.strategy.ts b/api/src/auth/passport/google.strategy.ts new file mode 100644 index 0000000..2e855fe --- /dev/null +++ b/api/src/auth/passport/google.strategy.ts @@ -0,0 +1,62 @@ +import passport from "passport"; +import { Strategy as GoogleStrategy } from "passport-google-oauth20"; +import User from "../../models/user.model"; +import Plan from "../../models/plan.model"; +import log from "../../utils/logging/logger"; +import env from "../../config/env"; + +export function setupGoogleStrategy() { + passport.use( + new GoogleStrategy( + { + clientID: env.GOOGLE_CLIENT_ID!, + clientSecret: env.GOOGLE_CLIENT_SECRET!, + callbackURL: "/api/auth/google/callback", + }, + async (_accessToken, _refreshToken, profile, done) => { + try { + let user = await User.findOne({ "oauth.googleId": profile.id }); + if (user) { + log({ + type: "success", + message: `Google login: existing user ${user.email}`, + }); + return done(null, user); + } + + const email = profile.emails?.[0].value; + if (email) { + user = await User.findOne({ email }); + if (user) { + user.oauth = { ...user.oauth, googleId: profile.id }; + await user.save(); + log({ + type: "success", + message: `Google login: linked Google to existing user ${user.email}`, + }); + return done(null, user); + } + } + + const basePlan = await Plan.findOne({ isDefault: true }); + const newUser = await User.create({ + oauth: { googleId: profile.id }, + email: profile.emails?.[0].value, + name: profile.displayName, + profilePicture: profile.photos?.[0]?.value, + username: undefined, + plan: basePlan ? basePlan._id : undefined, + }); + log({ + type: "success", + message: `Google login: new user created ${newUser.email}`, + }); + done(null, newUser); + } catch (err) { + log({ type: "error", message: "GoogleStrategy error", meta: err }); + done(err); + } + } + ) + ); +} diff --git a/api/src/auth/role.middleware.ts b/api/src/auth/role.middleware.ts new file mode 100644 index 0000000..ba02039 --- /dev/null +++ b/api/src/auth/role.middleware.ts @@ -0,0 +1,35 @@ +import { Request, Response, NextFunction } from "express"; +import IUser from "../types/user.types"; +import log, { formatNotification } from "../utils/logging/logger"; + +// Middleware to check if user is admin or superadmin +export function ensureAdmin( + req: Request, + res: Response, + next: NextFunction +): void { + const user = req.user as IUser | undefined; + if (user && (user.role === "admin" || user.role === "superadmin")) { + next(); + } else { + log({ type: "error", message: "Admin access required" }); + res.status(403).json(formatNotification("Admin access required", "error")); + } +} + +// Middleware to check if user is superadmin +export function ensureSuperadmin( + req: Request, + res: Response, + next: NextFunction +): void { + const user = req.user as IUser | undefined; + if (user && user.role === "superadmin") { + next(); + } else { + log({ type: "error", message: "Superadmin access required" }); + res + .status(403) + .json(formatNotification("Superadmin access required", "error")); + } +} diff --git a/api/src/config/env.ts b/api/src/config/env.ts new file mode 100644 index 0000000..3a01137 --- /dev/null +++ b/api/src/config/env.ts @@ -0,0 +1,105 @@ +import path from "path"; +import dotenv from "dotenv"; +import log from "../utils/logging/logger"; + +dotenv.config({ path: path.resolve(__dirname, "../../../.env") }); + +const REQUIRED_ENV_VARS = [ + "MONGO_URI", + "SESSION_SECRET", + "NEXTAUTH_SECRET", + "GOOGLE_CLIENT_ID", + "GOOGLE_CLIENT_SECRET", + "GITHUB_CLIENT_ID", + "GITHUB_CLIENT_SECRET", + "GITHUB_APP_ID", + "GITHUB_APP_SLUG", + "GITHUB_PRIVATE_KEY", + "SMTP_HOST", + "SMTP_PORT", + "SMTP_USER", + "SMTP_PASS", + "SMTP_FROM", + "INGRESS_BASE_DOMAIN", + "MANIFESTS_DIR", + "GROQ", + "RAZORPAY_KEY_ID", + "RAZORPAY_KEY_SECRET", + "PROMETHEUS_URL", +]; + +const env = { + // Node environment (development, production, etc.) + NODE_ENV: process.env.NODE_ENV || "development", + + // MongoDB connection string + MONGO_URI: process.env.MONGO_URI, + + // Frontend URL (used for CORS, redirects, etc.) + FRONTEND_URL: process.env.FRONTEND_URL || "http://localhost:3000", + + // Session and authentication secrets + SESSION_SECRET: process.env.SESSION_SECRET, + NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, + + // OAuth credentials for Google + GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, + + // OAuth credentials for GitHub + GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID, + GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET, + + // GitHub App credentials + GITHUB_APP_ID: process.env.GITHUB_APP_ID, + GITHUB_APP_SLUG: process.env.GITHUB_APP_SLUG, + GITHUB_PRIVATE_KEY: (process.env.GITHUB_PRIVATE_KEY || "").replace( + /\\n/g, + "\n" + ), + + // SMTP configuration for sending emails + SMTP_HOST: process.env.SMTP_HOST, + SMTP_PORT: process.env.SMTP_PORT, + SMTP_USER: process.env.SMTP_USER, + SMTP_PASS: process.env.SMTP_PASS, + SMTP_FROM: process.env.SMTP_FROM, + CUSTOM_DOMAIN: process.env.CUSTOM_DOMAIN, + + // Ingress domains + INGRESS_BASE_DOMAIN: process.env.INGRESS_BASE_DOMAIN, + + // Directory for manifests + MANIFESTS_DIR: process.env.MANIFESTS_DIR, + + // GROQ API key or endpoint + GROQ: process.env.GROQ, + + // Razorpay + RAZORPAY_KEY_ID: process.env.RAZORPAY_KEY_ID, + RAZORPAY_KEY_SECRET: process.env.RAZORPAY_KEY_SECRET, + RAZORPAY_WEBHOOK_SECRET: process.env.RAZORPAY_WEBHOOK_SECRET, + + // Prometheus URL for metrics + PROMETHEUS_URL: process.env.PROMETHEUS_URL || "http://localhost:9090", + + // Persistent volume root directory + VOLUME_ROOT_PATH: process.env.VOLUME_ROOT_PATH || "/mnt/kargo-volumes", +}; + +const missingVars = REQUIRED_ENV_VARS.filter( + (key) => !env[key as keyof typeof env] +); +if (missingVars.length > 0) { + log({ + type: "error", + message: `Missing required environment variables: ${missingVars.join( + ", " + )}`, + }); + throw new Error( + `Missing required environment variables: ${missingVars.join(", ")}` + ); +} + +export default env; diff --git a/api/src/controllers/application/applyApplication.controller.ts b/api/src/controllers/application/applyApplication.controller.ts new file mode 100644 index 0000000..0b933b3 --- /dev/null +++ b/api/src/controllers/application/applyApplication.controller.ts @@ -0,0 +1,184 @@ +import { Request, Response } from "express"; +import fs from "fs"; +import path from "path"; +import { exec as execCb } from "child_process"; +import { promisify } from "util"; +import { createPvcIfNotExists } from "../../utils/k8sPvcManager"; +import Application from "../../models/application.model"; +import asyncHandler from "../../utils/handlers/asyncHandler"; +import generateK8sManifests from "../../utils/k8s/k8sManifests"; +import log, { formatNotification } from "../../utils/logging/logger"; +import type IApplication from "../../types/application.types"; +import type { Document } from "mongoose"; +import env from "../../config/env"; + +const exec = promisify(execCb); + +function writeManifestFiles( + appDir: string, + manifests: Record +) { + for (const [filename, content] of Object.entries(manifests)) { + if (content) { + fs.writeFileSync(path.join(appDir, filename), content); + } + } +} + +async function applyManifestSequence( + appDir: string, + appName: string, + manifestFiles: string[], + res: Response +) { + try { + // Apply namespace first + await exec(`kubectl apply -f namespace.yaml`, { cwd: appDir }); + + // Apply PVs first if present + if (fs.existsSync(path.join(appDir, "pvs.yaml"))) { + await exec(`kubectl apply -f pvs.yaml`, { cwd: appDir }); + } + + // Apply PVCs next if present + if (fs.existsSync(path.join(appDir, "pvcs.yaml"))) { + await exec(`kubectl apply -f pvcs.yaml`, { cwd: appDir }); + } + + await exec(`kubectl apply -f secret.yaml`, { cwd: appDir }); + if (fs.existsSync(path.join(appDir, "imagepullsecret.yaml"))) { + await exec(`kubectl apply -f imagepullsecret.yaml`, { cwd: appDir }); + } + const { stdout } = await exec( + `kubectl apply -f . --prune -l app=${appName} --field-manager=application-controller`, + { cwd: appDir } + ); + log({ type: "success", message: `Application applied: ${appName}` }); + res.json({ + ...formatNotification("Application applied", "success"), + output: stdout, + }); + } catch (err: any) { + log({ type: "error", message: "Failed to apply manifests", meta: err }); + // Read all manifest files for debugging + const manifests: Record = {}; + for (const file of manifestFiles) { + const filePath = path.join(appDir, file); + if (fs.existsSync(filePath)) { + manifests[file] = fs.readFileSync(filePath, "utf8"); + } + } + res.status(500).json({ + ...formatNotification("Failed to apply manifests", "error"), + error: err.stderr || err.message, + manifests, + }); + } +} + +const applyApplication = asyncHandler(async (req: Request, res: Response) => { + const app = (await Application.findById(req.params.id)) as + | (IApplication & Document) + | null; + if (!app) { + log({ type: "error", message: "Application not found" }); + return res + .status(404) + .json(formatNotification("Application not found", "error")); + } + const userId = (app.owner as any).toString(); + const appId = (app._id as any).toString(); + const manifestsDir = env.MANIFESTS_DIR; + if (!manifestsDir) { + log({ type: "error", message: "MANIFESTS_DIR not set in env" }); + return res + .status(500) + .json(formatNotification("MANIFESTS_DIR not set in env", "error")); + } + const appDir = path.join(manifestsDir, userId, appId); + + if (fs.existsSync(appDir)) { + fs.rmSync(appDir, { recursive: true, force: true }); + } + fs.mkdirSync(appDir, { recursive: true }); + + const manifestsResult = generateK8sManifests(app); + const { + deployment: deploymentYaml, + service: serviceYaml, + ingress: ingressYaml, + secret: secretYaml, + imagepullsecret: imagePullSecretYaml, + pvcs: pvcsYaml, + } = manifestsResult; + + if (!app.namespace) { + log({ type: "error", message: "Application namespace is undefined" }); + return res + .status(500) + .json(formatNotification("Application namespace is undefined", "error")); + } + + const manifests: Record = { + "namespace.yaml": `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${app.namespace}\n`, + "secret.yaml": secretYaml, + "deployment.yaml": deploymentYaml, + "service.yaml": serviceYaml, + "ingress.yaml": ingressYaml, + "pvcs.yaml": pvcsYaml, + }; + + // Only add imagepullsecret if it exists + if (imagePullSecretYaml) { + manifests["imagepullsecret.yaml"] = imagePullSecretYaml; + } + + writeManifestFiles(appDir, manifests); + + const manifestFiles = Object.keys(manifests).filter((f) => manifests[f]); + for (const file of manifestFiles) { + const filePath = path.join(appDir, file); + if (fs.existsSync(filePath)) { + const content = fs.readFileSync(filePath, "utf8"); + log({ + type: "info", + message: `[applyApplication] ${file} content:\n${content}`, + }); + } + } + + // Apply namespace first + await exec(`kubectl apply -f namespace.yaml`, { cwd: appDir }); + + // Apply PVs first if present + if (manifestsResult.pvs) { + fs.writeFileSync(path.join(appDir, "pvs.yaml"), manifestsResult.pvs); + await exec(`kubectl apply -f pvs.yaml`, { cwd: appDir }); + } + + + // Apply PVCs next if present, but only if not already present in the cluster + if (pvcsYaml) { + // Try to parse the YAML and extract PVC names + const pvcNames: string[] = []; + const yamlDocs = pvcsYaml.split(/^---$/m); + for (const doc of yamlDocs) { + const match = doc.match(/name:\s*([\w-]+)/); + if (match) { + pvcNames.push(match[1]); + } + } + for (const pvcName of pvcNames) { + try { + await createPvcIfNotExists(app.namespace!, pvcName, path.join(appDir, "pvcs.yaml")); + } catch (e) { + log({ type: "warning", message: `PVC ${pvcName} could not be created: ${e}` }); + } + } + } + + // Now apply the rest (deployment, service, ingress, secrets, etc.) + await applyManifestSequence(appDir, app.name, manifestFiles, res); +}); + +export default applyApplication; diff --git a/api/src/controllers/application/createApplication.controller.ts b/api/src/controllers/application/createApplication.controller.ts new file mode 100644 index 0000000..c18ebbf --- /dev/null +++ b/api/src/controllers/application/createApplication.controller.ts @@ -0,0 +1,79 @@ +import { Request, Response } from "express"; +import Application from "../../models/application.model"; +import asyncHandler from "../../utils/handlers/asyncHandler"; +import { + getNamespace, + getResourceName, +} from "../../utils/k8s/helpers/k8sHelpers"; +import { mapPorts } from "../../utils/k8s/helpers/portHelpers"; +import log, { formatNotification } from "../../utils/logging/logger"; + +const createApplication = asyncHandler(async (req: Request, res: Response) => { + const { + name, + imageUrl, + imageTag, + env: envVars, + resources, + ports = [], + volumes, + livenessProbe, + readinessProbe, + command, + args, + labels, + annotations, + tolerations, + affinity, + } = req.body; + const credentials = req.body.credentials || []; + const owner = (req.user as any)?._id || req.body.owner; + const namespace = getNamespace(owner.toString(), name); + const deploymentName = getResourceName("deploy", name); + const serviceName = getResourceName("svc", name); + + const username = (req.user as any)?.username || req.body.username || "user"; + const updatedPorts = mapPorts(ports, username); + + try { + const app = await Application.create({ + name, + imageUrl, + imageTag, + namespace, + deploymentName, + serviceName, + env: envVars, + resources, + ports: updatedPorts, + volumes, + livenessProbe, + readinessProbe, + command, + args, + labels, + annotations, + nodeSelector: req.body.nodeSelector, + tolerations, + affinity, + owner, + credentials, + }); + log({ type: "success", message: `Application created: ${name}` }); + res.status(201).json({ + ...formatNotification("Application created", "success"), + application: app, + }); + } catch (err) { + log({ + type: "error", + message: "Failed to create application", + meta: err, + }); + res + .status(500) + .json(formatNotification("Failed to create application", "error")); + } +}); + +export default createApplication; diff --git a/api/src/controllers/application/deleteApplication.controller.ts b/api/src/controllers/application/deleteApplication.controller.ts new file mode 100644 index 0000000..496c0f0 --- /dev/null +++ b/api/src/controllers/application/deleteApplication.controller.ts @@ -0,0 +1,186 @@ +import { Request, Response } from "express"; +import Application from "../../models/application.model"; +import asyncHandler from "../../utils/handlers/asyncHandler"; +import log, { formatNotification } from "../../utils/logging/logger"; +import { spawn } from "child_process"; +import fs from "fs"; +import path from "path"; +import env from "../../config/env"; + +// Helper to stream progress +function streamStep( + res: Response, + message: string, + status: string = "progress" +) { + res.write(`data: ${JSON.stringify({ message, status })}\n\n`); +} + +const deleteApplication = asyncHandler(async (req: Request, res: Response) => { + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + + const app = await Application.findById(req.params.id); + if (!app) { + streamStep(res, "Application not found", "error"); + res.end(); + return; + } + const namespace = app.namespace || "default"; + const name = app.name; + const deployment = app.deploymentName || name; + const service = app.serviceName || name; + const userId = (app.owner as any)?.toString?.() || app.owner; + const appId = (app._id as any)?.toString?.() || app._id; + const manifestsDir = env.MANIFESTS_DIR; + const appDir = manifestsDir ? path.join(manifestsDir, userId, appId) : null; + + // Helper to run a kubectl command and stream output + function runKubectl(args: string[], stepMsg: string): Promise { + return new Promise((resolve, reject) => { + streamStep(res, stepMsg); + const proc = spawn("kubectl", args); + proc.stdout.on("data", (data) => { + streamStep(res, data.toString().trim()); + }); + proc.stderr.on("data", (data) => { + streamStep(res, data.toString().trim(), "warning"); + }); + proc.on("close", (code) => { + if (code === 0) resolve(); + else reject(new Error(`${stepMsg} failed with code ${code}`)); + }); + }); + } + + let errorOccurred = false; + let errorMsg = ""; + + try { + if (namespace !== "default") { + // Delete namespace (removes all resources inside) + try { + await runKubectl( + ["delete", "namespace", namespace], + `Deleting namespace: ${namespace}` + ); + } catch (err: any) { + errorOccurred = true; + errorMsg = err.message || `Failed to delete namespace: ${namespace}`; + streamStep(res, errorMsg, "error"); + } + } else { + // Delete individual resources in default namespace + // Delete deployment + try { + await runKubectl( + ["delete", "deployment", `${deployment}-deployment`, "-n", namespace], + `Deleting deployment: ${deployment}-deployment` + ); + } catch (err: any) { + streamStep( + res, + err.message || `Failed to delete deployment`, + "warning" + ); + } + // Delete service + try { + await runKubectl( + ["delete", "service", `${service}-service`, "-n", namespace], + `Deleting service: ${service}-service` + ); + } catch (err: any) { + streamStep(res, err.message || `Failed to delete service`, "warning"); + } + // Delete ingress + try { + await runKubectl( + ["delete", "ingress", `${name}-ingress`, "-n", namespace], + `Deleting ingress: ${name}-ingress` + ); + } catch (err: any) { + streamStep(res, err.message || `Failed to delete ingress`, "warning"); + } + // Delete secret + try { + await runKubectl( + ["delete", "secret", `${name}-env-secret`, "-n", namespace], + `Deleting secret: ${name}-env-secret` + ); + } catch (err: any) { + streamStep(res, err.message || `Failed to delete secret`, "warning"); + } + // Delete image pull secret + try { + await runKubectl( + ["delete", "secret", `${name}-regcred`, "-n", namespace], + `Deleting image pull secret: ${name}-regcred` + ); + } catch (err: any) { + streamStep( + res, + err.message || `Failed to delete image pull secret`, + "warning" + ); + } + // Delete PVCs and PVs (auto-generated volume) + const autoVolumeName = `${name}-data`; + try { + await runKubectl( + ["delete", "pvc", `${autoVolumeName}-pvc`, "-n", namespace], + `Deleting PVC: ${autoVolumeName}-pvc` + ); + } catch (err: any) { + streamStep(res, err.message || `Failed to delete PVC`, "warning"); + } + try { + await runKubectl( + ["delete", "pv", `${autoVolumeName}-pv`], + `Deleting PV: ${autoVolumeName}-pv` + ); + } catch (err: any) { + streamStep(res, err.message || `Failed to delete PV`, "warning"); + } + } + } catch (err: any) { + errorOccurred = true; + errorMsg = err.message || "Failed to delete application resources"; + streamStep(res, errorMsg, "error"); + } + + // Always attempt to remove manifests and DB record + try { + if (appDir && fs.existsSync(appDir)) { + const { rimraf } = await import("rimraf"); + await rimraf(appDir); + streamStep(res, `Deleted manifests directory: ${appDir}`); + } + } catch (err: any) { + streamStep( + res, + err.message || `Failed to delete manifests directory: ${appDir}`, + "warning" + ); + } + + try { + await Application.findByIdAndDelete(app._id); + streamStep(res, "Application removed from database", "success"); + } catch (err: any) { + streamStep( + res, + err.message || "Failed to remove application from database", + "error" + ); + errorOccurred = true; + } + + if (!errorOccurred) { + streamStep(res, "Application and all resources deleted", "success"); + } + res.end(); +}); + +export default deleteApplication; diff --git a/api/src/controllers/application/getApplication.controller.ts b/api/src/controllers/application/getApplication.controller.ts new file mode 100644 index 0000000..fee3434 --- /dev/null +++ b/api/src/controllers/application/getApplication.controller.ts @@ -0,0 +1,25 @@ +import { Request, Response } from "express"; +import Application from "../../models/application.model"; +import asyncHandler from "../../utils/handlers/asyncHandler"; +import log, { formatNotification } from "../../utils/logging/logger"; +import type IApplication from "../../types/application.types"; +import type { Document } from "mongoose"; + +const getApplication = asyncHandler(async (req: Request, res: Response) => { + const app = (await Application.findById(req.params.id)) as + | (IApplication & Document) + | null; + if (!app) { + log({ type: "error", message: "Application not found" }); + return res + .status(404) + .json(formatNotification("Application not found", "error")); + } + log({ type: "info", message: `Fetched application: ${app.name}` }); + res.json({ + ...formatNotification("Fetched application", "info"), + application: app, + }); +}); + +export default getApplication; diff --git a/api/src/controllers/application/getApplicationMetrics.controller.ts b/api/src/controllers/application/getApplicationMetrics.controller.ts new file mode 100644 index 0000000..91b9e36 --- /dev/null +++ b/api/src/controllers/application/getApplicationMetrics.controller.ts @@ -0,0 +1,84 @@ +import { Request, Response } from "express"; +import axios from "axios"; +import Application from "../../models/application.model"; +import asyncHandler from "../../utils/handlers/asyncHandler"; +import log, { formatNotification } from "../../utils/logging/logger"; +import env from "../../config/env"; + +const prometheusBaseUrl = (env as any).PROMETHEUS_URL; + +const getApplicationMetrics = asyncHandler( + async (req: Request, res: Response) => { + const app = await Application.findById(req.params.id); + if (!app) { + log({ type: "error", message: "Application not found" }); + return res + .status(404) + .json(formatNotification("Application not found", "error")); + } + const namespace = app.namespace || "default"; + const deploymentName = app.deploymentName || app.name; + // Prometheus pod name pattern: deploymentName-xxxx + const podRegex = `${deploymentName}-.*`; + const queries = { + cpu: `sum(rate(container_cpu_usage_seconds_total{namespace="${namespace}", pod=~"${podRegex}"}[5m]))`, + memory: `sum(container_memory_usage_bytes{namespace="${namespace}", pod=~"${podRegex}"})`, + storage: `sum(container_fs_usage_bytes{namespace="${namespace}", pod=~"${podRegex}"})`, + }; + const end = Math.floor(Date.now() / 1000); + const start = end - 3600; // last 1 hour + const step = 60; // 1-minute intervals + const results: Record = {}; + for (const [key, query] of Object.entries(queries)) { + const instantUrl = `${prometheusBaseUrl}/api/v1/query?query=${encodeURIComponent( + query + )}`; + const rangeUrl = `${prometheusBaseUrl}/api/v1/query_range?query=${encodeURIComponent( + query + )}&start=${start}&end=${end}&step=${step}`; + try { + const [instantRes, rangeRes] = await Promise.all([ + axios.get(instantUrl), + axios.get(rangeUrl), + ]); + results[key] = { + current: instantRes.data.data.result?.[0]?.value?.[1] + ? Number( + Number(instantRes.data.data.result[0].value[1]).toFixed(3) + ).toLocaleString(undefined, { maximumFractionDigits: 3 }) + : null, + history: rangeRes.data.data.result?.[0]?.values + ? rangeRes.data.data.result[0].values.map( + ([ts, val]: [number, string]) => [ + ts, + Number(Number(val).toFixed(3)).toLocaleString(undefined, { + maximumFractionDigits: 3, + }), + ] + ) + : [], + }; + } catch { + results[key] = { current: null, history: [] }; + } + } + // Return metrics and resource requests/limits + res.json({ + metrics: results, + resources: { + requests: { + cpuMilli: app.resources?.requests?.cpuMilli || null, + memoryMB: app.resources?.requests?.memoryMB || null, + storageGB: app.resources?.requests?.storageGB || null, + }, + limits: { + cpuMilli: app.resources?.limits?.cpuMilli || null, + memoryMB: app.resources?.limits?.memoryMB || null, + storageGB: app.resources?.limits?.storageGB || null, + }, + }, + }); + } +); + +export default getApplicationMetrics; diff --git a/api/src/controllers/application/getApplications.controller.ts b/api/src/controllers/application/getApplications.controller.ts new file mode 100644 index 0000000..f724af6 --- /dev/null +++ b/api/src/controllers/application/getApplications.controller.ts @@ -0,0 +1,30 @@ +import { Request, Response } from "express"; +import Application from "../../models/application.model"; +import asyncHandler from "../../utils/handlers/asyncHandler"; +import log, { formatNotification } from "../../utils/logging/logger"; + +const getApplications = asyncHandler(async (req: Request, res: Response) => { + try { + const owner = (req.user as any)?._id || req.query.owner; + const apps = await Application.find({ owner }); + log({ + type: "info", + message: `Fetched applications for owner: ${owner}`, + }); + res.json({ + ...formatNotification("Fetched applications", "info"), + applications: apps, + }); + } catch (err) { + log({ + type: "error", + message: "Failed to fetch applications", + meta: err, + }); + res + .status(500) + .json(formatNotification("Failed to fetch applications", "error")); + } +}); + +export default getApplications; diff --git a/api/src/controllers/application/getApplications.ts b/api/src/controllers/application/getApplications.ts new file mode 100644 index 0000000..f724af6 --- /dev/null +++ b/api/src/controllers/application/getApplications.ts @@ -0,0 +1,30 @@ +import { Request, Response } from "express"; +import Application from "../../models/application.model"; +import asyncHandler from "../../utils/handlers/asyncHandler"; +import log, { formatNotification } from "../../utils/logging/logger"; + +const getApplications = asyncHandler(async (req: Request, res: Response) => { + try { + const owner = (req.user as any)?._id || req.query.owner; + const apps = await Application.find({ owner }); + log({ + type: "info", + message: `Fetched applications for owner: ${owner}`, + }); + res.json({ + ...formatNotification("Fetched applications", "info"), + applications: apps, + }); + } catch (err) { + log({ + type: "error", + message: "Failed to fetch applications", + meta: err, + }); + res + .status(500) + .json(formatNotification("Failed to fetch applications", "error")); + } +}); + +export default getApplications; diff --git a/api/src/controllers/application/getApplicationsStatus.controller.ts b/api/src/controllers/application/getApplicationsStatus.controller.ts new file mode 100644 index 0000000..ea1c0d2 --- /dev/null +++ b/api/src/controllers/application/getApplicationsStatus.controller.ts @@ -0,0 +1,47 @@ +import { Request, Response } from "express"; +import Application from "../../models/application.model"; +import asyncHandler from "../../utils/handlers/asyncHandler"; +import { exec } from "child_process"; + +function getK8sStatus(namespace: string, deployment: string): Promise { + return new Promise((resolve) => { + exec( + `kubectl get deployment ${deployment}-deployment -n ${namespace} -o json`, + (err, stdout) => { + if (err) return resolve("offline"); + try { + const obj = JSON.parse(stdout); + const available = obj.status?.availableReplicas ?? 0; + const desired = obj.status?.replicas ?? 0; + if (desired === 0) return resolve("stopped"); + if (available === desired) return resolve("online"); + if (available === 0 && desired > 0) return resolve("starting"); + if (available < desired) return resolve("partially online"); + return resolve("unknown"); + } catch { + return resolve("unknown"); + } + } + ); + }); +} + +const getApplicationsStatus = asyncHandler(async (req: Request, res: Response) => { + const owner = (req.user as any)?._id || req.query.owner; + const apps = await Application.find({ owner }); + const statusResults = await Promise.all( + apps.map(async (app: any) => { + const namespace = app.namespace || "default"; + const deployment = app.deploymentName || app.name; + const status = await getK8sStatus(namespace, deployment); + return { + id: app._id, + name: app.name, + status, + }; + }) + ); + res.json({ status: statusResults }); +}); + +export default getApplicationsStatus; diff --git a/api/src/controllers/application/removeDeployment.controller.ts b/api/src/controllers/application/removeDeployment.controller.ts new file mode 100644 index 0000000..43a70bf --- /dev/null +++ b/api/src/controllers/application/removeDeployment.controller.ts @@ -0,0 +1,50 @@ +import { Request, Response } from "express"; +import path from "path"; +import { exec } from "child_process"; +import Application from "../../models/application.model"; +import asyncHandler from "../../utils/handlers/asyncHandler"; +import log, { formatNotification } from "../../utils/logging/logger"; +import env from "../../config/env"; + +const removeDeployment = asyncHandler(async (req: Request, res: Response) => { + const app = await Application.findById(req.params.id); + if (!app) { + log({ type: "error", message: "Application not found" }); + return res + .status(404) + .json(formatNotification("Application not found", "error")); + } + const userId = (app.owner as any).toString(); + const appId = (app._id as any).toString(); + const manifestsDir = env.MANIFESTS_DIR; + if (!manifestsDir) { + log({ type: "error", message: "MANIFESTS_DIR not set in env" }); + return res + .status(500) + .json(formatNotification("MANIFESTS_DIR not set in env", "error")); + } + const appDir = path.join(manifestsDir, userId, appId); + exec(`kubectl delete -f .`, { cwd: appDir }, (err, stdout, stderr) => { + if (err) { + log({ + type: "error", + message: "Failed to remove deployment", + meta: err, + }); + return res.status(500).json({ + ...formatNotification("Failed to remove deployment", "error"), + error: stderr, + }); + } + log({ + type: "success", + message: `Deployment removed for app: ${app.name}`, + }); + res.json({ + ...formatNotification("Deployment removed", "success"), + output: stdout, + }); + }); +}); + +export default removeDeployment; diff --git a/api/src/controllers/application/removeNamespace.controller.ts b/api/src/controllers/application/removeNamespace.controller.ts new file mode 100644 index 0000000..9b775bb --- /dev/null +++ b/api/src/controllers/application/removeNamespace.controller.ts @@ -0,0 +1,36 @@ +import { Request, Response } from "express"; +import { exec } from "child_process"; +import Application from "../../models/application.model"; +import asyncHandler from "../../utils/handlers/asyncHandler"; +import log, { formatNotification } from "../../utils/logging/logger"; + +const removeNamespace = asyncHandler(async (req: Request, res: Response) => { + const app = await Application.findById(req.params.id); + if (!app) { + log({ type: "error", message: "Application not found" }); + return res + .status(404) + .json(formatNotification("Application not found", "error")); + } + const namespace = app.namespace; + exec(`kubectl delete namespace ${namespace}`, (err, stdout, stderr) => { + if (err) { + log({ + type: "error", + message: "Failed to remove namespace", + meta: err, + }); + return res.status(500).json({ + ...formatNotification("Failed to remove namespace", "error"), + error: stderr, + }); + } + log({ type: "success", message: `Namespace removed: ${namespace}` }); + res.json({ + ...formatNotification("Namespace removed", "success"), + output: stdout, + }); + }); +}); + +export default removeNamespace; diff --git a/api/src/controllers/application/rolloutRestartDeployment.controller.ts b/api/src/controllers/application/rolloutRestartDeployment.controller.ts new file mode 100644 index 0000000..d538b6c --- /dev/null +++ b/api/src/controllers/application/rolloutRestartDeployment.controller.ts @@ -0,0 +1,45 @@ +import { Request, Response } from "express"; +import Application from "../../models/application.model"; +import asyncHandler from "../../utils/handlers/asyncHandler"; +import log, { formatNotification } from "../../utils/logging/logger"; +import { exec } from "child_process"; + +const rolloutRestartDeployment = asyncHandler( + async (req: Request, res: Response) => { + const app = await Application.findById(req.params.id); + if (!app) { + log({ type: "error", message: "Application not found" }); + return res + .status(404) + .json(formatNotification("Application not found", "error")); + } + const namespace = app.namespace || "default"; + const deploymentName = app.deploymentName || app.name; + exec( + `kubectl rollout restart deployment/${deploymentName}-deployment -n ${namespace}`, + (err, stdout, stderr) => { + if (err) { + log({ + type: "error", + message: "Failed to rollout restart deployment", + meta: err, + }); + return res.status(500).json({ + ...formatNotification("Failed to restart deployment", "error"), + error: stderr, + }); + } + log({ + type: "success", + message: `Deployment rollout restarted for app: ${app.name}`, + }); + res.json({ + ...formatNotification("Deployment rollout restarted", "success"), + output: stdout, + }); + } + ); + } +); + +export default rolloutRestartDeployment; diff --git a/api/src/controllers/application/runDockerHandler.controller.ts b/api/src/controllers/application/runDockerHandler.controller.ts new file mode 100644 index 0000000..c847793 --- /dev/null +++ b/api/src/controllers/application/runDockerHandler.controller.ts @@ -0,0 +1,37 @@ +import { Request, Response } from "express"; +import runDockerScript from "../../utils/k8s/docker-file"; +import log, { formatNotification } from "../../utils/logging/logger"; + +const runDockerHandler = async (req: Request, res: Response) => { + const { url } = req.body; + if (!url || typeof url !== "string") { + log({ type: "error", message: "Missing or invalid 'url' in body." }); + return res + .status(400) + .json(formatNotification("Missing or invalid 'url' in body.", "error")); + } + try { + const result = await runDockerScript(url); + if (result.error) { + log({ type: "error", message: `Docker script error: ${result.error}` }); + return res.status(500).json(formatNotification(result.error, "error")); + } + log({ + type: "success", + message: "Dockerfile and Compose generated successfully", + }); + res.status(200).json({ + ...formatNotification( + "Dockerfile and Compose generated successfully", + "success" + ), + dockerfile: result.dockerfile, + dockerCompose: result.dockerCompose, + }); + } catch (error: any) { + log({ type: "error", message: "Python script failed.", meta: error }); + res.status(500).json(formatNotification("Python script failed.", "error")); + } +}; + +export default runDockerHandler; diff --git a/api/src/controllers/application/scaleDeploymentZero.controller.ts b/api/src/controllers/application/scaleDeploymentZero.controller.ts new file mode 100644 index 0000000..c8a2877 --- /dev/null +++ b/api/src/controllers/application/scaleDeploymentZero.controller.ts @@ -0,0 +1,48 @@ +import { Request, Response } from "express"; +import Application from "../../models/application.model"; +import asyncHandler from "../../utils/handlers/asyncHandler"; +import log, { formatNotification } from "../../utils/logging/logger"; +import { exec } from "child_process"; + +const scaleDeploymentZero = asyncHandler( + async (req: Request, res: Response) => { + const app = await Application.findById(req.params.id); + if (!app) { + log({ type: "error", message: "Application not found" }); + return res + .status(404) + .json(formatNotification("Application not found", "error")); + } + const namespace = app.namespace || "default"; + const deploymentName = app.deploymentName || app.name; + exec( + `kubectl scale deployment/${deploymentName}-deployment --replicas=0 -n ${namespace}`, + (err, stdout, stderr) => { + if (err) { + log({ + type: "error", + message: "Failed to scale deployment to 0", + meta: err, + }); + return res.status(500).json({ + ...formatNotification( + "Failed to remove deployment (scale to 0)", + "error" + ), + error: stderr, + }); + } + log({ + type: "success", + message: `Deployment scaled to 0 for app: ${app.name}`, + }); + res.json({ + ...formatNotification("Deployment scaled to 0", "success"), + output: stdout, + }); + } + ); + } +); + +export default scaleDeploymentZero; diff --git a/api/src/controllers/application/streamApplicationLogs.controller.ts b/api/src/controllers/application/streamApplicationLogs.controller.ts new file mode 100644 index 0000000..2bc5631 --- /dev/null +++ b/api/src/controllers/application/streamApplicationLogs.controller.ts @@ -0,0 +1,136 @@ +import { Request, Response } from "express"; +import { spawn } from "child_process"; +import Application from "../../models/application.model"; +import asyncHandler from "../../utils/handlers/asyncHandler"; +import log, { formatNotification } from "../../utils/logging/logger"; + +const LOG_TAIL_COUNT = 200; +const LOG_THROTTLE_MS = 100; + +const streamApplicationLogs = asyncHandler( + async (req: Request, res: Response) => { + const app = await Application.findById(req.params.id); + if (!app) { + log({ type: "error", message: "Application not found" }); + return res + .status(404) + .json(formatNotification("Application not found", "error")); + } + const namespace = app.namespace; + log({ + type: "info", + message: `[streamApplicationLogs] Using namespace: ${namespace}, app: ${app.name}`, + }); + // Get all pod names for the app + const getPods = spawn( + "kubectl", + [ + "get", + "pods", + "-n", + namespace ?? "default", + "-l", + `app=${app.name}`, + "-o", + "jsonpath={.items[*].metadata.name}", + ], + { stdio: ["ignore", "pipe", "pipe"] } + ); + let podsOutput = ""; + (getPods.stdout as NodeJS.ReadableStream).on("data", (data: Buffer) => { + podsOutput += data.toString(); + }); + (getPods.stderr as NodeJS.ReadableStream).on("data", (data: Buffer) => { + log({ + type: "error", + message: `[streamApplicationLogs] getPods stderr: ${data.toString()}`, + }); + }); + getPods.on("close", (_code: number) => { + const podNames = podsOutput.trim().split(/\s+/).filter(Boolean); + log({ + type: podNames.length ? "info" : "error", + message: `[streamApplicationLogs] Pod names found: ${podNames.join( + ", " + )}`, + }); + if (!podNames.length) { + log({ type: "error", message: "No pods found" }); + res.status(404).json(formatNotification("No pods found", "error")); + return; + } + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + res.flushHeaders(); + // For each pod, stream logs + const logProcesses = podNames.map((podName) => { + return { + podName, + proc: spawn( + "kubectl", + [ + "logs", + podName, + "-n", + namespace ?? "default", + `--tail=${LOG_TAIL_COUNT}`, + "-f", + ], + { stdio: ["ignore", "pipe", "pipe"] } + ), + buffer: "", + sending: false, + }; + }); + // Helper to send logs for a pod + const sendBufferedLogs = (logObj: any) => { + if (logObj.sending || !logObj.buffer) return; + logObj.sending = true; + // Prefix each line with timestamp and pod name + const lines = logObj.buffer.split("\n").filter(Boolean); + for (const line of lines) { + const timestamp = new Date().toISOString(); + res.write(`data: ${timestamp} [${logObj.podName}] ${line}\n\n`); + } + logObj.buffer = ""; + setTimeout(() => { + logObj.sending = false; + if (logObj.buffer) sendBufferedLogs(logObj); + }, LOG_THROTTLE_MS); + }; + // Attach listeners for each pod log process + logProcesses.forEach((logObj) => { + (logObj.proc.stdout as NodeJS.ReadableStream).on( + "data", + (data: Buffer) => { + logObj.buffer += data.toString(); + sendBufferedLogs(logObj); + } + ); + (logObj.proc.stderr as NodeJS.ReadableStream).on( + "data", + (data: Buffer) => { + log({ + type: "error", + message: `[streamApplicationLogs] logs stderr: ${data.toString()}`, + }); + logObj.buffer += `[stderr] ${data.toString()}`; + sendBufferedLogs(logObj); + } + ); + logObj.proc.on("close", () => { + const timestamp = new Date().toISOString(); + res.write( + `event: end\ndata: ${timestamp} [${logObj.podName}] [log stream ended]\n\n` + ); + }); + }); + req.on("close", () => { + logProcesses.forEach((logObj) => logObj.proc.kill()); + }); + }); + } +); + +export default streamApplicationLogs; diff --git a/api/src/controllers/application/testImageAvailability.controller.ts b/api/src/controllers/application/testImageAvailability.controller.ts new file mode 100644 index 0000000..0e1b070 --- /dev/null +++ b/api/src/controllers/application/testImageAvailability.controller.ts @@ -0,0 +1,102 @@ +import { Request, Response } from "express"; +import testImageAvailability from "../../utils/docker/testImageAvailability"; +import log, { formatNotification } from "../../utils/logging/logger"; +import User from "../../models/user.model"; +import type IUser from "../../types/user.types"; +import type { Document } from "mongoose"; +import asyncHandler from "../../utils/handlers/asyncHandler"; + +const testImageAvailabilityController = asyncHandler(async (req: Request, res: Response) => { + const { imageUrl, imageTag, credentialIds } = req.body; + + if (!imageUrl || typeof imageUrl !== "string") { + log({ type: "error", message: "Missing or invalid 'imageUrl' in body." }); + return res + .status(400) + .json(formatNotification("Missing or invalid 'imageUrl' in body.", "error")); + } + + const tag = imageTag || "latest"; + + try { + // Get user's registry credentials + const userId = (req.user as any)?._id; + let credentials: any[] = []; + + if (userId) { + const user = (await User.findById(userId)) as (IUser & Document) | null; + if (user && user.credentials) { + // If specific credential IDs are provided, filter to only those + if (credentialIds && Array.isArray(credentialIds) && credentialIds.length > 0) { + credentials = user.credentials.filter(cred => + credentialIds.includes(cred.name + ":" + cred.registryType) + ); + } else { + credentials = user.credentials; + } + } + } + + log({ + type: "info", + message: `Testing image availability: ${imageUrl}:${tag} (${credentials.length} credentials available${credentialIds ? `, filtered: ${credentialIds.join(', ')}` : ''})`, + }); + + const result = await testImageAvailability(imageUrl, tag, credentials); + + if (result.available) { + log({ + type: "success", + message: `Image ${imageUrl}:${tag} is available`, + }); + + res.json({ + ...formatNotification("Image is available", "success"), + available: true, + authTested: result.authTested, + testedWith: result.testedWith, + suggestions: result.suggestions, + isArchitectureIssue: result.isArchitectureIssue, + architectureSupported: result.architectureSupported, + supportedArchitectures: result.supportedArchitectures, + clusterArchitectures: result.clusterArchitectures, + unsupportedNodes: result.unsupportedNodes, + recommendedNodeSelector: result.recommendedNodeSelector, + }); + } else { + log({ + type: "warning", + message: `Image ${imageUrl}:${tag} is not available: ${result.error}`, + }); + + res.status(404).json({ + ...formatNotification( + result.needsAuth + ? "Image not accessible. Check if the image exists and you have proper credentials configured." + : "Image not found or not accessible.", + "error" + ), + available: false, + needsAuth: result.needsAuth, + authTested: result.authTested, + error: result.error, + isArchitectureIssue: result.isArchitectureIssue, + suggestions: result.suggestions, + }); + } + } catch (error: any) { + log({ + type: "error", + message: "Error testing image availability", + meta: error, + }); + + res.status(500).json({ + ...formatNotification("Failed to test image availability", "error"), + available: false, + error: error.message || "Unknown error occurred", + }); + } +}); + +export default testImageAvailabilityController; diff --git a/api/src/controllers/application/updateApplication.controller.ts b/api/src/controllers/application/updateApplication.controller.ts new file mode 100644 index 0000000..405141e --- /dev/null +++ b/api/src/controllers/application/updateApplication.controller.ts @@ -0,0 +1,109 @@ +import { Request, Response } from "express"; +import Application from "../../models/application.model"; +import asyncHandler from "../../utils/handlers/asyncHandler"; +import { + getNamespace, + getResourceName, +} from "../../utils/k8s/helpers/k8sHelpers"; +import { mapPorts } from "../../utils/k8s/helpers/portHelpers"; +import { checkResourceQuota } from "../../utils/k8s/helpers/resourceQuota"; +import log, { formatNotification } from "../../utils/logging/logger"; + +const updateApplication = asyncHandler(async (req: Request, res: Response) => { + const { + name, + env: envVars, + resources, + ports = [], + volumes, + livenessProbe, + readinessProbe, + command, + args, + labels, + annotations, + tolerations, + affinity, + } = req.body; + let credentials = req.body.credentials; + if (credentials === undefined) { + const existingApp = await Application.findById(req.params.id); + credentials = existingApp?.credentials || []; + } + const owner = (req.user as any)?._id || req.body.owner; + const namespace = getNamespace(owner.toString(), name); + const deploymentName = getResourceName("deploy", name); + const serviceName = getResourceName("svc", name); + + const username = (req.user as any)?.username || req.body.username || "user"; + const updatedPorts = mapPorts(ports, username); + + try { + if (resources) { + const quota = await checkResourceQuota({ resources, owner, req }); + if (quota.exceeded) { + log({ + type: "warning", + message: "Resource allocation exceeds allowed quota.", + }); + return res.status(400).json({ + ...formatNotification( + "Resource allocation exceeds your allowed quota.", + "warning" + ), + allowed: quota.allowed, + usage: quota.usage, + }); + } + } + const app = await Application.findByIdAndUpdate( + req.params.id, + { + name, + imageUrl: req.body.imageUrl, + imageTag: req.body.imageTag, + namespace, + deploymentName, + serviceName, + env: envVars, + resources, + ports: updatedPorts, + volumes, + livenessProbe, + readinessProbe, + command, + args, + labels, + annotations, + nodeSelector: req.body.nodeSelector, + tolerations, + affinity, + owner, + credentials, // always update credentials + }, + { new: true } + ); + if (!app) { + log({ type: "error", message: "Application not found" }); + return res + .status(404) + .json(formatNotification("Application not found", "error")); + } + log({ type: "success", message: `Application updated: ${app.name}` }); + res.json({ + ...formatNotification("Application updated", "success"), + application: app, + }); + } catch (err) { + log({ + type: "error", + message: "Failed to update application", + meta: err, + }); + res + .status(500) + .json(formatNotification("Failed to update application", "error")); + } +}); + +export default updateApplication; diff --git a/api/src/controllers/auth/getMe.controller.ts b/api/src/controllers/auth/getMe.controller.ts new file mode 100644 index 0000000..8772ac0 --- /dev/null +++ b/api/src/controllers/auth/getMe.controller.ts @@ -0,0 +1,22 @@ +import { Request, Response } from "express"; +import User from "../../models/user.model"; +import log, { formatNotification } from "../../utils/logging/logger"; +import sanitizeUser from "../../utils/auth/sanitizeUser"; + +const getMe = async (req: Request, res: Response) => { + if (req.isAuthenticated && req.isAuthenticated()) { + const user = await User.findById((req.user as any)._id).populate("plan"); + if (!user) { + log({ type: "error", message: "User not found" }); + return res + .status(404) + .json(formatNotification("User not found", "error")); + } + res.json({ user: sanitizeUser(user) }); + } else { + log({ type: "error", message: "Not authenticated" }); + res.status(401).json(formatNotification("Not authenticated", "error")); + } +}; + +export default getMe; diff --git a/api/src/controllers/auth/githubAuth.controller.ts b/api/src/controllers/auth/githubAuth.controller.ts new file mode 100644 index 0000000..e69776b --- /dev/null +++ b/api/src/controllers/auth/githubAuth.controller.ts @@ -0,0 +1,7 @@ +import passport from "passport"; + +const githubAuth = passport.authenticate("github", { + scope: ["user:email"], +}); + +export default githubAuth; diff --git a/api/src/controllers/auth/githubCallback.controller.ts b/api/src/controllers/auth/githubCallback.controller.ts new file mode 100644 index 0000000..8973376 --- /dev/null +++ b/api/src/controllers/auth/githubCallback.controller.ts @@ -0,0 +1,12 @@ +import { Request, Response } from "express"; +import passport from "passport"; +import env from "../../config/env"; + +const githubCallback = [ + passport.authenticate("github", { failureRedirect: "/login?error=github" }), + (_req: Request, res: Response) => { + res.redirect((env.FRONTEND_URL || "http://localhost:3000") + "/settings"); + }, +]; + +export default githubCallback; diff --git a/api/src/controllers/auth/googleAuth.controller.ts b/api/src/controllers/auth/googleAuth.controller.ts new file mode 100644 index 0000000..62f1f33 --- /dev/null +++ b/api/src/controllers/auth/googleAuth.controller.ts @@ -0,0 +1,7 @@ +import passport from "passport"; + +const googleAuth = passport.authenticate("google", { + scope: ["email", "profile"], +}); + +export default googleAuth; diff --git a/api/src/controllers/auth/googleCallback.controller.ts b/api/src/controllers/auth/googleCallback.controller.ts new file mode 100644 index 0000000..11c91c2 --- /dev/null +++ b/api/src/controllers/auth/googleCallback.controller.ts @@ -0,0 +1,12 @@ +import { Request, Response } from "express"; +import passport from "passport"; +import env from "../../config/env"; + +const googleCallback = [ + passport.authenticate("google", { failureRedirect: "/login?error=google" }), + (_req: Request, res: Response) => { + res.redirect(env.FRONTEND_URL + "/settings"); + }, +]; + +export default googleCallback; diff --git a/api/src/controllers/auth/login.controller.ts b/api/src/controllers/auth/login.controller.ts new file mode 100644 index 0000000..b06448e --- /dev/null +++ b/api/src/controllers/auth/login.controller.ts @@ -0,0 +1,55 @@ +import { Request, Response, NextFunction } from "express"; +import passport from "passport"; +import log, { formatNotification } from "../../utils/logging/logger"; +import sanitizeUser from "../../utils/auth/sanitizeUser"; + +const login = async (req: Request, res: Response, next: NextFunction) => { + try { + await new Promise((resolve, reject) => { + passport.authenticate( + "local", + (err: any, user: Express.User, info: { message: any }) => { + if (err) { + log({ type: "error", message: "Login error", meta: err }); + return reject(err); + } + if (!user) { + log({ + type: "error", + message: info?.message || "Invalid email or password.", + }); + return res + .status(401) + .json( + formatNotification( + info?.message || + "Invalid email or password. Please try again.", + "error" + ) + ); + } + req.logIn(user, (err) => { + if (err) { + log({ type: "error", message: "Login error", meta: err }); + return reject(err); + } + log({ + type: "success", + message: `User logged in: ${user && (user as any).email}`, + }); + res.json({ + user: sanitizeUser(user), + ...formatNotification("Login successful!", "success"), + }); + resolve(); + }); + } + )(req, res, next); + }); + } catch (err) { + log({ type: "error", message: "Login failed", meta: err }); + next(err); + } +}; + +export default login; diff --git a/api/src/controllers/auth/logout.controller.ts b/api/src/controllers/auth/logout.controller.ts new file mode 100644 index 0000000..c97553b --- /dev/null +++ b/api/src/controllers/auth/logout.controller.ts @@ -0,0 +1,23 @@ +import { Request, Response } from "express"; +import log, { formatNotification } from "../../utils/logging/logger"; +import env from "../../config/env"; + +const logout = (req: Request, res: Response) => { + req.logout((err) => { + if (err) { + log({ type: "error", message: "Logout failed", meta: err }); + return res.status(500).json(formatNotification("Logout failed", "error")); + } + + res.clearCookie("connect.sid", { + httpOnly: true, + secure: env.NODE_ENV === "production", + sameSite: "lax", + }); + + log({ type: "success", message: "User logged out" }); + res.json(formatNotification("Logged out", "success")); + }); +}; + +export default logout; diff --git a/api/src/controllers/auth/register.controller.ts b/api/src/controllers/auth/register.controller.ts new file mode 100644 index 0000000..62026a3 --- /dev/null +++ b/api/src/controllers/auth/register.controller.ts @@ -0,0 +1,100 @@ +import { Request, Response, NextFunction } from "express"; +import bcrypt from "bcryptjs"; +import crypto from "crypto"; +import User from "../../models/user.model"; +import Plan from "../../models/plan.model"; +import { sendVerificationEmail } from "../../utils/auth/verification"; +import log, { formatNotification } from "../../utils/logging/logger"; +import isValidUsername from "../../utils/auth/isValidUsername"; +import env from "../../config/env"; + +const register = async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, password, name, username } = req.body; + + if (!email || !password || !name) { + log({ + type: "error", + message: "Please provide email, password, and name.", + }); + return res + .status(400) + .json( + formatNotification( + "Please provide email, password, and name.", + "error" + ) + ); + } + + if (username && !isValidUsername(username)) { + log({ type: "error", message: "Invalid username format." }); + return res + .status(400) + .json( + formatNotification( + "Invalid username. Only letters, numbers, underscores, and hyphens are allowed. No spaces.", + "error" + ) + ); + } + + const existing = await User.findOne({ email }); + if (existing) { + log({ + type: "warning", + message: `Account with email ${email} already exists.`, + }); + return res + .status(400) + .json( + formatNotification( + "An account with this email already exists. Please log in or use a different email.", + "warning" + ) + ); + } + + const hash = crypto + .createHash("md5") + .update(email.trim().toLowerCase()) + .digest("hex"); + + const gravatarUrl = `https://www.gravatar.com/avatar/${hash}?d=identicon`; + const hashedPassword = await bcrypt.hash(password, 10); + const verificationToken = crypto.randomBytes(32).toString("hex"); + + const basePlan = await Plan.findOne({ isDefault: true }); + + const newUser = await User.create({ + email, + password: hashedPassword, + name, + username, + profilePicture: gravatarUrl, + isVerified: false, + verificationToken, + plan: basePlan ? basePlan._id : undefined, + }); + + await sendVerificationEmail({ + to: email, + token: verificationToken, + domain: env.CUSTOM_DOMAIN || req.headers.origin || "", + name, + }); + + log({ type: "success", message: `User registered: ${email}` }); + res.json( + formatNotification( + "Registration successful! Please check your email to verify your account before logging in.", + "success" + ) + ); + } catch (err) { + log({ type: "error", message: "Registration failed", meta: err }); + next(err); + } +}; + +export default register; diff --git a/api/src/controllers/auth/resendVerification.controller.ts b/api/src/controllers/auth/resendVerification.controller.ts new file mode 100644 index 0000000..3a244f6 --- /dev/null +++ b/api/src/controllers/auth/resendVerification.controller.ts @@ -0,0 +1,75 @@ +import { Request, Response } from "express"; +import crypto from "crypto"; +import User from "../../models/user.model"; +import { sendVerificationEmail } from "../../utils/auth/verification"; +import log, { formatNotification } from "../../utils/logging/logger"; +import env from "../../config/env"; + +const resendVerification = async (req: Request, res: Response) => { + const { email } = req.body; + + if (!email) { + log({ type: "error", message: "Please provide your email address." }); + return res + .status(400) + .json(formatNotification("Please provide your email address.", "error")); + } + + const user = await User.findOne({ email }); + + if (!user) { + log({ + type: "error", + message: "No account found with this email address.", + }); + return res + .status(400) + .json( + formatNotification("No account found with this email address.", "error") + ); + } + + if (user.isVerified) { + log({ type: "warning", message: "This email is already verified." }); + return res + .status(400) + .json( + formatNotification( + "This email is already verified. Please log in.", + "warning" + ) + ); + } + + const token = crypto.randomBytes(32).toString("hex"); + user.verificationToken = token; + await user.save(); + + if (!env.CUSTOM_DOMAIN) { + log({ + type: "error", + message: "CUSTOM_DOMAIN is not set in environment variables.", + }); + return res + .status(500) + .json( + formatNotification( + "Server configuration error. Please contact support.", + "error" + ) + ); + } + + await sendVerificationEmail({ + to: user.email, + token, + domain: env.CUSTOM_DOMAIN, + name: user.name, + }); + + res.json({ + message: "Verification email resent! Please check your inbox.", + }); +}; + +export default resendVerification; diff --git a/api/src/controllers/auth/setUsername.controller.ts b/api/src/controllers/auth/setUsername.controller.ts new file mode 100644 index 0000000..3b45e87 --- /dev/null +++ b/api/src/controllers/auth/setUsername.controller.ts @@ -0,0 +1,87 @@ +import { Request, Response, NextFunction } from "express"; +import User from "../../models/user.model"; +import log, { formatNotification } from "../../utils/logging/logger"; +import type IUser from "../../types/user.types"; +import type { Document } from "mongoose"; +import isValidUsername from "../../utils/auth/isValidUsername"; +import sanitizeUser from "../../utils/auth/sanitizeUser"; + +const setUsername = async (req: Request, res: Response, next: NextFunction) => { + try { + if (!req.isAuthenticated?.() || !req.user) { + log({ + type: "error", + message: "You must be logged in to set a username.", + }); + return res + .status(401) + .json( + formatNotification( + "You must be logged in to set a username.", + "error" + ) + ); + } + + const user = req.user as IUser & Document; + + if (user.username) { + log({ + type: "warning", + message: "Username is already set and cannot be changed.", + }); + return res + .status(400) + .json( + formatNotification( + "Username is already set and cannot be changed.", + "warning" + ) + ); + } + + const { username } = req.body; + + // Normalize username to lowercase for Kubernetes compatibility + const normalizedUsername = username?.trim().toLowerCase(); + + if (!normalizedUsername || !isValidUsername(normalizedUsername)) { + log({ type: "error", message: "Invalid username format." }); + return res + .status(400) + .json( + formatNotification( + "Invalid username. Only lowercase letters, numbers, and hyphens are allowed. Must start and end with a letter or number. Max 63 characters.", + "error" + ) + ); + } + + const existing = await User.findOne({ username: normalizedUsername }); + if (existing) { + log({ type: "warning", message: "This username is already taken." }); + return res + .status(400) + .json( + formatNotification( + "This username is already taken. Please choose another.", + "warning" + ) + ); + } + + user.username = normalizedUsername; + await user.save(); + + log({ type: "success", message: `Username set for user: ${user.email}` }); + return res.json({ + ...formatNotification("Username set successfully!", "success"), + user: sanitizeUser(user), + }); + } catch (err) { + log({ type: "error", message: "Failed to set username", meta: err }); + next(err); + } +}; + +export default setUsername; diff --git a/api/src/controllers/auth/verifyEmail.controller.ts b/api/src/controllers/auth/verifyEmail.controller.ts new file mode 100644 index 0000000..9fbfdbd --- /dev/null +++ b/api/src/controllers/auth/verifyEmail.controller.ts @@ -0,0 +1,44 @@ +import { Request, Response } from "express"; +import User from "../../models/user.model"; +import log, { formatNotification } from "../../utils/logging/logger"; + +const verifyEmail = async (req: Request, res: Response) => { + const { token } = req.query; + + if (!token || typeof token !== "string") { + log({ type: "error", message: "Invalid or missing verification token." }); + return res + .status(400) + .json( + formatNotification("Invalid or missing verification token.", "error") + ); + } + + const user = await User.findOne({ verificationToken: token }); + + if (!user) { + log({ type: "error", message: "Invalid or expired verification token." }); + return res + .status(400) + .json( + formatNotification( + "Invalid or expired verification token. Please request a new one.", + "error" + ) + ); + } + + user.isVerified = true; + user.verificationToken = undefined; + await user.save(); + + log({ type: "success", message: `Email verified for user: ${user.email}` }); + return res.json( + formatNotification( + "Email verified successfully! You can now log in.", + "success" + ) + ); +}; + +export default verifyEmail; diff --git a/api/src/controllers/github/githubCallback.controller.ts b/api/src/controllers/github/githubCallback.controller.ts new file mode 100644 index 0000000..c4941fe --- /dev/null +++ b/api/src/controllers/github/githubCallback.controller.ts @@ -0,0 +1,39 @@ +import { Request, Response } from "express"; +import log, { formatNotification } from "../../utils/logging/logger"; +import getUserFromSession from "../../utils/github/getUserFromSession"; + +const githubCallback = async (req: Request, res: Response) => { + const installationId = req.body.installation_id as string; + const user = await getUserFromSession(req); + + if (!user || !installationId) { + log({ type: "error", message: "Missing user session or installation ID." }); + return res + .status(400) + .json( + formatNotification("Missing user session or installation ID.", "error") + ); + } + + try { + if (!user.githubInstallationId?.includes(installationId)) { + user.githubInstallationId = user.githubInstallationId || []; + user.githubInstallationId.push(installationId); + await user.save(); + } + log({ + type: "success", + message: `GitHub installation saved for user: ${user.email}`, + }); + res + .status(200) + .json(formatNotification("GitHub installation saved.", "success")); + } catch (err) { + log({ type: "error", message: "Error saving installation ID", meta: err }); + res + .status(500) + .json(formatNotification("Failed to save installation ID.", "error")); + } +}; + +export default githubCallback; diff --git a/api/src/controllers/github/githubInstall.controller.ts b/api/src/controllers/github/githubInstall.controller.ts new file mode 100644 index 0000000..63e88ce --- /dev/null +++ b/api/src/controllers/github/githubInstall.controller.ts @@ -0,0 +1,9 @@ +import { Request, Response } from "express"; +import env from "../../config/env"; + +const githubInstall = (_req: Request, res: Response) => { + const GITHUB_APP_SLUG = env.GITHUB_APP_SLUG; + res.redirect(`https://github.com/apps/${GITHUB_APP_SLUG}/installations/new`); +}; + +export default githubInstall; diff --git a/api/src/controllers/github/githubInstallationID.controller.ts b/api/src/controllers/github/githubInstallationID.controller.ts new file mode 100644 index 0000000..fbaadaa --- /dev/null +++ b/api/src/controllers/github/githubInstallationID.controller.ts @@ -0,0 +1,16 @@ +import { Request, Response } from "express"; +import log, { formatNotification } from "../../utils/logging/logger"; +import getUserFromSession from "../../utils/github/getUserFromSession"; + +const githubInstallationId = async (req: Request, res: Response) => { + const user = await getUserFromSession(req); + if (!user) { + log({ type: "error", message: "Not authenticated" }); + return res + .status(401) + .json(formatNotification("Not authenticated", "error")); + } + res.json({ installation_ids: user.githubInstallationId || [] }); +}; + +export default githubInstallationId; diff --git a/api/src/controllers/github/githubRepos.controller.ts b/api/src/controllers/github/githubRepos.controller.ts new file mode 100644 index 0000000..b13cb26 --- /dev/null +++ b/api/src/controllers/github/githubRepos.controller.ts @@ -0,0 +1,185 @@ +import { Request, Response } from "express"; +import axios from "axios"; +import log, { formatNotification } from "../../utils/logging/logger"; +import getUserFromSession from "../../utils/github/getUserFromSession"; +import createGitHubJwt from "../../utils/github/createGithubJWT"; + +const githubRepos = async (req: Request, res: Response) => { + try { + let installationIds: string[] = []; + let user = null; + let userChanged = false; + + // Always get user from session if possible + user = await getUserFromSession(req); + + if (req.query.installation_ids) { + installationIds = (req.query.installation_ids as string).split(","); + // If user exists, filter their githubInstallationId to only keep valid ones + if ( + user && + user.githubInstallationId && + Array.isArray(user.githubInstallationId) + ) { + const validIds = new Set(installationIds); + const before = user.githubInstallationId.length; + user.githubInstallationId = user.githubInstallationId.filter( + (id: string) => validIds.has(id) + ); + if (user.githubInstallationId.length !== before) { + userChanged = true; + } + } + } else if (user) { + if ( + !user.githubInstallationId || + user.githubInstallationId.length === 0 + ) { + log({ type: "warning", message: "GitHub not connected for user" }); + return res + .status(400) + .json(formatNotification("GitHub not connected for user", "warning")); + } + installationIds = user.githubInstallationId; + } + + if (!Array.isArray(installationIds) || installationIds.length === 0) { + log({ type: "error", message: "Invalid or missing installation IDs" }); + return res + .status(400) + .json( + formatNotification("Invalid or missing installation IDs", "error") + ); + } + + const jwtToken = createGitHubJwt(); + let allRepos: any[] = []; + let removedInstallationIds: string[] = []; + + for (const installationId of installationIds) { + try { + const tokenResponse = await axios.post( + `https://api.github.com/app/installations/${installationId}/access_tokens`, + {}, + { + headers: { + Authorization: `Bearer ${jwtToken}`, + Accept: "application/vnd.github+json", + }, + } + ); + const accessToken = tokenResponse.data.token; + + // Fetch all pages of repos for this installation + let page = 1; + let repos: any[] = []; + let hasMore = true; + while (hasMore) { + const reposResponse = await axios.get( + "https://api.github.com/installation/repositories", + { + headers: { + Authorization: `token ${accessToken}`, + Accept: "application/vnd.github+json", + }, + params: { + per_page: 100, + page, + }, + } + ); + const pageRepos = reposResponse.data.repositories || []; + repos = repos.concat(pageRepos); + if (pageRepos.length < 100) { + hasMore = false; + } else { + page++; + } + } + const extendedRepos = repos.map((repo: any) => ({ + id: repo.id, + name: repo.name, + full_name: repo.full_name, + html_url: repo.html_url, + description: repo.description, + private: repo.private, + fork: repo.fork, + owner_login: repo.owner?.login, + forks_count: repo.forks_count, + stargazers_count: repo.stargazers_count, + watchers_count: repo.watchers_count, + language: repo.language, + created_at: repo.created_at, + updated_at: repo.updated_at, + pushed_at: repo.pushed_at, + license: repo.license ? repo.license.spdx_id : null, + open_issues_count: repo.open_issues_count, + })); + allRepos = allRepos.concat(extendedRepos); + } catch (error: any) { + // If 404, 401, or 403, remove the installationId from user and DB + if (error.response && [404, 401, 403].includes(error.response.status)) { + // Remove from current request's installationIds + installationIds = installationIds.filter( + (id: string) => id !== installationId + ); + // Remove from user DB if user exists + if (user && user.githubInstallationId?.includes(installationId)) { + user.githubInstallationId = user.githubInstallationId.filter( + (id: string) => id !== installationId + ); + userChanged = true; + } + removedInstallationIds.push(installationId); + // Continue to next installationId instead of throwing + continue; + } else { + throw error; + } + } + } + + if (userChanged && user) { + await user.save(); + } + + if (removedInstallationIds.length > 0) { + log({ + type: "warning", + message: + "Some GitHub installations were invalid and have been removed.", + }); + return res.status(400).json({ + ...formatNotification( + "Some GitHub installations were invalid and have been removed. Please reconnect GitHub if needed.", + "warning" + ), + removedInstallationIds, + }); + } + + log({ + type: "success", + message: `Fetched ${allRepos.length} GitHub repositories`, + }); + res.json({ repositories: allRepos }); + } catch (error: any) { + log({ + type: "error", + message: "GitHub /repos error", + meta: error.response?.data || error.message, + }); + res + .status(500) + .json( + formatNotification( + error.response?.data?.message || + error.message || + "Internal Server Error", + "error" + ) + ); + } +}; + +export default githubRepos; diff --git a/api/src/controllers/github/githubSaveInstallationID.controller.ts b/api/src/controllers/github/githubSaveInstallationID.controller.ts new file mode 100644 index 0000000..c62188c --- /dev/null +++ b/api/src/controllers/github/githubSaveInstallationID.controller.ts @@ -0,0 +1,43 @@ +import { Request, Response } from "express"; +import log, { formatNotification } from "../../utils/logging/logger"; +import getUserFromSession from "../../utils/github/getUserFromSession"; + +const githubSaveInstallationID = async (req: Request, res: Response) => { + const { installation_id } = req.body; + const user = await getUserFromSession(req); + + if (!user) { + log({ type: "error", message: "Not authenticated" }); + return res + .status(401) + .json(formatNotification("Not authenticated", "error")); + } + if (!installation_id) { + log({ type: "error", message: "Missing installation ID" }); + return res + .status(400) + .json(formatNotification("Missing installation ID", "error")); + } + + try { + if (!user.githubInstallationId?.includes(installation_id)) { + user.githubInstallationId = user.githubInstallationId || []; + user.githubInstallationId.push(installation_id); + await user.save(); + } + log({ + type: "success", + message: `Installation ID saved for user: ${user.email}`, + }); + res + .status(200) + .json(formatNotification("Installation ID saved.", "success")); + } catch (err) { + log({ type: "error", message: "Error saving installation ID", meta: err }); + res + .status(500) + .json(formatNotification("Failed to save installation ID.", "error")); + } +}; + +export default githubSaveInstallationID; diff --git a/api/src/controllers/metrics/getOverallMetrics.controller.ts b/api/src/controllers/metrics/getOverallMetrics.controller.ts new file mode 100644 index 0000000..b0abe4d --- /dev/null +++ b/api/src/controllers/metrics/getOverallMetrics.controller.ts @@ -0,0 +1,52 @@ +import { Request, Response } from "express"; +import axios from "axios"; +import env from "../../config/env"; +import asyncHandler from "../../utils/handlers/asyncHandler"; + +const prometheusBaseUrl = (env as any).PROMETHEUS_URL; + +const queries = { + cpu: 'sum(rate(node_cpu_seconds_total{mode!="idle"}[5m]))', + memory: "sum(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes)", + storage: 'sum(node_filesystem_size_bytes{fstype!~"tmpfs|overlay"})', + pods: "count(kube_pod_info)", + network_rx: 'sum(rate(node_network_receive_bytes_total{device!="lo"}[5m]))', + network_tx: 'sum(rate(node_network_transmit_bytes_total{device!="lo"}[5m]))', + nodes: "count(kube_node_info)", + apiserver_uptime: 'time() - kube_pod_start_time{pod=~"kube-apiserver.*"}', +}; + +const getOverallMetrics = asyncHandler(async (_req: Request, res: Response) => { + const end = Math.floor(Date.now() / 1000); + const start = end - 3600; // last 1 hour + const step = 60; // 1-minute intervals + + const results: Record = {}; + + for (const [key, query] of Object.entries(queries)) { + const instantUrl = `${prometheusBaseUrl}/api/v1/query?query=${encodeURIComponent( + query + )}`; + const rangeUrl = `${prometheusBaseUrl}/api/v1/query_range?query=${encodeURIComponent( + query + )}&start=${start}&end=${end}&step=${step}`; + + try { + const [instantRes, rangeRes] = await Promise.all([ + axios.get(instantUrl), + axios.get(rangeUrl), + ]); + + results[key] = { + current: instantRes.data.data.result?.[0]?.value?.[1] ?? null, + history: rangeRes.data.data.result?.[0]?.values ?? [], + }; + } catch { + results[key] = { current: null, history: [] }; + } + } + + res.json({ metrics: results }); +}); + +export default getOverallMetrics; diff --git a/api/src/controllers/plans/createPlan.controller.ts b/api/src/controllers/plans/createPlan.controller.ts new file mode 100644 index 0000000..9e2e382 --- /dev/null +++ b/api/src/controllers/plans/createPlan.controller.ts @@ -0,0 +1,38 @@ +import { Request, Response, NextFunction } from "express"; +import Plan from "../../models/plan.model"; +import log, { formatNotification } from "../../utils/logging/logger"; + +// Create a new plan (admin only) +const createPlan = async (req: Request, res: Response, next: NextFunction) => { + try { + const { name, description, resources, isDefault, price, isActive } = + req.body; + if (!name || !resources) { + log({ type: "error", message: "Name and resources are required" }); + return res + .status(400) + .json(formatNotification("Name and resources are required", "error")); + } + if (isDefault) { + // Only one default plan allowed + await Plan.updateMany({ isDefault: true }, { isDefault: false }); + } + const plan = await Plan.create({ + name, + description, + resources, + isDefault, + price, + isActive, + }); + log({ type: "success", message: `Plan created: ${name}` }); + res + .status(201) + .json({ ...formatNotification("Plan created", "success"), plan }); + } catch (err) { + log({ type: "error", message: "Failed to create plan", meta: err }); + next(err); + } +}; + +export default createPlan; diff --git a/api/src/controllers/plans/deletePlan.controller.ts b/api/src/controllers/plans/deletePlan.controller.ts new file mode 100644 index 0000000..396b40b --- /dev/null +++ b/api/src/controllers/plans/deletePlan.controller.ts @@ -0,0 +1,16 @@ +import { Request, Response, NextFunction } from "express"; +import Plan from "../../models/plan.model"; + +// Delete a plan (admin only) +const deletePlan = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const plan = await Plan.findByIdAndDelete(id); + if (!plan) return res.status(404).json({ message: "Plan not found" }); + res.json({ message: "Plan deleted" }); + } catch (err) { + next(err); + } +}; + +export default deletePlan; diff --git a/api/src/controllers/plans/getPlanByID.controller.ts b/api/src/controllers/plans/getPlanByID.controller.ts new file mode 100644 index 0000000..77898a5 --- /dev/null +++ b/api/src/controllers/plans/getPlanByID.controller.ts @@ -0,0 +1,24 @@ +import { Request, Response, NextFunction } from "express"; +import Plan from "../../models/plan.model"; +import type IPlan from "../../types/plan.types"; +import type { Document } from "mongoose"; + +// Public: get a single plan by ID +const getPlanByID = async (req: Request, res: Response, next: NextFunction) => { + try { + let plan = (await Plan.findById(req.params.id)) as + | (IPlan & Document) + | null; + if (!plan) return res.status(404).json({ message: "Plan not found" }); + // Ensure plan has resources, requests, and limits fields + const p = plan.toObject(); + p.resources = p.resources || {}; + p.resources.requests = p.resources.requests || {}; + p.resources.limits = p.resources.limits || {}; + res.json(p); + } catch (err) { + next(err); + } +}; + +export default getPlanByID; diff --git a/api/src/controllers/plans/getPlans.controller.ts b/api/src/controllers/plans/getPlans.controller.ts new file mode 100644 index 0000000..105ab98 --- /dev/null +++ b/api/src/controllers/plans/getPlans.controller.ts @@ -0,0 +1,27 @@ +import { Request, Response, NextFunction } from "express"; +import Plan from "../../models/plan.model"; +import log, { formatNotification } from "../../utils/logging/logger"; +import type IPlan from "../../types/plan.types"; +import type { Document } from "mongoose"; + +// Get all plans +const getPlans = async (_req: Request, res: Response, next: NextFunction) => { + try { + let plans = await Plan.find(); + // Ensure all plans have resources, requests, and limits fields + plans = plans.map((plan: IPlan & Document) => { + const p = plan.toObject(); + p.resources = p.resources || {}; + p.resources.requests = p.resources.requests || {}; + p.resources.limits = p.resources.limits || {}; + return p; + }); + log({ type: "info", message: "Fetched all plans" }); + res.json({ ...formatNotification("Fetched all plans", "info"), plans }); + } catch (err) { + log({ type: "error", message: "Failed to fetch plans", meta: err }); + next(err); + } +}; + +export default getPlans; diff --git a/api/src/controllers/plans/razorpay.controller.ts b/api/src/controllers/plans/razorpay.controller.ts new file mode 100644 index 0000000..462a1df --- /dev/null +++ b/api/src/controllers/plans/razorpay.controller.ts @@ -0,0 +1,77 @@ +import { Request, Response } from "express"; +import Razorpay from "razorpay"; +import env from "../../config/env"; +import Plan from "../../models/plan.model"; +import getUserFromSession from "../../utils/auth/getUserFromSession"; + +const razorpay = new Razorpay({ + key_id: env.RAZORPAY_KEY_ID!, + key_secret: env.RAZORPAY_KEY_SECRET!, +}); + +export const createOrder = async (req: Request, res: Response) => { + try { + const planId = req.params.id; + const plan = await Plan.findById(planId); + if (!plan || !plan.price) { + console.error("Plan not found or price missing", { planId, plan }); + return res.status(404).json({ error: "Plan not found or price missing" }); + } + const shortReceipt = `plan_${planId}_${Date.now() + .toString() + .slice(-8)}`.slice(0, 40); + const order = await razorpay.orders.create({ + amount: plan.price, // price in paise + currency: "INR", + receipt: shortReceipt, // ensure <= 40 chars + notes: { planId }, + }); + res.json({ order }); + } catch (err) { + console.error("Razorpay order creation failed", err); + res.status(500).json({ + error: "Failed to create order", + details: err instanceof Error ? err.message : err, + }); + } +}; + +export const verifyPayment = async (req: Request, res: Response) => { + const { razorpay_order_id, razorpay_payment_id, razorpay_signature } = + req.body; + const crypto = require("crypto"); + const generated_signature = crypto + .createHmac("sha256", env.RAZORPAY_KEY_SECRET!) + .update(razorpay_order_id + "|" + razorpay_payment_id) + .digest("hex"); + if (generated_signature === razorpay_signature) { + // Find the Razorpay order to get the planId from notes + try { + const order = await razorpay.orders.fetch(razorpay_order_id); + const planId = order.notes?.planId; + if (!planId) { + return res + .status(400) + .json({ success: false, error: "Plan ID not found in order notes" }); + } + // Get user from session + const user = await getUserFromSession(req); + if (!user) { + return res + .status(401) + .json({ success: false, error: "User not authenticated" }); + } + user.plan = planId.toString(); + await user.save(); + return res.json({ success: true }); + } catch (err) { + return res.status(500).json({ + success: false, + error: "Failed to upgrade plan", + details: err instanceof Error ? err.message : err, + }); + } + } else { + return res.status(400).json({ success: false, error: "Invalid signature" }); + } +}; diff --git a/api/src/controllers/plans/updatePlan.controller.ts b/api/src/controllers/plans/updatePlan.controller.ts new file mode 100644 index 0000000..2d94b22 --- /dev/null +++ b/api/src/controllers/plans/updatePlan.controller.ts @@ -0,0 +1,25 @@ +import { Request, Response, NextFunction } from "express"; +import Plan from "../../models/plan.model"; + +// Update a plan (admin only) +const updatePlan = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const { name, description, resources, isDefault, price, isActive } = + req.body; + if (isDefault) { + await Plan.updateMany({ isDefault: true }, { isDefault: false }); + } + const plan = await Plan.findByIdAndUpdate( + id, + { name, description, resources, isDefault, price, isActive }, + { new: true } + ); + if (!plan) return res.status(404).json({ message: "Plan not found" }); + res.json({ message: "Plan updated", plan }); + } catch (err) { + next(err); + } +}; + +export default updatePlan; diff --git a/api/src/controllers/user/deleteRegistryCredential.controller.ts b/api/src/controllers/user/deleteRegistryCredential.controller.ts new file mode 100644 index 0000000..08ebda6 --- /dev/null +++ b/api/src/controllers/user/deleteRegistryCredential.controller.ts @@ -0,0 +1,30 @@ +import { Request, Response } from "express"; +import User from "../../models/user.model"; +import log, { formatNotification } from "../../utils/logging/logger"; +import type IUser from "../../types/user.types"; +import type { Document } from "mongoose"; + +// Remove a registry credential by name and type +const deleteRegistryCredential = async (req: Request, res: Response) => { + const userId = (req.user as any)?._id; + const { name, registryType } = req.body; + const user = (await User.findById(userId)) as (IUser & Document) | null; + if (!user) { + log({ type: "error", message: "User not found" }); + return res.status(404).json(formatNotification("User not found", "error")); + } + user.credentials = (user.credentials || []).filter( + (c) => !(c.name === name && c.registryType === registryType) + ); + await user.save(); + log({ + type: "success", + message: `Credential deleted for user: ${user.email}`, + }); + res.json({ + ...formatNotification("Credential deleted", "success"), + credentials: user.credentials, + }); +}; + +export default deleteRegistryCredential; diff --git a/api/src/controllers/user/getAllUsers.controller.ts b/api/src/controllers/user/getAllUsers.controller.ts new file mode 100644 index 0000000..52bd3d2 --- /dev/null +++ b/api/src/controllers/user/getAllUsers.controller.ts @@ -0,0 +1,15 @@ +import { Request, Response } from "express"; +import User from "../../models/user.model"; + +const getAllUsers = async (_req: Request, res: Response) => { + const users = await User.find( + {}, + "_id name email role username plan extraResources" + ).populate({ + path: "plan", + select: "_id name isDefault resources", + }); + res.json({ users }); +}; + +export default getAllUsers; diff --git a/api/src/controllers/user/getRegistryCredentials.controller.ts b/api/src/controllers/user/getRegistryCredentials.controller.ts new file mode 100644 index 0000000..ab533c1 --- /dev/null +++ b/api/src/controllers/user/getRegistryCredentials.controller.ts @@ -0,0 +1,20 @@ +import { Request, Response } from "express"; +import User from "../../models/user.model"; +import log, { formatNotification } from "../../utils/logging/logger"; +import type IUser from "../../types/user.types"; +import type { IRegistryCredential } from "../../types/user.types"; +import type { Document } from "mongoose"; + +// Get all registry credentials for the authenticated user +const getRegistryCredentials = async (req: Request, res: Response) => { + const userId = (req.user as any)?._id; + const user = (await User.findById(userId)) as (IUser & Document) | null; + if (!user) { + log({ type: "error", message: "User not found" }); + return res.status(404).json(formatNotification("User not found", "error")); + } + const credentials: IRegistryCredential[] = user.credentials || []; + res.json({ credentials }); +}; + +export default getRegistryCredentials; diff --git a/api/src/controllers/user/getUserResourceUsage.controller.ts b/api/src/controllers/user/getUserResourceUsage.controller.ts new file mode 100644 index 0000000..d2802e0 --- /dev/null +++ b/api/src/controllers/user/getUserResourceUsage.controller.ts @@ -0,0 +1,99 @@ +import { Request, Response, NextFunction } from "express"; +import User from "../../models/user.model"; +import log, { formatNotification } from "../../utils/logging/logger"; + +// Get total resource usage and allowed for a user +const getUserResourceUsage = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const userId = + (req.user && + ("id" in req.user ? (req.user as any).id : (req.user as any)._id)) || + req.params.id; + if (!userId) { + log({ type: "error", message: "User id required" }); + return res + .status(400) + .json(formatNotification("User id required", "error")); + } + // Get user with plan and extraResources + const user = await User.findById(userId).populate("plan"); + if (!user) { + log({ type: "error", message: "User not found" }); + return res + .status(404) + .json(formatNotification("User not found", "error")); + } + // Calculate allowed resources (plan + extraResources) + let planResources: any = {}; + if ( + user.plan && + typeof user.plan === "object" && + "resources" in user.plan + ) { + planResources = (user.plan as any).resources || {}; + } + const extra = user.extraResources || {}; + function parse(val: string | number | undefined | null): number { + if (val === undefined || val === null || val === "") return 0; + if (typeof val === "number") return val; + if (typeof val !== "string") return 0; + if (val.endsWith("m")) return parseInt(val) / 1000; + if (val.endsWith("Mi")) return parseInt(val); + if (val.endsWith("Gi")) return parseInt(val) * 1024; + return parseFloat(val); + } + const allowed = { + requests: { + cpuMilli: + parse(planResources.requests?.cpuMilli) + + parse(extra.requests?.cpuMilli), + memoryMB: + parse(planResources.requests?.memoryMB) + + parse(extra.requests?.memoryMB), + storageGB: + parse(planResources.requests?.storageGB) + + parse(extra.requests?.storageGB), + }, + limits: { + cpuMilli: + parse(planResources.limits?.cpuMilli) + parse(extra.limits?.cpuMilli), + memoryMB: + parse(planResources.limits?.memoryMB) + parse(extra.limits?.memoryMB), + storageGB: + parse(planResources.limits?.storageGB) + + parse(extra.limits?.storageGB), + }, + }; + // Sum all app resource usage for this user + const userIdForQuery = (user as any)._id || (user as any).id || user.id; + const apps = await ( + await import("../../models/application.model") + ).default.find({ owner: userIdForQuery }); + const usage = { + requests: { cpuMilli: 0, memoryMB: 0, storageGB: 0 }, + limits: { cpuMilli: 0, memoryMB: 0, storageGB: 0 }, + }; + for (const app of apps) { + usage.requests.cpuMilli += parse(app.resources?.requests?.cpuMilli); + usage.requests.memoryMB += parse(app.resources?.requests?.memoryMB); + usage.requests.storageGB += parse(app.resources?.requests?.storageGB); + usage.limits.cpuMilli += parse(app.resources?.limits?.cpuMilli); + usage.limits.memoryMB += parse(app.resources?.limits?.memoryMB); + usage.limits.storageGB += parse(app.resources?.limits?.storageGB); + } + return res.json({ allowed, usage }); + } catch (err) { + log({ + type: "error", + message: "Failed to get user resource usage", + meta: err, + }); + next(err); + } +}; + +export default getUserResourceUsage; diff --git a/api/src/controllers/user/getUserSettingsStats.controller.ts b/api/src/controllers/user/getUserSettingsStats.controller.ts new file mode 100644 index 0000000..6f7059e --- /dev/null +++ b/api/src/controllers/user/getUserSettingsStats.controller.ts @@ -0,0 +1,34 @@ +import { Request, Response } from "express"; +import Application from "../../models/application.model"; +import User from "../../models/user.model"; + +export default async function getUserSettingsStats( + req: Request, + res: Response +) { + const userId = (req.user && (req.user as any)._id) || (req as any).userId; + if (!userId) { + return res.status(400).json({ message: "User id required" }); + } + + const applications = await Application.find({ owner: userId }); + const numServers = applications.length; + + let numPorts = 0; + let numEnvVars = 0; + + applications.forEach((app) => { + numPorts += Array.isArray(app.ports) ? app.ports.length : 0; + numEnvVars += app.env ? Object.keys(app.env).length : 0; + }); + + const user = await User.findById(userId); + const numCreds = user?.credentials?.length || 0; + + res.json({ + servers: numServers, + ports: numPorts, + envVars: numEnvVars, + creds: numCreds, + }); +} diff --git a/api/src/controllers/user/updateUserExtraResources.controller.ts b/api/src/controllers/user/updateUserExtraResources.controller.ts new file mode 100644 index 0000000..1d56686 --- /dev/null +++ b/api/src/controllers/user/updateUserExtraResources.controller.ts @@ -0,0 +1,60 @@ +import { Request, Response, NextFunction } from "express"; +import User from "../../models/user.model"; +import { isValidObjectId } from "mongoose"; +import log, { formatNotification } from "../../utils/logging/logger"; +import type IUser from "../../types/user.types"; +import type { Document } from "mongoose"; + +// Admin: update extra resources of a user +const updateUserExtraResources = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { id } = req.params; + const { extraResources } = req.body; + + if (!isValidObjectId(id)) { + log({ type: "error", message: "Invalid user id" }); + return res + .status(400) + .json(formatNotification("Invalid user id", "error")); + } + + if (!extraResources) { + log({ type: "error", message: "Extra resources are required" }); + return res + .status(400) + .json(formatNotification("Extra resources are required", "error")); + } + + const user = (await User.findById(id)) as (IUser & Document) | null; + if (!user) { + log({ type: "error", message: "User not found" }); + return res + .status(404) + .json(formatNotification("User not found", "error")); + } + + user.extraResources = extraResources; + await user.save(); + log({ + type: "success", + message: `Extra resources updated for user: ${user.email}`, + }); + return res.json({ + ...formatNotification("Extra resources updated", "success"), + user, + }); + } catch (err) { + log({ + type: "error", + message: "Failed to update extra resources", + meta: err, + }); + next(err); + } +}; + +export default updateUserExtraResources; diff --git a/api/src/controllers/user/updateUserPlan.controller.ts b/api/src/controllers/user/updateUserPlan.controller.ts new file mode 100644 index 0000000..30c81b1 --- /dev/null +++ b/api/src/controllers/user/updateUserPlan.controller.ts @@ -0,0 +1,42 @@ +import User from "../../models/user.model"; +import Plan from "../../models/plan.model"; +import { Request, Response, NextFunction } from "express"; +import log from "../../utils/logging/logger"; + +const updateUserPlan = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { id } = req.params; + const { planId } = req.body; + const user = await User.findById(id); + if (!user) return res.status(404).json({ message: "User not found" }); + const plan = await Plan.findById(planId); + if (!plan) return res.status(404).json({ message: "Plan not found" }); + + // Allow: admin/superadmin for others, or self if superadmin + const reqUser = req.user as any; + const isSelf = reqUser && reqUser._id?.toString() === id; + const isAdmin = + reqUser && (reqUser.role === "admin" || reqUser.role === "superadmin"); + const isSuperadminSelf = isSelf && reqUser.role === "superadmin"; + if (!isAdmin && !isSuperadminSelf) { + return res.status(403).json({ message: "Not authorized to assign plan" }); + } + + user.plan = String(plan._id); + await user.save(); + res.json({ message: "Plan assigned to user", user }); + } catch (err) { + log({ + type: "error", + message: "Failed to update user plan", + meta: err, + }); + next(err); + } +}; + +export default updateUserPlan; diff --git a/api/src/controllers/user/updateUserResources.controller.ts b/api/src/controllers/user/updateUserResources.controller.ts new file mode 100644 index 0000000..ba0d7d6 --- /dev/null +++ b/api/src/controllers/user/updateUserResources.controller.ts @@ -0,0 +1,56 @@ +import { Request, Response, NextFunction } from "express"; +import User from "../../models/user.model"; +import { isValidObjectId } from "mongoose"; +import log, { formatNotification } from "../../utils/logging/logger"; +import type IUser from "../../types/user.types"; +import type { Document } from "mongoose"; + +// Admin: update resources of a user +const updateUserResources = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { id } = req.params; + const { resources } = req.body; + + if (!isValidObjectId(id)) { + log({ type: "error", message: "Invalid user id" }); + return res + .status(400) + .json(formatNotification("Invalid user id", "error")); + } + + if (!resources) { + log({ type: "error", message: "Resources are required" }); + return res + .status(400) + .json(formatNotification("Resources are required", "error")); + } + + const user = (await User.findById(id)) as (IUser & Document) | null; + if (!user) { + log({ type: "error", message: "User not found" }); + return res + .status(404) + .json(formatNotification("User not found", "error")); + } + + user.resources = resources; + await user.save(); + log({ + type: "success", + message: `Resources updated for user: ${user.email}`, + }); + return res.json({ + ...formatNotification("Resources updated", "success"), + user, + }); + } catch (err) { + log({ type: "error", message: "Failed to update resources", meta: err }); + next(err); + } +}; + +export default updateUserResources; diff --git a/api/src/controllers/user/updateUserRole.controller.ts b/api/src/controllers/user/updateUserRole.controller.ts new file mode 100644 index 0000000..b14a6ad --- /dev/null +++ b/api/src/controllers/user/updateUserRole.controller.ts @@ -0,0 +1,64 @@ +import { Request, Response, NextFunction } from "express"; +import User from "../../models/user.model"; +import { isValidObjectId } from "mongoose"; +import log, { formatNotification } from "../../utils/logging/logger"; +import type IUser from "../../types/user.types"; +import type { Document } from "mongoose"; + +// Superadmin: promote/demote admin or user +const updateUserRole = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { id } = req.params; + const { role } = req.body; + + if (!isValidObjectId(id)) { + return res.status(400).json({ message: "Invalid user id" }); + } + + if (!role || !["user", "admin", "superadmin"].includes(role)) { + return res.status(400).json({ message: "Invalid role" }); + } + + // Prevent superadmin from demoting themselves + if ( + req.user && + (req.user as any)._id?.toString() === id && + role !== "superadmin" + ) { + log({ type: "error", message: "Superadmin cannot demote themselves" }); + return res + .status(403) + .json( + formatNotification("Superadmin cannot demote themselves", "error") + ); + } + + const user = (await User.findById(id)) as (IUser & Document) | null; + if (!user) { + log({ type: "error", message: "User not found" }); + return res + .status(404) + .json(formatNotification("User not found", "error")); + } + + user.role = role; + await user.save(); + log({ + type: "success", + message: `User role updated to ${role} for user: ${user.email}`, + }); + return res.json({ + ...formatNotification(`User role updated to ${role}`, "success"), + user, + }); + } catch (err) { + log({ type: "error", message: "Failed to update user role", meta: err }); + next(err); + } +}; + +export default updateUserRole; diff --git a/api/src/controllers/user/upsertRegistryCredential.controller.ts b/api/src/controllers/user/upsertRegistryCredential.controller.ts new file mode 100644 index 0000000..c5c87c3 --- /dev/null +++ b/api/src/controllers/user/upsertRegistryCredential.controller.ts @@ -0,0 +1,39 @@ +import { Request, Response } from "express"; +import User from "../../models/user.model"; +import log, { formatNotification } from "../../utils/logging/logger"; +import type IUser from "../../types/user.types"; +import type { Document } from "mongoose"; + +// Add or update a registry credential for the authenticated user +const upsertRegistryCredential = async (req: Request, res: Response) => { + const userId = (req.user as any)?._id; + const { name, registryType, username, token } = req.body; + if (!name || !registryType || !username || !token) { + log({ type: "error", message: "All fields are required" }); + return res + .status(400) + .json(formatNotification("All fields are required", "error")); + } + const user = (await User.findById(userId)) as (IUser & Document) | null; + if (!user) { + log({ type: "error", message: "User not found" }); + return res.status(404).json(formatNotification("User not found", "error")); + } + const existing = user.credentials?.find( + (c) => c.name === name && c.registryType === registryType + ); + if (existing) { + existing.username = username; + existing.token = token; + } else { + user.credentials?.push({ name, registryType, username, token }); + } + await user.save(); + log({ type: "success", message: `Credential saved for user: ${user.email}` }); + res.json({ + ...formatNotification("Credential saved", "success"), + credentials: user.credentials, + }); +}; + +export default upsertRegistryCredential; diff --git a/api/src/models/application.model.ts b/api/src/models/application.model.ts new file mode 100644 index 0000000..e09e449 --- /dev/null +++ b/api/src/models/application.model.ts @@ -0,0 +1,97 @@ +import mongoose, { Document, Schema } from "mongoose"; +import IApplication from "../types/application.types"; + +export const ResourceSchema = new Schema( + { + cpuMilli: { type: Number, min: 0 }, // e.g., 250 = 0.25 vCPU + memoryMB: { type: Number, min: 0 }, // e.g., 512 + storageGB: { type: Number, min: 0 }, // e.g., 10 + }, + { _id: false } +); + +const PortSchema = new Schema( + { + containerPort: { type: Number, required: true }, + protocol: String, + subdomain: String, + }, + { _id: false } +); + +const SecretItemSchema = new Schema( + { + key: String, + path: String, + }, + { _id: false } +); + +const VolumeSchema = new Schema( + { + name: { type: String, required: true }, + mountPath: { type: String, required: true }, + type: String, + configMapName: String, + secretName: String, + claimName: String, + size: String, + readOnly: Boolean, + secretItems: [SecretItemSchema], + }, + { _id: false } +); + +const applicationSchema = new Schema( + { + name: { + type: String, + required: true, + validate: { + validator: function (v: string) { + return /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(v); + }, + message: + "Application name must be lowercase, alphanumeric, and may contain hyphens. No underscores or uppercase letters allowed. (Kubernetes DNS-1123 subdomain format)", + }, + minlength: 1, + maxlength: 63, + }, + imageUrl: { type: String, required: true }, + imageTag: { type: String, required: true }, + credentials: [ + { + name: { type: String, required: true }, + registryType: { type: String, required: true }, + username: { type: String, required: true }, + token: { type: String, required: true }, + }, + ], + namespace: String, + deploymentName: String, + serviceName: String, + env: { type: Map, of: String }, + owner: { type: Schema.Types.ObjectId, ref: "User", required: true }, + resources: { + requests: ResourceSchema, + limits: ResourceSchema, + }, + ports: [PortSchema], + volumes: [VolumeSchema], + livenessProbe: Schema.Types.Mixed, + readinessProbe: Schema.Types.Mixed, + command: [String], + args: [String], + labels: { type: Map, of: String }, + annotations: { type: Map, of: String }, + nodeSelector: { type: Object, default: {} }, + tolerations: [Schema.Types.Mixed], + affinity: Schema.Types.Mixed, + }, + { timestamps: true } +); + +export default mongoose.model( + "Application", + applicationSchema +); diff --git a/api/src/models/plan.model.ts b/api/src/models/plan.model.ts new file mode 100644 index 0000000..2dfc81d --- /dev/null +++ b/api/src/models/plan.model.ts @@ -0,0 +1,20 @@ +import mongoose, { Document, Schema } from "mongoose"; +import IPlan from "../types/plan.types"; +import { ResourceSchema } from "./application.model"; + +const planSchema = new Schema( + { + name: { type: String, required: true, unique: true }, + description: { type: String }, + resources: { + requests: ResourceSchema, + limits: ResourceSchema, + }, + isDefault: { type: Boolean, default: false }, + price: { type: Number /* Price in paise (integer) */ }, + isActive: { type: Boolean, default: true }, + }, + { timestamps: true } +); + +export default mongoose.model("Plan", planSchema); diff --git a/api/src/models/user.model.ts b/api/src/models/user.model.ts new file mode 100644 index 0000000..121e203 --- /dev/null +++ b/api/src/models/user.model.ts @@ -0,0 +1,58 @@ +import mongoose, { Document, Schema } from "mongoose"; +import IUser, { IOAuth, IRegistryCredential } from "../types/user.types"; +import { ResourceSchema } from "./application.model"; + +const oauthSchema = new Schema( + { + googleId: { type: String }, + githubId: { type: String }, + }, + { _id: false } +); + +const registryCredentialSchema = new Schema( + { + name: { type: String, required: true }, + registryType: { + type: String, + enum: ["dockerhub", "github", "gitlab", "other"], + required: true, + }, + username: { type: String, required: true }, + token: { type: String, required: true }, + }, + { _id: false } +); + +const userSchema = new Schema( + { + _id: { type: Schema.Types.ObjectId, auto: true }, + email: { type: String, required: true, unique: true }, + password: { type: String }, + oauth: { type: oauthSchema, default: {} }, + username: { type: String }, + profilePicture: { type: String }, + name: { type: String }, + githubInstallationId: { type: [String], default: [] }, + isVerified: { type: Boolean, default: false }, + verificationToken: { type: String }, + role: { + type: String, + enum: ["user", "admin", "superadmin"], + default: "user", + }, + resources: { + requests: ResourceSchema, + limits: ResourceSchema, + }, + extraResources: { + requests: ResourceSchema, + limits: ResourceSchema, + }, + plan: { type: Schema.Types.ObjectId, ref: "Plan", default: null }, + credentials: { type: [registryCredentialSchema], default: [] }, + }, + { timestamps: true } +); + +export default mongoose.model("User", userSchema); diff --git a/api/src/routes/application.routes.ts b/api/src/routes/application.routes.ts new file mode 100644 index 0000000..188c8e2 --- /dev/null +++ b/api/src/routes/application.routes.ts @@ -0,0 +1,68 @@ +// Application routes for managing user applications, deployments, logs, and metrics +import { Router, Request, Response, NextFunction } from "express"; +import ensureAuthenticated from "../utils/auth/ensureAuthenticated"; +import asyncHandler from "../utils/handlers/asyncHandler"; + +import createApplication from "../controllers/application/createApplication.controller"; +import getApplications from "../controllers/application/getApplications.controller"; +import getApplication from "../controllers/application/getApplication.controller"; +import updateApplication from "../controllers/application/updateApplication.controller"; +import deleteApplication from "../controllers/application/deleteApplication.controller"; +import applyApplication from "../controllers/application/applyApplication.controller"; +import runDockerHandler from "../controllers/application/runDockerHandler.controller"; +import removeDeployment from "../controllers/application/removeDeployment.controller"; +import removeNamespace from "../controllers/application/removeNamespace.controller"; +import streamApplicationLogs from "../controllers/application/streamApplicationLogs.controller"; +import getApplicationMetrics from "../controllers/application/getApplicationMetrics.controller"; +import getApplicationsStatus from "../controllers/application/getApplicationsStatus.controller"; +import scaleDeploymentZero from "../controllers/application/scaleDeploymentZero.controller"; +import rolloutRestartDeployment from "../controllers/application/rolloutRestartDeployment.controller"; +import testImageAvailabilityController from "../controllers/application/testImageAvailability.controller"; + +const validateObjectId = (req: Request, res: Response, next: NextFunction) => { + const id = req.params.id; + + if (!/^[0-9a-fA-F]{24}$/.test(id)) { + res.status(400).json({ message: "Invalid application ID format" }); + return; + } + next(); +}; + +const router = Router(); + +// All routes require authentication +router.use(ensureAuthenticated); + +// Create a new application +router.post("/", createApplication); +// Get all applications for the user +router.get("/", getApplications); +// Get live status for all applications - must be before /:id route +router.get("/status", getApplicationsStatus); +// Get a specific application by ID (with ObjectId validation) +router.get("/:id", validateObjectId, getApplication); +// Update an application by ID +router.put("/:id", validateObjectId, updateApplication); +// Delete an application by ID +router.delete("/:id", validateObjectId, deleteApplication); +// Apply (deploy) an application +router.post("/:id/apply", validateObjectId, applyApplication); +// Remove a deployment for an application +router.post("/:id/remove-deployment", validateObjectId, removeDeployment); +// Scale deployment to 0 replicas +router.post("/:id/scale-zero", validateObjectId, scaleDeploymentZero); +// Rollout restart deployment +router.post("/:id/rollout-restart", validateObjectId, rolloutRestartDeployment); +// Remove a namespace for an application +router.post("/:id/remove-namespace", validateObjectId, removeNamespace); +// Stream logs for an application +router.get("/:id/logs", validateObjectId, streamApplicationLogs); +// Get metrics for an application +router.get("/:id/metrics", validateObjectId, getApplicationMetrics); +// Test Docker image availability +router.post("/test-image", testImageAvailabilityController); +// Run a Docker handler (async) +router.post("/run-docker", asyncHandler(runDockerHandler)); + +export default router; diff --git a/api/src/routes/auth.routes.ts b/api/src/routes/auth.routes.ts new file mode 100644 index 0000000..9fbc514 --- /dev/null +++ b/api/src/routes/auth.routes.ts @@ -0,0 +1,43 @@ +// Authentication routes for user registration, login, OAuth, and profile management +import { Router } from "express"; +import asyncHandler from "../utils/handlers/asyncHandler"; +import ensureAuthenticated from "../utils/auth/ensureAuthenticated"; + +import register from "../controllers/auth/register.controller"; +import login from "../controllers/auth/login.controller"; +import logout from "../controllers/auth/logout.controller"; +import googleAuth from "../controllers/auth/googleAuth.controller"; +import googleCallback from "../controllers/auth/googleCallback.controller"; +import githubAuth from "../controllers/auth/githubAuth.controller"; +import githubCallback from "../controllers/auth/githubCallback.controller"; +import getMe from "../controllers/auth/getMe.controller"; +import setUsername from "../controllers/auth/setUsername.controller"; +import verifyEmail from "../controllers/auth/verifyEmail.controller"; +import resendVerification from "../controllers/auth/resendVerification.controller"; + +const router = Router(); + +// Register a new user +router.post("/register", asyncHandler(register)); +// Login user +router.post("/login", asyncHandler(login)); +// Logout user +router.post("/logout", logout); +// Google OAuth authentication +router.get("/google", googleAuth); +// Google OAuth callback +router.get("/google/callback", googleCallback); +// GitHub OAuth authentication +router.get("/github", githubAuth); +// GitHub OAuth callback +router.get("/github/callback", githubCallback); +// Get current user profile (requires authentication) +router.get("/me", ensureAuthenticated, asyncHandler(getMe)); +// Set or update username +router.post("/set-username", asyncHandler(setUsername)); +// Verify email address +router.get("/verify-email", asyncHandler(verifyEmail)); +// Resend verification email +router.post("/resend-verification", asyncHandler(resendVerification)); + +export default router; diff --git a/api/src/routes/github.routes.ts b/api/src/routes/github.routes.ts new file mode 100644 index 0000000..6c2b482 --- /dev/null +++ b/api/src/routes/github.routes.ts @@ -0,0 +1,35 @@ +// GitHub integration routes for Github App, installation, and repository management +import { Router } from "express"; + +import githubInstall from "../controllers/github/githubInstall.controller"; +import githubCallback from "../controllers/github/githubCallback.controller"; +import githubRepos from "../controllers/github/githubRepos.controller"; +import githubInstallationId from "../controllers/github/githubInstallationID.controller"; +import githubSaveInstallationID from "../controllers/github/githubSaveInstallationID.controller"; + +const router = Router(); + +// Start GitHub App installation flow +router.get("/install", githubInstall); + +// GitHub OAuth callback +router.post("/callback", (req, res) => { + githubCallback(req, res); +}); + +// Get user's GitHub repositories +router.get("/repos", (req, res) => { + githubRepos(req, res); +}); + +// Get GitHub installation ID for the user +router.get("/installation-id", (req, res) => { + githubInstallationId(req, res); +}); + +// Save GitHub installation ID for the user +router.post("/installation-id", (req, res) => { + githubSaveInstallationID(req, res); +}); + +export default router; diff --git a/api/src/routes/metrics.routes.ts b/api/src/routes/metrics.routes.ts new file mode 100644 index 0000000..7fca50c --- /dev/null +++ b/api/src/routes/metrics.routes.ts @@ -0,0 +1,29 @@ +import { Router, Request, Response } from "express"; +import asyncHandler from "../utils/handlers/asyncHandler"; +import axios from "axios"; +import env from "../config/env"; +import ensureAuthenticated from "../utils/auth/ensureAuthenticated"; +import getOverallMetrics from "../controllers/metrics/getOverallMetrics.controller"; + +// Add PROMETHEUS_URL to env type if not present +const prometheusBaseUrl = + (env as any).PROMETHEUS_URL || "http://localhost:9090"; + +const router = Router(); + +router.get("/apiuptime", ensureAuthenticated, (req: Request, res: Response) => { + const uptime = Math.round(process.uptime()); + const now = Math.floor(Date.now() / 1000); + const history = Array.from({ length: 60 }, (_, i) => { + const ts = now - (59 - i) * 60; + const up = Math.max(uptime - (59 - i) * 60, 0); + return [ts, up]; + }); + res.json({ uptime, history }); +}); + + + +router.get("/overall", ensureAuthenticated, getOverallMetrics); + +export default router; diff --git a/api/src/routes/plan.routes.ts b/api/src/routes/plan.routes.ts new file mode 100644 index 0000000..d13e395 --- /dev/null +++ b/api/src/routes/plan.routes.ts @@ -0,0 +1,39 @@ +// Plan routes for managing subscription plans and handling payments +import { Router } from "express"; +import { ensureAdmin } from "../auth/role.middleware"; +import asyncHandler from "../utils/handlers/asyncHandler"; +import express from "express"; +import getPlanByID from "../controllers/plans/getPlanByID.controller"; +import createPlan from "../controllers/plans/createPlan.controller"; +import getPlans from "../controllers/plans/getPlans.controller"; +import updatePlan from "../controllers/plans/updatePlan.controller"; +import deletePlan from "../controllers/plans/deletePlan.controller"; +import { + createOrder, + verifyPayment, +} from "../controllers/plans/razorpay.controller"; + +const router = Router(); + +// Public: Get a single plan by ID +router.get("/:id", asyncHandler(getPlanByID)); +// Public: Get all plans +router.get("/", asyncHandler(getPlans)); + +// Razorpay payment endpoints (public) +router.post("/:id/create-order", asyncHandler(createOrder)); +router.post("/verify-payment", express.json(), (req, res) => { + verifyPayment(req, res); +}); + +// All plan management routes below require admin privileges +router.use(ensureAdmin); + +// Admin: Create a new plan +router.post("/", asyncHandler(createPlan)); +// Admin: Update a plan by ID +router.put("/:id", asyncHandler(updatePlan)); +// Admin: Delete a plan by ID +router.delete("/:id", asyncHandler(deletePlan)); + +export default router; diff --git a/api/src/routes/user.routes.ts b/api/src/routes/user.routes.ts new file mode 100644 index 0000000..9d8af32 --- /dev/null +++ b/api/src/routes/user.routes.ts @@ -0,0 +1,68 @@ +// User routes for managing user resources, roles, plans, and registry credentials +import { Router } from "express"; +import ensureAuthenticated from "../utils/auth/ensureAuthenticated"; +import { ensureAdmin, ensureSuperadmin } from "../auth/role.middleware"; +import asyncHandler from "../utils/handlers/asyncHandler"; + +import updateUserResources from "../controllers/user/updateUserResources.controller"; +import updateUserRole from "../controllers/user/updateUserRole.controller"; +import getUserResourceUsage from "../controllers/user/getUserResourceUsage.controller"; +import getRegistryCredentials from "../controllers/user/getRegistryCredentials.controller"; +import upsertRegistryCredential from "../controllers/user/upsertRegistryCredential.controller"; +import deleteRegistryCredential from "../controllers/user/deleteRegistryCredential.controller"; +import updateUserExtraResources from "../controllers/user/updateUserExtraResources.controller"; +import updateUserPlan from "../controllers/user/updateUserPlan.controller"; +import getAllUsers from "../controllers/user/getAllUsers.controller"; +import getUserSettingsStats from "../controllers/user/getUserSettingsStats.controller"; + +const router = Router(); + +// All routes require authentication +router.use(ensureAuthenticated); + +// Admin: update resources of a user +router.put("/:id/resources", ensureAdmin, asyncHandler(updateUserResources)); + +// Superadmin: promote/demote any user (user <-> admin <-> superadmin) +router.put("/:id/role", ensureSuperadmin, asyncHandler(updateUserRole)); + +// Admin or self (if superadmin): assign a plan to a user +router.put("/:id/plan", ensureAuthenticated, asyncHandler(updateUserPlan)); + +// Admin: update extra resources of a user +router.put( + "/:id/extra-resources", + ensureAdmin, + asyncHandler(updateUserExtraResources) +); + +// Get total resource usage and allowed for a user (by admin or self) +router.get( + "/:id/resource-usage", + ensureAuthenticated, + asyncHandler(getUserResourceUsage) +); + +// Get total resource usage and allowed for the current user +router.get( + "/me/resource-usage", + ensureAuthenticated, + asyncHandler(getUserResourceUsage) +); + +// GET all users (admin/superadmin only): name, email, role, plan +router.get("/", ensureAdmin, asyncHandler(getAllUsers)); + +// Registry credentials management for the current user +router.get("/me/credentials", asyncHandler(getRegistryCredentials)); +router.post("/me/credentials", asyncHandler(upsertRegistryCredential)); +router.delete("/me/credentials", asyncHandler(deleteRegistryCredential)); + +// Add user stats route +router.get( + "/me/settings-stats", + ensureAuthenticated, + asyncHandler(getUserSettingsStats) +); + +export default router; diff --git a/api/src/server.ts b/api/src/server.ts new file mode 100644 index 0000000..eae5c55 --- /dev/null +++ b/api/src/server.ts @@ -0,0 +1,26 @@ +import env from "./config/env"; +import mongoose from "mongoose"; +import api from "./api"; +import log from "./utils/logging/logger"; +import { populateBasePlanIfEmpty } from "./utils/populateBasePlan"; + +const PORT = 5000; + +async function startServer() { + try { + await mongoose.connect(env.MONGO_URI!); + log({ type: "success", message: "MongoDB connected" }); + + await populateBasePlanIfEmpty(); + + api.listen(PORT, () => { + log({ type: "success", message: `Server running on port ${PORT}` }); + log({ type: "info", message: `API URL: http://localhost:${PORT}` }); + }); + } catch (err) { + log({ type: "error", message: "Server failed to start", meta: err }); + process.exit(1); + } +} + +startServer(); diff --git a/api/src/types/application.types.ts b/api/src/types/application.types.ts new file mode 100644 index 0000000..1d12af3 --- /dev/null +++ b/api/src/types/application.types.ts @@ -0,0 +1,53 @@ +import mongoose from "mongoose"; + +export interface Resource { + cpuMilli?: number; + memoryMB?: number; + storageGB?: number; +} +export default interface IApplication { + _id?: mongoose.Types.ObjectId | string; + name: string; + imageUrl: string; + imageTag: string; + namespace?: string; + deploymentName?: string; + serviceName?: string; + env?: Record; + owner: mongoose.Types.ObjectId; + resources?: { + requests?: Resource; + limits?: Resource; + }; + ports?: Array<{ + containerPort: number; + protocol?: string; + subdomain?: string; + }>; + volumes?: Array<{ + name: string; + mountPath: string; + type?: string; + configMapName?: string; + secretName?: string; + claimName?: string; + size?: string; + readOnly?: boolean; + secretItems?: Array<{ key: string; path: string }>; + }>; + livenessProbe?: Record; + readinessProbe?: Record; + command?: string[]; + args?: string[]; + labels?: Record; + annotations?: Record; + nodeSelector?: Record; + tolerations?: Array>; + affinity?: Record; + credentials?: Array<{ + name: string; + registryType: string; + username: string; + token: string; + }>; +} diff --git a/api/src/types/plan.types.ts b/api/src/types/plan.types.ts new file mode 100644 index 0000000..3b9fc16 --- /dev/null +++ b/api/src/types/plan.types.ts @@ -0,0 +1,16 @@ +import mongoose from "mongoose"; +import { Resource } from "./application.types"; + +export default interface IPlan { + _id?: mongoose.Types.ObjectId | string; + name: string; + description?: string; + resources: { + requests?: Resource; + limits?: Resource; + }; + isDefault?: boolean; + /** Price in paise (integer) */ + price?: number; + isActive?: boolean; +} diff --git a/api/src/types/user.types.ts b/api/src/types/user.types.ts new file mode 100644 index 0000000..da9db8f --- /dev/null +++ b/api/src/types/user.types.ts @@ -0,0 +1,38 @@ +import mongoose from "mongoose"; +import { Resource } from "./application.types"; + +export interface IOAuth { + googleId?: string; + githubId?: string; +} + +export interface IRegistryCredential { + name: string; + registryType: "dockerhub" | "github" | "gitlab" | "other"; + username: string; + token: string; +} + +export default interface IUser { + _id?: mongoose.Types.ObjectId | string; + email: string; + password?: string; + oauth?: IOAuth; + username?: string; + profilePicture?: string; + name?: string; + githubInstallationId?: string[]; + isVerified?: boolean; + verificationToken?: string; + role?: "user" | "admin" | "superadmin"; + resources?: { + requests?: Resource; + limits?: Resource; + }; + extraResources?: { + requests?: Resource; + limits?: Resource; + }; + plan?: mongoose.Types.ObjectId | string; + credentials?: IRegistryCredential[]; +} diff --git a/api/src/utils/auth/ensureAuthenticated.ts b/api/src/utils/auth/ensureAuthenticated.ts new file mode 100644 index 0000000..f34a401 --- /dev/null +++ b/api/src/utils/auth/ensureAuthenticated.ts @@ -0,0 +1,12 @@ +import { Response, NextFunction } from "express"; + +export default function ensureAuthenticated( + req: any, + res: Response, + next: NextFunction +) { + if (typeof req.isAuthenticated === "function" && req.isAuthenticated()) { + return next(); + } + res.status(401).json({ message: "Not authenticated" }); +} diff --git a/api/src/utils/auth/getUserFromSession.ts b/api/src/utils/auth/getUserFromSession.ts new file mode 100644 index 0000000..280bb83 --- /dev/null +++ b/api/src/utils/auth/getUserFromSession.ts @@ -0,0 +1,13 @@ +import { Request } from "express"; +import User from "../../models/user.model"; +import IUser from "../../types/user.types"; +import { Document } from "mongoose"; + +async function getUserFromSession( + req: Request +): Promise<(IUser & Document) | null> { + if (!req.user) return null; + return User.findById((req.user as any)._id); +} + +export default getUserFromSession; diff --git a/api/src/utils/auth/isValidUsername.ts b/api/src/utils/auth/isValidUsername.ts new file mode 100644 index 0000000..3745208 --- /dev/null +++ b/api/src/utils/auth/isValidUsername.ts @@ -0,0 +1,15 @@ +function isValidUsername(username: string): boolean { + // Kubernetes-compatible username validation + // Must be lowercase alphanumeric with hyphens only + // Must start and end with alphanumeric character + // Maximum 63 characters (DNS label limit) + const usernameRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/; + return ( + typeof username === "string" && + username.trim().length > 0 && + username.trim().length <= 63 && + usernameRegex.test(username.trim()) + ); +} + +export default isValidUsername; diff --git a/api/src/utils/auth/sanitizeUser.ts b/api/src/utils/auth/sanitizeUser.ts new file mode 100644 index 0000000..5f5e0f2 --- /dev/null +++ b/api/src/utils/auth/sanitizeUser.ts @@ -0,0 +1,10 @@ +import IUser from "../../types/user.types"; + +function sanitizeUser(user: any): Partial { + const safeUser = { ...(user.toObject?.() || user) }; + if (safeUser.password) delete safeUser.password; + if (safeUser.verificationToken) delete safeUser.verificationToken; + return safeUser; +} + +export default sanitizeUser; diff --git a/api/src/utils/auth/verification.ts b/api/src/utils/auth/verification.ts new file mode 100644 index 0000000..effff88 --- /dev/null +++ b/api/src/utils/auth/verification.ts @@ -0,0 +1,57 @@ +import sendMail from "../email/mailer"; + +export async function sendVerificationEmail({ + to, + token, + domain, + name, +}: { + to: string; + token: string; + domain: string; + name?: string; +}) { + const verifyUrl = `${domain}/auth/verify-email?token=${token}`; + const now = new Date(); + const formattedDate = now.toLocaleString("en-US", { + dateStyle: "medium", + timeStyle: "short", + }); + + let gravatarHash = ""; + try { + gravatarHash = require("crypto") + .createHash("md5") + .update(to.trim().toLowerCase()) + .digest("hex"); + } catch (e) { + gravatarHash = ""; + } + await sendMail({ + to, + subject: "Verify your email address", + html: ` +
+
+
+ avatar +

${ + name ? `Hello, ${name}!` : "Welcome!" + }

+

${to}

+

${formattedDate}

+
+

Verify your email address

+

Thank you for signing up! Please click the button below to verify your email address and activate your account.

+ +

If the button doesn't work, copy and paste this link into your browser:

+

${verifyUrl}

+
+

© ${now.getFullYear()} Kargo. All rights reserved.

+
+
+ `, + }); +} diff --git a/api/src/utils/docker/architectureValidation.ts b/api/src/utils/docker/architectureValidation.ts new file mode 100644 index 0000000..8166346 --- /dev/null +++ b/api/src/utils/docker/architectureValidation.ts @@ -0,0 +1,128 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +export interface NodeArchitectureInfo { + name: string; + arch: string; +} + +export interface ClusterArchitectureResult { + nodeArchitectures: string[]; + nodeDetails: NodeArchitectureInfo[]; + error?: string; +} + +export interface ArchitectureValidationResult { + isSupported: boolean; + supportedNodes: string[]; + unsupportedNodes: string[]; + suggestions: string[]; + recommendedNodeSelector?: { [key: string]: string }; +} + +/** + * Get architectures of all nodes in the cluster + */ +export async function getClusterArchitectures(): Promise { + try { + const { stdout } = await execAsync( + 'kubectl get nodes -o jsonpath="{range .items[*]}{.metadata.name}:{.status.nodeInfo.architecture}{\'\\n\'}{end}"', + { timeout: 10000 } + ); + + const nodeDetails: NodeArchitectureInfo[] = stdout + .trim() + .split('\n') + .filter(line => line.trim()) + .map(line => { + const [name, arch] = line.split(':'); + return { + name: name?.trim() || 'unknown', + arch: arch?.trim() || 'unknown' + }; + }) + .filter(node => node.name !== 'unknown' && node.arch !== 'unknown'); + + const nodeArchitectures = [...new Set(nodeDetails.map(node => node.arch))]; + + return { nodeArchitectures, nodeDetails }; + } catch (error) { + return { + nodeArchitectures: [], + nodeDetails: [], + error: `Failed to get cluster node architectures: ${error}` + }; + } +} + +/** + * Extract supported architectures from Docker image manifest + */ +export function getImageArchitectures(manifestData: any): string[] { + try { + // Handle different manifest types + if (manifestData.manifests && Array.isArray(manifestData.manifests)) { + return manifestData.manifests + .map((m: any) => m.platform?.architecture) + .filter(Boolean); + } else if (manifestData.architecture) { + return [manifestData.architecture]; + } + return []; + } catch (error) { + return []; + } +} + +/** + * Validate architecture compatibility between image and cluster + */ +export function validateArchitectureCompatibility( + imageArchs: string[], + clusterArchs: string[], + nodeDetails: NodeArchitectureInfo[] +): ArchitectureValidationResult { + const supportedNodes = nodeDetails + .filter(node => imageArchs.includes(node.arch)) + .map(node => node.name); + + const unsupportedNodes = nodeDetails + .filter(node => !imageArchs.includes(node.arch)) + .map(node => node.name); + + const suggestions: string[] = []; + let recommendedNodeSelector: { [key: string]: string } | undefined; + + if (supportedNodes.length === 0) { + suggestions.push( + `Image supports [${imageArchs.join(', ')}] but cluster has [${clusterArchs.join(', ')}].` + ); + suggestions.push("Consider using a multi-architecture image."); + } + + else if (unsupportedNodes.length > 0) { + + const supportedArchsInCluster = nodeDetails + .filter(node => imageArchs.includes(node.arch)) + .map(node => node.arch); + + const uniqueSupportedArchs = [...new Set(supportedArchsInCluster)]; + + // If only one architecture is supported in the cluster, use nodeSelector + if (uniqueSupportedArchs.length === 1) { + recommendedNodeSelector = { 'kubernetes.io/arch': uniqueSupportedArchs[0] }; + } + // If multiple architectures are supported, let Kubernetes scheduler decide + // but still could add nodeSelector for the primary supported arch if needed + } + + return { + isSupported: supportedNodes.length > 0, + supportedNodes, + unsupportedNodes, + suggestions, + recommendedNodeSelector + }; +} diff --git a/api/src/utils/docker/testImageAvailability.ts b/api/src/utils/docker/testImageAvailability.ts new file mode 100644 index 0000000..eccb3fb --- /dev/null +++ b/api/src/utils/docker/testImageAvailability.ts @@ -0,0 +1,377 @@ +import axios, { AxiosError } from 'axios'; +import type { IRegistryCredential } from "../../types/user.types"; +import log from "../logging/logger"; +import { getClusterArchitectures, validateArchitectureCompatibility, getImageArchitectures } from './architectureValidation'; + +interface ImageTestResult { + available: boolean; + error?: string; + needsAuth?: boolean; + authTested?: boolean; + testedWith?: string; + suggestions?: string[]; + isArchitectureIssue?: boolean; + architectureSupported?: boolean; + supportedArchitectures?: string[]; + clusterArchitectures?: string[]; + unsupportedNodes?: string[]; + recommendedNodeSelector?: { [key: string]: string }; +} + +export type { ImageTestResult }; + +interface RegistryInfo { + type: string; + registryUrl: string; + hostService: string; + tokenRealm: string; +} + +/** + * Validate image architecture against cluster + */ +async function validateImageArchitecture(manifestData: any): Promise> { + const imageArchs = getImageArchitectures(manifestData); + + log({ + type: "info", + message: `Found image architectures: ${imageArchs.join(', ') || 'none detected'}`, + }); + + const clusterResult = await getClusterArchitectures(); + + if (clusterResult.error) { + log({ + type: "warning", + message: `Architecture validation skipped: ${clusterResult.error}`, + }); + return { + available: false, + error: "Cannot validate image architecture compatibility - cluster access required for deployment.", + isArchitectureIssue: true, + suggestions: ["Architecture validation skipped: kubectl not available in this environment."] + }; + } + + if (imageArchs.length === 0) { + log({ + type: "warning", + message: "Could not determine image architecture from manifest", + }); + return { + available: false, + error: "Cannot determine image architecture - deployment blocked for safety.", + isArchitectureIssue: true, + suggestions: ["Could not determine image architecture from manifest."] + }; + } + + log({ + type: "info", + message: `Found cluster architectures: ${clusterResult.nodeArchitectures.join(', ')}`, + }); + + const validation = validateArchitectureCompatibility( + imageArchs, + clusterResult.nodeArchitectures, + clusterResult.nodeDetails + ); + + const suggestions = [...validation.suggestions]; + + if (!validation.isSupported) { + suggestions.push("Image architecture is not compatible with any cluster nodes."); + return { + available: false, + error: "Image cannot run on any cluster nodes - deployment blocked.", + isArchitectureIssue: true, + suggestions + }; + } + + log({ + type: "success", + message: `Architecture validation: compatible - ${suggestions.length} suggestions`, + }); + + return { + architectureSupported: validation.isSupported, + supportedArchitectures: imageArchs, + clusterArchitectures: clusterResult.nodeArchitectures, + unsupportedNodes: validation.unsupportedNodes, + recommendedNodeSelector: validation.recommendedNodeSelector, + suggestions + }; +} + +/** + * Test if a Docker image is available for pulling + * @param imageUrl The Docker image URL (e.g., nginx, ghcr.io/owner/repo) + * @param imageTag The image tag (e.g., latest, v1.0.0) + * @param credentials Array of registry credentials to try if public pull fails + * @returns Promise with test result + */ +export default async function testImageAvailability( + imageUrl: string, + imageTag: string = "latest", + credentials?: IRegistryCredential[] +): Promise { + const fullImageName = `${imageUrl}:${imageTag}`; + + log({ + type: "info", + message: `Testing availability of Docker image: ${fullImageName}`, + }); + + const registry = parseRegistryFromImage(imageUrl); + const name = normalizeImageName(imageUrl, registry); + + // First try without authentication (public image) + const publicResult = await testImagePull(name, imageTag, registry); + if (publicResult.available) { + log({ + type: "success", + message: `Image ${fullImageName} is publicly available`, + }); + + const architectureResult = await validateImageArchitecture(publicResult.manifestData); + + return { + available: true, + ...architectureResult + }; + } + + // If public pull failed and we have credentials, try with authentication + if (credentials && credentials.length > 0) { + log({ + type: "info", + message: `Public pull failed, testing with ${credentials.length} credential(s)`, + }); + + const suggestions: string[] = []; + let testedCredentials = 0; + + for (const credential of credentials) { + // Check if this credential is relevant for the image registry + if (isCredentialRelevant(imageUrl, credential)) { + testedCredentials++; + const authResult = await testImagePullWithAuth(name, imageTag, registry, credential); + + if (authResult.available) { + log({ + type: "success", + message: `Image ${fullImageName} is available with authentication`, + }); + + const architectureResult = await validateImageArchitecture(authResult.manifestData); + + return { + available: true, + authTested: true, + testedWith: `${credential.name} (${credential.registryType})`, + ...architectureResult + }; + } else { + suggestions.push(`Failed with ${credential.name} (${credential.registryType}): ${authResult.error}`); + } + } + } + + if (testedCredentials === 0) { + suggestions.push("No credentials found that match this registry. Consider adding credentials for the appropriate registry type."); + } + + return { + available: false, + needsAuth: true, + authTested: true, + error: `Image not accessible with ${testedCredentials} tested credential(s)`, + suggestions + }; + } + + // No credentials provided or available, return suggestions + const suggestions: string[] = []; + + // Determine registry type and suggest appropriate credentials + if (fullImageName.includes("ghcr.io")) { + suggestions.push("This appears to be a GitHub Container Registry image. Consider adding GitHub registry credentials."); + } else if (fullImageName.includes("registry.gitlab.com")) { + suggestions.push("This appears to be a GitLab Container Registry image. Consider adding GitLab registry credentials."); + } else if (!fullImageName.includes("/") || fullImageName.includes("docker.io")) { + suggestions.push("This appears to be a Docker Hub image. Consider adding Docker Hub registry credentials if it's private."); + } else { + suggestions.push("This appears to be a private registry image. Consider adding appropriate registry credentials."); + } + + return { + available: false, + needsAuth: true, + error: publicResult.error || "Image not publicly accessible and no credentials provided", + suggestions + }; +} + +/** + * Test image pull with authentication + */ +async function testImagePullWithAuth( + repo: string, + tag: string, + registry: RegistryInfo, + credential: IRegistryCredential +): Promise { + try { + const url = `${registry.registryUrl}/v2/${repo}/manifests/${tag}`; + + const headers: Record = { + Accept: 'application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json', + }; + + const token = await getAuthToken(repo, registry, credential); + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + const res = await axios.get(url, { + headers, + timeout: 10000, + }); + if (res.status === 200) { + return { available: true, manifestData: res.data }; + } + return { available: false, error: `Unexpected HTTP ${res.status}` }; + } catch (err) { + const e = err as AxiosError; + const errorData = e.response?.data as any; + const errorMsg = errorData?.errors?.[0]?.message || e.response?.statusText || e.message; + return { available: false, error: errorMsg }; + } +} + +async function testImagePull( + repo: string, + tag: string, + registry: RegistryInfo +): Promise { + try { + const url = `${registry.registryUrl}/v2/${repo}/manifests/${tag}`; + + const headers: Record = { + Accept: 'application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json', + }; + + // Get an auth token most registries require this + const token = await getAuthToken(repo, registry); + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + const res = await axios.get(url, { + headers, + timeout: 10000, + }); + if (res.status === 200) { + return { available: true, manifestData: res.data }; + } + return { available: false, error: `Unexpected HTTP ${res.status}` }; + } catch (err) { + const e = err as AxiosError; + const errorData = e.response?.data as any; + const errorMsg = errorData?.errors?.[0]?.message || e.response?.statusText || e.message; + return { available: false, error: errorMsg }; + } +} + +async function getAuthToken( + repo: string, + registry: RegistryInfo, + credential?: IRegistryCredential +): Promise { + try { + const service = registry.hostService; + const realm = registry.tokenRealm; + const scope = `repository:${repo}:pull`; + const params = { service, scope }; + const url = `${realm}?${new URLSearchParams(params).toString()}`; + + const opts: any = { timeout: 10000 }; + if (credential) { + opts.auth = { username: credential.username, password: credential.token }; + } + + const res = await axios.get(url, opts); + const token = res.data?.token; + + if (!token) { + return undefined; + } + + return token; + } catch (err) { + const e = err as AxiosError; + const errorData = e.response?.data as any; + const errorMsg = errorData?.details || e.response?.statusText || e.message; + return undefined; + } +} + +function parseRegistryFromImage(imageUrl: string): RegistryInfo { + const lower = imageUrl.toLowerCase(); + if (lower.includes('ghcr.io')) { + return { type: 'ghcr', registryUrl: 'https://ghcr.io', hostService: 'ghcr.io', tokenRealm: 'https://ghcr.io/token' }; + } + if (lower.includes('registry.gitlab.com')) { + return { type: 'gitlab', registryUrl: 'https://registry.gitlab.com', hostService: 'container_registry', tokenRealm: 'https://gitlab.com/jwt/auth' }; + } + if (lower.includes('docker.io') || lower.includes('index.docker.io') || !lower.includes('.')) { + return { type: 'dockerhub', registryUrl: 'https://registry-1.docker.io', hostService: 'registry.docker.io', tokenRealm: 'https://auth.docker.io/token' }; + } + + const hostMatch = imageUrl.match(/^([^\/]+)/); + const host = hostMatch ? hostMatch[1] : imageUrl; + return { + type: 'other', + registryUrl: `https://${host}`, + hostService: host, + tokenRealm: `https://${host}/v2/token` + }; +} + +function normalizeImageName(imageUrl: string, registry: RegistryInfo): string { + if (registry.type === 'dockerhub') { + if (!imageUrl.includes('/')) return `library/${imageUrl}`; + return imageUrl.replace(/^(docker\.io\/|index\.docker\.io\/)/, ''); + } + const registryHostname = new URL(registry.registryUrl).hostname; + return imageUrl.replace(new RegExp(`^${registryHostname}/`), ''); +} + +/** + * Check if a credential is relevant for the given image URL + */ +function isCredentialRelevant(imageUrl: string, credential: IRegistryCredential): boolean { + const lowerImageUrl = imageUrl.toLowerCase(); + + switch (credential.registryType) { + case "dockerhub": + // Docker Hub images don't have a hostname or use docker.io + return !lowerImageUrl.includes("/") || + lowerImageUrl.includes("docker.io") || + lowerImageUrl.includes("index.docker.io"); + + case "github": + return lowerImageUrl.includes("ghcr.io"); + + case "gitlab": + return lowerImageUrl.includes("registry.gitlab.com"); + + case "other": + // For custom registries, check if the credential name matches part of the URL + return credential.name ? lowerImageUrl.includes(credential.name.toLowerCase()) : false; + + default: + return false; + } +} diff --git a/api/src/utils/email/mailer.ts b/api/src/utils/email/mailer.ts new file mode 100644 index 0000000..4e8c4a8 --- /dev/null +++ b/api/src/utils/email/mailer.ts @@ -0,0 +1,41 @@ +import nodemailer from "nodemailer"; +import env from "../../config/env"; + +export const transporter = nodemailer.createTransport({ + host: env.SMTP_HOST, + port: Number(env.SMTP_PORT) || 587, + secure: false, + auth: { + user: env.SMTP_USER, + pass: env.SMTP_PASS, + }, +}); + +const FROM_ADDRESS = env.SMTP_FROM; + +export default async function sendMail({ + to, + subject, + html, + text, + from = FROM_ADDRESS, +}: { + to: string; + subject: string; + html?: string; + text?: string; + from?: string; +}) { + try { + await transporter.sendMail({ + from, + to, + subject, + html, + text, + }); + } catch (err) { + console.error("Failed to send email:", err); + throw new Error("Failed to send email."); + } +} diff --git a/api/src/utils/github/createGithubJWT.ts b/api/src/utils/github/createGithubJWT.ts new file mode 100644 index 0000000..1ed79e1 --- /dev/null +++ b/api/src/utils/github/createGithubJWT.ts @@ -0,0 +1,17 @@ +import jwt from "jsonwebtoken"; +import env from "../../config/env"; + +function createGitHubJWT() { + const now = Math.floor(Date.now() / 1000); + return jwt.sign( + { + iat: now - 60, + exp: now + 540, + iss: env.GITHUB_APP_ID, + }, + env.GITHUB_PRIVATE_KEY, + { algorithm: "RS256" } + ); +} + +export default createGitHubJWT; diff --git a/api/src/utils/github/getUserFromSession.ts b/api/src/utils/github/getUserFromSession.ts new file mode 100644 index 0000000..2f06eb6 --- /dev/null +++ b/api/src/utils/github/getUserFromSession.ts @@ -0,0 +1,13 @@ +import { Request } from "express"; +import User from "../../models/user.model"; +import type { Document } from "mongoose"; +import IUser from "../../types/user.types"; + +async function getUserFromSession( + req: Request +): Promise<(IUser & Document) | null> { + if (!req.user) return null; + return User.findById((req.user as any)._id); +} + +export default getUserFromSession; diff --git a/api/src/utils/handlers/asyncHandler.ts b/api/src/utils/handlers/asyncHandler.ts new file mode 100644 index 0000000..1c3d8fd --- /dev/null +++ b/api/src/utils/handlers/asyncHandler.ts @@ -0,0 +1,9 @@ +import { Request, Response, NextFunction, RequestHandler } from "express"; + +export default function asyncHandler( + fn: (...args: any[]) => Promise +): RequestHandler { + return (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +} diff --git a/api/src/utils/k8s/docker-file.ts b/api/src/utils/k8s/docker-file.ts new file mode 100644 index 0000000..fa7da63 --- /dev/null +++ b/api/src/utils/k8s/docker-file.ts @@ -0,0 +1,31 @@ +import { spawn } from "child_process"; +import path from "path"; +import fs from "fs/promises"; + +export default async function runDockerScript( + gitHubUrl: string +): Promise<{ dockerfile?: string; dockerCompose?: string; error?: string }> { + const scriptPath = path.resolve(__dirname, "../../../AI/docker.py"); + const outputPath = path.resolve(__dirname, "../../../AI/output"); + return new Promise((resolve, reject) => { + const python = spawn("python", ["-u", scriptPath, gitHubUrl]); + let stderr = ""; + python.stderr.on("data", (data) => { + stderr += data.toString(); + }); + python.on("close", async (code) => { + if (code !== 0 || stderr) { + return resolve({ error: stderr || `Python exited with code ${code}` }); + } + try { + const [dockerfile, dockerCompose] = await Promise.all([ + fs.readFile(path.join(outputPath, "Dockerfile"), "utf-8"), + fs.readFile(path.join(outputPath, "docker-compose.yml"), "utf-8"), + ]); + resolve({ dockerfile, dockerCompose }); + } catch (err) { + resolve({ error: "Failed to read Dockerfile or docker-compose.yml" }); + } + }); + }); +} diff --git a/api/src/utils/k8s/generators/generateDeployment.ts b/api/src/utils/k8s/generators/generateDeployment.ts new file mode 100644 index 0000000..fe665c2 --- /dev/null +++ b/api/src/utils/k8s/generators/generateDeployment.ts @@ -0,0 +1,188 @@ +import { dump } from "js-yaml"; +import stripDates from "../helpers/stripDates"; +import IApplication from "../../../types/application.types"; +import getEnvObject from "../helpers/getEnvObject"; +import toK8sResource from "../helpers/toK8sResource"; + +export default function generateDeployment( + sanitizedApp: any, + namespace: string +): string { + return [ + `apiVersion: apps/v1`, + `kind: Deployment`, + `metadata:`, + ` name: ${sanitizedApp.deploymentName || sanitizedApp.name}-deployment`, + ` namespace: ${namespace}`, + ` labels:`, + ` app: ${sanitizedApp.name}`, + ` deployment: ${sanitizedApp.deploymentName || sanitizedApp.name}`, + ` annotations:`, + ` keel.sh/policy: "force"`, + ` keel.sh/trigger: "poll"`, + ` keel.sh/container: "${sanitizedApp.name}"`, + ` keel.sh/match-tag: "true"`, + `spec:`, + ` replicas: 1`, + ` selector:`, + ` matchLabels:`, + ` app: ${sanitizedApp.name}`, + ` template:`, + ` metadata:`, + ` labels:`, + ` app: ${sanitizedApp.name}`, + ` deployment: ${sanitizedApp.deploymentName || sanitizedApp.name}`, + ` spec:`, + generateImagePullSecretsBlock(sanitizedApp), + generateNodeSelectorBlock(sanitizedApp), + ` containers:`, + ` - name: ${sanitizedApp.name}`, + ` image: ${sanitizedApp.imageUrl}:${sanitizedApp.imageTag}`, + generateEnvFromSecretBlock(sanitizedApp), + generateResourcesBlock(sanitizedApp.resources), + generatePortsBlock(sanitizedApp.ports), + generateVolumeMountsBlock(sanitizedApp.volumes), + generateCommandBlock(sanitizedApp.command), + generateArgsBlock(sanitizedApp.args), + generateProbeBlock("livenessProbe", sanitizedApp.livenessProbe), + generateProbeBlock("readinessProbe", sanitizedApp.readinessProbe), + generateAffinityBlock(sanitizedApp.affinity), + ` restartPolicy: Always`, + generateVolumesBlock(sanitizedApp.volumes), + generateTolerationsBlock(sanitizedApp.tolerations), + ] + .filter(Boolean) + .join("\n"); +} + +function generateAffinityBlock(affinity: any): string { + if (!affinity) return ""; + return ` affinity:\n${dump(stripDates(affinity), { + noRefs: true, + skipInvalid: true, + }) + .split("\n") + .map((line: string) => ` ${line}`) + .join("\n")}`; +} + +function generateArgsBlock(args: string[] | undefined): string { + if (!args?.length) return ""; + return ` args: ${JSON.stringify(args)}`; +} + +function generateCommandBlock(command: string[] | undefined): string { + if (!command?.length) return ""; + return ` command: ${JSON.stringify(command)}`; +} + +function generateEnvFromSecretBlock(sanitizedApp: IApplication): string { + const envObj = getEnvObject(sanitizedApp.env); + if (!envObj || Object.keys(envObj).length === 0) return ""; + return ` envFrom:\n - secretRef:\n name: ${sanitizedApp.name}-env-secret`; +} + +function generateImagePullSecretsBlock(sanitizedApp: IApplication): string { + if ( + !sanitizedApp.credentials || + !Array.isArray(sanitizedApp.credentials) || + sanitizedApp.credentials.length === 0 + ) { + return ""; + } + return ` imagePullSecrets:\n - name: ${sanitizedApp.name}-regcred`; +} + +function generateNodeSelectorBlock(sanitizedApp: any): string { + if ( + !sanitizedApp.nodeSelector || + Object.keys(sanitizedApp.nodeSelector).length === 0 + ) { + return ""; + } + + const yamlLines = Object.entries(sanitizedApp.nodeSelector).map( + ([key, value]) => ` ${key}: "${value}"` + ); + return ` nodeSelector:\n${yamlLines.join('\n')}`; +} + +function generatePortsBlock(ports: any[]): string { + if (!ports?.length) return ""; + return ( + ` ports:\n` + + ports + .map( + ( + p: { name?: string; containerPort: number; protocol?: string }, + idx: number + ) => + ` - name: port${idx}` + + `\n containerPort: ${p.containerPort}` + + `\n protocol: ${p.protocol || "TCP"}` + ) + .join("\n") + ); +} + +function generateProbeBlock( + type: "readinessProbe" | "livenessProbe", + probe: any +): string { + if (!probe) return ""; + return ` ${type}:\n${dump(stripDates(probe), { + noRefs: true, + skipInvalid: true, + }) + .split("\n") + .map((line) => ` ${line}`) + .join("\n")}`; +} + +function generateResourcesBlock(resources: any): string { + if (!resources) return ""; + return ` resources: + requests: + cpu: "${toK8sResource(resources.requests?.cpuMilli, "cpu")}" + memory: "${toK8sResource(resources.requests?.memoryMB, "memory")}" + limits: + cpu: "${toK8sResource(resources.limits?.cpuMilli, "cpu")}" + memory: "${toK8sResource(resources.limits?.memoryMB, "memory")}"`; +} + +function generateTolerationsBlock(tolerations: any[]): string { + if (!tolerations?.length) return ""; + return ` tolerations:\n${dump(stripDates(tolerations), { + noRefs: true, + skipInvalid: true, + }) + .split("\n") + .map((line) => ` ${line}`) + .join("\n")}`; +} + +function generateVolumeMountsBlock(volumes: any[]): string { + if (!volumes?.length) return ""; + return ( + ` volumeMounts:\n` + + volumes + .map( + (v: { name: string; mountPath: string }) => + ` - name: ${v.name}-pvc\n mountPath: ${v.mountPath}` + ) + .join("\n") + ); +} + +function generateVolumesBlock(volumes: any[]): string { + if (!volumes?.length) return ""; + return ( + ` volumes:\n` + + volumes + .map( + (v: any) => + ` - name: ${v.name}-pvc\n persistentVolumeClaim:\n claimName: ${v.name}-pvc` + ) + .join("\n") + ); +} diff --git a/api/src/utils/k8s/generators/generateImagePullSecret.ts b/api/src/utils/k8s/generators/generateImagePullSecret.ts new file mode 100644 index 0000000..7e97b4a --- /dev/null +++ b/api/src/utils/k8s/generators/generateImagePullSecret.ts @@ -0,0 +1,54 @@ +import type IApplication from "../../../types/application.types"; + +export default function generateImagePullSecret( + app: IApplication, + namespace: string +): string | undefined { + if ( + !app.credentials || + !Array.isArray(app.credentials) || + app.credentials.length === 0 + ) + return undefined; + // Only use the first credential for now (can be extended for multiple) + const cred = app.credentials[0]; + const auth = Buffer.from(`${cred.username}:${cred.token}`).toString("base64"); + // Determine registry server + let server = ""; + switch (cred.registryType) { + case "dockerhub": + server = "https://index.docker.io/v1/"; + break; + case "github": + server = "ghcr.io"; + break; + case "gitlab": + server = "registry.gitlab.com"; + break; + case "other": + default: + server = cred.name || ""; + break; + } + const dockerConfig = { + auths: { + [server]: { + auth, + username: cred.username, + password: cred.token, + }, + }, + }; + return ( + `apiVersion: v1\n` + + `kind: Secret\n` + + `metadata:\n` + + ` name: ${app.name}-regcred\n` + + ` namespace: ${namespace}\n` + + `type: kubernetes.io/dockerconfigjson\n` + + `data:\n` + + ` .dockerconfigjson: ${Buffer.from(JSON.stringify(dockerConfig)).toString( + "base64" + )}\n` + ); +} diff --git a/api/src/utils/k8s/generators/generateIngress.ts b/api/src/utils/k8s/generators/generateIngress.ts new file mode 100644 index 0000000..f858088 --- /dev/null +++ b/api/src/utils/k8s/generators/generateIngress.ts @@ -0,0 +1,71 @@ +import env from "../../../config/env"; + +export default function generateIngress( + sanitizedApp: any, + namespace: string +): string { + const ingressPorts = (sanitizedApp.ports || []).filter( + (p: any) => typeof p.subdomain === "string" && p.subdomain.trim() !== "" + ); + if (ingressPorts.length === 0) return ""; + const rules = ingressPorts + .map((p: any) => { + const host = p.subdomain.endsWith(".") + ? p.subdomain.slice(0, -1) + : p.subdomain; + return [ + ` - host: ${host}`, + ` http:`, + ` paths:`, + ` - path: /`, + ` pathType: Prefix`, + ` backend:`, + ` service:`, + ` name: ${ + sanitizedApp.serviceName || sanitizedApp.name + }-service`, + ` port:`, + ` number: ${p.servicePort || p.containerPort || 80}`, + ].join("\n"); + }) + .join("\n"); + // Collect all hosts for TLS + const hosts = ingressPorts.map((p: any) => + p.subdomain.endsWith(".") ? p.subdomain.slice(0, -1) : p.subdomain + ); + + // For development, skip cert-manager and TLS + const isDev = env.NODE_ENV === "development"; + const annotations = [ + ` kubernetes.io/ingress.class: traefik`, + !isDev ? ` cert-manager.io/cluster-issuer: letsencrypt-wildcard` : null, + ] + .filter(Boolean) + .join("\n"); + + const lines = [ + `---`, + `apiVersion: networking.k8s.io/v1`, + `kind: Ingress`, + `metadata:`, + ` name: ${sanitizedApp.name}-ingress`, + ` namespace: ${namespace}`, + ` labels:`, + ` app: ${sanitizedApp.name}`, + ` deployment: ${sanitizedApp.deploymentName}`, + ` annotations:`, + annotations, + `spec:`, + ` rules:`, + rules, + ]; + + if (!isDev) { + lines.push(` tls:`); + lines.push(` - hosts:`); + hosts.forEach((h: string) => lines.push(` - ${h}`)); + lines.push(` secretName: wildcard-tls`); + } + + return lines.join("\n"); +} diff --git a/api/src/utils/k8s/generators/generatePV.ts b/api/src/utils/k8s/generators/generatePV.ts new file mode 100644 index 0000000..6670590 --- /dev/null +++ b/api/src/utils/k8s/generators/generatePV.ts @@ -0,0 +1,37 @@ +import env from "../../../config/env"; +import path from "path"; + +export default function generatePV( + volume: any, + namespace: string, + userId: string, + appId: string +): string { + if (!volume.name || !volume.size) return ""; + // Use env var for root path, must be Linux-style absolute path + let rootPath = env.VOLUME_ROOT_PATH; + // Ensure rootPath is Linux-style absolute + if (!rootPath.startsWith("/")) { + throw new Error( + `VOLUME_ROOT_PATH must be a Linux-style absolute path (starts with /), got: ${rootPath}` + ); + } + // Always use posix.join for k8s hostPath + const hostPath = path.posix.join(rootPath, userId, appId, volume.name); + return [ + `apiVersion: v1`, + `kind: PersistentVolume`, + `metadata:`, + ` name: ${volume.name}-pv`, + ` labels:`, + ` app: ${namespace}`, + `spec:`, + ` capacity:`, + ` storage: ${volume.size}`, + ` accessModes: ["ReadWriteOnce"]`, + ` persistentVolumeReclaimPolicy: Retain`, + ` storageClassName: manual`, + ` hostPath:`, + ` path: ${hostPath}`, + ].join("\n"); +} diff --git a/api/src/utils/k8s/generators/generatePVC.ts b/api/src/utils/k8s/generators/generatePVC.ts new file mode 100644 index 0000000..d3dbf7d --- /dev/null +++ b/api/src/utils/k8s/generators/generatePVC.ts @@ -0,0 +1,17 @@ +export default function generatePVC(volume: any, namespace: string): string { + if (!volume.name || !volume.size) return ""; + return [ + `apiVersion: v1`, + `kind: PersistentVolumeClaim`, + `metadata:`, + ` name: ${volume.name}-pvc`, + ` namespace: ${namespace}`, + `spec:`, + ` accessModes: ["ReadWriteOnce"]`, + ` storageClassName: manual`, + ` resources:`, + ` requests:`, + ` storage: ${volume.size}`, + ` volumeName: ${volume.name}-pv`, + ].join("\n"); +} diff --git a/api/src/utils/k8s/generators/generateSecret.ts b/api/src/utils/k8s/generators/generateSecret.ts new file mode 100644 index 0000000..c39dcb8 --- /dev/null +++ b/api/src/utils/k8s/generators/generateSecret.ts @@ -0,0 +1,29 @@ +import IApplication from "../../../types/application.types"; +import getEnvObject from "../helpers/getEnvObject"; + +export default function generateSecret( + sanitizedApp: IApplication, + namespace: string +): string { + const envObj = getEnvObject(sanitizedApp.env); + const filtered = Object.entries(envObj).filter( + ([k, v]) => k && typeof v === "string" && v.length > 0 + ); + let data = filtered + .map( + ([key, value]) => + ` ${key}: ${Buffer.from(value, "utf8").toString("base64")}` + ) + .join("\n"); + if (!data) data = " # No environment variables provided\n"; + return [ + `apiVersion: v1`, + `kind: Secret`, + `metadata:`, + ` name: ${sanitizedApp.name}-env-secret`, + ` namespace: ${namespace}`, + `type: Opaque`, + `data:`, + data, + ].join("\n"); +} diff --git a/api/src/utils/k8s/generators/generateService.ts b/api/src/utils/k8s/generators/generateService.ts new file mode 100644 index 0000000..1d08a46 --- /dev/null +++ b/api/src/utils/k8s/generators/generateService.ts @@ -0,0 +1,45 @@ +export default function generateService( + sanitizedApp: any, + namespace: string +): string { + return [ + `apiVersion: v1`, + `kind: Service`, + `metadata:`, + ` name: ${sanitizedApp.serviceName || sanitizedApp.name}-service`, + ` namespace: ${namespace}`, + ` labels:`, + ` app: ${sanitizedApp.name}`, + ` deployment: ${sanitizedApp.deploymentName || sanitizedApp.name}`, + `spec:`, + ` selector:`, + ` app: ${sanitizedApp.name}`, + ` ports:`, + generateServicePortsBlock(sanitizedApp.ports), + ] + .filter(Boolean) + .join("\n"); +} + +function generateServicePortsBlock(ports: any[]): string { + if (!ports?.length) + return ` - protocol: TCP\n port: 80\n targetPort: 3000`; + return ports + .map( + ( + p: { + name?: string; + containerPort: number; + protocol?: string; + servicePort?: number; + }, + idx: number + ) => + ` - name: port${idx}\n protocol: ${ + p.protocol || "TCP" + }\n port: ${p.servicePort || p.containerPort}\n targetPort: ${ + p.containerPort + }` + ) + .join("\n"); +} diff --git a/api/src/utils/k8s/git_commit.ts b/api/src/utils/k8s/git_commit.ts new file mode 100644 index 0000000..867d6e2 --- /dev/null +++ b/api/src/utils/k8s/git_commit.ts @@ -0,0 +1,124 @@ +import createGitHubJWT from "../github/createGithubJWT"; +import axios from "axios"; + +const INSTALLATION_ID = process.env.GITHUB_INSTALLATION_ID!; + +const GITHUB_API = "https://api.github.com"; + +async function getInstallationToken(): Promise { + const jwtToken = createGitHubJWT(); + const res = await axios.post( + `${GITHUB_API}/app/installations/${INSTALLATION_ID}/access_tokens`, + {}, + { + headers: { + Authorization: `Bearer ${jwtToken}`, + Accept: "application/vnd.github+json", + }, + } + ); + return res.data.token; +} + +async function getDefaultBranch( + token: string, + owner: string, + repo: string +): Promise { + const res = await axios.get(`${GITHUB_API}/repos/${owner}/${repo}`, { + headers: { Authorization: `token ${token}` }, + }); + return res.data.default_branch; +} + +async function getBranchSHA( + token: string, + owner: string, + repo: string, + branch: string +): Promise { + const res = await axios.get( + `${GITHUB_API}/repos/${owner}/${repo}/git/ref/heads/${branch}`, + { + headers: { Authorization: `token ${token}` }, + } + ); + return res.data.object.sha; +} + +async function createBranch( + token: string, + owner: string, + repo: string, + newBranch: string, + sha: string +) { + await axios.post( + `${GITHUB_API}/repos/${owner}/${repo}/git/refs`, + { + ref: `refs/heads/${newBranch}`, + sha, + }, + { + headers: { Authorization: `token ${token}` }, + } + ); +} + +async function commitDockerfile( + token: string, + owner: string, + repo: string, + branch: string, + content: string +) { + const encoded = Buffer.from(content).toString("base64"); + await axios.put( + `${GITHUB_API}/repos/${owner}/${repo}/contents/Dockerfile`, + { + message: "Add Dockerfile via AI", + content: encoded, + branch, + }, + { + headers: { Authorization: `token ${token}` }, + } + ); +} + +async function createPR( + token: string, + owner: string, + repo: string, + branch: string, + base: string +) { + const res = await axios.post( + `${GITHUB_API}/repos/${owner}/${repo}/pulls`, + { + title: "Add Dockerfile (auto-generated)", + head: branch, + base, + body: "This PR adds a Dockerfile generated by our PaaS.", + }, + { + headers: { Authorization: `token ${token}` }, + } + ); + return res.data.html_url; +} + +export async function handleDockerfilePR( + owner: string, + repo: string, + dockerfileContent: string +): Promise { + const token = await getInstallationToken(); + const baseBranch = await getDefaultBranch(token, owner, repo); + const baseSha = await getBranchSHA(token, owner, repo, baseBranch); + const newBranch = `auto/dockerfile-${Date.now()}`; + await createBranch(token, owner, repo, newBranch, baseSha); + await commitDockerfile(token, owner, repo, newBranch, dockerfileContent); + const prUrl = await createPR(token, owner, repo, newBranch, baseBranch); + return prUrl; +} diff --git a/api/src/utils/k8s/helpers/getEnvObject.ts b/api/src/utils/k8s/helpers/getEnvObject.ts new file mode 100644 index 0000000..2994f66 --- /dev/null +++ b/api/src/utils/k8s/helpers/getEnvObject.ts @@ -0,0 +1,14 @@ +export default function getEnvObject(env: any): Record { + if (!env) return {}; + if (env instanceof Map) { + return Object.fromEntries(env.entries()); + } + if (typeof env.toObject === "function") { + return env.toObject(); + } + + if (typeof env === "object" && env !== null) { + return JSON.parse(JSON.stringify(env)); + } + return {}; +} diff --git a/api/src/utils/k8s/helpers/k8sHelpers.ts b/api/src/utils/k8s/helpers/k8sHelpers.ts new file mode 100644 index 0000000..732ff78 --- /dev/null +++ b/api/src/utils/k8s/helpers/k8sHelpers.ts @@ -0,0 +1,50 @@ +import env from "../../../config/env"; + +export function formatK8sName(base: string) { + return base + .toLowerCase() + .replace(/[^a-z0-9-]/g, "-") + .replace(/^-+|-+$/g, "") + .replace(/--+/g, "-"); +} + +export function getNamespace(userId: string, appName: string) { + return `ns-${formatK8sName(userId)}-${formatK8sName(appName)}`; +} + +export function getResourceName(type: string, appName: string) { + return `${type}-${formatK8sName(appName)}`; +} + +export const getBaseDomain = () => { + let domain = env.INGRESS_BASE_DOMAIN; + if (typeof domain === "string" && domain.startsWith(".")) + domain = domain.slice(1); + return domain; +}; + +export function buildIngressHost({ + username, + subdomain, +}: { + username: string; + subdomain?: string; +}) { + const baseDomain = getBaseDomain(); + if (typeof subdomain === "string" && subdomain.trim() !== "") { + return `${formatK8sName(subdomain)}.${baseDomain}`; + } + return `${formatK8sName(username)}.${baseDomain}`; +} + +export function buildSubdomainHost({ + subdomain, + username, +}: { + subdomain: string; + username: string; +}) { + return `${formatK8sName(subdomain)}-${formatK8sName( + username + )}.${getBaseDomain()}`; +} diff --git a/api/src/utils/k8s/helpers/portHelpers.ts b/api/src/utils/k8s/helpers/portHelpers.ts new file mode 100644 index 0000000..25e4a39 --- /dev/null +++ b/api/src/utils/k8s/helpers/portHelpers.ts @@ -0,0 +1,36 @@ +import { formatK8sName, getBaseDomain } from "./k8sHelpers"; + +interface Port { + subdomain?: string | null; + containerPort: number; + protocol: string; +} + +export function mapPorts(ports: Port[], username: string) { + const baseDomain = getBaseDomain(); + return ports.map((port: any, idx: number) => { + let subdomain = + typeof port.subdomain === "string" + ? port.subdomain.trim() + : port.subdomain !== undefined && port.subdomain !== null + ? String(port.subdomain).trim() + : ""; + if (subdomain) { + const fqdn = `${formatK8sName(subdomain)}.${formatK8sName( + username + )}.${baseDomain}`; + if ( + !subdomain.endsWith(baseDomain) && + !subdomain.endsWith(`.${baseDomain}`) + ) { + subdomain = fqdn; + } + } + return { + name: `port${idx}`, + containerPort: port.containerPort, + protocol: port.protocol, + subdomain, + }; + }); +} diff --git a/api/src/utils/k8s/helpers/resourceQuota.ts b/api/src/utils/k8s/helpers/resourceQuota.ts new file mode 100644 index 0000000..5a30d36 --- /dev/null +++ b/api/src/utils/k8s/helpers/resourceQuota.ts @@ -0,0 +1,116 @@ +export function parseResource(val: string | number | undefined) { + if (val === undefined || val === null || val === "") return 0; + if (typeof val === "number") return val; + if (typeof val !== "string") return 0; + if (val.endsWith("m")) return parseInt(val) / 1000; + if (val.endsWith("Mi")) return parseInt(val); + if (val.endsWith("Gi")) return parseInt(val) * 1024; + return parseFloat(val); +} + +interface ResourceQuota { + requests: { + cpuMilli?: string; + memoryMB?: string; + }; + limits: { + cpuMilli?: string; + memoryMB?: string; + }; +} + +interface CheckResourceQuotaResult { + allowed?: ResourceQuota; + usage?: ResourceQuota; + exceeded: boolean; +} + +export async function checkResourceQuota({ + resources, + owner, + req, +}: { + resources: ResourceQuota; + owner: string; + req: any; +}): Promise { + const userModel = await (await import("../../../models/user.model")).default + .findById(owner) + .populate("plan"); + if (userModel) { + let planResources: ResourceQuota = { requests: {}, limits: {} }; + if ( + userModel.plan && + typeof userModel.plan === "object" && + "resources" in userModel.plan + ) { + planResources = (userModel.plan as any).resources || {}; + } + const extra = userModel.extraResources || {}; + const allowed = { + requests: { + cpuMilli: ( + parseResource(planResources.requests?.cpuMilli) + + parseResource(extra.requests?.cpuMilli) + ).toString(), + memoryMB: ( + parseResource(planResources.requests?.memoryMB) + + parseResource(extra.requests?.memoryMB) + ).toString(), + }, + limits: { + cpuMilli: ( + parseResource(planResources.limits?.cpuMilli) + + parseResource(extra.limits?.cpuMilli) + ).toString(), + memoryMB: ( + parseResource(planResources.limits?.memoryMB) + + parseResource(extra.limits?.memoryMB) + ).toString(), + }, + }; + + const ApplicationModel = (await import("../../../models/application.model")) + .default; + const apps = await ApplicationModel.find({ + owner, + _id: { $ne: req.params.id }, + }); + const usage = { + requests: { cpu: 0, memory: 0 }, + limits: { cpu: 0, memory: 0 }, + }; + for (const app of apps) { + usage.requests.cpu += parseResource(app.resources?.requests?.cpuMilli); + usage.requests.memory += parseResource(app.resources?.requests?.memoryMB); + usage.limits.cpu += parseResource(app.resources?.limits?.cpuMilli); + usage.limits.memory += parseResource(app.resources?.limits?.memoryMB); + } + + usage.requests.cpu += parseResource(resources.requests?.cpuMilli); + usage.requests.memory += parseResource(resources.requests?.memoryMB); + usage.limits.cpu += parseResource(resources.limits?.cpuMilli); + usage.limits.memory += parseResource(resources.limits?.memoryMB); + + const usageString: ResourceQuota = { + requests: { + cpuMilli: usage.requests.cpu.toString(), + memoryMB: usage.requests.memory.toString(), + }, + limits: { + cpuMilli: usage.limits.cpu.toString(), + memoryMB: usage.limits.memory.toString(), + }, + }; + + if ( + usage.requests.cpu > parseFloat(allowed.requests.cpuMilli) || + usage.requests.memory > parseFloat(allowed.requests.memoryMB) || + usage.limits.cpu > parseFloat(allowed.limits.cpuMilli) || + usage.limits.memory > parseFloat(allowed.limits.memoryMB) + ) { + return { allowed, usage: usageString, exceeded: true }; + } + } + return { exceeded: false }; +} diff --git a/api/src/utils/k8s/helpers/stripDates.ts b/api/src/utils/k8s/helpers/stripDates.ts new file mode 100644 index 0000000..8de422f --- /dev/null +++ b/api/src/utils/k8s/helpers/stripDates.ts @@ -0,0 +1,24 @@ +export default function stripDates(obj: any): any { + const seen = new WeakSet(); + + function internalStrip(o: any): any { + if (o && typeof o === "object") { + if (seen.has(o)) return o; + seen.add(o); + } + + if (Array.isArray(o)) return o.map(internalStrip); + if (o && typeof o === "object") { + const clean: Record = {}; + for (const key in o) { + const val = o[key]; + clean[key] = + val instanceof Date ? val.toISOString() : internalStrip(val); + } + return clean; + } + return o; + } + + return internalStrip(obj); +} diff --git a/api/src/utils/k8s/helpers/toK8sResource.ts b/api/src/utils/k8s/helpers/toK8sResource.ts new file mode 100644 index 0000000..1dd2377 --- /dev/null +++ b/api/src/utils/k8s/helpers/toK8sResource.ts @@ -0,0 +1,30 @@ +export default function toK8sResource( + val: string | number | undefined, + type: "cpu" | "memory" +): string { + if (val === undefined || val === null || val === "") + return type === "cpu" ? "0m" : "0Mi"; + if (typeof val === "number") { + if (type === "cpu") { + // If value is less than 1, treat as millicores (e.g., 20 -> 20m) + if (val < 1) return `${Math.round(val * 1000)}m`; + return `${val}m`; + } + if (type === "memory") return `${val}Mi`; + } + if (typeof val === "string") { + // If already ends with m, Mi, Gi, etc., return as is + if (type === "cpu" && /m$/.test(val)) return val; + if (type === "memory" && /(Mi|Gi)$/.test(val)) return val; + // If it's a plain number string, add suffix + if (/^\d+$/.test(val)) { + return type === "cpu" ? `${val}m` : `${val}Mi`; + } + // If it's a float string for cpu, convert to millicores + if (type === "cpu" && /^\d*\.\d+$/.test(val)) { + return `${Math.round(parseFloat(val) * 1000)}m`; + } + return val; + } + return type === "cpu" ? "0m" : "0Mi"; +} diff --git a/api/src/utils/k8s/k8sManifests.ts b/api/src/utils/k8s/k8sManifests.ts new file mode 100644 index 0000000..5caa617 --- /dev/null +++ b/api/src/utils/k8s/k8sManifests.ts @@ -0,0 +1,89 @@ +import type IApplication from "../../types/application.types"; +import generateDEployment from "./generators/generateDeployment"; +import generateService from "./generators/generateService"; +import generateSecret from "./generators/generateSecret"; +import generateImagePullSecret from "./generators/generateImagePullSecret"; +import generatePVC from "./generators/generatePVC"; +import generatePV from "./generators/generatePV"; +import stripDates from "./helpers/stripDates"; +import generateIngress from "./generators/generateIngress"; + +export default function generateK8sManifests( + app: IApplication +): Record { + // Sanitize app object + const sanitizedApp = stripDates(app); + const namespace = app.namespace || "default"; + const userId = (app.owner as any)?.toString?.() || app.owner; + const appId = (app._id as any)?.toString?.() || app._id; + + // Auto-generate a single volume if storage is set in resources + let autoVolume = null; + let storageGB = 0; + if ( + app.resources?.requests?.storageGB && + app.resources.requests.storageGB > 0 + ) { + storageGB = app.resources.requests.storageGB; + } else if ( + app.resources?.limits?.storageGB && + app.resources.limits.storageGB > 0 + ) { + storageGB = app.resources.limits.storageGB; + } + if (storageGB > 0) { + autoVolume = { + name: `${app.name}-data`, + mountPath: "/data", + size: `${storageGB}Gi`, + accessModes: ["ReadWriteOnce"], + storageClassName: "manual", + readOnly: false, + type: "pvc", + }; + } + // Only use the auto-generated volume if present + const volumes = autoVolume ? [autoVolume] : []; + + // Generate all manifests + const deploymentYaml = generateDEployment( + { ...sanitizedApp, volumes }, + namespace + ); + const serviceYaml = generateService(sanitizedApp, namespace); + const ingressYaml = generateIngress(sanitizedApp, namespace); + const secretYaml = generateSecret(sanitizedApp, namespace); + const imagePullSecretYaml = + typeof generateImagePullSecret === "function" + ? generateImagePullSecret(sanitizedApp, namespace) + : undefined; + + // Generate PV and PVC manifests for persistent volumes + const pvManifests = volumes + .map((v) => generatePV(v, namespace, userId, appId)) + .filter((yaml) => yaml); + const pvcManifests = volumes + .map((v) => generatePVC(v, namespace)) + .filter((yaml) => yaml); + + // Compose output + const manifests: Record = { + deployment: (deploymentYaml || "") + "\n", + service: (serviceYaml || "") + "\n", + ingress: (ingressYaml || "") + "\n", + secret: (secretYaml || "") + "\n", + }; + + // Only add imagepullsecret if it exists + if (imagePullSecretYaml) { + manifests["imagepullsecret"] = imagePullSecretYaml + "\n"; + } + + if (pvManifests.length) { + manifests["pvs"] = pvManifests.join("\n---\n") + "\n"; + } + if (pvcManifests.length) { + manifests["pvcs"] = pvcManifests.join("\n---\n") + "\n"; + } + return manifests; +} diff --git a/api/src/utils/k8sPvcManager.ts b/api/src/utils/k8sPvcManager.ts new file mode 100644 index 0000000..0097d34 --- /dev/null +++ b/api/src/utils/k8sPvcManager.ts @@ -0,0 +1,40 @@ +import { exec } from "child_process"; + +/** + * Checks if a PVC exists in the given namespace. + * @param namespace Kubernetes namespace + * @param pvcName Name of the PVC + * @returns Promise + */ +export function pvcExists(namespace: string, pvcName: string): Promise { + return new Promise((resolve) => { + exec( + `kubectl get pvc ${pvcName} -n ${namespace}`, + (err) => { + if (err) return resolve(false); + return resolve(true); + } + ); + }); +} + +/** + * Creates a PVC from a manifest file if it does not already exist. + * @param namespace Kubernetes namespace + * @param pvcName Name of the PVC + * @param manifestPath Path to the PVC manifest YAML file + * @returns Promise true if created, false if already existed + */ +export async function createPvcIfNotExists(namespace: string, pvcName: string, manifestPath: string): Promise { + const exists = await pvcExists(namespace, pvcName); + if (exists) return false; + return new Promise((resolve, reject) => { + exec( + `kubectl apply -f ${manifestPath} -n ${namespace}`, + (err, stdout, stderr) => { + if (err) return reject(stderr || err.message); + resolve(true); + } + ); + }); +} \ No newline at end of file diff --git a/api/src/utils/logging/logger.ts b/api/src/utils/logging/logger.ts new file mode 100644 index 0000000..0ecaf1e --- /dev/null +++ b/api/src/utils/logging/logger.ts @@ -0,0 +1,49 @@ +import fs from "fs"; +import path from "path"; + +export type LogType = "success" | "error" | "info" | "warning"; + +interface LogOptions { + type: LogType; + message: string; + meta?: any; +} + +let logStream: fs.WriteStream | null = null; +const logsDir = path.resolve(__dirname, "../../logs"); +if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); +} +const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); +const logFilePath = path.join(logsDir, `server-${timestamp}.log`); +logStream = fs.createWriteStream(logFilePath, { flags: "a" }); + +export default function log({ type, message, meta }: LogOptions) { + const colorMap: Record = { + success: "\x1b[32m", // green + error: "\x1b[31m", // red + info: "\x1b[36m", // cyan + warning: "\x1b[33m", // yellow + }; + const reset = "\x1b[0m"; + const prefix = `[${type.toUpperCase()}]`; + const time = new Date().toISOString(); + const logMsg = `${colorMap[type]}${prefix}${reset} ${message}`; + const logLine = `${time} ${prefix} ${message}${ + meta ? ` | meta: ${JSON.stringify(meta, null, 2)}` : "" + }`; + if (type === "error") { + console.error(logMsg, meta || ""); + } else if (type === "warning") { + console.warn(logMsg, meta || ""); + } else { + console.log(logMsg, meta || ""); + } + if (logStream) { + logStream.write(logLine + "\n"); + } +} + +export function formatNotification(message: string, type: LogType = "info") { + return { message, type }; +} diff --git a/api/src/utils/populateBasePlan.ts b/api/src/utils/populateBasePlan.ts new file mode 100644 index 0000000..78f1522 --- /dev/null +++ b/api/src/utils/populateBasePlan.ts @@ -0,0 +1,31 @@ +import Plan from "../models/plan.model"; +import log from "./logging/logger"; + +/** + * Checks if the plans collection is empty and populates it with a base plan if needed. + */ +export async function populateBasePlanIfEmpty() { + const planCount = await Plan.countDocuments(); + if (planCount === 0) { + await Plan.create({ + name: "Base Plan", + description: "Default base plan automatically created on first start.", + resources: { + requests: { + cpuMilli: 15, // 15 m + memoryMB: 32, // 32 MB + storageGB: 1, // 1 GB + }, + limits: { + cpuMilli: 20, // 20 m + memoryMB: 64, // 64 MB + storageGB: 1, // 1 GB + }, + }, + isDefault: true, + price: 0, + isActive: true, + }); + log({ type: "info", message: "Base plan created as default." }); + } +} diff --git a/api/tsconfig.json b/api/tsconfig.json new file mode 100644 index 0000000..67ba70f --- /dev/null +++ b/api/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/types/**/*.d.ts"] +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0052419 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: "3.8" + +services: + kargo-backend: + image: ghcr.io/gdgvit/kargo-backend:latest + # For local development, comment the above line and uncomment the build context below: + # build: . + ports: + - "5000:5000" + environment: + - NODE_ENV=production + env_file: + - .env + restart: unless-stopped diff --git a/example.env b/example.env new file mode 100644 index 0000000..d542370 --- /dev/null +++ b/example.env @@ -0,0 +1,55 @@ +# Node environment +NODE_ENV= + +# MongoDB connection string +MONGO_URI= + +# The URL of the frontend application +FRONTEND_URL= + +# Secret keys for session management and NextAuth.js +SESSION_SECRET= + +# NextAuth.js secret for signing and encrypting session tokens +NEXTAUTH_SECRET= + +# Google OAuth credentials +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +# GitHub OAuth credentials +# Note: Do not confuse these with GitHub App client secrets, which are used for GitHub Apps authentication. OAuth credentials are specifically for standard OAuth flows, not for GitHub Apps. +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +# GitHub App credentials +GITHUB_APP_ID= +GITHUB_APP_SLUG= +GITHUB_PRIVATE_KEY= + +# SMTP server configuration for sending emails +SMTP_HOST= +SMTP_PORT= +SMTP_USER= +SMTP_PASS= +SMTP_FROM= +CUSTOM_DOMAIN= + +# Kubernetes Ingress +INGRESS_BASE_DOMAIN= + +# Directory to store generated Kubernetes manifests +MANIFESTS_DIR= + +# GROQ +GROQ= + +# Razorpay +RAZORPAY_KEY_ID= +RAZORPAY_KEY_SECRET= + +# Prometheus URL for metrics +PROMETHEUS_URL= # Use http://localhost:9090 or your prometheus url + +# Persistent volume root directory +VOLUME_ROOT_PATH= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..25e489f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,304 @@ +{ + "name": "kargo-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "kargo-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "razorpay": "^2.9.6" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/razorpay": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/razorpay/-/razorpay-2.9.6.tgz", + "integrity": "sha512-zsHAQzd6e1Cc6BNoCNZQaf65ElL6O6yw0wulxmoG5VQDr363fZC90Mp1V5EktVzG45yPyNomNXWlf4cQ3622gQ==", + "license": "MIT", + "dependencies": { + "axios": "^1.6.8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c3d1f95 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "kargo-backend", + "version": "1.0.0", + "description": "

\r \r \t\"GDSC\r \r \t

< Insert Project Title Here >

\r \t

< Insert Project Description Here >

\r

", + "main": "index.js", + "scripts": { + "dev": "cd api && npm run dev" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/GDGVIT/kargo-backend.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "bugs": { + "url": "https://github.com/GDGVIT/kargo-backend/issues" + }, + "homepage": "https://github.com/GDGVIT/kargo-backend#readme", + "dependencies": { + "razorpay": "^2.9.6" + } +}