gc-server3 1 месяц назад
Родитель
Сommit
54b02bb3d1
12 измененных файлов с 341 добавлено и 15 удалено
  1. 17 0
      app/auth2.py
  2. 17 12
      app/cleanup_comments.py
  3. 20 0
      app/config.py
  4. 0 0
      app/ldap.py
  5. 50 0
      app/main2.py
  6. 94 0
      app/main3.py
  7. 30 0
      app/oauth_token.py
  8. 4 3
      app/routes.py
  9. 21 0
      app/schemas.py
  10. 35 0
      app/users.py
  11. 2 0
      pyproject.toml
  12. 51 0
      uv.lock

+ 17 - 0
app/auth2.py

@@ -0,0 +1,17 @@
+from fastapi import Depends, HTTPException, status
+from fastapi.security import OAuth2PasswordBearer
+from .oauth_token import verify_token
+from .schemas import TokenData
+
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
+
+
+def get_current_user(token: str = Depends(oauth2_scheme)):
+    payload = verify_token(token)
+    username: str = payload.get("sub")
+    if username is None:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail="Invalid authentication credentials",
+        )
+    return TokenData(username=username)

+ 17 - 12
app/cleanup_comments.py

@@ -1,4 +1,4 @@
-from datetime import timedelta
+from datetime import timedelta, date
 
 from db import get_session
 import re
@@ -14,26 +14,31 @@ def main():
             comment_date = row.Timestamp + timedelta(minutes=i)
             comment_user = row.Benutzer_ID
             r = r.replace("'", "")
+            d, u = get_date_and_user_from_comment(r)
             db.execute(
                 text(
                     "INSERT INTO dbo.Forderungen_Kommentar "
                     f"VALUES ('{row.Client_DB}', '{row.Beleg_Nr}', '{comment_date}', '{comment_user}', '{r}')"
                 )
             )
-            # m = re.search(r"(\d\d\.\d\d?)\.?[\/\s](\w+)", r)
-            # if m:
-            #     comment_date = m[1]
-            #     comment_user = m[2]
-            # else:
-            #     if r.startswith("EWB"):
-            #         continue
-            #     az = re.search(r"^AZ\:?\s(\d+\/\d+)", r)
-            #     if az:
-            #         continue
-            #     print(r)
 
     db.commit()
 
 
+def get_date_and_user_from_comment(r: str) -> tuple[date, str]:
+    m = re.search(r"(\d\d\.\d\d?)\.?[\/\s](\w+)", r)
+    if m:
+        date_split = m[1].split(".")
+        return date(2026, date_split[1], date_split[0]), m[2]
+
+    if r.startswith("EWB"):
+        return None, "mde"
+
+    az = re.search(r"^AZ\:?\s(\d+\/\d+)", r)
+    if az:
+        return None, "mde"
+    return None, "mde"
+
+
 if __name__ == "__main__":
     main()

+ 20 - 0
app/config.py

@@ -0,0 +1,20 @@
+from pydantic_settings import BaseSettings
+
+
+class Settings(BaseSettings):
+    # auth
+    SECRET_KEY: str = "my_secret_key"
+    ALGORITHM: str = "HS256"
+    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
+    # ldap
+    LDAP_URL: str
+    LDAP_USER_DN: str
+    LDAP_BASE_DN: str
+    # database
+    DATABASE_URL: str = "sqlite:///./dev.db"
+
+    class Config:
+        env_file = ".env"
+
+
+settings = Settings()

+ 0 - 0
app/auth.py → app/ldap.py


+ 50 - 0
app/main2.py

@@ -0,0 +1,50 @@
+from fastapi import FastAPI, Depends, HTTPException, status
+from sqlalchemy.orm import Session
+from fastapi.security import OAuth2PasswordRequestForm
+from .oauth_token import create_access_token
+from .schemas import User, Token
+from .auth2 import get_current_user
+from .users import UserModel, get_password_hash, authenticate_user
+from .db import get_session
+
+app = FastAPI()
+
+
+@app.post("/token", response_model=Token)
+def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_session)):
+    user = authenticate_user(db, form_data.username, form_data.password)
+    print(user)
+    if not user:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail="Incorrect username or password",
+            headers={"WWW-Authenticate": "Bearer"},
+        )
+    access_token = create_access_token(data={"sub": user.username})
+    return {"access_token": access_token, "token_type": "bearer"}
+
+
+@app.get("/users/me")
+def read_users_me(current_user: User = Depends(get_current_user)):
+    return current_user
+
+
+@app.post("/users/createUsers")
+def create_user(user: User, db: Session = Depends(get_session), current_user: User = Depends(get_current_user)):
+    # Check if the username already exists
+    existing_user = db.query(UserModel).filter(UserModel.username == user.username).first()
+    if existing_user:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail="Username already exists",
+        )
+
+    hashed_password = get_password_hash(user.hashed_password)
+    # Create a new user object
+    new_user = UserModel(username=user.username, email=user.email, hashed_password=hashed_password)
+    # Add the new user to the database
+    db.add(new_user)
+    db.commit()
+    db.refresh(new_user)
+
+    return {"message": "User created successfully", "user_id": new_user.id}

+ 94 - 0
app/main3.py

@@ -0,0 +1,94 @@
+from typing import Annotated
+
+from fastapi import Depends, FastAPI, HTTPException, status
+from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
+from pydantic import BaseModel
+
+fake_users_db = {
+    "johndoe": {
+        "username": "johndoe",
+        "full_name": "John Doe",
+        "email": "johndoe@example.com",
+        "hashed_password": "fakehashedsecret",
+        "disabled": False,
+    },
+    "alice": {
+        "username": "alice",
+        "full_name": "Alice Wonderson",
+        "email": "alice@example.com",
+        "hashed_password": "fakehashedsecret2",
+        "disabled": True,
+    },
+}
+
+app = FastAPI()
+
+
+def fake_hash_password(password: str):
+    return "fakehashed" + password
+
+
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
+
+
+class User(BaseModel):
+    username: str
+    email: str | None = None
+    full_name: str | None = None
+    disabled: bool | None = None
+
+
+class UserInDB(User):
+    hashed_password: str
+
+
+def get_user(db, username: str):
+    if username in db:
+        user_dict = db[username]
+        return UserInDB(**user_dict)
+
+
+def fake_decode_token(token):
+    # This doesn't provide any security at all
+    # Check the next version
+    user = get_user(fake_users_db, token)
+    return user
+
+
+async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
+    user = fake_decode_token(token)
+    if not user:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail="Not authenticated",
+            headers={"WWW-Authenticate": "Bearer"},
+        )
+    return user
+
+
+async def get_current_active_user(
+    current_user: Annotated[User, Depends(get_current_user)],
+):
+    if current_user.disabled:
+        raise HTTPException(status_code=400, detail="Inactive user")
+    return current_user
+
+
+@app.post("/token")
+async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
+    user_dict = fake_users_db.get(form_data.username)
+    if not user_dict:
+        raise HTTPException(status_code=400, detail="Incorrect username or password")
+    user = UserInDB(**user_dict)
+    hashed_password = fake_hash_password(form_data.password)
+    if not hashed_password == user.hashed_password:
+        raise HTTPException(status_code=400, detail="Incorrect username or password")
+
+    return {"access_token": user.username, "token_type": "bearer"}
+
+
+@app.get("/users/me")
+async def read_users_me(
+    current_user: Annotated[User, Depends(get_current_active_user)],
+):
+    return current_user

+ 30 - 0
app/oauth_token.py

@@ -0,0 +1,30 @@
+from datetime import datetime, timedelta, timezone
+from jose import JWTError, jwt
+from fastapi import HTTPException, status
+from fastapi.security import OAuth2PasswordBearer
+from .config import settings
+
+SECRET_KEY = settings.SECRET_KEY
+ALGORITHM = settings.ALGORITHM
+ACCESS_TOKEN_EXPIRE_MINUTES = settings.ACCESS_TOKEN_EXPIRE_MINUTES
+
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
+
+
+def create_access_token(data: dict):
+    to_encode = data.copy()
+    expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+    to_encode.update({"exp": expire})
+    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
+
+
+def verify_token(token: str):
+    try:
+        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+        return payload
+    except JWTError:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail="Invalid or expired token",
+            headers={"WWW-Authenticate": "Bearer"},
+        )

+ 4 - 3
app/routes.py

@@ -19,7 +19,7 @@ from fastapi.templating import Jinja2Templates
 from sqlalchemy import text
 from sqlalchemy.orm import Session
 
-from .auth import ldap_authenticate
+from .ldap import ldap_authenticate
 from .db import get_session
 from .models import Bemerkung, Forderung
 from .schemas import BemerkungIn
@@ -198,8 +198,9 @@ def forderungen_liste(request: Request, db: Session = Depends(get_session), limi
     )
     # print(query)
     # q = db.execute(text("SELECT * FROM [dbo].[Forderungen]"))
-    q = db.execute(text(query)).fetchall()
-    col_names = list(q[0]._asdict().keys())
+    q_res = db.execute(text(query))
+    col_names = list(q_res.keys())
+    q = q_res.fetchall()
 
     filters = {}
     for f in forderung_filter_config:

+ 21 - 0
app/schemas.py

@@ -24,3 +24,24 @@ class ZahlungOut(BaseModel):
 class BemerkungIn(BaseModel):
     bemerkung: Optional[str]
     wiedervorlage_datum: Optional[date]
+
+
+class Token(BaseModel):
+    access_token: str
+    token_type: str
+
+
+class TokenData(BaseModel):
+    username: str | None = None
+
+
+class User(BaseModel):
+    username: str
+    email: str | None = None
+    hashed_password: str
+
+
+class UserInDB(User):
+    username: str
+    email: str | None = None
+    hashed_password: str

+ 35 - 0
app/users.py

@@ -0,0 +1,35 @@
+from sqlalchemy.orm import Session
+from .schemas import UserInDB
+from .db import Base
+from passlib.context import CryptContext
+from sqlalchemy import Column, Integer, String
+
+# Password hashing configuration
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+
+class UserModel(Base):
+    __tablename__ = "users"
+    id = Column(Integer, primary_key=True, index=True)
+    username = Column(String, unique=True, index=True)
+    email = Column(String, unique=True, index=True)
+    hashed_password = Column(String)
+
+
+def get_password_hash(password: str) -> str:
+    return pwd_context.hash(password)
+
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+    return pwd_context.verify(plain_password, hashed_password)
+
+
+def authenticate_user(db: Session, username: str, password: str) -> UserInDB | None:
+    user = get_by_username(db, username)
+    if not user or not verify_password(password, user.hashed_password):
+        return None
+    return user
+
+
+def get_by_username(db: Session, username: str) -> UserInDB | None:
+    return db.query(UserModel).filter(UserModel.username == username).first()

+ 2 - 0
pyproject.toml

@@ -20,4 +20,6 @@ dependencies = [
 	"python-dotenv>=1.2.1",
 	"docxtpl>=0.20.2",
 	"imap-tools>=1.12.1",
+	"passlib>=1.7.4",
+	"python-jose>=3.5.0",
 ]

+ 51 - 0
uv.lock

@@ -116,6 +116,18 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/a4/ad/e07939d8e020e513d3860400413ba1e0e06102c469639b440d921337efef/docxtpl-0.20.2-py3-none-any.whl", hash = "sha256:626d5c570a46a62b2ca73b4d08f1c240fa031a5bc45371e1466a4fe184923d10", size = 17881, upload-time = "2025-11-13T12:47:13.704Z" },
 ]
 
+[[package]]
+name = "ecdsa"
+version = "0.19.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/25/ca/8de7744cb3bc966c85430ca2d0fcaeea872507c6a4cf6e007f7fe269ed9d/ecdsa-0.19.2.tar.gz", hash = "sha256:62635b0ac1ca2e027f82122b5b81cb706edc38cd91c63dda28e4f3455a2bf930", size = 202432, upload-time = "2026-03-26T09:58:17.675Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl", hash = "sha256:840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399", size = 150818, upload-time = "2026-03-26T09:58:15.808Z" },
+]
+
 [[package]]
 name = "email-validator"
 version = "2.3.0"
@@ -646,6 +658,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" },
 ]
 
+[[package]]
+name = "passlib"
+version = "1.7.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
+]
+
 [[package]]
 name = "pyasn1"
 version = "0.6.3"
@@ -833,6 +854,20 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
 ]
 
+[[package]]
+name = "python-jose"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "ecdsa" },
+    { name = "pyasn1" },
+    { name = "rsa" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726, upload-time = "2025-05-28T17:31:54.288Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" },
+]
+
 [[package]]
 name = "python-multipart"
 version = "0.0.22"
@@ -892,9 +927,11 @@ dependencies = [
     { name = "ldap3" },
     { name = "openpyxl" },
     { name = "pandas" },
+    { name = "passlib" },
     { name = "pyodbc" },
     { name = "python-docx" },
     { name = "python-dotenv" },
+    { name = "python-jose" },
     { name = "python-multipart" },
     { name = "sqlalchemy" },
     { name = "uvicorn", extra = ["standard"] },
@@ -911,9 +948,11 @@ requires-dist = [
     { name = "ldap3", specifier = ">=2.9" },
     { name = "openpyxl", specifier = ">=3.1" },
     { name = "pandas", specifier = ">=2.0" },
+    { name = "passlib", specifier = ">=1.7.4" },
     { name = "pyodbc", specifier = ">=4.0" },
     { name = "python-docx", specifier = ">=0.8" },
     { name = "python-dotenv", specifier = ">=1.2.1" },
+    { name = "python-jose", specifier = ">=3.5.0" },
     { name = "python-multipart", specifier = ">=0.0.6" },
     { name = "sqlalchemy", specifier = ">=2.0" },
     { name = "uvicorn", extras = ["standard"], specifier = ">=0.22" },
@@ -999,6 +1038,18 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" },
 ]
 
+[[package]]
+name = "rsa"
+version = "4.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "pyasn1" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
+]
+
 [[package]]
 name = "sentry-sdk"
 version = "2.56.0"