feat: use auth context

This commit is contained in:
Jayden Pyles
2024-07-07 11:04:46 -05:00
parent ce00acc4cb
commit 67c0b125be
10 changed files with 295 additions and 185 deletions

View File

@@ -57,7 +57,10 @@ async def submit_scrape_job(job: SubmitScrapeJob):
json_scraped = jsonable_encoder(scraped)
job.result = json_scraped
job.id = uuid.uuid4().hex
await insert(jsonable_encoder(job))
if job.user:
await insert(jsonable_encoder(job))
return JSONResponse(content=json_scraped)
except Exception as e:
return JSONResponse(content={"error": str(e)}, status_code=500)

View File

@@ -21,7 +21,7 @@ class SubmitScrapeJob(pydantic.BaseModel):
id: Optional[str] = None
url: str
elements: list[Element]
user: str
user: Optional[str] = None
time_created: str
result: Optional[dict[str, Any]] = None

View File

@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { useAuth } from "../hooks/useAuth";
import { useAuth } from "../contexts/AuthContext";
import {
Box,
Drawer,
@@ -14,6 +14,7 @@ import {
Typography,
Button,
Switch,
Tooltip,
} from "@mui/material";
import HomeIcon from "@mui/icons-material/Home";
import HttpIcon from "@mui/icons-material/Http";
@@ -30,8 +31,6 @@ const NavDrawer: React.FC<NavDrawerProps> = ({ toggleTheme, isDarkMode }) => {
const { login, logout, user, isAuthenticated } = useAuth();
const [open, setOpen] = useState<boolean>(false);
console.log(user);
const toggleDrawer =
(open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
if (
@@ -85,7 +84,9 @@ const NavDrawer: React.FC<NavDrawerProps> = ({ toggleTheme, isDarkMode }) => {
>
<MenuIcon />
</IconButton>
<Switch checked={isDarkMode} onChange={toggleTheme} />
<Tooltip title="Dark Theme Toggle" placement="bottom">
<Switch checked={isDarkMode} onChange={toggleTheme} />
</Tooltip>
</div>
{isAuthenticated ? (
<div className="flex flex-row items-center">

View File

@@ -0,0 +1,76 @@
import React, { createContext, useContext, useState, useEffect } from "react";
import axios from "axios";
interface AuthContextProps {
user: any;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextProps | undefined>(undefined);
interface AuthProps {
children: React.ReactNode;
}
export const AuthProvider: React.FC<AuthProps> = ({ children }) => {
const [user, setUser] = useState<any>(null);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
useEffect(() => {
const token = localStorage.getItem("token");
if (token) {
axios
.get("http://localhost:8000/api/auth/users/me", {
headers: { Authorization: `Bearer ${token}` },
})
.then((response) => {
setUser(response.data);
setIsAuthenticated(true);
})
.catch(() => {
localStorage.removeItem("token");
});
}
}, []);
const login = async (email: string, password: string) => {
const params = new URLSearchParams();
params.append("username", email);
params.append("password", password);
const response = await axios.post(
"http://localhost:8000/api/auth/token",
params,
);
localStorage.setItem("token", response.data.access_token);
const userResponse = await axios.get(
"http://localhost:8000/api/auth/users/me",
{
headers: { Authorization: `Bearer ${response.data.access_token}` },
},
);
setUser(userResponse.data);
setIsAuthenticated(true);
};
const logout = () => {
localStorage.removeItem("token");
setUser(null);
setIsAuthenticated(false);
};
return (
<AuthContext.Provider value={{ user, isAuthenticated, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};

View File

@@ -1,52 +0,0 @@
import { useState, useEffect } from "react";
import axios from "axios";
type User = {
full_name: string;
email: string;
disabled: null | boolean;
};
export const useAuth = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const fetchUserInfo = async () => {
try {
const response = await axios.get("/api/auth/users/me", {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
setUser(response.data);
setIsAuthenticated(true);
} catch (error) {
setIsAuthenticated(false);
setUser(null);
}
};
const token = localStorage.getItem("token");
if (token) {
fetchUserInfo();
}
}, []);
const login = () => {
window.location.href = "http://localhost:8000/api/auth/login";
};
const logout = () => {
localStorage.removeItem("token");
setIsAuthenticated(false);
setUser(null);
};
return {
isAuthenticated,
user,
login,
logout,
};
};

View File

@@ -5,106 +5,10 @@ import React, { useState, useEffect } from "react";
import type { AppProps } from "next/app";
import Head from "next/head";
import { SessionProvider } from "next-auth/react";
import { ThemeProvider, createTheme, CssBaseline } from "@mui/material";
import { ThemeProvider, CssBaseline } from "@mui/material";
import NavDrawer from "../components/NavDrawer";
const lightTheme = createTheme({
palette: {
mode: "light",
primary: {
main: "#1976d2",
},
secondary: {
main: "#dc004e",
},
background: {
default: "#f4f6f8",
paper: "#ffffff",
},
text: {
primary: "#000000",
secondary: "#333333",
},
},
typography: {
fontFamily: "Roboto, sans-serif",
h1: {
fontWeight: 500,
},
h2: {
fontWeight: 500,
},
},
components: {
MuiButton: {
styleOverrides: {
root: {
borderRadius: 8,
color: "black",
},
},
},
MuiPaper: {
styleOverrides: {
root: {
padding: 16,
},
},
},
},
});
const darkTheme = createTheme({
palette: {
mode: "dark",
primary: {
main: "#90caf9",
},
secondary: {
main: "#f48fb1",
},
background: {
default: "#121212",
paper: "#1e1e1e",
},
text: {
primary: "#ffffff",
secondary: "#bbbbbb",
},
},
typography: {
fontFamily: "Roboto, sans-serif",
h1: {
fontWeight: 500,
color: "white",
},
h2: {
fontWeight: 500,
color: "white",
},
h4: {
fontWeight: 500,
color: "white",
},
},
components: {
MuiButton: {
styleOverrides: {
root: {
borderRadius: 8,
color: "white",
},
},
},
MuiPaper: {
styleOverrides: {
root: {
padding: 16,
},
},
},
},
});
import { darkTheme, lightTheme } from "../styles/themes";
import { AuthProvider } from "../contexts/AuthContext";
const App: React.FC<AppProps> = ({ Component, pageProps }) => {
const [isDarkMode, setIsDarkMode] = useState(false);
@@ -132,13 +36,13 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
<Head>
<title>Webapp Template</title>
</Head>
<SessionProvider session={pageProps.session}>
<AuthProvider>
<ThemeProvider theme={isDarkMode ? darkTheme : lightTheme}>
<CssBaseline />
<NavDrawer isDarkMode={isDarkMode} toggleTheme={toggleTheme} />
<Component {...pageProps} />
</ThemeProvider>
</SessionProvider>
</AuthProvider>
</>
);
};

View File

@@ -14,7 +14,7 @@ import {
Tooltip,
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import { useAuth } from "../hooks/useAuth";
import { useAuth } from "../contexts/AuthContext";
import { useRouter } from "next/router";
import CircularProgress from "@mui/material/CircularProgress";
@@ -120,7 +120,7 @@ const Home = () => {
return (
<Box
bgcolor="background.paper"
bgcolor="background.default"
display="flex"
flexDirection="column"
justifyContent="center"

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react";
import JobTable from "../components/JobTable";
import { useAuth } from "../hooks/useAuth";
import { useAuth } from "../contexts/AuthContext";
import { Box } from "@mui/system";
const Jobs = () => {
const { user } = useAuth();
@@ -14,11 +15,42 @@ const Jobs = () => {
body: JSON.stringify({ user: user?.email }),
})
.then((response) => response.json())
.then((data) => setJobs(data));
.then((data) => setJobs(data))
.catch((error) => {
console.error("Error fetching jobs:", error);
});
} else {
setJobs([]);
}
}, [user]);
return <JobTable jobs={jobs} />;
return (
<>
{user ? (
<JobTable jobs={jobs} />
) : (
<Box
bgcolor="background.default"
minHeight="100vh"
display="flex"
justifyContent="center"
alignItems="center"
>
<h4
style={{
color: "#fff",
padding: "20px",
borderRadius: "8px",
background: "rgba(0, 0, 0, 0.6)",
boxShadow: "0 4px 8px rgba(0, 0, 0, 0.2)",
}}
>
Previous jobs not viewable unless logged in.
</h4>
</Box>
)}
</>
);
};
export default Jobs;

View File

@@ -1,6 +1,9 @@
import React, { useState } from "react";
import axios from "axios";
import { Button, TextField, Typography, Container, Box } from "@mui/material";
import { Button, TextField, Typography, Box } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { useRouter } from "next/router";
import { useAuth } from "../contexts/AuthContext";
type Mode = "login" | "signup";
@@ -9,20 +12,17 @@ const AuthForm: React.FC = () => {
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [fullName, setFullName] = useState<string>("");
const theme = useTheme();
const router = useRouter();
const { login } = useAuth();
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
try {
if (mode === "login") {
const params = new URLSearchParams();
params.append("username", email);
params.append("password", password);
const response = await axios.post(
"http://localhost:8000/api/auth/token",
params,
);
localStorage.setItem("token", response.data.access_token);
await login(email, password);
alert("Login successful");
router.push("/");
} else {
await axios.post("http://localhost:8000/api/auth/signup", {
email: email,
@@ -30,6 +30,7 @@ const AuthForm: React.FC = () => {
full_name: fullName,
});
alert("Signup successful");
router.push("/login");
}
} catch (error) {
console.error(error);
@@ -42,13 +43,23 @@ const AuthForm: React.FC = () => {
};
return (
<Container component="main" maxWidth="xs">
<Box
bgcolor="background.default"
component="main"
minHeight="100vh"
sx={{ display: "flex", justifyContent: "center" }}
>
<Box
bgcolor="background.paper"
maxWidth="md"
sx={{
marginTop: 8,
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: 4,
marginTop: 4,
marginBottom: 4,
height: "50vh",
}}
>
<Typography component="h1" variant="h5">
@@ -100,22 +111,20 @@ const AuthForm: React.FC = () => {
type="submit"
fullWidth
variant="contained"
color="primary"
sx={{ mt: 3, mb: 2 }}
sx={{
mt: 3,
mb: 2,
color: theme.palette.mode === "light" ? "#000000" : "#ffffff",
}}
>
{mode.charAt(0).toUpperCase() + mode.slice(1)}
</Button>
<Button
onClick={toggleMode}
fullWidth
variant="outlined"
color="secondary"
>
Switch to {mode === "login" ? "Signup" : "Login"}
<Button onClick={toggleMode} fullWidth variant="text" color="primary">
{mode === "login" ? "No Account? Sign up" : "Login"}
</Button>
</Box>
</Box>
</Container>
</Box>
);
};

137
src/styles/themes.ts Normal file
View File

@@ -0,0 +1,137 @@
import { createTheme } from "@mui/material";
const commonThemeOptions = {
typography: {
fontFamily: "Roboto, sans-serif",
h1: {
fontWeight: 500,
},
h2: {
fontWeight: 500,
},
h4: {
fontWeight: 500,
},
body1: {
fontFamily: "Roboto, sans-serif",
},
body2: {
fontFamily: "Roboto, sans-serif",
},
button: {
fontFamily: "Roboto, sans-serif",
},
caption: {
fontFamily: "Roboto, sans-serif",
},
overline: {
fontFamily: "Roboto, sans-serif",
},
},
components: {
MuiCssBaseline: {
styleOverrides: {
body: {
fontFamily: "Roboto, sans-serif",
},
html: {
fontFamily: "Roboto, sans-serif",
},
"*": {
fontFamily: "Roboto, sans-serif",
},
},
},
MuiPaper: {
styleOverrides: {
root: {
padding: 16,
},
},
},
},
};
const lightTheme = createTheme({
palette: {
mode: "light",
primary: {
main: "#1976d2",
},
secondary: {
main: "#dc004e",
},
background: {
default: "#f4f6f8",
paper: "#ffffff",
},
text: {
primary: "#000000",
secondary: "#333333",
},
},
...commonThemeOptions,
});
const darkTheme = createTheme({
palette: {
mode: "dark",
primary: {
main: "#90caf9",
},
secondary: {
main: "#f48fb1",
},
background: {
default: "#121212",
paper: "#1e1e1e",
},
text: {
primary: "#ffffff",
secondary: "#bbbbbb",
},
},
typography: {
...commonThemeOptions.typography,
h1: {
...commonThemeOptions.typography.h1,
color: "#ffffff",
},
h2: {
...commonThemeOptions.typography.h2,
color: "#ffffff",
},
h4: {
...commonThemeOptions.typography.h4,
color: "#ffffff",
},
h5: {
color: "#ffffff",
},
},
components: {
...commonThemeOptions.components,
MuiCssBaseline: {
styleOverrides: {
body: {
fontFamily: "Roboto, sans-serif",
},
html: {
fontFamily: "Roboto, sans-serif",
},
"*": {
fontFamily: "Roboto, sans-serif",
},
},
},
MuiButton: {
styleOverrides: {
root: {
color: "white",
},
},
},
},
});
export { lightTheme, darkTheme };