Separate audit log db

This commit is contained in:
2026-01-23 18:14:09 +01:00
parent e578007115
commit c96918340a
11 changed files with 229 additions and 6 deletions

View File

@@ -4,6 +4,7 @@
.DEFAULT_GOAL := build
IMGFLOAT_DB_PATH ?= ./imgfloat.db
IMGFLOAT_AUDIT_DB_PATH ?= ./imgfloat.audit.db
IMGFLOAT_GITHUB_CLIENT_OWNER ?= imgfloat
IMGFLOAT_GITHUB_CLIENT_REPO ?= client
IMGFLOAT_GITHUB_CLIENT_VERSION ?= 1.0.0
@@ -25,6 +26,7 @@ RUNTIME_ENV = IMGFLOAT_ASSETS_PATH=$(IMGFLOAT_ASSETS_PATH) \
IMGFLOAT_GITHUB_CLIENT_VERSION=$(IMGFLOAT_GITHUB_CLIENT_VERSION) \
IMGFLOAT_COMMIT_URL_PREFIX=$(IMGFLOAT_COMMIT_URL_PREFIX) \
IMGFLOAT_DB_PATH=$(IMGFLOAT_DB_PATH) \
IMGFLOAT_AUDIT_DB_PATH=$(IMGFLOAT_AUDIT_DB_PATH) \
SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE=$(SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE) \
SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE=$(SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE) \
IMGFLOAT_TOKEN_ENCRYPTION_KEY=$(IMGFLOAT_TOKEN_ENCRYPTION_KEY)

View File

@@ -13,6 +13,7 @@ Define the following required environment variables:
| `IMGFLOAT_ASSETS_PATH` | Filesystem path to store uploaded assets | /var/imgfloat/assets |
| `IMGFLOAT_PREVIEWS_PATH` | Filesystem path to store generated image previews | /var/imgfloat/previews |
| `IMGFLOAT_DB_PATH` | Filesystem path to the SQLite database file | /var/imgfloat/imgfloat.db |
| `IMGFLOAT_AUDIT_DB_PATH` | Filesystem path to the SQLite audit log database file | /var/imgfloat/imgfloat-audit.db |
| `IMGFLOAT_INITIAL_TWITCH_USERNAME_SYSADMIN` | Twitch username of the initial sysadmin user | example_broadcaster |
| `IMGFLOAT_GITHUB_CLIENT_OWNER` | GitHub user or org which has the client repository | imgfloat |
| `IMGFLOAT_GITHUB_CLIENT_REPO` | Client repository name | client |

View File

@@ -1,4 +1,4 @@
package dev.kruhlmann.imgfloat.model;
package dev.kruhlmann.imgfloat.audit.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;

View File

@@ -0,0 +1,90 @@
package dev.kruhlmann.imgfloat.config;
import com.zaxxer.hikari.HikariDataSource;
import jakarta.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.configuration.FluentConfiguration;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.flyway.FlywayProperties;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@EnableJpaRepositories(
basePackages = "dev.kruhlmann.imgfloat.repository.audit",
entityManagerFactoryRef = "auditEntityManagerFactory",
transactionManagerRef = "auditTransactionManager"
)
public class AuditLogDataSourceConfig {
@Bean
@ConfigurationProperties("imgfloat.audit.datasource")
public DataSourceProperties auditDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("imgfloat.audit.datasource.hikari")
public HikariDataSource auditDataSource(
@Qualifier("auditDataSourceProperties") DataSourceProperties properties
) {
return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Bean
public LocalContainerEntityManagerFactoryBean auditEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("auditDataSource") DataSource dataSource,
JpaProperties jpaProperties,
HibernateProperties hibernateProperties
) {
return builder
.dataSource(dataSource)
.packages("dev.kruhlmann.imgfloat.audit.model")
.properties(hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings()))
.persistenceUnit("audit")
.build();
}
@Bean
public PlatformTransactionManager auditTransactionManager(
@Qualifier("auditEntityManagerFactory") EntityManagerFactory entityManagerFactory
) {
return new JpaTransactionManager(entityManagerFactory);
}
@Bean
@ConfigurationProperties("imgfloat.audit.flyway")
public FlywayProperties auditFlywayProperties() {
return new FlywayProperties();
}
@Bean(initMethod = "migrate")
public Flyway auditFlyway(
@Qualifier("auditDataSource") DataSource dataSource,
@Qualifier("auditFlywayProperties") FlywayProperties properties
) {
FluentConfiguration configuration = Flyway.configure().dataSource(dataSource);
if (properties.getLocations() != null && !properties.getLocations().isEmpty()) {
configuration.locations(properties.getLocations().toArray(new String[0]));
}
if (properties.isBaselineOnMigrate()) {
configuration.baselineOnMigrate(true);
}
if (properties.getBaselineVersion() != null) {
configuration.baselineVersion(properties.getBaselineVersion());
}
return configuration.load();
}
}

View File

@@ -0,0 +1,99 @@
package dev.kruhlmann.imgfloat.config;
import com.zaxxer.hikari.HikariDataSource;
import dev.kruhlmann.imgfloat.repository.audit.AuditLogRepository;
import jakarta.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.configuration.FluentConfiguration;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.flyway.FlywayProperties;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@EnableJpaRepositories(
basePackages = "dev.kruhlmann.imgfloat.repository",
excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = AuditLogRepository.class),
entityManagerFactoryRef = "entityManagerFactory",
transactionManagerRef = "transactionManager"
)
public class PrimaryDataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
@ConfigurationProperties("spring.datasource.hikari")
public HikariDataSource dataSource(@Qualifier("dataSourceProperties") DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("dataSource") DataSource dataSource,
JpaProperties jpaProperties,
HibernateProperties hibernateProperties
) {
return builder
.dataSource(dataSource)
.packages("dev.kruhlmann.imgfloat.model")
.properties(hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings()))
.persistenceUnit("primary")
.build();
}
@Bean
@Primary
public PlatformTransactionManager transactionManager(
@Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory
) {
return new JpaTransactionManager(entityManagerFactory);
}
@Bean
@Primary
@ConfigurationProperties("spring.flyway")
public FlywayProperties flywayProperties() {
return new FlywayProperties();
}
@Bean(initMethod = "migrate")
@Primary
public Flyway flyway(
@Qualifier("dataSource") DataSource dataSource,
@Qualifier("flywayProperties") FlywayProperties properties
) {
FluentConfiguration configuration = Flyway.configure().dataSource(dataSource);
if (properties.getLocations() != null && !properties.getLocations().isEmpty()) {
configuration.locations(properties.getLocations().toArray(new String[0]));
}
if (properties.isBaselineOnMigrate()) {
configuration.baselineOnMigrate(true);
}
if (properties.getBaselineVersion() != null) {
configuration.baselineVersion(properties.getBaselineVersion());
}
return configuration.load();
}
}

View File

@@ -37,6 +37,9 @@ public class SystemEnvironmentValidator {
@Value("${IMGFLOAT_DB_PATH:#{null}}")
private String dbPath;
@Value("${IMGFLOAT_AUDIT_DB_PATH:${IMGFLOAT_DB_PATH:#{null}}}")
private String auditDbPath;
@Value("${IMGFLOAT_INITIAL_TWITCH_USERNAME_SYSADMIN:#{null}}")
private String initialSysadmin;
@@ -76,6 +79,7 @@ public class SystemEnvironmentValidator {
checkString(twitchClientId, "TWITCH_CLIENT_ID", missing);
checkString(initialSysadmin, "IMGFLOAT_INITIAL_TWITCH_USERNAME_SYSADMIN", missing);
checkString(dbPath, "IMGFLOAT_DB_PATH", missing);
checkString(auditDbPath, "IMGFLOAT_AUDIT_DB_PATH", missing);
checkString(twitchClientSecret, "TWITCH_CLIENT_SECRET", missing);
checkString(assetsPath, "IMGFLOAT_ASSETS_PATH", missing);
checkString(previewsPath, "IMGFLOAT_PREVIEWS_PATH", missing);
@@ -93,6 +97,7 @@ public class SystemEnvironmentValidator {
log.info(" - SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE: {} ({} bytes)", springMaxFileSize, maxUploadBytes);
log.info(" - SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE: {} ({} bytes)", springMaxRequestSize, maxRequestBytes);
log.info(" - IMGFLOAT_DB_PATH: {}", dbPath);
log.info(" - IMGFLOAT_AUDIT_DB_PATH: {}", auditDbPath);
log.info(" - IMGFLOAT_INITIAL_TWITCH_USERNAME_SYSADMIN: {}", initialSysadmin);
log.info(" - IMGFLOAT_ASSETS_PATH: {}", assetsPath);
log.info(" - IMGFLOAT_PREVIEWS_PATH: {}", previewsPath);

View File

@@ -1,5 +1,6 @@
package dev.kruhlmann.imgfloat.model;
import dev.kruhlmann.imgfloat.audit.model.AuditLogEntry;
import java.time.Instant;
public record AuditLogEntryView(String id, String actor, String action, String details, Instant createdAt) {

View File

@@ -1,6 +1,6 @@
package dev.kruhlmann.imgfloat.repository;
package dev.kruhlmann.imgfloat.repository.audit;
import dev.kruhlmann.imgfloat.model.AuditLogEntry;
import dev.kruhlmann.imgfloat.audit.model.AuditLogEntry;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

View File

@@ -1,8 +1,8 @@
package dev.kruhlmann.imgfloat.service;
import dev.kruhlmann.imgfloat.model.AuditLogEntry;
import dev.kruhlmann.imgfloat.audit.model.AuditLogEntry;
import dev.kruhlmann.imgfloat.model.AuditLogEntryView;
import dev.kruhlmann.imgfloat.repository.AuditLogRepository;
import dev.kruhlmann.imgfloat.repository.audit.AuditLogRepository;
import dev.kruhlmann.imgfloat.util.LogSanitizer;
import java.util.List;
import java.util.Locale;

View File

@@ -20,7 +20,7 @@ spring:
thymeleaf:
cache: false
datasource:
url: jdbc:sqlite:${IMGFLOAT_DB_PATH}?busy_timeout=5000&journal_mode=WAL
url: jdbc:sqlite:${IMGFLOAT_DB_PATH:./imgfloat.db}?busy_timeout=5000&journal_mode=WAL
driver-class-name: org.sqlite.JDBC
hikari:
connection-init-sql: "PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;"
@@ -59,6 +59,20 @@ spring:
user-info-uri: https://api.twitch.tv/helix/users
user-name-attribute: login
imgfloat:
audit:
datasource:
url: jdbc:sqlite:${IMGFLOAT_AUDIT_DB_PATH:./imgfloat.audit.db}?busy_timeout=5000&journal_mode=WAL
driver-class-name: org.sqlite.JDBC
hikari:
connection-init-sql: "PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;"
maximum-pool-size: 1
minimum-idle: 1
flyway:
locations: classpath:db/audit
baseline-on-migrate: true
baseline-version: 1
management:
endpoints:
web:

View File

@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS channel_audit_log (
id TEXT PRIMARY KEY,
broadcaster TEXT NOT NULL,
actor TEXT,
action TEXT NOT NULL,
details TEXT,
created_at TIMESTAMP NOT NULL
);
CREATE INDEX IF NOT EXISTS channel_audit_log_broadcaster_idx ON channel_audit_log (broadcaster);
CREATE INDEX IF NOT EXISTS channel_audit_log_created_at_idx ON channel_audit_log (created_at);