feat/security #1
@@ -1,6 +1,6 @@
 | 
				
			|||||||
from datetime import timedelta, timezone, datetime
 | 
					from datetime import timedelta, timezone, datetime
 | 
				
			||||||
from typing import Annotated
 | 
					from typing import Annotated
 | 
				
			||||||
from fastapi import Depends, HTTPException, status
 | 
					from fastapi import Depends, HTTPException, Response, status
 | 
				
			||||||
from pydantic import BaseModel
 | 
					from pydantic import BaseModel
 | 
				
			||||||
import jwt
 | 
					import jwt
 | 
				
			||||||
from jwt.exceptions import InvalidTokenError
 | 
					from jwt.exceptions import InvalidTokenError
 | 
				
			||||||
@@ -102,7 +102,7 @@ async def get_current_active_user(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def login_for_access_token(
 | 
					async def login_for_access_token(
 | 
				
			||||||
    form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
 | 
					    form_data: Annotated[OAuth2PasswordRequestForm, Depends()], response: Response
 | 
				
			||||||
) -> Token:
 | 
					) -> Token:
 | 
				
			||||||
    user = authenticate_user(form_data.username, form_data.password)
 | 
					    user = authenticate_user(form_data.username, form_data.password)
 | 
				
			||||||
    if not user:
 | 
					    if not user:
 | 
				
			||||||
@@ -115,6 +115,9 @@ async def login_for_access_token(
 | 
				
			|||||||
    access_token = create_access_token(
 | 
					    access_token = create_access_token(
 | 
				
			||||||
        data={"sub": user.username}, expires_delta=access_token_expires
 | 
					        data={"sub": user.username}, expires_delta=access_token_expires
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    response.set_cookie(
 | 
				
			||||||
 | 
					        "Authorization", value=f"Bearer {access_token}", httponly=True, samesite="none"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    return Token(access_token=access_token, token_type="bearer")
 | 
					    return Token(access_token=access_token, token_type="bearer")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import { useEffect, useState } from "react";
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
import { baseUrl } from "./api";
 | 
					import { apiAuth, baseUrl, token } from "./api";
 | 
				
			||||||
 | 
					import useAuthContext from "./AuthContext";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//const debounce = <T extends (...args: any[]) => void>(
 | 
					//const debounce = <T extends (...args: any[]) => void>(
 | 
				
			||||||
//  func: T,
 | 
					//  func: T,
 | 
				
			||||||
@@ -61,18 +62,14 @@ export default function Analysis() {
 | 
				
			|||||||
  // Function to generate and fetch the graph image
 | 
					  // Function to generate and fetch the graph image
 | 
				
			||||||
  async function loadImage() {
 | 
					  async function loadImage() {
 | 
				
			||||||
    setLoading(true);
 | 
					    setLoading(true);
 | 
				
			||||||
    await fetch(`${baseUrl}api/analysis/image`, {
 | 
					    await apiAuth("analysis/image", params, "POST")
 | 
				
			||||||
      method: "POST",
 | 
					 | 
				
			||||||
      headers: {
 | 
					 | 
				
			||||||
        "Content-Type": "application/json",
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      body: JSON.stringify(params)
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
      .then((resp) => resp.json())
 | 
					 | 
				
			||||||
      .then((data) => {
 | 
					      .then((data) => {
 | 
				
			||||||
        setImage(data.image);
 | 
					        setImage(data.image);
 | 
				
			||||||
        setLoading(false);
 | 
					        setLoading(false);
 | 
				
			||||||
      });
 | 
					      }).catch((e) => {
 | 
				
			||||||
 | 
					        const { checkAuth } = useAuthContext();
 | 
				
			||||||
 | 
					        checkAuth();
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
@@ -92,6 +89,9 @@ export default function Analysis() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { user } = useAuthContext()!
 | 
				
			||||||
 | 
					  console.log(`logged in as ${user.username}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="stack column dropdown">
 | 
					    <div className="stack column dropdown">
 | 
				
			||||||
      <button onClick={() => setShowControlPanel(!showControlPanel)}>
 | 
					      <button onClick={() => setShowControlPanel(!showControlPanel)}>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								src/App.css
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								src/App.css
									
									
									
									
									
								
							@@ -12,16 +12,17 @@ body {
 | 
				
			|||||||
  height: 100%;
 | 
					  height: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
footer {
 | 
					 | 
				
			||||||
  font-size: x-small;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#root {
 | 
					#root {
 | 
				
			||||||
  max-width: 1280px;
 | 
					  max-width: 1280px;
 | 
				
			||||||
  margin: 0 auto;
 | 
					  margin: 0 auto;
 | 
				
			||||||
  padding: 8px;
 | 
					  padding: 8px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					footer {
 | 
				
			||||||
 | 
					  font-size: x-small;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.grey {
 | 
					.grey {
 | 
				
			||||||
  color: #444;
 | 
					  color: #444;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -37,6 +38,11 @@ footer {
 | 
				
			|||||||
  z-index: -1;
 | 
					  z-index: -1;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					input {
 | 
				
			||||||
 | 
					  padding: 0.2em 16px;
 | 
				
			||||||
 | 
					  margin-bottom: 0.5em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
h1,
 | 
					h1,
 | 
				
			||||||
h2,
 | 
					h2,
 | 
				
			||||||
h3 {
 | 
					h3 {
 | 
				
			||||||
@@ -120,7 +126,8 @@ h3 {
 | 
				
			|||||||
  margin: auto;
 | 
					  margin: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
button {
 | 
					button,
 | 
				
			||||||
 | 
					.button {
 | 
				
			||||||
  font-weight: bold;
 | 
					  font-weight: bold;
 | 
				
			||||||
  font-size: large;
 | 
					  font-size: large;
 | 
				
			||||||
  color: aliceblue;
 | 
					  color: aliceblue;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,10 @@
 | 
				
			|||||||
import Analysis from "./Analysis";
 | 
					import Analysis from "./Analysis";
 | 
				
			||||||
import "./App.css";
 | 
					import "./App.css";
 | 
				
			||||||
 | 
					import { AuthProvider } from "./AuthContext";
 | 
				
			||||||
import Footer from "./Footer";
 | 
					import Footer from "./Footer";
 | 
				
			||||||
import Header from "./Header";
 | 
					import Header from "./Header";
 | 
				
			||||||
import Rankings from "./Rankings";
 | 
					import Rankings from "./Rankings";
 | 
				
			||||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
 | 
					import { BrowserRouter, Routes, Route } from "react-router";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function App() {
 | 
					function App() {
 | 
				
			||||||
  //const [data, setData] = useState({ nodes: [], links: [] } as SociogramData);
 | 
					  //const [data, setData] = useState({ nodes: [], links: [] } as SociogramData);
 | 
				
			||||||
@@ -17,7 +18,11 @@ function App() {
 | 
				
			|||||||
      <Header />
 | 
					      <Header />
 | 
				
			||||||
      <Routes>
 | 
					      <Routes>
 | 
				
			||||||
        <Route index element={<Rankings />} />
 | 
					        <Route index element={<Rankings />} />
 | 
				
			||||||
        <Route path="/analysis" element={<Analysis />} />
 | 
					        <Route path="/analysis" element={
 | 
				
			||||||
 | 
					          <AuthProvider>
 | 
				
			||||||
 | 
					            <Analysis />
 | 
				
			||||||
 | 
					          </AuthProvider>
 | 
				
			||||||
 | 
					        } />
 | 
				
			||||||
      </Routes>
 | 
					      </Routes>
 | 
				
			||||||
      <Footer />
 | 
					      <Footer />
 | 
				
			||||||
    </BrowserRouter>
 | 
					    </BrowserRouter>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										87
									
								
								src/api.ts
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								src/api.ts
									
									
									
									
									
								
							@@ -1,4 +1,10 @@
 | 
				
			|||||||
 | 
					import { useContext } from "react";
 | 
				
			||||||
 | 
					import useAuthContext from "./AuthContext";
 | 
				
			||||||
 | 
					import { createCookie } from "react-router";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const baseUrl = import.meta.env.VITE_BASE_URL as string;
 | 
					export const baseUrl = import.meta.env.VITE_BASE_URL as string;
 | 
				
			||||||
 | 
					export const token = () => localStorage.getItem("access_token") as string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default async function api(path: string, data: any): Promise<any> {
 | 
					export default async function api(path: string, data: any): Promise<any> {
 | 
				
			||||||
  const request = new Request(`${baseUrl}${path}/`, {
 | 
					  const request = new Request(`${baseUrl}${path}/`, {
 | 
				
			||||||
    method: "POST",
 | 
					    method: "POST",
 | 
				
			||||||
@@ -15,3 +21,84 @@ export default async function api(path: string, data: any): Promise<any> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  return response;
 | 
					  return response;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function apiAuth(path: string, data: any, method: string = "GET"): Promise<any> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const req = new Request(`${baseUrl}api/${path}`, {
 | 
				
			||||||
 | 
					    method: method, headers: {
 | 
				
			||||||
 | 
					      "Authorization": `Bearer ${token()} `,
 | 
				
			||||||
 | 
					      'Content-Type': 'application/json'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    body: JSON.stringify(data),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  let resp: Response;
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    resp = await fetch(req);
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    throw new Error(`request failed: ${e}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!resp.ok) {
 | 
				
			||||||
 | 
					    if (resp.status === 401) {
 | 
				
			||||||
 | 
					      logout()
 | 
				
			||||||
 | 
					      throw new Error('Unauthorized');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return resp.json()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type User = {
 | 
				
			||||||
 | 
					  username: string;
 | 
				
			||||||
 | 
					  fullName: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function currentUser(): Promise<User> {
 | 
				
			||||||
 | 
					  if (!token()) throw new Error("you have no access token")
 | 
				
			||||||
 | 
					  const req = new Request(`${baseUrl}api/users/me/`, {
 | 
				
			||||||
 | 
					    method: "GET", headers: {
 | 
				
			||||||
 | 
					      "Authorization": `Bearer ${token()} `,
 | 
				
			||||||
 | 
					      'Content-Type': 'application/json'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  let resp: Response;
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    resp = await fetch(req);
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    throw new Error(`request failed: ${e}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!resp.ok) {
 | 
				
			||||||
 | 
					    if (resp.status === 401) {
 | 
				
			||||||
 | 
					      logout()
 | 
				
			||||||
 | 
					      throw new Error('Unauthorized');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return resp.json() as Promise<User>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type LoginRequest = {
 | 
				
			||||||
 | 
					  username: string;
 | 
				
			||||||
 | 
					  password: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export type Token = {
 | 
				
			||||||
 | 
					  access_token: string;
 | 
				
			||||||
 | 
					  token_type: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const login = (req: LoginRequest) => {
 | 
				
			||||||
 | 
					  fetch(`${baseUrl}api/token`, {
 | 
				
			||||||
 | 
					    method: "POST", headers: {
 | 
				
			||||||
 | 
					      'Content-Type': 'application/x-www-form-urlencoded',
 | 
				
			||||||
 | 
					    }, body: new URLSearchParams(req).toString()
 | 
				
			||||||
 | 
					  }).then(resp => resp.json() as unknown as Token).then(token => token ? localStorage.setItem("access_token", token.access_token) : console.log("token not acquired")).catch((e) => console.log("catch error " + e + " in login"));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const cookielogin = (req: LoginRequest) => {
 | 
				
			||||||
 | 
					  fetch(`${baseUrl}api/token`, {
 | 
				
			||||||
 | 
					    method: "POST", headers: {
 | 
				
			||||||
 | 
					      'Content-Type': 'application/x-www-form-urlencoded',
 | 
				
			||||||
 | 
					    }, body: new URLSearchParams(req).toString()
 | 
				
			||||||
 | 
					  }).then(resp => { createCookie(resp.headers.getSetCookie()) }).catch((e) => console.log("catch error " + e + " in login"));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const logout = () => localStorage.removeItem("access_token");
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user