From 9f28dfe7e879b7ccc073cb887aeb41bc0b038535 Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 22 May 2023 16:51:51 +0000 Subject: [PATCH] feat: use requests with auth header for home --- front/app/orgs/[orgslug]/(withmenu)/page.tsx | 18 +++++++---- front/services/auth/auth.ts | 3 +- front/services/courses/collections.ts | 9 ++++-- front/services/courses/courses.ts | 11 +++++-- front/services/utils/ts/requests.ts | 14 ++++++++ src/routers/auth.py | 34 ++++++++++++++------ src/security/auth.py | 34 ++++++++++---------- 7 files changed, 83 insertions(+), 40 deletions(-) diff --git a/front/app/orgs/[orgslug]/(withmenu)/page.tsx b/front/app/orgs/[orgslug]/(withmenu)/page.tsx index ee1d2d7f..896dced9 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/page.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/page.tsx @@ -1,17 +1,16 @@ export const dynamic = 'force-dynamic'; import { Metadata, ResolvingMetadata } from 'next'; -import { Menu } from "@components/UI/Elements/Menu"; import { getBackendUrl, getUriWithOrg } from "@services/config/config"; -import { getOrgCourses } from "@services/courses/courses"; +import { getCourse, getOrgCourses, getOrgCoursesWithAuthHeader } from "@services/courses/courses"; import CoursesLogo from "public/svg/courses.svg"; import CollectionsLogo from "public/svg/collections.svg"; import Link from "next/link"; import Image from "next/image"; -import { log } from "console"; -import AuthProvider from "@components/Security/AuthProvider"; -import { getOrgCollections } from "@services/courses/collections"; +import { getOrgCollections, getOrgCollectionsWithAuthHeader } from "@services/courses/collections"; import { getOrganizationContextInfo } from '@services/organizations/orgs'; +import { cookies } from 'next/headers'; + type MetadataProps = { params: { orgslug: string }; searchParams: { [key: string]: string | string[] | undefined }; @@ -21,6 +20,7 @@ export async function generateMetadata( { params }: MetadataProps, ): Promise { + // Get Org context information const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] }); return { @@ -31,8 +31,12 @@ export async function generateMetadata( const OrgHomePage = async (params: any) => { const orgslug = params.params.orgslug; - const courses = await getOrgCourses(orgslug, { revalidate: 360 , tags: ['courses'] }); - const collections = await getOrgCollections(); + const cookieStore = cookies(); + const access_token_cookie: any = cookieStore.get('access_token_cookie'); + + const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 360, tags: ['courses'] }, access_token_cookie.value); + const collections = await getOrgCollectionsWithAuthHeader(access_token_cookie.value); + // function to remove "course_" from the course_id function removeCoursePrefix(course_id: string) { diff --git a/front/services/auth/auth.ts b/front/services/auth/auth.ts index c952b2f8..5c16970c 100644 --- a/front/services/auth/auth.ts +++ b/front/services/auth/auth.ts @@ -12,8 +12,7 @@ export async function loginAndGetToken(username: string, password: string): Prom // Request Config // get origin - const origin = window.location.origin; - const HeadersConfig = new Headers({ "Content-Type": "application/x-www-form-urlencoded" , Origin: origin }); + const HeadersConfig = new Headers({ "Content-Type": "application/x-www-form-urlencoded" }); const urlencoded = new URLSearchParams({ username: username, password: password }); const requestOptions: any = { diff --git a/front/services/courses/collections.ts b/front/services/courses/collections.ts index b3e4c027..73805141 100644 --- a/front/services/courses/collections.ts +++ b/front/services/courses/collections.ts @@ -1,5 +1,5 @@ import { getAPIUrl } from "../config/config"; -import { RequestBody, errorHandling } from "@services/utils/ts/requests"; +import { RequestBody, RequestBodyWithAuthHeader, errorHandling } from "@services/utils/ts/requests"; /* This file includes only POST, PUT, DELETE requests @@ -19,7 +19,7 @@ export async function createCollection(collection: any) { return res; } -// Get collections +// Get collections // TODO : add per org filter export async function getOrgCollections() { const result: any = await fetch(`${getAPIUrl()}collections/page/1/limit/10`, { next: { revalidate: 10 } }); @@ -27,3 +27,8 @@ export async function getOrgCollections() { return res; } +export async function getOrgCollectionsWithAuthHeader(access_token: string) { + const result: any = await fetch(`${getAPIUrl()}collections/page/1/limit/10`, RequestBodyWithAuthHeader("GET", null, { revalidate: 10 }, access_token)); + const res = await errorHandling(result); + return res; +} diff --git a/front/services/courses/courses.ts b/front/services/courses/courses.ts index ea1b4941..1d6b75a6 100644 --- a/front/services/courses/courses.ts +++ b/front/services/courses/courses.ts @@ -1,5 +1,5 @@ import { getAPIUrl } from "@services/config/config"; -import { RequestBody, RequestBodyForm, errorHandling } from "@services/utils/ts/requests"; +import { RequestBody, RequestBodyForm, RequestBodyWithAuthHeader, errorHandling } from "@services/utils/ts/requests"; /* This file includes only POST, PUT, DELETE requests @@ -9,10 +9,17 @@ import { RequestBody, RequestBodyForm, errorHandling } from "@services/utils/ts/ export async function getOrgCourses(org_id: number, next: any) { const result: any = await fetch(`${getAPIUrl()}courses/org_slug/${org_id}/page/1/limit/10`, RequestBody("GET", null, next)); const res = await errorHandling(result); - + return res; } +export async function getOrgCoursesWithAuthHeader(org_id: number, next: any, access_token: string) { + const result: any = await fetch(`${getAPIUrl()}courses/org_slug/${org_id}/page/1/limit/10`, RequestBodyWithAuthHeader("GET", null, next, access_token)); + const res = await errorHandling(result); + return res; +} + + export async function getCourse(course_id: string, next: any) { const result: any = await fetch(`${getAPIUrl()}courses/${course_id}`, RequestBody("GET", null, next)); const res = await errorHandling(result); diff --git a/front/services/utils/ts/requests.ts b/front/services/utils/ts/requests.ts index 07ba1142..4400c59c 100644 --- a/front/services/utils/ts/requests.ts +++ b/front/services/utils/ts/requests.ts @@ -18,6 +18,20 @@ export const RequestBody = (method: string, data: any, next: any) => { return options; }; +export const RequestBodyWithAuthHeader = (method: string, data: any, next: any, token: string) => { + let HeadersConfig = new Headers({ Authorization: `Bearer ${token}` }); + let options: any = { + method: method, + headers: HeadersConfig, + redirect: "follow", + credentials: "include", + body: data, + // Next.js + next: next, + }; + return options; +}; + export const RequestBodyForm = (method: string, data: any, next: any) => { let HeadersConfig = new Headers({}); let options: any = { diff --git a/src/routers/auth.py b/src/routers/auth.py index b563212d..5f5eacb1 100644 --- a/src/routers/auth.py +++ b/src/routers/auth.py @@ -1,13 +1,14 @@ from urllib.request import Request -from fastapi import Depends, APIRouter, HTTPException, status, Request -from fastapi.security import OAuth2PasswordRequestForm +from fastapi import Depends, APIRouter, HTTPException, Response, status, Request +from fastapi.security import OAuth2PasswordRequestForm from src.security.auth import * from src.services.users.users import * router = APIRouter() -@router.post('/refresh') + +@router.post("/refresh") def refresh(Authorize: AuthJWT = Depends()): """ The jwt_refresh_token_required() function insures a valid refresh @@ -18,11 +19,17 @@ def refresh(Authorize: AuthJWT = Depends()): Authorize.jwt_refresh_token_required() current_user = Authorize.get_jwt_subject() - new_access_token = Authorize.create_access_token(subject=current_user) # type: ignore + new_access_token = Authorize.create_access_token(subject=current_user) # type: ignore return {"access_token": new_access_token} -@router.post('/login') -async def login(request: Request,Authorize: AuthJWT = Depends(), form_data: OAuth2PasswordRequestForm = Depends()): + +@router.post("/login") +async def login( + request: Request, + response: Response, + Authorize: AuthJWT = Depends(), + form_data: OAuth2PasswordRequestForm = Depends(), +): user = await authenticate_user(request, form_data.username, form_data.password) if not user: raise HTTPException( @@ -34,10 +41,17 @@ async def login(request: Request,Authorize: AuthJWT = Depends(), form_data: OAut 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) - Authorize.set_access_cookies(access_token) - return {"access_token": access_token , "refresh_token": refresh_token} + # set cookies using fastapi + response.set_cookie(key="access_token_cookie", value=access_token , httponly=False) -@router.delete('/logout') + result = { + "user": user, + "tokens": {"access_token": access_token, "refresh_token": refresh_token}, + } + return result + + +@router.delete("/logout") def logout(Authorize: AuthJWT = Depends()): """ Because the JWT are stored in an httponly cookie now, we cannot @@ -47,4 +61,4 @@ def logout(Authorize: AuthJWT = Depends()): Authorize.jwt_required() Authorize.unset_jwt_cookies() - return {"msg":"Successfully logout"} \ No newline at end of file + return {"msg": "Successfully logout"} diff --git a/src/security/auth.py b/src/security/auth.py index 7100a566..be7d37b1 100644 --- a/src/security/auth.py +++ b/src/security/auth.py @@ -10,26 +10,26 @@ from fastapi_jwt_auth import AuthJWT oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") + #### JWT Auth #################################################### class Settings(BaseModel): authjwt_secret_key: str = "secret" - authjwt_token_location = {"cookies"} + authjwt_token_location = {"cookies", "headers"} authjwt_cookie_csrf_protect = False - authjwt_access_token_expires = False # (pre-alpha only) # TODO: set to 1 hour - authjwt_cookie_samesite = "none" + authjwt_access_token_expires = False # (pre-alpha only) # TODO: set to 1 hour + authjwt_cookie_samesite = "lax" authjwt_cookie_secure = True - - -@AuthJWT.load_config # type: ignore + authjwt_cookie_domain = ".localhost" + + +@AuthJWT.load_config # type: ignore def get_config(): return Settings() - #### JWT Auth #################################################### - #### Classes #################################################### @@ -41,10 +41,11 @@ class Token(BaseModel): class TokenData(BaseModel): username: str | None = None + #### Classes #################################################### -async def authenticate_user(request: Request,email: str, password: str): +async def authenticate_user(request: Request, email: str, password: str): user = await security_get_user(request, email) if not user: return False @@ -64,29 +65,28 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None): return encoded_jwt - - async def get_current_user(request: Request, Authorize: AuthJWT = Depends()): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) - + try: Authorize.jwt_optional() username = Authorize.get_jwt_subject() or None token_data = TokenData(username=username) # type: ignore except JWTError: raise credentials_exception - if username: - user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email + if username: + user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email if user is None: raise credentials_exception return PublicUser(**user.dict()) else: return AnonymousUser() - -async def non_public_endpoint(current_user: PublicUser ): + + +async def non_public_endpoint(current_user: PublicUser): if isinstance(current_user, AnonymousUser): - raise HTTPException(status_code=401, detail="Not authenticated") \ No newline at end of file + raise HTTPException(status_code=401, detail="Not authenticated")