Skip to content
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
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
}

group = 'com.flexcodelabs'
version = '0.0.3'
version = '0.0.4'
description = 'Flextuma App'

java {
Expand Down Expand Up @@ -44,6 +44,7 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat-runtime'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'com.h2database:h2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication(excludeName = {
"org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration" })
@ConfigurationPropertiesScan
@org.springframework.boot.persistence.autoconfigure.EntityScan(basePackages = "com.flexcodelabs.flextuma.core.entities")
@org.springframework.data.jpa.repository.config.EnableJpaRepositories(basePackages = "com.flexcodelabs.flextuma.core.repositories")
@org.springframework.data.jpa.repository.config.EnableJpaAuditing(auditorAwareRef = "auditorProvider")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
Expand All @@ -14,10 +16,16 @@
import java.io.IOException;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestLoggingFilter extends OncePerRequestFilter {

private static final Logger log = LoggerFactory.getLogger("FLEXTUMA");

@Override
protected boolean shouldNotFilterErrorDispatch() {
return false;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Expand All @@ -30,31 +38,47 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse

try {
filterChain.doFilter(request, response);
} catch (Exception ex) {
logRequest(request, response, fullUri, startTime, 500, ex);
throw ex;
} finally {
String username = getUsername();
long duration = System.currentTimeMillis() - startTime;
int status = response.getStatus();
// Only log in finally if we haven't already logged via the catch block
// OR we rely on the response status. Best is to extract the logging logic
// into a helper method.
if (request.getAttribute("REQUEST_LOGGED") == null) {
logRequest(request, response, fullUri, startTime, response.getStatus(), null);
}
}
}

private void logRequest(HttpServletRequest request, HttpServletResponse response, String fullUri, long startTime,
int statusOverride, Exception ex) {
request.setAttribute("REQUEST_LOGGED", true);
String username = getUsername();
long duration = System.currentTimeMillis() - startTime;

boolean isError = status >= 400;
int status = statusOverride > 0 ? statusOverride : response.getStatus();
boolean isError = status >= 400 || ex != null;

String logColor = isError ? "\u001B[31m" : "\u001B[32m";
String reset = "\u001B[0m";
String logColor = isError ? "\u001B[31m" : "\u001B[32m";
String reset = "\u001B[0m";

String statusLog = logColor + (isError ? "ERROR" : "LOG") + reset;
String userInfo = "\u001B[33m[" + username + "]\u001B[0m";
String coloredMethod = logColor + request.getMethod() + reset;
String coloredUri = logColor + fullUri + reset;
String statusLog = logColor + (isError ? "ERROR" : "LOG") + reset;
String userInfo = "\u001B[33m[" + username + "]\u001B[0m";
String coloredMethod = logColor + request.getMethod() + reset;
String coloredUri = logColor + fullUri + reset;

org.slf4j.MDC.put("username", username);
try {
if (isError) {
log.error("{} {} {} {} {}ms", statusLog, userInfo, coloredMethod, coloredUri, duration);
} else {
log.info("{} {} {} {} {}ms", statusLog, userInfo, coloredMethod, coloredUri, duration);
}
} finally {
org.slf4j.MDC.remove("username");
org.slf4j.MDC.put("username", username);
try {
if (isError) {
log.error("{} {} {} {} {}ms - Status: {}", statusLog, userInfo, coloredMethod, coloredUri, duration,
status);
} else {
log.info("{} {} {} {} {}ms - Status: {}", statusLog, userInfo, coloredMethod, coloredUri, duration,
status);
}
} finally {
org.slf4j.MDC.remove("username");
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package com.flexcodelabs.flextuma.core.config.auth;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.validation.annotation.Validated;

import lombok.Getter;
import lombok.Setter;
import java.time.Duration;

@Configuration
@Validated
@ConfigurationProperties(prefix = "auth.cookie")
@Getter
@Setter
public class AuthCookieProperties {
private String name = "SESSION";
private long maxAge = 3600;
private boolean secure = true;
private String sameSite = "Lax";
private String path = "/";
public record AuthCookieProperties(
@NotBlank @DefaultValue("SESSION") String name,

@NotNull @DefaultValue("3600s") Duration maxAge,

@DefaultValue("true") boolean secure,

@DefaultValue("true") boolean httpOnly,

@NotBlank @DefaultValue("Strict") String sameSite,

@NotBlank @DefaultValue("/") String path) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class Role extends BaseEntity {

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "userprivilege", joinColumns = @JoinColumn(name = "role", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "privilege", referencedColumnName = "id"))
private Set<Privilege> privileges;
private Set<Privilege> privileges = new java.util.HashSet<>();

@PrePersist
public void ensureSystemValue() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public class User extends BaseEntity {

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "userrole", joinColumns = @JoinColumn(name = "owner", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "role", referencedColumnName = "id"))
private Set<Role> roles;
private Set<Role> roles = new java.util.HashSet<>();

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "organisation", referencedColumnName = "id", nullable = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ public class SmsCampaign extends Owner {
@Column(nullable = false)
private String name;

@Column(columnDefinition = "TEXT", nullable = false)
private String content;
@Column(columnDefinition = "TEXT", nullable = true)
private String description;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "template_id")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
public class SmsConnector extends Owner {

public static final String PLURAL = "connectors";
public static final String NAME_PLURAL = "SmsConnectors";
public static final String NAME_SINGULAR = "SmsConnector";
public static final String NAME_PLURAL = "SMS Connectors";
public static final String NAME_SINGULAR = "SMS Connector";

public static final String ALL = "ALL";
public static final String READ = ALL;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,84 @@ public class GlobalExceptionHandler {

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex) {
String message = "The request body is missing or the JSON format is invalid.";
String defaultMessage = "The request body is missing or the JSON format is invalid.";
if (ex.getMessage() != null && ex.getMessage().contains("Required request body is missing")) {
message = "Required request body is missing. Please provide the required data in the request body.";
defaultMessage = "Required request body is missing. Please provide the required data in the request body.";
}

Throwable mostSpecificCause = ex.getMostSpecificCause();
String detailedMessage = mostSpecificCause != null ? mostSpecificCause.getMessage() : null;

String message = defaultMessage;

if (detailedMessage != null && !detailedMessage.isBlank()) {
String enumMessage = tryBuildEnumErrorMessage(detailedMessage);
if (enumMessage != null) {
message = enumMessage;
} else {
message = defaultMessage + " Details: " + detailedMessage;
}
}

return buildResponse(message, HttpStatus.BAD_REQUEST);
}

private String tryBuildEnumErrorMessage(String detailedMessage) {
// Example detailedMessage:
// "JSON parse error: Cannot deserialize value of type
// `com.flexcodelabs.flextuma.core.enums.SmsCampaignStatus` from String \"Running\":
// not one of the values accepted for Enum class: [CANCELLED, COMPLETED, SCHEDULED,
// DRAFT, PROCESSING]"

if (!detailedMessage.contains("not one of the values accepted for Enum class")) {
return null;
}

String enumType = null;
String invalidValue = null;
String allowedValues = null;

int typeStart = detailedMessage.indexOf("`");
int typeEnd = detailedMessage.indexOf("`", typeStart + 1);
if (typeStart != -1 && typeEnd != -1 && typeEnd > typeStart + 1) {
enumType = detailedMessage.substring(typeStart + 1, typeEnd);
}

String fromStringToken = "from String \"";
int valueStart = detailedMessage.indexOf(fromStringToken);
if (valueStart != -1) {
valueStart += fromStringToken.length();
int valueEnd = detailedMessage.indexOf("\"", valueStart);
if (valueEnd != -1 && valueEnd > valueStart) {
invalidValue = detailedMessage.substring(valueStart, valueEnd);
}
}

int valuesStart = detailedMessage.indexOf('[');
int valuesEnd = detailedMessage.indexOf(']', valuesStart + 1);
if (valuesStart != -1 && valuesEnd != -1 && valuesEnd > valuesStart + 1) {
allowedValues = detailedMessage.substring(valuesStart + 1, valuesEnd);
}

if (allowedValues == null) {
return null;
}

StringBuilder builder = new StringBuilder("Invalid value");
if (invalidValue != null) {
builder.append(" '").append(invalidValue).append("'");
}
builder.append(" for enum");
if (enumType != null) {
int lastDot = enumType.lastIndexOf('.');
String simpleName = lastDot != -1 ? enumType.substring(lastDot + 1) : enumType;
builder.append(" ").append(simpleName);
}
builder.append(". Allowed values are: [").append(allowedValues).append("].");

return builder.toString();
}

@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Object> handleResponseStatusException(ResponseStatusException ex) {
HttpStatus status = HttpStatus.resolve(ex.getStatusCode().value());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ public Optional<User> getCurrentAuditor() {
}

Object principal = authentication.getPrincipal();
String username = null;

if (principal instanceof UserDetails userDetails) {
String username = userDetails.getUsername();
username = userDetails.getUsername();
} else if (principal instanceof String principalString) {
username = principalString;
}

if (username != null) {
FlushModeType originalFlushMode = entityManager.getFlushMode();
try {
entityManager.setFlushMode(FlushModeType.COMMIT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ public PasswordEncoder passwordEncoder() {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("SESSION");
serializer.setUseHttpOnlyCookie(true);
serializer.setSameSite("Lax");
return serializer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,9 @@ public Map<String, String> delete(UUID id) {

validateDelete(entity);

getRepository().deleteById(id);
getRepository().delete(entity);
entityManager.flush();
entityManager.clear();

onPostDelete(id);
eventPublisher.publishEvent(new EntityEvent<>(this, entity, EntityEvent.EntityEventType.DELETED));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,22 @@ public ResponseCookie createAuthCookie() {

String token = TokenGenerator.generateSecureToken(32);

return ResponseCookie.from(properties.getName(), token)
.httpOnly(true)
.secure(properties.isSecure())
.path(properties.getPath())
.maxAge(properties.getMaxAge())
.sameSite(properties.getSameSite())
return ResponseCookie.from(properties.name(), token)
.httpOnly(properties.httpOnly())
.secure(properties.secure())
.path(properties.path())
.maxAge(properties.maxAge())
.sameSite(properties.sameSite())
.build();
}

public ResponseCookie deleteAuthCookie() {
return ResponseCookie.from(properties.getName(), "")
.httpOnly(true)
.secure(properties.isSecure())
.path(properties.getPath())
return ResponseCookie.from(properties.name(), "")
.httpOnly(properties.httpOnly())
.secure(properties.secure())
.path(properties.path())
.maxAge(0)
.sameSite(properties.sameSite())
.build();
}
}
Loading