diff --git a/functions/.gitignore b/functions/.gitignore index 9be0f014..069d927c 100644 --- a/functions/.gitignore +++ b/functions/.gitignore @@ -1,4 +1,5 @@ # Compiled JavaScript files +lib lib/**/*.js lib/**/*.js.map diff --git a/functions/package.json b/functions/package.json index 77fe4025..d685fe70 100644 --- a/functions/package.json +++ b/functions/package.json @@ -12,7 +12,7 @@ "lint:fix": "npm run lint --fix", "logs": "firebase functions:log", "prettier:fix": "prettier --write .", - "serve": "npm run build && firebase emulators:start --only functions", + "serve": "npm run build && firebase emulators:start --only auth,functions,firestore", "shell": "npm run build && firebase functions:shell" }, "name": "functions", diff --git a/functions/src/triggers/users.ts b/functions/src/triggers/users.ts index c3401e23..5d65e7a0 100644 --- a/functions/src/triggers/users.ts +++ b/functions/src/triggers/users.ts @@ -52,7 +52,7 @@ export const tanamNewUserInit = onDocumentCreated("tanam-users/{docId}", async ( // Function to enforce role management on document update // This function will apply changes to custom claims when the role field is updated -export const onRoleChange = onDocumentUpdated("tanam-users/{docId}", async (event) => { +export const onTanamUserRoleChange = onDocumentUpdated("tanam-users/{docId}", async (event) => { const uid = event.params.docId; const beforeData = event?.data?.before.data(); const afterData = event?.data?.after.data(); diff --git a/hosting/src/app/(protected)/error/insufficient-role/page.tsx b/hosting/src/app/(protected)/error/insufficient-role/page.tsx new file mode 100644 index 00000000..9f68ba91 --- /dev/null +++ b/hosting/src/app/(protected)/error/insufficient-role/page.tsx @@ -0,0 +1,25 @@ +"use client"; +import Loader from "@/components/common/Loader"; +import Notification from "@/components/common/Notification"; +import PageHeader from "@/components/common/PageHeader"; +import {useAuthentication} from "@/hooks/useAuthentication"; +import {useTanamUser} from "@/hooks/useTanamUser"; +import {Suspense} from "react"; + +export default function ErrorInsufficientRolePage() { + const {authUser} = useAuthentication(); + const {error: userError} = useTanamUser(authUser?.uid); + + return ( + <> + }> + + + + + ); +} diff --git a/hosting/src/app/(protected)/error/page.tsx b/hosting/src/app/(protected)/error/page.tsx new file mode 100644 index 00000000..ba7ce983 --- /dev/null +++ b/hosting/src/app/(protected)/error/page.tsx @@ -0,0 +1,16 @@ +"use client"; +import Loader from "@/components/common/Loader"; +import Notification from "@/components/common/Notification"; +import PageHeader from "@/components/common/PageHeader"; +import {Suspense} from "react"; + +export default function ErrorPage() { + return ( + <> + }> + + + + + ); +} diff --git a/hosting/src/app/(protected)/layout.tsx b/hosting/src/app/(protected)/layout.tsx index 56371805..96eee477 100644 --- a/hosting/src/app/(protected)/layout.tsx +++ b/hosting/src/app/(protected)/layout.tsx @@ -1,9 +1,8 @@ "use client"; - import CmsLayout from "@/components/Layouts/CmsLayout"; -import React from "react"; import {useAuthentication} from "@/hooks/useAuthentication"; import {redirect} from "next/navigation"; +import React from "react"; interface ProtectedLayoutProps { children: React.ReactNode; diff --git a/hosting/src/components/Header/index.tsx b/hosting/src/components/Header/index.tsx index 750a625d..7e1ab75d 100644 --- a/hosting/src/components/Header/index.tsx +++ b/hosting/src/components/Header/index.tsx @@ -1,18 +1,13 @@ import DarkModeSwitcher from "@/components/Header/DarkModeSwitcher"; import DropdownUser from "@/components/Header/DropdownUser"; import {useAuthentication} from "@/hooks/useAuthentication"; +import {useTanamUser} from "@/hooks/useTanamUser"; import Image from "next/image"; import Link from "next/link"; -import {useEffect} from "react"; -import {useTanamUser} from "../../hooks/useTanamUser"; const Header = (props: {sidebarOpen: string | boolean | undefined; setSidebarOpen: (arg0: boolean) => void}) => { const {authUser} = useAuthentication(); - const {data: tanamUser, error: userError} = useTanamUser(authUser?.uid); - - useEffect(() => { - console.log("userError", userError); - }, [userError]); + const {data: tanamUser} = useTanamUser(authUser?.uid); return (
diff --git a/hosting/src/hooks/useAuthentication.tsx b/hosting/src/hooks/useAuthentication.tsx index 063740bc..f5c47a82 100644 --- a/hosting/src/hooks/useAuthentication.tsx +++ b/hosting/src/hooks/useAuthentication.tsx @@ -1,11 +1,16 @@ "use client"; import {firebaseAuth} from "@/plugins/firebase"; +import {TanamRole} from "@functions/models/TanamUser"; import {User} from "firebase/auth"; +import {redirect, usePathname} from "next/navigation"; import {useEffect, useState} from "react"; export function useAuthentication() { + const pathname = usePathname(); + const [error, setError] = useState(null); const [authUser, setUser] = useState(null); + const [userRole, setUserRole] = useState(null); const [isSignedIn, setIsSignedIn] = useState(null); useEffect(() => { @@ -13,11 +18,27 @@ export function useAuthentication() { console.log("[onAuthStateChanged]", {user}); setUser(user); setIsSignedIn(!!user); + fetchUserRole(); }); return () => unsubscribe(); }, []); + async function fetchUserRole() { + try { + const idTokenResult = await firebaseAuth.currentUser?.getIdTokenResult(); + + setUserRole((idTokenResult?.claims as {tanamRole: TanamRole}).tanamRole); + + // Redirect when user doesnt have claims + if (pathname !== "/error/insufficient-role" && (userRole === null || !userRole)) { + redirect("/error/insufficient-role"); + } + } catch (error) { + setError(error as Error); + } + } + async function signout() { console.log("[signout]"); try { @@ -30,6 +51,7 @@ export function useAuthentication() { return { isSignedIn, authUser, + userRole, error, signout, setError, diff --git a/hosting/src/hooks/useFirebaseUi.tsx b/hosting/src/hooks/useFirebaseUi.tsx index 06446557..dab985bf 100644 --- a/hosting/src/hooks/useFirebaseUi.tsx +++ b/hosting/src/hooks/useFirebaseUi.tsx @@ -1,7 +1,7 @@ "use client"; import {firebaseAuth} from "@/plugins/firebase"; +import {AuthCredential, EmailAuthProvider, GoogleAuthProvider} from "firebase/auth"; import {auth as firebaseAuthUi} from "firebaseui"; -import {AuthCredential, GoogleAuthProvider} from "firebase/auth"; import "firebaseui/dist/firebaseui.css"; import {useEffect, useState} from "react"; @@ -31,6 +31,10 @@ export function useFirebaseUi() { tosUrl: "https://github.com/oddbit/tanam/blob/main/docs/tos.md", privacyPolicyUrl: "https://github.com/oddbit/tanam/blob/main/docs/privacy-policy.md", signInOptions: [ + { + provider: EmailAuthProvider.PROVIDER_ID, + fullLabel: isSignUp ? "Sign up with email" : "Sign in with email", + }, { provider: GoogleAuthProvider.PROVIDER_ID, fullLabel: isSignUp ? "Sign up with Google" : "Sign in with Google", diff --git a/hosting/src/hooks/useTanamUser.tsx b/hosting/src/hooks/useTanamUser.tsx index 4ac40bd1..82fc68a0 100644 --- a/hosting/src/hooks/useTanamUser.tsx +++ b/hosting/src/hooks/useTanamUser.tsx @@ -29,6 +29,10 @@ export function useTanamUser(uid?: string): UseTanamDocumentsResult { const unsubscribe = onSnapshot( docRef, (snapshot) => { + if (!snapshot.exists()) { + setError(new UserNotification("error", "Access Denied", "Sorry you cant access the page")); + } + const tanamUser = TanamUserClient.fromFirestore(snapshot); setData(tanamUser); },