Skip to content

Commit

Permalink
Merge pull request #38 from rine77/37-feature_request-two-factor-auth…
Browse files Browse the repository at this point in the history
…entication-support

37 feature request two factor authentication support
  • Loading branch information
rine77 authored Nov 27, 2024
2 parents 5504b72 + 64c8dae commit 92db410
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 41 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,7 @@ cython_debug/
*creds.sec*

# test
test/
test/

#vscode
.history
30 changes: 14 additions & 16 deletions custom_components/homeassistantedupage/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import logging
import asyncio
from datetime import datetime, timedelta
from edupage_api.exceptions import BadCredentialsException, CaptchaException
from edupage_api.exceptions import BadCredentialsException, CaptchaException, SecondFactorFailedException
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.const import Platform
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .homeassistant_edupage import Edupage
from edupage_api.classes import Class
from edupage_api.people import EduTeacher
from edupage_api.people import Gender
from edupage_api.classrooms import Classroom
from .const import DOMAIN

from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import DOMAIN, CONF_PHPSESSID, CONF_SUBDOMAIN, CONF_STUDENT_ID, CONF_STUDENT_NAME

_LOGGER = logging.getLogger("custom_components.homeassistant_edupage")

Expand All @@ -26,16 +23,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}

username = entry.data["username"]
password = entry.data["password"]
subdomain = entry.data["subdomain"]
student_id = entry.data["student_id"]
edupage = Edupage(hass)
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
subdomain = entry.data[CONF_SUBDOMAIN]
PHPSESSID = entry.data[CONF_PHPSESSID]
student_id = entry.data[CONF_STUDENT_ID]
edupage = Edupage(hass=hass, sessionid=PHPSESSID)
coordinator = None

try:
login_success = await hass.async_add_executor_job(
edupage.login, username, password, subdomain
try:
await hass.async_add_executor_job(
edupage.login, username, password, subdomain
)
_LOGGER.debug("INIT login_success")

Expand All @@ -48,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return False

except Exception as e:
_LOGGER.error("INIT unexpected login error: %s", e)
_LOGGER.error("INIT unexpected login error: %s", e.with_traceback(None))
return False

fetch_lock = asyncio.Lock()
Expand Down
3 changes: 2 additions & 1 deletion custom_components/homeassistantedupage/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ async def async_get_events(self, hass, start_date: datetime, end_date: datetime)
if lesson.classes and lesson.classes[0].homeroom:
room = lesson.classes[0].homeroom.name

teacher_names = [teacher.name for teacher in lesson.teachers]
teacher_names = [teacher.name for teacher in lesson.teachers] if lesson.teachers else []

teachers = ", ".join(teacher_names) if teacher_names else "Unknown Teacher"

start_time = datetime.combine(current_date, lesson.start_time).astimezone(local_tz)
Expand Down
52 changes: 43 additions & 9 deletions custom_components/homeassistantedupage/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,51 @@
import logging
import voluptuous as vol
import time
from edupage_api import Edupage
from edupage_api.exceptions import BadCredentialsException, SecondFactorFailedException
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from.const import CONF_PHPSESSID, CONF_SUBDOMAIN, CONF_STUDENT_ID, CONF_STUDENT_NAME

_LOGGER = logging.getLogger(__name__)

CONF_SUBDOMAIN = "subdomain" # Lokal definiert

class EdupageConfigFlow(config_entries.ConfigFlow, domain="homeassistantedupage"):
"""Handle a config flow for Edupage."""

VERSION = 1

def login(self, api, user_input):
try:
second_factor = api.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD], user_input[CONF_SUBDOMAIN])
# TODO: add user select as dropdown?! for 2FA
confirmation_method = "1"

if confirmation_method == "1":
#TODO: waiting does not work, maybe cause of async?! SecondFactorFailedException is raised if no breakpoint debug is set
while not second_factor.is_confirmed():
time.sleep(0.5)
second_factor.finish()

elif confirmation_method == "2":
# TODO: how to do this in HA?!
code = input("Enter 2FA code (or 'resend' to resend the code): ")
while code.lower() == "resend":
second_factor.resend_notifications()
code = input("Enter 2FA code (or 'resend' to resend the code): ")
second_factor.finish_with_code(code)

except BadCredentialsException as e:
_LOGGER.error("Wrong username or password: %s", e)
except SecondFactorFailedException as e:
_LOGGER.error("Second factor failed: %s", e)

#TODO: what does HA expect here as return?!
if api.is_logged_in:
print("Logged in")
_LOGGER.info("Successfully logged in.")
else:
raise BadCredentialsException("Wrong username or password")

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
Expand All @@ -26,10 +58,9 @@ async def async_step_user(self, user_input=None):
# Login ausführen
_LOGGER.debug("Starting login process")
await self.hass.async_add_executor_job(
api.login,
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
user_input[CONF_SUBDOMAIN]
self.login,
api,
user_input
)
_LOGGER.debug("Login successful")

Expand All @@ -41,6 +72,9 @@ async def async_step_user(self, user_input=None):
errors["base"] = "no_students_found"
else:
# Speichere Benutzer-Eingaben
cookies = api.session.cookies.get_dict()
phpsess = cookies["PHPSESSID"]
user_input[CONF_PHPSESSID] = phpsess
self.user_data = user_input
self.students = {student.person_id: student.name for student in students}

Expand Down Expand Up @@ -76,8 +110,8 @@ async def async_step_select_student(self, user_input=None):
title=f"Edupage ({self.students[student_id]})",
data={
**self.user_data, # Login-Daten hinzufügen
"student_id": student_id,
"student_name": self.students[student_id],
CONF_STUDENT_ID: student_id,
CONF_STUDENT_NAME: self.students[student_id],
},
)

Expand Down
4 changes: 4 additions & 0 deletions custom_components/homeassistantedupage/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@

# Component domain, used to store component data in hass data.
DOMAIN = "homeassistantedupage"
CONF_SUBDOMAIN = "subdomain"
CONF_PHPSESSID = "PHPSESSID"
CONF_STUDENT_ID = "student_id"
CONF_STUDENT_NAME = "student_name"
31 changes: 17 additions & 14 deletions custom_components/homeassistantedupage/homeassistant_edupage.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import logging
import asyncio

from edupage_api import Login
from edupage_api import Edupage as APIEdupage
from edupage_api.classes import Class
from edupage_api.people import EduTeacher
from edupage_api.people import Gender
from edupage_api.classrooms import Classroom
from zoneinfo import ZoneInfo
from edupage_api.exceptions import BadCredentialsException, CaptchaException


from datetime import datetime
from datetime import date
from edupage_api.exceptions import BadCredentialsException, CaptchaException, SecondFactorFailedException
from homeassistant.helpers.update_coordinator import UpdateFailed
from concurrent.futures import ThreadPoolExecutor

_LOGGER = logging.getLogger(__name__)

class Edupage:
def __init__(self,hass):
def __init__(self,hass, sessionid = ''):
self.hass = hass
self.sessionid = sessionid
self.api = APIEdupage()

async def login(self, username: str, password: str, subdomain: str):
"""Perform login asynchronously."""
try:
result = await asyncio.to_thread(self.api.login, username, password, subdomain)
result = True
login = Login(self.api)
await asyncio.to_thread(
login.reload_data, subdomain, self.sessionid, username
)
if not self.api.is_logged_in:
#TODO: how to handle 2FA at this point?!
result = await asyncio.to_thread(self.api.login, username, password, subdomain)
_LOGGER.debug(f"EDUPAGE Login successful, result: {result}")
return result
except BadCredentialsException as e:
Expand All @@ -34,6 +32,11 @@ async def login(self, username: str, password: str, subdomain: str):

except CaptchaException as e:
_LOGGER.error("EDUPAGE login failed: CAPTCHA needed. %s", e)
return False

except SecondFactorFailedException as e:
#TODO hier müsste man dann irgendwie abfangen, falls die session mal abgelaufen ist. und dies dann auch irgendwie via HA sauber zum Nutzer bringen!?
_LOGGER.error("EDUPAGE login failed: 2FA error. %s", e)
return False

except Exception as e:
Expand Down

0 comments on commit 92db410

Please sign in to comment.