Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add basic self hosting support #11

Merged
merged 4 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions aws/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ def __init__(self, scope: Construct, id: str, **kwargs) -> None:
"OPENAI_API_KEY": ecs.Secret.from_secrets_manager(
tracecat_secret, field="openai-api-key"
),
"RESEND_API_KEY": ecs.Secret.from_secrets_manager(
tracecat_secret, field="resend-api-key"
),
}
if TRACECAT__APP_ENV == "prod":
shared_env = {
Expand Down
53 changes: 53 additions & 0 deletions docker-compose.self-host.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
version: "3.8"
services:
api:
extends:
file: docker-compose.yaml
service: api
environment:
TRACECAT__SELF_HOSTED_DB_BACKEND: ${TRACECAT__SELF_HOSTED_DB_BACKEND}
networks:
- nginx-network
- supabase_network_tracecat

runner:
extends:
file: docker-compose.yaml
service: runner
environment:
TRACECAT__SELF_HOSTED_DB_BACKEND: ${TRACECAT__SELF_HOSTED_DB_BACKEND}
networks:
- nginx-network
- supabase_network_tracecat

nginx:
extends:
file: docker-compose.yaml
service: nginx
networks:
- nginx-network
- supabase_network_tracecat

# frontend:
# build:
# context: ./frontend
# container_name: frontend
# restart: unless-stopped
# depends_on:
# - api
# environment:
# NEXT_PUBLIC_APP_ENV: "development"
# NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
# NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
# NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY}
# NEXT_PUBLIC_SELF_HOSTED: ${NEXT_PUBLIC_SELF_HOSTED}
# ports:
# - "3000:3000"
# networks:
# - nginx-network
# - supabase_network_tracecat

networks:
nginx-network:
supabase_network_tracecat:
external: true
5 changes: 5 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ services:
TRACECAT__SIGNING_SECRET: ${TRACECAT__SIGNING_SECRET}
TRACECAT__SERVICE_KEY: ${TRACECAT__SERVICE_KEY}
TRACECAT__DB_ENCRYPTION_KEY: ${TRACECAT__DB_ENCRYPTION_KEY}
TRACECAT__RUNNER_URL: ${TRACECAT__RUNNER_URL}
SUPABASE_JWT_SECRET: ${SUPABASE_JWT_SECRET}
SUPABASE_JWT_ALGORITHM: ${SUPABASE_JWT_ALGORITHM}
SUPABASE_PSQL_URL: ${SUPABASE_PSQL_URL}
OPENAI_API_KEY: ${OPENAI_API_KEY}
LOG_LEVEL: ${LOG_LEVEL}
container_name: api
restart: unless-stopped
depends_on:
Expand All @@ -35,7 +37,10 @@ services:
TRACECAT__SIGNING_SECRET: ${TRACECAT__SIGNING_SECRET}
TRACECAT__SERVICE_KEY: ${TRACECAT__SERVICE_KEY}
TRACECAT__DB_ENCRYPTION_KEY: ${TRACECAT__DB_ENCRYPTION_KEY}
TRACECAT__RUNNER_URL: ${TRACECAT__RUNNER_URL}
OPENAI_API_KEY: ${OPENAI_API_KEY}
SUPABASE_PSQL_URL: ${SUPABASE_PSQL_URL}
LOG_LEVEL: ${LOG_LEVEL}
container_name: runner
restart: unless-stopped
depends_on:
Expand Down
3 changes: 3 additions & 0 deletions frontend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
build
.next
3 changes: 3 additions & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ NEXT_PUBLIC_APP_URL=
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=

# Tracecat self-hosted only, please ignore if Cloud:
NEXT_PUBLIC_SELF_HOSTED=true

# Tracecat Cloud only, please ignore if self-hosted:
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_INGEST_HOST="https://www.your-domain.com/ingest"
Expand Down
21 changes: 21 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM node:21-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY . /app
WORKDIR /app

FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile

FROM base AS builder
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run build

FROM base
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=builder /app/next.config.mjs ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
EXPOSE 3000
CMD [ "pnpm", "start" ]
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"format": "pnpm prettier --check --ignore-path .gitignore .",
"lint:fix": "next lint --fix",
"lint": "next lint",
"prepare": "cd .. && husky frontend/.husky",
"prepare": "cd .. && husky frontend/.husky || true",
"preview": "next build && next start",
"start": "next start",
"typecheck": "tsc --noEmit"
Expand Down
25 changes: 2 additions & 23 deletions frontend/src/app/auth/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { redirect } from "next/navigation"
import { NextResponse } from "next/server"
import { createClient } from "@/utils/supabase/server"

import { createWorkflow } from "@/lib/flow"
import { newUserFlow } from "@/lib/auth"

export async function GET(request: Request) {
// The `/auth/callback` route is required for the server-side auth flow implemented
Expand All @@ -27,28 +27,7 @@ export async function GET(request: Request) {
console.error("Failed to get session")
return redirect("/?level=error&message=Could not authenticate user")
}
const response = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/users`, {
method: "PUT",
headers: {
Authorization: `Bearer ${session.access_token}`,
},
})

// If the user already exists, we'll get a 409 conflict
if (!response.ok && response.status !== 409) {
console.error("Failed to create user")
return redirect("/?level=error&message=Could not authenticate user")
}

if (response.status !== 409) {
console.log("New user created")
await createWorkflow(
session,
"My first workflow",
"Welcome to Tracecat. This is your first workflow!"
)
console.log("Created first workflow for new user")
}
await newUserFlow(session)
// URL to redirect to after sign up process completes
return NextResponse.redirect(`${origin}/workflows`)
}
9 changes: 5 additions & 4 deletions frontend/src/components/auth/forms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { SubmitButton } from "./submit-button"
export function SignInForm({
searchParams,
}: {
searchParams: { message: string }
searchParams: { level?: AlertLevel; message?: string }
}) {
const [isLoading, setIsLoading] = useState<boolean>(false)
async function onSubmit(event: React.SyntheticEvent<HTMLFormElement>) {
Expand Down Expand Up @@ -63,9 +63,10 @@ export function SignInForm({
Sign In
</SubmitButton>
{searchParams?.message && (
<p className="mt-4 bg-foreground/10 p-4 text-center text-foreground">
{searchParams.message}
</p>
<AlertNotification
level={searchParams?.level}
message={searchParams.message}
/>
)}
</CardFooter>
</form>
Expand Down
42 changes: 24 additions & 18 deletions frontend/src/components/auth/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { PasswordlessSignInForm } from "@/components/auth/forms"
import { PasswordlessSignInForm, SignInForm } from "@/components/auth/forms"
import {
GithubOAuthButton,
GoogleOAuthButton,
Expand Down Expand Up @@ -61,23 +61,29 @@ export default async function Login({
Enter your email below to create your account or sign in
</CardDescription>
</CardHeader>
<CardContent className="flex-col space-y-2">
<div className="mb-8 grid grid-cols-2 gap-2">
<GoogleOAuthButton />
<GithubOAuthButton />
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
</CardContent>
<PasswordlessSignInForm searchParams={searchParams} />
{Boolean(process.env.NEXT_PUBLIC_SELF_HOSTED) ? (
<SignInForm searchParams={searchParams} />
) : (
<>
<CardContent className="flex-col space-y-2">
<div className="mb-8 grid grid-cols-2 gap-2">
<GoogleOAuthButton />
<GithubOAuthButton />
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
</CardContent>
<PasswordlessSignInForm searchParams={searchParams} />
</>
)}
</div>
</div>
)
Expand Down
42 changes: 38 additions & 4 deletions frontend/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import { headers } from "next/headers"
import { redirect } from "next/navigation"
import { createClient } from "@/utils/supabase/server"
import { Session } from "@supabase/supabase-js"

import { createWorkflow } from "@/lib/flow"

type ThirdPartyAuthProvider = "google" | "github"

Expand All @@ -11,15 +14,21 @@ export async function signInFlow(formData: FormData) {
const password = formData.get("password") as string
const supabase = createClient()

const { error } = await supabase.auth.signInWithPassword({
const {
data: { session },
error,
} = await supabase.auth.signInWithPassword({
email,
password,
})

if (error) {
if (error || !session) {
console.error("error", error, "session", session)
return redirect("/?level=error&message=Could not authenticate user")
}

await newUserFlow(session)

return redirect("/workflows")
}

Expand All @@ -41,7 +50,7 @@ export async function signUpFlow(formData: FormData) {
return redirect("/?level=error&message=Could not authenticate user")
}

return redirect("/?message=Check email to continue sign in process")
return redirect("/?message=Check your email to continue")
}

export async function thirdPartyAuthFlow(provider: ThirdPartyAuthProvider) {
Expand Down Expand Up @@ -79,5 +88,30 @@ export async function signInWithEmailMagicLink(formData: FormData) {
if (error) {
return redirect("/?level=error&message=Could not authenticate user")
}
return redirect("/?message=Check email to continue sign in process")
return redirect("/?message=Check your email to continue")
}

export async function newUserFlow(session: Session) {
const response = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/users`, {
method: "PUT",
headers: {
Authorization: `Bearer ${session.access_token}`,
},
})

// If the user already exists, we'll get a 409 conflict
if (!response.ok && response.status !== 409) {
console.error("Failed to create user")
return redirect("/?level=error&message=Could not authenticate user")
}

if (response.status !== 409) {
console.log("New user created")
await createWorkflow(
session,
"My first workflow",
"Welcome to Tracecat. This is your first workflow!"
)
console.log("Created first workflow for new user")
}
}
4 changes: 4 additions & 0 deletions supabase/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Supabase
.branches
.temp
.env
Loading
Loading