feat/security #1
@@ -1,6 +1,6 @@
 | 
			
		||||
from datetime import timedelta, timezone, datetime
 | 
			
		||||
from typing import Annotated
 | 
			
		||||
from fastapi import Depends, HTTPException, status
 | 
			
		||||
from fastapi import Depends, HTTPException, Response, status
 | 
			
		||||
from pydantic import BaseModel
 | 
			
		||||
import jwt
 | 
			
		||||
from jwt.exceptions import InvalidTokenError
 | 
			
		||||
@@ -102,7 +102,7 @@ async def get_current_active_user(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def login_for_access_token(
 | 
			
		||||
    form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
 | 
			
		||||
    form_data: Annotated[OAuth2PasswordRequestForm, Depends()], response: Response
 | 
			
		||||
) -> Token:
 | 
			
		||||
    user = authenticate_user(form_data.username, form_data.password)
 | 
			
		||||
    if not user:
 | 
			
		||||
@@ -115,6 +115,9 @@ async def login_for_access_token(
 | 
			
		||||
    access_token = create_access_token(
 | 
			
		||||
        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")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
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>(
 | 
			
		||||
//  func: T,
 | 
			
		||||
@@ -61,18 +62,14 @@ export default function Analysis() {
 | 
			
		||||
  // Function to generate and fetch the graph image
 | 
			
		||||
  async function loadImage() {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    await fetch(`${baseUrl}api/analysis/image`, {
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      headers: {
 | 
			
		||||
        "Content-Type": "application/json",
 | 
			
		||||
      },
 | 
			
		||||
      body: JSON.stringify(params)
 | 
			
		||||
    })
 | 
			
		||||
      .then((resp) => resp.json())
 | 
			
		||||
    await apiAuth("analysis/image", params, "POST")
 | 
			
		||||
      .then((data) => {
 | 
			
		||||
        setImage(data.image);
 | 
			
		||||
        setLoading(false);
 | 
			
		||||
      });
 | 
			
		||||
      }).catch((e) => {
 | 
			
		||||
        const { checkAuth } = useAuthContext();
 | 
			
		||||
        checkAuth();
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
@@ -92,6 +89,9 @@ export default function Analysis() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const { user } = useAuthContext()!
 | 
			
		||||
  console.log(`logged in as ${user.username}`);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="stack column dropdown">
 | 
			
		||||
      <button onClick={() => setShowControlPanel(!showControlPanel)}>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								src/App.css
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								src/App.css
									
									
									
									
									
								
							@@ -12,16 +12,17 @@ body {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer {
 | 
			
		||||
  font-size: x-small;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#root {
 | 
			
		||||
  max-width: 1280px;
 | 
			
		||||
  margin: 0 auto;
 | 
			
		||||
  padding: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer {
 | 
			
		||||
  font-size: x-small;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.grey {
 | 
			
		||||
  color: #444;
 | 
			
		||||
}
 | 
			
		||||
@@ -37,6 +38,11 @@ footer {
 | 
			
		||||
  z-index: -1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input {
 | 
			
		||||
  padding: 0.2em 16px;
 | 
			
		||||
  margin-bottom: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1,
 | 
			
		||||
h2,
 | 
			
		||||
h3 {
 | 
			
		||||
@@ -120,7 +126,8 @@ h3 {
 | 
			
		||||
  margin: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button {
 | 
			
		||||
button,
 | 
			
		||||
.button {
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
  font-size: large;
 | 
			
		||||
  color: aliceblue;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
import Analysis from "./Analysis";
 | 
			
		||||
import "./App.css";
 | 
			
		||||
import { AuthProvider } from "./AuthContext";
 | 
			
		||||
import Footer from "./Footer";
 | 
			
		||||
import Header from "./Header";
 | 
			
		||||
import Rankings from "./Rankings";
 | 
			
		||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
 | 
			
		||||
import { BrowserRouter, Routes, Route } from "react-router";
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
  //const [data, setData] = useState({ nodes: [], links: [] } as SociogramData);
 | 
			
		||||
@@ -17,7 +18,11 @@ function App() {
 | 
			
		||||
      <Header />
 | 
			
		||||
      <Routes>
 | 
			
		||||
        <Route index element={<Rankings />} />
 | 
			
		||||
        <Route path="/analysis" element={<Analysis />} />
 | 
			
		||||
        <Route path="/analysis" element={
 | 
			
		||||
          <AuthProvider>
 | 
			
		||||
            <Analysis />
 | 
			
		||||
          </AuthProvider>
 | 
			
		||||
        } />
 | 
			
		||||
      </Routes>
 | 
			
		||||
      <Footer />
 | 
			
		||||
    </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 token = () => localStorage.getItem("access_token") as string;
 | 
			
		||||
 | 
			
		||||
export default async function api(path: string, data: any): Promise<any> {
 | 
			
		||||
  const request = new Request(`${baseUrl}${path}/`, {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
@@ -15,3 +21,84 @@ export default async function api(path: string, data: any): Promise<any> {
 | 
			
		||||
  }
 | 
			
		||||
  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