diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 6347d9d7..2df57459 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,7 @@ { "recommendations": [ "dbaeumer.vscode-eslint", - "bradlc.vscode-tailwindcss" + "bradlc.vscode-tailwindcss", + "prisma.prisma" ] } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 69cb1b8c..afb7787e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,14 @@ RUN go mod download COPY vendor/zoekt ./ RUN CGO_ENABLED=0 GOOS=linux go build -o /cmd/ ./cmd/... +# ------ Build Database ------ +FROM node-alpine AS database-builder +WORKDIR /app + +COPY package.json yarn.lock* ./ +COPY ./packages/db ./packages/db +RUN yarn workspace @sourcebot/db install --frozen-lockfile + # ------ Build Web ------ FROM node-alpine AS web-builder RUN apk add --no-cache libc6-compat @@ -17,6 +25,8 @@ WORKDIR /app COPY package.json yarn.lock* ./ COPY ./packages/web ./packages/web +COPY --from=database-builder /app/node_modules ./node_modules +COPY --from=database-builder /app/packages/db ./packages/db # Fixes arm64 timeouts RUN yarn config set registry https://registry.npmjs.org/ @@ -27,17 +37,15 @@ ENV NEXT_TELEMETRY_DISABLED=1 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_POSTHOG_PAPIK=BAKED_NEXT_PUBLIC_POSTHOG_PAPIK -# @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 -# ------ Build Database ------ -FROM node-alpine AS database-builder -WORKDIR /app +# @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 -COPY package.json yarn.lock* ./ -COPY ./packages/db ./packages/db -RUN yarn workspace @sourcebot/db install --frozen-lockfile +RUN yarn workspace @sourcebot/web build # ------ Build Backend ------ diff --git a/entrypoint.sh b/entrypoint.sh index 1a8355bc..e43c760c 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -107,46 +107,50 @@ echo -e "\e[34m[Info] Using config file at: '$CONFIG_PATH'.\e[0m" done } - -# 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. +# @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. # -# @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 -} +# # 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 +# } # Run supervisord diff --git a/package.json b/package.json index 4062ebdd..786cf2da 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "scripts": { "build": "yarn workspaces run build", "test": "yarn workspaces run test", - "dev": "npm-run-all --print-label --parallel dev:zoekt dev:backend dev:web dev:redis", - "dev:mt": "npm-run-all --print-label --parallel dev:zoekt:mt dev:backend dev:web dev:redis", + "dev": "yarn workspace @sourcebot/db prisma:migrate:dev && npm-run-all --print-label --parallel dev:zoekt dev:backend dev:web dev:redis", + "dev:mt": "yarn workspace @sourcebot/db prisma:migrate:dev && npm-run-all --print-label --parallel dev:zoekt:mt dev:backend dev:web dev:redis", "dev:zoekt": "export PATH=\"$PWD/bin:$PATH\" && export SRC_TENANT_ENFORCEMENT_MODE=none && zoekt-webserver -index .sourcebot/index -rpc", "dev:zoekt:mt": "export PATH=\"$PWD/bin:$PATH\" && export SRC_TENANT_ENFORCEMENT_MODE=strict && zoekt-webserver -index .sourcebot/index -rpc", "dev:backend": "yarn workspace @sourcebot/backend dev:watch", diff --git a/packages/backend/package.json b/packages/backend/package.json index 330b5b1e..244d6223 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -32,6 +32,7 @@ "lowdb": "^7.0.1", "micromatch": "^4.0.8", "posthog-node": "^4.2.1", + "@sourcebot/db": "^0.1.0", "simple-git": "^3.27.0", "strip-json-comments": "^5.0.1", "winston": "^3.15.0", diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index c81ddff4..3b416cbb 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -105,7 +105,7 @@ export const syncConfig = async (configPath: string, db: PrismaClient, signal: A name: repoName, tenantId: 0, // TODO: add support for tenantId in GitLab config isFork, - isArchived: project.archived, + isArchived: !!project.archived, metadata: { 'zoekt.web-url-type': 'gitlab', 'zoekt.web-url': project.web_url, diff --git a/packages/backend/src/gitlab.ts b/packages/backend/src/gitlab.ts index bf8dda6b..20d4e5b1 100644 --- a/packages/backend/src/gitlab.ts +++ b/packages/backend/src/gitlab.ts @@ -3,7 +3,7 @@ import micromatch from "micromatch"; import { createLogger } from "./logger.js"; import { GitLabConfig } from "./schemas/v2.js"; import { AppContext } from "./types.js"; -import { getTokenFromConfig, marshalBool, measure } from "./utils.js"; +import { getTokenFromConfig, measure } from "./utils.js"; const logger = createLogger("GitLab"); export const GITLAB_CLOUD_HOSTNAME = "gitlab.com"; diff --git a/packages/db/prisma/migrations/20250115193735_auth_js_models/migration.sql b/packages/db/prisma/migrations/20250115193735_auth_js_models/migration.sql new file mode 100644 index 00000000..a476629a --- /dev/null +++ b/packages/db/prisma/migrations/20250115193735_auth_js_models/migration.sql @@ -0,0 +1,45 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT, + "email" TEXT, + "emailVerified" DATETIME, + "image" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "VerificationToken" ( + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" DATETIME NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 96c97462..93b35d21 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -41,3 +41,48 @@ model Repo { @@unique([external_id, external_codeHostUrl]) } + +// @see : https://authjs.dev/concepts/database-models#user +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + accounts Account[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// @see : https://authjs.dev/concepts/database-models#account +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +// @see : https://authjs.dev/concepts/database-models#verificationtoken +model VerificationToken { + identifier String + token String + expires DateTime + + @@unique([identifier, token]) +} diff --git a/packages/web/next.config.mjs b/packages/web/next.config.mjs index fe7eda84..585bb245 100644 --- a/packages/web/next.config.mjs +++ b/packages/web/next.config.mjs @@ -22,10 +22,14 @@ const nextConfig = { // This is required to support PostHog trailing slash API requests skipTrailingSlashRedirect: true, + // @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, - } : {}) + // ...(process.env.NEXT_PUBLIC_DOMAIN_SUB_PATH ? { + // basePath: process.env.NEXT_PUBLIC_DOMAIN_SUB_PATH, + // } : {}) }; export default nextConfig; diff --git a/packages/web/package.json b/packages/web/package.json index 706b4329..b5079051 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -10,6 +10,7 @@ "test": "vitest" }, "dependencies": { + "@auth/prisma-adapter": "^2.7.4", "@codemirror/commands": "^6.6.0", "@codemirror/lang-cpp": "^6.0.2", "@codemirror/lang-css": "^6.3.0", @@ -39,6 +40,7 @@ "@hookform/resolvers": "^3.9.0", "@iconify/react": "^5.1.0", "@iizukak/codemirror-lang-wgsl": "^0.3.0", + "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", @@ -89,6 +91,7 @@ "http-status-codes": "^2.3.0", "lucide-react": "^0.435.0", "next": "14.2.21", + "next-auth": "^5.0.0-beta.25", "next-themes": "^0.3.0", "posthog-js": "^1.161.5", "pretty-bytes": "^6.1.1", @@ -119,9 +122,10 @@ "jsdom": "^25.0.1", "npm-run-all": "^4.1.5", "postcss": "^8", + "@sourcebot/db": "^0.1.0", "tailwindcss": "^3.4.1", "typescript": "^5", "vite-tsconfig-paths": "^5.1.3", "vitest": "^2.1.5" } -} +} \ No newline at end of file diff --git a/packages/web/src/app/api/(server)/auth/[...nextauth]/route.ts b/packages/web/src/app/api/(server)/auth/[...nextauth]/route.ts new file mode 100644 index 00000000..f5bc5b80 --- /dev/null +++ b/packages/web/src/app/api/(server)/auth/[...nextauth]/route.ts @@ -0,0 +1,2 @@ +import { handlers } from "@/auth"; +export const { GET, POST } = handlers; \ No newline at end of file diff --git a/packages/web/src/app/api/(server)/search/route.ts b/packages/web/src/app/api/(server)/search/route.ts index 9ba352a4..4255d33c 100644 --- a/packages/web/src/app/api/(server)/search/route.ts +++ b/packages/web/src/app/api/(server)/search/route.ts @@ -8,13 +8,15 @@ import { NextRequest } from "next/server"; export const POST = async (request: NextRequest) => { const body = await request.json(); - const tenantId = await request.headers.get("X-Tenant-ID"); + const tenantId = request.headers.get("X-Tenant-ID"); console.log(`Search request received. Tenant ID: ${tenantId}`); const parsed = await searchRequestSchema.safeParseAsync({ ...body, - ...(tenantId && { tenantId: parseInt(tenantId) }), + ...(tenantId ? { + tenantId: parseInt(tenantId) + } : {}), }); if (!parsed.success) { return serviceErrorResponse( diff --git a/packages/web/src/app/components/navigationMenu.tsx b/packages/web/src/app/components/navigationMenu.tsx index 229b7713..53c8d966 100644 --- a/packages/web/src/app/components/navigationMenu.tsx +++ b/packages/web/src/app/components/navigationMenu.tsx @@ -1,90 +1,112 @@ -'use client'; - import { Button } from "@/components/ui/button"; import { NavigationMenu as NavigationMenuBase, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu"; import Link from "next/link"; -import { GitHubLogoIcon, DiscordLogoIcon } from "@radix-ui/react-icons"; -import { SettingsDropdown } from "./settingsDropdown"; import { Separator } from "@/components/ui/separator"; import Image from "next/image"; import logoDark from "../../../public/sb_logo_dark_small.png"; import logoLight from "../../../public/sb_logo_light_small.png"; -import { useRouter } from "next/navigation"; +import { ProfilePicture } from "./profilePicture"; +import { signOut } from "@/auth"; +import { SettingsDropdown } from "./settingsDropdown"; +import { GitHubLogoIcon, DiscordLogoIcon } from "@radix-ui/react-icons"; +import { redirect } from "next/navigation"; const SOURCEBOT_DISCORD_URL = "https://discord.gg/6Fhp27x7Pb"; const SOURCEBOT_GITHUB_URL = "https://github.com/sourcebot-dev/sourcebot"; -export const NavigationMenu = () => { - const router = useRouter(); +export const NavigationMenu = async () => { return (
-
-
-
{ - router.push("/"); - }} - > - {"Sourcebot - {"Sourcebot -
+
+
+ + {"Sourcebot + {"Sourcebot + - - - - - - Search - - - - - - - Repositories - - - - - -
+ + + + + + Search + + + + + + + Repositories + + + + + +
-
+
+
{ + "use server"; + redirect(SOURCEBOT_DISCORD_URL); + }} + > +
+
{ + "use server"; + redirect(SOURCEBOT_GITHUB_URL); + }} + > - -
+ + +
{ + "use server"; + await signOut(); + }} + > + +
+
-
+ +
) diff --git a/packages/web/src/app/components/profilePicture.tsx b/packages/web/src/app/components/profilePicture.tsx new file mode 100644 index 00000000..91b54d25 --- /dev/null +++ b/packages/web/src/app/components/profilePicture.tsx @@ -0,0 +1,20 @@ +import { auth } from "@/auth" +import { + Avatar, + AvatarFallback, + AvatarImage, + } from "@/components/ui/avatar" + +export const ProfilePicture = async () => { + const session = await auth() + + return ( + + + U + + ) + } \ No newline at end of file diff --git a/packages/web/src/app/login/page.tsx b/packages/web/src/app/login/page.tsx new file mode 100644 index 00000000..1fcdc795 --- /dev/null +++ b/packages/web/src/app/login/page.tsx @@ -0,0 +1,87 @@ +import { providerMap, signIn } from "@/auth" +import { AuthError } from "next-auth" +import { redirect } from "next/navigation" +import logoDark from "@/public/sb_logo_dark_large.png"; +import logoLight from "@/public/sb_logo_light_large.png"; +import githubLogo from "@/public/github.svg"; +import Image from "next/image"; +import { Button } from "@/components/ui/button"; + +const SIGNIN_ERROR_URL = "/login"; + +export default async function Login(props: { + searchParams: { callbackUrl: string | undefined } +}) { + return ( +
+
+
+ {"Sourcebot + {"Sourcebot +
+ { + Object.values(providerMap) + .map((provider) => { + if (provider.id === "github") { + return { + provider, + logo: githubLogo, + } + } + + return { provider } + }) + .map(({ provider, logo }) => ( +
{ + "use server" + try { + await signIn(provider.id, { + redirectTo: props.searchParams?.callbackUrl ?? "", + }) + } catch (error) { + // Signin can fail for a number of reasons, such as the user + // not existing, or the user not having the correct role. + // In some cases, you may want to redirect to a custom error + if (error instanceof AuthError) { + return redirect(`${SIGNIN_ERROR_URL}?error=${error.type}`) + } + + // Otherwise if a redirects happens Next.js can handle it + // so you can just re-thrown the error and let Next.js handle it. + // Docs: + // https://nextjs.org/docs/app/api-reference/functions/redirect#server-component + throw error + } + }} + > + +
+ )) + } +
+
+ ) +} diff --git a/packages/web/src/app/page.tsx b/packages/web/src/app/page.tsx index 4e90d2ab..81c19628 100644 --- a/packages/web/src/app/page.tsx +++ b/packages/web/src/app/page.tsx @@ -2,8 +2,8 @@ import { listRepositories } from "@/lib/server/searchService"; import { isServiceError } from "@/lib/utils"; import Image from "next/image"; import { Suspense } from "react"; -import logoDark from "../../public/sb_logo_dark_large.png"; -import logoLight from "../../public/sb_logo_light_large.png"; +import logoDark from "@/public/sb_logo_dark_large.png"; +import logoLight from "@/public/sb_logo_light_large.png"; import { NavigationMenu } from "./components/navigationMenu"; import { RepositoryCarousel } from "./components/repositoryCarousel"; import { SearchBar } from "./components/searchBar"; diff --git a/packages/web/src/auth.ts b/packages/web/src/auth.ts new file mode 100644 index 00000000..96d6560c --- /dev/null +++ b/packages/web/src/auth.ts @@ -0,0 +1,38 @@ +import NextAuth from "next-auth" +import GitHub from "next-auth/providers/github" +import { PrismaAdapter } from "@auth/prisma-adapter" +import { prisma } from "@/prisma"; +import type { Provider } from "next-auth/providers" +import { AUTH_GITHUB_CLIENT_ID, AUTH_GITHUB_CLIENT_SECRET, AUTH_SECRET } from "./lib/environment"; + +const providers: Provider[] = [ + GitHub({ + clientId: AUTH_GITHUB_CLIENT_ID, + clientSecret: AUTH_GITHUB_CLIENT_SECRET, + }), +]; + +// @see: https://authjs.dev/guides/pages/signin +export const providerMap = providers + .map((provider) => { + if (typeof provider === "function") { + const providerData = provider() + return { id: providerData.id, name: providerData.name } + } else { + return { id: provider.id, name: provider.name } + } + }) + .filter((provider) => provider.id !== "credentials"); + + +export const { handlers, signIn, signOut, auth } = NextAuth({ + secret: AUTH_SECRET, + adapter: PrismaAdapter(prisma), + session: { + strategy: "jwt", + }, + providers: providers, + pages: { + signIn: "/login" + } +}) diff --git a/packages/web/src/components/ui/avatar.tsx b/packages/web/src/components/ui/avatar.tsx new file mode 100644 index 00000000..51e507ba --- /dev/null +++ b/packages/web/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/packages/web/src/lib/environment.ts b/packages/web/src/lib/environment.ts index 0102da6d..4725eb0d 100644 --- a/packages/web/src/lib/environment.ts +++ b/packages/web/src/lib/environment.ts @@ -6,3 +6,7 @@ export const ZOEKT_WEBSERVER_URL = getEnv(process.env.ZOEKT_WEBSERVER_URL, "http 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 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); \ No newline at end of file diff --git a/packages/web/src/lib/errorCodes.ts b/packages/web/src/lib/errorCodes.ts index 4144a232..2f5677f5 100644 --- a/packages/web/src/lib/errorCodes.ts +++ b/packages/web/src/lib/errorCodes.ts @@ -5,4 +5,5 @@ export enum ErrorCode { REPOSITORY_NOT_FOUND = 'REPOSITORY_NOT_FOUND', FILE_NOT_FOUND = 'FILE_NOT_FOUND', INVALID_REQUEST_BODY = 'INVALID_REQUEST_BODY', + NOT_AUTHENTICATED = 'NOT_AUTHENTICATED', } diff --git a/packages/web/src/lib/serviceError.ts b/packages/web/src/lib/serviceError.ts index b4883913..0d97e537 100644 --- a/packages/web/src/lib/serviceError.ts +++ b/packages/web/src/lib/serviceError.ts @@ -67,4 +67,12 @@ export const unexpectedError = (message: string): ServiceError => { errorCode: ErrorCode.UNEXPECTED_ERROR, message: `Unexpected error: ${message}`, }; +} + +export const notAuthenticated = (): ServiceError => { + return { + statusCode: StatusCodes.UNAUTHORIZED, + errorCode: ErrorCode.NOT_AUTHENTICATED, + message: "Not authenticated", + } } \ No newline at end of file diff --git a/packages/web/src/middleware.ts b/packages/web/src/middleware.ts new file mode 100644 index 00000000..febc8966 --- /dev/null +++ b/packages/web/src/middleware.ts @@ -0,0 +1,46 @@ + +import { auth } from "@/auth"; +import { Session } from "next-auth"; +import { NextRequest, NextResponse } from "next/server"; +import { notAuthenticated, serviceErrorResponse } from "./lib/serviceError"; + +interface NextAuthRequest extends NextRequest { + auth: Session | null; + } + +const apiMiddleware = (req: NextAuthRequest) => { + if (req.nextUrl.pathname.startsWith("/api/auth")) { + return NextResponse.next(); + } + + if (!req.auth) { + return serviceErrorResponse( + notAuthenticated(), + ); + } + + return NextResponse.next(); +} + +const defaultMiddleware = (req: NextAuthRequest) => { + if (!req.auth && req.nextUrl.pathname !== "/login") { + const newUrl = new URL("/login", req.nextUrl.origin); + return NextResponse.redirect(newUrl); + } + + return NextResponse.next(); +} + +export default auth(async (req) => { + if (req.nextUrl.pathname.startsWith("/api")) { + return apiMiddleware(req); + } + + return defaultMiddleware(req); +}) + + +export const config = { + // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher + matcher: ['/((?!_next/static|ingest|_next/image|favicon.ico|sitemap.xml|robots.txt).*)'], +} \ No newline at end of file diff --git a/packages/web/src/prisma.ts b/packages/web/src/prisma.ts new file mode 100644 index 00000000..5f5b674e --- /dev/null +++ b/packages/web/src/prisma.ts @@ -0,0 +1,6 @@ +import { PrismaClient } from "@sourcebot/db"; + +// @see: https://authjs.dev/getting-started/adapters/prisma +const globalForPrisma = globalThis as unknown as { prisma: PrismaClient } +export const prisma = globalForPrisma.prisma || new PrismaClient() +if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index f84fbc20..96eb94f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,6 +16,37 @@ "@types/json-schema" "^7.0.15" js-yaml "^4.1.0" +"@auth/core@0.37.2": + version "0.37.2" + resolved "https://registry.yarnpkg.com/@auth/core/-/core-0.37.2.tgz#0db8a94a076846bd88eb7f9273618513e2285cb2" + integrity sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw== + dependencies: + "@panva/hkdf" "^1.2.1" + "@types/cookie" "0.6.0" + cookie "0.7.1" + jose "^5.9.3" + oauth4webapi "^3.0.0" + preact "10.11.3" + preact-render-to-string "5.2.3" + +"@auth/core@0.37.4": + version "0.37.4" + resolved "https://registry.yarnpkg.com/@auth/core/-/core-0.37.4.tgz#c51410aa7d0997fa22a07a196d2c21c8b1bca71b" + integrity sha512-HOXJwXWXQRhbBDHlMU0K/6FT1v+wjtzdKhsNg0ZN7/gne6XPsIrjZ4daMcFnbq0Z/vsAbYBinQhhua0d77v7qw== + dependencies: + "@panva/hkdf" "^1.2.1" + jose "^5.9.6" + oauth4webapi "^3.1.1" + preact "10.24.3" + preact-render-to-string "6.5.11" + +"@auth/prisma-adapter@^2.7.4": + version "2.7.4" + resolved "https://registry.yarnpkg.com/@auth/prisma-adapter/-/prisma-adapter-2.7.4.tgz#4890be47a9f227f449832302d955c565c02879ee" + integrity sha512-3T/X94R9J1sxOLQtsD3ijIZ0JGHPXlZQxRr/8NpnZBJ3KGxun/mNsZ1MwMRhTxy0mmn9JWXk7u9+xCcVn0pu3A== + dependencies: + "@auth/core" "0.37.4" + "@babel/runtime@^7.18.6": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.7.tgz#7ffb53c37a8f247c8c4d335e89cdf16a2e0d0fb6" @@ -1270,6 +1301,11 @@ dependencies: "@octokit/openapi-types" "^22.2.0" +"@panva/hkdf@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@panva/hkdf/-/hkdf-1.2.1.tgz#cb0d111ef700136f4580349ff0226bf25c853f23" + integrity sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw== + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -1333,6 +1369,16 @@ dependencies: "@radix-ui/react-primitive" "2.0.0" +"@radix-ui/react-avatar@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.1.2.tgz#24af4c66bb5271460a4a6b74c4f4f9d4789d3d90" + integrity sha512-GaC7bXQZ5VgZvVvsJ5mu/AEbjYLnhhkoidOboC50Z6FFlLA03wG2ianUoH+zgDQ31/9gCF59bE4+2bBgTyMiig== + dependencies: + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-collection@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed" @@ -1348,6 +1394,11 @@ resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74" integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw== +"@radix-ui/react-compose-refs@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec" + integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw== + "@radix-ui/react-context@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.0.tgz#6df8d983546cfd1999c8512f3a8ad85a6e7fcee8" @@ -1503,6 +1554,13 @@ dependencies: "@radix-ui/react-slot" "1.1.0" +"@radix-ui/react-primitive@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz#6d9efc550f7520135366f333d1e820cf225fad9e" + integrity sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg== + dependencies: + "@radix-ui/react-slot" "1.1.1" + "@radix-ui/react-roving-focus@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e" @@ -1547,6 +1605,13 @@ dependencies: "@radix-ui/react-compose-refs" "1.1.0" +"@radix-ui/react-slot@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.1.tgz#ab9a0ffae4027db7dc2af503c223c978706affc3" + integrity sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-toast@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.2.2.tgz#fdd8ed0b80f47d6631dfd90278fee6debc06bf33" @@ -1850,6 +1915,11 @@ resolved "https://registry.yarnpkg.com/@types/braces/-/braces-3.0.4.tgz#403488dc1c8d0db288270d3bbf0ce5f9c45678b4" integrity sha512-0WR3b8eaISjEW7RpZnclONaLFDf7buaowRHdqLp4vLj54AsSAYWfh3DRbfiYJY9XDxMgx1B4sE1Afw2PGpuHOA== +"@types/cookie@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" + integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== + "@types/estree@1.0.6", "@types/estree@^1.0.0": version "1.0.6" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" @@ -2851,6 +2921,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + crelt@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" @@ -4338,6 +4413,11 @@ jiti@^1.21.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== +jose@^5.9.3, jose@^5.9.6: + version "5.9.6" + resolved "https://registry.yarnpkg.com/jose/-/jose-5.9.6.tgz#77f1f901d88ebdc405e57cce08d2a91f47521883" + integrity sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ== + "js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -4710,6 +4790,13 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +next-auth@^5.0.0-beta.25: + version "5.0.0-beta.25" + resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-5.0.0-beta.25.tgz#3a9f9734e1d8fa5ced545360f1afc24862cb92d5" + integrity sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog== + dependencies: + "@auth/core" "0.37.2" + next-themes@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a" @@ -4807,6 +4894,11 @@ nwsapi@^2.2.12: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.13.tgz#e56b4e98960e7a040e5474536587e599c4ff4655" integrity sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ== +oauth4webapi@^3.0.0, oauth4webapi@^3.1.1: + version "3.1.4" + resolved "https://registry.yarnpkg.com/oauth4webapi/-/oauth4webapi-3.1.4.tgz#50695385cea8e7a43f3e2e23bc33ea27faece4a7" + integrity sha512-eVfN3nZNbok2s/ROifO0UAc5G8nRoLSbrcKJ09OqmucgnhXEfdIQOR4gq1eJH1rN3gV7rNw62bDEgftsgFtBEg== + object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -5143,6 +5235,28 @@ posthog-node@^4.2.1: axios "^1.7.4" rusha "^0.8.14" +preact-render-to-string@5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz#23d17376182af720b1060d5a4099843c7fe92fe4" + integrity sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA== + dependencies: + pretty-format "^3.8.0" + +preact-render-to-string@6.5.11: + version "6.5.11" + resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz#467e69908a453497bb93d4d1fc35fb749a78e027" + integrity sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw== + +preact@10.11.3: + version "10.11.3" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.11.3.tgz#8a7e4ba19d3992c488b0785afcc0f8aa13c78d19" + integrity sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg== + +preact@10.24.3: + version "10.24.3" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.24.3.tgz#086386bd47071e3b45410ef20844c21e23828f64" + integrity sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA== + preact@^10.19.3: version "10.24.2" resolved "https://registry.yarnpkg.com/preact/-/preact-10.24.2.tgz#42179771d3b06e7adb884e3f8127ddd3d99b78f6" @@ -5163,6 +5277,11 @@ pretty-bytes@^6.1.1: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz#38cd6bb46f47afbf667c202cfc754bffd2016a3b" integrity sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ== +pretty-format@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385" + integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew== + prisma@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/prisma/-/prisma-6.2.1.tgz#457b210326d66d0e6f583cc6f9cd2819b984408f" @@ -5731,16 +5850,8 @@ string-argv@^0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5837,14 +5948,7 @@ string_decoder@^1.1.1, string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==