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 fastapi import FastAPI, Depends, Response
|
||||||
from pydantic import BaseSettings
|
|
||||||
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
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):
|
class Settings(BaseSettings):
|
||||||
@ -11,6 +13,19 @@ class Settings(BaseSettings):
|
|||||||
settings = Settings()
|
settings = Settings()
|
||||||
app = FastAPI()
|
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:
|
if settings.dev_mode:
|
||||||
import httpx
|
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
|
anyio==3.4.0
|
||||||
asgiref==3.4.1
|
asgiref==3.4.1
|
||||||
certifi==2021.10.8
|
certifi==2021.10.8
|
||||||
|
cffi==1.15.0
|
||||||
charset-normalizer==2.0.9
|
charset-normalizer==2.0.9
|
||||||
click==8.0.3
|
click==8.0.3
|
||||||
|
cryptography==36.0.1
|
||||||
fastapi==0.70.1
|
fastapi==0.70.1
|
||||||
h11==0.12.0
|
h11==0.12.0
|
||||||
httpcore==0.14.3
|
httpcore==0.14.3
|
||||||
httpx==0.21.1
|
httpx==0.21.1
|
||||||
idna==3.3
|
idna==3.3
|
||||||
|
passlib==1.7.4
|
||||||
|
pycparser==2.21
|
||||||
pydantic==1.8.2
|
pydantic==1.8.2
|
||||||
|
python-multipart==0.0.5
|
||||||
rfc3986==1.5.0
|
rfc3986==1.5.0
|
||||||
|
six==1.16.0
|
||||||
sniffio==1.2.0
|
sniffio==1.2.0
|
||||||
starlette==0.16.0
|
starlette==0.16.0
|
||||||
typing-extensions==4.0.1
|
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 = () => {
|
export const App: React.FC = () => {
|
||||||
return (
|
|
||||||
<div className="App">
|
const [loginToken, setLoginToken] = useState<string | null>(null);
|
||||||
hallo i bims 1 frontend
|
|
||||||
</div>
|
if (loginToken === null) {
|
||||||
);
|
return <Login setLoginToken={setLoginToken} />
|
||||||
|
} else {
|
||||||
|
return <MainView loginToken={loginToken} logout={() => { setLoginToken(null); }} />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
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