mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
Switch to using t3-env for env-var management (#230)
This commit is contained in:
parent
e8acfcca70
commit
483217bf56
49 changed files with 404 additions and 484 deletions
|
|
@ -1,11 +1,13 @@
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.dockerignore
|
.dockerignore
|
||||||
node_modules
|
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
README.md
|
README.md
|
||||||
.next
|
|
||||||
!.next/static
|
|
||||||
!.next/standalone
|
|
||||||
.git
|
.git
|
||||||
.sourcebot
|
.sourcebot
|
||||||
.env.local
|
packages/web/.next
|
||||||
|
!packages/web/.next/static
|
||||||
|
!packages/web/.next/standalone
|
||||||
|
**/node_modules
|
||||||
|
**/.env.local
|
||||||
|
**/.sentryclirc
|
||||||
|
**/.env.sentry-build-plugin
|
||||||
80
Dockerfile
80
Dockerfile
|
|
@ -1,5 +1,18 @@
|
||||||
|
# ------ Global scope variables ------
|
||||||
|
# Set of global build arguments.
|
||||||
|
# @see: https://docs.docker.com/build/building/variables/#scoping
|
||||||
|
|
||||||
|
ARG SOURCEBOT_VERSION
|
||||||
|
# PAPIK = Project API Key
|
||||||
|
# Note that this key does not need to be kept secret, so it's not
|
||||||
|
# necessary to use Docker build secrets here.
|
||||||
|
# @see: https://posthog.com/tutorials/api-capture-events#authenticating-with-the-project-api-key
|
||||||
|
ARG POSTHOG_PAPIK
|
||||||
|
ARG SENTRY_ENVIRONMENT
|
||||||
|
|
||||||
FROM node:20-alpine3.19 AS node-alpine
|
FROM node:20-alpine3.19 AS node-alpine
|
||||||
FROM golang:1.23.4-alpine3.19 AS go-alpine
|
FROM golang:1.23.4-alpine3.19 AS go-alpine
|
||||||
|
# ----------------------------------
|
||||||
|
|
||||||
# ------ Build Zoekt ------
|
# ------ Build Zoekt ------
|
||||||
FROM go-alpine AS zoekt-builder
|
FROM go-alpine AS zoekt-builder
|
||||||
|
|
@ -9,6 +22,7 @@ COPY vendor/zoekt/go.mod vendor/zoekt/go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY vendor/zoekt ./
|
COPY vendor/zoekt ./
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /cmd/ ./cmd/...
|
RUN CGO_ENABLED=0 GOOS=linux go build -o /cmd/ ./cmd/...
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
# ------ Build shared libraries ------
|
# ------ Build shared libraries ------
|
||||||
FROM node-alpine AS shared-libs-builder
|
FROM node-alpine AS shared-libs-builder
|
||||||
|
|
@ -23,9 +37,24 @@ RUN yarn workspace @sourcebot/db install --frozen-lockfile
|
||||||
RUN yarn workspace @sourcebot/schemas install --frozen-lockfile
|
RUN yarn workspace @sourcebot/schemas install --frozen-lockfile
|
||||||
RUN yarn workspace @sourcebot/crypto install --frozen-lockfile
|
RUN yarn workspace @sourcebot/crypto install --frozen-lockfile
|
||||||
RUN yarn workspace @sourcebot/error install --frozen-lockfile
|
RUN yarn workspace @sourcebot/error install --frozen-lockfile
|
||||||
|
# ------------------------------------
|
||||||
|
|
||||||
# ------ Build Web ------
|
# ------ Build Web ------
|
||||||
FROM node-alpine AS web-builder
|
FROM node-alpine AS web-builder
|
||||||
|
ENV DOCKER_BUILD=1
|
||||||
|
# -----------
|
||||||
|
# Global args
|
||||||
|
ARG SOURCEBOT_VERSION
|
||||||
|
ENV NEXT_PUBLIC_SOURCEBOT_VERSION=$SOURCEBOT_VERSION
|
||||||
|
ARG POSTHOG_PAPIK
|
||||||
|
ENV NEXT_PUBLIC_POSTHOG_PAPIK=$POSTHOG_PAPIK
|
||||||
|
ARG SENTRY_ENVIRONMENT
|
||||||
|
ENV NEXT_PUBLIC_SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
|
||||||
|
# Local args
|
||||||
|
ARG SENTRY_WEBAPP_DSN
|
||||||
|
ENV NEXT_PUBLIC_SENTRY_WEBAPP_DSN=$SENTRY_WEBAPP_DSN
|
||||||
|
# -----------
|
||||||
|
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
@ -43,26 +72,13 @@ RUN yarn config set network-timeout 1200000
|
||||||
RUN yarn workspace @sourcebot/web install --frozen-lockfile
|
RUN yarn workspace @sourcebot/web install --frozen-lockfile
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
# @see: https://phase.dev/blog/nextjs-public-runtime-variables/
|
|
||||||
ARG NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED=BAKED_NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED
|
|
||||||
ARG NEXT_PUBLIC_SOURCEBOT_VERSION=BAKED_NEXT_PUBLIC_SOURCEBOT_VERSION
|
|
||||||
ENV NEXT_PUBLIC_PUBLIC_SEARCH_DEMO=BAKED_NEXT_PUBLIC_PUBLIC_SEARCH_DEMO
|
|
||||||
ENV NEXT_PUBLIC_POSTHOG_PAPIK=BAKED_NEXT_PUBLIC_POSTHOG_PAPIK
|
|
||||||
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=BAKED_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
|
||||||
ENV NEXT_PUBLIC_SENTRY_ENVIRONMENT=BAKED_NEXT_PUBLIC_SENTRY_ENVIRONMENT
|
|
||||||
ENV NEXT_PUBLIC_SENTRY_WEBAPP_DSN=BAKED_NEXT_PUBLIC_SENTRY_WEBAPP_DSN
|
|
||||||
|
|
||||||
# @nocheckin: This was interfering with the the `matcher` regex in middleware.ts,
|
|
||||||
# causing regular expressions parsing errors when making a request. It's unclear
|
|
||||||
# why exactly this was happening, but it's likely due to a bad replacement happening
|
|
||||||
# in the `sed` command.
|
|
||||||
# @note: leading "/" is required for the basePath property. @see: https://nextjs.org/docs/app/api-reference/next-config-js/basePath
|
|
||||||
# ARG NEXT_PUBLIC_DOMAIN_SUB_PATH=/BAKED_NEXT_PUBLIC_DOMAIN_SUB_PATH
|
|
||||||
|
|
||||||
RUN yarn workspace @sourcebot/web build
|
RUN yarn workspace @sourcebot/web build
|
||||||
|
ENV DOCKER_BUILD=0
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
# ------ Build Backend ------
|
# ------ Build Backend ------
|
||||||
FROM node-alpine AS backend-builder
|
FROM node-alpine AS backend-builder
|
||||||
|
ENV DOCKER_BUILD=1
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json yarn.lock* ./
|
COPY package.json yarn.lock* ./
|
||||||
|
|
@ -75,10 +91,22 @@ COPY --from=shared-libs-builder /app/packages/crypto ./packages/crypto
|
||||||
COPY --from=shared-libs-builder /app/packages/error ./packages/error
|
COPY --from=shared-libs-builder /app/packages/error ./packages/error
|
||||||
RUN yarn workspace @sourcebot/backend install --frozen-lockfile
|
RUN yarn workspace @sourcebot/backend install --frozen-lockfile
|
||||||
RUN yarn workspace @sourcebot/backend build
|
RUN yarn workspace @sourcebot/backend build
|
||||||
|
ENV DOCKER_BUILD=0
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
# ------ Runner ------
|
# ------ Runner ------
|
||||||
FROM node-alpine AS runner
|
FROM node-alpine AS runner
|
||||||
|
# -----------
|
||||||
|
# Global args
|
||||||
|
ARG SOURCEBOT_VERSION
|
||||||
|
ENV SOURCEBOT_VERSION=$SOURCEBOT_VERSION
|
||||||
|
ARG POSTHOG_PAPIK
|
||||||
|
ENV POSTHOG_PAPIK=$POSTHOG_PAPIK
|
||||||
|
# Local args
|
||||||
|
# -----------
|
||||||
|
|
||||||
|
RUN echo "Sourcebot Version: $SOURCEBOT_VERSION"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
@ -90,14 +118,6 @@ ENV DATABASE_URL="postgresql://postgres@localhost:5432/sourcebot"
|
||||||
ENV REDIS_URL="redis://localhost:6379"
|
ENV REDIS_URL="redis://localhost:6379"
|
||||||
ENV SRC_TENANT_ENFORCEMENT_MODE=strict
|
ENV SRC_TENANT_ENFORCEMENT_MODE=strict
|
||||||
|
|
||||||
ARG SOURCEBOT_VERSION=unknown
|
|
||||||
ENV SOURCEBOT_VERSION=$SOURCEBOT_VERSION
|
|
||||||
RUN echo "Sourcebot Version: $SOURCEBOT_VERSION"
|
|
||||||
|
|
||||||
ARG PUBLIC_SEARCH_DEMO=false
|
|
||||||
ENV PUBLIC_SEARCH_DEMO=$PUBLIC_SEARCH_DEMO
|
|
||||||
RUN echo "Public Search Demo: $PUBLIC_SEARCH_DEMO"
|
|
||||||
|
|
||||||
# Valid values are: debug, info, warn, error
|
# Valid values are: debug, info, warn, error
|
||||||
ENV SOURCEBOT_LOG_LEVEL=info
|
ENV SOURCEBOT_LOG_LEVEL=info
|
||||||
|
|
||||||
|
|
@ -106,18 +126,9 @@ ENV SOURCEBOT_LOG_LEVEL=info
|
||||||
# will serve from http(s)://example.com/sb
|
# will serve from http(s)://example.com/sb
|
||||||
ENV DOMAIN_SUB_PATH=/
|
ENV DOMAIN_SUB_PATH=/
|
||||||
|
|
||||||
# PAPIK = Project API Key
|
|
||||||
# Note that this key does not need to be kept secret, so it's not
|
|
||||||
# necessary to use Docker build secrets here.
|
|
||||||
# @see: https://posthog.com/tutorials/api-capture-events#authenticating-with-the-project-api-key
|
|
||||||
ARG POSTHOG_PAPIK=
|
|
||||||
ENV POSTHOG_PAPIK=$POSTHOG_PAPIK
|
|
||||||
|
|
||||||
# Sourcebot collects anonymous usage data using [PostHog](https://posthog.com/). Uncomment this line to disable.
|
# Sourcebot collects anonymous usage data using [PostHog](https://posthog.com/). Uncomment this line to disable.
|
||||||
# ENV SOURCEBOT_TELEMETRY_DISABLED=1
|
# ENV SOURCEBOT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
ENV STRIPE_PUBLISHABLE_KEY=""
|
|
||||||
|
|
||||||
# Configure zoekt
|
# Configure zoekt
|
||||||
COPY vendor/zoekt/install-ctags-alpine.sh .
|
COPY vendor/zoekt/install-ctags-alpine.sh .
|
||||||
RUN ./install-ctags-alpine.sh && rm install-ctags-alpine.sh
|
RUN ./install-ctags-alpine.sh && rm install-ctags-alpine.sh
|
||||||
|
|
@ -179,3 +190,4 @@ EXPOSE 3000
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
ENTRYPOINT ["/sbin/tini", "--", "./entrypoint.sh"]
|
ENTRYPOINT ["/sbin/tini", "--", "./entrypoint.sh"]
|
||||||
|
# ------------------------------
|
||||||
|
|
@ -108,102 +108,6 @@ fi
|
||||||
echo "{\"version\": \"$SOURCEBOT_VERSION\", \"install_id\": \"$SOURCEBOT_INSTALL_ID\"}" > "$FIRST_RUN_FILE"
|
echo "{\"version\": \"$SOURCEBOT_VERSION\", \"install_id\": \"$SOURCEBOT_INSTALL_ID\"}" > "$FIRST_RUN_FILE"
|
||||||
|
|
||||||
|
|
||||||
# Update NextJs public env variables w/o requiring a rebuild.
|
|
||||||
# @see: https://phase.dev/blog/nextjs-public-runtime-variables/
|
|
||||||
{
|
|
||||||
# Infer NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED if it is not set
|
|
||||||
if [ -z "$NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED" ] && [ ! -z "$SOURCEBOT_TELEMETRY_DISABLED" ]; then
|
|
||||||
export NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED="$SOURCEBOT_TELEMETRY_DISABLED"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Infer NEXT_PUBLIC_SOURCEBOT_VERSION if it is not set
|
|
||||||
if [ -z "$NEXT_PUBLIC_SOURCEBOT_VERSION" ] && [ ! -z "$SOURCEBOT_VERSION" ]; then
|
|
||||||
export NEXT_PUBLIC_SOURCEBOT_VERSION="$SOURCEBOT_VERSION"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Infer NEXT_PUBLIC_PUBLIC_SEARCH_DEMO if it is not set
|
|
||||||
if [ -z "$NEXT_PUBLIC_PUBLIC_SEARCH_DEMO" ] && [ ! -z "$PUBLIC_SEARCH_DEMO" ]; then
|
|
||||||
export NEXT_PUBLIC_PUBLIC_SEARCH_DEMO="$PUBLIC_SEARCH_DEMO"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Always infer NEXT_PUBLIC_POSTHOG_PAPIK
|
|
||||||
export NEXT_PUBLIC_POSTHOG_PAPIK="$POSTHOG_PAPIK"
|
|
||||||
|
|
||||||
# Always infer NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
|
||||||
export NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="$STRIPE_PUBLISHABLE_KEY"
|
|
||||||
|
|
||||||
# Always infer NEXT_PUBLIC_SENTRY_ENVIRONMENT
|
|
||||||
export NEXT_PUBLIC_SENTRY_ENVIRONMENT="$SENTRY_ENVIRONMENT"
|
|
||||||
|
|
||||||
# Always infer NEXT_PUBLIC_SENTRY_WEBAPP_DSN
|
|
||||||
export NEXT_PUBLIC_SENTRY_WEBAPP_DSN="$SENTRY_WEBAPP_DSN"
|
|
||||||
|
|
||||||
# Iterate over all .js files in .next & public, making substitutions for the `BAKED_` sentinal values
|
|
||||||
# with their actual desired runtime value.
|
|
||||||
find /app/packages/web/public /app/packages/web/.next -type f -name "*.js" |
|
|
||||||
while read file; do
|
|
||||||
sed -i "s|BAKED_NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED|${NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED}|g" "$file"
|
|
||||||
sed -i "s|BAKED_NEXT_PUBLIC_SOURCEBOT_VERSION|${NEXT_PUBLIC_SOURCEBOT_VERSION}|g" "$file"
|
|
||||||
sed -i "s|BAKED_NEXT_PUBLIC_POSTHOG_PAPIK|${NEXT_PUBLIC_POSTHOG_PAPIK}|g" "$file"
|
|
||||||
sed -i "s|BAKED_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY|${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}|g" "$file"
|
|
||||||
sed -i "s|BAKED_NEXT_PUBLIC_SENTRY_ENVIRONMENT|${NEXT_PUBLIC_SENTRY_ENVIRONMENT}|g" "$file"
|
|
||||||
sed -i "s|BAKED_NEXT_PUBLIC_SENTRY_WEBAPP_DSN|${NEXT_PUBLIC_SENTRY_WEBAPP_DSN}|g" "$file"
|
|
||||||
sed -i "s|BAKED_NEXT_PUBLIC_PUBLIC_SEARCH_DEMO|${NEXT_PUBLIC_PUBLIC_SEARCH_DEMO}|g" "$file"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# @nocheckin: This was interfering with the the `matcher` regex in middleware.ts,
|
|
||||||
# causing regular expressions parsing errors when making a request. It's unclear
|
|
||||||
# why exactly this was happening, but it's likely due to a bad replacement happening
|
|
||||||
# in the `sed` command.
|
|
||||||
#
|
|
||||||
# # Update specifically NEXT_PUBLIC_DOMAIN_SUB_PATH w/o requiring a rebuild.
|
|
||||||
# # Ultimately, the DOMAIN_SUB_PATH sets the `basePath` param in the next.config.mjs.
|
|
||||||
# # Similar to above, we pass in a `BAKED_` sentinal value into next.config.mjs at build
|
|
||||||
# # time. Unlike above, the `basePath` configuration is set in files other than just javascript
|
|
||||||
# # code (e.g., manifest files, css files, etc.), so this section has subtle differences.
|
|
||||||
# #
|
|
||||||
# # @see: https://nextjs.org/docs/app/api-reference/next-config-js/basePath
|
|
||||||
# # @see: https://phase.dev/blog/nextjs-public-runtime-variables/
|
|
||||||
# {
|
|
||||||
# if [ ! -z "$DOMAIN_SUB_PATH" ]; then
|
|
||||||
# # If the sub-path is "/", this creates problems with certain replacements. For example:
|
|
||||||
# # /BAKED_NEXT_PUBLIC_DOMAIN_SUB_PATH/_next/image -> //_next/image (notice the double slash...)
|
|
||||||
# # To get around this, we default to an empty sub-path, which is the default when no sub-path is defined.
|
|
||||||
# if [ "$DOMAIN_SUB_PATH" = "/" ]; then
|
|
||||||
# DOMAIN_SUB_PATH=""
|
|
||||||
|
|
||||||
# # Otherwise, we need to ensure that the sub-path starts with a slash, since this is a requirement
|
|
||||||
# # for the basePath property. For example, assume DOMAIN_SUB_PATH=/bot, then:
|
|
||||||
# # /BAKED_NEXT_PUBLIC_DOMAIN_SUB_PATH/_next/image -> /bot/_next/image
|
|
||||||
# elif [[ ! "$DOMAIN_SUB_PATH" =~ ^/ ]]; then
|
|
||||||
# DOMAIN_SUB_PATH="/$DOMAIN_SUB_PATH"
|
|
||||||
# fi
|
|
||||||
# fi
|
|
||||||
|
|
||||||
# if [ ! -z "$DOMAIN_SUB_PATH" ]; then
|
|
||||||
# echo -e "\e[34m[Info] DOMAIN_SUB_PATH was set to "$DOMAIN_SUB_PATH". Overriding default path.\e[0m"
|
|
||||||
# fi
|
|
||||||
|
|
||||||
# # Always set NEXT_PUBLIC_DOMAIN_SUB_PATH to DOMAIN_SUB_PATH (even if it is empty!!)
|
|
||||||
# export NEXT_PUBLIC_DOMAIN_SUB_PATH="$DOMAIN_SUB_PATH"
|
|
||||||
|
|
||||||
# # Iterate over _all_ files in the web directory, making substitutions for the `BAKED_` sentinal values
|
|
||||||
# # with their actual desired runtime value.
|
|
||||||
# find /app/packages/web -type f |
|
|
||||||
# while read file; do
|
|
||||||
# # @note: the leading "/" is required here as it is included at build time. See Dockerfile.
|
|
||||||
# sed -i "s|/BAKED_NEXT_PUBLIC_DOMAIN_SUB_PATH|${NEXT_PUBLIC_DOMAIN_SUB_PATH}|g" "$file"
|
|
||||||
# done
|
|
||||||
# }
|
|
||||||
|
|
||||||
# Upload sourcemaps to Sentry
|
|
||||||
# @nocheckin
|
|
||||||
su -c "sentry-cli login --auth-token $SENTRY_AUTH_TOKEN"
|
|
||||||
su -c "sentry-cli sourcemaps inject --org sourcebot --project backend /app/packages/backend/dist"
|
|
||||||
su -c "sentry-cli sourcemaps upload --org sourcebot --project backend /app/packages/backend/dist"
|
|
||||||
|
|
||||||
|
|
||||||
# Start the database and wait for it to be ready before starting any other service
|
# Start the database and wait for it to be ready before starting any other service
|
||||||
if [ "$DATABASE_URL" = "postgresql://postgres@localhost:5432/sourcebot" ]; then
|
if [ "$DATABASE_URL" = "postgresql://postgres@localhost:5432/sourcebot" ]; then
|
||||||
su postgres -c "postgres -D $DB_DATA_DIR" &
|
su postgres -c "postgres -D $DB_DATA_DIR" &
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
POSTHOG_HOST=https://us.i.posthog.com
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
"@sourcebot/db": "^0.1.0",
|
"@sourcebot/db": "^0.1.0",
|
||||||
"@sourcebot/error": "^0.1.0",
|
"@sourcebot/error": "^0.1.0",
|
||||||
"@sourcebot/schemas": "^0.1.0",
|
"@sourcebot/schemas": "^0.1.0",
|
||||||
|
"@t3-oss/env-core": "^0.12.0",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"argparse": "^2.0.1",
|
"argparse": "^2.0.1",
|
||||||
"bullmq": "^5.34.10",
|
"bullmq": "^5.34.10",
|
||||||
|
|
@ -47,6 +48,7 @@
|
||||||
"prom-client": "^15.1.3",
|
"prom-client": "^15.1.3",
|
||||||
"simple-git": "^3.27.0",
|
"simple-git": "^3.27.0",
|
||||||
"strip-json-comments": "^5.0.1",
|
"strip-json-comments": "^5.0.1",
|
||||||
"winston": "^3.15.0"
|
"winston": "^3.15.0",
|
||||||
|
"zod": "^3.24.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
41
packages/backend/src/env.ts
Normal file
41
packages/backend/src/env.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { createEnv } from "@t3-oss/env-core";
|
||||||
|
import { z } from "zod";
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config({
|
||||||
|
path: './.env',
|
||||||
|
});
|
||||||
|
|
||||||
|
dotenv.config({
|
||||||
|
path: './.env.local',
|
||||||
|
override: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export const env = createEnv({
|
||||||
|
server: {
|
||||||
|
SOURCEBOT_ENCRYPTION_KEY: z.string(),
|
||||||
|
SOURCEBOT_LOG_LEVEL: z.enum(["info", "debug", "warn", "error"]).default("info"),
|
||||||
|
SOURCEBOT_TELEMETRY_DISABLED: z.enum(["true", "false"]).default("false"),
|
||||||
|
SOURCEBOT_INSTALL_ID: z.string().default("unknown"),
|
||||||
|
SOURCEBOT_VERSION: z.string().default("unknown"),
|
||||||
|
|
||||||
|
POSTHOG_PAPIK: z.string().optional(),
|
||||||
|
POSTHOG_HOST: z.string().url().default('https://us.i.posthog.com'),
|
||||||
|
|
||||||
|
FALLBACK_GITHUB_TOKEN: z.string().optional(),
|
||||||
|
FALLBACK_GITLAB_TOKEN: z.string().optional(),
|
||||||
|
FALLBACK_GITEA_TOKEN: z.string().optional(),
|
||||||
|
|
||||||
|
REDIS_URL: z.string().url().optional().default("redis://localhost:6379"),
|
||||||
|
|
||||||
|
SENTRY_BACKEND_DSN: z.string().optional(),
|
||||||
|
SENTRY_ENVIRONMENT: z.string().optional(),
|
||||||
|
|
||||||
|
LOGTAIL_TOKEN: z.string().optional(),
|
||||||
|
LOGTAIL_HOST: z.string().url().optional(),
|
||||||
|
},
|
||||||
|
runtimeEnv: process.env,
|
||||||
|
emptyStringAsUndefined: true,
|
||||||
|
// Skip environment variable validation in Docker builds.
|
||||||
|
skipValidation: process.env.DOCKER_BUILD === "1",
|
||||||
|
});
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import dotenv from 'dotenv';
|
|
||||||
import * as Sentry from "@sentry/node";
|
|
||||||
|
|
||||||
export const getEnv = (env: string | undefined, defaultValue?: string, required?: boolean) => {
|
|
||||||
if (required && !env && !defaultValue) {
|
|
||||||
const e = new Error(`Missing required environment variable: ${env}`);
|
|
||||||
Sentry.captureException(e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
return env ?? defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getEnvBoolean = (env: string | undefined, defaultValue: boolean) => {
|
|
||||||
if (!env) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
return env === 'true' || env === '1';
|
|
||||||
}
|
|
||||||
|
|
||||||
dotenv.config({
|
|
||||||
path: './.env',
|
|
||||||
});
|
|
||||||
dotenv.config({
|
|
||||||
path: './.env.local',
|
|
||||||
override: true
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export const SOURCEBOT_LOG_LEVEL = getEnv(process.env.SOURCEBOT_LOG_LEVEL, 'info')!;
|
|
||||||
export const SOURCEBOT_TELEMETRY_DISABLED = getEnvBoolean(process.env.SOURCEBOT_TELEMETRY_DISABLED, false)!;
|
|
||||||
export const SOURCEBOT_INSTALL_ID = getEnv(process.env.SOURCEBOT_INSTALL_ID, 'unknown')!;
|
|
||||||
export const SOURCEBOT_VERSION = getEnv(process.env.SOURCEBOT_VERSION, 'unknown')!;
|
|
||||||
export const POSTHOG_PAPIK = getEnv(process.env.POSTHOG_PAPIK);
|
|
||||||
export const POSTHOG_HOST = getEnv(process.env.POSTHOG_HOST);
|
|
||||||
|
|
||||||
export const FALLBACK_GITHUB_TOKEN = getEnv(process.env.FALLBACK_GITHUB_TOKEN);
|
|
||||||
export const FALLBACK_GITLAB_TOKEN = getEnv(process.env.FALLBACK_GITLAB_TOKEN);
|
|
||||||
export const FALLBACK_GITEA_TOKEN = getEnv(process.env.FALLBACK_GITEA_TOKEN);
|
|
||||||
|
|
||||||
export const INDEX_CONCURRENCY_MULTIPLE = getEnv(process.env.INDEX_CONCURRENCY_MULTIPLE);
|
|
||||||
export const REDIS_URL = getEnv(process.env.REDIS_URL, 'redis://localhost:6379')!;
|
|
||||||
|
|
||||||
export const SENTRY_BACKEND_DSN = getEnv(process.env.SENTRY_BACKEND_DSN);
|
|
||||||
export const SENTRY_ENVIRONMENT = getEnv(process.env.SENTRY_ENVIRONMENT, 'unknown')!;
|
|
||||||
|
|
||||||
export const LOGTAIL_TOKEN = getEnv(process.env.LOGTAIL_TOKEN);
|
|
||||||
export const LOGTAIL_HOST = getEnv(process.env.LOGTAIL_HOST);
|
|
||||||
|
|
@ -5,15 +5,15 @@ import fetch from 'cross-fetch';
|
||||||
import { createLogger } from './logger.js';
|
import { createLogger } from './logger.js';
|
||||||
import micromatch from 'micromatch';
|
import micromatch from 'micromatch';
|
||||||
import { PrismaClient } from '@sourcebot/db';
|
import { PrismaClient } from '@sourcebot/db';
|
||||||
import { FALLBACK_GITEA_TOKEN } from './environment.js';
|
|
||||||
import { processPromiseResults, throwIfAnyFailed } from './connectionUtils.js';
|
import { processPromiseResults, throwIfAnyFailed } from './connectionUtils.js';
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
|
import { env } from './env.js';
|
||||||
|
|
||||||
const logger = createLogger('Gitea');
|
const logger = createLogger('Gitea');
|
||||||
|
|
||||||
export const getGiteaReposFromConfig = async (config: GiteaConnectionConfig, orgId: number, db: PrismaClient) => {
|
export const getGiteaReposFromConfig = async (config: GiteaConnectionConfig, orgId: number, db: PrismaClient) => {
|
||||||
const tokenResult = config.token ? await getTokenFromConfig(config.token, orgId, db) : undefined;
|
const tokenResult = config.token ? await getTokenFromConfig(config.token, orgId, db) : undefined;
|
||||||
const token = tokenResult?.token ?? FALLBACK_GITEA_TOKEN;
|
const token = tokenResult?.token ?? env.FALLBACK_GITEA_TOKEN;
|
||||||
|
|
||||||
const api = giteaApi(config.url ?? 'https://gitea.com', {
|
const api = giteaApi(config.url ?? 'https://gitea.com', {
|
||||||
token: token,
|
token: token,
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import { createLogger } from "./logger.js";
|
||||||
import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js";
|
import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js";
|
||||||
import micromatch from "micromatch";
|
import micromatch from "micromatch";
|
||||||
import { PrismaClient } from "@sourcebot/db";
|
import { PrismaClient } from "@sourcebot/db";
|
||||||
import { FALLBACK_GITHUB_TOKEN } from "./environment.js";
|
|
||||||
import { BackendException, BackendError } from "@sourcebot/error";
|
import { BackendException, BackendError } from "@sourcebot/error";
|
||||||
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
|
import { env } from "./env.js";
|
||||||
|
|
||||||
const logger = createLogger("GitHub");
|
const logger = createLogger("GitHub");
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, o
|
||||||
const secretKey = tokenResult?.secretKey;
|
const secretKey = tokenResult?.secretKey;
|
||||||
|
|
||||||
const octokit = new Octokit({
|
const octokit = new Octokit({
|
||||||
auth: token ?? FALLBACK_GITHUB_TOKEN,
|
auth: token ?? env.FALLBACK_GITHUB_TOKEN,
|
||||||
...(config.url ? {
|
...(config.url ? {
|
||||||
baseUrl: `${config.url}/api/v3`
|
baseUrl: `${config.url}/api/v3`
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,16 @@ import { createLogger } from "./logger.js";
|
||||||
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"
|
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"
|
||||||
import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js";
|
import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js";
|
||||||
import { PrismaClient } from "@sourcebot/db";
|
import { PrismaClient } from "@sourcebot/db";
|
||||||
import { FALLBACK_GITLAB_TOKEN } from "./environment.js";
|
|
||||||
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
|
import { env } from "./env.js";
|
||||||
|
|
||||||
const logger = createLogger("GitLab");
|
const logger = createLogger("GitLab");
|
||||||
export const GITLAB_CLOUD_HOSTNAME = "gitlab.com";
|
export const GITLAB_CLOUD_HOSTNAME = "gitlab.com";
|
||||||
|
|
||||||
export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, orgId: number, db: PrismaClient) => {
|
export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, orgId: number, db: PrismaClient) => {
|
||||||
const tokenResult = config.token ? await getTokenFromConfig(config.token, orgId, db) : undefined;
|
const tokenResult = config.token ? await getTokenFromConfig(config.token, orgId, db) : undefined;
|
||||||
const token = tokenResult?.token ?? FALLBACK_GITLAB_TOKEN;
|
const token = tokenResult?.token ?? env.FALLBACK_GITLAB_TOKEN;
|
||||||
|
|
||||||
const api = new Gitlab({
|
const api = new Gitlab({
|
||||||
...(token ? {
|
...(token ? {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
import { SOURCEBOT_VERSION, SENTRY_BACKEND_DSN, SENTRY_ENVIRONMENT } from "./environment.js";
|
import { env } from "./env.js";
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: SENTRY_BACKEND_DSN,
|
dsn: env.SENTRY_BACKEND_DSN,
|
||||||
release: SOURCEBOT_VERSION,
|
release: env.SOURCEBOT_VERSION,
|
||||||
environment: SENTRY_ENVIRONMENT,
|
environment: env.SENTRY_ENVIRONMENT,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import winston, { format } from 'winston';
|
import winston, { format } from 'winston';
|
||||||
import { SOURCEBOT_LOG_LEVEL, LOGTAIL_TOKEN, LOGTAIL_HOST } from './environment.js';
|
|
||||||
import { Logtail } from '@logtail/node';
|
import { Logtail } from '@logtail/node';
|
||||||
import { LogtailTransport } from '@logtail/winston';
|
import { LogtailTransport } from '@logtail/winston';
|
||||||
|
import { env } from './env.js';
|
||||||
|
|
||||||
const { combine, colorize, timestamp, prettyPrint, errors, printf, label: labelFn } = format;
|
const { combine, colorize, timestamp, prettyPrint, errors, printf, label: labelFn } = format;
|
||||||
|
|
||||||
|
|
||||||
const createLogger = (label: string) => {
|
const createLogger = (label: string) => {
|
||||||
return winston.createLogger({
|
return winston.createLogger({
|
||||||
level: SOURCEBOT_LOG_LEVEL,
|
level: env.SOURCEBOT_LOG_LEVEL,
|
||||||
format: combine(
|
format: combine(
|
||||||
errors({ stack: true }),
|
errors({ stack: true }),
|
||||||
timestamp(),
|
timestamp(),
|
||||||
|
|
@ -31,10 +31,10 @@ const createLogger = (label: string) => {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
...(LOGTAIL_TOKEN && LOGTAIL_HOST ? [
|
...(env.LOGTAIL_TOKEN && env.LOGTAIL_HOST ? [
|
||||||
new LogtailTransport(
|
new LogtailTransport(
|
||||||
new Logtail(LOGTAIL_TOKEN, {
|
new Logtail(env.LOGTAIL_TOKEN, {
|
||||||
endpoint: LOGTAIL_HOST,
|
endpoint: env.LOGTAIL_HOST,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
] : []),
|
] : []),
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ import { DEFAULT_SETTINGS } from './constants.js';
|
||||||
import { Redis } from 'ioredis';
|
import { Redis } from 'ioredis';
|
||||||
import { ConnectionManager } from './connectionManager.js';
|
import { ConnectionManager } from './connectionManager.js';
|
||||||
import { RepoManager } from './repoManager.js';
|
import { RepoManager } from './repoManager.js';
|
||||||
import { INDEX_CONCURRENCY_MULTIPLE, REDIS_URL } from './environment.js';
|
import { env } from './env.js';
|
||||||
import { PromClient } from './promClient.js';
|
import { PromClient } from './promClient.js';
|
||||||
|
|
||||||
const logger = createLogger('main');
|
const logger = createLogger('main');
|
||||||
|
|
||||||
export const main = async (db: PrismaClient, context: AppContext) => {
|
export const main = async (db: PrismaClient, context: AppContext) => {
|
||||||
const redis = new Redis(REDIS_URL, {
|
const redis = new Redis(env.REDIS_URL, {
|
||||||
maxRetriesPerRequest: null
|
maxRetriesPerRequest: null
|
||||||
});
|
});
|
||||||
redis.ping().then(() => {
|
redis.ping().then(() => {
|
||||||
|
|
@ -23,8 +23,8 @@ export const main = async (db: PrismaClient, context: AppContext) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const settings = DEFAULT_SETTINGS;
|
const settings = DEFAULT_SETTINGS;
|
||||||
if (INDEX_CONCURRENCY_MULTIPLE) {
|
if (env.INDEX_CONCURRENCY_MULTIPLE) {
|
||||||
settings.indexConcurrencyMultiple = parseInt(INDEX_CONCURRENCY_MULTIPLE);
|
settings.indexConcurrencyMultiple = env.INDEX_CONCURRENCY_MULTIPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promClient = new PromClient();
|
const promClient = new PromClient();
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,29 @@
|
||||||
import { PostHog } from 'posthog-node';
|
import { PostHog } from 'posthog-node';
|
||||||
import { PosthogEvent, PosthogEventMap } from './posthogEvents.js';
|
import { PosthogEvent, PosthogEventMap } from './posthogEvents.js';
|
||||||
import { POSTHOG_HOST, POSTHOG_PAPIK, SOURCEBOT_INSTALL_ID, SOURCEBOT_TELEMETRY_DISABLED, SOURCEBOT_VERSION } from './environment.js';
|
import { env } from './env.js';
|
||||||
|
|
||||||
let posthog: PostHog | undefined = undefined;
|
let posthog: PostHog | undefined = undefined;
|
||||||
|
|
||||||
if (POSTHOG_PAPIK) {
|
if (env.POSTHOG_PAPIK) {
|
||||||
posthog = new PostHog(
|
posthog = new PostHog(
|
||||||
POSTHOG_PAPIK,
|
env.POSTHOG_PAPIK,
|
||||||
{
|
{
|
||||||
host: POSTHOG_HOST,
|
host: env.POSTHOG_HOST,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function captureEvent<E extends PosthogEvent>(event: E, properties: PosthogEventMap[E]) {
|
export function captureEvent<E extends PosthogEvent>(event: E, properties: PosthogEventMap[E]) {
|
||||||
if (SOURCEBOT_TELEMETRY_DISABLED) {
|
if (env.SOURCEBOT_TELEMETRY_DISABLED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
posthog?.capture({
|
posthog?.capture({
|
||||||
distinctId: SOURCEBOT_INSTALL_ID,
|
distinctId: env.SOURCEBOT_INSTALL_ID,
|
||||||
event: event,
|
event: event,
|
||||||
properties: {
|
properties: {
|
||||||
...properties,
|
...properties,
|
||||||
sourcebot_version: SOURCEBOT_VERSION,
|
sourcebot_version: env.SOURCEBOT_VERSION,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
|
||||||
NEXT_PUBLIC_POSTHOG_ASSET_HOST=https://us-assets.i.posthog.com
|
|
||||||
NEXT_PUBLIC_POSTHOG_UI_HOST=https://us.posthog.com
|
|
||||||
|
|
@ -1,22 +1,30 @@
|
||||||
|
await import("./src/env.mjs");
|
||||||
import { withSentryConfig } from "@sentry/nextjs";
|
import { withSentryConfig } from "@sentry/nextjs";
|
||||||
|
import { env } from "./src/env.mjs";
|
||||||
|
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
|
||||||
|
// This is required when using standalone builds.
|
||||||
|
// @see: https://env.t3.gg/docs/nextjs#create-your-schema
|
||||||
|
transpilePackages: ["@t3-oss/env-nextjs", "@t3-oss/env-core"],
|
||||||
|
|
||||||
// @see : https://posthog.com/docs/advanced/proxy/nextjs
|
// @see : https://posthog.com/docs/advanced/proxy/nextjs
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: "/ingest/static/:path*",
|
source: "/ingest/static/:path*",
|
||||||
destination: `${process.env.NEXT_PUBLIC_POSTHOG_ASSET_HOST}/static/:path*`,
|
destination: `${env.NEXT_PUBLIC_POSTHOG_ASSET_HOST}/static/:path*`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "/ingest/:path*",
|
source: "/ingest/:path*",
|
||||||
destination: `${process.env.NEXT_PUBLIC_POSTHOG_HOST}/:path*`,
|
destination: `${env.NEXT_PUBLIC_POSTHOG_HOST}/:path*`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "/ingest/decide",
|
source: "/ingest/decide",
|
||||||
destination: `${process.env.NEXT_PUBLIC_POSTHOG_HOST}/decide`,
|
destination: `${env.NEXT_PUBLIC_POSTHOG_HOST}/decide`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
@ -30,16 +38,7 @@ const nextConfig = {
|
||||||
hostname: '**',
|
hostname: '**',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
|
||||||
// @nocheckin: This was interfering with the the `matcher` regex in middleware.ts,
|
|
||||||
// causing regular expressions parsing errors when making a request. It's unclear
|
|
||||||
// why exactly this was happening, but it's likely due to a bad replacement happening
|
|
||||||
// in the `sed` command.
|
|
||||||
// @note: this is evaluated at build time.
|
|
||||||
// ...(process.env.NEXT_PUBLIC_DOMAIN_SUB_PATH ? {
|
|
||||||
// basePath: process.env.NEXT_PUBLIC_DOMAIN_SUB_PATH,
|
|
||||||
// } : {})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withSentryConfig(nextConfig, {
|
export default withSentryConfig(nextConfig, {
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@
|
||||||
"@ssddanbrown/codemirror-lang-twig": "^1.0.0",
|
"@ssddanbrown/codemirror-lang-twig": "^1.0.0",
|
||||||
"@stripe/react-stripe-js": "^3.1.1",
|
"@stripe/react-stripe-js": "^3.1.1",
|
||||||
"@stripe/stripe-js": "^5.6.0",
|
"@stripe/stripe-js": "^5.6.0",
|
||||||
|
"@t3-oss/env-nextjs": "^0.12.0",
|
||||||
"@tanstack/react-query": "^5.53.3",
|
"@tanstack/react-query": "^5.53.3",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"@tanstack/react-virtual": "^3.10.8",
|
"@tanstack/react-virtual": "^3.10.8",
|
||||||
|
|
@ -133,7 +134,7 @@
|
||||||
"tailwind-scrollbar-hide": "^1.1.7",
|
"tailwind-scrollbar-hide": "^1.1.7",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"usehooks-ts": "^3.1.0",
|
"usehooks-ts": "^3.1.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,9 @@ import { decrypt, encrypt } from "@sourcebot/crypto"
|
||||||
import { getConnection } from "./data/connection";
|
import { getConnection } from "./data/connection";
|
||||||
import { ConnectionSyncStatus, Prisma, OrgRole, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db";
|
import { ConnectionSyncStatus, Prisma, OrgRole, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db";
|
||||||
import { cookies, headers } from "next/headers"
|
import { cookies, headers } from "next/headers"
|
||||||
import { getStripe } from "@/lib/stripe"
|
|
||||||
import { getUser } from "@/data/user";
|
import { getUser } from "@/data/user";
|
||||||
import { Session } from "next-auth";
|
import { Session } from "next-auth";
|
||||||
import { STRIPE_PRODUCT_ID, CONFIG_MAX_REPOS_NO_TOKEN, EMAIL_FROM, SMTP_CONNECTION_URL, AUTH_URL } from "@/lib/environment";
|
import { env } from "@/env.mjs";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { render } from "@react-email/components";
|
import { render } from "@react-email/components";
|
||||||
import InviteUserEmail from "./emails/inviteUserEmail";
|
import InviteUserEmail from "./emails/inviteUserEmail";
|
||||||
|
|
@ -27,6 +26,7 @@ import { createTransport } from "nodemailer";
|
||||||
import { orgDomainSchema, orgNameSchema, repositoryQuerySchema } from "./lib/schemas";
|
import { orgDomainSchema, orgNameSchema, repositoryQuerySchema } from "./lib/schemas";
|
||||||
import { RepositoryQuery } from "./lib/types";
|
import { RepositoryQuery } from "./lib/types";
|
||||||
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "./lib/constants";
|
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "./lib/constants";
|
||||||
|
import { stripeClient } from "./lib/stripe";
|
||||||
|
|
||||||
const ajv = new Ajv({
|
const ajv = new Ajv({
|
||||||
validateFormats: false,
|
validateFormats: false,
|
||||||
|
|
@ -594,7 +594,7 @@ export const createInvites = async (emails: string[], domain: string): Promise<{
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send invites to recipients
|
// Send invites to recipients
|
||||||
if (SMTP_CONNECTION_URL && EMAIL_FROM) {
|
if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM) {
|
||||||
const origin = (await headers()).get('origin')!;
|
const origin = (await headers()).get('origin')!;
|
||||||
await Promise.all(emails.map(async (email) => {
|
await Promise.all(emails.map(async (email) => {
|
||||||
const invite = await prisma.invite.findUnique({
|
const invite = await prisma.invite.findUnique({
|
||||||
|
|
@ -619,9 +619,9 @@ export const createInvites = async (emails: string[], domain: string): Promise<{
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const inviteLink = `${origin}/redeem?invite_id=${invite.id}`;
|
const inviteLink = `${origin}/redeem?invite_id=${invite.id}`;
|
||||||
const transport = createTransport(SMTP_CONNECTION_URL);
|
const transport = createTransport(env.SMTP_CONNECTION_URL);
|
||||||
const html = await render(InviteUserEmail({
|
const html = await render(InviteUserEmail({
|
||||||
baseUrl: AUTH_URL,
|
baseUrl: env.AUTH_URL,
|
||||||
host: {
|
host: {
|
||||||
name: session.user.name ?? undefined,
|
name: session.user.name ?? undefined,
|
||||||
email: session.user.email!,
|
email: session.user.email!,
|
||||||
|
|
@ -637,7 +637,7 @@ export const createInvites = async (emails: string[], domain: string): Promise<{
|
||||||
|
|
||||||
const result = await transport.sendMail({
|
const result = await transport.sendMail({
|
||||||
to: email,
|
to: email,
|
||||||
from: EMAIL_FROM,
|
from: env.EMAIL_FROM,
|
||||||
subject: `Join ${invite.org.name} on Sourcebot`,
|
subject: `Join ${invite.org.name} on Sourcebot`,
|
||||||
html,
|
html,
|
||||||
text: `Join ${invite.org.name} on Sourcebot by clicking here: ${inviteLink}`,
|
text: `Join ${invite.org.name} on Sourcebot by clicking here: ${inviteLink}`,
|
||||||
|
|
@ -718,8 +718,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
|
||||||
const existingSeatCount = subscription.items.data[0].quantity;
|
const existingSeatCount = subscription.items.data[0].quantity;
|
||||||
const newSeatCount = (existingSeatCount || 1) + 1
|
const newSeatCount = (existingSeatCount || 1) + 1
|
||||||
|
|
||||||
const stripe = getStripe();
|
await stripeClient?.subscriptionItems.update(
|
||||||
await stripe.subscriptionItems.update(
|
|
||||||
subscription.items.data[0].id,
|
subscription.items.data[0].id,
|
||||||
{
|
{
|
||||||
quantity: newSeatCount,
|
quantity: newSeatCount,
|
||||||
|
|
@ -873,10 +872,16 @@ export const createOnboardingSubscription = async (domain: string) =>
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const stripe = getStripe();
|
if (!stripeClient) {
|
||||||
|
return {
|
||||||
|
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
|
errorCode: ErrorCode.STRIPE_CLIENT_NOT_INITIALIZED,
|
||||||
|
message: "Stripe client is not initialized.",
|
||||||
|
} satisfies ServiceError;
|
||||||
|
}
|
||||||
|
|
||||||
// @nocheckin
|
// @nocheckin
|
||||||
const test_clock = AUTH_URL !== "https://app.sourcebot.dev" ? await stripe.testHelpers.testClocks.create({
|
const test_clock = env.AUTH_URL !== "https://app.sourcebot.dev" ? await stripeClient.testHelpers.testClocks.create({
|
||||||
frozen_time: Math.floor(Date.now() / 1000)
|
frozen_time: Math.floor(Date.now() / 1000)
|
||||||
}) : null;
|
}) : null;
|
||||||
|
|
||||||
|
|
@ -886,7 +891,7 @@ export const createOnboardingSubscription = async (domain: string) =>
|
||||||
return org.stripeCustomerId;
|
return org.stripeCustomerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customer = await stripe.customers.create({
|
const customer = await stripeClient.customers.create({
|
||||||
name: org.name,
|
name: org.name,
|
||||||
email: user.email ?? undefined,
|
email: user.email ?? undefined,
|
||||||
test_clock: test_clock?.id,
|
test_clock: test_clock?.id,
|
||||||
|
|
@ -915,13 +920,13 @@ export const createOnboardingSubscription = async (domain: string) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const prices = await stripe.prices.list({
|
const prices = await stripeClient.prices.list({
|
||||||
product: STRIPE_PRODUCT_ID,
|
product: env.STRIPE_PRODUCT_ID,
|
||||||
expand: ['data.product'],
|
expand: ['data.product'],
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const subscription = await stripe.subscriptions.create({
|
const subscription = await stripeClient.subscriptions.create({
|
||||||
customer: customerId,
|
customer: customerId,
|
||||||
items: [{
|
items: [{
|
||||||
price: prices.data[0].id,
|
price: prices.data[0].id,
|
||||||
|
|
@ -974,6 +979,14 @@ export const createStripeCheckoutSession = async (domain: string) =>
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!stripeClient) {
|
||||||
|
return {
|
||||||
|
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
|
errorCode: ErrorCode.STRIPE_CLIENT_NOT_INITIALIZED,
|
||||||
|
message: "Stripe client is not initialized.",
|
||||||
|
} satisfies ServiceError;
|
||||||
|
}
|
||||||
|
|
||||||
const orgMembers = await prisma.userToOrg.findMany({
|
const orgMembers = await prisma.userToOrg.findMany({
|
||||||
where: {
|
where: {
|
||||||
orgId,
|
orgId,
|
||||||
|
|
@ -984,14 +997,13 @@ export const createStripeCheckoutSession = async (domain: string) =>
|
||||||
});
|
});
|
||||||
const numOrgMembers = orgMembers.length;
|
const numOrgMembers = orgMembers.length;
|
||||||
|
|
||||||
const stripe = getStripe();
|
|
||||||
const origin = (await headers()).get('origin')
|
const origin = (await headers()).get('origin')
|
||||||
const prices = await stripe.prices.list({
|
const prices = await stripeClient.prices.list({
|
||||||
product: STRIPE_PRODUCT_ID,
|
product: env.STRIPE_PRODUCT_ID,
|
||||||
expand: ['data.product'],
|
expand: ['data.product'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const stripeSession = await stripe.checkout.sessions.create({
|
const stripeSession = await stripeClient.checkout.sessions.create({
|
||||||
customer: org.stripeCustomerId as string,
|
customer: org.stripeCustomerId as string,
|
||||||
payment_method_types: ['card'],
|
payment_method_types: ['card'],
|
||||||
line_items: [
|
line_items: [
|
||||||
|
|
@ -1033,9 +1045,16 @@ export const getCustomerPortalSessionLink = async (domain: string): Promise<stri
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const stripe = getStripe();
|
if (!stripeClient) {
|
||||||
|
return {
|
||||||
|
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
|
errorCode: ErrorCode.STRIPE_CLIENT_NOT_INITIALIZED,
|
||||||
|
message: "Stripe client is not initialized.",
|
||||||
|
} satisfies ServiceError;
|
||||||
|
}
|
||||||
|
|
||||||
const origin = (await headers()).get('origin')
|
const origin = (await headers()).get('origin')
|
||||||
const portalSession = await stripe.billingPortal.sessions.create({
|
const portalSession = await stripeClient.billingPortal.sessions.create({
|
||||||
customer: org.stripeCustomerId as string,
|
customer: org.stripeCustomerId as string,
|
||||||
return_url: `${origin}/${domain}/settings/billing`,
|
return_url: `${origin}/${domain}/settings/billing`,
|
||||||
});
|
});
|
||||||
|
|
@ -1064,8 +1083,15 @@ export const getSubscriptionBillingEmail = async (domain: string): Promise<strin
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const stripe = getStripe();
|
if (!stripeClient) {
|
||||||
const customer = await stripe.customers.retrieve(org.stripeCustomerId);
|
return {
|
||||||
|
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
|
errorCode: ErrorCode.STRIPE_CLIENT_NOT_INITIALIZED,
|
||||||
|
message: "Stripe client is not initialized.",
|
||||||
|
} satisfies ServiceError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const customer = await stripeClient.customers.retrieve(org.stripeCustomerId);
|
||||||
if (!('email' in customer) || customer.deleted) {
|
if (!('email' in customer) || customer.deleted) {
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
@ -1086,8 +1112,15 @@ export const changeSubscriptionBillingEmail = async (domain: string, newEmail: s
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const stripe = getStripe();
|
if (!stripeClient) {
|
||||||
await stripe.customers.update(org.stripeCustomerId, {
|
return {
|
||||||
|
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
|
errorCode: ErrorCode.STRIPE_CLIENT_NOT_INITIALIZED,
|
||||||
|
message: "Stripe client is not initialized.",
|
||||||
|
} satisfies ServiceError;
|
||||||
|
}
|
||||||
|
|
||||||
|
await stripeClient.customers.update(org.stripeCustomerId, {
|
||||||
email: newEmail,
|
email: newEmail,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1143,8 +1176,7 @@ export const removeMemberFromOrg = async (memberId: string, domain: string): Pro
|
||||||
const existingSeatCount = subscription.items.data[0].quantity;
|
const existingSeatCount = subscription.items.data[0].quantity;
|
||||||
const newSeatCount = (existingSeatCount || 1) - 1;
|
const newSeatCount = (existingSeatCount || 1) - 1;
|
||||||
|
|
||||||
const stripe = getStripe();
|
await stripeClient?.subscriptionItems.update(
|
||||||
await stripe.subscriptionItems.update(
|
|
||||||
subscription.items.data[0].id,
|
subscription.items.data[0].id,
|
||||||
{
|
{
|
||||||
quantity: newSeatCount,
|
quantity: newSeatCount,
|
||||||
|
|
@ -1198,8 +1230,7 @@ export const leaveOrg = async (domain: string): Promise<{ success: boolean } | S
|
||||||
const existingSeatCount = subscription.items.data[0].quantity;
|
const existingSeatCount = subscription.items.data[0].quantity;
|
||||||
const newSeatCount = (existingSeatCount || 1) - 1;
|
const newSeatCount = (existingSeatCount || 1) - 1;
|
||||||
|
|
||||||
const stripe = getStripe();
|
await stripeClient?.subscriptionItems.update(
|
||||||
await stripe.subscriptionItems.update(
|
|
||||||
subscription.items.data[0].id,
|
subscription.items.data[0].id,
|
||||||
{
|
{
|
||||||
quantity: newSeatCount,
|
quantity: newSeatCount,
|
||||||
|
|
@ -1307,8 +1338,15 @@ const _fetchSubscriptionForOrg = async (orgId: number, prisma: Prisma.Transactio
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const stripe = getStripe();
|
if (!stripeClient) {
|
||||||
const subscriptions = await stripe.subscriptions.list({
|
return {
|
||||||
|
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
|
errorCode: ErrorCode.STRIPE_CLIENT_NOT_INITIALIZED,
|
||||||
|
message: "Stripe client is not initialized.",
|
||||||
|
} satisfies ServiceError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptions = await stripeClient.subscriptions.list({
|
||||||
customer: org.stripeCustomerId
|
customer: org.stripeCustomerId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1389,11 +1427,11 @@ const parseConnectionConfig = (connectionType: string, config: string) => {
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
if (!hasToken && numRepos && numRepos > CONFIG_MAX_REPOS_NO_TOKEN) {
|
if (!hasToken && numRepos && numRepos > env.CONFIG_MAX_REPOS_NO_TOKEN) {
|
||||||
return {
|
return {
|
||||||
statusCode: StatusCodes.BAD_REQUEST,
|
statusCode: StatusCodes.BAD_REQUEST,
|
||||||
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
||||||
message: `You must provide a token to sync more than ${CONFIG_MAX_REPOS_NO_TOKEN} repositories.`,
|
message: `You must provide a token to sync more than ${env.CONFIG_MAX_REPOS_NO_TOKEN} repositories.`,
|
||||||
} satisfies ServiceError;
|
} satisfies ServiceError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { CircleXIcon } from "lucide-react";
|
||||||
import { useDomain } from "@/hooks/useDomain";
|
import { useDomain } from "@/hooks/useDomain";
|
||||||
import { unwrapServiceError } from "@/lib/utils";
|
import { unwrapServiceError } from "@/lib/utils";
|
||||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||||
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
|
import { env } from "@/env.mjs";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { ConnectionSyncStatus, RepoIndexingStatus } from "@sourcebot/db";
|
import { ConnectionSyncStatus, RepoIndexingStatus } from "@sourcebot/db";
|
||||||
import { getConnections } from "@/actions";
|
import { getConnections } from "@/actions";
|
||||||
|
|
@ -20,14 +20,14 @@ export const ErrorNavIndicator = () => {
|
||||||
queryKey: ['repos', domain],
|
queryKey: ['repos', domain],
|
||||||
queryFn: () => unwrapServiceError(getRepos(domain)),
|
queryFn: () => unwrapServiceError(getRepos(domain)),
|
||||||
select: (data) => data.filter(repo => repo.repoIndexingStatus === RepoIndexingStatus.FAILED),
|
select: (data) => data.filter(repo => repo.repoIndexingStatus === RepoIndexingStatus.FAILED),
|
||||||
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: connections, isPending: isPendingConnections, isError: isErrorConnections } = useQuery({
|
const { data: connections, isPending: isPendingConnections, isError: isErrorConnections } = useQuery({
|
||||||
queryKey: ['connections', domain],
|
queryKey: ['connections', domain],
|
||||||
queryFn: () => unwrapServiceError(getConnections(domain)),
|
queryFn: () => unwrapServiceError(getConnections(domain)),
|
||||||
select: (data) => data.filter(connection => connection.syncStatus === ConnectionSyncStatus.FAILED),
|
select: (data) => data.filter(connection => connection.syncStatus === ConnectionSyncStatus.FAILED),
|
||||||
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPendingRepos || isErrorRepos || isPendingConnections || isErrorConnections) {
|
if (isPendingRepos || isErrorRepos || isPendingConnections || isErrorConnections) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { getRepos } from "@/actions";
|
||||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||||
import { useDomain } from "@/hooks/useDomain";
|
import { useDomain } from "@/hooks/useDomain";
|
||||||
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
|
import { env } from "@/env.mjs";
|
||||||
import { unwrapServiceError } from "@/lib/utils";
|
import { unwrapServiceError } from "@/lib/utils";
|
||||||
import { RepoIndexingStatus } from "@prisma/client";
|
import { RepoIndexingStatus } from "@prisma/client";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
@ -19,7 +19,7 @@ export const ProgressNavIndicator = () => {
|
||||||
queryKey: ['repos', domain],
|
queryKey: ['repos', domain],
|
||||||
queryFn: () => unwrapServiceError(getRepos(domain)),
|
queryFn: () => unwrapServiceError(getRepos(domain)),
|
||||||
select: (data) => data.filter(repo => repo.repoIndexingStatus === RepoIndexingStatus.IN_INDEX_QUEUE || repo.repoIndexingStatus === RepoIndexingStatus.INDEXING),
|
select: (data) => data.filter(repo => repo.repoIndexingStatus === RepoIndexingStatus.IN_INDEX_QUEUE || repo.repoIndexingStatus === RepoIndexingStatus.INDEXING),
|
||||||
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPending || isError || inProgressRepos.length === 0) {
|
if (isPending || isError || inProgressRepos.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { useDomain } from "@/hooks/useDomain";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { unwrapServiceError } from "@/lib/utils";
|
import { unwrapServiceError } from "@/lib/utils";
|
||||||
import { getRepos } from "@/actions";
|
import { getRepos } from "@/actions";
|
||||||
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
|
import { env } from "@/env.mjs";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
Carousel,
|
Carousel,
|
||||||
|
|
@ -22,7 +22,7 @@ export function RepositorySnapshot() {
|
||||||
const { data: repos, isPending, isError } = useQuery({
|
const { data: repos, isPending, isError } = useQuery({
|
||||||
queryKey: ['repos', domain],
|
queryKey: ['repos', domain],
|
||||||
queryFn: () => unwrapServiceError(getRepos(domain)),
|
queryFn: () => unwrapServiceError(getRepos(domain)),
|
||||||
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPending || isError || !repos) {
|
if (isPending || isError || !repos) {
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,10 @@ import { useMemo } from "react"
|
||||||
import { KeymapType } from "@/lib/types"
|
import { KeymapType } from "@/lib/types"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useKeymapType } from "@/hooks/useKeymapType"
|
import { useKeymapType } from "@/hooks/useKeymapType"
|
||||||
import { NEXT_PUBLIC_SOURCEBOT_VERSION } from "@/lib/environment.client";
|
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { signOut } from "next-auth/react"
|
import { signOut } from "next-auth/react"
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
interface SettingsDropdownProps {
|
interface SettingsDropdownProps {
|
||||||
menuButtonClassName?: string;
|
menuButtonClassName?: string;
|
||||||
|
|
@ -147,7 +146,7 @@ export const SettingsDropdown = ({
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="px-2 py-1 text-sm text-muted-foreground">
|
<div className="px-2 py-1 text-sm text-muted-foreground">
|
||||||
version: {NEXT_PUBLIC_SOURCEBOT_VERSION}
|
version: {env.NEXT_PUBLIC_SOURCEBOT_VERSION}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { useDomain } from "@/hooks/useDomain";
|
||||||
import { getConnections } from "@/actions";
|
import { getConnections } from "@/actions";
|
||||||
import { unwrapServiceError } from "@/lib/utils";
|
import { unwrapServiceError } from "@/lib/utils";
|
||||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||||
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
|
import { env } from "@/env.mjs";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { ConnectionSyncStatus } from "@prisma/client";
|
import { ConnectionSyncStatus } from "@prisma/client";
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ export const WarningNavIndicator = () => {
|
||||||
queryKey: ['connections', domain],
|
queryKey: ['connections', domain],
|
||||||
queryFn: () => unwrapServiceError(getConnections(domain)),
|
queryFn: () => unwrapServiceError(getConnections(domain)),
|
||||||
select: (data) => data.filter(connection => connection.syncStatus === ConnectionSyncStatus.SYNCED_WITH_WARNINGS),
|
select: (data) => data.filter(connection => connection.syncStatus === ConnectionSyncStatus.SYNCED_WITH_WARNINGS),
|
||||||
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPending || isError || connections.length === 0) {
|
if (isPending || isError || connections.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { useCallback } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { flagConnectionForSync, getConnectionInfo } from "@/actions";
|
import { flagConnectionForSync, getConnectionInfo } from "@/actions";
|
||||||
import { isServiceError, unwrapServiceError } from "@/lib/utils";
|
import { isServiceError, unwrapServiceError } from "@/lib/utils";
|
||||||
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
|
import { env } from "@/env.mjs";
|
||||||
import { ConnectionSyncStatus } from "@sourcebot/db";
|
import { ConnectionSyncStatus } from "@sourcebot/db";
|
||||||
import { FiLoader } from "react-icons/fi";
|
import { FiLoader } from "react-icons/fi";
|
||||||
import { CircleCheckIcon, AlertTriangle, CircleXIcon } from "lucide-react";
|
import { CircleCheckIcon, AlertTriangle, CircleXIcon } from "lucide-react";
|
||||||
|
|
@ -31,7 +31,7 @@ export const Overview = ({ connectionId }: OverviewProps) => {
|
||||||
const { data: connection, isPending, error, refetch } = useQuery({
|
const { data: connection, isPending, error, refetch } = useQuery({
|
||||||
queryKey: ['connection', domain, connectionId],
|
queryKey: ['connection', domain, connectionId],
|
||||||
queryFn: () => unwrapServiceError(getConnectionInfo(connectionId, domain)),
|
queryFn: () => unwrapServiceError(getConnectionInfo(connectionId, domain)),
|
||||||
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSecretsNavigation = useCallback(() => {
|
const handleSecretsNavigation = useCallback(() => {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { Search, Loader2 } from "lucide-react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { RepoListItemSkeleton } from "./repoListItemSkeleton";
|
import { RepoListItemSkeleton } from "./repoListItemSkeleton";
|
||||||
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
|
import { env } from "@/env.mjs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { MultiSelect } from "@/components/ui/multi-select";
|
import { MultiSelect } from "@/components/ui/multi-select";
|
||||||
|
|
@ -78,7 +78,7 @@ export const RepoList = ({ connectionId }: RepoListProps) => {
|
||||||
return new Date(a.indexedAt ?? new Date()).getTime() - new Date(b.indexedAt ?? new Date()).getTime();
|
return new Date(a.indexedAt ?? new Date()).getTime() - new Date(b.indexedAt ?? new Date()).getTime();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: connection, isPending: isConnectionPending, error: isConnectionError } = useQuery({
|
const { data: connection, isPending: isConnectionPending, error: isConnectionError } = useQuery({
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { InfoCircledIcon } from "@radix-ui/react-icons";
|
||||||
import { getConnections } from "@/actions";
|
import { getConnections } from "@/actions";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
|
import { env } from "@/env.mjs";
|
||||||
import { RepoIndexingStatus, ConnectionSyncStatus } from "@sourcebot/db";
|
import { RepoIndexingStatus, ConnectionSyncStatus } from "@sourcebot/db";
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
@ -43,7 +43,7 @@ export const ConnectionList = ({
|
||||||
const { data: unfilteredConnections, isPending, error } = useQuery({
|
const { data: unfilteredConnections, isPending, error } = useQuery({
|
||||||
queryKey: ['connections', domain],
|
queryKey: ['connections', domain],
|
||||||
queryFn: () => unwrapServiceError(getConnections(domain)),
|
queryFn: () => unwrapServiceError(getConnections(domain)),
|
||||||
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
||||||
});
|
});
|
||||||
|
|
||||||
const connections = useMemo(() => {
|
const connections = useMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ import { columns, RepositoryColumnInfo } from "./columns";
|
||||||
import { unwrapServiceError } from "@/lib/utils";
|
import { unwrapServiceError } from "@/lib/utils";
|
||||||
import { getRepos } from "@/actions";
|
import { getRepos } from "@/actions";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
|
|
||||||
import { useDomain } from "@/hooks/useDomain";
|
import { useDomain } from "@/hooks/useDomain";
|
||||||
import { RepoIndexingStatus } from "@sourcebot/db";
|
import { RepoIndexingStatus } from "@sourcebot/db";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export const RepositoryTable = () => {
|
export const RepositoryTable = () => {
|
||||||
const domain = useDomain();
|
const domain = useDomain();
|
||||||
|
|
@ -19,7 +19,7 @@ export const RepositoryTable = () => {
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
return await unwrapServiceError(getRepos(domain));
|
return await unwrapServiceError(getRepos(domain));
|
||||||
},
|
},
|
||||||
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
||||||
refetchIntervalInBackground: true,
|
refetchIntervalInBackground: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import { CodePreviewPanel } from "./components/codePreviewPanel";
|
||||||
import { FilterPanel } from "./components/filterPanel";
|
import { FilterPanel } from "./components/filterPanel";
|
||||||
import { SearchResultsPanel } from "./components/searchResultsPanel";
|
import { SearchResultsPanel } from "./components/searchResultsPanel";
|
||||||
import { useDomain } from "@/hooks/useDomain";
|
import { useDomain } from "@/hooks/useDomain";
|
||||||
import { NEXT_PUBLIC_PUBLIC_SEARCH_DEMO } from "@/lib/environment.client";
|
|
||||||
|
|
||||||
const DEFAULT_MAX_MATCH_DISPLAY_COUNT = 10000;
|
const DEFAULT_MAX_MATCH_DISPLAY_COUNT = 10000;
|
||||||
|
|
||||||
|
|
@ -105,7 +104,6 @@ const SearchPageInternal = () => {
|
||||||
const fileLanguages = searchResponse.Result.Files?.map(file => file.Language) || [];
|
const fileLanguages = searchResponse.Result.Files?.map(file => file.Language) || [];
|
||||||
|
|
||||||
captureEvent("search_finished", {
|
captureEvent("search_finished", {
|
||||||
query: NEXT_PUBLIC_PUBLIC_SEARCH_DEMO ? searchQuery : null, // @nocheckin
|
|
||||||
contentBytesLoaded: searchResponse.Result.ContentBytesLoaded,
|
contentBytesLoaded: searchResponse.Result.ContentBytesLoaded,
|
||||||
indexBytesLoaded: searchResponse.Result.IndexBytesLoaded,
|
indexBytesLoaded: searchResponse.Result.IndexBytesLoaded,
|
||||||
crashes: searchResponse.Result.Crashes,
|
crashes: searchResponse.Result.Crashes,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { isServiceError } from "@/lib/utils";
|
||||||
import { getCurrentUserRole } from "@/actions";
|
import { getCurrentUserRole } from "@/actions";
|
||||||
import { getOrgFromDomain } from "@/data/org";
|
import { getOrgFromDomain } from "@/data/org";
|
||||||
import { ChangeOrgDomainCard } from "./components/changeOrgDomainCard";
|
import { ChangeOrgDomainCard } from "./components/changeOrgDomainCard";
|
||||||
import { SOURCEBOT_ROOT_DOMAIN } from "@/lib/environment";
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
interface GeneralSettingsPageProps {
|
interface GeneralSettingsPageProps {
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -42,7 +42,7 @@ export default async function GeneralSettingsPage({ params: { domain } }: Genera
|
||||||
<ChangeOrgDomainCard
|
<ChangeOrgDomainCard
|
||||||
orgDomain={org.domain}
|
orgDomain={org.domain}
|
||||||
currentUserRole={currentUserRole}
|
currentUserRole={currentUserRole}
|
||||||
rootDomain={SOURCEBOT_ROOT_DOMAIN}
|
rootDomain={env.SOURCEBOT_ROOT_DOMAIN}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { OrgRole } from "@sourcebot/db";
|
import { OrgRole } from "@sourcebot/db";
|
||||||
import { resolveServerPath } from "@/app/api/(client)/client";
|
|
||||||
import { useToast } from "@/components/hooks/use-toast";
|
import { useToast } from "@/components/hooks/use-toast";
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||||
import { Avatar, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
|
@ -124,8 +123,7 @@ export const InvitesList = ({ invites, currentUserRole }: InviteListProps) => {
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
title="Copy invite link"
|
title="Copy invite link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const basePath = `${window.location.origin}${resolveServerPath('/')}`;
|
const url = createPathWithQueryParams(`${window.location.origin}/redeem?invite_id=${invite.id}`);
|
||||||
const url = createPathWithQueryParams(`${basePath}redeem?invite_id=${invite.id}`);
|
|
||||||
navigator.clipboard.writeText(url)
|
navigator.clipboard.writeText(url)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast({
|
toast({
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_DOMAIN_SUB_PATH } from "@/lib/environment.client";
|
|
||||||
import { fileSourceResponseSchema, getVersionResponseSchema, listRepositoriesResponseSchema, searchResponseSchema } from "@/lib/schemas";
|
import { fileSourceResponseSchema, getVersionResponseSchema, listRepositoriesResponseSchema, searchResponseSchema } from "@/lib/schemas";
|
||||||
import { FileSourceRequest, FileSourceResponse, GetVersionResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "@/lib/types";
|
import { FileSourceRequest, FileSourceResponse, GetVersionResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "@/lib/types";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
|
|
||||||
export const search = async (body: SearchRequest, domain: string): Promise<SearchResponse> => {
|
export const search = async (body: SearchRequest, domain: string): Promise<SearchResponse> => {
|
||||||
const path = resolveServerPath("/api/search");
|
const result = await fetch("/api/search", {
|
||||||
const result = await fetch(path, {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -20,8 +18,7 @@ export const search = async (body: SearchRequest, domain: string): Promise<Searc
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchFileSource = async (body: FileSourceRequest, domain: string): Promise<FileSourceResponse> => {
|
export const fetchFileSource = async (body: FileSourceRequest, domain: string): Promise<FileSourceResponse> => {
|
||||||
const path = resolveServerPath("/api/source");
|
const result = await fetch("/api/source", {
|
||||||
const result = await fetch(path, {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -34,8 +31,7 @@ export const fetchFileSource = async (body: FileSourceRequest, domain: string):
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getRepos = async (domain: string): Promise<ListRepositoriesResponse> => {
|
export const getRepos = async (domain: string): Promise<ListRepositoriesResponse> => {
|
||||||
const path = resolveServerPath("/api/repos");
|
const result = await fetch("/api/repos", {
|
||||||
const result = await fetch(path, {
|
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -47,8 +43,7 @@ export const getRepos = async (domain: string): Promise<ListRepositoriesResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getVersion = async (): Promise<GetVersionResponse> => {
|
export const getVersion = async (): Promise<GetVersionResponse> => {
|
||||||
const path = resolveServerPath("/api/version");
|
const result = await fetch("/api/version", {
|
||||||
const result = await fetch(path, {
|
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -56,13 +51,3 @@ export const getVersion = async (): Promise<GetVersionResponse> => {
|
||||||
}).then(response => response.json());
|
}).then(response => response.json());
|
||||||
return getVersionResponseSchema.parse(result);
|
return getVersionResponseSchema.parse(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a subpath to a api route on the server (e.g., /api/search),
|
|
||||||
* returns the full path to that route on the server, taking into account
|
|
||||||
* the base path (if any).
|
|
||||||
*/
|
|
||||||
export const resolveServerPath = (path: string) => {
|
|
||||||
assert(path.startsWith("/"));
|
|
||||||
return `${NEXT_PUBLIC_DOMAIN_SUB_PATH}${path}`;
|
|
||||||
}
|
|
||||||
|
|
@ -2,9 +2,9 @@ import { headers } from 'next/headers';
|
||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
import { prisma } from '@/prisma';
|
import { prisma } from '@/prisma';
|
||||||
import { STRIPE_WEBHOOK_SECRET } from '@/lib/environment';
|
|
||||||
import { getStripe } from '@/lib/stripe';
|
|
||||||
import { ConnectionSyncStatus, StripeSubscriptionStatus } from '@sourcebot/db';
|
import { ConnectionSyncStatus, StripeSubscriptionStatus } from '@sourcebot/db';
|
||||||
|
import { stripeClient } from '@/lib/stripe';
|
||||||
|
import { env } from '@/env.mjs';
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
const body = await req.text();
|
const body = await req.text();
|
||||||
|
|
@ -14,12 +14,19 @@ export async function POST(req: NextRequest) {
|
||||||
return new Response('No signature', { status: 400 });
|
return new Response('No signature', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!stripeClient) {
|
||||||
|
return new Response('Stripe client not initialized', { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!env.STRIPE_WEBHOOK_SECRET) {
|
||||||
|
return new Response('Stripe webhook secret not set', { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stripe = getStripe();
|
const event = stripeClient.webhooks.constructEvent(
|
||||||
const event = stripe.webhooks.constructEvent(
|
|
||||||
body,
|
body,
|
||||||
signature,
|
signature,
|
||||||
STRIPE_WEBHOOK_SECRET!
|
env.STRIPE_WEBHOOK_SECRET
|
||||||
);
|
);
|
||||||
|
|
||||||
if (event.type === 'customer.subscription.deleted') {
|
if (event.type === 'customer.subscription.deleted') {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { SOURCEBOT_VERSION } from "@/lib/environment";
|
import { env } from "@/env.mjs";
|
||||||
import { GetVersionResponse } from "@/lib/types";
|
import { GetVersionResponse } from "@/lib/types";
|
||||||
|
|
||||||
// Note: In Next.JS 14, GET methods with no params are cached by default at build time.
|
// Note: In Next.JS 14, GET methods with no params are cached by default at build time.
|
||||||
|
|
@ -10,6 +10,6 @@ export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export const GET = async () => {
|
export const GET = async () => {
|
||||||
return Response.json({
|
return Response.json({
|
||||||
version: SOURCEBOT_VERSION,
|
version: env.NEXT_PUBLIC_SOURCEBOT_VERSION,
|
||||||
} satisfies GetVersionResponse);
|
} satisfies GetVersionResponse);
|
||||||
}
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import { PostHogProvider } from "./posthogProvider";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Sourcebot",
|
title: "Sourcebot",
|
||||||
|
|
@ -26,7 +27,7 @@ export default function RootLayout({
|
||||||
<body>
|
<body>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<PostHogProvider>
|
<PostHogProvider disabled={env.SOURCEBOT_TELEMETRY_DISABLED === "true"}>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { redirect } from "next/navigation";
|
||||||
import { OnboardHeader } from "./components/onboardHeader";
|
import { OnboardHeader } from "./components/onboardHeader";
|
||||||
import { OnboardingSteps } from "@/lib/constants";
|
import { OnboardingSteps } from "@/lib/constants";
|
||||||
import { LogoutEscapeHatch } from "../components/logoutEscapeHatch";
|
import { LogoutEscapeHatch } from "../components/logoutEscapeHatch";
|
||||||
import { SOURCEBOT_ROOT_DOMAIN } from "@/lib/environment";
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export default async function Onboarding() {
|
export default async function Onboarding() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
@ -19,7 +19,7 @@ export default async function Onboarding() {
|
||||||
description="Create a organization for your team to search and share code across your repositories."
|
description="Create a organization for your team to search and share code across your repositories."
|
||||||
step={OnboardingSteps.CreateOrg}
|
step={OnboardingSteps.CreateOrg}
|
||||||
/>
|
/>
|
||||||
<OrgCreateForm rootDomain={SOURCEBOT_ROOT_DOMAIN} />
|
<OrgCreateForm rootDomain={env.SOURCEBOT_ROOT_DOMAIN} />
|
||||||
<LogoutEscapeHatch className="absolute top-0 right-0 p-4 sm:p-12" />
|
<LogoutEscapeHatch className="absolute top-0 right-0 p-4 sm:p-12" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
'use client'
|
'use client'
|
||||||
import { NEXT_PUBLIC_POSTHOG_PAPIK, NEXT_PUBLIC_POSTHOG_UI_HOST, NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED, NEXT_PUBLIC_PUBLIC_SEARCH_DEMO } from '@/lib/environment.client'
|
|
||||||
import posthog from 'posthog-js'
|
import posthog from 'posthog-js'
|
||||||
import { usePostHog } from 'posthog-js/react'
|
import { usePostHog } from 'posthog-js/react'
|
||||||
import { PostHogProvider as PHProvider } from 'posthog-js/react'
|
import { PostHogProvider as PHProvider } from 'posthog-js/react'
|
||||||
import { resolveServerPath } from './api/(client)/client'
|
|
||||||
import { isDefined } from '@/lib/utils'
|
|
||||||
import { usePathname, useSearchParams } from "next/navigation"
|
import { usePathname, useSearchParams } from "next/navigation"
|
||||||
import { useEffect, Suspense } from "react"
|
import { Suspense, useEffect } from "react"
|
||||||
|
import { env } from '@/env.mjs'
|
||||||
const POSTHOG_ENABLED = isDefined(NEXT_PUBLIC_POSTHOG_PAPIK) && !NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED;
|
|
||||||
|
|
||||||
function PostHogPageView() {
|
function PostHogPageView() {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|
@ -30,26 +26,23 @@ function PostHogPageView() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SuspendedPostHogPageView() {
|
interface PostHogProviderProps {
|
||||||
return <Suspense fallback={null}>
|
children: React.ReactNode
|
||||||
<PostHogPageView />
|
disabled: boolean
|
||||||
</Suspense>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
export function PostHogProvider({ children, disabled }: PostHogProviderProps) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (POSTHOG_ENABLED) {
|
if (!disabled && env.NEXT_PUBLIC_POSTHOG_PAPIK) {
|
||||||
|
posthog.init(env.NEXT_PUBLIC_POSTHOG_PAPIK, {
|
||||||
// @see next.config.mjs for path rewrites to the "/ingest" route.
|
// @see next.config.mjs for path rewrites to the "/ingest" route.
|
||||||
const posthogHostPath = resolveServerPath('/ingest');
|
api_host: "/ingest",
|
||||||
|
ui_host: env.NEXT_PUBLIC_POSTHOG_UI_HOST,
|
||||||
posthog.init(NEXT_PUBLIC_POSTHOG_PAPIK!, {
|
|
||||||
api_host: posthogHostPath,
|
|
||||||
ui_host: NEXT_PUBLIC_POSTHOG_UI_HOST,
|
|
||||||
person_profiles: 'identified_only',
|
person_profiles: 'identified_only',
|
||||||
capture_pageview: NEXT_PUBLIC_PUBLIC_SEARCH_DEMO, // @nocheckin Disable automatic pageview capture if we're not in public demo mode
|
capture_pageview: false, // @nocheckin Disable automatic pageview capture if we're not in public demo mode
|
||||||
autocapture: false, // Disable automatic event capture
|
autocapture: false, // Disable automatic event capture
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
sanitize_properties: !NEXT_PUBLIC_PUBLIC_SEARCH_DEMO ? (properties: Record<string, any>, _event: string) => {
|
sanitize_properties: (properties: Record<string, any>, _event: string) => {
|
||||||
// https://posthog.com/docs/libraries/js#config
|
// https://posthog.com/docs/libraries/js#config
|
||||||
if (properties['$current_url']) {
|
if (properties['$current_url']) {
|
||||||
properties['$current_url'] = null;
|
properties['$current_url'] = null;
|
||||||
|
|
@ -59,16 +52,18 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return properties;
|
return properties;
|
||||||
} : undefined
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log("PostHog telemetry disabled");
|
console.log("PostHog telemetry disabled");
|
||||||
}
|
}
|
||||||
}, [])
|
}, [disabled])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PHProvider client={posthog}>
|
<PHProvider client={posthog}>
|
||||||
<SuspendedPostHogPageView />
|
<Suspense fallback={null}>
|
||||||
|
<PostHogPageView />
|
||||||
|
</Suspense>
|
||||||
{children}
|
{children}
|
||||||
</PHProvider>
|
</PHProvider>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,7 @@ import Credentials from "next-auth/providers/credentials"
|
||||||
import EmailProvider from "next-auth/providers/nodemailer";
|
import EmailProvider from "next-auth/providers/nodemailer";
|
||||||
import { PrismaAdapter } from "@auth/prisma-adapter"
|
import { PrismaAdapter } from "@auth/prisma-adapter"
|
||||||
import { prisma } from "@/prisma";
|
import { prisma } from "@/prisma";
|
||||||
import {
|
import { env } from "@/env.mjs";
|
||||||
AUTH_GITHUB_CLIENT_ID,
|
|
||||||
AUTH_GITHUB_CLIENT_SECRET,
|
|
||||||
AUTH_GOOGLE_CLIENT_ID,
|
|
||||||
AUTH_GOOGLE_CLIENT_SECRET,
|
|
||||||
AUTH_SECRET,
|
|
||||||
AUTH_URL,
|
|
||||||
AUTH_CREDENTIALS_LOGIN_ENABLED,
|
|
||||||
EMAIL_FROM,
|
|
||||||
SMTP_CONNECTION_URL
|
|
||||||
} from "./lib/environment";
|
|
||||||
import { User } from '@sourcebot/db';
|
import { User } from '@sourcebot/db';
|
||||||
import 'next-auth/jwt';
|
import 'next-auth/jwt';
|
||||||
import type { Provider } from "next-auth/providers";
|
import type { Provider } from "next-auth/providers";
|
||||||
|
|
@ -44,24 +34,24 @@ declare module 'next-auth/jwt' {
|
||||||
export const getProviders = () => {
|
export const getProviders = () => {
|
||||||
const providers: Provider[] = [];
|
const providers: Provider[] = [];
|
||||||
|
|
||||||
if (AUTH_GITHUB_CLIENT_ID && AUTH_GITHUB_CLIENT_SECRET) {
|
if (env.AUTH_GITHUB_CLIENT_ID && env.AUTH_GITHUB_CLIENT_SECRET) {
|
||||||
providers.push(GitHub({
|
providers.push(GitHub({
|
||||||
clientId: AUTH_GITHUB_CLIENT_ID,
|
clientId: env.AUTH_GITHUB_CLIENT_ID,
|
||||||
clientSecret: AUTH_GITHUB_CLIENT_SECRET,
|
clientSecret: env.AUTH_GITHUB_CLIENT_SECRET,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AUTH_GOOGLE_CLIENT_ID && AUTH_GOOGLE_CLIENT_SECRET) {
|
if (env.AUTH_GOOGLE_CLIENT_ID && env.AUTH_GOOGLE_CLIENT_SECRET) {
|
||||||
providers.push(Google({
|
providers.push(Google({
|
||||||
clientId: AUTH_GOOGLE_CLIENT_ID,
|
clientId: env.AUTH_GOOGLE_CLIENT_ID,
|
||||||
clientSecret: AUTH_GOOGLE_CLIENT_SECRET,
|
clientSecret: env.AUTH_GOOGLE_CLIENT_SECRET,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SMTP_CONNECTION_URL && EMAIL_FROM) {
|
if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM) {
|
||||||
providers.push(EmailProvider({
|
providers.push(EmailProvider({
|
||||||
server: SMTP_CONNECTION_URL,
|
server: env.SMTP_CONNECTION_URL,
|
||||||
from: EMAIL_FROM,
|
from: env.EMAIL_FROM,
|
||||||
maxAge: 60 * 10,
|
maxAge: 60 * 10,
|
||||||
generateVerificationToken: async () => {
|
generateVerificationToken: async () => {
|
||||||
const token = String(Math.floor(100000 + Math.random() * 900000));
|
const token = String(Math.floor(100000 + Math.random() * 900000));
|
||||||
|
|
@ -69,7 +59,7 @@ export const getProviders = () => {
|
||||||
},
|
},
|
||||||
sendVerificationRequest: async ({ identifier, provider, token }) => {
|
sendVerificationRequest: async ({ identifier, provider, token }) => {
|
||||||
const transport = createTransport(provider.server);
|
const transport = createTransport(provider.server);
|
||||||
const html = await render(MagicLinkEmail({ baseUrl: AUTH_URL, token: token }));
|
const html = await render(MagicLinkEmail({ baseUrl: env.AUTH_URL, token: token }));
|
||||||
const result = await transport.sendMail({
|
const result = await transport.sendMail({
|
||||||
to: identifier,
|
to: identifier,
|
||||||
from: provider.from,
|
from: provider.from,
|
||||||
|
|
@ -86,7 +76,7 @@ export const getProviders = () => {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AUTH_CREDENTIALS_LOGIN_ENABLED) {
|
if (env.AUTH_CREDENTIALS_LOGIN_ENABLED) {
|
||||||
providers.push(Credentials({
|
providers.push(Credentials({
|
||||||
credentials: {
|
credentials: {
|
||||||
email: {},
|
email: {},
|
||||||
|
|
@ -102,7 +92,7 @@ export const getProviders = () => {
|
||||||
|
|
||||||
// authorize runs in the edge runtime (where we cannot make DB calls / access environment variables),
|
// authorize runs in the edge runtime (where we cannot make DB calls / access environment variables),
|
||||||
// so we need to make a request to the server to verify the credentials.
|
// so we need to make a request to the server to verify the credentials.
|
||||||
const response = await fetch(new URL('/api/auth/verifyCredentials', AUTH_URL), {
|
const response = await fetch(new URL('/api/auth/verifyCredentials', env.AUTH_URL), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ email, password }),
|
body: JSON.stringify({ email, password }),
|
||||||
});
|
});
|
||||||
|
|
@ -125,11 +115,11 @@ export const getProviders = () => {
|
||||||
return providers;
|
return providers;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useSecureCookies = AUTH_URL?.startsWith("https://") ?? false;
|
const useSecureCookies = env.AUTH_URL?.startsWith("https://") ?? false;
|
||||||
const hostName = AUTH_URL ? new URL(AUTH_URL).hostname : "localhost";
|
const hostName = env.AUTH_URL ? new URL(env.AUTH_URL).hostname : "localhost";
|
||||||
|
|
||||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
secret: AUTH_SECRET,
|
secret: env.AUTH_SECRET,
|
||||||
adapter: PrismaAdapter(prisma),
|
adapter: PrismaAdapter(prisma),
|
||||||
session: {
|
session: {
|
||||||
strategy: "jwt",
|
strategy: "jwt",
|
||||||
|
|
|
||||||
60
packages/web/src/env.mjs
Normal file
60
packages/web/src/env.mjs
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { createEnv } from "@t3-oss/env-nextjs";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const env = createEnv({
|
||||||
|
server: {
|
||||||
|
// Zoekt
|
||||||
|
ZOEKT_WEBSERVER_URL: z.string().url().default('http://localhost:6070'),
|
||||||
|
SHARD_MAX_MATCH_COUNT: z.number().default(10000),
|
||||||
|
TOTAL_MAX_MATCH_COUNT: z.number().default(100000),
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
AUTH_SECRET: z.string(),
|
||||||
|
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
|
||||||
|
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
|
||||||
|
AUTH_GOOGLE_CLIENT_ID: z.string().optional(),
|
||||||
|
AUTH_GOOGLE_CLIENT_SECRET: z.string().optional(),
|
||||||
|
AUTH_URL: z.string().url(),
|
||||||
|
AUTH_CREDENTIALS_LOGIN_ENABLED: z.boolean().default(true),
|
||||||
|
|
||||||
|
// Email
|
||||||
|
SMTP_CONNECTION_URL: z.string().url().optional(),
|
||||||
|
EMAIL_FROM: z.string().email().optional(),
|
||||||
|
|
||||||
|
// Stripe
|
||||||
|
STRIPE_SECRET_KEY: z.string().optional(),
|
||||||
|
STRIPE_PRODUCT_ID: z.string().optional(),
|
||||||
|
STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
CONFIG_MAX_REPOS_NO_TOKEN: z.number().default(500),
|
||||||
|
SOURCEBOT_ROOT_DOMAIN: z.string().default("localhost:3000"),
|
||||||
|
NODE_ENV: z.enum(["development", "test", "production"]),
|
||||||
|
SOURCEBOT_TELEMETRY_DISABLED: z.enum(["true", "false"]).default("false"),
|
||||||
|
},
|
||||||
|
// @NOTE: Make sure you destructure all client variables in the
|
||||||
|
// `experimental__runtimeEnv` block below.
|
||||||
|
client: {
|
||||||
|
// PostHog
|
||||||
|
NEXT_PUBLIC_POSTHOG_PAPIK: z.string().optional(),
|
||||||
|
NEXT_PUBLIC_POSTHOG_HOST: z.string().url().default('https://us.i.posthog.com'),
|
||||||
|
NEXT_PUBLIC_POSTHOG_ASSET_HOST: z.string().url().default('https://us-assets.i.posthog.com'),
|
||||||
|
NEXT_PUBLIC_POSTHOG_UI_HOST: z.string().url().default('https://us.posthog.com'),
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
NEXT_PUBLIC_SOURCEBOT_VERSION: z.string().default('unknown'),
|
||||||
|
NEXT_PUBLIC_POLLING_INTERVAL_MS: z.number().default(5000),
|
||||||
|
},
|
||||||
|
// For Next.js >= 13.4.4, you only need to destructure client variables:
|
||||||
|
experimental__runtimeEnv: {
|
||||||
|
NEXT_PUBLIC_POSTHOG_PAPIK: process.env.NEXT_PUBLIC_POSTHOG_PAPIK,
|
||||||
|
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||||
|
NEXT_PUBLIC_POSTHOG_ASSET_HOST: process.env.NEXT_PUBLIC_POSTHOG_ASSET_HOST,
|
||||||
|
NEXT_PUBLIC_POSTHOG_UI_HOST: process.env.NEXT_PUBLIC_POSTHOG_UI_HOST,
|
||||||
|
NEXT_PUBLIC_SOURCEBOT_VERSION: process.env.NEXT_PUBLIC_SOURCEBOT_VERSION,
|
||||||
|
NEXT_PUBLIC_POLLING_INTERVAL_MS: process.env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
||||||
|
},
|
||||||
|
// Skip environment variable validation in Docker builds.
|
||||||
|
skipValidation: process.env.DOCKER_BUILD === "1",
|
||||||
|
emptyStringAsUndefined: true,
|
||||||
|
});
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { CaptureOptions } from "posthog-js";
|
import { CaptureOptions } from "posthog-js";
|
||||||
import posthog from "posthog-js";
|
import posthog from "posthog-js";
|
||||||
import { PosthogEvent, PosthogEventMap } from "../lib/posthogEvents";
|
import { PosthogEvent, PosthogEventMap } from "../lib/posthogEvents";
|
||||||
import { NEXT_PUBLIC_SOURCEBOT_VERSION } from "@/lib/environment.client";
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export function captureEvent<E extends PosthogEvent>(event: E, properties: PosthogEventMap[E], options?: CaptureOptions) {
|
export function captureEvent<E extends PosthogEvent>(event: E, properties: PosthogEventMap[E], options?: CaptureOptions) {
|
||||||
if(!options) {
|
if(!options) {
|
||||||
|
|
@ -12,7 +12,7 @@ export function captureEvent<E extends PosthogEvent>(event: E, properties: Posth
|
||||||
options.send_instantly = true;
|
options.send_instantly = true;
|
||||||
posthog.capture(event, {
|
posthog.capture(event, {
|
||||||
...properties,
|
...properties,
|
||||||
sourcebot_version: NEXT_PUBLIC_SOURCEBOT_VERSION,
|
sourcebot_version: env.NEXT_PUBLIC_SOURCEBOT_VERSION,
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import 'client-only';
|
|
||||||
|
|
||||||
import { getEnv, getEnvBoolean, getEnvNumber } from "./utils";
|
|
||||||
|
|
||||||
export const NEXT_PUBLIC_POSTHOG_PAPIK = getEnv(process.env.NEXT_PUBLIC_POSTHOG_PAPIK);
|
|
||||||
export const NEXT_PUBLIC_POSTHOG_HOST = getEnv(process.env.NEXT_PUBLIC_POSTHOG_HOST);
|
|
||||||
export const NEXT_PUBLIC_POSTHOG_UI_HOST = getEnv(process.env.NEXT_PUBLIC_POSTHOG_UI_HOST);
|
|
||||||
export const NEXT_PUBLIC_POSTHOG_ASSET_HOST = getEnv(process.env.NEXT_PUBLIC_POSTHOG_ASSET_HOST);
|
|
||||||
export const NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED = getEnvBoolean(process.env.NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED, false);
|
|
||||||
export const NEXT_PUBLIC_SOURCEBOT_VERSION = getEnv(process.env.NEXT_PUBLIC_SOURCEBOT_VERSION, "unknown")!;
|
|
||||||
export const NEXT_PUBLIC_DOMAIN_SUB_PATH = getEnv(process.env.NEXT_PUBLIC_DOMAIN_SUB_PATH, "")!;
|
|
||||||
export const NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = getEnv(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
|
|
||||||
export const NEXT_PUBLIC_POLLING_INTERVAL_MS = getEnvNumber(process.env.NEXT_PUBLIC_POLLING_INTERVAL_MS, 5000);
|
|
||||||
export const NEXT_PUBLIC_PUBLIC_SEARCH_DEMO = getEnvBoolean(process.env.NEXT_PUBLIC_PUBLIC_SEARCH_DEMO, false);
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import 'server-only';
|
|
||||||
|
|
||||||
import { getEnv, getEnvBoolean, getEnvNumber } from "./utils";
|
|
||||||
|
|
||||||
export const ZOEKT_WEBSERVER_URL = getEnv(process.env.ZOEKT_WEBSERVER_URL, "http://localhost:6070")!;
|
|
||||||
export const SHARD_MAX_MATCH_COUNT = getEnvNumber(process.env.SHARD_MAX_MATCH_COUNT, 10000);
|
|
||||||
export const TOTAL_MAX_MATCH_COUNT = getEnvNumber(process.env.TOTAL_MAX_MATCH_COUNT, 100000);
|
|
||||||
export const SOURCEBOT_VERSION = getEnv(process.env.SOURCEBOT_VERSION, 'unknown')!;
|
|
||||||
export const NODE_ENV = process.env.NODE_ENV;
|
|
||||||
|
|
||||||
export const AUTH_SECRET = getEnv(process.env.AUTH_SECRET); // Generate using `npx auth secret`
|
|
||||||
export const AUTH_GITHUB_CLIENT_ID = getEnv(process.env.AUTH_GITHUB_CLIENT_ID);
|
|
||||||
export const AUTH_GITHUB_CLIENT_SECRET = getEnv(process.env.AUTH_GITHUB_CLIENT_SECRET);
|
|
||||||
export const AUTH_GOOGLE_CLIENT_ID = getEnv(process.env.AUTH_GOOGLE_CLIENT_ID);
|
|
||||||
export const AUTH_GOOGLE_CLIENT_SECRET = getEnv(process.env.AUTH_GOOGLE_CLIENT_SECRET);
|
|
||||||
export const AUTH_URL = getEnv(process.env.AUTH_URL)!;
|
|
||||||
export const AUTH_CREDENTIALS_LOGIN_ENABLED = getEnvBoolean(process.env.AUTH_CREDENTIALS_LOGIN_ENABLED, true);
|
|
||||||
|
|
||||||
export const STRIPE_SECRET_KEY = getEnv(process.env.STRIPE_SECRET_KEY);
|
|
||||||
export const STRIPE_PRODUCT_ID = getEnv(process.env.STRIPE_PRODUCT_ID);
|
|
||||||
export const STRIPE_WEBHOOK_SECRET = getEnv(process.env.STRIPE_WEBHOOK_SECRET);
|
|
||||||
|
|
||||||
export const CONFIG_MAX_REPOS_NO_TOKEN = getEnvNumber(process.env.CONFIG_MAX_REPOS_NO_TOKEN, 500);
|
|
||||||
|
|
||||||
export const SMTP_CONNECTION_URL = getEnv(process.env.SMTP_CONNECTION_URL);
|
|
||||||
export const EMAIL_FROM = getEnv(process.env.EMAIL_FROM);
|
|
||||||
|
|
||||||
export const SOURCEBOT_ROOT_DOMAIN = getEnv(process.env.SOURCEBOT_ROOT_DOMAIN, "localhost:3000")!;
|
|
||||||
export const PUBLIC_SEARCH_DEMO = getEnvBoolean(process.env.PUBLIC_SEARCH_DEMO, false);
|
|
||||||
|
|
@ -19,4 +19,5 @@ export enum ErrorCode {
|
||||||
STRIPE_CHECKOUT_ERROR = 'STRIPE_CHECKOUT_ERROR',
|
STRIPE_CHECKOUT_ERROR = 'STRIPE_CHECKOUT_ERROR',
|
||||||
SECRET_ALREADY_EXISTS = 'SECRET_ALREADY_EXISTS',
|
SECRET_ALREADY_EXISTS = 'SECRET_ALREADY_EXISTS',
|
||||||
SUBSCRIPTION_ALREADY_EXISTS = 'SUBSCRIPTION_ALREADY_EXISTS',
|
SUBSCRIPTION_ALREADY_EXISTS = 'SUBSCRIPTION_ALREADY_EXISTS',
|
||||||
|
STRIPE_CLIENT_NOT_INITIALIZED = 'STRIPE_CLIENT_NOT_INITIALIZED',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
export type PosthogEventMap = {
|
export type PosthogEventMap = {
|
||||||
search_finished: {
|
search_finished: {
|
||||||
query: string | null,
|
|
||||||
contentBytesLoaded: number,
|
contentBytesLoaded: number,
|
||||||
indexBytesLoaded: number,
|
indexBytesLoaded: number,
|
||||||
crashes: number,
|
crashes: number,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import escapeStringRegexp from "escape-string-regexp";
|
import escapeStringRegexp from "escape-string-regexp";
|
||||||
import { SHARD_MAX_MATCH_COUNT, TOTAL_MAX_MATCH_COUNT } from "../environment";
|
import { env } from "@/env.mjs";
|
||||||
import { listRepositoriesResponseSchema, zoektSearchResponseSchema } from "../schemas";
|
import { listRepositoriesResponseSchema, zoektSearchResponseSchema } from "../schemas";
|
||||||
import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "../types";
|
import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "../types";
|
||||||
import { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } from "../serviceError";
|
import { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } from "../serviceError";
|
||||||
|
|
@ -59,8 +59,8 @@ export const search = async ({ query, maxMatchDisplayCount, whole}: SearchReques
|
||||||
ChunkMatches: true,
|
ChunkMatches: true,
|
||||||
MaxMatchDisplayCount: maxMatchDisplayCount,
|
MaxMatchDisplayCount: maxMatchDisplayCount,
|
||||||
Whole: !!whole,
|
Whole: !!whole,
|
||||||
ShardMaxMatchCount: SHARD_MAX_MATCH_COUNT,
|
ShardMaxMatchCount: env.SHARD_MAX_MATCH_COUNT,
|
||||||
TotalMaxMatchCount: TOTAL_MAX_MATCH_COUNT,
|
TotalMaxMatchCount: env.TOTAL_MAX_MATCH_COUNT,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { ZOEKT_WEBSERVER_URL } from "../environment"
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
|
|
||||||
interface ZoektRequest {
|
interface ZoektRequest {
|
||||||
path: string,
|
path: string,
|
||||||
|
|
@ -17,7 +16,7 @@ export const zoektFetch = async ({
|
||||||
cache,
|
cache,
|
||||||
}: ZoektRequest) => {
|
}: ZoektRequest) => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
new URL(path, ZOEKT_WEBSERVER_URL),
|
new URL(path, env.ZOEKT_WEBSERVER_URL),
|
||||||
{
|
{
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
import 'server-only';
|
import 'server-only';
|
||||||
|
import { env } from '@/env.mjs'
|
||||||
|
import Stripe from "stripe";
|
||||||
|
|
||||||
import Stripe from 'stripe'
|
export const stripeClient =
|
||||||
import { STRIPE_SECRET_KEY } from './environment'
|
env.STRIPE_SECRET_KEY
|
||||||
|
? new Stripe(env.STRIPE_SECRET_KEY)
|
||||||
let stripeInstance: Stripe | null = null;
|
: undefined;
|
||||||
export const getStripe = () => {
|
|
||||||
if (!stripeInstance) {
|
|
||||||
stripeInstance = new Stripe(STRIPE_SECRET_KEY!);
|
|
||||||
}
|
|
||||||
return stripeInstance;
|
|
||||||
}
|
|
||||||
|
|
@ -158,30 +158,6 @@ export const isServiceError = (data: unknown): data is ServiceError => {
|
||||||
'message' in data;
|
'message' in data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getEnv = (env: string | undefined, defaultValue?: string) => {
|
|
||||||
return env ?? defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getEnvNumber = (env: string | undefined, defaultValue: number = 0) => {
|
|
||||||
if (!env) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const num = Number(env);
|
|
||||||
if (isNaN(num)) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return num;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getEnvBoolean = (env: string | undefined, defaultValue: boolean) => {
|
|
||||||
if (!env) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
return env === 'true' || env === '1';
|
|
||||||
}
|
|
||||||
|
|
||||||
// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
||||||
export const base64Decode = (base64: string): string => {
|
export const base64Decode = (base64: string): string => {
|
||||||
const binString = atob(base64);
|
const binString = atob(base64);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,6 @@
|
||||||
"@/public/*": ["./public/*"]
|
"@/public/*": ["./public/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/env.mjs"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
yarn.lock
20
yarn.lock
|
|
@ -3555,6 +3555,18 @@
|
||||||
"@swc/counter" "^0.1.3"
|
"@swc/counter" "^0.1.3"
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
|
"@t3-oss/env-core@0.12.0", "@t3-oss/env-core@^0.12.0":
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.npmjs.org/@t3-oss/env-core/-/env-core-0.12.0.tgz#d5b6d92bf07d2f3ccdf59cc428f1faf114350d35"
|
||||||
|
integrity sha512-lOPj8d9nJJTt81mMuN9GMk8x5veOt7q9m11OSnCBJhwp1QrL/qR+M8Y467ULBSm9SunosryWNbmQQbgoiMgcdw==
|
||||||
|
|
||||||
|
"@t3-oss/env-nextjs@^0.12.0":
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.npmjs.org/@t3-oss/env-nextjs/-/env-nextjs-0.12.0.tgz#a3a89c5d2eca35c96e8ffd5fe97922873a39c7a0"
|
||||||
|
integrity sha512-rFnvYk1049RnNVUPvY8iQ55AuQh1Rr+qZzQBh3t++RttCGK4COpXGNxS4+45afuQq02lu+QAOy/5955aU8hRKw==
|
||||||
|
dependencies:
|
||||||
|
"@t3-oss/env-core" "0.12.0"
|
||||||
|
|
||||||
"@tanstack/query-core@5.59.0":
|
"@tanstack/query-core@5.59.0":
|
||||||
version "5.59.0"
|
version "5.59.0"
|
||||||
resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.0.tgz"
|
resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.0.tgz"
|
||||||
|
|
@ -10474,10 +10486,10 @@ yocto-queue@^0.1.0:
|
||||||
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
|
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
zod@^3.23.8:
|
zod@^3.24.2:
|
||||||
version "3.23.8"
|
version "3.24.2"
|
||||||
resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz"
|
resolved "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
|
||||||
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
|
integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==
|
||||||
|
|
||||||
zwitch@^2.0.4:
|
zwitch@^2.0.4:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue