Implement frontend user login

This commit is contained in:
Kai Vogelgesang 2021-12-20 13:04:55 +01:00
parent dea9fd0bde
commit 774032a9fe
Signed by: kai
GPG Key ID: 0A95D3B6E62C0879
7 changed files with 229 additions and 10 deletions

74
backend/auth.py Normal file
View File

@ -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",
}

View File

@ -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

4
backend/models.py Normal file
View File

@ -0,0 +1,4 @@
from pydantic import BaseModel
class User(BaseModel):
username: str

View File

@ -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

View File

@ -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 (
<div className="App">
hallo i bims 1 frontend
</div>
);
const [loginToken, setLoginToken] = useState<string | null>(null);
if (loginToken === null) {
return <Login setLoginToken={setLoginToken} />
} else {
return <MainView loginToken={loginToken} logout={() => { setLoginToken(null); }} />
}
}
export default App;

68
frontend/src/Login.tsx Normal file
View File

@ -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<string>("");
const [password, setPassword] = useState<string>("");
const handleSubmit = async (e: React.MouseEvent<HTMLButtonElement>) => {
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 <>
<h1>login pls</h1>
<form>
<label htmlFor="username">Username</label>
<input
type="text"
id="username"
placeholder="joe"
value={username}
onChange={(e) => { setUsername(e.target.value) }}>
</input>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
placeholder="mama"
value={password}
onChange={(e) => { setPassword(e.target.value) }}>
</input>
<button type="submit" onClick={handleSubmit}>Ok</button>
</form>
</>
}
export default Login;

46
frontend/src/MainView.tsx Normal file
View File

@ -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 <>
<p>
Token: {data.name}
</p>
<p>
Foo: {data.foo}
</p>
</>
}
}
export default MainView;