diff --git a/backend/src/main/java/ervu_business_metrics/dao/FailedAuthDao.java b/backend/src/main/java/ervu_business_metrics/dao/FailedAuthDao.java
new file mode 100644
index 0000000..b5483d6
--- /dev/null
+++ b/backend/src/main/java/ervu_business_metrics/dao/FailedAuthDao.java
@@ -0,0 +1,37 @@
+package ervu_business_metrics.dao;
+
+import java.sql.Timestamp;
+
+import ervu_business_metrics.config.KafkaEnabledCondition;
+import ervu_business_metrics.model.sso.AuthEventResponse;
+import org.jooq.DSLContext;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.stereotype.Repository;
+
+import static ru.micord.webbpm.ervu.business_metrics.db_beans.auth.Tables.FAILED_AUTH;
+
+/**
+ * @author gulnaz
+ */
+@Repository
+@Conditional(KafkaEnabledCondition.class)
+public class FailedAuthDao {
+
+ private final DSLContext dslContext;
+
+ public FailedAuthDao(DSLContext dslContext) {
+ this.dslContext = dslContext;
+ }
+
+ public void save(AuthEventResponse authEventResponse) {
+ dslContext.insertInto(FAILED_AUTH)
+ .set(FAILED_AUTH.ID, authEventResponse.id())
+ .set(FAILED_AUTH.USER_ID, authEventResponse.userId())
+ .set(FAILED_AUTH.DATETIME, new Timestamp(Long.parseLong(authEventResponse.timeCreated())))
+ .execute();
+ }
+
+ public void clear() {
+ dslContext.truncate(FAILED_AUTH).execute();
+ }
+}
diff --git a/backend/src/main/java/ervu_business_metrics/kafka/listener/FailedAuthListener.java b/backend/src/main/java/ervu_business_metrics/kafka/listener/FailedAuthListener.java
new file mode 100644
index 0000000..28676fa
--- /dev/null
+++ b/backend/src/main/java/ervu_business_metrics/kafka/listener/FailedAuthListener.java
@@ -0,0 +1,44 @@
+package ervu_business_metrics.kafka.listener;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import ervu_business_metrics.config.KafkaEnabledCondition;
+import ervu_business_metrics.model.sso.AuthEventResponse;
+import ervu_business_metrics.service.FailedAuthService;
+import exception.JsonParseException;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author gulnaz
+ */
+@Component
+@Conditional(KafkaEnabledCondition.class)
+public class FailedAuthListener {
+ private static final String FAILED_AUTH_EVENT = "USER_AUTHENTICATION_ERROR";
+
+ private final ObjectMapper objectMapper;
+ private final FailedAuthService failedAuthService;
+
+ public FailedAuthListener(ObjectMapper objectMapper, FailedAuthService failedAuthService) {
+ this.objectMapper = objectMapper;
+ this.failedAuthService = failedAuthService;
+ }
+
+ @KafkaListener(id = "${kafka.auth.events.group.id}", topics = "${kafka.sso.auth.events.response}")
+ public void listenFailedAuthEvent(String kafkaMessage) {
+ AuthEventResponse response;
+
+ try {
+ response = objectMapper.readValue(kafkaMessage, AuthEventResponse.class);
+ }
+ catch (JsonProcessingException e) {
+ throw new JsonParseException(e);
+ }
+
+ if (response.userId() != null && FAILED_AUTH_EVENT.equals(response.eventType())) {
+ failedAuthService.save(response);
+ }
+ }
+}
diff --git a/backend/src/main/java/ervu_business_metrics/model/sso/AuthEventResponse.java b/backend/src/main/java/ervu_business_metrics/model/sso/AuthEventResponse.java
new file mode 100644
index 0000000..de65571
--- /dev/null
+++ b/backend/src/main/java/ervu_business_metrics/model/sso/AuthEventResponse.java
@@ -0,0 +1,10 @@
+package ervu_business_metrics.model.sso;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * @author gulnaz
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record AuthEventResponse(String id, String userId, String eventType, String timeCreated) {
+}
diff --git a/backend/src/main/java/ervu_business_metrics/service/FailedAuthService.java b/backend/src/main/java/ervu_business_metrics/service/FailedAuthService.java
new file mode 100644
index 0000000..037bf20
--- /dev/null
+++ b/backend/src/main/java/ervu_business_metrics/service/FailedAuthService.java
@@ -0,0 +1,26 @@
+package ervu_business_metrics.service;
+
+import ervu_business_metrics.dao.FailedAuthDao;
+import ervu_business_metrics.model.sso.AuthEventResponse;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author gulnaz
+ */
+@Service
+public class FailedAuthService {
+
+ private final FailedAuthDao failedAuthDao;
+
+ public FailedAuthService(FailedAuthDao failedAuthDao) {
+ this.failedAuthDao = failedAuthDao;
+ }
+
+ public void save(AuthEventResponse authEventResponse) {
+ failedAuthDao.save(authEventResponse);
+ }
+
+ public void clear() {
+ failedAuthDao.clear();
+ }
+}
diff --git a/backend/src/main/java/ervu_business_metrics/service/scheduler/FailedAuthSchedulerService.java b/backend/src/main/java/ervu_business_metrics/service/scheduler/FailedAuthSchedulerService.java
new file mode 100644
index 0000000..4318212
--- /dev/null
+++ b/backend/src/main/java/ervu_business_metrics/service/scheduler/FailedAuthSchedulerService.java
@@ -0,0 +1,28 @@
+package ervu_business_metrics.service.scheduler;
+
+import ervu_business_metrics.config.KafkaEnabledCondition;
+import ervu_business_metrics.service.FailedAuthService;
+import net.javacrumbs.shedlock.core.SchedulerLock;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author gulnaz
+ */
+@Service
+@Conditional(KafkaEnabledCondition.class)
+public class FailedAuthSchedulerService {
+
+ private final FailedAuthService failedAuthService;
+
+ public FailedAuthSchedulerService(FailedAuthService failedAuthService) {
+ this.failedAuthService = failedAuthService;
+ }
+
+ @Scheduled(cron = "${scheduler.failed_auth_cleanup.cron:0 0 0 * * *}")
+ @SchedulerLock(name = "clearFailedAuth")
+ public void clear() {
+ failedAuthService.clear();
+ }
+}
diff --git a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Auth.java b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Auth.java
index 193c038..5ad6d3e 100644
--- a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Auth.java
+++ b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Auth.java
@@ -13,6 +13,7 @@ import org.jooq.impl.SchemaImpl;
import ru.micord.webbpm.ervu.business_metrics.db_beans.DefaultCatalog;
import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.ActiveSession;
+import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.FailedAuth;
/**
@@ -33,6 +34,11 @@ public class Auth extends SchemaImpl {
*/
public final ActiveSession ACTIVE_SESSION = ActiveSession.ACTIVE_SESSION;
+ /**
+ * События неуспешной аутентификации
+ */
+ public final FailedAuth FAILED_AUTH = FailedAuth.FAILED_AUTH;
+
/**
* No further instances allowed
*/
@@ -49,7 +55,8 @@ public class Auth extends SchemaImpl {
@Override
public final List
> getTables() {
return Arrays.asList(
- ActiveSession.ACTIVE_SESSION
+ ActiveSession.ACTIVE_SESSION,
+ FailedAuth.FAILED_AUTH
);
}
}
diff --git a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Keys.java b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Keys.java
index cdff6e9..b6df807 100644
--- a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Keys.java
+++ b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Keys.java
@@ -10,7 +10,9 @@ import org.jooq.impl.DSL;
import org.jooq.impl.Internal;
import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.ActiveSession;
+import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.FailedAuth;
import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.records.ActiveSessionRecord;
+import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.records.FailedAuthRecord;
/**
@@ -25,4 +27,5 @@ public class Keys {
// -------------------------------------------------------------------------
public static final UniqueKey PK_ACTIVE_SESSION = Internal.createUniqueKey(ActiveSession.ACTIVE_SESSION, DSL.name("pk_active_session"), new TableField[] { ActiveSession.ACTIVE_SESSION.SESSION_ID, ActiveSession.ACTIVE_SESSION.DOMAIN_ID }, true);
+ public static final UniqueKey FAILED_AUTH_PKEY = Internal.createUniqueKey(FailedAuth.FAILED_AUTH, DSL.name("failed_auth_pkey"), new TableField[] { FailedAuth.FAILED_AUTH.ID }, true);
}
diff --git a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Tables.java b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Tables.java
index a956554..f657710 100644
--- a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Tables.java
+++ b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Tables.java
@@ -5,6 +5,7 @@ package ru.micord.webbpm.ervu.business_metrics.db_beans.auth;
import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.ActiveSession;
+import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.FailedAuth;
/**
@@ -17,4 +18,9 @@ public class Tables {
* The table auth.active_session.
*/
public static final ActiveSession ACTIVE_SESSION = ActiveSession.ACTIVE_SESSION;
+
+ /**
+ * События неуспешной аутентификации
+ */
+ public static final FailedAuth FAILED_AUTH = FailedAuth.FAILED_AUTH;
}
diff --git a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/tables/FailedAuth.java b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/tables/FailedAuth.java
new file mode 100644
index 0000000..c18de5b
--- /dev/null
+++ b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/tables/FailedAuth.java
@@ -0,0 +1,230 @@
+/*
+ * This file is generated by jOOQ.
+ */
+package ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables;
+
+
+import java.sql.Timestamp;
+import java.util.Collection;
+
+import org.jooq.Condition;
+import org.jooq.Field;
+import org.jooq.Name;
+import org.jooq.PlainSQL;
+import org.jooq.QueryPart;
+import org.jooq.SQL;
+import org.jooq.Schema;
+import org.jooq.Select;
+import org.jooq.Stringly;
+import org.jooq.Table;
+import org.jooq.TableField;
+import org.jooq.TableOptions;
+import org.jooq.UniqueKey;
+import org.jooq.impl.DSL;
+import org.jooq.impl.SQLDataType;
+import org.jooq.impl.TableImpl;
+
+import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.Auth;
+import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.Keys;
+import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.records.FailedAuthRecord;
+
+
+/**
+ * События неуспешной аутентификации
+ */
+@SuppressWarnings({ "all", "unchecked", "rawtypes" })
+public class FailedAuth extends TableImpl {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * The reference instance of auth.failed_auth
+ */
+ public static final FailedAuth FAILED_AUTH = new FailedAuth();
+
+ /**
+ * The class holding records for this type
+ */
+ @Override
+ public Class getRecordType() {
+ return FailedAuthRecord.class;
+ }
+
+ /**
+ * The column auth.failed_auth.id. Идентификатор события
+ */
+ public final TableField ID = createField(DSL.name("id"), SQLDataType.VARCHAR(128).nullable(false), this, "Идентификатор события");
+
+ /**
+ * The column auth.failed_auth.user_id. Идентификатор
+ * пользователя
+ */
+ public final TableField USER_ID = createField(DSL.name("user_id"), SQLDataType.VARCHAR(128).nullable(false), this, "Идентификатор пользователя");
+
+ /**
+ * The column auth.failed_auth.datetime. Дата и время события
+ */
+ public final TableField DATETIME = createField(DSL.name("datetime"), SQLDataType.TIMESTAMP(0), this, "Дата и время события");
+
+ private FailedAuth(Name alias, Table aliased) {
+ this(alias, aliased, (Field>[]) null, null);
+ }
+
+ private FailedAuth(Name alias, Table aliased, Field>[] parameters, Condition where) {
+ super(alias, null, aliased, parameters, DSL.comment("События неуспешной аутентификации"), TableOptions.table(), where);
+ }
+
+ /**
+ * Create an aliased auth.failed_auth table reference
+ */
+ public FailedAuth(String alias) {
+ this(DSL.name(alias), FAILED_AUTH);
+ }
+
+ /**
+ * Create an aliased auth.failed_auth table reference
+ */
+ public FailedAuth(Name alias) {
+ this(alias, FAILED_AUTH);
+ }
+
+ /**
+ * Create a auth.failed_auth table reference
+ */
+ public FailedAuth() {
+ this(DSL.name("failed_auth"), null);
+ }
+
+ @Override
+ public Schema getSchema() {
+ return aliased() ? null : Auth.AUTH;
+ }
+
+ @Override
+ public UniqueKey getPrimaryKey() {
+ return Keys.FAILED_AUTH_PKEY;
+ }
+
+ @Override
+ public FailedAuth as(String alias) {
+ return new FailedAuth(DSL.name(alias), this);
+ }
+
+ @Override
+ public FailedAuth as(Name alias) {
+ return new FailedAuth(alias, this);
+ }
+
+ @Override
+ public FailedAuth as(Table> alias) {
+ return new FailedAuth(alias.getQualifiedName(), this);
+ }
+
+ /**
+ * Rename this table
+ */
+ @Override
+ public FailedAuth rename(String name) {
+ return new FailedAuth(DSL.name(name), null);
+ }
+
+ /**
+ * Rename this table
+ */
+ @Override
+ public FailedAuth rename(Name name) {
+ return new FailedAuth(name, null);
+ }
+
+ /**
+ * Rename this table
+ */
+ @Override
+ public FailedAuth rename(Table> name) {
+ return new FailedAuth(name.getQualifiedName(), null);
+ }
+
+ /**
+ * Create an inline derived table from this table
+ */
+ @Override
+ public FailedAuth where(Condition condition) {
+ return new FailedAuth(getQualifiedName(), aliased() ? this : null, null, condition);
+ }
+
+ /**
+ * Create an inline derived table from this table
+ */
+ @Override
+ public FailedAuth where(Collection extends Condition> conditions) {
+ return where(DSL.and(conditions));
+ }
+
+ /**
+ * Create an inline derived table from this table
+ */
+ @Override
+ public FailedAuth where(Condition... conditions) {
+ return where(DSL.and(conditions));
+ }
+
+ /**
+ * Create an inline derived table from this table
+ */
+ @Override
+ public FailedAuth where(Field condition) {
+ return where(DSL.condition(condition));
+ }
+
+ /**
+ * Create an inline derived table from this table
+ */
+ @Override
+ @PlainSQL
+ public FailedAuth where(SQL condition) {
+ return where(DSL.condition(condition));
+ }
+
+ /**
+ * Create an inline derived table from this table
+ */
+ @Override
+ @PlainSQL
+ public FailedAuth where(@Stringly.SQL String condition) {
+ return where(DSL.condition(condition));
+ }
+
+ /**
+ * Create an inline derived table from this table
+ */
+ @Override
+ @PlainSQL
+ public FailedAuth where(@Stringly.SQL String condition, Object... binds) {
+ return where(DSL.condition(condition, binds));
+ }
+
+ /**
+ * Create an inline derived table from this table
+ */
+ @Override
+ @PlainSQL
+ public FailedAuth where(@Stringly.SQL String condition, QueryPart... parts) {
+ return where(DSL.condition(condition, parts));
+ }
+
+ /**
+ * Create an inline derived table from this table
+ */
+ @Override
+ public FailedAuth whereExists(Select> select) {
+ return where(DSL.exists(select));
+ }
+
+ /**
+ * Create an inline derived table from this table
+ */
+ @Override
+ public FailedAuth whereNotExists(Select> select) {
+ return where(DSL.notExists(select));
+ }
+}
diff --git a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/tables/records/FailedAuthRecord.java b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/tables/records/FailedAuthRecord.java
new file mode 100644
index 0000000..d9e319b
--- /dev/null
+++ b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/tables/records/FailedAuthRecord.java
@@ -0,0 +1,98 @@
+/*
+ * This file is generated by jOOQ.
+ */
+package ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.records;
+
+
+import java.sql.Timestamp;
+
+import org.jooq.Record1;
+import org.jooq.impl.UpdatableRecordImpl;
+
+import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.FailedAuth;
+
+
+/**
+ * События неуспешной аутентификации
+ */
+@SuppressWarnings({ "all", "unchecked", "rawtypes" })
+public class FailedAuthRecord extends UpdatableRecordImpl {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Setter for auth.failed_auth.id. Идентификатор события
+ */
+ public void setId(String value) {
+ set(0, value);
+ }
+
+ /**
+ * Getter for auth.failed_auth.id. Идентификатор события
+ */
+ public String getId() {
+ return (String) get(0);
+ }
+
+ /**
+ * Setter for auth.failed_auth.user_id. Идентификатор
+ * пользователя
+ */
+ public void setUserId(String value) {
+ set(1, value);
+ }
+
+ /**
+ * Getter for auth.failed_auth.user_id. Идентификатор
+ * пользователя
+ */
+ public String getUserId() {
+ return (String) get(1);
+ }
+
+ /**
+ * Setter for auth.failed_auth.datetime. Дата и время события
+ */
+ public void setDatetime(Timestamp value) {
+ set(2, value);
+ }
+
+ /**
+ * Getter for auth.failed_auth.datetime. Дата и время события
+ */
+ public Timestamp getDatetime() {
+ return (Timestamp) get(2);
+ }
+
+ // -------------------------------------------------------------------------
+ // Primary key information
+ // -------------------------------------------------------------------------
+
+ @Override
+ public Record1 key() {
+ return (Record1) super.key();
+ }
+
+ // -------------------------------------------------------------------------
+ // Constructors
+ // -------------------------------------------------------------------------
+
+ /**
+ * Create a detached FailedAuthRecord
+ */
+ public FailedAuthRecord() {
+ super(FailedAuth.FAILED_AUTH);
+ }
+
+ /**
+ * Create a detached, initialised FailedAuthRecord
+ */
+ public FailedAuthRecord(String id, String userId, Timestamp datetime) {
+ super(FailedAuth.FAILED_AUTH);
+
+ setId(id);
+ setUserId(userId);
+ setDatetime(datetime);
+ resetChangedOnNotNull();
+ }
+}
diff --git a/backend/src/main/resources/config/v_1.0/20250512-SUPPORT-9166_add_failed_auth_table.xml b/backend/src/main/resources/config/v_1.0/20250512-SUPPORT-9166_add_failed_auth_table.xml
new file mode 100644
index 0000000..a0bf683
--- /dev/null
+++ b/backend/src/main/resources/config/v_1.0/20250512-SUPPORT-9166_add_failed_auth_table.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ create failed_auth table
+
+ CREATE TABLE IF NOT EXISTS auth.failed_auth (
+ id VARCHAR(128) NOT NULL PRIMARY KEY,
+ user_id VARCHAR(128) NOT NULL,
+ datetime timestamp without time zone
+ );
+
+ ALTER TABLE auth.failed_auth OWNER TO ervu_business_metrics;
+
+ COMMENT ON TABLE auth.failed_auth IS 'События неуспешной аутентификации';
+ COMMENT ON COLUMN auth.failed_auth.id IS 'Идентификатор события';
+ COMMENT ON COLUMN auth.failed_auth.user_id IS 'Идентификатор пользователя';
+ COMMENT ON COLUMN auth.failed_auth.datetime IS 'Дата и время события';
+
+
+
diff --git a/backend/src/main/resources/config/v_1.0/changelog-1.0.xml b/backend/src/main/resources/config/v_1.0/changelog-1.0.xml
index 81e4a68..e134261 100644
--- a/backend/src/main/resources/config/v_1.0/changelog-1.0.xml
+++ b/backend/src/main/resources/config/v_1.0/changelog-1.0.xml
@@ -32,6 +32,7 @@
+
\ No newline at end of file
diff --git a/config/micord.env b/config/micord.env
index 9cb6bd8..dcac6eb 100644
--- a/config/micord.env
+++ b/config/micord.env
@@ -37,4 +37,6 @@ SESSION_SYNC_CRON=0 */10 * * * *
KAFKA_SSO_SESSIONS_STATE_RECEIVE=sso.session.state.receive
KAFKA_SSO_SESSIONS_STATE_RESPONSE=sso.sessions.state.response
KAFKA_SESSION_GROUP_ID=ervu-business-metrics-backend-session
-SESSION_FETCH_TIMEOUT=15
\ No newline at end of file
+SESSION_FETCH_TIMEOUT=15
+KAFKA_SSO_AUTH_EVENTS_RESPONSE=sso.auth.events
+KAFKA_AUTH_EVENTS_GROUP_ID=ervu-business-metrics-backend-auth-events