diff --git a/apps/client-ts/next.config.mjs b/apps/client-ts/next.config.mjs index 5a13038ed..0050161f4 100644 --- a/apps/client-ts/next.config.mjs +++ b/apps/client-ts/next.config.mjs @@ -1,6 +1,15 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: 'standalone' + output: 'standalone', + async redirects() { + return [ + { + source: '/', + destination: '/connections', + permanent: true + } + ] + } }; export default nextConfig; diff --git a/apps/client-ts/package.json b/apps/client-ts/package.json index 72f395fcb..a85f29df0 100644 --- a/apps/client-ts/package.json +++ b/apps/client-ts/package.json @@ -27,8 +27,6 @@ "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-scroll-area": "^1.0.5", - "@stytch/nextjs": "^18.0.0", - "@stytch/vanilla-js": "^4.7.1", "@tanstack/react-query": "^5.12.2", "@tanstack/react-query-devtools": "^5.25.0", "@tanstack/react-query-next-experimental": "^5.25.0", @@ -50,14 +48,15 @@ "react-hook-form": "^7.51.0", "recharts": "^2.10.1", "sonner": "^1.4.3", - "stytch": "^10.5.0", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7", "zod": "^3.22.4", - "zustand": "^4.4.7" + "zustand": "^4.4.7", + "js-cookie": "^3.0.5" }, "devDependencies": { "@types/cookies": "^0.9.0", + "@types/js-cookie": "^3.0.6", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/apps/client-ts/src/app/api-keys/page.tsx b/apps/client-ts/src/app/(Dashboard)/api-keys/page.tsx similarity index 57% rename from apps/client-ts/src/app/api-keys/page.tsx rename to apps/client-ts/src/app/(Dashboard)/api-keys/page.tsx index 675cc647e..0e35dbe7d 100644 --- a/apps/client-ts/src/app/api-keys/page.tsx +++ b/apps/client-ts/src/app/(Dashboard)/api-keys/page.tsx @@ -34,7 +34,8 @@ import config from "@/lib/config"; import * as z from "zod" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" -import { DataTableLoading } from "../../components/shared/data-table-loading"; +import { DataTableLoading } from "@/components/shared/data-table-loading"; +import { Heading } from "@/components/ui/heading"; const formSchema = z.object({ @@ -116,13 +117,15 @@ export default function Page() { }; return ( -
-
+
+
-

Api Keys

-

Manage your api keys.

+
-
+
+ + + ) + : + ( + <> + + Add New Api Key + + Never share this key, you must saved it it will be displayed once ! + + + + + +
+
+ ( + + API Key Identifier + + + + + This is the API Key Identifier of system. + + + + )} + /> +
-
- - - - - - + + + + + + + + )}
diff --git a/apps/client-ts/src/app/b2c/profile/page.tsx b/apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx similarity index 62% rename from apps/client-ts/src/app/b2c/profile/page.tsx rename to apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx index 45978a789..a3c9f2f6b 100644 --- a/apps/client-ts/src/app/b2c/profile/page.tsx +++ b/apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx @@ -11,16 +11,31 @@ import { import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator" import { useRouter } from "next/navigation"; -import { useStytch, useStytchSession, useStytchUser } from "@stytch/nextjs"; +import Cookies from 'js-cookie'; +import useProfileStore from "@/state/profileStore"; +import useProjectStore from "@/state/projectStore" +import { useQueryClient } from '@tanstack/react-query'; + const Profile = () => { - const stytch = useStytch(); - // Get the Stytch User object if available - const { user } = useStytchUser(); - // Get the Stytch Session object if available - const { session } = useStytchSession(); + + const {profile,setProfile} = useProfileStore(); + const { idProject, setIdProject } = useProjectStore(); + const queryClient = useQueryClient(); + + + const router = useRouter(); + const onLogout = () => { + router.push('/b2c/login') + Cookies.remove("access_token") + setProfile(null) + setIdProject("") + queryClient.clear() + + } + return (
@@ -28,7 +43,7 @@ const Profile = () => { Profile
- {user?.name.first_name} {user?.name.last_name} + {profile?.first_name} {profile?.last_name}
@@ -36,17 +51,14 @@ const Profile = () => {

Connected user

- +
-
diff --git a/apps/client-ts/src/app/configuration/page.tsx b/apps/client-ts/src/app/(Dashboard)/configuration/page.tsx similarity index 97% rename from apps/client-ts/src/app/configuration/page.tsx rename to apps/client-ts/src/app/(Dashboard)/configuration/page.tsx index d59f55602..af682001c 100644 --- a/apps/client-ts/src/app/configuration/page.tsx +++ b/apps/client-ts/src/app/(Dashboard)/configuration/page.tsx @@ -41,6 +41,7 @@ import AddAuthCredentials from "@/components/Configuration/AddAuthCredentials"; import AuthCredentialsTable from "@/components/Configuration/AuthCredentialsTable"; import useConnectionStrategies from "@/hooks/useConnectionStrategies"; import { extractAuthMode,extractProvider,extractVertical} from '@panora/shared' +import { Heading } from "@/components/ui/heading"; export default function Page() { const {idProject} = useProjectStore(); @@ -112,10 +113,13 @@ export default function Page() { return ( -
+
-

Configuration

+
diff --git a/apps/client-ts/src/app/connections/page.tsx b/apps/client-ts/src/app/(Dashboard)/connections/page.tsx similarity index 53% rename from apps/client-ts/src/app/connections/page.tsx rename to apps/client-ts/src/app/(Dashboard)/connections/page.tsx index 763a9cf11..e9fc5059b 100644 --- a/apps/client-ts/src/app/connections/page.tsx +++ b/apps/client-ts/src/app/(Dashboard)/connections/page.tsx @@ -1,14 +1,17 @@ 'use client'; import ConnectionTable from "@/components/Connection/ConnectionTable"; +import { Heading } from "@/components/ui/heading"; export default function ConnectionPage() { return ( -
+
-

Connections

-

Connections between your product and your users’ accounts on third-party software.

+
diff --git a/apps/client-ts/src/app/events/page.tsx b/apps/client-ts/src/app/(Dashboard)/events/page.tsx similarity index 67% rename from apps/client-ts/src/app/events/page.tsx rename to apps/client-ts/src/app/(Dashboard)/events/page.tsx index f48d1c961..64da7e27e 100644 --- a/apps/client-ts/src/app/events/page.tsx +++ b/apps/client-ts/src/app/(Dashboard)/events/page.tsx @@ -1,14 +1,18 @@ 'use client'; import EventsTable from "@/components/Events/EventsTable"; +import { Heading } from "@/components/ui/heading"; import { Suspense } from 'react' export default function Page() { return ( -
+
-

Events

+
diff --git a/apps/client-ts/src/app/(Dashboard)/layout.tsx b/apps/client-ts/src/app/(Dashboard)/layout.tsx new file mode 100644 index 000000000..cf01b3cf5 --- /dev/null +++ b/apps/client-ts/src/app/(Dashboard)/layout.tsx @@ -0,0 +1,59 @@ +'use client' +import { Inter } from "next/font/google"; +import "./../globals.css"; +import { RootLayout } from "@/components/RootLayout"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import Cookies from 'js-cookie'; +import useFetchUserMutation from "@/hooks/mutations/useFetchUserMutation"; + + + +const inter = Inter({ subsets: ["latin"] }); + +export default function Layout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + + const [userInitialized,setUserInitialized] = useState(false) + const router = useRouter() + const {mutate: fetchUserMutate} = useFetchUserMutation() + + + + useEffect(() => { + if(!Cookies.get('access_token')) + { + router.replace("/b2c/login") + } + else + { + + fetchUserMutate(Cookies.get('access_token'),{ + onError: () => router.replace("/b2c/login"), + onSuccess: () => setUserInitialized(true) + }) + } + },[]) + + + + + return ( + <> {userInitialized ? ( + <> + + {children} + + + ) : ( + <> + + + )} + + + ); +} diff --git a/apps/client-ts/src/app/api-keys/layout.tsx b/apps/client-ts/src/app/api-keys/layout.tsx deleted file mode 100644 index b291e97b0..000000000 --- a/apps/client-ts/src/app/api-keys/layout.tsx +++ /dev/null @@ -1,29 +0,0 @@ -'use client' -import { Inter } from "next/font/google"; -import "./../globals.css"; -import { RootLayout } from "@/components/RootLayout"; -import { useStytchSession } from "@stytch/nextjs"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; -import config from "@/lib/config"; - -export default function Layout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - const { session,isInitialized } = useStytchSession(); - const router = useRouter(); - useEffect(() => { - if(config.DISTRIBUTION !== "selfhosted" && isInitialized && !session){ - router.replace("/b2c/login"); - } - }, [session,isInitialized, router]); - - return ( - <> - - {children} - - ); -} diff --git a/apps/client-ts/src/app/authenticate/page.tsx b/apps/client-ts/src/app/authenticate/page.tsx deleted file mode 100644 index 1ce16c49b..000000000 --- a/apps/client-ts/src/app/authenticate/page.tsx +++ /dev/null @@ -1,79 +0,0 @@ -"use client"; - -import { Suspense, useEffect } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useStytchUser, useStytch } from "@stytch/nextjs"; -import useProfile from "@/hooks/useProfile"; -import useProfileMutation from "@/hooks/mutations/useProfileMutation"; -//import useOrganisations from "@/hooks/useOrganisations"; - -const OAUTH_TOKEN = "oauth"; -const MAGIC_LINKS_TOKEN = "magic_links"; - -/** - * During both the Magic link and OAuth flows, Stytch will redirect the user back to your application to a specified redirect URL (see Login.tsx). - * Stytch will append query parameters to the redirect URL which are then used to complete the authentication flow. - * A redirect URL for this example app will look something like: http://localhost:3000/authenticate?stytch_token_type=magic_links&token=abc123 - * - * The AuthenticatePage will detect the presence of a token in the query parameters, and attempt to authenticate it. - * - * On successful authentication, a session will be created and the user will be redirect to /profile. - */ -const InnerAuthenticate = () => { - const { user, isInitialized } = useStytchUser(); - const stytch = useStytch(); - const router = useRouter(); - const searchParams = useSearchParams(); - const { data, isLoading } = useProfile(user?.user_id!); - const { mutate } = useProfileMutation(); - //const { data: orgs, isLoading: isloadingOrganisations } = useOrganisations(); - - useEffect(() => { - if (stytch && !user && isInitialized) { - const token = searchParams.get("token"); - const stytch_token_type = searchParams.get("stytch_token_type"); - - if (token && stytch_token_type === OAUTH_TOKEN) { - stytch.oauth.authenticate(token, { - session_duration_minutes: 60, - }); - } else if (token && stytch_token_type === MAGIC_LINKS_TOKEN) { - stytch.magicLinks.authenticate(token, { - session_duration_minutes: 60, - }); - } - } - }, [isInitialized, router, searchParams, stytch, user]); - - useEffect(() => { - if (!isInitialized) { - return; - } - if (user) { - if (!data) { - mutate({ - first_name: user.name.first_name, - last_name: user.name.last_name, - email: user.emails[0].email, - stytch_id_user: user.user_id, - strategy: 'b2c', - //id_organization: orgs && orgs[0].id_organization - }); - } - router.replace("/b2c/profile"); - } - }, [router, user, isInitialized, data, mutate]); - - return null; -}; - -const Authenticate = () => { - - return ( - - - - ) -} - -export default Authenticate; \ No newline at end of file diff --git a/apps/client-ts/src/app/b2c/login/page.tsx b/apps/client-ts/src/app/b2c/login/page.tsx index 444dfcf28..34f943715 100644 --- a/apps/client-ts/src/app/b2c/login/page.tsx +++ b/apps/client-ts/src/app/b2c/login/page.tsx @@ -1,17 +1,98 @@ 'use client'; +import CreateUserForm from "@/components/Auth/CustomLoginComponent/CreateUserForm"; +import LoginUserForm from "@/components/Auth/CustomLoginComponent/LoginUserForm"; +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs" +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import Cookies from 'js-cookie'; +import useProfileStore from "@/state/profileStore"; +import useFetchUserMutation from "@/hooks/mutations/useFetchUserMutation"; -import LoginWithStytchSDKUI from "@/components/Auth/b2c/LoginWithStytchSDKUI"; -export default async function Page() { + +export default function Page() { + + const [userInitialized,setUserInitialized] = useState(true) + const {mutate : fetchUserMutate} = useFetchUserMutation() + const router = useRouter() + const {profile} = useProfileStore(); + + useEffect(() => { + + if(profile) + { + router.replace('/connections'); + } + + },[profile]); + + useEffect(() => { + + if(!Cookies.get('access_token')) + { + setUserInitialized(false); + } + + if(Cookies.get('access_token') && !profile) + { + fetchUserMutate(Cookies.get('access_token'),{ + onError: () => setUserInitialized(false) + }) + } + + // if(profile) + // { + // router.replace('/connections'); + // } + + },[]) + return ( -
-
- - -
-
- Login Page Image + <> + + {!userInitialized ? + ( +
+
+ + + + Login + Create Account + + + + + + + + +
+
+ Login Page Image +
-
+ ) : + ( + <> + ) + } + ) } diff --git a/apps/client-ts/src/app/b2c/profile/layout.tsx b/apps/client-ts/src/app/b2c/profile/layout.tsx deleted file mode 100644 index d08ecad2d..000000000 --- a/apps/client-ts/src/app/b2c/profile/layout.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client' -import "./../../globals.css"; -import { RootLayout } from "@/components/RootLayout"; -import { useStytchSession } from "@stytch/nextjs"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; - - -export default function Layout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - const { session,isInitialized } = useStytchSession(); - const router = useRouter(); - useEffect(() => { - if (isInitialized && !session) { - router.replace("/b2c/login"); - } - }, [session, isInitialized, router]); - //console.log('WEBAPP DOMAIN is '+ process.env.NEXT_PUBLIC_WEBAPP_DOMAIN) - return ( - <> - - {children} - - ); -} diff --git a/apps/client-ts/src/app/configuration/layout.tsx b/apps/client-ts/src/app/configuration/layout.tsx deleted file mode 100644 index 38ce8043d..000000000 --- a/apps/client-ts/src/app/configuration/layout.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client' -import { Inter } from "next/font/google"; -import "./../globals.css"; -import { RootLayout } from "@/components/RootLayout"; -import { useStytchSession } from "@stytch/nextjs"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; -import config from "@/lib/config"; - -export default function Layout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - const { session,isInitialized } = useStytchSession(); - - const router = useRouter(); - useEffect(() => { - if(config.DISTRIBUTION !== "selfhosted" && isInitialized && !session){ - router.push("/b2c/login"); - } - }, [session, isInitialized, router]); - - return ( - <> - - {children} - - ); -} diff --git a/apps/client-ts/src/app/connections/layout.tsx b/apps/client-ts/src/app/connections/layout.tsx deleted file mode 100644 index 1ff2e6a2f..000000000 --- a/apps/client-ts/src/app/connections/layout.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client' -import { Inter } from "next/font/google"; -import "./../globals.css"; -import { RootLayout } from "@/components/RootLayout"; -import { useStytchSession } from "@stytch/nextjs"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; -import config from "@/lib/config"; - -const inter = Inter({ subsets: ["latin"] }); - -export default function Layout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - const { session,isInitialized } = useStytchSession(); - const router = useRouter(); - useEffect(() => { - if(config.DISTRIBUTION !== "selfhosted" && isInitialized && !session){ - router.push("/b2c/login"); - } - }, [session,isInitialized, router]); - - return ( - <> - - {children} - - ); -} diff --git a/apps/client-ts/src/app/dashboard/layout.tsx b/apps/client-ts/src/app/dashboard/layout.tsx deleted file mode 100644 index 3d766efc6..000000000 --- a/apps/client-ts/src/app/dashboard/layout.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client' -import { Inter } from "next/font/google"; -import "./../globals.css"; -import { RootLayout } from "@/components/RootLayout"; -import { useStytchSession } from "@stytch/nextjs"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; -import config from "@/lib/config"; - -const inter = Inter({ subsets: ["latin"] }); - -export default function Layout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - const { session,isInitialized } = useStytchSession(); - const router = useRouter(); - useEffect(() => { - if(config.DISTRIBUTION !== "selfhosted" && isInitialized && !session){ - router.replace("/b2c/login"); - } - }, [session,isInitialized, router]); - - return ( - <> - - {children} - - ); -} diff --git a/apps/client-ts/src/app/events/layout.tsx b/apps/client-ts/src/app/events/layout.tsx deleted file mode 100644 index 6916486b2..000000000 --- a/apps/client-ts/src/app/events/layout.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client' -import { Inter } from "next/font/google"; -import "./../globals.css"; -import { RootLayout } from "@/components/RootLayout"; -import { useStytchSession } from "@stytch/nextjs"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; -import config from "@/lib/config"; - -const inter = Inter({ subsets: ["latin"] }); - -export default function Layout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - const { session, isInitialized} = useStytchSession(); - const router = useRouter(); - useEffect(() => { - - if(config.DISTRIBUTION !== "selfhosted" && isInitialized && !session){ - router.replace("/b2c/login"); - } - }, [session,isInitialized, router]); - - return ( - <> - - {children} - - ); -} diff --git a/apps/client-ts/src/app/layout.tsx b/apps/client-ts/src/app/layout.tsx index 865a96b3e..f1ba7bb41 100644 --- a/apps/client-ts/src/app/layout.tsx +++ b/apps/client-ts/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Inter } from "next/font/google"; import "./globals.css"; import { Provider } from "@/components/Provider/provider"; import { Toaster } from "@/components/ui/sonner" +import {ThemeProvider} from '@/components/Nav/theme-provider' const inter = Inter({ subsets: ["latin"] }); @@ -17,12 +18,20 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - - - {children} - - + + + + + {children} + + + + ); diff --git a/apps/client-ts/src/components/Auth/CustomLoginComponent/CreateUserForm.tsx b/apps/client-ts/src/components/Auth/CustomLoginComponent/CreateUserForm.tsx new file mode 100644 index 000000000..314114c9b --- /dev/null +++ b/apps/client-ts/src/components/Auth/CustomLoginComponent/CreateUserForm.tsx @@ -0,0 +1,165 @@ +"use client" +import React from 'react' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import * as z from "zod" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { PasswordInput } from '@/components/ui/password-input' +import useCreateUserMutation from '@/hooks/mutations/useCreateUserMutation' + +const formSchema = z.object({ + first_name: z.string().min(2,{ + message:"Enter First Name" + }), + last_name : z.string().min(2, { + message: "Enter Last Name.", + }), + email : z.string().email({ + message: "Enter valid Email.", + }), + password : z.string().min(2, { + message: "Enter Password.", + }), + +}) + +const CreateUserForm = () => { + + const {mutate : createUserMutate} = useCreateUserMutation(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + first_name:'', + last_name:'', + email:'', + password:'' + }, + }) + + const onSubmit = (values: z.infer) => { + // console.log(values) + createUserMutate({ + first_name:values.first_name, + last_name:values.last_name, + email:values.email, + strategy:'b2c', + password_hash:values.password + }, + { + onSuccess:() => { + form.reset(); + } + }); + + + } + + + return ( + <> +
+ + + + Sign Up + + Create your account. + + + +
+
+
+ ( + + First Name + + + + + + )} + /> +
+
+ ( + + Last Name + + + + + + )} + /> +
+
+
+ ( + + Email + + + + + + )} + /> +
+
+ ( + + Password + + + + + + )} + /> +
+
+
+ + + +
+
+ + + ) +} + +export default CreateUserForm \ No newline at end of file diff --git a/apps/client-ts/src/components/Auth/CustomLoginComponent/LoginUserForm.tsx b/apps/client-ts/src/components/Auth/CustomLoginComponent/LoginUserForm.tsx new file mode 100644 index 000000000..5726493bc --- /dev/null +++ b/apps/client-ts/src/components/Auth/CustomLoginComponent/LoginUserForm.tsx @@ -0,0 +1,122 @@ +"use client" +import React from 'react' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { PasswordInput } from '@/components/ui/password-input' +import * as z from "zod" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import useLoginMutation from '@/hooks/mutations/useLoginMutation' +import { useRouter } from "next/navigation"; + +const formSchema = z.object({ + email: z.string().email({ + message:"Enter valid Email" + }), + password : z.string().min(2, { + message: "Enter Password.", + }), +}) + +const LoginUserForm = () => { + + const router = useRouter() + + const {mutate : loginMutate} = useLoginMutation() + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email:'', + password:'' + }, + }) + +const onSubmit = (values: z.infer) => { + + loginMutate({ + email:values.email, + password_hash:values.password + }, + { + onSuccess: () => router.replace("/connections") + }) + +} + + + + return ( + <> +
+ + + + + Login + + Enter your Email and Password to login. + + + +
+ ( + + Email + + + + + + )} + /> +
+
+ ( + + Password + + + + + + )} + /> +
+ +
+ + + +
+
+ + + ) +} + +export default LoginUserForm \ No newline at end of file diff --git a/apps/client-ts/src/components/Auth/DashboardClient.tsx b/apps/client-ts/src/components/Auth/DashboardClient.tsx deleted file mode 100644 index 095366fd7..000000000 --- a/apps/client-ts/src/components/Auth/DashboardClient.tsx +++ /dev/null @@ -1,386 +0,0 @@ -'use client' - -import { - FormEventHandler, - MouseEventHandler, - useEffect, - useState, -} from "react"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import Link from "next/link"; -import { Input } from "@/components/ui/input" - -import { - createOidcSSOConn, - createSamlSSOConn, - deleteMember, - invite, -} from "@/lib/stytch/api"; -import { - Member, - OIDCConnection, - Organization, - SAMLConnection, -} from "@/lib/stytch/loadStytch"; -import { - Avatar, - AvatarFallback, - AvatarImage, -} from "@/components/ui/avatar" -import { Button } from "@/components/ui/button" -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card" - -import { Separator } from "@/components/ui/separator" -import { CircleIcon } from "@radix-ui/react-icons" -import { useCreateSamlSso } from "@/hooks/stytch/useCreateSamlSso"; -import { useCreateOidcSso } from "@/hooks/stytch/useCreateOidcSso"; -import { useInvite } from "@/hooks/stytch/useInvite"; -import { useDeleteMember } from "@/hooks/stytch/useDeleteMember"; - - -type Props = { - org: Organization; - user: Member; - members: Member[]; - saml_connections: SAMLConnection[]; - oidc_connections: OIDCConnection[]; -}; - -const isValidEmail = (emailValue: string) => { - // Overly simple email address regex - const regex = /\S+@\S+\.\S+/; - return regex.test(emailValue); -}; - -const isAdmin = (member: Member) => !!member.trusted_metadata!.admin; - -const SSO_METHOD = { - SAML: "SAML", - OIDC: "OIDC", -}; - -const MemberRow = ({ member, user }: { member: Member; user: Member; }) => { - const router = useRouter(); - const pathname = usePathname(); - const [isDisabled, setIsDisabled] = useState(false); - - const { mutate, isLoading, error, data } = useDeleteMember(); - - const doDelete: MouseEventHandler = (e) => { - e.preventDefault(); - setIsDisabled(true); - mutate(member.member_id); - // Force a reload to refresh the user list - router.replace(pathname); - // TODO: Success toast? - }; - - const canDelete = - /* Do not let members delete themselves! */ - member.member_id !== user.member_id && - /* Only admins can delete! */ - isAdmin(user); - - const deleteButton = ( - - ); - - return ( -
-
- - - OM - -
-

- {member.email_address} ( - {member.status}) -

-

{isAdmin(member) ? "@admin" : "@member"}

-
-
- - {canDelete ? deleteButton : null} -
- ); -}; - -const MemberList = ({ - members, - user, - org, -}: Pick) => { - const router = useRouter(); - const pathname = usePathname(); - const [email, setEmail] = useState(""); - const [isDisabled, setIsDisabled] = useState(true); - - useEffect(() => { - setIsDisabled(!isValidEmail(email)); - }, [email]); - - const { mutate, isLoading, error, data } = useInvite(); - - - const onInviteSubmit: FormEventHandler = (e) => { - e.preventDefault(); - // Disable button right away to prevent sending emails twice - if (isDisabled) { - return; - } else { - setIsDisabled(true); - } - mutate(email); - // Force a reload to refresh the user list - router.replace(pathname); - }; - - return ( - <> -
-

People with access

-
- {members.map((member) => ( - - ))} -
-
- -
-

Invite new member

-
- setEmail(e.target.value)} - type="email" - className="col-span-3 flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" - /> - -
-
- - ); -}; - -const IDPList = ({ - user, - saml_connections, - oidc_connections, -}: Pick) => { - const [idpNameSAML, setIdpNameSAML] = useState(""); - const [idpNameOIDC, setIdpNameOIDC] = useState(""); - const [ssoMethod, setSsoMethod] = useState(SSO_METHOD.SAML); - const router = useRouter(); - const searchParams = useSearchParams(); - - const { mutate: samlMutate, isLoading: samlLoading, error: samlError, data: samlData } = useCreateSamlSso(); - const { mutate: oidcMutate, isLoading: oidcLoading, error: oidcError, data: oidcData } = useCreateOidcSso(); - - - const onSamlCreate: FormEventHandler = (e) => { - e.preventDefault(); - samlMutate(idpNameSAML); - if (samlError) { - alert("Error creating connection"); - return; - } - const conn = samlData as any; - router.push( - `/${searchParams.get('slug')}/dashboard/saml/${conn.connection_id}` - ); - }; - - const onOidcCreate: FormEventHandler = (e) => { - e.preventDefault(); - oidcMutate(idpNameOIDC); - if (oidcError) { - alert("Error creating connection"); - return; - } - const conn = oidcData as any; - router.push( - `/${searchParams.get('slug')}/dashboard/oidc/${conn.connection_id}` - ); - }; - - return ( - <> -
- <> -

SSO Connections

-

SAML

- {saml_connections.length === 0 &&

No connections configured.

} -
    - {saml_connections.map((conn) => ( -
  • - - - {conn!.display_name} ({conn!.status}) - - -
  • - ))} -
-

OIDC

- {oidc_connections.length === 0 &&

No connections configured.

} -
    - {oidc_connections.map((conn) => ( -
  • - - - {conn!.display_name} ({conn!.status}) - - -
  • - ))} -
- -
- - {/*Only admins can create new SSO Connection*/} - {isAdmin(user) && ( -
-

Create a new SSO Connection

-
- setIdpNameSAML(e.target.value) - : (e) => setIdpNameOIDC(e.target.value) - } - /> - -
-
- setSsoMethod(SSO_METHOD.SAML)} - checked={ssoMethod === SSO_METHOD.SAML} - /> - - setSsoMethod(SSO_METHOD.OIDC)} - checked={ssoMethod === SSO_METHOD.OIDC} - /> - -
-
- )} - - ); -}; - -{/*
-

- MFA Setting: {org.mfa_policy} -

- - -
-*/} - -const DashboardClient = ({ - org, - user, - members, - saml_connections, - oidc_connections, -}: { - org: Organization, - user: Member, - members: Member[], - saml_connections: SAMLConnection[], - oidc_connections: OIDCConnection[] -}) => { - - return ( -
- - - Organization - -
- - {org.organization_name} -
-
-
- -

Connected user

- -
- - -
- - -
- -      - -
-
-
-
- ); -}; - - -export default DashboardClient; diff --git a/apps/client-ts/src/components/Auth/DiscoveryClient.tsx b/apps/client-ts/src/components/Auth/DiscoveryClient.tsx deleted file mode 100644 index d864119c2..000000000 --- a/apps/client-ts/src/components/Auth/DiscoveryClient.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client'; - -import { useState } from "react"; - -/*const CreateNewOrganization = () => { - const [orgName, setOrgName] = useState(""); - const [requireMFA, setRequireMFA] = useState(false); - return ( -
-

Or, create a new Organization

- -
- - setOrgName(e.target.value)} - /> -
- setRequireMFA(!requireMFA)} - checked={requireMFA} - /> - -
- -
-
- ); -};*/ - - -const DiscoveryClient = ({ - children, -}: { - children: React.ReactNode -}) => { - return ( -
- {children} - {/**/} -
- ); -}; - -export default DiscoveryClient; diff --git a/apps/client-ts/src/components/Auth/DiscoveryServer.tsx b/apps/client-ts/src/components/Auth/DiscoveryServer.tsx deleted file mode 100644 index 887b72eef..000000000 --- a/apps/client-ts/src/components/Auth/DiscoveryServer.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import loadStytch, { DiscoveredOrganizations } from "@/lib/stytch/loadStytch"; -import { getDiscoverySessionData } from "@/lib/stytch/sessionService"; -import { cookies } from "next/headers"; -import Link from "next/link"; -import { CardTitle, CardHeader, CardContent, CardDescription, Card } from "../ui/card"; - -async function getProps() { - const discoverySessionData = getDiscoverySessionData( - cookies().get('session')?.value, - cookies().get('intermediate_session')?.value, - ); - if (discoverySessionData.error) { - console.log("No session tokens found..."); - return { redirect: { statusCode: 307, destination: `/auth/login` } }; - } - - const { discovered_organizations } = - await loadStytch().discovery.organizations.list({ - intermediate_session_token: discoverySessionData.intermediateSession, - session_jwt: discoverySessionData.sessionJWT, - }); - - console.log(discovered_organizations); - - return { - discovered_organizations - }; -} - -type Props = { - discovered_organizations: DiscoveredOrganizations; -}; - -const DiscoveredOrganizationsList = ({ discovered_organizations }: Props) => { - const formatMembership = ({ - membership, - organization, - }: Pick) => { - if (membership!.type === "pending_member") { - return `Join ${organization!.organization_name}`; - } - if (membership!.type === "eligible_to_join_by_email_domain") { - return `Join ${organization!.organization_name} via your ${membership!.details!.domain} email`; - } - if (membership!.type === "invited_member") { - return `Accept Invite for ${organization!.organization_name}`; - } - return `Continue to ${organization!.organization_name}`; - }; - - return ( - - - - Your Organizations - {discovered_organizations.length === 0 && ( -

No existing organizations.

- )}
-
- - {discovered_organizations.map(({ organization, membership }) => ( - -
-
-

- {formatMembership({ organization, membership })} -

-
-
- - ))} - -
-
- ); - }; - -const DiscoveryServer = async () => { - const {discovered_organizations} = await getProps(); - return ( - - ); -}; - -export default DiscoveryServer; \ No newline at end of file diff --git a/apps/client-ts/src/components/Auth/LoginDiscoveryForm.tsx b/apps/client-ts/src/components/Auth/LoginDiscoveryForm.tsx deleted file mode 100644 index 7294babf2..000000000 --- a/apps/client-ts/src/components/Auth/LoginDiscoveryForm.tsx +++ /dev/null @@ -1,79 +0,0 @@ -'use client'; - -import {FormEventHandler, useState} from "react"; -import {useRouter} from "next/navigation"; -import Link from "next/link"; -import {EmailLoginForm} from "./EmailLoginForm"; -import {discoveryStart} from "@/lib/stytch/api"; -import {OAuthButton, OAuthProviders} from "./OAuthButton"; -import { Input } from "@/components/ui/input" -import { Button } from "../ui/button"; -import { Badge } from "../ui/badge"; -import { Card } from "../ui/card"; - -const ContinueToTenantForm = ({ onBack }: { onBack: () => void }) => { - const [slug, setSlug] = useState(""); - const router = useRouter(); - - const onSubmit: FormEventHandler = async (e) => { - e.preventDefault(); - router.push(`/auth/${slug}/login`); - }; - - return ( - -

Enter your organization

-

- Don't know your organization's Domain? -

-

- Login to find your{" "} - - organizations - -

-
- setSlug(e.target.value)} - placeholder="acme-corp" - className="mt-4" - /> - -
-
- ); -}; - -type Props = { domain: string; }; - -const LoginDiscoveryForm = ({domain}: Props) => { - const [isDiscovery, setIsDiscovery] = useState(true); - - if (isDiscovery) { - return ( - <> - -

- We'll email you a magic code for a password-free sign in. -
- Or you can{" "} - setIsDiscovery(false)}> - sign in manually instead - - . -

-
- - - - ); - } else { - return setIsDiscovery(true)} />; - } -}; - -export default LoginDiscoveryForm; diff --git a/apps/client-ts/src/components/Auth/OAuthButton.tsx b/apps/client-ts/src/components/Auth/OAuthButton.tsx deleted file mode 100644 index ce73950a6..000000000 --- a/apps/client-ts/src/components/Auth/OAuthButton.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import Link from "next/link"; -import GoogleIconSvg from "../../../public/icons/google"; -import {formatOAuthDiscoveryStartURL, formatOAuthStartURL} from "@/lib/stytch/loadStytch"; -import MicrosoftIconSvg from "../../../public/icons/microsoft"; - - -export enum OAuthProviders { - Google = 'google', - Microsoft = 'microsoft', -} - -const providerInfo = { - [OAuthProviders.Google]: { - providerTypeTitle: 'Google', - providerIcon: , - }, - [OAuthProviders.Microsoft]: { - providerTypeTitle: 'Microsoft', - providerIcon: , - } -} - -type Props = { - providerType: OAuthProviders; - hostDomain: string; - orgSlug?: string; -}; - -export const OAuthButton = ({ providerType, hostDomain, orgSlug }: Props) => { - const isDiscovery = orgSlug == null; - const oAuthStartURL = isDiscovery ? formatOAuthDiscoveryStartURL(hostDomain, providerType) : formatOAuthStartURL(hostDomain, providerType, orgSlug); - - return ( - -
{providerInfo[providerType].providerIcon}
- {`Continue with ${providerInfo[providerType].providerTypeTitle}`} - - ); -}; diff --git a/apps/client-ts/src/components/Auth/SignupForm.tsx b/apps/client-ts/src/components/Auth/SignupForm.tsx deleted file mode 100644 index 886c4da33..000000000 --- a/apps/client-ts/src/components/Auth/SignupForm.tsx +++ /dev/null @@ -1,126 +0,0 @@ -'use client'; - -import { - ChangeEventHandler, - FormEventHandler, - useEffect, - useState, -} from "react"; -import { discoveryStart } from "@/lib/stytch/api"; -import {OAuthButton, OAuthProviders} from "./OAuthButton"; -import { Input } from "@/components/ui/input" - -const STATUS = { - INIT: 0, - SENT: 1, - ERROR: 2, -}; - -const isValidEmail = (emailValue: string) => { - // Overly simple email address regex - const regex = /\S+@\S+\.\S+/; - return regex.test(emailValue); -}; - -const isValidOrgName = (organizationName: string) => { - return organizationName.length > 3; -}; - -type Props = { domain: string; }; - -const SignupForm = ({ domain }: Props) => { - const [emlSent, setEMLSent] = useState(STATUS.INIT); - const [email, setEmail] = useState(""); - const [isDisabled, setIsDisabled] = useState(true); - - useEffect(() => { - const isValid = isValidEmail(email); - setIsDisabled(!isValid); - }, [email]); - - const onEmailChange: ChangeEventHandler = (e) => { - setEmail(e.target.value); - if (isValidEmail(e.target.value)) { - setIsDisabled(false); - } else { - setIsDisabled(true); - } - }; - - const onSubmit: FormEventHandler = async (e) => { - e.preventDefault(); - // Disable button right away to prevent sending emails twice - if (isDisabled) { - return; - } else { - setIsDisabled(true); - } - - if (isValidEmail(email)) { - const resp = await discoveryStart(email); - if (resp.status === 200) { - setEMLSent(STATUS.SENT); - } else { - setEMLSent(STATUS.ERROR); - } - } - }; - - const handleTryAgain = (e: any) => { - e.preventDefault(); - e.stopPropagation(); - setEMLSent(STATUS.INIT); - // setEmail(''); - // setOrganizationName(''); - }; - - return ( -
- {emlSent === STATUS.INIT && ( - <> -

Sign up

-
- - setEmail(e.target.value)} - type="email" - /> - -
- or - - - - )} - {emlSent === STATUS.SENT && ( - <> -

Check your email

-

{`An email was sent to ${email}`}

- - Click here to try again. - - - )} - {emlSent === STATUS.ERROR && ( -
-

Something went wrong!

-

{`Failed to send email to ${email}`}

- - Click here to try again. - -
- )} -
- ); -}; - -export default SignupForm; diff --git a/apps/client-ts/src/components/Auth/TenantedLoginForm.tsx b/apps/client-ts/src/components/Auth/TenantedLoginForm.tsx deleted file mode 100644 index 895aa0277..000000000 --- a/apps/client-ts/src/components/Auth/TenantedLoginForm.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client" - -import { login } from "@/lib/stytch/api"; -import { - formatSSOStartURL, - Organization, -} from "@/lib/stytch/loadStytch"; -import { EmailLoginForm } from "./EmailLoginForm"; -import {OAuthButton, OAuthProviders} from "./OAuthButton"; -import { Card } from "../ui/card"; - -type Props = { - org: Organization; - domain: string; -}; -const TenantedLoginForm = ({ org, domain }: Props) => { - return ( - - login(email, org.organization_id)} - > - {org.sso_default_connection_id && ( -
-

- Or, use this organization's  - - Preferred Identity Provider - -

-
-
- )} -
- - - {/* Login with Google*/} - {/**/} -
- ); -}; - - -export default TenantedLoginForm; diff --git a/apps/client-ts/src/components/Auth/b2c/LoginWithStytchSDKUI.tsx b/apps/client-ts/src/components/Auth/b2c/LoginWithStytchSDKUI.tsx deleted file mode 100644 index 15674fe51..000000000 --- a/apps/client-ts/src/components/Auth/b2c/LoginWithStytchSDKUI.tsx +++ /dev/null @@ -1,80 +0,0 @@ -'use client'; - -import { StytchLogin } from '@stytch/nextjs'; -import { StytchLoginConfig, OAuthProviders, Products, StyleConfig, StytchEvent, StytchError } from '@stytch/vanilla-js'; -import { getDomainFromWindow } from '@/lib/stytch/urlUtils'; -import { Button } from '@/components/ui/button'; -import { Icons } from '@/components/shared/icons'; - -const sdkStyle: StyleConfig = { - fontFamily: '"Helvetica New", Helvetica, sans-serif', - buttons: { - primary: { - backgroundColor: '#19303d', - textColor: '#ffffff', - }, - }, -}; - -const sdkConfig: StytchLoginConfig = { - products: [Products.emailMagicLinks], - emailMagicLinksOptions: { - loginRedirectURL: getDomainFromWindow() + '/authenticate', - loginExpirationMinutes: 30, - signupRedirectURL: getDomainFromWindow() + '/authenticate', - signupExpirationMinutes: 30, - createUserAsPending: false, - } -}; - -const callbackConfig = { - onEvent: (message: StytchEvent) => console.log(message), - onError: (error: StytchError) => console.log(error), -} - -const getOauthUrl = (provider: string) => { - const isTest = process.env.NEXT_PUBLIC_STYTCH_PROJECT_ENV == 'test'; - const baseStytch = isTest ? "test" : "api" - return `https://${baseStytch}.stytch.com/v1/public/oauth/${provider.toLowerCase()}/start?public_token=${process.env.NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN}`; -} - -const LoginWithStytchSDKUI = () => { - return ( - <> -
- -
-
-
- -
-
- - Or continue with - -
-
- - - - ) -} - -export default LoginWithStytchSDKUI; \ No newline at end of file diff --git a/apps/client-ts/src/components/Connection/AddConnectionButton.tsx b/apps/client-ts/src/components/Connection/AddConnectionButton.tsx index 1344dce95..341f451a3 100644 --- a/apps/client-ts/src/components/Connection/AddConnectionButton.tsx +++ b/apps/client-ts/src/components/Connection/AddConnectionButton.tsx @@ -169,6 +169,20 @@ const AddConnectionButton = ({ + {idProject==="" ? ( + <> + + + +

You have to create project in order to create Connection.

+ + + + + ) + : + ( + <> Share this magic link with your customers @@ -244,6 +258,8 @@ const AddConnectionButton = ({ + + )}
) diff --git a/apps/client-ts/src/components/Connection/ConnectionTable.tsx b/apps/client-ts/src/components/Connection/ConnectionTable.tsx index 32339635f..9cd911554 100644 --- a/apps/client-ts/src/components/Connection/ConnectionTable.tsx +++ b/apps/client-ts/src/components/Connection/ConnectionTable.tsx @@ -59,30 +59,30 @@ export default function ConnectionTable() { return ( <>
-
- - - Linked +
+ + + Linked -

{linkedConnections("valid")?.length}

+

{linkedConnections("valid")?.length}

- - - Incomplete Link + + + Incomplete Link -

{linkedConnections("1")?.length}

+

{linkedConnections("1")?.length}

- - - Relink Needed + + + Relink Needed -

{linkedConnections("2")?.length}

+

{linkedConnections("2")?.length}

diff --git a/apps/client-ts/src/components/Connection/columns.tsx b/apps/client-ts/src/components/Connection/columns.tsx index 6c6283b8f..32e142b53 100644 --- a/apps/client-ts/src/components/Connection/columns.tsx +++ b/apps/client-ts/src/components/Connection/columns.tsx @@ -7,6 +7,10 @@ import { Checkbox } from "@/components/ui/checkbox" import { Connection } from "./data/schema" import { DataTableColumnHeader } from "./../shared/data-table-column-header" +import React,{ useState } from "react" +import { ClipboardIcon } from '@radix-ui/react-icons' +import { toast } from "sonner" + function truncateMiddle(str: string, maxLength: number) { if (str.length <= maxLength) { @@ -20,10 +24,10 @@ function truncateMiddle(str: string, maxLength: number) { function insertDots(originalString: string): string { if(!originalString) return ""; - if (originalString.length <= 50) { - return originalString; - } - return originalString.substring(0, 50 - 3) + '...'; + // if (originalString.length <= 50) { + // return originalString; + // } + return originalString.substring(0, 7) + '...'; } function formatISODate(ISOString: string): string { @@ -44,6 +48,26 @@ function formatISODate(ISOString: string): string { return formatter.format(date); } +const connectionTokenComponent = ({row}:{row:any}) => { + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(row.getValue("connectionToken")); + toast("Connection Token copied to clipboard!!") + } catch (err) { + console.error('Failed to copy: ', err); + } + }; + + return ( +
+ {truncateMiddle(row.getValue("connectionToken"),6)} + + + +
+ ) +} + export const columns: ColumnDef[] = [ { id: "select", @@ -187,23 +211,17 @@ export const columns: ColumnDef[] = [ //const label = labels.find((label) => label.value === row.original.date) return ( -
+
{formatISODate(row.getValue("date"))}
) }, }, - /*{ + { accessorKey: "connectionToken", header: ({ column }) => ( ), - cell: ({ row }) => { -
-
- {insertDots(row.getValue("connectionToken"))} -
-
- }, - }*/ + cell: connectionTokenComponent + } ] \ No newline at end of file diff --git a/apps/client-ts/src/components/Nav/main-nav.tsx b/apps/client-ts/src/components/Nav/main-nav.tsx index f51e83b42..913a06b28 100644 --- a/apps/client-ts/src/components/Nav/main-nav.tsx +++ b/apps/client-ts/src/components/Nav/main-nav.tsx @@ -11,15 +11,15 @@ export function MainNav({ onLinkClick: (name: string) => void; className: string; }) { - const [selectedItem, setSelectedItem] = useState("connections"); + const [selectedItem, setSelectedItem] = useState(""); const pathname = usePathname(); useEffect(() => { setSelectedItem(pathname.substring(1)) }, [pathname]) const navItemClassName = (itemName: string) => - `text-sm border-b font-medium w-full text-left px-4 py-2 dark:hover:bg-zinc-900 hover:bg-zinc-200 cursor-pointer ${ - selectedItem === itemName ? 'dark:bg-zinc-800 bg-zinc-200' : 'text-muted-foreground' + `group flex items-center rounded-md px-3 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground cursor-pointer ${ + selectedItem === itemName ? 'bg-accent' : 'transparent' } transition-colors`; function click(e: MouseEvent, name: string) { @@ -29,7 +29,7 @@ export function MainNav({ return ( + +
{children}
+
- )} -
- {/**/} -
-
- ); + + + ) + }; \ No newline at end of file diff --git a/apps/client-ts/src/components/shared/team-switcher.tsx b/apps/client-ts/src/components/shared/team-switcher.tsx index 13ba7ceb7..b4e7d5362 100644 --- a/apps/client-ts/src/components/shared/team-switcher.tsx +++ b/apps/client-ts/src/components/shared/team-switcher.tsx @@ -55,6 +55,7 @@ import * as z from "zod" import config from "@/lib/config" import { Skeleton } from "@/components/ui/skeleton"; import useProfileStore from "@/state/profileStore" +import { projects as Project } from 'api'; const projectFormSchema = z.object({ @@ -66,7 +67,7 @@ const projectFormSchema = z.object({ type PopoverTriggerProps = React.ComponentPropsWithoutRef interface TeamSwitcherProps extends PopoverTriggerProps { - userId: string; + projects:Project[] } interface ModalObj { @@ -74,16 +75,25 @@ interface ModalObj { status?: number; // 0 for org, 1 for project } -export default function TeamSwitcher({ className, userId }: TeamSwitcherProps) { +export default function TeamSwitcher({ className ,projects}: TeamSwitcherProps) { const [open, setOpen] = useState(false) const [showNewDialog, setShowNewDialog] = useState({ open: false, }) + + + const { profile } = useProfileStore(); const { idProject, setIdProject } = useProjectStore(); - const { profile } = useProfileStore(); - const projects = profile?.projects; + // const projects = profile?.projects; + + // useEffect(() => { + // if(idProject==="" && projects) + // { + // setIdProject(projects[0]?.id_project) + // } + // },[projects]) const handleOpenChange = (open: boolean) => { setShowNewDialog(prevState => ({ ...prevState, open })); diff --git a/apps/client-ts/src/components/ui/heading.tsx b/apps/client-ts/src/components/ui/heading.tsx new file mode 100644 index 000000000..4471e7b6b --- /dev/null +++ b/apps/client-ts/src/components/ui/heading.tsx @@ -0,0 +1,13 @@ +interface HeadingProps { + title: string; + description: string; +} + +export const Heading: React.FC = ({ title, description }) => { + return ( +
+

{title}

+

{description}

+
+ ); +}; diff --git a/apps/client-ts/src/hooks/mutations/useApiKeyMutation.tsx b/apps/client-ts/src/hooks/mutations/useApiKeyMutation.tsx index bfb3af782..85d52e345 100644 --- a/apps/client-ts/src/hooks/mutations/useApiKeyMutation.tsx +++ b/apps/client-ts/src/hooks/mutations/useApiKeyMutation.tsx @@ -1,6 +1,7 @@ import config from '@/lib/config'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { toast } from "sonner" +import Cookies from 'js-cookie'; interface IApiKeyDto { projectId: string; @@ -13,25 +14,25 @@ const useApiKeyMutation = () => { const addApiKey = async (data: IApiKeyDto) => { //TODO: in cloud environment this step must be done when user logs in directly inside his dashboard // Fetch the token - const loginResponse = await fetch(`${config.API_URL}/auth/login`, { - method: 'POST', - body: JSON.stringify({ id_user: data.userId.trim(), password_hash: 'my_password' }), - headers: { - 'Content-Type': 'application/json', - }, - }); + // const loginResponse = await fetch(`${config.API_URL}/auth/login`, { + // method: 'POST', + // body: JSON.stringify({ id_user: data.userId.trim(), password_hash: 'my_password' }), + // headers: { + // 'Content-Type': 'application/json', + // }, + // }); - if (!loginResponse.ok) { - throw new Error('Failed to login'); - } - const { access_token } = await loginResponse.json(); + // if (!loginResponse.ok) { + // throw new Error('Failed to login'); + // } + // const { access_token } = await loginResponse.json(); const response = await fetch(`${config.API_URL}/auth/generate-apikey`, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${access_token}`, + 'Authorization': `Bearer ${Cookies.get('access_token')}`, }, }); diff --git a/apps/client-ts/src/hooks/mutations/useCreateUserMutation.tsx b/apps/client-ts/src/hooks/mutations/useCreateUserMutation.tsx new file mode 100644 index 000000000..c6ff96fa2 --- /dev/null +++ b/apps/client-ts/src/hooks/mutations/useCreateUserMutation.tsx @@ -0,0 +1,71 @@ +import config from '@/lib/config'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { toast } from "sonner" + +interface IUserDto { + first_name: string, + last_name: string, + email: string, + strategy: string, + password_hash: string, + id_organisation?: string, +} +const useCreateUserMutation = () => { + + const addUser = async (userData: IUserDto) => { + // Fetch the token + const response = await fetch(`${config.API_URL}/auth/register`, { + method: 'POST', + body: JSON.stringify(userData), + headers: { + 'Content-Type': 'application/json', + }, + }); + + + + if (!response.ok) { + throw new Error("Email already associated with other account!!") + } + + + + + return response.json(); + }; + return useMutation({ + mutationFn: addUser, + onMutate: () => { + toast("User is being created !", { + description: "", + action: { + label: "Close", + onClick: () => console.log("Close"), + }, + }) + }, + onError: (error) => { + toast("User generation failed !", { + description: error as any, + action: { + label: "Close", + onClick: () => console.log("Close"), + }, + }) + }, + onSuccess: (data) => { + + toast("User has been generated !", { + description: "", + action: { + label: "Close", + onClick: () => console.log("Close"), + }, + }) + }, + onSettled: () => { + }, + }); +}; + +export default useCreateUserMutation; diff --git a/apps/client-ts/src/hooks/mutations/useFetchUserMutation.tsx b/apps/client-ts/src/hooks/mutations/useFetchUserMutation.tsx new file mode 100644 index 000000000..51a5ac22b --- /dev/null +++ b/apps/client-ts/src/hooks/mutations/useFetchUserMutation.tsx @@ -0,0 +1,79 @@ +import config from '@/lib/config'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { toast } from "sonner" +import useProfileStore from '@/state/profileStore'; +import { projects as Project } from 'api'; +import Cookies from 'js-cookie'; + + +type IUserDto = { + id_user: string; + email: string; + first_name: string; + last_name: string; + id_organization?: string; + } + + +const useFetchUserMutation = () => { + + const {setProfile} = useProfileStore() + + const verifyUser = async (cookie : string | undefined) => { + // Fetch the token + const response = await fetch(`${config.API_URL}/auth/profile`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${cookie}` + }, + }); + + if (!response.ok) { + Cookies.remove('access_token') + throw new Error("Fetch User Failed!!") + } + + return response.json(); + }; + return useMutation({ + mutationFn: verifyUser, + onMutate: () => { + // toast("Fetching the user !", { + // description: "", + // action: { + // label: "Close", + // onClick: () => console.log("Close"), + // }, + // }) + }, + onError: (error) => { + Cookies.remove('access_token') + toast.error("Fetch User failed !", { + description: error as any, + action: { + label: "Close", + onClick: () => console.log("Close"), + }, + }) + }, + onSuccess: (data : IUserDto) => { + + setProfile(data); + // Cookies.set('access_token',data.access_token,{expires:1}); + // console.log("Bearer Token in client Side : ",data.access_token); + + toast.success("User has been fetched !", { + description: "", + action: { + label: "Close", + onClick: () => console.log("Close"), + }, + }) + }, + onSettled: () => { + }, + }); +}; + +export default useFetchUserMutation; diff --git a/apps/client-ts/src/hooks/mutations/useLoginMutation.tsx b/apps/client-ts/src/hooks/mutations/useLoginMutation.tsx new file mode 100644 index 000000000..5939dd8dc --- /dev/null +++ b/apps/client-ts/src/hooks/mutations/useLoginMutation.tsx @@ -0,0 +1,94 @@ +import config from '@/lib/config'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { toast } from "sonner" +import useProfileStore from '@/state/profileStore'; +import { projects as Project } from 'api'; +import Cookies from 'js-cookie'; + +type IUserDto = { + id_user: string; + email: string; + first_name: string; + last_name: string; + id_organization?: string; +} + +interface ILoginInputDto { + email:string, + password_hash:string +} + + + +interface ILoginOutputDto { + user: IUserDto, + access_token: string + +} + + +const useLoginMutation = () => { + + const {setProfile} = useProfileStore() + + const loginUser = async (userData: ILoginInputDto) => { + // Fetch the token + const response = await fetch(`${config.API_URL}/auth/login`, { + method: 'POST', + body: JSON.stringify(userData), + headers: { + 'Content-Type': 'application/json', + }, + }); + + + + if (!response.ok) { + throw new Error("Login Failed!!") + } + + + + + return response.json(); + }; + return useMutation({ + mutationFn: loginUser, + onMutate: () => { + // toast("Logging the user !", { + // description: "", + // action: { + // label: "Close", + // onClick: () => console.log("Close"), + // }, + // }) + }, + onError: (error) => { + toast.error("User generation failed !", { + description: error as any, + action: { + label: "Close", + onClick: () => console.log("Close"), + }, + }) + }, + onSuccess: (data : ILoginOutputDto) => { + + setProfile(data.user); + Cookies.set('access_token',data.access_token,{expires:1}); + console.log("Bearer Token in client Side : ",data.access_token); + + toast.success("User has been generated !", { + description: "", + action: { + label: "Close", + onClick: () => console.log("Close"), + }, + }) + }, + onSettled: () => { + }, + }); +}; + +export default useLoginMutation; diff --git a/apps/client-ts/src/hooks/mutations/useProjectMutation.tsx b/apps/client-ts/src/hooks/mutations/useProjectMutation.tsx index 8717f1e88..42c01a4bc 100644 --- a/apps/client-ts/src/hooks/mutations/useProjectMutation.tsx +++ b/apps/client-ts/src/hooks/mutations/useProjectMutation.tsx @@ -46,10 +46,8 @@ const useProjectMutation = () => { }) }, onSuccess: (data) => { - queryClient.invalidateQueries({ - queryKey: ['projects'], - refetchType: 'active', - }) + + // console.log(data) queryClient.setQueryData(['projects'], (oldQueryData = []) => { return [...oldQueryData, data]; }); diff --git a/apps/client-ts/src/hooks/stytch/useCreateOidcSso.tsx b/apps/client-ts/src/hooks/stytch/useCreateOidcSso.tsx deleted file mode 100644 index 536f05301..000000000 --- a/apps/client-ts/src/hooks/stytch/useCreateOidcSso.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { createOidcSSOConn, createSamlSSOConn } from '@/lib/stytch/api'; -import { useCallback, useState } from 'react'; - -export const useCreateOidcSso = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [data, setData] = useState(null); - - const mutate = useCallback(async (display_name: string) => { - setIsLoading(true); - setError(null); - try { - const response = await createOidcSSOConn(display_name) - - if (!response.ok) { - throw new Error('Network response was not ok'); - } - - const data = await response.json(); - setData(data); - } catch (error) { - setError(error instanceof Error ? error.message : String(error)); - } finally { - setIsLoading(false); - } - }, []); - - return { mutate, isLoading, error, data }; -}; diff --git a/apps/client-ts/src/hooks/stytch/useCreateSamlSso.tsx b/apps/client-ts/src/hooks/stytch/useCreateSamlSso.tsx deleted file mode 100644 index 6ef006c05..000000000 --- a/apps/client-ts/src/hooks/stytch/useCreateSamlSso.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { createSamlSSOConn } from '@/lib/stytch/api'; -import { useCallback, useState } from 'react'; - -export const useCreateSamlSso = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [data, setData] = useState(null); - - const mutate = useCallback(async (display_name: string) => { - setIsLoading(true); - setError(null); - try { - const response = await createSamlSSOConn(display_name) - - if (!response.ok) { - throw new Error('Network response was not ok'); - } - - const data = await response.json(); - setData(data); - } catch (error) { - setError(error instanceof Error ? error.message : String(error)); - } finally { - setIsLoading(false); - } - }, []); - - return { mutate, isLoading, error, data }; -}; diff --git a/apps/client-ts/src/hooks/stytch/useDeleteMember.tsx b/apps/client-ts/src/hooks/stytch/useDeleteMember.tsx deleted file mode 100644 index 85f45afdd..000000000 --- a/apps/client-ts/src/hooks/stytch/useDeleteMember.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { deleteMember, invite } from '@/lib/stytch/api'; -import { useCallback, useState } from 'react'; - -export const useDeleteMember = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [data, setData] = useState(null); - - const mutate = useCallback(async (member_id: string) => { - setIsLoading(true); - setError(null); - try { - const response = await deleteMember(member_id) - - if (!response.ok) { - throw new Error('Network response was not ok'); - } - - const data = await response.json(); - setData(data); - } catch (error) { - setError(error instanceof Error ? error.message : String(error)); - } finally { - setIsLoading(false); - } - }, []); - - return { mutate, isLoading, error, data }; -}; diff --git a/apps/client-ts/src/hooks/stytch/useInvite.tsx b/apps/client-ts/src/hooks/stytch/useInvite.tsx deleted file mode 100644 index 67973b71f..000000000 --- a/apps/client-ts/src/hooks/stytch/useInvite.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { invite } from '@/lib/stytch/api'; -import { useCallback, useState } from 'react'; - -export const useInvite = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [data, setData] = useState(null); - - const mutate = useCallback(async (email: string) => { - setIsLoading(true); - setError(null); - try { - const response = await invite(email) - - if (!response.ok) { - throw new Error('Network response was not ok'); - } - - const data = await response.json(); - setData(data); - } catch (error) { - setError(error instanceof Error ? error.message : String(error)); - } finally { - setIsLoading(false); - } - }, []); - - return { mutate, isLoading, error, data }; -}; diff --git a/apps/client-ts/src/hooks/useProjectsByUser.tsx b/apps/client-ts/src/hooks/useProjectsByUser.tsx index 0c4c375d8..24a2a5d93 100644 --- a/apps/client-ts/src/hooks/useProjectsByUser.tsx +++ b/apps/client-ts/src/hooks/useProjectsByUser.tsx @@ -3,14 +3,15 @@ import { useQuery } from '@tanstack/react-query'; import { projects as Project } from 'api'; -const useProjectsByUser = (stytchUserId: string) => { +const useProjectsByUser = (userId: string | undefined) => { return useQuery({ queryKey: ['projects'], + enabled: userId!==undefined, queryFn: async (): Promise => { - if(stytchUserId === "" || !stytchUserId){ + if(userId === "" || !userId){ return []; } - const response = await fetch(`${config.API_URL}/projects/${stytchUserId}`); + const response = await fetch(`${config.API_URL}/projects/${userId}`); if (!response.ok) { throw new Error('Network response was not ok'); } diff --git a/apps/client-ts/src/lib/stytch/api.ts b/apps/client-ts/src/lib/stytch/api.ts deleted file mode 100644 index 4465b20c7..000000000 --- a/apps/client-ts/src/lib/stytch/api.ts +++ /dev/null @@ -1,127 +0,0 @@ -export const discoveryStart = async (email: string) => - fetch("/api/discovery/start", { - method: "POST", - body: JSON.stringify({ - email, - }), - }); - -export const login = async (email: string, organization_id: string) => - fetch("/api/login", { - method: "POST", - body: JSON.stringify({ - email, - organization_id, - }), - }); - -export const invite = async (email: string) => - fetch("/api/invite", { - method: "POST", - body: JSON.stringify({ - email, - }), - }); - -export const deleteMember = async (member_id: string) => - fetch("/api/delete_member", { - method: "POST", - body: JSON.stringify({ - member_id, - }), - }); - -export const createSamlSSOConn = async (display_name: string) => - fetch("/api/sso/saml/create", { - method: "POST", - body: JSON.stringify({ - display_name, - }), - }); - -export const createOrganizationFromDiscovery = async ( - organization_name: string -) => - fetch("/api/discovery/create", { - method: "POST", - body: JSON.stringify({ - organization_name, - }), - }); - -export const updateSamlSSOConn = async ({ - display_name, - idp_sso_url, - idp_entity_id, - email_attribute, - first_name_attribute, - last_name_attribute, - certificate, - connection_id, -}: { - display_name: string; - idp_sso_url: string; - idp_entity_id: string; - email_attribute: string; - first_name_attribute: string; - last_name_attribute: string; - certificate: string; - connection_id: string; -}) => - fetch("/api/sso/saml/update", { - method: "POST", - body: JSON.stringify({ - display_name, - idp_sso_url, - idp_entity_id, - email_attribute, - first_name_attribute, - last_name_attribute, - certificate, - connection_id, - }), - }); - -export const createOidcSSOConn = async (display_name: string) => - fetch("/api/sso/oidc/create", { - method: "POST", - body: JSON.stringify({ - display_name, - }), - }); - -export const updateOidcSSOConn = async ({ - display_name, - client_id, - client_secret, - issuer, - authorization_url, - token_url, - userinfo_url, - jwks_url, - connection_id, -}: { - display_name: string; - client_id: string; - client_secret: string; - issuer: string; - authorization_url: string; - token_url: string; - userinfo_url: string; - jwks_url: string; - connection_id: string; -}) => - fetch("/api/sso/oidc/update", { - method: "POST", - body: JSON.stringify({ - display_name, - client_id, - client_secret, - issuer, - authorization_url, - token_url, - userinfo_url, - jwks_url, - connection_id, - }), - }); diff --git a/apps/client-ts/src/lib/stytch/loadStytch.ts b/apps/client-ts/src/lib/stytch/loadStytch.ts deleted file mode 100644 index d59f3feb5..000000000 --- a/apps/client-ts/src/lib/stytch/loadStytch.ts +++ /dev/null @@ -1,74 +0,0 @@ -import * as stytch from "stytch"; - -let client: stytch.B2BClient; - -export const publicToken = process.env.NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN; - -export type Member = Awaited< - ReturnType ->["member"]; -export type Organization = Awaited< - ReturnType ->["organization"]; -export type SessionsAuthenticateResponse = Awaited< - ReturnType ->; -export type SAMLConnection = Awaited< - ReturnType ->["connection"]; - -export type OIDCConnection = Awaited< - ReturnType ->["connection"]; - -export type DiscoveredOrganizations = Awaited< - ReturnType ->["discovered_organizations"]; - -const stytchEnv = - process.env.NEXT_PUBLIC_STYTCH_PROJECT_ENV === "live" - ? stytch.envs.live - : stytch.envs.test; - -export const formatSSOStartURL = (redirectDomain: string, connection_id: string): string => { - const redirectURL = redirectDomain + "/api/callback"; - return `${stytchEnv}public/sso/start?connection_id=${connection_id}&public_token=${publicToken}&login_redirect_url=${redirectURL}`; -}; - -// No need to worry about CNames for OAuth Start URL's as Stytch will automatically redirect to the registered CName -export const formatOAuthDiscoveryStartURL = (redirectDomain: string, provider: string): string => { - const redirectURL = redirectDomain + "/api/callback"; - return `${stytchEnv}b2b/public/oauth/${provider}/discovery/start?public_token=${publicToken}&discovery_redirect_url=${redirectURL}`; -}; - -export const formatOAuthStartURL = (redirectDomain: string, provider: string, org_slug: string): string => { - const redirectURL = redirectDomain + "/api/callback"; - return `${stytchEnv}b2b/public/oauth/${provider}/start?public_token=${publicToken}&slug=${org_slug}&login_redirect_url=${redirectURL}`; -}; - -const loadStytch = () => { - if (!client) { - client = new stytch.B2BClient({ - project_id: process.env.NEXT_PUBLIC_STYTCH_PROJECT_ID || "", - secret: process.env.NEXT_PUBLIC_STYTCH_SECRET || "", - env: stytchEnv, - }); - } - - return client; -}; - -let clientB2C: stytch.Client; -export const loadB2CStytch = () => { - if (!clientB2C) { - clientB2C = new stytch.Client({ - project_id: process.env.NEXT_PUBLIC_STYTCH_PROJECT_ID || '', - secret: process.env.NEXT_PUBLIC_STYTCH_SECRET || '', - env: process.env.NEXT_PUBLIC_STYTCH_PROJECT_ENV === 'live' ? stytch.envs.live : stytch.envs.test, - }); - } - return clientB2C; -}; - - -export default loadStytch; diff --git a/apps/client-ts/src/lib/stytch/memberService.ts b/apps/client-ts/src/lib/stytch/memberService.ts deleted file mode 100644 index 120da482f..000000000 --- a/apps/client-ts/src/lib/stytch/memberService.ts +++ /dev/null @@ -1,19 +0,0 @@ -import loadStytch from "./loadStytch"; - -const stytch = loadStytch(); - - -export const invite = async (email: string, organization_id: string) => { - return stytch.magicLinks.email.invite({ - email_address: email, - organization_id: organization_id, - }); -}; - -export const deleteMember = async (member_id: string, organization_id: string) => { - return stytch.organizations.members.delete({ - organization_id, - member_id, - }); -} - diff --git a/apps/client-ts/src/lib/stytch/orgService.ts b/apps/client-ts/src/lib/stytch/orgService.ts deleted file mode 100644 index 967d8d128..000000000 --- a/apps/client-ts/src/lib/stytch/orgService.ts +++ /dev/null @@ -1,49 +0,0 @@ -import loadStytch, { Member, Organization } from "./loadStytch"; - -const stytch = loadStytch(); - -export const findByID = async (organization_id: string): Promise => { - const orgGetPromise = stytch.organizations.get({ organization_id }); - - try { - const orgResult = await orgGetPromise; - const org = orgResult.organization; - console.log("Organization found for id", organization_id); - return org; - } catch (e) { - console.error("Failed to search for org by id", organization_id); - return null; - } -}; - -export const findBySlug = async (slug: string): Promise => { - const orgSearchPromise = stytch.organizations.search({ - query: { - operator: 'AND', - operands: [{ filter_name: "organization_slugs", filter_value: [slug] }], - }, - }); - - try { - const orgResult = await orgSearchPromise; - if (orgResult.organizations.length == 0) { - console.error("Organization not found for slug", slug); - return null; - } - const org = orgResult.organizations[0]; - console.log("Organization found for slug", slug); - return org; - } catch (e) { - console.error("Failed to search for org by slug", e); - return null; - } -}; - -export const findAllMembers = async (organization_id: string): Promise => { - return stytch.organizations.members - .search({ - organization_ids: [organization_id], - }) - .then((res) => res.members); -} - diff --git a/apps/client-ts/src/lib/stytch/sessionService.ts b/apps/client-ts/src/lib/stytch/sessionService.ts deleted file mode 100644 index db0e04115..000000000 --- a/apps/client-ts/src/lib/stytch/sessionService.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import Cookies from "cookies"; -import loadStytch, { Member, SessionsAuthenticateResponse } from "./loadStytch"; -import { NextRequest, NextResponse } from "next/server"; - -export const SESSION_DURATION_MINUTES = 60; -export const INTERMEDIATE_SESSION_DURATION_MINUTES = 10; - -export const SESSION_SYMBOL = Symbol("session"); -const SESSION_COOKIE = "session"; -const INTERMEDIATE_SESSION_COOKIE = "intermediate_session"; - -const stytch = loadStytch(); - -type ExchangeResult = { kind: "discovery" | "login"; token: string; }; - -export async function exchangeToken(req: NextRequest): Promise { - const query = req.nextUrl.searchParams; - if ( - query.get("stytch_token_type") === "multi_tenant_magic_links" && - query.get("token") - ) { - return await handleMagicLinkCallback(req); - } - - if (query.get("stytch_token_type") === "sso" && query.get("token")) { - return await handleSSOCallback(req); - } - - if (query.get("stytch_token_type") === "discovery" && query.get("token")) { - return await handleEmailMagicLinksDiscoveryCallback(req); - } - - if (query.get("stytch_token_type") === "discovery_oauth" && query.get("token")) { - return await handleOAuthDiscoveryCallback(req); - } - - if (query.get("stytch_token_type") === "oauth" && query.get("token")) { - return await handleOAuthCallback(req); - } - - console.log("No token found in req.query", query.get("token")); - throw Error("No token found"); -} - -async function handleMagicLinkCallback( - req: NextRequest -): Promise { - const query = req.nextUrl.searchParams; - const authRes = await stytch.magicLinks.authenticate({ - magic_links_token: query.get("token") as string, - session_duration_minutes: SESSION_DURATION_MINUTES, - }); - - return { - kind: "login", - token: authRes.session_jwt as string, - }; -} - -async function handleSSOCallback(req: NextRequest): Promise { - const query = req.nextUrl.searchParams; - const authRes = await stytch.sso.authenticate({ - sso_token: query.get("token") as string, - session_duration_minutes: SESSION_DURATION_MINUTES, - }); - - return { - kind: "login", - token: authRes.session_jwt as string, - }; -} - -async function handleEmailMagicLinksDiscoveryCallback( - req: NextRequest -): Promise { - const query = req.nextUrl.searchParams; - const authRes = await stytch.magicLinks.discovery.authenticate({ - discovery_magic_links_token: query.get('token') as string, - }); - - return { - kind: "discovery", - token: authRes.intermediate_session_token as string, - }; -} - -async function handleOAuthDiscoveryCallback( - req: NextRequest -): Promise { - const query = req.nextUrl.searchParams; - - const authRes = await stytch.oauth.discovery.authenticate({ - discovery_oauth_token: query.get("token") as string, - }); - - return { - kind: "discovery", - token: authRes.intermediate_session_token as string, - }; -} - -async function handleOAuthCallback( - req: NextRequest -): Promise { - const query = req.nextUrl.searchParams; - - const authRes = await stytch.oauth.authenticate({ - oauth_token: query.get("token") as string, - session_duration_minutes: SESSION_DURATION_MINUTES, - }); - - return { - kind: "login", - token: authRes.session_jwt as string, - }; -} - -export function setSession( - res: NextResponse, - sessionJWT: string -) { - res.cookies.set({ - name: SESSION_COOKIE, - value: sessionJWT, - httpOnly: true, - maxAge: 1000 * 60 * SESSION_DURATION_MINUTES, // minutes to milliseconds - }) -} - -export function clearSession( - res: NextResponse -) { - res.cookies.set({ - name: SESSION_COOKIE, - value: "", - httpOnly: true, - maxAge: 0, // minutes to milliseconds - }) -} - -export function setIntermediateSession( - res: NextResponse, - intermediateSessionToken: string -) { - res.cookies.set({ - name: INTERMEDIATE_SESSION_COOKIE, - value: intermediateSessionToken, - httpOnly: true, - maxAge: 1000 * 60 * INTERMEDIATE_SESSION_DURATION_MINUTES, // minutes to milliseconds - }) -} - -export function clearIntermediateSession( - res: NextResponse -) { - res.cookies.set({ - name: INTERMEDIATE_SESSION_COOKIE, - value: "", - httpOnly: true, - maxAge: 0, // minutes to milliseconds - }) - -} - -type DiscoverySessionData = - | { - sessionJWT: string; - intermediateSession: undefined; - isDiscovery: false; - error: false; - } - | { - sessionJWT: undefined; - intermediateSession: string; - isDiscovery: true; - error: false; - } - | { error: true }; - - export function getDiscoverySessionData( - sessionCookie?: string, - intermediateSessionCookie?: string - ): DiscoverySessionData { - if (sessionCookie) { - return { - sessionJWT: sessionCookie, - intermediateSession: undefined, - isDiscovery: false, - error: false, - }; - } - - if (intermediateSessionCookie) { - return { - sessionJWT: undefined, - intermediateSession: intermediateSessionCookie, - isDiscovery: true, - error: false, - }; - } - return { error: true }; - } - -/** - * useAuth will return the authentication result for the logged-in user. - * It can only be called in functions wrapped with {@link withSession}` - * @param context - */ -export function getAuthData( - header: string | null -): SessionsAuthenticateResponse { - // @ts-ignore - if (!header) { - throw Error("useAuth called in route not protected by withSession"); - } - // @ts-ignore - return JSON.parse(header) as AuthenticateResponse; -} - -export function revokeSession(req: NextRequest, res: NextResponse) { - const sessionJWT = req.cookies.get(SESSION_COOKIE)?.value; - if (!sessionJWT) { - return; - } - // Delete the session cookie by setting maxAge to 0 - res.cookies.set(SESSION_COOKIE, "", { maxAge: 0 }); - // Call Stytch in the background to terminate the session - // But don't block on it! - stytch.sessions - .revoke({ session_jwt: sessionJWT }) - .then(() => { - console.log("Session successfully revoked"); - }) - .catch((err) => { - console.error("Could not revoke session", err); - }); -} diff --git a/apps/client-ts/src/lib/stytch/ssoService.ts b/apps/client-ts/src/lib/stytch/ssoService.ts deleted file mode 100644 index 5904cb9b1..000000000 --- a/apps/client-ts/src/lib/stytch/ssoService.ts +++ /dev/null @@ -1,40 +0,0 @@ -import loadStytch from "./loadStytch"; - -const stytch = loadStytch(); - - -export const list = async (organization_id: string) => { - return stytch.sso.getConnections({ organization_id }); -}; -export const createSaml = async (display_name: string, organization_id: string) => { - return stytch.sso.saml.createConnection({ - organization_id, - display_name, - }); -}; -export const createOidc = async (display_name: string, organization_id: string) => { - return stytch.sso.oidc.createConnection({ - organization_id, - display_name, - }); -}; -// export const delete = async (member_id: string, organization_id: string) => { -// return stytch.organizations.members.delete({ -// organization_id, -// member_id, -// }) -// } -// export const findBySessionToken = async function (sessionToken: string): Promise => { -// return stytch.sessions.authenticate({ -// session_duration_minutes: 30, // extend the session a bit -// session_token: sessionToken -// }) -// .then(res => { -// return res.member -// }) -// .catch(err => { -// console.error('Could not find member by session token', err) -// return null -// }) -// } - diff --git a/apps/client-ts/src/lib/stytch/urlUtils.ts b/apps/client-ts/src/lib/stytch/urlUtils.ts deleted file mode 100644 index 902eb3b8a..000000000 --- a/apps/client-ts/src/lib/stytch/urlUtils.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Use on the frontend (React components) to get domain -export const getDomainFromWindow = () => { - // First, check if this function is being called on the frontend. If so, get domain from window - if (typeof window !== "undefined") { - return window.location.origin; - } - - return null; -}; - -// Use on the backend (API, getServerSideProps) to get the host domain -export const getDomainFromRequest = (host?: string, protocol?: string) => { - const h = host || ""; - const p = protocol ? "https://" : "http://"; - - return p + h; -}; diff --git a/apps/client-ts/src/middleware.ts b/apps/client-ts/src/middleware.ts deleted file mode 100644 index 5493294c4..000000000 --- a/apps/client-ts/src/middleware.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; -import loadStytch, { Member, Organization, loadB2CStytch } from '@/lib/stytch/loadStytch'; -import { clearIntermediateSession, clearSession, exchangeToken, getDiscoverySessionData, revokeSession, setIntermediateSession, setSession } from '@/lib/stytch/sessionService'; -import { MfaRequired } from 'stytch'; -import { toDomain } from '@/lib/utils'; -import CONFIG from "@/lib/config"; - -const OAUTH_TOKEN = 'oauth'; -const MAGIC_LINKS_TOKEN = 'magic_links'; -const RESET_LOGIN = 'login'; - -const stytchClient = loadB2CStytch(); -const stytch = loadStytch(); - -function redirectToSMSMFA(organization: Organization, member: Member, mfa_required: MfaRequired | null ) { - if(mfa_required != null && mfa_required.secondary_auth_initiated == "sms_otp") { - // An OTP code is automatically sent if Stytch knows the member's phone number - return `/${organization.organization_slug}/smsmfa?sent=true&org_id=${organization.organization_id}&member_id=${member.member_id}`; - } - return `/${organization.organization_slug}/smsmfa?sent=false&org_id=${organization.organization_id}&member_id=${member.member_id}`; -} - - -export async function middleware(request: NextRequest) { - if (request.nextUrl.pathname == '/'){ - return NextResponse.redirect(new URL('/connections', request.url)) - } - - if(CONFIG.DISTRIBUTION !== "managed"){ - return NextResponse.next(); - } - - if (request.nextUrl.pathname.startsWith('/api/callback')) { - const searchParams = request.nextUrl.searchParams; - const query = searchParams.get('slug'); - - return exchangeToken(request) - .then((exchangeResult) => { - if (exchangeResult.kind === "login") { - const response = NextResponse.redirect(new URL(`/auth/${query}/dashboard`, request.url)); - setSession(response, exchangeResult.token); // Set session using response cookies. - return response; - } else { - const response = NextResponse.redirect(new URL(`/auth/discovery`, request.url)); - setIntermediateSession(response, exchangeResult.token); // Set intermediate session using response cookies. - return response; - } - }) - .catch((error) => { - return NextResponse.redirect(new URL("/auth/login", request.url)); - }); - } - - if(request.nextUrl.pathname.startsWith('/api/discovery/start')){ - return NextResponse.next(); - } - - if(request.nextUrl.pathname.startsWith('/api/discovery/create')){ - const intermediateSession = request.cookies.get("intermediate_session")?.value; - //console.log("intrm session => "+ intermediateSession); - if (!intermediateSession) { - return NextResponse.redirect(new URL("/auth/discovery", request.url)); - } - const body = await request.text(); - const parts = body.split('='); - const organization_name = parts[1]; - //console.log("organization_name => "+ organization_name) - //const { organization_name, require_mfa } = body; - try { - const { member, organization, session_jwt, intermediate_session_token } = - await stytch.discovery.organizations.create({ - intermediate_session_token: intermediateSession, - email_allowed_domains: [], - organization_slug: organization_name, - organization_name: organization_name, - session_duration_minutes: 60, - mfa_policy: "OPTIONAL" - }); - - // Make the organization discoverable to other emails - try { - await stytch.organizations.update({ - organization_id: organization!.organization_id, - email_jit_provisioning: "RESTRICTED", - sso_jit_provisioning: "ALL_ALLOWED", - email_allowed_domains: [toDomain(member.email_address)], - }); - } catch (e) { - throw e; - } - - // Mark the first user in the organization as the admin - await stytch.organizations.members.update({ - organization_id: organization!.organization_id, - member_id: member.member_id, - trusted_metadata: { admin: true }, - }); - - if(session_jwt === "") { - const response = NextResponse.redirect(new URL(`/auth/${organization!.organization_slug}/smsmfa?sent=false&org_id=${organization!.organization_id}&member_id=${member.member_id}`, request.url)); - setIntermediateSession(response, intermediate_session_token) - clearSession(response) - return response; - } - const response = NextResponse.redirect(new URL(`/auth/${organization!.organization_slug}/dashboard`, request.url)); - clearIntermediateSession(response); - setSession(response, session_jwt); - return response; - } catch (error) { - return NextResponse.redirect(new URL(`/auth/discovery`, request.url)); - } - } - - if(request.nextUrl.pathname.match(/^\/api\/discovery\/([^\/]+)$/)){ - const discoverySessionData = getDiscoverySessionData(request.cookies.get('session')?.value, request.cookies.get('intermediate_session')?.value); - if (discoverySessionData.error) { - console.log("No session tokens found..."); - return { redirect: { statusCode: 307, destination: `/auth/login` } }; - } - const match = request.nextUrl.pathname.match(/^\/api\/discovery\/([^\/]+)$/); - if(!match) return NextResponse.redirect(new URL("/auth/discovery", request.url)); - const orgId = match[1]; - console.log(orgId) - if (!orgId || Array.isArray(orgId)) { - return NextResponse.redirect(new URL("/auth/discovery", request.url)); - } - if(discoverySessionData){ - //console.log(JSON.stringify("data : " + discoverySessionData)) - } - const exchangeSession = async () => { - if (discoverySessionData.isDiscovery) { - return await stytch.discovery.intermediateSessions.exchange({ - intermediate_session_token: discoverySessionData.intermediateSession, - organization_id: orgId, - session_duration_minutes: 60, - }); - } - //console.log('one '+ orgId); - //console.log('two '+ discoverySessionData.sessionJWT); - const res = await stytch.sessions.exchange({ - organization_id: orgId, - session_jwt: discoverySessionData.sessionJWT, - }); - //console.log('res is '+ res); - - return res; - }; - - try { - const { session_jwt, organization, member, intermediate_session_token, mfa_required } = await exchangeSession(); - //console.log(`DATA from exchange session: ${session_jwt} ${organization} ${member} ${intermediate_session_token} ${mfa_required}`) - if(session_jwt === "") { - const responseString = redirectToSMSMFA(organization, member, mfa_required!); - //console.log(`response string: ${responseString}`) - const response = NextResponse.redirect(new URL(responseString, request.url)) - setIntermediateSession(response, intermediate_session_token) - clearSession(response) - return response; - } - const response = NextResponse.redirect(new URL(`/auth/${organization.organization_slug}/dashboard`, request.url)); - setSession(response, session_jwt); - clearIntermediateSession(response); - return response; - } catch (error) { - //console.log("error inside org "+ error) - return NextResponse.redirect(new URL('/auth/discovery', request.url)); - } - } - - if(request.nextUrl.pathname.startsWith('/api/logout')){ - const response = NextResponse.redirect(new URL("/b2c/login", request.url)); - const sessionJWT = request.cookies.get("stytch_session_jwt")?.value; - if (!sessionJWT) { - return; - } - // Delete the session cookie by setting maxAge to 0 - response.cookies.set("stytch_session_jwt", "", { maxAge: 0 }); - // Call Stytch in the background to terminate the session - // But don't block on it! - stytchClient.sessions - .revoke({ session_jwt: sessionJWT }) - .then(() => { - console.log("Session successfully revoked"); - }) - .catch((err: any) => { - console.error("Could not revoke session", err); - }); - return response; - } - - const sessionJWT = request.cookies.get("stytch_session_jwt")?.value; - //console.log(sessionJWT) - if (!sessionJWT) { - return NextResponse.redirect(new URL("/b2c/login", request.url)); - } - - // loadStytch() is a helper function for initalizing the Stytch Backend SDK. See the function definition for more details. - - try { - // Authenticate the session JWT. If an error is thrown the session authentication has failed. - await stytchClient.sessions.authenticateJwt({session_jwt: sessionJWT}); - return NextResponse.next(); - } catch (e) { - return NextResponse.redirect(new URL("/b2c/login", request.url)); - } - - /*const sessionJWT = request.cookies.get("session")?.value; - - if (!sessionJWT) { - return NextResponse.redirect(new URL("/auth/login", request.url)); - } - - try { - let sessionAuthRes; - try { - sessionAuthRes = await stytch.sessions.authenticate({ - session_duration_minutes: 30, // extend the session a bit - session_jwt: sessionJWT, - }); - } catch (err) { - console.error("Could not find member by session token", err); - return NextResponse.redirect(new URL("/auth/login", request.url)); - } - //console.log(sessionAuthRes); - - let response; - if(request.nextUrl.pathname == '/profile'){ - response = NextResponse.redirect(new URL(`/auth/${sessionAuthRes.organization.organization_slug}/dashboard`, request.url)); - }else{ - response = NextResponse.next(); - } - // Stytch issues a new JWT on every authenticate call - store it on the UA for faster validation next time - setSession(response, sessionAuthRes.session_jwt); - - const isAdmin = sessionAuthRes.member.trusted_metadata!.admin as boolean; - if (!isAdmin) { - return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403 }); - } - - response.headers.set('x-member-org', sessionAuthRes.member.organization_id); - - return response; - } catch (err) { - return new Response(JSON.stringify({ error: "Session invalid" }), { status: 401 }); - } */ -} - -export const config = { - matcher: [ - '/', - '/profile', - '/api-keys', - '/connections', - '/configuration', - '/events', - '/auth/[slug]/dashboard/:path*', - '/api/callback', - '/api/discovery/:path*', - '/api/logout', - '/api/delete_member', - '/api/invite', - '/api/sso/:path*' - ], -} \ No newline at end of file diff --git a/apps/client-ts/src/state/profileStore.ts b/apps/client-ts/src/state/profileStore.ts index 64ea05259..91f7a83de 100644 --- a/apps/client-ts/src/state/profileStore.ts +++ b/apps/client-ts/src/state/profileStore.ts @@ -1,8 +1,6 @@ import { create } from 'zustand'; -import { projects as Project } from 'api'; -type User_ = User & { projects: Project[] }; -type User = { +type User_ = { id_user: string; email: string; first_name: string; @@ -11,14 +9,14 @@ type User = { } interface ProfileState { - profile: User_ | null; - setProfile: (profile: User_) => void; + profile: User_ | null; + setProfile: (profile: User_ | null) => void; } const useProfileStore = create()((set) => ({ profile: null, - setProfile: (profile_: User_) => set({ profile: profile_ }), + setProfile: (profile_: User_ | null) => set({ profile: profile_ }), })); export default useProfileStore; diff --git a/apps/client-ts/src/state/projectStore.ts b/apps/client-ts/src/state/projectStore.ts index 5c5e6e736..80fcd8dc9 100644 --- a/apps/client-ts/src/state/projectStore.ts +++ b/apps/client-ts/src/state/projectStore.ts @@ -6,7 +6,7 @@ interface ProjectState { } const useProjectStore = create()((set) => ({ - idProject: "123", + idProject: "", setIdProject: (id) => set({ idProject: id }), })); diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index adde91bb6..89df95576 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -66,11 +66,10 @@ model tcg_users { model users { id_user String @id(map: "pk_users") @db.Uuid identification_strategy String - email String? + email String? @unique(map: "unique_email") password_hash String? first_name String last_name String - id_stytch String? @unique(map: "force_stytch_id_unique") created_at DateTime @default(now()) @db.Timestamp(6) modified_at DateTime @default(now()) @db.Timestamp(6) api_keys api_keys[] diff --git a/packages/api/scripts/init.sql b/packages/api/scripts/init.sql index e63d06fb3..9cb143fc2 100644 --- a/packages/api/scripts/init.sql +++ b/packages/api/scripts/init.sql @@ -74,11 +74,10 @@ CREATE TABLE users password_hash text NULL, first_name text NOT NULL, last_name text NOT NULL, - id_stytch text NULL, created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL DEFAULT NOW(), CONSTRAINT PK_users PRIMARY KEY ( id_user ), - CONSTRAINT force_stytch_id_unique UNIQUE ( id_stytch ) + CONSTRAINT unique_email UNIQUE ( email ) ); @@ -90,7 +89,6 @@ STYTCH_B2B STYTCH_B2C'; COMMENT ON COLUMN users.created_at IS 'DEFAULT NOW() to automatically insert a value if nothing supplied'; -COMMENT ON CONSTRAINT force_stytch_id_unique ON users IS 'force unique on stytch id'; diff --git a/packages/api/scripts/seed.sql b/packages/api/scripts/seed.sql index 6c986975c..8d2cc1472 100644 --- a/packages/api/scripts/seed.sql +++ b/packages/api/scripts/seed.sql @@ -2,7 +2,7 @@ -- ('55222419-795d-4183-8478-361626363e58', 'Acme Inc', 'cust_stripe_acme_56604f75-7bf8-4541-9ab4-5928aade4bb8' ); INSERT INTO users (id_user, identification_strategy, email, password_hash, first_name, last_name) VALUES -('0ce39030-2901-4c56-8db0-5e326182ec6b', 'b2c','audrey@aubry.io', '$2b$10$Nxcp3x0yDaCrMrhZQ6IiNeqk0BxxDTnfn9iGG2UK5nWMh/UB6LgZu', 'Audrey', 'Aubry'); +('0ce39030-2901-4c56-8db0-5e326182ec6b', 'b2c','local@panora.dev', '$2b$10$Y7Q8TWGyGuc5ecdIASbBsuXMo3q/Rs3/cnY.mLZP4tUgfGUOCUBlG', 'local', 'Panora'); INSERT INTO projects (id_project, name, sync_mode, id_user) VALUES diff --git a/packages/api/src/@core/auth/auth.controller.ts b/packages/api/src/@core/auth/auth.controller.ts index f2dd303f4..6a14ec74f 100644 --- a/packages/api/src/@core/auth/auth.controller.ts +++ b/packages/api/src/@core/auth/auth.controller.ts @@ -6,6 +6,7 @@ import { UseGuards, Query, Res, + Request, Param, } from '@nestjs/common'; import { Response } from 'express'; @@ -59,15 +60,35 @@ export class AuthController { return this.authService.getUsers(); } - @ApiOperation({ - operationId: 'getUser', - summary: 'Get a specific user by ID', - }) - @ApiResponse({ status: 200, description: 'Returns the user data.' }) - @ApiResponse({ status: 404, description: 'User not found.' }) - @Get('users/:stytchId') - async getUser(@Param('stytchId') stytchId: string) { - return this.authService.getUserByStytchId(stytchId); + // @ApiOperation({ + // operationId: 'getUser', + // summary: 'Get a specific user by ID', + // }) + // @ApiResponse({ status: 200, description: 'Returns the user data.' }) + // @ApiResponse({ status: 404, description: 'User not found.' }) + // @Get('users/:stytchId') + // async getUser(@Param('stytchId') stytchId: string) { + // return this.authService.getUserByStytchId(stytchId); + // } + + // @ApiOperation({ operationId: 'generateApiKey', summary: 'Create API Key' }) + // @ApiBody({ type: ApiKeyDto }) + // @ApiResponse({ status: 201 }) + // @UseGuards(JwtAuthGuard) + // @Get('users/currentUser') + // async getCurrentUser(@Body() data: ApiKeyDto): Promise<{ api_key: string }> { + // return this.authService.generateApiKeyForUser( + // data.userId, + // data.projectId, + // data.keyName, + // ); + // } + + @ApiResponse({ status: 201 }) + @UseGuards(JwtAuthGuard) + @Get('profile') + async getProfile(@Request() req) { + return this.authService.verifyUser(req.user); } @ApiOperation({ operationId: 'getApiKeys', summary: 'Retrieve API Keys' }) diff --git a/packages/api/src/@core/auth/auth.service.ts b/packages/api/src/@core/auth/auth.service.ts index d2a974d1e..e08a4befd 100644 --- a/packages/api/src/@core/auth/auth.service.ts +++ b/packages/api/src/@core/auth/auth.service.ts @@ -12,7 +12,8 @@ import { v4 as uuidv4 } from 'uuid'; import { LoggerService } from '@@core/logger/logger.service'; import { handleServiceError } from '@@core/utils/errors'; import { LoginDto } from './dto/login.dto'; -import { users as User } from '@prisma/client'; +import { VerifyUserDto } from './dto/verify-user.dto'; + //TODO: Ensure the JWT is used for user session authentication and that it's short-lived. @Injectable() @@ -32,23 +33,29 @@ export class AuthService { handleServiceError(error, this.logger); } } - async getUserByStytchId(stytchId: string) { + async verifyUser(verifyUser: VerifyUserDto) { try { const user = await this.prisma.users.findUnique({ where: { - id_stytch: stytchId, - identification_strategy: 'b2c', - }, - }); - const projects = await this.prisma.projects.findMany({ - where: { - id_user: user.id_user, + id_user: verifyUser.id_user, }, }); - return { - ...user, - projects: projects, - }; + + if (!user) { + throw new UnauthorizedException('user does not exist!'); + } + + return verifyUser; + + // const projects = await this.prisma.projects.findMany({ + // where: { + // id_user: user.id_user, + // }, + // }); + // return { + // ...user, + // projects: projects, + // }; } catch (error) { handleServiceError(error, this.logger); } @@ -64,19 +71,19 @@ export class AuthService { async register(user: CreateUserDto) { try { - /*const foundUser = await this.prisma.users.findFirst({ + + + const foundUser = await this.prisma.users.findFirst({ where: { email: user.email }, }); if (foundUser) { - throw new BadRequestException('email already exists'); + throw new BadRequestException('Email is already exists!!'); } - const savedUser = await this.createUser(user); + return await this.createUser(user); + - const { password_hash, ...resp_user } = savedUser; - return resp_user;*/ - return; } catch (error) { handleServiceError(error, this.logger); } @@ -84,83 +91,86 @@ export class AuthService { async createUser(user: CreateUserDto, id_user?: string) { try { - /*const salt = await bcrypt.genSalt(); - const hashedPassword = await bcrypt.hash(user.password_hash, salt); - + const hashedPassword = await bcrypt.hash(user.password_hash, 10); return await this.prisma.users.create({ data: { - ...user, + // ...user, id_user: id_user || uuidv4(), password_hash: hashedPassword, - }, - });*/ - return await this.prisma.users.upsert({ - where: { - id_stytch: user.stytch_id_user, - }, - update: { - identification_strategy: 'b2c', - first_name: user.first_name, - last_name: user.last_name, - email: user.email, - password_hash: '', - created_at: new Date(), - id_user: id_user || uuidv4(), - }, - create: { - id_stytch: user.stytch_id_user, identification_strategy: 'b2c', first_name: user.first_name, last_name: user.last_name, email: user.email, - password_hash: '', created_at: new Date(), - id_user: id_user || uuidv4(), }, }); + // return await this.prisma.users.upsert({ + // where: { + // email: user.email, + // }, + // update: { + // identification_strategy: 'b2c', + // first_name: user.first_name, + // last_name: user.last_name, + // email: user.email, + // password_hash: '', + // created_at: new Date(), + // id_user: id_user || uuidv4(), + // }, + // create: { + // identification_strategy: 'b2c', + // first_name: user.first_name, + // last_name: user.last_name, + // email: user.email, + // password_hash: '', + // created_at: new Date(), + // id_user: id_user || uuidv4(), + // }, + // }); } catch (error) { + console.log(error) handleServiceError(error, this.logger); } } - //TODO async login(user: LoginDto) { try { - let foundUser: User; - - if (user.id_user) { - foundUser = await this.prisma.users.findUnique({ - where: { id_user: user.id_user }, - }); - } - - if (!foundUser && user.email) { - foundUser = await this.prisma.users.findFirst({ - where: { email: user.email }, - }); - } + const foundUser = await this.prisma.users.findUnique({ + where: { + email: user.email + } + }); if (!foundUser) { - throw new UnauthorizedException('user not found inside login function'); + throw new UnauthorizedException('user does not exist!'); } - //TODO: - /*const isEq = await bcrypt.compare( + const isEq = await bcrypt.compare( user.password_hash, foundUser.password_hash, ); if (!isEq) throw new UnauthorizedException('Invalid credentials.'); - */ - const { password_hash, ...userData } = foundUser; + + + const { ...userData } = foundUser; const payload = { email: userData.email, sub: userData.id_user, + first_name: userData.first_name, + last_name: userData.last_name }; + + return { - user: userData, + user: { + id_user: foundUser.id_user, + email: foundUser.email, + first_name: foundUser.first_name, + last_name: foundUser.last_name + }, access_token: this.jwtService.sign(payload, { secret: process.env.JWT_SECRET, }), // token used to generate api keys diff --git a/packages/api/src/@core/auth/dto/create-user.dto.ts b/packages/api/src/@core/auth/dto/create-user.dto.ts index 8acc2a15f..3f4bdca76 100644 --- a/packages/api/src/@core/auth/dto/create-user.dto.ts +++ b/packages/api/src/@core/auth/dto/create-user.dto.ts @@ -8,8 +8,6 @@ export class CreateUserDto { @ApiProperty() email: string; @ApiProperty() - stytch_id_user: string; - @ApiProperty() strategy: string; @ApiPropertyOptional() password_hash: string; diff --git a/packages/api/src/@core/auth/dto/login.dto.ts b/packages/api/src/@core/auth/dto/login.dto.ts index 01f5d4166..51f135074 100644 --- a/packages/api/src/@core/auth/dto/login.dto.ts +++ b/packages/api/src/@core/auth/dto/login.dto.ts @@ -4,7 +4,7 @@ export class LoginDto { @ApiProperty() id_user?: string; @ApiProperty() - email?: string; + email: string; @ApiProperty() password_hash: string; } diff --git a/packages/api/src/@core/auth/dto/verify-user.dto.ts b/packages/api/src/@core/auth/dto/verify-user.dto.ts new file mode 100644 index 000000000..a336549a5 --- /dev/null +++ b/packages/api/src/@core/auth/dto/verify-user.dto.ts @@ -0,0 +1,7 @@ + +export class VerifyUserDto { + id_user: string; + email: string; + first_name: string; + last_name: string; +} diff --git a/packages/api/src/@core/auth/guards/jwt-auth.guard.ts b/packages/api/src/@core/auth/guards/jwt-auth.guard.ts index 2155290ed..6844da61b 100644 --- a/packages/api/src/@core/auth/guards/jwt-auth.guard.ts +++ b/packages/api/src/@core/auth/guards/jwt-auth.guard.ts @@ -1,5 +1,22 @@ -import { Injectable } from '@nestjs/common'; +import { + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { Observable } from 'rxjs'; @Injectable() -export class JwtAuthGuard extends AuthGuard('jwt') {} +export class JwtAuthGuard extends AuthGuard('jwt') { + + canActivate(context: ExecutionContext): boolean | Promise | Observable { + return super.canActivate(context); + } + + handleRequest(err: any, user: any, _info: any, _context: ExecutionContext, _status?: any): TUser { + if (err || !user) { + throw err || new UnauthorizedException(); + } + return user; + } +} \ No newline at end of file diff --git a/packages/api/src/@core/auth/strategies/jwt.strategy.ts b/packages/api/src/@core/auth/strategies/jwt.strategy.ts index b9ac9971e..76b6abf75 100644 --- a/packages/api/src/@core/auth/strategies/jwt.strategy.ts +++ b/packages/api/src/@core/auth/strategies/jwt.strategy.ts @@ -15,6 +15,6 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { } async validate(payload: any) { - return { userId: payload.sub, email: payload.email }; + return { id_user: payload.sub, email: payload.email, first_name: payload.first_name, last_name: payload.last_name }; } } diff --git a/packages/api/src/@core/connections-strategies/connections-strategies.service.ts b/packages/api/src/@core/connections-strategies/connections-strategies.service.ts index baae66bd6..842be80ca 100644 --- a/packages/api/src/@core/connections-strategies/connections-strategies.service.ts +++ b/packages/api/src/@core/connections-strategies/connections-strategies.service.ts @@ -28,7 +28,7 @@ export class ConnectionsStrategiesService { constructor( private prisma: PrismaService, private configService: ConfigService, - ) {} + ) { } async isCustomCredentials(projectId: string, type: string) { const res = await this.prisma.connection_strategies.findFirst({ diff --git a/packages/api/src/@core/connections/connections.controller.ts b/packages/api/src/@core/connections/connections.controller.ts index 2bbdb4053..cbe8f3088 100644 --- a/packages/api/src/@core/connections/connections.controller.ts +++ b/packages/api/src/@core/connections/connections.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Query, Res } from '@nestjs/common'; +import { Controller, Get, Query, Res, Param } from '@nestjs/common'; import { Response } from 'express'; import { CrmConnectionsService } from './crm/services/crm.connection.service'; import { LoggerService } from '@@core/logger/logger.service'; @@ -139,4 +139,20 @@ export class ConnectionsController { async getConnections() { return await this.prisma.connections.findMany(); } + + // @ApiOperation({ + // operationId: 'getConnectionsByUser', + // summary: 'Retrieve connections by user', + // }) + // @ApiResponse({ status: 200 }) + // @Get(':userId') + // getProjectsByUser(@Param('userId') userId: string) { + // return this.prisma.connections.findMany( + // { + // where: { + // id + // } + // } + // ); + // } } diff --git a/packages/api/src/@core/projects/dto/create-project.dto.ts b/packages/api/src/@core/projects/dto/create-project.dto.ts index 20f9f4614..9f737a3db 100644 --- a/packages/api/src/@core/projects/dto/create-project.dto.ts +++ b/packages/api/src/@core/projects/dto/create-project.dto.ts @@ -4,7 +4,7 @@ export class CreateProjectDto { @ApiProperty() name: string; @ApiProperty() - id_organization: string; + id_organization?: string; @ApiProperty() id_user: string; } diff --git a/packages/api/src/@core/projects/projects.service.ts b/packages/api/src/@core/projects/projects.service.ts index ce279ebbe..86900fe23 100644 --- a/packages/api/src/@core/projects/projects.service.ts +++ b/packages/api/src/@core/projects/projects.service.ts @@ -33,10 +33,10 @@ export class ProjectsService { async createProject(data: CreateProjectDto) { try { - const { id_organization, ...rest } = data; + // const { id_organization, ...rest } = data; const res = await this.prisma.projects.create({ data: { - ...rest, + name: data.name, sync_mode: 'pool', id_project: uuidv4(), id_user: data.id_user, diff --git a/packages/api/swagger/swagger-spec.json b/packages/api/swagger/swagger-spec.json index 5d7d3ebbd..b1874aed1 100644 --- a/packages/api/swagger/swagger-spec.json +++ b/packages/api/swagger/swagger-spec.json @@ -148,26 +148,23 @@ ] } }, - "/auth/users/{stytchId}": { + "/auth/profile": { "get": { - "operationId": "getUser", - "summary": "Get a specific user by ID", - "parameters": [ - { - "name": "stytchId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], + "operationId": "AuthController_getProfile", + "parameters": [], "responses": { "200": { - "description": "Returns the user data." + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyUserDto" + } + } + } }, - "404": { - "description": "User not found." + "201": { + "description": "" } }, "tags": [ @@ -4496,9 +4493,6 @@ "email": { "type": "string" }, - "stytch_id_user": { - "type": "string" - }, "strategy": { "type": "string" }, @@ -4513,7 +4507,6 @@ "first_name", "last_name", "email", - "stytch_id_user", "strategy" ] }, @@ -4531,9 +4524,33 @@ } }, "required": [ + "email", "password_hash" ] }, + "VerifyUserDto": { + "type": "object", + "properties": { + "id_user": { + "type": "string" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + } + }, + "required": [ + "id_user", + "email", + "first_name", + "last_name" + ] + }, "ApiKeyDto": { "type": "object", "properties": { @@ -4627,7 +4644,6 @@ }, "required": [ "name", - "id_organization", "id_user" ] }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c68e13426..013adf21a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,12 +90,6 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) - '@stytch/nextjs': - specifier: ^18.0.0 - version: 18.0.0(@stytch/vanilla-js@4.7.7)(react-dom@18.2.0)(react@18.2.0) - '@stytch/vanilla-js': - specifier: ^4.7.1 - version: 4.7.7 '@tanstack/react-query': specifier: ^5.12.2 version: 5.29.0(react@18.2.0) @@ -129,6 +123,9 @@ importers: date-fns: specifier: ^3.3.1 version: 3.6.0 + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 lucide-react: specifier: ^0.344.0 version: 0.344.0(react@18.2.0) @@ -159,9 +156,6 @@ importers: sonner: specifier: ^1.4.3 version: 1.4.41(react-dom@18.2.0)(react@18.2.0) - stytch: - specifier: ^10.5.0 - version: 10.13.0 tailwind-merge: specifier: ^2.2.1 version: 2.2.2 @@ -178,6 +172,9 @@ importers: '@types/cookies': specifier: ^0.9.0 version: 0.9.0 + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 '@types/node': specifier: ^20 version: 20.12.7 @@ -4323,31 +4320,6 @@ packages: '@sinonjs/commons': 3.0.1 dev: true - /@stytch/core@2.11.0: - resolution: {integrity: sha512-bGsWPySgqJVvxxCN7dmOImGuuZiKefsAE/7V/ap0RTN/YwUD1Gp1vQCdWaARFfYQDfM5xHJWtGP5oGvVQYIEwQ==} - dependencies: - uuid: 8.3.2 - dev: false - - /@stytch/nextjs@18.0.0(@stytch/vanilla-js@4.7.7)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-8maWCqL/zRe73WSSX0x4v51AP6lUBw2p9XHrDce4g4HwjyCOOdjdnBu3c9AgxQJuHfuBC1xldtp5+eft9SdCyw==} - peerDependencies: - '@stytch/vanilla-js': ^4.7.0 - react: '>= 17.0.2' - react-dom: '>= 17.0.2' - dependencies: - '@stytch/vanilla-js': 4.7.7 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /@stytch/vanilla-js@4.7.7: - resolution: {integrity: sha512-/BGN8wzaetYH1EweluNn3/d0MqceA7ff6mB/wPTk5qbuOdMAlLT6egVp/2y4SqSmIfBQg8rGsHI8SWeghMv0oA==} - dependencies: - '@stytch/core': 2.11.0 - '@types/google-one-tap': 1.2.6 - dev: false - /@swc/helpers@0.5.2: resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} dependencies: @@ -4577,10 +4549,6 @@ packages: '@types/serve-static': 1.15.7 dev: true - /@types/google-one-tap@1.2.6: - resolution: {integrity: sha512-REmJsXVHvKb/sgI8DF+7IesMbDbcsEokHBqxU01ENZ8d98UPWdRLhUCtxEm9bhNFFg6PJGy7PNFdvovp0hK3jA==} - dev: false - /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: @@ -4618,6 +4586,10 @@ packages: pretty-format: 29.7.0 dev: true + /@types/js-cookie@3.0.6: + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + dev: true + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true @@ -9320,6 +9292,11 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + /js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + dev: false + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}