Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions client/src/components/Sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext, useEffect } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { Nav } from 'react-bootstrap';
import { NavLink } from 'react-router';
import { useTranslation } from 'react-i18next';
Expand All @@ -22,6 +22,7 @@ interface Props {
function Sidebar(props: React.PropsWithChildren<Props>): React.ReactNode {
const { signOut, user } = useContext(AuthContext);
const { currentPage, setNewPage } = useContext(SidebarContext);
const [lastSeen, setLastSeen] = useState('');
const { t } = useTranslation();
const build = `Build: ${env.VITE_BUILD}`;

Expand All @@ -46,7 +47,20 @@ function Sidebar(props: React.PropsWithChildren<Props>): React.ReactNode {
return '';
};

useEffect(() => {}, [user, currentPage]);
useEffect(() => {
if (user && user.lastLogin) {
const utcString = user.lastLogin.endsWith('Z') ? user.lastLogin : `${user.lastLogin}Z`;
const fmtted = new Date(utcString).toLocaleString(navigator.language, {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
setLastSeen(fmtted);
Comment on lines +52 to +66
}
}, [user, currentPage]);

return (
<>
Expand Down Expand Up @@ -95,9 +109,12 @@ function Sidebar(props: React.PropsWithChildren<Props>): React.ReactNode {

{/* Footer at the bottom */}
<div className="mt-auto text-center text-muted py-3">
<small data-testid="footer-text">
{build}
</small>
{lastSeen && (
<div>
<small>{`Last seen ${lastSeen}`}</small>
</div>
)}
<small data-testid="footer-text">{build}</small>
</div>
</div>

Expand Down
3 changes: 2 additions & 1 deletion client/src/context/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
admin: registerResponse.admin,
createdAt: new Date(registerResponse.createdAt),
gravatarImageUrl: registerResponse.gravatarImageUrl,
lang: registerResponse.lang
lang: registerResponse.lang,
lastLogin: registerResponse.lastLogin
};

setSigned(true);
Expand Down
1 change: 1 addition & 0 deletions client/src/types/SigninResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export type SignInResponse = {
token: string;
gravatarImageUrl: string;
lang: string;
lastLogin: string;
};
1 change: 1 addition & 0 deletions client/src/types/UserResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export type UserResponse = {
createdAt: Date;
gravatarImageUrl: string;
lang: string;
lastLogin: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public class UserEntity implements UserDetails {
@Column(name = "last_password_change", nullable = false)
private LocalDateTime lastPasswordChange;

@Column(name = "last_login")
private LocalDateTime lastLogin;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
Expand Down Expand Up @@ -173,4 +176,12 @@ public LocalDateTime getLastPasswordChange() {
public void setLastPasswordChange(LocalDateTime lastPasswordChange) {
this.lastPasswordChange = lastPasswordChange;
}

public LocalDateTime getLastLogin() {
return lastLogin;
}

public void setLastLogin(LocalDateTime lastLogin) {
this.lastLogin = lastLogin;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public record UserResponse(
Boolean admin,
LocalDateTime createdAt,
LocalDateTime inactivatedAt,
LocalDateTime lastLogin,
String gravatarImageUrl) {

/**
Expand All @@ -27,6 +28,7 @@ public static UserResponse fromEntity(UserEntity user, String gravatarUrl) {
user.getAdmin(),
user.getCreatedAt(),
user.getInactivatedAt(),
user.getLastLogin(),
gravatarUrl);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public record UserResponseWithToken(
Boolean admin,
LocalDateTime createdAt,
LocalDateTime inactivatedAt,
LocalDateTime lastLogin,
String gravatarImageUrl,
String token,
String lang) {
Expand All @@ -31,6 +32,7 @@ public static UserResponseWithToken fromEntity(
user.getAdmin(),
user.getCreatedAt(),
user.getInactivatedAt(),
user.getLastLogin(),
gravatarUrl,
token,
user.getLang());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,14 @@ public UserResponseWithToken signInUser(LoginRequest login) {

logger.info("User authenticated! Token {}", token.substring(0, 6) + "...");

final UserEntity responseUser = cloneUser(user, user.getLastLogin());

user.setLastLogin(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
userPwdLimitRepository.deleteAllForUser(user.getId());
userRepository.save(user);

return UserResponseWithToken.fromEntity(
user, token, getGravatarImageUrl(login.email()).orElse(null));
responseUser, token, getGravatarImageUrl(login.email()).orElse(null));
} catch (BadCredentialsException e) {
logger.error(
"BadCredentialsException when logging in user {}: {}", user.getId(), e.getMessage());
Expand Down Expand Up @@ -553,4 +557,23 @@ private boolean hasValidMailgunApiKey() {
return Optional.ofNullable(apiKey).isPresent()
&& !"invalid-api-key-only-placeholder".equals(apiKey);
}

private UserEntity cloneUser(UserEntity user, LocalDateTime lastLogin) {
UserEntity u = new UserEntity();
u.setId(user.getId());
u.setEmail(user.getEmail());
u.setPassword(user.getPassword());
u.setAdmin(user.getAdmin());
u.setCreatedAt(user.getCreatedAt());
u.setInactivatedAt(user.getInactivatedAt());
u.setName(user.getName());
u.setEmailConfirmedAt(user.getEmailConfirmedAt());
u.setEmailUuid(user.getEmailUuid());
u.setResetPasswordExpiration(user.getResetPasswordExpiration());
u.setResetToken(user.getResetToken());
u.setLang(user.getLang());
u.setLastPasswordChange(user.getLastPasswordChange());
u.setLastLogin(lastLogin);
return u;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE tasknote.users ADD COLUMN last_login TIMESTAMP;
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,31 @@ class AuthenticationControllerTest {
@Test
@DisplayName("Sign up happy path should succeed")
void signup_happyPath_shouldSucceed() throws Exception {
LoginRequest request = new LoginRequest("user@domain.com", "abcde123456", "abcde123456", "en");
LoginRequest request =
new LoginRequest("user@domain.com", "abcde123456", "abcde123456", "en");
final String token = "xaxbxcxdx1x2x3A@";

UserResponseWithToken response =
new UserResponseWithToken(
123L, null, request.email(), false, LocalDateTime.now(), null, null, token, "en");
123L,
null,
request.email(),
false,
LocalDateTime.now(),
null,
null,
null,
token,
"en");
when(authService.signUpNewUser(request)).thenReturn(response);

String jsonString =
"""
{
"email": "user@domain.com",
"password": "abcde123456",
"passwordAgain": "abcde123456"
"passwordAgain": "abcde123456",
"timezone": "UTC"
}
""";

Expand All @@ -63,20 +74,31 @@ void signup_happyPath_shouldSucceed() throws Exception {
@Test
@DisplayName("Sign up bad email request should fail")
void signup_badEmailRequest_shouldFail() throws Exception {
LoginRequest request = new LoginRequest("user@domain..com", "abcde123456", "abcde123456", "en");
LoginRequest request =
new LoginRequest("user@domain..com", "abcde123456", "abcde123456", "en");
final String token = "xaxbxcxdx1x2x3@A";

UserResponseWithToken response =
new UserResponseWithToken(
123L, null, request.email(), false, LocalDateTime.now(), null, null, token, "en");
123L,
null,
request.email(),
false,
LocalDateTime.now(),
null,
null,
null,
token,
"en");
when(authService.signUpNewUser(request)).thenReturn(response);

String jsonString =
"""
{
"email": "user@domain..com",
"password": "abcde123456",
"passwordAgain": "abcde123456"
"passwordAgain": "abcde123456",
"timezone": "UTC"
}
""";

Expand All @@ -94,7 +116,8 @@ void signup_badEmailRequest_shouldFail() throws Exception {
@Test
@DisplayName("Sign up email already exists should fail")
void signup_userAlreadyExists_shouldFail() throws Exception {
LoginRequest request = new LoginRequest("user@domain.com", "abcde123456", "abcde123456", "en");
LoginRequest request =
new LoginRequest("user@domain.com", "abcde123456", "abcde123456", "en");

when(authService.signUpNewUser(request)).thenThrow(new EmailAlreadyExistsException());

Expand All @@ -104,7 +127,8 @@ void signup_userAlreadyExists_shouldFail() throws Exception {
"email": "user@domain.com",
"password": "abcde123456",
"passwordAgain": "abcde123456",
"lang": "en"
"lang": "en",
"timezone": "UTC"
}
""";

Expand All @@ -122,12 +146,22 @@ void signup_userAlreadyExists_shouldFail() throws Exception {
@Test
@DisplayName("Sign in happy path should succeed")
void signin_happyPath_shouldSucceed() throws Exception {
LoginRequest request = new LoginRequest("user@domain.com", "abcde123456", "abcde123456", "en");
LoginRequest request =
new LoginRequest("user@domain.com", "abcde123456", "abcde123456", "en");
final String token = "xaxbxcxdx1x2x3A@";

UserResponseWithToken response =
new UserResponseWithToken(
123L, null, request.email(), false, LocalDateTime.now(), null, null, token, "en");
123L,
null,
request.email(),
false,
LocalDateTime.now(),
null,
null,
null,
token,
"en");
when(authService.signInUser(request)).thenReturn(response);

String jsonString =
Expand All @@ -136,7 +170,8 @@ void signin_happyPath_shouldSucceed() throws Exception {
"email": "user@domain.com",
"password": "abcde123456",
"passwordAgain": "abcde123456",
"lang": "en"
"lang": "en",
"timezone": "UTC"
}
""";

Expand All @@ -158,15 +193,17 @@ void signin_happyPath_shouldSucceed() throws Exception {
@Test
@DisplayName("Sign in invalid credentials should fail")
void signIn_invalidCredentials_shouldFail() throws Exception {
LoginRequest request = new LoginRequest("user@domain.com", "abcde123456", "abcde123456", "en");
LoginRequest request =
new LoginRequest("user@domain.com", "abcde123456", "abcde123456", "en");

when(authService.signInUser(request)).thenReturn(null);

String jsonString =
"""
{
"email": "user@domain.com",
"password": "abcde123456"
"password": "abcde123456",
"timezone": "UTC"
}
""";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class UserControllerTest {
@WithMockUser(username = "user@domain.com", password = "abcde123456A@")
void getAllUsers_happyPath_shouldSucceed() throws Exception {
UserResponse userResponse =
new UserResponse(1L, "John", "email@test.com", false, null, null, null);
new UserResponse(1L, "John", "email@test.com", false, null, null, null, null);
when(authService.getAllUsers()).thenReturn(List.of(userResponse));

mockMvc
Expand Down Expand Up @@ -68,7 +68,7 @@ void getAllUsers_unauthorized_shouldFail() throws Exception {
@WithMockUser(username = "user@domain.com", password = "abcde123456A@")
void patchUserInfo_happyPath_shouldSucceed() throws Exception {
UserResponse response =
new UserResponse(1L, "John", "email@example.com", false, null, null, null);
new UserResponse(1L, "John", "email@example.com", false, null, null, null, null);
UserPatchRequest request = new UserPatchRequest("John Doe", response.email(), null, null, null);
when(authService.patchUserInfo(request)).thenReturn(response);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ void refresh_unauthorized_shouldFail() throws Exception {
@DisplayName("Delete account happy path should succeed")
@WithMockUser(username = "user@domain.com", password = "abcde123456A@")
void deleteAccount_happyPath_shouldSucceed() throws Exception {
UserResponse response = new UserResponse(1L, "John", "email@test.com", false, null, null, null);
UserResponse response =
new UserResponse(1L, "John", "email@test.com", false, null, null, null, null);
when(userSessionService.deleteCurrentUserAccount()).thenReturn(response);

mockMvc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ void signUpNewUser_happyPath_shouldSucceed() {
Assertions.assertNull(token.token());
Assertions.assertEquals(entity.getEmail(), token.email());
Assertions.assertNotNull(entity.getEmailUuid());
verify(userRepository).save(any(UserEntity.class));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ void deleteCurrentUserAccount_happyPath_shouldSucceed() {
when(noteService.getAllNotes()).thenReturn(List.of(note));
when(authService.deleteUserAccount())
.thenReturn(
new UserResponse(user.getId(), "user", user.getEmail(), false, null, null, null));
new UserResponse(
user.getId(), "user", user.getEmail(), false, null, null, null, null));

// Act
UserResponse response = userSessionService.deleteCurrentUserAccount();
Expand Down
Loading