| Current Path : /var/git/cesa-v3/docs/specs/ |
| Current File : /var/git/cesa-v3/docs/specs/member-portal-login-design.md |
# Member portal login (V3) — design ## Data model | Table | Entity | Notes | |-------|--------|-------| | `user_login` | `UserLogin` | `id`, `contact_id`, `username`, `password`, `salt` | ## Security ``` form_login → chain provider ├─ users (UserList) ├─ member_portal_users (UserLogin + firm validation) └─ echasl23_users (UserOld) ``` - **Hasher:** `MemberPortalPasswordHasher` — plain compare; `crypt(plain, salt)` when `salt` is non-empty. - **Roles:** `MemberPortalUserService::resolveRoles()` → `ROLE_MEMBER_FIRM`, `ROLE_USER`, `ROLE_LOGGED_IN`, `ROLE_MY_DETAILS`, `IS_AUTHENTICATED_FULLY`. - **Firm resolution:** `MemberContactRepository::findByContactIdWithMemberFirm()`. ## Services | Class | Responsibility | |-------|----------------| | `MemberPortalUserService` | Firm context, roles, session flags | | `MemberPortalPasswordHasher` | Legacy password verification | | `MemberPortalUserProvider` | Load `UserLogin`; reject when firm missing | ## Portal integration `MemberFirmTransformationPortalService::resolvePortalContact()` accepts `UserList` (email + mandated principal) or `UserLogin` (contact id + mandated principal). ## Database validation (cesa_main) Verified against live schema: | Table / column | Entity mapping | Notes | |----------------|----------------|-------| | `user_login.id` | `UserLogin.id` | int PK auto_increment | | `user_login.contact_id` | `UserLogin.contactId` | Matches `contacts.ContactsID` and `membercontacts.ContactID` (2,729+ rows) | | `user_login.username` | `UserLogin.username` | `varchar(12)` — entity length set to 12 | | `user_login.password` | `UserLogin.password` | `varchar(50)` | | `user_login.salt` | `UserLogin.salt` | `varchar(50)`, nullable | | `membercontacts.ContactID` | `MemberContact.id` | Join key for portal firm resolution | | `membercontacts.OfficeID` | `MemberContact.member_office` | Direct FK to `tblMemberFirmOffices` (not `officecontacts` junction) | | `tblmemberfirms.MemberStatus` | `MemberFirm.member_status` | Values include `Member`, `Retired`; no `Testing` in DB (kept in code for legacy parity) | Firm eligibility counts (username/password accounts with `contact_id > 0`): **1,652** can resolve a firm with status in `Member` / `Testing` / `Retired` via `membercontacts` → office → firm — same count as legacy `contacts` → `officecontacts` path. **522** accounts have no `membercontacts` row and cannot pass firm checks on either system.