mirror of
https://github.com/jaypyles/Scraperr.git
synced 2025-12-16 04:36:14 +00:00
feat: use auth context
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
76
src/contexts/AuthContext.tsx
Normal file
76
src/contexts/AuthContext.tsx
Normal 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;
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
137
src/styles/themes.ts
Normal 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 };
|
||||
Reference in New Issue
Block a user