Class Name: AccountController¶
Last Updated: 2025-10-22 Source Code: https://github.com/AANP-IT/I2C.Salesforce.Metadata/blob/STAGING/force-app/main/default/classes/AccountController.cls
API Name: AccountController Type: Lightning Web Component Controller (AuraEnabled) Test Coverage: AccountControllerTest.cls Created: 29-Jul-2024 Author: Arina Volchivschi Last Modified: 08-May-2025 by Antoneac Victor
Business Purpose¶
This comprehensive controller manages all user profile and account data operations for AANP's member portal and registration system. It handles member registration, profile updates, account information retrieval, fellowship management, committee membership verification, and demographic data collection. The class serves as the primary API layer between Lightning Web Components and Salesforce data, supporting member self-service portals, registration workflows, profile management, and business account (NPO) administration.
Class Overview¶
Scope and Sharing¶
- Sharing Model: WITHOUT SHARING - Bypasses ALL security
- Access Modifier: public
- Interfaces Implemented: None
CRITICAL SECURITY NOTE: Uses 'without sharing' which bypasses record-level security. Users can access ANY account data they can query. This is extremely dangerous for member data.
Key Responsibilities¶
- User registration and account creation
- Profile data retrieval for person and business accounts
- Account upsert operations
- Picklist values retrieval (gender, ethnicity, race, schools)
- Email validation and updates
- Password management
- Fellowship directory and detail pages
- Committee membership verification
- NPO (business) account access control
- Student membership eligibility calculation
- File server URL generation
- Image upload for profile photos
- Community subscription queries
- Account contact relationship management
Public Methods¶
The class contains 25 @AuraEnabled public methods. Due to the extensive size, I'll document key methods in detail and summarize others:
getUserInfo¶
Purpose: Retrieves current user's account information based on account type (person or business).
Parameters:
- accountType (String): 'business' or any other value defaults to person account
Returns: Map<String,Object> - User data with account fields
Security Check: Validates User and Account object accessibility
Business Logic: - Switch on accountType: 'business' → getBusinessAccountInfo(), else → getPersonAccountInfo() - Returns populated field map for consumption by LWC
upsertAccount¶
Purpose: Creates or updates Account record, defaulting to PersonAccount record type if not specified.
Parameters:
- accountData (Account): Account record to upsert
Returns: String - Account ID
Security Check: Validates Account isUpdateable and isCreateable
Business Logic: 1. Check CRUD permissions 2. Set RecordTypeId to PersonAccount if null 3. Database.upsert() the account 4. Return Account ID
Issues: - Uses Database.upsert but doesn't handle insert vs update differently - Exception returns DML message but loses context
registerUser¶
@AuraEnabled
public static String registerUser(User user, Account accountData, String password, Boolean sendEmail)
Purpose: Registers new external user with person account, creates user via Site.createExternalUser(), and publishes platform event for permission set assignment.
Parameters:
- user (User): User record to create
- accountData (Account): Associated person account
- password (String): User password
- sendEmail (Boolean): Whether to send welcome email
Returns: String - Account ID
Business Logic:
-
Set Savepoint:
-
Check Existing Account:
- Searches by PersonEmail
-
Updates existing if found
-
Create Account (if not exists):
-
Skips insert in tests (test framework limitation)
-
Query Profile and Permission Set Group:
-
CRITICAL: Hardcoded profile and permission set names
-
Create User:
-
Publish Permission Set Assignment Event:
-
Async permission set assignment via platform event
-
Error Handling:
Issues: - Test.isRunningTest() in production code (line 76) - Hardcoded profile/permission set names will break if renamed - No validation of password strength - Exposes full stack trace to user (security risk)
Picklist Methods¶
getGenderValues(), getEthnicityValues(), getRaceValues(), getUsaSchoolsValues()
All four methods follow identical pattern:
Purpose: Retrieves picklist values for demographic fields using Schema describe.
Returns: List<String> - Picklist labels
Fields Retrieved: - Gender: Profile_Gender_Identity__c - Ethnicity: Profile_Race_Ethnicity__c - Race: Profile_Race_Ethnicity__c (same as ethnicity - BUG?) - Schools: Profile_School_Name__c
Note: getRaceValues() and getEthnicityValues() query same field. Likely copy-paste error.
isEmailExisting / isEmailExistingPortal¶
@AuraEnabled(cacheable=false)
public static Boolean isEmailExisting(String email)
@AuraEnabled(cacheable=false)
public static Boolean isEmailExistingPortal(String email, String exemptAccountId)
Purpose: Validates email uniqueness before allowing registration or email change.
isEmailExisting: - Queries User.Email - Returns true if email exists
isEmailExistingPortal: - Queries User.Email AND Account email fields (PersonEmail, Email_Secondary__c, Email_Tertiary__c) - Excludes specific accountId (for self-update scenarios) - More comprehensive check for portal email updates
allowStudentMembershipForAccount¶
@AuraEnabled(cacheable=true)
public static Boolean allowStudentMembershipForAccount(final String accountId)
Purpose: Calculates whether account is eligible for student membership based on business rules.
Business Rules: 1. If any membership with Member_Type__c = 'np member' or 'retired member' → return FALSE (ineligible) 2. Count total days as student member 3. If zero student days → return TRUE (never been student) 4. Calculate total years: totalStudentDays / 365 5. Return TRUE if < 5 years, else FALSE
Logic:
for (Membership__c m : memberships) {
String type = m.Member_Type__c != null ? m.Member_Type__c.toLowerCase() : '';
if (type == 'np member' || type == 'retired member') {
return false; // Once NP/retired, never student again
}
if (type.contains('student') && m.Start_Date__c != null && m.End_Date__c != null) {
totalStudentDays += m.Start_Date__c.daysBetween(m.End_Date__c);
}
}
Issues: - Hardcoded member types - Case-sensitive comparison (type.contains('student')) - No handling of overlapping memberships
Fellowship Methods¶
getFellowsList(), getFellowDetailsById()
Supports fellowship directory display and detail pages.
getFellowsList: - Queries all accounts where Is_a_Fellow__c = true - Returns Name, Induction Year, Location, Title
getFellowDetailsById: - Queries specific fellow's full details including Photo, Bio - Used for fellow detail page
Committee Methods¶
hasCommitteeRole(), getCommitteeByStateCodeAndRole(), isPartOfCommittee()
Support committee membership verification and display.
hasCommitteeRole: - Checks if account has specific role on active committee - Active = End_Date__c = NULL OR > TODAY
getCommitteeByStateCodeAndRole: - Finds committee member by state and role (e.g., State Rep for Texas) - Returns most recent (ORDER BY Start_Date__c DESC)
Business Account Methods¶
checkNPOAccess(), getBusinessAccountInfo(), getAccountContactRelationshipRecords()
Support NPO (Non-Profit Organization) business account management.
checkNPOAccess: - Verifies user has NPO access via AccountContactRelation roles - Roles: 'Company Administrator', 'Addt'l NPO Access', 'Billing Contact'
getBusinessAccountInfo: - Retrieves business account for current user via Contact relationships - Returns account with specific roles only
Private/Helper Methods¶
getPersonAccountInfo¶
Purpose: Queries massive set of person account fields for current user.
Fields Queried: 100+ fields including: - Profile data (demographics, certifications, practice info) - Membership data (type, status, end date, auto-renewal) - Fellow information - Committee roles - Payment methods - Data sharing agreements
Returns: User.getPopulatedFieldsAsMap()
Issue: Returns ALL populated fields regardless of FLS (with 'without sharing')
getBusinessAccountInfo¶
Purpose: Retrieves business account info for NPO users.
Logic: 1. Query current user (basic fields + ContactId) 2. Query Account via AccountContactRelation with specific roles 3. Combine into map
Dependencies¶
Apex Classes¶
- None directly called
- Uses Site methods for user creation
- EventBus for platform events
Salesforce Objects¶
Account (Standard/Person Account) - 100+ fields accessed - Both person and business account support
User (Standard) - User creation, email validation, password changes
Membership__c (Custom) - Membership eligibility calculations
Subscription__c (Custom) - Active community subscriptions
Committee_member__c (Custom) - Committee role verification
AccountContactRelation (Standard) - Business account access control
User_Creation__e (Platform Event) - Async permission set assignment
Custom Settings/Metadata¶
- Label.Variable_B2b_Site_Master_Label - B2B site URL
External Services¶
- Site.createExternalUser() - Experience Cloud
- Site.changePassword() - Experience Cloud
Design Patterns¶
- Controller Pattern: Lightning component backend
- DTO Pattern: Returns Map
for flexible LWC consumption - Factory Pattern: PERSON_ACCOUNT_RECORD_TYPE_ID static initialization
- Savepoint Pattern: Transaction control in registerUser()
- Cache Pattern: communityIdToWebStoreIdCache (but in wrong class!)
Why These Patterns: - Controller pattern standard for LWC integration - DTO pattern enables flexible front-end data binding - Savepoint ensures registration rollback on failure - Cache improves performance for repeated lookups
Pattern Issues: - communityIdToWebStoreIdCache is Commerce-related, doesn't belong in AccountController - No service layer separation (business logic mixed with controller)
Governor Limits Considerations¶
SOQL Queries: Varies by method, up to 5 per call (registerUser) DML Operations: Up to 2 (registerUser: account + user) CPU Time: Medium (large field queries) Heap Size: High (getPersonAccountInfo returns 100+ fields)
Bulkification: N/A (single user/account per call)
Async Processing: Yes (platform event for permission sets)
Governor Limit Risks: - MEDIUM: getPersonAccountInfo queries 100+ fields (CPU + heap) - MEDIUM: Multiple queries per transaction in some methods - LOW: DML operations minimal
Performance Considerations: - Large queries on Account (100+ fields) could be slow - Cacheable methods help performance - Platform event async processing prevents blocking
Recommendations: 1. Split getPersonAccountInfo into multiple smaller queries 2. Only query fields actually needed by calling LWC 3. Consider field sets for dynamic field selection 4. Monitor heap consumption
Error Handling¶
Strategy: Try-catch with AuraHandledException
Logging: - System.debug for errors - No persistent logging - Some methods expose full stack trace to users (SECURITY RISK)
User Notifications: - All methods throw AuraHandledException - Error messages vary from generic to detailed - Some expose too much technical detail
Validation: - CRUD/FLS checks in some methods - Email validation via queries - Minimal input validation
Recommended Improvements: 1. CRITICAL: Don't expose stack traces to users 2. HIGH: Add persistent error logging 3. MEDIUM: Standardize error messages 4. MEDIUM: Add input validation for all parameters 5. LOW: Implement custom exception class for better error handling
Security Considerations¶
Sharing Rules: BYPASSED - Uses 'without sharing' - CRITICAL SECURITY RISK: Users can query ANY account data - No record-level security enforcement - Inappropriate for member data
Field-Level Security: PARTIALLY CHECKED - Some methods check FLS with sObjectType.isAccessible() - But 'without sharing' means checks are insufficient - getPersonAccountInfo returns ALL fields regardless of FLS
CRUD Permissions: PARTIALLY CHECKED - Some methods validate CRUD with isUpdateable(), isCreateable() - Inconsistently applied
Input Validation: MINIMAL - Email validation via query - No validation of most string inputs - No sanitization of user input
Security Risks: - CRITICAL: WITHOUT SHARING allows access to all accounts - HIGH: Stack traces exposed to users - HIGH: No validation of account ownership in most methods - MEDIUM: Hardcoded profile/permission set names - MEDIUM: Password strength not validated - LOW: CustomHandlerException getMessage() override (line 411) returns test message always
Exposure Examples:
// User could call with ANY accountId:
getFellowDetailsById('001xxx') // Returns ANY fellow's data
getLastMembershipCreatedDate('001xxx') // Returns ANY account's membership date
Mitigation Recommendations: 1. CRITICAL: Change to 'with sharing' immediately 2. CRITICAL: Validate accountId belongs to current user 3. HIGH: Remove stack trace from error messages 4. HIGH: Validate all account access against UserInfo.getUserId() 5. MEDIUM: Implement service layer with proper security 6. MEDIUM: Consistent FLS/CRUD checks on ALL methods
Test Class¶
Test Class: AccountControllerTest.cls Coverage: To be determined
Test Scenarios That Should Be Covered:
✓ getUserInfo: - Person account type - Business account type - Invalid account type - User without account access
✓ upsertAccount: - Insert new account - Update existing account - Null RecordTypeId (should default) - Missing required fields
✓ registerUser: - New user registration - Existing account (PersonEmail match) - Invalid password - Profile not found - Permission set group not found - Platform event published
✓ Picklist Methods: - Valid field returns values - Empty picklist - Field doesn't exist
✓ Email Validation: - Email exists - Email doesn't exist - Portal validation with exempt account - Multiple email fields (secondary, tertiary)
✓ Student Eligibility: - No memberships - Student < 5 years - Student >= 5 years - NP member (ineligible) - Retired member (ineligible) - Mixed membership types
✓ Fellowship Methods: - Get all fellows - Get specific fellow details - Fellow not found
✓ Committee Methods: - Has role (active) - Has role (expired) - No role - Multiple roles - State rep lookup
✓ Security: - WITHOUT SHARING behavior (can access other accounts) - CRUD/FLS violations - Account ownership validation (should fail without it)
Testing Challenges: - WITHOUT SHARING makes security testing difficult - Site.createExternalUser() requires Experience Cloud setup - Platform events in tests - Large query in getPersonAccountInfo (test data volume)
Test Data Requirements: - Person accounts with full profile data - Business accounts with contact relationships - Memberships of various types - Committee memberships - Fellows - Users with various profiles
Changes & History¶
- Created: 2024-07-29 by Arina Volchivschi
- Modified: 2024-09-03 by Ecaterina Popa
- Last Modified: 2025-05-08 by Antoneac Victor
- Purpose: Member portal and registration system controller
- Evolution: Accumulated many methods over time, needs refactoring
⚠️ Pre-Go-Live Concerns¶
CRITICAL - Fix Before Go-Live¶
- WITHOUT SHARING SECURITY RISK: Class uses 'without sharing' which bypasses ALL record-level security. Members can access other members' accounts. Change to 'with sharing' immediately. Add account ownership validation to ALL methods.
- STACK TRACE EXPOSURE: Line 103 exposes full stack trace to users. Security vulnerability. Remove stack trace from error messages.
- TEST LOGIC IN PRODUCTION: Lines 76, 275, 293 have Test.isRunningTest() checks. Remove or refactor into dependency injection.
- HARDCODED PROFILE/PERMISSION SET: Lines 83-84 hardcode 'AANP External Identity User' and 'AANP_Commerce_Shopper'. Will break if renamed. Use custom metadata.
HIGH - Address Soon After Go-Live¶
- NO ACCOUNT OWNERSHIP VALIDATION: Most methods accept accountId but don't verify it belongs to current user. Critical security flaw with WITHOUT SHARING.
- INCONSISTENT SECURITY CHECKS: Some methods check FLS/CRUD, others don't. Standardize security across all methods.
- GETRACEVALUES BUG: Line 165 queries Profile_Race_Ethnicity__c (same as getEthnicityValues). Should be different field or method is duplicate.
- MASSIVE FIELD QUERY: getPersonAccountInfo queries 100+ fields. Performance and heap concerns. Query only needed fields.
- CUSTOMHANDLEREXCEPTION BUG: Lines 410-412 override getMessage() to always return test message. Remove or fix properly.
MEDIUM - Future Enhancement¶
- NO SERVICE LAYER: All business logic in controller. Extract to service classes for reusability and testing.
- MIXED RESPONSIBILITIES: Handles user registration, profile management, committees, fellowships, etc. Split into multiple focused controllers.
- COMUNITYIDTOWEBSTOREIDCACHE: Lines 11 - Commerce cart cache in account controller. Move to CartController.
- NO INPUT VALIDATION: Most methods don't validate string inputs. Add validation for all user-provided data.
- STUDENT ELIGIBILITY LOGIC: Hardcoded membership types and business rules. Move to custom metadata configuration.
LOW - Monitor¶
- CODE ORGANIZATION: 700+ line class with 25+ methods. Refactor into multiple smaller classes.
- HARDCODED STRINGS: Many hardcoded values ('Active', 'Checkout', 'Company Administrator', etc). Use constants or custom metadata.
- ERROR MESSAGE CONSISTENCY: Error messages vary widely. Standardize format and detail level.
- SAVEPOINT USAGE: Only registerUser uses savepoint. Consider for other complex operations.
Maintenance Notes¶
Complexity: HIGH (25+ methods, 700+ lines, multiple business domains) Recommended Review Schedule: Monthly, before any security audits
Key Maintainer Notes:
🚨 CRITICAL SECURITY ISSUE - WITHOUT SHARING:
- Impact: Members can access ANY other member's account data - Risk: GDPR/CCPA violation, data breach, member privacy compromised - Fix: Change to 'with sharing' and add account ownership validation:private static void validateAccountOwnership(Id accountId) {
Id userId = UserInfo.getUserId();
User u = [SELECT AccountId FROM User WHERE Id = :userId];
if (u.AccountId != accountId) {
throw new AuraHandledException('Access Denied');
}
}
📋 Usage Patterns: - Called from multiple LWCs in member portal - Registration flow uses registerUser() - Profile pages use getUserInfo() and upsertAccount() - Fellowship directory uses getFellowsList() - Committee lookups throughout portal
🧪 Testing Requirements: - Test WITHOUT SHARING bypass (verify can access other accounts) - Test each of 25+ methods - Test site methods (createExternalUser, changePassword) - Test platform event publishing - Test all picklist methods - Mock large queries for performance
🔧 Configuration Dependencies: - Profile: 'AANP External Identity User' must exist - Permission Set Group: 'AANP_Commerce_Shopper' must exist - Record Type: PersonAccount must exist - Platform Event: User_Creation__e must be configured - Experience Cloud: Site must be configured - Custom Label: Variable_B2b_Site_Master_Label
⚠️ Gotchas and Warnings: - WITHOUT SHARING bypasses all security - Returns 100+ fields in some queries - Test.isRunningTest() in production code - Hardcoded profile/permission set names - Stack traces exposed to users - getRaceValues() and getEthnicityValues() query same field - CustomHandlerException always returns test message - No validation of account ownership
📅 When to Review This Class: - IMMEDIATELY: Fix WITHOUT SHARING security issue - Before any security audit - When adding new profile fields - During GDPR compliance review - When Experience Cloud configuration changes - If member data access issues reported - Before any major release
🛑 Emergency Deactivation:
Cannot easily deactivate - critical for portal functionality. If security issue discovered:
// Add at start of each method:
if (!checkSecurityDisabled()) {
// Validate account ownership
validateAccountOwnership(accountId);
}
private static Boolean checkSecurityDisabled() {
// Check custom metadata to temporarily disable security
// Only for emergency - not a permanent solution
return false;
}
🔍 Debugging Tips: - Enable debug logs for portal users - Check User.AccountId matches Account.Id in queries - Verify profile and permission set assignments - Test with multiple user personas - Check platform event delivery - Monitor heap size for large queries - Verify RecordTypeId assignments
📊 Monitoring Checklist: - Daily: Registration success/failure rate - Daily: Login/profile access errors - Weekly: Query performance (getPersonAccountInfo) - Weekly: Platform event delivery rates - Monthly: Security audit for unauthorized access - Alert: Spike in Aura Handled Exceptions - Alert: Registration failures
🔗 Related Components: - LWCs: Multiple portal components call these methods - Experience Cloud: Site configuration - User_Creation__e: Platform event - AccountTriggerHandler: May have related logic - ProfileManagementLWC: Primary consumer - RegistrationLWC: Uses registerUser() - FellowshipDirectoryLWC: Uses fellowship methods
Business Owner¶
Primary: Member Experience / Portal Team Secondary: IT Security / Membership Operations Stakeholders: GDPR Compliance, Security, Member Services, Product, Development