Implement frontend user login
This commit is contained in:
parent
dea9fd0bde
commit
774032a9fe
74
backend/auth.py
Normal file
74
backend/auth.py
Normal 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",
|
||||
}
|
@ -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
4
backend/models.py
Normal file
@ -0,0 +1,4 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
@ -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
|
||||
|
@ -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
68
frontend/src/Login.tsx
Normal 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
46
frontend/src/MainView.tsx
Normal 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;
|
Loading…
Reference in New Issue
Block a user