This commit is contained in:
Patrick Haßel 2025-08-09 10:29:11 +02:00
parent 31f6295833
commit 55b1af9242
22 changed files with 131 additions and 72 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
/images/
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/

View File

@ -5,7 +5,7 @@ spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
#-
spring.jpa.hibernate.ddl-auto=create
#spring.jpa.hibernate.ddl-auto=create
#-
spring.jackson.serialization.indent_output=true
#-

View File

@ -0,0 +1,10 @@
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AdminService {
password: string = "";
}

View File

@ -0,0 +1,7 @@
<input type="password" [(ngModel)]="adminService.password">
<br>
<br>
<div class="button buttonGoTo" routerLink="/gallery">
Zur Galerie &rarr;
</div>

View File

@ -0,0 +1,4 @@
input {
border: 1px solid red;
background-color: white;
}

View File

@ -0,0 +1,23 @@
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {AdminService} from '../admin.service';
import {RouterLink} from '@angular/router';
@Component({
selector: 'app-admin',
imports: [
FormsModule,
RouterLink
],
templateUrl: './admin.component.html',
styleUrl: './admin.component.less'
})
export class AdminComponent {
constructor(
readonly adminService: AdminService,
) {
//
}
}

View File

@ -1,3 +1,5 @@
<div routerLink="admin" class="admin">Admin</div>
<div id="title" class="coloredText">
<div>Isabell⚭Timo</div>
<div>09. August 2025</div>

View File

@ -4,3 +4,9 @@
font-size: 150%;
margin-bottom: @space;
}
.admin {
font-size: 50%;
color: gray;
margin: calc(@space / 2);
}

View File

@ -1,9 +1,9 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {RouterLink, RouterOutlet} from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
imports: [RouterOutlet, RouterLink],
templateUrl: './app.component.html',
styleUrl: './app.component.less'
})

View File

@ -2,8 +2,10 @@ import {Routes} from '@angular/router';
import {UploadComponent} from './upload/upload.component';
import {GalleryComponent} from './gallery/gallery.component';
import {PendingChangesGuard} from './upload/pending.guard';
import {AdminComponent} from './admin/admin.component';
export const routes: Routes = [
{path: 'admin', component: AdminComponent},
{path: 'gallery', component: GalleryComponent},
{path: '', component: UploadComponent, canDeactivate: [PendingChangesGuard]},
{path: '**', redirectTo: ''},

View File

@ -1,11 +1,16 @@
import {orNull, validateDate, validateString} from '../validators';
import {orNull, validateBoolean, validateDate, validateString} from '../validators';
export class Picture {
static trackBy(_: number, picture: Picture) {
return picture.uuid;
}
constructor(
readonly uuid: string,
readonly originalDate: Date | null,
readonly uploadDate: Date,
readonly visible: boolean,
) {
//
}
@ -15,6 +20,7 @@ export class Picture {
validateString(json.uuid),
orNull(json.originalDate, validateDate),
validateDate(json.uploadDate),
validateBoolean(json.visible),
);
}

View File

@ -8,7 +8,7 @@
</div>
<div id="images" *ngIf="pictures.length > 0">
<img [src]="pictureUrl(picture)" *ngFor="let picture of sorted()" alt="" loading="lazy">
<img [src]="pictureUrl(picture)" *ngFor="let picture of sorted(); trackBy: Picture.trackBy" alt="" loading="lazy" (click)="toggleVisible(picture)" [class.hiddenBorder]="!picture.visible">
</div>
<div class="noImages" *ngIf="pictures.length === 0">

View File

@ -10,3 +10,8 @@ img {
.noImages {
margin: calc(5 * @space) 0;
}
.hiddenBorder {
border: 0.25em solid red;
box-sizing: border-box;
}

View File

@ -16,6 +16,8 @@ import {PictureService} from './picture.service';
})
export class GalleryComponent implements OnInit {
protected readonly Picture = Picture;
protected pictures: Picture[] = [];
protected sortByUpload: boolean = true;
@ -27,7 +29,7 @@ export class GalleryComponent implements OnInit {
}
ngOnInit(): void {
this.pictureService.all(list => this.pictures = list);
this.pictureService.all(list => this.update(list));
}
pictureUrl(picture: Picture): string {
@ -38,4 +40,12 @@ export class GalleryComponent implements OnInit {
return this.pictures.sort(this.sortByUpload ? Picture.byUploadDateDesc : Picture.byOriginalDateDesc);
}
toggleVisible(picture: Picture) {
this.pictureService.toggleVisible(picture, list => this.update(list));
}
private update(list: Picture[]) {
this.pictures = list;
}
}

View File

@ -2,6 +2,7 @@ import {Injectable} from "@angular/core";
import {HttpClient} from "@angular/common/http";
import {map} from "rxjs";
import {Picture} from "./Picture";
import {AdminService} from '../admin.service';
@Injectable({
providedIn: 'root'
@ -12,12 +13,13 @@ export class PictureService {
constructor(
readonly http: HttpClient,
readonly adminService: AdminService,
) {
//
}
all(next: (list: Picture[]) => any): void {
this.http.get<any[]>(this.apiUrl("http", ['Picture', 'all'])).pipe(map(list => list.map(Picture.fromJson))).subscribe(next);
this.http.post<any[]>(this.apiUrl("http", ['Picture', 'all']), this.adminService.password).pipe(map(list => list.map(Picture.fromJson))).subscribe(next);
}
upload(file: File, success: () => any, error: () => any): void {
@ -26,7 +28,15 @@ export class PictureService {
this.http.post(this.apiUrl("http", ['Picture', 'upload']), form).subscribe({next: success, error: error});
}
apiUrl(protocol: string, path: string[]): string {
toggleVisible(picture: Picture, next: (list: Picture[]) => any) {
if (!this.adminService.password) {
throw new Error("You are not logged in.");
}
this.http.post<any[]>(this.apiUrl("http", ['Picture', picture.uuid, 'visible', !picture.visible]), this.adminService.password).pipe(map(list => list.map(Picture.fromJson))).subscribe(next);
}
apiUrl(protocol: string, path: any[]): string {
// return `${protocol}${this.secure}://${location.hostname}:8084/${path.join('/')}`;
return `${protocol}${this.secure}://${location.hostname}/api/${path.join('/')}`;
}

View File

@ -5,6 +5,13 @@ export function validateString(value: any) {
return value;
}
export function validateBoolean(value: any) {
if (!(typeof value === 'boolean')) {
throw new Error("Not a boolean: " + JSON.stringify(value));
}
return value;
}
export function validateDate(value: any): Date {
const date = new Date(Date.parse(validateString(value)));
if (!date) {

View File

@ -1,7 +1,7 @@
@import "config";
body {
font-size: 3vh;
font-size: 2.8vh;
font-family: sans-serif;
text-align: center;
user-select: none;

View File

@ -1,54 +0,0 @@
package de.ph87.isabell_und_timo.picture;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
import java.util.stream.Collectors;
@Slf4j
@CrossOrigin
@RestController
@RequiredArgsConstructor
@RequestMapping("PictureAdmin")
public class PictureAdminController {
private final PictureService pictureService;
private final PictureRepository pictureRepository;
@NonNull
@GetMapping("visible")
public String visible(@NonNull final HttpServletRequest request) {
verifyLocal(request);
return pictureRepository
.findAllDtoByVisibleTrue()
.stream()
.map(p -> "<a href='/PictureAdmin/%s/visible/false'><img src='/Picture/%s/preview' loading='lazy' alt='%s' style='width: 100%%;'></a>\n".formatted(p.uuid, p.uuid, p.uuid))
.collect(Collectors.joining("\n"));
}
// @GetMapping("{uuid}/visible/{visible}")
// public void visible(@PathVariable @NonNull final String uuid, @PathVariable final boolean visible, @NonNull final HttpServletRequest request, @NonNull final HttpServletResponse response) throws IOException {
// verifyLocal(request);
// pictureService.visible(uuid, visible);
// response.sendRedirect(request.get);
// }
private static void verifyLocal(final HttpServletRequest request) {
if (!request.getRemoteAddr().startsWith("10.") && !request.getRemoteAddr().equals("127.0.0.1")) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
}
}

View File

@ -23,6 +23,7 @@ import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@ -65,22 +66,34 @@ public class PictureController {
return factory.createMultipartConfig();
}
private static boolean isAdmin(@Nullable final String password) {
return password != null && password.equals("IbPAadstIT25!");
}
@NonNull
@GetMapping("all")
public List<PictureDto> all() {
@PostMapping("all")
public List<PictureDto> all(@RequestBody(required = false) final String password) {
if (isAdmin(password)) {
return pictureRepository.findAllDto();
}
return pictureRepository.findAllDtoByVisibleTrue();
}
@NonNull
@PostMapping("{uuid}/visible/{visible}")
public List<PictureDto> visible(@PathVariable @NonNull final String uuid, @PathVariable final boolean visible, @RequestBody(required = false) final String password) {
if (isAdmin(password)) {
pictureService.visible(uuid, visible);
}
return all(password);
}
@GetMapping("{uuid}/preview")
public void preview(@PathVariable @NonNull final String uuid, @NonNull final HttpServletRequest request, @NonNull final HttpServletResponse response) throws IOException {
final PictureInternal pictureDto = pictureRepository.findDtoByUuid(uuid).orElseThrow(() -> {
log.warn("Tried accessing NON-EXISTENT picture: {}", request);
return new ResponseStatusException(HttpStatus.NOT_FOUND);
});
if (!pictureDto.visible) {
log.warn("Tried accessing INVISIBLE picture: {}", request);
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
try (final FileInputStream input = new FileInputStream(pictureDto.getPreviewPath().toFile())) {
response.getOutputStream().write(input.readAllBytes());
}

View File

@ -18,10 +18,13 @@ public class PictureDto {
@NonNull
public final ZonedDateTime uploadDate;
public final boolean visible;
public PictureDto(@NonNull final Picture picture) {
this.uuid = picture.getUuid();
this.originalDate = picture.getOriginalDate();
this.uploadDate = picture.getUploadDate();
this.visible = picture.isVisible();
}
}

View File

@ -9,6 +9,10 @@ import java.util.Optional;
public interface PictureRepository extends ListCrudRepository<Picture, String> {
@NonNull
@Query("select new de.ph87.isabell_und_timo.picture.PictureDto(p) from Picture p")
List<PictureDto> findAllDto();
@NonNull
@Query("select new de.ph87.isabell_und_timo.picture.PictureDto(p) from Picture p where p.visible = true")
List<PictureDto> findAllDtoByVisibleTrue();

View File

@ -4,7 +4,6 @@ import jakarta.annotation.Nullable;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -19,8 +18,6 @@ public class PictureService {
private final PictureRepository pictureRepository;
private final ResourcePatternResolver resourcePatternResolver;
@NonNull
@Transactional
public PictureInternal create(
@ -41,9 +38,11 @@ public class PictureService {
return new PictureInternal(picture);
}
@Transactional
public void visible(final @NonNull String uuid, final boolean visible) {
final Picture picture = pictureRepository.findById(uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
picture.setVisible(visible);
log.info("Visible: {}", picture);
}
}