Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2406 Add lastVisit field and set it when user logs in #2527

Merged
merged 13 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface User {
spentTime: number;
studyDaysInCurrentMonth: number;
userId: string;
lastVisit: string;
}

export interface UserMapped extends User {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,27 @@
</td>
</ng-container>

<ng-container matColumnDef="lastDone">
<ng-container matColumnDef="lastVisit">
<th mat-header-cell *matHeaderCellDef mat-sort-header>
{{
'Admin.Modules.Users.Components.UsersTable.ColumnsName.LastVisit'
| translate
}}
</th>
<td mat-cell *matCellDef="let user">
<div *ngIf="user.lastVisit" class="subItem">
{{ user.lastVisit | date: 'medium' }}
</div>
</td>
</ng-container>

<ng-container matColumnDef="lastDone">
<th mat-header-cell *matHeaderCellDef mat-sort-header>
{{
'Admin.Modules.Users.Components.UsersTable.ColumnsName.LastStudyActivity'
| translate
}}
</th>
<td mat-cell *matCellDef="let user">
<div *ngIf="user.lastDone" class="subItem">
{{ user.lastDone | date: 'medium' }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('UsersComponent', () => {
active: true,
firstDone: '2021-12-13T19:07:04.832',
lastDone: '2021-12-15T19:07:04.832',
lastVisit: '2023-10-24T19:07:04.832',
lastWeek: [],
studyDaysInCurrentMonth: 1,
diagnosticProgress: {
Expand All @@ -70,6 +71,7 @@ describe('UsersComponent', () => {
active: true,
firstDone: '2021-12-17T19:07:04.832',
lastDone: '2021-12-20T19:07:04.832',
lastVisit: '2023-10-24T19:07:04.832',
lastWeek: [],
studyDaysInCurrentMonth: 2,
diagnosticProgress: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class UsersComponent implements OnInit, OnDestroy {
'name',
'firstDone',
'lastDone',
'lastVisit',
'currentWeek',
'spentTime',
'doneExercises',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { getRandomString } from '@shared/helpers/get-random-string';

export class AdminApiServiceFake
implements Pick<AdminApiService,
'getUserWeeklyStatistics' | 'getUserYearlyStatistics' | 'getUserDailyDetailStatistics' | 'getUsers' | 'getUserDailyDetailStatistics'> {
'getUserWeeklyStatistics' | 'getUserYearlyStatistics' | 'getUsers' | 'getUserDailyDetailStatistics'> {
private readonly options: IOptions = {};

constructor(o?: IOptions) {
Expand Down Expand Up @@ -94,6 +94,7 @@ export class AdminApiServiceFake
Date.now() - getRandomIntInclusive(0, 365 * 24 * 60 * 60 * 1000),
).toISOString();
const lastDone = dayjs(firstDone).add(1, 'month').toISOString();
const lastVisit = dayjs(lastDone).add(1, 'day').toISOString();

const lastWeek: number[] = [];
for (let dayNumber = 0; dayNumber < DAYS_IN_WEEK; dayNumber++) {
Expand Down Expand Up @@ -126,6 +127,7 @@ export class AdminApiServiceFake
userId: '1234',
spentTime: 10,
doneExercises: 2,
lastVisit,
});
}

Expand Down
1 change: 1 addition & 0 deletions frontend-angular/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"Favorite": "Favorite",
"FirstVisit": "First visit",
"LastVisit": "Last visit",
"LastStudyActivity": "Last study activity",
"CurrentWeek": "Current week",
"Name": "Name",
"Progress": "Progress",
Expand Down
1 change: 1 addition & 0 deletions frontend-angular/src/assets/i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"Favorite": "Любимый",
"FirstVisit": "Первый визит",
"LastVisit": "Последнее посещение",
"LastStudyActivity": "Последнее учебное занятие",
"CurrentWeek": "Текущая неделя",
"Name": "Имя",
"Progress": "Прогресс",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.epam.brn.auth.filter

import com.epam.brn.service.UserAccountService
import org.apache.logging.log4j.kotlin.logger
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Component
class RememberLastVisitFilter(
private val userAccountService: UserAccountService,
) : OncePerRequestFilter() {

private val log = logger()

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
markVisit()
filterChain.doFilter(request, response)
}

private fun markVisit() {
try {
if (SecurityContextHolder.getContext().authentication != null) userAccountService.markVisitForCurrentUser()
} catch (e: Exception) {
log.error("Error: ${e.message}", e)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.epam.brn.config

import com.epam.brn.auth.filter.FirebaseTokenAuthenticationFilter
import com.epam.brn.auth.filter.RememberLastVisitFilter
import com.epam.brn.enums.BrnRole
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
Expand All @@ -21,7 +22,8 @@ import javax.servlet.http.HttpServletResponse
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
class WebSecurityBasicConfiguration(
private val firebaseTokenAuthenticationFilter: FirebaseTokenAuthenticationFilter
private val firebaseTokenAuthenticationFilter: FirebaseTokenAuthenticationFilter,
private val rememberLastVisitFilter: RememberLastVisitFilter,
) : WebSecurityConfigurerAdapter() {

@Throws(Exception::class)
Expand All @@ -32,6 +34,7 @@ class WebSecurityBasicConfiguration(
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(firebaseTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
.addFilterAfter(rememberLastVisitFilter, UsernamePasswordAuthenticationFilter::class.java)
.authorizeRequests()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**").hasRole(BrnRole.ADMIN)
.and()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ data class UserWithAnalyticsResponse(
var diagnosticProgress: Map<AudiometryType, Boolean> = mapOf(AudiometryType.SIGNALS to true), // todo fill by user
var doneExercises: Int = 0, // for all time
var spentTime: Duration = Duration.ZERO, // spent time by doing exercises for all time
/**
* Last visit to our site
*/
val lastVisit: LocalDateTime? = null,
)
3 changes: 3 additions & 0 deletions src/main/kotlin/com/epam/brn/model/UserAccount.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ data class UserAccount(
@LastModifiedBy
@Column(name = "changed_by")
var changedBy: String = "",
@Column(name = "last_visit")
var lastVisit: LocalDateTime? = null,
var avatar: String? = null,
var photo: String? = null,
var description: String? = null,
Expand Down Expand Up @@ -102,6 +104,7 @@ data class UserAccount(
email = email,
bornYear = bornYear,
gender = gender?.let { BrnGender.valueOf(it) },
lastVisit = lastVisit ?: created,
)

override fun equals(other: Any?): Boolean {
Expand Down
11 changes: 11 additions & 0 deletions src/main/kotlin/com/epam/brn/repo/UserAccountRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import com.epam.brn.model.UserAccount
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.util.Optional

@Repository
Expand Down Expand Up @@ -50,4 +53,12 @@ interface UserAccountRepository : JpaRepository<UserAccount, Long> {
left JOIN FETCH u.headphones where roles.name = :roleName"""
)
fun findUsersAccountsByRole(roleName: String): List<UserAccount>

@Transactional
@Modifying
@Query(
"""update UserAccount u SET u.lastVisit = :lastVisit
where u.email = :email"""
)
fun updateLastVisitByEmail(email: String, lastVisit: LocalDateTime)
}
2 changes: 2 additions & 0 deletions src/main/kotlin/com/epam/brn/service/UserAccountService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ interface UserAccountService {
fun updateDoctorForPatient(userId: Long, doctorId: Long): UserAccount
fun removeDoctorFromPatient(userId: Long): UserAccount
fun getPatientsForDoctor(doctorId: Long): List<UserAccountDto>

fun markVisitForCurrentUser()
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.epam.brn.model.UserAccount
import com.epam.brn.repo.UserAccountRepository
import com.epam.brn.service.HeadphonesService
import com.epam.brn.service.RoleService
import com.epam.brn.service.TimeService
import com.epam.brn.service.UserAccountService
import com.google.firebase.auth.UserRecord
import org.springframework.data.domain.Pageable
Expand All @@ -24,7 +25,8 @@ import java.security.Principal
class UserAccountServiceImpl(
private val userAccountRepository: UserAccountRepository,
private val roleService: RoleService,
private val headphonesService: HeadphonesService
private val headphonesService: HeadphonesService,
private val timeService: TimeService
) : UserAccountService {

override fun findUserByEmail(email: String): UserAccountDto =
Expand Down Expand Up @@ -67,12 +69,17 @@ class UserAccountServiceImpl(
.toSet()

override fun getCurrentUser(): UserAccount {
val authentication = SecurityContextHolder.getContext().authentication
val email = authentication.name ?: getNameFromPrincipals(authentication)
val email = getCurrentUserEmail()
return userAccountRepository.findUserAccountByEmail(email)
.orElseThrow { EntityNotFoundException("No user was found for email=$email") }
}

override fun markVisitForCurrentUser() {
val email = getCurrentUserEmail()
val lastVisit = timeService.now()
return userAccountRepository.updateLastVisitByEmail(email, lastVisit)
}

override fun getCurrentUserDto(): UserAccountDto =
getCurrentUser().toDto()

Expand Down Expand Up @@ -149,6 +156,11 @@ class UserAccountServiceImpl(
return this
}

private fun getCurrentUserEmail(): String {
val authentication = SecurityContextHolder.getContext().authentication
return authentication.name ?: getNameFromPrincipals(authentication)
}

private fun getNameFromPrincipals(authentication: Authentication): String {
val principal = authentication.principal
if (principal is UserDetails)
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/db/migration/V220231024_2406.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
alter table user_account add column if not exists last_visit timestamp;
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,10 @@ internal class FirebaseTokenAuthenticationFilterTest {
every { firebaseAuth.verifyIdToken(token, true) } returns firebaseTokenMock
every { firebaseTokenMock.email } returns email
every { brainUpUserDetailsService.loadUserByUsername(email) } returns customUserDetailsMock

// WHEN
firebaseTokenAuthenticationFilter.doFilter(request, response, filterChain)

// THEN
val authentication = SecurityContextHolder.getContext().authentication
assertNotNull(authentication)
Expand Down Expand Up @@ -127,6 +129,7 @@ internal class FirebaseTokenAuthenticationFilterTest {

// WHEN
firebaseTokenAuthenticationFilter.doFilter(requestMock, responseMock, filterChain)

// THEN
val authentication = SecurityContextHolder.getContext().authentication
assertNotNull(authentication)
Expand All @@ -151,9 +154,16 @@ internal class FirebaseTokenAuthenticationFilterTest {
val filterChain = FilterChain { _, _ -> }

every { tokenHelperUtils.getBearerToken(requestMock) } returns tokenMock
every { firebaseAuth.verifyIdToken(tokenMock, true) } throws (FirebaseAuthException(FirebaseException(ErrorCode.INVALID_ARGUMENT, "Token invalid", null)))
every {
firebaseAuth.verifyIdToken(
tokenMock,
true
)
} throws (FirebaseAuthException(FirebaseException(ErrorCode.INVALID_ARGUMENT, "Token invalid", null)))

// WHEN
firebaseTokenAuthenticationFilter.doFilter(requestMock, responseMock, filterChain)

// THEN
val authentication = SecurityContextHolder.getContext().authentication
assertNull(authentication)
Expand All @@ -176,8 +186,10 @@ internal class FirebaseTokenAuthenticationFilterTest {

every { tokenHelperUtils.getBearerToken(requestMock) } returns tokenMock
every { firebaseAuth.verifyIdToken(tokenMock, true) } throws (IllegalArgumentException())

// WHEN
firebaseTokenAuthenticationFilter.doFilter(requestMock, responseMock, filterChain)

// THEN
val authentication = SecurityContextHolder.getContext().authentication
assertNull(authentication)
Expand Down Expand Up @@ -208,6 +220,7 @@ internal class FirebaseTokenAuthenticationFilterTest {

// WHEN
firebaseTokenAuthenticationFilter.doFilter(requestMock, responseMock, filterChain)

// THEN
val authentication = SecurityContextHolder.getContext().authentication
assertNull(authentication)
Expand Down
Loading
Loading