This commit is contained in:
Victor
2024-03-27 10:51:41 +01:00
commit 1b61871097
38 changed files with 9828 additions and 0 deletions

13
src/App.jsx Normal file
View File

@@ -0,0 +1,13 @@
import React from "react";
import { Authenticated } from "./components";
import { Router } from "./router";
const App = () => (
<Authenticated>
<Router />
</Authenticated>
);
export default App;

30
src/api/authentication.js Normal file
View File

@@ -0,0 +1,30 @@
import axios from "axios";
export const isLoggedIn = async () => {
try {
const response = await axios.get("/authenticate");
return response.data;
} catch (error) {
return error.response.data;
}
};
export const login = async (username, password) => {
try {
const response = await axios.post("/authenticate", { username, password });
return response.data;
} catch (error) {
return error.response.data;
}
};
export const logout = async () => {
try {
const response = await axios.delete("/authenticate");
return response.data;
} catch (error) {
return error.response.data;
}
};

2
src/api/index.js Normal file
View File

@@ -0,0 +1,2 @@
export * from './authentication'
export * from './user'

14
src/api/user.js Normal file
View File

@@ -0,0 +1,14 @@
import axios from "axios";
export const createUser = async (username, password, confirmation) => {
try {
const response = await axios.post("/user", {
username,
password,
confirmation,
});
return response.data;
} catch (error) {
return error.response.data;
}
};

View File

View File

@@ -0,0 +1,7 @@
import React from "react";
import { AuthenticationProvider } from "../contexts";
export const Authenticated = ({ children }) => {
return <AuthenticationProvider>{children}</AuthenticationProvider>;
};

1
src/components/index.js Normal file
View File

@@ -0,0 +1 @@
export * from "./authenticated";

View File

@@ -0,0 +1,98 @@
import React, { useEffect, useState } from "react";
import {
createUser as registerApi,
isLoggedIn,
login as loginApi,
logout as logoutApi,
} from "../api";
import { useQuery } from "../hooks";
import { useLocation, useNavigate } from "react-router-dom";
const initState = {
user: undefined,
};
export const AuthenticationContext = React.createContext({
...initState,
login: () => {},
register: () => {},
logout: () => {},
});
export const AuthenticationProvider = ({ children }) => {
const navigate = useNavigate();
const location = useLocation();
const query = useQuery();
const [authState, setAuthState] = useState(initState);
const [isLoading, setIsLoading] = useState(true);
const redirect = () => {
navigate(query.get("redirect_uri") || "/");
};
const login = async (email, password) => {
loginApi(email, password)
.then((user) => {
setAuthState({ user });
redirect();
})
.catch(() => {
setAuthState({ user: undefined });
});
};
const register = async (username, password, confirmation) => {
registerApi(username, password, confirmation).then((user) => {
setAuthState({ user });
redirect();
});
};
const logout = () => {
logoutApi().then(() => {
setAuthState(initState);
navigate(`/login`);
});
};
useEffect(() => {
isLoggedIn()
.then((user) => {
if (user === "Unauthorized") throw new Error("Unauthorized");
setAuthState({ user });
if (location.pathname === "/login") {
redirect();
}
})
.catch(() => {
setAuthState({ user: undefined });
if (!location.pathname.match(/^(\/|\/login|\/register)$/)) {
navigate(`/login?redirect_uri=${encodeURI(location.pathname)}`);
}
})
.finally(() => {
setIsLoading(false);
});
}, []);
useEffect(() => {
if (location.pathname === "/logout") {
logout();
}
}, [location]);
if (isLoading) {
return <p>Loading...</p>;
}
return (
<AuthenticationContext.Provider
value={{ ...authState, login, logout, register }}
>
{children}
</AuthenticationContext.Provider>
);
};

1
src/contexts/index.js Normal file
View File

@@ -0,0 +1 @@
export * from './auth-context'

2
src/hooks/index.js Normal file
View File

@@ -0,0 +1,2 @@
export * from "./use-auth";
export * from "./use-query";

7
src/hooks/use-auth.js Normal file
View File

@@ -0,0 +1,7 @@
import React from "react";
import { AuthenticationContext } from "../contexts";
export function useAuth() {
return React.useContext(AuthenticationContext);
}

9
src/hooks/use-query.js Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react'
import { useLocation } from 'react-router-dom'
export const useQuery = () => {
const { search } = useLocation()
return React.useMemo(() => new URLSearchParams(search), [search])
}

19
src/main.jsx Normal file
View File

@@ -0,0 +1,19 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./app.jsx";
import "./assets/styles/index.css";
import { BrowserRouter } from "react-router-dom";
import { CookiesProvider } from "react-cookie";
import setupAxios from "./setupAxios";
setupAxios();
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<BrowserRouter>
<CookiesProvider>
<App />
</CookiesProvider>
</BrowserRouter>
</React.StrictMode>,
);

View File

@@ -0,0 +1,2 @@
export * from './login'
export * from './register'

View File

@@ -0,0 +1,42 @@
import React, { useState } from "react";
import { useAuth } from "../../hooks";
export const Login = () => {
const { login } = useAuth();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState();
const onSubmit = async (e) => {
e.preventDefault();
const response = await login(username, password);
if (response && !response.success) {
setError(response.error);
}
};
return (
<div>
<h1>Login page</h1>
{error && <p className="text-red-500">{error}</p>}
<form onSubmit={onSubmit}>
<input
type="text"
value={username}
placeholder="username"
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
value={password}
placeholder="password"
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">submit</button>
</form>
</div>
);
};

View File

@@ -0,0 +1,49 @@
import React, { useState } from "react";
import { useAuth } from "../../hooks";
export const Register = () => {
const { register } = useAuth();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [confirmation, setConfirmation] = useState("");
const [error, setError] = useState();
const onSubmit = async (e) => {
e.preventDefault();
const response = await register(username, password, confirmation);
if (response && !response.success) {
setError(response.error);
}
};
return (
<div>
<h1>Register page</h1>
{error && <p className="text-red-500">{error}</p>}
<form onSubmit={onSubmit}>
<input
type="text"
value={username}
placeholder="username"
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
value={password}
placeholder="password"
onChange={(e) => setPassword(e.target.value)}
/>
<input
type="password"
value={confirmation}
placeholder="confirmation"
onChange={(e) => setConfirmation(e.target.value)}
/>
<button type="submit">submit</button>
</form>
</div>
);
};

14
src/pages/home.jsx Normal file
View File

@@ -0,0 +1,14 @@
import React from "react";
import { useAuth } from "../hooks";
export const Home = () => {
const { user } = useAuth();
return (
<div>
<h1>Home page</h1>
{user && <h2>Hello {user.user.username}</h2>}
</div>
);
};

2
src/pages/index.js Normal file
View File

@@ -0,0 +1,2 @@
export * from "./authenticated";
export * from "./home";

12
src/router.jsx Normal file
View File

@@ -0,0 +1,12 @@
import React from "react";
import { Route, Routes } from "react-router-dom";
import { Home, Login, Register } from "./pages";
export const Router = () => (
<Routes>
<Route index element={<Home />} />
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
</Routes>
);

8
src/setupAxios.js Normal file
View File

@@ -0,0 +1,8 @@
import axios from "axios";
const setupAxios = () => {
axios.defaults.baseURL = import.meta.env.VITE_API_URL;
axios.defaults.withCredentials = true;
};
export default setupAxios;