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
065    /** Creates a new instance of BaseLoginCommand */
066    protected BaseLoginCommand(final CommandSecurityDefinition commandSecurityDefinition,
067            final List<FieldDefinition> formFieldDefinition) {
068        super(commandSecurityDefinition, formFieldDefinition, false);
069    }
070    
071    protected UserLoginPasswordString checkPassword(final String password, final Party party, final String userLoginPasswordTypeName,
072            final boolean deleteOnSuccess) {
073        var userLoginPasswordType = userControl.getUserLoginPasswordTypeByName(userLoginPasswordTypeName);
074        var userLoginPassword = deleteOnSuccess? userControl.getUserLoginPasswordForUpdate(party, userLoginPasswordType):
075            userControl.getUserLoginPassword(party, userLoginPasswordType);
076        UserLoginPasswordString result = null;
077
078        if(userLoginPassword != null) {
079            var userLoginPasswordEncoderType = userLoginPassword.getUserLoginPasswordType().getUserLoginPasswordEncoderType();
080            var userLoginPasswordEncoderTypeName = userLoginPasswordEncoderType.getUserLoginPasswordEncoderTypeName();
081            var userLoginPasswordString = userControl.getUserLoginPasswordString(userLoginPassword);
082            
083            if(userLoginPasswordEncoderTypeName.equals(UserConstants.UserLoginPasswordEncoderType_SHA1)) {
084                result = Sha1Utils.getInstance().encode(userLoginPasswordString.getSalt(), password).equals(userLoginPasswordString.getPassword()) ? userLoginPasswordString: null;
085            } else if(userLoginPasswordEncoderTypeName.equals(UserConstants.UserLoginPasswordEncoderType_TEXT)) {
086                result = password.equals(userLoginPasswordString.getPassword())? userLoginPasswordString: null;
087            }
088            
089            if(deleteOnSuccess && result != null) {
090                userControl.deleteUserLoginPassword(userLoginPassword, getPartyPK());
091            }
092        }
093        
094        return result;
095    }
096    
097    // TODO: Recovered password should become regular password if that ends up being the password that matches, also,
098    // the recovered password should be deleted if the user logs in using their regular one. Changing a password should also
099    // make sure a recovered password does not exist.
100    protected boolean checkPasswords(final UserLoginStatus userLoginStatus, final String password, final Party party, final boolean doStatusChecks) {
101        var result = checkPassword(password, party, UserConstants.UserLoginPasswordType_STRING, false);
102        
103        if(result == null) {
104            result = checkPassword(password, party, UserConstants.UserLoginPasswordType_RECOVERED_STRING, true);
105        }
106        
107        if(result == null) {
108            addExecutionError(ExecutionErrors.IncorrectPassword.name());
109        } else if(doStatusChecks) {
110            var partyTypePasswordStringPolicy = partyControl.getPartyTypePasswordStringPolicy(party.getLastDetail().getPartyType());
111            
112            if(partyTypePasswordStringPolicy != null) {
113                var partyTypePasswordStringPolicyDetail = partyTypePasswordStringPolicy.getLastDetail();
114                var maximumPasswordLifetime = partyTypePasswordStringPolicyDetail.getMaximumPasswordLifetime();
115                var expiredLoginsPermitted = partyTypePasswordStringPolicyDetail.getExpiredLoginsPermitted();
116                
117                if(maximumPasswordLifetime != null) {
118                    var expirationWarningTime = partyTypePasswordStringPolicyDetail.getExpirationWarningTime();
119                    var changedTime = result.getChangedTime();
120                    
121                    if((session.getStartTime() - changedTime) > maximumPasswordLifetime) {
122                        userLoginStatus.setExpiredCount(userLoginStatus.getExpiredCount() + 1);
123                        
124                        addExecutionWarning(ExecutionWarnings.PasswordExpired.name());
125                    } else if(expirationWarningTime != null) {
126                        var expirationTime = changedTime + maximumPasswordLifetime;
127                        var warningTime = expirationTime - expirationWarningTime;
128                        
129                        if(session.getStartTime() > warningTime) {
130                            var timeUnitOfMeasureKind = uomControl.getUnitOfMeasureKindByUnitOfMeasureKindUseTypeUsingNames(UomConstants.UnitOfMeasureKindUseType_TIME);
131                            var remainingTime = UnitOfMeasureUtils.getInstance().formatUnitOfMeasure(getUserVisit(), timeUnitOfMeasureKind, Long.valueOf(expirationTime - session.getStartTime()));
132                            
133                            addExecutionWarning(ExecutionWarnings.PasswordExpiration.name(), remainingTime);
134                        }
135                    }
136                }
137                
138                if(expiredLoginsPermitted != null && userLoginStatus.getExpiredCount() > expiredLoginsPermitted) {
139                    result = null;
140                    addExecutionError(ExecutionErrors.MaximumExpiredLoginsPermittedExceeded.name(), expiredLoginsPermitted);
141                }
142
143                if(userLoginStatus.getForceChange()) {
144                    addExecutionWarning(ExecutionWarnings.ForcePasswordChange.name());
145                }
146            }
147        }
148        
149        return result != null;
150    }
151
152    protected String getSecurityRoleGroupName(final PartyType partyType) {
153        String securityRoleGroupName = null;
154        var partyTypeName = partyType.getPartyTypeName();
155
156        if(partyTypeName.equals(CUSTOMER.name())) {
157            securityRoleGroupName = SecurityRoleGroups.Customer.name();
158        } else if(partyTypeName.equals(EMPLOYEE.name())) {
159            securityRoleGroupName = Employee.name();
160        } else if(partyTypeName.equals(VENDOR.name())) {
161            securityRoleGroupName = SecurityRoleGroups.Vendor.name();
162        }
163
164        return securityRoleGroupName;
165    }
166
167    protected void clearLoginFailures(final UserLoginStatus userLoginStatus) {
168        // Audit trail for callers of this function should be created by the callers.
169        userLoginStatus.setFailureCount(0);
170        userLoginStatus.setFirstFailureTime(null);
171        userLoginStatus.setLastFailureTime(null);
172    }
173
174    protected void addRemoteInet4AddressToParty(final Party party, final Integer remoteInet4Address) {
175        if(remoteInet4Address != null) {
176            var partyPK = party.getPrimaryKey();
177            var partyContactMechanism = contactControl.getPartyContactMechanismByInet4Address(party, remoteInet4Address);
178
179            if(partyContactMechanism == null) {
180                var contactMechanismName = sequenceGeneratorLogic.getNextSequenceValue(null, SequenceTypes.CONTACT_MECHANISM.name());
181                var contactMechanismType = contactControl.getContactMechanismTypeByName(ContactMechanismTypes.INET_4.name());
182                var contactMechanism = contactControl.createContactMechanism(contactMechanismName, contactMechanismType, false, partyPK);
183
184                contactControl.createContactInet4Address(contactMechanism, remoteInet4Address, partyPK);
185                partyContactMechanism = contactControl.createPartyContactMechanism(party, contactMechanism, null, false, 1, partyPK);
186            }
187
188            var contactMechanismPurpose = contactControl.getContactMechanismPurposeByName(ContactMechanismPurposes.INET_4_LOGIN.name());
189            var partyContactMechanismPurpose = contactControl.getPartyContactMechanismPurpose(partyContactMechanism, contactMechanismPurpose);
190            if(partyContactMechanismPurpose == null) {
191                contactControl.createPartyContactMechanismPurpose(partyContactMechanism, contactMechanismPurpose, false, 1, partyPK);
192            }
193        }
194    }
195
196    protected void successfulLogin(final UserLoginStatus userLoginStatus, final Party party, final PartyRelationship partyRelationship,
197            final Integer remoteInet4Address) {
198        var userVisit = getUserVisitForUpdate();
199        var userKey = userVisit.getUserKey();
200        var userKeyDetailValue = userControl.getUserKeyDetailValueByPKForUpdate(userKey.getLastDetail().getPrimaryKey());
201
202        userControl.associatePartyToUserVisit(userVisit, party, partyRelationship, session.getStartTime());
203        
204        // Only update the UserKeyDetail if the party has changed
205        var partyPK = party.getPrimaryKey();
206        var partyRelationshipPK = partyRelationship == null? null: partyRelationship.getPrimaryKey();
207        var userKeyPartyPK = userKeyDetailValue.getPartyPK();
208        var userKeyPartyRelationshipPK = userKeyDetailValue.getPartyRelationshipPK();
209        
210        if(userKeyPartyPK == null || !userKeyPartyPK.equals(partyPK)
211                || userKeyPartyRelationshipPK == null || !userKeyPartyRelationshipPK.equals(partyRelationshipPK)) {
212            userKeyDetailValue.setPartyPK(partyPK);
213            userKeyDetailValue.setPartyRelationshipPK(partyRelationshipPK);
214            userControl.updateUserKeyFromValue(userKeyDetailValue);
215        }
216
217        clearLoginFailures(userLoginStatus);
218
219        userLoginStatus.setLastLoginTime(session.getStartTime());
220
221        addRemoteInet4AddressToParty(party, remoteInet4Address);
222
223        // TODO: Create audit trail
224    }
225    
226    protected void unsuccessfulLogin(final UserLoginStatus userLoginStatus) {
227        var failureCount = userLoginStatus.getFailureCount();
228        
229        userLoginStatus.setFailureCount(failureCount + 1);
230        if(userLoginStatus.getFirstFailureTime() == null) {
231            userLoginStatus.setFirstFailureTime(session.getStartTime());
232        }
233        userLoginStatus.setLastFailureTime(session.getStartTime());
234        
235        // TODO: Create audit trail
236    }
237    
238}