Skip to content

Commit 4ade2ef

Browse files
committed
✨️ User Creation/Login Update
1 parent 8028b2e commit 4ade2ef

11 files changed

Lines changed: 299 additions & 33 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ __pycache__/
1212
# Distribution / packaging
1313
.Python
1414
build/
15+
testing/
1516
develop-eggs/
1617
dist/
1718
downloads/
@@ -130,3 +131,4 @@ dmypy.json
130131

131132
# Pyre type checker
132133
.pyre/
134+
/testing/

assets/database.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import mysql.connector
2-
from env import *
2+
import env as e
33
# env (enviorment) is the env.py file where all of the
44
# variables are stored for the database access create
55
# your own file with all the needed variables (see below)
66

77

88
def openDBConnection():
99
db = mysql.connector.connect(
10-
host=host,
11-
user=username,
12-
passwd=password,
13-
database=database
10+
host=e.host,
11+
user=e.username,
12+
passwd=e.password,
13+
database=e.database
1414
)
1515
return db

functions/hashing.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,74 @@
1+
from fastapi import Depends, HTTPException, status
12
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
3+
from jose import jwt, JWTError
24
from passlib.context import CryptContext
5+
from starlette.status import HTTP_401_UNAUTHORIZED
6+
7+
from functions import user as u
8+
import env as e
9+
from functions.user import get_user
10+
from models.tokendata import TokenData
11+
from models.user import User
312

413
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
5-
oauth2_sheme = OAuth2PasswordBearer(tokenUrl="token")
14+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
615

7-
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
816
ALOGRITHM = "HS256"
917
ACCESS_TOKEN_EXPIRE_MINUTES = 30
1018

19+
1120
def verify_password(plain_password, hashed_password):
1221
return pwd_context.verify(plain_password, hashed_password)
1322

1423

1524
def get_password_hash(password):
16-
return pwd_context.hash(password)
25+
return pwd_context.hash(password)
26+
27+
28+
def authenticate_user(EMAIL: str, PASSWORD: str):
29+
account = u.get_user(EMAIL=EMAIL)
30+
if not account:
31+
return False
32+
if not verify_password(PASSWORD, account.PASSWORD_HASH):
33+
return False
34+
return account
35+
36+
37+
async def get_current_user(token: str = Depends(oauth2_scheme)):
38+
credential_email_none_exception = HTTPException(
39+
status_code=HTTP_401_UNAUTHORIZED,
40+
detail="Could not validate credentials [email none]",
41+
headers={"WWW-Authenticate": "Bearer"}
42+
)
43+
credential_jwt_exception = HTTPException(
44+
status_code=HTTP_401_UNAUTHORIZED,
45+
detail="Could not validate credentials [jwt]",
46+
headers={"WWW-Authenticate": "Bearer"}
47+
)
48+
credential_email_db_exception = HTTPException(
49+
status_code=HTTP_401_UNAUTHORIZED,
50+
detail="Could not validate credentials [email-db]",
51+
headers={"WWW-Authenticate": "Bearer"}
52+
)
53+
try:
54+
# dekodieren des JWT
55+
payload = jwt.decode(token, e.SECRET_KEY, algorithms=[ALOGRITHM])
56+
# get EMAIL out to the JWT package
57+
EMAIL: str = payload.get("sub")
58+
# check if field EMAIL is set
59+
if EMAIL is None:
60+
# if not raise credential exception
61+
raise credential_email_none_exception
62+
token_data = TokenData(EMAIL=EMAIL)
63+
except JWTError:
64+
raise credential_jwt_exception
65+
user = get_user(EMAIL=token_data.EMAIL)
66+
if user is None:
67+
raise credential_email_db_exception
68+
return user
69+
70+
71+
async def get_current_active_user(current_user: User = Depends(get_current_user)):
72+
if not current_user.ACTIVE:
73+
raise HTTPException(status_code=400, detail="Inactive user")
74+
return current_user

functions/sessionkey.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from datetime import timedelta, datetime
2+
from typing import Optional
3+
from jose import jwt
4+
5+
import env as e
6+
import functions.hashing as hash
7+
8+
9+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
10+
to_encode = data.copy()
11+
if expires_delta:
12+
expire = datetime.utcnow() + expires_delta
13+
else:
14+
expire = datetime.utcnow() + timedelta(weeks=4)
15+
to_encode.update({"exp": expire})
16+
encode_jwt = jwt.encode(to_encode, e.SECRET_KEY, algorithm=hash.ALOGRITHM)
17+
return encode_jwt

functions/user.py

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from fastapi import Depends
2+
13
from assets.database import openDBConnection
24
from models.sessionkey import SessionKey
35
from models.user import User
@@ -11,15 +13,74 @@ def get_user(ID=None, EMAIL=None, SESSION_KEY=None):
1113
cursor.execute("SELECT * FROM USERDATA WHERE ID = %s", (ID,))
1214
return fetch_data(cursor)
1315
if EMAIL is not None:
14-
cursor.execute("SELECT * FROM USERDATA WHERE ID = %s", (EMAIL,))
16+
cursor.execute("SELECT * FROM USERDATA WHERE EMAIL = %s", (EMAIL,))
1517
return fetch_data(cursor)
1618
if SESSION_KEY is not None:
17-
cursor.execute("SELECT * FROM USERDATA WHERE ID = %s", (SESSION_KEY,))
19+
cursor.execute("SELECT * FROM USERDATA WHERE SESSION_KEY = %s", (SESSION_KEY,))
1820
return fetch_data(cursor)
1921
else:
2022
raise ValueError('USER not Valid')
2123

2224

25+
def push_data(u: User):
26+
db = openDBConnection()
27+
cursor = db.cursor()
28+
29+
SQL = """
30+
UPDATE USERDATA SET
31+
EMAIL = %s,
32+
PASSWORD_HASH = %s,
33+
NAME = %s,
34+
LASTNAME = %s,
35+
CLASS = %s,
36+
PERMISSION_LEVEL = %s,
37+
LAST_IP = %s,
38+
ACTIVE = %s
39+
WHERE ID = %s
40+
"""
41+
PARAM = (
42+
u.EMAIL,
43+
u.PASSWORD_HASH,
44+
u.NAME,
45+
u.LASTNAME,
46+
u.CLASS,
47+
u.PERMISSION_LEVEL,
48+
str(u.LAST_IP),
49+
u.ACTIVE,
50+
u.ID
51+
)
52+
53+
cursor.execute(SQL, PARAM)
54+
db.commit()
55+
cursor.close()
56+
db.close()
57+
58+
59+
def create_user(u: User):
60+
db = openDBConnection()
61+
cursor = db.cursor()
62+
63+
SQL = """
64+
INSERT INTO USERDATA (EMAIL, PASSWORD_HASH, NAME, LASTNAME, CLASS, PERMISSION_LEVEL, LAST_IP, ACTIVE)
65+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
66+
"""
67+
PARAM = (
68+
u.EMAIL,
69+
u.PASSWORD_HASH,
70+
u.NAME,
71+
u.LASTNAME,
72+
u.CLASS,
73+
u.PERMISSION_LEVEL,
74+
str(u.LAST_IP),
75+
u.ACTIVE
76+
)
77+
78+
cursor.execute(SQL, PARAM)
79+
db.commit()
80+
cursor.close()
81+
db.close()
82+
83+
2384
def fetch_data(cursor):
2485
data = cursor.fetchone()
2586
return User(
@@ -32,5 +93,19 @@ def fetch_data(cursor):
3293
PERMISSION_LEVEL=data[6],
3394
LAST_IP=data[7],
3495
ACTIVE=data[8],
35-
SESSION_KEY=SessionKey(key=data[9])
3696
)
97+
98+
99+
def is_teacher(EMAIL: str):
100+
db = openDBConnection()
101+
cursor = db.cursor()
102+
103+
cursor.execute("SELECT EMAIL FROM GLOBAL_TEACHERS WHERE EMAIL = %s", (EMAIL,))
104+
105+
data: str = cursor.fetchone()
106+
107+
if data is not None:
108+
if data[0].lower() == EMAIL.lower():
109+
return True
110+
return False
111+
return False

main.py

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,111 @@
1-
from fastapi import FastAPI
1+
from datetime import timedelta
2+
from typing import Optional
23

3-
import functions
4-
import models
5-
import assets
4+
from fastapi import FastAPI, Depends, HTTPException, status
5+
from fastapi.security import OAuth2PasswordRequestForm
6+
from pydantic import ValidationError
7+
from starlette.responses import JSONResponse
8+
9+
from functions.hashing import get_current_active_user, authenticate_user, get_password_hash
10+
from functions.sessionkey import create_access_token
11+
from functions.user import create_user, get_user, is_teacher
12+
from models.token import Token
13+
from models.user import User
14+
from models.apikey import ApiKey
615

716
app = FastAPI()
817

918

19+
# Post
20+
21+
22+
@app.post("/token", response_model=Token)
23+
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
24+
account = authenticate_user(form_data.username, form_data.password)
25+
if not account:
26+
raise HTTPException(
27+
status_code=status.HTTP_401_UNAUTHORIZED,
28+
detail="Incorrect username or password",
29+
headers={"WWW-Authenticate": "Bearer"},
30+
)
31+
access_token_expires = timedelta(weeks=4)
32+
access_token = create_access_token(
33+
data={"sub": account.EMAIL}, expires_delta=access_token_expires
34+
)
35+
return {"access_token": access_token, "token_type": "bearer"}
36+
37+
38+
# User get Operrations
39+
40+
41+
@app.get("/users/me", response_model=User, description="shows the current active logged in user")
42+
async def read_users_me(current_user: User = Depends(get_current_active_user)):
43+
return current_user
44+
45+
46+
@app.get("/users/{id}", response_model=User)
47+
async def read_user_by_id(id: int, current_user: User = Depends(get_current_active_user)):
48+
if current_user.PERMISSION_LEVEL >= 3:
49+
return get_user(ID=id)
50+
else:
51+
return JSONResponse(status_code=status.HTTP_403_FORBIDDEN,
52+
content="Not sufficent Permissions to view other users")
53+
54+
55+
# Put
56+
57+
@app.put("/admin/create/user")
58+
async def admin_create_user(user: User, apikey: Optional[ApiKey] = None,
59+
current_user: User = Depends(get_current_active_user)):
60+
if current_user.PERMISSION_LEVEL >= 3:
61+
user.PASSWORD_HASH = get_password_hash(user.PASSWORD_HASH)
62+
create_user(user)
63+
return JSONResponse(status_code=status.HTTP_201_CREATED, content="User was successfully created")
64+
else:
65+
return JSONResponse(
66+
status_code=status.HTTP_403_FORBIDDEN,
67+
content="Not sufficent Permissions to create other users"
68+
)
69+
70+
71+
@app.put("/create/user")
72+
async def form_create_user(api_key: str, u_email: str, u_password: str, u_class: str):
73+
try:
74+
ApiKey(APIKEY=api_key)
75+
except ValidationError as e:
76+
return JSONResponse(
77+
status_code=status.HTTP_403_FORBIDDEN,
78+
content=e.errors()
79+
)
80+
81+
PERMISSION_LEVEL = 0
82+
if is_teacher(u_email):
83+
PERMISSION_LEVEL = 1
1084

85+
NAME = u_email.split(".")[0].capitalize()
86+
LASTNAME = u_email.split(".")[1].split("@")[0].capitalize()
1187

88+
if LASTNAME[:-2].isdigit():
89+
LASTNAME = LASTNAME[:-2]
1290

1391

92+
try:
93+
account = User(
94+
EMAIL=u_email,
95+
PASSWORD_HASH=get_password_hash(u_password),
96+
NAME=NAME,
97+
LASTNAME=LASTNAME,
98+
CLASS=u_class,
99+
PERMISSION_LEVEL=PERMISSION_LEVEL,
100+
ACTIVE=False
101+
)
102+
except ValidationError as e:
103+
raise HTTPException(
104+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
105+
detail=e.errors()
106+
)
107+
create_user(account)
108+
return JSONResponse(
109+
status_code=status.HTTP_200_OK,
110+
content="User succesfully created"
111+
)

models/sessionkey.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,21 @@
44

55

66
class SessionKey(BaseModel):
7-
key: str
7+
key: Optional[str] = None
88
valid: Optional[bool] = False
99

10-
@root_validator
11-
def check_session_key(cls, values):
12-
db = openDBConnection()
13-
cursor = db.cursor()
10+
# @root_validator
11+
# def check_session_key(cls, values):
12+
# db = openDBConnection()
13+
# cursor = db.cursor()
14+
#
15+
# cursor.execute("SELECT SESSION_KEY FROM USERDATA WHERE SESSION_KEY = %s", (values['key'],))
16+
# data = cursor.fetchone()
17+
#
18+
# if data[0] is not None:
19+
# values['valid'] = True
20+
# return values
21+
#
22+
# raise ValueError('SessionKey is not valid')
1423

15-
cursor.execute("SELECT SESSION_KEY FROM USERDATA WHERE SESSION_KEY = %s", (values['key'],))
16-
data = cursor.fetchone()
1724

18-
if data[0] is not None:
19-
values['valid'] = True
20-
return values
21-
22-
raise ValueError('SessionKey is not valid')

models/token.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from pydantic import BaseModel
2+
3+
4+
class Token(BaseModel):
5+
access_token: str
6+
token_type: str

models/tokendata.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from typing import Optional
2+
3+
from pydantic import BaseModel
4+
5+
6+
class TokenData(BaseModel):
7+
EMAIL: Optional[str] = None

0 commit comments

Comments
 (0)