commit 1de8a8df8c6b7a09e10a3580012cc381ec40c114 Author: Rainnny7 Date: Fri Sep 13 20:32:08 2024 -0400 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80ab21c --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +*.class +*.log +*.ctxt +.mtj.tmp/ +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar +hs_err_pid* +replay_pid* +.idea +cmake-build-*/ +.idea/**/mongoSettings.xml +*.iws +out/ +build/ +work/ +target/ +.idea_modules/ +atlassian-ide-plugin.xml +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +git.properties +pom.xml.versionsBackup \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a1e7fae --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# Stage 1: Build the application +FROM maven:3.9.9-eclipse-temurin-17-alpine AS builder + +# Set the working directory +WORKDIR /home/container + +# Copy the current directory contents into the container at /home/container +COPY . . + +# Build the jar +RUN mvn package -T2C -q -Dmaven.test.skip -DskipTests + +# Stage 2: Create the final lightweight image +FROM eclipse-temurin:17.0.12_7-jre-focal + +# Set the working directory +WORKDIR /home/container + +# Copy the built jar file from the builder stage +COPY --from=builder /home/container/target/API.jar . + +# We're running in production +ENV APP_ENV "production" + +# Run the jar file +CMD java -jar API.jar -Djava.awt.headless=true \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..37c14d0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + + + me.braydon + API + 1.0.0 + + + + 17 + ${java.version} + ${java.version} + UTF-8 + + + + + ${project.artifactId} + + + + org.springframework.boot + spring-boot-maven-plugin + + + build-info + + build-info + + + + ${project.description} + + + + + + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.projectlombok + lombok + 1.18.34 + provided + + + + + + + + + + + org.questdb + questdb + 8.1.1 + compile + + + \ No newline at end of file diff --git a/src/main/java/cc/pulseapp/api/PulseAPI.java b/src/main/java/cc/pulseapp/api/PulseAPI.java new file mode 100644 index 0000000..d35e733 --- /dev/null +++ b/src/main/java/cc/pulseapp/api/PulseAPI.java @@ -0,0 +1,34 @@ +package cc.pulseapp.api; + +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Objects; + +/** + * @author Braydon + */ +@SpringBootApplication +@Log4j2(topic = "PulseApp") +public class PulseAPI { + @SneakyThrows + public static void main(@NonNull String[] args) { + // Load the application.yml configuration file + File config = new File("application.yml"); + if (!config.exists()) { // Saving the default config if it doesn't exist locally + Files.copy(Objects.requireNonNull(PulseAPI.class.getResourceAsStream("/application.yml")), config.toPath(), StandardCopyOption.REPLACE_EXISTING); + log.info("Saved the default configuration to '{}', please re-launch the application", + config.getAbsolutePath() + ); + return; + } + log.info("Found configuration at '{}'", config.getAbsolutePath()); + SpringApplication.run(PulseAPI.class, args); // Start the app + } +} \ No newline at end of file diff --git a/src/main/java/cc/pulseapp/api/common/EnvironmentUtils.java b/src/main/java/cc/pulseapp/api/common/EnvironmentUtils.java new file mode 100644 index 0000000..5c4e061 --- /dev/null +++ b/src/main/java/cc/pulseapp/api/common/EnvironmentUtils.java @@ -0,0 +1,20 @@ +package cc.pulseapp.api.common; + +import lombok.Getter; +import lombok.experimental.UtilityClass; + +/** + * @author Braydon + */ +@UtilityClass +public final class EnvironmentUtils { + /** + * Is the app running in a production environment? + */ + @Getter private static final boolean production; + + static { + String appEnv = System.getenv("APP_ENV"); + production = appEnv != null && (appEnv.equals("production")); + } +} \ No newline at end of file diff --git a/src/main/java/cc/pulseapp/api/common/IPUtils.java b/src/main/java/cc/pulseapp/api/common/IPUtils.java new file mode 100644 index 0000000..785254a --- /dev/null +++ b/src/main/java/cc/pulseapp/api/common/IPUtils.java @@ -0,0 +1,44 @@ +package cc.pulseapp.api.common; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.NonNull; +import lombok.experimental.UtilityClass; + +/** + * @author Braydon + */ +@UtilityClass +public final class IPUtils { + private static final String[] IP_HEADERS = new String[] { + "CF-Connecting-IP", + "X-Forwarded-For" + }; + + /** + * Get the real IP from the given request. + * + * @param request the request + * @return the real IP + */ + @NonNull + public static String getRealIp(@NonNull HttpServletRequest request) { + String ip = request.getRemoteAddr(); + for (String headerName : IP_HEADERS) { + String header = request.getHeader(headerName); + if (header == null) { + continue; + } + if (!header.contains(",")) { // Handle single IP + ip = header; + break; + } + // Handle multiple IPs + String[] ips = header.split(","); + for (String ipHeader : ips) { + ip = ipHeader; + break; + } + } + return ip; + } +} diff --git a/src/main/java/cc/pulseapp/api/exception/ExceptionController.java b/src/main/java/cc/pulseapp/api/exception/ExceptionController.java new file mode 100644 index 0000000..95e1a2d --- /dev/null +++ b/src/main/java/cc/pulseapp/api/exception/ExceptionController.java @@ -0,0 +1,38 @@ +package cc.pulseapp.api.exception; + +import cc.pulseapp.api.model.ErrorResponse; +import jakarta.servlet.http.HttpServletRequest; +import lombok.NonNull; +import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController; +import org.springframework.boot.web.error.ErrorAttributeOptions; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +/** + * The route to handle errors for this app. + * + * @author Braydon + */ +@RestController +@RequestMapping(value = "/error", produces = MediaType.APPLICATION_JSON_VALUE) +public final class ExceptionController extends AbstractErrorController { + public ExceptionController(@NonNull ErrorAttributes errorAttributes) { + super(errorAttributes); + } + + @RequestMapping @ResponseBody @NonNull + public ResponseEntity onError(@NonNull HttpServletRequest request) { + Map error = getErrorAttributes(request, ErrorAttributeOptions.of( + ErrorAttributeOptions.Include.MESSAGE + )); + HttpStatus status = getStatus(request); // The status code + return new ResponseEntity<>(new ErrorResponse(status, (String) error.get("message")), status); + } +} \ No newline at end of file diff --git a/src/main/java/cc/pulseapp/api/exception/impl/BadRequestException.java b/src/main/java/cc/pulseapp/api/exception/impl/BadRequestException.java new file mode 100644 index 0000000..5ccf25d --- /dev/null +++ b/src/main/java/cc/pulseapp/api/exception/impl/BadRequestException.java @@ -0,0 +1,18 @@ +package cc.pulseapp.api.exception.impl; + +import lombok.NonNull; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * This exception is raised + * when a bad request is made. + * + * @author Braydon + */ +@ResponseStatus(HttpStatus.BAD_REQUEST) +public final class BadRequestException extends RuntimeException { + public BadRequestException(@NonNull String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/cc/pulseapp/api/log/RequestLogger.java b/src/main/java/cc/pulseapp/api/log/RequestLogger.java new file mode 100644 index 0000000..803c3d7 --- /dev/null +++ b/src/main/java/cc/pulseapp/api/log/RequestLogger.java @@ -0,0 +1,47 @@ +package cc.pulseapp.api.log; + +import cc.pulseapp.api.common.IPUtils; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +/** + * Responsible for logging request and + * response transactions to the terminal. + * + * @author Braydon + */ +@ControllerAdvice +@Slf4j(topic = "Req/Res Transaction") +public class RequestLogger implements ResponseBodyAdvice { + @Override + public boolean supports(@NonNull MethodParameter returnType, @NonNull Class> converterType) { + return true; + } + + @Override + public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType, + @NonNull Class> selectedConverterType, + @NonNull ServerHttpRequest rawRequest, @NonNull ServerHttpResponse rawResponse) { + HttpServletRequest request = ((ServletServerHttpRequest) rawRequest).getServletRequest(); + HttpServletResponse response = ((ServletServerHttpResponse) rawResponse).getServletResponse(); + + // Get the request ip ip + String ip = IPUtils.getRealIp(request); + + log.info("%s | %s %s %s %s".formatted( + ip, request.getMethod(), request.getRequestURI(), request.getProtocol(), response.getStatus() + )); + return body; + } +} \ No newline at end of file diff --git a/src/main/java/cc/pulseapp/api/model/ErrorResponse.java b/src/main/java/cc/pulseapp/api/model/ErrorResponse.java new file mode 100644 index 0000000..faa12fe --- /dev/null +++ b/src/main/java/cc/pulseapp/api/model/ErrorResponse.java @@ -0,0 +1,43 @@ +package cc.pulseapp.api.model; + +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; +import org.springframework.http.HttpStatus; + +import java.util.Date; + +/** + * A basic response model. + * + * @author Braydon + */ +@Getter @ToString +public class ErrorResponse { + /** + * The status code of this error. + */ + @NonNull private final HttpStatus status; + + /** + * The HTTP code of this error. + */ + private final int code; + + /** + * The message of this error. + */ + @NonNull private final String message; + + /** + * The timestamp this error occurred. + */ + @NonNull private final Date timestamp; + + public ErrorResponse(@NonNull HttpStatus status, @NonNull String message) { + this.status = status; + this.message = message; + this.code = status.value(); + timestamp = new Date(); + } +} \ No newline at end of file diff --git a/src/main/java/cc/pulseapp/api/model/User.java b/src/main/java/cc/pulseapp/api/model/User.java new file mode 100644 index 0000000..766120a --- /dev/null +++ b/src/main/java/cc/pulseapp/api/model/User.java @@ -0,0 +1,36 @@ +package cc.pulseapp.api.model; + +import lombok.*; + +import java.util.Date; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString +public final class User { + /** + * The snowflake id of this user. + */ + @EqualsAndHashCode.Include private final long id; + + /** + * This user's username. + */ + @NonNull private final String username; + + /** + * The password for this user. + */ + @NonNull private final String password; + + /** + * The salt for this user's password. + */ + @NonNull private final String passwordSalt; + + /** + * The date this user last logged in. + */ + @NonNull private final Date lastLogin; +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..978cd76 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,26 @@ +# Server Configuration +server: + address: 0.0.0.0 + port: 7500 + +# Log Configuration +logging: + file: + path: "./logs" + +# Sentry Configuration +sentry: + dsn: "CHANGE_ME" + tracesSampleRate: 1.0 + environment: "development" + +# QuestDB Configuration (Metrics) +questdb: + enabled: false + uri: "http::addr=localhost:9000;username=tether;password=p4$$w0rd;auto_flush_interval=5000;" + +# Spring Configuration +spring: + # Ignore + banner: + location: "classpath:banner.txt" \ No newline at end of file diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..e61122e --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,12 @@ + _____ _ _____ _____ + | __ \ | | /\ /\ | __ \_ _| + | |__) | _| |___ ___ / \ _ __ _ __ / \ | |__) || | + | ___/ | | | / __|/ _ \ / /\ \ | '_ \| '_ \ / /\ \ | ___/ | | + | | | |_| | \__ \ __// ____ \| |_) | |_) | / ____ \| | _| |_ + |_| \__,_|_|___/\___/_/ \_\ .__/| .__/ /_/ \_\_| |_____| + | | | | + |_| |_| + + | API Version - v${application.version} + | Spring Version - ${spring-boot.formatted-version} +______________________________________________________________________