Audit (Spring Boot)

Introduction

Three approaches to introducing auditing into an application:


References

https://www.baeldung.com/database-auditing-jpa

https://thorben-janssen.com/hibernate-envers-extend-standard-revision/

https://thorben-janssen.com/hibernate-envers-query-data-audit-log/

It might be used, eg, for storing who hard-deleted a record.

https://stackoverflow.com/questions/37815460/ways-to-pass-additional-data-to-custom-revisionentity-in-hibernate-envers/37816083

https://www.baeldung.com/jpa-entity-lifecycle-events

https://docs.jboss.org/envers/docs/

https://docs.jboss.org/envers/docs/#tables

https://medium.com/@denuwanhimangahettiarachchi/maintain-the-data-versioning-info-with-spring-data-envers-42b6dfc19e27

https://adamzareba.github.io/Audit-entities-with-Hibernate-Envers/

https://bytefish.de/blog/hibernate_envers_versioning_and_auditing/

https://vladmihalcea.com/the-best-way-to-implement-an-audit-log-using-hibernate-envers/


Entity technical fields

How these fields are exactly filled will depend on which of the auditing strategies is used.


BaseEntity.java

package edu.cou.myapp.audit;


/**

 * Technical fields common to all entities.

 */

@NoArgsConstructor

@Data

@MappedSuperclass

public class BaseEntity {


/**

 * JPA optimistic locking.

 * Technical field only nullable in object (in table: not null default 0).

 */

@Version

@Column(name = "version", nullable = false)

@Nullable

private Long version;


/**

 * Created instant.

 * In table: datetime not null default current_timestamp

 */

@CreationTimestamp

@Column(name = "created_instant", nullable = false, updatable = false)

private Instant createdInstant;


/**

 * Created by (id).

 * Set on @PrePersist. In table: bigint nullable

 */

@Column(name = "created_by", nullable = true, updatable = false)

@Nullable

private Long createdBy;


/**

 * Modified instant. In table: datetime not null default current_timestamp

 * Set on @PrePersist, @PreUpdate.

 */

@UpdateTimestamp

@Column(name = "modified_instant", nullable = false)

private Instant modifiedInstant;


/**

 * Modified by (id). In table: bigint nullable

 * Set on @PrePersist, @PreUpdate.

 */

@Column(name = "modified_by", nullable = true)

@Nullable

private Long modifiedBy;


  /**

   * Inform current user, if known

   */

  @PrePersist

  public void preInsert() {

    this.createdBy = SecurityHelper.getCurrentId().orElse(null);

    this.modifiedBy = this.createdBy;

  }


  /**

   * Inform current user, if known

   */

  @PreUpdate

  public void preUpdate() {

    this.modifiedBy = SecurityHelper.getCurrentId().orElse(null);

  }


}


Whatever.java

package edu.cou.myapp;


@NoArgsConstructor

@RequiredArgsConstructor

@EqualsAndHashCode(callSuper = false)

@Data

@Table(name = "whatever")

@Entity

public class Whatever extends BaseEntity {

  ...

}



[1/3] Auditing with JPA (@PrePersist, @PreUpdate and @PreRemove)

Eg: https://www.baeldung.com/database-auditing-jpa#auditing


[2/3] Hibernate Envers (easy auditing / versioning)

Hibernate sequences (liquibase)

Note: With Liquibase >= 4.0.0 use lowercase "_aud" suffix instead of "_AUD" (because of case sensitivity with PostgreSQL).

                     If necessary, application.properties could be set (not recommended):

                     # spring.jpa.properties.org.hibernate.envers.audit_table_suffix=_AUD


Remark: When an Envers application using Hibernate <6 is migrated for Hibernate >=6, it fails because it doesn't find the sequence 'revinfo_seq'. It can be fixed reverting the Hibernate naming strategy in application.properties:

#DOC {standard (>=6 default), legacy (>=5.3, <6), single (<5.3), custom class} MYAPP uses legacy cause it was created before hibernate 6

spring.jpa.properties.hibernate.id.db_structure_naming_strategy=legacy


[1/2] For Hibernate >=6 (revinfo_rev_seq)

Eg: H2 - Hibernate 6.2.5 - Spring Boot 3.1.1 (as automatically generated by Hibernate):

#Only table is created

CREATE MEMORY TABLE "PUBLIC"."REVINFO"(

    "REV" INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1 RESTART WITH 3) DEFAULT ON NULL NOT NULL,

    "REVTSTMP" BIGINT

);

ALTER TABLE "PUBLIC"."REVINFO" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_6C" PRIMARY KEY("REV"); 

Eg: PosgreSQL - Hibernate 6.2.5 - Spring Boot 3.1.1 (as automatically generated by Hibernate):

#Both table and sequence are created

CREATE TABLE revinfo (

rev int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY( INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 CACHE 1 NO CYCLE),

revtstmp int8 NULL,

CONSTRAINT revinfo_pkey PRIMARY KEY (rev)

);


CREATE SEQUENCE public.revinfo_rev_seq

INCREMENT BY 1

MINVALUE 1

MAXVALUE 9223372036854775807

START 1

CACHE 1

NO CYCLE;


Envers seems to be able to use either  field level "revinfo_rev_seq" or table level "revinfo_seq":

# hibernate_sequence needed by Hibernate Envers - Easy Entity Auditing

# H2 sequence data type is BIGINT [https://www.h2database.com/html/commands.html#create_sequence]

databaseChangeLog:

  - changeSet:

      id: "002"

      author: thatsme

      failOnError: true

      comment: "Create sequence for 'revinfo' pk, needed by envers audit"

      changes:

        - createSequence:

            sequenceName: revinfo_rev_seq

            incrementBy: 1

            cycle: false

            cacheSize: 1

            #minValue: (int)-2147483648 (long)-9223372036854775808

            minValue: 1

            #maxValue: (int)2147483647 (long)9223372036854775807

            maxValue: 9223372036854775807

            startValue: 1


[2/2] For Hibernate <6 (hibernate_sequence)

By default, Hibernate <6 uses the sequence "hibernate_sequence" for the "revinfo" table primary key.

Liquibase script for creating this sequence can be found at page "Hibernate ORM (JPA)":

https://sites.google.com/site/pawneecity/java-development/hibernate-java#h.9fenronhfrt9


revinfo (liquibase)

Envers uses a revision info table to store the revision number, the timestamp and (if extended) any another information , eg: who inserted, updated or deleted a record.

WARNING: It's highly recommended to always use a 'CustomRevisionEntity' that extends 'DefaultRevisionEntity' to avoid later changes to field names (see CustomRevisionEntity).


Extending DefaultRevisionEntity (as of hibernate-envers-6.2.6.Final)

databaseChangeLog:


  - changeSet:

      id: "003"

      author: thatsme

      failOnError: true

      comment: "Creation of table revinfo (extending DefaultRevisionEntity)"

      changes:

        - createTable:

            tableName: revinfo

            columns:

             - column:

                  name: id

                  type: INTEGER

                  autoIncrement: true

                  constraints:

                     primaryKey: true

                     nullable: false

             - column:

                  name: timestamp

                  type: bigint

             - column:

                  name: user_id

                  type: bigint

                  remarks: Who inserted, updated or deleted a record (custom)

Without extending DefaultRevisionEntity  (as of hibernate-envers-6.2.6.Final)

databaseChangeLog:


  - changeSet:

      id: "003"

      author: thatsme

      failOnError: true

      comment: "Creation of table revinfo (not extending DefaultRevisionEntity)"

      changes:

        - createTable:

            tableName: revinfo

            columns:

             - column:

                  name: rev

                  type: bigint

                  autoIncrement: true

                  constraints:

                     primaryKey: true

                     nullable: false

             - column:

                  name: revtstmp

                  type: bigint

             - column:

                  name: user_id

                  type: bigint

                  remarks: Who inserted, updated or deleted a record (custom)


pom.xml (envers)

Tested w/ Spring Boot 3.2.0

<dependencies>

<!-- Audit: Envers queries available in repositories for Spring Data JPA -->

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-envers</artifactId>

</dependency>

</dependencies>

Sample table and its corresponding _aud (liquibase)

Let's create a sample table "app_request" and its corresponding auditing one "app_request_aud".

The primary key of the _aud table is the combination of the original primary key plus 'rev'.

Note: The 'rev' field must be of the same type than in table 'revinfo' (probable a "bigint").

Auditing type 'revtype' values are:

databaseChangeLog:


  - changeSet:

      id: "029"

      author: thatsme

      failOnError: true

      comment: "Creation of table app_request and its auditing"

      changes:

        - createTable:

            tableName: app_request

            columns:

             - column:

                  name: uuid

                  type: uuid

                  constraints:

                    nullable: false

                    primaryKey: true

                  remarks: "Request identifier"

             - column:

                  name: create_instant

                  type: timestamp

                  defaultValueComputed: current_timestamp

                  constraints:

                    nullable: false              

                  remarks: "Create instant"

             - column:

                  name: update_idp

                  type: bigint

                  constraints:

                    nullable: true

                  remarks: "Last update idp"

             - column:

                  name: version

                  type: bigint

                  constraints:

                    nullable: false

                    defaultValue: 0

                  remarks: "JPA version"

             - column:

                  name: update_instant

                  type: timestamp

                  defaultValueComputed: current_timestamp

                  constraints:

                    nullable: false

                  remarks: "Last update instant"


        - createTable:

            tableName: app_request_aud

            columns:

               - column:

                  name: uuid

                  type: uuid

                  constraints:

                    nullable: false

                    primaryKey: true

                    primaryKeyName: PK_APP_REQUEST_aud

                  remarks: "Request identifier"

               - column:

                  name: rev

                  type: bigint

                  constraints:

                     nullable: false

                     primaryKey: true

                     primaryKeyName: PK_APP_REQUEST_aud

                  remarks: "Auditing revision"

               - column:

                  name: revtype

                  type: tinyint

                  remarks: "Auditing type. 0=ADD, 1=MOD, 2=DEL"

               - column:

                  name: create_instant

                  type: timestamp

               - column:

                  name: update_idp

                  type: bigint

               - column:

                  name: version

                  type: bigint

               - column:

                  name: update_instant

                  type: timestamp

                  

        - addForeignKeyConstraint:

              baseColumnNames: rev

              baseTableName: app_request_aud

              constraintName: fk_app_request_aud_revinfo

              referencedColumnNames: rev

              referencedTableName: revinfo



App (Spring Boot entry point)

Notice the @EnableEnversRepositories annotation (before Spring Boot 2.5 use instead @EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class)).

package edu.cou.myapp;


import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.envers.repository.support.EnversRevisionRepositoryFactoryBean;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;


/**

 * Myapp app.<br>

 */

@Configuration

@ComponentScan(basePackages = { "edu.cou.myapp" })

@EnableEnversRepositories

@SpringBootApplication

public class App {


  /**

   * Main entry point.

   * 

   * @param args -

   */

  public static void main(String[] args) {

    SpringApplication.run(App.class, args);

  }


}



app_request (JPA Entity)

Notice the @Audited annotation

@Component

@SuppressFBWarnings

@Table(name = "app_request")

@NoArgsConstructor

@Data

@Audited

@Entity

public class AppRequest implements Serializable {


  /**

   * 

   */

  private static final long serialVersionUID = 2045704706981241836L;


  /**

   * Do never set this value, managed by JPA.

   */

  @Column(name = "version", nullable = false)

  @Version

  private Long version;


  /**

   * Do never set this value, implicitly assigned.

   */

  @Column(name = "update_instant", nullable = false)

  @UpdateTimestamp

  private Instant updateInstant;


  /**

   * Do always set this value (idp of the current user).

   */

  @Column(name = "update_idp", nullable = false)

  private Long updateIdp;


  /**

   * 

   */

  @Id

  @Column(name = "uuid")

  @Getter(AccessLevel.PUBLIC)

  @Setter(AccessLevel.PUBLIC)

  private UUID uuid;


  @Column(name = "create_instant")

  @Getter(AccessLevel.PUBLIC)

  @Setter(AccessLevel.PUBLIC)

  @UpdateTimestamp

  private Instant createInstant;


  /**

   * Constructor.

   * 

   * @param updateIdp

   * @param uuid

   */

  public AppRequest(final @Nullable Long updateIdp, final @NotNull UUID uuid) {

    this();

    this.updateIdp = updateIdp;

    this.uuid = uuid;

  }


}



app_request (JPA Repository)

Notice that it must extend 'RevisionRepository', which adds the following 4 methods:

@Repository

public interface AppRequestRepo extends JpaRepository<AppRequest, UUID>,

    RevisionRepository<AppRequest, UUID, Long> {


  /**

   * It removes records which 'created_instant' is older than specified instant.<br>

   * <br>

   * 

   * @param instant -

   * @return -

   */

  long deleteByCreateInstantBefore(final Date instant);


  /**

   * @param uuid

   * @return -

   */

  @Transactional

  Long deleteByUuid(UUID uuid);


  /**

   * @param uuid

   * @return -

   */

  Optional<AppRequest> findByUuid(UUID uuid);


  /**

   * @return -

   */

  @Query(nativeQuery = true, value = "SELECT COUNT(*) FROM app_request")

  Long getAppRequestRows();


}


Controller operation for retrieving  revisions of a given entity

  /**

   * Sample Envers query. <br>

   * 

   * @param rId

   * @return All revisions for given entity identifier.

   */

  @Operation(summary = "Revisions for given entity identifier", security = { @SecurityRequirement(

      name = "bearer-key") }, parameters = { @Parameter(name = "Authorization",

          in = ParameterIn.HEADER, required = true) })

  @PreAuthorize("hasAuthority('SUPER')")

  @GetMapping(value = "/revisions")

  public ResponseEntity<Revisions<Integer, AppRequest>> getCalendarTfGlobalConfigs(//

      @Parameter(description = "Request unique identifier", required = true,

          example = "3fa85f64-5717-4562-b3fc-2c963f66afa6") @PathVariable("rId") UUID rId //

  ) {


    Revisions<Integer, AppRequest> ret = this.appRequestRepository.findRevisions(rId);


    //

    return new ResponseEntity<Revisions<Integer, AppRequest>>(ret, HttpStatus.OK);

  }


Warning: Instances of class Revisions don't usually serialize fine in JSON. Error thrown, in debug mode, is:

Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Index -1 out of bounds for length 0]


An alternative w/out metadata is shown in the following example:

@Operation(summary = "[SUPER] Revisions for given entity identifier", security = {

      @SecurityRequirement(name = "bearer-key") })

@PreAuthorize("hasAuthority('SUPER')")  

@GetMapping(value = "/{id}/revisions")

  public ResponseEntity<?> getRevisions(//

      @Parameter(description = "Id", required = true,

          example = "color_red") @PathVariable("id") @NotBlank String mid

  ) {

    Revisions<Long, Translation> revisions = this.translationService.getTranslationRevisions(id);


    List<Revision<Long, Translation>> revisionsList = revisions.getContent();


    // Returning only records, w/out metadata

    List<Translation> ret = new ArrayList<>();

    for (Revision<Long, Translation> x : revisionsList) {

      ret.add(x.getEntity());

    }


    return new ResponseEntity<>(ret, HttpStatus.OK);

  }


Who inserted, updated or deleted a record?

By extending Envers table 'revinfo' w/ custom field 'user_id', we'll also be able to know who deleted a record


AuditConfig.java

package edu.cou.myapp;


@EnableEnversRepositories

@Configuration

public class AuditConfig {

}

CustomRevisionListener.java

package edu.cou.myapp.audit;


public class CustomRevisionListener implements RevisionListener {


  @Override

  public void newRevision(Object revisionEntity) {

    CustomRevisionEntity rev = (CustomRevisionEntity) revisionEntity;


    /*- DOC. If you use spring security, you could use SpringSecurityContextHolder:

     * final UserContext userContext = UserContextHolder.getUserContext()

     */


    /*- Populate the id of the current user */

     Optional<Long> userIdOpt =SecurityHelper.getCurrentId();

     rev.setUserId(userIdOpt.orElse(null));

  }


}

CustomRevisionEntity.java (extending DefaultRevisionEntity

package edu.cou.myapp.audit;


/**

 * Envers custom 'revinfo' table.

 * 

 * Add userId to the Envers revision table. It will, also, allow to know who has deleted a record.

 * 

 * <pre>

 * WARNING: Extending DefaultRevisionEntity changes the default Envers field names.

 * https://stackoverflow.com/questions/12736811/why-does-overriding-change-column-names

 * https://hibernate.atlassian.net/browser/HHH-11325

 * 

 * "rev" -> "id"

 * "revtstmp" -> "timestamp"

 * </pre>

 */

@Data

@EqualsAndHashCode(callSuper=true)

@RevisionEntity(CustomRevisionListener.class)

@Table(name = "revinfo")

@Entity

public class CustomRevisionEntity extends DefaultRevisionEntity{


  private static final long serialVersionUID = -8490066597393478856L;

  

  /*- Table field 'user_id' */

  @Nullable

  private Long userId;

}

CustomRevisionEntity.java (workaround w/out extending DefaultRevisionEntity)

package edu.cou.myapp.audit;


/**

 * Envers custom 'revinfo' table.

 * 

 * Add userId to the Envers revision table. It will, also, allow to know who has deleted a record.

 * 

 * <pre>

 * WARNING: Extending DefaultRevisionEntity changes the default Envers field names.

 * https://stackoverflow.com/questions/12736811/why-does-overriding-change-column-names

 * https://hibernate.atlassian.net/browser/HHH-11325

 * 

 * "rev" -> "id"

 * "revtstmp" -> "timestamp"

 * </pre>

 */

@Data

@EqualsAndHashCode(callSuper=true)

@RevisionEntity(CustomRevisionListener.class)

@Table(name = "revinfo")

@Entity

public class CustomRevisionEntity /*extends DefaultRevisionEntity*/ implements Serializable {


 private static final long serialVersionUID = 5012215631880447514L;

  

  /*- Workaround, don't do this in new projects

   * https://stackoverflow.com/questions/12736811/why-does-overriding-change-column-names

   */

  @EqualsAndHashCode.Include

  @Id

  @GeneratedValue

  @RevisionNumber

  private long rev;


  /*- Workaround, don't do this in new projects

   * https://stackoverflow.com/questions/12736811/why-does-overriding-change-column-names

   */

  @EqualsAndHashCode.Include

  @RevisionTimestamp

  private long revtstmp;


  /*- Table field 'user_id' */

  @Nullable

  private Long userId;


  @Override

  public String toString() {

    return "CustomRevisionEntity(revId = " + this.rev + ", revDate = " + DateTimeFormatter.INSTANCE

        .format(new Date(this.revtstmp)) + ")";

  }

}

Car.java (entity for testing purposes)

package edu.cou.myapp.entity;


@NoArgsConstructor

@RequiredArgsConstructor

@Data

@EqualsAndHashCode(callSuper=true)

@Audited

@Entity

@Table(name = "car")

public class Car extends BaseEntity{

  

  /**

   * Record identifier (Vehicle Identification Number).

   */

  @Id

  @Column(name = "vin", nullable = false)

  @NonNull

  private Long vin;


  @Column(name = "plate", nullable = true)

  @Nullable

  private String plate;

}

CarRepo.java

package edu.cou.myapp.repo;


public interface CarRepo extends JpaRepository<Car, Long>, RevisionRepository<Car, Long, Long> {


}

SecurityHelper.java

package edu.cou.myapp.security;


@UtilityClass

public class SecurityHelper {

  

  /** Fake user id form testing purposes only */

  public static final Long USER_ID_FAKE = 2727L;


  /** Simulate, for testing purposes only, getting the user id from the Spring Security Context */

  public static Optional<Long> getCurrentId() {

    return Optional.ofNullable(USER_ID_FAKE);

  }


}

application.properties (main)

spring.jpa.open-in-view=false

spring.datasource.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=LEGACY

spring.datasource.username=sa

CasrTest.java (unit test)

package edu.cou.myapp.entity;


@Slf4j

@DataJpaTest

class CarTest {


  @Inject

  private PlatformTransactionManager platformTransactionManager;


  @Inject

  private ApplicationContext applicationContext;


  @Inject

  private CarRepo carRepo;


  /**

   * Delete an entity and ensure Envers audit stores the current userId.

   * 

   * @throws InterruptedException

   */

  @WithMockUser(username = "johndoe", authorities = { "SUPER", "MANAGER" })

  @Test

  void test() throws InterruptedException {

    TransactionTemplate tx = new TransactionTemplate(this.platformTransactionManager);

    tx.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);


    Car e = new Car(556699L);

    Car eSaved = tx.execute(status -> this.carRepo.save(e));


    log.info(">>> Created: " + eSaved.getCreatedInstant());

    log.info(">>> Created: " + eSaved.getCreatedBy());

    log.info(">>> Modified: " + eSaved.getModifiedInstant());

    log.info(">>> Modified: " + eSaved.getModifiedBy());


    // Delete the record

    tx.executeWithoutResult(status -> this.carRepo.delete(eSaved));


    JpaTransactionManager tm = this.applicationContext.getBean(JpaTransactionManager.class);

    EntityManagerFactory emf = tm.getEntityManagerFactory();


    tx.executeWithoutResult(status -> {

      EntityManager em = EntityManagerFactoryUtils.getTransactionalEntityManager(emf);


      // Ensure that the userId that deleted the record has been saved by Envers audit

      AuditReader auditReader = AuditReaderFactory.get(em);


      AuditQuery q = auditReader.createQuery().forRevisionsOfEntity(Car.class, false, true);

      // q.addProjection(AuditEntity.revisionNumber());

      q.add(AuditEntity.revisionProperty("userId").eq(SecurityHelper.USER_ID_FAKE));


      List<?> revs = q.getResultList();


      // Encure there are exactly 2 revisions (insert + delete)

      Assertions.assertEquals(2, revs.size());


      // The 2nd revision is the DELETE

      Object[] rev2 = (Object[]) revs.get(1);

      // The 2nd element of the array is the CustomRevisionEntity

      CustomRevisionEntity cre2 = (CustomRevisionEntity) rev2[1];

      log.info(cre2.toString());


      // Ensure the userId has been saved in the revision of the DELETE

      Assertions.assertEquals(SecurityHelper.USER_ID_FAKE, cre2.getUserId());


    });


  }


}


Issues (envers)

Issue is fixed and verified to work out as expected on Spring Boot 2.3.1.RELEASE

See:

https://github.com/spring-projects/spring-data-envers/issues/215


[3/3] Spring Data JPA (@CreatedDate, @CreatedBy, @LastModifiedDate, @LastModifiedBy)

I never was able to make it work out together with Hibernate Envers.

Eg: https://www.baeldung.com/database-auditing-jpa#spring

In other projects w/ out Envers, eg ssz, it worked out fine:

MyEntity.java

package edu.cou.myapp.entity;


@EntityListeners(AuditingEntityListener.class)

@Entity

public class MyEntity {


   @Column(name = "modified_by")

   @LastModifiedBy

   private String modifiedBy;

}



AuditConfig.java

package edu.cou.myapp;


import edu.cou.myapp.audit.AuditorAwareImpl;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.domain.AuditorAware;

import org.springframework.data.envers.repository.config.EnableEnversRepositories;

import org.springframework.data.jpa.repository.config.EnableJpaAuditing;


/**

 * Audit config.

 */

@EnableJpaAuditing(auditorAwareRef = "auditorProvider")

//@EnableEnversRepositories

@Configuration

public class AuditConfig {


  /*- Used by JpaAuditing */

  @Bean

  AuditorAware<Long> auditorProvider() {

    return new AuditorAwareImpl();

  }


}

AuditorAwareImpl.java

package edu.cou.myapp.audit;


import edu.cou.myapp.security.CustomUserDetails;

import java.util.Optional;

import org.springframework.data.domain.AuditorAware;

import org.springframework.security.core.context.SecurityContextHolder;


/**

 * Used by JpaAuditing

 */

public class AuditorAwareImpl implements AuditorAware<Long> {


  @Override

  public Optional<Long> getCurrentAuditor() {

    var authentication = SecurityContextHolder.getContext().getAuthentication();


    if (authentication == null || !authentication.isAuthenticated()) {

      return Optional.empty();

    }


    return Optional.of(((CustomUserDetails) authentication.getPrincipal()).getId());

  }


}