001// --------------------------------------------------------------------------------
002// Copyright 2002-2026 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017package com.echothree.control.user.authentication.server.command;
018
019import com.echothree.model.control.contact.common.ContactMechanismPurposes;
020import com.echothree.model.control.contact.common.ContactMechanismTypes;
021import com.echothree.model.control.contact.server.control.ContactControl;
022import static com.echothree.model.control.party.common.PartyTypes.CUSTOMER;
023import static com.echothree.model.control.party.common.PartyTypes.EMPLOYEE;
024import static com.echothree.model.control.party.common.PartyTypes.VENDOR;
025import com.echothree.model.control.party.server.control.PartyControl;
026import com.echothree.model.control.security.common.SecurityRoleGroups;
027import static com.echothree.model.control.security.common.SecurityRoleGroups.Employee;
028import com.echothree.model.control.sequence.common.SequenceTypes;
029import com.echothree.model.control.sequence.server.logic.SequenceGeneratorLogic;
030import com.echothree.model.control.uom.common.UomConstants;
031import com.echothree.model.control.uom.server.control.UomControl;
032import com.echothree.model.control.user.common.UserConstants;
033import com.echothree.model.data.party.server.entity.Party;
034import com.echothree.model.data.party.server.entity.PartyRelationship;
035import com.echothree.model.data.party.server.entity.PartyType;
036import com.echothree.model.data.user.server.entity.UserLoginPasswordString;
037import com.echothree.model.data.user.server.entity.UserLoginStatus;
038import com.echothree.util.common.form.BaseForm;
039import com.echothree.util.common.message.ExecutionErrors;
040import com.echothree.util.common.message.ExecutionWarnings;
041import com.echothree.util.common.validation.FieldDefinition;
042import com.echothree.util.server.control.BaseSimpleCommand;
043import com.echothree.util.server.control.CommandSecurityDefinition;
044import com.echothree.util.server.persistence.Sha1Utils;
045import com.echothree.util.server.string.UnitOfMeasureUtils;
046import java.util.List;
047import javax.inject.Inject;
048
049public abstract class BaseLoginCommand<F extends BaseForm>
050        extends BaseSimpleCommand<F> {
051    
052    @Inject
053    ContactControl contactControl;
054
055    @Inject
056    PartyControl partyControl;
057
058    @Inject
059    UomControl uomControl;
060
061    @Inject
062    SequenceGeneratorLogic sequenceGeneratorLogic;
063
064    @Inject
065    Sha1Utils sha1Utils;
066
067    /** Creates a new instance of BaseLoginCommand */
068    protected BaseLoginCommand(final CommandSecurityDefinition commandSecurityDefinition,
069            final List<FieldDefinition> formFieldDefinition) {
070        super(commandSecurityDefinition, formFieldDefinition, false);
071    }
072    
073    protected UserLoginPasswordString checkPassword(final String password, final Party party, final String userLoginPasswordTypeName,
074            final boolean deleteOnSuccess) {
075        var userLoginPasswordType = userControl.getUserLoginPasswordTypeByName(userLoginPasswordTypeName);
076        var userLoginPassword = deleteOnSuccess? userControl.getUserLoginPasswordForUpdate(party, userLoginPasswordType):
077            userControl.getUserLoginPassword(party, userLoginPasswordType);
078        UserLoginPasswordString result = null;
079
080        if(userLoginPassword != null) {
081            var userLoginPasswordEncoderType = userLoginPassword.getUserLoginPasswordType().getUserLoginPasswordEncoderType();
082            var userLoginPasswordEncoderTypeName = userLoginPasswordEncoderType.getUserLoginPasswordEncoderTypeName();
083            var userLoginPasswordString = userControl.getUserLoginPasswordString(userLoginPassword);
084            
085            if(userLoginPasswordEncoderTypeName.equals(UserConstants.UserLoginPasswordEncoderType_SHA1)) {
086                result = sha1Utils.encode(userLoginPasswordString.getSalt(), password).equals(userLoginPasswordString.getPassword()) ? userLoginPasswordString: null;
087            } else if(userLoginPasswordEncoderTypeName.equals(UserConstants.UserLoginPasswordEncoderType_TEXT)) {
088                result = password.equals(userLoginPasswordString.getPassword())? userLoginPasswordString: null;
089            }
090            
091            if(deleteOnSuccess && result != null) {
092                userControl.deleteUserLoginPassword(userLoginPassword, getPartyPK());
093            }
094        }
095        
096        return result;
097    }
098    
099    // TODO: Recovered password should become regular password if that ends up being the password that matches, also,
100    // the recovered password should be deleted if the user logs in using their regular one. Changing a password should also
101    // make sure a recovered password does not exist.
102    protected boolean checkPasswords(final UserLoginStatus userLoginStatus, final String password, final Party party, final boolean doStatusChecks) {
103        var result = checkPassword(password, party, UserConstants.UserLoginPasswordType_STRING, false);
104        
105        if(result == null) {
106            result = checkPassword(password, party, UserConstants.UserLoginPasswordType_RECOVERED_STRING, true);
107        }
108        
109        if(result == null) {
110            addExecutionError(ExecutionErrors.IncorrectPassword.name());
111        } else if(doStatusChecks) {
112            var partyTypePasswordStringPolicy = partyControl.getPartyTypePasswordStringPolicy(party.getLastDetail().getPartyType());
113            
114            if(partyTypePasswordStringPolicy != null) {
115                var partyTypePasswordStringPolicyDetail = partyTypePasswordStringPolicy.getLastDetail();
116                var maximumPasswordLifetime = partyTypePasswordStringPolicyDetail.getMaximumPasswordLifetime();
117                var expiredLoginsPermitted = partyTypePasswordStringPolicyDetail.getExpiredLoginsPermitted();
118                
119                if(maximumPasswordLifetime != null) {
120                    var expirationWarningTime = partyTypePasswordStringPolicyDetail.getExpirationWarningTime();
121                    var changedTime = result.getChangedTime();
122                    
123                    if((session.getStartTime() - changedTime) > maximumPasswordLifetime) {
124                        userLoginStatus.setExpiredCount(userLoginStatus.getExpiredCount() + 1);
125                        
126                        addExecutionWarning(ExecutionWarnings.PasswordExpired.name());
127                    } else if(expirationWarningTime != null) {
128                        var expirationTime = changedTime + maximumPasswordLifetime;
129                        var warningTime = expirationTime - expirationWarningTime;
130                        
131                        if(session.getStartTime() > warningTime) {
132                            var timeUnitOfMeasureKind = uomControl.getUnitOfMeasureKindByUnitOfMeasureKindUseTypeUsingNames(UomConstants.UnitOfMeasureKindUseType_TIME);
133                            var remainingTime = UnitOfMeasureUtils.getInstance().formatUnitOfMeasure(getUserVisit(), timeUnitOfMeasureKind, Long.valueOf(expirationTime - session.getStartTime()));
134                            
135                            addExecutionWarning(ExecutionWarnings.PasswordExpiration.name(), remainingTime);
136                        }
137                    }
138                }
139                
140                if(expiredLoginsPermitted != null && userLoginStatus.getExpiredCount() > expiredLoginsPermitted) {
141                    result = null;
142                    addExecutionError(ExecutionErrors.MaximumExpiredLoginsPermittedExceeded.name(), expiredLoginsPermitted);
143                }
144
145                if(userLoginStatus.getForceChange()) {
146                    addExecutionWarning(ExecutionWarnings.ForcePasswordChange.name());
147                }
148            }
149        }
150        
151        return result != null;
152    }
153
154    protected String getSecurityRoleGroupName(final PartyType partyType) {
155        String securityRoleGroupName = null;
156        var partyTypeName = partyType.getPartyTypeName();
157
158        if(partyTypeName.equals(CUSTOMER.name())) {
159            securityRoleGroupName = SecurityRoleGroups.Customer.name();
160        } else if(partyTypeName.equals(EMPLOYEE.name())) {
161            securityRoleGroupName = Employee.name();
162        } else if(partyTypeName.equals(VENDOR.name())) {
163            securityRoleGroupName = SecurityRoleGroups.Vendor.name();
164        }
165
166        return securityRoleGroupName;
167    }
168
169    protected void clearLoginFailures(final UserLoginStatus userLoginStatus) {
170        // Audit trail for callers of this function should be created by the callers.
171        userLoginStatus.setFailureCount(0);
172        userLoginStatus.setFirstFailureTime(null);
173        userLoginStatus.setLastFailureTime(null);
174    }
175
176    protected void addRemoteInet4AddressToParty(final Party party, final Integer remoteInet4Address) {
177        if(remoteInet4Address != null) {
178            var partyPK = party.getPrimaryKey();
179            var partyContactMechanism = contactControl.getPartyContactMechanismByInet4Address(party, remoteInet4Address);
180
181            if(partyContactMechanism == null) {
182                var contactMechanismName = sequenceGeneratorLogic.getNextSequenceValue(null, SequenceTypes.CONTACT_MECHANISM.name());
183                var contactMechanismType = contactControl.getContactMechanismTypeByName(ContactMechanismTypes.INET_4.name());
184                var contactMechanism = contactControl.createContactMechanism(contactMechanismName, contactMechanismType, false, partyPK);
185
186                contactControl.createContactInet4Address(contactMechanism, remoteInet4Address, partyPK);
187                partyContactMechanism = contactControl.createPartyContactMechanism(party, contactMechanism, null, false, 1, partyPK);
188            }
189
190            var contactMechanismPurpose = contactControl.getContactMechanismPurposeByName(ContactMechanismPurposes.INET_4_LOGIN.name());
191            var partyContactMechanismPurpose = contactControl.getPartyContactMechanismPurpose(partyContactMechanism, contactMechanismPurpose);
192            if(partyContactMechanismPurpose == null) {
193                contactControl.createPartyContactMechanismPurpose(partyContactMechanism, contactMechanismPurpose, false, 1, partyPK);
194            }
195        }
196    }
197
198    protected void successfulLogin(final UserLoginStatus userLoginStatus, final Party party, final PartyRelationship partyRelationship,
199            final Integer remoteInet4Address) {
200        var userVisit = getUserVisitForUpdate();
201        var userKey = userVisit.getUserKey();
202        var userKeyDetailValue = userControl.getUserKeyDetailValueByPKForUpdate(userKey.getLastDetail().getPrimaryKey());
203
204        userControl.associatePartyToUserVisit(userVisit, party, partyRelationship, session.getStartTime());
205        
206        // Only update the UserKeyDetail if the party has changed
207        var partyPK = party.getPrimaryKey();
208        var partyRelationshipPK = partyRelationship == null? null: partyRelationship.getPrimaryKey();
209        var userKeyPartyPK = userKeyDetailValue.getPartyPK();
210        var userKeyPartyRelationshipPK = userKeyDetailValue.getPartyRelationshipPK();
211        
212        if(userKeyPartyPK == null || !userKeyPartyPK.equals(partyPK)
213                || userKeyPartyRelationshipPK == null || !userKeyPartyRelationshipPK.equals(partyRelationshipPK)) {
214            userKeyDetailValue.setPartyPK(partyPK);
215            userKeyDetailValue.setPartyRelationshipPK(partyRelationshipPK);
216            userControl.updateUserKeyFromValue(userKeyDetailValue);
217        }
218
219        clearLoginFailures(userLoginStatus);
220
221        userLoginStatus.setLastLoginTime(session.getStartTime());
222
223        addRemoteInet4AddressToParty(party, remoteInet4Address);
224
225        // TODO: Create audit trail
226    }
227    
228    protected void unsuccessfulLogin(final UserLoginStatus userLoginStatus) {
229        var failureCount = userLoginStatus.getFailureCount();
230        
231        userLoginStatus.setFailureCount(failureCount + 1);
232        if(userLoginStatus.getFirstFailureTime() == null) {
233            userLoginStatus.setFirstFailureTime(session.getStartTime());
234        }
235        userLoginStatus.setLastFailureTime(session.getStartTime());
236        
237        // TODO: Create audit trail
238    }
239    
240}