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

poc #68

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open

poc #68

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
2 changes: 1 addition & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET
# host
NEXT_PUBLIC_SITE_URL=http://localhost:3000
SITE_URL=http://localhost:3000
# email
[email protected]
RESEND_API_KEY=RESEND_API_KEY
Expand Down
12 changes: 10 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
FROM node:18-alpine


ARG NEXT_PUBLIC_SSO_DOMAIN
ARG NEXT_PUBLIC_SB_SSO_DOMAIN
ARG NEXT_PUBLIC_SUPABASE_URL
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
ARG NEXT_PUBLIC_SITE_URL
Expand All @@ -13,7 +13,10 @@ ARG SUPABASE_PROJECT_REF
ARG GITHUB_PROXY_CALLBACK_URL
ARG DIGGER_WEBHOOK_SECRET
ARG DIGGER_TRIGGER_APPLY_URL
ENV NEXT_PUBLIC_SSO_DOMAIN=$NEXT_PUBLIC_SSO_DOMAIN
ARG NEXT_PUBLIC_GITHUB_APP_SLUG
ARG NEXT_PUBLIC_HIDE_FREE_TRIAL_DIALOG
ARG NEXT_PUBLIC_SKIP_ORG_CREATION
ENV NEXT_PUBLIC_SB_SSO_DOMAIN=$NEXT_PUBLIC_SB_SSO_DOMAIN
ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
Expand All @@ -24,6 +27,11 @@ ENV SUPABASE_PROJECT_REF=${SUPABASE_PROJECT_REF}
ENV GITHUB_PROXY_CALLBACK_URL=${GITHUB_PROXY_CALLBACK_URL}
ENV DIGGER_WEBHOOK_SECRET=${DIGGER_WEBHOOK_SECRET}
ENV DIGGER_TRIGGER_APPLY_URL=${DIGGER_TRIGGER_APPLY_URL}
ENV NEXT_PUBLIC_GITHUB_APP_SLUG=${NEXT_PUBLIC_GITHUB_APP_SLUG}
ENV NEXT_PUBLIC_HIDE_FREE_TRIAL_DIALOG=${NEXT_PUBLIC_HIDE_FREE_TRIAL_DIALOG}
ENV NEXT_PUBLIC_SKIP_ORG_CREATION=${NEXT_PUBLIC_SKIP_ORG_CREATION}

RUN env

# Install pnpm
RUN npm install -g pnpm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,31 @@ async function OrganizationSubscriptionSidebarCard({

const isOrganizationAdmin = userRole === 'admin' || userRole === 'owner'

switch (normalizedSubscription.type) {
case 'bypassed_enterprise_organization':
return null;
case 'trialing':
return <FreeTrialComponent
organizationId={organizationId}
planName={normalizedSubscription.product.name ?? 'Digger Plan'}
daysRemaining={differenceInDays(new Date(normalizedSubscription.trialEnd), new Date())}
/>
case 'active':
return null;
default:
return <>
<FreeTrialDialog
isOrganizationAdmin={isOrganizationAdmin}
if (process.env.NEXT_PUBLIC_HIDE_FREE_TRIAL_DIALOG !== "true") {
switch (normalizedSubscription.type) {
case 'bypassed_enterprise_organization':
return null;
case 'trialing':
return <FreeTrialComponent
organizationId={organizationId}
activeProducts={activeProducts}
defaultOpen={true}
planName={normalizedSubscription.product.name ?? 'Digger Plan'}
daysRemaining={differenceInDays(new Date(normalizedSubscription.trialEnd), new Date())}
/>
</>
case 'active':
return null;
default:
return <>
<FreeTrialDialog
isOrganizationAdmin={isOrganizationAdmin}
organizationId={organizationId}
activeProducts={activeProducts}
defaultOpen={true}
/>
</>
}
}


}

async function OrganizationSidebarInternal({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ export async function GET(req: NextRequest) {
const initialOrgId = req.cookies.get('organization')?.value;
if (initialOrgId) {
console.log('Initial org id from cookies:', initialOrgId);
return NextResponse.redirect(new URL(`/org/${initialOrgId}`, req.url));
return NextResponse.redirect(toSiteURL(`/org/${initialOrgId}`));
}
const initialOrganization = await getInitialOrganizationToRedirectTo();
if (initialOrganization.status === 'error') {
return NextResponse.redirect(toSiteURL('/500'));
}
return NextResponse.redirect(new URL(`/org/${initialOrganization.data}`, req.url));
return NextResponse.redirect(toSiteURL(`/org/${initialOrganization.data}`));
} catch (error) {
console.error('Failed to load dashboard:', error);
// Redirect to an error page or show an error message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,30 @@ import { CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useToast } from "@/components/ui/use-toast";
import { createOrganization } from "@/data/user/organizations";
import { createOrganization, setUserMetaDataWithOrgCreated } from "@/data/user/organizations";
import { generateOrganizationSlug } from "@/lib/utils";
import { CreateOrganizationSchema, createOrganizationSchema } from "@/utils/zod-schemas/organization";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import Cookies from 'js-cookie';
import { useEffect } from "react";
import { useForm } from "react-hook-form";

type OrganizationCreationProps = {
onSuccess: () => void;
};

export function OrganizationCreation({ onSuccess }: OrganizationCreationProps) {

useEffect(() => {
if (process.env.NEXT_PUBLIC_SKIP_ORG_CREATION === "true") {
setUserMetaDataWithOrgCreated()
onSuccess()
return
}
}, [onSuccess])


const { toast } = useToast();
const { register, handleSubmit, setValue, formState: { errors, isValid } } = useForm<CreateOrganizationSchema>({
resolver: zodResolver(createOrganizationSchema),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
createDefaultUserPrivateInfo,
createDefaultUserProfile,
} from '@/data/user/user';
import { createOrganization } from '@/data/user/organizations';
import { supabaseAdminClient } from '@/supabase-clients/admin/supabaseAdminClient';
import { toSiteURL } from '@/utils/helpers';
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
Expand All @@ -25,22 +24,60 @@ export async function GET(request: Request) {

// TODO: find out how user profile and private info are created automatically
const userId = data.user?.id;
createDefaultUserProfile(userId!);
createDefaultUserPrivateInfo(userId!);
// await createDefaultUserProfile(userId!);
// await createDefaultUserPrivateInfo(userId!);
const defaultOrgTitle = process.env.DEFAULT_ORG_TITLE || 'digger';
const defaultOrgSlug = process.env.DEFAULT_ORG_SLUG || 'digger';

console.log('Session set successfully:', data.session);
// creating the default org and membership
try {
const { data, error } = await supabaseAdminClient
.from('organizations')
.select('*')
.eq('slug', defaultOrgSlug)
.single();

if (error) {
throw error;
}

const orgId = data.id;

const { count } = await supabase
.from('organization_members')
.select('*', { count: 'exact', head: true })
.eq('member_id', userId!)
.eq('organization_id', orgId);

if (count === 0) {
const { error: orgMemberErrors } = await supabaseAdminClient
.from('organization_members')
.insert([
{
member_id: userId!,
organization_id: orgId,
member_role: 'owner',
},
]);
}
} catch (error) {
console.log('could not get orgid or create org membership:', error);
createOrganization(defaultOrgTitle, defaultOrgSlug, {
isOnboardingFlow: false,
});
}
} catch (error) {
console.error('Error setting session:', error);
}
}

let redirectTo = new URL('/dashboard', requestUrl.origin);
let redirectTo = toSiteURL('/dashboard');

if (next) {
// decode next param
const decodedNext = decodeURIComponent(next);
// validate next param
redirectTo = new URL(decodedNext, requestUrl.origin);
redirectTo = toSiteURL(decodedNext);
}

return NextResponse.redirect(redirectTo);
Expand Down
5 changes: 4 additions & 1 deletion src/app/(dynamic-pages)/(login-pages)/login/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use client';
import { useConfig } from '@/app/AppProviders';
import DefaultLoginTabs from '@/components/Auth/DefaultLoginTabs';
import SSOLoginTabs from '@/components/Auth/SSOLoginTabs';

Expand All @@ -9,9 +10,11 @@ export function Login({
next?: string;
nextActionType?: string;
}) {
const config = useConfig();

return (
<>
{process.env.NEXT_PUBLIC_SSO_DOMAIN ? <SSOLoginTabs></SSOLoginTabs> : <DefaultLoginTabs></DefaultLoginTabs>}
{process.env.NEXT_PUBLIC_SB_SSO_DOMAIN ? <SSOLoginTabs></SSOLoginTabs> : <DefaultLoginTabs></DefaultLoginTabs>}
</>
);
}
21 changes: 19 additions & 2 deletions src/app/AppProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AppProgressBar as ProgressBar } from "next-nprogress-bar";
import type React from "react";
import { Suspense } from "react";
import { createContext, Suspense, useContext, useEffect, useState } from "react";
import { Toaster as SonnerToaster } from "sonner";
import { ThemeProvider } from "./ThemeProvider";
import { Config } from "./api/config/route";
import { useMyReportWebVitals } from "./reportWebVitals";

// Create a client
const queryClient = new QueryClient();

const ConfigContext = createContext<Config>({});


/**
* This is a wrapper for the app that provides the supabase client, the router event wrapper
* the react-query client, supabase listener, and the navigation progress bar.
Expand All @@ -23,12 +27,22 @@ export function AppProviders({
}: {
children: React.ReactNode;
}) {
const [config, setConfig] = useState({});

useEffect(() => {
fetch('/api/config')
.then(res => res.json())
.then(data => setConfig(data));
}, []);

useMyReportWebVitals();
return (
<>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<QueryClientProvider client={queryClient}>
{children}
<ConfigContext.Provider value={config}>
{children}
</ConfigContext.Provider>
<SonnerToaster theme={"light"} />
<Suspense>
<ProgressBar
Expand All @@ -43,3 +57,6 @@ export function AppProviders({
</>
);
}

export const useConfig = () => useContext(ConfigContext);

12 changes: 12 additions & 0 deletions src/app/api/config/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NextResponse } from 'next/server';

export const dynamic = 'force-dynamic';

export interface Config {
SITE_URL?: string;
}
export async function GET() {
return NextResponse.json({
SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
});
}
2 changes: 1 addition & 1 deletion src/app/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';

export async function GET() {
if (process.env.NEXT_PUBLIC_SSO_DOMAIN !== undefined) {
if (process.env.NEXT_PUBLIC_SB_SSO_DOMAIN !== undefined) {
return NextResponse.redirect(
new URL('/auth/sso-verify', process.env.NEXT_PUBLIC_SITE_URL),
);
Expand Down
4 changes: 3 additions & 1 deletion src/components/Auth/SSOLoginTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use client';
import { useConfig } from '@/app/AppProviders';
import { Button } from "@/components/ui/button";
import { supabaseAnonClient } from '@/supabase-clients/anon/supabaseAnonClient';
import { useRouter } from 'next/navigation';
Expand All @@ -11,10 +12,11 @@ export default function DefaultLoginTabs({
nextActionType?: string;
}) {

const config = useConfig();
const router = useRouter();

async function ssoLogin() {
const SSO_DOMAIN = process.env.NEXT_PUBLIC_SSO_DOMAIN || ''
const SSO_DOMAIN = process.env.NEXT_PUBLIC_SB_SSO_DOMAIN || ''
const { data, error } = await supabaseAnonClient.auth.signInWithSSO({
domain: SSO_DOMAIN,
});
Expand Down
43 changes: 24 additions & 19 deletions src/data/user/organizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,26 @@ export const getOrganizationSlugByOrganizationId = async (
return data.slug;
};

export const setUserMetaDataWithOrgCreated = async () => {
const supabaseClient = createSupabaseUserServerActionClient();
const updateUserMetadataPayload: Partial<AuthUserMetadata> = {
onboardingHasCreatedOrganization: true,
};

const updateUserMetadataResponse = await supabaseClient.auth.updateUser({
data: updateUserMetadataPayload,
});

if (updateUserMetadataResponse.error) {
console.error(
'Error updating user metadata:',
updateUserMetadataResponse.error,
);

throw updateUserMetadataResponse.error;
}
};

export const createOrganization = async (
name: string,
slug: string,
Expand Down Expand Up @@ -114,24 +134,7 @@ export const createOrganization = async (
return { status: 'error', message: updateError.message };
}

const updateUserMetadataPayload: Partial<AuthUserMetadata> = {
onboardingHasCreatedOrganization: true,
};

const updateUserMetadataResponse = await supabaseClient.auth.updateUser({
data: updateUserMetadataPayload,
});

if (updateUserMetadataResponse.error) {
console.error(
'Error updating user metadata:',
updateUserMetadataResponse.error,
);
return {
status: 'error',
message: updateUserMetadataResponse.error.message,
};
}
const response = await setUserMetaDataWithOrgCreated();

const refreshSessionResponse = await refreshSessionAction();
if (refreshSessionResponse.status === 'error') {
Expand Down Expand Up @@ -621,7 +624,9 @@ export async function getInitialOrganizationToRedirectTo(): Promise<
};
}

export async function getMaybeInitialOrganizationToRedirectTo(): Promise<SAPayload<string | null>> {
export async function getMaybeInitialOrganizationToRedirectTo(): Promise<
SAPayload<string | null>
> {
const initialOrganization = await getInitialOrganizationToRedirectTo();
if (initialOrganization.status === 'error') {
return {
Expand Down
Loading