Add endpoints, services, and security controls for particle file management, including save and download APIs.
This commit is contained in:
parent
c72703ea32
commit
7fc25f46f3
|
|
@ -38,6 +38,8 @@ public class SecurityConfig {
|
|||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
|
||||
.requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.anyRequest().permitAll()
|
||||
)
|
||||
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
package com.alttd.altitudeweb.controllers.particles;
|
||||
|
||||
import com.alttd.altitudeweb.api.ParticlesApi;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
public class ParticleController implements ParticlesApi {
|
||||
|
||||
@Value("${login.secret:#{null}}")
|
||||
private String loginSecret;
|
||||
|
||||
@Value("${particles.file_path}")
|
||||
private String particlesFilePath;
|
||||
|
||||
@Value("${notification.server.url:http://localhost:8080}")
|
||||
private String notificationServerUrl;
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Resource> downloadFile(String authorization, String filename) throws Exception {
|
||||
if (authorization == null || !authorization.equals(loginSecret)) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
File file = new File(particlesFilePath);
|
||||
if (!file.exists() || !file.isDirectory()) {
|
||||
log.error("Particles file path {} is not a directory, not downloading particles file", particlesFilePath);
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
File targetFile = new File(file, filename);
|
||||
return getFileForDownload(targetFile, filename);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Resource> downloadFileForUser(String authorization, String uuid, String filename) throws Exception {
|
||||
if (authorization == null || !authorization.equals(loginSecret)) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
File file = new File(particlesFilePath);
|
||||
if (!file.exists() || !file.isDirectory()) {
|
||||
log.error("Particles file path {} is not a directory, not downloading particles user file", particlesFilePath);
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
File targetDir = new File(file, uuid);
|
||||
if (targetDir.exists()) {
|
||||
return getFileForDownload(targetDir, filename);
|
||||
} else {
|
||||
log.warn("User {} does not have a directory for particles files", uuid);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<Resource> getFileForDownload(File file, String filename) {
|
||||
File targetFile = new File(file, filename);
|
||||
if (!targetFile.exists()) {
|
||||
log.warn("Particles file {} does not exist", targetFile.getAbsolutePath());
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
if (!targetFile.isFile()) {
|
||||
log.warn("Particles file {} is not a file", targetFile.getAbsolutePath());
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
|
||||
try {
|
||||
Path path = targetFile.toPath();
|
||||
ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentLength(targetFile.length())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
|
||||
.body(resource);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to read particles file {}: {}", targetFile.getAbsolutePath(), e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> saveFile(String filename, MultipartFile content) throws Exception {
|
||||
File file = new File(particlesFilePath);
|
||||
if (!file.exists() || !file.isDirectory()) {
|
||||
log.error("Particles file path {} is not a directory, not saving particles file", particlesFilePath);
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
ResponseEntity<Void> voidResponseEntity = writeContentToFile(file, filename, content);
|
||||
notifyServerOfFileUpload(filename);
|
||||
return voidResponseEntity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> saveFileForUser(String uuid, String filename, MultipartFile content) throws Exception {
|
||||
File file = new File(particlesFilePath);
|
||||
if (!file.exists() || !file.isDirectory()) {
|
||||
log.error("Particles file path {} is not a directory, not saving particles user file", particlesFilePath);
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
File targetDir = new File(file, uuid);
|
||||
if (!file.exists()) {
|
||||
log.debug("Creating particles directory {}", targetDir.getAbsolutePath());
|
||||
if (targetDir.mkdirs()) {
|
||||
log.info("Created particles user directory {}", targetDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
ResponseEntity<Void> voidResponseEntity = writeContentToFile(file, filename, content);
|
||||
notifyServerOfFileUpload(uuid, filename);
|
||||
return voidResponseEntity;
|
||||
}
|
||||
|
||||
private void notifyServerOfFileUpload(String filename) {
|
||||
String notificationUrl = String.format("%s/notify/%s.json", notificationServerUrl, filename);
|
||||
sendNotification(notificationUrl, String.format("file upload: %s", filename));
|
||||
}
|
||||
|
||||
private void notifyServerOfFileUpload(String uuid, String filename) {
|
||||
String notificationUrl = String.format("%s/notify/%s/%s.json", notificationServerUrl, uuid, filename);
|
||||
sendNotification(notificationUrl, String.format("file upload for user %s: %s", uuid, filename));
|
||||
}
|
||||
|
||||
private void sendNotification(String notificationUrl, String logDescription) {
|
||||
try {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
ResponseEntity<String> response = restTemplate.getForEntity(notificationUrl, String.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
log.info("Successfully notified server of {}", logDescription);
|
||||
} else {
|
||||
log.warn("Failed to notify server of {}, status: {}",
|
||||
logDescription, response.getStatusCode());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error notifying server of {}", logDescription, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private ResponseEntity<Void> writeContentToFile(File dir, String filename, MultipartFile content) {
|
||||
File targetFile = new File(dir, filename);
|
||||
if (!Files.isWritable(targetFile.toPath())) {
|
||||
log.error("Particles file {} is not writable", targetFile.getAbsolutePath());
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
|
||||
if (targetFile.exists()) {
|
||||
log.warn("Overwriting existing particles file {}", targetFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
try {
|
||||
content.transferTo(targetFile);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to write particles file {}", targetFile.getAbsolutePath(), e);
|
||||
return ResponseEntity.status(500).build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,4 +6,6 @@ database.user=${DB_USER:root}
|
|||
database.password=${DB_PASSWORD:root}
|
||||
cors.allowed-origins=${CORS:https://alttd.com}
|
||||
login.secret=${LOGIN_SECRET:SET_TOKEN}
|
||||
particles.file_path=${user.home}/.altitudeweb/particles
|
||||
notification.server.url=10.0.0.107:8080
|
||||
logging.level.com.alttd.altitudeweb=INFO
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {ParticleComponent} from './components/particle/particle.component';
|
|||
import {FramesComponent} from './components/frames/frames.component';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {RenderContainerComponent} from './components/render-container/render-container.component';
|
||||
import {ParticlesService} from '../../api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-particles',
|
||||
|
|
@ -55,6 +56,7 @@ export class ParticlesComponent {
|
|||
private intersectionPlaneService: IntersectionPlaneService,
|
||||
private particleManagerService: ParticleManagerService,
|
||||
private matSnackBar: MatSnackBar,
|
||||
private particlesService: ParticlesService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -93,9 +95,38 @@ export class ParticlesComponent {
|
|||
return this.particleManagerService.generateJson();
|
||||
}
|
||||
|
||||
public getJsonFile(): Blob {
|
||||
const jsonContent = this.generateJson();
|
||||
return new Blob([jsonContent], {type: 'application/json'});
|
||||
}
|
||||
|
||||
public saveJsonToFile(): void {
|
||||
const jsonContent = this.generateJson();
|
||||
const blob = new Blob([jsonContent], {type: 'application/json'});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// Create a temporary link element
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'particle-data.json';
|
||||
|
||||
// Append to the document, click it, and remove it
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Clean up
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
this.matSnackBar.open('JSON file downloaded', '', {duration: 2000});
|
||||
}
|
||||
|
||||
|
||||
public copyJson() {
|
||||
navigator.clipboard.writeText(this.generateJson()).then(() => {
|
||||
this.matSnackBar.open('Copied to clipboard', '', {duration: 2000})
|
||||
});
|
||||
//TODO validation
|
||||
this.particlesService.saveFile(this.particleManagerService.getParticleData().particle_name, this.getJsonFile());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,18 @@ components:
|
|||
schemas:
|
||||
PermissionClaim:
|
||||
$ref: './schemas/permissions/permissions.yml#/components/schemas/PermissionClaim'
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
tags:
|
||||
- name: history
|
||||
description: Retrieves punishment history
|
||||
- name: team
|
||||
description: Retrieves information about the staff team
|
||||
- name: particles
|
||||
description: All actions related to particles
|
||||
paths:
|
||||
/team/{team}:
|
||||
$ref: './schemas/team/team.yml#/getTeam'
|
||||
|
|
@ -46,3 +53,11 @@ paths:
|
|||
$ref: './schemas/login/login.yml#/RequestNewUserLogin'
|
||||
/login/userLogin/{code}:
|
||||
$ref: './schemas/login/login.yml#/UserLogin'
|
||||
/files/save/{filename}:
|
||||
$ref: './schemas/particles/particles.yml#/SaveFile'
|
||||
/files/save/{uuid}/{filename}:
|
||||
$ref: './schemas/particles/particles.yml#/SaveFileForUser'
|
||||
/files/download/{filename}/{secret}:
|
||||
$ref: './schemas/particles/particles.yml#/DownloadFile'
|
||||
/files/download/{uuid}/{filename}:
|
||||
$ref: './schemas/particles/particles.yml#/DownloadFileForUser'
|
||||
|
|
|
|||
176
open_api/src/main/resources/schemas/particles/particles.yml
Normal file
176
open_api/src/main/resources/schemas/particles/particles.yml
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
components:
|
||||
parameters:
|
||||
Filename:
|
||||
name: filename
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The name of the file
|
||||
Secret:
|
||||
name: Authorization
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Secret
|
||||
schemas:
|
||||
FileData:
|
||||
type: object
|
||||
required:
|
||||
- content
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
format: binary
|
||||
description: The content of the file
|
||||
|
||||
SaveFile:
|
||||
post:
|
||||
tags:
|
||||
- particles
|
||||
summary: Save a file
|
||||
description: Save a file to the server (requires authorization)
|
||||
operationId: saveFile
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Filename'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FileData'
|
||||
responses:
|
||||
'200':
|
||||
description: File saved successfully
|
||||
'401':
|
||||
description: Unauthorized - Invalid or missing token
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||
'403':
|
||||
description: Forbidden - Insufficient permissions
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||
|
||||
SaveFileForUser:
|
||||
post:
|
||||
tags:
|
||||
- particles
|
||||
summary: Save a file for a specific user
|
||||
description: Save a file to the server for a specific user (requires head_mod permission)
|
||||
operationId: saveFileForUser
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
|
||||
- $ref: '#/components/parameters/Filename'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FileData'
|
||||
responses:
|
||||
'200':
|
||||
description: File saved successfully
|
||||
'401':
|
||||
description: Unauthorized - Invalid or missing token
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||
'403':
|
||||
description: Forbidden - Insufficient permissions
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||
|
||||
DownloadFile:
|
||||
get:
|
||||
tags:
|
||||
- particles
|
||||
summary: Download a file
|
||||
description: Download a file from the server using a secret key
|
||||
operationId: downloadFile
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Secret'
|
||||
- $ref: '#/components/parameters/Filename'
|
||||
responses:
|
||||
'200':
|
||||
description: File downloaded successfully
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
'404':
|
||||
description: File not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||
|
||||
DownloadFileForUser:
|
||||
get:
|
||||
tags:
|
||||
- particles
|
||||
summary: Download a file for a specific user
|
||||
description: Download a file from the server for a specific user (requires authorization)
|
||||
operationId: downloadFileForUser
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Secret'
|
||||
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
|
||||
- $ref: '#/components/parameters/Filename'
|
||||
responses:
|
||||
'200':
|
||||
description: File downloaded successfully
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
'401':
|
||||
description: Unauthorized - Invalid or missing token
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||
'404':
|
||||
description: File not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||
Loading…
Reference in New Issue
Block a user