mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-18 11:59:26 +00:00
feat: init auth across the app
This commit is contained in:
parent
9479a4b127
commit
3b85a73ec1
12 changed files with 204 additions and 29 deletions
16
app.py
16
app.py
|
|
@ -1,6 +1,9 @@
|
|||
from urllib.request import Request
|
||||
from fastapi import FastAPI
|
||||
from src.main import global_router
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi_jwt_auth.exceptions import AuthJWTException
|
||||
|
||||
########################
|
||||
# Pre-Alpha Version 0.1.0
|
||||
|
|
@ -8,7 +11,8 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||
# (c) LearnHouse 2022
|
||||
########################
|
||||
|
||||
# Init
|
||||
#
|
||||
# Global Config
|
||||
app = FastAPI(
|
||||
title="LearnHouse",
|
||||
description="LearnHouse is a new open-source platform tailored for learning experiences.",
|
||||
|
|
@ -24,8 +28,16 @@ app.add_middleware(
|
|||
allow_headers=["*"]
|
||||
)
|
||||
|
||||
app.include_router(global_router)
|
||||
#
|
||||
# Exception Handler
|
||||
@app.exception_handler(AuthJWTException)
|
||||
def authjwt_exception_handler(request: Request, exc: AuthJWTException):
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"detail": exc.message}
|
||||
)
|
||||
|
||||
app.include_router(global_router)
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import Link from "next/link";
|
||||
import { AuthContext } from "../security/AuthProvider";
|
||||
|
||||
export const HeaderProfileBox = () => {
|
||||
const auth: any = React.useContext(AuthContext);
|
||||
|
||||
return (
|
||||
<ProfileArea>
|
||||
{" "}
|
||||
<span>HeaderProfileBox</span>{" "}
|
||||
<span>HeaderProfileBox {String(auth.isAuthenticated)}</span>{" "}
|
||||
<UnidentifiedArea>
|
||||
<ul>
|
||||
<li>
|
||||
|
|
|
|||
47
front/components/security/AuthProvider.tsx
Normal file
47
front/components/security/AuthProvider.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { getRefreshToken, getUserInfo } from "../../services/auth/auth";
|
||||
|
||||
export const AuthContext: any = React.createContext({});
|
||||
|
||||
export interface Auth {
|
||||
access_token: string;
|
||||
isAuthenticated: boolean;
|
||||
userInfo: {};
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const AuthProvider = (props: any) => {
|
||||
const [auth, setAuth] = React.useState<Auth>({ access_token: "", isAuthenticated: false, userInfo: {}, isLoading: true });
|
||||
|
||||
async function checkRefreshToken() {
|
||||
let data = await getRefreshToken();
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
async function checkAuth() {
|
||||
let access_token = await checkRefreshToken();
|
||||
let isAuthenticated = false;
|
||||
let userInfo = {};
|
||||
let isLoading = false;
|
||||
|
||||
if (access_token) {
|
||||
userInfo = await getUserInfo(access_token);
|
||||
isAuthenticated = true;
|
||||
setAuth({ access_token, isAuthenticated, userInfo, isLoading });
|
||||
} else if (!access_token) {
|
||||
isAuthenticated = false;
|
||||
setAuth({ access_token, isAuthenticated, userInfo, isLoading });
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mvp) : fix performance issues > no need to check auth on every render
|
||||
useEffect(() => {
|
||||
if (!auth.isAuthenticated) {
|
||||
checkAuth();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <AuthContext.Provider value={auth}>{props.children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
export default AuthProvider;
|
||||
|
|
@ -6,8 +6,10 @@ import Link from "next/link";
|
|||
export const Menu = () => {
|
||||
return (
|
||||
<GlobalHeader>
|
||||
|
||||
<LogoArea>
|
||||
<Logo>
|
||||
<img style={{ width: "30px", opacity: "0.9", margin: "10px", paddingRight: "4px" }} src="./learnhouse_icon.png" alt="" />
|
||||
<Link href={"/"}>
|
||||
<a>
|
||||
<img src="./learnhouse_logo.png" alt="" />
|
||||
|
|
@ -50,7 +52,9 @@ const Logo = styled.div`
|
|||
display: flex;
|
||||
place-items: center;
|
||||
padding-left: 20px;
|
||||
|
||||
a{
|
||||
margin: 0;
|
||||
}
|
||||
img {
|
||||
width: 100px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,23 +2,26 @@ import React from "react";
|
|||
import Head from "next/head";
|
||||
import { Header } from "./header";
|
||||
import styled from "styled-components";
|
||||
import AuthProvider from "../security/AuthProvider";
|
||||
|
||||
const Layout = (props: any) => {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>{props.title}</title>
|
||||
<meta name="description" content={props.description} />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Header></Header>
|
||||
<Main className="min-h-screen">{props.children}</Main>
|
||||
<AuthProvider>
|
||||
<Head>
|
||||
<title>{props.title}</title>
|
||||
<meta name="description" content={props.description} />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Header></Header>
|
||||
<Main className="min-h-screen">{props.children}</Main>
|
||||
|
||||
<Footer>
|
||||
<a href="" target="_blank" rel="noopener noreferrer">
|
||||
<img src="/learnhouse_icon.png" alt="Learnhouse Logo" />
|
||||
</a>
|
||||
</Footer>
|
||||
<Footer>
|
||||
<a href="" target="_blank" rel="noopener noreferrer">
|
||||
<img src="/learnhouse_icon.png" alt="Learnhouse Logo" />
|
||||
</a>
|
||||
</Footer>
|
||||
</AuthProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -33,9 +36,9 @@ const Footer = styled.footer`
|
|||
margin: 20px;
|
||||
font-size: 16px;
|
||||
|
||||
img{
|
||||
width: 40px;
|
||||
opacity: 0.40;
|
||||
img {
|
||||
width: 20px;
|
||||
opacity: 0.4;
|
||||
display: inline;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 116 KiB |
|
|
@ -5,7 +5,8 @@ interface LoginAndGetTokenResponse {
|
|||
token_type: "string";
|
||||
}
|
||||
|
||||
// TODO : everything in this file need to be refactored / mvp phase
|
||||
// ⚠️ mvp phase code
|
||||
// TODO : everything in this file need to be refactored including security issues fix
|
||||
|
||||
export async function loginAndGetToken(username: string, password: string): Promise<LoginAndGetTokenResponse> {
|
||||
// Request Config
|
||||
|
|
@ -21,21 +22,34 @@ export async function loginAndGetToken(username: string, password: string): Prom
|
|||
};
|
||||
|
||||
// fetch using await and async
|
||||
const response = await fetch(`${getAPIUrl()}auth/token`, requestOptions);
|
||||
const response = await fetch(`${getAPIUrl()}auth/login`, requestOptions);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getUserInfo(token: string): Promise<any> {
|
||||
const HeadersConfig = new Headers({ Authorization: `Bearer ${token}`, Origin: "http://localhost:3000" });
|
||||
|
||||
const requestOptions: any = {
|
||||
method: "GET",
|
||||
headers: HeadersConfig,
|
||||
redirect: "follow",
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
};
|
||||
|
||||
return fetch(`${getAPIUrl()}auth/users/me`, requestOptions)
|
||||
return fetch(`${getAPIUrl()}users/profile`, requestOptions)
|
||||
.then((result) => result.json())
|
||||
.catch((error) => console.log("error", error));
|
||||
}
|
||||
|
||||
export async function getRefreshToken(): Promise<any> {
|
||||
const requestOptions: any = {
|
||||
method: "POST",
|
||||
redirect: "follow",
|
||||
credentials: "include",
|
||||
};
|
||||
|
||||
return fetch(`${getAPIUrl()}auth/refresh`, requestOptions)
|
||||
.then((result) => result.json())
|
||||
.catch((error) => console.log("error", error));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
|
||||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500&display=swap');
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
font-family: 'DM Sans' , -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@ pymongo==4.1.1
|
|||
python-multipart
|
||||
python-jose
|
||||
passlib
|
||||
fastapi-jwt-auth
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
from urllib.request import Request
|
||||
from fastapi import Depends, APIRouter, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from src.services.auth import *
|
||||
|
|
@ -5,9 +6,10 @@ from src.services.users import *
|
|||
from datetime import timedelta
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# DEPRECATED
|
||||
@router.post("/token", response_model=Token)
|
||||
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
"""
|
||||
|
|
@ -29,3 +31,44 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(
|
|||
response.set_cookie(key="user_token", value=access_token, httponly=True, expires="3600",secure=True)
|
||||
|
||||
return response
|
||||
|
||||
@router.post('/refresh')
|
||||
def refresh(Authorize: AuthJWT = Depends()):
|
||||
"""
|
||||
The jwt_refresh_token_required() function insures a valid refresh
|
||||
token is present in the request before running any code below that function.
|
||||
we can use the get_jwt_subject() function to get the subject of the refresh
|
||||
token, and use the create_access_token() function again to make a new access token
|
||||
"""
|
||||
Authorize.jwt_refresh_token_required()
|
||||
|
||||
current_user = Authorize.get_jwt_subject()
|
||||
new_access_token = Authorize.create_access_token(subject=current_user)
|
||||
return {"access_token": new_access_token}
|
||||
|
||||
@router.post('/login')
|
||||
async def login(Authorize: AuthJWT = Depends(), form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
user = await authenticate_user(form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect Email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
access_token = Authorize.create_access_token(subject=form_data.username)
|
||||
refresh_token = Authorize.create_refresh_token(subject=form_data.username)
|
||||
Authorize.set_refresh_cookies(refresh_token)
|
||||
return {"access_token": access_token , "refresh_token": refresh_token}
|
||||
|
||||
@router.delete('/logout')
|
||||
def logout(Authorize: AuthJWT = Depends()):
|
||||
"""
|
||||
Because the JWT are stored in an httponly cookie now, we cannot
|
||||
log the user out by simply deleting the cookies in the frontend.
|
||||
We need the backend to send us a response to delete the cookies.
|
||||
"""
|
||||
Authorize.jwt_required()
|
||||
|
||||
Authorize.unset_jwt_cookies()
|
||||
return {"msg":"Successfully logout"}
|
||||
|
|
@ -9,7 +9,7 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
|
|||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# DEPRECATED
|
||||
@router.get("/me")
|
||||
async def api_get_current_user(current_user: User = Depends(get_current_user)):
|
||||
"""
|
||||
|
|
@ -17,6 +17,13 @@ async def api_get_current_user(current_user: User = Depends(get_current_user)):
|
|||
"""
|
||||
return current_user.dict()
|
||||
|
||||
@router.get("/profile")
|
||||
async def api_get_current_user(current_user: User = Depends(get_current_user_jwt)):
|
||||
"""
|
||||
Get current user
|
||||
"""
|
||||
return current_user.dict()
|
||||
|
||||
|
||||
@router.get("/username/{username}")
|
||||
async def api_get_user_by_username(username: str):
|
||||
|
|
|
|||
|
|
@ -5,9 +5,28 @@ from passlib.context import CryptContext
|
|||
from jose import JWTError, jwt
|
||||
from datetime import datetime, timedelta
|
||||
from src.services.users import *
|
||||
from fastapi import Cookie, FastAPI
|
||||
from src.services.security import *
|
||||
from fastapi_jwt_auth import AuthJWT
|
||||
from fastapi_jwt_auth.exceptions import AuthJWTException
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
|
||||
|
||||
#### JWT Auth ####################################################
|
||||
class Settings(BaseModel):
|
||||
authjwt_secret_key: str = "secret"
|
||||
authjwt_token_location = {"cookies", "headers"}
|
||||
authjwt_cookie_csrf_protect = False
|
||||
|
||||
@AuthJWT.load_config
|
||||
def get_config():
|
||||
return Settings()
|
||||
|
||||
|
||||
|
||||
#### JWT Auth ####################################################
|
||||
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
|
@ -42,7 +61,7 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
|||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
# DEPRECATED
|
||||
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -57,7 +76,28 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
|
|||
token_data = TokenData(username=username)
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
user = await security_get_user(username=token_data.username)
|
||||
user = await security_get_user(email=token_data.username)
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return PublicUser(**user.dict())
|
||||
|
||||
|
||||
async def get_current_user_jwt(Authorize: AuthJWT = Depends()):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
try:
|
||||
Authorize.jwt_required()
|
||||
username = Authorize.get_jwt_subject()
|
||||
token_data = TokenData(username=username)
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
user = await security_get_user(email=token_data.username) # treated as an email
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return PublicUser(**user.dict())
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue