From 3ec9d90cb62e6085309b4afd005996d6f0842b06 Mon Sep 17 00:00:00 2001 From: Sid Ravinutala Date: Mon, 18 Mar 2024 09:14:07 -0400 Subject: [PATCH 1/4] sort results by content_id --- core_backend/app/contents/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core_backend/app/contents/models.py b/core_backend/app/contents/models.py index 101dac256..b4b089876 100644 --- a/core_backend/app/contents/models.py +++ b/core_backend/app/contents/models.py @@ -134,7 +134,7 @@ async def get_list_of_content_from_db( """ Retrieves all content from the database """ - stmt = select(ContentDB) + stmt = select(ContentDB).order_by(ContentDB.content_id) if offset > 0: stmt = stmt.offset(offset) if limit is not None: From 0dd746cf0d8a3c2e5fa7ae43194a6987fe100642 Mon Sep 17 00:00:00 2001 From: Sid Ravinutala Date: Tue, 19 Mar 2024 01:41:44 -0400 Subject: [PATCH 2/4] Auth and access level --- admin_app_2/package-lock.json | 37 +++++ admin_app_2/package.json | 1 + admin_app_2/src/app/content/edit/page.tsx | 41 ++++-- admin_app_2/src/app/content/layout.tsx | 17 +++ admin_app_2/src/app/content/page.tsx | 44 +++--- admin_app_2/src/app/layout.tsx | 10 +- admin_app_2/src/app/login/page.tsx | 138 ++++++++++++++++++ admin_app_2/src/app/page.tsx | 18 +-- admin_app_2/src/components/ContentCard.tsx | 16 +- admin_app_2/src/components/ContentModal.tsx | 22 +-- admin_app_2/src/components/NavBar.tsx | 5 +- .../src/components/ProtectedComponent.tsx | 38 +++++ admin_app_2/src/utils/api.ts | 52 +++++-- admin_app_2/src/utils/auth.tsx | 90 ++++++++++++ 14 files changed, 445 insertions(+), 84 deletions(-) create mode 100644 admin_app_2/src/app/content/layout.tsx create mode 100644 admin_app_2/src/app/login/page.tsx create mode 100644 admin_app_2/src/components/ProtectedComponent.tsx create mode 100644 admin_app_2/src/utils/auth.tsx diff --git a/admin_app_2/package-lock.json b/admin_app_2/package-lock.json index b6d04a0ff..975be8874 100644 --- a/admin_app_2/package-lock.json +++ b/admin_app_2/package-lock.json @@ -24,6 +24,7 @@ "@types/react-dom": "^18", "eslint": "^8", "eslint-config-next": "14.1.3", + "prettier-plugin-organize-imports": "^3.2.4", "typescript": "^5" } }, @@ -3837,6 +3838,42 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-organize-imports": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz", + "integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==", + "dev": true, + "peerDependencies": { + "@volar/vue-language-plugin-pug": "^1.0.4", + "@volar/vue-typescript": "^1.0.4", + "prettier": ">=2.0", + "typescript": ">=2.9" + }, + "peerDependenciesMeta": { + "@volar/vue-language-plugin-pug": { + "optional": true + }, + "@volar/vue-typescript": { + "optional": true + } + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/admin_app_2/package.json b/admin_app_2/package.json index ac5c21d1e..6ebc3b262 100644 --- a/admin_app_2/package.json +++ b/admin_app_2/package.json @@ -25,6 +25,7 @@ "@types/react-dom": "^18", "eslint": "^8", "eslint-config-next": "14.1.3", + "prettier-plugin-organize-imports": "^3.2.4", "typescript": "^5" } } diff --git a/admin_app_2/src/app/content/edit/page.tsx b/admin_app_2/src/app/content/edit/page.tsx index 9c8482393..61cff70db 100644 --- a/admin_app_2/src/app/content/edit/page.tsx +++ b/admin_app_2/src/app/content/edit/page.tsx @@ -1,8 +1,10 @@ "use client"; import LanguageButtonBar from "@/components/LanguageButtonBar"; import { Layout } from "@/components/Layout"; +import { FullAccessComponent } from "@/components/ProtectedComponent"; import { appColors, appStyles, sizes } from "@/utils"; import { apiCalls } from "@/utils/api"; +import { useAuth } from "@/utils/auth"; import { ChevronLeft } from "@mui/icons-material"; import { Button, CircularProgress, TextField, Typography } from "@mui/material"; import Alert from "@mui/material/Alert"; @@ -29,12 +31,13 @@ const AddEditContentPage = () => { const [content, setContent] = React.useState(null); const [isLoading, setIsLoading] = React.useState(true); + const { token } = useAuth(); React.useEffect(() => { if (!content_id) { setIsLoading(false); return; } else { - apiCalls.getContent(content_id).then((data) => { + apiCalls.getContent(content_id, token!).then((data) => { setContent(data); setIsLoading(false); }); @@ -58,17 +61,19 @@ const AddEditContentPage = () => { ); } return ( - -
- - - - + + +
+ + + + + - + ); }; @@ -84,6 +89,8 @@ const ContentBox = ({ const [isTitleEmpty, setIsTitleEmpty] = React.useState(false); const [isContentEmpty, setIsContentEmpty] = React.useState(false); + const { token } = useAuth(); + const router = useRouter(); const saveContent = async (content: Content) => { const body: EditContentBody = { @@ -95,8 +102,8 @@ const ContentBox = ({ const promise = content.content_id === null - ? apiCalls.addContent(body) - : apiCalls.editContent(content.content_id, body); + ? apiCalls.addContent(body, token!) + : apiCalls.editContent(content.content_id, body, token!); const result = promise .then((data) => { @@ -162,7 +169,7 @@ const ContentBox = ({ "& .MuiFormHelperText-root": { backgroundColor: appColors.lightGrey, mx: 0, - my: 0, // Set your desired background color here + my: 0, }, }} value={content ? content.content_title : ""} @@ -180,7 +187,7 @@ const ContentBox = ({ "& .MuiFormHelperText-root": { backgroundColor: appColors.lightGrey, mx: 0, - my: 0, // Set your desired background color here + my: 0, }, }} label="Content" @@ -234,11 +241,13 @@ const ContentBox = ({ }; const Header = ({ content_id }: { content_id: number | null }) => { + const router = useRouter(); + return ( (window.location.href = "/content/")} + onClick={() => (content_id ? router.back() : router.push("/content"))} /> {content_id ? ( diff --git a/admin_app_2/src/app/content/layout.tsx b/admin_app_2/src/app/content/layout.tsx new file mode 100644 index 000000000..4032b0840 --- /dev/null +++ b/admin_app_2/src/app/content/layout.tsx @@ -0,0 +1,17 @@ +import NavBar from "@/components/NavBar"; +import { ProtectedComponent } from "@/components/ProtectedComponent"; +import AuthProvider from "@/utils/auth"; +import React from "react"; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + ); +} diff --git a/admin_app_2/src/app/content/page.tsx b/admin_app_2/src/app/content/page.tsx index 20584074d..1a6d08c31 100644 --- a/admin_app_2/src/app/content/page.tsx +++ b/admin_app_2/src/app/content/page.tsx @@ -1,8 +1,9 @@ "use client"; import ContentCard from "@/components/ContentCard"; import { Layout } from "@/components/Layout"; -import { LANGUAGE_OPTIONS, appColors, sizes } from "@/utils"; +import { LANGUAGE_OPTIONS, sizes } from "@/utils"; import { apiCalls } from "@/utils/api"; +import { useAuth } from "@/utils/auth"; import { Add, ChevronLeft, ChevronRight } from "@mui/icons-material"; import { Box, Button, CircularProgress, Grid, Typography } from "@mui/material"; import Alert from "@mui/material/Alert"; @@ -26,17 +27,19 @@ const CardsView = () => { const [displayLanguage, setDisplayLanguage] = React.useState( LANGUAGE_OPTIONS[0].label, ); + const { accessLevel } = useAuth(); + return ( - + ); }; -const CardsUtilityStrip = () => { +const CardsUtilityStrip = ({ editAccess }: { editAccess: boolean }) => { return ( { gap={sizes.baseGap} > - @@ -70,6 +73,8 @@ const CardsGrid = ({ displayLanguage }: { displayLanguage: string }) => { const action = searchParams.get("action") || null; const content_id = Number(searchParams.get("content_id")) || null; + const { token, accessLevel } = useAuth(); + const getSnackMessage = ( action: string | null, content_id: number | null, @@ -90,23 +95,24 @@ const CardsGrid = ({ displayLanguage }: { displayLanguage: string }) => { const onSuccessfulDelete = (content_id: number) => { setIsLoading(true); setRefreshKey((prevKey) => prevKey + 1); - console.log("hello"); setSnackMessage(`Content #${content_id} deleted successfully`); }; React.useEffect(() => { - apiCalls - .getContentList() - .then((data) => { - setCards(data); - setMaxPages(Math.ceil(data.length / MAX_CARDS_PER_PAGE)); - setIsLoading(false); - }) - .catch((error) => { - console.error("Failed to fetch content:", error); - setIsLoading(false); - }); - }, [refreshKey]); + if (token) { + apiCalls + .getContentList(token!) + .then((data) => { + setCards(data); + setMaxPages(Math.ceil(data.length / MAX_CARDS_PER_PAGE)); + setIsLoading(false); + }) + .catch((error) => { + console.error("Failed to fetch content:", error); + setIsLoading(false); + }); + } + }, [refreshKey, token]); if (isLoading) { return ( @@ -180,6 +186,10 @@ const CardsGrid = ({ displayLanguage }: { displayLanguage: string }) => { onFailedDelete={(content_id: number) => { setSnackMessage(`Failed to delete content #${content_id}`); }} + deleteContent={(content_id: number) => { + return apiCalls.deleteContent(content_id, token!); + }} + editAccess={accessLevel === "fullaccess"} /> ))} diff --git a/admin_app_2/src/app/layout.tsx b/admin_app_2/src/app/layout.tsx index 7a2883b46..317f27dd1 100644 --- a/admin_app_2/src/app/layout.tsx +++ b/admin_app_2/src/app/layout.tsx @@ -1,10 +1,9 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import NavBar from "@/components/NavBar"; import theme from "@/theme"; +import AuthProvider from "@/utils/auth"; import { CssBaseline, ThemeProvider } from "@mui/material"; +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; import React from "react"; - const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { @@ -25,8 +24,7 @@ export default function RootLayout({ - - {children} + {children} diff --git a/admin_app_2/src/app/login/page.tsx b/admin_app_2/src/app/login/page.tsx new file mode 100644 index 000000000..82d9cecb7 --- /dev/null +++ b/admin_app_2/src/app/login/page.tsx @@ -0,0 +1,138 @@ +"use client"; +import { useAuth } from "@/utils/auth"; +import LockOutlinedIcon from "@mui/icons-material/LockOutlined"; +import Alert from "@mui/material/Alert"; +import Avatar from "@mui/material/Avatar"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import CssBaseline from "@mui/material/CssBaseline"; +import Grid from "@mui/material/Grid"; +import Paper from "@mui/material/Paper"; +import TextField from "@mui/material/TextField"; +import Typography from "@mui/material/Typography"; +import * as React from "react"; + +const Login = () => { + const [isUsernameEmpty, setIsUsernameEmpty] = React.useState(false); + const [isPasswordEmpty, setIsPasswordEmpty] = React.useState(false); + const { login, loginError } = useAuth(); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const data = new FormData(event.currentTarget); + const username = data.get("username") as string; + const password = data.get("password") as string; + + if ( + username === "" || + password === "" || + username === null || + password === null + ) { + username === "" || username === null ? setIsUsernameEmpty(true) : null; + password === "" || password === null ? setIsPasswordEmpty(true) : null; + } else { + login(username, password); + } + }; + + return ( + + + + + + + + + + Sign in + + + + {" "} + {/* Reserve space for the alert */} + {loginError && {loginError}} + + { + setIsUsernameEmpty(false); + }} + /> + { + setIsPasswordEmpty(false); + }} + /> + + + + + + ); +}; + +export default Login; diff --git a/admin_app_2/src/app/page.tsx b/admin_app_2/src/app/page.tsx index 0811d8758..24e0ecb69 100644 --- a/admin_app_2/src/app/page.tsx +++ b/admin_app_2/src/app/page.tsx @@ -1,22 +1,14 @@ "use client"; -import { Button } from "@mui/material"; import { useRouter } from "next/navigation"; +import { useEffect } from "react"; export default function Home() { const router = useRouter(); - const handleLogin = () => { + + useEffect(() => { router.push("/content"); - }; + }, []); - return ( -
-

Login page

- -
- ); + return
; } diff --git a/admin_app_2/src/components/ContentCard.tsx b/admin_app_2/src/components/ContentCard.tsx index d879e5bc6..8a9539efa 100644 --- a/admin_app_2/src/components/ContentCard.tsx +++ b/admin_app_2/src/components/ContentCard.tsx @@ -1,11 +1,10 @@ -import { appColors, appStyles, sizes } from "@/utils"; -import { Edit, Delete } from "@mui/icons-material"; import { ContentViewModal, DeleteContentModal, } from "@/components/ContentModal"; -import { apiCalls } from "@/utils/api"; -import { Button, IconButton, Card, Typography } from "@mui/material"; +import { appColors, appStyles, sizes } from "@/utils"; +import { Delete, Edit } from "@mui/icons-material"; +import { Button, Card, IconButton, Typography } from "@mui/material"; import Link from "next/link"; import React from "react"; import { Layout } from "./Layout"; @@ -17,6 +16,8 @@ const ContentCard = ({ last_modified, onSuccessfulDelete, onFailedDelete, + deleteContent, + editAccess, }: { title: string; text: string; @@ -24,6 +25,8 @@ const ContentCard = ({ last_modified: string; onSuccessfulDelete: (content_id: number) => void; onFailedDelete: (content_id: number) => void; + deleteContent: (content_id: number) => Promise; + editAccess: boolean; }) => { const [openReadModal, setOpenReadModal] = React.useState(false); const [openDeleteModal, setOpenDeleteModal] = React.useState(false); @@ -72,13 +75,14 @@ const ContentCard = ({ -
setOpenDeleteModal(true)} > @@ -93,6 +97,7 @@ const ContentCard = ({ last_modified={last_modified} open={openReadModal} onClose={() => setOpenReadModal(false)} + editAccess={editAccess} /> setOpenDeleteModal(false)} onSuccessfulDelete={onSuccessfulDelete} onFailedDelete={onFailedDelete} + deleteContent={deleteContent} /> ); diff --git a/admin_app_2/src/components/ContentModal.tsx b/admin_app_2/src/components/ContentModal.tsx index 2449da082..435676b92 100644 --- a/admin_app_2/src/components/ContentModal.tsx +++ b/admin_app_2/src/components/ContentModal.tsx @@ -1,23 +1,20 @@ import { appColors, appStyles, sizes } from "@/utils"; import { Close, - Delete, Edit, RemoveRedEye, ThumbDown, ThumbUp, } from "@mui/icons-material"; import { Box, Button, Fade, Modal, Typography } from "@mui/material"; -import Link from "next/link"; -import { apiCalls } from "../utils/api"; -import LanguageButtonBar from "./LanguageButtonBar"; -import { Layout } from "./Layout"; -import React from "react"; import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; import DialogContent from "@mui/material/DialogContent"; import DialogContentText from "@mui/material/DialogContentText"; import DialogTitle from "@mui/material/DialogTitle"; +import Link from "next/link"; +import LanguageButtonBar from "./LanguageButtonBar"; +import { Layout } from "./Layout"; const ContentViewModal = ({ title, @@ -26,6 +23,7 @@ const ContentViewModal = ({ last_modified, open, onClose, + editAccess, }: { title: string; text: string; @@ -33,6 +31,7 @@ const ContentViewModal = ({ last_modified: string; open: boolean; onClose: () => void; + editAccess: boolean; }) => { return ( - - +
); }; diff --git a/admin_app_2/src/components/ContentCard.tsx b/admin_app_2/src/components/ContentCard.tsx index 8a9539efa..e1d583b69 100644 --- a/admin_app_2/src/components/ContentCard.tsx +++ b/admin_app_2/src/components/ContentCard.tsx @@ -74,12 +74,14 @@ const ContentCard = ({ Read - - - +
- - - +
= ({ }) => { const router = useRouter(); const { token } = useAuth(); + const pathname = usePathname(); useEffect(() => { if (!token) { - router.push("/login"); + router.push("/login?sourcePage=" + encodeURIComponent(pathname)); } }, [token]); diff --git a/admin_app_2/src/utils/api.ts b/admin_app_2/src/utils/api.ts index 929961c62..62c21fbd4 100644 --- a/admin_app_2/src/utils/api.ts +++ b/admin_app_2/src/utils/api.ts @@ -42,23 +42,6 @@ const getContent = async (content_id: number, token: string) => { }); }; -const getLoginToken = async (username: string, password: string) => { - const formData = new FormData(); - formData.append("username", username); - formData.append("password", password); - return fetch(`${BACKEND_ROOT_PATH}/login`, { - method: "POST", - body: formData, - }).then((response) => { - if (response.ok) { - let resp = response.json(); - return resp; - } else { - throw new Error("Error fetching login token"); - } - }); -}; - const deleteContent = async (content_id: number, token: string) => { return fetch(`${BACKEND_ROOT_PATH}/content/${content_id}/delete`, { method: "DELETE", @@ -134,6 +117,23 @@ const addContent = async (content: ContentBody, token: string) => { }); }; +const getLoginToken = async (username: string, password: string) => { + const formData = new FormData(); + formData.append("username", username); + formData.append("password", password); + return fetch(`${BACKEND_ROOT_PATH}/login`, { + method: "POST", + body: formData, + }).then((response) => { + if (response.ok) { + let resp = response.json(); + return resp; + } else { + throw new Error("Error fetching login token"); + } + }); +}; + export const apiCalls = { getContentList, getContent, diff --git a/admin_app_2/src/utils/auth.tsx b/admin_app_2/src/utils/auth.tsx index b066fc21e..e08370010 100644 --- a/admin_app_2/src/utils/auth.tsx +++ b/admin_app_2/src/utils/auth.tsx @@ -20,36 +20,44 @@ type AuthProviderProps = { const AuthProvider = ({ children }: AuthProviderProps) => { const [user, setUser] = useState(null); + const getInitialToken = () => { if (typeof window !== "undefined") { return localStorage.getItem("token"); } return null; }; - const [token, setToken] = useState(getInitialToken); const [loginError, setLoginError] = useState(null); + + const getInitialAccessLevel = () => { + if (typeof window !== "undefined") { + return localStorage.getItem("accessLevel") as "readonly" | "fullaccess"; + } + return "readonly"; + }; const [accessLevel, setAccessLevel] = useState<"readonly" | "fullaccess">( - "readonly", + getInitialAccessLevel, ); + const searchParams = useSearchParams(); const router = useRouter(); const login = async (username: string, password: string) => { - const fromPage = searchParams.has("fromPage") - ? decodeURIComponent(searchParams.get("fromPage") as string) + const sourcePage = searchParams.has("sourcePage") + ? decodeURIComponent(searchParams.get("sourcePage") as string) : "/"; apiCalls .getLoginToken(username, password) .then(({ access_token, access_level }) => { localStorage.setItem("token", access_token); - + localStorage.setItem("accessLevel", access_level); setUser(username); setToken(access_token); setAccessLevel(access_level); - router.push(fromPage); + router.push(sourcePage); }) .catch((error) => { setLoginError("Invalid username or password"); @@ -59,6 +67,7 @@ const AuthProvider = ({ children }: AuthProviderProps) => { const logout = () => { localStorage.removeItem("token"); + localStorage.removeItem("accessLevel"); setUser(null); setToken(null); setAccessLevel("readonly");