Skip to content

Commit

Permalink
Implement Authentication #11
Browse files Browse the repository at this point in the history
  • Loading branch information
mr-w1lde committed Nov 5, 2024
1 parent 7d3ce26 commit b015920
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package net.furizon.backend.feature.authentication.usecase;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.furizon.backend.feature.authentication.action.createSession.CreateSessionAction;
import net.furizon.backend.feature.authentication.dto.LoginResponse;
import net.furizon.backend.feature.authentication.validation.CreateLoginSessionValidation;
import net.furizon.backend.infrastructure.security.SecurityConfig;
import net.furizon.backend.infrastructure.security.session.action.clearNewestUserSessions.ClearNewestUserSessionsAction;
import net.furizon.backend.infrastructure.security.session.finder.SessionFinder;
import net.furizon.backend.infrastructure.security.token.TokenMetadata;
import net.furizon.backend.infrastructure.security.token.encoder.TokenEncoder;
import net.furizon.backend.infrastructure.usecase.UseCase;
Expand All @@ -14,17 +18,34 @@

@Component
@RequiredArgsConstructor
@Slf4j
public class CreateLoginSessionUseCase implements UseCase<CreateLoginSessionUseCase.Input, LoginResponse> {
private final CreateLoginSessionValidation validation;

private final CreateSessionAction createSessionAction;

private final TokenEncoder tokenEncoder;

private final SessionFinder sessionFinder;

private final ClearNewestUserSessionsAction clearNewestUserSessionsAction;

private final SecurityConfig securityConfig;

@Transactional
@Override
public @NotNull LoginResponse executor(@NotNull CreateLoginSessionUseCase.Input input) {
final var userId = validation.validateAndGetUserId(input);
int sessionsCount = sessionFinder.getUserSessionsCount(userId);
if (sessionsCount >= securityConfig.getSession().getMaxAllowedSessionsSize()) {
log.warn(
"Maximum allowed sessions size reached. Sessions count = '{}', userId = '{}'; Running the cleaning",
sessionsCount,
userId
);
clearNewestUserSessionsAction.invoke(userId);
}

final var sessionId = createSessionAction.invoke(
userId,
input.clientIp,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package net.furizon.backend.feature.user;

import lombok.Data;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.time.OffsetDateTime;
import java.util.UUID;

@Data
public class UserSession {
@NotNull
private final UUID sessionId;

@Nullable
private final String userAgent;

@NotNull
private final OffsetDateTime createdAt;

@NotNull
private final OffsetDateTime lastUsageAt;
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
package net.furizon.backend.feature.user.controller;

import lombok.RequiredArgsConstructor;
import net.furizon.backend.feature.user.UserSession;
import net.furizon.backend.feature.user.usecase.GetUserSessionsUseCase;
import net.furizon.backend.infrastructure.security.FurizonUser;
import net.furizon.backend.infrastructure.usecase.UseCaseExecutor;
import org.jetbrains.annotations.NotNull;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class SimpleUserController {
public class UserController {
private final UseCaseExecutor executor;

@GetMapping("/me")
public FurizonUser getMe(
@AuthenticationPrincipal @NotNull FurizonUser user
) {
return user;
}

@GetMapping("/me/sessions")
public List<UserSession> getMeSessions(
@AuthenticationPrincipal @NotNull FurizonUser user
) {
return executor.execute(
GetUserSessionsUseCase.class,
new GetUserSessionsUseCase.Input(user.getUserId())
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package net.furizon.backend.feature.user.usecase;

import lombok.RequiredArgsConstructor;
import net.furizon.backend.feature.user.UserSession;
import net.furizon.backend.infrastructure.security.session.finder.SessionFinder;
import net.furizon.backend.infrastructure.usecase.UseCase;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@RequiredArgsConstructor
public class GetUserSessionsUseCase implements UseCase<GetUserSessionsUseCase.Input, List<UserSession>> {
private final SessionFinder sessionFinder;

@Override
public @NotNull List<UserSession> executor(@NotNull Input input) {
return sessionFinder.getUserSessions(input.userId)
.stream()
.map(session ->
new UserSession(
session.getId(),
session.getUserAgent(),
session.getCreatedAt(),
session.getModifiedAt()
)
)
.toList();
}

public record Input(long userId) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.time.OffsetDateTime;
import java.util.UUID;
Expand All @@ -15,9 +16,15 @@ public class Session {
@NotNull
private final UUID id;

@Nullable
private final String userAgent;

@NotNull
private final OffsetDateTime createdAt;

@NotNull
private final OffsetDateTime modifiedAt;

@NotNull
private final OffsetDateTime expiresAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package net.furizon.backend.infrastructure.security.session.action.clearNewestUserSessions;

public interface ClearNewestUserSessionsAction {
void invoke(long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package net.furizon.backend.infrastructure.security.session.action.clearNewestUserSessions;

import lombok.RequiredArgsConstructor;
import net.furizon.backend.infrastructure.security.SecurityConfig;
import net.furizon.jooq.infrastructure.command.SqlCommand;
import org.jooq.util.postgres.PostgresDSL;
import org.springframework.stereotype.Component;

import static net.furizon.jooq.generated.Tables.SESSIONS;

@Component
@RequiredArgsConstructor
public class JooqClearNewestUserSessionsAction implements ClearNewestUserSessionsAction {
private final SqlCommand sqlCommand;

private final SecurityConfig config;

@Override
public void invoke(long userId) {
sqlCommand.execute(
PostgresDSL.deleteFrom(SESSIONS)
.where(SESSIONS.USER_ID.eq(userId))
.and(
SESSIONS.CREATED_AT.eq(
PostgresDSL
.select(SESSIONS.CREATED_AT)
.from(SESSIONS)
.where(SESSIONS.USER_ID.eq(userId))
.orderBy(SESSIONS.CREATED_AT)
.limit(1)
// -1 because we are planing to insert one more session after the clean
.offset(config.getSession().getMaxAllowedSessionsSize() - 1)
)
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public void invoke(@NotNull UUID sessionId, @NotNull String clientIp) {
.set(SESSIONS.LAST_USED_BY_IP_ADDRESS, clientIp)
.set(SESSIONS.MODIFIED_AT, now)
.set(SESSIONS.EXPIRES_AT, expireAt)
.where(SESSIONS.ID.eq(sessionId))
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,40 @@ public class JooqSessionFinder implements SessionFinder {
PostgresDSL
.select(
SESSIONS.ID,
SESSIONS.USER_AGENT,
SESSIONS.CREATED_AT,
SESSIONS.MODIFIED_AT,
SESSIONS.EXPIRES_AT
)
.from(SESSIONS)
.where(SESSIONS.USER_ID.eq(userId))
.orderBy(SESSIONS.CREATED_AT.desc())
)
.stream()
.map(JooqSessionMapper::map)
.toList();
}

@Override
public int getUserSessionsCount(long userId) {
return sqlQuery.count(
PostgresDSL
.select(SESSIONS.ID)
.from(SESSIONS)
.where(SESSIONS.USER_ID.eq(userId))
);
}

@Override
public @Nullable Pair<Session, Authentication> findSessionWithAuthenticationById(UUID sessionId) {
return sqlQuery
.fetchFirst(
PostgresDSL
.select(
SESSIONS.ID,
SESSIONS.USER_AGENT,
SESSIONS.CREATED_AT,
SESSIONS.MODIFIED_AT,
SESSIONS.EXPIRES_AT,
AUTHENTICATIONS.AUTHENTICATION_ID,
AUTHENTICATIONS.AUTHENTICATION_EMAIL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public interface SessionFinder {
@NotNull
List<Session> getUserSessions(long userId);

int getUserSessionsCount(long userId);

@Nullable
Pair<Session, Authentication> findSessionWithAuthenticationById(UUID sessionId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ public class JooqSessionMapper {
public static Session map(Record record) {
return Session.builder()
.id(record.get(SESSIONS.ID))
.userAgent(record.get(SESSIONS.USER_AGENT))
.createdAt(record.get(SESSIONS.CREATED_AT))
.modifiedAt(record.get(SESSIONS.MODIFIED_AT))
.expiresAt(record.get(SESSIONS.EXPIRES_AT))
.build();
}
Expand Down

0 comments on commit b015920

Please sign in to comment.