diff --git a/README.md b/README.md index 0f2a709..a54d268 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ The app which fulfills the following requirements for a - Deployment of a productive version with precompiled frontend server - Deployment to Azure App Service Free Plan with Actions on Pull Request - Handle Front and Backend routing in FastAPI both together +- Handle 404 page +- A frame for further development with server side Authentication ## Prerequisites Here are the prerequisites that you have to install before running the app diff --git a/backend/api.py b/backend/api.py new file mode 100644 index 0000000..308ff75 --- /dev/null +++ b/backend/api.py @@ -0,0 +1,6 @@ +from fastapi import APIRouter + +api_router = APIRouter() +@api_router.get("/data") +def read_hello(): + return {"message": "Hello from API"} \ No newline at end of file diff --git a/backend/auth.py b/backend/auth.py new file mode 100644 index 0000000..74045e0 --- /dev/null +++ b/backend/auth.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel + +class LoginItem(BaseModel): + message: str + +auth_router = APIRouter() +@auth_router.post("/login") +async def login(loginitem: LoginItem): + data = jsonable_encoder(loginitem) + return {"token": "ABCDEF", "data": data} + +@auth_router.post("/logout") +async def login(): + return {"message": 'Loggout ok'} \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index dc32e61..f989625 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,16 +3,17 @@ from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, RedirectResponse from pathlib import Path +from auth import auth_router +from api import api_router # Init FastAPI app = FastAPI() origins = ["http://localhost:5173", "localhost:5173"] app.add_middleware(CORSMiddleware,allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) -# API Router -api_router = APIRouter() -@api_router.get("/hello") -def read_hello(): - return {"message": "Hello from API"} +# Register Auth Router +app.include_router(auth_router, prefix="/auth") + +# Register API Router app.include_router(api_router, prefix="/api") # Frontend Router diff --git a/frontend/index.html b/frontend/index.html index f8f4500..2c68b6d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,9 @@ - + - EntraID-Web-Auth-Reference + FastAPI-Reference
diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..31c71c0 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg new file mode 100644 index 0000000..22bb158 --- /dev/null +++ b/frontend/public/logo.svg @@ -0,0 +1,775 @@ + + + + diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/404.jsx b/frontend/src/404.jsx new file mode 100644 index 0000000..7cec48f --- /dev/null +++ b/frontend/src/404.jsx @@ -0,0 +1,10 @@ +import { Link } from "react-router-dom"; +export default function NotFound() { + return ( + <> +

404

+ Goto Home + + ); + } + \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2dd6d00..04565fc 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,7 +1,9 @@ import { useState, useEffect } from 'react' import { Routes, Route } from "react-router-dom"; -import Login from './components/Login'; +import { RequireToken, setToken } from "./components/auth"; +import Login from './Login'; import Home from './pages/Home'; +import NotFound from './404'; import './App.css' function App() { @@ -9,8 +11,9 @@ function App() { <>
- }/> + }/> }/> + }/>
diff --git a/frontend/src/Login.jsx b/frontend/src/Login.jsx new file mode 100644 index 0000000..a1c31b3 --- /dev/null +++ b/frontend/src/Login.jsx @@ -0,0 +1,37 @@ +import { useNavigate } from "react-router"; +import { Link } from "react-router-dom"; +import { fetchToken, login, logout } from "./components/auth"; +import logo from './assets/logo.png' +export default function Login() { + const navigate = useNavigate(); + const token = fetchToken(); + + const loginFunc = async () => { + await login(); + navigate('/'); + } + + return ( + <> +
+ + logo + +
+

Login to FastAPI-Reference

+

Just press the button to automatically logon without any forms

+ { + token ? (<> +

you are logged in

+ Goto Home + + ) + : + ( + + ) + } + + ); + } + \ No newline at end of file diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png new file mode 100644 index 0000000..13af071 Binary files /dev/null and b/frontend/src/assets/logo.png differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/components/Login.jsx b/frontend/src/components/Login.jsx deleted file mode 100644 index 9390ec5..0000000 --- a/frontend/src/components/Login.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Link } from 'react-router-dom' -export default function Login() { - return ( - <> -

login page

- Go back - - ); - } - \ No newline at end of file diff --git a/frontend/src/components/api.js b/frontend/src/components/api.js new file mode 100644 index 0000000..397e855 --- /dev/null +++ b/frontend/src/components/api.js @@ -0,0 +1,14 @@ +export const backendUrl = import.meta.env.MODE === 'production' ? '': 'http://localhost:8000'; + +export const apiHello = async () => { + try { + const response = await fetch(`${backendUrl}/api/data`); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const data = await response.json(); + return data; + } catch (error) { + console.error('Error fetching data:', error); + } + }; \ No newline at end of file diff --git a/frontend/src/components/auth.js b/frontend/src/components/auth.js new file mode 100644 index 0000000..3e5f6eb --- /dev/null +++ b/frontend/src/components/auth.js @@ -0,0 +1,75 @@ +import { useEffect } from "react"; +import { useLocation, useNavigate } from "react-router-dom" +import { backendUrl } from "./api" + +export const setToken = (token) => { + console.log(token, "setToken"); + localStorage.setItem('mytoken', token) // make up your own token +} + +export const clearToken = () => { + localStorage.removeItem('mytoken'); + }; + +export const fetchToken = () => { + let token = localStorage.getItem('mytoken') + console.log(token, "fetchToken"); + return token +} + +export const RequireToken = ({children}) =>{ + const auth = fetchToken() + const location = useLocation() + const navigate = useNavigate(); + + useEffect(() => { + if (!auth) { + navigate("/login", { state: { from: location } }); + } + }, [auth, navigate, location]); + + return children; +}; + +export const login = async () => { + try { + //const navigate = useNavigate(); + const response = await fetch(`${backendUrl}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ message: "login attempt" }), + }); + + if (!response.ok) { throw new Error("Network response was not ok"); } + + const data = await response.json(); + console.log(data.token, "data.token"); + if (data.token) { + setToken(data.token); + //navigate("/"); + } + } catch (error) { + console.error("Error during login:", error); + } + }; + + +export const logout = async () => { + try { + //const navigate = useNavigate(); + const response = await fetch(`${backendUrl}/auth/logout`, { + method: "POST", + headers: { "Content-Type": "application/json" } + }); + + if (!response.ok) { throw new Error("Network response was not ok"); } + + const data = await response.json(); + console.log(data.message); + clearToken(); + //navigate("/"); + } catch (error) { + console.error("Error during logout:", error); + } + }; + diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 8e9379d..809d842 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -8,6 +8,6 @@ createRoot(document.getElementById('root')).render( - , - , + + ) diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index a7da3ad..f59aed0 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,59 +1,64 @@ import { useState, useEffect } from 'react' -import reactLogo from '../assets/react.svg' -import { Link } from 'react-router-dom' +import logo from '../assets/logo.png' +import { Link, useNavigate } from 'react-router-dom' import './Home.css' +import { apiHello } from '../components/api' +import { fetchToken, logout, login } from "../components/auth"; function Home() { - const [count, setCount] = useState(0) - const [hello, setHello] = useState(null) + const [count, setCount] = useState(0); + const [data, setData] = useState(null); + const navigate = useNavigate(); + const setCountFunc = () => { setCount(count + 1); } - const apiHost= import.meta.env.MODE === 'production' ? '': 'http://localhost:8000'; + const logoutFunc = async () => { + await logout(); + navigate('/'); + } + + const fetchData = async () => { + const data = await apiHello() + setData(data) + } useEffect(() => { - // Fetch JSON data from the API endpoint - fetch(`${apiHost}/api/hello`) - .then(response => { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }) - .then(data => setHello(data)) - .catch(error => console.error('Error fetching data:', error)); - }, []); // Empty dependency array runs this effect once on mount + fetchData(); + }, []); return ( <>
- {/* W - Vite logo - */} - - React logo - + + logo +
-

Vite + React

+

FastAPI-Reference

- Go to Login + { + fetchToken() ? (<> +

you are logged in

+ + + ) + : + <> + }
-

- Edit src/App.jsx and save to test HMR +

+ Click on the Logo to learn more

+

{import.meta.env.MODE}

+

+ {data ? data.message : 'Loading...'} +

-

- Click on the Vite and React logos to learn more -

-

{import.meta.env.MODE}

-

- {hello ? hello.message : 'Loading...'} -

) }