admin
This commit is contained in:
parent
31f6295833
commit
55b1af9242
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
/images/
|
||||
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
|
||||
@ -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
|
||||
#-
|
||||
|
||||
10
src/main/angular/src/app/admin.service.ts
Normal file
10
src/main/angular/src/app/admin.service.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AdminService {
|
||||
|
||||
password: string = "";
|
||||
|
||||
}
|
||||
7
src/main/angular/src/app/admin/admin.component.html
Normal file
7
src/main/angular/src/app/admin/admin.component.html
Normal file
@ -0,0 +1,7 @@
|
||||
<input type="password" [(ngModel)]="adminService.password">
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div class="button buttonGoTo" routerLink="/gallery">
|
||||
Zur Galerie →
|
||||
</div>
|
||||
4
src/main/angular/src/app/admin/admin.component.less
Normal file
4
src/main/angular/src/app/admin/admin.component.less
Normal file
@ -0,0 +1,4 @@
|
||||
input {
|
||||
border: 1px solid red;
|
||||
background-color: white;
|
||||
}
|
||||
23
src/main/angular/src/app/admin/admin.component.ts
Normal file
23
src/main/angular/src/app/admin/admin.component.ts
Normal 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,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -4,3 +4,9 @@
|
||||
font-size: 150%;
|
||||
margin-bottom: @space;
|
||||
}
|
||||
|
||||
.admin {
|
||||
font-size: 50%;
|
||||
color: gray;
|
||||
margin: calc(@space / 2);
|
||||
}
|
||||
|
||||
@ -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'
|
||||
})
|
||||
|
||||
@ -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: ''},
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -10,3 +10,8 @@ img {
|
||||
.noImages {
|
||||
margin: calc(5 * @space) 0;
|
||||
}
|
||||
|
||||
.hiddenBorder {
|
||||
border: 0.25em solid red;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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('/')}`;
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
@import "config";
|
||||
|
||||
body {
|
||||
font-size: 3vh;
|
||||
font-size: 2.8vh;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user