Add ServiceOnly annotation.
This commit is contained in:
parent
52be976286
commit
ae8595db07
|
@ -0,0 +1,39 @@
|
|||
package nl.andrewlalis.gymboard_api.config;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* An interceptor that checks that requests to endpoints annotated with
|
||||
* {@link ServiceOnly} have a valid service secret header value.
|
||||
*/
|
||||
@Component
|
||||
public class ServiceAccessInterceptor implements HandlerInterceptor {
|
||||
public static final String HEADER_NAME = "X-Gymboard-Service-Secret";
|
||||
|
||||
@Value("${app.service-secret}")
|
||||
private String serviceSecret;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
Method handlerMethod = ((HandlerMethod) handler).getMethod();
|
||||
Class<?> handlerClass = handlerMethod.getDeclaringClass();
|
||||
|
||||
ServiceOnly methodAnnotation = handlerMethod.getAnnotation(ServiceOnly.class);
|
||||
ServiceOnly classAnnotation = handlerClass.getAnnotation(ServiceOnly.class);
|
||||
if (methodAnnotation != null || classAnnotation != null) {
|
||||
String secret = request.getHeader(HEADER_NAME);
|
||||
if (secret == null || !secret.trim().equals(serviceSecret)) {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package nl.andrewlalis.gymboard_api.config;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation that can be applied to a controller or controller method to
|
||||
* restrict access to only requests from another service that provide a
|
||||
* legitimate service secret.
|
||||
* @see ServiceAccessInterceptor
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ServiceOnly {}
|
|
@ -22,9 +22,11 @@ public class WebComponents {
|
|||
|
||||
@Value("${app.cdn-origin}")
|
||||
private String cdnOrigin;
|
||||
@Value("${app.cdn-secret}")
|
||||
private String cdnSecret;
|
||||
|
||||
@Bean
|
||||
public CdnClient cdnClient() {
|
||||
return new CdnClient(cdnOrigin);
|
||||
return new CdnClient(cdnOrigin, cdnSecret);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package nl.andrewlalis.gymboard_api.domains.api.service.cdn_client;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import nl.andrewlalis.gymboard_api.config.ServiceAccessInterceptor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
@ -13,17 +14,19 @@ import java.time.Duration;
|
|||
public class CdnClient {
|
||||
private final HttpClient httpClient;
|
||||
private final String baseUrl;
|
||||
private final String cdnSecret;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public final UploadsClient uploads;
|
||||
public final FilesClient files;
|
||||
|
||||
public CdnClient(String baseUrl) {
|
||||
public CdnClient(String baseUrl, String cdnSecret) {
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(3))
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.build();
|
||||
this.baseUrl = baseUrl;
|
||||
this.cdnSecret = cdnSecret;
|
||||
this.objectMapper = new ObjectMapper();
|
||||
this.uploads = new UploadsClient(this);
|
||||
this.files = new FilesClient(this);
|
||||
|
@ -32,6 +35,7 @@ public class CdnClient {
|
|||
public <T> T get(String urlPath, Class<T> responseType) throws IOException, InterruptedException {
|
||||
HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + urlPath))
|
||||
.GET()
|
||||
.header(ServiceAccessInterceptor.HEADER_NAME, cdnSecret)
|
||||
.build();
|
||||
HttpResponse<String> response = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() == 200) {
|
||||
|
@ -48,6 +52,7 @@ public class CdnClient {
|
|||
.POST(HttpRequest.BodyPublishers.ofFile(filePath))
|
||||
.header("Content-Type", contentType)
|
||||
.header("X-Gymboard-Filename", filePath.getFileName().toString())
|
||||
.header(ServiceAccessInterceptor.HEADER_NAME, cdnSecret)
|
||||
.build();
|
||||
HttpResponse<String> response = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
||||
return objectMapper.readValue(response.body(), responseType);
|
||||
|
@ -56,6 +61,7 @@ public class CdnClient {
|
|||
public void post(String urlPath) throws IOException, InterruptedException {
|
||||
HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + urlPath))
|
||||
.POST(HttpRequest.BodyPublishers.noBody())
|
||||
.header(ServiceAccessInterceptor.HEADER_NAME, cdnSecret)
|
||||
.build();
|
||||
HttpResponse<Void> response = httpClient.send(req, HttpResponse.BodyHandlers.discarding());
|
||||
if (response.statusCode() != 200) {
|
||||
|
@ -65,7 +71,9 @@ public class CdnClient {
|
|||
|
||||
public void delete(String urlPath) throws IOException, InterruptedException {
|
||||
HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + urlPath))
|
||||
.DELETE().build();
|
||||
.DELETE()
|
||||
.header(ServiceAccessInterceptor.HEADER_NAME, cdnSecret)
|
||||
.build();
|
||||
HttpResponse<Void> response = httpClient.send(req, HttpResponse.BodyHandlers.discarding());
|
||||
if (response.statusCode() >= 400) {
|
||||
throw new IOException("Request failed with code " + response.statusCode());
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.domains.submission.controller;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.config.ServiceOnly;
|
||||
import nl.andrewlalis.gymboard_api.domains.submission.dto.SubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.VideoProcessingCompletePayload;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService;
|
||||
|
@ -28,9 +29,8 @@ public class SubmissionController {
|
|||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping(path = "/video-processing-complete")
|
||||
@PostMapping(path = "/video-processing-complete") @ServiceOnly
|
||||
public ResponseEntity<Void> handleVideoProcessingComplete(@RequestBody VideoProcessingCompletePayload taskStatus) {
|
||||
// TODO: Validate that the request came ONLY from the CDN service.
|
||||
submissionService.handleVideoProcessingComplete(taskStatus);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import nl.andrewlalis.gymboard_api.domains.submission.model.SubmissionProperties
|
|||
import nl.andrewlalis.gymboard_api.util.ULID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.util.Pair;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
@ -37,16 +36,15 @@ public class SampleSubmissionGenerator implements SampleDataGenerator {
|
|||
private final ExerciseRepository exerciseRepository;
|
||||
private final SubmissionRepository submissionRepository;
|
||||
private final ULID ulid;
|
||||
private final CdnClient cdnClient;
|
||||
|
||||
@Value("${app.cdn-origin}")
|
||||
private String cdnOrigin;
|
||||
|
||||
public SampleSubmissionGenerator(GymRepository gymRepository, UserRepository userRepository, ExerciseRepository exerciseRepository, SubmissionRepository submissionRepository, ULID ulid) {
|
||||
public SampleSubmissionGenerator(GymRepository gymRepository, UserRepository userRepository, ExerciseRepository exerciseRepository, SubmissionRepository submissionRepository, ULID ulid, CdnClient cdnClient) {
|
||||
this.gymRepository = gymRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.exerciseRepository = exerciseRepository;
|
||||
this.submissionRepository = submissionRepository;
|
||||
this.ulid = ulid;
|
||||
this.cdnClient = cdnClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -143,8 +141,6 @@ public class SampleSubmissionGenerator implements SampleDataGenerator {
|
|||
* @throws Exception If an error occurs.
|
||||
*/
|
||||
private Map<Long, Pair<String, String>> generateUploads() throws Exception {
|
||||
final CdnClient cdnClient = new CdnClient(cdnOrigin);
|
||||
|
||||
List<Long> taskIds = new ArrayList<>();
|
||||
taskIds.add(cdnClient.uploads.uploadVideo(Path.of("sample_data", "sample_video_curl.mp4"), "video/mp4"));
|
||||
taskIds.add(cdnClient.uploads.uploadVideo(Path.of("sample_data", "sample_video_ohp.mp4"), "video/mp4"));
|
||||
|
|
|
@ -15,7 +15,9 @@ spring.mail.protocol=smtp
|
|||
spring.mail.properties.mail.smtp.timeout=10000
|
||||
|
||||
app.auth.private-key-location=./private_key.der
|
||||
app.service-secret=testing
|
||||
app.web-origin=http://localhost:9000
|
||||
app.cdn-origin=http://localhost:8082
|
||||
app.cdn-secret=testing
|
||||
|
||||
#logging.level.root=DEBUG
|
||||
|
|
|
@ -14,6 +14,8 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
|||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.Executor;
|
||||
|
@ -21,12 +23,23 @@ import java.util.concurrent.Executors;
|
|||
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class Config {
|
||||
public class Config implements WebMvcConfigurer {
|
||||
@Value("${app.web-origin}")
|
||||
private String webOrigin;
|
||||
@Value("${app.api-origin}")
|
||||
private String apiOrigin;
|
||||
|
||||
private final ServiceAccessInterceptor serviceAccessInterceptor;
|
||||
|
||||
public Config(ServiceAccessInterceptor serviceAccessInterceptor) {
|
||||
this.serviceAccessInterceptor = serviceAccessInterceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(serviceAccessInterceptor);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsFilter corsFilter() {
|
||||
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package nl.andrewlalis.gymboardcdn;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* An interceptor that checks that requests to endpoints annotated with
|
||||
* {@link ServiceOnly} have a valid service secret header value.
|
||||
*/
|
||||
@Component
|
||||
public class ServiceAccessInterceptor implements HandlerInterceptor {
|
||||
private static final String HEADER_NAME = "X-Gymboard-Service-Secret";
|
||||
|
||||
@Value("${app.service-secret}")
|
||||
private String serviceSecret;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
Method handlerMethod = ((HandlerMethod) handler).getMethod();
|
||||
Class<?> handlerClass = handlerMethod.getDeclaringClass();
|
||||
|
||||
ServiceOnly methodAnnotation = handlerMethod.getAnnotation(ServiceOnly.class);
|
||||
ServiceOnly classAnnotation = handlerClass.getAnnotation(ServiceOnly.class);
|
||||
if (methodAnnotation != null || classAnnotation != null) {
|
||||
String secret = request.getHeader(HEADER_NAME);
|
||||
if (secret == null || !secret.trim().equals(serviceSecret)) {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package nl.andrewlalis.gymboardcdn;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation that can be applied to a controller or controller method to
|
||||
* restrict access to only requests from another service that provide a
|
||||
* legitimate service secret.
|
||||
* @see ServiceAccessInterceptor
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ServiceOnly {}
|
|
@ -1,6 +1,7 @@
|
|||
package nl.andrewlalis.gymboardcdn.files;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import nl.andrewlalis.gymboardcdn.ServiceOnly;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
@ -37,9 +38,8 @@ public class FileController {
|
|||
}
|
||||
}
|
||||
|
||||
@DeleteMapping(path = "/files/{id}")
|
||||
@DeleteMapping(path = "/files/{id}") @ServiceOnly
|
||||
public void deleteFile(@PathVariable String id) {
|
||||
// TODO: Secure this so only API can access it!
|
||||
try {
|
||||
fileStorageService.delete(id);
|
||||
} catch (IOException e) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package nl.andrewlalis.gymboardcdn.uploads.api;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import nl.andrewlalis.gymboardcdn.ServiceOnly;
|
||||
import nl.andrewlalis.gymboardcdn.uploads.service.UploadService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
@ -20,7 +21,7 @@ public class UploadController {
|
|||
return uploadService.processableVideoUpload(request);
|
||||
}
|
||||
|
||||
@PostMapping(path = "/uploads/video/{taskId}/start")
|
||||
@PostMapping(path = "/uploads/video/{taskId}/start") @ServiceOnly
|
||||
public void startVideoProcessing(@PathVariable long taskId) {
|
||||
uploadService.startVideoProcessing(taskId);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package nl.andrewlalis.gymboardcdn.uploads.model;
|
||||
|
||||
public record VideoProcessingTaskStatusUpdate(
|
||||
long taskId,
|
||||
String status,
|
||||
String videoFileId,
|
||||
String thumbnailFileId
|
||||
) {
|
||||
public VideoProcessingTaskStatusUpdate(VideoProcessingTask task) {
|
||||
this(task.getId(), task.getStatus().name(), task.getVideoFileId(), task.getThumbnailFileId());
|
||||
}
|
||||
}
|
|
@ -2,11 +2,11 @@ package nl.andrewlalis.gymboardcdn.uploads.service;
|
|||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import nl.andrewlalis.gymboardcdn.files.FileMetadata;
|
||||
import nl.andrewlalis.gymboardcdn.files.FileStorageService;
|
||||
import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTask;
|
||||
import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTaskRepository;
|
||||
import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTaskStatusUpdate;
|
||||
import nl.andrewlalis.gymboardcdn.uploads.service.process.ThumbnailGenerator;
|
||||
import nl.andrewlalis.gymboardcdn.uploads.service.process.VideoProcessor;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -42,6 +42,9 @@ public class VideoProcessingService {
|
|||
@Value("${app.api-origin}")
|
||||
private String apiOrigin;
|
||||
|
||||
@Value("${app.api-secret}")
|
||||
private String apiSecret;
|
||||
|
||||
public VideoProcessingService(Executor videoProcessingExecutor,
|
||||
VideoProcessingTaskRepository taskRepo,
|
||||
FileStorageService fileStorageService,
|
||||
|
@ -80,15 +83,18 @@ public class VideoProcessingService {
|
|||
for (var task : oldTasks) {
|
||||
if (task.getStatus() == VideoProcessingTask.Status.COMPLETED) {
|
||||
log.info("Deleting completed task {}.", task.getId());
|
||||
deleteAllTaskFiles(task);
|
||||
taskRepo.delete(task);
|
||||
} else if (task.getStatus() == VideoProcessingTask.Status.FAILED) {
|
||||
log.info("Deleting failed task {}.", task.getId());
|
||||
taskRepo.delete(task);
|
||||
} else if (task.getStatus() == VideoProcessingTask.Status.IN_PROGRESS) {
|
||||
log.info("Task {} was in progress for too long; deleting.", task.getId());
|
||||
deleteAllTaskFiles(task);
|
||||
taskRepo.delete(task);
|
||||
} else if (task.getStatus() == VideoProcessingTask.Status.WAITING) {
|
||||
log.info("Task {} was waiting for too long; deleting.", task.getId());
|
||||
deleteAllTaskFiles(task);
|
||||
taskRepo.delete(task);
|
||||
}
|
||||
}
|
||||
|
@ -156,32 +162,21 @@ public class VideoProcessingService {
|
|||
log.error("Failed to copy processed video to final storage location.", e);
|
||||
updateTask(task, VideoProcessingTask.Status.FAILED);
|
||||
} finally {
|
||||
try {
|
||||
fileStorageService.delete(task.getUploadFileId());
|
||||
Files.deleteIfExists(rawUploadFile);
|
||||
Files.deleteIfExists(videoFile);
|
||||
Files.deleteIfExists(thumbnailFile);
|
||||
} catch (IOException e) {
|
||||
log.error("Couldn't delete temporary output files for uploaded video {}", uploadFile);
|
||||
e.printStackTrace();
|
||||
}
|
||||
deleteAllTaskFiles(task);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an update message to the Gymboard API when a task finishes its
|
||||
* processing.
|
||||
* processing. Note that Gymboard API will also eventually poll the CDN's
|
||||
* own API to get task status if we fail to send it, so there's some
|
||||
* redundancy built-in.
|
||||
* @param task The task to send.
|
||||
*/
|
||||
private void sendTaskCompleteToApi(VideoProcessingTask task) {
|
||||
ObjectNode obj = objectMapper.createObjectNode();
|
||||
obj.put("taskId", task.getId());
|
||||
obj.put("status", task.getStatus().name());
|
||||
obj.put("videoFileId", task.getVideoFileId());
|
||||
obj.put("thumbnailFileId", task.getThumbnailFileId());
|
||||
String json;
|
||||
try {
|
||||
json = objectMapper.writeValueAsString(obj);
|
||||
json = objectMapper.writeValueAsString(new VideoProcessingTaskStatusUpdate(task));
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("JSON error while sending task data to API for task " + task.getId(), e);
|
||||
return;
|
||||
|
@ -189,6 +184,7 @@ public class VideoProcessingService {
|
|||
HttpClient httpClient = HttpClient.newBuilder().build();
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(apiOrigin + "/submissions/video-processing-complete"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("X-Gymboard-Service-Secret", apiSecret)
|
||||
.timeout(Duration.ofSeconds(3))
|
||||
.POST(HttpRequest.BodyPublishers.ofString(json))
|
||||
.build();
|
||||
|
@ -201,4 +197,33 @@ public class VideoProcessingService {
|
|||
log.error("Failed to send HTTP request to API.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to delete all temporary files related to a task's
|
||||
* processing operations. If the task is FAILED, then files are kept for
|
||||
* debugging purposes.
|
||||
* @param task The task to delete files for.
|
||||
*/
|
||||
private void deleteAllTaskFiles(VideoProcessingTask task) {
|
||||
if (task.getStatus() == VideoProcessingTask.Status.FAILED) {
|
||||
log.warn("Retaining files for failed task {}, upload id {}.", task.getId(), task.getUploadFileId());
|
||||
return;
|
||||
}
|
||||
Path dir = fileStorageService.getStoragePathForFile(task.getUploadFileId()).getParent();
|
||||
try (var s = Files.list(dir)) {
|
||||
var files = s.toList();
|
||||
for (var file : files) {
|
||||
String filename = file.getFileName().toString().strip();
|
||||
if (Files.isRegularFile(file) && filename.startsWith(task.getUploadFileId())) {
|
||||
try {
|
||||
Files.delete(file);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to delete file " + file + " related to task " + task.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to list files in " + dir + " when deleting files for task " + task.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,9 @@ spring.jpa.hibernate.ddl-auto=update
|
|||
|
||||
server.port=8082
|
||||
|
||||
# A secret header token that other services must provide to use service-only endpoints.
|
||||
app.service-secret=testing
|
||||
|
||||
app.web-origin=http://localhost:9000
|
||||
app.api-origin=http://localhost:8080
|
||||
app.files.storage-dir=./cdn-files/
|
||||
app.files.temp-dir=./cdn-files/tmp/
|
||||
app.api-secret=testing
|
||||
|
|
Loading…
Reference in New Issue