Refactor particle management: remove secret key from file operations, add ParticleManagerComponent, update frame naming conventions, and enhance style and functionality.

This commit is contained in:
akastijn 2026-01-04 05:22:04 +01:00
parent 63aa7fd550
commit 8a0843128c
12 changed files with 164 additions and 35 deletions

View File

@ -57,6 +57,8 @@ public class SecurityConfig {
.requestMatchers("/api/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.requestMatchers("/api/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.requestMatchers("/api/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
//TODO allow users access to their own folder
.requestMatchers("/api/files/download/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.requestMatchers("/api/history/admin/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.requestMatchers("/api/login/userLogin/**").permitAll()
.anyRequest().permitAll()

View File

@ -34,24 +34,17 @@ public class ParticleController implements ParticlesApi {
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();
}
public ResponseEntity<Resource> downloadFile(String filename) {
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);
return getFileForDownload(file, 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();
}
public ResponseEntity<Resource> downloadFileForUser(String uuid, String filename) {
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);
@ -67,6 +60,7 @@ public class ParticleController implements ParticlesApi {
}
private ResponseEntity<Resource> getFileForDownload(File file, String filename) {
filename += ".json";
File targetFile = new File(file, filename);
if (!targetFile.exists()) {
log.warn("Particles file {} does not exist", targetFile.getAbsolutePath());
@ -95,7 +89,7 @@ public class ParticleController implements ParticlesApi {
}
@Override
public ResponseEntity<Void> saveFile(String filename, MultipartFile content) throws Exception {
public ResponseEntity<Void> saveFile(String filename, MultipartFile content) {
File file = new File(particlesFilePath);
if (!file.exists() || !file.isDirectory()) {
log.error("Particles file path {} is not a directory, not saving particles file", particlesFilePath);
@ -107,7 +101,7 @@ public class ParticleController implements ParticlesApi {
}
@Override
public ResponseEntity<Void> saveFileForUser(String uuid, String filename, MultipartFile content) throws Exception {
public ResponseEntity<Void> saveFileForUser(String uuid, String filename, MultipartFile content) {
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);
@ -127,7 +121,7 @@ public class ParticleController implements ParticlesApi {
}
private void notifyServerOfFileUpload(String filename) {
String notificationUrl = String.format("%s/notify/%s.json", notificationServerUrl, filename);
String notificationUrl = String.format("http://%s/notify/%s.json", notificationServerUrl, filename);
sendNotification(notificationUrl, String.format("file upload: %s", filename));
}
@ -154,6 +148,7 @@ public class ParticleController implements ParticlesApi {
private ResponseEntity<Void> writeContentToFile(File dir, String filename, MultipartFile content) {
filename += ".json";
File targetFile = new File(dir, filename);
if (!Files.isWritable(targetFile.toPath())) {
log.error("Particles file {} is not writable", targetFile.getAbsolutePath());

View File

@ -0,0 +1,39 @@
<div class="card-div">
<mat-card>
<mat-card-header>
<mat-card-title>
<span>Particle Manager</span>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<br>
<div class="row">
<div class="column">
<mat-form-field appearance="outline" class="type-field">
<mat-label>Particle to download</mat-label>
<input type="text"
[(ngModel)]="selectedParticle"
placeholder="Name of particle to download"
matInput>
</mat-form-field>
<button mat-icon-button (click)="downloadParticle()">
<mat-icon>download</mat-icon>
</button>
</div>
<div class="row">
<mat-form-field appearance="outline" class="type-field">
<mat-label>Particle to upload</mat-label>
<input type="text"
disabled
[ngModel]="createdParticleName"
placeholder="Name of particle to upload"
matInput>
</mat-form-field>
<button mat-icon-button (click)="uploadParticle()">
<mat-icon>upload</mat-icon>
</button>
</div>
</div>
</mat-card-content>
</mat-card>
</div>

View File

@ -0,0 +1,6 @@
.card-div {
mat-card {
background-color: var(--color-primary);
color: var(--font-color);
}
}

View File

@ -0,0 +1,86 @@
import {Component, inject} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {MatInput, MatLabel} from '@angular/material/input';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';
import {ParticlesService} from '@api';
import {ParticleManagerService} from '@pages/particles/services/particle-manager.service';
import {FrameManagerService} from '@pages/particles/services/frame-manager.service';
@Component({
selector: 'app-particle-manager',
imports: [
FormsModule,
MatFormFieldModule,
MatInput,
MatLabel,
MatCard,
MatCardContent,
MatCardHeader,
MatCardTitle,
MatIconModule,
MatButtonModule,
],
templateUrl: './particle-manager.component.html',
styleUrl: './particle-manager.component.scss'
})
export class ParticleManagerComponent {
private readonly particlesService = inject(ParticlesService)
private readonly particleManagerService = inject(ParticleManagerService)
private readonly frameManagerService = inject(FrameManagerService)
private selectedParticleName: string = '';
set selectedParticle(particle: string) {
this.selectedParticleName = particle;
}
get selectedParticle(): string {
return this.selectedParticleName;
}
get createdParticleName(): string {
return this.particleManagerService.getParticleData().particle_name;
}
protected downloadParticle() {
if (this.selectedParticleName === '') {
return;
}
this.particlesService
.downloadFile(this.selectedParticleName)
.subscribe({
next: data => {
data.text().then(text => {
if (text.startsWith('<')) {
console.error("Failed to download particle: Invalid file format")
return;
}
this.particleManagerService.loadParticleData(text)
this.frameManagerService.switchFrame(this.particleManagerService.getCurrentFrame())
})
},
error: () => {
console.error("Failed to download particle: Invalid file name")
},
}
)
}
protected uploadParticle() {
this.particlesService
.saveFile(this.createdParticleName, new Blob([this.particleManagerService.generateJson()], {type: 'application/json'}))
.subscribe({
next: () => {
console.log("Successfully uploaded particle")
},
error: () => {
console.error("Failed to upload particle")
}
})
}
}

View File

@ -26,6 +26,9 @@
</div>
</div>
<div class="flex side-column">
<app-particle-manager>
</app-particle-manager>
<app-frames>
<button mat-icon-button matTooltip="Copy JSON to clipboard" (click)="copyJson()">
<mat-icon>content_copy</mat-icon>

View File

@ -25,6 +25,7 @@ import {RenderContainerComponent} from './components/render-container/render-con
import {ParticlesService} from '@api';
import {MatTooltipModule} from '@angular/material/tooltip';
import {FullSizeComponent} from '@shared-components/full-size/full-size.component';
import {ParticleManagerComponent} from '@pages/particles/components/particle-manager/particle-manager.component';
@Component({
selector: 'app-particles',
@ -47,7 +48,8 @@ import {FullSizeComponent} from '@shared-components/full-size/full-size.componen
FramesComponent,
RenderContainerComponent,
MatTooltipModule,
FullSizeComponent
FullSizeComponent,
ParticleManagerComponent
],
templateUrl: './particles.component.html',
styleUrl: './particles.component.scss'

View File

@ -1,4 +1,4 @@
import { Injectable } from '@angular/core';
import {inject, Injectable} from '@angular/core';
import {ParticleManagerService} from './particle-manager.service';
/**
@ -8,15 +8,14 @@ import { ParticleManagerService } from './particle-manager.service';
providedIn: 'root'
})
export class FrameManagerService {
constructor(private particleManager: ParticleManagerService) {}
private readonly particleManager = inject(ParticleManagerService);
/**
* Adds a new frame
*/
addFrame(): void {
const frames = this.particleManager.getFrames();
const frameId = `frame${frames.length + 1}`;
const frameId = `frame-${frames.length}`;
frames.push(frameId);
this.particleManager.setFrames(frames);

View File

@ -39,7 +39,7 @@ export class IntersectionPlaneService {
* Creates the intersection plane and adds it to the scene
*/
createIntersectionPlane(): THREE.Mesh {
const planeGeometry = new THREE.PlaneGeometry(3, 3);
const planeGeometry = new THREE.PlaneGeometry(5, 5);
const planeMaterial = new THREE.MeshBasicMaterial({
color: 0x00AA00,
transparent: true,

View File

@ -27,11 +27,11 @@ export class ParticleManagerService {
random_offset: 0,
stationary: true,
frames: {
'frame1': []
'frame-0': []
}
};
private currentFrame: string = 'frame1';
private frames: string[] = ['frame1'];
private currentFrame: string = 'frame-0';
private frames: string[] = ['frame-0'];
private selectedColor: string = '#ff0000';
private selectedParticle: Particle = Particle.DUST;
private selectedSize: number = 1;
@ -301,4 +301,10 @@ export class ParticleManagerService {
this.clearParticleVisuals();
this.renderFrameParticles(this.currentFrame);
}
public loadParticleData(data: string): void {
this.particleData = JSON.parse(data);
this.setCurrentFrame('frame-0');
this.frames = Object.keys(this.particleData.frames);
}
}

View File

@ -79,7 +79,7 @@ paths:
$ref: './schemas/particles/particles.yml#/SaveFile'
/api/files/save/{uuid}/{filename}:
$ref: './schemas/particles/particles.yml#/SaveFileForUser'
/api/files/download/{filename}/{secret}:
/api/files/download/{filename}:
$ref: './schemas/particles/particles.yml#/DownloadFile'
/api/files/download/{uuid}/{filename}:
$ref: './schemas/particles/particles.yml#/DownloadFileForUser'

View File

@ -7,13 +7,6 @@ components:
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
@ -109,10 +102,9 @@ DownloadFile:
tags:
- particles
summary: Download a file
description: Download a file from the server using a secret key
description: Download a file from the server
operationId: downloadFile
parameters:
- $ref: '#/components/parameters/Secret'
- $ref: '#/components/parameters/Filename'
responses:
'200':
@ -140,12 +132,11 @@ DownloadFileForUser:
tags:
- particles
summary: Download a file for a specific user
description: Download a file from the server for a specific user (requires authorization)
description: Download a file from the server for a specific user
operationId: downloadFileForUser
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/Secret'
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
- $ref: '#/components/parameters/Filename'
responses: