diff --git a/backend/api/user.py b/backend/api/user.py index 4d3b523..b7ec372 100644 --- a/backend/api/user.py +++ b/backend/api/user.py @@ -15,7 +15,7 @@ openapi_tags = { # TODO: Add security using HTTP Bearer Tokens # TODO: Enable authorization by passing user uuid to API # TODO: Create custom exceptions -@api.get("all", response_model=List[User], tags=["Users"]) +@api.get("/all", response_model=List[User], tags=["Users"]) def get_all(user_id: str, user_svc: UserService = Depends()): subject = user_svc.get_user_by_uuid(user_id) diff --git a/compass/app/admin/layout.tsx b/compass/app/admin/layout.tsx index e9b670d..68d4d55 100644 --- a/compass/app/admin/layout.tsx +++ b/compass/app/admin/layout.tsx @@ -3,6 +3,11 @@ import Sidebar from "@/components/resource/Sidebar"; import React, { useState } from "react"; import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline"; +import { createClient } from "@/utils/supabase/client"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; +import User, { Role } from "@/utils/models/User"; +import Loading from "@/components/auth/Loading"; export default function RootLayout({ children, @@ -10,33 +15,86 @@ export default function RootLayout({ children: React.ReactNode; }) { const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const router = useRouter(); + const [user, setUser] = useState(); + + useEffect(() => { + async function getUser() { + const supabase = createClient(); + + const { data, error } = await supabase.auth.getUser(); + + console.log(data, error); + + if (error) { + console.log("Accessed admin page but not logged in"); + router.push("auth/login"); + return; + } + + const userData = await fetch( + `${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}` + ); + + const user: User = await userData.json(); + + if (user.role !== Role.ADMIN) { + console.log( + `Accessed admin page but incorrect permissions: ${user.username} ${user.role}` + ); + router.push("auth/login"); + return; + } + + setUser(user); + } + + getUser(); + }, [router]); return (
- {/* button to open sidebar */} - - {/* sidebar */} -
- -
- {/* page ui */} -
- {children} -
+ {user ? ( +
+ {/* button to open sidebar */} + + {/* sidebar */} +
+ +
+ {/* page ui */} +
+ {children} +
+
+ ) : ( + + )}
); } diff --git a/compass/app/admin/page.tsx b/compass/app/admin/page.tsx index 0b6e467..fede31d 100644 --- a/compass/app/admin/page.tsx +++ b/compass/app/admin/page.tsx @@ -2,15 +2,43 @@ import { PageLayout } from "@/components/PageLayout"; import { Table } from "@/components/Table/Index"; +import User from "@/utils/models/User"; +import { createClient } from "@/utils/supabase/client"; import { UsersIcon } from "@heroicons/react/24/solid"; +import { useEffect, useState } from "react"; export default function Page() { + const [users, setUsers] = useState([]); + + useEffect(() => { + async function getUser() { + const supabase = createClient(); + + const { data, error } = await supabase.auth.getUser(); + + if (error) { + console.log("Accessed admin page but not logged in"); + return; + } + + const userListData = await fetch( + `${process.env.NEXT_PUBLIC_HOST}/api/user/all?uuid=${data.user.id}` + ); + + const users: User[] = await userListData.json(); + + setUsers(users); + } + + getUser(); + }, []); + return (
{/* icon + title */} }> - +
); diff --git a/compass/app/api/user/all/route.ts b/compass/app/api/user/all/route.ts new file mode 100644 index 0000000..b8d1518 --- /dev/null +++ b/compass/app/api/user/all/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; + +export async function GET(request: Request) { + const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user/all`; + + console.log(apiEndpoint); + + const { searchParams } = new URL(request.url); + const uuid = searchParams.get("uuid"); + + const data = await fetch(`${apiEndpoint}?user_id=${uuid}`); + + return NextResponse.json(await data.json(), { status: data.status }); +} diff --git a/compass/app/auth/login/page.tsx b/compass/app/auth/login/page.tsx index 8033b62..e196e04 100644 --- a/compass/app/auth/login/page.tsx +++ b/compass/app/auth/login/page.tsx @@ -1,6 +1,5 @@ // pages/index.tsx "use client"; - import Button from "@/components/Button"; import Input from "@/components/Input"; import InlineLink from "@/components/InlineLink"; @@ -14,30 +13,26 @@ import { useRouter } from "next/navigation"; export default function Page() { const router = useRouter(); - - useEffect(() => { - const supabase = createClient(); - - async function checkUser() { - const { data } = await supabase.auth.getUser(); - - if (data.user) { - router.push("/resource"); - } - } - - checkUser(); - }, [router]); - const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [emailError, setEmailError] = useState(""); const [passwordError, setPasswordError] = useState(""); const [loginError, setLoginError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const supabase = createClient(); + async function checkUser() { + const { data } = await supabase.auth.getUser(); + if (data.user) { + router.push("/resource"); + } + } + checkUser(); + }, [router]); const handleEmailChange = (event: React.ChangeEvent) => { setEmail(event.currentTarget.value); - setEmail; }; const handlePasswordChange = ( @@ -51,28 +46,28 @@ export default function Page() { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (email.trim().length === 0) { - console.log(email); setEmailError("Please enter your email."); return; } - if (!emailRegex.test(email)) { setEmailError("Please enter a valid email address."); return; } - setEmailError(""); if (password.trim().length === 0) { - console.log(password); setPasswordError("Please enter your password."); return; } - setPasswordError(""); + setIsLoading(true); const error = await login(email, password); - setLoginError(error); + setIsLoading(false); + + if (error) { + setLoginError(error); + } }; return ( @@ -83,13 +78,11 @@ export default function Page() { width={100} height={91} /> -

Login

-
{emailError && } -
{passwordError && } -
Forgot password? - +
- {loginError && } ); diff --git a/compass/app/layout.tsx b/compass/app/layout.tsx index b80e6a1..4a1426f 100644 --- a/compass/app/layout.tsx +++ b/compass/app/layout.tsx @@ -1,4 +1,9 @@ +"use client"; + import "../styles/globals.css"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; +import { createClient } from "@/utils/supabase/client"; export default function RootLayout({ // Layouts must accept a children prop. diff --git a/compass/app/page.tsx b/compass/app/page.tsx index 9299861..0deb2e5 100644 --- a/compass/app/page.tsx +++ b/compass/app/page.tsx @@ -6,4 +6,6 @@ export default function Page() { const router = useRouter(); router.push("/auth/login"); + + return

GO TO LOGIN PAGE (/auth/login)

; } diff --git a/compass/app/resource/layout.tsx b/compass/app/resource/layout.tsx index 4900dce..f598b09 100644 --- a/compass/app/resource/layout.tsx +++ b/compass/app/resource/layout.tsx @@ -4,8 +4,10 @@ import React, { useState } from "react"; import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline"; import { createClient } from "@/utils/supabase/client"; import { useEffect } from "react"; -import { User } from "@supabase/supabase-js"; import { useRouter } from "next/navigation"; +import User, { Role } from "@/utils/models/User"; +import Loading from "@/components/auth/Loading"; + export default function RootLayout({ children, }: { @@ -14,9 +16,11 @@ export default function RootLayout({ const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [user, setUser] = useState(); const router = useRouter(); + useEffect(() => { - const supabase = createClient(); async function getUser() { + const supabase = createClient(); + const { data, error } = await supabase.auth.getUser(); console.log(data, error); @@ -26,48 +30,60 @@ export default function RootLayout({ router.push("auth/login"); return; } - setUser(data.user); + const userData = await fetch( `${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}` ); + + setUser(await userData.json()); } getUser(); }, [router]); + return (
- {/* button to open sidebar */} - - {/* sidebar */} -
- -
- {/* page ui */} -
- {children} -
+ {user ? ( +
+ {/* button to open sidebar */} + + {/* sidebar */} +
+ +
+ {/* page ui */} +
+ {children} +
+
+ ) : ( + + )}
); } diff --git a/compass/components/Button.tsx b/compass/components/Button.tsx index a9067c0..fee1387 100644 --- a/compass/components/Button.tsx +++ b/compass/components/Button.tsx @@ -3,7 +3,7 @@ import { FunctionComponent, ReactNode } from "react"; type ButtonProps = { children: ReactNode; onClick?: (event: React.MouseEvent) => void; - type?: "button" | "submit" | "reset"; // specify possible values for type + type?: "button" | "submit" | "reset"; disabled?: boolean; }; @@ -13,11 +13,11 @@ const Button: FunctionComponent = ({ disabled, onClick, }) => { - const buttonClassName = `inline-block rounded border ${ + const buttonClassName = `inline-flex items-center justify-center rounded border ${ disabled ? "bg-gray-400 text-gray-600 cursor-not-allowed" : "border-purple-600 bg-purple-600 text-white hover:bg-transparent hover:text-purple-600 focus:outline-none focus:ring active:text-purple-500" - } px-4 py-1 text-md font-semibold w-20 h-10 text-center`; + } px-4 py-2 text-md font-semibold w-full sm:w-auto`; return ( ); }; diff --git a/compass/components/Table/Index.tsx b/compass/components/Table/Index.tsx index 5863933..931b039 100644 --- a/compass/components/Table/Index.tsx +++ b/compass/components/Table/Index.tsx @@ -1,6 +1,6 @@ // for showcasing to compass -import usersImport from "./users.json"; +import users from "./users.json"; import { Cell, ColumnDef, @@ -32,22 +32,7 @@ import { } from "@heroicons/react/24/solid"; import TagsInput from "../TagsInput/Index"; import { rankItem } from "@tanstack/match-sorter-utils"; -import { TableCell } from "./TableCell"; -import { PrimaryTableCell } from "./PrimaryTableCell"; - -const usersExample = usersImport as unknown as User[]; - -type User = { - id: number; - created_at: any; - username: string; - role: "administrator" | "employee" | "volunteer"; - email: string; - program: "domestic" | "economic" | "community"; - experience: number; - group?: string; - visible: boolean; -}; +import User from "@/utils/models/User"; // For search const fuzzyFilter = ( @@ -66,17 +51,17 @@ const fuzzyFilter = ( return itemRank.passed; }; -export const Table = () => { +export const Table = ({ users }: { users: User[] }) => { const columnHelper = createColumnHelper(); useEffect(() => { - const sortedUsers = [...usersExample].sort((a, b) => + const sortedUsers = [...users].sort((a, b) => a.visible === b.visible ? 0 : a.visible ? -1 : 1 ); setData(sortedUsers); - }, []); + }, [users]); - const deleteUser = (userId) => { + const deleteUser = (userId: number) => { console.log(data); setData((currentData) => currentData.filter((user) => user.id !== userId) @@ -188,10 +173,10 @@ export const Table = () => { }), ]; - const [data, setData] = useState([...usersExample]); + const [data, setData] = useState([...users]); const addUser = () => { - setData([...data, {}]); + setData([...data]); }; // Searching diff --git a/compass/components/auth/Loading.module.css b/compass/components/auth/Loading.module.css new file mode 100644 index 0000000..3458b07 --- /dev/null +++ b/compass/components/auth/Loading.module.css @@ -0,0 +1,43 @@ +/* components/Loading.module.css */ +.loadingOverlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; +} + +.loadingContent { + text-align: center; +} + +.loadingTitle { + font-size: 2rem; + font-weight: bold; + color: #5b21b6; + margin-top: 1rem; +} + +.loadingSpinner { + width: 50px; + height: 50px; + border: 4px solid #5b21b6; + border-top: 4px solid #fff; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 2rem auto; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/compass/components/auth/Loading.tsx b/compass/components/auth/Loading.tsx new file mode 100644 index 0000000..a655fb3 --- /dev/null +++ b/compass/components/auth/Loading.tsx @@ -0,0 +1,22 @@ +// components/Loading.js +import styles from "./Loading.module.css"; +import Image from "next/image"; + +const Loading = () => { + return ( +
+
+ +

Loading...

+
+
+
+ ); +}; + +export default Loading; diff --git a/compass/components/resource/Sidebar.tsx b/compass/components/resource/Sidebar.tsx index 882302c..ffd4b56 100644 --- a/compass/components/resource/Sidebar.tsx +++ b/compass/components/resource/Sidebar.tsx @@ -13,9 +13,15 @@ interface SidebarProps { setIsSidebarOpen: React.Dispatch>; name: string; email: string; + admin: boolean; } -const Sidebar: React.FC = ({ setIsSidebarOpen, name, email }) => { +const Sidebar: React.FC = ({ + setIsSidebarOpen, + name, + email, + admin, +}) => { return (
{/* button to close sidebar */} @@ -39,12 +45,38 @@ const Sidebar: React.FC = ({ setIsSidebarOpen, name, email }) => { Pages
diff --git a/compass/components/resource/SidebarItem.tsx b/compass/components/resource/SidebarItem.tsx index 47bc29e..34d3541 100644 --- a/compass/components/resource/SidebarItem.tsx +++ b/compass/components/resource/SidebarItem.tsx @@ -1,27 +1,31 @@ +import Link from "next/link"; + interface SidebarItemProps { icon: React.ReactElement; text: string; active: boolean; + redirect: string; } export const SidebarItem: React.FC = ({ icon, text, active, + redirect, }) => { return ( - {icon} {text} - + ); }; diff --git a/compass/utils/models/User.ts b/compass/utils/models/User.ts index 7700e86..0c55114 100644 --- a/compass/utils/models/User.ts +++ b/compass/utils/models/User.ts @@ -20,4 +20,5 @@ export default interface User { program: Program[]; role: Role; created_at: Date; + visible: boolean; }