nanopy-bank 1.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nanopy_bank/__init__.py +20 -0
- nanopy_bank/api/__init__.py +10 -0
- nanopy_bank/api/server.py +242 -0
- nanopy_bank/app.py +282 -0
- nanopy_bank/cli.py +152 -0
- nanopy_bank/core/__init__.py +85 -0
- nanopy_bank/core/audit.py +404 -0
- nanopy_bank/core/auth.py +306 -0
- nanopy_bank/core/bank.py +407 -0
- nanopy_bank/core/beneficiary.py +258 -0
- nanopy_bank/core/branch.py +319 -0
- nanopy_bank/core/fees.py +243 -0
- nanopy_bank/core/holding.py +416 -0
- nanopy_bank/core/models.py +308 -0
- nanopy_bank/core/products.py +300 -0
- nanopy_bank/data/__init__.py +31 -0
- nanopy_bank/data/demo.py +846 -0
- nanopy_bank/documents/__init__.py +11 -0
- nanopy_bank/documents/statement.py +304 -0
- nanopy_bank/sepa/__init__.py +10 -0
- nanopy_bank/sepa/sepa.py +452 -0
- nanopy_bank/storage/__init__.py +11 -0
- nanopy_bank/storage/json_storage.py +127 -0
- nanopy_bank/storage/sqlite_storage.py +326 -0
- nanopy_bank/ui/__init__.py +14 -0
- nanopy_bank/ui/pages/__init__.py +33 -0
- nanopy_bank/ui/pages/accounts.py +85 -0
- nanopy_bank/ui/pages/advisor.py +140 -0
- nanopy_bank/ui/pages/audit.py +73 -0
- nanopy_bank/ui/pages/beneficiaries.py +115 -0
- nanopy_bank/ui/pages/branches.py +64 -0
- nanopy_bank/ui/pages/cards.py +36 -0
- nanopy_bank/ui/pages/common.py +18 -0
- nanopy_bank/ui/pages/dashboard.py +100 -0
- nanopy_bank/ui/pages/fees.py +60 -0
- nanopy_bank/ui/pages/holding.py +943 -0
- nanopy_bank/ui/pages/loans.py +105 -0
- nanopy_bank/ui/pages/login.py +174 -0
- nanopy_bank/ui/pages/sepa.py +118 -0
- nanopy_bank/ui/pages/settings.py +48 -0
- nanopy_bank/ui/pages/transfers.py +94 -0
- nanopy_bank/ui/pages.py +16 -0
- nanopy_bank-1.0.8.dist-info/METADATA +72 -0
- nanopy_bank-1.0.8.dist-info/RECORD +47 -0
- nanopy_bank-1.0.8.dist-info/WHEEL +5 -0
- nanopy_bank-1.0.8.dist-info/entry_points.txt +2 -0
- nanopy_bank-1.0.8.dist-info/top_level.txt +1 -0
nanopy_bank/core/auth.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication and User Management
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Optional, List
|
|
9
|
+
import uuid
|
|
10
|
+
import hashlib
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UserRole(Enum):
|
|
14
|
+
"""User roles with different access levels"""
|
|
15
|
+
CLIENT = "client" # Voir ses comptes uniquement
|
|
16
|
+
ADVISOR = "advisor" # Voir son portefeuille clients
|
|
17
|
+
DIRECTOR = "director" # Voir son agence + equipe
|
|
18
|
+
ADMIN = "admin" # Acces admin banque
|
|
19
|
+
HOLDING = "holding" # Acces groupe/holding
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UserStatus(Enum):
|
|
23
|
+
"""User account status"""
|
|
24
|
+
ACTIVE = "active"
|
|
25
|
+
LOCKED = "locked"
|
|
26
|
+
PENDING = "pending"
|
|
27
|
+
DISABLED = "disabled"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class User:
|
|
32
|
+
"""
|
|
33
|
+
User account for authentication
|
|
34
|
+
"""
|
|
35
|
+
user_id: str = field(default_factory=lambda: f"USR{uuid.uuid4().hex[:8].upper()}")
|
|
36
|
+
|
|
37
|
+
# Credentials
|
|
38
|
+
client_id: str = "" # 8-digit bank identifier (identifiant bancaire)
|
|
39
|
+
email: str = ""
|
|
40
|
+
password_hash: str = ""
|
|
41
|
+
|
|
42
|
+
# Role
|
|
43
|
+
role: UserRole = UserRole.CLIENT
|
|
44
|
+
|
|
45
|
+
# Links to entities
|
|
46
|
+
customer_id: Optional[str] = None # If role=CLIENT
|
|
47
|
+
employee_id: Optional[str] = None # If role=ADVISOR/DIRECTOR/ADMIN
|
|
48
|
+
holding_id: Optional[str] = None # If role=HOLDING
|
|
49
|
+
|
|
50
|
+
# Profile
|
|
51
|
+
display_name: str = ""
|
|
52
|
+
avatar_url: str = ""
|
|
53
|
+
language: str = "fr"
|
|
54
|
+
timezone: str = "Europe/Paris"
|
|
55
|
+
|
|
56
|
+
# Security
|
|
57
|
+
status: UserStatus = UserStatus.ACTIVE
|
|
58
|
+
mfa_enabled: bool = False
|
|
59
|
+
mfa_secret: str = ""
|
|
60
|
+
|
|
61
|
+
# Session
|
|
62
|
+
last_login: Optional[datetime] = None
|
|
63
|
+
last_ip: str = ""
|
|
64
|
+
failed_attempts: int = 0
|
|
65
|
+
locked_until: Optional[datetime] = None
|
|
66
|
+
|
|
67
|
+
# Metadata
|
|
68
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
69
|
+
updated_at: datetime = field(default_factory=datetime.now)
|
|
70
|
+
|
|
71
|
+
def set_password(self, password: str):
|
|
72
|
+
"""Hash and set password"""
|
|
73
|
+
salt = uuid.uuid4().hex[:16]
|
|
74
|
+
self.password_hash = f"{salt}:{hashlib.sha256((salt + password).encode()).hexdigest()}"
|
|
75
|
+
|
|
76
|
+
def check_password(self, password: str) -> bool:
|
|
77
|
+
"""Verify password"""
|
|
78
|
+
if not self.password_hash or ":" not in self.password_hash:
|
|
79
|
+
return False
|
|
80
|
+
salt, hash_value = self.password_hash.split(":", 1)
|
|
81
|
+
return hashlib.sha256((salt + password).encode()).hexdigest() == hash_value
|
|
82
|
+
|
|
83
|
+
def can_access(self, required_role: UserRole) -> bool:
|
|
84
|
+
"""Check if user has required access level"""
|
|
85
|
+
hierarchy = {
|
|
86
|
+
UserRole.CLIENT: 1,
|
|
87
|
+
UserRole.ADVISOR: 2,
|
|
88
|
+
UserRole.DIRECTOR: 3,
|
|
89
|
+
UserRole.ADMIN: 4,
|
|
90
|
+
UserRole.HOLDING: 5,
|
|
91
|
+
}
|
|
92
|
+
return hierarchy.get(self.role, 0) >= hierarchy.get(required_role, 0)
|
|
93
|
+
|
|
94
|
+
def to_dict(self) -> dict:
|
|
95
|
+
return {
|
|
96
|
+
"user_id": self.user_id,
|
|
97
|
+
"client_id": self.client_id,
|
|
98
|
+
"email": self.email,
|
|
99
|
+
"role": self.role.value,
|
|
100
|
+
"customer_id": self.customer_id,
|
|
101
|
+
"employee_id": self.employee_id,
|
|
102
|
+
"holding_id": self.holding_id,
|
|
103
|
+
"display_name": self.display_name,
|
|
104
|
+
"avatar_url": self.avatar_url,
|
|
105
|
+
"language": self.language,
|
|
106
|
+
"timezone": self.timezone,
|
|
107
|
+
"status": self.status.value,
|
|
108
|
+
"mfa_enabled": self.mfa_enabled,
|
|
109
|
+
"last_login": self.last_login.isoformat() if self.last_login else None,
|
|
110
|
+
"last_ip": self.last_ip,
|
|
111
|
+
"created_at": self.created_at.isoformat(),
|
|
112
|
+
"updated_at": self.updated_at.isoformat(),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class Session:
|
|
118
|
+
"""
|
|
119
|
+
User session
|
|
120
|
+
"""
|
|
121
|
+
session_id: str = field(default_factory=lambda: uuid.uuid4().hex)
|
|
122
|
+
user_id: str = ""
|
|
123
|
+
|
|
124
|
+
# Session info
|
|
125
|
+
ip_address: str = ""
|
|
126
|
+
user_agent: str = ""
|
|
127
|
+
device_type: str = "" # web, mobile, api
|
|
128
|
+
|
|
129
|
+
# Timing
|
|
130
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
131
|
+
expires_at: Optional[datetime] = None
|
|
132
|
+
last_activity: datetime = field(default_factory=datetime.now)
|
|
133
|
+
|
|
134
|
+
# Status
|
|
135
|
+
is_active: bool = True
|
|
136
|
+
|
|
137
|
+
def is_expired(self) -> bool:
|
|
138
|
+
if self.expires_at and datetime.now() > self.expires_at:
|
|
139
|
+
return True
|
|
140
|
+
return not self.is_active
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class AuthService:
|
|
144
|
+
"""
|
|
145
|
+
Authentication service
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self):
|
|
149
|
+
self.users: dict[str, User] = {}
|
|
150
|
+
self.sessions: dict[str, Session] = {}
|
|
151
|
+
self._create_demo_users()
|
|
152
|
+
|
|
153
|
+
def _create_demo_users(self):
|
|
154
|
+
"""Create demo users for testing"""
|
|
155
|
+
# Client - 1xxxxxxx
|
|
156
|
+
client = User(
|
|
157
|
+
user_id="USR_CLIENT",
|
|
158
|
+
client_id="10000001",
|
|
159
|
+
email="client@nanopybank.fr",
|
|
160
|
+
role=UserRole.CLIENT,
|
|
161
|
+
customer_id="CUST001",
|
|
162
|
+
display_name="Jean Dupont"
|
|
163
|
+
)
|
|
164
|
+
client.set_password("demo123")
|
|
165
|
+
self.users[client.client_id] = client
|
|
166
|
+
|
|
167
|
+
# Advisor - 2xxxxxxx
|
|
168
|
+
advisor = User(
|
|
169
|
+
user_id="USR_ADVISOR",
|
|
170
|
+
client_id="20000001",
|
|
171
|
+
email="advisor@nanopybank.fr",
|
|
172
|
+
role=UserRole.ADVISOR,
|
|
173
|
+
employee_id="EMP003",
|
|
174
|
+
display_name="Thomas Moreau"
|
|
175
|
+
)
|
|
176
|
+
advisor.set_password("demo123")
|
|
177
|
+
self.users[advisor.client_id] = advisor
|
|
178
|
+
|
|
179
|
+
# Director - 3xxxxxxx
|
|
180
|
+
director = User(
|
|
181
|
+
user_id="USR_DIRECTOR",
|
|
182
|
+
client_id="30000001",
|
|
183
|
+
email="director@nanopybank.fr",
|
|
184
|
+
role=UserRole.DIRECTOR,
|
|
185
|
+
employee_id="EMP002",
|
|
186
|
+
display_name="Camille Leroy"
|
|
187
|
+
)
|
|
188
|
+
director.set_password("demo123")
|
|
189
|
+
self.users[director.client_id] = director
|
|
190
|
+
|
|
191
|
+
# Admin - 4xxxxxxx
|
|
192
|
+
admin = User(
|
|
193
|
+
user_id="USR_ADMIN",
|
|
194
|
+
client_id="40000001",
|
|
195
|
+
email="admin@nanopybank.fr",
|
|
196
|
+
role=UserRole.ADMIN,
|
|
197
|
+
employee_id="EMP001",
|
|
198
|
+
display_name="Laurent Dubois"
|
|
199
|
+
)
|
|
200
|
+
admin.set_password("demo123")
|
|
201
|
+
self.users[admin.client_id] = admin
|
|
202
|
+
|
|
203
|
+
# Holding - 5xxxxxxx
|
|
204
|
+
holding = User(
|
|
205
|
+
user_id="USR_HOLDING",
|
|
206
|
+
client_id="50000001",
|
|
207
|
+
email="holding@novaxgenesis.fr",
|
|
208
|
+
role=UserRole.HOLDING,
|
|
209
|
+
holding_id="HOLD001",
|
|
210
|
+
display_name="Nova x Genesis"
|
|
211
|
+
)
|
|
212
|
+
holding.set_password("demo123")
|
|
213
|
+
self.users[holding.client_id] = holding
|
|
214
|
+
|
|
215
|
+
def login(self, email: str, password: str, ip: str = "") -> Optional[Session]:
|
|
216
|
+
"""Authenticate user by email and create session (legacy)"""
|
|
217
|
+
# Find user by email
|
|
218
|
+
user = None
|
|
219
|
+
for u in self.users.values():
|
|
220
|
+
if u.email == email:
|
|
221
|
+
user = u
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
if not user:
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
return self._authenticate(user, password, ip)
|
|
228
|
+
|
|
229
|
+
def login_by_client_id(self, client_id: str, password: str, ip: str = "") -> Optional[Session]:
|
|
230
|
+
"""Authenticate user by bank client ID and create session"""
|
|
231
|
+
user = self.users.get(client_id)
|
|
232
|
+
|
|
233
|
+
if not user:
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
return self._authenticate(user, password, ip)
|
|
237
|
+
|
|
238
|
+
def _authenticate(self, user: User, password: str, ip: str = "") -> Optional[Session]:
|
|
239
|
+
"""Internal authentication logic"""
|
|
240
|
+
if user.status != UserStatus.ACTIVE:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
if user.locked_until and datetime.now() < user.locked_until:
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
if not user.check_password(password):
|
|
247
|
+
user.failed_attempts += 1
|
|
248
|
+
if user.failed_attempts >= 5:
|
|
249
|
+
from datetime import timedelta
|
|
250
|
+
user.locked_until = datetime.now() + timedelta(minutes=15)
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
# Success
|
|
254
|
+
user.failed_attempts = 0
|
|
255
|
+
user.last_login = datetime.now()
|
|
256
|
+
user.last_ip = ip
|
|
257
|
+
|
|
258
|
+
# Create session
|
|
259
|
+
session = Session(
|
|
260
|
+
user_id=user.user_id,
|
|
261
|
+
ip_address=ip,
|
|
262
|
+
)
|
|
263
|
+
self.sessions[session.session_id] = session
|
|
264
|
+
|
|
265
|
+
return session
|
|
266
|
+
|
|
267
|
+
def logout(self, session_id: str):
|
|
268
|
+
"""End session"""
|
|
269
|
+
if session_id in self.sessions:
|
|
270
|
+
self.sessions[session_id].is_active = False
|
|
271
|
+
|
|
272
|
+
def get_user(self, email: str) -> Optional[User]:
|
|
273
|
+
"""Get user by email"""
|
|
274
|
+
for user in self.users.values():
|
|
275
|
+
if user.email == email:
|
|
276
|
+
return user
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
def get_user_by_client_id(self, client_id: str) -> Optional[User]:
|
|
280
|
+
"""Get user by bank client ID"""
|
|
281
|
+
return self.users.get(client_id)
|
|
282
|
+
|
|
283
|
+
def get_user_by_id(self, user_id: str) -> Optional[User]:
|
|
284
|
+
"""Get user by ID"""
|
|
285
|
+
for user in self.users.values():
|
|
286
|
+
if user.user_id == user_id:
|
|
287
|
+
return user
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
def get_session(self, session_id: str) -> Optional[Session]:
|
|
291
|
+
"""Get active session"""
|
|
292
|
+
session = self.sessions.get(session_id)
|
|
293
|
+
if session and not session.is_expired():
|
|
294
|
+
session.last_activity = datetime.now()
|
|
295
|
+
return session
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# Global auth service
|
|
300
|
+
_auth_service: Optional[AuthService] = None
|
|
301
|
+
|
|
302
|
+
def get_auth_service() -> AuthService:
|
|
303
|
+
global _auth_service
|
|
304
|
+
if _auth_service is None:
|
|
305
|
+
_auth_service = AuthService()
|
|
306
|
+
return _auth_service
|