Initial Commit

This commit is contained in:
Braydon 2024-09-13 20:32:08 -04:00
commit 1de8a8df8c
14 changed files with 454 additions and 0 deletions

29
.gitignore vendored Normal file

@ -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

26
Dockerfile Normal file

@ -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

0
README.md Normal file

81
pom.xml Normal file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/>
</parent>
<!-- Project Details -->
<groupId>me.braydon</groupId>
<artifactId>API</artifactId>
<version>1.0.0</version>
<!-- Properties -->
<properties>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- Build Config -->
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- Spring -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>build-info</id>
<goals>
<goal>build-info</goal>
</goals>
<configuration>
<additionalProperties>
<description>${project.description}</description>
</additionalProperties>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<!-- Dependencies -->
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Libraries -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
<!-- Error Reporting & Metrics -->
<!-- <dependency>-->
<!-- <groupId>io.sentry</groupId>-->
<!-- <artifactId>sentry-spring-boot-starter-jakarta</artifactId>-->
<!-- <version>8.0.0-alpha.4</version>-->
<!-- <scope>compile</scope>-->
<!-- </dependency>-->
<dependency>
<groupId>org.questdb</groupId>
<artifactId>questdb</artifactId>
<version>8.1.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

@ -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
}
}

@ -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"));
}
}

@ -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;
}
}

@ -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<ErrorResponse> onError(@NonNull HttpServletRequest request) {
Map<String, Object> 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);
}
}

@ -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);
}
}

@ -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<Object> {
@Override
public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType,
@NonNull Class<? extends HttpMessageConverter<?>> 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;
}
}

@ -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();
}
}

@ -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;
}

@ -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"

@ -0,0 +1,12 @@
_____ _ _____ _____
| __ \ | | /\ /\ | __ \_ _|
| |__) | _| |___ ___ / \ _ __ _ __ / \ | |__) || |
| ___/ | | | / __|/ _ \ / /\ \ | '_ \| '_ \ / /\ \ | ___/ | |
| | | |_| | \__ \ __// ____ \| |_) | |_) | / ____ \| | _| |_
|_| \__,_|_|___/\___/_/ \_\ .__/| .__/ /_/ \_\_| |_____|
| | | |
|_| |_|
| API Version - v${application.version}
| Spring Version - ${spring-boot.formatted-version}
______________________________________________________________________