Skip to content

Commit 4eb2ae5

Browse files
authored
chore: Login refactoring (#304)
* Fix DEGIRO 2FA redirect * Fix typos * Improved login * chore: Update Dependencies
1 parent 6aa1df5 commit 4eb2ae5

File tree

15 files changed

+346
-218
lines changed

15 files changed

+346
-218
lines changed

THIRD_PARTY_LICENSES.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
322322

323323

324324
cryptography
325-
46.0.3
325+
46.0.4
326326
Apache-2.0 OR BSD-3-Clause
327327
This software is made available under the terms of *either* of the licenses
328328
found in LICENSE.APACHE or LICENSE.BSD. Contributions to cryptography are made

poetry.lock

Lines changed: 78 additions & 83 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ dependencies = [
2323
"iso10383 >=2025.2.10",
2424
"requests >=2.32.4",
2525
"ibind[oauth] >=0.1.22",
26-
"cryptography >=45.0.6",
26+
"cryptography >=46.0.4",
2727
"packaging >=25.0",
2828
"python-dateutil >=2.9.0.post0",
2929
"markdown >=3.9",
@@ -52,7 +52,7 @@ pytest-django = "^4.11.1"
5252
pook = "^2.1.4"
5353
pyinstrument = "^5.1.2"
5454
licensecheck = "^2025.1.0"
55-
pip-licenses = "^5.5.0"
55+
pip-licenses = "^5.5.1"
5656
briefcase = "^0.3.26"
5757
deptry = "^0.23.0"
5858
pre-commit = "^4.5.1"
Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,54 @@
1+
from abc import ABC, abstractmethod
12
from dataclasses import dataclass
23
from typing import Any, Dict
34

45

56
@dataclass
6-
class BaseCredentials:
7+
class BaseCredentials(ABC):
8+
"""
9+
Base class for broker credentials.
10+
11+
All credential classes must implement:
12+
- to_auth_params(): Convert credentials to authentication parameters
13+
"""
14+
715
def to_dict(self) -> dict:
16+
"""
17+
Convert credentials to a dictionary containing all fields.
18+
19+
Returns:
20+
Dictionary with all credential fields
21+
"""
822
return self.__dict__
923

1024
@classmethod
1125
def from_dict(cls, data: Dict[str, Any]) -> "BaseCredentials":
26+
"""
27+
Create credentials instance from a dictionary.
28+
29+
Args:
30+
data: Dictionary containing credential fields
31+
32+
Returns:
33+
Credentials instance
34+
"""
1235
return cls(**data)
36+
37+
@abstractmethod
38+
def to_auth_params(self) -> dict:
39+
"""
40+
Convert credentials to authentication parameters.
41+
42+
This method must be implemented by all credential classes to provide
43+
the specific parameters needed for their authentication service.
44+
45+
Returns:
46+
Dictionary with authentication parameters for the broker's auth service
47+
48+
Note:
49+
This method should return only the fields needed for authentication,
50+
potentially with field name transformations (e.g., apikey → api_key).
51+
The remember_me parameter should be omitted as it's only relevant
52+
during initial manual login, not auto-authentication.
53+
"""
54+
pass

src/stonks_overwatch/config/bitvavo.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,23 @@ def has_minimal_credentials(self) -> bool:
2929
"""Check if minimal credentials are provided."""
3030
return bool(self.apikey and self.apisecret)
3131

32+
def to_auth_params(self) -> dict:
33+
"""
34+
Convert credentials to authentication parameters.
35+
36+
Returns:
37+
Dictionary with authentication parameters for Bitvavo auth service
38+
39+
Note:
40+
remember_me is intentionally omitted as this method is used for
41+
auto-authentication with already-stored credentials. The auth service
42+
will use its default value (False).
43+
"""
44+
return {
45+
"api_key": self.apikey,
46+
"api_secret": self.apisecret,
47+
}
48+
3249

3350
class BitvavoConfig(BaseConfig):
3451
config_key = BrokerName.BITVAVO.value

src/stonks_overwatch/config/degiro.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@ def from_request(cls, request) -> "DegiroCredentials":
3232
def has_minimal_credentials(self) -> bool:
3333
return bool(self.username and self.password and (self.totp_secret_key or self.one_time_password))
3434

35+
def to_auth_params(self) -> dict:
36+
"""
37+
Convert credentials to authentication parameters.
38+
39+
Returns:
40+
Dictionary with authentication parameters for DEGIRO auth service
41+
42+
Note:
43+
remember_me is intentionally omitted as this method is used for
44+
auto-authentication with already-stored credentials. The auth service
45+
will use its default value (False).
46+
"""
47+
return {
48+
"username": self.username,
49+
"password": self.password,
50+
"one_time_password": getattr(self, "one_time_password", None),
51+
}
52+
3553

3654
class DegiroConfig(BaseConfig):
3755
config_key = BrokerName.DEGIRO.value

src/stonks_overwatch/config/ibkr.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,27 @@ def from_dict(cls, data: Dict[str, Any]) -> "IbkrCredentials":
9696
filtered_data = {k: v for k, v in data.items() if k in valid_fields}
9797
return cls(**filtered_data)
9898

99+
def to_auth_params(self) -> dict:
100+
"""
101+
Convert credentials to authentication parameters.
102+
103+
Returns:
104+
Dictionary with authentication parameters for IBKR auth service
105+
106+
Note:
107+
remember_me is intentionally omitted as this method is used for
108+
auto-authentication with already-stored credentials. The auth service
109+
will use its default value (False).
110+
"""
111+
return {
112+
"access_token": getattr(self, "access_token", ""),
113+
"access_token_secret": getattr(self, "access_token_secret", ""),
114+
"consumer_key": getattr(self, "consumer_key", ""),
115+
"dh_prime": getattr(self, "dh_prime", ""),
116+
"encryption_key": getattr(self, "encryption_key", None),
117+
"signature_key": getattr(self, "signature_key", None),
118+
}
119+
99120

100121
class IbkrConfig(BaseConfig):
101122
config_key = BrokerName.IBKR.value

src/stonks_overwatch/core/aggregators/base_aggregator.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from abc import ABC, abstractmethod
99
from typing import Any, Dict, List, Optional
1010

11+
from stonks_overwatch.constants import BrokerName
1112
from stonks_overwatch.core.exceptions import DataAggregationException
1213
from stonks_overwatch.core.factories.broker_factory import BrokerFactory
1314
from stonks_overwatch.core.service_types import ServiceType
@@ -40,7 +41,7 @@ def __init__(self, service_type: ServiceType):
4041
f"[AGGREGATOR|{service_type.value.upper()}]",
4142
)
4243

43-
self._broker_services: Dict[str, Any] = {}
44+
self._broker_services: Dict[BrokerName, Any] = {}
4445
self._initialize_broker_services()
4546

4647
@property
@@ -76,20 +77,19 @@ def _initialize_broker_services(self) -> None:
7677
except Exception as e:
7778
self._logger.warning(f"Failed to initialize {broker_name} service: {e}")
7879

79-
def _broker_supports_service(self, broker_name: str) -> bool:
80+
def _broker_supports_service(self, broker_name: BrokerName) -> bool:
8081
"""
8182
Check if a broker supports a specific service type.
8283
8384
Args:
8485
broker_name: Name of the broker
85-
service_type: Type of service to check
8686
8787
Returns:
8888
True if broker supports the service, False otherwise
8989
"""
9090
return self._factory.broker_supports_service(broker_name, self._service_type)
9191

92-
def _get_broker_service(self, broker_name: str) -> Optional[Any]:
92+
def _get_broker_service(self, broker_name: BrokerName) -> Optional[Any]:
9393
"""
9494
Get a broker service instance for the specified broker.
9595
@@ -111,7 +111,7 @@ def _get_broker_service(self, broker_name: str) -> Optional[Any]:
111111
self._logger.error(f"Failed to create {self._service_type.value} service for {broker_name}: {e}")
112112
return None
113113

114-
def _is_broker_enabled(self, broker_name: str, selected_portfolio: PortfolioId) -> bool:
114+
def _is_broker_enabled(self, broker_name: BrokerName, selected_portfolio: PortfolioId) -> bool:
115115
"""
116116
Check if a broker is enabled for the selected portfolio.
117117
@@ -152,7 +152,7 @@ def _is_broker_enabled(self, broker_name: str, selected_portfolio: PortfolioId)
152152
# Fallback: assume enabled if broker has services
153153
return broker_name in self._broker_services
154154

155-
def _get_enabled_brokers(self, selected_portfolio: PortfolioId) -> List[str]:
155+
def _get_enabled_brokers(self, selected_portfolio: PortfolioId) -> List[BrokerName]:
156156
"""
157157
Get the list of brokers that are enabled for the selected portfolio.
158158
@@ -170,7 +170,7 @@ def _get_enabled_brokers(self, selected_portfolio: PortfolioId) -> List[str]:
170170
if self._is_broker_enabled(broker_name, selected_portfolio)
171171
]
172172

173-
def _collect_broker_data(self, selected_portfolio: PortfolioId, method_name: str) -> Dict[str, Any]:
173+
def _collect_broker_data(self, selected_portfolio: PortfolioId, method_name: str) -> Dict[BrokerName, Any]:
174174
"""
175175
Collect data from all enabled brokers using the specified method.
176176

src/stonks_overwatch/middleware/authentication.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from django.shortcuts import redirect
1111
from django.urls import resolve
1212

13-
from stonks_overwatch.constants.brokers import BrokerName
1413
from stonks_overwatch.core.authentication_locator import get_authentication_service
1514
from stonks_overwatch.core.factories.broker_factory import BrokerFactory
1615
from stonks_overwatch.core.factories.broker_registry import BrokerRegistry
@@ -94,8 +93,8 @@ def _is_authenticated_with_any_broker(self, request) -> bool:
9493
"""
9594
Check if user is authenticated with any broker.
9695
97-
This method checks for authentication across all registered brokers,
98-
including broker-specific session keys.
96+
This method checks for authentication across all registered brokers
97+
using their broker-specific session keys.
9998
10099
Args:
101100
request: The HTTP request containing session data
@@ -104,19 +103,9 @@ def _is_authenticated_with_any_broker(self, request) -> bool:
104103
True if authenticated with at least one broker, False otherwise
105104
"""
106105
try:
107-
# Check DEGIRO authentication (backward compatibility)
108-
if self.auth_service.is_user_authenticated(request):
109-
self.logger.debug("User authenticated with DEGIRO")
110-
return True
111-
112-
# Check broker-specific authentication for other brokers
113106
registered_brokers = self.registry.get_registered_brokers()
114107

115108
for broker_name in registered_brokers:
116-
# Skip DEGIRO as it's already checked above
117-
if broker_name == BrokerName.DEGIRO:
118-
continue
119-
120109
# Check broker-specific session key
121110
broker_auth_key = SessionKeys.get_authenticated_key(broker_name)
122111
if request.session.get(broker_auth_key, False):

src/stonks_overwatch/services/brokers/ibkr/services/portfolio.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def get_portfolio(self) -> List[PortfolioEntry]:
6666

6767
for position in all_positions:
6868
try:
69+
self.logger.debug(f"Position: {position}")
6970
entry = self.__create_portfolio_entry(position, base_currency)
7071
portfolio.append(entry)
7172
except Exception as e:

0 commit comments

Comments
 (0)