diff --git a/backend/auth.py b/backend/auth.py new file mode 100644 index 0000000..e2fb864 --- /dev/null +++ b/backend/auth.py @@ -0,0 +1,74 @@ +import os +import json + +from fastapi import APIRouter, Depends, HTTPException, status, Header +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel +from passlib.context import CryptContext as PasswordContext +from cryptography.fernet import Fernet, InvalidToken, InvalidSignature + +from models import User + +AUTH_ERROR = HTTPException(status.HTTP_400_BAD_REQUEST) + +password_context = PasswordContext(schemes=["bcrypt"], deprecated="auto") + +SECRET_KEY = os.environ["SECRET_KEY"] +crypto_context = Fernet(SECRET_KEY) + +users_db = { + "kai": { + "username": "kai", + "hashed_password": "$2b$12$mh5hSniE.1SqxK3IDDTIO.1jDKgU0KX2eZev3yFu4Z1ZUPXWMw2Xa", + } +} + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login") + + +class UserInDb(User): + hashed_password: str + + +def get_current_user(token: str = Depends(oauth2_scheme)): + try: + token_data = json.loads(crypto_context.decrypt(token.encode())) + except (InvalidToken, InvalidSignature): + raise AUTH_ERROR + return User(username=token_data["username"]) + + +auth_router = APIRouter() + + +class TokenLoginResponse(BaseModel): + access_token: str + token_type: str + + +@auth_router.post( + "/login", + tags=["auth"], + responses={ + status.HTTP_400_BAD_REQUEST: {}, + status.HTTP_200_OK: { + "model": TokenLoginResponse, + }, + }, +) +async def login(form_data: OAuth2PasswordRequestForm = Depends()): + + if form_data.username not in users_db: + raise AUTH_ERROR + + user = UserInDb(**users_db[form_data.username]) + + if not password_context.verify(form_data.password, user.hashed_password): + raise AUTH_ERROR + + token = crypto_context.encrypt(json.dumps({"username": user.username}).encode()) + + return { + "access_token": token, + "token_type": "bearer", + } diff --git a/backend/main.py b/backend/main.py index 5a2ee50..3d7d658 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,7 +1,9 @@ -from fastapi import FastAPI, Response -from pydantic import BaseSettings - +from fastapi import FastAPI, Depends, Response from fastapi.staticfiles import StaticFiles +from pydantic import BaseSettings, BaseModel + +from models import User +from auth import get_current_user, auth_router class Settings(BaseSettings): @@ -11,6 +13,19 @@ class Settings(BaseSettings): settings = Settings() app = FastAPI() + +app.include_router( + auth_router, + prefix="/auth", + tags=["auth"] +) + + +@app.get("/test") +async def read_test(user = Depends(get_current_user)): + return {"name": user.username, "foo": "bar"} + + if settings.dev_mode: import httpx diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..4ae707c --- /dev/null +++ b/backend/models.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class User(BaseModel): + username: str \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 38a4a7d..57bfc8e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,16 +1,22 @@ anyio==3.4.0 asgiref==3.4.1 certifi==2021.10.8 +cffi==1.15.0 charset-normalizer==2.0.9 click==8.0.3 +cryptography==36.0.1 fastapi==0.70.1 h11==0.12.0 httpcore==0.14.3 httpx==0.21.1 idna==3.3 +passlib==1.7.4 +pycparser==2.21 pydantic==1.8.2 +python-multipart==0.0.5 rfc3986==1.5.0 +six==1.16.0 sniffio==1.2.0 starlette==0.16.0 typing-extensions==4.0.1 -uvicorn==0.16.0 +uvicorn==0.15.0 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 891155e..7401d35 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,17 @@ -import React from 'react'; +import { useState } from 'react'; +import Login from './Login'; +import MainView from './MainView'; + export const App: React.FC = () => { - return ( -
- hallo i bims 1 frontend -
- ); + + const [loginToken, setLoginToken] = useState(null); + + if (loginToken === null) { + return + } else { + return { setLoginToken(null); }} /> + } } export default App; diff --git a/frontend/src/Login.tsx b/frontend/src/Login.tsx new file mode 100644 index 0000000..d396edf --- /dev/null +++ b/frontend/src/Login.tsx @@ -0,0 +1,68 @@ +import React, { useState } from "react"; + +type LoginResponse = { + access_token: string, + token_type: "bearer", +}; + +type LoginError = { + detail: string +} + +const LOGIN_ENDPOINT = "auth/login" + +export const Login: React.FC<{ + setLoginToken: (token: string) => void +}> = ({ setLoginToken }) => { + + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + + const handleSubmit = async (e: React.MouseEvent) => { + e.preventDefault(); + + let formData = new FormData(); + formData.append("grant_type", "password"); + formData.append("username", username); + formData.append("password", password); + + const response = await fetch(LOGIN_ENDPOINT, { + method: "POST", + cache: "no-cache", + body: formData, + }); + + if (response.ok) { + const data = await response.json() as LoginResponse; + setLoginToken(data.access_token); + } else { + const data = await response.json() as LoginError; + alert(data.detail); + } + } + + return <> +

login pls

+
+ + { setUsername(e.target.value) }}> + + + { setPassword(e.target.value) }}> + + +
+ +} + +export default Login; \ No newline at end of file diff --git a/frontend/src/MainView.tsx b/frontend/src/MainView.tsx new file mode 100644 index 0000000..ca103d2 --- /dev/null +++ b/frontend/src/MainView.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from "react"; + +const ENDPOINT = "test" + +export const MainView: React.FC<{ + loginToken: string, + logout: () => void, +}> = ({ loginToken, logout }) => { + + const [data, setData] = useState<{ name: string, foo: string } | undefined>(); + + useEffect(() => { + const fetchData = async () => { + const response = await fetch(ENDPOINT, { + headers: { + Authorization: `Bearer ${loginToken}`, + } + }); + + if (response.status !== 200) { + alert("big oof"); + } + + setData(await response.json()); + } + + fetchData(); + }, [loginToken]); + + if (!data) { + return <> + fetching data... + + } else { + return <> +

+ Token: {data.name} +

+

+ Foo: {data.foo} +

+ + } +} + +export default MainView; \ No newline at end of file