Full Code of sahat/hackathon-starter for AI

master 7453b92b4c78 cached
144 files
1.7 MB
553.3k tokens
85 symbols
1 requests
Download .txt
Showing preview only (1,814K chars total). Download the full file or copy to clipboard to get everything.
Repository: sahat/hackathon-starter
Branch: master
Commit: 7453b92b4c78
Files: 144
Total size: 1.7 MB

Directory structure:
gitextract_js3nbrx3/

├── .gitattributes
├── .github/
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── build.yml
│       └── dependabot-automerge.yml
├── .gitignore
├── .husky/
│   └── pre-commit
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── PROD_CHECKLIST.md
├── README.md
├── SECURITY.md
├── app.js
├── config/
│   ├── flash.js
│   ├── morgan.js
│   ├── nodemailer.js
│   ├── passport.js
│   └── token-revocation.js
├── controllers/
│   ├── ai-agent.js
│   ├── ai.js
│   ├── api.js
│   ├── contact.js
│   ├── home.js
│   ├── user.js
│   └── webauthn.js
├── eslint.config.mjs
├── models/
│   ├── Session.js
│   └── User.js
├── package.json
├── patches/
│   ├── passport+0.7.0.patch
│   ├── passport-oauth1+1.3.0.patch
│   └── passport-oauth2+1.8.0.patch
├── public/
│   ├── css/
│   │   └── main.scss
│   ├── js/
│   │   ├── lib/
│   │   │   └── .gitkeep
│   │   └── main.js
│   ├── privacy-policy.html
│   └── terms-of-use.html
├── test/
│   ├── TESTING.md
│   ├── app-links.test.js
│   ├── app.test.js
│   ├── auth.opt.test.js
│   ├── contact.test.js
│   ├── docs-links.test.js
│   ├── e2e/
│   │   ├── chart.e2e.test.js
│   │   ├── foursquare.e2e.test.js
│   │   ├── giphy.e2e.test.js
│   │   ├── google-maps.e2e.test.js
│   │   ├── here-maps.e2e.test.js
│   │   ├── llm-classifier.e2e.test.js
│   │   ├── lob.e2e.test.js
│   │   ├── nyt.e2e.test.js
│   │   ├── openai-moderation.e2e.test.js
│   │   ├── rag.e2e.test.js
│   │   ├── trakt.e2e.test.js
│   │   └── twilio.e2e.test.js
│   ├── e2e-nokey/
│   │   ├── github-api.e2e.test.js
│   │   ├── lastfm.e2e.test.js
│   │   ├── pubchem.e2e.test.js
│   │   ├── scraping.e2e.test.js
│   │   ├── upload.e2e.test.js
│   │   └── wikipedia.e2e.test.js
│   ├── fixtures/
│   │   ├── GET_https%3A%2F%2Fapi.giphy.com%2Fv1%2Fgifs%2Fsearch%3Fq%3DHappy%26limit%3D20%26offset%3D0%26rating%3Dg%26lang%3Den.json
│   │   ├── GET_https%3A%2F%2Fapi.giphy.com%2Fv1%2Fgifs%2Fsearch%3Fq%3Dfunny%2Bcat%26limit%3D20%26offset%3D0%26rating%3Dg%26lang%3Den.json
│   │   ├── GET_https%3A%2F%2Fapi.github.com%2Frepos%2Fsahat%2Fhackathon-starter%2Fstargazers%3Fper_page%3D10.json
│   │   ├── GET_https%3A%2F%2Fapi.github.com%2Frepos%2Fsahat%2Fhackathon-starter.json
│   │   ├── GET_https%3A%2F%2Fapi.nytimes.com%2Fsvc%2Fbooks%2Fv3%2Flists%2Fcurrent%2Fyoung-adult-hardcover.json.json
│   │   ├── GET_https%3A%2F%2Fapi.trakt.tv%2Fmovies%2Fmercy-2026%3Fextended%3Dfull%252Cimages.json
│   │   ├── GET_https%3A%2F%2Fapi.trakt.tv%2Fmovies%2Ftrending%3Flimit%3D6%26extended%3Dimages.json
│   │   ├── GET_https%3A%2F%2Fen.wikipedia.org%2Fw%2Fapi.php%3Faction%3Dparse%26format%3Djson%26origin%3D_%26page%3DNode.js%26prop%3Dsections.json
│   │   ├── GET_https%3A%2F%2Fen.wikipedia.org%2Fw%2Fapi.php%3Faction%3Dquery%26format%3Djson%26origin%3D_%26list%3Dsearch%26srsearch%3Djavascript%26srlimit%3D10.json
│   │   ├── GET_https%3A%2F%2Fen.wikipedia.org%2Fw%2Fapi.php%3Faction%3Dquery%26format%3Djson%26origin%3D_%26prop%3Dextracts%26explaintext%3D1%26titles%3DNode.js%26exintro%3D1.json
│   │   ├── GET_https%3A%2F%2Fen.wikipedia.org%2Fw%2Fapi.php%3Faction%3Dquery%26format%3Djson%26origin%3D_%26prop%3Dpageimages%257Cpageterms%26titles%3DNode.js%26pithumbsize%3D400.json
│   │   ├── GET_https%3A%2F%2Fplaces-api.foursquare.com%2Fplaces%2F427ea800f964a520b1211fe3.json
│   │   ├── GET_https%3A%2F%2Fplaces-api.foursquare.com%2Fplaces%2Fsearch%3Fll%3D47.609657%252C-122.342148%26limit%3D10.json
│   │   ├── GET_https%3A%2F%2Fpubchem.ncbi.nlm.nih.gov%2Frest%2Fpug%2Fcompound%2Fcid%2F2244%2FJSON.json
│   │   ├── GET_https%3A%2F%2Fpubchem.ncbi.nlm.nih.gov%2Frest%2Fpug%2Fcompound%2Fcid%2F2244%2Fsynonyms%2FJSON.json
│   │   ├── GET_https%3A%2F%2Fpubchem.ncbi.nlm.nih.gov%2Frest%2Fpug_view%2Fdata%2Fcompound%2F2244%2FJSON%3Fheading%3DSafety%2Band%2BHazards.json
│   │   ├── GET_https%3A%2F%2Fpubchem.ncbi.nlm.nih.gov%2Frest%2Fpug_view%2Fdata%2Fcompound%2F2244%2FJSON%3Fheading%3DUse%2Band%2BManufacturing.json
│   │   ├── GET_https%3A%2F%2Fwww.alphavantage.co%2Fquery%3Ffunction%3DTIME_SERIES_DAILY%26symbol%3DMSFT%26outputsize%3Dcompact.json
│   │   ├── POST_https%3A%2F%2Fapi.openai.com%2Fv1%2Fmoderations_624f7df3dc5f.json
│   │   ├── POST_https%3A%2F%2Fapi.openai.com%2Fv1%2Fmoderations_c6b4d54f3bd4.json
│   │   └── fixture_manifest.json
│   ├── flash.test.js
│   ├── models.test.js
│   ├── morgan.test.js
│   ├── nodemailer.test.js
│   ├── passport.test.js
│   ├── playwright.config.js
│   ├── token-revocation.test.js
│   ├── tools/
│   │   ├── fixture-helpers.js
│   │   ├── playwright-start-and-log.js
│   │   ├── server-axios-fixtures.js
│   │   ├── server-fetch-fixtures.js
│   │   ├── simple-link-image-check.js
│   │   └── start-with-memory-db.js
│   └── webauthn.test.js
└── views/
    ├── account/
    │   ├── forgot.pug
    │   ├── login.pug
    │   ├── profile.pug
    │   ├── reset.pug
    │   ├── signup.pug
    │   ├── totp-setup.pug
    │   ├── two-factor.pug
    │   ├── webauthn-login.pug
    │   └── webauthn-register.pug
    ├── ai/
    │   ├── ai-agent.pug
    │   ├── index.pug
    │   ├── llm-camera.pug
    │   ├── llm-classifier.pug
    │   ├── openai-moderation.pug
    │   └── rag.pug
    ├── api/
    │   ├── chart.pug
    │   ├── facebook.pug
    │   ├── foursquare.pug
    │   ├── giphy.pug
    │   ├── github.pug
    │   ├── google-drive.pug
    │   ├── google-maps.pug
    │   ├── google-sheets.pug
    │   ├── here-maps.pug
    │   ├── index.pug
    │   ├── lastfm.pug
    │   ├── lob.pug
    │   ├── nyt.pug
    │   ├── paypal.pug
    │   ├── pubchem.pug
    │   ├── quickbooks.pug
    │   ├── scraping.pug
    │   ├── steam.pug
    │   ├── stripe.pug
    │   ├── trakt.pug
    │   ├── tumblr.pug
    │   ├── twilio.pug
    │   ├── twitch.pug
    │   ├── upload.pug
    │   └── wikipedia.pug
    ├── contact.pug
    ├── home.pug
    ├── layout.pug
    └── partials/
        ├── flash.pug
        ├── footer.pug
        └── header.pug

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
* text=auto eol=lf


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
<!-- IMPORTANT: maintainers may close PRs that fail the checks below without review. -->

## Checklist

- [ ] I acknowledge that submissions that include copy-paste of AI-generated content taken at face value (PR text, code, commit message, documentation, etc.) most likely have errors and hence will be rejected entirely and marked as spam or invalid
- [ ] I manually tested the change with a running instance, DB, and valid API keys where applicable
- [ ] Added/updated tests if the existing tests do not cover this change
- [ ] README or other relevant docs are updated
- [ ] `--no-verify` was not used for the commit(s)
- [ ] `npm run lint` passed locally without any errors
- [ ] `npm test` passed locally without any errors
- [ ] `npm run test:e2e:replay` passed locally without any errors
- [ ] `npm run test:e2e:custom -- --project=chromium-nokey-live` passed locally without any errors
- [ ] PR diff does not include unrelated changes
- [ ] PR title follows Conventional Commits — https://www.conventionalcommits.org/en

## Description

<!-- A short summary (Conventional Commits-style preferred).  -->

<!-- Fixes: issue link -->

## Screenshots of UI changes (browser) and logs/test results (console, terminal, shell, cmd)


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: npm
    directory: '/'
    schedule:
      interval: 'daily'
    target-branch: 'master'
    open-pull-requests-limit: 10
    versioning-strategy: increase
    commit-message:
      prefix: 'chore'
      include: 'scope'
    groups:
      major-updates:
        update-types: ['major']
      minor-updates:
        update-types: ['minor']
      patch-updates:
        update-types: ['patch']

  - package-ecosystem: 'github-actions'
    directory: '/'
    schedule:
      interval: 'monthly'
    target-branch: 'master'
    open-pull-requests-limit: 3
    commit-message:
      prefix: 'chore'
      include: 'scope'


================================================
FILE: .github/workflows/build.yml
================================================
name: Node.js CI

on:
  push:
    branches: ['master']
  pull_request:
    branches: ['master']

permissions:
  contents: read
  pull-requests: read

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: ${{ matrix.os }}
    env:
      RUN_E2E: ${{ vars.RUN_E2E }} # from repository settings -> Actions -> Variables
    strategy:
      matrix:
        node-version: [24.x]
        os: [ubuntu-latest, windows-latest]
    steps:
      - uses: actions/checkout@v6
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v6
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm install
      - run: npm run lint-check
      - run: npm run test

      # For testing in Windows CI, we need to limit the path to exclude the additional executables
      # that the default github runner has, but are not on a vanilla Windows OS installation.
      - if: ${{ (env.RUN_E2E == 'true' || github.repository == 'sahat/hackathon-starter') && matrix.os == 'windows-latest' }}
        env:
          PATH: 'C:\Windows\System32;C:\Windows'
        run: npm run test:e2e:replay

        # if not Windows, run normally
      - if: ${{ (env.RUN_E2E == 'true' || github.repository == 'sahat/hackathon-starter') && matrix.os != 'windows-latest' }}
        run: npm run test:e2e:replay

      - name: Upload tmp as an artifact (Playwrite artifacts, code coverage report, etc)
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: tmp-artifacts-${{ matrix.os }}-${{ github.job }}-${{ github.run_id }}
          path: tmp/**


================================================
FILE: .github/workflows/dependabot-automerge.yml
================================================
name: Dependabot Automerge

on:
  workflow_run:
    workflows: ['Node.js CI']
    types: [completed]

permissions:
  contents: write
  pull-requests: write

jobs:
  dependabot-automerge:
    if: >
      github.event.workflow_run.conclusion == 'success' &&
      github.event.workflow_run.event == 'pull_request' &&
      github.event.workflow_run.actor.login == 'dependabot[bot]'
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - name: Checkout
        uses: actions/checkout@v6
      - name: Automerge Dependabot PRs if all checks have passed
        shell: bash
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUM: ${{ fromJSON(toJson(github.event.workflow_run.pull_requests))[0].number }}
          REPO: ${{ github.repository }}
        run: |
          echo "Attempting to merge PR #${PR_NUM} in ${REPO}"
          gh pr merge "$PR_NUM" --squash --admin

  Sync-patches-after-dependabot-automerge:
    needs: [dependabot-automerge]
    runs-on: ubuntu-latest
    env:
      RUN_E2E: ${{ vars.RUN_E2E }} # from repository settings -> Actions -> Variables
    permissions:
      contents: write
      pull-requests: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6
        with:
          ref: master

      - name: Set up Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 'lts/*'
          cache: 'npm'

      - name: Rename patch-package files to match current versions
        id: rename-patches
        shell: bash
        run: |
          shopt -s nullglob

          get_version() {
            jq -r ".dependencies[\"$1\"] // .devDependencies[\"$1\"]" package.json
          }

          CHANGED=0

          for PATCH in patches/*.patch; do
            BASE=$(basename "$PATCH" .patch)

            NAME_WITHOUT_VERSION="${BASE%+*}"
            if [[ "$NAME_WITHOUT_VERSION" == @*+* ]]; then
              PACKAGE="${NAME_WITHOUT_VERSION/+//}"
            else
              PACKAGE="$NAME_WITHOUT_VERSION"
            fi

            VERSION=$(get_version "$PACKAGE")

            if [ "$VERSION" == "null" ]; then
              echo "Skipping $PACKAGE — not found in package.json"
              continue
            fi

            VERSION="${VERSION#^}"
            NEW_NAME="$(echo "$PACKAGE" | sed 's|/|+|g')+${VERSION}.patch"

            if [ "$BASE.patch" != "$NEW_NAME" ]; then
              echo "Renaming $BASE.patch -> $NEW_NAME"
              git mv "$PATCH" "patches/$NEW_NAME"
              CHANGED=1
            fi
          done

          # Expose whether any files changed as a step output so it can be safely
          # referenced by later step `if` conditions without static analyzer warnings.
          echo "changed=$CHANGED" >> $GITHUB_OUTPUT

      - name: Install dependencies
        if: ${{ steps.rename-patches.outputs.changed == '1' }}
        run: npm ci

      - name: Run tests
        if: ${{ steps.rename-patches.outputs.changed == '1' }}
        run: npm test

      - name: Run e2e tests
        if: ${{ steps.rename-patches.outputs.changed == '1' && (env.RUN_E2E == 'true' || github.repository == 'sahat/hackathon-starter') }}
        run: npm run test:e2e:replay

      - name: Run e2e tests that don't require API keys against live APIs
        if: ${{ steps.rename-patches.outputs.changed == '1' && (env.RUN_E2E == 'true' || github.repository == 'sahat/hackathon-starter') }}
        run: npm run test:e2e:custom -- --project=chromium-nokey-live

      - name: Commit and push patch renames
        if: ${{ steps.rename-patches.outputs.changed == '1' }}
        run: |
          git config user.name "github-actions"
          git config user.email "github-actions@github.com"
          git add patches/
          git commit -m "chore: sync patch-package filenames with current versions"
          git push


================================================
FILE: .gitignore
================================================
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
*.swp

pids
logs
results
tmp

# Optional npm cache directory
.npm

#Build
public/css/main.css
.nyc_output/*

# API keys and secrets
.env
.env.example
test/.env.test

# Dependency directory
node_modules
bower_components

# Uploads
uploads

# Ingestion folders
rag_input

# Editors
.idea
.vscode
*.iml
modules.xml
*.ipr

# Folder config file
Desktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# OS metadata
.DS_Store
Thumbs.db
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent


================================================
FILE: .husky/pre-commit
================================================
#!/bin/sh

# Save the list of currently staged files
STAGED_FILES=$(git diff --cached --name-only)

# Check for staged files with unstaged modifications
MODIFIED_FILES=$(git diff --name-only)

# Find files that overlap between staged and modified without using process substitution
CONFLICTING_FILES=""
for file in $STAGED_FILES; do
  if echo "$MODIFIED_FILES" | grep -qx "$file"; then
    CONFLICTING_FILES="$CONFLICTING_FILES$file\n"
  fi
done

# Abort if there are conflicts
if [ -n "$CONFLICTING_FILES" ]; then
  echo "Error: The following staged files have unstaged modifications, which can cause issues with the pre-commit eslint fix and prettier rewrite execution:"
  echo -e "$CONFLICTING_FILES" # Use -e for newline interpretation in echo
  echo "Please stage the changes or reset them before committing."
  echo "If this is a temporary local commit, you can also use the --no-verify flag to bypass the pre-commit test and linting. i.e. 'git commit --no-verify'"
  exit 1 # Abort commit
fi

# Run tests and linting
npm test
npm run lint

# Re-stage files after lint fixes (only staged files)
# Use a portable alternative for xargs
echo "$STAGED_FILES" | while IFS= read -r file; do
  if [ -f "$file" ]; then
    git add "$file"
  fi
done


================================================
FILE: .prettierignore
================================================
# Ignore artifacts:
build
coverage


================================================
FILE: .prettierrc
================================================
{
  "plugins": ["@prettier/plugin-pug"],
  "singleQuote": true,
  "printWidth": 300
}


================================================
FILE: CHANGELOG.md
================================================
# Changelog

---

### 10.0.0 (February 08, 2026)

New AI and Integration Features

- AI: AI Agent (ReAct: Reasoning+Acting) boilerplate with LangChain as a starting point for AI Agent development with support for:
  - Tool execution with automatic retry middleware for transient failures
  - MongoDB session persistence for chat history for authenticated users
  - Input guardrails for safety against prompt injection/jailbreak (Llama Guard 4)
  - Conversation summarization for long conversations to stay within context limits
  - Real-time streaming for live response chat experience using Server-Sent Events (SSE)
  - Streaming of the Agent's internal chatter, tool calls, etc., for debugging
- AI: RAG boilerplate (LangChain, Huggingface, Groq (Llama 3.3), MongoDB Vector Search, Keyv caching)
- AI: Serverless LLM integration - text classification (Llama 3.3 hosted on Groq)
- AI: Vision - device camera and LLM vision model usage (Llama 4 Scout hosted on Groq)
- AI: OpenAI Moderation model usage example

- API Integration: trakt.tv
- API Integration: Wikipedia (@nikeshadhikari9)
- API Integration: Pubchem chemical info data source (@hemanthsavasere)
- API Integration: ~~Tenor~~ GIPHY (@DanielLuu122 @YasharF)

New Core Features

- 2FA via email and code generator apps (TOTP)
- Login with passkeys (biometrics, Face ID, etc.)
- Passwordless authentication (login via email link)
- OAuth token revocation (RFC 7009-style and provider-specific variants) when users unlink an OAuth provider or delete their account
- Login with Discord
- Login with Microsoft (@dev-shahed)
- Multiple profile picture support

Enhancements

- Enhanced Express.js logging with custom Morgan configuration
- Reduced startup friction for new projects by making reCAPTCHA credentials optional
- Consolidated the AI integrations to be separate from API integrations
- Refactored Passport.js strategies to use a common auth-login handler for easier swapping of OAuth providers, maintenance, and core testing
- Updated the included sample Terms of Service and Privacy Policy for formatting and compliance with Google and Facebook requirements
- Various visual and UX improvements
- Improved pre-commit hook scripts for running `eslint --fix` and `Prettier --write` on files being committed
- Consolidated temporary artifacts in tmp/

Bug Fixes

- Fix Facebook OAuth: missing email scope, and infinite loop in certain cases
- Fix upload folder being created in controllers/ instead of the app root
- Fix error handling issues in Google Sheets and Google Drive integration
- Fix various npm script-related issues for Windows development environments
- Fix error from not having husky installed in production environments when using `npm ci --omit dev`

Chores & Maintenance

- Replaced unmaintained express-flash npm package with our own middleware (@Prasanth-S7)
- Replaced moment.js in favor of the native Node.js date API
- Updated minimum engine to Node.js 24.13 which is the latest fully security-patched LTS version.
- Updated dependencies
- Improved dependabot and GitHub Action scripts to automate keeping dependencies up-to-date.
- Updated Google Maps API integration
- Updated Google branding per their requirements
- Updated NYT API integration to use v3 endpoint
- Updated QuickBooks API integration per required changes
- Migrated Foursquare API integration to use the new Places API endpoints (@mheavey2)
- Migrated reCAPTCHA to GCP
- Removed Pinterest OAuth and API Integration
- Removed SendGrid references as they no longer offer a reasonable free tier for hackathon participants (@nylla8444)
- Removed lodash dependency, as much of the functionality can be fulfilled with current versions of JS with minimal code.
- Removed Airbnb eslint (fork) usage in favor of direct rules within eslint 9 configs
- Removed docker support documentation as it won't be officially supported any more (Docker workflows don't align with the hackathon development model and deployment environments vary too widely for a single Docker configuration to be useful or maintainable.)
- Added Pull Request template with a checklist to remind devs on various pre-checks for shippable code
- Updated various documentation (@YasharF @nylla8444 @FrontendBy-GJ)

Tests

- Add API call recording and replay capability and fixtures to enable end-to-end testing without API keys
- Add Playwright harness for UI-driven testing and end-to-end (E2E) test examples
  - Base harness and E2E for automated UI testing (@akilesh1706 @YasharF)
  - E2E tests for GitHub integration (@akilesh1706)
  - E2E tests for last.fm integration (@hsavasere)
  - E2E tests for the web scraping (@Mrinank-Bhowmick)
  - E2E tests for OpenAI Moderation (@Mrinank-Bhowmick)
  - E2E tests for Pubchem integration (@hemanthsavasere)
  - E2E tests for Lob integration (@hemanthsavasere)
  - E2E tests for trakt.tv integration (@hemanthsavasere)
  - E2E tests for NY Times integration (@Vedant794)
  - E2E tests for Wikipedia integration (@nikeshadhikari9)
  - E2E tests for Google Maps integration (@AndersonTsaiTW)
  - E2E tests for the file upload (@hemanthsavasere)
  - E2E tests for Twilio integration (@henockt)
  - E2E tests for HERE Maps integration (@AndersonTsaiTW)
  - E2E tests for Foursquare integration (@Sid0004)
  - E2E tests for ChartJS and Alpha Vantage integration (@AndersonTsaiTW)

### 9.0.0 (April 12, 2025)

New Features

- Introduced "Logout Everywhere" functionality for enhanced security (Thanks to @vimark1).
- Added support for Google Analytics 4, Facebook Pixel, and Open Graph metadata.

Enhancements

- Removed unnecessary session saves for uninitialized sessions.
- Cleaned up GitHub Actions by removing unnecessary CodeQL references.
- Updated documentation for improved clarity and relevance.
- Optimized Dockerfile and updated Docker image for better performance (Thanks to @akarys2304).
- Replaced favicon.png with favicon.ico to match browser default requests.
- Added Apple touch icons.
- Refactored Nodemailer calls into config/nodemailer.js for unified security and configuration settings.
- Removed redundant installation of body-parser, now included with ExpressJS.
- Renamed getValidateReCAPTCHA to validateReCAPTCHA for better clarity.
- Adopted Prettier for consistent code formatting.
- Suppressed unactionable Sass import deprecation warnings.
- Renamed handleOAuth2Callback to saveOAuth2UserTokens for clarity.

Security Updates

- Addressed Host-header Injection vulnerability in Password Reset & Email Verification (CVE-2025-29036).
- Added upload size limit for Multer and moved its configuration to api.js.
- Replaced MD5 with SHA256 for Gravatar generation.

Bug Fixes

- Updated to the latest HERE Maps API as the prior API version calls were no longer working.
- Corrected the path for popper.js.
- Fixed pre-commit test and lint execution.
- Updated the default privacy policy to comply with Facebook terms and other regulations.
- Improved OAuth2 token handling logic:
  - Properly save tokens without expiration dates.
  - Consolidated token-saving logic across all providers to fix multiple issues.
  - Prevented infinite redirect loops in isAuthorized during failed token refresh attempts.

Chore & Maintenance

- [Breaking] Upgraded to Express 5.x.
- [Breaking] Migrated from axios to Node.js's built-in fetch, reducing dependencies and improving performance.
- Switched from the deprecated nyc to c8 for code coverage reporting.
- Updated all dependencies.

Tests

- Added unit tests for isAuthorized and saveOAuth2UserTokens in config/passport.js.
- Fixed unit tests for app.js.

### 8.1.0 (February 1, 2025)

Security Enhancements

- Added URL validation for redirects through session.returnTo (CWE-601).
- Fixed OAuth state parameter generation and handling to address CSRF attack vectors in the OAuth workflow.
- Added additional sanitization for user input in database queries using $eq in MongoDB.

API and Integration:

- Unified formatting for authentication parameters in route definitions and passport.js configuration.
- Refactored common code for OAuth 2 token processing in passport strategies to improve maintainability.
- Reworked the GitHub and Twitch API integration examples with additional data from the APIs.
- Reworked the Twilio API integration example to use Twilio’s sandbox servers and test phone numbers.
- Upgraded the Pinterest API example to use v5 calls instead of the broken v1.
- Reworked the Tumblr API integration example with additional data from the API.
- Added a properly working OAuth 1.0a integration for Tumblr.
- Removed sign-in by Snapchat due to increased difficulty for developers and a focus on hackathon participants.
- Removed Foursquare OAuth authorization and updated the API demo with new examples.
- Renamed Twitter to X (Some of the backend and code still reference Twitter due to upstream dependencies, and the login button is using Twitter colors pending X addition to bootstrap-social).

Update/Upgrades:

- Dropped support for Nodejs < 22 due to ESM module import issues prior to that version.
- Migrated from the unmaintained passport-linkedin-oauth2 to a passport-openidconnect strategy.
  - Added support and examples for openid-client.
- Migrated from the deprecated paypal-rest-sdk to an example without the SDK, providing OAuth calls depending on the page state.
- Migrated from the unmaintained bootstrap-social to a fork that can be easily patched and updated.
- Migrated eslint to v9, and its new config format (breaking change).
- Migrated Husky to v9, and its new config format (breaking change). Fixed Windows commit issue.
- Updated dependencies.
- Added temporary patch files for connect-flash and passport-openidconnect based on pending pull requests or issues on GitHub.

Other:

- Fixed a bug that prevented profile pictures from being displayed.
- Added authentication link/unlink options to the user profile page for all OAuth/Identity providers.
- Fixed typos, broken links, and minor formatting alignment issues on various pages.
- Fixed spelling errors in startup information displayed in the console.
- Refactored URL validation in unit tests for Gravatar generation to conform with CodeQL rules. Even though CodeQL does vulnerability checks, this is not a security issue since it is unit tests.
- Updated the placeholder main.js to use the current format (not deprecated JS).
- Updated the GitHub repo worker/runner configs to use proper permissions
- Return exit code 1 if there is a database connection issue at startup.
- Added the --trace-deprecation flag to startup to provide better information on runtime deprecation warnings.
- .gitignore file to exclude the uploads path.
- Updated the copyright year.
- Updated documentation.

### 8.0.0 (July 28, 2023)

- Security: Renamed the cookie and set secure attribute for cookie transmission when https is present
- Security: Migrated off known deprecated, vulnerable or unmaintained dependencies
- Security: Added express rate limiter
- Added additional sanitization and validation for external inputs. Lusca provides input protection. The additional sanitization and validation are to add another layer of protection.
- Added patch-package for temporary patching dependencies
- Temporary patch for passportjs to handle logout failures
- Temporary patch for passport-oauth2: better auth failure reporting
- Removed broken Instagram oauth support as Meta no longer supports it
- Added handler for 404(page not found) to avoid 500 errors when a route is not found
- Fixed unhandled error during logout
- Fixed pug tags with multiple attributes (thanks to @soundz77)
- Added Lint-stage and Husky to lint all commits
- Fix req.logout for passport 0.6
- Fix broken unit test
- Update default gravatar
- Visual UI improvements
- Added Github Actions: NodeJS CI check unit test and lint
- Upgrade nodejs for docker
- Removed express-handlebars npm package as it was not used and is not that popular compared to pug (breaking change)
- Removed chalk npm package as it was not used (breaking change)
- Updated documentation

- Upgraded to mongoose 7 (breaking change)
- Upgraded to popper2
- Migrated from googleapis npm package to @googleapis/drive and @googleapis/sheets to reduce size and improve performance (breaking change)
- Migrated from passport-twitch-new to twitch-passport (breaking change)
- Migrated from lob to @lob/lob-typescript-sdk (breaking change)
- Migrated from deprecated node-sass to Dart Sass
- Migrated off passport-openid (breaking change)
- Migrated off nodemailer-sendgrid (breaking change)
- Migrated off passport-twitter and twitter-lite (breaking change)
- Migrated off node-quickbooks (breaking change)
- Updated dependencies
- Removed travis.yml

API example changes:

- Removed the twitter API example as the APIs are actively changing and mostly not free (breaking change)
- Removed the Instagram API example as it was broken and Meta has significantly reduced the API scope and availablity for devs
- Improved the Chartjs+AlphaVantage to handle API failures
- Fix minor formatting issues and missing images
- Tumblr - Fixed the Tumblr example and moved off tumblrjs (breaking change)
- Added missing parameters for the Lob's new API requirements
- Improved the Last.fm API example as the artist image is no longer vended by last.fm

### 7.0.0 (Mar 26, 2022)

- Dropped support for Node.js <16
- Switched to Bootstrap 5
- Removed older Bootstrap 4 themes
- Updated dependencies

### 6.0.0 (January 2, 2020)

- Dropped support for NodeJS 8.x, due to its EOL
- Use HTML5 native client form validation (thanks to @peterblazejewicz)
- Fix navbar rendering issues when using themes (thanks to @peterblazejewicz)
- Fix button formatting issues when applying themes (thanks to @peterblazejewicz)
- Fixed drop down menu to show correct formatting from the theme (thanks to @jonasroslund)
- Config mongoose to use the new Server Discovery and Monitoring
- Fix validation bug in Twitter, Pinterest, and Twilio API examples
- Fix HERE icon in the API examples
- Fix minor issues in Stripe and Lob API examples
- Update dependencies
- Update documentation (thanks in part to @noftaly, @yanivm)

### 5.2.0 (July 28, 2019)

- Added API example: Google Drive (thanks to @tanaydin)
- Added Google Sheets API example (thanks to @clarkngo)
- Added HERE Maps API example
- Added support for Intuit Quickbooks API
- Improved Lob.com API example
- Added support for email verification
- Added support for refreshing OAuth tokens
- Fixed bug when users attempt to login by email for accounts that are created with a sign in provider
- Fixed bug in the password reset
- Added CSRF check to the File Upload API example -- security improvement -- breaking change
- Added validation check to password reset token -- security improvement
- Fixed missing await in the Foursquare API example
- Fixed Google Oauth2 profile picture (thanks to @tanaydin)
- Removed deprecated Instagram API calls -- breaking change
- Upgrade to login by LinkedIn v2, remove LinkedIn API example -- breaking change
- Removed express-validator in favor of validator.js -- breaking change
- Removed Aviary API example since the service has been shutdown
- Added additional unit tests for the user model (thanks to @Tolsee)
- Updated Steam's logo
- Updated dependencies
- Updated documentation (thanks in part to @TheMissingNTLDR, @Coteh)

### 5.1.4 (May 14, 2019)

- Migrate from requestjs to axios (thanks to @FX-Wood)
- Enable page templates to add items to the HTML head element
- Fix bold font issue on macs (thanks to @neighlyd)
- Use BASE_URL for github
- Update min node engine to require Feb 2019 NodeJS security release
- Add Node.js 12 to the travis build
- Update dependencies
- Update documentation (thanks in part to @anubhavsrivastava, @Fullchee, @luckymurari)

### 5.1.3 (April 7, 2019)

- Update Steam API Integration
- Upgrade flatly theme files to 4.3.1
- Migrate from bcrypt-nodejs to bcrypt
- Use BASE_URL for twitter and facebook callbacks
- Add a ChartJS example in combination with Alpha Vantage API usage (thanks to @T-travis)
- Improve Github integration – use the user’s private email address if there is no public email listed (thanks to @danielhunt)
- Improve the error handling for the NYT API Example
- Add lodash 4.7
- Fixed gender radio buttons spacing
- Fixed alignment Issue for login / sign in buttons at certain screen widths. (thanks to @eric-sciberras)
- Remove Mozilla Persona information from README since it has been deprecated
- Remove utils
- Remove GSDK since it does not support Bootstrap 4(thanks to @laurenquinn5924)
- Adding additional tests to cover some of the API examples
- Add prod-checklist.md
- Update dependencies
- Update documentation (thanks in part to @GregBrimble)

### 5.1.2 (January 13, 2019)

- Added Login by Snapchat (thanks to @nicholasgonzalezsc)
- Migrate the Foursquare API example to use Axios calls instead of the npm library.
- Fixed minor visual issue in the web scraping example.
- Fixed issue with Popper.js integration (thanks to @binarymax and @Furchin)
- Fixed wrapping issues in the navbar and logo indentation (thanks to @estevanmaito)
- Fixed MongoDB deprecation warnings
- Add production error handler middleware that returns 500 to handle errors. Also, handle server errors in the lastfm API example (thanks to @jagatfx)
- Added autocomplete properties to the views to address Chrome warnings (thanks to @peterblazejewicz)
- Fixed issues in the unit tests.
- Fixed issues in the modern theme variables and imports to be consistent (thanks to @monkeywithacupcake)
- Upgraded to Fontawesome to the latest version (thanks in part to @gesa)
- Upgraded eslint to v5.
- Updated dependencies
- Updated copyright year to include 2019
- Minor code formatting improvements
- Replaced mLab instructions with MongoDB Atlas instructions (thanks to @mgautam98)
- Fixed issues in the readme (thanks to @nero-adaware , @empurium, @aschwtzr)

### 5.1.1 (July 5, 2018)

- Upgraded FontAwesome to FontAwesome v5.1 - FontAwsome is now integrated using its npm package
- Fixed bug with JS libraries missing in Windows Dev envs
- Enabled autofocus in the Contact view when the user is logged in
- Fixed Home always being active (@dkimot)
- Modified Lob example to address recent API changes
- Updated Twilio API (@garretthogan)
- Fixed Twitter API (@garretthogan)
- Dependency updates

### 5.1.0 (May 9, 2018)

- Bootstrap 4.1 upgrade (breaking change)
- Addition of popper.js
- jQuery and Bootstrap will be pulled in the project using their npm packages
- Dockerfile will use development instead of production
- Security improvement by removing X-Powered-By header
- Express errorhandler will only be used in development to match its documentation
- Removed deprecated Instagram popular images API call from the Instagram example (@nacimgoura)
- Removed `mongoose global.Promise` as it is no longer needed (@nacimgoura)
- Refactoring of GitHub, last.fm api, twitter examples and code improvements to use ES6/ES7 features (@nacimgoura)
- Add NodeJS 10 in travis.yml (@nacimgoura)
- Improvements to the Steam API example (@nacimgoura)
- Readme and documentation improvements (thanks in part to @nacimgoura)
- Dependency updates

### 5.0.0 (April 1, 2018)

- NodeJS 8.0+ is now required
- Removed dependency on Bluebird in favor of native NodeJS promisify support
- Font awesome 5 Upgrade
- Fix console warning about Foursquare API version
- Added environment configs to eslint configs and cleaned up code (Thanks to @nacimgoura)
- Fixed eslint rules to better match the project
- Fixed Instagram API example view (@nacimgoura)
- Adding additional code editor related files to .gitignore (@nacimgoura)
- Upgraded syntax at various places to use ES6 syntax (Thanks to @nacimgoura)
- Re-added travis-ci.yml (Thanks to @nacimgoura)
- Fixed bug in Steam API when the user had no achievements (Thanks to @nacimgoura)
- Readme and documentation improvements
- Dependency updates

### 4.4.0 (March 23, 2018)

- Added Docker support (Thanks to @gregorysobotka, @praveenweb, @ryanhanwu). The initial integration has also been upgraded to use NodeJS 8 and Mongo 3.6.
- Removed dependency on async in favor of using promises (@fmcarvalho). Note that the promise support will be upgraded in the upcoming releases to remove the use of Bluebird.
- The contact form will no longer ask for the user's name and email address if they have logged-in already
- Adding a confirmation prompt when a user asks for their account to be deleted
- Fixed Steam Oauth and API integration
- Fixed Last.fm API example (@JonLim)
- Fixed Google Map integration example (@whmsysu)
- Fixed Twitter API integration (@shahzeb1)
- Fixed Facebook integration/request scope (@RobTS)
- Removed MONGOLAB_URI env var, use MONGODB_URI instead
- Preserve the query parameters during authentication session returns (@shreedharshetty)
- normalizeEmail options key remove_dots changed to gmail_remove_dots (@amakhnev)
- Fixed Heroku re-deploy issue (@gballet)
- Migrated from Jade to Pug
- Migrated from GitHub npm package to @octokit/rest to address the related deprecation warning. See https://git.io/vNB11
- Dependency update and upgrades
- Updated left over port 3000 to the current default of port of 8080
- Removed bitgo.pug since bitgo has not been supported by hackathon-starter since v4.1.0
- Removed bitgo from api/index view (@JonLim)
- Fixed unsecure external content by switching them to https
- New address for the Live Demo site
- Code formatting, text prompt, and Readme improvements

### 4.3.0 (November 6, 2016)

- [Added new theme](http://demos.creative-tim.com/get-shit-done/index.html) by Creative Tim (Thanks @conacelelena)
- Added ESLint configuration to _package.json_
- Added _yarn.lock_ (Thanks @niallobrien)
- Added **express-status-monitor** (to see it in action: `/status`)
- Added missing error handling checks (Thanks @dskrepps)
- Server address during the app startup is now clickable (⌘ + LMB) (Thanks @niallobrien)
- Fixed redirect issue in the account page (Thanks @YasharF)
- Fixed `Mongoose.promise` issue (Thanks @starcharles)
- Removed "My Friends" from Facebook API example due to Graph API changes
- Removed iOS7 theme
- `User` model unit tests improvements (Thanks @andela-rekemezie)
- Switched from **github-api** to the more popular **github** NPM module
- Updated Yarn and NPM dependencies

### 4.2.1 (September 6, 2016)

- User model minor code refactoring
- Fixed gravatar display issue on the profile page
- Pretty terminal logs for database connection and app server
- Added compiled _main.css_ to _.gitignore_

### 4.2.0 (August 21, 2016)

- Converted templates from jade to pug (See [Rename from "Jade"](https://github.com/pugjs/pug#rename-from-jade))

### 4.1.1 (August 20, 2016)

- Updated dependencies

### 4.1.0 (July 23, 2016)

- Improved redirect logic after login [#435](https://github.com/sahat/hackathon-starter/pull/435)
- Removed Venmo API (see [Venmo Halts New Developer Access To Its API](https://techcrunch.com/2016/02/26/how-not-to-run-a-platform/))
- Removed BitGo API due to issues with `secp256k1` dependency on Windows

### 4.0.1 (May 17, 2016)

- Renamed `MONGODB` to `MONGODB_URI` environment variable
- Set engine `"node": "6.1.0"` in _package.json_

### 4.0.0 (May 13, 2016)

- **ECMAScript 2015 support!** (Make sure you are using Node.js 6.0+)
- Thanks @vanshady and @prashcr
- Added `<meta theme-color>` support for _Chrome for Android_
- Added Yahoo Finance API example
- Updated Aviary API example
- Flash an error message when updating email to that which is already taken
- Removing an email address during profile update is no longer possible
- PayPal API example now uses _return_url_ and _cancel_url_ from `.env`
- Added client-side `required=true` attributes to input fields
- Fixed broken `show()` function in the GitHub API example
- Fixed YQL query in the Yahoo Weather API example
- Fixed _Can't set headers after they are sent_ error in Stripe API example
- Code refactoring and cleanup
- Updated Travis-CI Node.js version
- Updated NPM dependencies
- Removed Mandrill references

### 3.5.0 (March 4, 2016)

- Added file upload example
- Added Pinterest API example
- Added timestamp support to the User schema
- Fixed `next` parameter being _undefined_ inside `getReset` handler
- Refactored querysting param usage in _api.js_ controller
- Removed _setup.js_ (generator) due to its limited functionality and a lack of updates

### 3.4.1 (February 6, 2016)

- Added "Obtaining Twilio API Keys" instructions.
- Updated Bootstrap v3.3.6.
- Updated jQuery v2.2.0.
- Updated Font Awesome v4.5.0.
- Removed `debug` and `outputStyle` from the Sass middleware options.
- Removed `connect-assets` (no longer used) from _package.json_`.
- Fixed Font Awesome icon syntax error in _profile.jade_.
- Fixed Cheerio broken link.

### 3.4.0 (January 5, 2016)

- Use `dontenv` package for managing API keys and secrets.
- Removed _secrets.js_ (replaced by _.env.example_).
- Added .env to .gitignore.
- Fixed broken Aviary API image.

### 3.3.1 (December 25, 2015)

- Use `connect-mongo` ES5 fallback for backward-compatibility with Node.js version `< 4.0`.

### 3.3.0 (December 19, 2015)

- Steam authorization via OpenID.
- Code style update. (No longer use "one-liners" without braces)
- Updated LinkedIn scope from `r_fullprofile` to `r_basicprofile` due to API changes.
- Added LICENSE file.
- Removed [Bitcore](https://bitcore.io/) example due to installation issues on Windows 10.

### 3.2.0 (October 19, 2015)

- Added Google Analytics script.
- Split _api.js_ `require` intro declaration and initialization for better performance. (See <a href="https://github.com/sahat/hackathon-starter/issues/247">#247</a>)
- Removed [ionicons](http://ionicons.com).
- Removed [connect-assets](https://github.com/adunkman/connect-assets). (Replaced by [node-sass-middleware](https://github.com/sass/node-sass-middleware))
- Fixed alignment styling on /login, /profile and /account
- Fixed Stripe API `POST` request.
- Converted LESS to Sass stylesheets.
- Set `node_js` version to "stable" in _.travis.yml_.
- Removed `mocha.opts` file, pass options directly to package.json
- README cleanup and fixes.
- Updated Font Awesome to 4.4.0

### 3.1.0 (August 25, 2015)

- Added Bitcore example.
- Added Bitgo example.
- Lots of README fixes.
- Fixed Google OAuth profile image url.
- Fixed a bug where `connect-assets` served all JS assets twice.
- Fixed missing `csrf` token in the Twilio API example form.
- Removed `multer` middleware.
- Removed Ordrx API. (Shutdown)

### 3.0.3 (May 14, 2015)

- Added favicon.
- Fixed an email issue with Google login.

### 3.0.2 (March 31, 2015)

- Renamed `navbar.jade` to `header.jade`.
- Fixed typos in README. Thanks @josephahn and @rstormsf.
- Fix radio button alignment on small screens in Profile page.
- Increased `bcrypt.genSalt()` from **5** to **10**.
- Updated package dependencies.
- Updated Font Awesome `4.3.0`.
- Updated Bootstrap `3.3.4`.
- Removed Ionicons.
- Removed unused `User` variable in _controllers/api.js_.
- Removed Nodejitsu instructions from README.

### 3.0.1 (February 23, 2015)

- Reverted Sass to LESS stylesheets. See <a href="https://github.com/sahat/hackathon-starter/issues/233">#233</a>.
- Convert email to lower case in Passport's LocalStrategy during login.
- New Lob API.
- Updated Font Awesome to 4.3.0
- Updated Bootstrap and Flatly theme to 3.3.2.

### 3.0.0 (January 11, 2015)

- New Ordr.in API example.
- Brought back PayPal API example.
- Added `xframe` and xssProtection` protection via **lusca** module.
- No more CSRF route whitelisting, either enable or dsiable it globally.
- Simplified "remember original destination" middleware.
  - Instead of excluding certain routes, you now have to "opt-in" for the routes you wish to remember for a redirect after successful authentication.
- Converted LESS to Sass.
- Updated Bootstrap to 3.3.1 and Font Awesome to 4.2.0.
- Updated jQuery to 2.1.3 and Bootstrap to 3.3.1 JS files.
- Updated Ionicons to 2.0.
- Faster travis-ci builds using `sudo: false`.
- Fixed YUI url on Yahoo API example.
- Fixed `mongo-connect` deprecation warning.
- Code cleanup throughout the project.
- Updated `secrets.js` notice.
- Simplified the generator (`setup.js`), no longer removes auth providers.
- Added `git remote rm origin` to Getting Started instructions in README.

### 2.4.0 (November 8, 2014)

- Bootstrap 3.3.0.
- Flatly 3.3.0 theme.
- User model cleanup.
- Removed `helperContext` from connect-assets middleware.

### 2.3.4 (October 27, 2014)

- Font Awesome 4.2.0 [01e7bd5c09926911ca856fe4990e6067d9148694](https://github.com/sahat/hackathon-starter/commit/01e7bd5c09926911ca856fe4990e6067d9148694)
- Code cleanup in `app.js` and `controllers/api.js`. [8ce48f767c0146062296685cc101acf3d5d224d9](https://github.com/sahat/hackathon-starter/commit/8ce48f767c0146062296685cc101acf3d5d224d9) [cdbb9d1888a96bbba92d4d14deec99a8acba2618](https://github.com/sahat/hackathon-starter/commit/cdbb9d1888a96bbba92d4d14deec99a8acba2618)
- Updated Stripe API example. [afef373cd57b6a44bf856eb093e8f2801fc2dbe2](https://github.com/sahat/hackathon-starter/commit/afef373cd57b6a44bf856eb093e8f2801fc2dbe2)
- Added 1-step deployment process with Heroku and mLab add-on. [c5def7b7b3b98462e9a2e7896dc11aaec1a48b3f](https://github.com/sahat/hackathon-starter/commit/c5def7b7b3b98462e9a2e7896dc11aaec1a48b3f)
- Updated Twitter apps dashboard url. [e378fbbc24e269de69494d326bc20fcb641c0697](https://github.com/sahat/hackathon-starter/commit/e378fbbc24e269de69494d326bc20fcb641c0697)
- Fixed dead links in the README. [78fac5489c596e8bcef0ab11a96e654335573bb4](https://github.com/sahat/hackathon-starter/commit/78fac5489c596e8bcef0ab11a96e654335573bb4)

### 2.3.3 (September 1, 2014)

- Use _https_ (instead of http) profile image URL with Twitter authentication

### 2.3.2 (July 28, 2014)

- Fixed an issue with connect-assets when running `app.js` from an outside folder
- Temporarily disabled `setup.js` on Windows platform until [blessed](https://github.com/chjj/blessed) fixes its problems

### 2.3.1 (July 15, 2014)

- Migrated to Nodemailer 1.0

### 2.3 (July 2, 2014)

- Bootstrap 3.2
- New default theme
- Ionicons fonts
- Fixed bodyParser deprecation warning
- Minor visual updates
- CSS cleanup via RECESS
- Replaced `navbar-brand` image with a font icon

### 2.2.1 (June 17, 2014)

- Added IBM Codename: BlueMix deployment instructions

### 2.2 (June 6, 2014)

- Use Lodash instead of Underscore.js
- Replaced all occurrences of `_.findWhere` with `_.find`
- Added a flash message when user deletes an account
- Updated and clarified some comments
- Updated the Remove Auth message in `setup.js`
- Cleaned up `styles.less`
- Redesigned API Examples page
- Updated Last.fm API example
- Updated Steam API example
- Updated Instagram API example
- Updated Facebook API example
- Updated jQuery to 2.1.1
- Fixed a bug that didn't remove Instagram Auth properly
- Fixed Foursquare secret token

### 2.1.4 (June 5, 2014)

- Fixed a bug related to `returnTo` url (#155)

### 2.1.3 (June 3, 2014)

- Font Awesome 4.1
- Updated icons on some API examples
- Use LESS files for _bootstrap-social_ and _font-awesome_

### 2.1.2 (June 2, 2014)

- Improved Twilio API example
- Updated dependencies

### 2.1.1 (May 29, 2014)

- Added **Compose new Tweet** to Twitter API example
- Fixed email service indentation
- Fixed Mailgun and Mandrill secret.js properties
- Renamed `navigation.jade` to `navbar.jade`

### 2.1 (May 13, 2014)

- New and improved generator - **setup.js**
- Added Yahoo API
- CSS and templates cleanup
- Minor improvement to the default theme
- `cluster_app.js` has been moved into **setup.js**

### 2.0.4 (April 26, 2014)

- Added Mandrill e-mail service (via generator)

### 2.0.3 (April 25, 2014)

- LinkedIn API: Fixed an error if a user did not specify education on LinkedIn
- Removed email constraint when linking OAuth accounts in order to be able to merge accounts that use the same email address
- Check if email address is already taken when creating a new local account
  - Previously relied on Validation Error 11000, which doesn't always work
- When creating a local account, checks if e-mail address is already taken
- Flash notifications can now be dismissed by clicking on �?

### 2.0.2 (April 22, 2014)

- Added Instagram Authentication
- Added Instagram API example
- Updated Instagram Strategy to use a "fake" email address similar to Twitter Startegy

### 2.0.1 (April 18, 2014)

- Conditional CSRF support using [lusca](https://github.com/krakenjs/lusca)
- Fixed EOL problem in `generator.js` for Windows users
- Fixed outdated csrf token string on profile.jade
- Code cleanup

### 2.0.0 (April 15, 2014)

There are have been over **500+** commits since the initial announcement in
January 2014 and over a **120** issues and pull requests from **28** contributors.

- Documentation grew **8x** in size since the announcement on Hacker News
- Upgraded to Express 4.0
- Generator for adding/removing authentication providers
- New Instagram authentication that can be added via generator
- Forgot password and password reset for Local authentication
- Added LinkedIn authentication and API example
- Added Stripe API example
- Added Venmo API example
- Added Clockwork SMS example
- Nicer Facebook API example
- Pre-populated secrets.js with API keys (not linked to my personal accounts)
- Grid layout with company logos on API Examples page
- Added tests (Mocha, Chai, Supertest)
- Gravatar pictures in Navbar and Profile page
- Tracks last visited URL before signing in to redirect back to original destination
- CSRF protection
- Gzip compression and static assets caching
- Client-side JavaScript is automatically minified+concatenated in production
- Navbar, flash messages, footer refactored into partial templates
- Support for Node.js clusters
- Support for Mailgun email service
- Support for environment variables in secrets.js
- Switched from less-middleware to connect-assets
- Bug fixes related to multi-authentication login and account linking
- Other small fixes and changes that are too many to list


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2014-2026 Sahat Yalkabov

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: PROD_CHECKLIST.md
================================================
If you are done with your hackathon and thinking about launching your project into production, or if you are just using this boilerplate to start your soon to be in production application, this document is a checklist to help you get your application production ready.

- Remove unused code and configs
- Add a proxy such as Cloudflare in front of your production deployment. Adjust the numberOfProxies logic in app.js if needed
- Update the session cookie configs with sameSite attribute, domain, and path
- Add Terms of Service and Privacy Policy
- Update `LICENSE.md` and the relevant license field in package.json if applicable - See [npm's doc](https://docs.npmjs.com/files/package.json#license).
- Add [sitemap.xml](https://en.wikipedia.org/wiki/Sitemaps) and [robots.txt](https://moz.com/learn/seo/robotstxt)
- Update Google Analytics ID
- Add Facebook App/Pixel ID
- Add Winston Logging, and replace console.log statements with Winston; have a process for monitoring errors to identify bugs or other issues after launch.
- SEO and Social Media Improvements
- Create a deployment pipeline with a pre-prod/integration test stage.
- (optional) Add email verification _Some experimental data has shown that bogus email addresses are not a significant problem in many cases_
- (optional) Add a filter with [disposable-email-domains](https://www.npmjs.com/package/disposable-email-domains). _Some experimental data has shown that use of disposable emails is typically rare, and in many cases it might not be worth adding the filter._

### Remove unused code and configs

The following is a list of various code that you may not potentially be using and you could remove depending on your application:

- Unused keys from .env file
- /controllers/api.js entirely
- /views/api entirely
- app.js:
  - multer
  - apiController
  - Openshift env references
  - csrf check exception for /api/upload
  - All API example routes
  - OAuth routes for authentications that you are not using (i.e. GitHub, LinkedIn, etc. based on your app)
  - All OAuth authorization routes
- passport.js all references and functions related to:
  - Github, LinkedIn, OpenID, OAuth, OAuth2
- model/User.js
  - key pairs for Github, LinkedIn, Steam
- package.json
  - @octokit/rest, lastfm, lob, multer, node-linkedin, passport-github2, passport-linkedin-oauth2, passport-oauth, paypal-rest-sdk, stripe, twilio
- /test
  - Replace E2E and API example tests with new tests for your application
- views/account/login.pug
  - Some or all of the last form-group set, which are the social login choices
- views/account/profile.pug
  - Link/unlink buttons for GitHub, LinkedIn, Steam
- Remove README, changelog and this guide if not using them
- Create a domain whitelist for your app in Here's developer portal if you are using the HERE Maps API.
- Add unit tests so you can test and incorporate dependency and upstream updates with less effort. GPT tools may create some good unit tests with very low effort.

### Search Engine Optimization (SEO)

Note that SEO only applies to the pages that will be publicly visible with no authentication. Note that some of the following fields need to be added to the HTML header section similar to the page [title](https://github.com/sahat/hackathon-starter/blob/master/views/layout.pug#L9)

- Add Open Graph fields for SEO
  Open Graph data:
  ```
  <meta property="og:title" content="Title">
  <meta property="og:type" content="website">
  <meta property="og:url" content="http://www.example.com/article.html">
  <meta property="og:image" content="http://www.example.com/image.png">
  ```
- Add a page description, which will show up in the search results of the search engine.

  ```
  <meta name="Description" content="Description about the page.">
  ```


================================================
FILE: README.md
================================================
![](https://lh4.googleusercontent.com/-PVw-ZUM9vV8/UuWeH51os0I/AAAAAAAAD6M/0Ikg7viJftQ/w1286-h566-no/hackathon-starter-logo.jpg)
Hackathon Starter
=======================

**Live Demo**: [Link](https://hackathon-starter-1.ydftech.com)

Jump to [What's new?](https://github.com/sahat/hackathon-starter/blob/master/CHANGELOG.md)

A boilerplate for **Node.js** web applications.

If you have attended any hackathons in the past, then you know how much time it takes to get a project started: decide on what to build, pick a programming language, pick a web framework, pick a CSS framework. A while later, you might have an initial project up on GitHub, and only then can other team members start contributing. Or how about doing something as simple as _Sign in with Facebook_ authentication? You can spend hours on it if you are not familiar with how OAuth 2.0 works.

When I started this project, my primary focus was on **simplicity** and **ease of use**.
I also tried to make it as **generic** and **reusable** as possible to cover most use cases of hackathon web apps, without being too specific. In the worst case, you can use this as a learning guide for your projects, if for example you are only interested in **Sign in with Google** authentication and nothing else.

### Testimonials

> [**"Nice! That README alone is already gold!"**](https://www.producthunt.com/tech/hackathon-starter#comment-224732)<br>
> — Adrian Le Bas

> [**"Awesome. Simply awesome."**](https://www.producthunt.com/tech/hackathon-starter#comment-224966)<br>
> — Steven Rueter

> [**"I'm using it for a year now and many projects, it's an awesome boilerplate and the project is well maintained!"**](https://www.producthunt.com/tech/hackathon-starter#comment-228610)<br>
> — Kevin Granger

> **"Small world with Sahat's project. We were using his hackathon starter for our hackathon this past weekend and got some prizes. Really handy repo!"**<br>
> — Interview candidate for one of the companies I used to work with.

<h4 align="center">Modern Theme</h4>

![](https://lh6.googleusercontent.com/-KQTmCFNK6MM/U7OZpznjDuI/AAAAAAAAERc/h3jR27Uy1lE/w1366-h1006-no/Screenshot+2014-07-02+01.32.22.png)

<h4 align="center">Flatly Bootstrap Theme</h4>

![](https://lh5.googleusercontent.com/-oJ-7bSYisRY/U1a-WhK_LoI/AAAAAAAAECM/a04fVYgefzw/w1474-h1098-no/Screen+Shot+2014-04-22+at+3.08.33+PM.png)

<h4 align="center">API Examples</h4>

![](https://lh5.googleusercontent.com/-BJD2wK8CvC8/VLodBsyL-NI/AAAAAAAAEx0/SafE6o_qq_I/w1818-h1186-no/Screenshot%2B2015-01-17%2B00.25.49.png)

## Table of Contents

- [Features](#features)
- [Prerequisites](#prerequisites)
- [Getting Started](#getting-started)
- [HTTPS Proxy](#https-proxy)
- [Obtaining API Keys](#obtaining-api-keys)
- [Web Analytics](#web-analytics)
- [Open Graph](#open-graph)
- [Project Structure](#project-structure)
- [List of Packages](#list-of-packages)
- [Useful Tools and Resources](#useful-tools-and-resources)
- [Recommended Design Resources](#recommended-design-resources)
- [Recommended Node.js Libraries](#recommended-nodejs-libraries)
- [Recommended Client-side Libraries](#recommended-client-side-libraries)
- [Using AI Assistants](#using-ai-assistants)
- [FAQ](#faq)
- [How It Works](#how-it-works-mini-guides)
- [Cheatsheets](#cheatsheets)
  - [ES6](#-es6-cheatsheet)
  - [JavaScript Date](#-javascript-date-cheatsheet)
  - [Mongoose Cheatsheet](#mongoose-cheatsheet)
- [Deployment](#deployment)
- [Production](#production)
- [Changelog](#changelog)
- [Contributing](#contributing)
- [License](#license)

## Features

- Login
  - **Local Authentication** Sign in with Email and Password, Passwordless, Passkey / Biometrics
  - **OAuth 2.0 Authentication:** Sign in with Google, Microsoft, Facebook, LinkedIn, X (Twitter), Twitch, GitHub, Discord
- **User Profile and Account Management**
  - Gravatar
  - Profile Details
  - Password management (Change, Reset, Forgot)
  - Verify Email
  - **Two-Factor Authentication (2FA)** Email codes and Authenticator apps
  - Link multiple OAuth provider accounts to one account
  - OAuth token revocation
  - Delete Account
- Contact Form (powered by SMTP via Mailgun, AWS SES, etc.)
- File upload
- Device camera
- **AI Examples and Boilerplates**
  - AI Agent ReAct (Reasoning + Acting) with tool calling, MongoDB session persistence, and input guardrails
  - RAG with semantic and embedding caching
  - Llama 3.3, Llama 4 Scout (vision use case)
  - OpenAI Moderation
  - Support for a range of foundational and embedding models (DeepSeek, Llama, Mistral, Sentence Transformers, etc.) via LangChain, Groq, and Hugging Face
- **API Examples**
  - **Backoffice:** Lob (USPS Mail), Paypal, Quickbooks, Stripe, Twilio (text messaging)
  - **Data, Media & Entertainment:** Alpha Vantage (stocks and finance info) with ChartJS, Github, Foursquare, Last.fm, New York Times, PubChem (chemical information), Trakt.tv (movies/TV), Twitch, Tumblr (OAuth 1.0a example), Steam (OpenID), Web Scraping, GIPHY
  - **Maps and Location:** Google Maps, HERE Maps
  - **Productivity:** Google Drive, Google Sheets

- Flash notifications
- reCAPTCHA and rate limit protection
- CSRF protection
- MVC Project Structure
- Node.js clusters support
- HTTPS Proxy support (via ngrok, Cloudflare, etc.)
- Sass stylesheets
- Bootstrap 5
- "Go to production" checklist

## Prerequisites

- MongoDB (local install OR hosted)
  - Local Install: [MongoDB](https://www.mongodb.com/download-center/community)
  - Hosted: No need to install, see the MongoDB Atlas section

- [Node.js LTS 24](http://nodejs.org)
  - Highly recommended: Use/Upgrade your Node.js to the latest Node.js LTS version.
- Command Line Tools
- <img src="https://upload.wikimedia.org/wikipedia/commons/1/1b/Apple_logo_grey.svg" height="17">&nbsp;**Mac OS X:** [Xcode](https://itunes.apple.com/us/app/xcode/id497799835?mt=12) (or **OS X 10.9+**: `xcode-select --install`)
- <img src="https://upload.wikimedia.org/wikipedia/commons/8/87/Windows_logo_-_2021.svg" height="17">&nbsp;**Windows:** [Visual Studio Code](https://code.visualstudio.com) + [Windows Subsystem for Linux - Ubuntu](https://learn.microsoft.com/en-us/windows/wsl/install) OR [Visual Studio](https://www.visualstudio.com/products/visual-studio-community-vs)
- <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/UbuntuCoF.svg/512px-UbuntuCoF.svg.png?20120210072525" height="17">&nbsp;**Ubuntu** / <img src="https://upload.wikimedia.org/wikipedia/commons/3/3f/Linux_Mint_logo_without_wordmark.svg" height="17">&nbsp;**Linux Mint:** `sudo apt-get install build-essential`
- <img src="https://upload.wikimedia.org/wikipedia/commons/3/3f/Fedora_logo.svg" height="17">&nbsp;**Fedora**: `sudo dnf groupinstall "Development Tools"`
- <img src="https://en.opensuse.org/images/b/be/Logo-geeko_head.png" height="17">&nbsp;**OpenSUSE:** `sudo zypper install --type pattern devel_basis`

**Note:** If you are new to Node or Express, you may find [Node.js & Express From Scratch series](https://www.youtube.com/watch?v=Ad2ngx6CT0M&list=PLillGF-RfqbYRpji8t4SxUkMxfowG4Kqp&index=3) helpful for learning the basics of Node and Express. Alternatively, here is another great tutorial for complete beginners - [Getting Started With Node.js, Express, MongoDB](https://www.freecodecamp.org/news/build-a-restful-api-using-node-express-and-mongodb/).

## Getting Started

**Step 1:** The easiest way to get started is to clone the repository:

```bash
# Get the latest snapshot
git clone https://github.com/sahat/hackathon-starter.git myproject

# Change directory
cd myproject

# Install NPM dependencies
npm install

# Then simply start your app
npm start
```

**Note:** I highly recommend installing [Nodemon](https://github.com/remy/nodemon). It watches for any changes in your node.js app and automatically restarts the server. Once installed, instead of `node app.js` use `nodemon app.js`. It will
save you a lot of time in the long run, because you won't need to manually restart the server each time you make a small change in code. To install, run `sudo npm install -g nodemon`.

**Step 2:** Obtain API Keys and change configs if needed
After completing step 1 and locally installing MongoDB, you should be able to access the application through a web browser and use local user accounts. However, certain functions like API integrations may not function correctly until you obtain specific keys from service providers. The keys provided in the project serve as placeholders, and you can retain them for features you are not currently utilizing. To incorporate the acquired keys into the application, you have two options:

1.  Set environment variables in your console session: Alternatively, you can set the keys as environment variables directly through the command prompt. For instance, in bash, you can use the `export` command like this: `export FACEBOOK_SECRET=xxxxxx`. This method is considered a better practice as it reduces the risk of accidentally including your secrets in a code repository.
2.  Replace the keys in the `.env.example` file: Open the `.env.example` file and update the placeholder keys with the newly acquired ones. This method has the risk of accidental checking-in of your secrets to code repos.

_What to get and configure:_

- SMTP
  - For user workflows for reset password and verify email
  - For contact form processing
- reCAPTCHA
  - For contact form submission, but you can skip it during your development
- OAuth for social logins (Sign in with / Login with)
  - Depending on your application need, obtain keys from Google, Facebook, X (Twitter), LinkedIn, Twitch, GitHub. You don't have to obtain valid keys for any provider that you don't need. Just remove the buttons and links in the login and account pug views before your demo.
- API keys for service providers that you need in the API Examples if you are planning to use them.

- MongoDB Atlas
  - If you are using MongoDB Atlas instead of a local db, set the MONGODB_URI to your db URI (including your db user/password).

- Email address
  - Set SITE_CONTACT_EMAIL as your incoming email address for messages sent to you through the contact form.
  - Set TRANSACTION_EMAIL as the "From" address for emails sent to users through the lost password or email verification emails to users. You may set this to the same address as SITE_CONTACT_EMAIL.

**Step 3:** Setup an HTTPS proxy to access the app with an https address:
See

- [HTTPS Proxy](#https-proxy)

**Step 4:** Develop your application and customize the experience

- Check out [How It Works](#how-it-works-mini-guides)

**Step 5:** Optional - deploy to production
See:

- [Deployment](#deployment)
- [prod-checklist.md](https://github.com/sahat/hackathon-starter/blob/master/prod-checklist.md)

## HTTPS Proxy:

If you want to use an API that requires HTTPS (for example, GitHub or Facebook), you need to run the app with an HTTPS URL. You can do this by setting up an HTTPS proxy using either ngrok or Cloudflare.
Note: When using an HTTPS proxy, you may get a CSRF mismatch error if you try to directly access the app at `http://localhost:8080` instead of the HTTPS proxy address.

### ngrok

- Download [ngrok](https://ngrok.com/download).
- Start ngrok.
- Set your BASE_URL to the forwarding address from ngrok (i.e., `https://3ccb-1234-abcd.ngrok-free.app`). This will be the HTTPS address for accessing the app.

### Cloudflare

- Download and install [cloudflared](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads).
- For a quick, free tunnel with a random subdomain under `trycloudflare.com`, execute:

```
cloudflared tunnel --url http://localhost:8080
```

- Set BASE_URL to the HTTPS address for the tunnel.

#### Cloudflare with your own domain name

If you own a domain managed by Cloudflare, you can use Cloudflare's Zero Trust portal to set up a tunnel to your app that is activated by a system service. Alternatively, you can create a tunnel and route a subdomain you like to your app using their CLI:

```
cloudflared tunnel login
cloudflared tunnel create myapptunnel
cloudflared tunnel route dns myapptunnel myappsubdomain.mydomain.com
cloudflared tunnel --url http://localhost:8080 run myapptunnel
```

Then set BASE_URL to the HTTPS address for the tunnel.
Note that the tunnel and DNS route are a one-time setup. To reactivate the HTTPS tunnel to your app later, such as after a system restart, just rerun:

```
cloudflared tunnel --url http://localhost:8080 run myapptunnel
```

To clean up your own domain's related configurations when you're done:

- Delete the tunnel by executing `cloudflared tunnel delete myapptunnel`
- Remove the `myappsubdomain` DNS entry from your domain through the Cloudflare web UI.
- Remove `%USERPROFILE%\.cloudflared` (Windows) or `~/.cloudflared` (Linux/macOS) if you want to clear local credentials.

# Obtaining API Keys

You will need to obtain appropriate credentials (Client ID, Client Secret, API Key, or Username & Password) for API and service providers which you need. See Step 2 in the Getting started section for more info.

## SMTP

Obtain SMTP credentials from a provider for transactional emails. Set the SMTP_USER, SMTP_PASSWORD, and SMTP_HOST environment variables accordingly. When picking the SMTP host, keep in mind that the app is configured to use secure SMTP transmissions over port 465 out of the box. You have the flexibility to select any provider that suits your needs or take advantage of one of the following providers, each offering a free tier for your convenience.

| Provider | Free Tier                  | Website                 |
| -------- | -------------------------- | ----------------------- |
| SMTP2Go  | 1000 emails/month for free | https://www.smtp2go.com |
| Brevo    | 300 emails/day for free    | https://www.brevo.com   |

<hr>

<img src="https://i.imgur.com/jULUCKF.png" height="75">

- Visit <a href="https://developers.facebook.com/" target="_blank">Facebook Developers</a>
- Click **My Apps**, then select \*_Add a New App_ from the dropdown menu
- Enter a new name for your app
- Click on the **Create App ID** button
- Find the Facebook Login Product and click on **Facebook Login**
- Instead of going through their Quickstart, click on **Settings** for your app in the top left corner
- Copy and paste _App ID_ and _App Secret_ keys into `.env`
- **Note:** _App ID_ is **FACEBOOK_ID**, _App Secret_ is **FACEBOOK_SECRET** in `.env`
- Enter `localhost` under _App Domains_
- Choose a **Category** that best describes your app
- Click on **+ Add Platform** and select **Website**
- Enter your BASE*URL value (i.e. `http://localhost:8080`, etc) under \_Site URL*
- Click on the _Settings_ tab in the left nav under Facebook Login
- Enter your BASE_URL value followed by /auth/facebook/callback (i.e. `http://localhost:8080/auth/facebook/callback` ) under Valid OAuth redirect URIs

**Note:** After a successful sign-in with Facebook, a user will be redirected back to the home page with appended hash `#_=_` in the URL. It is _not_ a bug. See this [Stack Overflow](https://stackoverflow.com/questions/7131909/facebook-callback-appends-to-return-url) discussion for ways to handle it.

<hr>

<img src="https://imgur.com/2P4UMvC.png" height="75">

- Go to <a href="https://foursquare.com/developers" target="_blank">Foursquare for Developers</a> and log in

- Click on **Create a new project** button
- Enter your _Organization_ and _Project Name_
- Click **Create**
- Navigate to your project
- Click **Settings** in the left-hand-side menu
- Generate a Service API Key
- Copy and paste the Service API Key as `FOURSQUARE_APIKEY` in your `.env` file

<hr>

<img src="https://i.imgur.com/oUob1wG.png" height="75">

- Go to <a href="https://github.com/settings/profile" target="_blank">Account Settings</a>
- Select **Developer settings** from the sidebar
- Then click on **OAuth Apps** and then on **Register new application**
- Enter _Application Name_ and _Homepage URL_. Enter your BASE_URL value (i.e. `http://localhost:8080`, etc) as the homepage URL.
- For _Authorization Callback URL_: your BASE_URL value followed by /auth/github/callback (i.e. `http://localhost:8080/auth/github/callback` )
- Click **Register application**
- Now copy and paste _Client ID_ and _Client Secret_ keys into `.env` file

<hr>

<img src="https://i.imgur.com/ddl2VjR.png" height="75">

- Go to <a href="https://developers.giphy.com/" target="_blank">GIPHY Developers website</a>
- Login or create a new account and login.
- Select **Dashboard** from the navigation bar
- Then click on **Create an API Key** and then select **API** and click on **Next Step**.
- Enter _App Name_ and _App Description_. Select **Web** and create a beta key.
- Now copy and paste the API key into `.env` file as GIPHY_API_KEY.

<hr>

<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Google_2015_logo.svg/1000px-Google_2015_logo.svg.png" height="50">

- Visit <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a>
- Click on the **Create Project** button
- Enter _Project Name_, then click **Create**
- Copy the Project ID for your project and add it as GOOGLE_PROJECT_ID in your `.env` file.
- Then add the APIs and services that apply to your application. If the UI doesn't let you add them during project creation, you can add them later via the **APIs & Services** page (left sidebar) or by searching for them in the top search bar.

**Sign in with Google:**

- Go to the **Credentials** tab, click **Create credentials**, and choose **OAuth client ID**.
- Select **Web application**, and for **Authorized redirect URI** use your BASE_URL value followed by `/auth/google/callback` (for example `http://localhost:8080/auth/google/callback`).
- Copy and paste the **Client ID** and **Client secret** into your `.env` file.
- Update the OAuth consent screen if needed.

**Other APIs:**

Open the **Enabled APIs & services** page for your project in the Google Cloud Console (APIs & Services). Click **+ Enable APIs and services** (top of the page), find the services you need, and enable them:

- **reCAPTCHA Enterprise API** (recommended for the contact form)
- **Google Drive API**
- **Google Sheets API**
- **Maps JavaScript API**

Next, create API keys for the services you enabled:

- For backend API calls (Google Drive and Sheets), create a single API key and restrict it to those APIs. Copy the key as `GOOGLE_API_KEY` into your `.env` file.
- For reCAPTCHA, follow the instructions at <a href="https://cloud.google.com/recaptcha/docs/create-key-website#create-recaptcha-key-Cloud%20console" target="_blank">Google Cloud reCAPTCHA Docs</a> and create a web checkbox reCAPTCHA key. No code changes are required. Just copy the reCAPTCHA site key into `.env` as `GOOGLE_RECAPTCHA_SITE_KEY`.
- For the Google Maps JavaScript API, use a separate API key from your backend key because the key is sent to the front end and can be exposed. Restrict this key to your website on the credentials page. Do NOT use "localhost" as the restriction since it is not unique to your application. Once configured, copy the API key as `GOOGLE_MAP_API_KEY` into your `.env` file.

<hr>

<img src="https://cdn.worldvectorlogo.com/logos/discord-6.svg" height="50">

- Go to <a href="https://discord.com/developers/teams" target="_blank">Teams tab</a> in the Discord Developer Portal and create a new team. This allows you to manage your Discord applications under a team name instead of your personal account.
- After creating a team, switch to the <a href="https://discord.com/developers/applications" target="_blank">Applications tab</a> in the Discord Developer Portal.
- Click on **New Application** and give your app a name. When prompted, select your team as the owner.
- In the left sidebar, click on **OAuth2** > **General**.
- Copy the **Client ID** and **Client Secret** (you may need to "reset" the client secret to obtain it for the first time), then paste them into your `.env` file as `DISCORD_CLIENT_ID` and `DISCORD_CLIENT_SECRET`, or set them as environment variables.
- In the left sidebar, click on **OAuth2** > **URL Generator**.
- Under **Scopes**, select `identify` and `email`.
- Under **Redirects**, add your BASE_URL value followed by `/auth/discord/callback` (i.e. `http://localhost:8080/auth/discord/callback`).
- Save changes.

<hr>

<img src="https://upload.wikimedia.org/wikipedia/commons/c/c7/HERE_logo.svg" height="75">

- Go to <a href="https://developer.here.com" target="_blank">https://developer.here.com</a>
- Sign up for the Base plan. The Base plan require a credit card to start, but you get 30,000 map renders for free each month.
- Create JAVASCRIPT/REST credentials. Copy and paste the API key into the `.env` file as HERE_API_KEY, or set it up as an environment variable.
- **Set up Trusted Domain** - Your credentials will go to the client-side (browser of the users). You need to enable trusted domains and add your test domain address. Otherwise, others may be able to use your credentials on other websites, go through your quota, and potentially leave you with a bill.

<hr>

<img src="https://i.imgur.com/OEVF7HK.png" height="75">

- Go to <a href="https://huggingface.co" target="_blank">https://huggingface.co</a> and create an account.
- Go to your Account Settings and create a new Access Token. Make sure you have granted the **"Make calls to Inference Provider"** permission to your token.
- Add your token as `HUGGINGFACE_KEY` to your `.env` file or as an environment variable.

<hr>

<img src="https://i.imgur.com/Lw5Jb7A.png" height="50">

- Go to <a href="https://developer.intuit.com/app/developer/qbo/docs/get-started" target="_blank">https://developer.intuit.com/app/developer/qbo/docs/get-started</a>
- Use the Sign Up option in the upper right corner of the screen (navbar) to get a free developer account and a sandbox company.
- Create a new app by going to your Dashboard using the My Apps option in the top nav bar or by going to <a href="https://developer.intuit.com/app/developer/myapps" target="_blank">https://developer.intuit.com/app/developer/myapps</a>
- In your App, under Development, Keys & OAuth (right nav), find the Client ID and Client Secret for your `.env` file

<hr>

<img src="https://content.linkedin.com/content/dam/me/business/en-us/amp/brand-site/v2/bg/LI-Logo.svg.original.svg" height="50">

- Sign in at <a href="https://developer.linkedin.com/" target="_blank">LinkedIn Developer Network</a>
- From the account name dropdown menu select **API Keys**
- _It may ask you to sign in once again_
- Click **+ Add New Application** button
- Fill out all the _required_ fields
- **OAuth 2.0 Redirect URLs**: your BASE_URL value followed by /auth/linkedin/callback (i.e. `http://localhost:8080/auth/linkedin/callback` )
- **JavaScript API Domains**: your BASE_URL value (i.e. `http://localhost:8080`, etc).
- For **Default Application Permissions** make sure at least the following is checked:
- `r_basicprofile`
- Finish by clicking **Add Application** button
- Copy and paste _API Key_ and _Secret Key_ keys into `.env` file
- _API Key_ is your **clientID**
- _Secret Key_ is your **clientSecret**

<hr>

<img src="https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg" height="50">

- Go to <a href="https://entra.microsoft.com/" target="_blank">Microsoft Entra admin center</a> and sign in
- Click **App registrations** > **+ New registration**
- Enter an application name (e.g., "Hackathon Starter App") and select **Accounts in any organizational directory and personal Microsoft accounts**
- Set **Redirect URI** to **Web** with your BASE_URL followed by `/auth/microsoft/callback` (e.g., `http://localhost:8080/auth/microsoft/callback`)
- Click **Register**, then copy the **Application (client) ID** to `.env` as `MICROSOFT_CLIENT_ID`
- Go to **Certificates & secrets** > **+ New client secret**, add a description and expiration, then click **Add**
- Copy the secret **Value** immediately to `.env` as `MICROSOFT_CLIENT_SECRET` (won't be visible again)

<hr>

<img src="https://s3-us-west-2.amazonaws.com/public.lob.com/dashboard/navbar/lob-logo.svg" height="50">

- Visit <a href="https://dashboard.lob.com/register" target="_blank">Lob Dashboard</a>
- Create an account
- Once logged into the dashboard, go to Settings in the bottom left corner of the page. (If there is a bottom pop-up, you may need to close it to see the Settings option.)
- Go to the API Keys tab and get your Secret API key for the Test Environment. No physical paper mail will be sent out if you use the Test key, but you can see the PDF of what would have been mailed from your app (with some limitations) through the dashboard. If you use the Live key, they will actually print a physical letter, put it in an envelope with postage, place it in a USPS mailbox, and bill you for it.

<hr>

<img src="https://i.imgur.com/iCsCgp6.png" height="75">

The OpenAI moderation API for checking harmful inputs is free to use as long as you have paid credits in your OpenAI developer account. The cost of using their other models depends on the model, as well as the input and output size of the API call.

- Visit <a href="https://platform.openai.com/api-keys" target="_blank">OpenAI API Keys</a>
- Sign in or create an OpenAI account.
- Click on **Create new secret key** to generate an API key.
- Copy and paste the generated API key into your `.env` file as `OPENAI_API_KEY` or set it as an environment variable.

<hr>

<img src="https://imgur.com/VpWnjp1.png" height="75">

- Visit <a href="https://developer.paypal.com" target="_blank">PayPal Developer</a>
- Log in to your PayPal account
- Click **Applications > Create App** in the navigation bar
- Enter _Application Name_, then click **Create app**
- Copy and paste _Client ID_ and _Secret_ keys into `.env` file
- _App ID_ is **client_id**, _App Secret_ is **client_secret**
- Change **host** to api.paypal.com if you want to test against production and use the live credentials

<hr>

<img src="https://upload.wikimedia.org/wikipedia/commons/a/ae/Steam_logo.svg" height="75">

- Go to <a href="http://steamcommunity.com/dev/apikey" target="_blank">http://steamcommunity.com/dev/apikey</a>
- Sign in with your existing Steam account
- Enter your _Domain Name_ based on your BASE_URL, then and click **Register**
- Copy and paste _Key_ into `.env` file

<hr>

<img src="https://stripe.com/img/about/logos/logos/black@2x.png" height="75">

- <a href="https://stripe.com/" target="_blank">Sign up</a> or log into your <a href="https://manage.stripe.com" target="_blank">dashboard</a>
- Click on your profile and click on Account Settings
- Then click on **API Keys**
- Copy the **Secret Key**. and add this into `.env` file

<hr>

<img src="https://i.imgur.com/dSwblOk.png" height="50">

- Visit <a href="https://groq.com" target="_blank">Groq</a>
- Sign in or create a Groq account.
- Click on **Create API Key** to generate a new key. You will also be able to access your API key under your account settings in the API Keys tab.
- Copy and paste the generated API key into your `.env` file as `GROQ_API_KEY` or set it as an environment variable.

<hr>

<img src="https://i.imgur.com/Adtl9qg.png" height="75">

- Sign up or sign in to your trakt.tv account and go to <a href="https://trakt.tv/oauth/applications" target="_blank">Trakt.tv Applications</a>.
- Create a new application and fill in the required fields:
  - **Name**: Your app name.
  - **Redirect URI**: Set to your BASE_URL value followed by `/auth/trakt/callback` (i.e. `http://localhost:8080/auth/trakt/callback` or `ngrokURL/auth/trakt/callback`)
  - Leave the JavaScript origins blank as we won't be using client-side API calls.
- Click **Save App**.
- Copy and paste the **Client ID** and **Client Secret** into your `.env` file as `TRAKT_ID` and `TRAKT_SECRET` or set them as your environment variables.

<hr>

<img src="https://i.imgur.com/gUngyyW.png" height="50">

- Go to <a href="http://www.tumblr.com/oauth/apps" target="_blank">http://www.tumblr.com/oauth/apps</a>
- Once signed in, click **+Register application**
- Fill in all the details
- For **Default Callback URL**: your BASE_URL value followed by /auth/tumblr/callback (i.e. `http://localhost:8080/auth/tumblr/callback` )
- Click **✔Register**
- Copy and paste _OAuth consumer key_ and _OAuth consumer secret_ keys into `.env` file

<hr>

<img src="https://www.freepnglogos.com/uploads/twitch-logo-image-hd-31.png" height="75">

- Visit the <a href="https://dev.twitch.tv/console" target="_blank">Twitch developer console</a>
- If prompted, authorize the dashboard to access your twitch account
- In the Console, click on Register Your Application
- Enter the name of your application
- Use OAuth Redirect URLs enter your BASE_URL value followed by /auth/twitch/callback (i.e. `http://localhost:8080/auth/twitch/callback` )
- Set Category to Website Integration and press the Create button
- After the application has been created, click on the Manage button
- Copy and paste _Client ID_ into `.env`
- If there is no Client Secret displayed, click on the New Secret button and then copy and paste the _Client secret_ into `.env`

<hr>

<img src="https://s3.amazonaws.com/ahoy-assets.twilio.com/global/images/wordmark.svg" height="75">

- Go to <a href="https://www.twilio.com/try-twilio" target="_blank">https://www.twilio.com/try-twilio</a>
- Sign up for an account.
- Once logged into the dashboard, expand the link 'show api credentials'
- Copy your Account Sid and Auth Token
- Note that you also need to set TWILIO_FROM_NUMBER environment variable to a number you have registered with Twilio and are authorized to send messages from. The +15005550006 placeholder in the .env.example file can be used in the sandbox environment for testing without registering a sending phone number with Twilio.

<hr>

<img src="https://i.imgur.com/QMjwCk6.png" height="50">

- Sign in at <a href="https://developer.x.com/" target="_blank">https://developer.x.com/</a>
- Start with the Free tier
- Click **Create a new application**
- Enter your application name, website and description. Set the website as your BASE_URL value (i.e. `http://localhost:8080`, etc).
- For **Callback URL**: your BASE_URL value followed by /auth/x/callback (i.e. `http://localhost:8080/auth/x/callback` )
- Go to **Settings** tab
- Under _Application Type_ select **Read and Write** access
- Check the box **Allow this application to be used to Sign in with X**
- Click **Update this X's applications settings**
- Copy and paste _Consumer Key_ and _Consumer Secret_ keys into `.env` file

<hr>

## Web Analytics

This project supports integrating web analytics tools such as Google Analytics 4 and Facebook Pixel, along with Open Graph metadata for social sharing. Below are instructions to help you set up these features in your application.

### Google Analytics 4 Setup

- Go to [Google Analytics](https://analytics.google.com)
- Create a new GA4 property so you create a Measurement ID.
- Copy and paste your Measurement ID into `.env` file or set it up as an env variable

### Facebook Pixel

**Optional:** It is highly recommended to set up a business with Facebook that your personal account along with others you authorize can manage. You would need to Go to [Meta Business Suite](https://business.facebook.com/), register a business and add a business page and your website as an asset for the business.

- Go to [Meta Event Manager](https://www.facebook.com/events_manager)
- If you have set up a business, switch from your personal to your business account and pick your business asset using the drop down in the upper right corner of the page.
- Use the Connect Data option to add a Web data source and create a Pixel ID
- Copy and paste the Pixel ID into `.env` file for FACEBOOK_PIXEL_ID or set it up as an environment variable

## Open Graph

The metadata for Open Graph is only set up for the home page (`home.pug`). Update it to suit your application. You can also add Open Graph metadata to any other page that you plan to share through social media by including the relevant data in the corresponding view.

## Project Structure

| Name                             | Description                                                          |
| -------------------------------- | -------------------------------------------------------------------- |
| **config**/flash.js              | Flash middleware (replacement for express-flash)                     |
| **config**/morgan.js             | Configuration for request logging with morgan.                       |
| **config**/nodemailer.js         | Configuration and helper function for sending email with nodemailer. |
| **config**/passport.js           | Passport Local and OAuth strategies, plus login middleware.          |
| **config**/token-revocation.js   | Helper for revoking OAuth tokens.                                    |
| **controllers**/ai.js            | Controller for /ai route and all ai examples and boilerplates.       |
| **controllers**/api.js           | Controller for /api route and all api examples.                      |
| **controllers**/contact.js       | Controller for contact form.                                         |
| **controllers**/home.js          | Controller for home page (index).                                    |
| **controllers**/user.js          | Controller for user account management.                              |
| **controllers**/webauthn.js      | Controller for webauthn management (passkey / biometrics login)      |
| **models**/User.js               | Mongoose schema and model for User.                                  |
| **public**/                      | Static assets (fonts, css, js, img).                                 |
| **public**/**js**/application.js | Specify client-side JavaScript dependencies.                         |
| **public**/**js**/app.js         | Place your client-side JavaScript here.                              |
| **public**/**css**/main.scss     | Main stylesheet for your app.                                        |
| **test**/\*.js                   | Tests, related configs and helpers.                                  |
| **views/account**/               | Templates for _login, password reset, signup, profile, webauthn_     |
| **views/ai**/                    | Templates for AI examples and boilerplates.                          |
| **views/api**/                   | Templates for API examples.                                          |
| **views/partials**/flash.pug     | Error, info and success flash notifications.                         |
| **views/partials**/header.pug    | Navbar partial template.                                             |
| **views/partials**/footer.pug    | Footer partial template.                                             |
| **views**/layout.pug             | Base template.                                                       |
| **views**/home.pug               | Home page template.                                                  |
| .env.example                     | Your API keys, tokens, passwords and database URI.                   |
| .gitignore                       | Folder and files ignored by git.                                     |
| app.js                           | The main application file.                                           |
| eslint.config.mjs                | Rules for eslint linter.                                             |
| package.json                     | NPM dependencies.                                                    |
| package-lock.json                | Contains exact versions of NPM dependencies in package.json.         |

**Note:** There is no preference for how you name or structure your views.
You could place all your templates in a top-level `views` directory without
having a nested folder structure if that makes things easier for you.
Just don't forget to update `extends ../layout` and corresponding
`res.render()` paths in controllers.

## List of Packages

**Dependencies**

Required to run the project before your modifications

| Package                       | Description                                                           |
| ----------------------------- | --------------------------------------------------------------------- |
| @fortawesome/fontawesome-free | Symbol and Icon library.                                              |
| @googleapis/drive             | Google Drive API integration library.                                 |
| @googleapis/sheets            | Google Sheets API integration library.                                |
| @huggingface/inference        | Client library for Hugging Face Inference providers                   |
| @keyv/mongo                   | MongoDB storage adapter for Keyv                                      |
| @langchain/community          | Third party integrations for Langchain                                |
| @langchain/core               | Base LangChain abstractions and Expression Language                   |
| @langchain/mongodb            | MongoDB integrations for LangChain                                    |
| @langchain/textsplitters      | LangChain text splitters for RAG pipelines                            |
| @lob/lob-typescript-sdk       | Lob (USPS mailing / physical mailing service) library.                |
| @node-rs/bcrypt               | Library for hashing and salting user passwords.                       |
| @octokit/rest                 | GitHub API library.                                                   |
| @passport-js/passport-twitter | X (Twitter) login support (OAuth 2).                                  |
| @popperjs/core                | Frontend js library for poppers and tooltips.                         |
| @simplewebauthn/browser       | WebAuthn frontend library (passkey / biometrics authentication)       |
| @simplewebauthn/server        | WebAuthn backend library (passkey / biometrics authentication)        |
| bootstrap                     | CSS Framework.                                                        |
| bootstrap-social              | Social buttons library.                                               |
| bowser                        | User agent parser                                                     |
| chart.js                      | Front-end js library for creating charts.                             |
| cheerio                       | Scrape web pages using jQuery-style syntax.                           |
| compression                   | Node.js compression middleware.                                       |
| connect-mongo                 | MongoDB session store for Express.                                    |
| errorhandler                  | Development-only error handler middleware.                            |
| express                       | Node.js web framework.                                                |
| express-rate-limit            | Rate limiting middleware for abuse protection.                        |
| express-session               | Simple session middleware for Express.                                |
| jquery                        | Front-end JS library to interact with HTML elements.                  |
| keyv                          | key-value storage with support for multiple backends                  |
| langchain                     | Framework for developing LLM applications                             |
| lastfm                        | Last.fm API library.                                                  |
| lusca                         | CSRF middleware.                                                      |
| mailchecker                   | Verifies that an email address is valid and not a disposable address. |
| mongodb                       | MongoDB driver                                                        |
| mongoose                      | MongoDB ODM.                                                          |
| morgan                        | HTTP request logger middleware for node.js.                           |
| multer                        | Node.js middleware for handling `multipart/form-data`.                |
| nodemailer                    | Node.js library for sending emails.                                   |
| oauth                         | OAuth API library without middleware constraints.                     |
| otpauth                       | One-Time Password (TOTP/HOTP) library for 2FA authenticator apps.     |
| passport                      | Simple and elegant authentication library for node.js.                |
| passport-facebook             | Sign-in with Facebook plugin.                                         |
| passport-github2              | Sign-in with GitHub plugin.                                           |
| passport-google-oauth         | Sign-in with Google plugin.                                           |
| passport-local                | Sign-in with Username and Password plugin.                            |
| passport-oauth                | Allows you to set up your own OAuth 1.0a and OAuth 2.0 strategies.    |
| passport-oauth2-refresh       | A library to refresh OAuth 2.0 access tokens using refresh tokens.    |
| passport-steam-openid         | OpenID 2.0 Steam plugin.                                              |
| patch-package                 | Fix broken node modules ahead of fixes by maintainers.                |
| pdfjs-dist                    | PDF parser                                                            |
| pug                           | Template engine for Express.                                          |
| sass                          | Sass compiler to generate CSS with superpowers.                       |
| stripe                        | Official Stripe API library.                                          |
| twilio                        | Twilio API library.                                                   |
| twitch-passport               | Sign-in with Twitch plugin.                                           |
| validator                     | A library of string validators and sanitizers.                        |

**Dev Dependencies**

Required during code development for testing, Hygiene, code styling, etc.

| Package                     | Description                                                                 |
| --------------------------- | --------------------------------------------------------------------------- |
| @eslint/js                  | ESLint JavaScript language implementation.                                  |
| @playwright/test            | Automated end-to-end web testing framework (supports headless web browsers) |
| @prettier/plugin-pug        | Prettier plugin for formatting pug templates                                |
| c8                          | Coverage test.                                                              |
| chai                        | BDD/TDD assertion library.                                                  |
| eslint-config-prettier      | Make ESLint and Prettier play nice with each other.                         |
| eslint                      | Linter JavaScript.                                                          |
| eslint-plugin-chai-friendly | Makes eslint friendly towards Chai.js 'expect' and 'should' statements.     |
| eslint-plugin-import-x      | ESLint plugin with rules that help validate proper imports.                 |
| globals                     | ESLint global identifiers from different JavaScript environments.           |
| husky                       | Git hook manager to automate tasks with git.                                |
| mocha                       | Test framework.                                                             |
| mongodb-memory-server       | In memory mongodb server for testing, so tests can be ran without a DB.     |
| prettier                    | Code formatter.                                                             |
| sinon                       | Test spies, stubs and mocks for JavaScript.                                 |
| supertest                   | HTTP assertion library.                                                     |

## Useful Tools and Resources

- [Microsoft Copilot](https://copilot.microsoft.com/) - Free AI Assistant that can help you with coding questions as well
- [HTML to Pug converter](https://html-to-pug.com/) - HTML to PUG is a free online converter helping you to convert HTML files to pug syntax in real-time.
- [Favicon Generator](http://realfavicongenerator.net/) - Generate favicons for PC, Android, iOS, Windows 8.

## Recommended Design Resources

- [Code Guide](http://codeguide.co/) - Standards for developing flexible, durable, and sustainable HTML and CSS.
- [Bootsnipp](http://bootsnipp.com/) - Code snippets for Bootstrap.
- [Bootstrap Zero](https://www.bootstrapzero.com) - Free Bootstrap templates themes.
- [Google Bootstrap](http://todc.github.io/todc-bootstrap/) - Google-styled theme for Bootstrap.
- [Font Awesome Icons](https://fontawesome.com) - It's already part of the Hackathon Starter, so use this page as a reference.
- [Colors](http://clrs.cc) - A nicer color palette for the web.
- [Creative Button Styles](http://tympanus.net/Development/CreativeButtons/) - awesome button styles.
- [Creative Link Effects](http://tympanus.net/Development/CreativeLinkEffects/) - Beautiful link effects in CSS.
- [Medium Scroll Effect](http://codepen.io/andreasstorm/pen/pyjEh) - Fade in/out header background image as you scroll.
- [GeoPattern](https://github.com/btmills/geopattern) - SVG background pattern generator.
- [Trianglify](https://github.com/qrohlf/trianglify) - SVG low-poly background pattern generator.

## Recommended Node.js Libraries

- [Nodemon](https://github.com/remy/nodemon) - Automatically restart Node.js server on code changes.
- [geoip-lite](https://github.com/bluesmoon/node-geoip) - Geolocation coordinates from IP address.
- [Filesize.js](http://filesizejs.com/) - Pretty file sizes, e.g. `filesize(265318); // "265.32 kB"`.
- [Numeral.js](http://numeraljs.com) - Library for formatting and manipulating numbers.
- [sharp](https://github.com/lovell/sharp) - Node.js module for resizing JPEG, PNG, WebP and TIFF images.

## Recommended Client-side Libraries

- [Framework7](https://framework7.io/) - Full Featured HTML Framework For Building iOS7 Apps.
- [InstantClick](http://instantclick.io) - Makes your pages load instantly by pre-loading them on mouse hover.
- [NProgress.js](https://github.com/rstacruz/nprogress) - Slim progress bars like on YouTube and Medium.
- [Hover](https://github.com/IanLunn/Hover) - Awesome CSS3 animations on mouse hover.
- [Magnific Popup](http://dimsemenov.com/plugins/magnific-popup/) - Responsive jQuery Lightbox Plugin.
- [Offline.js](http://github.hubspot.com/offline/docs/welcome/) - Detect when user's internet connection goes offline.
- [Alertify.js](https://alertifyjs.com) - Sweet looking alerts and browser dialogs.
- [selectize.js](http://selectize.github.io/selectize.js) - Styleable select elements and input tags.
- [drop.js](http://github.hubspot.com/drop/docs/welcome/) - Powerful Javascript and CSS library for creating dropdowns and other floating displays.
- [scrollReveal.js](https://github.com/jlmakes/scrollReveal.js) - Declarative on-scroll reveal animations.

## Using AI Assistants

AI tools and large language models (LLMs) can greatly accelerate your ramp-up time, efficiency, and productivity during hackathons. Many of these tools are available for free and offer features that can significantly enhance your coding experience.

You have two main options for accessing these tools:

- **Web-based chat interfaces**: Platforms like [ChatGPT](https://chat.openai.com/) and [MS Copilot](https://copilot.microsoft.com/)
- **Integrated code assistants**: Tools like [Amazon Q (CodeWhisperer)](https://aws.amazon.com/q/developer/pricing/), [GitHub Copilot](https://github.com/features/copilot), and Gemini Code Assist integrate directly into code editors, such as Visual Studio Code.

Integrated tools, like plugins for Visual Studio Code, let you reference your code directly without needing to copy-paste, making them easier to use in many cases. Web-based assistants, on the other hand, require manual copy-pasting but can offer a different approach without impacting the "context" for your integrated tool. Tools and models perform differently depending on their update cycles, so results may vary. If an integrated tool struggles with a task, try copy-pasting the relevant code into a web assistant to troubleshoot. A good starting point is combining Amazon Q and MS Copilot, as these tools tend to produce fewer issues like outdated syntax, vulnerable code, or incomplete solutions compared to other assistants.

### Providing Context to AI Tools

Context for LLMs is the additional information that the model needs to make sense of how it should respond to your question, which in coding is probably your existing code, example implementation, or specifications that you might copy-paste or pass to the model. Keep in mind that integrated assistants may not automatically include your project files as the context and may try to answer your question without looking at your code. To include the context:

- **Amazon Q**: Use `@[filename]` to specify a file or `@workspace` to include the entire project.
- **GitHub Copilot**: Click the "Add Context" button in the chat and manually add specific files or choose Codebase for full project context. Note that you need to set the copilot mode to "Ask", "Edit", etc based on your intended conversation.

### Example Prompts to Get You Started

**Explaining Code and Concepts**

- "Can you explain how this project handles sanitization of user inputs?"
- "What does function `x` in file `y` do?" (_Copy-paste code into a web-based assistant if using one._)
- "Can you walk me through what this regex does?"

**Adding New Features**

- "I want to add login functionality for [OAuth2 provider]. The project already includes similar logins for other providers. Can you guide me through the required changes to `app.js`, `config/passport.js`, `models/User.js`, and the relevant views?"  
  _Pro Tip:_ If the assistant misses some changes, follow up with specific files or provide relevant documentation for better accuracy.
- "Can you help me design an addition to this project to do the following. I don't need any code yet, and want to work on the design and refine it before moving to an implementation. --- continue with a bullet point list of your requirements"

**Debugging or Fixing Code**

- "I modified the function `x` below to achieve `y`, but I get the following error. Can you help me fix it? --- Can have blocks afterward with a header like `==== error ====` and `==== function x ====` afterward."
- "Can you help me fix a bug in the following function or function `x`. It is supposed to return `y` when it gets input `i` but it is returning `z`."
- "Can you check my comments for spelling issues?".

## FAQ

### Why do I get `403 Error: Forbidden` when submitting a form?

You need to add the following hidden input element to your form. This has been added in the [pull request #40](https://github.com/sahat/hackathon-starter/pull/40) as part of the CSRF protection.

```
input(type='hidden', name='_csrf', value=_csrf)
```

**Note:** It is now possible to whitelist certain URLs. In other words, you can specify a list of routes that should bypass the CSRF verification check.

**Note 2:** To whitelist dynamic URLs use regular expression tests inside the CSRF middleware to see if `req.originalUrl` matches your desired pattern.

### I am getting MongoDB Connection Error, how do I fix it?

That's a custom error message defined in `app.js` to indicate that there was a problem connecting to MongoDB:

```js
mongoose.connection.on('error', (err) => {
  console.error(err);
  console.log('%s MongoDB connection error. Please make sure MongoDB is running.');
  process.exit(1);
});
```

You need to have a MongoDB server running before launching `app.js`. You can download MongoDB [here](https://www.mongodb.com/try/download/community), or install it via a package manager.
Windows users, read [Install MongoDB on Windows](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-windows//).

**Tip:** If you are always connected to the internet, you could just use [MongoDB Atlas](https://www.mongodb.com) instead of downloading and installing MongoDB locally. You will only need to update the database credentials in the `.env` file.

**NOTE:** MongoDB Atlas (cloud database) is required for vector store, index, and search features used in AI integrations. These features are NOT available in locally installed MongoDBs.

### I get an error when I deploy my app, why?

Chances are you haven't changed the _Database URI_ in `.env`. If `MONGODB` is set to `localhost`, it will only work on your machine as long as MongoDB is running. When you deploy to Render, OpenShift, or some other provider, you will not have MongoDB running on `localhost`. You need to create an account with [MongoDB Atlas](https://www.mongodb.com), then create a free tier database.
See [Deployment](#deployment) for more information on how to set up an account and a new database step-by-step with MongoDB Atlas.

### Why do you have all routes defined in app.js?

For the sake of simplicity. While there might be a better approach, such as passing `app` context to each controller as outlined in this [blog](http://timstermatic.github.io/blog/2013/08/17/a-simple-mvc-framework-with-node-and-express/), I find such a style to be confusing for beginners. It took me a long time to grasp the concept of `exports` and `module.exports`, let alone having a global `app` reference in other files. That to me is backward thinking.
The `app.js` is the "heart of the app", it should be the one referencing models, routes, controllers, etc.
When working solo on small projects, I prefer to have everything inside `app.js` as is the case with [this](<(https://github.com/sahat/ember-sass-express-starter/blob/master/app.js)>) REST API server.

## How It Works (mini guides)

This section is intended for giving you a detailed explanation of how a particular functionality works. Maybe you are just curious about how it works, or perhaps you are lost and confused while reading the code, I hope it provides some guidance to you.

### Custom HTML and CSS Design 101

[HTML5 UP](http://html5up.net/) has many beautiful templates that you can download for free.

When you download the ZIP file, it will come with _index.html_, _images_, _CSS_ and _js_ folders. So, how do you integrate it with Hackathon Starter? Hackathon Starter uses the Bootstrap CSS framework, but these templates do not.
Trying to use both CSS files at the same time will likely result in undesired effects.

**Note:** Using the custom templates approach, you should understand that you cannot reuse any of the views I have created: layout, the home page, API browser, login, signup, account management, contact. Those views were built using Bootstrap grid and styles. You will have to manually update the grid using a different syntax provided in the template. **Having said that, you can mix and match if you want to do so: Use Bootstrap for the main app interface, and a custom template for a landing page.**

Let's start from the beginning. For this example I will use [Escape Velocity](http://html5up.net/escape-velocity/) template:
![Alt](http://html5up.net/uploads/images/escape-velocity.jpg)

**Note:** For the sake of simplicity I will only consider `index.html`, and skip `left-sidebar.html`,
`no-sidebar.html`, `right-sidebar.html`.

Move all JavaScript files from `html5up-escape-velocity/js` to `public/js`. Then move all CSS files from `html5up-escape-velocity/css` to `public/css`. And finally, move all images from `html5up-escape-velocity/images` to `public/images`. You could move it to the existing **img** folder, but that would require manually changing every `img` reference. Grab the contents of `index.html` and paste it into [HTML To Pug](https://html-to-pug.com/).

**Note:** Do not forget to update all the CSS and JS paths accordingly.

Create a new file `escape-velocity.pug` and paste the Pug markup in `views` folder.
Whenever you see the code `res.render('account/login')` - that means it will search for `views/account/login.pug` file.

Let's see how it looks. Create a new controller **escapeVelocity** inside `controllers/home.js`:

```js
exports.escapeVelocity = (req, res) => {
  res.render('escape-velocity', {
    title: 'Landing Page',
  });
};
```

And then create a route in `app.js`. I placed it right after the index controller:

```js
app.get('/escape-velocity', homeController.escapeVelocity);
```

Restart the server (if you are not using **nodemon**); then you should see the new template at `http://localhost:8080/escape-velocity`

I will stop right here, but if you would like to use this template as more than just a single page, take a look at how these Pug templates work: `layout.pug` - base template, `index.pug` - home page, `partials/header.pug` - Bootstrap navbar, `partials/footer.pug` - sticky footer. You will have to manually break it apart into smaller pieces. Figure out which part of the template you want to keep the same on all pages - that's your new `layout.pug`.
Then, each page that changes, be it `index.pug`, `about.pug`, `contact.pug`
will be embedded in your new `layout.pug` via `block content`. Use existing templates as a reference.

This is a rather lengthy process, and templates you get from elsewhere might have yet another grid system. That's why I chose _Bootstrap_ for the Hackathon Starter.
Many people are already familiar with _Bootstrap_, plus it's easy to get started with it if you have never used _Bootstrap_.
You can also buy many beautifully designed _Bootstrap_ themes at various vendors, and use them as a drop-in replacement for Hackathon Starter, just make sure they support the latest version of Bootstrap. However, if you would like to go with a completely custom HTML/CSS design, this should help you to get started!

<hr>

### How do flash messages work in this project?

Flash messages allow you to display a message at the end of the request and access it on the next request and only the next request. For instance, on a failed login attempt, you would display an alert with some error message, but as soon as you refresh that page or visit a different page and come back to the login page, that error message will be gone. It is only displayed once.
This project uses a middleware for displaying flash messages. You don't have to explicitly send a flash message to every view inside `res.render()`.
All flash messages are available in your views via `messages` object by default.

Flash messages have a two-step process. You use `req.flash('errors', { msg: 'Error messages goes here' }`
to create a flash message in your controllers, and then display them in your views:

```pug
if messages.errors
  .alert.alert-danger.fade.in
    each error in messages.errors
      div= error.msg
```

In the first step, `'errors'` is the name of a flash message, which should match the name of the property on `messages` object in your views. You place alert messages inside `if message.errors` because you don't want to show them flash messages are present.
The reason why you pass an error like `{ msg: 'Error message goes here' }` instead of just a string - `'Error message goes here'`, is for the sake of consistency.
To clarify that, _express-validator_ module which is used for validating and sanitizing user's input, returns all errors as an array of objects, where each object has a `msg` property with a message why an error has occurred. Here is a more general example of what express-validator returns when there are errors present:

```js
[
  { param: 'name', msg: 'Name is required', value: '<received input>' },
  { param: 'email', msg: 'A valid email is required', value: '<received input>' },
];
```

To keep consistent with that style, you should pass all flash messages as `{ msg: 'My flash message' }` instead of a string. Otherwise, you will see an alert box without an error message. That is because in **partials/flash.pug** template it will try to output `error.msg` (i.e. `"My flash message".msg`), in other words, it will try to call a `msg` method on a _String_ object, which will return _undefined_. Everything I just mentioned about errors, also applies to "info" and "success" flash messages, and you could even create a new one yourself, such as:

**Data Usage Controller (Example)**

```
req.flash('warning', { msg: 'You have exceeded 90% of your data usage' });
```

**User Account Page (Example)**

```pug
if messages.warning
  .alert.alert-warning.fade.in
    each warning in messages.warning
      div= warning.msg
```

`partials/flash.pug` is a partial template that contains how flash messages are formatted. Previously, flash messages were scattered throughout each view that used flash messages (contact, login, signup, profile), but now, thankfully it uses a _DRY_ approach.

The flash messages partial template is _included_ in the `layout.pug`, along with footer and navigation.

```pug
body
  include partials/header

  .container
    include partials/flash
    block content

  include partials/footer
```

If you have any further questions about flash messages, please feel free to open an issue, and I will update this mini-guide accordingly, or send a pull request if you would like to include something that I missed.

<hr>

### How do I create a new page?

A more correct way to say this would be "How do I create a new route?" The main file `app.js` contains all the routes.
Each route has a callback function associated with it. Sometimes you will see three or more arguments for a route. In a case like that, the first argument is still a URL string, while middle arguments are what's called middleware. Think of middleware as a door. If this door prevents you from continuing forward, you won't get to your callback function. One such example is a route that requires authentication.

```js
app.get('/account', passportConfig.isAuthenticated, userController.getAccount);
```

It always goes from left to right. A user visits `/account` page. Then `isAuthenticated` middleware checks if you are authenticated:

```js
exports.isAuthenticated = (req, res, next) => {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect('/login');
};
```

If you are authenticated, you let this visitor pass through your "door" by calling `return next();`. It then proceeds to the
next middleware until it reaches the last argument, which is a callback function that typically renders a template on `GET` requests or redirects on `POST` requests. In this case, if you are authenticated, you will be redirected to the _Account Management_ page; otherwise, you will be redirected to the _Login_ page.

```js
exports.getAccount = (req, res) => {
  res.render('account/profile', {
    title: 'Account Management',
  });
};
```

Express.js has `app.get`, `app.post`, `app.put`, `app.delete`, but for the most part, you will only use the first two HTTP verbs, unless you are building a RESTful API.
If you just want to display a page, then use `GET`, if you are submitting a form, sending a file then use `POST`.

Here is a typical workflow for adding new routes to your application. Let's say we are building a page that lists all books from the database.

**Step 1.** Start by defining a route.

```js
app.get('/books', bookController.getBooks);
```

---

**Note:** As of Express 4.x you can define your routes like so:

```js
app.route('/books').get(bookController.getBooks).post(bookController.createBooks).put(bookController.updateBooks).delete(bookController.deleteBooks);
```

And here is how a route would look if it required an _authentication_ and an _authorization_ middleware:

```js
app.route('/api/twitch').all(passportConfig.isAuthenticated).all(passportConfig.isAuthorized).get(apiController.getTwitch).post(apiController.postTwitch);
```

Use whichever style makes sense to you. Either one is acceptable. I think that chaining HTTP verbs on `app.route` is a very clean and elegant approach, but on the other hand, I can no longer see all my routes at a glance when you have one route per line.

**Step 2.** Create a new schema and a model `Book.js` inside the _models_ directory.

```js
const mongoose = require('mongoose');

const bookSchema = new mongoose.Schema({
  name: String,
});

const Book = mongoose.model('Book', bookSchema);
module.exports = Book;
```

**Step 3.** Create a new controller file called `book.js` inside the _controllers_ directory.

```js
/**
 * GET /books
 * List all books.
 */
const Book = require('../models/Book.js');

exports.getBooks = (req, res) => {
  Book.find((err, docs) => {
    res.render('books', { books: docs });
  });
};
```

**Step 4.** Import that controller in `app.js`.

```js
const bookController = require('./controllers/book');
```

**Step 5.** Create `books.pug` template.

```pug
extends layout

block content
  .page-header
    h3 All Books

  ul
    each book in books
      li= book.name
```

That's it! I will say that you could have combined Step 1, 2, 3 as following:

```js
app.get('/books', (req, res) => {
  Book.find((err, docs) => {
    res.render('books', { books: docs });
  });
});
```

Sure, it's simpler, but as soon as you pass 1000 lines of code in `app.js` it becomes a little challenging to navigate the file.
I mean, the whole point of this boilerplate project was to separate concerns, so you could work with your teammates without running into _MERGE CONFLICTS_. Imagine you have four developers working on a single `app.js`, I promise you it won't be fun resolving merge conflicts all the time.
If you are the only developer, then it's okay. But as I said, once it gets up to a certain LoC size, it becomes difficult to maintain everything in a single file.

That's all there is to it. Express.js is super simple to use.
Most of the time you will be dealing with other APIs to do the real work:
[Mongoose](http://mongoosejs.com/docs/guide.html) for querying database, socket.io for sending and receiving messages over WebSockets, sending emails via [Nodemailer](http://nodemailer.com/), form validation using [validator.js](https://github.com/validatorjs/validator.js) library, parsing websites using [Cheerio](https://github.com/cheeriojs/cheerio), etc.

<hr>

### AI Agent Controller

LangChain v1 ReAct agent intended as a starting point for building new AI agents. The end-to-end implementation supports:

- **Tool execution** with automatic retry middleware for transient failures
- **MongoDB session persistence** - Chat history persists across page reloads (authenticated users: permanent until account deletion; unauthenticated: tied to Express session lifecycle)
- **Input guardrails** - Prompt injection/jailbreak detection using a guard model (e.g., Llama Guard 4)
- **Conversation summarization** - Long conversations are condensed to stay within context limits
- **Real-time streaming** - Server-Sent Events (SSE) for live responses

To build your Agent using this controller as a starting point, you need to do two things:

#### 1. Define the agent's role

Edit the `systemPrompt` in `createAIAgent()` to describe what the agent does and which tools it can use.

```
systemPrompt: `You are a helpful [... e.g. travel, personal assistant, exam grading] agent.

Your responsibilities:

1. [YOUR_RESPONSIBILITY_1]
2. [YOUR_RESPONSIBILITY_2]
3. [YOUR_RESPONSIBILITY_3]

Available tools:
[LIST_YOUR_TOOLS_HERE]`
```

---

#### 2. Replace the tools

Add tools specific to your project by replacing the existing tools in the `tools` array inside `createAIAgent()`.  
The existing tool functions can be removed.

Tools follow this structure and use a Zod schema for input validation:

```js
const myTool = tool(
  async ({ input }, config) => {
    config.writer?.({ message: 'Calling my service...' });

    // Call your API or database
    const result = await callYourAPI(input);

    return JSON.stringify(result);
  },
  {
    name: 'my_tool',
    description: 'Does something specific',
    schema: z.object({
      input: z.string().describe('The input'),
    }),
  },
);
```

---

#### Other Functions in ai-agent.js

These functions handle streaming, parsing, and session management and typically do not need modification:

- `promptGuardMiddleware()` : LangChain middleware that classifies user input before the agent processes it. Blocks unsafe prompts and redirects the conversation.
- `getCheckpointer()` : Initializes the MongoDB checkpointer for session persistence.
- `cleanupOrphanedTempSessions()` : Cleans up checkpoint data for unauthenticated users whose Express sessions have expired. Called on app startup.
- `getAIAgent(req, res)` : Express route (GET /ai/ai-agent) - Renders the AI agent demo page and loads prior messages.
- `postAIAgentChat(req, res)` : Express route (POST /ai/ai-agent/chat) - Main SSE endpoint. Streams AI responses, tool progress, and debug data.
- `postAIAgentReset(req, res)` : Express route (POST /ai/ai-agent/reset) - Clears the user's chat session from MongoDB.
- `deleteUserAIAgentData(userId)` : Called when a user deletes their account to clean up their chat data.
- `sendSSE(res, eventType, data)` : Sends typed SSE events to the frontend.
- `extractAIMessages(data)` : Extracts user-visible AI messages from agent stream updates.
- `extractStatus(data)` : Derives tool call and completion status messages.

#### Environment Variables

The AI Agent requires these environment variables:

- `GROQ_API_KEY` : Your Groq API key
- `GROQ_MODEL` : The main LLM model (e.g., `llama-3.3-70b-versatile`)
- `GROQ_MODEL_PROMPT_GUARD` : The guard model for input safety (e.g., `meta-llama/llama-guard-4-12b`)

### How do I use Socket.io with Hackathon Starter?

[Dan Stroot](https://github.com/dstroot) submitted an excellent [pull request](https://github.com/dstroot/hackathon-starter/commit/0a632def1ce8da446709d92812423d337c977d75) that adds a real-time dashboard with socket.io.
And as much as I'd like to add it to the project, I think it violates one of the main principles of the Hackathon Starter:

> When I started this project, my primary focus was on simplicity and ease of use.
> I also tried to make it as generic and reusable as possible to cover most use cases of
> hackathon web apps, **without being too specific**.

When I need to use socket.io, I **really** need it, but most of the time - I don't. But more importantly, WebSockets support is still experimental on most hosting providers.
Due to past provider issues with WebSockets, I have not include socket.io as part of the Hackathon Starter. _For now..._
If you need to use socket.io in your app, please continue reading.

First, you need to install socket.io:

```js
npm install socket.io
```

Replace `const app = express();` with the following code:

```js
const app = express();
const server = require('http').Server(app);
const io = require('socket.io')(server);
```

I like to have the following code organization in `app.js` (from top to bottom): module dependencies,
import controllers, import configs, connect to database, express configuration, routes,
start the server, socket.io stuff. That way I always know where to look for things.

Add the following code at the end of `app.js`:

```js
io.on('connection', (socket) => {
  socket.emit('greet', { hello: 'Hey there browser!' });
  socket.on('respond', (data) => {
    console.log(data);
  });
  socket.on('disconnect', () => {
    console.log('Socket disconnected');
  });
});
```

One last thing left to change:

```js
app.listen(app.get('port'), () => {
```

to

```js
server.listen(app.get('port'), () => {
```

At this point, we are done with the back-end.

You now have a choice - to include your JavaScript code in Pug templates or have all your client-side JavaScript in a separate file - in `app.js`. I admit, when I first started with Node.js and JavaScript in general, I placed all JavaScript code inside templates because I have access to template variables passed in from Express right then and there. It's the easiest thing you can do, but also the least efficient and harder to maintain. Since then I almost never include inline JavaScript inside templates anymore.

But it's also understandable if you want to take the easier road. Most of the time you don't even care about performance during hackathons, you just want to _"get shit done"_ before the time runs out. Well, either way, use whichever approach makes more sense to you. At the end of the day, it's **what** you build that matters, not **how** you build it.

If you want to stick all your JavaScript inside templates, then in `layout.pug` - your main template file, add this to the `head` block.

```pug
script(src='/socket.io/socket.io.js')
script.
  let socket = io.connect(window.location.href);
  socket.on('greet', function (data) {
    console.log(data);
    socket.emit('respond', { message: 'Hey there, server!' });
  });
```

**Note:** Notice the path of the `socket.io.js`, you don't actually have to have `socket.io.js` file anywhere in your project; it will be generated automatically at runtime.

If you want to have JavaScript code separate from templates, move that inline script code into `app.js`, inside the `$(document).ready()` function:

```js
$(document).ready(function () {
  // Place JavaScript code here...
  let socket = io.connect(window.location.href);
  socket.on('greet', function (data) {
    console.log(data);
    socket.emit('respond', { message: 'Hey there, server!' });
  });
});
```

And we are done!

## Cheatsheets

### <img src="https://frontendmasters.com/assets/es6-logo.png" height="34" align="top"> ES6 Cheatsheet

#### Declarations

Declares a read-only named constant.

```js
const name = 'yourName';
```

Declares a block scope local variable.

```js
let index = 0;
```

#### Template Strings

Using the **\`${}\`** syntax, strings can embed expressions.

```js
const name = 'Oggy';
const age = 3;

console.log(`My cat is named ${name} and is ${age} years old.`);
```

#### Modules

To import functions, objects, or primitives exported from an external module. These are the most common types of importing.

```js
const name = require('module-name');
```

```js
const { foo, bar } = require('module-name');
```

To export functions, objects, or primitives from a given file or module.

```js
module.exports = { myFunction };
```

```js
module.exports.name = 'yourName';
```

```js
module.exports = myFunctionOrClass;
```

#### Spread Operator

The spread operator allows an expression to be expanded in places where multiple arguments (for function calls) or multiple elements (for array literals) are expected.

```js
myFunction(...iterableObject);
```

```jsx
<ChildComponent {...this.props} />
```

#### Promises

A Promise is used in asynchronous computations to represent an operation that hasn't completed yet but is expected in the future.

```js
var p = new Promise(function (resolve, reject) {});
```

The `catch()` method returns a Promise and deals with rejected cases only.

```js
p.catch(function (reason) {
  /* handle rejection */
});
```

The `then()` method returns a Promise. It takes two arguments: callback for the success & failure cases.

```js
p.then(
  function (value) {
    /* handle fulfillment */
  },
  function (reason) {
    /* handle rejection */
  },
);
```

The `Promise.all(iterable)` method returns a promise that resolves when all of the promises in the iterable argument have resolved or rejects with the reason of the first passed promise that rejects.

```js
Promise.all([p1, p2, p3]).then(function (values) {
  console.log(values);
});
```

#### Arrow Functions

Arrow function expression. Shorter syntax & lexically binds the `this` value. Arrow functions are anonymous.

```js
(singleParam) => {
  statements;
};
```

```js
() => {
  statements;
};
```

```js
(param1, param2) => expression;
```

```js
const arr = [1, 2, 3, 4, 5];
const squares = arr.map((x) => x * x);
```

#### Classes

The class declaration creates a new class using prototype-based inheritance.

```js
class Person {
  constructor(name, age, gender) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }

  incrementAge() {
    this.age++;
  }
}
```

:gift: **Credits**: [DuckDuckGo](https://duckduckgo.com/?q=es6+cheatsheet&ia=cheatsheet&iax=1) and [@DrkSephy](https://github.com/DrkSephy/es6-cheatsheet).

:top: <sub>[**back to top**](#table-of-contents)</sub>

### <img src="http://i.stack.imgur.com/Mmww2.png" height="34" align="top"> JavaScript Date Cheatsheet

#### Unix Timestamp (seconds)

```js
Math.floor(Date.now() / 1000);
```

#### Add 30 minutes to a Date object

```js
var now = new Date();
now.setMinutes(now.getMinutes() + 30);
```

#### Date Formatting

```js
// DD-MM-YYYY
var now = new Date();

var DD = now.getDate();
var MM = now.getMonth() + 1;
var YYYY = now.getFullYear();

if (DD < 10) {
  DD = '0' + DD;
}

if (MM < 10) {
  MM = '0' + MM;
}

console.log(MM + '-' + DD + '-' + YYYY); // 03-30-2016
```

```js
// hh:mm (12 hour time with am/pm)
var now = new Date();
var hours = now.getHours();
var minutes = now.getMinutes();
var amPm = hours >= 12 ? 'pm' : 'am';

hours = hours % 12;
hours = hours ? hours : 12;
minutes = minutes < 10 ? '0' + minutes : minutes;

console.log(hours + ':' + minutes + ' ' + amPm); // 1:43 am
```

#### Next week Date object

```js
var today = new Date();
var nextWeek = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000);
```

#### Yesterday Date object

```js
var today = new Date();
var yesterday = date.setDate(date.getDate() - 1);
```

:top: <sub>[**back to top**](#table-of-contents)</sub>

### Mongoose Cheatsheet

#### Find all users:

```js
User.find((err, users) => {
  console.log(users);
});
```

#### Find a user by email:

```js
let userEmail = 'example@gmail.com';
User.findOne({ email: { $eq: email.toLowerCase() } }).then((user) => {
  console.log(user);
});
```

#### Find 5 most recent user accounts:

```js
User.find()
  .sort({ _id: -1 })
  .limit(5)
  .exec((err, users) => {
    console.log(users);
  });
```

#### Get the total count of a field from all documents:

Let's suppose that each user has a `votes` field and you would like to count the total number of votes in your database across all users. One very inefficient way would be to loop through each document and manually accumulate the count. Or you could use [MongoDB Aggregation Framework](https://docs.mongodb.org/manual/core/aggregation-introduction/) instead:

```js
User.aggregate({ $group: { _id: null, total: { $sum: '$votes' } } }, (err, votesCount) => {
  console.log(votesCount.total);
});
```

:top: <sub>[**back to top**](#table-of-contents)</sub>

## Deployment

Using a local instance on your laptop with ngrok is a good solution for your demo during the hackathon, and you wouldn't necessarily need to deploy to a cloud platform. If you wish to have your app run 24x7 for a general audience, once you are ready to deploy your app, you will need to create an account with a cloud platform to host it. There are a number of cloud service providers out there that you can research. Service providers like AWS and Azure provide a free tier of service which can help you get started with just some minor costs (such as traffic overage if any, etc).

---

### Hosted MongoDB Atlas

<img src="https://www.mongodb.com/assets/images/global/MongoDB_Logo_Dark.svg" width="200">

- Go to [mongodb.com](https://www.mongodb.com/)
- Click the green **Try free** button
- Fill in your information then hit **Create your Atlas account**
- You will be redirected to Create New Cluster page.
- Select a **Cloud Provider and Region**
- Set the cluster Tier to Free Forever **Shared** Cluster
- Give Cluster a name (default: Cluster0)
- Click on the green **:zap:Create Cluster button**
- Now, to access your database you need to create a DB user. To create a new MongoDB user, from the **Clusters view**, select the **Security tab**
- Under the **MongoDB Users** tab, click on **+Add New User**
- Fill in a username and password and give it either **Atlas Admin** User Privilege
- Next, you will need to create an IP address whitelist and obtain the connection URI. In the Clusters view, under the cluster details (i.e. SANDBOX - Cluster0), click on the **CONNECT** button.
- Under section **(1) Check the IP Whitelist**, click on **ALLOW ACCESS FROM ANYWHERE**. The form will add a field with `0.0.0.0/0`. Click **SAVE** to save the `0.0.0.0/0` whitelist.
- Under section **(2) Choose a connection method**, click on **Connect Your Application**
- In the new screen, select **Node.js** as Driver and version **3.6 or later**.
- Finally, copy the URI connection string and replace the URI in MONGODB_URI of `.env.example` with this URI string. Make sure to replace the <PASSWORD> with the db User password that you created under the Security tab.
- Note that after some of the steps in the Atlas UI, you may see a banner stating `We are deploying your changes`. You will need to wait for the deployment to finish before using the DB in your application.

## Production

If you are starting with this boilerplate to build an application for prod deployment, or if after your hackathon you would like to get your project hardened for production use, see [prod-checklist.md](https://github.com/sahat/hackathon-starter/blob/master/prod-checklist.md).

## Testing

Hackathon Starter includes both unit tests and end-to-end (E2E) tests.

- **Unit tests** focus on core functionality, such as user account management.
- **E2E tests** use [Playwright](https://playwright.dev/) to run the application in a headless Chrome browser, making live API calls and verifying rendered views. These tests are located in `test/e2e/` and `test/e2e-nokey/`. The nokey tests are tests that don't require API keys to run.

During a hackathon, you typically don't need to worry about E2E tests; they can slow you down when you're focused on rapid prototyping. However, if you plan to launch your project for real-world use, adding and maintaining E2E tests is strongly recommended. They help ensure that future changes don't unintentionally break existing functionality.

The existing E2E tests cover the example API integrations included in the starter project. You can use these as **examples or templates** when creating your own test files, adapting them to match your project's specific views and workflows.

You can run the tests using:

```bash
npm test                  # unit tests (core functions)
npm run test:e2e:live     # All E2E tests with previously recorded API responses
npm run test:e2e:replay   # E2E (replay fixtures - recorded API responses)
```

You can run a single E2E Test file like the following:

```bash
# Run tests in a single test file against live APIs
npx playwright test test/e2e.../testfile.e2e.test.js --config=test/playwright.config.js --project=chromium

# Run tests in a single test file while replaying recorded API responses from the fixtures
npx playwright test test/e2e.../testfile.e2e.test.js --config=test/playwright.config.js --project=chromium-replay

# Run tests in a single test file against live APIs and capture the API responses as fixtures for replay later
npx playwright test test/e2e.../testfile.e2e.test.js --config=test/playwright.config.js --project=chromium-record
```

For more information on creating or running E2E tests see [test/TESTING.md](https://github.com/sahat/hackathon-starter/blob/master/test/TESTING.md)

## Changelog

You can find the changelog for the project in: [CHANGELOG.md](https://github.com/sahat/hackathon-starter/blob/master/CHANGELOG.md)

## Contributing

If something is unclear, confusing, or needs to be refactored, please let me know.
Pull requests are always welcome, but due to the opinionated nature of this project, I cannot accept every pull request. Please open an issue before submitting a pull request. This project uses [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) with a few minor exceptions. If you are submitting a pull request that involves Pug templates, please make sure you are using _spaces_, not tabs.

## License

The MIT License (MIT)

Copyright (c) 2014-2026 Sahat Yalkabov

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Supported Versions

| Version | Supported          |
| ------- | ------------------ |
| latest  | :white_check_mark: |
| master  | :white_check_mark: |
| other   | :x:                |

## Reporting a Vulnerability

PRIOR TO SUBMITTING SECURITY CONCERNS/REPORTS:

1. Research Wikipedia and other sources about hackathons to become more familiar with the potential uses of this project, the intended settings, and usage environments.
2. Read README.md entirely, including the introduction and the steps for obtaining API keys, which includes replacing the .env values. The provided values in the .env file are placeholders, not a batch of keys exposed through GitHub.
3. Read PROD_CHECKLIST.md. Hackathon projects are not production projects, and this checklist is to help users with their next steps to move from a prototype state to a production state.

SUBMITTING SECURITY CONCERNS/REPORTS:

1. Complete the above steps 1 to 3.
2. If you still believe you have identified an issue, please submit it as a GitHub Issue at https://github.com/sahat/hackathon-starter/issues with the relevant information for discussion and clarification.
   Submissions requiring registration with third-party websites will be marked/reported as spam.


================================================
FILE: app.js
================================================
/**
 * Module dependencies.
 */
const path = require('node:path');
const express = require('express');
const compression = require('compression');
const session = require('express-session');
const errorHandler = require('errorhandler');
const lusca = require('lusca');
const { MongoStore } = require('connect-mongo');
const mongoose = require('mongoose');
const passport = require('passport');
const rateLimit = require('express-rate-limit');
const { flash } = require('./config/flash');

/**
 * Load environment variables from .env file, where API keys and passwords are configured.
 */
try {
  process.loadEnvFile('.env.example');
} catch (err) {
  if (err && err.code === 'ENOENT') {
    console.log('No .env.example file found. This is OK if the required environment variables are already set in your environment.');
  } else {
    console.error('Error loading .env.example file:', err);
  }
}

/**
 * Set config values
 */
const secureTransfer = process.env.BASE_URL.startsWith('https');

/**
 * Rate limiting configuration
 * This is a basic rate limiting configuration. You may want to adjust the settings
 * based on your application's needs and the expected traffic patterns.
 * Also, consider adding a proxy such as cloudflare for production.
 */
const RATE_LIMIT_GLOBAL = parseInt(process.env.RATE_LIMIT_GLOBAL, 10) || 200; // Default to 200 per 15 min if env variable not set
const RATE_LIMIT_STRICT = parseInt(process.env.RATE_LIMIT_STRICT, 10) || 5; // Default to 5 per hr if env variable not set
const RATE_LIMIT_LOGIN = parseInt(process.env.RATE_LIMIT_LOGIN, 10) || 10; // Default to 10 per hr if env variable not set

// Global Rate Limiter Config
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: RATE_LIMIT_GLOBAL, // requests per 15 minutes
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
// Strict Auth Rate Limiter Config for signup, password recover, account verification, login by email, send 2FA email
const strictLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: RATE_LIMIT_STRICT, // attempts per hour
  standardHeaders: true,
  legacyHeaders: false,
});

// Login Rate Limiter Config
const loginLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: RATE_LIMIT_LOGIN, // attempts per hour
  standardHeaders: true,
  legacyHeaders: false,
});
// Login 2FA Rate Limiter Config - allow more requests for 2FA pages per login to avoid UX issues.
// This is after a valid username/password submission, so the attack surface is smaller
// and we want to avoid locking out legitimate users who mistype their 2FA code.
const login2FALimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: RATE_LIMIT_LOGIN * 5,
  standardHeaders: true,
  legacyHeaders: false,
});

// This logic for numberOfProxies works for local testing, ngrok use, single host deployments
// behind cloudflare, etc. You may need to change it for more complex network settings.
// See readme.md for more info.
let numberOfProxies;
if (secureTransfer) numberOfProxies = 1;
else numberOfProxies = 0;

/**
 * Controllers (route handlers).
 */
const homeController = require('./controllers/home');
const userController = require('./controllers/user');
const apiController = require('./controllers/api');
const aiController = require('./controllers/ai');
const aiAgentController = require('./controllers/ai-agent');

const contactController = require('./controllers/contact');
const webauthnController = require('./controllers/webauthn');

/**
 * API keys and Passport configuration.
 */
const passportConfig = require('./config/passport');

/**
 * Request logging configuration
 */
const { morganLogger } = require('./config/morgan');

/**
 * Create Express server.
 */
const app = express();
console.log('Run this app using "npm start" to include sass/scss/css builds.\n');

/**
 * Connect to MongoDB.
 */
mongoose.connect(process.env.MONGODB_URI);
mongoose.connection.on('error', (err) => {
  console.error(err);
  console.log('MongoDB connection error. Please make sure MongoDB is running.');
  process.exit(1);
});
mongoose.connection.once('open', () => {
  // Clean up orphaned temp AI agent sessions (Express sessions expired but chat checkpoint data remains)
  aiAgentController.cleanupOrphanedTempSessions();
});

/**
 * Express configuration.
 */
app.set('host', process.env.OPENSHIFT_NODEJS_IP || '0.0.0.0');
app.set('port', process.env.PORT || process.env.OPENSHIFT_NODEJS_PORT || 8080);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.set('trust proxy', numberOfProxies);
app.use(morganLogger());
app.use(compression());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(limiter);
app.use(
  session({
    resave: true, // Only save session if modified
    saveUninitialized: false, // Do not save sessions until we have something to store
    secret: process.env.SESSION_SECRET,
    name: 'startercookie', // change the cookie name for additional security in production
    cookie: {
      maxAge: 1209600000, // Two weeks in milliseconds
      secure: secureTransfer,
    },
    store: MongoStore.create({ mongoUrl: process.env.MONGODB_URI }),
  }),
);
app.use(passport.initialize());
app.use(passport.session());
app.use(flash);
app.use((req, res, next) => {
  if (req.path === '/api/upload' || req.path === '/ai/llm-camera') {
    // Multer multipart/form-data handling needs to occur before the Lusca CSRF check.
    // WARN: Any path that is not protected by CSRF here should have lusca.csrf() chained
    // in their route handler.
    next();
  } else {
    lusca.csrf()(req, res, next);
  }
});
app.use(lusca.xframe('SAMEORIGIN'));
app.use(lusca.xssProtection(true));
app.disable('x-powered-by');
app.use((req, res, next) => {
  res.locals.user = req.user;
  next();
});
// Function to validate if the URL is a safe relative path
const isSafeRedirect = (url) => /^\/[a-zA-Z0-9/_-]*$/.test(url);
app.use((req, res, next) => {
  // After successful login, redirect back to the intended page
  // Only set returnTo for GET requests (Only pages that a user can navigate to)
  if (req.method !== 'GET') {
    return next();
  }

  if (!req.user && req.path !== '/login' && !req.path.startsWith('/login/webauthn-') && req.path !== '/signup' && !req.path.startsWith('/auth') && !req.path.includes('.')) {
    const returnTo = req.originalUrl;
    if (isSafeRedirect(returnTo)) {
      req.session.returnTo = returnTo;
    } else {
      req.session.returnTo = '/';
    }
  } else if (req.user && (req.path === '/account' || req.path.startsWith('/api'))) {
    const returnTo = req.originalUrl;
    if (isSafeRedirect(returnTo)) {
      req.session.returnTo = returnTo;
      if (req.path.startsWith('/api/') && !req.session.baseReturnTo) {
        req.session.baseReturnTo = '/api';
      }
    } else {
      req.session.returnTo = '/';
      req.session.baseReturnTo = '/';
    }
  }
  next();
});
app.use('/', express.static(path.join(__dirname, 'public'), { maxAge: 31557600000 }));
app.use('/js/lib', express.static(path.join(__dirname, 'node_modules/chart.js/dist'), { maxAge: 31557600000 }));
app.use('/js/lib', express.static(path.join(__dirname, 'node_modules/@popperjs/core/dist/umd'), { maxAge: 31557600000 }));
app.use('/js/lib', express.static(path.join(__dirname, 'node_modules/bootstrap/dist/js'), { maxAge: 31557600000 }));
app.use('/js/lib', express.static(path.join(__dirname, 'node_modules/jquery/dist'), { maxAge: 31557600000 }));
app.use('/js/lib', express.static(path.join(__dirname, 'node_modules/@simplewebauthn/browser/dist/bundle'), { maxAge: 31557600000 }));
app.use('/webfonts', express.static(path.join(__dirname, 'node_modules/@fortawesome/fontawesome-free/webfonts'), { maxAge: 31557600000 }));
app.use('/image-cache', express.static(path.join(__dirname, 'tmp/image-cache'), { maxAge: 31557600000 }));

/**
 * Analytics IDs needed thru layout.pug; set as express local so we don't have to pass them with each render call
 */
app.locals.FACEBOOK_ID = process.env.FACEBOOK_ID ? process.env.FACEBOOK_ID : null;
app.locals.GOOGLE_ANALYTICS_ID = process.env.GOOGLE_ANALYTICS_ID ? process.env.GOOGLE_ANALYTICS_ID : null;
app.locals.FACEBOOK_PIXEL_ID = process.env.FACEBOOK_PIXEL_ID ? process.env.FACEBOOK_PIXEL_ID : null;

/**
 * Primary app routes.
 */
app.get('/', homeController.index);
app.get('/login', userController.getLogin);
app.post('/login', loginLimiter, userController.postLogin);
app.get('/login/verify/:token', loginLimiter, userController.getLoginByEmail);
app.get('/login/2fa', login2FALimiter, userController.getTwoFactor);
app.post('/login/2fa', login2FALimiter, userController.postTwoFactor);
app.post('/login/2fa/resend', strictLimiter, userController.resendTwoFactorCode);
app.get('/login/2fa/totp', login2FALimiter, userController.getTotpVerify);
app.post('/login/2fa/totp', login2FALimiter, userController.postTotpVerify);
app.post('/login/webauthn-start', loginLimiter, webauthnController.postLoginStart);
app.get('/login/webauthn-start', (req, res) => res.redirect('/login')); // webauthn-start requires a POST
app.post('/login/webauthn-verify', loginLimiter, webauthnController.postLoginVerify);
app.get('/logout', userController.logout);
app.get('/forgot', userController.getForgot);
app.post('/forgot', strictLimiter, userController.postForgot);
app.get('/reset/:token', userController.getReset);
app.post('/reset/:token', loginLimiter, userController.postReset);
app.get('/signup', userController.getSignup);
app.post('/signup', userController.postSignup);
app.get('/contact', strictLimiter, contactController.getContact);
app.post('/contact', contactController.postContact);
app.get('/account/verify', passportConfig.isAuthenticated, userController.getVerifyEmail);
app.get('/account/verify/:token', passportConfig.isAuthenticated, userController.getVerifyEmailToken);
app.get('/account', passportConfig.isAuthenticated, userController.getAccount);
app.post('/account/profile', passportConfig.isAuthenticated, userController.postUpdateProfile);
app.post('/account/password', passportConfig.isAuthenticated, userController.postUpdatePassword);
app.post('/account/2fa/email/enable', passportConfig.isAuthenticated, userController.postEnable2FA);
app.post('/account/2fa/email/remove', passportConfig.isAuthenticated, userController.postRemoveEmail2FA);
app.get('/account/2fa/totp/setup', passportConfig.isAuthenticated, userController.getTotpSetup);
app.post('/account/2fa/totp/setup', passportConfig.isAuthenticated, userController.postTotpSetup);
app.post('/account/2fa/totp/remove', passportConfig.isAuthenticated, userController.postRemoveTotp);
app.post('/account/delete', passportConfig.isAuthenticated, userController.postDeleteAccount);
app.post('/account/logout-everywhere', passportConfig.isAuthenticated, userController.postLogoutEverywhere);
app.get('/account/unlink/:provider', passportConfig.isAuthenticated, userController.getOauthUnlink);
app.post('/account/webauthn/register', passportConfig.isAuthenticated, webauthnController.postRegisterStart);
app.get('/account/webauthn/register', (req, res) => res.redirect('/account')); // webauthn/register start requires a POST
app.post('/account/webauthn/verify', passportConfig.isAuthenticated, webauthnController.postRegisterVerify);
app.post('/account/webauthn/remove', passportConfig.isAuthenticated, webauthnController.postRemove);

/**
 * API examples routes.
 */
app.get('/api', apiController.getApi);
app.get('/api/lastfm', apiController.getLastfm);
app.get('/api/nyt', apiController.getNewYorkTimes);
app.get('/api/steam', passportConfig.isAuthenticated, passportConfig.isAuthorized, apiController.getSteam);
app.get('/api/stripe', apiController.getStripe);
app.post('/api/stripe', apiController.postStripe);
app.get('/api/scraping', apiController.getScraping);
app.get('/api/twilio', apiController.getTwilio);
app.post('/api/twilio', apiController.postTwilio);
app.get('/api/foursquare', apiController.getFoursquare);
app.get('/api/tumblr', passportConfig.isAuthenticated, passportConfig.isAuthorized, apiController.getTumblr);
app.get('/api/facebook', passportConfig.isAuthenticated, passportConfig.isAuthorized, apiController.getFacebook);
app.get('/api/github', apiController.getGithub);
app.get('/api/twitch', passportConfig.isAuthenticated, passportConfig.isAuthorized, apiController.getTwitch);
app.get('/api/paypal', apiController.getPayPal);
app.get('/api/paypal/success', apiController.getPayPalSuccess);
app.get('/api/paypal/cancel', apiController.getPayPalCancel);
app.get('/api/lob', apiController.getLob);
app.get('/api/upload', lusca({ csrf: true }), apiController.getFileUpload);
app.post('/api/upload', strictLimiter, apiController.uploadMiddleware, lusca({ csrf: true }), apiController.postFileUpload);
app.get('/api/here-maps', apiController.getHereMaps);
app.get('/api/google-maps', apiController.getGoogleMaps);
app.get('/api/google/drive', passportConfig.isAuthenticated, passportConfig.isAuthorized, apiController.getGoogleDrive);
app.get('/api/chart', apiController.getChart);
app.get('/api/google/sheets', passportConfig.isAuthenticated, passportConfig.isAuthorized, apiController.getGoogleSheets);
app.get('/api/quickbooks', passportConfig.isAuthenticated, passportConfig.isAuthorized, apiController.getQuickbooks);
app.get('/api/trakt', apiController.getTrakt);
app.get('/api/pubchem', apiController.getPubChem);
app.get('/api/wikipedia', apiController.getWikipedia);
app.get('/api/giphy', apiController.getGiphy);

/**
 * AI Integrations and Boilerplate example routes.
 */
app.get('/ai', aiController.getAi);
app.get('/ai/openai-moderation', aiController.getOpenAIModeration);
app.post('/ai/openai-moderation', aiController.postOpenAIModeration);
app.get('/ai/llm-classifier', aiController.getLLMClassifier);
app.post('/ai/llm-classifier', aiController.postLLMClassifier);
app.get('/ai/llm-camera', lusca({ csrf: true }), aiController.getLLMCamera);
app.post('/ai/llm-camera', strictLimiter, aiController.imageUploadMiddleware, lusca({ csrf: true }), aiController.postLLMCamera);
app.get('/ai/rag', aiController.getRag);
app.post('/ai/rag/ingest', aiController.postRagIngest);
app.post('/ai/rag/ask', aiController.postRagAsk);
app.get('/ai/ai-agent', aiAgentController.getAIAgent);
app.post('/ai/ai-agent/chat', aiAgentController.postAIAgentChat);
app.post('/ai/ai-agent/reset', aiAgentController.postAIAgentReset);

/**
 * OAuth authentication failure handler (common for all providers)
 * passport.js requires a static route for failureRedirect.
 * With this auth failure handler, we can decide where to redirect the user
 * and avoid infinite loops in cases when they navigate to a route
 * protected by isAuthorized and the user is not authorized.
 */
app.get('/auth/failure', (req, res) => {
  // Check if a flash message for 'errors' already exists in the session (do not consume it)
  const hasErrorFlash = req.session && req.session.flash && req.session.flash.errors && req.session.flash.errors.length > 0;

  if (!hasErrorFlash) {
    req.flash('errors', { msg: 'Authentication failed or provider account is already linked.' });
  }
  const { returnTo, baseReturnTo } = req.session;
  req.session.returnTo = undefined;
  req.session.baseReturnTo = undefined;
  const redirectTarget = baseReturnTo || returnTo;

  if (!redirectTarget || !isSafeRedirect(redirectTarget) || redirectTarget === req.originalUrl || redirectTarget.startsWith('/auth/')) {
    return res.redirect('/');
  }
  res.redirect(redirectTarget);
});

/**
 * OAuth authentication routes. (Sign in)
 */
app.get('/auth/facebook', passport.authenticate('facebook'));
app.get('/auth/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/auth/failure' }), (req, res) => {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/github', passport.authenticate('github'));
app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/auth/failure' }), (req, res) => {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/google', passport.authenticate('google'));
app.get('/auth/google/callback', passport.authenticate('google', { failureRedirect: '/auth/failure' }), (req, res) => {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/x', passport.authenticate('X'));
app.get('/auth/x/callback', passport.authenticate('X', { failureRedirect: '/auth/failure' }), (req, res) => {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/linkedin', passport.authenticate('linkedin'));
app.get('/auth/linkedin/callback', passport.authenticate('linkedin', { failureRedirect: '/auth/failure' }), (req, res) => {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/microsoft', passport.authenticate('microsoft'));
app.get('/auth/microsoft/callback', passport.authenticate('microsoft', { failureRedirect: '/auth/failure' }), (req, res) => {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/twitch', passport.authenticate('twitch'));
app.get('/auth/twitch/callback', passport.authenticate('twitch', { failureRedirect: '/auth/failure' }), (req, res) => {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/discord', passport.authenticate('discord'));
app.get('/auth/discord/callback', passport.authenticate('discord', { failureRedirect: '/auth/failure' }), (req, res) => {
  res.redirect(req.session.returnTo || '/');
});

/**
 * OAuth authorization routes. (API examples)
 */
app.get('/auth/tumblr', passport.authorize('tumblr'));
app.get('/auth/tumblr/callback', passport.authorize('tumblr', { failureRedirect: '/auth/failure' }), (req, res) => {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/steam', passport.authorize('steam-openid'));
app.get('/auth/steam/callback', passport.authorize('steam-openid', { failureRedirect: '/auth/failure' }), (req, res) => {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/trakt', passport.authorize('trakt'));
app.get('/auth/trakt/callback', passport.authorize('trakt', { failureRedirect: '/auth/failure' }), (req, res) => {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/quickbooks', passport.authorize('quickbooks'));
app.get('/auth/quickbooks/callback', passport.authorize('quickbooks', { failureRedirect: '/auth/failure' }), (req, res) => {
  res.redirect(req.session.returnTo || '/');
});

/**
 * Error Handler.
 */
app.use((req, res, next) => {
  const err = new Error('Not Found');
  err.status = 404;
  res.status(404).send('Page Not Found');
});

if (process.env.NODE_ENV === 'development') {
  // only use in development
  app.use(errorHandler());
} else {
  app.use((err, req, res) => {
    console.error(err);
    res.status(500).send('Server Error');
  });
}

/**
 * Start Express server.
 */
app.listen(app.get('port'), () => {
  const { BASE_URL } = process.env;
  const colonIndex = BASE_URL.lastIndexOf(':');
  const port = parseInt(BASE_URL.slice(colonIndex + 1), 10);

  if (!BASE_URL.startsWith('http://localhost')) {
    console.log(
      `The BASE_URL environment variable is set to ${BASE_URL}.
If you open the app directly at http://localhost:${app.get('port')} instead of via your HTTPS-terminating endpoint (e.g., ngrok, Cloudflare, or similar), CSRF checks may fail and OAuth sign-in will be rejected due to a redirect mismatch.
To avoid this, set BASE_URL to the HTTPS endpoint and always access the app through it in your browser.
`,
    );
  } else if (app.get('port') !== port) {
    console.warn(`WARNING: The BASE_URL environment variable and the App have a port mismatch. If you plan to view the app in your browser using the localhost address, you may need to adjust one of the ports to make them match. BASE_URL: ${BASE_URL}\n`);
  }

  console.log(`App is running on http://localhost:${app.get('port')} in ${app.get('env')} mode.`);
  console.log('Press CTRL-C to stop.');
});

module.exports = app;


================================================
FILE: config/flash.js
================================================
const { format } = require('node:util');

// Flash Middleware as a replacement for express-flash / connect-flash
// Those packages are unmaintained and have some issues. This is a simple
// implementation that provides the same functionality.
exports.flash = (req, res, next) => {
  if (req.flash) return next();
  req.flash = (type, message, ...args) => {
    const flashMessages = (req.session.flash ||= {});
    if (!type) {
      req.session.flash = {};
      return { ...flashMessages };
    }
    if (!message) {
      const retrieved = flashMessages[type] || [];
      delete flashMessages[type];
      return retrieved;
    }
    const arr = (flashMessages[type] ||= []);
    if (args.length) arr.push(format(message, ...args));
    else if (Array.isArray(message)) {
      arr.push(...message);
      return arr.length;
    } else arr.push(message);
    return arr;
  };
  res.render = ((r) =>
    function (...args) {
      // Retrieve and clear all flash messages for this render
      const raw = req.flash();

      // Normalize to arrays of { msg } objects to match express-flash contract
      const messages = {};
      for (const [type, list] of Object.entries(raw)) {
        const arr = Array.isArray(list) ? list : [list];
        messages[type] = arr.map((item) => (item && typeof item === 'object' && 'msg' in item ? item : { msg: String(item) }));
      }

      res.locals.messages = messages;
      return r.apply(this, args);
    })(res.render);
  next();
};


================================================
FILE: config/morgan.js
================================================
const logger = require('morgan');
const Bowser = require('bowser');

// Color definitions for console output
const colors = {
  red: '\x1b[31m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  cyan: '\x1b[36m',
  reset: '\x1b[0m',
};

// Custom colored status token
logger.token('colored-status', (req, res) => {
  const status = res.statusCode;
  let color;
  if (status >= 500) color = colors.red;
  else if (status >= 400) color = colors.yellow;
  else if (status >= 300) color = colors.cyan;
  else color = colors.green;

  return color + status + colors.reset;
});

// Custom token for timestamp without timezone offset
logger.token('short-date', () => {
  const now = new Date();
  return now.toLocaleString('sv').replace(',', '');
});

// Custom token for simplified user agent using Bowser
logger.token('parsed-user-agent', (req) => {
  const userAgent = req.headers['user-agent'];
  if (!userAgent) return 'Unknown';
  const parsedUA = Bowser.parse(userAgent);
  const osName = parsedUA.os.name || 'Unknown';
  const browserName = parsedUA.browser.name || 'Unknown';

  // Get major version number
  const version = parsedUA.browser.version || '';
  const majorVersion = version.split('.')[0];

  return `${osName}/${browserName} v${majorVersion}`;
});

// Track bytes actually sent
logger.token('bytes-sent', (req, res) => {
  // Check for original uncompressed size first
  let length =
    res.getHeader('X-Original-Content-Length') || // Some compression middlewares add this
    res.get('x-content-length') || // Alternative header
    res.getHeader('Content-Length');

  // For static files
  if (!length && res.locals && res.locals.stat) {
    length = res.locals.stat.size;
  }

  // For response bodies (API responses)
  if (!length && res._contentLength) {
    length = res._contentLength;
  }

  // If we found a length, format it
  if (length && Number.isNaN(Number(length)) === false) {
    return `${(parseInt(length, 10) / 1024).toFixed(2)}KB`;
  }

  // For chunked responses
  const transferEncoding = res.getHeader('Transfer-Encoding');
  if (transferEncoding === 'chunked') {
    return 'chunked';
  }

  return '-';
});

// Track partial response info
logger.token('transfer-state', (req, res) => {
  if (!res._header) return 'NO_RESPONSE';
  if (res.finished) return 'COMPLETE';
  return 'PARTIAL';
});

// Define the custom request log format
// In development/test environments, include the full IP address in the logs to facilitate debugging,
// especially when collaborating with other developers testing the running instance.
// In production, omit the IP address to reduce the risk of leaking sensitive information and to support
// compliance with GDPR and other privacy regulations.
// Also using a function so we can test it in our unit tests.
const getMorganFormat = () =>
  process.env.NODE_ENV === 'production' ? ':short-date :method :url :colored-status :response-time[0]ms :bytes-sent :transfer-state - :parsed-user-agent' : ':short-date :method :url :colored-status :response-time[0]ms :bytes-sent :transfer-state :remote-addr :parsed-user-agent';

// Set the format once at initialization for the actual middleware so we don't have to evaluate on each call
const morganFormat = getMorganFormat();

// Create a middleware to capture original content length
const captureContentLength = (req, res, next) => {
  const originalWrite = res.write;
  const originalEnd = res.end;
  let length = 0;

  res.write = (...args) => {
    const [chunk] = args;
    if (chunk) {
      length += chunk.length;
    }
    return originalWrite.apply(res, args);
  };

  res.end = (...args) => {
    const [chunk] = args;
    if (chunk) {
      length += chunk.length;
    }
    if (length > 0) {
      res._contentLength = length;
    }
    return originalEnd.apply(res, args);
  };

  next();
};

exports.morganLogger = () => (req, res, next) => {
  captureContentLength(req, res, () => {
    logger(morganFormat, {
      immediate: false,
    })(req, res, next);
  });
};

// Expose for testing
exports._getMorganFormat = getMorganFormat;


================================================
FILE: config/nodemailer.js
================================================
const nodemailer = require('nodemailer');

/**
 * Helper Function to Send Mail.
 */
exports.sendMail = (settings) => {
  const transportConfig = {
    host: process.env.SMTP_HOST,
    port: 465,
    secure: true,
    auth: {
      user: process.env.SMTP_USER,
      pass: process.env.SMTP_PASSWORD,
    },
  };

  let transporter = nodemailer.createTransport(transportConfig);

  return transporter
    .sendMail(settings.mailOptions)
    .then(() => {
      settings.req.flash(settings.successfulType, { msg: settings.successfulMsg });
    })
    .catch((err) => {
      if (err.message === 'self signed certificate in certificate chain') {
        console.log('WARNING: Self signed certificate in certificate chain. Retrying with the self signed certificate. Use a valid certificate if in production.');
        transportConfig.tls = transportConfig.tls || {};
        transportConfig.tls.rejectUnauthorized = false;
        transporter = nodemailer.createTransport(transportConfig);
        return transporter
          .sendMail(settings.mailOptions)
          .then(() => {
            settings.req.flash(settings.successfulType, { msg: settings.successfulMsg });
          })
          .catch((retryErr) => {
            console.log(settings.loggingError, retryErr);
            settings.req.flash(settings.errorType, { msg: settings.errorMsg });
            return retryErr;
          });
      }
      console.log(settings.loggingError, err);
      settings.req.flash(settings.errorType, { msg: settings.errorMsg });
      return err;
    });
};


================================================
FILE: config/passport.js
================================================
const passport = require('passport');
const refresh = require('passport-oauth2-refresh');
const { Strategy: LocalStrategy } = require('passport-local');
const { Strategy: FacebookStrategy } = require('passport-facebook');
const { Strategy: TwitterStrategy } = require('@passport-js/passport-twitter');
const { Strategy: TwitchStrategy } = require('twitch-passport');
const { Strategy: GitHubStrategy } = require('passport-github2');
const { OAuth2Strategy: GoogleStrategy } = require('passport-google-oauth');
const { SteamOpenIdStrategy } = require('passport-steam-openid');
const { OAuthStrategy } = require('passport-oauth');
const { OAuth2Strategy } = require('passport-oauth');
const { OAuth } = require('oauth');
const validator = require('validator');

const User = require('../models/User');

passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
  try {
    return done(null, await User.findById(id));
  } catch (error) {
    return done(error);
  }
});

/**
 * Sign in using Email and Password.
 */
passport.use(
  new LocalStrategy({ usernameField: 'email' }, (email, password, done) => {
    User.findOne({ email: { $eq: email.toLowerCase() } })
      .then((user) => {
        if (!user) {
          return done(null, false, { msg: `Email ${email} not found.` });
        }
        if (!user.password) {
          return done(null, false, {
            msg: 'Your account was created with a sign-in provider. You can log in using the provider or an email link. To enable email and password login, set a new password in your profile settings.',
          });
        }
        user.comparePassword(password, (err, isMatch) => {
          if (err) {
            return done(err);
          }
          if (isMatch) {
            return done(null, user);
          }
          return done(null, false, { msg: 'Invalid email or password.' });
        });
      })
      .catch((err) => done(err));
  }),
);

/**
 * OAuth Strategy Overview
 *
 * - User is already logged in.
 *   - Check if there is an existing account with a provider id.
 *     - If there is, return an error message. (Account merging not supported)
 *     - Else link new OAuth account with currently logged-in user.
 * - User is not logged in.
 *   - Check if it's a returning user.
 *     - If returning user, sign in and we are done.
 *     - Else check if there is an existing account with user's email.
 *       - If there is, return an error message.
 *       - Else create a new account.
 */

/**
 * Helper function that contains the shared post-profile OAuth logic
 * (supports OAuth 1.0a and OAuth 2.0 providers).
 * Returns User (new or updated) on success or throws Error on failure.
 */
async function handleAuthLogin(req, accessToken, refreshToken, providerName, params, providerProfile, sessionAlreadyLoggedIn, tokenSecret, oauth2provider, tokenConfig = {}, refreshTokenExpiration = null) {
  if (sessionAlreadyLoggedIn) {
    const existingUser = await User.findOne({
      [providerName]: { $eq: providerProfile.id },
    });
    if (existingUser && existingUser.id !== req.user.id) {
      throw new Error('PROVIDER_COLLISION');
    }
    let user;
    if (oauth2provider) {
      user = await saveOAuth2UserTokens(req, accessToken, refreshToken, params.expires_in, refreshTokenExpiration, providerName, tokenConfig);
    } else {
      user = await User.findById(req.user.id);
      user.tokens.push({ kind: providerName, accessToken, ...(tokenSecret && { tokenSecret }) });
    }
    user[providerName] = providerProfile.id;
    user.profile.name = user.profile.name || providerProfile.name;
    user.profile.gender = user.profile.gender || providerProfile.gender;

    if (providerProfile.picture) {
      if (!user.profile.pictures || user.profile.pictureSource === undefined) {
        // legacy account (pre-multi-picture support)
        user.profile.pictures = new Map();
        user.profile.picture = providerProfile.picture;
        user.profile.pictureSource = providerName;
      }
      user.profile.pictures.set(providerName, providerProfile.picture);
      if (user.profile.pictureSource === 'gravatar') {
        user.profile.picture = providerProfile.picture;
        user.profile.pictureSource = providerName;
      }
    }

    user.profile.location = user.profile.location || providerProfile.location;
    user.profile.website = user.profile.website || providerProfile.website;
    user.profile.email = user.profile.email || providerProfile.email;
    await user.save();
    return user;
  }
  // User is not logged in:
  const existingUser = await User.findOne({ [providerName]: { $eq: providerProfile.id } });
  if (existingUser) {
    return existingUser;
  }
  const normalizedEmail = providerProfile.email ? validator.normalizeEmail(providerProfile.email, { gmail_remove_dots: false }) : undefined;
  if (!normalizedEmail) {
    throw new Error('EMAIL_REQUIRED');
  }
  const existingEmailUser = await User.findOne({
    email: { $eq: normalizedEmail },
  });
  if (existingEmailUser) {
    throw new Error('EMAIL_COLLISION');
  }
  const user = new User();
  user.email = normalizedEmail;
  user[providerName] = providerProfile.id;
  req.user = user;
  if (oauth2provider) {
    await saveOAuth2UserTokens(req, accessToken, refreshToken, params.expires_in, refreshTokenExpiration, providerName, tokenConfig);
  } else {
    user.tokens.push({ kind: providerName, accessToken, ...(tokenSecret && { tokenSecret }) });
  }
  user.profile.name = providerProfile.name;
  user.profile.gender = providerProfile.gender;

  if (providerProfile.picture) {
    user.profile.pictures = new Map();
    user.profile.pictures.set(providerName, providerProfile.picture);
    user.profile.picture = providerProfile.picture;
    user.profile.pictureSource = providerName;
  }

  user.profile.location = providerProfile.location;
  user.profile.website = providerProfile.website;
  user.profile.email = providerProfile.email;
  await user.save();
  return user;
}

/**
 * Helper function to handle OAuth errors with provider-specific messages.
 * Returns true if error was handled, false otherwise.
 */
function authError2Flash(err, req, done, providerDisplayName) {
  if (err.message === 'PROVIDER_COLLISION') {
    req.flash('errors', { msg: `There is another account in our system linked to your ${providerDisplayName} account. Please delete the duplicate account before linking ${providerDisplayName} to your current account.` });
    if (req.session) req.session.returnTo = undefined;
    done(null, req.user);
    return true;
  }
  if (err.message === 'EMAIL_COLLISION') {
    req.flash('errors', { msg: `Unable to sign in with ${providerDisplayName} at this time. If you have an existing account in our system, please sign in by email and link your account to ${providerDisplayName} in your user profile settings.` });
    done(null, false);
    return true;
  }
  if (err.message === 'EMAIL_REQUIRED') {
    req.flash('errors', { msg: `Unable to sign in with ${providerDisplayName}. No email address was provided for account creation.` });
    done(null, false);
    return true;
  }
  return false;
}

/**
 * Common function to handle OAuth2 token processing and saving user data.
 *
 * This function is to handle various scenarios that we would run into when it comes to
 * processing the OAuth2 tokens and saving the user data.
 *
 * If we have an existing tokens:
 *    - Updates the access token
 *    - Updates access token expiration if provided
 *    - Updates refresh token if provided
 *    - Updates refresh token expiration if provided
 *    - Removes expiration dates if new tokens don't have them
 *
 * If no tokens exists:
 *    - Creates new token entry with provided tokens and expirations
 */
async function saveOAuth2UserTokens(req, accessToken, refreshToken, accessTokenExpiration, refreshTokenExpiration, providerName, tokenConfig = {}) {
  try {
    let user = await User.findById(req.user._id);
    if (!user) {
      // If user is not found in DB, use the one from the request because we are creating a new user
      ({ user } = req);
    }
    const providerToken = user.tokens.find((token) => token.kind === providerName);
    if (providerToken) {
      providerToken.accessToken = accessToken;
      if (accessTokenExpiration) {
        providerToken.accessTokenExpires = new Date(Date.now() + accessTokenExpiration * 1000).toISOString();
      } else {
        delete providerToken.accessTokenExpires;
      }
      if (refreshToken) {
        providerToken.refreshToken = refreshToken;
      }
      if (refreshTokenExpiration) {
        providerToken.refreshTokenExpires = new Date(Date.now() + refreshTokenExpiration * 1000).toISOString();
      } else if (refreshToken) {
        // Only delete refresh token expiration if we got a new refresh token and don't have an expiration for it
        delete providerToken.refreshTokenExpires;
      }
    } else {
      const newToken = {
        kind: providerName,
        accessToken,
        ...(accessTokenExpiration && {
          accessTokenExpires: new Date(Date.now() + accessTokenExpiration * 1000).toISOString(),
        }),
        ...(refreshToken && { refreshToken }),
        ...(refreshTokenExpiration && {
          refreshTokenExpires: new Date(Date.now() + refreshTokenExpiration * 1000).toISOString(),
        }),
      };
      user.tokens.push(newToken);
    }

    if (tokenConfig) {
      Object.assign(user, tokenConfig);
    }

    user.markModified('tokens');
    await user.save();
    return user;
  } catch (err) {
    throw new Error(err);
  }
}

/**
 * Sign in with Facebook.
 */

FacebookStrategy.prototype.authorizationParams = function () {
  return { auth_type: 'rerequest' };
};

passport.use(
  new FacebookStrategy(
    {
      clientID: process.env.FACEBOOK_ID,
      clientSecret: process.env.FACEBOOK_SECRET,
      callbackURL: `${process.env.BASE_URL}/auth/facebook/callback`,
      profileFields: ['name', 'email', 'link', 'locale', 'timezone', 'gender'],
      scope: ['public_profile', 'email'],
      state: true,
      passReqToCallback: true,
    },
    async (req, accessToken, refreshToken, params, profile, done) => {
      // Facebook does not provide a refresh token but includes an expiration for the access token
      try {
        const providerProfile = {
          id: profile.id,
          name: `${profile.name.givenName} ${profile.name.familyName}`,
          gender: profile._json.gender,
          picture: `https://graph.facebook.com/${profile.id}/picture?type=large`,
          location: profile._json.location ? profile._json.location.name : '',
          email: profile._json.email,
        };
        try {
          const sessionAlreadyLoggedIn = !!req.user;
          const user = await handleAuthLogin(req, accessToken, null, 'facebook', params, providerProfile, sessionAlreadyLoggedIn, null, true);
          if (sessionAlreadyLoggedIn && req.user.id === user.id) {
            req.flash('info', { msg: 'Facebook account has been linked.' });
          }
          return done(null, user);
        } catch (err) {
          if (authError2Flash(err, req, done, 'Facebook')) return;
          throw err;
        }
      } catch (err) {
        return done(err);
      }
    },
  ),
);

/**
 * Sign in with GitHub.
 */
passport.use(
  new GitHubStrategy(
    {
      clientID: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
      callbackURL: `${process.env.BASE_URL}/auth/github/callback`,
      state: true,
      passReqToCallback: true,
      scope: ['user:email'],
    },
    async (req, accessToken, refreshToken, params, profile, done) => {
      // GitHub does not provide a refresh token or an expiration
      try {
        // Github may return a list of email addresses instead of just one
        // Sort by primary, then by verified, then pick the first one in the list
        const sortedEmails = (profile.emails || []).slice().sort((a, b) => {
          if (b.primary !== a.primary) return b.primary - a.primary;
          if (b.verified !== a.verified) return b.verified - a.verified;
          return 0;
        });
        const providerProfile = {
          id: profile.id,
          name: profile.displayName,
          picture: profile._json.avatar_url,
          location: profile._json.location,
          website: profile._json.blog,
          email: sortedEmails.length > 0 ? sortedEmails[0].value : null,
        };
        try {
          const sessionAlreadyLoggedIn = !!req.user;
          const user = await handleAuthLogin(req, accessToken, null, 'github', params, providerProfile, sessionAlreadyLoggedIn, null, true);
          if (sessionAlreadyLoggedIn && req.user.id === user.id) {
            req.flash('info', { msg: 'GitHub account has been linked.' });
          }
          return done(null, user);
        } catch (err) {
          if (authError2Flash(err, req, done, 'GitHub')) return;
          throw err;
        }
      } catch (err) {
        return done(err);
      }
    },
  ),
);

/**
 * Sign in with X.
 */
passport.use(
  new TwitterStrategy(
    {
      consumerKey: process.env.X_KEY,
      consumerSecret: process.env.X_SECRET,
      callbackURL: `${process.env.BASE_URL}/auth/x/callback`,
      state: true,
      passReqToCallback: true,
    },
    async (req, accessToken, tokenSecret, profile, done) => {
      try {
        // X will not provide an email address.  Period.
        // But a person's X username is guaranteed to be unique
        // so we can "fake" placeholder X email address as follows:
        const providerProfile = {
          id: profile.id,
          name: profile.displayName,
          location: profile._json.location,
          picture: profile._json.profile_image_url_https,
          email: `${profile.username}@placeholder-x.email`,
        };
        try {
          const sessionAlreadyLoggedIn = !!req.user;
          const user = await handleAuthLogin(req, accessToken, null, 'x', {}, providerProfile, sessionAlreadyLoggedIn, tokenSecret, false);
          if (sessionAlreadyLoggedIn && req.user.id === user.id) {
            req.flash('info', { msg: 'X account has been linked.' });
          }
          return done(null, user);
        } catch (err) {
          if (authError2Flash(err, req, done, 'X')) return;
          throw err;
        }
      } catch (err) {
        return done(err);
      }
    },
  ),
);

/**
 * Sign in with Google.
 */
const googleStrategyConfig = new GoogleStrategy(
  {
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: '/auth/google/callback',
    scope: ['profile', 'email', 'https://www.googleapis.com/auth/drive.metadata.readonly', 'https://www.googleapis.com/auth/spreadsheets.readonly'],
    accessType: 'offline',
    prompt: 'consent',
    state: true,
    passReqToCallback: true,
  },
  async (req, accessToken, refreshToken, params, profile, done) => {
    try {
      const providerProfile = {
        id: profile.id,
        name: profile.displayName,
        gender: profile._json.gender,
        picture: profile._json.picture,
        email: profile.emails && profile.emails[0] && profile.emails[0].value ? profile.emails[0].value : undefined,
      };
      try {
        const sessionAlreadyLoggedIn = !!req.user;
        const user = await handleAuthLogin(req, accessToken, refreshToken, 'google', params, providerProfile, sessionAlreadyLoggedIn, null, true);
        if (sessionAlreadyLoggedIn && req.user.id === user.id) {
          req.flash('info', { msg: 'Google account has been linked.' });
        }
        return done(null, user);
      } catch (err) {
        if (authError2Flash(err, req, done, 'Google')) return;
        throw err;
      }
    } catch (err) {
      return done(err);
    }
  },
);
passport.use('google', googleStrategyConfig);
refresh.use('google', googleStrategyConfig);

/**
 * Sign in with LinkedIn using OAuth2.
 */
const linkedinStrategyConfig = new OAuth2Strategy(
  {
    authorizationURL: 'https://www.linkedin.com/oauth/v2/authorization',
    tokenURL: 'https://www.linkedin.com/oauth/v2/accessToken',
    clientID: process.env.LINKEDIN_ID,
    clientSecret: process.env.LINKEDIN_SECRET,
    callbackURL: `${process.env.BASE_URL}/auth/linkedin/callback`,
    scope: ['openid', 'profile', 'email'].join(' '),
    state: true,
    passReqToCallback: true,
  },
  async (req, accessToken, refreshToken, params, profile, done) => {
    const sessionAlreadyLoggedIn = !!req.user;
    try {
      // Fetch LinkedIn profile using accessToken
      const response = await fetch('https://api.linkedin.com/v2/userinfo', {
        headers: { Authorization: `Bearer ${accessToken}` },
      });
      if (!response.ok) {
        return done(new Error('Failed to fetch LinkedIn profile'));
      }
      const linkedinProfile = await response.json();
      if (!linkedinProfile || !linkedinProfile.sub || !linkedinProfile.name) {
        req.flash('errors', { msg: 'Invalid LinkedIn profile data' });
        return sessionAlreadyLoggedIn ? done(null, req.user) : done(null, false);
      }
      const providerProfile = {
        id: linkedinProfile.sub,
        name: linkedinProfile.name,
        picture: linkedinProfile.picture || undefined,
        email: linkedinProfile.email,
      };
      try {
        const user = await handleAuthLogin(req, accessToken, refreshToken, 'linkedin', params, providerProfile, sessionAlreadyLoggedIn, null, true);
        if (sessionAlreadyLoggedIn && req.user.id === user.id) {
          req.flash('info', { msg: 'LinkedIn account has been linked.' });
        }
        return done(null, user);
      } catch (err) {
        if (authError2Flash(err, req, done, 'LinkedIn')) return;
        throw err;
      }
    } catch (err) {
      return done(err);
    }
  },
);
passport.use('linkedin', linkedinStrategyConfig);
refresh.use('linkedin', linkedinStrategyConfig);

/**
 * Sign in with Microsoft using OAuth2Strategy.
 */
const microsoftStrategyConfig = new OAuth2Strategy(
  {
    authorizationURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
    tokenURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
    clientID: process.env.MICROSOFT_CLIENT_ID,
    clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
    callbackURL: `${process.env.BASE_URL}/auth/microsoft/callback`,
    // Note: To get a refresh token, add 'offline_access' to the scope list.
    // Trade-off: Users will see a permission approval screen every time they login with 'offline_access' in scope.
    scope: ['openid', 'profile', 'email', 'User.Read'].join(' '),
    state: true,
    passReqToCallback: true,
  },
  async (req, accessToken, refreshToken, params, profile, done) => {
    const sessionAlreadyLoggedIn = !!req.user;
    try {
      // Fetch Microsoft profile using accessToken
      const response = await fetch('https://graph.microsoft.com/v1.0/me', {
        headers: { Authorization: `Bearer ${accessToken}` },
      });
      if (!response.ok) {
        return done(new Error('Failed to fetch Microsoft profile'));
      }
      const microsoftProfile = await response.json();
      if (!microsoftProfile || !microsoftProfile.id || !microsoftProfile.displayName) {
        req.flash('errors', { msg: 'Invalid Microsoft profile data' });
        return sessionAlreadyLoggedIn ? done(null, req.user) : done(null, false);
      }
      const providerProfile = {
        id: microsoftProfile.id,
        name: microsoftProfile.displayName,
        email: microsoftProfile.mail || microsoftProfile.userPrincipalName,
      };
      try {
        const user = await handleAuthLogin(req, accessToken, refreshToken, 'microsoft', params, providerProfile, sessionAlreadyLoggedIn, null, true, {}, params.refresh_token_expires_in);
        if (sessionAlreadyLoggedIn && req.user.id === user.id) {
          req.flash('info', { msg: 'Microsoft account has been linked.' });
        }
        return done(null, user);
      } catch (err) {
        if (authError2Flash(err, req, done, 'Microsoft')) return;
        throw err;
      }
    } catch (err) {
      return done(err);
    }
  },
);
passport.use('microsoft', microsoftStrategyConfig);
refresh.use('microsoft', microsoftStrategyConfig);

/**
 * Twitch API OAuth.
 */
const twitchStrategyConfig = new TwitchStrategy(
  {
    clientID: process.env.TWITCH_CLIENT_ID,
    clientSecret: process.env.TWITCH_CLIENT_SECRET,
    callbackURL: `${process.env.BASE_URL}/auth/twitch/callback`,
    scope: ['user:read:email', 'channel:read:subscriptions', 'moderator:read:followers'],
    state: true,
    passReqToCallback: true,
  },
  async (req, accessToken, refreshToken, params, profile, done) => {
    try {
      const providerProfile = {
        id: profile.id,
        name: profile.display_name,
        email: profile?._json?.data?.[0]?.email ?? profile?.email ?? null,
        picture: profile.profile_image_url,
      };
      try {
        const sessionAlreadyLoggedIn = !!req.user;
        const user = await handleAuthLogin(req, accessToken, refreshToken, 'twitch', params, providerProfile, sessionAlreadyLoggedIn, null, true);
        if (sessionAlreadyLoggedIn && req.user.id === user.id) {
          req.flash('info', { msg: 'Twitch account has been linked.' });
        }
        return done(null, user);
      } catch (err) {
        if (authError2Flash(err, req, done, 'Twitch')) return;
        throw err;
      }
    } catch (err) {
      return done(err);
    }
  },
);
passport.use('twitch', twitchStrategyConfig);
refresh.use('twitch', twitchStrategyConfig);

/**
 * Tumblr API OAuth.
 */
passport.use(
  'tumblr',
  new OAuthStrategy(
    {
      requestTokenURL: 'https://www.tumblr.com/oauth/request_token',
      accessTokenURL: 'https://www.tumblr.com/oauth/access_token',
      userAuthorizationURL: 'https://www.tumblr.com/oauth/authorize',
      consumerKey: process.env.TUMBLR_KEY,
      consumerSecret: process.env.TUMBLR_SECRET,
      callbackURL: '/auth/tumblr/callback',
      state: true,
      passReqToCallback: true,
    },
    async (req, token, tokenSecret, profile, done) => {
      try {
        if (!token || !tokenSecret) {
          throw new Error('Missing or invalid token/tokenSecret');
        }
        // Helper function to generate the OAuth 1.0a authHeader for Tumblr API.
        // This function is not going to make any actual calls to
        // tumblr's /request_token or /access_token endpoints.
        function getTumblrAuthHeader(url, method) {
          const oauth = new OAuth('https://www.tumblr.com/oauth/request_token', 'https://www.tumblr.com/oauth/access_token', process.env.TUMBLR_KEY, process.env.TUMBLR_SECRET, '1.0A', null, 'HMAC-SHA1');
          return oauth.authHeader(url, token, tokenSecret, method);
        }
        const userInfoURL = 'https://api.tumblr.com/v2/user/info';
        const response = await fetch(userInfoURL, { headers: { Authorization: getTumblrAuthHeader(userInfoURL, 'GET') } });
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        // Extract user info from the API response
        const tumblrUser = data.response.user;
        const primaryBlog = tumblrUser.blogs?.find((blog) => blog.primary) || tumblrUser.blogs?.[0];
        const providerProfile = {
          id: primaryBlog.uuid || tumblrUser.name,
          name: tumblrUser.name,
          picture: primaryBlog?.avatar?.[0]?.url,
          website: primaryBlog?.url,
        };
        try {
          const sessionAlreadyLoggedIn = !!req.user;
          const user = await handleAuthLogin(req, token, null, 'tumblr', {}, providerProfile, sessionAlreadyLoggedIn, tokenSecret, false);
          if (sessionAlreadyLoggedIn && req.user.id === user.id) {
            req.flash('info', { msg: 'Tumblr account has been linked.' });
          }
          return done(null, user);
        } catch (err) {
          if (authError2Flash(err, req, done, 'Tumblr')) return;
          throw err;
        }
      } catch (err) {
        if (err.response) {
          // Log API response error details for debugging
          console.error('Tumblr API Error:', {
            status: err.response.status,
            headers: err.response.headers,
            data: err.response.data,
          });
        } else {
          console.error('Unexpected Error:', err.message);
        }
        return done(err);
      }
    },
  ),
);

/**
 * Steam API OpenID.
 */
passport.use(
  new SteamOpenIdStrategy(
    {
      apiKey: process.env.STEAM_KEY,
      returnURL: `${process.env.BASE_URL}/auth/steam/callback`,
      profile: true,
      state: true,
    },
    async (req, identifier, profile, done) => {
      const steamId = identifier.match(/\d+$/)[0];
      const profileURL = `http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${process.env.STEAM_KEY}&steamids=${steamId}`;
      const sessionAlreadyLoggedIn = !!req.user;
      // Fetch Steam profile data
      let providerProfile;
      try {
        const response = await fetch(profileURL);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        const players = data && data.response && Array.isArray(data.response.players) ? data.response.players : [];
        if (players.length === 0) {
          req.flash('errors', { msg: 'Invalid Steam profile data' });
          return sessionAlreadyLoggedIn ? done(null, req.user) : done(null, false);
        }
        providerProfile = {
          id: steamId,
          name: data.response.players[0].personaname,
          picture: data.response.players[0].avatarmedium,
        };
      } catch (err) {
        console.log(err);
        return done(err);
      }

      try {
        const user = await handleAuthLogin(req, steamId, null, 'steam', {}, providerProfile, sessionAlreadyLoggedIn, null, false);
        if (sessionAlreadyLoggedIn && req.user.id === user.id) {
          req.flash('info', { msg: 'Steam account has been linked.' });
        }
        return done(null, user);
      } catch (err) {
        if (authError2Flash(err, req, done, 'Steam')) return;
        return done(err);
      }
    },
  ),
);

/**
 * Intuit/QuickBooks API OAuth.
 */
const quickbooksStrategyConfig = new OAuth2Strategy(
  {
    authorizationURL: 'https://appcenter.intuit.com/connect/oauth2',
    tokenURL: 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer',
    clientID: process.env.QUICKBOOKS_CLIENT_ID,
    clientSecret: process.env.QUICKBOOKS_CLIENT_SECRET,
    callbackURL: `${process.env.BASE_URL}/auth/quickbooks/callback`,
    scope: ['com.intuit.quickbooks.accounting'],
    state: true,
    passReqToCallback: true,
  },
  async (req, accessToken, refreshToken, params, profile, done) => {
    try {
      const user = await saveOAuth2UserTokens(req, accessToken, refreshToken, params.expires_in, params.x_refresh_token_expires_in, 'quickbooks', { quickbooks: req.query.realmId });
      req.flash('info', { msg: 'Quickbooks account has been linked.' });
      return done(null, user);
    } catch (err) {
      return done(err);
    }
  },
);
passport.use('quickbooks', quickbooksStrategyConfig);
refresh.use('quickbooks', quickbooksStrategyConfig);

/**
 * trakt.tv API OAuth.
 */
const traktStrategyConfig = new OAuth2Strategy(
  {
    authorizationURL: 'https://api.trakt.tv/oauth/authorize',
    tokenURL: 'https://api.trakt.tv/oauth/token',
    clientID: process.env.TRAKT_ID,
    clientSecret: process.env.TRAKT_SECRET,
    callbackURL: `${process.env.BASE_URL}/auth/trakt/callback`,
    state: true,
    passReqToCallback: true,
  },
  async (req, accessToken, refreshToken, params, profile, done) => {
    try {
      const response = await fetch('https://api.trakt.tv/users/me?extended=full', {
        method: 'GET',
        headers: {
          Authorization: `Bearer ${accessToken}`,
          'trakt-api-version': 2,
          'trakt-api-key': process.env.TRAKT_ID,
          'Content-Type': 'application/json',
          'User-Agent': 'Hackathon-Starter',
        },
      });
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const data = await response.json();
      if (!data?.ids?.slug || !data?.name) {
        req.flash('errors', { msg: 'Invalid Trakt profile data' });
        return req.user ? done(null, req.user) : done(null, false);
      }
      const providerProfile = {
        id: data.ids.slug,
        name: data.name,
        gender: data.gender,
        picture: data.images?.avatar?.full,
        location: data.location,
      };
      const sessionAlreadyLoggedIn = !!req.user;
      try {
        const user = await handleAuthLogin(req, accessToken, refreshToken, 'trakt', params, providerProfile, sessionAlreadyLoggedIn, null, true, { trakt: data.ids.slug }, params.x_refresh_token_expires_in || null);
        if (sessionAlreadyLoggedIn && req.user.id === user.id) {
          req.flash('info', { msg: 'Trakt account has been linked.' });
        }
        return done(null, user);
      } catch (err) {
        if (authError2Flash(err, req, done, 'Trakt')) return;
        return done(err);
      }
    } catch (err) {
      return done(err);
    }
  },
);
passport.use('trakt', traktStrategyConfig);
refresh.use('trakt', traktStrategyConfig);

/**
 * Sign in with Discord using OAuth2Strategy.
 */
const discordStrategyConfig = new OAuth2Strategy(
  {
    authorizationURL: 'https://discord.com/api/oauth2/authorize',
    tokenURL: 'https://discord.com/api/oauth2/token',
    clientID: process.env.DISCORD_CLIENT_ID,
    clientSecret: process.env.DISCORD_CLIENT_SECRET,
    callbackURL: `${process.env.BASE_URL}/auth/discord/callback`,
    scope: ['identify', 'email'].join(' '),
    state: true,
    passReqToCallback: true,
  },
  async (req, accessToken, refreshToken, params, profile, done) => {
    const sessionAlreadyLoggedIn = !!req.user;
    try {
      // Fetch Discord profile using accessToken
      const response = await fetch('https://discord.com/api/users/@me', {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      });
      if (!response.ok) {
        return done(new Error('Failed to fetch Discord profile'));
      }
      const discordProfile = await response.json();
      if (!discordProfile || !discordProfile.id || !discordProfile.username) {
        req.flash('errors', { msg: 'Invalid Discord profile data' });
        return sessionAlreadyLoggedIn ? done(null, req.user) : done(null, false);
      }
      const providerProfile = {
        id: discordProfile.id,
        name: discordProfile.username,
        email: discordProfile.email,
        picture: discordProfile.avatar ? `https://cdn.discordapp.com/avatars/${discordProfile.id}/${discordProfile.avatar}.png` : undefined,
      };
      try {
        const user = await handleAuthLogin(req, accessToken, refreshToken, 'discord', params, providerProfile, sessionAlreadyLoggedIn, null, true);
        if (sessionAlreadyLoggedIn && req.user.id === user.id) {
          req.flash('info', { msg: 'Discord account has been linked.' });
        }
        return done(null, user);
      } catch (err) {
        if (authError2Flash(err, req, done, 'Discord')) return;
        throw err;
      }
    } catch (err) {
      return done(err);
    }
  },
);
passport.use('discord', discordStrategyConfig);
refresh.use('discord', discordStrategyConfig);

/**
 * Token Revocation Config
 *
 * Providers with a revocation endpoint. Used by config/token-revocation.js
 * to revoke tokens on unlink or account deletion.
 *
 * authMethod values:
 *   'body'           – client_id + client_secret + token in form-encoded body
 *   'basic'          – HTTP Basic auth (client_id:client_secret) + token in form body
 *   'token_only'     – only the token in form-encoded body
 *   'client_id_only' – client_id + token in body (no client_secret)
 *   'json_body'      – JSON body with token, client_id, client_secret
 *   'trakt'          – JSON body + trakt-api-key / trakt-api-version headers
 *   'facebook'       – HTTP DELETE with access_token as query param
 *   'github'         – HTTP DELETE with Basic auth + JSON body
 *   'oauth1'         – OAuth 1.0a signed POST (needs consumerKey/consumerSecret)
 */
const providerRevocationConfig = {
  google: {
    revokeURL: 'https://oauth2.googleapis.com/revoke',
    clientId: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    authMethod: 'basic',
  },
  facebook: {
    revokeURL: 'https://graph.facebook.com/me/permissions',
    authMethod: 'facebook',
  },
  github: {
    revokeURL: `https://api.github.com/applications/${process.env.GITHUB_ID}/token`,
    clientId: process.env.GITHUB_ID,
    clientSecret: process.env.GITHUB_SECRET,
    authMethod: 'github',
  },
  x: {
    revokeURL: 'https://api.x.com/1.1/oauth/invalidate_token',
    consumerKey: process.env.X_KEY,
    consumerSecret: process.env.X_SECRET,
    authMethod: 'oauth1',
  },
  linkedin: {
    revokeURL: 'https://www.linkedin.com/oauth/v2/revoke',
    clientId: process.env.LINKEDIN_ID,
    clientSecret: process.env.LINKEDIN_SECRET,
    authMethod: 'body',
  },
  discord: {
    revokeURL: 'https://discord.com/api/oauth2/token/revoke',
    clientId: process.env.DISCORD_CLIENT_ID,
    clientSecret: process.env.DISCORD_CLIENT_SECRET,
    authMethod: 'body',
  },
  twitch: {
    revokeURL: 'https://id.twitch.tv/oauth2/revoke',
    clientId: process.env.TWITCH_CLIENT_ID,
    authMethod: 'client_id_only',
  },
  trakt: {
    revokeURL: 'https://api.trakt.tv/oauth/revoke',
    clientId: process.env.TRAKT_ID,
    clientSecret: process.env.TRAKT_SECRET,
    authMethod: 'trakt',
  },
  quickbooks: {
    revokeURL: 'https://developer.api.intuit.com/v2/oauth2/tokens/revoke',
    clientId: process.env.QUICKBOOKS_CLIENT_ID,
    clientSecret: process.env.QUICKBOOKS_CLIENT_SECRET,
    authMethod: 'basic',
  },
};

exports.providerRevocationConfig = providerRevocationConfig;

/**
 * Login Required middleware.
 */
exports.isAuthenticated = (req, res, next) => {
  if (req.isAuthenticated()) {
    return next();
  }
  req.flash('errors', { msg: 'You need to be logged in to access that page.' });
  res.redirect('/login');
};

/**
 * Authorization Required middleware.
 */
exports.isAuthorized = async (req, res, next) => {
  const provider = req.path.split('/')[2];
  const token = req.user.tokens.find((token) => token.kind === provider);
  if (token) {
    if (token.accessTokenExpires && new Date(token.accessTokenExpires).getTime() < Date.now() - 1 * 60 * 1000) {
      if (token.refreshToken) {
        if (token.refreshTokenExpires && new Date(token.refreshTokenExpires).getTime() < Date.now() - 1 * 60 * 1000) {
          return res.redirect(`/auth/${provider}`);
        }
        try {
          const newTokens = await new Pro
Download .txt
gitextract_js3nbrx3/

├── .gitattributes
├── .github/
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── build.yml
│       └── dependabot-automerge.yml
├── .gitignore
├── .husky/
│   └── pre-commit
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── PROD_CHECKLIST.md
├── README.md
├── SECURITY.md
├── app.js
├── config/
│   ├── flash.js
│   ├── morgan.js
│   ├── nodemailer.js
│   ├── passport.js
│   └── token-revocation.js
├── controllers/
│   ├── ai-agent.js
│   ├── ai.js
│   ├── api.js
│   ├── contact.js
│   ├── home.js
│   ├── user.js
│   └── webauthn.js
├── eslint.config.mjs
├── models/
│   ├── Session.js
│   └── User.js
├── package.json
├── patches/
│   ├── passport+0.7.0.patch
│   ├── passport-oauth1+1.3.0.patch
│   └── passport-oauth2+1.8.0.patch
├── public/
│   ├── css/
│   │   └── main.scss
│   ├── js/
│   │   ├── lib/
│   │   │   └── .gitkeep
│   │   └── main.js
│   ├── privacy-policy.html
│   └── terms-of-use.html
├── test/
│   ├── TESTING.md
│   ├── app-links.test.js
│   ├── app.test.js
│   ├── auth.opt.test.js
│   ├── contact.test.js
│   ├── docs-links.test.js
│   ├── e2e/
│   │   ├── chart.e2e.test.js
│   │   ├── foursquare.e2e.test.js
│   │   ├── giphy.e2e.test.js
│   │   ├── google-maps.e2e.test.js
│   │   ├── here-maps.e2e.test.js
│   │   ├── llm-classifier.e2e.test.js
│   │   ├── lob.e2e.test.js
│   │   ├── nyt.e2e.test.js
│   │   ├── openai-moderation.e2e.test.js
│   │   ├── rag.e2e.test.js
│   │   ├── trakt.e2e.test.js
│   │   └── twilio.e2e.test.js
│   ├── e2e-nokey/
│   │   ├── github-api.e2e.test.js
│   │   ├── lastfm.e2e.test.js
│   │   ├── pubchem.e2e.test.js
│   │   ├── scraping.e2e.test.js
│   │   ├── upload.e2e.test.js
│   │   └── wikipedia.e2e.test.js
│   ├── fixtures/
│   │   ├── GET_https%3A%2F%2Fapi.giphy.com%2Fv1%2Fgifs%2Fsearch%3Fq%3DHappy%26limit%3D20%26offset%3D0%26rating%3Dg%26lang%3Den.json
│   │   ├── GET_https%3A%2F%2Fapi.giphy.com%2Fv1%2Fgifs%2Fsearch%3Fq%3Dfunny%2Bcat%26limit%3D20%26offset%3D0%26rating%3Dg%26lang%3Den.json
│   │   ├── GET_https%3A%2F%2Fapi.github.com%2Frepos%2Fsahat%2Fhackathon-starter%2Fstargazers%3Fper_page%3D10.json
│   │   ├── GET_https%3A%2F%2Fapi.github.com%2Frepos%2Fsahat%2Fhackathon-starter.json
│   │   ├── GET_https%3A%2F%2Fapi.nytimes.com%2Fsvc%2Fbooks%2Fv3%2Flists%2Fcurrent%2Fyoung-adult-hardcover.json.json
│   │   ├── GET_https%3A%2F%2Fapi.trakt.tv%2Fmovies%2Fmercy-2026%3Fextended%3Dfull%252Cimages.json
│   │   ├── GET_https%3A%2F%2Fapi.trakt.tv%2Fmovies%2Ftrending%3Flimit%3D6%26extended%3Dimages.json
│   │   ├── GET_https%3A%2F%2Fen.wikipedia.org%2Fw%2Fapi.php%3Faction%3Dparse%26format%3Djson%26origin%3D_%26page%3DNode.js%26prop%3Dsections.json
│   │   ├── GET_https%3A%2F%2Fen.wikipedia.org%2Fw%2Fapi.php%3Faction%3Dquery%26format%3Djson%26origin%3D_%26list%3Dsearch%26srsearch%3Djavascript%26srlimit%3D10.json
│   │   ├── GET_https%3A%2F%2Fen.wikipedia.org%2Fw%2Fapi.php%3Faction%3Dquery%26format%3Djson%26origin%3D_%26prop%3Dextracts%26explaintext%3D1%26titles%3DNode.js%26exintro%3D1.json
│   │   ├── GET_https%3A%2F%2Fen.wikipedia.org%2Fw%2Fapi.php%3Faction%3Dquery%26format%3Djson%26origin%3D_%26prop%3Dpageimages%257Cpageterms%26titles%3DNode.js%26pithumbsize%3D400.json
│   │   ├── GET_https%3A%2F%2Fplaces-api.foursquare.com%2Fplaces%2F427ea800f964a520b1211fe3.json
│   │   ├── GET_https%3A%2F%2Fplaces-api.foursquare.com%2Fplaces%2Fsearch%3Fll%3D47.609657%252C-122.342148%26limit%3D10.json
│   │   ├── GET_https%3A%2F%2Fpubchem.ncbi.nlm.nih.gov%2Frest%2Fpug%2Fcompound%2Fcid%2F2244%2FJSON.json
│   │   ├── GET_https%3A%2F%2Fpubchem.ncbi.nlm.nih.gov%2Frest%2Fpug%2Fcompound%2Fcid%2F2244%2Fsynonyms%2FJSON.json
│   │   ├── GET_https%3A%2F%2Fpubchem.ncbi.nlm.nih.gov%2Frest%2Fpug_view%2Fdata%2Fcompound%2F2244%2FJSON%3Fheading%3DSafety%2Band%2BHazards.json
│   │   ├── GET_https%3A%2F%2Fpubchem.ncbi.nlm.nih.gov%2Frest%2Fpug_view%2Fdata%2Fcompound%2F2244%2FJSON%3Fheading%3DUse%2Band%2BManufacturing.json
│   │   ├── GET_https%3A%2F%2Fwww.alphavantage.co%2Fquery%3Ffunction%3DTIME_SERIES_DAILY%26symbol%3DMSFT%26outputsize%3Dcompact.json
│   │   ├── POST_https%3A%2F%2Fapi.openai.com%2Fv1%2Fmoderations_624f7df3dc5f.json
│   │   ├── POST_https%3A%2F%2Fapi.openai.com%2Fv1%2Fmoderations_c6b4d54f3bd4.json
│   │   └── fixture_manifest.json
│   ├── flash.test.js
│   ├── models.test.js
│   ├── morgan.test.js
│   ├── nodemailer.test.js
│   ├── passport.test.js
│   ├── playwright.config.js
│   ├── token-revocation.test.js
│   ├── tools/
│   │   ├── fixture-helpers.js
│   │   ├── playwright-start-and-log.js
│   │   ├── server-axios-fixtures.js
│   │   ├── server-fetch-fixtures.js
│   │   ├── simple-link-image-check.js
│   │   └── start-with-memory-db.js
│   └── webauthn.test.js
└── views/
    ├── account/
    │   ├── forgot.pug
    │   ├── login.pug
    │   ├── profile.pug
    │   ├── reset.pug
    │   ├── signup.pug
    │   ├── totp-setup.pug
    │   ├── two-factor.pug
    │   ├── webauthn-login.pug
    │   └── webauthn-register.pug
    ├── ai/
    │   ├── ai-agent.pug
    │   ├── index.pug
    │   ├── llm-camera.pug
    │   ├── llm-classifier.pug
    │   ├── openai-moderation.pug
    │   └── rag.pug
    ├── api/
    │   ├── chart.pug
    │   ├── facebook.pug
    │   ├── foursquare.pug
    │   ├── giphy.pug
    │   ├── github.pug
    │   ├── google-drive.pug
    │   ├── google-maps.pug
    │   ├── google-sheets.pug
    │   ├── here-maps.pug
    │   ├── index.pug
    │   ├── lastfm.pug
    │   ├── lob.pug
    │   ├── nyt.pug
    │   ├── paypal.pug
    │   ├── pubchem.pug
    │   ├── quickbooks.pug
    │   ├── scraping.pug
    │   ├── steam.pug
    │   ├── stripe.pug
    │   ├── trakt.pug
    │   ├── tumblr.pug
    │   ├── twilio.pug
    │   ├── twitch.pug
    │   ├── upload.pug
    │   └── wikipedia.pug
    ├── contact.pug
    ├── home.pug
    ├── layout.pug
    └── partials/
        ├── flash.pug
        ├── footer.pug
        └── header.pug
Download .txt
SYMBOL INDEX (85 symbols across 19 files)

FILE: app.js
  constant RATE_LIMIT_GLOBAL (line 40) | const RATE_LIMIT_GLOBAL = parseInt(process.env.RATE_LIMIT_GLOBAL, 10) ||...
  constant RATE_LIMIT_STRICT (line 41) | const RATE_LIMIT_STRICT = parseInt(process.env.RATE_LIMIT_STRICT, 10) || 5;
  constant RATE_LIMIT_LOGIN (line 42) | const RATE_LIMIT_LOGIN = parseInt(process.env.RATE_LIMIT_LOGIN, 10) || 10;

FILE: config/passport.js
  function handleAuthLogin (line 78) | async function handleAuthLogin(req, accessToken, refreshToken, providerN...
  function authError2Flash (line 162) | function authError2Flash(err, req, done, providerDisplayName) {
  function saveOAuth2UserTokens (line 198) | async function saveOAuth2UserTokens(req, accessToken, refreshToken, acce...
  function getTumblrAuthHeader (line 600) | function getTumblrAuthHeader(url, method) {

FILE: config/token-revocation.js
  function generateOAuth1Header (line 4) | function generateOAuth1Header(method, url, consumerKey, consumerSecret, ...
  constant REQUIRED_FIELDS (line 29) | const REQUIRED_FIELDS = {
  constant REVOKE_TIMEOUT_MS (line 41) | const REVOKE_TIMEOUT_MS = 8000;
  function revokeToken (line 43) | async function revokeToken(revokeURL, token, tokenTypeHint, config, toke...
  function revokeProviderTokens (line 130) | async function revokeProviderTokens(providerName, tokenData) {
  function revokeAllProviderTokens (line 143) | async function revokeAllProviderTokens(tokens) {

FILE: controllers/ai-agent.js
  constant MAX_MESSAGE_LENGTH (line 10) | const MAX_MESSAGE_LENGTH = 400;
  constant TEMP_SESSION_PREFIX (line 27) | const TEMP_SESSION_PREFIX = 'temp_';
  function getCheckpointer (line 109) | async function getCheckpointer() {
  function sendSSE (line 177) | function sendSSE(res, eventType, data) {
  function extractAIMessages (line 185) | function extractAIMessages(data) {
  function extractStatus (line 202) | function extractStatus(data) {
  function createAIAgent (line 608) | async function createAIAgent() {

FILE: controllers/ai.js
  constant RAG_CHUNKS (line 31) | const RAG_CHUNKS = 'rag_chunks';
  constant LLM_SEMANTIC_CACHE (line 32) | const LLM_SEMANTIC_CACHE = 'llm_sem_cache';
  function prepareRagFolder (line 43) | function prepareRagFolder() {
  function initializeEmbeddingCaches (line 58) | function initializeEmbeddingCaches(mongoUri) {
  function normalizeTextForCaching (line 77) | function normalizeTextForCaching(text) {
  function createCacheKey (line 84) | function createCacheKey(text, modelName, prefix) {
  function createCachedEmbeddings (line 94) | function createCachedEmbeddings(baseEmbeddings, modelName) {
  function createCollectionForVectorSearch (line 152) | async function createCollectionForVectorSearch(db, collectionName, index...
  function setupRagCollection (line 182) | async function setupRagCollection(db) {
  function setVectorIndex (line 199) | async function setVectorIndex(collection, indexDefinition) {
  function configureVectorIndex (line 217) | async function configureVectorIndex(db) {

FILE: controllers/api.js
  function getTumblrAuthHeader (line 81) | function getTumblrAuthHeader(url, method) {
  function getPayPalAccessToken (line 918) | async function getPayPalAccessToken() {
  constant TRAKT_IMAGE_CACHE_LIMIT (line 1261) | const TRAKT_IMAGE_CACHE_LIMIT = 20;
  function traktUrlToFilename (line 1262) | function traktUrlToFilename(url) {
  function fetchAndCacheTraktImage (line 1272) | async function fetchAndCacheTraktImage(imageUrl) {
  function fetchTraktUserProfile (line 1321) | async function fetchTraktUserProfile(traktToken) {
  function fetchTraktUserHistory (line 1336) | async function fetchTraktUserHistory(traktToken, limit) {
  function fetchTraktTrendingMovies (line 1350) | async function fetchTraktTrendingMovies(limit) {
  function fetchMovieDetails (line 1377) | async function fetchMovieDetails(slug, watchers) {

FILE: controllers/contact.js
  function validateReCAPTCHA (line 4) | async function validateReCAPTCHA(token) {

FILE: controllers/user.js
  function sendPasswordlessLoginLinkIfUserExists (line 165) | async function sendPasswordlessLoginLinkIfUserExists(user, req) {
  function sendPasswordlessSignupLink (line 201) | async function sendPasswordlessSignupLink(user, req) {
  function sendTwoFactorEmail (line 787) | async function sendTwoFactorEmail(email, code, req, successMsg = 'A veri...

FILE: controllers/webauthn.js
  function generateDefaultPublicKey (line 5) | function generateDefaultPublicKey() {
  constant DUMMY_COSE_PUBLIC_KEY (line 16) | const DUMMY_COSE_PUBLIC_KEY = generateDefaultPublicKey();

FILE: models/Session.js
  method removeSessionByUserId (line 14) | removeSessionByUserId(userId) {

FILE: test/contact.test.js
  constant OLD_ENV (line 11) | const OLD_ENV = { ...process.env };
  function setupApp (line 13) | function setupApp(controller) {

FILE: test/e2e-nokey/github-api.e2e.test.js
  function gotoGithubWithRateLimitRetry (line 19) | async function gotoGithubWithRateLimitRetry(sharedPage, request) {

FILE: test/e2e/llm-classifier.e2e.test.js
  function extractQpmFromLog (line 17) | function extractQpmFromLog() {

FILE: test/e2e/rag.e2e.test.js
  function writeMinimalPdf (line 25) | function writeMinimalPdf(filePath) {

FILE: test/morgan.test.js
  method getHeader (line 32) | getHeader(name) {
  method get (line 35) | get(name) {
  method setHeader (line 38) | setHeader(name, value) {

FILE: test/tools/fixture-helpers.js
  constant MANIFEST_PATH (line 5) | const MANIFEST_PATH = path.resolve(__dirname, '..', 'fixtures', 'fixture...
  function hashBody (line 7) | function hashBody(body) {
  function keyFor (line 25) | function keyFor(method, url, body) {
  function registerTestInManifest (line 40) | function registerTestInManifest(testFile) {
  function isInManifest (line 55) | function isInManifest(id) {

FILE: test/tools/server-axios-fixtures.js
  constant FIXTURES_DIR (line 31) | const FIXTURES_DIR = path.resolve(__dirname, '..', 'fixtures');
  function installServerAxiosFixtures (line 33) | function installServerAxiosFixtures({ mode = process.env.API_MODE } = {}) {

FILE: test/tools/server-fetch-fixtures.js
  constant FIXTURES_DIR (line 28) | const FIXTURES_DIR = path.resolve(__dirname, '..', 'fixtures');
  function installServerApiFixtures (line 30) | function installServerApiFixtures({ mode = process.env.API_MODE } = {}) {

FILE: test/tools/simple-link-image-check.js
  constant ROOT (line 15) | const ROOT = process.cwd();
  constant VIEWS_DIR (line 16) | const VIEWS_DIR = path.join(ROOT, 'views');
  constant TARGET_VIEW_DIRS (line 17) | const TARGET_VIEW_DIRS = ['ai', 'api', '', 'accounts', 'partials'];
  constant MARKDOWN_FILES (line 18) | const MARKDOWN_FILES = ['README.md', 'PROD_CHECKLIST.md'];
  constant SKIP_KEYWORDS (line 21) | const SKIP_KEYWORDS = ['localhost', 'example.com', 'ngrok', 'hackathon-s...
  constant DEFAULT_USER_AGENT (line 24) | const DEFAULT_USER_AGENT = 'curl/7.85.0';
  constant TIMEOUT (line 25) | const TIMEOUT = 10000;
  constant BLOCKED_PHRASES (line 27) | const BLOCKED_PHRASES = ['this website is using a security service to pr...
  function extractUrlsFromHtmlLike (line 29) | function extractUrlsFromHtmlLike(text) {
  function extractUrlsFromMarkdown (line 42) | function extractUrlsFromMarkdown(md) {
  function normalizeUrl (line 54) | function normalizeUrl(u) {
  constant RETRIES (line 67) | const RETRIES = 2;
  constant BACKOFF_MS (line 68) | const BACKOFF_MS = 10000;
  function checkUrl (line 70) | async function checkUrl(initialUrl, timeout = TIMEOUT) {
  function scanViews (line 114) | function scanViews() {
  function scanMarkdown (line 161) | function scanMarkdown() {
  function dedupeList (line 178) | function dedupeList(list) {
  function checkList (line 188) | async function checkList(deduped) {
  function run (line 210) | async function run() {
  function getViewsChecks (line 251) | function getViewsChecks() {
  function getMarkdownChecks (line 258) | function getMarkdownChecks() {
Condensed preview — 144 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,891K chars).
[
  {
    "path": ".gitattributes",
    "chars": 19,
    "preview": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 1234,
    "preview": "<!-- IMPORTANT: maintainers may close PRs that fail the checks below without review. -->\n\n## Checklist\n\n- [ ] I acknowle"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 660,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: npm\n    directory: '/'\n    schedule:\n      interval: 'daily'\n    target-branc"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 1684,
    "preview": "name: Node.js CI\n\non:\n  push:\n    branches: ['master']\n  pull_request:\n    branches: ['master']\n\npermissions:\n  contents"
  },
  {
    "path": ".github/workflows/dependabot-automerge.yml",
    "chars": 3916,
    "preview": "name: Dependabot Automerge\n\non:\n  workflow_run:\n    workflows: ['Node.js CI']\n    types: [completed]\n\npermissions:\n  con"
  },
  {
    "path": ".gitignore",
    "chars": 615,
    "preview": "lib-cov\n*.seed\n*.log\n*.csv\n*.dat\n*.out\n*.pid\n*.gz\n*.swp\n\npids\nlogs\nresults\ntmp\n\n# Optional npm cache directory\n.npm\n\n#Bu"
  },
  {
    "path": ".husky/pre-commit",
    "chars": 1247,
    "preview": "#!/bin/sh\n\n# Save the list of currently staged files\nSTAGED_FILES=$(git diff --cached --name-only)\n\n# Check for staged f"
  },
  {
    "path": ".prettierignore",
    "chars": 35,
    "preview": "# Ignore artifacts:\nbuild\ncoverage\n"
  },
  {
    "path": ".prettierrc",
    "chars": 86,
    "preview": "{\n  \"plugins\": [\"@prettier/plugin-pug\"],\n  \"singleQuote\": true,\n  \"printWidth\": 300\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 34144,
    "preview": "# Changelog\n\n---\n\n### 10.0.0 (February 08, 2026)\n\nNew AI and Integration Features\n\n- AI: AI Agent (ReAct: Reasoning+Acti"
  },
  {
    "path": "LICENSE",
    "chars": 1086,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2014-2026 Sahat Yalkabov\n\nPermission is hereby granted, free of charge, to any pers"
  },
  {
    "path": "PROD_CHECKLIST.md",
    "chars": 3766,
    "preview": "If you are done with your hackathon and thinking about launching your project into production, or if you are just using "
  },
  {
    "path": "README.md",
    "chars": 85723,
    "preview": "![](https://lh4.googleusercontent.com/-PVw-ZUM9vV8/UuWeH51os0I/AAAAAAAAD6M/0Ikg7viJftQ/w1286-h566-no/hackathon-starter-l"
  },
  {
    "path": "SECURITY.md",
    "chars": 1255,
    "preview": "# Security Policy\n\n## Supported Versions\n\n| Version | Supported          |\n| ------- | ------------------ |\n| latest  | "
  },
  {
    "path": "app.js",
    "chars": 19994,
    "preview": "/**\n * Module dependencies.\n */\nconst path = require('node:path');\nconst express = require('express');\nconst compression"
  },
  {
    "path": "config/flash.js",
    "chars": 1485,
    "preview": "const { format } = require('node:util');\n\n// Flash Middleware as a replacement for express-flash / connect-flash\n// Thos"
  },
  {
    "path": "config/morgan.js",
    "chars": 4065,
    "preview": "const logger = require('morgan');\nconst Bowser = require('bowser');\n\n// Color definitions for console output\nconst color"
  },
  {
    "path": "config/nodemailer.js",
    "chars": 1554,
    "preview": "const nodemailer = require('nodemailer');\n\n/**\n * Helper Function to Send Mail.\n */\nexports.sendMail = (settings) => {\n "
  },
  {
    "path": "config/passport.js",
    "chars": 36204,
    "preview": "const passport = require('passport');\nconst refresh = require('passport-oauth2-refresh');\nconst { Strategy: LocalStrateg"
  },
  {
    "path": "config/token-revocation.js",
    "chars": 5839,
    "preview": "const crypto = require('node:crypto');\nconst { providerRevocationConfig } = require('./passport');\n\nfunction generateOAu"
  },
  {
    "path": "controllers/ai-agent.js",
    "chars": 23954,
    "preview": "const validator = require('validator');\nconst mongoose = require('mongoose');\nconst { ChatGroq } = require('@langchain/g"
  },
  {
    "path": "controllers/ai.js",
    "chars": 29317,
    "preview": "const crypto = require('node:crypto');\nconst fs = require('node:fs');\nconst path = require('node:path');\nconst multer = "
  },
  {
    "path": "controllers/api.js",
    "chars": 56956,
    "preview": "const crypto = require('node:crypto');\nconst fs = require('node:fs');\nconst path = require('node:path');\nconst cheerio ="
  },
  {
    "path": "controllers/contact.js",
    "chars": 3766,
    "preview": "const validator = require('validator');\nconst nodemailerConfig = require('../config/nodemailer');\n\nasync function valida"
  },
  {
    "path": "controllers/home.js",
    "chars": 149,
    "preview": "/**\n * GET /\n * Home page.\n */\nexports.index = (req, res) => {\n  res.render('home', {\n    title: 'Home',\n    siteURL: pr"
  },
  {
    "path": "controllers/user.js",
    "chars": 38731,
    "preview": "const crypto = require('node:crypto');\nconst passport = require('passport');\nconst validator = require('validator');\ncon"
  },
  {
    "path": "controllers/webauthn.js",
    "chars": 9324,
    "preview": "const crypto = require('node:crypto');\nconst { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenti"
  },
  {
    "path": "eslint.config.mjs",
    "chars": 3205,
    "preview": "import chaiFriendly from 'eslint-plugin-chai-friendly';\nimport globals from 'globals';\nimport eslintConfigPrettier from "
  },
  {
    "path": "models/Session.js",
    "chars": 499,
    "preview": "const mongoose = require('mongoose');\n\nconst sessionSchema = new mongoose.Schema({\n  session: String,\n  expires: Date,\n}"
  },
  {
    "path": "models/User.js",
    "chars": 8201,
    "preview": "const crypto = require('node:crypto');\nconst bcrypt = require('@node-rs/bcrypt');\nconst mongoose = require('mongoose');\n"
  },
  {
    "path": "package.json",
    "chars": 4411,
    "preview": "{\n  \"name\": \"hackathon-starter\",\n  \"version\": \"10.0.0\",\n  \"description\": \"A boilerplate for Node.js web applications\",\n "
  },
  {
    "path": "patches/passport+0.7.0.patch",
    "chars": 1811,
    "preview": "diff --git a/node_modules/passport/lib/sessionmanager.js b/node_modules/passport/lib/sessionmanager.js\nindex 81b59b1..17"
  },
  {
    "path": "patches/passport-oauth1+1.3.0.patch",
    "chars": 3089,
    "preview": "diff --git a/node_modules/passport-oauth1/lib/strategy.js b/node_modules/passport-oauth1/lib/strategy.js\nindex 337c7e8.."
  },
  {
    "path": "patches/passport-oauth2+1.8.0.patch",
    "chars": 4074,
    "preview": "diff --git a/node_modules/passport-oauth2/lib/strategy.js b/node_modules/passport-oauth2/lib/strategy.js\nindex 8575b72.."
  },
  {
    "path": "public/css/main.scss",
    "chars": 1845,
    "preview": "@import 'node_modules/bootstrap/scss/bootstrap';\n@import 'node_modules/bootstrap-social/bootstrap-social.scss';\n@import "
  },
  {
    "path": "public/js/lib/.gitkeep",
    "chars": 73,
    "preview": "# empty gitkeep file to assure creation of public/js/lib directory by git"
  },
  {
    "path": "public/js/main.js",
    "chars": 65,
    "preview": "/* global $ */\n\n$(() => {\n  // Place JavaScript code here...\n});\n"
  },
  {
    "path": "public/privacy-policy.html",
    "chars": 4131,
    "preview": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />"
  },
  {
    "path": "public/terms-of-use.html",
    "chars": 25405,
    "preview": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />"
  },
  {
    "path": "test/TESTING.md",
    "chars": 11525,
    "preview": "# Testing Guide\n\nThis document describes the test organization, fixture system, and how to create and run new tests in t"
  },
  {
    "path": "test/app-links.test.js",
    "chars": 695,
    "preview": "const { expect } = require('chai');\nconst { getViewsChecks, checkList } = require('./tools/simple-link-image-check');\n\nd"
  },
  {
    "path": "test/app.test.js",
    "chars": 2139,
    "preview": "const request = require('supertest');\nconst { MongoMemoryServer } = require('mongodb-memory-server');\n\nlet mongoServer;\n"
  },
  {
    "path": "test/auth.opt.test.js",
    "chars": 4116,
    "preview": "const path = require('node:path');\nconst { expect } = require('chai');\nconst sinon = require('sinon');\nconst mongoose = "
  },
  {
    "path": "test/contact.test.js",
    "chars": 4661,
    "preview": "const { expect } = require('chai');\nconst sinon = require('sinon');\nconst request = require('supertest');\nconst express "
  },
  {
    "path": "test/docs-links.test.js",
    "chars": 705,
    "preview": "const { expect } = require('chai');\nconst { getMarkdownChecks, checkList } = require('./tools/simple-link-image-check');"
  },
  {
    "path": "test/e2e/chart.e2e.test.js",
    "chars": 4157,
    "preview": "const testFileName = 'e2e/chart.e2e.test.js';\nprocess.env.API_TEST_FILE = testFileName;\nconst { test, expect } = require"
  },
  {
    "path": "test/e2e/foursquare.e2e.test.js",
    "chars": 5377,
    "preview": "process.env.API_TEST_FILE = 'e2e/foursquare.e2e.test.js';\nconst { test, expect } = require('@playwright/test');\nconst { "
  },
  {
    "path": "test/e2e/giphy.e2e.test.js",
    "chars": 1973,
    "preview": "process.env.API_TEST_FILE = 'e2e/giphy.e2e.test.js';\nconst { test, expect } = require('@playwright/test');\nconst { regis"
  },
  {
    "path": "test/e2e/google-maps.e2e.test.js",
    "chars": 6552,
    "preview": "const { test, expect } = require('@playwright/test');\n\n// Skip this suite entirely when running in replay/record-fixture"
  },
  {
    "path": "test/e2e/here-maps.e2e.test.js",
    "chars": 2888,
    "preview": "const { test, expect } = require('@playwright/test');\n\n// Skip this suite entirely when running in record/replay fixture"
  },
  {
    "path": "test/e2e/llm-classifier.e2e.test.js",
    "chars": 7125,
    "preview": "process.env.API_TEST_FILE = 'e2e/llm-classifier.e2e.test.js';\nconst { test, expect } = require('@playwright/test');\ncons"
  },
  {
    "path": "test/e2e/lob.e2e.test.js",
    "chars": 4871,
    "preview": "process.env.API_TEST_FILE = 'e2e/lob.e2e.test.js';\nconst { test, expect } = require('@playwright/test');\nconst { registe"
  },
  {
    "path": "test/e2e/nyt.e2e.test.js",
    "chars": 2728,
    "preview": "process.env.API_TEST_FILE = 'e2e/nyt.e2e.test.js';\nconst { test, expect } = require('@playwright/test');\nconst { registe"
  },
  {
    "path": "test/e2e/openai-moderation.e2e.test.js",
    "chars": 2856,
    "preview": "process.env.API_TEST_FILE = 'e2e/openai-moderation.e2e.test.js';\nconst { test, expect } = require('@playwright/test');\nc"
  },
  {
    "path": "test/e2e/rag.e2e.test.js",
    "chars": 10969,
    "preview": "process.env.API_TEST_FILE = 'e2e/rag.e2e.test.js';\nconst { test, expect } = require('@playwright/test');\nconst fs = requ"
  },
  {
    "path": "test/e2e/trakt.e2e.test.js",
    "chars": 9698,
    "preview": "process.env.API_TEST_FILE = 'e2e/trakt.e2e.test.js';\nconst { test, expect } = require('@playwright/test');\nconst { regis"
  },
  {
    "path": "test/e2e/twilio.e2e.test.js",
    "chars": 4482,
    "preview": "process.env.API_TEST_FILE = 'e2e/twilio.e2e.test.js';\nconst { test, expect } = require('@playwright/test');\n\n// Skip thi"
  },
  {
    "path": "test/e2e-nokey/github-api.e2e.test.js",
    "chars": 5280,
    "preview": "process.env.API_TEST_FILE = 'e2e-nokey/github-api.e2e.test.js';\nconst { test, expect } = require('@playwright/test');\nco"
  },
  {
    "path": "test/e2e-nokey/lastfm.e2e.test.js",
    "chars": 10356,
    "preview": "process.env.API_TEST_FILE = 'e2e-nokey/lastfm.e2e.test.js';\nconst { test, expect } = require('@playwright/test');\n\n// Sk"
  },
  {
    "path": "test/e2e-nokey/pubchem.e2e.test.js",
    "chars": 9048,
    "preview": "process.env.API_TEST_FILE = 'e2e-nokey/pubchem.e2e.test.js';\nconst { test, expect } = require('@playwright/test');\nconst"
  },
  {
    "path": "test/e2e-nokey/scraping.e2e.test.js",
    "chars": 1602,
    "preview": "process.env.API_TEST_FILE = 'e2e-nokey/scraping.e2e.test.js';\nconst { test, expect } = require('@playwright/test');\ncons"
  },
  {
    "path": "test/e2e-nokey/upload.e2e.test.js",
    "chars": 13658,
    "preview": "const { test, expect } = require('@playwright/test');\nconst fs = require('fs');\nconst path = require('path');\n\ntest.desc"
  },
  {
    "path": "test/e2e-nokey/wikipedia.e2e.test.js",
    "chars": 3997,
    "preview": "process.env.API_TEST_FILE = 'e2e-nokey/wikipedia.e2e.test.js';\nconst { test, expect } = require('@playwright/test');\ncon"
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fapi.giphy.com%2Fv1%2Fgifs%2Fsearch%3Fq%3DHappy%26limit%3D20%26offset%3D0%26rating%3Dg%26lang%3Den.json",
    "chars": 218238,
    "preview": "{\n  \"data\": [\n    {\n      \"type\": \"gif\",\n      \"id\": \"fUQ4rhUZJYiQsas6WD\",\n      \"url\": \"https://giphy.com/gifs/muppetwi"
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fapi.giphy.com%2Fv1%2Fgifs%2Fsearch%3Fq%3Dfunny%2Bcat%26limit%3D20%26offset%3D0%26rating%3Dg%26lang%3Den.json",
    "chars": 220459,
    "preview": "{\n  \"data\": [\n    {\n      \"type\": \"gif\",\n      \"id\": \"SjvgcbEMEptM0KpvJ4\",\n      \"url\": \"https://giphy.com/gifs/cat-hitt"
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fapi.github.com%2Frepos%2Fsahat%2Fhackathon-starter%2Fstargazers%3Fper_page%3D10.json",
    "chars": 10507,
    "preview": "[\n  {\n    \"login\": \"sahat\",\n    \"id\": 544954,\n    \"node_id\": \"MDQ6VXNlcjU0NDk1NA==\",\n    \"avatar_url\": \"https://avatars."
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fapi.github.com%2Frepos%2Fsahat%2Fhackathon-starter.json",
    "chars": 5882,
    "preview": "{\n  \"id\": 14370955,\n  \"node_id\": \"MDEwOlJlcG9zaXRvcnkxNDM3MDk1NQ==\",\n  \"name\": \"hackathon-starter\",\n  \"full_name\": \"saha"
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fapi.nytimes.com%2Fsvc%2Fbooks%2Fv3%2Flists%2Fcurrent%2Fyoung-adult-hardcover.json.json",
    "chars": 19721,
    "preview": "{\n  \"status\": \"OK\",\n  \"copyright\": \"Copyright (c) 2025 The New York Times Company. All Rights Reserved.\",\n  \"num_results"
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fapi.trakt.tv%2Fmovies%2Fmercy-2026%3Fextended%3Dfull%252Cimages.json",
    "chars": 1997,
    "preview": "{\n  \"ids\": { \"imdb\": \"tt31050594\", \"plex\": { \"guid\": \"65b41a7eb5cab988940e14b1\", \"slug\": \"mercy-2026\" }, \"slug\": \"mercy-"
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fapi.trakt.tv%2Fmovies%2Ftrending%3Flimit%3D6%26extended%3Dimages.json",
    "chars": 11822,
    "preview": "[\n  {\n    \"watchers\": 1765,\n    \"movie\": {\n      \"title\": \"Mercy\",\n      \"year\": 2026,\n      \"ids\": { \"trakt\": 1000015, "
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fen.wikipedia.org%2Fw%2Fapi.php%3Faction%3Dparse%26format%3Djson%26origin%3D_%26page%3DNode.js%26prop%3Dsections.json",
    "chars": 3613,
    "preview": "{\n  \"parse\": {\n    \"title\": \"Node.js\",\n    \"pageid\": 26415635,\n    \"sections\": [\n      { \"toclevel\": 1, \"level\": \"2\", \"l"
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fen.wikipedia.org%2Fw%2Fapi.php%3Faction%3Dquery%26format%3Djson%26origin%3D_%26list%3Dsearch%26srsearch%3Djavascript%26srlimit%3D10.json",
    "chars": 4444,
    "preview": "{\n  \"batchcomplete\": \"\",\n  \"continue\": { \"sroffset\": 10, \"continue\": \"-||\" },\n  \"query\": {\n    \"searchinfo\": { \"totalhit"
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fen.wikipedia.org%2Fw%2Fapi.php%3Faction%3Dquery%26format%3Djson%26origin%3D_%26prop%3Dextracts%26explaintext%3D1%26titles%3DNode.js%26exintro%3D1.json",
    "chars": 1549,
    "preview": "{\n  \"batchcomplete\": \"\",\n  \"query\": {\n    \"pages\": {\n      \"26415635\": {\n        \"pageid\": 26415635,\n        \"ns\": 0,\n  "
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fen.wikipedia.org%2Fw%2Fapi.php%3Faction%3Dquery%26format%3Djson%26origin%3D_%26prop%3Dpageimages%257Cpageterms%26titles%3DNode.js%26pithumbsize%3D400.json",
    "chars": 498,
    "preview": "{\n  \"batchcomplete\": \"\",\n  \"query\": {\n    \"pages\": {\n      \"26415635\": {\n        \"pageid\": 26415635,\n        \"ns\": 0,\n  "
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fplaces-api.foursquare.com%2Fplaces%2F427ea800f964a520b1211fe3.json",
    "chars": 65004,
    "preview": "{\n  \"fsq_place_id\": \"427ea800f964a520b1211fe3\",\n  \"latitude\": 47.609334087942955,\n  \"longitude\": -122.34147579532495,\n  "
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fplaces-api.foursquare.com%2Fplaces%2Fsearch%3Fll%3D47.609657%252C-122.342148%26limit%3D10.json",
    "chars": 86797,
    "preview": "{\n  \"results\": [\n    {\n      \"fsq_place_id\": \"419e8900f964a520301e1fe3\",\n      \"latitude\": 47.6097907988089,\n      \"long"
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fpubchem.ncbi.nlm.nih.gov%2Frest%2Fpug%2Fcompound%2Fcid%2F2244%2FJSON.json",
    "chars": 10101,
    "preview": "{\n  \"PC_Compounds\": [\n    {\n      \"id\": {\n        \"id\": {\n          \"cid\": 2244\n        }\n      },\n      \"atoms\": {\n    "
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fpubchem.ncbi.nlm.nih.gov%2Frest%2Fpug%2Fcompound%2Fcid%2F2244%2Fsynonyms%2FJSON.json",
    "chars": 23259,
    "preview": "{\n  \"InformationList\": {\n    \"Information\": [\n      {\n        \"CID\": 2244,\n        \"Synonym\": [\n          \"aspirin\",\n   "
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fpubchem.ncbi.nlm.nih.gov%2Frest%2Fpug_view%2Fdata%2Fcompound%2F2244%2FJSON%3Fheading%3DSafety%2Band%2BHazards.json",
    "chars": 163721,
    "preview": "{\n  \"Record\": {\n    \"RecordType\": \"CID\",\n    \"RecordNumber\": 2244,\n    \"RecordTitle\": \"Aspirin\",\n    \"Section\": [\n      "
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fpubchem.ncbi.nlm.nih.gov%2Frest%2Fpug_view%2Fdata%2Fcompound%2F2244%2FJSON%3Fheading%3DUse%2Band%2BManufacturing.json",
    "chars": 73330,
    "preview": "{\n  \"Record\": {\n    \"RecordType\": \"CID\",\n    \"RecordNumber\": 2244,\n    \"RecordTitle\": \"Aspirin\",\n    \"Section\": [\n      "
  },
  {
    "path": "test/fixtures/GET_https%3A%2F%2Fwww.alphavantage.co%2Fquery%3Ffunction%3DTIME_SERIES_DAILY%26symbol%3DMSFT%26outputsize%3Dcompact.json",
    "chars": 17561,
    "preview": "{\n  \"Meta Data\": {\n    \"1. Information\": \"Daily Prices (open, high, low, close) and Volumes\",\n    \"2. Symbol\": \"MSFT\",\n "
  },
  {
    "path": "test/fixtures/POST_https%3A%2F%2Fapi.openai.com%2Fv1%2Fmoderations_624f7df3dc5f.json",
    "chars": 1696,
    "preview": "{\n  \"id\": \"modr-4717\",\n  \"model\": \"omni-moderation-latest\",\n  \"results\": [\n    {\n      \"flagged\": false,\n      \"categori"
  },
  {
    "path": "test/fixtures/POST_https%3A%2F%2Fapi.openai.com%2Fv1%2Fmoderations_c6b4d54f3bd4.json",
    "chars": 1689,
    "preview": "{\n  \"id\": \"modr-8183\",\n  \"model\": \"omni-moderation-latest\",\n  \"results\": [\n    {\n      \"flagged\": true,\n      \"categorie"
  },
  {
    "path": "test/fixtures/fixture_manifest.json",
    "chars": 326,
    "preview": "[\n  \"e2e-nokey/github-api.e2e.test.js\",\n  \"e2e-nokey/pubchem.e2e.test.js\",\n  \"e2e-nokey/scraping.e2e.test.js\",\n  \"e2e-no"
  },
  {
    "path": "test/flash.test.js",
    "chars": 4018,
    "preview": "const { expect } = require('chai');\nconst sinon = require('sinon');\nconst { flash } = require('../config/flash');\n\ndescr"
  },
  {
    "path": "test/models.test.js",
    "chars": 36715,
    "preview": "const crypto = require('node:crypto');\nconst { expect } = require('chai');\nconst sinon = require('sinon');\nconst bcrypt "
  },
  {
    "path": "test/morgan.test.js",
    "chars": 4421,
    "preview": "const morgan = require('morgan');\nconst { expect } = require('chai');\nconst sinon = require('sinon');\n\n// Import the mor"
  },
  {
    "path": "test/nodemailer.test.js",
    "chars": 4687,
    "preview": "const { expect } = require('chai');\nconst sinon = require('sinon');\nconst nodemailer = require('nodemailer');\n\ndescribe("
  },
  {
    "path": "test/passport.test.js",
    "chars": 40676,
    "preview": "const path = require('node:path');\nconst { expect } = require('chai');\nconst sinon = require('sinon');\nconst refresh = r"
  },
  {
    "path": "test/playwright.config.js",
    "chars": 3262,
    "preview": "const fs = require('node:fs');\nconst path = require('node:path');\nconst { defineConfig, devices } = require('@playwright"
  },
  {
    "path": "test/token-revocation.test.js",
    "chars": 11380,
    "preview": "const path = require('node:path');\nconst { expect } = require('chai');\nconst sinon = require('sinon');\nprocess.loadEnvFi"
  },
  {
    "path": "test/tools/fixture-helpers.js",
    "chars": 2023,
    "preview": "const crypto = require('node:crypto');\nconst fs = require('node:fs');\nconst path = require('node:path');\n\nconst MANIFEST"
  },
  {
    "path": "test/tools/playwright-start-and-log.js",
    "chars": 1017,
    "preview": "// tools/start-and-log.js\nconst fs = require('node:fs');\nconst path = require('node:path');\nconst { spawn } = require('n"
  },
  {
    "path": "test/tools/server-axios-fixtures.js",
    "chars": 3491,
    "preview": "/**\n * Server-side Axios API Fixture System\n *\n * This module uses Axios interceptors to record and replay HTTP response"
  },
  {
    "path": "test/tools/server-fetch-fixtures.js",
    "chars": 3157,
    "preview": "/**\n * Server-side fetch() API Fixture System\n *\n * This module monkey-patches the global fetch() function to record and"
  },
  {
    "path": "test/tools/simple-link-image-check.js",
    "chars": 9628,
    "preview": "#!/usr/bin/env node\n/*\n Simple link & image checker (no external deps)\n - Scans Pug views under views/ai and views/api f"
  },
  {
    "path": "test/tools/start-with-memory-db.js",
    "chars": 2653,
    "preview": "#!/usr/bin/env node\n// test/helpers/start-with-memory-db.js\n// Starts mongodb-memory-server and then starts the app (req"
  },
  {
    "path": "test/webauthn.test.js",
    "chars": 22412,
    "preview": "/* eslint-disable global-require */\nconst crypto = require('node:crypto');\nconst path = require('node:path');\nconst { ex"
  },
  {
    "path": "views/account/forgot.pug",
    "chars": 667,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h3 Forgot Password\n  form(method='POST')\n    input("
  },
  {
    "path": "views/account/login.pug",
    "chars": 5157,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h3 Sign in\n  form(method='POST')\n    input(type='hi"
  },
  {
    "path": "views/account/profile.pug",
    "chars": 12969,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h3 Profile Information\n\n  form(action='/account/pro"
  },
  {
    "path": "views/account/reset.pug",
    "chars": 929,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h3 Reset Password\n  form(method='POST')\n    input(t"
  },
  {
    "path": "views/account/signup.pug",
    "chars": 2052,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h3 Sign up\n  form#signup-form(method='POST')\n    in"
  },
  {
    "path": "views/account/totp-setup.pug",
    "chars": 1118,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h3 Setup Authenticator App\n\n  .row\n    .col-md-8.of"
  },
  {
    "path": "views/account/two-factor.pug",
    "chars": 1668,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h3 Two-Factor Authentication\n\n  .row\n    .col-md-8."
  },
  {
    "path": "views/account/webauthn-login.pug",
    "chars": 1867,
    "preview": "extends ../layout\n\nblock head\n  script(src='/js/lib/index.umd.min.js')\n  script#webauthnOptions(type='application/json')"
  },
  {
    "path": "views/account/webauthn-register.pug",
    "chars": 1925,
    "preview": "extends ../layout\n\nblock head\n  script(src='/js/lib/index.umd.min.js')\n  script#webauthnOptions(type='application/json')"
  },
  {
    "path": "views/ai/ai-agent.pug",
    "chars": 12206,
    "preview": "extends ../layout\n\nblock content\n  .container.mt-4\n    .row\n      .col-12\n        .d-flex.justify-content-between.align-"
  },
  {
    "path": "views/ai/index.pug",
    "chars": 4169,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h3 AI Integration Examples\n\n  .row.g-3\n    .col-md-"
  },
  {
    "path": "views/ai/llm-camera.pug",
    "chars": 6978,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fas.fa-camera.fa-sm.me-2\n      | Groq Ll"
  },
  {
    "path": "views/ai/llm-classifier.pug",
    "chars": 2311,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fas.fa-network-wired.fa-sm.me-2(style='c"
  },
  {
    "path": "views/ai/openai-moderation.pug",
    "chars": 2852,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fas.fa-robot.fa-sm.me-2(style='color: #1"
  },
  {
    "path": "views/ai/rag.pug",
    "chars": 6621,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fas.fa-search.me-2\n      | Retrieval-Aug"
  },
  {
    "path": "views/api/chart.pug",
    "chars": 1802,
    "preview": "extends ../layout\n\nblock head\n  script(src='/js/lib/chart.umd.js')\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2"
  },
  {
    "path": "views/api/facebook.pug",
    "chars": 1156,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fab.fa-square-facebook.fa-sm.me-2(style="
  },
  {
    "path": "views/api/foursquare.pug",
    "chars": 2279,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fab.fa-foursquare.fa-sm.me-2\n      | Fou"
  },
  {
    "path": "views/api/giphy.pug",
    "chars": 1533,
    "preview": "extends ../layout\n\nblock content\n  h2\n    i.fas.fa-images.fa-sm.me-2\n    | GIPHY API\n\n  .btn-group.d-flex(role='group')\n"
  },
  {
    "path": "views/api/github.pug",
    "chars": 7168,
    "preview": "extends ../layout\n\nblock content\n  h2\n    i.fab.fa-github.fa-sm.me-2\n    | GitHub API\n\n  .btn-group.d-flex(role='group')"
  },
  {
    "path": "views/api/google-drive.pug",
    "chars": 766,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fab.fa-google-drive.fa-sm.me-2\n      | G"
  },
  {
    "path": "views/api/google-maps.pug",
    "chars": 7048,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fas.fa-map.fa-sm.me-2\n      | Google Map"
  },
  {
    "path": "views/api/google-sheets.pug",
    "chars": 1317,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fas.fa-table.fa-sm.me-2(style='color: #1"
  },
  {
    "path": "views/api/here-maps.pug",
    "chars": 3188,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fas.fa-map-location.me-2\n      | HERE Ma"
  },
  {
    "path": "views/api/index.pug",
    "chars": 6728,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h3 API Integration Examples\n\n  .row\n    .col-md-4\n "
  },
  {
    "path": "views/api/lastfm.pug",
    "chars": 1292,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.far.fa-play-circle.fa-sm.me-2(style='col"
  },
  {
    "path": "views/api/lob.pug",
    "chars": 2124,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.far.fa-envelope.fa-sm.me-2\n      | Lob A"
  },
  {
    "path": "views/api/nyt.pug",
    "chars": 882,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.far.fa-building.fa-sm.me-2\n      | New Y"
  },
  {
    "path": "views/api/paypal.pug",
    "chars": 1207,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fab.fa-paypal.fa-sm.me-2(style='color: #"
  },
  {
    "path": "views/api/pubchem.pug",
    "chars": 6199,
    "preview": "extends ../layout\n\nblock content\n  h2\n    i.fas.fa-flask.fa-sm.me-2\n    | PubChem API - Chemical Information\n\n  .btn-gro"
  },
  {
    "path": "views/api/quickbooks.pug",
    "chars": 792,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fas.fa-file-invoice-dollar.me-2\n      | "
  },
  {
    "path": "views/api/scraping.pug",
    "chars": 677,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fab.fa-hacker-news.fa-sm.me-2(style='col"
  },
  {
    "path": "views/api/steam.pug",
    "chars": 1567,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fab.fa-square-steam.fa-sm.me-2\n      | S"
  },
  {
    "path": "views/api/stripe.pug",
    "chars": 3434,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fab.fa-cc-stripe.fa-sm.me-2\n      | Stri"
  },
  {
    "path": "views/api/trakt.pug",
    "chars": 5262,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fas.fa-video.fa-sm.me-2\n      | Trakt.tv"
  },
  {
    "path": "views/api/tumblr.pug",
    "chars": 1714,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fab.fa-tumblr-square.fa-sm.me-2\n      | "
  },
  {
    "path": "views/api/twilio.pug",
    "chars": 3604,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fas.fa-phone.fa-sm.me-2(style='color: #f"
  },
  {
    "path": "views/api/twitch.pug",
    "chars": 2513,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fab.fa-twitch.fa-sm.me-2\n      | Twitch "
  },
  {
    "path": "views/api/upload.pug",
    "chars": 939,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fas.fa-upload.fa-sm.me-2\n      | File Up"
  },
  {
    "path": "views/api/wikipedia.pug",
    "chars": 2958,
    "preview": "extends ../layout\n\nblock content\n  .pb-2.mt-2.mb-4.border-bottom\n    h2\n      i.fab.fa-wikipedia-w.me-2\n      | Wikipedi"
  },
  {
    "path": "views/contact.pug",
    "chars": 1804,
    "preview": "extends layout\n\nblock head\n  if sitekey\n    script(src='https://www.google.com/recaptcha/enterprise.js', async='', defer"
  },
  {
    "path": "views/home.pug",
    "chars": 2162,
    "preview": "extends layout\nblock head\n  //- Opengraph tags\n  meta(property='og:title', content='Hackathon Starter')\n  meta(property="
  },
  {
    "path": "views/layout.pug",
    "chars": 2663,
    "preview": "doctype html\nhtml.h-100(lang='en')\n  head\n    meta(charset='utf-8')\n    meta(http-equiv='X-UA-Compatible', content='IE=e"
  },
  {
    "path": "views/partials/flash.pug",
    "chars": 631,
    "preview": "if messages.errors\n  .alert.alert-danger.alert-dismissible(role='alert')\n    each error in messages.errors\n      div= er"
  },
  {
    "path": "views/partials/footer.pug",
    "chars": 494,
    "preview": "footer.mt-auto.py-3.bg-light\n  .container.d-flex.justify-content-between\n    span © 2026 Company, Inc. All Rights Reserv"
  },
  {
    "path": "views/partials/header.pug",
    "chars": 1248,
    "preview": "nav.navbar.navbar-expand-lg.navbar-dark.bg-dark\n  .container\n    a.navbar-brand(href='/')\n      img.d-inline-block.align"
  }
]

About this extraction

This page contains the full source code of the sahat/hackathon-starter GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 144 files (1.7 MB), approximately 553.3k tokens, and a symbol index with 85 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!