Skip to content

Commit fa4930b

Browse files
authored
Add password handling (#369)
* Add password handling * Add tests * Spotless
1 parent 7e50d3a commit fa4930b

File tree

10 files changed

+223
-27
lines changed

10 files changed

+223
-27
lines changed

services/chatbot/src/chatbot/langgraph_agent.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,13 @@ async def build_langgraph_agent(api_key, model_name, user_jwt):
226226
toolkit = SQLDatabaseToolkit(db=postgresdb, llm=llm)
227227
logger.debug("SQL Database toolkit created")
228228

229-
mcp_client = get_mcp_client(user_jwt)
230-
mcp_tools = await mcp_client.get_tools()
231-
logger.debug("MCP tools loaded: %d tools", len(mcp_tools))
229+
mcp_tools = []
230+
try:
231+
mcp_client = get_mcp_client(user_jwt)
232+
mcp_tools = await mcp_client.get_tools()
233+
logger.debug("MCP tools loaded: %d tools", len(mcp_tools))
234+
except Exception as e:
235+
logger.error("Failed to load MCP tools, continuing without them: %s", e)
232236

233237
db_tools = toolkit.get_tools()
234238
logger.debug("Database tools loaded: %d tools", len(db_tools))

services/chatbot/src/mcpserver/server.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,40 +30,52 @@
3030

3131
def get_api_key():
3232
global API_KEY
33-
# Try 5 times to get client auth
34-
MAX_ATTEMPTS = 5
33+
if API_KEY is not None:
34+
return API_KEY
35+
# Try multiple times to get client auth (identity service may not be ready yet)
36+
MAX_ATTEMPTS = 10
3537
for i in range(MAX_ATTEMPTS):
3638
logger.info(f"Attempt {i+1} to get API key...")
37-
if API_KEY is None:
38-
login_body = {"email": Config.API_USER, "password": Config.API_PASSWORD}
39-
auth_url = f"{BASE_IDENTITY_URL}/identity/management/user/apikey"
40-
headers = {
41-
"Content-Type": "application/json",
42-
}
39+
login_body = {"email": Config.API_USER, "password": Config.API_PASSWORD}
40+
auth_url = f"{BASE_IDENTITY_URL}/identity/management/user/apikey"
41+
headers = {
42+
"Content-Type": "application/json",
43+
}
44+
try:
4345
with httpx.Client(
4446
base_url=BASE_URL,
4547
headers=headers,
4648
verify=False,
4749
) as client:
4850
response = client.post(auth_url, json=login_body)
4951
if response.status_code != 200:
52+
logger.error(
53+
f"Failed to get API key in attempt {i+1}: {response.status_code} {response.text}. Sleeping for {i+1} seconds..."
54+
)
55+
# Reset test users if credentials are rejected
56+
try:
57+
reset_url = f"{BASE_IDENTITY_URL}/identity/api/auth/reset-test-users"
58+
reset_resp = client.post(reset_url)
59+
logger.info(f"Reset test users response: {reset_resp.status_code}")
60+
except Exception as reset_err:
61+
logger.error(f"Failed to reset test users: {reset_err}")
5062
if i == MAX_ATTEMPTS - 1:
51-
logger.error(
52-
f"Failed to get API key after {i+1} attempts: {response.status_code} {response.text}"
53-
)
5463
raise Exception(
55-
f"Failed to get API key after {i+1} attempts: {response.status_code} {response.text}"
64+
f"Failed to get API key after {MAX_ATTEMPTS} attempts: {response.status_code} {response.text}"
5665
)
57-
logger.error(
58-
f"Failed to get API key in attempt {i+1}: {response.status_code} {response.text}. Sleeping for {i} seconds..."
59-
)
60-
time.sleep(i)
66+
time.sleep(i + 1)
67+
continue
6168
response_json = response.json()
62-
logger.info(f"Response: {response_json}")
6369
API_KEY = response_json.get("apiKey")
6470
if API_KEY:
65-
logger.debug("MCP Server API Key obtained successfully.")
66-
return API_KEY
71+
logger.info("MCP Server API Key obtained successfully.")
72+
return API_KEY
73+
logger.error(f"API key not found in response: {response_json}")
74+
except httpx.HTTPError as e:
75+
logger.error(f"HTTP error in attempt {i+1}: {e}")
76+
if i == MAX_ATTEMPTS - 1:
77+
raise
78+
time.sleep(i + 1)
6779
return API_KEY
6880

6981

services/identity/src/main/java/com/crapi/CRAPIBootApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import org.springframework.boot.SpringApplication;
1818
import org.springframework.boot.autoconfigure.SpringBootApplication;
19+
import org.springframework.scheduling.annotation.EnableScheduling;
1920

2021
/*
2122
* Need to give path for application.properties file
@@ -25,6 +26,7 @@
2526
@PropertySource(value = "file:/home/hasher/Music/resources/application.properties", ignoreResourceNotFound = true)
2627
})*/
2728
@SpringBootApplication(scanBasePackages = {"com.crapi"})
29+
@EnableScheduling
2830
public class CRAPIBootApplication {
2931

3032
public static void main(String[] args) {

services/identity/src/main/java/com/crapi/constant/UserMessage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public class UserMessage {
4949
public static final String EMAIL_NOT_REGISTERED = "Given Email is not registered! ";
5050
public static final String INVALID_OTP = "Invalid OTP! Please try again..";
5151
public static final String ERROR = "ERROR..";
52-
public static final String OTP_VARIFIED_SUCCESS = "OTP verified";
52+
public static final String OTP_VERIFIED_SUCCESS = "OTP verified";
5353
public static final String OTP_SEND_SUCCESS_ON_EMAIL = "OTP Sent on the provided email, ";
5454
public static final String EXCEED_NUMBER_OF_ATTEMPS = "You've exceeded the number of attempts.";
5555
public static final String PASSWORD_GOT_RESET = "Password reset successful.";

services/identity/src/main/java/com/crapi/entity/User.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ public class User {
4444

4545
private LocalDate createdOn = LocalDate.now();
4646

47+
private LocalDate passwordUpdatedAt = LocalDate.of(2000, 1, 1);
48+
4749
private String code;
4850

4951
// @OneToOne

services/identity/src/main/java/com/crapi/service/Impl/OtpServiceImpl.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,10 @@ public CRAPIResponse validateOtp(OtpForm otpForm) {
7979
otp = otpRepository.findByUser(user);
8080
if (validateOTPAndEmail(otp, otpForm)) {
8181
user.setPassword(encoder.encode(otpForm.getPassword()));
82+
user.setPasswordUpdatedAt(java.time.LocalDate.now());
8283
userRepository.save(user);
8384
otp.setStatus(EStatus.INACTIVE.toString());
84-
validateOTPResponse = new CRAPIResponse(UserMessage.OTP_VARIFIED_SUCCESS, 200);
85+
validateOTPResponse = new CRAPIResponse(UserMessage.OTP_VERIFIED_SUCCESS, 200);
8586
} else {
8687
otp.setCount(otp.getCount() + 1);
8788
validateOTPResponse = new CRAPIResponse(UserMessage.INVALID_OTP, 500);
@@ -103,9 +104,10 @@ public CRAPIResponse secureValidateOtp(OtpForm otpForm) {
103104
otp = otpRepository.findByUser(user);
104105
if (validateOTPAndEmail(otp, otpForm)) {
105106
user.setPassword(encoder.encode(otpForm.getPassword()));
107+
user.setPasswordUpdatedAt(java.time.LocalDate.now());
106108
userRepository.save(user);
107109
otp.setStatus(EStatus.INACTIVE.toString());
108-
validateOTPResponse = new CRAPIResponse(UserMessage.OTP_VARIFIED_SUCCESS, 200);
110+
validateOTPResponse = new CRAPIResponse(UserMessage.OTP_VERIFIED_SUCCESS, 200);
109111
} else if (otp.getCount() == 9) {
110112
otp.setCount(otp.getCount() + 1);
111113
invalidateOtp(otp);

services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import jakarta.servlet.http.HttpServletRequest;
3434
import jakarta.transaction.Transactional;
3535
import java.text.ParseException;
36+
import java.time.LocalDate;
3637
import lombok.extern.slf4j.Slf4j;
3738
import org.apache.logging.log4j.LogManager;
3839
import org.apache.logging.log4j.core.impl.Log4jContextFactory;
@@ -172,6 +173,7 @@ public User updateUserPassword(String password, String email) {
172173
User user = userRepository.findByEmail(email);
173174
if (user != null) {
174175
user.setPassword(encoder.encode(password));
176+
user.setPasswordUpdatedAt(LocalDate.now());
175177
userRepository.saveAndFlush(user);
176178
}
177179
return user;
@@ -188,6 +190,7 @@ public CRAPIResponse resetPassword(LoginForm loginForm, HttpServletRequest reque
188190
User user = getUserFromToken(request);
189191
if (user != null) {
190192
user.setPassword(encoder.encode(loginForm.getPassword()));
193+
user.setPasswordUpdatedAt(LocalDate.now());
191194
userRepository.saveAndFlush(user);
192195
return new CRAPIResponse(UserMessage.PASSWORD_GOT_RESET, 200);
193196
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.crapi.service;
2+
3+
import com.crapi.constant.TestUsers;
4+
import com.crapi.entity.User;
5+
import com.crapi.model.SeedUser;
6+
import com.crapi.repository.UserRepository;
7+
import java.time.LocalDate;
8+
import java.util.ArrayList;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
import org.springframework.beans.factory.annotation.Autowired;
12+
import org.springframework.scheduling.annotation.Scheduled;
13+
import org.springframework.security.crypto.password.PasswordEncoder;
14+
import org.springframework.stereotype.Component;
15+
import org.springframework.transaction.annotation.Transactional;
16+
17+
@Component
18+
public class UserResetJob {
19+
20+
private static final Logger logger = LoggerFactory.getLogger(UserResetJob.class);
21+
22+
private static final LocalDate DEFAULT_PASSWORD_DATE = LocalDate.of(2000, 1, 1);
23+
24+
@Autowired private UserRepository userRepository;
25+
26+
@Autowired private PasswordEncoder encoder;
27+
28+
@Scheduled(fixedRate = 3600000) // every hour
29+
@Transactional
30+
public void resetTestUserCredsIfChanged() {
31+
ArrayList<SeedUser> testUsers = new TestUsers().getUsers();
32+
int resetCount = 0;
33+
34+
for (SeedUser seedUser : testUsers) {
35+
User user = userRepository.findByEmail(seedUser.getEmail());
36+
if (user == null) {
37+
continue;
38+
}
39+
LocalDate updatedAt = user.getPasswordUpdatedAt();
40+
if (updatedAt != null && !updatedAt.equals(DEFAULT_PASSWORD_DATE)) {
41+
user.setPassword(encoder.encode(seedUser.getPassword()));
42+
user.setPasswordUpdatedAt(DEFAULT_PASSWORD_DATE);
43+
userRepository.saveAndFlush(user);
44+
resetCount++;
45+
logger.info("Reset password for test user: {}", seedUser.getEmail());
46+
}
47+
}
48+
49+
if (resetCount > 0) {
50+
logger.info("Reset credentials for {} test user(s)", resetCount);
51+
} else {
52+
logger.debug("All test user credentials are unchanged, no reset needed");
53+
}
54+
}
55+
}

services/identity/src/test/java/com/crapi/service/Impl/OtpServiceImplTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public void validateOtpSuccess() {
9797
CRAPIResponse crapiAPIResponse = otpService.validateOtp(otpForm);
9898
Mockito.verify(userRepository, Mockito.times(1)).save(Mockito.any());
9999
Mockito.verify(otpRepository, Mockito.times(1)).save(Mockito.any());
100-
Assertions.assertEquals(UserMessage.OTP_VARIFIED_SUCCESS, crapiAPIResponse.getMessage());
100+
Assertions.assertEquals(UserMessage.OTP_VERIFIED_SUCCESS, crapiAPIResponse.getMessage());
101101
Assertions.assertEquals(HttpStatus.OK.value(), crapiAPIResponse.getStatus());
102102
}
103103

@@ -128,7 +128,7 @@ public void secureValidateOtpSuccess() {
128128
CRAPIResponse crapiAPIResponse = otpService.secureValidateOtp(otpForm);
129129
Mockito.verify(userRepository, Mockito.times(1)).save(Mockito.any());
130130
Mockito.verify(otpRepository, Mockito.times(1)).save(Mockito.any());
131-
Assertions.assertEquals(UserMessage.OTP_VARIFIED_SUCCESS, crapiAPIResponse.getMessage());
131+
Assertions.assertEquals(UserMessage.OTP_VERIFIED_SUCCESS, crapiAPIResponse.getMessage());
132132
Assertions.assertEquals(HttpStatus.OK.value(), crapiAPIResponse.getStatus());
133133
}
134134

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package com.crapi.service;
2+
3+
import com.crapi.constant.TestUsers;
4+
import com.crapi.entity.User;
5+
import com.crapi.model.SeedUser;
6+
import com.crapi.repository.UserRepository;
7+
import java.time.LocalDate;
8+
import java.util.ArrayList;
9+
import org.junit.Test;
10+
import org.junit.jupiter.api.Assertions;
11+
import org.junit.runner.RunWith;
12+
import org.mockito.InjectMocks;
13+
import org.mockito.Mock;
14+
import org.mockito.Mockito;
15+
import org.mockito.junit.MockitoJUnitRunner;
16+
import org.springframework.security.crypto.password.PasswordEncoder;
17+
18+
@RunWith(MockitoJUnitRunner.class)
19+
public class UserResetJobTest {
20+
21+
private static final LocalDate DEFAULT_PASSWORD_DATE = LocalDate.of(2000, 1, 1);
22+
23+
@InjectMocks private UserResetJob userResetJob;
24+
25+
@Mock private UserRepository userRepository;
26+
27+
@Mock private PasswordEncoder encoder;
28+
29+
@Test
30+
public void resetSkipsWhenPasswordNotChanged() {
31+
ArrayList<SeedUser> testUsers = new TestUsers().getUsers();
32+
for (SeedUser seedUser : testUsers) {
33+
User user =
34+
new User(seedUser.getEmail(), seedUser.getNumber(), "encoded", seedUser.getRole());
35+
user.setPasswordUpdatedAt(DEFAULT_PASSWORD_DATE);
36+
Mockito.when(userRepository.findByEmail(seedUser.getEmail())).thenReturn(user);
37+
}
38+
39+
userResetJob.resetTestUserCredsIfChanged();
40+
41+
Mockito.verify(userRepository, Mockito.never()).saveAndFlush(Mockito.any());
42+
}
43+
44+
@Test
45+
public void resetResetsWhenPasswordChanged() {
46+
ArrayList<SeedUser> testUsers = new TestUsers().getUsers();
47+
SeedUser firstUser = testUsers.get(0);
48+
49+
// First user has changed password (non-default date)
50+
User changedUser =
51+
new User(firstUser.getEmail(), firstUser.getNumber(), "encoded", firstUser.getRole());
52+
changedUser.setPasswordUpdatedAt(LocalDate.now());
53+
Mockito.when(userRepository.findByEmail(firstUser.getEmail())).thenReturn(changedUser);
54+
Mockito.when(encoder.encode(firstUser.getPassword())).thenReturn("resetEncoded");
55+
Mockito.when(userRepository.saveAndFlush(Mockito.any())).thenReturn(changedUser);
56+
57+
// Remaining users have default date
58+
for (int i = 1; i < testUsers.size(); i++) {
59+
SeedUser seedUser = testUsers.get(i);
60+
User user =
61+
new User(seedUser.getEmail(), seedUser.getNumber(), "encoded", seedUser.getRole());
62+
user.setPasswordUpdatedAt(DEFAULT_PASSWORD_DATE);
63+
Mockito.when(userRepository.findByEmail(seedUser.getEmail())).thenReturn(user);
64+
}
65+
66+
userResetJob.resetTestUserCredsIfChanged();
67+
68+
Mockito.verify(userRepository, Mockito.times(1)).saveAndFlush(Mockito.any());
69+
Assertions.assertEquals("resetEncoded", changedUser.getPassword());
70+
Assertions.assertEquals(DEFAULT_PASSWORD_DATE, changedUser.getPasswordUpdatedAt());
71+
}
72+
73+
@Test
74+
public void resetResetsAllWhenAllPasswordsChanged() {
75+
ArrayList<SeedUser> testUsers = new TestUsers().getUsers();
76+
for (SeedUser seedUser : testUsers) {
77+
User user =
78+
new User(seedUser.getEmail(), seedUser.getNumber(), "encoded", seedUser.getRole());
79+
user.setPasswordUpdatedAt(LocalDate.now());
80+
Mockito.when(userRepository.findByEmail(seedUser.getEmail())).thenReturn(user);
81+
Mockito.when(userRepository.saveAndFlush(Mockito.any())).thenReturn(user);
82+
}
83+
Mockito.when(encoder.encode(Mockito.anyString())).thenReturn("resetEncoded");
84+
85+
userResetJob.resetTestUserCredsIfChanged();
86+
87+
Mockito.verify(userRepository, Mockito.times(testUsers.size())).saveAndFlush(Mockito.any());
88+
}
89+
90+
@Test
91+
public void resetSkipsNullUser() {
92+
ArrayList<SeedUser> testUsers = new TestUsers().getUsers();
93+
for (SeedUser seedUser : testUsers) {
94+
Mockito.when(userRepository.findByEmail(seedUser.getEmail())).thenReturn(null);
95+
}
96+
97+
userResetJob.resetTestUserCredsIfChanged();
98+
99+
Mockito.verify(userRepository, Mockito.never()).saveAndFlush(Mockito.any());
100+
}
101+
102+
@Test
103+
public void resetSkipsWhenPasswordUpdatedAtNull() {
104+
ArrayList<SeedUser> testUsers = new TestUsers().getUsers();
105+
for (SeedUser seedUser : testUsers) {
106+
User user =
107+
new User(seedUser.getEmail(), seedUser.getNumber(), "encoded", seedUser.getRole());
108+
user.setPasswordUpdatedAt(null);
109+
Mockito.when(userRepository.findByEmail(seedUser.getEmail())).thenReturn(user);
110+
}
111+
112+
userResetJob.resetTestUserCredsIfChanged();
113+
114+
Mockito.verify(userRepository, Mockito.never()).saveAndFlush(Mockito.any());
115+
}
116+
}

0 commit comments

Comments
 (0)