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.
Files changed (47) hide show
  1. nanopy_bank/__init__.py +20 -0
  2. nanopy_bank/api/__init__.py +10 -0
  3. nanopy_bank/api/server.py +242 -0
  4. nanopy_bank/app.py +282 -0
  5. nanopy_bank/cli.py +152 -0
  6. nanopy_bank/core/__init__.py +85 -0
  7. nanopy_bank/core/audit.py +404 -0
  8. nanopy_bank/core/auth.py +306 -0
  9. nanopy_bank/core/bank.py +407 -0
  10. nanopy_bank/core/beneficiary.py +258 -0
  11. nanopy_bank/core/branch.py +319 -0
  12. nanopy_bank/core/fees.py +243 -0
  13. nanopy_bank/core/holding.py +416 -0
  14. nanopy_bank/core/models.py +308 -0
  15. nanopy_bank/core/products.py +300 -0
  16. nanopy_bank/data/__init__.py +31 -0
  17. nanopy_bank/data/demo.py +846 -0
  18. nanopy_bank/documents/__init__.py +11 -0
  19. nanopy_bank/documents/statement.py +304 -0
  20. nanopy_bank/sepa/__init__.py +10 -0
  21. nanopy_bank/sepa/sepa.py +452 -0
  22. nanopy_bank/storage/__init__.py +11 -0
  23. nanopy_bank/storage/json_storage.py +127 -0
  24. nanopy_bank/storage/sqlite_storage.py +326 -0
  25. nanopy_bank/ui/__init__.py +14 -0
  26. nanopy_bank/ui/pages/__init__.py +33 -0
  27. nanopy_bank/ui/pages/accounts.py +85 -0
  28. nanopy_bank/ui/pages/advisor.py +140 -0
  29. nanopy_bank/ui/pages/audit.py +73 -0
  30. nanopy_bank/ui/pages/beneficiaries.py +115 -0
  31. nanopy_bank/ui/pages/branches.py +64 -0
  32. nanopy_bank/ui/pages/cards.py +36 -0
  33. nanopy_bank/ui/pages/common.py +18 -0
  34. nanopy_bank/ui/pages/dashboard.py +100 -0
  35. nanopy_bank/ui/pages/fees.py +60 -0
  36. nanopy_bank/ui/pages/holding.py +943 -0
  37. nanopy_bank/ui/pages/loans.py +105 -0
  38. nanopy_bank/ui/pages/login.py +174 -0
  39. nanopy_bank/ui/pages/sepa.py +118 -0
  40. nanopy_bank/ui/pages/settings.py +48 -0
  41. nanopy_bank/ui/pages/transfers.py +94 -0
  42. nanopy_bank/ui/pages.py +16 -0
  43. nanopy_bank-1.0.8.dist-info/METADATA +72 -0
  44. nanopy_bank-1.0.8.dist-info/RECORD +47 -0
  45. nanopy_bank-1.0.8.dist-info/WHEEL +5 -0
  46. nanopy_bank-1.0.8.dist-info/entry_points.txt +2 -0
  47. nanopy_bank-1.0.8.dist-info/top_level.txt +1 -0
@@ -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