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);
},